From bbf843dde6dcdcfdb95407e6a3d8f1f1768d8308 Mon Sep 17 00:00:00 2001 From: TomZz Date: Sun, 14 Jun 2026 19:29:37 +0800 Subject: [PATCH] feat: settings dialog using Settings component #19 --- locales/en.yml | 14 +- locales/zh-CN.yml | 15 +- src/app/dialogs.rs | 927 +++++++++++++++++++++++---------------------- src/app/mod.rs | 3 + src/app/startup.rs | 5 + src/app/theme.rs | 94 +---- src/app/ui.rs | 2 +- src/session/mod.rs | 2 +- 8 files changed, 515 insertions(+), 547 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 373219b..c7acae4 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -67,7 +67,6 @@ terminal_font_size: "Terminal Font Size" system_default: "System Default" ui_font_family: "UI Font" terminal_font_family: "Terminal Font" -theme_management_hint: "Theme mode and light/dark theme families are managed from the top-bar theme button." monitoring_position: "Monitoring Position" position_bottom: "Bottom" position_sidebar: "Sidebar" @@ -95,7 +94,7 @@ n_folders: "%{folders} folders" n_files_and_folders: "%{files} files and %{folders} folders" reset: "Reset" reset_layout: "Reset Default Layout" -reset_layout_success: "Layout reset. Window size takes effect on restart." +reset_layout_hint: "Reset panel layout proportion. Window size will take effect on next startup." transfers: "Transfers" no_transfers_yet: "No transfers yet" @@ -109,9 +108,18 @@ session: "Session" software_builtin: "Built-in" retry: "Retry" right_click_copy_paste: "Right-click Copy/Paste" -copy_paste_hint: "Tip: Regardless of this switch, you can always use %{key} + C/V to copy/paste." +copy_paste_hint: "Regardless of this switch, you can always use %{key} + C/V to copy/paste." edit_file: "Edit File" edit_file_tooltip: "Download to temporary directory, open in default editor, and auto upload on save." auto_saved_and_uploaded: "Auto-saved & uploaded: %{base}" scroll: "Scroll ↓" clone: "Clone" +settings_general: "General" +settings_key_bindings: "Key Bindings" +settings_help: "Help" +settings_about: "About" +settings_group_appearance: "Appearance" +settings_group_font: "Font" +settings_group_other: "Other" +theme: "Theme" +theme_mode: "Theme Mode" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 79c1833..0eb4623 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -69,7 +69,6 @@ terminal_font_size: "终端字体大小" system_default: "系统默认" ui_font_family: "界面字体" terminal_font_family: "终端字体" -theme_management_hint: "深色/浅色模式以及主题配色由顶部栏的样式按钮统一管理。" monitoring_position: "系统监控显示位置" position_bottom: "底部" position_sidebar: "侧边栏" @@ -97,7 +96,7 @@ n_folders: "%{folders} 个文件夹" n_files_and_folders: "%{files} 个文件及 %{folders} 个文件夹" reset: "重置" reset_layout: "恢复默认布局" -reset_layout_success: "布局占比已恢复默认。窗口大小将在下次启动时生效。" +reset_layout_hint: "重置面板布局占比,窗口大小将在下次启动时生效" transfers: "传输记录" no_transfers_yet: "暂无传输记录" @@ -111,9 +110,19 @@ session: "会话" software_builtin: "软件内置" retry: "重试" right_click_copy_paste: "右键复制/粘贴" -copy_paste_hint: "提示:不管是否开启此开关,都可以使用 %{key} + C/V 快捷键进行复制和粘贴。" +copy_paste_hint: "不管是否开启此开关,都可以使用 %{key} + C/V 快捷键进行复制和粘贴。" edit_file: "编辑文件" edit_file_tooltip: "下载到临时目录,使用默认编辑器打开,并在保存后自动上传。" auto_saved_and_uploaded: "已自动保存并上传: %{base}" scroll: "滑动查看 ↓" clone: "克隆" +about_feedback_hint: "如果遇到问题或者有什么建议,请前往我们的 GitHub 项目中反馈。" +settings_general: "常规" +settings_key_bindings: "快捷键" +settings_help: "帮助" +settings_about: "关于" +settings_group_appearance: "外观" +settings_group_font: "字体" +settings_group_other: "其它" +theme: "主题" +theme_mode: "主题模式" diff --git a/src/app/dialogs.rs b/src/app/dialogs.rs index 295953d..b8713f1 100644 --- a/src/app/dialogs.rs +++ b/src/app/dialogs.rs @@ -867,465 +867,498 @@ impl Ashell { window.open_dialog(cx, move |dialog: Dialog, _window, _| { dialog .title(t!("settings").to_string()) - .w(px(560.)) + .w(px(840.)) + .h(px(560.)) .content({ let view = view.clone(); - move |content, window, cx| { + move |content, _window, _cx| { + use gpui_component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; + use gpui::IntoElement; + let version = env!("CARGO_PKG_VERSION"); + let view_clone_for_general = view.clone(); + content.child( - v_flex() - .gap_3() - .child( - h_flex() - .items_center() - .gap_3() - .child( - div() - .w(px(240.)) - .child(t!("ui_font_size").to_string()), - ) - .child( - Button::new("ui-font-size-down") - .label("-") - .on_click(window.listener_for( - &view, - |this, _, _, cx| { - this.change_ui_font_size(-1.0, cx) - }, - )), - ) - .child(div().min_w(px(64.)).text_center().child(format!( - "{:.0}px", - view.read(cx).ui_font_size - ))) - .child( - Button::new("ui-font-size-up") - .label("+") - .on_click(window.listener_for( - &view, - |this, _, _, cx| { - this.change_ui_font_size(1.0, cx) - }, - )), - ), - ) - .child( - h_flex() - .items_center() - .gap_3() - .child( - div() - .w(px(240.)) - .child(t!("terminal_font_size").to_string()), - ) - .child( - Button::new("font-size-down") - .label("-") - .on_click(window.listener_for( - &view, - |this, _, _, cx| { - this.change_terminal_font_size(-1.0, cx) - }, - )), - ) - .child(div().min_w(px(64.)).text_center().child(format!( - "{:.0}px", - view.read(cx).terminal_font_size - ))) - .child( - Button::new("font-size-up") - .label("+") - .on_click(window.listener_for( - &view, - |this, _, _, cx| { - this.change_terminal_font_size(1.0, cx) - }, - )), - ), - ) - .child( - h_flex() - .items_center() - .gap_3() - .child( - div() - .w(px(240.)) - .child(t!("ui_font_family").to_string()), - ) - .child( - Button::new("ui-font-dropdown") - .small() - .icon(IconName::ChevronsUpDown) - .label({ - let current = view - .read(cx) - .ui_font_family - .to_string(); - let names = cx.text_system().all_font_names(); - if current == *".SystemUIFont" - || current.is_empty() - || !names.contains(¤t) - { - t!("system_default").to_string() - } else if current == "Maple Mono NF CN" { - format!("Maple Mono NF CN ({})", t!("software_builtin")) - } else { - current - } - }) - .dropdown_menu_with_anchor( - Anchor::BottomRight, - { - let view = view.clone(); - move |mut menu, window, cx| { - let current = view - .read(cx) - .ui_font_family - .to_string(); - let mut names = - cx.text_system().all_font_names(); - menu = menu.min_w(200.).max_h( - px(320.), - ).scrollable(true); - // "System Default" entry - menu = menu.item( - PopupMenuItem::new( - t!( - "system_default" - ) - .to_string(), - ) - .checked( - current == *".SystemUIFont" - || current - .is_empty(), - ) - .on_click(window - .listener_for( - &view, - move |this, _, window, - cx| { - this.change_ui_font_family( - ".SystemUIFont", - window, - cx, - ); - }, - )), - ); - - let maple_font = "Maple Mono NF CN".to_string(); - if names.contains(&maple_font) { - names.retain(|n| n != &maple_font); - menu = menu.item( - PopupMenuItem::new(format!("{} ({})", maple_font, t!("software_builtin"))) - .checked(current == maple_font) - .on_click(window.listener_for(&view, move |this, _, window, cx| { - this.change_ui_font_family("Maple Mono NF CN", window, cx); - })) - ).separator(); + Settings::new("settings") + .sidebar_width(px(180.)) + .page( + SettingPage::new(t!("settings_general").to_string()) + .icon(IconName::Settings) + .default_open(true) + .group( + SettingGroup::new() + .title(t!("settings_group_appearance").to_string()) + .item( + SettingItem::new( + t!("theme_mode").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, _window, cx| { + let (follow_system, is_dark_mode) = { + let state = view.read(cx); + (state.follow_system_theme, state.theme_mode.is_dark()) + }; + Button::new("theme-mode-dropdown") + .small() + .icon(if follow_system { IconName::Sun } else if is_dark_mode { IconName::Moon } else { IconName::Sun }) + .label(if follow_system { t!("follow_system").to_string() } else if is_dark_mode { t!("use_dark_mode").to_string() } else { t!("use_light_mode").to_string() }) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + let (follow_system, is_dark_mode) = { + let state = view.read(cx); + (state.follow_system_theme, state.theme_mode.is_dark()) + }; + menu = menu.min_w(160.) + .item( + PopupMenuItem::new(t!("follow_system").to_string()) + .checked(follow_system) + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.set_follow_system_theme(true, window, cx) + })) + ) + .item( + PopupMenuItem::new(t!("use_light_mode").to_string()) + .checked(!follow_system && !is_dark_mode) + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.switch_theme_mode(crate::app::ThemeMode::Light, window, cx) + })) + ) + .item( + PopupMenuItem::new(t!("use_dark_mode").to_string()) + .checked(!follow_system && is_dark_mode) + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.switch_theme_mode(crate::app::ThemeMode::Dark, window, cx) + })) + ); + menu + } + }) + .into_any_element() } - - for name in names { - let checked = - name == current; - menu = menu.item( - PopupMenuItem::new( - name.clone(), - ) - .checked(checked) - .on_click(window - .listener_for( - &view, - move |this, _, - window, cx| { - this - .change_ui_font_family( - &name, - window, - cx, - ); - }, - )), - ); + }) + ) + ) + .item( + SettingItem::new( + t!("light_theme").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, _window, cx| { + let current_theme = view.read(cx).light_theme_name.to_string(); + Button::new("light-theme-dropdown") + .small() + .icon(IconName::Sun) + .label(current_theme.clone()) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + let current_theme = view.read(cx).light_theme_name.to_string(); + let themes = gpui_component::ThemeRegistry::global(cx).sorted_themes(); + let light_themes: Vec<_> = themes.into_iter().filter(|t| !t.mode.is_dark()).map(|t| t.name.clone()).collect(); + menu = menu.min_w(160.).max_h(px(320.)).scrollable(true); + for theme_name in light_themes { + let checked = theme_name == current_theme; + menu = menu.item( + PopupMenuItem::new(theme_name.clone()) + .checked(checked) + .on_click(window.listener_for(&view, move |this, _, window, cx| { + this.apply_theme(theme_name.clone(), window, cx) + })) + ); + } + menu + } + }) + .into_any_element() } - menu - } - }, - ), - ), - ) - .child( - h_flex() - .items_center() - .gap_3() - .child( - div() - .w(px(240.)) - .child(t!("terminal_font_family").to_string()), - ) - .child( - Button::new("terminal-font-dropdown") - .small() - .icon(IconName::ChevronsUpDown) - .label({ - let current = view.read(cx).terminal_font_family.to_string(); - if current == "Maple Mono NF CN" { - format!("Maple Mono NF CN ({})", t!("software_builtin")) - } else { - current - } - }) - .dropdown_menu_with_anchor( - Anchor::BottomRight, - { - let view = view.clone(); - move |mut menu, window, cx| { - let current = view - .read(cx) - .terminal_font_family - .to_string(); - let mut names = - cx.text_system().all_font_names(); - menu = menu.min_w(200.).max_h( - px(320.), - ).scrollable(true); - - let maple_font = "Maple Mono NF CN".to_string(); - if names.contains(&maple_font) { - names.retain(|n| n != &maple_font); - menu = menu.item( - PopupMenuItem::new(format!("{} ({})", maple_font, t!("software_builtin"))) - .checked(current == maple_font) - .on_click(window.listener_for(&view, move |this, _, _window, cx| { - this.change_terminal_font_family("Maple Mono NF CN", cx); - })) - ).separator(); + }) + ) + ) + .item( + SettingItem::new( + t!("dark_theme").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, _window, cx| { + let current_theme = view.read(cx).dark_theme_name.to_string(); + Button::new("dark-theme-dropdown") + .small() + .icon(IconName::Moon) + .label(current_theme.clone()) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + let current_theme = view.read(cx).dark_theme_name.to_string(); + let themes = gpui_component::ThemeRegistry::global(cx).sorted_themes(); + let dark_themes: Vec<_> = themes.into_iter().filter(|t| t.mode.is_dark()).map(|t| t.name.clone()).collect(); + menu = menu.min_w(160.).max_h(px(320.)).scrollable(true); + for theme_name in dark_themes { + let checked = theme_name == current_theme; + menu = menu.item( + PopupMenuItem::new(theme_name.clone()) + .checked(checked) + .on_click(window.listener_for(&view, move |this, _, window, cx| { + this.apply_theme(theme_name.clone(), window, cx) + })) + ); + } + menu + } + }) + .into_any_element() } - - for name in names { - let checked = - name == current; - menu = menu.item( - PopupMenuItem::new( - name.clone(), - ) - .checked(checked) - .on_click(window - .listener_for( - &view, - move |this, _, - _window, cx| { - this - .change_terminal_font_family( - &name, - cx, - ); - }, - )), - ); - } - menu - } - }, - ), - ), - ) - .child( - v_flex() - .gap_1() - .child( - h_flex() - .items_center() - .gap_3() - .child(div().w(px(240.)).child(t!("right_click_copy_paste").to_string())) - .child( - Switch::new("right-click-copy-paste") - .checked(view.read(cx).config.right_click_copy_paste()) - .on_click(window.listener_for( - &view, - |this, checked, _, cx| { - this.config.set_right_click_copy_paste(*checked); - let _ = this.config.save(); - cx.notify(); - }, - )), + }) + ) ) ) - .child( - div() - .text_size(rems(0.85)) - .text_color(cx.theme().muted_foreground) - .child(t!("copy_paste_hint", key = if cfg!(target_os = "macos") { "Command" } else { "Ctrl" }).to_string()) + .group( + SettingGroup::new() + .title(t!("settings_group_font").to_string()) + .item( + SettingItem::new( + t!("ui_font_size").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, window, cx| { + h_flex() + .items_center() + .gap_3() + .child(Button::new("ui-font-size-down").small().label("-").on_click(window.listener_for(&view, |this, _, _, cx| this.change_ui_font_size(-1.0, cx)))) + .child(div().min_w(px(64.)).text_center().child(format!("{:.0}px", view.read(cx).ui_font_size))) + .child(Button::new("ui-font-size-up").small().label("+").on_click(window.listener_for(&view, |this, _, _, cx| this.change_ui_font_size(1.0, cx)))) + .into_any_element() + } + }) + ) + ) + .item( + SettingItem::new( + t!("terminal_font_size").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, window, cx| { + h_flex() + .items_center() + .gap_3() + .child(Button::new("terminal-font-size-down").small().label("-").on_click(window.listener_for(&view, |this, _, _, cx| this.change_terminal_font_size(-1.0, cx)))) + .child(div().min_w(px(64.)).text_center().child(format!("{:.0}px", view.read(cx).terminal_font_size))) + .child(Button::new("terminal-font-size-up").small().label("+").on_click(window.listener_for(&view, |this, _, _, cx| this.change_terminal_font_size(1.0, cx)))) + .into_any_element() + } + }) + ) + ) + .item( + SettingItem::new( + t!("ui_font_family").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, _window, cx| { + Button::new("ui-font-dropdown") + .small() + .icon(IconName::ChevronsUpDown) + .label({ + let current = view.read(cx).ui_font_family.to_string(); + let names = cx.text_system().all_font_names(); + if current == *".SystemUIFont" || current.is_empty() || !names.contains(¤t) { + t!("system_default").to_string() + } else if current == "Maple Mono NF CN" { + format!("Maple Mono NF CN ({})", t!("software_builtin")) + } else { + current + } + }) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + let current = view.read(cx).ui_font_family.to_string(); + let mut names = cx.text_system().all_font_names(); + menu = menu.min_w(200.).max_h(px(320.)).scrollable(true); + menu = menu.item( + PopupMenuItem::new(t!("system_default").to_string()) + .checked(current == *".SystemUIFont" || current.is_empty()) + .on_click(window.listener_for(&view, move |this, _, window, cx| { + this.change_ui_font_family(".SystemUIFont", window, cx); + })) + ); + let maple_font = "Maple Mono NF CN".to_string(); + if names.contains(&maple_font) { + names.retain(|n| n != &maple_font); + menu = menu.item( + PopupMenuItem::new(format!("{} ({})", maple_font, t!("software_builtin"))) + .checked(current == maple_font) + .on_click(window.listener_for(&view, move |this, _, window, cx| { + this.change_ui_font_family("Maple Mono NF CN", window, cx); + })) + ).separator(); + } + for name in names { + let checked = name == current; + menu = menu.item( + PopupMenuItem::new(name.clone()) + .checked(checked) + .on_click(window.listener_for(&view, move |this, _, window, cx| { + this.change_ui_font_family(&name, window, cx); + })) + ); + } + menu + } + }) + .into_any_element() + } + }) + ) + ) + .item( + SettingItem::new( + t!("terminal_font_family").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, _window, cx| { + Button::new("terminal-font-dropdown") + .small() + .icon(IconName::ChevronsUpDown) + .label({ + let current = view.read(cx).terminal_font_family.to_string(); + if current == "Maple Mono NF CN" { + format!("Maple Mono NF CN ({})", t!("software_builtin")) + } else { + current + } + }) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + let current = view.read(cx).terminal_font_family.to_string(); + let mut names = cx.text_system().all_font_names(); + menu = menu.min_w(200.).max_h(px(320.)).scrollable(true); + let maple_font = "Maple Mono NF CN".to_string(); + if names.contains(&maple_font) { + names.retain(|n| n != &maple_font); + menu = menu.item( + PopupMenuItem::new(format!("{} ({})", maple_font, t!("software_builtin"))) + .checked(current == maple_font) + .on_click(window.listener_for(&view, move |this, _, _window, cx| { + this.change_terminal_font_family("Maple Mono NF CN", cx); + })) + ).separator(); + } + for name in names { + let checked = name == current; + menu = menu.item( + PopupMenuItem::new(name.clone()) + .checked(checked) + .on_click(window.listener_for(&view, move |this, _, _window, cx| { + this.change_terminal_font_family(&name, cx); + })) + ); + } + menu + } + }) + .into_any_element() + } + }) + ) + ) + ) + .group( + SettingGroup::new() + .title(t!("settings_group_other").to_string()) + .item( + SettingItem::new( + t!("right_click_copy_paste").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, window, cx| { + Switch::new("right-click-copy-paste") + .small() + .checked(view.read(cx).config.right_click_copy_paste()) + .on_click(window.listener_for(&view, |this, checked, _, cx| { + this.config.set_right_click_copy_paste(*checked); + let _ = this.config.save(); + cx.notify(); + })) + .into_any_element() + } + }) + ).description(t!("copy_paste_hint", key = if cfg!(target_os = "macos") { "Command" } else { "Ctrl" }).to_string()) + ) + .item( + SettingItem::new( + t!("monitoring_position").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, _window, cx| { + Button::new("monitoring-position-dropdown") + .small() + .icon(IconName::PanelLeftOpen) + .label({ + let pos = view.read(cx).config.monitoring_position().to_string(); + if pos == "Sidebar" { + t!("position_sidebar").to_string() + } else if pos == "Hidden" { + t!("position_hidden").to_string() + } else { + t!("position_bottom").to_string() + } + }) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + let pos = view.read(cx).config.monitoring_position().to_string(); + menu = menu.min_w(160.) + .item( + PopupMenuItem::new(t!("position_bottom").to_string()) + .checked(pos == "Bottom") + .on_click(window.listener_for(&view, |this, _, _window, cx| { + this.config.set_monitoring_position("Bottom"); + let _ = this.config.save(); + cx.notify(); + })) + ) + .item( + PopupMenuItem::new(t!("position_sidebar").to_string()) + .checked(pos == "Sidebar") + .on_click(window.listener_for(&view, |this, _, _window, cx| { + this.config.set_monitoring_position("Sidebar"); + let _ = this.config.save(); + cx.notify(); + })) + ) + .item( + PopupMenuItem::new(t!("position_hidden").to_string()) + .checked(pos == "Hidden") + .on_click(window.listener_for(&view, |this, _, _window, cx| { + this.config.set_monitoring_position("Hidden"); + let _ = this.config.save(); + cx.notify(); + })) + ); + menu + } + }) + .into_any_element() + } + }) + ) + ) + .item( + SettingItem::new( + t!("language").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, _window, cx| { + Button::new("language-dropdown") + .small() + .icon(IconName::Globe) + .label({ + let current_locale = view.read(cx).config.locale().to_string(); + if current_locale == "en" { + t!("english").to_string() + } else if current_locale == "zh-CN" { + t!("chinese").to_string() + } else { + t!("follow_system").to_string() + } + }) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + let current_locale = view.read(cx).config.locale().to_string(); + menu = menu.min_w(160.) + .item( + PopupMenuItem::new(t!("follow_system").to_string()) + .checked(current_locale == "system") + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.set_display_language("system", window, cx) + })) + ) + .separator() + .item( + PopupMenuItem::new(t!("english").to_string()) + .checked(current_locale == "en") + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.set_display_language("en", window, cx) + })) + ) + .item( + PopupMenuItem::new(t!("chinese").to_string()) + .checked(current_locale == "zh-CN") + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.set_display_language("zh-CN", window, cx) + })) + ); + menu + } + }) + .into_any_element() + } + }) + ) + ) + .item( + SettingItem::new( + t!("reset_layout").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, window, _cx| { + Button::new("reset-layout") + .small() + .label(t!("reset").to_string()) + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.reset_layout(window, cx); + })) + .into_any_element() + } + }) + ).description(t!("reset_layout_hint").to_string()) + ) ) ) - .child( - h_flex() - .items_center() - .gap_3() - .child(div().w(px(240.)).child(t!("monitoring_position").to_string())) - .child( - Button::new("monitoring-position-dropdown") - .small() - .icon(IconName::PanelLeftOpen) - .label({ - let pos = view.read(cx).config.monitoring_position().to_string(); - if pos == "Sidebar" { - t!("position_sidebar").to_string() - } else if pos == "Hidden" { - t!("position_hidden").to_string() - } else { - t!("position_bottom").to_string() - } - }) - .dropdown_menu_with_anchor(Anchor::BottomRight, { - let view = view.clone(); - move |mut menu, window, cx| { - let pos = view.read(cx).config.monitoring_position().to_string(); - menu = menu - .min_w(160.) - .item( - PopupMenuItem::new(t!("position_bottom").to_string()) - .checked(pos == "Bottom") - .on_click(window.listener_for( - &view, - |this, _, _window, cx| { - this.config.set_monitoring_position("Bottom"); - let _ = this.config.save(); - cx.notify(); - }, - )), - ) - .item( - PopupMenuItem::new(t!("position_sidebar").to_string()) - .checked(pos == "Sidebar") - .on_click(window.listener_for( - &view, - |this, _, _window, cx| { - this.config.set_monitoring_position("Sidebar"); - let _ = this.config.save(); - cx.notify(); - }, - )), - ) - .item( - PopupMenuItem::new(t!("position_hidden").to_string()) - .checked(pos == "Hidden") - .on_click(window.listener_for( - &view, - |this, _, _window, cx| { - this.config.set_monitoring_position("Hidden"); - let _ = this.config.save(); - cx.notify(); - }, - )), - ); - menu - } - }), + .page( + SettingPage::new(t!("settings_key_bindings").to_string()) + .icon(IconName::SquareTerminal) + ) + .page( + SettingPage::new(t!("settings_help").to_string()) + .icon(IconName::BookOpen) + ) + .page( + SettingPage::new(t!("settings_about").to_string()) + .icon(IconName::Info) + .group( + SettingGroup::new() + .item(SettingItem::render(move |_, _window, cx| { + v_flex() + .gap_2() + .items_center() + .child(div().text_size(rems(1.5)).font_weight(FontWeight::BOLD).child("Ashell")) + .child(div().text_size(rems(0.9)).child(format!("Version {}", version))) + .child( + div() + .text_size(rems(0.9)) + .text_color(cx.theme().muted_foreground) + .child("A GPUI Component based SSH and local terminal client"), + ) + .child( + div() + .text_size(rems(0.9)) + .text_color(cx.theme().muted_foreground) + .child(t!("about_feedback_hint")), + ) + .child( + Button::new("github-link") + .label("https://github.com/rust-kotlin/ashell") + .ghost() + .on_click(|_, _window, _cx| { + let _ = open::that("https://github.com/rust-kotlin/ashell"); + }), + ) + })) ) ) - .child( - h_flex() - .items_center() - .gap_3() - .child(div().w(px(240.)).child(t!("language").to_string())) - .child( - Button::new("language-dropdown") - .small() - .icon(IconName::Globe) - .label({ - let current_locale = - view.read(cx).config.locale().to_string(); - if current_locale == "en" { - t!("english").to_string() - } else if current_locale == "zh-CN" { - t!("chinese").to_string() - } else { - t!("follow_system").to_string() - } - }) - .dropdown_menu_with_anchor(Anchor::BottomRight, { - let view = view.clone(); - move |mut menu, window, cx| { - let current_locale = view - .read(cx) - .config - .locale() - .to_string(); - menu = menu - .min_w(160.) - .item( - PopupMenuItem::new( - t!("follow_system").to_string(), - ) - .checked(current_locale == "system") - .on_click(window.listener_for( - &view, - |this, _, window, cx| { - this.set_display_language( - "system", window, cx, - ) - }, - )), - ) - .separator() - .item( - PopupMenuItem::new( - t!("english").to_string(), - ) - .checked(current_locale == "en") - .on_click(window.listener_for( - &view, - |this, _, window, cx| { - this.set_display_language( - "en", window, cx, - ) - }, - )), - ) - .item( - PopupMenuItem::new( - t!("chinese").to_string(), - ) - .checked(current_locale == "zh-CN") - .on_click(window.listener_for( - &view, - |this, _, window, cx| { - this.set_display_language( - "zh-CN", window, cx, - ) - }, - )), - ); - menu - } - }), - ), - ) - .child( - h_flex() - .items_center() - .gap_3() - .child( - div().w(px(240.)).child(t!("reset_layout").to_string()), - ) - .child( - Button::new("reset-layout") - .label(t!("reset").to_string()) - .on_click(window.listener_for( - &view, - |this, _, window, cx| { - this.reset_layout(window, cx); - }, - )), - ), - ) - .child( - div() - .text_size(rems(1.0)) - .text_color(cx.theme().muted_foreground) - .child(t!("theme_management_hint")), - ), ) } }) diff --git a/src/app/mod.rs b/src/app/mod.rs index f848d73..c04370b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -217,6 +217,7 @@ pub(crate) struct Ashell { pub(crate) selector_selection: usize, pub(crate) workspace_panels: Entity, pub(crate) body_panels: Entity, + pub(crate) is_layout_reset: bool, pub(crate) terminal_scrollbars: HashMap, pub(crate) remote_files_scroll_handle: UniformListScrollHandle, pub(crate) disk_scroll_handle: gpui::ScrollHandle, @@ -398,6 +399,7 @@ impl Ashell { selector_selection: 0, workspace_panels, body_panels, + is_layout_reset: false, terminal_scrollbars: HashMap::new(), remote_files_scroll_handle: UniformListScrollHandle::new(), disk_scroll_handle: gpui::ScrollHandle::new(), @@ -813,3 +815,4 @@ impl Ashell { )) } } + diff --git a/src/app/startup.rs b/src/app/startup.rs index a817078..7ed3d43 100644 --- a/src/app/startup.rs +++ b/src/app/startup.rs @@ -221,7 +221,12 @@ pub(crate) fn open_main_window(cx: &mut App) { let workspace_panels_clone = view.read(cx).workspace_panels.clone(); let body_panels_clone = view.read(cx).body_panels.clone(); + let view_clone = view.clone(); window.on_window_should_close(cx, move |window: &mut gpui::Window, cx: &mut gpui::App| { + if view_clone.read(cx).is_layout_reset { + tracing::info!("[ui] layout was reset, skipping save layout state."); + return true; + } tracing::info!("[ui] main application window closed, saving layout state..."); let mut config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory()); let current_bounds = window.window_bounds(); diff --git a/src/app/theme.rs b/src/app/theme.rs index e189c86..873d4e3 100644 --- a/src/app/theme.rs +++ b/src/app/theme.rs @@ -1,11 +1,8 @@ use anyhow::{Context as _, Result}; -use gpui::{Anchor, App, Context, IntoElement, SharedString, Window, px}; +use gpui::{App, Context, SharedString, Window, px}; use gpui_component::{ - ActiveTheme as _, IconName, Sizable as _, Theme, ThemeMode, ThemeRegistry, - button::{Button, ButtonVariants as _}, - menu::{DropdownMenu as _, PopupMenuItem}, + ActiveTheme as _, Theme, ThemeMode, ThemeRegistry, }; -use rust_i18n::t; use crate::Ashell; @@ -147,92 +144,5 @@ impl Ashell { } } - pub(crate) fn theme_dropdown(&self, cx: &mut Context) -> impl IntoElement { - let view = cx.entity(); - let themes = ThemeRegistry::global(cx) - .sorted_themes() - .into_iter() - .cloned() - .collect::>(); - let light_themes = themes - .iter() - .filter(|theme| !theme.mode.is_dark()) - .map(|theme| theme.name.clone()) - .collect::>(); - let dark_themes = themes - .iter() - .filter(|theme| theme.mode.is_dark()) - .map(|theme| theme.name.clone()) - .collect::>(); - let follow_system = self.follow_system_theme; - let is_dark_mode = cx.theme().mode.is_dark(); - let light_theme_name = self.light_theme_name.clone(); - let dark_theme_name = self.dark_theme_name.clone(); - let icon = if follow_system { - IconName::Sun - } else if is_dark_mode { - IconName::Moon - } else { - IconName::Sun - }; - Button::new("theme-dropdown") - .ghost() - .small() - .icon(icon) - .dropdown_menu_with_anchor(Anchor::BottomRight, move |mut menu, window, _| { - menu = menu - .min_w(220.) - .item( - PopupMenuItem::new(t!("follow_system")) - .checked(follow_system) - .on_click(window.listener_for(&view, |this, _, window, cx| { - this.set_follow_system_theme(true, window, cx) - })), - ) - .item( - PopupMenuItem::new(t!("use_light_mode")) - .checked(!follow_system && !is_dark_mode) - .on_click(window.listener_for(&view, |this, _, window, cx| { - this.switch_theme_mode(ThemeMode::Light, window, cx) - })), - ) - .item( - PopupMenuItem::new(t!("use_dark_mode")) - .checked(!follow_system && is_dark_mode) - .on_click(window.listener_for(&view, |this, _, window, cx| { - this.switch_theme_mode(ThemeMode::Dark, window, cx) - })), - ) - .separator() - .label(t!("light_theme").to_string()); - - for theme_name in light_themes.clone() { - let checked = theme_name == light_theme_name; - menu = menu.item( - PopupMenuItem::new(theme_name.clone()) - .checked(checked) - .on_click(window.listener_for(&view, move |this, _, window, cx| { - this.apply_theme(theme_name.clone(), window, cx) - })), - ); - } - - menu = menu.separator(); - menu = menu.label(t!("dark_theme").to_string()); - - for theme_name in dark_themes.clone() { - let checked = theme_name == dark_theme_name; - menu = menu.item( - PopupMenuItem::new(theme_name.clone()) - .checked(checked) - .on_click(window.listener_for(&view, move |this, _, window, cx| { - this.apply_theme(theme_name.clone(), window, cx) - })), - ); - } - - menu - }) - } } diff --git a/src/app/ui.rs b/src/app/ui.rs index d8a2d48..2644dfd 100644 --- a/src/app/ui.rs +++ b/src/app/ui.rs @@ -1209,7 +1209,7 @@ impl Ashell { this.show_settings_dialog(window, cx) })), ) - .child(self.theme_dropdown(cx)), + ) .child( div() diff --git a/src/session/mod.rs b/src/session/mod.rs index 06d33d7..561a80e 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -285,10 +285,10 @@ impl Ashell { self.config.set_layout_state(None, None, None); let _ = self.config.save(); + self.is_layout_reset = true; self.workspace_panels = cx.new(|_| gpui_component::resizable::ResizableState::default()); self.body_panels = cx.new(|_| gpui_component::resizable::ResizableState::default()); - self.status = t!("reset_layout_success").into(); cx.notify(); }