diff --git a/crates/nyaterm-desktop/src/features/app_state/construct.rs b/crates/nyaterm-desktop/src/features/app_state/construct.rs index bd92b4438..448fe0ddb 100644 --- a/crates/nyaterm-desktop/src/features/app_state/construct.rs +++ b/crates/nyaterm-desktop/src/features/app_state/construct.rs @@ -1,9 +1,9 @@ use crate::models::{ - ActivityBarLayoutState, BottomPanelMode, HeaderStatusState, MainMode, NavItem, PanelSide, - RightFocus, SessionEventBridge, SettingsTab, StoreStatus, TerminalFramePipeline, + ActivityBarLayoutState, BottomPanelMode, NavItem, PanelSide, SessionEventBridge, StoreStatus, + TerminalFramePipeline, }; use crate::terminal::initial_terminal_screen; -use gpui::{Context, ScrollHandle}; +use gpui::Context; use nyaterm_core::{ AiSettings, AppRuntime, AppSettingsSummary, CLOUD_SYNC_HISTORY_LIMIT, CloudSyncSettings, CloudSyncState, ConnectionStore, KeywordHighlightConfig, NativeServices, TranslationSettings, @@ -11,7 +11,7 @@ use nyaterm_core::{ }; use nyaterm_terminal::TerminalOutputDecoder; use nyaterm_transport::{SessionManager, SftpDuplicatePolicy}; -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use super::super::settings::{SettingsFeatureFocus, SettingsFeatureState}; @@ -21,10 +21,10 @@ use super::super::{ QuickCommandFeatureFocus, QuickCommandFeatureState, RecordingFeatureState, RemoteOpsFeatureFocus, RemoteOpsFeatureState, SecurityFeatureFocus, SecurityFeatureState, SendCommandFeatureFocus, SendCommandFeatureState, SessionFeatureFocus, SessionFeatureState, - ShellFeatureState, SyncInputFeatureState, TerminalFeatureFocus, TerminalFeatureState, - TextInputRegistry, TransferFeatureFocus, TransferFeatureState, TranslationFeatureState, - TunnelFeatureState, UpdateFeatureState, ai_active_profile_drafts, ai_usage_counts, - appearance_font_options, quick_command_sort_mode_from_setting, + ShellFeatureInit, ShellFeatureState, SyncInputFeatureState, TerminalFeatureFocus, + TerminalFeatureState, TextInputRegistry, TransferFeatureFocus, TransferFeatureState, + TranslationFeatureState, TunnelFeatureState, UpdateFeatureState, ai_active_profile_drafts, + ai_usage_counts, appearance_font_options, quick_command_sort_mode_from_setting, quick_command_view_mode_from_setting, spawn_command_persistence_worker, }; use super::NyaTermApp; @@ -444,17 +444,28 @@ impl NyaTermApp { }, ), text_inputs: TextInputRegistry::default(), - shell: ShellFeatureState::new( - if settings.ui_serial_send_visible { + shell: ShellFeatureState::new(ShellFeatureInit { + bottom_panel_mode: if settings.ui_serial_send_visible { BottomPanelMode::CommandSend } else if settings.ui_quick_cmd_visible { BottomPanelMode::QuickCommands } else { BottomPanelMode::Hidden }, - quick_cmd_height, - serial_send_height, - ), + quick_commands_height: quick_cmd_height, + command_send_height: serial_send_height, + active_left_panel, + active_right_panel, + left_open_panels, + right_open_panels, + panel_stack_sizes, + panel_multi_open, + left_sidebar_collapsed, + right_inspector_collapsed, + left_panel_width, + right_panel_width, + activity_bar_layout, + }), sync_input: SyncInputFeatureState::new(cx.focus_handle()), keyword_highlights, settings, @@ -472,53 +483,6 @@ impl NyaTermApp { active_snapshot_password_prompt: None, pending_session_events: VecDeque::new(), diagnostic_log_last_at: HashMap::new(), - last_viewport_size: (1280., 800.), - wallpaper_tile_dimensions: None, - last_viewport_change_at: None, - title_drag_active_until: None, - selected_nav: NavItem::Workspace, - main_mode: MainMode::Workspace, - settings_active_tab: SettingsTab::General, - settings_expanded_groups: HashSet::from(["workspace".to_string()]), - settings_draft_snapshot: None, - settings_window: None, - settings_window_open_pending: false, - settings_previous_left_collapsed: None, - settings_previous_right_collapsed: None, - active_left_panel, - active_right_panel, - left_open_panels, - right_open_panels, - panel_stack_sizes, - panel_multi_open, - right_focus: RightFocus::Default, - left_sidebar_collapsed, - right_inspector_collapsed, - mobile_left_open: false, - mobile_right_open: false, - left_panel_width, - right_panel_width, - panel_resize: None, - panel_stack_resize: None, - activity_bar_layout, - activity_bar_context_menu: None, - title_menu_open: None, - title_menu_submenu: None, - header_status: HeaderStatusState::default(), - open_tabs_menu_open: false, - new_session_menu_open: false, - new_session_all_sessions_open: false, - new_session_group_menu_path: Vec::new(), - session_tab_strip_scroll: ScrollHandle::new(), - session_tab_scroll_into_view_pending: false, - last_connect_failure_name: None, - last_connect_failure_error: None, - workspace_split: None, - workspace_split_resize: None, - session_pane_roots: HashMap::new(), - session_tab_owner: HashMap::new(), - focused_terminal_window_leaf_id: None, - workspace_pane_layout_restored: false, startup_restore_complete: false, } } diff --git a/crates/nyaterm-desktop/src/features/app_state/mod.rs b/crates/nyaterm-desktop/src/features/app_state/mod.rs index a2bac36fe..972ce196f 100644 --- a/crates/nyaterm-desktop/src/features/app_state/mod.rs +++ b/crates/nyaterm-desktop/src/features/app_state/mod.rs @@ -1,8 +1,8 @@ -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, mpsc}; use std::time::Instant; -use gpui::{ScrollHandle, WindowHandle}; +use gpui::WindowHandle; use nyaterm_core::{ AppRuntime, AppSettingsSummary, CommandHistoryEntry, Group, KeywordHighlightConfig, NativeServices, OtpEntry, ProxyConfig, ProxyGroup, QuickCommand, QuickCommandCategory, @@ -21,7 +21,6 @@ use super::remote_editor_window::RemoteFileEditorWindow; use super::runtime_jobs::{CommandPersistenceRequest, CommandPersistenceResult}; use super::session::SessionFeatureState; use super::settings::{SecurityFeatureState, SettingsFeatureState}; -use super::settings_window::SettingsWindow; use super::shell::ShellFeatureState; use super::sync::CloudSyncFeatureState; use super::sync_input::SyncInputFeatureState; @@ -32,11 +31,8 @@ use super::translation::TranslationFeatureState; use super::tunnels::TunnelFeatureState; use super::update::UpdateFeatureState; use crate::models::{ - ActivityBarContextMenuState, ActivityBarLayoutState, ConfigPathPromptKind, - DiagnosticsPathPromptKind, HeaderStatusState, KeywordHighlightPathPromptKind, MainMode, - NavItem, PanelResizeState, PanelStackResizeState, RightFocus, SettingsTab, - SnapshotPasswordPromptState, StoreStatus, TitleMenu, TitleMenuSubmenu, WorkspacePaneNode, - WorkspaceSplitResizeState, WorkspaceSplitState, + ConfigPathPromptKind, DiagnosticsPathPromptKind, KeywordHighlightPathPromptKind, + SnapshotPasswordPromptState, StoreStatus, }; mod construct; @@ -104,70 +100,5 @@ pub struct NyaTermApp { pub(in crate::features) active_snapshot_password_prompt: Option, pub(in crate::features) pending_session_events: VecDeque, pub(in crate::features) diagnostic_log_last_at: HashMap<&'static str, Instant>, - pub(in crate::features) last_viewport_size: (f32, f32), - /// Cached intrinsic dimensions for the current tiled wallpaper path. - pub(in crate::features) wallpaper_tile_dimensions: Option<(String, u32, u32)>, - /// When the window viewport last changed (resize/drag geometry). - pub(in crate::features) last_viewport_change_at: Option, - /// Deadline for treating title-bar window dragging as active. - pub(in crate::features) title_drag_active_until: Option, - pub(in crate::features) selected_nav: NavItem, - pub(in crate::features) main_mode: MainMode, - pub(in crate::features) settings_active_tab: SettingsTab, - /// Expanded multi-item groups in the settings sidebar (Tauri keeps this local to the page). - pub(in crate::features) settings_expanded_groups: HashSet, - /// Committed values captured when the in-window settings page opens. - pub(in crate::features) settings_draft_snapshot: Option, - pub(in crate::features) settings_window: Option>, - pub(in crate::features) settings_window_open_pending: bool, - /// Main workspace panel state to restore after leaving the in-window settings page. - pub(in crate::features) settings_previous_left_collapsed: Option, - pub(in crate::features) settings_previous_right_collapsed: Option, - pub(in crate::features) active_left_panel: Option, - pub(in crate::features) active_right_panel: Option, - pub(in crate::features) left_open_panels: Vec, - pub(in crate::features) right_open_panels: Vec, - pub(in crate::features) panel_stack_sizes: HashMap, - pub(in crate::features) panel_multi_open: bool, - pub(in crate::features) right_focus: RightFocus, - pub(in crate::features) left_sidebar_collapsed: bool, - pub(in crate::features) right_inspector_collapsed: bool, - pub(in crate::features) mobile_left_open: bool, - pub(in crate::features) mobile_right_open: bool, - pub(in crate::features) left_panel_width: f32, - pub(in crate::features) right_panel_width: f32, - pub(in crate::features) panel_resize: Option, - pub(in crate::features) panel_stack_resize: Option, - pub(in crate::features) activity_bar_layout: ActivityBarLayoutState, - pub(in crate::features) activity_bar_context_menu: Option, - pub(in crate::features) title_menu_open: Option, - pub(in crate::features) title_menu_submenu: Option, - pub(in crate::features) header_status: HeaderStatusState, - /// Open-tabs overflow menu (Tauri TabBar expand-more when many tabs). - pub(in crate::features) open_tabs_menu_open: bool, - /// New-session menu next to the tab strip + control. - pub(in crate::features) new_session_menu_open: bool, - /// Whether the Tauri-style "All sessions" submenu is expanded. - pub(in crate::features) new_session_all_sessions_open: bool, - /// Hovered group ids from the root submenu through the deepest open child menu. - pub(in crate::features) new_session_group_menu_path: Vec, - /// Horizontal scroll handle for the global session tab strip (scroll-into-view). - pub(in crate::features) session_tab_strip_scroll: ScrollHandle, - /// Request scroll-into-view of the active tab on next paint (Tauri TabBar). - pub(in crate::features) session_tab_scroll_into_view_pending: bool, - /// Last failed connect name (shown as ephemeral failed tab chrome). - pub(in crate::features) last_connect_failure_name: Option, - /// Last failed connect error text. - pub(in crate::features) last_connect_failure_error: Option, - /// Legacy/global active pane tree view: mirrors the active tab's per-tab root when split. - pub(in crate::features) workspace_split: Option, - pub(in crate::features) workspace_split_resize: Option, - /// Per-tab pane trees keyed by tab-root session id (Tauri `Tab.root`). - pub(in crate::features) session_pane_roots: HashMap, - /// Leaf session id → owning tab-root session id (hidden from tab strip when secondary). - pub(in crate::features) session_tab_owner: HashMap, - pub(in crate::features) focused_terminal_window_leaf_id: Option, - /// Whether we already attempted startup restore of global workspace pane splits. - pub(in crate::features) workspace_pane_layout_restored: bool, pub(in crate::features) startup_restore_complete: bool, } diff --git a/crates/nyaterm-desktop/src/features/commands/command_runtime/suggestions.rs b/crates/nyaterm-desktop/src/features/commands/command_runtime/suggestions.rs index 2aa834b5b..af3b66550 100644 --- a/crates/nyaterm-desktop/src/features/commands/command_runtime/suggestions.rs +++ b/crates/nyaterm-desktop/src/features/commands/command_runtime/suggestions.rs @@ -1154,7 +1154,7 @@ impl NyaTermApp { insets.left, insets.top, self.terminal_gutter_width_px_for_session(session_id), - self.last_viewport_size, + self.shell.viewport.size, cursor_row, cursor_col, menu_w, diff --git a/crates/nyaterm-desktop/src/features/connections/connection_import_runtime.rs b/crates/nyaterm-desktop/src/features/connections/connection_import_runtime.rs index 6ffd81d98..d5158047b 100644 --- a/crates/nyaterm-desktop/src/features/connections/connection_import_runtime.rs +++ b/crates/nyaterm-desktop/src/features/connections/connection_import_runtime.rs @@ -47,8 +47,8 @@ impl NyaTermApp { self.connection_state.open_import_dialog(); self.connection_state.close_list_more_menu(); - self.title_menu_open = None; - self.title_menu_submenu = None; + self.shell.chrome.title_menu_open = None; + self.shell.chrome.title_menu_submenu = None; self.terminal.view.status = "select a connection import source".to_string(); let import_focus = self.connection_state.import_focus_handle(); window.focus(&import_focus); diff --git a/crates/nyaterm-desktop/src/features/connections/connections/menus.rs b/crates/nyaterm-desktop/src/features/connections/connections/menus.rs index 92d8dd3de..19f4e5358 100644 --- a/crates/nyaterm-desktop/src/features/connections/connections/menus.rs +++ b/crates/nyaterm-desktop/src/features/connections/connections/menus.rs @@ -185,8 +185,8 @@ impl NyaTermApp { ) -> bool { if self.saved_connection_start_is_pending_or_queued(&connection) { self.terminal.view.status = format!("{} is already queued", connection.name); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); return false; } @@ -200,8 +200,8 @@ impl NyaTermApp { "queued {name} ({} pending)", self.pending_saved_connection_queue.len() ); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); true } diff --git a/crates/nyaterm-desktop/src/features/inspector/ai_ask.rs b/crates/nyaterm-desktop/src/features/inspector/ai_ask.rs index a1570855b..92d3dfff9 100644 --- a/crates/nyaterm-desktop/src/features/inspector/ai_ask.rs +++ b/crates/nyaterm-desktop/src/features/inspector/ai_ask.rs @@ -687,7 +687,7 @@ impl NyaTermApp { |this, _, _, cx| { this.ai.discovery.menu_open = false; - this.settings_active_tab = + this.shell.navigation.settings.active_tab = SettingsTab::AiModels; this.open_page( NavItem::Settings, diff --git a/crates/nyaterm-desktop/src/features/inspector/ai_widgets/messages.rs b/crates/nyaterm-desktop/src/features/inspector/ai_widgets/messages.rs index 0e17b8e44..e73ab118b 100644 --- a/crates/nyaterm-desktop/src/features/inspector/ai_widgets/messages.rs +++ b/crates/nyaterm-desktop/src/features/inspector/ai_widgets/messages.rs @@ -86,7 +86,7 @@ impl NyaTermApp { }); let quote_text = state.text.clone(); let copy_text = state.text.clone(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y, menu_max_h) = ai_message_menu_position( f32::from(state.x), f32::from(state.y), diff --git a/crates/nyaterm-desktop/src/features/inspector/ai_widgets/transcript.rs b/crates/nyaterm-desktop/src/features/inspector/ai_widgets/transcript.rs index c1731bd23..cf39035b8 100644 --- a/crates/nyaterm-desktop/src/features/inspector/ai_widgets/transcript.rs +++ b/crates/nyaterm-desktop/src/features/inspector/ai_widgets/transcript.rs @@ -112,7 +112,7 @@ impl NyaTermApp { .cursor_pointer() .hover(|this| this.bg(rgb(0x2ea043))) .on_click(cx.listener(|this, _, _, cx| { - this.settings_active_tab = SettingsTab::AiGeneral; + this.shell.navigation.settings.active_tab = SettingsTab::AiGeneral; this.open_page(NavItem::Settings, cx); })) .child(self.tr("ai.setupAction")), diff --git a/crates/nyaterm-desktop/src/features/inspector/right_shell.rs b/crates/nyaterm-desktop/src/features/inspector/right_shell.rs index 0225257d1..cb9b26f23 100644 --- a/crates/nyaterm-desktop/src/features/inspector/right_shell.rs +++ b/crates/nyaterm-desktop/src/features/inspector/right_shell.rs @@ -9,9 +9,9 @@ impl NyaTermApp { window: &mut gpui::Window, cx: &mut Context, ) -> impl IntoElement { - let mut width = self.right_panel_width.clamp(200., 720.); - if !cfg!(target_os = "macos") && self.last_viewport_size.0 < 768. { - width = width.min((self.last_viewport_size.0 - 80.).max(120.)); + let mut width = self.shell.panels.right_width.clamp(200., 720.); + if !cfg!(target_os = "macos") && self.shell.viewport.size.0 < 768. { + width = width.min((self.shell.viewport.size.0 - 80.).max(120.)); } let palette = self.theme_palette(); div() diff --git a/crates/nyaterm-desktop/src/features/layout/activity_bar.rs b/crates/nyaterm-desktop/src/features/layout/activity_bar.rs index 384259d45..7ce744141 100644 --- a/crates/nyaterm-desktop/src/features/layout/activity_bar.rs +++ b/crates/nyaterm-desktop/src/features/layout/activity_bar.rs @@ -15,12 +15,12 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let Some(menu) = self.activity_bar_context_menu.clone() else { + let Some(menu) = self.shell.chrome.activity_bar_context_menu.clone() else { return div().into_any_element(); }; let entry_id = menu.entry_id.clone(); - let show_labels = self.activity_bar_layout.show_labels; - let (viewport_w, viewport_h) = self.last_viewport_size; + let show_labels = self.shell.chrome.activity_bar_layout.show_labels; + let (viewport_w, viewport_h) = self.shell.viewport.size; let menu_w = 180.; let submenu_w = 164.; let margin = 8.; @@ -191,7 +191,7 @@ impl NyaTermApp { let bottom_entries = self.activity_entries_for_zone(bottom_zone); let top_len = top_entries.len(); let bottom_len = bottom_entries.len(); - let show_labels = self.activity_bar_layout.show_labels; + let show_labels = self.shell.chrome.activity_bar_layout.show_labels; let palette = self.theme_palette(); // Tauri DropZone: gap-0.5 pt-1 diff --git a/crates/nyaterm-desktop/src/features/layout/prompts.rs b/crates/nyaterm-desktop/src/features/layout/prompts.rs index 1bfd1faa5..6f87cbe28 100644 --- a/crates/nyaterm-desktop/src/features/layout/prompts.rs +++ b/crates/nyaterm-desktop/src/features/layout/prompts.rs @@ -67,7 +67,7 @@ impl NyaTermApp { .child( div() .id("duplicate-prompt-dialog") - .w(px((self.last_viewport_size.0 - 32.).min(448.).max(280.))) + .w(px((self.shell.viewport.size.0 - 32.).min(448.).max(280.))) .rounded_md() .border_1() .border_color(rgb(palette.border)) @@ -616,7 +616,7 @@ impl NyaTermApp { prompt.id ))) .w_full() - .max_h(px((self.last_viewport_size.1 - 32.).max(240.))) + .max_h(px((self.shell.viewport.size.1 - 32.).max(240.))) .overflow_y_scroll() .rounded_md() .border_1() diff --git a/crates/nyaterm-desktop/src/features/layout/sidebar/shell.rs b/crates/nyaterm-desktop/src/features/layout/sidebar/shell.rs index aefd58146..9188989cf 100644 --- a/crates/nyaterm-desktop/src/features/layout/sidebar/shell.rs +++ b/crates/nyaterm-desktop/src/features/layout/sidebar/shell.rs @@ -9,9 +9,9 @@ impl NyaTermApp { window: &mut gpui::Window, cx: &mut Context, ) -> impl IntoElement { - let mut width = self.left_panel_width.clamp(160., 720.); - if !cfg!(target_os = "macos") && self.last_viewport_size.0 < 1024. { - width = width.min((self.last_viewport_size.0 - 80.).max(120.)); + let mut width = self.shell.panels.left_width.clamp(160., 720.); + if !cfg!(target_os = "macos") && self.shell.viewport.size.0 < 1024. { + width = width.min((self.shell.viewport.size.0 - 80.).max(120.)); } let palette = self.theme_palette(); div() diff --git a/crates/nyaterm-desktop/src/features/layout/title_bar/bar.rs b/crates/nyaterm-desktop/src/features/layout/title_bar/bar.rs index a5325e01e..4e10b7bc8 100644 --- a/crates/nyaterm-desktop/src/features/layout/title_bar/bar.rs +++ b/crates/nyaterm-desktop/src/features/layout/title_bar/bar.rs @@ -28,8 +28,8 @@ impl NyaTermApp { let palette = self.theme_palette(); let macos = cfg!(target_os = "macos"); let compact_layout = !cfg!(target_os = "macos"); - let narrow_left = compact_layout && self.last_viewport_size.0 < 1024.; - let narrow_right = compact_layout && self.last_viewport_size.0 < 768.; + let narrow_left = compact_layout && self.shell.viewport.size.0 < 1024.; + let narrow_right = compact_layout && self.shell.viewport.size.0 < 768.; let header_status_visible = self.settings.ui_header_status_visible; let header_status = self.header_status_content(); // Match Tauri Header: h-10. @@ -207,7 +207,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let menu_open = self.header_status.menu_open; + let menu_open = self.shell.chrome.header_status.menu_open; let select_label = self.tr("headerStatus.select"); div() @@ -406,14 +406,14 @@ impl NyaTermApp { } pub(in crate::features) fn toggle_header_status_menu(&mut self, cx: &mut Context) { - self.header_status.menu_open = !self.header_status.menu_open; - if self.header_status.menu_open { - self.title_menu_open = None; - self.title_menu_submenu = None; - self.open_tabs_menu_open = false; - self.new_session_menu_open = false; - self.new_session_all_sessions_open = false; - self.new_session_group_menu_path.clear(); + self.shell.chrome.header_status.menu_open = !self.shell.chrome.header_status.menu_open; + if self.shell.chrome.header_status.menu_open { + self.shell.chrome.title_menu_open = None; + self.shell.chrome.title_menu_submenu = None; + self.shell.chrome.open_tabs_menu_open = false; + self.shell.chrome.new_session_menu_open = false; + self.shell.chrome.new_session_all_sessions_open = false; + self.shell.chrome.new_session_group_menu_path.clear(); } cx.notify(); } @@ -425,8 +425,8 @@ impl NyaTermApp { ) { self.settings.ui_header_status_mode = mode.persistence_id().to_string(); self.settings.ui_header_status_visible = true; - self.header_status.menu_open = false; - self.header_status.rendered_minute = current_unix_minute(); + self.shell.chrome.header_status.menu_open = false; + self.shell.chrome.header_status.rendered_minute = current_unix_minute(); self.persist_header_status_settings(); cx.notify(); } @@ -437,13 +437,13 @@ impl NyaTermApp { cx: &mut Context, ) { self.settings.ui_header_status_visible = visible; - self.header_status.menu_open = false; + self.shell.chrome.header_status.menu_open = false; self.persist_header_status_settings(); cx.notify(); } fn persist_header_status_settings(&mut self) { - if self.settings_draft_snapshot.is_some() { + if self.shell.navigation.settings.draft_snapshot.is_some() { self.terminal.view.status = "header status changed; apply settings to persist".to_string(); } else { @@ -461,14 +461,14 @@ impl NyaTermApp { self.settings.ui_header_status_visible && HeaderStatusMode::from_setting(&self.settings.ui_header_status_mode) == HeaderStatusMode::DateTime - && self.header_status.rendered_minute != current_unix_minute() + && self.shell.chrome.header_status.rendered_minute != current_unix_minute() } pub(in crate::features) fn refresh_header_status_clock(&mut self) -> bool { if !self.header_status_clock_refresh_due() { return false; } - self.header_status.rendered_minute = current_unix_minute(); + self.shell.chrome.header_status.rendered_minute = current_unix_minute(); true } @@ -509,7 +509,7 @@ impl NyaTermApp { if let Some(failed) = self.failed_session_display_name() { return failed; } - if let Some(failed) = self.last_connect_failure_name.as_ref() { + if let Some(failed) = self.shell.chrome.last_connect_failure_name.as_ref() { return failed.clone(); } "NyaTerm".to_string() @@ -530,7 +530,8 @@ impl NyaTermApp { if self.has_pending_session_start() { return Some("icons/conn/connect.svg"); } - if self.has_failed_session_start() || self.last_connect_failure_name.is_some() { + if self.has_failed_session_start() || self.shell.chrome.last_connect_failure_name.is_some() + { return Some("icons/session/disconnect.svg"); } None @@ -541,7 +542,7 @@ impl NyaTermApp { menu: TitleMenu, cx: &mut Context, ) -> impl IntoElement { - let open = self.title_menu_open == Some(menu); + let open = self.shell.chrome.title_menu_open == Some(menu); let id_label = menu.label(); let label = self.tr(menu.i18n_key()); let palette = self.theme_palette(); @@ -585,25 +586,25 @@ impl NyaTermApp { menu: TitleMenu, cx: &mut Context, ) { - self.title_menu_open = if self.title_menu_open == Some(menu) { + self.shell.chrome.title_menu_open = if self.shell.chrome.title_menu_open == Some(menu) { None } else { Some(menu) }; - self.title_menu_submenu = None; - if self.title_menu_open.is_some() { - self.header_status.menu_open = false; - self.open_tabs_menu_open = false; - self.new_session_menu_open = false; - self.new_session_all_sessions_open = false; - self.new_session_group_menu_path.clear(); + self.shell.chrome.title_menu_submenu = None; + if self.shell.chrome.title_menu_open.is_some() { + self.shell.chrome.header_status.menu_open = false; + self.shell.chrome.open_tabs_menu_open = false; + self.shell.chrome.new_session_menu_open = false; + self.shell.chrome.new_session_all_sessions_open = false; + self.shell.chrome.new_session_group_menu_path.clear(); } cx.notify(); } pub(in crate::features) fn close_title_menu(&mut self, cx: &mut Context) { - self.title_menu_submenu = None; - if self.title_menu_open.take().is_some() { + self.shell.chrome.title_menu_submenu = None; + if self.shell.chrome.title_menu_open.take().is_some() { cx.notify(); } } @@ -613,8 +614,8 @@ impl NyaTermApp { submenu: TitleMenuSubmenu, cx: &mut Context, ) { - if self.title_menu_submenu != Some(submenu) { - self.title_menu_submenu = Some(submenu); + if self.shell.chrome.title_menu_submenu != Some(submenu) { + self.shell.chrome.title_menu_submenu = Some(submenu); cx.notify(); } } diff --git a/crates/nyaterm-desktop/src/features/layout/title_bar/menu.rs b/crates/nyaterm-desktop/src/features/layout/title_bar/menu.rs index 6cd8c335c..7be8b403a 100644 --- a/crates/nyaterm-desktop/src/features/layout/title_bar/menu.rs +++ b/crates/nyaterm-desktop/src/features/layout/title_bar/menu.rs @@ -43,10 +43,10 @@ impl NyaTermApp { Some(shortcut("tab.newSession", "Ctrl+Shift+N")), cx.listener(|this, _, window, cx| { this.close_title_menu(cx); - this.open_tabs_menu_open = false; - this.new_session_menu_open = false; - this.new_session_all_sessions_open = false; - this.new_session_group_menu_path.clear(); + this.shell.chrome.open_tabs_menu_open = false; + this.shell.chrome.new_session_menu_open = false; + this.shell.chrome.new_session_all_sessions_open = false; + this.shell.chrome.new_session_group_menu_path.clear(); this.open_connection_editor(None, None, false, window, cx); }), )) @@ -266,7 +266,7 @@ impl NyaTermApp { } } - items.when_some(self.title_menu_submenu, |this, submenu| { + items.when_some(self.shell.chrome.title_menu_submenu, |this, submenu| { this.child(self.title_menu_submenu(submenu, cx)) }) } @@ -280,7 +280,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let open = self.title_menu_submenu == Some(submenu); + let open = self.shell.chrome.title_menu_submenu == Some(submenu); title_menu_submenu_trigger( palette, id, @@ -293,10 +293,10 @@ impl NyaTermApp { } }), cx.listener(move |this, _, _, cx| { - if this.title_menu_submenu == Some(submenu) { - this.title_menu_submenu = None; + if this.shell.chrome.title_menu_submenu == Some(submenu) { + this.shell.chrome.title_menu_submenu = None; } else { - this.title_menu_submenu = Some(submenu); + this.shell.chrome.title_menu_submenu = Some(submenu); } cx.notify(); }), diff --git a/crates/nyaterm-desktop/src/features/layout/workspace/surface/empty.rs b/crates/nyaterm-desktop/src/features/layout/workspace/surface/empty.rs index 9bc5c6511..47cda64e7 100644 --- a/crates/nyaterm-desktop/src/features/layout/workspace/surface/empty.rs +++ b/crates/nyaterm-desktop/src/features/layout/workspace/surface/empty.rs @@ -151,7 +151,7 @@ impl NyaTermApp { let error = self .active_failed_session() .map(|failed| failed.error.clone()) - .or_else(|| self.last_connect_failure_error.clone()) + .or_else(|| self.shell.chrome.last_connect_failure_error.clone()) .unwrap_or_default(); div() diff --git a/crates/nyaterm-desktop/src/features/layout/workspace/surface/menus.rs b/crates/nyaterm-desktop/src/features/layout/workspace/surface/menus.rs index d8b391911..dc4fcd616 100644 --- a/crates/nyaterm-desktop/src/features/layout/workspace/surface/menus.rs +++ b/crates/nyaterm-desktop/src/features/layout/workspace/surface/menus.rs @@ -165,7 +165,9 @@ impl NyaTermApp { .as_deref() .is_some_and(|id| self.tab_root_for_session(id) == session_id); let leaf_ids = self - .session_pane_roots + .shell + .workspace + .pane_roots .get(&session_id) .map(|root| root.session_ids()) .unwrap_or_else(|| vec![session_id.clone()]); @@ -282,7 +284,7 @@ impl NyaTermApp { let no_shell_sessions_label = self.tr("terminal.noShellSessions"); let recent_sessions_label = self.tr("terminal.recentSessions"); let no_recent_sessions_label = self.tr("terminal.noRecentSessions"); - let all_sessions_open = self.new_session_all_sessions_open; + let all_sessions_open = self.shell.chrome.new_session_all_sessions_open; // Tauri TabBar new-session: shell sessions + recent by last_used. let mut shell: Vec<_> = self .connections @@ -478,7 +480,7 @@ impl NyaTermApp { &mut self, cx: &mut Context, ) -> impl IntoElement { - let path = self.new_session_group_menu_path.clone(); + let path = self.shell.chrome.new_session_group_menu_path.clone(); let visible_group_ids = new_session_visible_group_ids(&self.connections, &self.connection_groups); let mut parent_group_id = None; @@ -715,23 +717,26 @@ impl NyaTermApp { } fn open_new_session_all_sessions_menu(&mut self, cx: &mut Context) { - if !self.new_session_all_sessions_open { - self.new_session_all_sessions_open = true; - self.new_session_group_menu_path.clear(); + if !self.shell.chrome.new_session_all_sessions_open { + self.shell.chrome.new_session_all_sessions_open = true; + self.shell.chrome.new_session_group_menu_path.clear(); cx.notify(); } } fn toggle_new_session_all_sessions_menu(&mut self, cx: &mut Context) { - self.new_session_all_sessions_open = !self.new_session_all_sessions_open; - self.new_session_group_menu_path.clear(); + self.shell.chrome.new_session_all_sessions_open = + !self.shell.chrome.new_session_all_sessions_open; + self.shell.chrome.new_session_group_menu_path.clear(); cx.notify(); } fn close_new_session_all_sessions_menu(&mut self, cx: &mut Context) { - if self.new_session_all_sessions_open || !self.new_session_group_menu_path.is_empty() { - self.new_session_all_sessions_open = false; - self.new_session_group_menu_path.clear(); + if self.shell.chrome.new_session_all_sessions_open + || !self.shell.chrome.new_session_group_menu_path.is_empty() + { + self.shell.chrome.new_session_all_sessions_open = false; + self.shell.chrome.new_session_group_menu_path.clear(); cx.notify(); } } @@ -742,21 +747,27 @@ impl NyaTermApp { depth: usize, cx: &mut Context, ) { - let unchanged = self.new_session_all_sessions_open - && self.new_session_group_menu_path.get(depth) == Some(&group_id) - && self.new_session_group_menu_path.len() == depth + 1; + let unchanged = self.shell.chrome.new_session_all_sessions_open + && self.shell.chrome.new_session_group_menu_path.get(depth) == Some(&group_id) + && self.shell.chrome.new_session_group_menu_path.len() == depth + 1; if unchanged { return; } - self.new_session_all_sessions_open = true; - self.new_session_group_menu_path.truncate(depth); - self.new_session_group_menu_path.push(group_id); + self.shell.chrome.new_session_all_sessions_open = true; + self.shell + .chrome + .new_session_group_menu_path + .truncate(depth); + self.shell.chrome.new_session_group_menu_path.push(group_id); cx.notify(); } fn truncate_new_session_group_menu(&mut self, depth: usize, cx: &mut Context) { - if self.new_session_group_menu_path.len() > depth { - self.new_session_group_menu_path.truncate(depth); + if self.shell.chrome.new_session_group_menu_path.len() > depth { + self.shell + .chrome + .new_session_group_menu_path + .truncate(depth); cx.notify(); } } diff --git a/crates/nyaterm-desktop/src/features/layout/workspace/surface/tabs.rs b/crates/nyaterm-desktop/src/features/layout/workspace/surface/tabs.rs index e4991c0fe..79a6c4f3a 100644 --- a/crates/nyaterm-desktop/src/features/layout/workspace/surface/tabs.rs +++ b/crates/nyaterm-desktop/src/features/layout/workspace/surface/tabs.rs @@ -368,7 +368,7 @@ impl NyaTermApp { .then_with(|| left.2.cmp(&right.2)) .then_with(|| left.3.cmp(&right.3)) }); - if self.session_tab_scroll_into_view_pending { + if self.shell.chrome.session_tab_scroll_into_view_pending { if let Some(active_id) = self.session.active_id.as_deref() { if let Some(index) = sessions.iter().position(|session| session.id == active_id) { let pending_count = transient_tabs @@ -376,10 +376,13 @@ impl NyaTermApp { .filter(|(pending_index, _, _, _, _, _)| *pending_index <= index) .count(); let child_index = index + pending_count; - self.session_tab_strip_scroll.scroll_to_item(child_index); + self.shell + .chrome + .session_tab_strip_scroll + .scroll_to_item(child_index); } } - self.session_tab_scroll_into_view_pending = false; + self.shell.chrome.session_tab_scroll_into_view_pending = false; } let mut tabs = div() .id("session-tab-strip-scroll") @@ -391,7 +394,7 @@ impl NyaTermApp { // Tauri tab-strip-scroll: horizontal overflow instead of clipping tabs. .overflow_x_scroll() .overflow_y_hidden() - .track_scroll(&self.session_tab_strip_scroll); + .track_scroll(&self.shell.chrome.session_tab_strip_scroll); let mut transient_cursor = 0usize; for (tab_index, session) in sessions.into_iter().enumerate() { @@ -436,7 +439,9 @@ impl NyaTermApp { .as_deref() .is_some_and(|id| self.tab_root_for_session(id) == session.id); let leaf_ids = self - .session_pane_roots + .shell + .workspace + .pane_roots .get(&session.id) .map(|root| root.session_ids()) .unwrap_or_else(|| vec![session.id.clone()]); @@ -695,11 +700,17 @@ impl NyaTermApp { } // Tauri TabBar trailing chrome: optional open-tabs overflow menu + new session menu. - let open_tabs_menu = self.open_tabs_menu_open; - let new_session_menu = self.new_session_menu_open; + let open_tabs_menu = self.shell.chrome.open_tabs_menu_open; + let new_session_menu = self.shell.chrome.new_session_menu_open; let open_tabs_label = self.tr("terminal.openTabs").to_string(); let new_session_label = self.tr("terminal.newSession").to_string(); - let tab_strip_has_overflow = self.session_tab_strip_scroll.max_offset().width > px(0.); + let tab_strip_has_overflow = self + .shell + .chrome + .session_tab_strip_scroll + .max_offset() + .width + > px(0.); // Tauri shows Open Tabs only when the strip actually overflows. let show_open_tabs_menu = tab_strip_has_overflow || open_tabs_menu; diff --git a/crates/nyaterm-desktop/src/features/mod.rs b/crates/nyaterm-desktop/src/features/mod.rs index 25792b066..275d8771e 100644 --- a/crates/nyaterm-desktop/src/features/mod.rs +++ b/crates/nyaterm-desktop/src/features/mod.rs @@ -87,7 +87,7 @@ pub(in crate::features) use session::{ pub(in crate::features) use settings::{SecurityFeatureFocus, SecurityFeatureState}; pub(in crate::features) use shell::{ ChromeTooltip, SessionTabDragPayload, SessionTabDragPreview, SessionTabTooltip, - ShellFeatureState, TAB_MOUSE_ACTIONS, TabMouseActionTarget, + ShellFeatureInit, ShellFeatureState, TAB_MOUSE_ACTIONS, TabMouseActionTarget, }; pub(in crate::features) use shell::{ appearance_font_options, appearance_font_stack, gpui_code_font_family, diff --git a/crates/nyaterm-desktop/src/features/pages/connections/menus.rs b/crates/nyaterm-desktop/src/features/pages/connections/menus.rs index a0e422886..5dcd0638d 100644 --- a/crates/nyaterm-desktop/src/features/pages/connections/menus.rs +++ b/crates/nyaterm-desktop/src/features/pages/connections/menus.rs @@ -59,7 +59,7 @@ impl NyaTermApp { cx: &mut Context, ) -> AnyElement { let palette = self.theme_palette(); - let (viewport_width, viewport_height) = self.last_viewport_size; + let (viewport_width, viewport_height) = self.shell.viewport.size; let max_height = connection_menu_max_height(viewport_height); let submenu_offset = connection_submenu_offset(f32::from(x), viewport_width); deferred( @@ -130,7 +130,7 @@ impl NyaTermApp { cx: &mut Context, ) -> AnyElement { let palette = self.theme_palette(); - let max_height = connection_menu_max_height(self.last_viewport_size.1); + let max_height = connection_menu_max_height(self.shell.viewport.size.1); let ungrouped_ids = move_ids.clone(); let mut panel = div() .id(SharedString::from(id)) diff --git a/crates/nyaterm-desktop/src/features/pages/remote/docker_view.rs b/crates/nyaterm-desktop/src/features/pages/remote/docker_view.rs index b214ad631..fe6ef0e66 100644 --- a/crates/nyaterm-desktop/src/features/pages/remote/docker_view.rs +++ b/crates/nyaterm-desktop/src/features/pages/remote/docker_view.rs @@ -290,7 +290,7 @@ impl NyaTermApp { self.tr("dockerManager.compose").to_string(), ], self.tr("common.more").to_string(), - self.right_panel_width, + self.shell.panels.right_width, self.remote_ops.docker.tab_menu_open, cx, )) diff --git a/crates/nyaterm-desktop/src/features/pages/remote/process_view.rs b/crates/nyaterm-desktop/src/features/pages/remote/process_view.rs index a09a51d75..74063717a 100644 --- a/crates/nyaterm-desktop/src/features/pages/remote/process_view.rs +++ b/crates/nyaterm-desktop/src/features/pages/remote/process_view.rs @@ -99,7 +99,7 @@ impl NyaTermApp { .cloned() .collect::>(); // Responsive mode first so hidden columns do not keep invalid sort keys. - let mode = process_display_mode(self.right_panel_width); + let mode = process_display_mode(self.shell.panels.right_width); if mode != ProcessDisplayMode::Wide && self.remote_ops.process.sort_key == RemoteProcessSortKey::User { diff --git a/crates/nyaterm-desktop/src/features/pages/settings/mod.rs b/crates/nyaterm-desktop/src/features/pages/settings/mod.rs index b8c82da18..e3227e293 100644 --- a/crates/nyaterm-desktop/src/features/pages/settings/mod.rs +++ b/crates/nyaterm-desktop/src/features/pages/settings/mod.rs @@ -21,7 +21,7 @@ impl NyaTermApp { &mut self, cx: &mut Context, ) -> impl IntoElement { - self.settings_surface(self.last_viewport_size.0, false, cx) + self.settings_surface(self.shell.viewport.size.0, false, cx) } pub(in crate::features) fn settings_window_view( @@ -61,8 +61,8 @@ impl NyaTermApp { // Tauri SettingsPage shell: compact header + narrow nav + scroll content. let palette = self.theme_palette(); let settings_title = self.tr("settings.title"); - let active_group = self.tr(self.settings_active_tab.group_i18n_key()); - let active_label = self.tr(self.settings_active_tab.i18n_key()); + let active_group = self.tr(self.shell.navigation.settings.active_tab.group_i18n_key()); + let active_label = self.tr(self.shell.navigation.settings.active_tab.i18n_key()); let back_label = self.tr("common.close"); div() .flex() @@ -303,9 +303,24 @@ impl NyaTermApp { } else { 224. }; - let workspace_expanded = self.settings_expanded_groups.contains("workspace"); - let terminal_expanded = self.settings_expanded_groups.contains("terminal_session"); - let ai_expanded = self.settings_expanded_groups.contains("ai_group"); + let workspace_expanded = self + .shell + .navigation + .settings + .expanded_groups + .contains("workspace"); + let terminal_expanded = self + .shell + .navigation + .settings + .expanded_groups + .contains("terminal_session"); + let ai_expanded = self + .shell + .navigation + .settings + .expanded_groups + .contains("ai_group"); let mut sidebar_nav = div() .id(SharedString::from("settings-sidebar-scroll")) @@ -540,8 +555,14 @@ impl NyaTermApp { this.tooltip(move |_, cx| cx.new(|_| ChromeTooltip::new(title)).into()) }) .on_click(cx.listener(move |this, _, _, cx| { - if !this.settings_expanded_groups.insert(group.to_string()) { - this.settings_expanded_groups.remove(group); + if !this + .shell + .navigation + .settings + .expanded_groups + .insert(group.to_string()) + { + this.shell.navigation.settings.expanded_groups.remove(group); } cx.notify(); })) @@ -556,7 +577,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let selected = self.settings_active_tab == tab; + let selected = self.shell.navigation.settings.active_tab == tab; let label = self.tr(tab.i18n_key()); // Tauri settings nav item: soft primary fill, no permanent green border. @@ -619,7 +640,7 @@ impl NyaTermApp { this.tooltip(move |_, cx| cx.new(|_| ChromeTooltip::new(label)).into()) }) .on_click(cx.listener(move |this, _, _, cx| { - this.settings_active_tab = tab; + this.shell.navigation.settings.active_tab = tab; this.settings_state.appearance.menu_open = None; cx.notify(); })) @@ -631,7 +652,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let active_tab = self.settings_active_tab; + let active_tab = self.shell.navigation.settings.active_tab; let active_label = self.tr(active_tab.i18n_key()); let content = self.settings_tab_content(active_tab, cx); let compact = viewport_width < 640.; diff --git a/crates/nyaterm-desktop/src/features/pages/settings/sync_backup/cloud_sync/mod.rs b/crates/nyaterm-desktop/src/features/pages/settings/sync_backup/cloud_sync/mod.rs index 2a20b9302..c5c12ee10 100644 --- a/crates/nyaterm-desktop/src/features/pages/settings/sync_backup/cloud_sync/mod.rs +++ b/crates/nyaterm-desktop/src/features/pages/settings/sync_backup/cloud_sync/mod.rs @@ -436,7 +436,8 @@ impl NyaTermApp { "cloud-open-security", self.tr("settings.openSecuritySettings"), cx.listener(|this, _, _, cx| { - this.settings_active_tab = SettingsTab::Security; + this.shell.navigation.settings.active_tab = + SettingsTab::Security; cx.notify(); }), )), diff --git a/crates/nyaterm-desktop/src/features/pages/settings/workspace/appearance.rs b/crates/nyaterm-desktop/src/features/pages/settings/workspace/appearance.rs index ca35911d7..8f0eb893d 100644 --- a/crates/nyaterm-desktop/src/features/pages/settings/workspace/appearance.rs +++ b/crates/nyaterm-desktop/src/features/pages/settings/workspace/appearance.rs @@ -57,7 +57,7 @@ impl NyaTermApp { settings_switch( palette, "appearance-panel-multi-open", - self.panel_multi_open, + self.shell.panels.multi_open, cx.listener(|this, _, _, cx| { this.toggle_panel_multi_open(cx); }), diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/browser/view.rs b/crates/nyaterm-desktop/src/features/pages/transfers/browser/view.rs index 8d50c2bec..60ff078ba 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/browser/view.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/browser/view.rs @@ -287,7 +287,7 @@ impl NyaTermApp { } else { // Tauri File Explorer virtual list (30px rows and overscan). let viewport_rows = transfer_browser_viewport_rows( - self.last_viewport_size.1, + self.shell.viewport.size.1, self.transfer.panel.height, self.transfer.browser.viewport_height, ); @@ -527,7 +527,7 @@ impl NyaTermApp { let parent_count = usize::from(current_path != "/" && current_path != "."); let total = this.visible_transfer_browser_entries().len() + parent_count; let viewport_rows = transfer_browser_viewport_rows( - this.last_viewport_size.1, + this.shell.viewport.size.1, this.transfer.panel.height, this.transfer.browser.viewport_height, ); diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays.rs index cb988fcee..ba800a7c1 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays.rs @@ -32,7 +32,7 @@ impl NyaTermApp { let description = self .tr("fileTransfer.deleteConfirmDesc") .replace("{{name}}", &state.title); - let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 320.); + let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 320.); div() .id(SharedString::from("transfer-job-delete-overlay")) @@ -152,7 +152,7 @@ impl NyaTermApp { let retry_id = state.job_id.clone(); let open_id = state.job_id.clone(); let delete_id = state.job_id.clone(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y, menu_max_height) = transfer_menu_position( f32::from(state.x), f32::from(state.y), diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_context.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_context.rs index d85dd666a..dfb80ceca 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_context.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_context.rs @@ -46,7 +46,7 @@ impl NyaTermApp { .map(|entry| self.enabled_transfer_file_ai_actions_for_entry(entry)) .unwrap_or_default(); let has_ai_actions = !selected_file_ai_actions.is_empty(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let preferred_height = if state.is_current_directory { 380. } else if state.is_parent { diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_create.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_create.rs index 027c6993a..b8597d8ee 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_create.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_create.rs @@ -23,7 +23,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 500.); + let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 500.); let state = self .transfer .file_ops @@ -228,7 +228,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 500.); + let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 500.); let state = self .transfer .file_ops @@ -431,7 +431,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 480.); + let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 480.); let state = self .transfer .file_ops diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_delete_move.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_delete_move.rs index d0f1f5af8..e333c9110 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_delete_move.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_delete_move.rs @@ -42,7 +42,7 @@ impl NyaTermApp { .map(|path| remote_file_name(path)) .collect::>(); let remaining_items = delete_count.saturating_sub(preview_items.len()); - let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 320.); + let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 320.); div() .id(SharedString::from("transfer-delete-overlay")) @@ -170,7 +170,7 @@ impl NyaTermApp { cx, ) .into_any_element(); - let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 384.); + let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 384.); div() .id(SharedString::from("transfer-move-overlay")) diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_editor.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_editor.rs index 6f47685a2..2ea9cf279 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_editor.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_editor.rs @@ -365,7 +365,7 @@ impl NyaTermApp { .min_w_0() .flex() .overflow_x_scroll(); - let tabs_menu_max_height = (self.last_viewport_size.1 - 48.).clamp(160., 360.); + let tabs_menu_max_height = (self.shell.viewport.size.1 - 48.).clamp(160., 360.); let tabs_menu_bg = if standalone { rgb(palette.surface) } else { diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_favorites.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_favorites.rs index ee0686f8d..9b72d96bf 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_favorites.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_favorites.rs @@ -57,7 +57,7 @@ impl NyaTermApp { .iter() .cloned() .collect::>(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y, menu_max_height) = transfer_menu_position( f32::from(state.x), f32::from(state.y), diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_properties.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_properties.rs index 590a0f79a..fc65183a2 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_properties.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_properties.rs @@ -80,8 +80,8 @@ impl NyaTermApp { let property_mode = parse_transfer_mode(&state.mode_value) .or(entry.permissions) .unwrap_or(0o644); - let dialog_width = (self.last_viewport_size.0 - 32.).min(460.).max(280.); - let dialog_max_height = (self.last_viewport_size.1 * 0.75).clamp(320., 720.); + let dialog_width = (self.shell.viewport.size.0 - 32.).min(460.).max(280.); + let dialog_max_height = (self.shell.viewport.size.1 * 0.75).clamp(320., 720.); let owner_input = self .text_input_box( "transfer.properties.owner", diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_unknown.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_unknown.rs index 9fd357e74..87e77c8ff 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_unknown.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_unknown.rs @@ -38,7 +38,7 @@ impl NyaTermApp { } else { state.entry.name.clone() }; - let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 512.); + let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 512.); let description = self .tr("fileExplorer.unknownFileTypeDesc") .replace("{{name}}", &name); diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_upload.rs b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_upload.rs index ed07678ad..02ca43573 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/overlays_upload.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/overlays_upload.rs @@ -48,7 +48,7 @@ impl NyaTermApp { x: px(24.), y: px(24.), }); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y, menu_max_height) = transfer_menu_position( f32::from(state.x), f32::from(state.y), diff --git a/crates/nyaterm-desktop/src/features/pages/transfers/path_bar.rs b/crates/nyaterm-desktop/src/features/pages/transfers/path_bar.rs index 967645427..2cbe481a1 100644 --- a/crates/nyaterm-desktop/src/features/pages/transfers/path_bar.rs +++ b/crates/nyaterm-desktop/src/features/pages/transfers/path_bar.rs @@ -316,7 +316,7 @@ impl NyaTermApp { TransferBrowserChildrenMenuStatus::Error(_) => 112., }, }; - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y, menu_max_height) = transfer_menu_position( f32::from(menu.x), f32::from(menu.y), diff --git a/crates/nyaterm-desktop/src/features/pages/workspace/mod.rs b/crates/nyaterm-desktop/src/features/pages/workspace/mod.rs index d8a3dece1..f19df376e 100644 --- a/crates/nyaterm-desktop/src/features/pages/workspace/mod.rs +++ b/crates/nyaterm-desktop/src/features/pages/workspace/mod.rs @@ -17,8 +17,8 @@ impl NyaTermApp { // register/close/idle already keep terminal_windows coherent. let multi_leaf = self.terminal_windows_is_multi_leaf(); let has_connect_failure = self.has_failed_session_start() - || (self.last_connect_failure_name.is_some() - && self.last_connect_failure_error.is_some()); + || (self.shell.chrome.last_connect_failure_name.is_some() + && self.shell.chrome.last_connect_failure_error.is_some()); let show_tab_strip = !multi_leaf && (self.ordered_tab_session_count() > 0 || self.has_pending_session_start() @@ -52,8 +52,8 @@ impl NyaTermApp { return self.pending_workspace_state().into_any_element(); } if self.has_failed_session_start() - || (self.last_connect_failure_name.is_some() - && self.last_connect_failure_error.is_some()) + || (self.shell.chrome.last_connect_failure_name.is_some() + && self.shell.chrome.last_connect_failure_error.is_some()) { return self.failed_workspace_state().into_any_element(); } @@ -72,10 +72,10 @@ impl NyaTermApp { .into_any_element(); } } - let root = self - .workspace_split - .clone() - .unwrap_or_else(|| WorkspacePaneNode::leaf(self.session.active_id.clone().unwrap())); + let root = + self.shell.workspace.split.clone().unwrap_or_else(|| { + WorkspacePaneNode::leaf(self.session.active_id.clone().unwrap()) + }); let show_chrome = root.is_split(); div() diff --git a/crates/nyaterm-desktop/src/features/pages/workspace/terminal_windows.rs b/crates/nyaterm-desktop/src/features/pages/workspace/terminal_windows.rs index 395493bf8..45fd3fa63 100644 --- a/crates/nyaterm-desktop/src/features/pages/workspace/terminal_windows.rs +++ b/crates/nyaterm-desktop/src/features/pages/workspace/terminal_windows.rs @@ -77,7 +77,9 @@ impl NyaTermApp { .unwrap_or(("Session", "icons/conn/terminal.svg")); let custom_color = self.session.tab_colors.get(tab_id).copied(); let leaf_ids = self - .session_pane_roots + .shell + .workspace + .pane_roots .get(tab_id) .map(|root| root.session_ids()) .unwrap_or_else(|| vec![tab_id.clone()]); @@ -327,7 +329,8 @@ impl NyaTermApp { .on_click(cx.listener({ let leaf_id = id.clone(); move |this, _, window, cx| { - this.focused_terminal_window_leaf_id = Some(leaf_id.clone()); + this.shell.workspace.focused_terminal_leaf_id = + Some(leaf_id.clone()); this.start_local_session(window, cx); } })), @@ -396,7 +399,8 @@ impl NyaTermApp { cx.listener({ let leaf_id = id.clone(); move |this, _, _, cx| { - this.focused_terminal_window_leaf_id = Some(leaf_id.clone()); + this.shell.workspace.focused_terminal_leaf_id = + Some(leaf_id.clone()); cx.notify(); } }), diff --git a/crates/nyaterm-desktop/src/features/panels/active_session_menu_overlay.rs b/crates/nyaterm-desktop/src/features/panels/active_session_menu_overlay.rs index d7508eb06..aa42da7a1 100644 --- a/crates/nyaterm-desktop/src/features/panels/active_session_menu_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/active_session_menu_overlay.rs @@ -29,7 +29,7 @@ impl NyaTermApp { } else { self.tr("tabCtx.disconnect").to_string() }; - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y) = active_session_clamped_menu_position( f32::from(menu.x) - 132., f32::from(menu.y) + 8., diff --git a/crates/nyaterm-desktop/src/features/panels/connection_import_overlay.rs b/crates/nyaterm-desktop/src/features/panels/connection_import_overlay.rs index 66a58f7f5..15a5143f9 100644 --- a/crates/nyaterm-desktop/src/features/panels/connection_import_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/connection_import_overlay.rs @@ -13,8 +13,8 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let narrow = self.last_viewport_size.0 < 520.; - let width = (self.last_viewport_size.0 - 32.).clamp(280., 480.); + let narrow = self.shell.viewport.size.0 < 520.; + let width = (self.shell.viewport.size.0 - 32.).clamp(280., 480.); let docs_url = if self .settings .language diff --git a/crates/nyaterm-desktop/src/features/panels/multi_line_paste_overlay.rs b/crates/nyaterm-desktop/src/features/panels/multi_line_paste_overlay.rs index ede19a72f..7e55869d8 100644 --- a/crates/nyaterm-desktop/src/features/panels/multi_line_paste_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/multi_line_paste_overlay.rs @@ -14,7 +14,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let draft = self .terminal .paste diff --git a/crates/nyaterm-desktop/src/features/panels/quick_command_category_menu_overlay.rs b/crates/nyaterm-desktop/src/features/panels/quick_command_category_menu_overlay.rs index 93ea180b9..0282c9436 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_command_category_menu_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_command_category_menu_overlay.rs @@ -14,7 +14,7 @@ impl NyaTermApp { let category_id = menu.category_id.clone(); let rename_id = category_id.clone(); let delete_id = category_id.clone(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y) = quick_command_category_clamped_menu_position( f32::from(menu.x), f32::from(menu.y), diff --git a/crates/nyaterm-desktop/src/features/panels/quick_command_category_overlays.rs b/crates/nyaterm-desktop/src/features/panels/quick_command_category_overlays.rs index 7f8dc9159..885309761 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_command_category_overlays.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_command_category_overlays.rs @@ -38,7 +38,7 @@ impl NyaTermApp { .child( div() .id(SharedString::from("quick-command-category-delete-dialog")) - .w(px((self.last_viewport_size.0 - 32.).clamp(280., 384.))) + .w(px((self.shell.viewport.size.0 - 32.).clamp(280., 384.))) .rounded_md() .border_1() .border_color(rgb(0x7f1d1d)) @@ -136,7 +136,7 @@ impl NyaTermApp { .child( div() .id(SharedString::from("quick-command-category-rename-dialog")) - .w(px((self.last_viewport_size.0 - 32.).clamp(280., 384.))) + .w(px((self.shell.viewport.size.0 - 32.).clamp(280., 384.))) .rounded_md() .border_1() .border_color(rgb(palette.border)) diff --git a/crates/nyaterm-desktop/src/features/panels/quick_command_delete_overlay.rs b/crates/nyaterm-desktop/src/features/panels/quick_command_delete_overlay.rs index 9b417ddae..a8936f556 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_command_delete_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_command_delete_overlay.rs @@ -35,7 +35,7 @@ impl NyaTermApp { .child( div() .id(SharedString::from("quick-command-delete-dialog")) - .w(px((self.last_viewport_size.0 - 32.).clamp(280., 384.))) + .w(px((self.shell.viewport.size.0 - 32.).clamp(280., 384.))) .rounded_md() .border_1() .border_color(rgb(0x7f1d1d)) diff --git a/crates/nyaterm-desktop/src/features/panels/quick_command_details_overlay.rs b/crates/nyaterm-desktop/src/features/panels/quick_command_details_overlay.rs index 6ad7a945d..40a3a831c 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_command_details_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_command_details_overlay.rs @@ -30,7 +30,7 @@ impl NyaTermApp { .filter(|description| !description.is_empty()); let command_text = command.command.clone(); let estimated_h = if description.is_some() { 224. } else { 182. }; - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (popover_x, popover_y) = quick_command_details_popover_position( f32::from(anchor_x) - 304., f32::from(anchor_y) - estimated_h - 6., diff --git a/crates/nyaterm-desktop/src/features/panels/quick_command_editor_overlay.rs b/crates/nyaterm-desktop/src/features/panels/quick_command_editor_overlay.rs index 40e660f60..c6cf3fa90 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_command_editor_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_command_editor_overlay.rs @@ -17,7 +17,7 @@ impl NyaTermApp { &mut self, cx: &mut Context, ) -> AnyElement { - self.quick_command_editor_surface(self.last_viewport_size.0, false, cx) + self.quick_command_editor_surface(self.shell.viewport.size.0, false, cx) } pub(in crate::features) fn quick_command_editor_window_view( diff --git a/crates/nyaterm-desktop/src/features/panels/quick_command_import_overlay.rs b/crates/nyaterm-desktop/src/features/panels/quick_command_import_overlay.rs index ac0d3a51e..8a0927db8 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_command_import_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_command_import_overlay.rs @@ -48,7 +48,7 @@ impl NyaTermApp { .child( div() .id(SharedString::from("quick-command-import-dialog")) - .w(px((self.last_viewport_size.0 - 32.).clamp(280., 380.))) + .w(px((self.shell.viewport.size.0 - 32.).clamp(280., 380.))) .max_w_full() .rounded_md() .border_1() diff --git a/crates/nyaterm-desktop/src/features/panels/quick_command_row_menu_overlay.rs b/crates/nyaterm-desktop/src/features/panels/quick_command_row_menu_overlay.rs index 410567615..1cb8991af 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_command_row_menu_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_command_row_menu_overlay.rs @@ -16,7 +16,7 @@ impl NyaTermApp { let all_command_id = command_id.clone(); let delete_command_id = command_id.clone(); let can_send_to_all = self.live_session_count() > 1; - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y) = quick_command_clamped_menu_position( f32::from(menu.x), f32::from(menu.y), diff --git a/crates/nyaterm-desktop/src/features/panels/quick_command_variable_overlay.rs b/crates/nyaterm-desktop/src/features/panels/quick_command_variable_overlay.rs index cfd2a01f7..3775dd83e 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_command_variable_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_command_variable_overlay.rs @@ -147,7 +147,7 @@ impl NyaTermApp { .child( div() .id(SharedString::from("quick-command-variable-dialog")) - .w(px((self.last_viewport_size.0 - 32.).clamp(280., 400.))) + .w(px((self.shell.viewport.size.0 - 32.).clamp(280., 400.))) .max_w_full() .rounded_md() .border_1() @@ -173,7 +173,7 @@ impl NyaTermApp { div() .id("quick-command-variable-body") .p_5() - .max_h(px((self.last_viewport_size.1 * 0.6).clamp(180., 420.))) + .max_h(px((self.shell.viewport.size.1 * 0.6).clamp(180., 420.))) .overflow_y_scroll() .child(rows) .child( diff --git a/crates/nyaterm-desktop/src/features/panels/quick_commands_panel/panel/mod.rs b/crates/nyaterm-desktop/src/features/panels/quick_commands_panel/panel/mod.rs index 51d8c228b..4e0d34dd5 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_commands_panel/panel/mod.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_commands_panel/panel/mod.rs @@ -63,11 +63,11 @@ impl NyaTermApp { let category_sidebar = self.quick_command_category_sidebar(categories, palette, cx); let view_mode = self.quick_command_state.list.view_mode; let tile_columns = quick_command_tile_column_count( - self.last_viewport_size.0, - self.left_panel_width, - self.right_panel_width, - !self.left_sidebar_collapsed, - !self.right_inspector_collapsed, + self.shell.viewport.size.0, + self.shell.panels.left_width, + self.shell.panels.right_width, + !self.shell.panels.left_collapsed, + !self.shell.panels.right_collapsed, ); let logical_row_height = match view_mode { QuickCommandViewMode::Tile => 32., diff --git a/crates/nyaterm-desktop/src/features/panels/quick_switch_overlay.rs b/crates/nyaterm-desktop/src/features/panels/quick_switch_overlay.rs index 6ba155048..8fd0ebe80 100644 --- a/crates/nyaterm-desktop/src/features/panels/quick_switch_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/quick_switch_overlay.rs @@ -14,7 +14,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let items = self.filtered_quick_switch_items(cx); self.update_quick_switch_state(cx, |store| { store.clamp_quick_switch_selected_index(items.len()) @@ -28,7 +28,7 @@ impl NyaTermApp { cx, ); let query_focus = query_input.read(cx).focus_handle(); - let list_max_height = (self.last_viewport_size.1 * 0.55).clamp(160., 384.); + let list_max_height = (self.shell.viewport.size.1 * 0.55).clamp(160., 384.); let selected_row_bg = rgba((palette.primary << 8) | 0x26); let hover_row_bg = self.shell_surface_color(palette.hover); let mut rows = div() diff --git a/crates/nyaterm-desktop/src/features/panels/session_confirm_overlays.rs b/crates/nyaterm-desktop/src/features/panels/session_confirm_overlays.rs index 079347932..ba4992ef5 100644 --- a/crates/nyaterm-desktop/src/features/panels/session_confirm_overlays.rs +++ b/crates/nyaterm-desktop/src/features/panels/session_confirm_overlays.rs @@ -54,7 +54,7 @@ impl NyaTermApp { .child( div() .id("close-all-sessions-confirm-dialog") - .w(px((self.last_viewport_size.0 - 32.).clamp(280., 400.))) + .w(px((self.shell.viewport.size.0 - 32.).clamp(280., 400.))) .max_w_full() .mx_4() .rounded_md() diff --git a/crates/nyaterm-desktop/src/features/panels/session_overlays.rs b/crates/nyaterm-desktop/src/features/panels/session_overlays.rs index 92d687af4..7c55a5e52 100644 --- a/crates/nyaterm-desktop/src/features/panels/session_overlays.rs +++ b/crates/nyaterm-desktop/src/features/panels/session_overlays.rs @@ -23,7 +23,7 @@ impl NyaTermApp { ) .into_any_element(); let can_save = !self.session.dialogs.rename_draft.trim().is_empty(); - let dialog_width = (self.last_viewport_size.0 - 32.).clamp(280., 320.); + let dialog_width = (self.shell.viewport.size.0 - 32.).clamp(280., 320.); div() .id(SharedString::from("rename-tab-overlay")) @@ -350,7 +350,7 @@ impl NyaTermApp { .into_any_element(); let can_submit = !self.session.dialogs.startup_command_draft.trim().is_empty(); let delay_label = format!("{} ms", self.session.dialogs.startup_command_delay_ms); - let dialog_width = (self.last_viewport_size.0 - 32.).clamp(280., 448.); + let dialog_width = (self.shell.viewport.size.0 - 32.).clamp(280., 448.); div() .id(SharedString::from("startup-command-overlay")) diff --git a/crates/nyaterm-desktop/src/features/panels/sync_groups_overlay.rs b/crates/nyaterm-desktop/src/features/panels/sync_groups_overlay.rs index 6166d151b..1a0b0d93b 100644 --- a/crates/nyaterm-desktop/src/features/panels/sync_groups_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/sync_groups_overlay.rs @@ -17,7 +17,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let dialog_width = (viewport_w - 24.).clamp(280., 900.); let dialog_height = (viewport_h - 56.).clamp(280., 500.); let groups_width = (dialog_width * 0.23).clamp(160., 208.); diff --git a/crates/nyaterm-desktop/src/features/panels/tab_actions_overlay/compact.rs b/crates/nyaterm-desktop/src/features/panels/tab_actions_overlay/compact.rs index edc833665..2ffaf531d 100644 --- a/crates/nyaterm-desktop/src/features/panels/tab_actions_overlay/compact.rs +++ b/crates/nyaterm-desktop/src/features/panels/tab_actions_overlay/compact.rs @@ -33,7 +33,7 @@ impl NyaTermApp { _session_count: usize, cx: &mut Context, ) -> gpui::AnyElement { - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let menu_max_height = (viewport_h - 16.).clamp(160., 440.); let (menu_x, menu_y) = if let Some((x, y)) = self.session.dialogs.tab_actions_anchor { clamp_tab_actions_position(x, y, 240., menu_max_height, viewport_w, viewport_h) diff --git a/crates/nyaterm-desktop/src/features/panels/tab_actions_overlay/overlay.rs b/crates/nyaterm-desktop/src/features/panels/tab_actions_overlay/overlay.rs index 7f4e49a8e..a55468380 100644 --- a/crates/nyaterm-desktop/src/features/panels/tab_actions_overlay/overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/tab_actions_overlay/overlay.rs @@ -69,9 +69,9 @@ impl NyaTermApp { .active_id .as_deref() .map(|id| self.tab_root_for_session(id)) - .and_then(|root| self.session_pane_roots.get(&root)) + .and_then(|root| self.shell.workspace.pane_roots.get(&root)) .is_some_and(|root| root.is_split()) - || self.workspace_split.is_some(); + || self.shell.workspace.split.is_some(); let scroll_offset = self .terminal .view diff --git a/crates/nyaterm-desktop/src/features/panels/temporary_ssh_link_overlay.rs b/crates/nyaterm-desktop/src/features/panels/temporary_ssh_link_overlay.rs index a88381247..c90852801 100644 --- a/crates/nyaterm-desktop/src/features/panels/temporary_ssh_link_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/temporary_ssh_link_overlay.rs @@ -55,7 +55,7 @@ impl NyaTermApp { .child( div() .id(SharedString::from("temporary-ssh-link-dialog")) - .w(px((self.last_viewport_size.0 - 32.).clamp(280., 480.))) + .w(px((self.shell.viewport.size.0 - 32.).clamp(280., 480.))) .max_w_full() .mx_4() .rounded_md() diff --git a/crates/nyaterm-desktop/src/features/panels/terminal_actions_overlay.rs b/crates/nyaterm-desktop/src/features/panels/terminal_actions_overlay.rs index 2f4a345f1..406452fce 100644 --- a/crates/nyaterm-desktop/src/features/panels/terminal_actions_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/terminal_actions_overlay.rs @@ -14,7 +14,7 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let visible_text = self.active_terminal_visible_text(); let buffer_tail = self.active_terminal_buffer_tail(); let visible_lines = visible_text.lines().count(); @@ -347,7 +347,7 @@ impl NyaTermApp { self.tr("terminalActions.sessionLog"), cx.listener(|this, _, _, cx| { this.terminal.menus.actions_open = false; - this.right_focus = RightFocus::Recording; + this.shell.panels.right_focus = RightFocus::Recording; cx.notify(); }), )) diff --git a/crates/nyaterm-desktop/src/features/panels/update_overlay.rs b/crates/nyaterm-desktop/src/features/panels/update_overlay.rs index 8c3515b85..d6a22dd32 100644 --- a/crates/nyaterm-desktop/src/features/panels/update_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/update_overlay.rs @@ -21,8 +21,8 @@ impl NyaTermApp { let available = !checking && !failed && update_info.as_ref().is_some_and(|info| info.available); let portable = self.runtime.mode() == RuntimeMode::Portable; - let (_, viewport_h) = self.last_viewport_size; - let dialog_width = (self.last_viewport_size.0 - 32.).clamp(320., 560.); + let (_, viewport_h) = self.shell.viewport.size; + let dialog_width = (self.shell.viewport.size.0 - 32.).clamp(320., 560.); let release_url = update_info .as_ref() .and_then(|info| info.html_url.clone()) diff --git a/crates/nyaterm-desktop/src/features/remote/remote_runtime/docker.rs b/crates/nyaterm-desktop/src/features/remote/remote_runtime/docker.rs index eeedb39d4..a50fe9084 100644 --- a/crates/nyaterm-desktop/src/features/remote/remote_runtime/docker.rs +++ b/crates/nyaterm-desktop/src/features/remote/remote_runtime/docker.rs @@ -233,7 +233,7 @@ impl NyaTermApp { if !command.ends_with('\n') { command.push('\n'); } - self.selected_nav = NavItem::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; if self.send_terminal_input(command.into_bytes(), cx) { self.remote_ops.docker.status = status; self.terminal.view.status = self.remote_ops.docker.status.clone(); diff --git a/crates/nyaterm-desktop/src/features/root.rs b/crates/nyaterm-desktop/src/features/root.rs index 34140a25c..3bdaf2524 100644 --- a/crates/nyaterm-desktop/src/features/root.rs +++ b/crates/nyaterm-desktop/src/features/root.rs @@ -82,23 +82,23 @@ impl NyaTermApp { .on_mouse_down( MouseButton::Left, cx.listener(|this, _, _, cx| { - let changed = this.title_menu_open.is_some() - || this.header_status.menu_open - || this.open_tabs_menu_open - || this.new_session_menu_open - || this.new_session_all_sessions_open - || !this.new_session_group_menu_path.is_empty() + let changed = this.shell.chrome.title_menu_open.is_some() + || this.shell.chrome.header_status.menu_open + || this.shell.chrome.open_tabs_menu_open + || this.shell.chrome.new_session_menu_open + || this.shell.chrome.new_session_all_sessions_open + || !this.shell.chrome.new_session_group_menu_path.is_empty() || this.remote_ops.docker.tab_menu_open || this.remote_ops.docker.header_menu_open || this.connection_state.list_more_menu_is_open(); if changed { - this.title_menu_open = None; - this.title_menu_submenu = None; - this.header_status.menu_open = false; - this.open_tabs_menu_open = false; - this.new_session_menu_open = false; - this.new_session_all_sessions_open = false; - this.new_session_group_menu_path.clear(); + this.shell.chrome.title_menu_open = None; + this.shell.chrome.title_menu_submenu = None; + this.shell.chrome.header_status.menu_open = false; + this.shell.chrome.open_tabs_menu_open = false; + this.shell.chrome.new_session_menu_open = false; + this.shell.chrome.new_session_all_sessions_open = false; + this.shell.chrome.new_session_group_menu_path.clear(); this.remote_ops.docker.tab_menu_open = false; this.remote_ops.docker.header_menu_open = false; this.connection_state.close_list_more_menu(); @@ -205,7 +205,7 @@ impl NyaTermApp { .into(); if let Some((tile_width, tile_height)) = wallpaper_tile_size { let (columns, rows) = - wallpaper_tile_grid(self.last_viewport_size, (tile_width, tile_height)); + wallpaper_tile_grid(self.shell.viewport.size, (tile_width, tile_height)); let mut layer = div() .absolute() .inset_0() @@ -251,11 +251,12 @@ impl NyaTermApp { } fn wallpaper_tile_size(&mut self, path: &str) -> (f32, f32) { - if let Some((cached_path, width, height)) = self.wallpaper_tile_dimensions.as_ref() + if let Some((cached_path, width, height)) = + self.shell.viewport.wallpaper_tile_dimensions.as_ref() && cached_path == path { return fit_wallpaper_tile_size( - self.last_viewport_size, + self.shell.viewport.size, (*width as f32, *height as f32), ); } @@ -263,8 +264,8 @@ impl NyaTermApp { .ok() .filter(|(width, height)| *width > 0 && *height > 0) .unwrap_or((256, 256)); - self.wallpaper_tile_dimensions = Some((path.to_string(), width, height)); - fit_wallpaper_tile_size(self.last_viewport_size, (width as f32, height as f32)) + self.shell.viewport.wallpaper_tile_dimensions = Some((path.to_string(), width, height)); + fit_wallpaper_tile_size(self.shell.viewport.size, (width as f32, height as f32)) } fn workspace_surface( @@ -273,7 +274,9 @@ impl NyaTermApp { window: &mut Window, cx: &mut Context, ) -> AnyElement { - if self.main_mode == MainMode::Page && self.selected_nav == NavItem::Settings { + if self.shell.navigation.main_mode == MainMode::Page + && self.shell.navigation.selected_nav == NavItem::Settings + { div() .flex() .flex_1() @@ -285,15 +288,15 @@ impl NyaTermApp { let compact_layout = !cfg!(target_os = "macos"); let has_left_activity_items = self.activity_side_has_items(ActivitySide::Left); let has_right_activity_items = self.activity_side_has_items(ActivitySide::Right); - let left_overlay_mode = compact_layout && self.last_viewport_size.0 < 1024.; - let right_overlay_mode = compact_layout && self.last_viewport_size.0 < 768.; + let left_overlay_mode = compact_layout && self.shell.viewport.size.0 < 1024.; + let right_overlay_mode = compact_layout && self.shell.viewport.size.0 < 768.; let left_drawer_open = has_left_activity_items && left_overlay_mode - && self.mobile_left_open + && self.shell.panels.mobile_left_open && self.left_side_open(); let right_drawer_open = has_right_activity_items && right_overlay_mode - && self.mobile_right_open + && self.shell.panels.mobile_right_open && self.right_side_open(); let mut surface = div() .flex() @@ -324,7 +327,7 @@ impl NyaTermApp { this.child(self.activity_bar(ActivitySide::Right, cx)) }); - if self.terminal_windows_is_multi_leaf() && self.new_session_menu_open { + if self.terminal_windows_is_multi_leaf() && self.shell.chrome.new_session_menu_open { surface = surface.child(self.render_new_session_menu(cx)); } @@ -336,8 +339,8 @@ impl NyaTermApp { .inset_0() .bg(rgba(0x00000080)) .on_click(cx.listener(|this, _, _, cx| { - this.mobile_left_open = false; - this.mobile_right_open = false; + this.shell.panels.mobile_left_open = false; + this.shell.panels.mobile_right_open = false; cx.notify(); })), ); @@ -439,9 +442,9 @@ impl NyaTermApp { ) .on_click(cx.listener(move |this, _, _, cx| { if left { - this.mobile_left_open = false; + this.shell.panels.mobile_left_open = false; } else { - this.mobile_right_open = false; + this.shell.panels.mobile_right_open = false; } cx.notify(); })), @@ -633,9 +636,10 @@ impl NyaTermApp { .when(quick_switch_open, |this| { this.child(self.quick_switch_overlay(cx)) }) - .when(self.activity_bar_context_menu.is_some(), |this| { - this.child(self.activity_bar_context_menu_overlay(cx)) - }) + .when( + self.shell.chrome.activity_bar_context_menu.is_some(), + |this| this.child(self.activity_bar_context_menu_overlay(cx)), + ) .when(overlay.locked, |this| { this.child(self.lock_screen_overlay(window, cx)) }) @@ -657,8 +661,8 @@ impl NyaTermApp { } fn modal_child_window_open(&self) -> bool { - self.settings_window.is_some() - || self.settings_window_open_pending + self.shell.navigation.settings.window.is_some() + || self.shell.navigation.settings.window_open_pending || self.quick_command_state.editor.window.is_some() || self.quick_command_state.editor.window_open_pending || self.connection_state.editor_modal_window_open_or_pending() @@ -667,9 +671,9 @@ impl NyaTermApp { } fn activate_modal_child_window(&mut self, cx: &mut Context) -> bool { - if self.settings_window.is_some() { + if self.shell.navigation.settings.window.is_some() { self.activate_settings_window(cx) - } else if self.settings_window_open_pending { + } else if self.shell.navigation.settings.window_open_pending { true } else if self.quick_command_state.editor.window.is_some() { self.activate_quick_command_window(cx) diff --git a/crates/nyaterm-desktop/src/features/session/session_dialog_runtime.rs b/crates/nyaterm-desktop/src/features/session/session_dialog_runtime.rs index 1684e46dc..563208660 100644 --- a/crates/nyaterm-desktop/src/features/session/session_dialog_runtime.rs +++ b/crates/nyaterm-desktop/src/features/session/session_dialog_runtime.rs @@ -214,9 +214,8 @@ impl NyaTermApp { self.clear_terminal_mouse_report_for_session(session_id); self.session.start.reconnect_failures.remove(session_id); self.session.order.retain(|id| id != session_id); - self.session_tab_owner.remove(session_id); // If this leaf was a tab root, drop its pane tree (prune will rekey survivors). - self.session_pane_roots.remove(session_id); + self.shell.workspace.remove_session(session_id); let multiplex_key = self .session .metadata diff --git a/crates/nyaterm-desktop/src/features/session/session_lifecycle.rs b/crates/nyaterm-desktop/src/features/session/session_lifecycle.rs index c11426f91..e86e3ce12 100644 --- a/crates/nyaterm-desktop/src/features/session/session_lifecycle.rs +++ b/crates/nyaterm-desktop/src/features/session/session_lifecycle.rs @@ -105,8 +105,8 @@ impl NyaTermApp { ); } } - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); } @@ -169,8 +169,8 @@ impl NyaTermApp { existing_multiplex, cx, ); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); } @@ -420,8 +420,8 @@ impl NyaTermApp { self.session .busy_actions .retain(|id, _| self.session.metadata.contains_key(id)); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); } @@ -463,17 +463,7 @@ impl NyaTermApp { .insert(new_id.to_string(), history); } - let mut pane_roots = std::mem::take(&mut self.session_pane_roots); - for root in pane_roots.values_mut() { - root.replace_session_id(old_id, new_id); - } - if let Some(root) = pane_roots.remove(old_id) { - pane_roots.insert(new_id.to_string(), root); - } - self.session_pane_roots = pane_roots; - if let Some(root) = self.workspace_split.as_mut() { - root.replace_session_id(old_id, new_id); - } + self.shell.workspace.replace_session_id(old_id, new_id); if let Some(root) = self.terminal.windows.tree.as_mut() { root.replace_tab_id(old_id, new_id); } @@ -481,7 +471,6 @@ impl NyaTermApp { if self.session.active_id.as_deref() == Some(old_id) { self.activate_session_id(new_id); } - self.rebuild_session_tab_owners(); self.sync_workspace_split_from_active_tab(); } } diff --git a/crates/nyaterm-desktop/src/features/session/session_order.rs b/crates/nyaterm-desktop/src/features/session/session_order.rs index 49a81635f..274588035 100644 --- a/crates/nyaterm-desktop/src/features/session/session_order.rs +++ b/crates/nyaterm-desktop/src/features/session/session_order.rs @@ -128,7 +128,9 @@ impl NyaTermApp { /// True when this session is a secondary leaf inside another tab's pane tree /// (Tauri: multiple SessionPanes under one Tab, only one strip entry). pub(in crate::features) fn is_secondary_pane_session(&self, session_id: &str) -> bool { - self.session_tab_owner + self.shell + .workspace + .tab_owner .get(session_id) .is_some_and(|owner| owner != session_id) } @@ -138,7 +140,7 @@ impl NyaTermApp { let mut current = session_id.to_string(); // Flatten owner chains defensively. for _ in 0..8 { - match self.session_tab_owner.get(¤t) { + match self.shell.workspace.tab_owner.get(¤t) { Some(owner) if owner != ¤t => current = owner.clone(), _ => break, } @@ -176,7 +178,7 @@ impl NyaTermApp { return active.to_string(); } } - if let Some(root) = self.session_pane_roots.get(tab_root) { + if let Some(root) = self.shell.workspace.pane_roots.get(tab_root) { if let Some(first) = root.session_ids().into_iter().next() { return first; } diff --git a/crates/nyaterm-desktop/src/features/session/session_runtime/background.rs b/crates/nyaterm-desktop/src/features/session/session_runtime/background.rs index d1d118a95..904583e2e 100644 --- a/crates/nyaterm-desktop/src/features/session/session_runtime/background.rs +++ b/crates/nyaterm-desktop/src/features/session/session_runtime/background.rs @@ -50,10 +50,10 @@ impl NyaTermApp { if !self.session.start.select_pending(&request_id) { return; } - self.open_tabs_menu_open = false; - self.new_session_menu_open = false; - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.chrome.open_tabs_menu_open = false; + self.shell.chrome.new_session_menu_open = false; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); } @@ -80,10 +80,10 @@ impl NyaTermApp { if !self.session.start.select_failed(&request_id) { return; } - self.open_tabs_menu_open = false; - self.new_session_menu_open = false; - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.chrome.open_tabs_menu_open = false; + self.shell.chrome.new_session_menu_open = false; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); } @@ -96,8 +96,8 @@ impl NyaTermApp { return; }; if !self.session.start.has_failed() { - self.last_connect_failure_name = None; - self.last_connect_failure_error = None; + self.shell.chrome.last_connect_failure_name = None; + self.shell.chrome.last_connect_failure_error = None; } self.terminal.view.status = format!( "closed failed connection {}", @@ -136,8 +136,8 @@ impl NyaTermApp { } = registration; if reconnect_session_id.is_none() { - self.last_connect_failure_name = None; - self.last_connect_failure_error = None; + self.shell.chrome.last_connect_failure_name = None; + self.shell.chrome.last_connect_failure_error = None; } self.session.start.panes.insert( request_id.clone(), @@ -174,8 +174,8 @@ impl NyaTermApp { // Status + connecting tab already show progress; avoid full terminal decode // work on the click path before the worker even starts. let _ = append_start_log; - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); request_id } @@ -469,8 +469,8 @@ impl NyaTermApp { match event.result { Ok(success) => { let ui_register_started_at = Instant::now(); - self.last_connect_failure_name = None; - self.last_connect_failure_error = None; + self.shell.chrome.last_connect_failure_name = None; + self.shell.chrome.last_connect_failure_error = None; let session_info = success.session_info; let session_id = session_info.id.clone(); let reconnect_session_id = pending @@ -640,7 +640,7 @@ impl NyaTermApp { self.schedule_startup_command(session_id.clone(), startup_command, cx); } self.apply_pending_workspace_split_for_duplicate(&session_id); - self.selected_nav = NavItem::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; let ui_register_duration = ui_register_started_at.elapsed(); let request_to_ui_duration = requested_at .map(|requested_at| requested_at.elapsed()) @@ -685,8 +685,8 @@ impl NyaTermApp { let _ = self.session.start.reconnect_replace_id.take(); } if !reconnect_failure { - self.last_connect_failure_name = Some(connection_name.clone()); - self.last_connect_failure_error = Some(error.clone()); + self.shell.chrome.last_connect_failure_name = Some(connection_name.clone()); + self.shell.chrome.last_connect_failure_error = Some(error.clone()); } if let Some(session_id) = reconnect_session_id { if self.session.metadata.contains_key(&session_id) { @@ -725,7 +725,7 @@ impl NyaTermApp { connection_name )); } - self.selected_nav = NavItem::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; let request_to_ui_duration = requested_at .map(|requested_at| requested_at.elapsed()) .unwrap_or(worker_duration + worker_to_ui_duration); diff --git a/crates/nyaterm-desktop/src/features/session/session_runtime/start.rs b/crates/nyaterm-desktop/src/features/session/session_runtime/start.rs index 0ce78c39f..c5cd7dc54 100644 --- a/crates/nyaterm-desktop/src/features/session/session_runtime/start.rs +++ b/crates/nyaterm-desktop/src/features/session/session_runtime/start.rs @@ -80,8 +80,8 @@ impl NyaTermApp { if self.saved_connection_start_is_pending_or_queued(&connection) { self.terminal.view.status = format!("{} is already connecting or queued", connection.name); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); return; } diff --git a/crates/nyaterm-desktop/src/features/session/session_state.rs b/crates/nyaterm-desktop/src/features/session/session_state.rs index 8be78fe71..1531569a9 100644 --- a/crates/nyaterm-desktop/src/features/session/session_state.rs +++ b/crates/nyaterm-desktop/src/features/session/session_state.rs @@ -244,10 +244,7 @@ impl NyaTermApp { pub(in crate::features) fn activate_session_id(&mut self, session_id: &str) -> Option { self.session.start.active_pending = None; self.session.start.active_failed = None; - self.open_tabs_menu_open = false; - self.new_session_menu_open = false; - self.new_session_all_sessions_open = false; - self.new_session_group_menu_path.clear(); + self.shell.chrome.prepare_session_switch(); // Session switch resets terminal-output credential autofill (Tauri XTerminal remount). self.terminal.assist.reset_for_session_switch(); let previous_session_id = self.session.active_id.clone(); @@ -265,7 +262,6 @@ impl NyaTermApp { self.transfer.queue.job_delete = None; self.reset_remote_runtime_for_session_switch(); } - self.session_tab_scroll_into_view_pending = true; // Keep workspace_split mirrored to the active tab's per-tab pane root. self.sync_workspace_split_from_active_tab(); self.transfer.browser.auto_sync_cwd_last_at = None; @@ -279,9 +275,9 @@ impl NyaTermApp { // Transfer browser state is only needed when the transfers panel is open // or we already have cached browser state for this session. Skipping the // full reset on every activate keeps connect/switch chrome responsive. - let transfers_panel_visible = self.active_left_panel == Some(NavItem::Transfers) - || self.active_right_panel == Some(NavItem::Transfers) - || self.selected_nav == NavItem::Transfers; + let transfers_panel_visible = self.shell.panels.active_left == Some(NavItem::Transfers) + || self.shell.panels.active_right == Some(NavItem::Transfers) + || self.shell.navigation.selected_nav == NavItem::Transfers; if transfers_panel_visible || self.transfer.browser.session_cache.contains_key(session_id) || !self.transfer.browser.entries.is_empty() @@ -435,7 +431,7 @@ impl NyaTermApp { } else { format!("active {}", short_id(&focus_id)) }; - self.selected_nav = NavItem::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; cx.notify(); } @@ -464,8 +460,8 @@ impl NyaTermApp { let next_index = (active_index as isize + offset).rem_euclid(len) as usize; let session_id = sessions[next_index].id.clone(); self.activate_session_id_with_surface_sync(&session_id, cx); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.terminal.view.status = format!("active {}", short_id(&session_id)); cx.notify(); } @@ -484,49 +480,30 @@ impl NyaTermApp { let index = index.min(sessions.len().saturating_sub(1)); let session_id = sessions[index].id.clone(); self.activate_session_id_with_surface_sync(&session_id, cx); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.terminal.view.status = format!("active {}", short_id(&session_id)); cx.notify(); } pub(in crate::features) fn toggle_open_tabs_menu(&mut self, cx: &mut Context) { - self.open_tabs_menu_open = !self.open_tabs_menu_open; - if self.open_tabs_menu_open { - self.new_session_menu_open = false; - self.new_session_all_sessions_open = false; - self.new_session_group_menu_path.clear(); - self.title_menu_open = None; - } + self.shell.chrome.toggle_open_tabs_menu(); cx.notify(); } pub(in crate::features) fn close_open_tabs_menu(&mut self, cx: &mut Context) { - if self.open_tabs_menu_open { - self.open_tabs_menu_open = false; + if self.shell.chrome.close_open_tabs_menu() { cx.notify(); } } pub(in crate::features) fn toggle_new_session_menu(&mut self, cx: &mut Context) { - self.new_session_menu_open = !self.new_session_menu_open; - if self.new_session_menu_open { - self.open_tabs_menu_open = false; - self.title_menu_open = None; - } - self.new_session_all_sessions_open = false; - self.new_session_group_menu_path.clear(); + self.shell.chrome.toggle_new_session_menu(); cx.notify(); } pub(in crate::features) fn close_new_session_menu(&mut self, cx: &mut Context) { - let changed = self.new_session_menu_open - || self.new_session_all_sessions_open - || !self.new_session_group_menu_path.is_empty(); - self.new_session_menu_open = false; - self.new_session_all_sessions_open = false; - self.new_session_group_menu_path.clear(); - if changed { + if self.shell.chrome.close_new_session_menu() { cx.notify(); } } diff --git a/crates/nyaterm-desktop/src/features/session/startup_restore_runtime.rs b/crates/nyaterm-desktop/src/features/session/startup_restore_runtime.rs index 7c17567b7..d0d68d5b5 100644 --- a/crates/nyaterm-desktop/src/features/session/startup_restore_runtime.rs +++ b/crates/nyaterm-desktop/src/features/session/startup_restore_runtime.rs @@ -198,7 +198,7 @@ impl NyaTermApp { .into_iter() .map(|session| { let mut tab = self.serialize_open_tab_for_session(&session); - if let Some(root) = self.session_pane_roots.get(&session.id) { + if let Some(root) = self.shell.workspace.pane_roots.get(&session.id) { if root.is_split() { if let Some(pane_root) = self.workspace_pane_to_restorable_pane(root) { tab.root = Some(pane_root); @@ -241,14 +241,16 @@ impl NyaTermApp { /// entry whose `root` is a Tauri RestorablePaneNode tree (for interop). fn serialize_open_tabs_as_single_pane_tab(&self) -> Option> { // Only collapse to one open_tabs entry when exactly one split tree covers every session. - if self.session_pane_roots.len() > 1 { + if self.shell.workspace.pane_roots.len() > 1 { return None; } let root = self - .session_pane_roots + .shell + .workspace + .pane_roots .values() .find(|root| root.is_split()) - .or(self.workspace_split.as_ref())?; + .or(self.shell.workspace.split.as_ref())?; if !root.is_split() { return None; } @@ -464,7 +466,7 @@ impl NyaTermApp { self.mark_startup_restore_complete(cx); // After all tabs reconnect, attempt multi-leaf then global pane layout restore. self.terminal.windows.restored = false; - self.workspace_pane_layout_restored = false; + self.shell.workspace.pane_layout_restored = false; self.try_restore_terminal_window_layout(); // Prefer stored ui.workspace_pane_layout only when no open_tabs per-tab roots exist. // open_tabs[].root maps to per-tab session_pane_roots (Tauri Tab.root). @@ -479,7 +481,7 @@ impl NyaTermApp { if pending_layouts.is_empty() { self.try_restore_workspace_pane_layout(); } else { - self.workspace_pane_layout_restored = true; + self.shell.workspace.pane_layout_restored = true; for layout in pending_layouts { self.apply_restorable_workspace_pane_layout(layout); } @@ -498,9 +500,11 @@ impl NyaTermApp { } if self.terminal_windows_is_multi_leaf() { self.terminal.view.status = "restored workspace tabs and window layout".to_string(); - } else if !self.session_pane_roots.is_empty() + } else if !self.shell.workspace.pane_roots.is_empty() || self - .workspace_split + .shell + .workspace + .split .as_ref() .is_some_and(|root| root.is_split()) { @@ -600,20 +604,23 @@ impl NyaTermApp { return; }; // Avoid clobbering an existing distinct per-tab tree for the same root. - if let Some(existing) = self.session_pane_roots.get(&first) { + if let Some(existing) = self.shell.workspace.pane_roots.get(&first) { if existing != &restored { // Prefer the newly restored tree from open_tabs for this root. } } - self.session_pane_roots.insert(first.clone(), restored); + self.shell + .workspace + .pane_roots + .insert(first.clone(), restored); self.rebuild_session_tab_owners(); if self.session.active_id.is_none() { self.session.active_id = Some(first); } self.sync_workspace_split_from_active_tab(); - self.workspace_pane_layout_restored = true; - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.workspace.pane_layout_restored = true; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.terminal.view.status = "restored pane layout from open_tabs root".to_string(); } } diff --git a/crates/nyaterm-desktop/src/features/settings/security_runtime/unlock.rs b/crates/nyaterm-desktop/src/features/settings/security_runtime/unlock.rs index 61eb0748d..6f8a332e0 100644 --- a/crates/nyaterm-desktop/src/features/settings/security_runtime/unlock.rs +++ b/crates/nyaterm-desktop/src/features/settings/security_runtime/unlock.rs @@ -76,7 +76,7 @@ impl NyaTermApp { ) { self.security.unlock.master_required_prompt_open = false; self.security.unlock.pending_action = None; - self.settings_active_tab = SettingsTab::Security; + self.shell.navigation.settings.active_tab = SettingsTab::Security; self.open_page(NavItem::Settings, cx); } diff --git a/crates/nyaterm-desktop/src/features/settings/settings_runtime/draft.rs b/crates/nyaterm-desktop/src/features/settings/settings_runtime/draft.rs index bf0b8d65e..efbe879f6 100644 --- a/crates/nyaterm-desktop/src/features/settings/settings_runtime/draft.rs +++ b/crates/nyaterm-desktop/src/features/settings/settings_runtime/draft.rs @@ -8,10 +8,10 @@ use crate::models::{CloudSyncSecretDraft, MainMode, NavItem, TranslationSecretDr impl NyaTermApp { pub(in crate::features) fn begin_settings_draft(&mut self) { - if self.settings_draft_snapshot.is_some() { + if self.shell.navigation.settings.draft_snapshot.is_some() { return; } - self.settings_draft_snapshot = Some(SettingsDraftSnapshot { + self.shell.navigation.settings.draft_snapshot = Some(SettingsDraftSnapshot { settings: self.settings.clone(), ai_settings: self.ai.settings.config.clone(), ai_model_draft: self.ai.settings.model_draft.clone(), @@ -28,7 +28,7 @@ impl NyaTermApp { } pub(in crate::features) fn settings_draft_dirty(&self) -> bool { - let Some(snapshot) = self.settings_draft_snapshot.as_ref() else { + let Some(snapshot) = self.shell.navigation.settings.draft_snapshot.as_ref() else { return false; }; snapshot.settings != self.settings @@ -50,7 +50,7 @@ impl NyaTermApp { &mut self, cx: &mut Context, ) -> bool { - if self.settings_draft_snapshot.is_none() { + if self.shell.navigation.settings.draft_snapshot.is_none() { return false; } self.store_status.message = "settings draft changed".to_string(); @@ -241,10 +241,10 @@ impl NyaTermApp { } pub(in crate::features) fn rebase_open_settings_draft(&mut self) { - if self.settings_draft_snapshot.is_none() { + if self.shell.navigation.settings.draft_snapshot.is_none() { return; } - self.settings_draft_snapshot = None; + self.shell.navigation.settings.draft_snapshot = None; self.settings_master_password_enabled = self.settings.has_master_password; self.settings_master_password_draft.clear(); self.begin_settings_draft(); @@ -255,7 +255,7 @@ impl NyaTermApp { close_after_apply: bool, cx: &mut Context, ) { - if self.settings_draft_snapshot.is_none() { + if self.shell.navigation.settings.draft_snapshot.is_none() { if close_after_apply { self.finish_settings_page(cx); } @@ -357,7 +357,7 @@ impl NyaTermApp { store.save_workspace_pane_layout(None) }); } - self.settings_draft_snapshot = None; + self.shell.navigation.settings.draft_snapshot = None; self.store_status.message = "settings applied".to_string(); self.store_status.ready = true; self.terminal.view.status = "settings applied".to_string(); @@ -378,7 +378,7 @@ impl NyaTermApp { } pub(in crate::features) fn cancel_settings(&mut self, cx: &mut Context) { - if let Some(snapshot) = self.settings_draft_snapshot.take() { + if let Some(snapshot) = self.shell.navigation.settings.draft_snapshot.take() { self.apply_gpui_settings(snapshot.settings); self.ai.settings.config = snapshot.ai_settings; self.ai.settings.model_draft = snapshot.ai_model_draft; @@ -410,7 +410,7 @@ impl NyaTermApp { if self.settings_draft_dirty() { self.apply_settings_draft(true, cx); } else { - self.settings_draft_snapshot = None; + self.shell.navigation.settings.draft_snapshot = None; self.finish_settings_page(cx); } } @@ -454,22 +454,30 @@ impl NyaTermApp { self.forget_text_inputs("ai.settings.action."); self.forget_text_inputs("ai.settings.manual-model."); self.forget_text_inputs("keyword.highlight."); - self.settings_window = None; - self.settings_window_open_pending = false; - if self.main_mode == MainMode::Page && self.selected_nav == NavItem::Settings { - self.main_mode = MainMode::Workspace; - self.left_sidebar_collapsed = self - .settings_previous_left_collapsed + self.shell.navigation.settings.window = None; + self.shell.navigation.settings.window_open_pending = false; + if self.shell.navigation.main_mode == MainMode::Page + && self.shell.navigation.selected_nav == NavItem::Settings + { + self.shell.navigation.main_mode = MainMode::Workspace; + self.shell.panels.left_collapsed = self + .shell + .navigation + .settings + .previous_left_collapsed .take() - .unwrap_or_else(|| self.active_left_panel.is_none()); - self.right_inspector_collapsed = self - .settings_previous_right_collapsed + .unwrap_or_else(|| self.shell.panels.active_left.is_none()); + self.shell.panels.right_collapsed = self + .shell + .navigation + .settings + .previous_right_collapsed .take() - .unwrap_or_else(|| self.active_right_panel.is_none()); + .unwrap_or_else(|| self.shell.panels.active_right.is_none()); self.persist_ui_layout(); } else { - self.settings_previous_left_collapsed = None; - self.settings_previous_right_collapsed = None; + self.shell.navigation.settings.previous_left_collapsed = None; + self.shell.navigation.settings.previous_right_collapsed = None; } self.terminal.view.status = "settings closed".to_string(); cx.notify(); diff --git a/crates/nyaterm-desktop/src/features/settings/settings_runtime/general_interaction.rs b/crates/nyaterm-desktop/src/features/settings/settings_runtime/general_interaction.rs index b62446bdc..c7c14676c 100644 --- a/crates/nyaterm-desktop/src/features/settings/settings_runtime/general_interaction.rs +++ b/crates/nyaterm-desktop/src/features/settings/settings_runtime/general_interaction.rs @@ -24,7 +24,9 @@ impl NyaTermApp { ) { self.settings.startup_restore_window_layout = !self.settings.startup_restore_window_layout; self.save_general_settings(cx); - if !self.settings.startup_restore_window_layout && self.settings_draft_snapshot.is_none() { + if !self.settings.startup_restore_window_layout + && self.shell.navigation.settings.draft_snapshot.is_none() + { // Clear stored layouts when the user disables restore. let _ = ConnectionStore::open_with_portable_key_path( self.runtime.config_dir(), @@ -149,7 +151,7 @@ impl NyaTermApp { self.settings.interaction_command_suggestions_enabled = !self.settings.interaction_command_suggestions_enabled; if !self.settings.interaction_command_suggestions_enabled - && self.settings_draft_snapshot.is_none() + && self.shell.navigation.settings.draft_snapshot.is_none() { self.terminal.assist.clear_command_tracking(); } diff --git a/crates/nyaterm-desktop/src/features/settings/settings_runtime/terminal_remote.rs b/crates/nyaterm-desktop/src/features/settings/settings_runtime/terminal_remote.rs index 2f0d9a3f2..697ea01e6 100644 --- a/crates/nyaterm-desktop/src/features/settings/settings_runtime/terminal_remote.rs +++ b/crates/nyaterm-desktop/src/features/settings/settings_runtime/terminal_remote.rs @@ -48,7 +48,7 @@ impl NyaTermApp { ) { let next = (self.settings.terminal_scrollback_lines as i32 + delta).clamp(100, 100_000); self.settings.terminal_scrollback_lines = next as u32; - if self.settings_draft_snapshot.is_none() { + if self.shell.navigation.settings.draft_snapshot.is_none() { self.enforce_terminal_scrollback_limit(); } self.save_terminal_settings(cx); diff --git a/crates/nyaterm-desktop/src/features/settings_window.rs b/crates/nyaterm-desktop/src/features/settings_window.rs index 04489171c..812af766c 100644 --- a/crates/nyaterm-desktop/src/features/settings_window.rs +++ b/crates/nyaterm-desktop/src/features/settings_window.rs @@ -24,10 +24,18 @@ impl SettingsWindow { impl Render for SettingsWindow { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.app.read(cx).settings_draft_snapshot.is_none() { + if self + .app + .read(cx) + .shell + .navigation + .settings + .draft_snapshot + .is_none() + { self.app.update(cx, |app, cx| { - app.settings_window = None; - app.settings_window_open_pending = false; + app.shell.navigation.settings.window = None; + app.shell.navigation.settings.window_open_pending = false; cx.notify(); }); window.defer(cx, |window, _| window.remove_window()); @@ -77,7 +85,7 @@ impl Render for SettingsWindow { impl NyaTermApp { pub(in crate::features) fn activate_settings_window(&mut self, cx: &mut Context) -> bool { - let Some(handle) = self.settings_window else { + let Some(handle) = self.shell.navigation.settings.window else { return false; }; let app = cx.entity(); @@ -87,8 +95,14 @@ impl NyaTermApp { .is_err() { let _ = app.update(cx, |app, cx| { - if app.settings_window.is_some_and(|current| current == handle) { - app.settings_window = None; + if app + .shell + .navigation + .settings + .window + .is_some_and(|current| current == handle) + { + app.shell.navigation.settings.window = None; cx.notify(); } }); @@ -101,15 +115,15 @@ impl NyaTermApp { if self.activate_settings_window(cx) { return true; } - if self.settings_window_open_pending { + if self.shell.navigation.settings.window_open_pending { return true; } - self.settings_window_open_pending = true; + self.shell.navigation.settings.window_open_pending = true; cx.notify(); let app = cx.entity(); cx.defer(move |cx| { - let should_open = app.read(cx).settings_window_open_pending; + let should_open = app.read(cx).shell.navigation.settings.window_open_pending; if should_open { open_settings_window_now_from_app(app, cx); } @@ -119,17 +133,24 @@ impl NyaTermApp { } fn open_settings_window_now_from_app(app: Entity, cx: &mut App) { - if app.read(cx).settings_window.is_some() { + if app.read(cx).shell.navigation.settings.window.is_some() { let _ = app.update(cx, |app, cx| { - app.settings_window_open_pending = false; + app.shell.navigation.settings.window_open_pending = false; app.activate_settings_window(cx); cx.notify(); }); return; } - if app.read(cx).settings_draft_snapshot.is_none() { + if app + .read(cx) + .shell + .navigation + .settings + .draft_snapshot + .is_none() + { let _ = app.update(cx, |app, cx| { - app.settings_window_open_pending = false; + app.shell.navigation.settings.window_open_pending = false; cx.notify(); }); return; @@ -152,8 +173,8 @@ fn open_settings_window_now_from_app(app: Entity, cx: &mut App) { window.on_window_should_close(cx, move |_, cx| { close_app.update(cx, |app, cx| { app.cancel_settings(cx); - app.settings_window = None; - app.settings_window_open_pending = false; + app.shell.navigation.settings.window = None; + app.shell.navigation.settings.window_open_pending = false; }); true }); @@ -163,19 +184,19 @@ fn open_settings_window_now_from_app(app: Entity, cx: &mut App) { let _ = app.update(cx, |app, cx| match result { Ok(handle) => { - app.settings_window = Some(handle); - app.settings_window_open_pending = false; - app.settings_previous_left_collapsed = None; - app.settings_previous_right_collapsed = None; + app.shell.navigation.settings.window = Some(handle); + app.shell.navigation.settings.window_open_pending = false; + app.shell.navigation.settings.previous_left_collapsed = None; + app.shell.navigation.settings.previous_right_collapsed = None; cx.notify(); } Err(error) => { - app.settings_window = None; - app.settings_window_open_pending = false; - app.main_mode = MainMode::Page; - app.selected_nav = crate::models::NavItem::Settings; - app.left_sidebar_collapsed = true; - app.right_inspector_collapsed = true; + app.shell.navigation.settings.window = None; + app.shell.navigation.settings.window_open_pending = false; + app.shell.navigation.main_mode = MainMode::Page; + app.shell.navigation.selected_nav = crate::models::NavItem::Settings; + app.shell.panels.left_collapsed = true; + app.shell.panels.right_collapsed = true; app.terminal.view.status = format!("failed to open settings window: {error}"); cx.notify(); } diff --git a/crates/nyaterm-desktop/src/features/shell/activity_bar_runtime.rs b/crates/nyaterm-desktop/src/features/shell/activity_bar_runtime.rs index 3a0636d8f..fbf33ef3c 100644 --- a/crates/nyaterm-desktop/src/features/shell/activity_bar_runtime.rs +++ b/crates/nyaterm-desktop/src/features/shell/activity_bar_runtime.rs @@ -73,14 +73,16 @@ impl NyaTermApp { }; zones .into_iter() - .any(|zone| !self.activity_bar_layout.zone(zone).is_empty()) + .any(|zone| !self.shell.chrome.activity_bar_layout.zone(zone).is_empty()) } pub(in crate::features) fn activity_entries_for_zone( &self, zone: ActivityBarZone, ) -> Vec { - self.activity_bar_layout + self.shell + .chrome + .activity_bar_layout .zone(zone) .iter() .filter_map(|id| ActivityBarEntry::from_persistence_id(id)) @@ -88,15 +90,20 @@ impl NyaTermApp { } pub(in crate::features) fn sync_activity_layout_to_settings(&mut self) { - self.settings.ui_activity_bar_left_top = self.activity_bar_layout.left_top.clone(); - self.settings.ui_activity_bar_left_bottom = self.activity_bar_layout.left_bottom.clone(); - self.settings.ui_activity_bar_right_top = self.activity_bar_layout.right_top.clone(); - self.settings.ui_activity_bar_right_bottom = self.activity_bar_layout.right_bottom.clone(); - self.settings.ui_activity_bar_show_labels = self.activity_bar_layout.show_labels; + self.settings.ui_activity_bar_left_top = + self.shell.chrome.activity_bar_layout.left_top.clone(); + self.settings.ui_activity_bar_left_bottom = + self.shell.chrome.activity_bar_layout.left_bottom.clone(); + self.settings.ui_activity_bar_right_top = + self.shell.chrome.activity_bar_layout.right_top.clone(); + self.settings.ui_activity_bar_right_bottom = + self.shell.chrome.activity_bar_layout.right_bottom.clone(); + self.settings.ui_activity_bar_show_labels = + self.shell.chrome.activity_bar_layout.show_labels; } pub(in crate::features) fn apply_activity_layout_from_settings(&mut self) { - self.activity_bar_layout = ActivityBarLayoutState { + self.shell.chrome.activity_bar_layout = ActivityBarLayoutState { left_top: self.settings.ui_activity_bar_left_top.clone(), left_bottom: self.settings.ui_activity_bar_left_bottom.clone(), right_top: self.settings.ui_activity_bar_right_top.clone(), @@ -110,7 +117,14 @@ impl NyaTermApp { let mut seen = std::collections::HashSet::new(); for zone in ActivityBarZone::all() { let mut next = Vec::new(); - for id in self.activity_bar_layout.zone(zone).iter().cloned() { + for id in self + .shell + .chrome + .activity_bar_layout + .zone(zone) + .iter() + .cloned() + { if id == "fileTransfer" { continue; } @@ -118,7 +132,7 @@ impl NyaTermApp { next.push(id); } } - *self.activity_bar_layout.zone_mut(zone) = next; + *self.shell.chrome.activity_bar_layout.zone_mut(zone) = next; } // Keep intentionally empty zones empty. Tauri only restores missing entries; // it does not repopulate a zone after the user moves its last item away. @@ -131,15 +145,20 @@ impl NyaTermApp { .cloned() .collect::>(); if !missing.is_empty() { - self.activity_bar_layout.zone_mut(zone).extend(missing); + self.shell + .chrome + .activity_bar_layout + .zone_mut(zone) + .extend(missing); seen.extend(defaults.zone(zone).iter().cloned()); } } } pub(in crate::features) fn toggle_activity_bar_labels(&mut self, cx: &mut Context) { - self.activity_bar_layout.show_labels = !self.activity_bar_layout.show_labels; - self.terminal.view.status = if self.activity_bar_layout.show_labels { + self.shell.chrome.activity_bar_layout.show_labels = + !self.shell.chrome.activity_bar_layout.show_labels; + self.terminal.view.status = if self.shell.chrome.activity_bar_layout.show_labels { "activity labels shown".to_string() } else { "activity labels hidden".to_string() @@ -156,8 +175,8 @@ impl NyaTermApp { event: &MouseDownEvent, cx: &mut Context, ) { - self.title_menu_open = None; - self.activity_bar_context_menu = Some(ActivityBarContextMenuState { + self.shell.chrome.title_menu_open = None; + self.shell.chrome.activity_bar_context_menu = Some(ActivityBarContextMenuState { entry_id, zone, index, @@ -169,7 +188,7 @@ impl NyaTermApp { } pub(in crate::features) fn open_activity_bar_move_submenu(&mut self, cx: &mut Context) { - let Some(menu) = self.activity_bar_context_menu.as_mut() else { + let Some(menu) = self.shell.chrome.activity_bar_context_menu.as_mut() else { return; }; if !menu.move_submenu_open { @@ -179,7 +198,7 @@ impl NyaTermApp { } pub(in crate::features) fn close_activity_bar_context_menu(&mut self, cx: &mut Context) { - self.activity_bar_context_menu = None; + self.shell.chrome.activity_bar_context_menu = None; cx.notify(); } @@ -190,7 +209,8 @@ impl NyaTermApp { target_index: Option, cx: &mut Context, ) { - let Some((source_zone, source_index)) = self.activity_bar_layout.find_entry(&entry_id) + let Some((source_zone, source_index)) = + self.shell.chrome.activity_bar_layout.find_entry(&entry_id) else { self.terminal.view.status = "activity item not found".to_string(); cx.notify(); @@ -199,14 +219,19 @@ impl NyaTermApp { // Same entry dropped on itself — no-op. if source_zone == target_zone { - let len = self.activity_bar_layout.zone(target_zone).len(); + let len = self + .shell + .chrome + .activity_bar_layout + .zone(target_zone) + .len(); let mut insert_at = target_index.unwrap_or(len); if source_index < insert_at { insert_at = insert_at.saturating_sub(1); } insert_at = insert_at.min(len.saturating_sub(1)); if insert_at == source_index { - self.activity_bar_context_menu = None; + self.shell.chrome.activity_bar_context_menu = None; cx.notify(); return; } @@ -214,16 +239,31 @@ impl NyaTermApp { // Remove from source. let removed = self + .shell + .chrome .activity_bar_layout .zone_mut(source_zone) .remove(source_index); - let mut insert_at = - target_index.unwrap_or_else(|| self.activity_bar_layout.zone(target_zone).len()); + let mut insert_at = target_index.unwrap_or_else(|| { + self.shell + .chrome + .activity_bar_layout + .zone(target_zone) + .len() + }); if source_zone == target_zone && source_index < insert_at { insert_at = insert_at.saturating_sub(1); } - insert_at = insert_at.min(self.activity_bar_layout.zone(target_zone).len()); - self.activity_bar_layout + insert_at = insert_at.min( + self.shell + .chrome + .activity_bar_layout + .zone(target_zone) + .len(), + ); + self.shell + .chrome + .activity_bar_layout .zone_mut(target_zone) .insert(insert_at, removed); @@ -234,7 +274,7 @@ impl NyaTermApp { self.clear_activity_entry_from_side(&entry_id, source_side); } - self.activity_bar_context_menu = None; + self.shell.chrome.activity_bar_context_menu = None; self.terminal.view.status = format!( "moved {} to {}", entry_id, @@ -254,27 +294,34 @@ impl NyaTermApp { fn clear_activity_entry_from_side(&mut self, entry_id: &str, side: PanelSide) { match side { PanelSide::Left => { - self.left_open_panels.retain(|id| id != entry_id); + self.shell.panels.left_open.retain(|id| id != entry_id); if self - .active_left_panel + .shell + .panels + .active_left .is_some_and(|item| item.persistence_id() == entry_id) { - self.active_left_panel = None; + self.shell.panels.active_left = None; } - if self.left_open_panels.is_empty() && self.active_left_panel.is_none() { - self.left_sidebar_collapsed = true; + if self.shell.panels.left_open.is_empty() && self.shell.panels.active_left.is_none() + { + self.shell.panels.left_collapsed = true; } } PanelSide::Right => { - self.right_open_panels.retain(|id| id != entry_id); + self.shell.panels.right_open.retain(|id| id != entry_id); if self - .active_right_panel + .shell + .panels + .active_right .is_some_and(|item| item.persistence_id() == entry_id) { - self.active_right_panel = None; + self.shell.panels.active_right = None; } - if self.right_open_panels.is_empty() && self.active_right_panel.is_none() { - self.right_inspector_collapsed = true; + if self.shell.panels.right_open.is_empty() + && self.shell.panels.active_right.is_none() + { + self.shell.panels.right_collapsed = true; } } } @@ -293,11 +340,11 @@ impl NyaTermApp { self.open_panel(item, cx); if !cfg!(target_os = "macos") { match side { - Some(PanelSide::Left) if self.last_viewport_size.0 < 1024. => { - self.mobile_left_open = true; + Some(PanelSide::Left) if self.shell.viewport.size.0 < 1024. => { + self.shell.panels.mobile_left_open = true; } - Some(PanelSide::Right) if self.last_viewport_size.0 < 768. => { - self.mobile_right_open = true; + Some(PanelSide::Right) if self.shell.viewport.size.0 < 768. => { + self.shell.panels.mobile_right_open = true; } _ => {} } @@ -327,11 +374,11 @@ impl NyaTermApp { self.open_panel(NavItem::Recording, cx); if !cfg!(target_os = "macos") { match side { - Some(PanelSide::Left) if self.last_viewport_size.0 < 1024. => { - self.mobile_left_open = true; + Some(PanelSide::Left) if self.shell.viewport.size.0 < 1024. => { + self.shell.panels.mobile_left_open = true; } - Some(PanelSide::Right) if self.last_viewport_size.0 < 768. => { - self.mobile_right_open = true; + Some(PanelSide::Right) if self.shell.viewport.size.0 < 768. => { + self.shell.panels.mobile_right_open = true; } _ => {} } @@ -345,7 +392,8 @@ impl NyaTermApp { pub(in crate::features) fn activity_entry_selected(&self, entry: ActivityBarEntry) -> bool { match entry { ActivityBarEntry::Panel(NavItem::Settings) => { - self.settings_window.is_some() || self.main_mode == MainMode::Page + self.shell.navigation.settings.window.is_some() + || self.shell.navigation.main_mode == MainMode::Page } ActivityBarEntry::Panel(item) => self.panel_entry_selected(item), ActivityBarEntry::QuickCommands => { @@ -365,13 +413,13 @@ impl NyaTermApp { let Some(side) = self.panel_side_for_item(item) else { return false; }; - if self.panel_multi_open { + if self.shell.panels.multi_open { let id = item.persistence_id(); self.side_open_panel_ids(side).iter().any(|open| open == id) || self.side_overlay_panel(side) == Some(item) || match side { - PanelSide::Left => self.active_left_panel == Some(item), - PanelSide::Right => self.active_right_panel == Some(item), + PanelSide::Left => self.shell.panels.active_left == Some(item), + PanelSide::Right => self.shell.panels.active_right == Some(item), } } else { match side { diff --git a/crates/nyaterm-desktop/src/features/shell/event_pump/helpers.rs b/crates/nyaterm-desktop/src/features/shell/event_pump/helpers.rs index bfbcf7d2d..af82e487a 100644 --- a/crates/nyaterm-desktop/src/features/shell/event_pump/helpers.rs +++ b/crates/nyaterm-desktop/src/features/shell/event_pump/helpers.rs @@ -174,10 +174,6 @@ pub(super) fn window_geometry_churn_active( }) } -pub(super) fn title_drag_active(title_drag_active_until: Option, now: Instant) -> bool { - title_drag_active_until.is_some_and(|until| now < until) -} - pub(super) fn connect_settle_active(until: Option, now: Instant) -> bool { until.is_some_and(|until| now < until) } @@ -439,7 +435,7 @@ mod tests { terminal_input_idle_remaining_delay, terminal_log_plain_text, terminal_output_dropped_marker, terminal_performance_tick_session_ids, terminal_render_work_pressure_active, terminal_user_scroll_frame_apply_pending, - title_drag_active, viewport_change_terminal_session_ids, window_geometry_churn_active, + viewport_change_terminal_session_ids, window_geometry_churn_active, }; #[test] @@ -545,14 +541,6 @@ mod tests { )); } - #[test] - fn title_drag_active_until_deadline() { - let now = Instant::now(); - assert!(!title_drag_active(None, now)); - assert!(title_drag_active(Some(now + Duration::from_millis(1)), now)); - assert!(!title_drag_active(Some(now), now)); - } - #[test] fn connect_settle_active_until_deadline() { let now = Instant::now(); diff --git a/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs b/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs index 7a0119d17..319259dc3 100644 --- a/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs +++ b/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs @@ -10,7 +10,7 @@ use crate::features::shell::event_pump::helpers::{ connect_settle_active, connect_settle_deadline, diagnostic_log_due, pending_session_status_message, remote_refresh_due, runtime_output_pressure_active_from_counts, runtime_tick_interval_for_pressure, runtime_ui_notify_allowed, - terminal_cell_metrics_refresh_needed, terminal_input_idle_remaining_delay, title_drag_active, + terminal_cell_metrics_refresh_needed, terminal_input_idle_remaining_delay, viewport_change_terminal_session_ids, window_geometry_churn_active, }; use crate::features::{ @@ -59,13 +59,14 @@ impl NyaTermApp { window: &mut Window, cx: &mut Context, ) -> bool { - let before_viewport = self.last_viewport_size; let before_metrics = self.terminal.layout.cell_metrics; let vs = window.viewport_size(); - self.last_viewport_size = (f32::from(vs.width), f32::from(vs.height)); - if self.last_viewport_size != before_viewport { + let viewport_changed = self + .shell + .viewport + .update_size((f32::from(vs.width), f32::from(vs.height)), Instant::now()); + if viewport_changed { // Geometry churn (resize / some window managers during move). - self.last_viewport_change_at = Some(Instant::now()); self.notify_terminal_surfaces_for_viewport_change(cx); } if terminal_cell_metrics_refresh_needed(self.terminal.layout.cell_metrics) { @@ -76,8 +77,7 @@ impl NyaTermApp { self.resize_all_known_terminal_surfaces(); self.refresh_visible_terminal_surfaces(cx); } - self.last_viewport_size != before_viewport - || self.terminal.layout.cell_metrics != before_metrics + viewport_changed || self.terminal.layout.cell_metrics != before_metrics } fn notify_terminal_surfaces_for_viewport_change(&mut self, cx: &mut Context) { @@ -219,11 +219,13 @@ impl NyaTermApp { } pub(in crate::features) fn mark_title_drag_activity(&mut self) { - self.title_drag_active_until = Some(Instant::now() + TITLE_DRAG_ACTIVE_HOLD); + self.shell + .viewport + .mark_title_drag(Instant::now(), TITLE_DRAG_ACTIVE_HOLD); } pub(in crate::features) fn title_drag_active(&self, now: Instant) -> bool { - title_drag_active(self.title_drag_active_until, now) + self.shell.viewport.title_drag_active(now) } pub(in crate::features) fn should_log_slow_diagnostic( @@ -305,7 +307,7 @@ impl NyaTermApp { // idle cadence so full plane ticks do not stack on compositor paints. let now = Instant::now(); if self.title_drag_active(now) - || window_geometry_churn_active(self.last_viewport_change_at, now) + || window_geometry_churn_active(self.shell.viewport.last_change_at, now) { return RUNTIME_IDLE_TICK_INTERVAL; } @@ -323,7 +325,7 @@ impl NyaTermApp { if !self.terminal.view.runtime.event_pump_started { return false; } - if self.last_viewport_size != viewport_size + if self.shell.viewport.size != viewport_size || terminal_cell_metrics_refresh_needed(self.terminal.layout.cell_metrics) { return true; diff --git a/crates/nyaterm-desktop/src/features/shell/event_pump/planes.rs b/crates/nyaterm-desktop/src/features/shell/event_pump/planes.rs index aab678d50..58622d74d 100644 --- a/crates/nyaterm-desktop/src/features/shell/event_pump/planes.rs +++ b/crates/nyaterm-desktop/src/features/shell/event_pump/planes.rs @@ -161,7 +161,7 @@ impl NyaTermApp { } return true; } - let geometry_churn = window_geometry_churn_active(self.last_viewport_change_at, now); + let geometry_churn = window_geometry_churn_active(self.shell.viewport.last_change_at, now); let calm_tick = self.runtime_quiet_tick_allowed(); if geometry_churn && calm_tick { dirty |= self.drive_pending_focus(window, cx); @@ -603,7 +603,7 @@ impl NyaTermApp { // Idle-plane work does not drain output; one pressure sample is enough for the stage. let output_pressure = self.runtime_output_pressure_active(); let now = Instant::now(); - let geometry_churn = window_geometry_churn_active(self.last_viewport_change_at, now); + let geometry_churn = window_geometry_churn_active(self.shell.viewport.last_change_at, now); let connect_settle = connect_settle_active(self.terminal.view.runtime.connect_settle_until, now); // Geometry churn / connect settle: keep focus only (no remote/layout/DB). diff --git a/crates/nyaterm-desktop/src/features/shell/global_shortcut_runtime.rs b/crates/nyaterm-desktop/src/features/shell/global_shortcut_runtime.rs index 52cb0b6e1..216565515 100644 --- a/crates/nyaterm-desktop/src/features/shell/global_shortcut_runtime.rs +++ b/crates/nyaterm-desktop/src/features/shell/global_shortcut_runtime.rs @@ -32,7 +32,7 @@ impl NyaTermApp { // Dismiss strip menus with Escape (Tauri dropdown dismiss). if event.keystroke.key.as_str() == "escape" - && (self.open_tabs_menu_open || self.new_session_menu_open) + && (self.shell.chrome.open_tabs_menu_open || self.shell.chrome.new_session_menu_open) { self.close_open_tabs_menu(cx); self.close_new_session_menu(cx); diff --git a/crates/nyaterm-desktop/src/features/shell/mod.rs b/crates/nyaterm-desktop/src/features/shell/mod.rs index 5e84cb0b7..206671e6a 100644 --- a/crates/nyaterm-desktop/src/features/shell/mod.rs +++ b/crates/nyaterm-desktop/src/features/shell/mod.rs @@ -20,7 +20,7 @@ pub(in crate::features) use activity_bar_runtime::{ pub(in crate::features) use appearance::{ appearance_font_options, appearance_font_stack, gpui_code_font_family, }; -pub(in crate::features) use state::ShellFeatureState; +pub(in crate::features) use state::{ShellFeatureInit, ShellFeatureState}; pub(in crate::features) use tab_mouse::{ ChromeTooltip, SessionTabDragPayload, SessionTabDragPreview, SessionTabTooltip, TAB_MOUSE_ACTIONS, TabMouseActionTarget, diff --git a/crates/nyaterm-desktop/src/features/shell/navigation_runtime.rs b/crates/nyaterm-desktop/src/features/shell/navigation_runtime.rs index d86140777..cf1b19d23 100644 --- a/crates/nyaterm-desktop/src/features/shell/navigation_runtime.rs +++ b/crates/nyaterm-desktop/src/features/shell/navigation_runtime.rs @@ -7,23 +7,35 @@ impl NyaTermApp { pub(in crate::features) fn open_page(&mut self, item: NavItem, cx: &mut Context) { if item == NavItem::Settings || item.opens_settings() { self.begin_settings_draft(); - if let Some(group) = self.settings_active_tab.expandable_group_id() { - self.settings_expanded_groups.insert(group.to_string()); + if let Some(group) = self + .shell + .navigation + .settings + .active_tab + .expandable_group_id() + { + self.shell + .navigation + .settings + .expanded_groups + .insert(group.to_string()); } - if self.main_mode != MainMode::Page { - self.settings_previous_left_collapsed = Some(self.left_sidebar_collapsed); - self.settings_previous_right_collapsed = Some(self.right_inspector_collapsed); + if self.shell.navigation.main_mode != MainMode::Page { + self.shell.navigation.settings.previous_left_collapsed = + Some(self.shell.panels.left_collapsed); + self.shell.navigation.settings.previous_right_collapsed = + Some(self.shell.panels.right_collapsed); } if self.open_settings_window(cx) { - self.main_mode = MainMode::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.terminal.view.status = "settings opened".to_string(); cx.notify(); return; } - self.main_mode = MainMode::Page; - self.selected_nav = NavItem::Settings; - self.left_sidebar_collapsed = true; - self.right_inspector_collapsed = true; + self.shell.navigation.main_mode = MainMode::Page; + self.shell.navigation.selected_nav = NavItem::Settings; + self.shell.panels.left_collapsed = true; + self.shell.panels.right_collapsed = true; self.terminal.view.status = "settings opened".to_string(); cx.notify(); return; @@ -43,14 +55,14 @@ impl NyaTermApp { return; } - if self.panel_multi_open && self.panel_side_for_item(item).is_some() { + if self.shell.panels.multi_open && self.panel_side_for_item(item).is_some() { self.open_or_toggle_panel(item, cx); return; } - self.main_mode = MainMode::Workspace; - self.selected_nav = item; - self.right_focus = if item == NavItem::Recording { + self.shell.navigation.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = item; + self.shell.panels.right_focus = if item == NavItem::Recording { RightFocus::Recording } else { RightFocus::Default @@ -58,35 +70,35 @@ impl NyaTermApp { match self.panel_side_for_item(item) { Some(PanelSide::Left) => { - let already_open = - self.active_left_panel == Some(item) && !self.left_sidebar_collapsed; + let already_open = self.shell.panels.active_left == Some(item) + && !self.shell.panels.left_collapsed; if already_open { - self.left_sidebar_collapsed = true; - self.active_left_panel = None; + self.shell.panels.left_collapsed = true; + self.shell.panels.active_left = None; self.terminal.view.status = format!("{} closed", item.label()); } else { - self.active_left_panel = Some(item); - self.left_sidebar_collapsed = false; + self.shell.panels.active_left = Some(item); + self.shell.panels.left_collapsed = false; self.terminal.view.status = format!("{} opened", item.label()); } } Some(PanelSide::Right) => { - let already_open = - self.active_right_panel == Some(item) && !self.right_inspector_collapsed; + let already_open = self.shell.panels.active_right == Some(item) + && !self.shell.panels.right_collapsed; if already_open { - self.right_inspector_collapsed = true; - self.active_right_panel = None; - self.right_focus = RightFocus::Default; + self.shell.panels.right_collapsed = true; + self.shell.panels.active_right = None; + self.shell.panels.right_focus = RightFocus::Default; self.terminal.view.status = format!("{} closed", item.label()); } else { - self.active_right_panel = Some(item); - self.right_inspector_collapsed = false; + self.shell.panels.active_right = Some(item); + self.shell.panels.right_collapsed = false; self.terminal.view.status = format!("{} opened", item.label()); } } None => { - self.left_sidebar_collapsed = false; - self.right_inspector_collapsed = false; + self.shell.panels.left_collapsed = false; + self.shell.panels.right_collapsed = false; } } @@ -95,21 +107,21 @@ impl NyaTermApp { } pub(in crate::features) fn ensure_panel_open(&mut self, item: NavItem) { - if self.panel_multi_open && self.panel_side_for_item(item).is_some() { + if self.shell.panels.multi_open && self.panel_side_for_item(item).is_some() { self.ensure_panel_in_stack(item); return; } - self.main_mode = MainMode::Workspace; - self.selected_nav = item; + self.shell.navigation.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = item; match self.panel_side_for_item(item) { Some(PanelSide::Left) => { - self.active_left_panel = Some(item); - self.left_sidebar_collapsed = false; + self.shell.panels.active_left = Some(item); + self.shell.panels.left_collapsed = false; } Some(PanelSide::Right) => { - self.active_right_panel = Some(item); - self.right_inspector_collapsed = false; - self.right_focus = if item == NavItem::Recording { + self.shell.panels.active_right = Some(item); + self.shell.panels.right_collapsed = false; + self.shell.panels.right_focus = if item == NavItem::Recording { RightFocus::Recording } else { RightFocus::Default @@ -124,34 +136,40 @@ impl NyaTermApp { } pub(in crate::features) fn toggle_left_sidebar(&mut self, cx: &mut Context) { - if self.panel_multi_open { + if self.shell.panels.multi_open { if self.left_side_open() { - self.left_open_panels.clear(); - self.active_left_panel = None; - self.left_sidebar_collapsed = true; + self.shell.panels.left_open.clear(); + self.shell.panels.active_left = None; + self.shell.panels.left_collapsed = true; self.terminal.view.status = "left sidebar collapsed".to_string(); } else if let Some(panel) = self + .shell + .chrome .activity_bar_layout .first_panel_on_side(PanelSide::Left) { - self.active_left_panel = Some(panel); - self.left_open_panels.clear(); + self.shell.panels.active_left = Some(panel); + self.shell.panels.left_open.clear(); if Self::is_stackable_panel_id(panel.persistence_id()) { - self.left_open_panels + self.shell + .panels + .left_open .push(panel.persistence_id().to_string()); } - self.left_sidebar_collapsed = false; + self.shell.panels.left_collapsed = false; self.terminal.view.status = "left sidebar expanded".to_string(); } - } else if self.left_sidebar_collapsed || self.active_left_panel.is_none() { - self.active_left_panel = self + } else if self.shell.panels.left_collapsed || self.shell.panels.active_left.is_none() { + self.shell.panels.active_left = self + .shell + .chrome .activity_bar_layout .first_panel_on_side(PanelSide::Left); - self.left_sidebar_collapsed = false; + self.shell.panels.left_collapsed = false; self.terminal.view.status = "left sidebar expanded".to_string(); } else { - self.active_left_panel = None; - self.left_sidebar_collapsed = true; + self.shell.panels.active_left = None; + self.shell.panels.left_collapsed = true; self.terminal.view.status = "left sidebar collapsed".to_string(); } self.persist_ui_layout(); @@ -159,34 +177,40 @@ impl NyaTermApp { } pub(in crate::features) fn toggle_right_inspector(&mut self, cx: &mut Context) { - if self.panel_multi_open { + if self.shell.panels.multi_open { if self.right_side_open() { - self.right_open_panels.clear(); - self.active_right_panel = None; - self.right_inspector_collapsed = true; + self.shell.panels.right_open.clear(); + self.shell.panels.active_right = None; + self.shell.panels.right_collapsed = true; self.terminal.view.status = "right sidebar collapsed".to_string(); } else if let Some(panel) = self + .shell + .chrome .activity_bar_layout .first_panel_on_side(PanelSide::Right) { - self.active_right_panel = Some(panel); - self.right_open_panels.clear(); + self.shell.panels.active_right = Some(panel); + self.shell.panels.right_open.clear(); if Self::is_stackable_panel_id(panel.persistence_id()) { - self.right_open_panels + self.shell + .panels + .right_open .push(panel.persistence_id().to_string()); } - self.right_inspector_collapsed = false; + self.shell.panels.right_collapsed = false; self.terminal.view.status = "right sidebar expanded".to_string(); } - } else if self.right_inspector_collapsed || self.active_right_panel.is_none() { - self.active_right_panel = self + } else if self.shell.panels.right_collapsed || self.shell.panels.active_right.is_none() { + self.shell.panels.active_right = self + .shell + .chrome .activity_bar_layout .first_panel_on_side(PanelSide::Right); - self.right_inspector_collapsed = false; + self.shell.panels.right_collapsed = false; self.terminal.view.status = "right sidebar expanded".to_string(); } else { - self.active_right_panel = None; - self.right_inspector_collapsed = true; + self.shell.panels.active_right = None; + self.shell.panels.right_collapsed = true; self.terminal.view.status = "right sidebar collapsed".to_string(); } self.persist_ui_layout(); @@ -194,46 +218,50 @@ impl NyaTermApp { } pub(in crate::features) fn toggle_mobile_left_drawer(&mut self, cx: &mut Context) { - if self.mobile_left_open { - self.mobile_left_open = false; + if self.shell.panels.mobile_left_open { + self.shell.panels.mobile_left_open = false; } else { - if self.active_left_panel.is_none() { - self.active_left_panel = self + if self.shell.panels.active_left.is_none() { + self.shell.panels.active_left = self + .shell + .chrome .activity_bar_layout .first_panel_on_side(PanelSide::Left); } - self.left_sidebar_collapsed = false; - self.mobile_left_open = true; + self.shell.panels.left_collapsed = false; + self.shell.panels.mobile_left_open = true; } cx.notify(); } pub(in crate::features) fn toggle_mobile_right_drawer(&mut self, cx: &mut Context) { - if self.mobile_right_open { - self.mobile_right_open = false; + if self.shell.panels.mobile_right_open { + self.shell.panels.mobile_right_open = false; } else { - if self.active_right_panel.is_none() { - self.active_right_panel = self + if self.shell.panels.active_right.is_none() { + self.shell.panels.active_right = self + .shell + .chrome .activity_bar_layout .first_panel_on_side(PanelSide::Right); } - self.right_inspector_collapsed = false; - self.mobile_right_open = true; + self.shell.panels.right_collapsed = false; + self.shell.panels.mobile_right_open = true; } cx.notify(); } pub(in crate::features) fn current_left_panel(&self) -> Option { - if self.left_sidebar_collapsed { + if self.shell.panels.left_collapsed { return None; } - if self.panel_multi_open { + if self.shell.panels.multi_open { if self.side_overlay_panel(PanelSide::Left).is_some() || !self.side_open_panel_ids(PanelSide::Left).is_empty() { return self .side_overlay_panel(PanelSide::Left) - .or(self.active_left_panel) + .or(self.shell.panels.active_left) .or_else(|| { self.side_open_panel_ids(PanelSide::Left) .first() @@ -242,52 +270,55 @@ impl NyaTermApp { } return None; } - self.active_left_panel + self.shell.panels.active_left } pub(in crate::features) fn current_right_panel(&self) -> Option { - if self.right_inspector_collapsed { + if self.shell.panels.right_collapsed { return None; } - if self.panel_multi_open { + if self.shell.panels.multi_open { if self.side_overlay_panel(PanelSide::Right).is_some() || !self.side_open_panel_ids(PanelSide::Right).is_empty() { return self .side_overlay_panel(PanelSide::Right) - .or(self.active_right_panel) + .or(self.shell.panels.active_right) .or_else(|| { self.side_open_panel_ids(PanelSide::Right) .first() .and_then(|id| NavItem::from_persistence_id(id)) }) - .or(if self.right_focus == RightFocus::Recording { + .or(if self.shell.panels.right_focus == RightFocus::Recording { Some(NavItem::Recording) } else { None }); } - return if self.right_focus == RightFocus::Recording { + return if self.shell.panels.right_focus == RightFocus::Recording { Some(NavItem::Recording) } else { None }; } - self.active_right_panel - .or(if self.right_focus == RightFocus::Recording { + self.shell.panels.active_right.or( + if self.shell.panels.right_focus == RightFocus::Recording { Some(NavItem::Recording) } else { None - }) + }, + ) } pub(in crate::features) fn left_side_open(&self) -> bool { self.current_left_panel().is_some() - || (self.panel_multi_open && !self.side_open_panel_ids(PanelSide::Left).is_empty()) + || (self.shell.panels.multi_open + && !self.side_open_panel_ids(PanelSide::Left).is_empty()) } pub(in crate::features) fn right_side_open(&self) -> bool { self.current_right_panel().is_some() - || (self.panel_multi_open && !self.side_open_panel_ids(PanelSide::Right).is_empty()) + || (self.shell.panels.multi_open + && !self.side_open_panel_ids(PanelSide::Right).is_empty()) } } diff --git a/crates/nyaterm-desktop/src/features/shell/panel_resize_runtime.rs b/crates/nyaterm-desktop/src/features/shell/panel_resize_runtime.rs index 9aa388d2e..aca588822 100644 --- a/crates/nyaterm-desktop/src/features/shell/panel_resize_runtime.rs +++ b/crates/nyaterm-desktop/src/features/shell/panel_resize_runtime.rs @@ -6,14 +6,10 @@ use nyaterm_core::ConnectionStore; use crate::features::NyaTermApp; use crate::models::{ - BottomPanelMode, NavItem, PanelResizeSide, PanelResizeState, PanelSide, - TransferHeightResizeState, panel_collapsed_from_persistence, + BottomPanelMode, NavItem, PanelResizeSide, PanelSide, TransferHeightResizeState, + panel_collapsed_from_persistence, }; -const LEFT_PANEL_MIN: f32 = 160.; -const LEFT_PANEL_MAX: f32 = 720.; -const RIGHT_PANEL_MIN: f32 = 200.; -const RIGHT_PANEL_MAX: f32 = 720.; const QUICK_CMD_HEIGHT_MIN: f32 = 36.; const SERIAL_SEND_HEIGHT_MIN: f32 = 60.; const BOTTOM_PANEL_HEIGHT_MAX: f32 = 520.; @@ -32,15 +28,7 @@ impl NyaTermApp { event: &MouseDownEvent, cx: &mut Context, ) { - let start_width = match side { - PanelResizeSide::Left => px(self.left_panel_width), - PanelResizeSide::Right => px(self.right_panel_width), - }; - self.panel_resize = Some(PanelResizeState { - side, - start_x: event.position.x, - start_width, - }); + self.shell.panels.start_resize(side, event.position.x); self.terminal.view.status = match side { PanelResizeSide::Left => "resizing left panel".to_string(), PanelResizeSide::Right => "resizing right panel".to_string(), @@ -53,22 +41,16 @@ impl NyaTermApp { event: &MouseMoveEvent, cx: &mut Context, ) { - let Some(state) = self.panel_resize else { + let Some((side, width)) = self.shell.panels.update_resize(event.position.x) else { return; }; - let delta = f32::from(event.position.x - state.start_x); - let start = f32::from(state.start_width); - match state.side { + match side { PanelResizeSide::Left => { - self.left_panel_width = (start + delta).clamp(LEFT_PANEL_MIN, LEFT_PANEL_MAX); - self.terminal.view.status = - format!("left panel: {:.0}px", self.left_panel_width.round()); + self.terminal.view.status = format!("left panel: {:.0}px", width.round()); } PanelResizeSide::Right => { // Right handle sits on the left edge of the right panel: drag left grows width. - self.right_panel_width = (start - delta).clamp(RIGHT_PANEL_MIN, RIGHT_PANEL_MAX); - self.terminal.view.status = - format!("right panel: {:.0}px", self.right_panel_width.round()); + self.terminal.view.status = format!("right panel: {:.0}px", width.round()); } } cx.notify(); @@ -79,12 +61,12 @@ impl NyaTermApp { _event: &MouseUpEvent, cx: &mut Context, ) { - if self.panel_resize.take().is_some() { + if self.shell.panels.finish_resize() { self.persist_panel_widths(); self.terminal.view.status = format!( "panel sizes L{:.0}/R{:.0}", - self.left_panel_width.round(), - self.right_panel_width.round() + self.shell.panels.left_width.round(), + self.shell.panels.right_width.round() ); cx.notify(); } @@ -95,34 +77,34 @@ impl NyaTermApp { } pub(in crate::features) fn apply_ui_layout_from_settings(&mut self) { - self.left_panel_width = self.settings.ui_left_panel_width as f32; - self.right_panel_width = self.settings.ui_right_panel_width as f32; + self.shell.panels.left_width = self.settings.ui_left_panel_width as f32; + self.shell.panels.right_width = self.settings.ui_right_panel_width as f32; self.transfer.panel.height = self.settings.ui_transfer_height as f32; self.shell.bottom_panel.quick_commands_height = self.settings.ui_quick_cmd_height as f32; self.shell.bottom_panel.command_send_height = self.settings.ui_serial_send_height as f32; self.apply_activity_layout_from_settings(); - self.active_left_panel = self + self.shell.panels.active_left = self .settings .ui_active_left_panel .as_deref() .and_then(NavItem::from_persistence_id) .filter(|item| self.panel_side_for_item(*item) == Some(PanelSide::Left)); - self.active_right_panel = self + self.shell.panels.active_right = self .settings .ui_active_right_panel .as_deref() .and_then(NavItem::from_persistence_id) .filter(|item| self.panel_side_for_item(*item) == Some(PanelSide::Right)); - self.left_sidebar_collapsed = panel_collapsed_from_persistence( + self.shell.panels.left_collapsed = panel_collapsed_from_persistence( self.settings.ui_left_panel_collapsed, self.settings.ui_panel_multi_open, - self.active_left_panel.is_some(), + self.shell.panels.active_left.is_some(), !self.settings.ui_left_open_panels.is_empty(), ); - self.right_inspector_collapsed = panel_collapsed_from_persistence( + self.shell.panels.right_collapsed = panel_collapsed_from_persistence( self.settings.ui_right_panel_collapsed, self.settings.ui_panel_multi_open, - self.active_right_panel.is_some(), + self.shell.panels.active_right.is_some(), !self.settings.ui_right_open_panels.is_empty(), ); self.apply_panel_stack_from_settings(); @@ -132,9 +114,10 @@ impl NyaTermApp { } pub(in crate::features) fn persist_ui_layout(&mut self) { - self.settings.ui_left_panel_width = self.left_panel_width.round().clamp(160., 720.) as u32; + self.settings.ui_left_panel_width = + self.shell.panels.left_width.round().clamp(160., 720.) as u32; self.settings.ui_right_panel_width = - self.right_panel_width.round().clamp(200., 720.) as u32; + self.shell.panels.right_width.round().clamp(200., 720.) as u32; self.settings.ui_transfer_height = self.transfer.panel.height.round().clamp(60., 600.) as u32; self.settings.ui_quick_cmd_height = @@ -150,13 +133,17 @@ impl NyaTermApp { .round() .clamp(SERIAL_SEND_HEIGHT_MIN, BOTTOM_PANEL_HEIGHT_MAX) as u32; self.settings.ui_active_left_panel = self - .active_left_panel + .shell + .panels + .active_left .map(|item| item.persistence_id().to_string()); self.settings.ui_active_right_panel = self - .active_right_panel + .shell + .panels + .active_right .map(|item| item.persistence_id().to_string()); - self.settings.ui_left_panel_collapsed = self.left_sidebar_collapsed; - self.settings.ui_right_panel_collapsed = self.right_inspector_collapsed; + self.settings.ui_left_panel_collapsed = self.shell.panels.left_collapsed; + self.settings.ui_right_panel_collapsed = self.shell.panels.right_collapsed; self.settings.ui_saved_connections_sort_mode = self .connection_state .list_sort_mode() diff --git a/crates/nyaterm-desktop/src/features/shell/panel_stack_runtime.rs b/crates/nyaterm-desktop/src/features/shell/panel_stack_runtime.rs index d8322a2c6..be4f1058e 100644 --- a/crates/nyaterm-desktop/src/features/shell/panel_stack_runtime.rs +++ b/crates/nyaterm-desktop/src/features/shell/panel_stack_runtime.rs @@ -10,8 +10,8 @@ use nyaterm_core::{AgentCommandExecutionMode, truncate_preview}; use crate::features::{ChromeTooltip, NyaTermApp, TextInputSetup, panel_header_with_actions}; use crate::models::{ - ActivityBarZone, MainMode, NavItem, NetworkTab, PanelSide, PanelStackResizeState, RightFocus, - SecurityAuthTab, SettingsTab, + ActivityBarZone, MainMode, NavItem, NetworkTab, PanelSide, RightFocus, SecurityAuthTab, + SettingsTab, }; use crate::theme::ThemePalette; @@ -28,41 +28,45 @@ impl NyaTermApp { } pub(in crate::features) fn toggle_panel_multi_open(&mut self, cx: &mut Context) { - self.panel_multi_open = !self.panel_multi_open; - if self.panel_multi_open { - if self.left_open_panels.is_empty() { - if let Some(panel) = self.active_left_panel { + self.shell.panels.multi_open = !self.shell.panels.multi_open; + if self.shell.panels.multi_open { + if self.shell.panels.left_open.is_empty() { + if let Some(panel) = self.shell.panels.active_left { let id = panel.persistence_id().to_string(); if Self::is_stackable_panel_id(&id) { - self.left_open_panels.push(id); + self.shell.panels.left_open.push(id); } } } - if self.right_open_panels.is_empty() { - if let Some(panel) = self.active_right_panel { + if self.shell.panels.right_open.is_empty() { + if let Some(panel) = self.shell.panels.active_right { let id = panel.persistence_id().to_string(); if Self::is_stackable_panel_id(&id) { - self.right_open_panels.push(id); + self.shell.panels.right_open.push(id); } } } self.terminal.view.status = "multi-open panels enabled".to_string(); } else { // Collapse to active-only mode. - if self.active_left_panel.is_none() { - self.active_left_panel = self - .left_open_panels + if self.shell.panels.active_left.is_none() { + self.shell.panels.active_left = self + .shell + .panels + .left_open .first() .and_then(|id| NavItem::from_persistence_id(id)); } - if self.active_right_panel.is_none() { - self.active_right_panel = self - .right_open_panels + if self.shell.panels.active_right.is_none() { + self.shell.panels.active_right = self + .shell + .panels + .right_open .first() .and_then(|id| NavItem::from_persistence_id(id)); } - self.left_open_panels.clear(); - self.right_open_panels.clear(); + self.shell.panels.left_open.clear(); + self.shell.panels.right_open.clear(); self.terminal.view.status = "single panel mode".to_string(); } self.persist_ui_layout(); @@ -70,10 +74,10 @@ impl NyaTermApp { } pub(in crate::features) fn side_open_panel_ids(&self, side: PanelSide) -> Vec { - if !self.panel_multi_open { + if !self.shell.panels.multi_open { let active = match side { - PanelSide::Left => self.active_left_panel, - PanelSide::Right => self.active_right_panel, + PanelSide::Left => self.shell.panels.active_left, + PanelSide::Right => self.shell.panels.active_right, }; return active .map(|item| item.persistence_id().to_string()) @@ -82,8 +86,8 @@ impl NyaTermApp { } let open = match side { - PanelSide::Left => &self.left_open_panels, - PanelSide::Right => &self.right_open_panels, + PanelSide::Left => &self.shell.panels.left_open, + PanelSide::Right => &self.shell.panels.right_open, }; if open.is_empty() { return Vec::new(); @@ -95,7 +99,7 @@ impl NyaTermApp { }; let mut ordered = Vec::new(); for zone in zones { - for id in self.activity_bar_layout.zone(zone) { + for id in self.shell.chrome.activity_bar_layout.zone(zone) { if open_set.contains(id) && Self::is_stackable_panel_id(id) { ordered.push(id.clone()); } @@ -105,27 +109,25 @@ impl NyaTermApp { } pub(in crate::features) fn side_overlay_panel(&self, side: PanelSide) -> Option { - if !self.panel_multi_open { + if !self.shell.panels.multi_open { return None; } let active = match side { - PanelSide::Left => self.active_left_panel, - PanelSide::Right => self.active_right_panel, + PanelSide::Left => self.shell.panels.active_left, + PanelSide::Right => self.shell.panels.active_right, }?; let id = active.persistence_id(); Self::is_exclusive_panel_id(id).then_some(active) } pub(in crate::features) fn panel_stack_weight(&self, panel_id: &str) -> f32 { - self.panel_stack_sizes - .get(panel_id) - .copied() - .filter(|value| value.is_finite() && *value > 0.) - .unwrap_or(1.) + self.shell.panels.stack_weight(panel_id) } pub(in crate::features) fn panel_side_for_item(&self, item: NavItem) -> Option { - self.activity_bar_layout + self.shell + .chrome + .activity_bar_layout .side_for_entry(item.persistence_id()) .or_else(|| item.is_left_panel().then_some(PanelSide::Left)) .or_else(|| item.is_right_panel().then_some(PanelSide::Right)) @@ -140,7 +142,7 @@ impl NyaTermApp { self.open_page(NavItem::Settings, cx); return; } - if !self.panel_multi_open { + if !self.shell.panels.multi_open { self.open_panel(item, cx); return; } @@ -151,18 +153,18 @@ impl NyaTermApp { return; }; - self.main_mode = MainMode::Workspace; - self.selected_nav = item; + self.shell.navigation.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = item; if item == NavItem::Recording { - self.right_focus = RightFocus::Recording; + self.shell.panels.right_focus = RightFocus::Recording; } else { - self.right_focus = RightFocus::Default; + self.shell.panels.right_focus = RightFocus::Default; } if Self::is_exclusive_panel_id(&id) { let active = match side { - PanelSide::Left => self.active_left_panel, - PanelSide::Right => self.active_right_panel, + PanelSide::Left => self.shell.panels.active_left, + PanelSide::Right => self.shell.panels.active_right, }; if active == Some(item) { // Dismiss exclusive overlay to stack. @@ -172,24 +174,24 @@ impl NyaTermApp { .find_map(|open_id| NavItem::from_persistence_id(&open_id)); match side { PanelSide::Left => { - self.active_left_panel = fallback; - self.left_sidebar_collapsed = fallback.is_none(); + self.shell.panels.active_left = fallback; + self.shell.panels.left_collapsed = fallback.is_none(); } PanelSide::Right => { - self.active_right_panel = fallback; - self.right_inspector_collapsed = fallback.is_none(); + self.shell.panels.active_right = fallback; + self.shell.panels.right_collapsed = fallback.is_none(); } } self.terminal.view.status = format!("{} closed", item.label()); } else { match side { PanelSide::Left => { - self.active_left_panel = Some(item); - self.left_sidebar_collapsed = false; + self.shell.panels.active_left = Some(item); + self.shell.panels.left_collapsed = false; } PanelSide::Right => { - self.active_right_panel = Some(item); - self.right_inspector_collapsed = false; + self.shell.panels.active_right = Some(item); + self.shell.panels.right_collapsed = false; } } self.terminal.view.status = format!("{} opened", item.label()); @@ -200,13 +202,13 @@ impl NyaTermApp { } let open_list = match side { - PanelSide::Left => &mut self.left_open_panels, - PanelSide::Right => &mut self.right_open_panels, + PanelSide::Left => &mut self.shell.panels.left_open, + PanelSide::Right => &mut self.shell.panels.right_open, }; let is_open = open_list.iter().any(|value| value == &id); let active = match side { - PanelSide::Left => self.active_left_panel, - PanelSide::Right => self.active_right_panel, + PanelSide::Left => self.shell.panels.active_left, + PanelSide::Right => self.shell.panels.active_right, }; // If exclusive overlay is showing and stacked panel already open, reveal stack. @@ -217,12 +219,12 @@ impl NyaTermApp { { match side { PanelSide::Left => { - self.active_left_panel = Some(item); - self.left_sidebar_collapsed = false; + self.shell.panels.active_left = Some(item); + self.shell.panels.left_collapsed = false; } PanelSide::Right => { - self.active_right_panel = Some(item); - self.right_inspector_collapsed = false; + self.shell.panels.active_right = Some(item); + self.shell.panels.right_collapsed = false; } } self.terminal.view.status = format!("{} focused", item.label()); @@ -247,14 +249,14 @@ impl NyaTermApp { }; match side { PanelSide::Left => { - self.active_left_panel = next_active; - self.left_sidebar_collapsed = - next_active.is_none() && self.left_open_panels.is_empty(); + self.shell.panels.active_left = next_active; + self.shell.panels.left_collapsed = + next_active.is_none() && self.shell.panels.left_open.is_empty(); } PanelSide::Right => { - self.active_right_panel = next_active; - self.right_inspector_collapsed = - next_active.is_none() && self.right_open_panels.is_empty(); + self.shell.panels.active_right = next_active; + self.shell.panels.right_collapsed = + next_active.is_none() && self.shell.panels.right_open.is_empty(); } } self.terminal.view.status = format!("{} closed", item.label()); @@ -262,12 +264,12 @@ impl NyaTermApp { open_list.push(id); match side { PanelSide::Left => { - self.active_left_panel = Some(item); - self.left_sidebar_collapsed = false; + self.shell.panels.active_left = Some(item); + self.shell.panels.left_collapsed = false; } PanelSide::Right => { - self.active_right_panel = Some(item); - self.right_inspector_collapsed = false; + self.shell.panels.active_right = Some(item); + self.shell.panels.right_collapsed = false; } } self.terminal.view.status = format!("{} opened", item.label()); @@ -277,39 +279,45 @@ impl NyaTermApp { } pub(in crate::features) fn ensure_panel_in_stack(&mut self, item: NavItem) { - self.main_mode = MainMode::Workspace; - self.selected_nav = item; - if !self.panel_multi_open { + self.shell.navigation.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = item; + if !self.shell.panels.multi_open { self.ensure_panel_open(item); return; } let id = item.persistence_id().to_string(); match self.panel_side_for_item(item) { Some(PanelSide::Left) => { - self.left_sidebar_collapsed = false; + self.shell.panels.left_collapsed = false; if Self::is_exclusive_panel_id(&id) { - self.active_left_panel = Some(item); + self.shell.panels.active_left = Some(item); } else { - if !self.left_open_panels.iter().any(|value| value == &id) { - self.left_open_panels.push(id); + if !self.shell.panels.left_open.iter().any(|value| value == &id) { + self.shell.panels.left_open.push(id); } - self.active_left_panel = Some(item); + self.shell.panels.active_left = Some(item); } } Some(PanelSide::Right) => { - self.right_inspector_collapsed = false; - self.right_focus = if item == NavItem::Recording { + self.shell.panels.right_collapsed = false; + self.shell.panels.right_focus = if item == NavItem::Recording { RightFocus::Recording } else { RightFocus::Default }; if Self::is_exclusive_panel_id(&id) { - self.active_right_panel = Some(item); + self.shell.panels.active_right = Some(item); } else { - if !self.right_open_panels.iter().any(|value| value == &id) { - self.right_open_panels.push(id); + if !self + .shell + .panels + .right_open + .iter() + .any(|value| value == &id) + { + self.shell.panels.right_open.push(id); } - self.active_right_panel = Some(item); + self.shell.panels.active_right = Some(item); } } None => {} @@ -325,15 +333,13 @@ impl NyaTermApp { container_height: f32, cx: &mut Context, ) { - self.panel_stack_resize = Some(PanelStackResizeState { + self.shell.panels.start_stack_resize( side, - above_id: above_id.clone(), - below_id: below_id.clone(), - start_y: event.position.y, - above_weight: self.panel_stack_weight(&above_id), - below_weight: self.panel_stack_weight(&below_id), - container_height: container_height.max(1.), - }); + above_id, + below_id, + event.position.y, + container_height, + ); self.terminal.view.status = "resizing panel stack".to_string(); cx.notify(); } @@ -343,24 +349,9 @@ impl NyaTermApp { event: &MouseMoveEvent, cx: &mut Context, ) { - let Some(state) = self.panel_stack_resize.clone() else { - return; - }; - let delta_px = f32::from(event.position.y - state.start_y); - let pair = state.above_weight + state.below_weight; - if pair <= 0. || state.container_height <= 0. { - return; + if self.shell.panels.update_stack_resize(event.position.y) { + cx.notify(); } - let px_per_weight = state.container_height / pair; - let min_weight = (48. / px_per_weight).min(pair / 2.).max(0.05); - let next_above = - (state.above_weight + delta_px / px_per_weight).clamp(min_weight, pair - min_weight); - let next_below = pair - next_above; - self.panel_stack_sizes - .insert(state.above_id.clone(), next_above); - self.panel_stack_sizes - .insert(state.below_id.clone(), next_below); - cx.notify(); } pub(in crate::features) fn finish_panel_stack_resize( @@ -368,7 +359,7 @@ impl NyaTermApp { _event: &MouseUpEvent, cx: &mut Context, ) { - if self.panel_stack_resize.take().is_some() { + if self.shell.panels.finish_stack_resize() { self.persist_ui_layout(); self.terminal.view.status = "panel stack sizes saved".to_string(); cx.notify(); @@ -376,11 +367,13 @@ impl NyaTermApp { } pub(in crate::features) fn sync_panel_stack_to_settings(&mut self) { - self.settings.ui_panel_multi_open = self.panel_multi_open; - self.settings.ui_left_open_panels = self.left_open_panels.clone(); - self.settings.ui_right_open_panels = self.right_open_panels.clone(); + self.settings.ui_panel_multi_open = self.shell.panels.multi_open; + self.settings.ui_left_open_panels = self.shell.panels.left_open.clone(); + self.settings.ui_right_open_panels = self.shell.panels.right_open.clone(); self.settings.ui_panel_stack_sizes = self - .panel_stack_sizes + .shell + .panels + .stack_sizes .iter() .filter_map(|(key, value)| { let scaled = (*value * 1000.).round(); @@ -390,29 +383,29 @@ impl NyaTermApp { } pub(in crate::features) fn apply_panel_stack_from_settings(&mut self) { - self.panel_multi_open = self.settings.ui_panel_multi_open; - self.left_open_panels = self.settings.ui_left_open_panels.clone(); - self.right_open_panels = self.settings.ui_right_open_panels.clone(); - self.panel_stack_sizes = self + self.shell.panels.multi_open = self.settings.ui_panel_multi_open; + self.shell.panels.left_open = self.settings.ui_left_open_panels.clone(); + self.shell.panels.right_open = self.settings.ui_right_open_panels.clone(); + self.shell.panels.stack_sizes = self .settings .ui_panel_stack_sizes .iter() .filter_map(|(key, value)| (*value > 0).then(|| (key.clone(), (*value as f32) / 1000.))) .collect(); - if self.panel_multi_open { - if self.left_open_panels.is_empty() { - if let Some(panel) = self.active_left_panel { + if self.shell.panels.multi_open { + if self.shell.panels.left_open.is_empty() { + if let Some(panel) = self.shell.panels.active_left { let id = panel.persistence_id().to_string(); if Self::is_stackable_panel_id(&id) { - self.left_open_panels.push(id); + self.shell.panels.left_open.push(id); } } } - if self.right_open_panels.is_empty() { - if let Some(panel) = self.active_right_panel { + if self.shell.panels.right_open.is_empty() { + if let Some(panel) = self.shell.panels.active_right { let id = panel.persistence_id().to_string(); if Self::is_stackable_panel_id(&id) { - self.right_open_panels.push(id); + self.shell.panels.right_open.push(id); } } } @@ -433,14 +426,24 @@ impl NyaTermApp { PanelSide::Left => self.current_left_panel(), PanelSide::Right => self.current_right_panel(), } - .or_else(|| self.activity_bar_layout.first_panel_on_side(side)) + .or_else(|| { + self.shell + .chrome + .activity_bar_layout + .first_panel_on_side(side) + }) .unwrap_or(NavItem::Workspace); self.single_side_panel(side, fallback, window, cx) - } else if open_ids.len() == 1 || !self.panel_multi_open { + } else if open_ids.len() == 1 || !self.shell.panels.multi_open { let panel = open_ids .first() .and_then(|id| NavItem::from_persistence_id(id)) - .or_else(|| self.activity_bar_layout.first_panel_on_side(side)) + .or_else(|| { + self.shell + .chrome + .activity_bar_layout + .first_panel_on_side(side) + }) .unwrap_or(NavItem::Workspace); self.single_side_panel(side, panel, window, cx) } else { @@ -701,7 +704,7 @@ impl NyaTermApp { cx.listener(|this, _, _, cx| { this.ai.history.open = false; this.ai.panel.execution_menu_open = false; - this.settings_active_tab = SettingsTab::AiGeneral; + this.shell.navigation.settings.active_tab = SettingsTab::AiGeneral; this.open_page(NavItem::Settings, cx); }), )) diff --git a/crates/nyaterm-desktop/src/features/shell/state.rs b/crates/nyaterm-desktop/src/features/shell/state.rs index 77b27bfbc..68a2eea35 100644 --- a/crates/nyaterm-desktop/src/features/shell/state.rs +++ b/crates/nyaterm-desktop/src/features/shell/state.rs @@ -4,12 +4,44 @@ //! lifecycles that span those views so the composition root does not retain //! independently mutable mirrors. -use gpui::Pixels; +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, Instant}; -use crate::models::{BottomPanelMode, BottomPanelResizeState}; +use gpui::{Pixels, ScrollHandle, WindowHandle}; + +use super::super::app_state::SettingsDraftSnapshot; +use super::super::settings_window::SettingsWindow; +use crate::models::{ + ActivityBarContextMenuState, ActivityBarLayoutState, BottomPanelMode, BottomPanelResizeState, + HeaderStatusState, MainMode, NavItem, PanelResizeSide, PanelResizeState, PanelSide, + PanelStackResizeState, RightFocus, SettingsTab, TitleMenu, TitleMenuSubmenu, WorkspacePaneNode, + WorkspaceSplitResizeState, WorkspaceSplitState, +}; pub(in crate::features) struct ShellFeatureState { pub bottom_panel: ShellBottomPanelState, + pub viewport: ShellViewportState, + pub navigation: ShellNavigationState, + pub panels: ShellPanelState, + pub chrome: ShellChromeState, + pub workspace: ShellWorkspaceState, +} + +pub(in crate::features) struct ShellFeatureInit { + pub bottom_panel_mode: BottomPanelMode, + pub quick_commands_height: f32, + pub command_send_height: f32, + pub active_left_panel: Option, + pub active_right_panel: Option, + pub left_open_panels: Vec, + pub right_open_panels: Vec, + pub panel_stack_sizes: HashMap, + pub panel_multi_open: bool, + pub left_sidebar_collapsed: bool, + pub right_inspector_collapsed: bool, + pub left_panel_width: f32, + pub right_panel_width: f32, + pub activity_bar_layout: ActivityBarLayoutState, } pub(in crate::features) struct ShellBottomPanelState { @@ -19,19 +51,145 @@ pub(in crate::features) struct ShellBottomPanelState { pub resize: Option, } +/// Window geometry and viewport-derived caches. +pub(in crate::features) struct ShellViewportState { + pub size: (f32, f32), + pub wallpaper_tile_dimensions: Option<(String, u32, u32)>, + pub last_change_at: Option, + pub title_drag_active_until: Option, +} + +/// Top-level page navigation and the settings-page/window lifecycle. +pub(in crate::features) struct ShellNavigationState { + pub selected_nav: NavItem, + pub main_mode: MainMode, + pub settings: ShellSettingsNavigationState, +} + +pub(in crate::features) struct ShellSettingsNavigationState { + pub active_tab: SettingsTab, + pub expanded_groups: HashSet, + pub draft_snapshot: Option, + pub window: Option>, + pub window_open_pending: bool, + pub previous_left_collapsed: Option, + pub previous_right_collapsed: Option, +} + +/// Side-panel selection, stack layout and resize interaction state. +pub(in crate::features) struct ShellPanelState { + pub active_left: Option, + pub active_right: Option, + pub left_open: Vec, + pub right_open: Vec, + pub stack_sizes: HashMap, + pub multi_open: bool, + pub right_focus: RightFocus, + pub left_collapsed: bool, + pub right_collapsed: bool, + pub mobile_left_open: bool, + pub mobile_right_open: bool, + pub left_width: f32, + pub right_width: f32, + pub resize: Option, + pub stack_resize: Option, +} + +/// Activity bar, title menus, tab-strip menus and connection-failure chrome. +pub(in crate::features) struct ShellChromeState { + pub activity_bar_layout: ActivityBarLayoutState, + pub activity_bar_context_menu: Option, + pub title_menu_open: Option, + pub title_menu_submenu: Option, + pub header_status: HeaderStatusState, + pub open_tabs_menu_open: bool, + pub new_session_menu_open: bool, + pub new_session_all_sessions_open: bool, + pub new_session_group_menu_path: Vec, + pub session_tab_strip_scroll: ScrollHandle, + pub session_tab_scroll_into_view_pending: bool, + pub last_connect_failure_name: Option, + pub last_connect_failure_error: Option, +} + +/// Global and per-tab pane trees for the workspace surface. +pub(in crate::features) struct ShellWorkspaceState { + pub split: Option, + pub split_resize: Option, + pub pane_roots: HashMap, + pub tab_owner: HashMap, + pub focused_terminal_leaf_id: Option, + pub pane_layout_restored: bool, +} + impl ShellFeatureState { - pub(in crate::features) fn new( - bottom_panel_mode: BottomPanelMode, - quick_commands_height: f32, - command_send_height: f32, - ) -> Self { + pub(in crate::features) fn new(init: ShellFeatureInit) -> Self { Self { bottom_panel: ShellBottomPanelState { - mode: bottom_panel_mode, - quick_commands_height, - command_send_height, + mode: init.bottom_panel_mode, + quick_commands_height: init.quick_commands_height, + command_send_height: init.command_send_height, resize: None, }, + viewport: ShellViewportState { + size: (1280., 800.), + wallpaper_tile_dimensions: None, + last_change_at: None, + title_drag_active_until: None, + }, + navigation: ShellNavigationState { + selected_nav: NavItem::Workspace, + main_mode: MainMode::Workspace, + settings: ShellSettingsNavigationState { + active_tab: SettingsTab::General, + expanded_groups: HashSet::from(["workspace".to_string()]), + draft_snapshot: None, + window: None, + window_open_pending: false, + previous_left_collapsed: None, + previous_right_collapsed: None, + }, + }, + panels: ShellPanelState { + active_left: init.active_left_panel, + active_right: init.active_right_panel, + left_open: init.left_open_panels, + right_open: init.right_open_panels, + stack_sizes: init.panel_stack_sizes, + multi_open: init.panel_multi_open, + right_focus: RightFocus::Default, + left_collapsed: init.left_sidebar_collapsed, + right_collapsed: init.right_inspector_collapsed, + mobile_left_open: false, + mobile_right_open: false, + left_width: init.left_panel_width, + right_width: init.right_panel_width, + resize: None, + stack_resize: None, + }, + chrome: ShellChromeState { + activity_bar_layout: init.activity_bar_layout, + activity_bar_context_menu: None, + title_menu_open: None, + title_menu_submenu: None, + header_status: HeaderStatusState::default(), + open_tabs_menu_open: false, + new_session_menu_open: false, + new_session_all_sessions_open: false, + new_session_group_menu_path: Vec::new(), + session_tab_strip_scroll: ScrollHandle::new(), + session_tab_scroll_into_view_pending: false, + last_connect_failure_name: None, + last_connect_failure_error: None, + }, + workspace: ShellWorkspaceState { + split: None, + split_resize: None, + pane_roots: HashMap::new(), + tab_owner: HashMap::new(), + focused_terminal_leaf_id: None, + pane_layout_restored: false, + }, } } } @@ -77,16 +235,225 @@ impl ShellBottomPanelState { } } +impl ShellViewportState { + pub(in crate::features) fn update_size(&mut self, size: (f32, f32), now: Instant) -> bool { + if self.size == size { + return false; + } + self.size = size; + self.last_change_at = Some(now); + true + } + + pub(in crate::features) fn mark_title_drag(&mut self, now: Instant, hold: Duration) { + self.title_drag_active_until = Some(now + hold); + } + + pub(in crate::features) fn title_drag_active(&self, now: Instant) -> bool { + self.title_drag_active_until + .is_some_and(|until| now < until) + } +} + +impl ShellPanelState { + const LEFT_WIDTH_MIN: f32 = 160.; + const LEFT_WIDTH_MAX: f32 = 720.; + const RIGHT_WIDTH_MIN: f32 = 200.; + const RIGHT_WIDTH_MAX: f32 = 720.; + + pub(in crate::features) fn start_resize(&mut self, side: PanelResizeSide, start_x: Pixels) { + let start_width = match side { + PanelResizeSide::Left => self.left_width, + PanelResizeSide::Right => self.right_width, + }; + self.resize = Some(PanelResizeState { + side, + start_x, + start_width: gpui::px(start_width), + }); + } + + pub(in crate::features) fn update_resize( + &mut self, + current_x: Pixels, + ) -> Option<(PanelResizeSide, f32)> { + let state = self.resize?; + let delta = f32::from(current_x - state.start_x); + let start = f32::from(state.start_width); + let width = match state.side { + PanelResizeSide::Left => { + self.left_width = (start + delta).clamp(Self::LEFT_WIDTH_MIN, Self::LEFT_WIDTH_MAX); + self.left_width + } + PanelResizeSide::Right => { + self.right_width = + (start - delta).clamp(Self::RIGHT_WIDTH_MIN, Self::RIGHT_WIDTH_MAX); + self.right_width + } + }; + Some((state.side, width)) + } + + pub(in crate::features) fn finish_resize(&mut self) -> bool { + self.resize.take().is_some() + } + + pub(in crate::features) fn stack_weight(&self, panel_id: &str) -> f32 { + self.stack_sizes + .get(panel_id) + .copied() + .filter(|value| value.is_finite() && *value > 0.) + .unwrap_or(1.) + } + + pub(in crate::features) fn start_stack_resize( + &mut self, + side: PanelSide, + above_id: String, + below_id: String, + start_y: Pixels, + container_height: f32, + ) { + self.stack_resize = Some(PanelStackResizeState { + side, + above_weight: self.stack_weight(&above_id), + below_weight: self.stack_weight(&below_id), + above_id, + below_id, + start_y, + container_height: container_height.max(1.), + }); + } + + pub(in crate::features) fn update_stack_resize(&mut self, current_y: Pixels) -> bool { + let Some(state) = self.stack_resize.as_ref() else { + return false; + }; + let delta_px = f32::from(current_y - state.start_y); + let pair = state.above_weight + state.below_weight; + if pair <= 0. || state.container_height <= 0. { + return false; + } + let px_per_weight = state.container_height / pair; + let min_weight = (48. / px_per_weight).min(pair / 2.).max(0.05); + let next_above = + (state.above_weight + delta_px / px_per_weight).clamp(min_weight, pair - min_weight); + let next_below = pair - next_above; + self.stack_sizes.insert(state.above_id.clone(), next_above); + self.stack_sizes.insert(state.below_id.clone(), next_below); + true + } + + pub(in crate::features) fn finish_stack_resize(&mut self) -> bool { + self.stack_resize.take().is_some() + } +} + +impl ShellChromeState { + pub(in crate::features) fn prepare_session_switch(&mut self) { + self.open_tabs_menu_open = false; + self.close_new_session_menu(); + self.session_tab_scroll_into_view_pending = true; + } + + pub(in crate::features) fn toggle_open_tabs_menu(&mut self) { + self.open_tabs_menu_open = !self.open_tabs_menu_open; + if self.open_tabs_menu_open { + self.close_new_session_menu(); + self.title_menu_open = None; + } + } + + pub(in crate::features) fn close_open_tabs_menu(&mut self) -> bool { + std::mem::take(&mut self.open_tabs_menu_open) + } + + pub(in crate::features) fn toggle_new_session_menu(&mut self) { + self.new_session_menu_open = !self.new_session_menu_open; + if self.new_session_menu_open { + self.open_tabs_menu_open = false; + self.title_menu_open = None; + } + self.new_session_all_sessions_open = false; + self.new_session_group_menu_path.clear(); + } + + pub(in crate::features) fn close_new_session_menu(&mut self) -> bool { + let changed = self.new_session_menu_open + || self.new_session_all_sessions_open + || !self.new_session_group_menu_path.is_empty(); + self.new_session_menu_open = false; + self.new_session_all_sessions_open = false; + self.new_session_group_menu_path.clear(); + changed + } +} + +impl ShellWorkspaceState { + pub(in crate::features) fn rebuild_tab_owners(&mut self) { + let mut owners = HashMap::new(); + for (tab_root, tree) in &self.pane_roots { + for leaf in tree.session_ids() { + owners.insert(leaf, tab_root.clone()); + } + } + self.tab_owner = owners; + } + + pub(in crate::features) fn replace_session_id(&mut self, old_id: &str, new_id: &str) { + for root in self.pane_roots.values_mut() { + root.replace_session_id(old_id, new_id); + } + if let Some(root) = self.pane_roots.remove(old_id) { + self.pane_roots.insert(new_id.to_string(), root); + } + if let Some(root) = self.split.as_mut() { + root.replace_session_id(old_id, new_id); + } + self.rebuild_tab_owners(); + } + + pub(in crate::features) fn remove_session(&mut self, session_id: &str) { + self.tab_owner.remove(session_id); + self.pane_roots.remove(session_id); + } +} + #[cfg(test)] mod tests { + use std::collections::HashMap; + use std::time::{Duration, Instant}; + use gpui::px; - use super::ShellFeatureState; - use crate::models::BottomPanelMode; + use super::{ShellFeatureInit, ShellFeatureState}; + use crate::models::{ + ActivityBarLayoutState, BottomPanelMode, PanelResizeSide, PanelSide, WorkspacePaneNode, + WorkspaceSplitDirection, + }; + + fn shell(mode: BottomPanelMode) -> ShellFeatureState { + ShellFeatureState::new(ShellFeatureInit { + bottom_panel_mode: mode, + quick_commands_height: 120., + command_send_height: 180., + active_left_panel: None, + active_right_panel: None, + left_open_panels: Vec::new(), + right_open_panels: Vec::new(), + panel_stack_sizes: HashMap::new(), + panel_multi_open: false, + left_sidebar_collapsed: true, + right_inspector_collapsed: true, + left_panel_width: 240., + right_panel_width: 320., + activity_bar_layout: ActivityBarLayoutState::default(), + }) + } #[test] fn bottom_panel_resize_updates_only_the_mode_that_started_the_drag() { - let mut shell = ShellFeatureState::new(BottomPanelMode::QuickCommands, 120., 180.); + let mut shell = shell(BottomPanelMode::QuickCommands); assert!(shell.bottom_panel.start_resize(px(400.))); shell.bottom_panel.mode = BottomPanelMode::CommandSend; @@ -99,9 +466,109 @@ mod tests { #[test] fn hidden_bottom_panel_does_not_start_resize() { - let mut shell = ShellFeatureState::new(BottomPanelMode::Hidden, 120., 180.); + let mut shell = shell(BottomPanelMode::Hidden); assert!(!shell.bottom_panel.start_resize(px(400.))); assert!(shell.bottom_panel.resize.is_none()); } + + #[test] + fn panel_resize_clamps_each_side_and_finishes_once() { + let mut shell = shell(BottomPanelMode::Hidden); + + shell.panels.start_resize(PanelResizeSide::Left, px(100.)); + assert_eq!( + shell.panels.update_resize(px(-100.)), + Some((PanelResizeSide::Left, 160.)) + ); + assert!(shell.panels.finish_resize()); + assert!(!shell.panels.finish_resize()); + + shell.panels.start_resize(PanelResizeSide::Right, px(100.)); + assert_eq!( + shell.panels.update_resize(px(-500.)), + Some((PanelResizeSide::Right, 720.)) + ); + } + + #[test] + fn panel_stack_resize_preserves_pair_weight() { + let mut shell = shell(BottomPanelMode::Hidden); + shell.panels.stack_sizes.insert("above".to_string(), 2.); + shell.panels.stack_sizes.insert("below".to_string(), 1.); + shell.panels.start_stack_resize( + PanelSide::Left, + "above".to_string(), + "below".to_string(), + px(100.), + 300., + ); + + assert!(shell.panels.update_stack_resize(px(150.))); + let total = shell.panels.stack_sizes["above"] + shell.panels.stack_sizes["below"]; + assert!((total - 3.).abs() < f32::EPSILON); + assert!(shell.panels.finish_stack_resize()); + } + + #[test] + fn chrome_menu_transitions_are_mutually_exclusive() { + let mut shell = shell(BottomPanelMode::Hidden); + shell.chrome.new_session_menu_open = true; + shell.chrome.new_session_all_sessions_open = true; + shell + .chrome + .new_session_group_menu_path + .push("group".to_string()); + + shell.chrome.toggle_open_tabs_menu(); + assert!(shell.chrome.open_tabs_menu_open); + assert!(!shell.chrome.new_session_menu_open); + assert!(!shell.chrome.new_session_all_sessions_open); + assert!(shell.chrome.new_session_group_menu_path.is_empty()); + + shell.chrome.toggle_new_session_menu(); + assert!(!shell.chrome.open_tabs_menu_open); + assert!(shell.chrome.new_session_menu_open); + } + + #[test] + fn viewport_tracks_only_real_geometry_changes_and_title_drag_deadline() { + let mut shell = shell(BottomPanelMode::Hidden); + let now = Instant::now(); + assert!(!shell.viewport.update_size((1280., 800.), now)); + assert!(shell.viewport.update_size((1024., 768.), now)); + assert_eq!(shell.viewport.last_change_at, Some(now)); + + shell + .viewport + .mark_title_drag(now, Duration::from_millis(10)); + assert!(shell.viewport.title_drag_active(now)); + assert!( + !shell + .viewport + .title_drag_active(now + Duration::from_millis(10)) + ); + } + + #[test] + fn workspace_rebuilds_and_renames_tab_ownership() { + let mut shell = shell(BottomPanelMode::Hidden); + shell.workspace.pane_roots.insert( + "root".to_string(), + WorkspacePaneNode::Split { + id: "split".to_string(), + direction: WorkspaceSplitDirection::Vertical, + ratio_percent: 50, + first: Box::new(WorkspacePaneNode::leaf("root".to_string())), + second: Box::new(WorkspacePaneNode::leaf("leaf".to_string())), + }, + ); + shell.workspace.rebuild_tab_owners(); + assert_eq!(shell.workspace.tab_owner["leaf"], "root"); + + shell.workspace.replace_session_id("root", "renamed"); + assert!(shell.workspace.pane_roots.contains_key("renamed")); + assert_eq!(shell.workspace.tab_owner["leaf"], "renamed"); + assert_eq!(shell.workspace.tab_owner["renamed"], "renamed"); + } } diff --git a/crates/nyaterm-desktop/src/features/shell/tab_windows_runtime.rs b/crates/nyaterm-desktop/src/features/shell/tab_windows_runtime.rs index bf4976a55..a4643e41b 100644 --- a/crates/nyaterm-desktop/src/features/shell/tab_windows_runtime.rs +++ b/crates/nyaterm-desktop/src/features/shell/tab_windows_runtime.rs @@ -25,7 +25,7 @@ impl NyaTermApp { .collect::>(); if live_ids.is_empty() { self.terminal.windows.tree = None; - self.focused_terminal_window_leaf_id = None; + self.shell.workspace.focused_terminal_leaf_id = None; return; } @@ -44,7 +44,7 @@ impl NyaTermApp { } if let Some(root) = self.terminal.windows.tree.as_mut() { - let preferred = self.focused_terminal_window_leaf_id.clone(); + let preferred = self.shell.workspace.focused_terminal_leaf_id.clone(); for tab_id in &live_ids { root.ensure_tab(tab_id, preferred.as_deref()); } @@ -52,11 +52,13 @@ impl NyaTermApp { let _ = root.set_active_tab(&active); } if self - .focused_terminal_window_leaf_id + .shell + .workspace + .focused_terminal_leaf_id .as_ref() .is_none_or(|id| !root.leaf_ids().iter().any(|leaf| leaf == id)) { - self.focused_terminal_window_leaf_id = root.first_leaf_id(); + self.shell.workspace.focused_terminal_leaf_id = root.first_leaf_id(); } } } @@ -75,7 +77,7 @@ impl NyaTermApp { } let active = self.session.active_id.clone(); let root = TerminalWindowNode::leaf(tab_ids, active); - self.focused_terminal_window_leaf_id = root.first_leaf_id(); + self.shell.workspace.focused_terminal_leaf_id = root.first_leaf_id(); self.terminal.windows.tree = Some(root); } @@ -90,10 +92,10 @@ impl NyaTermApp { let _ = set_leaf_active(root, &leaf_id, &session_id); let _ = root.set_active_tab(&session_id); } - self.focused_terminal_window_leaf_id = Some(leaf_id); + self.shell.workspace.focused_terminal_leaf_id = Some(leaf_id); self.activate_session_id_with_surface_sync(&session_id, cx); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; cx.notify(); } @@ -115,7 +117,7 @@ impl NyaTermApp { }; let _ = root.set_active_tab(&tab_id); if let Some(leaf_id) = find_leaf_with_tab(root, &tab_id) { - self.focused_terminal_window_leaf_id = Some(leaf_id); + self.shell.workspace.focused_terminal_leaf_id = Some(leaf_id); } } @@ -131,7 +133,7 @@ impl NyaTermApp { }; if root.place_tab_before(&tab_id, &before_tab_id) { let _ = root.set_active_tab(&tab_id); - self.focused_terminal_window_leaf_id = + self.shell.workspace.focused_terminal_leaf_id = find_leaf_with_tab(root, &tab_id).or_else(|| root.first_leaf_id()); self.activate_session_id_with_surface_sync(&tab_id, cx); self.terminal.view.status = format!( @@ -188,11 +190,11 @@ impl NyaTermApp { return; } let _ = root.set_active_tab(&tab_id); - self.focused_terminal_window_leaf_id = + self.shell.workspace.focused_terminal_leaf_id = find_leaf_with_tab(root, &tab_id).or_else(|| root.first_leaf_id()); self.activate_session_id_with_surface_sync(&tab_id, cx); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; let zone_label = match zone { TabDockZone::Center => "merged into leaf".to_string(), TabDockZone::Edge(edge) => format!("split to {}", edge.label()), @@ -224,20 +226,20 @@ impl NyaTermApp { return; }; // Clear global pane splits so multi-leaf rendering takes precedence cleanly. - self.workspace_split = None; - self.workspace_split_resize = None; + self.shell.workspace.split = None; + self.shell.workspace.split_resize = None; if let Some(active) = self.session.active_id.clone() { let mut root = layout; let _ = root.set_active_tab(&active); - self.focused_terminal_window_leaf_id = + self.shell.workspace.focused_terminal_leaf_id = find_leaf_with_tab(&root, &active).or_else(|| root.first_leaf_id()); self.terminal.windows.tree = Some(root); } else { - self.focused_terminal_window_leaf_id = layout.first_leaf_id(); + self.shell.workspace.focused_terminal_leaf_id = layout.first_leaf_id(); self.terminal.windows.tree = Some(layout); } - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.terminal.view.status = format!("applied {}", mode.label().to_ascii_lowercase()); self.persist_terminal_window_layout(); // Global pane layout is obsolete while multi-leaf is active. @@ -296,11 +298,11 @@ impl NyaTermApp { if !matches!(restored, TerminalWindowNode::Split { .. }) { return; } - self.focused_terminal_window_leaf_id = restored.first_leaf_id(); + self.shell.workspace.focused_terminal_leaf_id = restored.first_leaf_id(); if let Some(active) = self.session.active_id.clone() { let mut root = restored; let _ = root.set_active_tab(&active); - self.focused_terminal_window_leaf_id = + self.shell.workspace.focused_terminal_leaf_id = find_leaf_with_tab(&root, &active).or_else(|| root.first_leaf_id()); self.terminal.windows.tree = Some(root); } else { diff --git a/crates/nyaterm-desktop/src/features/shell/workspace_runtime.rs b/crates/nyaterm-desktop/src/features/shell/workspace_runtime.rs index 31a4a8d71..7cab3c342 100644 --- a/crates/nyaterm-desktop/src/features/shell/workspace_runtime.rs +++ b/crates/nyaterm-desktop/src/features/shell/workspace_runtime.rs @@ -23,10 +23,10 @@ impl NyaTermApp { pub(in crate::features) fn prune_workspace_split(&mut self) { let live_ids = self.live_session_ids_with_disconnected(); - let before_roots = self.session_pane_roots.clone(); - let root_keys: Vec = self.session_pane_roots.keys().cloned().collect(); + let before_roots = self.shell.workspace.pane_roots.clone(); + let root_keys: Vec = self.shell.workspace.pane_roots.keys().cloned().collect(); for tab_root in root_keys { - let Some(root) = self.session_pane_roots.remove(&tab_root) else { + let Some(root) = self.shell.workspace.pane_roots.remove(&tab_root) else { continue; }; match root.prune(&live_ids) { @@ -41,7 +41,7 @@ impl NyaTermApp { .next() .unwrap_or_else(|| tab_root.clone()) }; - self.session_pane_roots.insert(key, node); + self.shell.workspace.pane_roots.insert(key, node); } // Single leaf collapses to no stored tree for this tab. } @@ -49,13 +49,13 @@ impl NyaTermApp { } } // Also prune legacy workspace_split if roots empty (migration path). - if self.session_pane_roots.is_empty() { - if let Some(root) = self.workspace_split.take() { + if self.shell.workspace.pane_roots.is_empty() { + if let Some(root) = self.shell.workspace.split.take() { match root.prune(&live_ids) { Some(node) => { if node.is_split() { if let Some(first) = node.session_ids().into_iter().next() { - self.session_pane_roots.insert(first.clone(), node); + self.shell.workspace.pane_roots.insert(first.clone(), node); } } else if let WorkspacePaneNode::Leaf { session_id } = node { if self.session.active_id.is_none() { @@ -69,7 +69,7 @@ impl NyaTermApp { } self.rebuild_session_tab_owners(); self.sync_workspace_split_from_active_tab(); - if self.session_pane_roots != before_roots { + if self.shell.workspace.pane_roots != before_roots { self.persist_workspace_pane_layout(); if self.startup_restore_complete { self.persist_open_tabs(); @@ -89,29 +89,21 @@ impl NyaTermApp { /// Rebuild leaf→tab-root ownership from `session_pane_roots`. pub(in crate::features) fn rebuild_session_tab_owners(&mut self) { - self.session_tab_owner.clear(); - let roots: Vec<(String, WorkspacePaneNode)> = self - .session_pane_roots - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - for (tab_root, tree) in roots { - for leaf in tree.session_ids() { - self.session_tab_owner.insert(leaf, tab_root.clone()); - } - } + self.shell.workspace.rebuild_tab_owners(); } /// Expose the active tab's pane tree via `workspace_split` for existing renderers. pub(in crate::features) fn sync_workspace_split_from_active_tab(&mut self) { let Some(active) = self.session.active_id.clone() else { - self.workspace_split = None; + self.shell.workspace.split = None; self.sync_terminal_frame_snapshot_priority(); return; }; let tab_root = self.tab_root_for_session(&active); - self.workspace_split = self - .session_pane_roots + self.shell.workspace.split = self + .shell + .workspace + .pane_roots .get(&tab_root) .filter(|root| root.is_split()) .cloned(); @@ -123,9 +115,9 @@ impl NyaTermApp { return; }; let tab_root = self.tab_root_for_session(&active); - if let Some(root) = self.workspace_split.clone() { + if let Some(root) = self.shell.workspace.split.clone() { if root.is_split() { - self.session_pane_roots.insert(tab_root, root); + self.shell.workspace.pane_roots.insert(tab_root, root); self.rebuild_session_tab_owners(); } } @@ -139,7 +131,7 @@ impl NyaTermApp { ) { let split_id = uuid(); let tab_root = self.tab_root_for_session(&primary_session_id); - if let Some(root) = self.session_pane_roots.get_mut(&tab_root) { + if let Some(root) = self.shell.workspace.pane_roots.get_mut(&tab_root) { if root.split_leaf( &primary_session_id, secondary_session_id.clone(), @@ -149,8 +141,8 @@ impl NyaTermApp { self.rebuild_session_tab_owners(); self.activate_session_id(&secondary_session_id); self.sync_workspace_split_from_active_tab(); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.persist_workspace_pane_layout(); if self.startup_restore_complete { self.persist_open_tabs(); @@ -166,12 +158,12 @@ impl NyaTermApp { first: Box::new(WorkspacePaneNode::leaf(primary_session_id.clone())), second: Box::new(WorkspacePaneNode::leaf(secondary_session_id.clone())), }; - self.session_pane_roots.insert(tab_root, root); + self.shell.workspace.pane_roots.insert(tab_root, root); self.rebuild_session_tab_owners(); self.activate_session_id(&secondary_session_id); self.sync_workspace_split_from_active_tab(); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.persist_workspace_pane_layout(); if self.startup_restore_complete { self.persist_open_tabs(); @@ -188,8 +180,8 @@ impl NyaTermApp { } self.activate_session_id_with_surface_sync(&session_id, cx); self.sync_workspace_split_from_active_tab(); - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.terminal.view.status = format!("focused pane {}", short_id(&session_id)); cx.notify(); } @@ -241,18 +233,18 @@ impl NyaTermApp { return; }; let tab_root = self.tab_root_for_session(&active_id); - let Some(root) = self.session_pane_roots.remove(&tab_root) else { - self.workspace_split = None; + let Some(root) = self.shell.workspace.pane_roots.remove(&tab_root) else { + self.shell.workspace.split = None; self.terminal.view.status = "workspace is not split".to_string(); cx.notify(); return; }; - self.workspace_split_resize = None; + self.shell.workspace.split_resize = None; if let Some(collapsed) = collapse_around_session(root.clone(), &active_id) { match collapsed { WorkspacePaneNode::Split { .. } => { - self.session_pane_roots.insert(tab_root, collapsed); + self.shell.workspace.pane_roots.insert(tab_root, collapsed); self.terminal.view.status = "collapsed focused split".to_string(); } WorkspacePaneNode::Leaf { session_id } => { @@ -295,7 +287,7 @@ impl NyaTermApp { ) { (Some(direction), Some(start_ratio)) => (direction, start_ratio), _ => { - let Some(root) = self.workspace_split.as_ref() else { + let Some(root) = self.shell.workspace.split.as_ref() else { return; }; let Some(direction) = root.direction_for_split(&split_id) else { @@ -308,7 +300,7 @@ impl NyaTermApp { } } } else { - let Some(root) = self.workspace_split.as_ref() else { + let Some(root) = self.shell.workspace.split.as_ref() else { return; }; let Some(direction) = root.direction_for_split(&split_id) else { @@ -323,7 +315,7 @@ impl NyaTermApp { WorkspaceSplitDirection::Horizontal => event.position.y, WorkspaceSplitDirection::Vertical => event.position.x, }; - self.workspace_split_resize = Some(WorkspaceSplitResizeState { + self.shell.workspace.split_resize = Some(WorkspaceSplitResizeState { split_id, direction, start_pos, @@ -339,7 +331,7 @@ impl NyaTermApp { event: &MouseMoveEvent, cx: &mut Context, ) { - let Some(state) = self.workspace_split_resize.clone() else { + let Some(state) = self.shell.workspace.split_resize.clone() else { return; }; let current = match state.direction { @@ -365,7 +357,7 @@ impl NyaTermApp { } } if !applied { - if let Some(root) = self.workspace_split.as_mut() { + if let Some(root) = self.shell.workspace.split.as_mut() { if root.set_ratio_for_split(&state.split_id, next) { applied = true; self.write_back_active_tab_pane_root(); @@ -383,7 +375,7 @@ impl NyaTermApp { _event: &MouseUpEvent, cx: &mut Context, ) { - if let Some(state) = self.workspace_split_resize.take() { + if let Some(state) = self.shell.workspace.split_resize.take() { let ratio = self .terminal .windows @@ -391,7 +383,9 @@ impl NyaTermApp { .as_ref() .and_then(|root| root.ratio_for_split(&state.split_id)) .or_else(|| { - self.workspace_split + self.shell + .workspace + .split .as_ref() .and_then(|root| root.ratio_for_split(&state.split_id)) }); @@ -401,7 +395,9 @@ impl NyaTermApp { if self.terminal_windows_is_multi_leaf() { self.persist_terminal_window_layout(); } else if self - .workspace_split + .shell + .workspace + .split .as_ref() .is_some_and(|root| root.is_split()) { @@ -472,12 +468,16 @@ impl NyaTermApp { .map(|session| session.id) .collect::>(); let layout = self - .workspace_split + .shell + .workspace + .split .as_ref() .filter(|root| root.is_split()) .and_then(|root| root.serialize_layout(&ordered)) .or_else(|| { - self.session_pane_roots + self.shell + .workspace + .pane_roots .values() .find(|root| root.is_split()) .and_then(|root| root.serialize_layout(&ordered)) @@ -496,16 +496,16 @@ impl NyaTermApp { } pub(in crate::features) fn try_restore_workspace_pane_layout(&mut self) { - if self.workspace_pane_layout_restored { + if self.shell.workspace.pane_layout_restored { return; } if !self.settings.startup_restore || !self.settings.startup_restore_window_layout { - self.workspace_pane_layout_restored = true; + self.shell.workspace.pane_layout_restored = true; return; } // Multi-leaf tab windows take visual precedence; skip pane restore when active. if self.terminal_windows_is_multi_leaf() { - self.workspace_pane_layout_restored = true; + self.shell.workspace.pane_layout_restored = true; return; } let ordered = self @@ -516,11 +516,11 @@ impl NyaTermApp { if ordered.len() < 2 { // After startup finishes, don't keep waiting forever for a second tab. if self.startup_restore_complete { - self.workspace_pane_layout_restored = true; + self.shell.workspace.pane_layout_restored = true; } return; } - self.workspace_pane_layout_restored = true; + self.shell.workspace.pane_layout_restored = true; let Ok(store) = ConnectionStore::open_with_portable_key_path( self.runtime.config_dir(), self.runtime.portable_key_path().map(ToOwned::to_owned), @@ -548,12 +548,12 @@ impl NyaTermApp { } // Install as a per-tab root under the first leaf (legacy global layout path). if let Some(first) = restored.session_ids().into_iter().next() { - self.session_pane_roots.insert(first, restored); + self.shell.workspace.pane_roots.insert(first, restored); self.rebuild_session_tab_owners(); self.sync_workspace_split_from_active_tab(); } - self.selected_nav = NavItem::Workspace; - self.main_mode = MainMode::Workspace; + self.shell.navigation.selected_nav = NavItem::Workspace; + self.shell.navigation.main_mode = MainMode::Workspace; self.terminal.view.status = "restored workspace pane layout".to_string(); } } diff --git a/crates/nyaterm-desktop/src/features/sync/cloud_sync_runtime/settings.rs b/crates/nyaterm-desktop/src/features/sync/cloud_sync_runtime/settings.rs index 58e40b724..debc28664 100644 --- a/crates/nyaterm-desktop/src/features/sync/cloud_sync_runtime/settings.rs +++ b/crates/nyaterm-desktop/src/features/sync/cloud_sync_runtime/settings.rs @@ -27,7 +27,7 @@ impl NyaTermApp { || (!self.settings.has_master_password && self.settings_master_password_draft.is_empty())) { - self.settings_active_tab = SettingsTab::Security; + self.shell.navigation.settings.active_tab = SettingsTab::Security; self.cloud_sync.status = "configure a master password before enabling cloud sync".to_string(); self.terminal.view.status = self.cloud_sync.status.clone(); diff --git a/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/action_links.rs b/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/action_links.rs index 9a1a4d1bc..6e03254b0 100644 --- a/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/action_links.rs +++ b/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/action_links.rs @@ -14,7 +14,7 @@ impl NyaTermApp { let Some(menu) = self.terminal.menus.action_link_menu.clone() else { return div().into_any_element(); }; - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; let (menu_x, menu_y) = clamp_menu_position( f32::from(menu.x), f32::from(menu.y), diff --git a/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/menu.rs b/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/menu.rs index b3634477d..34ac614bd 100644 --- a/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/menu.rs +++ b/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/menu.rs @@ -143,7 +143,7 @@ impl NyaTermApp { }; let _ = selection_link_kind; - let (viewport_w, viewport_h) = self.last_viewport_size; + let (viewport_w, viewport_h) = self.shell.viewport.size; // Approximate height from max-h for clamp (scrollable menus can be shorter). let (menu_x, menu_y) = clamp_menu_position( f32::from(menu.x), diff --git a/crates/nyaterm-desktop/src/features/terminal/terminal_runtime/buffer.rs b/crates/nyaterm-desktop/src/features/terminal/terminal_runtime/buffer.rs index 6f0317126..f5621dcc0 100644 --- a/crates/nyaterm-desktop/src/features/terminal/terminal_runtime/buffer.rs +++ b/crates/nyaterm-desktop/src/features/terminal/terminal_runtime/buffer.rs @@ -890,7 +890,7 @@ impl NyaTermApp { } fn terminal_session_has_visible_surface(&self, session_id: &str) -> bool { - if session_id.is_empty() || self.main_mode != MainMode::Workspace { + if session_id.is_empty() || self.shell.navigation.main_mode != MainMode::Workspace { return false; } self.visible_terminal_session_ids() @@ -899,7 +899,7 @@ impl NyaTermApp { } pub(in crate::features) fn visible_terminal_session_ids(&self) -> Vec<&str> { - if self.main_mode != MainMode::Workspace { + if self.shell.navigation.main_mode != MainMode::Workspace { return Vec::new(); } if let Some(root) = self.terminal.windows.tree.as_ref() @@ -907,7 +907,7 @@ impl NyaTermApp { { return terminal_window_node_visible_tab_ids(root); } - if let Some(root) = self.workspace_split.as_ref() { + if let Some(root) = self.shell.workspace.split.as_ref() { return workspace_pane_node_visible_session_ids(root); } self.session.active_id.iter().map(String::as_str).collect() diff --git a/crates/nyaterm-desktop/src/features/terminal/terminal_runtime/sessions.rs b/crates/nyaterm-desktop/src/features/terminal/terminal_runtime/sessions.rs index 9ac773121..618af586f 100644 --- a/crates/nyaterm-desktop/src/features/terminal/terminal_runtime/sessions.rs +++ b/crates/nyaterm-desktop/src/features/terminal/terminal_runtime/sessions.rs @@ -55,7 +55,7 @@ impl NyaTermApp { // Tauri: closing a strip tab closes the whole tab tree; closing a secondary leaf // only removes that pane. Strip close uses the tab-root id. let close_ids = if !self.is_secondary_pane_session(&session_id) { - if let Some(root) = self.session_pane_roots.get(&session_id) { + if let Some(root) = self.shell.workspace.pane_roots.get(&session_id) { root.session_ids() } else { vec![session_id.clone()] diff --git a/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/action_links.rs b/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/action_links.rs index 1cf04f1f9..3d4a746c5 100644 --- a/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/action_links.rs +++ b/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/action_links.rs @@ -277,7 +277,9 @@ impl NyaTermApp { { return window_root.active_tabs(); } - self.workspace_split + self.shell + .workspace + .split .as_ref() .map(|root| root.session_ids()) .or_else(|| self.session.active_id.clone().map(|id| vec![id])) diff --git a/crates/nyaterm-desktop/src/features/transfers/transfer_paths.rs b/crates/nyaterm-desktop/src/features/transfers/transfer_paths.rs index 4c5e5eba8..0e9e77a04 100644 --- a/crates/nyaterm-desktop/src/features/transfers/transfer_paths.rs +++ b/crates/nyaterm-desktop/src/features/transfers/transfer_paths.rs @@ -189,7 +189,7 @@ impl NyaTermApp { } let Some(config) = self.session.active_ssh_config.clone() else { self.terminal.view.status = "start an SSH session first".to_string(); - self.selected_nav = NavItem::Transfers; + self.shell.navigation.selected_nav = NavItem::Transfers; cx.notify(); return; }; diff --git a/docs/architecture/gpui-migration-status.md b/docs/architecture/gpui-migration-status.md index 43c2429e9..a07cf198c 100644 --- a/docs/architecture/gpui-migration-status.md +++ b/docs/architecture/gpui-migration-status.md @@ -9,7 +9,7 @@ Last updated from the working tree on 2026-07-28. | Metric | Current value | Notes | | --- | ---: | --- | -| `NyaTermApp` fields | 102 | Counted from `features/app_state/mod.rs`; down from 585, still transitional. | +| `NyaTermApp` fields | 55 | Counted from `features/app_state/mod.rs`; down from 585, still transitional. | | `impl NyaTermApp` blocks | 238 | Spread across 233 files under `crates/nyaterm-desktop/src`. | | `#[path = "..."]` declarations in desktop | 0 | Cleared. Every directory is a real module; the boundary script fails on any new occurrence. | | `use super::*` imports in desktop | 0 | Cleared in production and test modules; guarded crate-wide. | @@ -331,11 +331,17 @@ these as staged extraction candidates, not as formatting-only refactor targets. retains session metadata/host queries, status messages, focus and redraw coordination. This state remains transient and changes no session or settings persistence format. -- `ShellFeatureState` is the new owner for cross-view shell interaction state, - starting with the bottom-panel mode, quick-command/send-command heights and - resize lifecycle. Render helpers stay on views, and `NyaTermApp` still owns - settings persistence after a drag completes. This creates the boundary for a - later cohesive shell-chrome cut without introducing a mirror. +- `ShellFeatureState` now owns the complete cross-view window interaction + cluster. Its `viewport`, `navigation`, `panels`, `chrome` and `workspace` + children contain geometry bookkeeping, settings navigation/window state, + side-panel stacks and resize lifecycles, title/tab menus and per-tab pane + ownership respectively; the existing `bottom_panel` child still owns its + mode, heights and drag state. Forty-seven top-level app fields became these + five focused children, so `NyaTermApp` dropped from 102 fields to 55 without + creating a writable mirror. Pure viewport, panel resize/stack resize, chrome + menu and workspace ownership transitions execute on the child states. + Render helpers remain on views, while `NyaTermApp` retains settings + persistence, GPUI notification, terminal coordination and event routing. - The Entity Store projection layer is gone entirely, in two steps. First, the six domain stores (`Ai`, `CloudSync`, `Connections`, `RemoteOps`, @@ -1414,7 +1420,7 @@ honest remaining list. compiler-confirmed final pass also removed `features/prelude.rs`, so modules cannot regain the same implicit dependency surface through a shared import bucket. -3. Largely done. `NyaTermApp` is down from 585 fields to 102, across seventeen +3. Largely done. `NyaTermApp` is down from 585 fields to 55, across seventeen feature-state structs. The latest cohesive cuts moved sixteen terminal command-assistance and credential-prompt fields into `TerminalFeatureState::assist`, then seventeen transient settings fields @@ -1424,9 +1430,11 @@ honest remaining list. conflict and GitHub device-flow state, recording and SSH-tunnel runtime resources with their job/UI lifecycle state, then the complete live session runtime with nested session-start, prompt and dialog ownership, then terminal - presentation runtime followed by sync-input, screen-lock and bottom-panel - interaction lifecycles. What is left is a long tail, and much of it is - genuinely app-level (stores, runtime, services, persisted collections). + presentation runtime followed by sync-input and screen-lock interaction + lifecycles, and finally forty-seven window interaction fields under the + shell's viewport, navigation, panel, chrome and workspace children. What is + left is a long tail, and much of it is genuinely app-level (stores, runtime, + services, persisted collections). Group by cohesion where a cluster exists; do not force the count down for its own sake. Method ownership is now moving too, which is what grouping the fields alone @@ -1434,7 +1442,7 @@ honest remaining list. it belongs on that state, and the `NyaTermApp` method becomes a forwarder that owns `cx.notify()`. That is enforced by the type system rather than by convention — a handler taking `&mut TransferBrowserState` cannot reach the - session list no matter what a later edit tries. Eighty-four methods or + session list no matter what a later edit tries. More than one hundred methods or self-contained transitions have moved this way across transfers, security, the shell, sync input, the send command bar, AI, quick commands, cloud sync, recording, session starts and terminal paste review; @@ -1445,7 +1453,9 @@ honest remaining list. of sync, closing a pending session start cannot update its maps without also applying the active pending/failed fallback rules, and paste editing cannot mutate its UTF-8 cursor without also clearing stale selection/IME state. - Those are the kinds of signals to look for. + Shell viewport timing, panel drags, mutually-exclusive tab menus and pane + ownership rebuilding now have the same property. Those are the kinds of + signals to look for. Two caveats worth keeping. Render helpers stay on the view even when they read one state — moving element construction onto a data struct trades one coupling for a worse one. And a method that reads a state plus `self.tr(...)` diff --git a/scripts/check-architecture-boundaries.sh b/scripts/check-architecture-boundaries.sh index fc1636cc1..4d55e3bd5 100755 --- a/scripts/check-architecture-boundaries.sh +++ b/scripts/check-architecture-boundaries.sh @@ -128,6 +128,20 @@ check_no_matches \ '^[[:space:]]*pub\(in crate::features\)[[:space:]]+(bottom_panel|quick_cmd_height|serial_send_height|bottom_panel_resize)[[:space:]]*:' \ crates/nyaterm-desktop/src/features/app_state/mod.rs +check_no_matches \ + "window shell state must stay grouped under ShellFeatureState" \ + '^[[:space:]]*pub\(in crate::features\)[[:space:]]+(last_viewport_size|wallpaper_tile_dimensions|last_viewport_change_at|title_drag_active_until|selected_nav|main_mode|settings_active_tab|settings_expanded_groups|settings_draft_snapshot|settings_window|settings_window_open_pending|settings_previous_left_collapsed|settings_previous_right_collapsed|active_left_panel|active_right_panel|left_open_panels|right_open_panels|panel_stack_sizes|panel_multi_open|right_focus|left_sidebar_collapsed|right_inspector_collapsed|mobile_left_open|mobile_right_open|left_panel_width|right_panel_width|panel_resize|panel_stack_resize|activity_bar_layout|activity_bar_context_menu|title_menu_open|title_menu_submenu|header_status|open_tabs_menu_open|new_session_menu_open|new_session_all_sessions_open|new_session_group_menu_path|session_tab_strip_scroll|session_tab_scroll_into_view_pending|last_connect_failure_name|last_connect_failure_error|workspace_split|workspace_split_resize|session_pane_roots|session_tab_owner|focused_terminal_window_leaf_id|workspace_pane_layout_restored)[[:space:]]*:' \ + crates/nyaterm-desktop/src/features/app_state/mod.rs + +check_no_matches \ + "panel resize transitions must stay on ShellPanelState" \ + 'PanelResizeState' \ + crates/nyaterm-desktop/src/features/shell/panel_resize_runtime.rs +check_no_matches \ + "panel stack resize transitions must stay on ShellPanelState" \ + 'PanelStackResizeState' \ + crates/nyaterm-desktop/src/features/shell/panel_stack_runtime.rs + check_no_matches \ "screen lock fields must stay grouped under SecurityFeatureState" \ '^[[:space:]]*pub\(in crate::features\)[[:space:]]+(lock_focus|lock_password_draft|lock_status|is_locked|last_user_activity_at)[[:space:]]*:' \