From 510543bd47e1e47471d012aa0912eef519c78d5f Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Thu, 20 Nov 2025 07:42:33 -0500 Subject: [PATCH 1/7] feat: add custom appearance settings with theme customization - Introduce an Appearance tab in the settings window allowing users to select theme mode and customize colors. - Replace the old ConfiguredTheme with a new ThemeMode enum that includes a Custom option. - Add a CustomTheme struct to store hex color values and provide default theme colors. - Extend UIVisualSettings and UIState to hold theme_mode and custom_theme data. - Update theme initialization to apply custom colors when ThemeMode::Custom is selected, including a color parser. - Modify settings UI to include radio buttons for theme selection and text boxes for custom color inputs, with automatic save handling. --- src/gui/settings_window/appearance_view.rs | 104 +++++++++++++++++++++ src/gui/settings_window/general_view.rs | 25 +---- src/gui/settings_window/mod.rs | 6 ++ src/gui/ui.rs | 11 ++- src/gui/ui_theme.rs | 52 +++++++++-- src/lib.rs | 3 +- src/utils.rs | 35 ++++++- 7 files changed, 199 insertions(+), 37 deletions(-) create mode 100644 src/gui/settings_window/appearance_view.rs diff --git a/src/gui/settings_window/appearance_view.rs b/src/gui/settings_window/appearance_view.rs new file mode 100644 index 00000000..61d0394d --- /dev/null +++ b/src/gui/settings_window/appearance_view.rs @@ -0,0 +1,104 @@ +use druid::widget::{ + ControllerHost, CrossAxisAlignment, Flex, Label, RadioGroup, TextBox, +}; +use druid::{LensExt, Widget, WidgetExt}; + +use crate::gui::settings_window::rules_view; +use crate::gui::ui::{ + UISettings, UIState, UIVisualSettings, SAVE_UI_SETTINGS, +}; +use crate::utils::{CustomTheme, ThemeMode}; + +pub(crate) fn appearance_content() -> impl Widget { + const TEXT_SIZE: f64 = 13.0; + let save_command = SAVE_UI_SETTINGS.with(()); + + let theme_radio_group = ControllerHost::new( + RadioGroup::column(vec![ + ("Match system", ThemeMode::Auto), + ("Light", ThemeMode::Light), + ("Dark", ThemeMode::Dark), + ("Custom", ThemeMode::Custom), + ]), + rules_view::SubmitCommandOnDataChange { + command: save_command.clone(), + }, + ) + .lens( + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::theme_mode), + ); + + let theme_radio_row = Flex::row() + .with_child(Label::new("Theme").with_text_size(TEXT_SIZE)) + .with_flex_spacer(1.0) + .with_child(theme_radio_group); + + // Custom theme inputs + let window_bg_input = make_color_input("Window Background", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::window_background), + save_command.clone()); + + let text_color_input = make_color_input("Text Color", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::text_color), + save_command.clone()); + + let active_tab_bg_input = make_color_input("Active Tab Background", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::active_tab_background), + save_command.clone()); + + let active_tab_text_input = make_color_input("Active Tab Text", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::active_tab_text), + save_command.clone()); + + let inactive_tab_text_input = make_color_input("Inactive Tab Text", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::inactive_tab_text), + save_command.clone()); + + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(theme_radio_row) + .with_default_spacer() + .with_child(Label::new("Custom Theme Colors (Hex)").with_text_size(TEXT_SIZE)) + .with_spacer(5.0) + .with_child(window_bg_input) + .with_spacer(5.0) + .with_child(text_color_input) + .with_spacer(5.0) + .with_child(active_tab_bg_input) + .with_spacer(5.0) + .with_child(active_tab_text_input) + .with_spacer(5.0) + .with_child(inactive_tab_text_input) +} + +fn make_color_input(label: &str, lens: impl druid::Lens + 'static, save_command: druid::Command) -> impl Widget { + let input = ControllerHost::new( + TextBox::new(), + rules_view::SubmitCommandOnDataChange { + command: save_command, + }, + ) + .lens(lens); + + Flex::row() + .with_child(Label::new(label).with_text_size(13.0)) + .with_flex_spacer(1.0) + .with_child(input.fix_width(100.0)) +} diff --git a/src/gui/settings_window/general_view.rs b/src/gui/settings_window/general_view.rs index ddae2307..280664fb 100644 --- a/src/gui/settings_window/general_view.rs +++ b/src/gui/settings_window/general_view.rs @@ -9,33 +9,14 @@ use crate::gui::ui::{ UIBehavioralSettings, UISettings, UIState, UIVisualSettings, SAVE_BEHAVIORAL_SETTINGS, SAVE_UI_SETTINGS, }; -use crate::utils::ConfiguredTheme; + pub(crate) fn general_content() -> impl Widget { const TEXT_SIZE: f64 = 13.0; let save_command = SAVE_UI_SETTINGS.with(()); - let theme_radio_group = ControllerHost::new( - RadioGroup::column(vec![ - ("Match system", ConfiguredTheme::Auto), - ("Light", ConfiguredTheme::Light), - ("Dark", ConfiguredTheme::Dark), - ]), - rules_view::SubmitCommandOnDataChange { - command: save_command.clone(), - }, - ) - .lens( - UIState::ui_settings - .then(UISettings::visual_settings) - .then(UIVisualSettings::theme), - ); - let theme_radio_row = Flex::row() - .with_child(Label::new("Theme").with_text_size(TEXT_SIZE)) - .with_flex_spacer(1.0) - .with_child(theme_radio_group); let hotkeys_switch = ControllerHost::new( Switch::new(), @@ -66,8 +47,8 @@ pub(crate) fn general_content() -> impl Widget { let mut col = Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) - .with_child(theme_radio_row) - .with_default_spacer() + //.with_child(theme_radio_row) + //.with_default_spacer() .with_child(hotkeys_row) .with_default_spacer(); diff --git a/src/gui/settings_window/mod.rs b/src/gui/settings_window/mod.rs index d5328ad7..be06f9c0 100644 --- a/src/gui/settings_window/mod.rs +++ b/src/gui/settings_window/mod.rs @@ -12,6 +12,7 @@ use crate::gui::ui_theme; use crate::gui::ui_theme::{GeneralTheme, SettingsWindowTheme}; mod advanced_view; +mod appearance_view; mod general_view; mod rules_view; @@ -102,6 +103,10 @@ fn view_switcher(browsers_arc: Arc>) -> ViewSwitcher { settings_view_container("settings-tab-advanced", advanced_view::advanced_content()) } + SettingsTab::APPEARANCE => settings_view_container( + "settings-tab-appearance", + appearance_view::appearance_content(), + ), }, ) } @@ -137,6 +142,7 @@ fn sidebar_items() -> impl Widget { .cross_axis_alignment(CrossAxisAlignment::Fill) .with_child(tab_button("settings-tab-general", SettingsTab::GENERAL)) .with_child(tab_button("settings-tab-rules", SettingsTab::RULES)) + .with_child(tab_button("settings-tab-appearance", SettingsTab::APPEARANCE)) .with_child(tab_button("settings-tab-advanced", SettingsTab::ADVANCED)) .with_flex_spacer(1.0) .fix_width(SIDEBAR_ITEM_WIDTH) diff --git a/src/gui/ui.rs b/src/gui/ui.rs index 2b78b24d..850e6e1d 100644 --- a/src/gui/ui.rs +++ b/src/gui/ui.rs @@ -21,7 +21,9 @@ use crate::gui::main_window::{ use crate::gui::ui::SettingsTab::GENERAL; use crate::gui::{about_dialog, main_window, settings_window, ui_theme}; use crate::url_rule::UrlGlobMatcher; -use crate::utils::{BehavioralConfig, Config, ConfiguredTheme, ProfileAndOptions, UIConfig}; +use crate::utils::{ + BehavioralConfig, Config, CustomTheme, ProfileAndOptions, ThemeMode, UIConfig, +}; use crate::{CommonBrowserProfile, MessageToMain}; pub struct UI { @@ -71,7 +73,8 @@ impl UI { UIVisualSettings { show_hotkeys: ui_config.show_hotkeys, quit_on_lost_focus: ui_config.quit_on_lost_focus, - theme: ui_config.theme, + theme_mode: ui_config.theme_mode.clone(), + custom_theme: ui_config.custom_theme.clone(), } } @@ -238,7 +241,8 @@ pub struct UISettings { pub struct UIVisualSettings { pub show_hotkeys: bool, pub quit_on_lost_focus: bool, - pub theme: ConfiguredTheme, + pub theme_mode: ThemeMode, + pub custom_theme: CustomTheme, } #[derive(Clone, Debug, Data, Lens)] @@ -257,6 +261,7 @@ pub enum SettingsTab { GENERAL, RULES, ADVANCED, + APPEARANCE, } impl UISettings { diff --git a/src/gui/ui_theme.rs b/src/gui/ui_theme.rs index b6aedbde..225b3c11 100644 --- a/src/gui/ui_theme.rs +++ b/src/gui/ui_theme.rs @@ -1,14 +1,15 @@ use crate::gui::ui::UIState; -use crate::utils::ConfiguredTheme; +use crate::utils::{CustomTheme, ThemeMode}; use dark_light::Mode; use druid::{Color, Data, Env, Key}; use serde::{Deserialize, Serialize}; use tracing::warn; -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Data)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Data)] pub enum UITheme { Light, Dark, + Custom(CustomTheme), } pub fn initialize_theme(env: &mut Env, ui_state: &UIState) { @@ -17,10 +18,11 @@ pub fn initialize_theme(env: &mut Env, ui_state: &UIState) { } fn get_active_ui_theme(ui_state: &UIState) -> UITheme { - match ui_state.ui_settings.visual_settings.theme { - ConfiguredTheme::Auto => detect_system_theme(), - ConfiguredTheme::Light => UITheme::Light, - ConfiguredTheme::Dark => UITheme::Dark, + match ui_state.ui_settings.visual_settings.theme_mode { + ThemeMode::Auto => detect_system_theme(), + ThemeMode::Light => UITheme::Light, + ThemeMode::Dark => UITheme::Dark, + ThemeMode::Custom => UITheme::Custom(ui_state.ui_settings.visual_settings.custom_theme.clone()), } } @@ -155,6 +157,34 @@ fn get_theme(ui_theme: UITheme) -> Theme { let theme = match ui_theme { UITheme::Light => light_theme, UITheme::Dark => dark_theme, + UITheme::Custom(custom) => { + let mut theme = dark_theme; // Start with dark theme as base + + if let Ok(color) = parse_color(&custom.window_background) { + theme.druid_builtin.window_background_color = color.clone(); + theme.general.window_background_color = color.clone(); + theme.main.window_background_color = color.clone(); + } + + if let Ok(color) = parse_color(&custom.text_color) { + theme.druid_builtin.text_color = color.clone(); + theme.main.browser_label_color = color.clone(); + } + + if let Ok(color) = parse_color(&custom.active_tab_background) { + theme.settings.active_tab_background_color = color; + } + + if let Ok(color) = parse_color(&custom.active_tab_text) { + theme.settings.active_tab_text_color = color; + } + + if let Ok(color) = parse_color(&custom.inactive_tab_text) { + theme.settings.inactive_tab_text_color = color; + } + + theme + } }; return theme; @@ -404,3 +434,13 @@ struct Palette {} //.adding(UI_FONT, FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(15.0)) //.adding(UI_FONT_BOLD, FontDescriptor::new(FontFamily::SYSTEM_UI).with_weight(FontWeight::BOLD).with_size(15.0)) //.adding(UI_FONT_ITALIC, FontDescriptor::new(FontFamily::SYSTEM_UI).with_style(FontStyle::Italic).with_size(15.0)) + +fn parse_color(hex: &str) -> Result { + if hex.len() != 7 || !hex.starts_with('#') { + return Err(()); + } + let r = u8::from_str_radix(&hex[1..3], 16).map_err(|_| ())?; + let g = u8::from_str_radix(&hex[3..5], 16).map_err(|_| ())?; + let b = u8::from_str_radix(&hex[5..7], 16).map_err(|_| ())?; + Ok(Color::rgb8(r, g, b)) +} diff --git a/src/lib.rs b/src/lib.rs index 29f408c3..d4e49d1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -936,7 +936,8 @@ pub fn handle_messages_to_main( let ui_config = UIConfig { show_hotkeys: settings.show_hotkeys, quit_on_lost_focus: settings.quit_on_lost_focus, - theme: settings.theme, + theme_mode: settings.theme_mode, + custom_theme: settings.custom_theme, }; let mut config = app_finder.load_config(); diff --git a/src/utils.rs b/src/utils.rs index e1c66d43..bdb57a27 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,7 +5,7 @@ use std::{fs, u32}; use druid::image::imageops::FilterType; use druid::image::{ImageFormat, Rgba}; -use druid::{image, Data}; +use druid::{image, Data, Lens}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use tracing::{debug, info}; @@ -75,7 +75,8 @@ pub struct UIConfig { // linux calls this even when just opening a context menu (e.g the 3-dot menu) pub quit_on_lost_focus: bool, - pub theme: ConfiguredTheme, + pub theme_mode: ThemeMode, + pub custom_theme: CustomTheme, } impl Default for UIConfig { @@ -83,16 +84,40 @@ impl Default for UIConfig { UIConfig { show_hotkeys: true, quit_on_lost_focus: false, - theme: ConfiguredTheme::Auto, + theme_mode: ThemeMode::Auto, + custom_theme: CustomTheme::default(), } } } -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Data, PartialEq)] -pub enum ConfiguredTheme { +#[derive(Serialize, Deserialize, Debug, Clone, Data, PartialEq)] +pub enum ThemeMode { Auto, Light, Dark, + Custom, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Data, Lens, PartialEq)] +pub struct CustomTheme { + pub window_background: String, + pub text_color: String, + pub active_tab_background: String, + pub active_tab_text: String, + pub inactive_tab_text: String, + // Add more fields as needed for full customization +} + +impl Default for CustomTheme { + fn default() -> Self { + Self { + window_background: "#292929".to_string(), + text_color: "#f0f0ea".to_string(), + active_tab_background: "#195ac2".to_string(), + active_tab_text: "#ffffff".to_string(), + inactive_tab_text: "#ffffff".to_string(), + } + } } #[derive(Serialize, Deserialize, Debug, Default, Clone)] From 1973035e674068ef726a319aaef7858892f0279b Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Thu, 20 Nov 2025 21:02:59 -0500 Subject: [PATCH 2/7] feat: add customizable hover background color support - Add `hover_background` to `CustomTheme` with default RGBA value - Extend `MainWindowTheme` with `hover_background_color` and ENV key - Parse and apply the custom hover color when building the theme - Introduce a color input in the Appearance settings to let users edit the hover background - Update main window widget rendering to use the new env color for hover/selected states - Minor cleanup of imports and minor UI adjustments related to the new feature --- src/gui/main_window.rs | 4 ++-- src/gui/settings_window/appearance_view.rs | 9 +++++++++ src/gui/settings_window/general_view.rs | 2 +- src/gui/ui_theme.rs | 14 ++++++++++++++ src/utils.rs | 3 ++- 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/gui/main_window.rs b/src/gui/main_window.rs index 25696617..51998083 100644 --- a/src/gui/main_window.rs +++ b/src/gui/main_window.rs @@ -459,10 +459,10 @@ fn create_browser( let container = FocusWidget::new( container, - |ctx, _: &((bool, UISettings), UIBrowser), _env| { + |ctx, _: &((bool, UISettings), UIBrowser), env| { let size = ctx.size(); let rounded_rect = size.to_rounded_rect(5.0); - let color = Color::rgba(1.0, 1.0, 1.0, 0.25); + let color = env.get(MainWindowTheme::ENV_HOVER_BACKGROUND_COLOR); ctx.fill(rounded_rect, &color); }, |ctx, (_, data): &((bool, UISettings), UIBrowser), _env| { diff --git a/src/gui/settings_window/appearance_view.rs b/src/gui/settings_window/appearance_view.rs index 61d0394d..2579c131 100644 --- a/src/gui/settings_window/appearance_view.rs +++ b/src/gui/settings_window/appearance_view.rs @@ -71,6 +71,13 @@ pub(crate) fn appearance_content() -> impl Widget { .then(CustomTheme::inactive_tab_text), save_command.clone()); + let hover_background_input = make_color_input("Hover/Selected Background", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::hover_background), + save_command.clone()); + Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(theme_radio_row) @@ -86,6 +93,8 @@ pub(crate) fn appearance_content() -> impl Widget { .with_child(active_tab_text_input) .with_spacer(5.0) .with_child(inactive_tab_text_input) + .with_spacer(5.0) + .with_child(hover_background_input) } fn make_color_input(label: &str, lens: impl druid::Lens + 'static, save_command: druid::Command) -> impl Widget { diff --git a/src/gui/settings_window/general_view.rs b/src/gui/settings_window/general_view.rs index 280664fb..bf02a92d 100644 --- a/src/gui/settings_window/general_view.rs +++ b/src/gui/settings_window/general_view.rs @@ -1,5 +1,5 @@ use druid::widget::{ - Button, ControllerHost, CrossAxisAlignment, Flex, Label, LineBreaking, RadioGroup, Switch, + Button, ControllerHost, CrossAxisAlignment, Flex, Label, LineBreaking, Switch, }; use druid::{LensExt, Widget, WidgetExt}; diff --git a/src/gui/ui_theme.rs b/src/gui/ui_theme.rs index 225b3c11..18df0658 100644 --- a/src/gui/ui_theme.rs +++ b/src/gui/ui_theme.rs @@ -86,6 +86,7 @@ fn get_theme(ui_theme: UITheme) -> Theme { hotkey_border_color: Color::rgba(0.4, 0.4, 0.4, 0.9), hotkey_text_color: Color::rgb8(128, 128, 128), options_button_text_color: Color::rgb8(128, 128, 128), + hover_background_color: Color::rgba(1.0, 1.0, 1.0, 0.25), }, settings: SettingsWindowTheme { active_tab_background_color: Color::rgb8(25, 90, 194), @@ -141,6 +142,7 @@ fn get_theme(ui_theme: UITheme) -> Theme { hotkey_border_color: Color::rgba(0.4, 0.4, 0.4, 0.9), hotkey_text_color: Color::rgb8(128, 128, 128), options_button_text_color: Color::rgb8(128, 128, 128), + hover_background_color: Color::rgba(1.0, 1.0, 1.0, 0.25), }, settings: SettingsWindowTheme { active_tab_background_color: Color::rgb8(25, 90, 194), @@ -183,6 +185,10 @@ fn get_theme(ui_theme: UITheme) -> Theme { theme.settings.inactive_tab_text_color = color; } + if let Ok(color) = parse_color(&custom.hover_background) { + theme.main.hover_background_color = color; + } + theme } }; @@ -236,6 +242,7 @@ pub(crate) struct MainWindowTheme { hotkey_border_color: Color, hotkey_text_color: Color, options_button_text_color: Color, + hover_background_color: Color, } impl MainWindowTheme { @@ -269,6 +276,9 @@ impl MainWindowTheme { pub const ENV_OPTIONS_BUTTON_TEXT_COLOR: Key = Key::new("software.browsers.theme.main.options_button_text_color"); + pub const ENV_HOVER_BACKGROUND_COLOR: Key = + Key::new("software.browsers.theme.main.hover_background_color"); + fn set_env_to_theme(&self, env: &mut Env) { env.set(Self::ENV_WINDOW_BACKGROUND_COLOR, self.window_background_color); env.set(Self::ENV_WINDOW_BORDER_COLOR, self.window_border_color); @@ -283,6 +293,10 @@ impl MainWindowTheme { Self::ENV_OPTIONS_BUTTON_TEXT_COLOR, self.options_button_text_color, ); + env.set( + Self::ENV_HOVER_BACKGROUND_COLOR, + self.hover_background_color, + ); } } diff --git a/src/utils.rs b/src/utils.rs index bdb57a27..d7583588 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -105,7 +105,7 @@ pub struct CustomTheme { pub active_tab_background: String, pub active_tab_text: String, pub inactive_tab_text: String, - // Add more fields as needed for full customization + pub hover_background: String, // Add more fields as needed for full customization } impl Default for CustomTheme { @@ -116,6 +116,7 @@ impl Default for CustomTheme { active_tab_background: "#195ac2".to_string(), active_tab_text: "#ffffff".to_string(), inactive_tab_text: "#ffffff".to_string(), + hover_background: "#ffffff40".to_string(), // 25% white } } } From 621e54d8c305d49993eabb6285ab234e4a0d9af1 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Fri, 21 Nov 2025 00:34:30 -0500 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20add=20focus=E2=80=91specific=20env?= =?UTF-8?q?=20customization=20and=20hover=20color=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce `FocusWidget::with_env_on_focus` to apply a custom `Env` when the widget gains focus, affecting event, lifecycle, update, layout, and paint handling. - Extend `MainWindowTheme` with new keys `ENV_HOVER_TEXT_COLOR` and `ENV_HOVER_SECONDARY_TEXT_COLOR` and propagate them through theme initialization and environment setup. - Add corresponding fields (`hover_text_color`, `hover_secondary_text_color`) to the theme struct and default values in `CustomTheme`. - Update UI settings view to expose color inputs for hover text, secondary text, and hover secondary text, enabling user customization. - Adjust theme parsing to read the new color values from `CustomTheme` and apply them to the main window theme. --- src/gui/focus_widget.rs | 48 ++++++++++++++++++++-- src/gui/main_window.rs | 16 +++++++- src/gui/settings_window/appearance_view.rs | 27 ++++++++++++ src/gui/ui_theme.rs | 32 +++++++++++++++ src/utils.rs | 8 +++- 5 files changed, 125 insertions(+), 6 deletions(-) diff --git a/src/gui/focus_widget.rs b/src/gui/focus_widget.rs index 69d68e82..8f144941 100644 --- a/src/gui/focus_widget.rs +++ b/src/gui/focus_widget.rs @@ -15,6 +15,8 @@ pub struct FocusWidget { inner: W, paint_fn_on_focus: fn(ctx: &mut PaintCtx, data: &S, env: &Env), lifecycle_fn: fn(ctx: &mut LifeCycleCtx, data: &S, env: &Env), + env_fn_on_focus: Option Env>, + is_focused: bool, } impl FocusWidget {} @@ -29,8 +31,15 @@ impl FocusWidget { inner, paint_fn_on_focus, lifecycle_fn, + env_fn_on_focus: None, + is_focused: false, } } + + pub fn with_env_on_focus(mut self, f: fn(&Env) -> Env) -> Self { + self.env_fn_on_focus = Some(f); + self + } } impl> Widget for FocusWidget { @@ -93,7 +102,13 @@ impl> Widget for FocusWidget { _ => {} } - self.inner.event(ctx, event, data, env); + let mut local_env = env.clone(); + if ctx.has_focus() { + if let Some(f) = self.env_fn_on_focus { + local_env = f(env); + } + } + self.inner.event(ctx, event, data, &local_env); } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &S, env: &Env) { @@ -104,6 +119,7 @@ impl> Widget for FocusWidget { ctx.register_for_focus(); } LifeCycle::FocusChanged(to_focused) => { + self.is_focused = *to_focused; if *to_focused { // enable scrolling once getting edge cases right // (sometimes too eager to scroll top/bottom item) @@ -113,6 +129,7 @@ impl> Widget for FocusWidget { (self.lifecycle_fn)(ctx, data, env); } ctx.request_paint(); + ctx.request_layout(); } LifeCycle::HotChanged(to_hot) => { if *to_hot && !ctx.has_focus() { @@ -129,23 +146,46 @@ impl> Widget for FocusWidget { } _ => {} } - self.inner.lifecycle(ctx, event, data, env); + let mut local_env = env.clone(); + if ctx.has_focus() { + if let Some(f) = self.env_fn_on_focus { + local_env = f(env); + } + } + self.inner.lifecycle(ctx, event, data, &local_env); } fn update(&mut self, ctx: &mut UpdateCtx, old_data: &S, data: &S, env: &Env) { /*if old_data.glow_hot != data.glow_hot { ctx.request_paint(); }*/ - self.inner.update(ctx, old_data, data, env); + let mut local_env = env.clone(); + if ctx.has_focus() { + if let Some(f) = self.env_fn_on_focus { + local_env = f(env); + } + } + self.inner.update(ctx, old_data, data, &local_env); } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &S, env: &Env) -> Size { - self.inner.layout(ctx, bc, data, env) + let mut local_env = env.clone(); + if self.is_focused { + if let Some(f) = self.env_fn_on_focus { + local_env = f(env); + } + } + self.inner.layout(ctx, bc, data, &local_env) } fn paint(&mut self, ctx: &mut PaintCtx, data: &S, env: &Env) { if ctx.has_focus() { (self.paint_fn_on_focus)(ctx, data, env); + if let Some(f) = self.env_fn_on_focus { + let new_env = f(env); + self.inner.paint(ctx, data, &new_env); + return; + } } self.inner.paint(ctx, data, env); } diff --git a/src/gui/main_window.rs b/src/gui/main_window.rs index 51998083..e08d21c3 100644 --- a/src/gui/main_window.rs +++ b/src/gui/main_window.rs @@ -476,7 +476,21 @@ fn create_browser( .ok(); } }, - ); + ) + .with_env_on_focus(|env| { + let mut new_env = env.clone(); + let hover_text_color = env.get(MainWindowTheme::ENV_HOVER_TEXT_COLOR); + let hover_secondary_text_color = env.get(MainWindowTheme::ENV_HOVER_SECONDARY_TEXT_COLOR); + + new_env.set(MainWindowTheme::ENV_BROWSER_LABEL_COLOR, hover_text_color.clone()); + new_env.set(MainWindowTheme::ENV_PROFILE_LABEL_COLOR, hover_secondary_text_color.clone()); + new_env.set(MainWindowTheme::ENV_HOTKEY_TEXT_COLOR, hover_secondary_text_color.clone()); + new_env.set( + MainWindowTheme::ENV_OPTIONS_BUTTON_TEXT_COLOR, + hover_secondary_text_color.clone(), + ); + new_env + }); let container = Container::new(container); diff --git a/src/gui/settings_window/appearance_view.rs b/src/gui/settings_window/appearance_view.rs index 2579c131..af988274 100644 --- a/src/gui/settings_window/appearance_view.rs +++ b/src/gui/settings_window/appearance_view.rs @@ -78,6 +78,27 @@ pub(crate) fn appearance_content() -> impl Widget { .then(CustomTheme::hover_background), save_command.clone()); + let hover_text_input = make_color_input("Hover/Selected Text", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::hover_text), + save_command.clone()); + + let secondary_text_input = make_color_input("Secondary Text", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::secondary_text), + save_command.clone()); + + let hover_secondary_text_input = make_color_input("Hover/Selected Secondary Text", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::hover_secondary_text), + save_command.clone()); + Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(theme_radio_row) @@ -95,6 +116,12 @@ pub(crate) fn appearance_content() -> impl Widget { .with_child(inactive_tab_text_input) .with_spacer(5.0) .with_child(hover_background_input) + .with_spacer(5.0) + .with_child(hover_text_input) + .with_spacer(5.0) + .with_child(secondary_text_input) + .with_spacer(5.0) + .with_child(hover_secondary_text_input) } fn make_color_input(label: &str, lens: impl druid::Lens + 'static, save_command: druid::Command) -> impl Widget { diff --git a/src/gui/ui_theme.rs b/src/gui/ui_theme.rs index 18df0658..6b1783ed 100644 --- a/src/gui/ui_theme.rs +++ b/src/gui/ui_theme.rs @@ -87,6 +87,8 @@ fn get_theme(ui_theme: UITheme) -> Theme { hotkey_text_color: Color::rgb8(128, 128, 128), options_button_text_color: Color::rgb8(128, 128, 128), hover_background_color: Color::rgba(1.0, 1.0, 1.0, 0.25), + hover_text_color: Color::rgb8(255, 255, 255), + hover_secondary_text_color: Color::rgb8(255, 255, 255), }, settings: SettingsWindowTheme { active_tab_background_color: Color::rgb8(25, 90, 194), @@ -143,6 +145,8 @@ fn get_theme(ui_theme: UITheme) -> Theme { hotkey_text_color: Color::rgb8(128, 128, 128), options_button_text_color: Color::rgb8(128, 128, 128), hover_background_color: Color::rgba(1.0, 1.0, 1.0, 0.25), + hover_text_color: Color::rgb8(0, 0, 0), + hover_secondary_text_color: Color::rgb8(0, 0, 0), }, settings: SettingsWindowTheme { active_tab_background_color: Color::rgb8(25, 90, 194), @@ -189,6 +193,18 @@ fn get_theme(ui_theme: UITheme) -> Theme { theme.main.hover_background_color = color; } + if let Ok(color) = parse_color(&custom.hover_text) { + theme.main.hover_text_color = color; + } + + if let Ok(color) = parse_color(&custom.secondary_text) { + theme.main.profile_label_color = color; + } + + if let Ok(color) = parse_color(&custom.hover_secondary_text) { + theme.main.hover_secondary_text_color = color; + } + theme } }; @@ -243,6 +259,8 @@ pub(crate) struct MainWindowTheme { hotkey_text_color: Color, options_button_text_color: Color, hover_background_color: Color, + hover_text_color: Color, + hover_secondary_text_color: Color, } impl MainWindowTheme { @@ -279,6 +297,12 @@ impl MainWindowTheme { pub const ENV_HOVER_BACKGROUND_COLOR: Key = Key::new("software.browsers.theme.main.hover_background_color"); + pub const ENV_HOVER_TEXT_COLOR: Key = + Key::new("software.browsers.theme.main.hover_text_color"); + + pub const ENV_HOVER_SECONDARY_TEXT_COLOR: Key = + Key::new("software.browsers.theme.main.hover_secondary_text_color"); + fn set_env_to_theme(&self, env: &mut Env) { env.set(Self::ENV_WINDOW_BACKGROUND_COLOR, self.window_background_color); env.set(Self::ENV_WINDOW_BORDER_COLOR, self.window_border_color); @@ -297,6 +321,14 @@ impl MainWindowTheme { Self::ENV_HOVER_BACKGROUND_COLOR, self.hover_background_color, ); + env.set( + Self::ENV_HOVER_TEXT_COLOR, + self.hover_text_color, + ); + env.set( + Self::ENV_HOVER_SECONDARY_TEXT_COLOR, + self.hover_secondary_text_color, + ); } } diff --git a/src/utils.rs b/src/utils.rs index d7583588..7f3c7900 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -105,7 +105,10 @@ pub struct CustomTheme { pub active_tab_background: String, pub active_tab_text: String, pub inactive_tab_text: String, - pub hover_background: String, // Add more fields as needed for full customization + pub hover_background: String, + pub hover_text: String, + pub secondary_text: String, + pub hover_secondary_text: String, } impl Default for CustomTheme { @@ -117,6 +120,9 @@ impl Default for CustomTheme { active_tab_text: "#ffffff".to_string(), inactive_tab_text: "#ffffff".to_string(), hover_background: "#ffffff40".to_string(), // 25% white + hover_text: "#ffffff".to_string(), + secondary_text: "#bebebe".to_string(), + hover_secondary_text: "#ffffff".to_string(), } } } From a88e72ada523b9cbda8706424e77b66080b077e5 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:30:15 -0500 Subject: [PATCH 4/7] feat: Add UI options for custom font sizes and families, and implement browser focus state. --- src/gui/focus_widget.rs | 52 ++++++++- src/gui/main_window.rs | 119 +++++++++++++-------- src/gui/settings_window/appearance_view.rs | 56 +++++++++- src/gui/ui.rs | 13 +++ src/gui/ui_theme.rs | 46 +++++--- src/utils.rs | 8 ++ 6 files changed, 230 insertions(+), 64 deletions(-) diff --git a/src/gui/focus_widget.rs b/src/gui/focus_widget.rs index 8f144941..ae820d5d 100644 --- a/src/gui/focus_widget.rs +++ b/src/gui/focus_widget.rs @@ -10,11 +10,14 @@ pub trait FocusData { pub const FOCUS_WIDGET_SET_FOCUS_ON_HOVER: Selector = Selector::new("focus_widget.set_focus"); +pub const FOCUS_WIDGET_RESIGN_FOCUS: Selector = + Selector::new("focus_widget.resign_focus"); pub struct FocusWidget { inner: W, paint_fn_on_focus: fn(ctx: &mut PaintCtx, data: &S, env: &Env), lifecycle_fn: fn(ctx: &mut LifeCycleCtx, data: &S, env: &Env), + hover_lost_fn: Option, env_fn_on_focus: Option Env>, is_focused: bool, } @@ -31,11 +34,17 @@ impl FocusWidget { inner, paint_fn_on_focus, lifecycle_fn, + hover_lost_fn: None, env_fn_on_focus: None, is_focused: false, } } + pub fn on_hover_lost(mut self, f: fn(ctx: &mut LifeCycleCtx, data: &S, env: &Env)) -> Self { + self.hover_lost_fn = Some(f); + self + } + pub fn with_env_on_focus(mut self, f: fn(&Env) -> Env) -> Self { self.env_fn_on_focus = Some(f); self @@ -57,6 +66,14 @@ impl> Widget for FocusWidget { ctx.set_handled(); ctx.request_update(); } + Event::Command(cmd) if cmd.is(FOCUS_WIDGET_RESIGN_FOCUS) => { + if ctx.has_focus() { + ctx.resign_focus(); + } + ctx.request_paint(); + ctx.set_handled(); + ctx.request_update(); + } Event::WindowConnected => { if data.has_autofocus() { // ask for focus on launch @@ -103,7 +120,11 @@ impl> Widget for FocusWidget { } let mut local_env = env.clone(); - if ctx.has_focus() { + if ctx.has_focus() != self.is_focused { + self.is_focused = ctx.has_focus(); + ctx.request_layout(); + } + if self.is_focused { if let Some(f) = self.env_fn_on_focus { local_env = f(env); } @@ -126,8 +147,8 @@ impl> Widget for FocusWidget { if !ctx.is_hot() { ctx.scroll_to_view(); } - (self.lifecycle_fn)(ctx, data, env); } + (self.lifecycle_fn)(ctx, data, env); ctx.request_paint(); ctx.request_layout(); } @@ -142,12 +163,28 @@ impl> Widget for FocusWidget { ); ctx.submit_command(cmd); //ctx.request_paint(); + } else if !*to_hot { + if let Some(f) = self.hover_lost_fn { + f(ctx, data, env); + } + if ctx.has_focus() { + let cmd = Command::new( + FOCUS_WIDGET_RESIGN_FOCUS, + ctx.widget_id(), + Target::Widget(ctx.widget_id()), + ); + ctx.submit_command(cmd); + } } } _ => {} } let mut local_env = env.clone(); - if ctx.has_focus() { + if ctx.has_focus() != self.is_focused { + self.is_focused = ctx.has_focus(); + ctx.request_layout(); + } + if self.is_focused { if let Some(f) = self.env_fn_on_focus { local_env = f(env); } @@ -160,7 +197,11 @@ impl> Widget for FocusWidget { ctx.request_paint(); }*/ let mut local_env = env.clone(); - if ctx.has_focus() { + if ctx.has_focus() != self.is_focused { + self.is_focused = ctx.has_focus(); + ctx.request_layout(); + } + if self.is_focused { if let Some(f) = self.env_fn_on_focus { local_env = f(env); } @@ -170,6 +211,7 @@ impl> Widget for FocusWidget { fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &S, env: &Env) -> Size { let mut local_env = env.clone(); + // info!("FocusWidget: layout, widget_id: {:?}, is_focused: {}", ctx.widget_id(), self.is_focused); if self.is_focused { if let Some(f) = self.env_fn_on_focus { local_env = f(env); @@ -179,7 +221,7 @@ impl> Widget for FocusWidget { } fn paint(&mut self, ctx: &mut PaintCtx, data: &S, env: &Env) { - if ctx.has_focus() { + if self.is_focused { (self.paint_fn_on_focus)(ctx, data, env); if let Some(f) = self.env_fn_on_focus { let new_env = f(env); diff --git a/src/gui/main_window.rs b/src/gui/main_window.rs index e08d21c3..fd7dbf5c 100644 --- a/src/gui/main_window.rs +++ b/src/gui/main_window.rs @@ -322,22 +322,24 @@ pub(crate) fn calculate_window_position( return Point::new(x, y); } -fn create_browser_label() -> Label<((bool, UISettings), UIBrowser)> { - let browser_label = Label::dynamic( - |((incognito_mode, _), item): &((bool, UISettings), UIBrowser), _env| { - let mut name = item.browser_name.clone(); - if item.supports_incognito && *incognito_mode { - name += " 👓"; - } - name - }, +fn create_browser_label() -> impl Widget<((bool, UISettings), UIBrowser)> { + let label_fn = |((_incognito_mode, _), item): &((bool, UISettings), UIBrowser), _env: &_| { + item.browser_name.clone() + }; + + Either::new( + |(_, item): &((bool, UISettings), UIBrowser), _| item.is_focused, + Label::dynamic(label_fn) + .with_font(MainWindowTheme::ENV_BROWSER_LABEL_FONT) + .with_line_break_mode(LineBreaking::Clip) + .with_text_alignment(TextAlignment::Start) + .with_text_color(MainWindowTheme::ENV_HOVER_TEXT_COLOR), + Label::dynamic(label_fn) + .with_font(MainWindowTheme::ENV_BROWSER_LABEL_FONT) + .with_line_break_mode(LineBreaking::Clip) + .with_text_alignment(TextAlignment::Start) + .with_text_color(MainWindowTheme::ENV_BROWSER_LABEL_COLOR), ) - .with_text_size(MainWindowTheme::ENV_BROWSER_LABEL_SIZE) - .with_line_break_mode(LineBreaking::Clip) - .with_text_alignment(TextAlignment::Start) - .with_text_color(MainWindowTheme::ENV_BROWSER_LABEL_COLOR); - - browser_label } fn create_browser( @@ -372,14 +374,23 @@ fn create_browser( let item_label = Either::new( |(_, item): &((bool, UISettings), UIBrowser), _env| item.supports_profiles, { - let profile_label = - Label::dynamic(|(_, item): &((bool, UISettings), UIBrowser), _env: &_| { - item.profile_name.clone() - }) - .with_text_size(MainWindowTheme::ENV_PROFILE_LABEL_SIZE) - .with_line_break_mode(LineBreaking::Clip) - .with_text_alignment(TextAlignment::Start) - .with_text_color(MainWindowTheme::ENV_PROFILE_LABEL_COLOR); + let profile_label_fn = |(_, item): &((bool, UISettings), UIBrowser), _env: &_| { + item.profile_name.clone() + }; + + let profile_label = Either::new( + |(_, item): &((bool, UISettings), UIBrowser), _| item.is_focused, + Label::dynamic(profile_label_fn) + .with_font(MainWindowTheme::ENV_PROFILE_LABEL_FONT) + .with_line_break_mode(LineBreaking::Clip) + .with_text_alignment(TextAlignment::Start) + .with_text_color(MainWindowTheme::ENV_HOVER_SECONDARY_TEXT_COLOR), + Label::dynamic(profile_label_fn) + .with_font(MainWindowTheme::ENV_PROFILE_LABEL_FONT) + .with_line_break_mode(LineBreaking::Clip) + .with_text_alignment(TextAlignment::Start) + .with_text_color(MainWindowTheme::ENV_PROFILE_LABEL_COLOR), + ); let profile_row = Flex::row() //.with_child(profile_icon) @@ -415,16 +426,24 @@ fn create_browser( ui_settings.visual_settings.show_hotkeys && item.filtered_index < 9 }, { - let hotkey_label = + let make_hotkey_label = |color: druid::Key| { Label::dynamic(|(_, item): &((bool, UISettings), UIBrowser), _env: &_| { let hotkey_number = item.filtered_index + 1; let hotkey = hotkey_number.to_string(); hotkey }) - .with_font(font) - .with_text_color(MainWindowTheme::ENV_HOTKEY_TEXT_COLOR) + .with_font(font.clone()) + .with_text_color(color) .fix_size(text_size, text_size) - .padding(4.0); + }; + + let hotkey_label = Either::new( + |(_, item): &((bool, UISettings), UIBrowser), _| item.is_focused, + make_hotkey_label(MainWindowTheme::ENV_HOVER_SECONDARY_TEXT_COLOR) + .padding(4.0), + make_hotkey_label(MainWindowTheme::ENV_HOTKEY_TEXT_COLOR) + .padding(4.0), + ); let hotkey_label = Container::new(hotkey_label) .background(MainWindowTheme::ENV_HOTKEY_BACKGROUND_COLOR) @@ -459,11 +478,13 @@ fn create_browser( let container = FocusWidget::new( container, - |ctx, _: &((bool, UISettings), UIBrowser), env| { - let size = ctx.size(); - let rounded_rect = size.to_rounded_rect(5.0); - let color = env.get(MainWindowTheme::ENV_HOVER_BACKGROUND_COLOR); - ctx.fill(rounded_rect, &color); + |ctx, (_, data): &((bool, UISettings), UIBrowser), env| { + if data.is_focused { + let size = ctx.size(); + let rounded_rect = size.to_rounded_rect(5.0); + let color = env.get(MainWindowTheme::ENV_HOVER_BACKGROUND_COLOR); + ctx.fill(rounded_rect, &color); + } }, |ctx, (_, data): &((bool, UISettings), UIBrowser), _env| { if ctx.has_focus() { @@ -474,22 +495,30 @@ fn create_browser( Target::Global, ) .ok(); + } else { + ctx.get_external_handle() + .submit_command( + SET_FOCUSED_INDEX, + None, + Target::Global, + ) + .ok(); } }, ) - .with_env_on_focus(|env| { - let mut new_env = env.clone(); - let hover_text_color = env.get(MainWindowTheme::ENV_HOVER_TEXT_COLOR); - let hover_secondary_text_color = env.get(MainWindowTheme::ENV_HOVER_SECONDARY_TEXT_COLOR); - - new_env.set(MainWindowTheme::ENV_BROWSER_LABEL_COLOR, hover_text_color.clone()); - new_env.set(MainWindowTheme::ENV_PROFILE_LABEL_COLOR, hover_secondary_text_color.clone()); - new_env.set(MainWindowTheme::ENV_HOTKEY_TEXT_COLOR, hover_secondary_text_color.clone()); - new_env.set( - MainWindowTheme::ENV_OPTIONS_BUTTON_TEXT_COLOR, - hover_secondary_text_color.clone(), - ); - new_env + .on_hover_lost(|ctx, (_, data), _| { + if data.is_focused { + ctx.get_external_handle() + .submit_command( + SET_FOCUSED_INDEX, + None, + Target::Global, + ) + .ok(); + } + }) + .env_scope(|env, (_, data): &((bool, UISettings), UIBrowser)| { + // env scope removed as we handle colors explicitly }); let container = Container::new(container); diff --git a/src/gui/settings_window/appearance_view.rs b/src/gui/settings_window/appearance_view.rs index af988274..ad37a98f 100644 --- a/src/gui/settings_window/appearance_view.rs +++ b/src/gui/settings_window/appearance_view.rs @@ -1,7 +1,7 @@ use druid::widget::{ ControllerHost, CrossAxisAlignment, Flex, Label, RadioGroup, TextBox, }; -use druid::{LensExt, Widget, WidgetExt}; +use druid::{Color, LensExt, Widget, WidgetExt}; use crate::gui::settings_window::rules_view; use crate::gui::ui::{ @@ -99,6 +99,34 @@ pub(crate) fn appearance_content() -> impl Widget { .then(CustomTheme::hover_secondary_text), save_command.clone()); + let primary_font_size_input = make_input("Primary Font Size", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::primary_font_size), + save_command.clone()); + + let primary_font_family_input = make_input("Primary Font Family", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::primary_font_family), + save_command.clone()); + + let secondary_font_size_input = make_input("Secondary Font Size", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::secondary_font_size), + save_command.clone()); + + let secondary_font_family_input = make_input("Secondary Font Family", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::secondary_font_family), + save_command.clone()); + Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(theme_radio_row) @@ -122,6 +150,32 @@ pub(crate) fn appearance_content() -> impl Widget { .with_child(secondary_text_input) .with_spacer(5.0) .with_child(hover_secondary_text_input) + .with_spacer(10.0) + .with_child(Label::new("Fonts").with_text_size(14.0).with_text_color(Color::WHITE)) + .with_spacer(5.0) + .with_child(primary_font_size_input) + .with_spacer(5.0) + .with_child(primary_font_family_input) + .with_spacer(5.0) + .with_child(secondary_font_size_input) + .with_spacer(5.0) + .with_child(secondary_font_family_input) +} + +fn make_input(label: &str, lens: impl druid::Lens + 'static, save_command: druid::Command) -> impl Widget { + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(Label::new(label).with_text_size(12.0)) + .with_spacer(2.0) + .with_child( + druid::widget::TextBox::new() + .with_placeholder(label) + .lens(lens) + .controller(rules_view::SubmitCommandOnDataChange { + command: save_command, + }) + .expand_width() + ) } fn make_color_input(label: &str, lens: impl druid::Lens + 'static, save_command: druid::Command) -> impl Widget { diff --git a/src/gui/ui.rs b/src/gui/ui.rs index 850e6e1d..c60402bb 100644 --- a/src/gui/ui.rs +++ b/src/gui/ui.rs @@ -131,6 +131,7 @@ impl UI { unique_id: p.get_unique_id(), unique_app_id: p.get_unique_app_id(), filtered_index: i, // TODO: filter against current url + is_focused: false, }) .collect(); } @@ -366,9 +367,12 @@ pub struct UIBrowser { pub unique_id: String, pub(crate) unique_app_id: String, + // index in list of actually visible browsers for current url + // (correctly set only in filtered_browsers list) // index in list of actually visible browsers for current url // (correctly set only in filtered_browsers list) pub(crate) filtered_index: usize, + pub(crate) is_focused: bool, } impl UIBrowser { @@ -691,6 +695,15 @@ impl AppDelegate for UIDelegate { } else if cmd.is(SET_FOCUSED_INDEX) { let profile_index = cmd.get_unchecked(SET_FOCUSED_INDEX); data.focused_index = profile_index.clone(); + + let browsers_mut = Arc::make_mut(&mut data.filtered_browsers); + for (i, browser) in browsers_mut.iter_mut().enumerate() { + if let Some(index) = profile_index { + browser.is_focused = browser.browser_profile_index == *index; + } else { + browser.is_focused = false; + } + } Handled::Yes } else if cmd.is(COPY_LINK_TO_CLIPBOARD) { copy_to_clipboard(data.url.as_str()); diff --git a/src/gui/ui_theme.rs b/src/gui/ui_theme.rs index 6b1783ed..4ee527af 100644 --- a/src/gui/ui_theme.rs +++ b/src/gui/ui_theme.rs @@ -1,7 +1,7 @@ use crate::gui::ui::UIState; use crate::utils::{CustomTheme, ThemeMode}; use dark_light::Mode; -use druid::{Color, Data, Env, Key}; +use druid::{Color, Data, Env, FontDescriptor, FontFamily, Key}; use serde::{Deserialize, Serialize}; use tracing::warn; @@ -78,9 +78,9 @@ fn get_theme(ui_theme: UITheme) -> Theme { main: MainWindowTheme { window_background_color: Color::rgba(0.15, 0.15, 0.15, 0.9), window_border_color: Color::rgba(0.5, 0.5, 0.5, 0.9), - browser_label_size: 12.0, + browser_label_font: FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(12.0), browser_label_color: Color::rgb8(255, 255, 255), - profile_label_size: 11.0, + profile_label_font: FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(11.0), profile_label_color: Color::rgb8(190, 190, 190), hotkey_background_color: Color::rgba(0.15, 0.15, 0.15, 1.0), hotkey_border_color: Color::rgba(0.4, 0.4, 0.4, 0.9), @@ -136,9 +136,9 @@ fn get_theme(ui_theme: UITheme) -> Theme { main: MainWindowTheme { window_background_color: Color::rgba8(215, 215, 215, 230), window_border_color: Color::rgba(0.7, 0.7, 0.7, 0.9), - browser_label_size: 12.0, + browser_label_font: FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(12.0), browser_label_color: Color::rgb8(0, 0, 0), - profile_label_size: 11.0, + profile_label_font: FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(11.0), profile_label_color: Color::rgb8(30, 30, 30), hotkey_background_color: Color::rgb8(215, 215, 215), hotkey_border_color: Color::rgba(0.4, 0.4, 0.4, 0.9), @@ -205,6 +205,14 @@ fn get_theme(ui_theme: UITheme) -> Theme { theme.main.hover_secondary_text_color = color; } + if let Ok(font) = parse_font(&custom.primary_font_family, &custom.primary_font_size) { + theme.main.browser_label_font = font; + } + + if let Ok(font) = parse_font(&custom.secondary_font_family, &custom.secondary_font_size) { + theme.main.profile_label_font = font; + } + theme } }; @@ -250,9 +258,9 @@ impl GeneralTheme { pub(crate) struct MainWindowTheme { window_background_color: Color, window_border_color: Color, - browser_label_size: f64, + browser_label_font: FontDescriptor, browser_label_color: Color, - profile_label_size: f64, + profile_label_font: FontDescriptor, profile_label_color: Color, hotkey_background_color: Color, hotkey_border_color: Color, @@ -270,14 +278,14 @@ impl MainWindowTheme { pub const ENV_WINDOW_BORDER_COLOR: Key = Key::new("software.browsers.theme.main.window_border_color"); - pub const ENV_BROWSER_LABEL_SIZE: Key = - Key::new("software.browsers.theme.main.browser_label_size"); + pub const ENV_BROWSER_LABEL_FONT: Key = + Key::new("software.browsers.theme.main.browser_label_font"); pub const ENV_BROWSER_LABEL_COLOR: Key = Key::new("software.browsers.theme.main.browser_label_color"); - pub const ENV_PROFILE_LABEL_SIZE: Key = - Key::new("software.browsers.theme.main.profile_label_size"); + pub const ENV_PROFILE_LABEL_FONT: Key = + Key::new("software.browsers.theme.main.profile_label_font"); pub const ENV_PROFILE_LABEL_COLOR: Key = Key::new("software.browsers.theme.main.profile_label_color"); @@ -306,9 +314,9 @@ impl MainWindowTheme { fn set_env_to_theme(&self, env: &mut Env) { env.set(Self::ENV_WINDOW_BACKGROUND_COLOR, self.window_background_color); env.set(Self::ENV_WINDOW_BORDER_COLOR, self.window_border_color); - env.set(Self::ENV_BROWSER_LABEL_SIZE, self.browser_label_size); + env.set(Self::ENV_BROWSER_LABEL_FONT, self.browser_label_font.clone()); env.set(Self::ENV_BROWSER_LABEL_COLOR, self.browser_label_color); - env.set(Self::ENV_PROFILE_LABEL_SIZE, self.profile_label_size); + env.set(Self::ENV_PROFILE_LABEL_FONT, self.profile_label_font.clone()); env.set(Self::ENV_PROFILE_LABEL_COLOR, self.profile_label_color); env.set(Self::ENV_HOTKEY_BACKGROUND_COLOR, self.hotkey_background_color); env.set(Self::ENV_HOTKEY_BORDER_COLOR, self.hotkey_border_color); @@ -490,3 +498,15 @@ fn parse_color(hex: &str) -> Result { let b = u8::from_str_radix(&hex[5..7], 16).map_err(|_| ())?; Ok(Color::rgb8(r, g, b)) } + +fn parse_font(family: &str, size: &str) -> Result { + let size = size.parse::().map_err(|_| ())?; + let family = match family { + "default" | "System UI" => FontFamily::SYSTEM_UI, + "Serif" => FontFamily::SERIF, + "Sans Serif" => FontFamily::SANS_SERIF, + "Monospace" => FontFamily::MONOSPACE, + name => FontFamily::new_unchecked(name), + }; + Ok(FontDescriptor::new(family).with_size(size)) +} diff --git a/src/utils.rs b/src/utils.rs index 7f3c7900..2bcf9628 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -109,6 +109,10 @@ pub struct CustomTheme { pub hover_text: String, pub secondary_text: String, pub hover_secondary_text: String, + pub primary_font_size: String, + pub primary_font_family: String, + pub secondary_font_size: String, + pub secondary_font_family: String, } impl Default for CustomTheme { @@ -123,6 +127,10 @@ impl Default for CustomTheme { hover_text: "#ffffff".to_string(), secondary_text: "#bebebe".to_string(), hover_secondary_text: "#ffffff".to_string(), + primary_font_size: "12.0".to_string(), + primary_font_family: "default".to_string(), + secondary_font_size: "11.0".to_string(), + secondary_font_family: "default".to_string(), } } } From 982368af7c2e1b1889a6b9a872cb474a469c16b1 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 22 Nov 2025 02:15:42 -0500 Subject: [PATCH 5/7] Remove redundant `SET_FOCUSED_INDEX` command submission from `else` block. --- src/gui/main_window.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/gui/main_window.rs b/src/gui/main_window.rs index fd7dbf5c..d967fd8d 100644 --- a/src/gui/main_window.rs +++ b/src/gui/main_window.rs @@ -495,14 +495,6 @@ fn create_browser( Target::Global, ) .ok(); - } else { - ctx.get_external_handle() - .submit_command( - SET_FOCUSED_INDEX, - None, - Target::Global, - ) - .ok(); } }, ) From 6e8b3487d9090b446c2347736317c6e7494adab7 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sat, 22 Nov 2025 19:18:16 -0500 Subject: [PATCH 6/7] feat: Add customizable hotkey background and text colors, including hover states, to appearance settings and apply them to hotkey labels. --- src/gui/main_window.rs | 17 +++++----- src/gui/settings_window/appearance_view.rs | 36 ++++++++++++++++++++++ src/gui/ui_theme.rs | 36 ++++++++++++++++++++++ src/utils.rs | 8 +++++ 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/gui/main_window.rs b/src/gui/main_window.rs index d967fd8d..23b7f073 100644 --- a/src/gui/main_window.rs +++ b/src/gui/main_window.rs @@ -439,17 +439,18 @@ fn create_browser( let hotkey_label = Either::new( |(_, item): &((bool, UISettings), UIBrowser), _| item.is_focused, - make_hotkey_label(MainWindowTheme::ENV_HOVER_SECONDARY_TEXT_COLOR) - .padding(4.0), + make_hotkey_label(MainWindowTheme::ENV_HOVER_HOTKEY_TEXT_COLOR) + .padding(4.0) + .background(MainWindowTheme::ENV_HOVER_HOTKEY_BACKGROUND_COLOR) + .rounded(5.0) + .border(MainWindowTheme::ENV_HOTKEY_BORDER_COLOR, 0.5), make_hotkey_label(MainWindowTheme::ENV_HOTKEY_TEXT_COLOR) - .padding(4.0), + .padding(4.0) + .background(MainWindowTheme::ENV_HOTKEY_BACKGROUND_COLOR) + .rounded(5.0) + .border(MainWindowTheme::ENV_HOTKEY_BORDER_COLOR, 0.5), ); - let hotkey_label = Container::new(hotkey_label) - .background(MainWindowTheme::ENV_HOTKEY_BACKGROUND_COLOR) - .rounded(5.0) - .border(MainWindowTheme::ENV_HOTKEY_BORDER_COLOR, 0.5); - hotkey_label }, Label::new(""), diff --git a/src/gui/settings_window/appearance_view.rs b/src/gui/settings_window/appearance_view.rs index ad37a98f..070d3b47 100644 --- a/src/gui/settings_window/appearance_view.rs +++ b/src/gui/settings_window/appearance_view.rs @@ -99,6 +99,34 @@ pub(crate) fn appearance_content() -> impl Widget { .then(CustomTheme::hover_secondary_text), save_command.clone()); + let hotkey_background_input = make_color_input("Hotkey Background", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::hotkey_background), + save_command.clone()); + + let hotkey_text_input = make_color_input("Hotkey Text", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::hotkey_text), + save_command.clone()); + + let hover_hotkey_background_input = make_color_input("Hover Hotkey Background", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::hover_hotkey_background), + save_command.clone()); + + let hover_hotkey_text_input = make_color_input("Hover Hotkey Text", + UIState::ui_settings + .then(UISettings::visual_settings) + .then(UIVisualSettings::custom_theme) + .then(CustomTheme::hover_hotkey_text), + save_command.clone()); + let primary_font_size_input = make_input("Primary Font Size", UIState::ui_settings .then(UISettings::visual_settings) @@ -150,6 +178,14 @@ pub(crate) fn appearance_content() -> impl Widget { .with_child(secondary_text_input) .with_spacer(5.0) .with_child(hover_secondary_text_input) + .with_spacer(5.0) + .with_child(hotkey_background_input) + .with_spacer(5.0) + .with_child(hotkey_text_input) + .with_spacer(5.0) + .with_child(hover_hotkey_background_input) + .with_spacer(5.0) + .with_child(hover_hotkey_text_input) .with_spacer(10.0) .with_child(Label::new("Fonts").with_text_size(14.0).with_text_color(Color::WHITE)) .with_spacer(5.0) diff --git a/src/gui/ui_theme.rs b/src/gui/ui_theme.rs index 4ee527af..1d03d946 100644 --- a/src/gui/ui_theme.rs +++ b/src/gui/ui_theme.rs @@ -89,6 +89,8 @@ fn get_theme(ui_theme: UITheme) -> Theme { hover_background_color: Color::rgba(1.0, 1.0, 1.0, 0.25), hover_text_color: Color::rgb8(255, 255, 255), hover_secondary_text_color: Color::rgb8(255, 255, 255), + hover_hotkey_background_color: Color::rgba(0.15, 0.15, 0.15, 1.0), + hover_hotkey_text_color: Color::rgb8(255, 255, 255), }, settings: SettingsWindowTheme { active_tab_background_color: Color::rgb8(25, 90, 194), @@ -147,6 +149,8 @@ fn get_theme(ui_theme: UITheme) -> Theme { hover_background_color: Color::rgba(1.0, 1.0, 1.0, 0.25), hover_text_color: Color::rgb8(0, 0, 0), hover_secondary_text_color: Color::rgb8(0, 0, 0), + hover_hotkey_background_color: Color::rgb8(215, 215, 215), + hover_hotkey_text_color: Color::rgb8(0, 0, 0), }, settings: SettingsWindowTheme { active_tab_background_color: Color::rgb8(25, 90, 194), @@ -213,6 +217,22 @@ fn get_theme(ui_theme: UITheme) -> Theme { theme.main.profile_label_font = font; } + if let Ok(color) = parse_color(&custom.hotkey_background) { + theme.main.hotkey_background_color = color; + } + + if let Ok(color) = parse_color(&custom.hotkey_text) { + theme.main.hotkey_text_color = color; + } + + if let Ok(color) = parse_color(&custom.hover_hotkey_background) { + theme.main.hover_hotkey_background_color = color; + } + + if let Ok(color) = parse_color(&custom.hover_hotkey_text) { + theme.main.hover_hotkey_text_color = color; + } + theme } }; @@ -269,6 +289,8 @@ pub(crate) struct MainWindowTheme { hover_background_color: Color, hover_text_color: Color, hover_secondary_text_color: Color, + hover_hotkey_background_color: Color, + hover_hotkey_text_color: Color, } impl MainWindowTheme { @@ -311,6 +333,12 @@ impl MainWindowTheme { pub const ENV_HOVER_SECONDARY_TEXT_COLOR: Key = Key::new("software.browsers.theme.main.hover_secondary_text_color"); + pub const ENV_HOVER_HOTKEY_BACKGROUND_COLOR: Key = + Key::new("software.browsers.theme.main.hover_hotkey_background_color"); + + pub const ENV_HOVER_HOTKEY_TEXT_COLOR: Key = + Key::new("software.browsers.theme.main.hover_hotkey_text_color"); + fn set_env_to_theme(&self, env: &mut Env) { env.set(Self::ENV_WINDOW_BACKGROUND_COLOR, self.window_background_color); env.set(Self::ENV_WINDOW_BORDER_COLOR, self.window_border_color); @@ -337,6 +365,14 @@ impl MainWindowTheme { Self::ENV_HOVER_SECONDARY_TEXT_COLOR, self.hover_secondary_text_color, ); + env.set( + Self::ENV_HOVER_HOTKEY_BACKGROUND_COLOR, + self.hover_hotkey_background_color, + ); + env.set( + Self::ENV_HOVER_HOTKEY_TEXT_COLOR, + self.hover_hotkey_text_color, + ); } } diff --git a/src/utils.rs b/src/utils.rs index 2bcf9628..a11f2ccf 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -113,6 +113,10 @@ pub struct CustomTheme { pub primary_font_family: String, pub secondary_font_size: String, pub secondary_font_family: String, + pub hotkey_background: String, + pub hotkey_text: String, + pub hover_hotkey_background: String, + pub hover_hotkey_text: String, } impl Default for CustomTheme { @@ -131,6 +135,10 @@ impl Default for CustomTheme { primary_font_family: "default".to_string(), secondary_font_size: "11.0".to_string(), secondary_font_family: "default".to_string(), + hotkey_background: "#292929".to_string(), + hotkey_text: "#808080".to_string(), + hover_hotkey_background: "#292929".to_string(), + hover_hotkey_text: "#ffffff".to_string(), } } } From 96d041f29ecd4d51380e7416fd76f8acf33128e3 Mon Sep 17 00:00:00 2001 From: Jon Rogers <67245+devnullvoid@users.noreply.github.com> Date: Sun, 23 Nov 2025 04:09:27 -0500 Subject: [PATCH 7/7] refactor: use theme constants for environment label styling - Replace hardcoded font size and color with `MainWindowTheme::ENV_PROFILE_LABEL_FONT` and `ENV_PROFILE_LABEL_COLOR`. - Aligns label appearance with the app's theming system for consistent UI. - Simplifies future theme customizations and reduces duplicated style values. --- src/gui/main_window.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/main_window.rs b/src/gui/main_window.rs index 23b7f073..b3fcae94 100644 --- a/src/gui/main_window.rs +++ b/src/gui/main_window.rs @@ -116,8 +116,8 @@ impl MainWindow { const BOTTOM_ROW_HEIGHT: f64 = 18.0; let url_label = Label::dynamic(|data: &UIState, _| ellipsize(data.url.as_str(), 28)) - .with_text_size(12.0) - .with_text_color(Color::from_hex_str("808080").unwrap()) + .with_font(MainWindowTheme::ENV_PROFILE_LABEL_FONT) + .with_text_color(MainWindowTheme::ENV_PROFILE_LABEL_COLOR) .with_line_break_mode(LineBreaking::Clip) .with_text_alignment(TextAlignment::Start) .fix_height(BOTTOM_ROW_HEIGHT)