diff --git a/src/gui/focus_widget.rs b/src/gui/focus_widget.rs index 69d68e8..ae820d5 100644 --- a/src/gui/focus_widget.rs +++ b/src/gui/focus_widget.rs @@ -10,11 +10,16 @@ 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, } impl FocusWidget {} @@ -29,8 +34,21 @@ 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 + } } impl> Widget for FocusWidget { @@ -48,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 @@ -93,7 +119,17 @@ impl> Widget for FocusWidget { _ => {} } - self.inner.event(ctx, event, data, env); + let mut local_env = env.clone(); + 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); + } + } + self.inner.event(ctx, event, data, &local_env); } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &S, env: &Env) { @@ -104,15 +140,17 @@ 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) 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(); } LifeCycle::HotChanged(to_hot) => { if *to_hot && !ctx.has_focus() { @@ -125,27 +163,71 @@ 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); + } } } _ => {} } - self.inner.lifecycle(ctx, event, data, env); + let mut local_env = env.clone(); + 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); + } + } + 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() != 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); + } + } + 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(); + // 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); + } + } + self.inner.layout(ctx, bc, data, &local_env) } 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); + 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 2569661..b3fcae9 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) @@ -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 - }, - ) - .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); +fn create_browser_label() -> impl Widget<((bool, UISettings), UIBrowser)> { + let label_fn = |((_incognito_mode, _), item): &((bool, UISettings), UIBrowser), _env: &_| { + item.browser_name.clone() + }; - browser_label + 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), + ) } 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,21 +426,30 @@ 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 = Container::new(hotkey_label) - .background(MainWindowTheme::ENV_HOTKEY_BACKGROUND_COLOR) - .rounded(5.0) - .border(MainWindowTheme::ENV_HOTKEY_BORDER_COLOR, 0.5); + }; + + let hotkey_label = Either::new( + |(_, item): &((bool, UISettings), UIBrowser), _| item.is_focused, + 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) + .background(MainWindowTheme::ENV_HOTKEY_BACKGROUND_COLOR) + .rounded(5.0) + .border(MainWindowTheme::ENV_HOTKEY_BORDER_COLOR, 0.5), + ); hotkey_label }, @@ -459,11 +479,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 = Color::rgba(1.0, 1.0, 1.0, 0.25); - 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() { @@ -476,7 +498,21 @@ fn create_browser( .ok(); } }, - ); + ) + .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 new file mode 100644 index 0000000..070d3b4 --- /dev/null +++ b/src/gui/settings_window/appearance_view.rs @@ -0,0 +1,230 @@ +use druid::widget::{ + ControllerHost, CrossAxisAlignment, Flex, Label, RadioGroup, TextBox, +}; +use druid::{Color, 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()); + + 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()); + + 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()); + + 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) + .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) + .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) + .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) + .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) + .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 { + 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 ddae230..bf02a92 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}; @@ -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 d5328ad..be06f9c 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 2b78b24..c60402b 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(), } } @@ -128,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(); } @@ -238,7 +242,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 +262,7 @@ pub enum SettingsTab { GENERAL, RULES, ADVANCED, + APPEARANCE, } impl UISettings { @@ -361,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 { @@ -686,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 b6aedbd..1d03d94 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 druid::{Color, Data, Env, FontDescriptor, FontFamily, 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()), } } @@ -76,14 +78,19 @@ 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), 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), + 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), @@ -131,14 +138,19 @@ 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), 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), + 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), @@ -155,6 +167,74 @@ 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; + } + + if let Ok(color) = parse_color(&custom.hover_background) { + 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; + } + + 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; + } + + 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 + } }; return theme; @@ -198,14 +278,19 @@ 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, hotkey_text_color: Color, options_button_text_color: Color, + 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 { @@ -215,14 +300,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"); @@ -239,12 +324,27 @@ 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"); + + 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"); + + 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); - 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); @@ -253,6 +353,26 @@ impl MainWindowTheme { Self::ENV_OPTIONS_BUTTON_TEXT_COLOR, self.options_button_text_color, ); + env.set( + 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, + ); + 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, + ); } } @@ -404,3 +524,25 @@ 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)) +} + +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/lib.rs b/src/lib.rs index 29f408c..d4e49d1 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 e1c66d4..a11f2cc 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,63 @@ 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, + pub hover_background: String, + 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, + pub hotkey_background: String, + pub hotkey_text: String, + pub hover_hotkey_background: String, + pub hover_hotkey_text: String, +} + +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(), + hover_background: "#ffffff40".to_string(), // 25% white + 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(), + hotkey_background: "#292929".to_string(), + hotkey_text: "#808080".to_string(), + hover_hotkey_background: "#292929".to_string(), + hover_hotkey_text: "#ffffff".to_string(), + } + } } #[derive(Serialize, Deserialize, Debug, Default, Clone)]