From 6b5018f220cda2bc0b6f69be101b4f213a7ffea0 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:16:21 +0800 Subject: [PATCH] fix(theme): persist advanced selection and cursor text colors (#300) Synchronize advanced color picker changes with the editor draft through the existing input handler. Preview cursor text color and cover saving, applying, and reopening the selected colors. --- .../settings_pane/theme_advanced.rs | 17 +++ .../gpui_shell/settings_pane/theme_editor.rs | 10 +- .../settings_pane/theme_editor/view.rs | 15 ++- .../settings_pane/theme_studio_tests.rs | 108 ++++++++++++++++++ 4 files changed, 148 insertions(+), 2 deletions(-) diff --git a/nebula_app/src/gpui_shell/settings_pane/theme_advanced.rs b/nebula_app/src/gpui_shell/settings_pane/theme_advanced.rs index 914700f..118fbe0 100644 --- a/nebula_app/src/gpui_shell/settings_pane/theme_advanced.rs +++ b/nebula_app/src/gpui_shell/settings_pane/theme_advanced.rs @@ -136,6 +136,7 @@ impl ThemeAdvancedEditor { let ansi_pickers = (0..ANSI_COUNT) .map(|index| { new_color_picker( + ThemeAdvancedField::Ansi(index), Some(definition.terminal.palette.get(index).unwrap_or([0, 0, 0])), ansi_inputs[index].clone(), window, @@ -145,6 +146,7 @@ impl ThemeAdvancedEditor { }) .collect(); let selection_background_picker = new_color_picker( + ThemeAdvancedField::SelectionBackground, definition.terminal.selection_background, selection_background_input.clone(), window, @@ -152,6 +154,7 @@ impl ThemeAdvancedEditor { &mut picker_subscriptions, ); let selection_foreground_picker = new_color_picker( + ThemeAdvancedField::SelectionForeground, definition.terminal.selection_foreground, selection_foreground_input.clone(), window, @@ -159,6 +162,7 @@ impl ThemeAdvancedEditor { &mut picker_subscriptions, ); let cursor_text_picker = new_color_picker( + ThemeAdvancedField::CursorText, definition.terminal.cursor_text, cursor_text_input.clone(), window, @@ -211,6 +215,17 @@ impl ThemeAdvancedEditor { } } + #[cfg(test)] + pub(super) fn picker(&self, field: ThemeAdvancedField) -> Option<&Entity> { + match field { + ThemeAdvancedField::Ansi(index) => self.ansi_pickers.get(index), + ThemeAdvancedField::SelectionBackground => Some(&self.selection_background_picker), + ThemeAdvancedField::SelectionForeground => Some(&self.selection_foreground_picker), + ThemeAdvancedField::CursorText => Some(&self.cursor_text_picker), + _ => None, + } + } + /// Replace all control values after a template/reset/import operation. /// Invalid input errors belong to the old draft and therefore disappear. pub(super) fn sync_from_definition( @@ -557,6 +572,7 @@ impl ThemeAdvancedEditor { } fn new_color_picker( + field: ThemeAdvancedField, color: Option, input: Entity, window: &mut Window, @@ -590,6 +606,7 @@ fn new_color_picker( input.update(cx, |state, cx| { state.set_value(format_hex_rgb(rgb), window, cx); }); + this.on_theme_advanced_input(field, window, cx); }, ); subscriptions.push(subscription); diff --git a/nebula_app/src/gpui_shell/settings_pane/theme_editor.rs b/nebula_app/src/gpui_shell/settings_pane/theme_editor.rs index 23321b8..448f297 100644 --- a/nebula_app/src/gpui_shell/settings_pane/theme_editor.rs +++ b/nebula_app/src/gpui_shell/settings_pane/theme_editor.rs @@ -938,7 +938,7 @@ impl SettingsPane { cx.notify(); } - fn on_theme_advanced_input( + pub(super) fn on_theme_advanced_input( &mut self, field: ThemeAdvancedField, window: &mut Window, @@ -964,6 +964,14 @@ impl SettingsPane { cx.notify(); } + #[cfg(test)] + pub(super) fn theme_advanced_picker( + &self, + field: ThemeAdvancedField, + ) -> Option> { + self.theme_editor.as_ref()?.advanced_editor.as_ref()?.picker(field).cloned() + } + fn request_close_theme_editor(&mut self, window: &mut Window, cx: &mut Context) { if self.theme_editor.as_ref().is_some_and(|editor| editor.save_busy) { return; diff --git a/nebula_app/src/gpui_shell/settings_pane/theme_editor/view.rs b/nebula_app/src/gpui_shell/settings_pane/theme_editor/view.rs index 6ab62f3..d206ed3 100644 --- a/nebula_app/src/gpui_shell/settings_pane/theme_editor/view.rs +++ b/nebula_app/src/gpui_shell/settings_pane/theme_editor/view.rs @@ -21,6 +21,8 @@ impl SettingsPane { let foreground = theme_color(draft.terminal.foreground, 1.0); let accent = theme_color(resolved_ui.accent, 1.0); let cursor = theme_color(draft.terminal.cursor.unwrap_or(draft.terminal.foreground), 1.0); + let cursor_text = + theme_color(draft.terminal.cursor_text.unwrap_or(draft.terminal.background), 1.0); let selection_background = theme_color( draft .terminal @@ -233,7 +235,18 @@ impl SettingsPane { .gap(px(7.0)) .items_center() .child(div().text_color(accent).child("❯")) - .child(div().w(px(8.0)).h(px(font_size * line_height)).bg(cursor)), + .child( + div() + .debug_selector(|| "theme-editor-preview-cursor".to_owned()) + .w(px(font_size * 0.78)) + .h(px(font_size * line_height)) + .flex() + .items_center() + .justify_center() + .bg(cursor) + .text_color(cursor_text) + .child("A"), + ), ) .into_any_element(), }; diff --git a/nebula_app/src/gpui_shell/settings_pane/theme_studio_tests.rs b/nebula_app/src/gpui_shell/settings_pane/theme_studio_tests.rs index f4d331f..be86875 100644 --- a/nebula_app/src/gpui_shell/settings_pane/theme_studio_tests.rs +++ b/nebula_app/src/gpui_shell/settings_pane/theme_studio_tests.rs @@ -483,6 +483,28 @@ fn editor_template(pane: &Entity, cx: &mut VisualTestContext) -> T pane.read_with(cx, |pane, _| pane.theme_editor.as_ref().expect("theme editor state").template) } +fn emit_advanced_picker_color( + pane: &Entity, + field: super::theme_advanced::ThemeAdvancedField, + color: [u8; 3], + cx: &mut VisualTestContext, +) { + let picker = pane + .read_with(cx, |pane, _| pane.theme_advanced_picker(field).expect("advanced color picker")); + picker.update(cx, |_, cx| { + let [r, g, b] = color; + let hsla: gpui::Hsla = gpui::Rgba { + r: f32::from(r) / 255.0, + g: f32::from(g) / 255.0, + b: f32::from(b) / 255.0, + a: 1.0, + } + .into(); + cx.emit(gpui_component::color_picker::ColorPickerEvent::Change(Some(hsla))); + }); + draw(cx); +} + #[gpui::test] fn theme_editor_opens_from_the_picker_with_common_fields_and_collapsed_advanced( cx: &mut TestAppContext, @@ -728,6 +750,92 @@ fn theme_editor_save_and_apply_persists_copy_and_reloads_its_foreground(cx: &mut assert_eq!(palette_foreground(&mut window), applied.color("terminal", "foreground").unwrap()); } +#[gpui::test] +fn advanced_selection_and_cursor_text_picker_colors_preview_persist_apply_and_reopen( + cx: &mut TestAppContext, +) { + use super::theme_advanced::ThemeAdvancedField; + + let _fixture_guard = lock_theme_studio(); + let _settings_guard = SettingsBytesGuard::capture(); + std::fs::write(nebula_settings::settings_path(), TEST_SETTINGS) + .expect("write isolated advanced color settings"); + let mut theme_cleanup = CustomThemeCleanup::default(); + let (pane, mut window) = open_settings(cx); + let before_library_ids = custom_theme_ids(); + let saved_name = format!("Advanced colors native test {}", std::process::id()); + let selection_background = [0x1b, 0x2c, 0x3d]; + let selection_foreground = [0xe1, 0xd2, 0xc3]; + let cursor_text = [0x12, 0xeb, 0x77]; + + open_theme_editor(&mut window); + edit_input("theme-editor-name", &saved_name, &mut window); + click("theme-editor-advanced-toggle", &mut window); + assert!(window.debug_bounds("theme-editor-preview-cursor").is_some()); + + emit_advanced_picker_color( + &pane, + ThemeAdvancedField::SelectionBackground, + selection_background, + &mut window, + ); + emit_advanced_picker_color( + &pane, + ThemeAdvancedField::SelectionForeground, + selection_foreground, + &mut window, + ); + emit_advanced_picker_color(&pane, ThemeAdvancedField::CursorText, cursor_text, &mut window); + + let draft = editor_draft(&pane, &mut window); + assert_eq!(draft.terminal.selection_background, Some(selection_background)); + assert_eq!(draft.terminal.selection_foreground, Some(selection_foreground)); + assert_eq!(draft.terminal.cursor_text, Some(cursor_text)); + + click("theme-editor-save-apply", &mut window); + assert!(window.debug_bounds("theme-editor-dialog").is_none()); + + let after_documents = custom_theme_documents(); + for document in &after_documents { + if document.id().is_some_and(|id| !before_library_ids.iter().any(|before| before == id)) + && document.name().starts_with(&saved_name) + { + theme_cleanup.track(document); + } + } + let applied_id = RuntimeSettings::load().custom_theme.expect("advanced color theme is active"); + let applied = after_documents + .iter() + .find(|document| document.id() == Some(applied_id.as_str())) + .expect("advanced color theme remains in the library"); + let applied_definition = applied.definition().expect("saved advanced color definition"); + assert_eq!(applied_definition.terminal.selection_background, Some(selection_background)); + assert_eq!(applied_definition.terminal.selection_foreground, Some(selection_foreground)); + assert_eq!(applied_definition.terminal.cursor_text, Some(cursor_text)); + + window.read(|cx| { + let palette = &cx.global::().palette; + let bytes = |color: gpui::Rgba| { + [ + (color.r * 255.0).round() as u8, + (color.g * 255.0).round() as u8, + (color.b * 255.0).round() as u8, + ] + }; + assert_eq!(bytes(palette.selection), selection_background); + assert_eq!(palette.selection_foreground.map(bytes), Some(selection_foreground)); + assert_eq!(palette.cursor_text.map(bytes), Some(cursor_text)); + }); + + click("open-theme-picker", &mut window); + assert!(window.debug_bounds("appearance-picker-dialog").is_some()); + click("customize-theme", &mut window); + let reopened = editor_draft(&pane, &mut window); + assert_eq!(reopened.terminal.selection_background, Some(selection_background)); + assert_eq!(reopened.terminal.selection_foreground, Some(selection_foreground)); + assert_eq!(reopened.terminal.cursor_text, Some(cursor_text)); +} + #[gpui::test] fn invalid_color_stays_dirty_and_save_rejects_after_a_later_valid_edit(cx: &mut TestAppContext) { let _fixture_guard = lock_theme_studio();