From 4ab3d2a1e1e3f45552edcb4ec26a0496487699b2 Mon Sep 17 00:00:00 2001 From: unsecretised Date: Thu, 11 Jun 2026 23:56:02 +0800 Subject: [PATCH 1/2] basic improved settings UI written by heavily guided AI fixes #253 --- src/app.rs | 49 +- src/app/pages/settings.rs | 1081 +++++++++++++++++++++---------------- src/app/tile.rs | 2 + src/app/tile/elm.rs | 6 +- src/app/tile/update.rs | 157 +++++- src/styles.rs | 66 +++ 6 files changed, 884 insertions(+), 477 deletions(-) diff --git a/src/app.rs b/src/app.rs index 99c740d..a42522e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -41,6 +41,50 @@ pub enum Page { Settings, } +/// The settings panel tabs +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SettingsTab { + General, + Appearance, + Commands, +} + +/// Actions that open a native file dialog +#[derive(Debug, Clone)] +pub enum FileDialogAction { + PickModeFile(String), + EditSearchDir(String), + AddSearchDir, +} + +/// Config fields that can be individually reset to default +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ResetField { + ToggleHotkey, + ClipboardHotkey, + Placeholder, + SearchUrl, + DebounceDelay, + StartAtLogin, + AutoUpdate, + HapticFeedback, + ShowMenubarIcon, + ClipboardHistory, + MainPage, + ShowScrollbar, + ClearOnHide, + ClearOnEnter, + ShowIcons, + Font, + EventDuration, + TextColor, + BackgroundColor, + Aliases, + Modes, + SearchDirs, + ShellCommands, +} + impl std::fmt::Display for Page { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self.to_owned() { @@ -98,8 +142,11 @@ pub enum Message { RunFunction(Function), OpenFocused, SetConfig(SetConfigFields), - OpenFileDialogue(String), + OpenFileDialog(FileDialogAction), + FileDialogResult(Option>), ReturnFocus, + SwitchSettingsTab(SettingsTab), + ResetField(ResetField), EscKeyPressed(Id), UpdateEvents, ClearSearchResults, diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 71fb374..36d307e 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -2,504 +2,713 @@ use std::collections::HashMap; +use iced::border::Radius; use iced::widget::Slider; use iced::widget::Space; use iced::widget::TextInput; +use iced::widget::button; use iced::widget::checkbox; use iced::widget::radio; use iced::widget::text_input; +use iced::Border; + +use crate::styles::tint; +use crate::styles::with_alpha; use crate::app::Editable; +use crate::app::FileDialogAction; +use crate::app::ResetField; use crate::app::SetConfigBufferFields; use crate::app::SetConfigThemeFields; +use crate::app::SettingsTab; use crate::commands::Function; use crate::config::MainPage; use crate::config::Shelly; use crate::styles::delete_button_style; use crate::styles::settings_add_button_style; use crate::styles::settings_checkbox_style; +use crate::styles::settings_container_style; use crate::styles::settings_radio_button_style; use crate::styles::settings_save_button_style; use crate::styles::settings_slider_style; +use crate::styles::settings_tab_style; use crate::styles::settings_text_input_item_style; use crate::{ app::{SetConfigFields, pages::prelude::*}, config::Config, }; -const SETTINGS_ITEM_PADDING: u16 = 5; -const SETTINGS_ITEM_HEIGHT: u32 = 80; -const SETTINGS_ITEM_COL_SPACING: u32 = 5; +const SETTINGS_ITEM_PADDING: u16 = 4; +const SETTINGS_ITEM_HEIGHT: u32 = 55; +const SETTINGS_ITEM_COL_SPACING: u32 = 3; -pub fn settings_page(config: Config) -> Element<'static, Message> { +pub fn settings_page(config: Config, settings_tab: SettingsTab) -> Element<'static, Message> { let config = Box::new(config.clone()); let theme = config.theme.clone(); - let hotkey_theme = theme.clone(); - let hotkey = settings_item_column([ - settings_hint_text(theme.clone(), "Toggle hotkey"), - text_input("Toggle Hotkey", &config.toggle_hotkey) - .on_input(|input| Message::SetConfig(SetConfigFields::ToggleHotkey(input.clone()))) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) - .style(move |_, _| settings_text_input_item_style(&hotkey_theme)) - .into(), - notice_item(theme.clone(), "Use \"+\" as a seperator"), - ]); - - let cb_theme = theme.clone(); - let cb_hotkey = settings_item_column([ - settings_hint_text(theme.clone(), "Clipboard hotkey"), - text_input("Clipboard Hotkey", &config.clipboard_hotkey) - .on_input(|input| Message::SetConfig(SetConfigFields::ClipboardHotkey(input.clone()))) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) - .style(move |_, _| settings_text_input_item_style(&cb_theme)) - .into(), - notice_item(theme.clone(), "Use \"+\" as a seperator"), - ]); - - let placeholder_theme = theme.clone(); - let placeholder_setting = settings_item_column([ - settings_hint_text(theme.clone(), "Set the rustcast placeholder"), - text_input("Set Placeholder", &config.placeholder) - .on_input(|input| Message::SetConfig(SetConfigFields::PlaceHolder(input.clone()))) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) - .style(move |_, _| settings_text_input_item_style(&placeholder_theme)) - .into(), - notice_item(theme.clone(), "What the text box shows when its empty"), - ]); + let tabs_row = Row::from_iter([ + tab_button("General", SettingsTab::General, settings_tab, theme.clone()), + tab_button("Appearance", SettingsTab::Appearance, settings_tab, theme.clone()), + tab_button("Commands", SettingsTab::Commands, settings_tab, theme.clone()), + ]) + .spacing(2) + .width(Length::Fill); + + let tab_content: Column<'static, Message> = match settings_tab { + SettingsTab::General => general_tab(config.clone(), theme.clone()), + SettingsTab::Appearance => appearance_tab(config.clone(), theme.clone()), + SettingsTab::Commands => commands_tab(config.clone(), theme.clone()), + }; + let items = Column::from_iter([ + tabs_row.into(), + tab_content.into(), + Space::new().height(10).into(), + Row::from_iter([ + savebutton(theme.clone()), + copy_config_button(config), + wiki_button(theme.clone()), + ]) + .spacing(5) + .width(Length::Fill) + .into(), + ]) + .spacing(10); + + container(items) + .style(move |_| settings_container_style(&theme)) + .height(Length::Fill) + .width(Length::Fill) + .padding(12) + .align_x(Alignment::Center) + .into() +} + +fn tab_button( + label: &'static str, + tab: SettingsTab, + active: SettingsTab, + theme: crate::config::Theme, +) -> Element<'static, Message> { + let is_active = tab == active; let theme_clone = theme.clone(); - let search = settings_item_column([ - settings_hint_text(theme.clone(), "Set the search URL"), - text_input("Set Search URL", &config.search_url) - .on_input(|input| Message::SetConfig(SetConfigFields::SearchUrl(input.clone()))) - .on_submit(Message::WriteConfig(false)) + Button::new( + Text::new(label) + .align_x(Alignment::Center) .width(Length::Fill) - .style(move |_, _| settings_text_input_item_style(&theme_clone)) - .into(), - notice_item(theme.clone(), "Which search engine to use (%s = query)"), - ]); + .font(theme.font()), + ) + .style(move |_, status| settings_tab_style(&theme_clone, is_active, status)) + .width(Length::Fill) + .on_press(Message::SwitchSettingsTab(tab)) + .into() +} +fn reset_button(theme: crate::config::Theme, field: ResetField) -> Element<'static, Message> { let theme_clone = theme.clone(); - let clipboard_history = Row::from_iter([ - settings_hint_text(theme.clone(), "Enable Clipboard history"), - checkbox(config.clone().cbhist) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(|input| Message::SetConfig(SetConfigFields::ClipboardHistory(input))) - .into(), - notice_item( - theme.clone(), - "If you want your clipboard history to be stored", - ), - ]) - .align_y(Alignment::Center) - .spacing(SETTINGS_ITEM_COL_SPACING * 2) - .padding(SETTINGS_ITEM_PADDING) - .height(SETTINGS_ITEM_HEIGHT); + Button::new( + Text::new("R") + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .size(13) + .font(theme.font()), + ) + .style(move |_, _| { + button::Style { + text_color: theme_clone.text_color(0.5), + background: Some(Background::Color(with_alpha( + tint(theme_clone.bg_color(), 0.06), + 0.20, + ))), + border: Border { + color: theme_clone.text_color(0.15), + width: 0.5, + radius: Radius::new(4), + }, + ..Default::default() + } + }) + .width(30) + .height(26) + .on_press(Message::ResetField(field)) + .into() +} +fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'static, Message> { let theme_clone = theme.clone(); - let current_delay = config.debounce_delay; - let debounce = settings_item_column([ - settings_hint_text(theme.clone(), "Set the debounce time"), - text_input("Set Debounce time (ms)", &config.debounce_delay.to_string()) - .on_input(move |input: String| { - let delay = input.parse::().unwrap_or(current_delay); - Message::SetConfig(SetConfigFields::DebounceDelay(delay)) - }) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) - .style(move |_, _| settings_text_input_item_style(&theme_clone)) - .into(), - notice_item( - theme.clone(), - "How quickly you want file searching to return a value", - ), - ]); + let hotkey = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Toggle hotkey"), + text_input("Toggle Hotkey", &config.toggle_hotkey) + .on_input(|input| Message::SetConfig(SetConfigFields::ToggleHotkey(input.clone()))) + .on_submit(Message::WriteConfig(false)) + .width(Length::Fill) + .style(move |_, _| settings_text_input_item_style(&theme_clone)) + .into(), + notice_item(theme.clone(), "Use \"+\" as a seperator"), + ]), + ResetField::ToggleHotkey, + theme.clone(), + ); let theme_clone = theme.clone(); - let start_at_login = settings_item_row([ - settings_hint_text(theme.clone(), "Start at login"), - checkbox(config.clone().start_at_login) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(Message::ToggleAutoStartup) - .into(), - notice_item(theme.clone(), "If you want rustcast to start on login"), - ]); + let cb_hotkey = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Clipboard hotkey"), + text_input("Clipboard Hotkey", &config.clipboard_hotkey) + .on_input(|input| { + Message::SetConfig(SetConfigFields::ClipboardHotkey(input.clone())) + }) + .on_submit(Message::WriteConfig(false)) + .width(Length::Fill) + .style(move |_, _| settings_text_input_item_style(&theme_clone)) + .into(), + notice_item(theme.clone(), "Use \"+\" as a seperator"), + ]), + ResetField::ClipboardHotkey, + theme.clone(), + ); let theme_clone = theme.clone(); - let auto_update = settings_item_row([ - settings_hint_text(theme.clone(), "Auto update"), - checkbox(config.clone().auto_update) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(move |input| Message::SetConfig(SetConfigFields::SetAutoUpdate(input))) - .into(), - notice_item( - theme.clone(), - "If rustcast should automatically update itself", - ), - ]); + let placeholder_setting = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Set the rustcast placeholder"), + text_input("Set Placeholder", &config.placeholder) + .on_input(|input| { + Message::SetConfig(SetConfigFields::PlaceHolder(input.clone())) + }) + .on_submit(Message::WriteConfig(false)) + .width(Length::Fill) + .style(move |_, _| settings_text_input_item_style(&theme_clone)) + .into(), + notice_item(theme.clone(), "What the text box shows when its empty"), + ]), + ResetField::Placeholder, + theme.clone(), + ); let theme_clone = theme.clone(); - let haptic = Row::from_iter([ - settings_hint_text(theme.clone(), "Haptic feedback"), - checkbox(config.clone().haptic_feedback) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(|input| Message::SetConfig(SetConfigFields::HapticFeedback(input))) - .into(), - notice_item( - theme.clone(), - "If there should be haptic feedback when you type", - ), - ]) - .align_y(Alignment::Center) - .spacing(SETTINGS_ITEM_COL_SPACING * 2) - .padding(SETTINGS_ITEM_PADDING) - .height(SETTINGS_ITEM_HEIGHT); + let search = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Set the search URL"), + text_input("Set Search URL", &config.search_url) + .on_input(|input| { + Message::SetConfig(SetConfigFields::SearchUrl(input.clone())) + }) + .on_submit(Message::WriteConfig(false)) + .width(Length::Fill) + .style(move |_, _| settings_text_input_item_style(&theme_clone)) + .into(), + notice_item(theme.clone(), "Which search engine to use (%s = query)"), + ]), + ResetField::SearchUrl, + theme.clone(), + ); let theme_clone = theme.clone(); - let tray_icon = settings_item_row([ - settings_hint_text(theme.clone(), "Show menubar icon"), - checkbox(config.clone().show_trayicon) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(|input| Message::SetConfig(SetConfigFields::ShowMenubarIcon(input))) - .into(), - notice_item( - theme.clone(), - "If the menubar icon should be shown in rustcast", - ), - ]); + let current_delay = config.debounce_delay; + let debounce = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Set the debounce time"), + text_input("Set Debounce time (ms)", &config.debounce_delay.to_string()) + .on_input(move |input: String| { + let delay = input.parse::().unwrap_or(current_delay); + Message::SetConfig(SetConfigFields::DebounceDelay(delay)) + }) + .on_submit(Message::WriteConfig(false)) + .width(Length::Fill) + .style(move |_, _| settings_text_input_item_style(&theme_clone)) + .into(), + notice_item( + theme.clone(), + "How quickly you want file searching to return a value", + ), + ]), + ResetField::DebounceDelay, + theme.clone(), + ); let theme_clone = theme.clone(); - let auto_suggest = settings_item_column([ - settings_hint_text(theme.clone(), "Suggestions on open"), + let start_at_login = settings_row_with_reset( settings_item_row([ - radio( - "Favourites", - MainPage::Favourites, - Some(config.main_page), - |page| Message::SetConfig(SetConfigFields::SetPage(page)), - ) - .style({ - let theme_clone = theme_clone.clone(); - move |_, _| settings_radio_button_style(&theme_clone.clone()) - }) - .into(), - radio( - "Frequents", - MainPage::FrequentlyUsed, - Some(config.main_page), - |page| Message::SetConfig(SetConfigFields::SetPage(page)), - ) - .style({ - let theme_clone = theme_clone.clone(); - move |_, _| settings_radio_button_style(&theme_clone.clone()) - }) - .into(), - radio("Events", MainPage::Events, Some(config.main_page), |page| { - Message::SetConfig(SetConfigFields::SetPage(page)) - }) - .style({ - let theme_clone = theme_clone.clone(); - move |_, _| settings_radio_button_style(&theme_clone.clone()) - }) - .into(), - radio("Nothing", MainPage::Blank, Some(config.main_page), |page| { - Message::SetConfig(SetConfigFields::SetPage(page)) - }) - .style(move |_, _| settings_radio_button_style(&theme_clone.clone())) - .into(), + settings_hint_text(theme.clone(), "Start at login"), + checkbox(config.clone().start_at_login) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(Message::ToggleAutoStartup) + .into(), + notice_item(theme.clone(), "If you want rustcast to start on login"), + ]), + ResetField::StartAtLogin, + theme.clone(), + ); + + let theme_clone = theme.clone(); + let auto_update = settings_row_with_reset( + settings_item_row([ + settings_hint_text(theme.clone(), "Auto update"), + checkbox(config.clone().auto_update) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(move |input| { + Message::SetConfig(SetConfigFields::SetAutoUpdate(input)) + }) + .into(), + notice_item( + theme.clone(), + "If rustcast should automatically update itself", + ), + ]), + ResetField::AutoUpdate, + theme.clone(), + ); + + let theme_clone = theme.clone(); + let haptic = settings_row_with_reset( + Row::from_iter([ + settings_hint_text(theme.clone(), "Haptic feedback"), + checkbox(config.clone().haptic_feedback) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(|input| { + Message::SetConfig(SetConfigFields::HapticFeedback(input)) + }) + .into(), + notice_item( + theme.clone(), + "If there should be haptic feedback when you type", + ), ]) - .spacing(30) - .into(), - notice_item(theme.clone(), "What an empty query should show"), - ]); + .align_y(Alignment::Center) + .spacing(SETTINGS_ITEM_COL_SPACING * 2) + .padding(SETTINGS_ITEM_PADDING) + .height(SETTINGS_ITEM_HEIGHT), + ResetField::HapticFeedback, + theme.clone(), + ); let theme_clone = theme.clone(); - let show_scrollbar = settings_item_row([ - settings_hint_text(theme.clone(), "Show scrollbar"), - checkbox(config.theme.show_scroll_bar) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(|input| { - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::ShowScrollBar(input), - )) - }) - .into(), - notice_item(theme.clone(), "If there should be a scrollbar"), - ]); + let tray_icon = settings_row_with_reset( + settings_item_row([ + settings_hint_text(theme.clone(), "Show menubar icon"), + checkbox(config.clone().show_trayicon) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(|input| { + Message::SetConfig(SetConfigFields::ShowMenubarIcon(input)) + }) + .into(), + notice_item( + theme.clone(), + "If the menubar icon should be shown in rustcast", + ), + ]), + ResetField::ShowMenubarIcon, + theme.clone(), + ); let theme_clone = theme.clone(); - let clear_on_hide = settings_item_row([ - settings_hint_text(theme.clone(), "Clear on hide"), - checkbox(config.clone().buffer_rules.clear_on_hide) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(move |input| { - Message::SetConfig(SetConfigFields::SetBufferFields( - SetConfigBufferFields::ClearOnHide(input), - )) - }) - .into(), - notice_item( - theme.clone(), - "If the query should be cleared when rustcast is hidden", - ), - ]); + let clipboard_history = settings_row_with_reset( + Row::from_iter([ + settings_hint_text(theme.clone(), "Enable Clipboard history"), + checkbox(config.clone().cbhist) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(|input| { + Message::SetConfig(SetConfigFields::ClipboardHistory(input)) + }) + .into(), + notice_item( + theme.clone(), + "If you want your clipboard history to be stored", + ), + ]) + .align_y(Alignment::Center) + .spacing(SETTINGS_ITEM_COL_SPACING * 2) + .padding(SETTINGS_ITEM_PADDING) + .height(SETTINGS_ITEM_HEIGHT), + ResetField::ClipboardHistory, + theme.clone(), + ); let theme_clone = theme.clone(); - let clear_on_enter = settings_item_row([ - settings_hint_text(theme.clone(), "Clear on enter"), - checkbox(config.clone().buffer_rules.clear_on_enter) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(move |input| { - Message::SetConfig(SetConfigFields::SetBufferFields( - SetConfigBufferFields::ClearOnEnter(input), - )) - }) + let auto_suggest = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Suggestions on open"), + settings_item_row([ + radio( + "Favourites", + MainPage::Favourites, + Some(config.main_page), + |page| Message::SetConfig(SetConfigFields::SetPage(page)), + ) + .style({ + let theme_clone = theme_clone.clone(); + move |_, _| settings_radio_button_style(&theme_clone.clone()) + }) + .into(), + radio( + "Frequents", + MainPage::FrequentlyUsed, + Some(config.main_page), + |page| Message::SetConfig(SetConfigFields::SetPage(page)), + ) + .style({ + let theme_clone = theme_clone.clone(); + move |_, _| settings_radio_button_style(&theme_clone.clone()) + }) + .into(), + radio( + "Events", + MainPage::Events, + Some(config.main_page), + |page| Message::SetConfig(SetConfigFields::SetPage(page)), + ) + .style({ + let theme_clone = theme_clone.clone(); + move |_, _| settings_radio_button_style(&theme_clone.clone()) + }) + .into(), + radio( + "Nothing", + MainPage::Blank, + Some(config.main_page), + |page| Message::SetConfig(SetConfigFields::SetPage(page)), + ) + .style(move |_, _| settings_radio_button_style(&theme_clone.clone())) + .into(), + ]) + .spacing(30) .into(), - notice_item( - theme.clone(), - "If the query should be cleared when an app is opened", - ), - ]); + notice_item(theme.clone(), "What an empty query should show"), + ]), + ResetField::MainPage, + theme.clone(), + ); + + Column::from_iter([ + hotkey, cb_hotkey, placeholder_setting, search, debounce, start_at_login, auto_update, + haptic, tray_icon, clipboard_history, auto_suggest, + ]) + .spacing(10) +} +fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'static, Message> { let theme_clone = theme.clone(); - let show_icons = settings_item_row([ - settings_hint_text(theme.clone(), "Show icons"), - checkbox(config.clone().theme.show_icons) - .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(move |input| { - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::ShowIcons(input), - )) - }) - .into(), - notice_item(theme.clone(), "If you want app icons to be visible"), - ]); + let show_scrollbar = settings_row_with_reset( + settings_item_row([ + settings_hint_text(theme.clone(), "Show scrollbar"), + checkbox(config.theme.show_scroll_bar) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(|input| { + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::ShowScrollBar(input), + )) + }) + .into(), + notice_item(theme.clone(), "If there should be a scrollbar"), + ]), + ResetField::ShowScrollbar, + theme.clone(), + ); let theme_clone = theme.clone(); - let font_family = settings_item_column([ - settings_hint_text(theme.clone(), "Set Font family"), - text_input( - "Font family", - &config.theme.font.clone().unwrap_or("".to_string()), - ) - .on_input(move |input: String| { - Message::SetConfig(SetConfigFields::SetThemeFields(SetConfigThemeFields::Font( - input, - ))) - }) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) - .style(move |_, _| settings_text_input_item_style(&theme_clone)) - .into(), - notice_item(theme.clone(), "What font rustcast should use"), - ]); + let clear_on_hide = settings_row_with_reset( + settings_item_row([ + settings_hint_text(theme.clone(), "Clear on hide"), + checkbox(config.clone().buffer_rules.clear_on_hide) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(move |input| { + Message::SetConfig(SetConfigFields::SetBufferFields( + SetConfigBufferFields::ClearOnHide(input), + )) + }) + .into(), + notice_item( + theme.clone(), + "If the query should be cleared when rustcast is hidden", + ), + ]), + ResetField::ClearOnHide, + theme.clone(), + ); let theme_clone = theme.clone(); - let event_duration = settings_item_column([ - settings_hint_text(theme.clone(), "Set Event duration"), - text_input("Event duration", &config.event_duration.to_string()) + let clear_on_enter = settings_row_with_reset( + settings_item_row([ + settings_hint_text(theme.clone(), "Clear on enter"), + checkbox(config.clone().buffer_rules.clear_on_enter) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(move |input| { + Message::SetConfig(SetConfigFields::SetBufferFields( + SetConfigBufferFields::ClearOnEnter(input), + )) + }) + .into(), + notice_item( + theme.clone(), + "If the query should be cleared when an app is opened", + ), + ]), + ResetField::ClearOnEnter, + theme.clone(), + ); + + let theme_clone = theme.clone(); + let show_icons = settings_row_with_reset( + settings_item_row([ + settings_hint_text(theme.clone(), "Show icons"), + checkbox(config.clone().theme.show_icons) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(move |input| { + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::ShowIcons(input), + )) + }) + .into(), + notice_item(theme.clone(), "If you want app icons to be visible"), + ]), + ResetField::ShowIcons, + theme.clone(), + ); + + let theme_clone = theme.clone(); + let font_family = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Set Font family"), + text_input( + "Font family", + &config.theme.font.clone().unwrap_or("".to_string()), + ) .on_input(move |input: String| { - Message::SetConfig(SetConfigFields::SetEventDuration(input)) + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::Font(input), + )) }) .on_submit(Message::WriteConfig(false)) .width(Length::Fill) .style(move |_, _| settings_text_input_item_style(&theme_clone)) .into(), - notice_item( - theme.clone(), - "How many minutes from now the events should be displayed", - ), - ]); + notice_item(theme.clone(), "What font rustcast should use"), + ]), + ResetField::Font, + theme.clone(), + ); + + let theme_clone = theme.clone(); + let event_duration = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Set Event duration"), + text_input("Event duration", &config.event_duration.to_string()) + .on_input(move |input: String| { + Message::SetConfig(SetConfigFields::SetEventDuration(input)) + }) + .on_submit(Message::WriteConfig(false)) + .width(Length::Fill) + .style(move |_, _| settings_text_input_item_style(&theme_clone)) + .into(), + notice_item( + theme.clone(), + "How many minutes from now the events should be displayed", + ), + ]), + ResetField::EventDuration, + theme.clone(), + ); let theme_clone = theme.clone(); let theme_clone_1 = theme.clone(); let theme_clone_2 = theme.clone(); let theme_clone_3 = theme.clone(); - let text_clr = Column::from_iter([ - settings_hint_text(theme.clone(), "Set text colour"), + let text_clr = settings_row_with_reset( Column::from_iter([ - settings_hint_text( - theme.clone(), - format!("R value: {}", theme_clone.text_color.0), - ), - Slider::new( - 0..=100, - (theme_clone.text_color.0 * 100.) as i32, - move |change| { - let txt_clr = theme_clone.text_color; - let change = change as f32 / 100.; - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::TextColor(change, txt_clr.1, txt_clr.2), - )) - }, - ) - .style(move |_, _| settings_slider_style(&theme_clone_1)) - .width((WINDOW_WIDTH / 5.) * 4.) - .into(), - settings_hint_text( - theme.clone(), - format!("G value: {}", theme_clone.text_color.1), - ), - Slider::new( - 0..=100, - (theme_clone.text_color.1 * 100.) as i32, - move |change| { - let txt_clr = theme_clone.text_color; - let change = change as f32 / 100.; - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::TextColor(txt_clr.0, change, txt_clr.2), - )) - }, - ) - .style(move |_, _| settings_slider_style(&theme_clone_2)) - .width((WINDOW_WIDTH / 5.) * 4.) - .into(), - settings_hint_text( - theme.clone(), - format!("B value: {}", theme_clone.text_color.2), - ), - Slider::new( - 0..=100, - (theme_clone.text_color.2 * 100.) as i32, - move |change| { - let txt_clr = theme_clone.text_color; - let change = change as f32 / 100.; - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::TextColor(txt_clr.0, txt_clr.1, change), - )) - }, - ) - .style(move |_, _| settings_slider_style(&theme_clone_3)) - .width((WINDOW_WIDTH / 5.) * 4.) + settings_hint_text(theme.clone(), "Set text colour"), + Column::from_iter([ + settings_hint_text( + theme.clone(), + format!("R value: {}", theme_clone.text_color.0), + ), + Slider::new( + 0..=100, + (theme_clone.text_color.0 * 100.) as i32, + move |change| { + let txt_clr = theme_clone.text_color; + let change = change as f32 / 100.; + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::TextColor(change, txt_clr.1, txt_clr.2), + )) + }, + ) + .style(move |_, _| settings_slider_style(&theme_clone_1)) + .width((WINDOW_WIDTH / 5.) * 4.) + .into(), + settings_hint_text( + theme.clone(), + format!("G value: {}", theme_clone.text_color.1), + ), + Slider::new( + 0..=100, + (theme_clone.text_color.1 * 100.) as i32, + move |change| { + let txt_clr = theme_clone.text_color; + let change = change as f32 / 100.; + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::TextColor(txt_clr.0, change, txt_clr.2), + )) + }, + ) + .style(move |_, _| settings_slider_style(&theme_clone_2)) + .width((WINDOW_WIDTH / 5.) * 4.) + .into(), + settings_hint_text( + theme.clone(), + format!("B value: {}", theme_clone.text_color.2), + ), + Slider::new( + 0..=100, + (theme_clone.text_color.2 * 100.) as i32, + move |change| { + let txt_clr = theme_clone.text_color; + let change = change as f32 / 100.; + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::TextColor(txt_clr.0, txt_clr.1, change), + )) + }, + ) + .style(move |_, _| settings_slider_style(&theme_clone_3)) + .width((WINDOW_WIDTH / 5.) * 4.) + .into(), + notice_item(theme.clone(), "Text colour in RGB format"), + ]) + .spacing(7) + .width(Length::Fill) + .align_x(Alignment::Center) .into(), - notice_item(theme.clone(), "Text colour in RGB format"), - ]) - .spacing(7) - .width(Length::Fill) - .align_x(Alignment::Center) - .into(), - ]); + ]), + ResetField::TextColor, + theme.clone(), + ); let theme_clone = theme.clone(); let theme_clone_1 = theme.clone(); let theme_clone_2 = theme.clone(); let theme_clone_3 = theme.clone(); - let bg_clr = Column::from_iter([ - settings_hint_text(theme.clone(), "Set background colour"), + let bg_clr = settings_row_with_reset( Column::from_iter([ - settings_hint_text( - theme.clone(), - format!("R value: {}", theme_clone.background_color.0), - ), - Slider::new( - 0..=100, - (theme_clone.background_color.0 * 100.) as i32, - move |change| { - let txt_clr = theme_clone.background_color; - let change = change as f32 / 100.; - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::BackgroundColor(change, txt_clr.1, txt_clr.2), - )) - }, - ) - .style(move |_, _| settings_slider_style(&theme_clone_1)) - .width((WINDOW_WIDTH / 5.) * 4.) - .into(), - settings_hint_text( - theme.clone(), - format!("G value: {}", theme_clone.background_color.1), - ), - Slider::new( - 0..=100, - (theme_clone.background_color.1 * 100.) as i32, - move |change| { - let txt_clr = theme_clone.background_color; - let change = change as f32 / 100.; - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::BackgroundColor(txt_clr.0, change, txt_clr.2), - )) - }, - ) - .style(move |_, _| settings_slider_style(&theme_clone_2)) - .width((WINDOW_WIDTH / 5.) * 4.) - .into(), - settings_hint_text( - theme.clone(), - format!("B value: {}", theme_clone.background_color.2), - ), - Slider::new( - 0..=100, - (theme_clone.background_color.2 * 100.) as i32, - move |change| { - let txt_clr = theme_clone.background_color; - let change = change as f32 / 100.; - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::BackgroundColor(txt_clr.0, txt_clr.1, change), - )) - }, - ) - .style(move |_, _| settings_slider_style(&theme_clone_3)) - .width((WINDOW_WIDTH / 5.) * 4.) + settings_hint_text(theme.clone(), "Set background colour"), + Column::from_iter([ + settings_hint_text( + theme.clone(), + format!("R value: {}", theme_clone.background_color.0), + ), + Slider::new( + 0..=100, + (theme_clone.background_color.0 * 100.) as i32, + move |change| { + let txt_clr = theme_clone.background_color; + let change = change as f32 / 100.; + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::BackgroundColor( + change, txt_clr.1, txt_clr.2, + ), + )) + }, + ) + .style(move |_, _| settings_slider_style(&theme_clone_1)) + .width((WINDOW_WIDTH / 5.) * 4.) + .into(), + settings_hint_text( + theme.clone(), + format!("G value: {}", theme_clone.background_color.1), + ), + Slider::new( + 0..=100, + (theme_clone.background_color.1 * 100.) as i32, + move |change| { + let txt_clr = theme_clone.background_color; + let change = change as f32 / 100.; + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::BackgroundColor( + txt_clr.0, change, txt_clr.2, + ), + )) + }, + ) + .style(move |_, _| settings_slider_style(&theme_clone_2)) + .width((WINDOW_WIDTH / 5.) * 4.) + .into(), + settings_hint_text( + theme.clone(), + format!("B value: {}", theme_clone.background_color.2), + ), + Slider::new( + 0..=100, + (theme_clone.background_color.2 * 100.) as i32, + move |change| { + let txt_clr = theme_clone.background_color; + let change = change as f32 / 100.; + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::BackgroundColor( + txt_clr.0, txt_clr.1, change, + ), + )) + }, + ) + .style(move |_, _| settings_slider_style(&theme_clone_3)) + .width((WINDOW_WIDTH / 5.) * 4.) + .into(), + notice_item(theme.clone(), "Background colour in RGB format"), + ]) + .spacing(7) + .width(Length::Fill) + .align_x(Alignment::Center) .into(), - notice_item(theme.clone(), "Background colour in RGB format"), - ]) - .spacing(7) - .width(Length::Fill) - .align_x(Alignment::Center) - .into(), - ]); + ]), + ResetField::BackgroundColor, + theme.clone(), + ); - let items = Column::from_iter([ - hotkey.into(), - cb_hotkey.into(), - placeholder_setting.into(), - search.into(), - debounce.into(), - start_at_login.into(), - auto_update.into(), - haptic.into(), - tray_icon.into(), - clipboard_history.into(), - auto_suggest.into(), - show_scrollbar.into(), - clear_on_hide.into(), - clear_on_enter.into(), - show_icons.into(), - font_family.into(), - event_duration.into(), - text_clr.into(), - bg_clr.into(), - settings_hint_text(theme.clone(), "Aliases"), + Column::from_iter([ + show_scrollbar, clear_on_hide, clear_on_enter, show_icons, font_family, event_duration, + text_clr, bg_clr, + ]) + .spacing(10) +} + +fn commands_tab(config: Box, theme: crate::config::Theme) -> Column<'static, Message> { + Column::from_iter([ + section_header_with_reset("Aliases", ResetField::Aliases, theme.clone()), aliases_item(config.aliases.clone(), &theme), - settings_hint_text(theme.clone(), "Modes"), + section_header_with_reset("Modes", ResetField::Modes, theme.clone()), modes_item(config.modes.clone(), &theme), - settings_hint_text(theme.clone(), "Search Directories"), + section_header_with_reset("Search Directories", ResetField::SearchDirs, theme.clone()), search_dirs_item(&theme, config.search_dirs.clone()), - Space::new().height(30).into(), - settings_hint_text(theme.clone(), "Shell commands"), + Space::new().height(10).into(), + section_header_with_reset("Shell commands", ResetField::ShellCommands, theme.clone()), shell_commands_item(config.shells.clone(), theme.clone()), - Row::from_iter([ - savebutton(theme.clone()), - default_button(theme.clone()), - copy_config_button(config), - wiki_button(theme.clone()), - ]) - .spacing(5) - .width(Length::Fill) - .into(), ]) - .spacing(10); + .spacing(10) +} - container(items) - .style(move |_| result_row_container_style(&theme, false)) - .height(Length::Fill) +fn section_header_with_reset( + label: &'static str, + field: ResetField, + theme: crate::config::Theme, +) -> Element<'static, Message> { + Row::from_iter([ + settings_hint_text(theme.clone(), label), + reset_button(theme, field), + ]) + .align_y(Alignment::Center) + .spacing(5) + .width(Length::Fill) + .into() +} + +fn settings_row_with_reset( + content: impl Into>, + field: ResetField, + theme: crate::config::Theme, +) -> Element<'static, Message> { + Row::from_iter([content.into(), reset_button(theme, field)]) + .align_y(Alignment::Center) + .spacing(5) .width(Length::Fill) - .padding(10) - .align_x(Alignment::Center) .into() } @@ -516,19 +725,6 @@ fn savebutton(theme: Theme) -> Element<'static, Message> { .into() } -fn default_button(theme: Theme) -> Element<'static, Message> { - Button::new( - Text::new("To default") - .align_x(Alignment::Center) - .width(Length::Fill) - .font(theme.font()), - ) - .style(move |_, _| settings_save_button_style(&theme)) - .width(Length::Fill) - .on_press(Message::SetConfig(SetConfigFields::ToDefault)) - .into() -} - fn wiki_button(theme: Theme) -> Element<'static, Message> { Button::new( Text::new("Open file") @@ -577,7 +773,6 @@ fn settings_item_column( Column::from_iter(elems) .spacing(SETTINGS_ITEM_COL_SPACING) .padding(SETTINGS_ITEM_PADDING) - .height(SETTINGS_ITEM_HEIGHT) } fn settings_item_row( @@ -741,7 +936,9 @@ fn modes_item(modes: HashMap, theme: &Theme) -> Element<'static, }) .into(), Button::new(Text::new(display_val)) - .on_press(Message::OpenFileDialogue(key.to_owned())) + .on_press(Message::OpenFileDialog(FileDialogAction::PickModeFile( + key.to_owned(), + ))) .style(move |_, _| settings_add_button_style(&theme_clone_1.clone())) .into(), Button::new("Delete") @@ -779,43 +976,15 @@ fn modes_item(modes: HashMap, theme: &Theme) -> Element<'static, fn dir_picker_button(directory: String, dir: &str, theme: Theme) -> Button<'static, Message> { let home = std::env::var("HOME").unwrap_or("/".to_string()); Button::new(Text::new(dir.to_owned().replace(&home, "~"))) - .on_press_with(move || { - rfd::FileDialog::new() - .set_directory(home.clone()) - .set_can_create_directories(false) - .pick_folder() - .map(|path| { - let new = path.to_str().unwrap_or("").to_string(); - Message::SetConfig(SetConfigFields::SearchDirs(Editable::Update { - old: directory.clone(), - new, - })) - }) - .unwrap_or(Message::SetConfig(SetConfigFields::SearchDirs( - Editable::Update { - old: directory.clone(), - new: directory.clone(), - }, - ))) - }) + .on_press(Message::OpenFileDialog(FileDialogAction::EditSearchDir( + directory.clone(), + ))) .style(move |_, _| settings_add_button_style(&theme.clone())) } fn dir_adder_button(dir: &str, theme: Theme) -> Button<'static, Message> { Button::new(Text::new(dir.to_owned())) - .on_press_with(move || { - rfd::FileDialog::new() - .set_directory(std::env::var("HOME").unwrap_or("/".to_string())) - .set_can_create_directories(false) - .pick_folder() - .map(|path| { - let new = path.to_str().unwrap_or("").to_string(); - Message::SetConfig(SetConfigFields::SearchDirs(Editable::Create(new))) - }) - .unwrap_or(Message::SetConfig(SetConfigFields::SearchDirs( - Editable::Create(String::new()), - ))) - }) + .on_press(Message::OpenFileDialog(FileDialogAction::AddSearchDir)) .style(move |_, _| settings_add_button_style(&theme.clone())) } diff --git a/src/app/tile.rs b/src/app/tile.rs index 5214b6e..7e128af 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -199,6 +199,8 @@ pub struct Tile { page: Page, pub height: f32, pub file_search_sender: Option)>>, + pub file_dialog_open: bool, + pub settings_tab: crate::app::SettingsTab, debouncer: Debouncer, } diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 0e1db6b..5bc0fa5 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -19,7 +19,7 @@ use rayon::slice::ParallelSliceMut; use crate::app::pages::emoji::emoji_page; use crate::app::pages::settings::settings_page; use crate::app::tile::{AppIndex, Hotkeys}; -use crate::app::{DEFAULT_WINDOW_HEIGHT, ToApp, ToApps}; +use crate::app::{DEFAULT_WINDOW_HEIGHT, SettingsTab, ToApp, ToApps}; use crate::config::Theme; use crate::debounce::Debouncer; use crate::platform::macos::events::Event; @@ -95,6 +95,8 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) { page: Page::Main, height: DEFAULT_WINDOW_HEIGHT, file_search_sender: None, + file_dialog_open: false, + settings_tab: SettingsTab::General, debouncer: Debouncer::new(config.debounce_delay), }, Task::batch([open.map(|_| Message::OpenWindow)]), @@ -141,7 +143,7 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { .collect(), tile.focus_id, ), - Page::Settings => settings_page(tile.config.clone()), + Page::Settings => settings_page(tile.config.clone(), tile.settings_tab), Page::FileSearch | Page::Main => container(Column::from_iter( tile.results.iter().enumerate().map(|(i, app)| { app.clone().render( diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 384531d..45848e8 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -18,6 +18,8 @@ use rayon::slice::ParallelSliceMut; use url::Url; use crate::app::Editable; +use crate::app::FileDialogAction; +use crate::app::ResetField; use crate::app::SetConfigBufferFields; use crate::app::SetConfigFields; use crate::app::SetConfigThemeFields; @@ -481,6 +483,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ]) } + Message::SwitchSettingsTab(tab) => { + tile.settings_tab = tab; + Task::none() + } + Message::SwitchToPage(page) => { let task = match &page { Page::ClipboardHistory => { @@ -548,7 +555,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Message::HideWindow(a) => { - if tile.page == Page::Settings { + if tile.file_dialog_open { return Task::none(); } info!("Hiding RustCast window"); @@ -748,23 +755,83 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } } - Message::OpenFileDialogue(mode_name) => rfd::FileDialog::new() - .add_filter("shell", &["sh", "bash", "zsh"]) - .set_directory( - std::env::var("HOME").unwrap_or("".to_string()) + "/.config/rustcast/config.toml", - ) - .pick_file() - .and_then(|path| { - path.to_str().map(|path_str| { - Task::batch([ - Task::done(Message::SetConfig(SetConfigFields::Modes( - Editable::Create((mode_name, path_str.to_string())), - ))), - Task::done(Message::WriteConfig(false)), - ]) - }) - }) - .unwrap_or(Task::none()), + Message::OpenFileDialog(action) => { + tile.file_dialog_open = true; + let home = std::env::var("HOME").unwrap_or("/".to_string()); + match action { + FileDialogAction::PickModeFile(mode_name) => { + let future = async move { + let handle = rfd::AsyncFileDialog::new() + .add_filter("shell", &["sh", "bash", "zsh"]) + .set_directory(home.clone() + "/.config/rustcast") + .pick_file() + .await; + match handle { + Some(file) => { + let path_str = file.path().to_string_lossy().to_string(); + Message::FileDialogResult(Some(Box::new( + Message::SetConfig(SetConfigFields::Modes( + Editable::Create((mode_name, path_str)), + )), + ))) + } + None => Message::FileDialogResult(None), + } + }; + Task::perform(future, |msg| msg) + } + FileDialogAction::EditSearchDir(old_dir) => { + let future = async move { + let handle = rfd::AsyncFileDialog::new() + .set_directory(home.clone()) + .set_can_create_directories(false) + .pick_folder() + .await; + match handle { + Some(folder) => { + let new = folder.path().to_string_lossy().to_string(); + Message::FileDialogResult(Some(Box::new( + Message::SetConfig(SetConfigFields::SearchDirs( + Editable::Update { old: old_dir, new }, + )), + ))) + } + None => Message::FileDialogResult(None), + } + }; + Task::perform(future, |msg| msg) + } + FileDialogAction::AddSearchDir => { + let future = async move { + let handle = rfd::AsyncFileDialog::new() + .set_directory(home) + .set_can_create_directories(false) + .pick_folder() + .await; + match handle { + Some(folder) => { + let new = folder.path().to_string_lossy().to_string(); + Message::FileDialogResult(Some(Box::new( + Message::SetConfig(SetConfigFields::SearchDirs( + Editable::Create(new), + )), + ))) + } + None => Message::FileDialogResult(None), + } + }; + Task::perform(future, |msg| msg) + } + } + } + + Message::FileDialogResult(inner) => { + tile.file_dialog_open = false; + match inner { + Some(msg) => handle_update(tile, *msg), + None => Task::none(), + } + } Message::SetConfig(config) => { let mut final_config = tile.config.clone(); @@ -908,6 +975,58 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } + Message::ResetField(field) => { + let default = Config::default(); + match field { + ResetField::ToggleHotkey => tile.config.toggle_hotkey = default.toggle_hotkey, + ResetField::ClipboardHotkey => { + tile.config.clipboard_hotkey = default.clipboard_hotkey + } + ResetField::Placeholder => tile.config.placeholder = default.placeholder, + ResetField::SearchUrl => tile.config.search_url = default.search_url, + ResetField::DebounceDelay => tile.config.debounce_delay = default.debounce_delay, + ResetField::StartAtLogin => tile.config.start_at_login = default.start_at_login, + ResetField::AutoUpdate => tile.config.auto_update = default.auto_update, + ResetField::HapticFeedback => { + tile.config.haptic_feedback = default.haptic_feedback + } + ResetField::ShowMenubarIcon => { + tile.config.show_trayicon = default.show_trayicon + } + ResetField::ClipboardHistory => tile.config.cbhist = default.cbhist, + ResetField::MainPage => tile.config.main_page = default.main_page, + ResetField::ShowScrollbar => { + tile.config.theme.show_scroll_bar = default.theme.show_scroll_bar + } + ResetField::ClearOnHide => { + tile.config.buffer_rules.clear_on_hide = + default.buffer_rules.clear_on_hide + } + ResetField::ClearOnEnter => { + tile.config.buffer_rules.clear_on_enter = + default.buffer_rules.clear_on_enter + } + ResetField::ShowIcons => { + tile.config.theme.show_icons = default.theme.show_icons + } + ResetField::Font => tile.config.theme.font = default.theme.font, + ResetField::EventDuration => { + tile.config.event_duration = default.event_duration + } + ResetField::TextColor => { + tile.config.theme.text_color = default.theme.text_color + } + ResetField::BackgroundColor => { + tile.config.theme.background_color = default.theme.background_color + } + ResetField::Aliases => tile.config.aliases = default.aliases, + ResetField::Modes => tile.config.modes = default.modes, + ResetField::SearchDirs => tile.config.search_dirs = default.search_dirs, + ResetField::ShellCommands => tile.config.shells = default.shells, + } + Task::none() + } + Message::WriteConfig(page_switch) => { let config_file_path = std::env::var("HOME").unwrap_or("".to_string()) + "/.config/rustcast/config.toml"; @@ -1328,6 +1447,8 @@ mod tests { page: Page::Main, height: DEFAULT_WINDOW_HEIGHT, file_search_sender: None, + file_dialog_open: false, + settings_tab: crate::app::SettingsTab::General, debouncer: crate::debounce::Debouncer::new(10), } } diff --git a/src/styles.rs b/src/styles.rs index 91861f8..c23bba0 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -246,6 +246,72 @@ pub fn settings_add_button_style(theme: &ConfigTheme) -> button::Style { } } +/// Style for settings tab buttons with active/inactive and hover states. +pub fn settings_tab_style( + theme: &ConfigTheme, + active: bool, + status: button::Status, +) -> button::Style { + let base = theme.bg_color(); + if active { + let bg_alpha = match status { + button::Status::Pressed => 0.55, + button::Status::Hovered => 0.45, + _ => 0.35, + }; + let tc_alpha = match status { + button::Status::Pressed => 0.7, + _ => 1.0, + }; + button::Style { + text_color: theme.text_color(tc_alpha), + background: Some(Background::Color(with_alpha(tint(base, 0.12), bg_alpha))), + border: Border { + color: theme.text_color(0.25), + width: 0.5, + radius: Radius::new(6.).top(6.), + }, + ..Default::default() + } + } else { + let (bg_opt, text_alpha) = match status { + button::Status::Pressed => ( + Some(Background::Color(with_alpha(tint(base, 0.06), 0.30))), + 0.7, + ), + button::Status::Hovered => ( + Some(Background::Color(with_alpha(tint(base, 0.04), 0.20))), + 0.9, + ), + _ => (None, 0.5), + }; + button::Style { + text_color: theme.text_color(text_alpha), + background: bg_opt, + border: Border { + color: theme.text_color(0.10), + width: 0.0, + radius: Radius::new(6.).top(6.), + }, + ..Default::default() + } + } +} + +/// Clean container style for the settings panel (non-glass, flat). +pub fn settings_container_style(theme: &ConfigTheme) -> container::Style { + container::Style { + background: Some(Background::Color(with_alpha(tint(theme.bg_color(), 0.04), 0.25))), + border: Border { + color: theme.text_color(0.15), + width: 0.5, + radius: Radius::new(10), + }, + text_color: Some(theme.text_color(1.0)), + ..Default::default() + } +} + pub fn settings_checkbox_style(theme: &ConfigTheme) -> checkbox::Style { checkbox::Style { background: Background::Color(Color::TRANSPARENT), From cfb56688dcc8bf46a05807b100cb52e22d63702a Mon Sep 17 00:00:00 2001 From: unsecretised Date: Fri, 12 Jun 2026 00:06:39 +0800 Subject: [PATCH 2/2] support light and dark mode fixes #232 --- src/app.rs | 5 +- src/app/pages/settings.rs | 186 +++++++++++++++++++++++++------------- src/app/tile.rs | 16 ++++ src/app/tile/update.rs | 79 +++++++++------- src/config.rs | 45 ++++++++- src/platform/macos/mod.rs | 10 ++ src/styles.rs | 5 +- 7 files changed, 245 insertions(+), 101 deletions(-) diff --git a/src/app.rs b/src/app.rs index a42522e..6dc963b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::app::apps::{App, AppCommand, ICNS_ICON}; use crate::commands::Function; -use crate::config::{Config, MainPage, Shelly}; +use crate::config::{Config, MainPage, Shelly, ThemeMode}; use crate::debounce::DebouncePolicy; use crate::platform::macos::launching::Shortcut; use crate::utils::icns_data_to_handle; @@ -79,6 +79,7 @@ pub enum ResetField { EventDuration, TextColor, BackgroundColor, + ThemeMode, Aliases, Modes, SearchDirs, @@ -166,6 +167,7 @@ pub enum Message { SetFileSearchSender(tokio::sync::watch::Sender<(String, Vec)>), DebouncedSearch(Id), CheckEventTap, + ThemeModeChanged(bool), } #[derive(Debug, Clone)] @@ -198,6 +200,7 @@ pub enum SetConfigThemeFields { BackgroundColor(f32, f32, f32), ShowIcons(bool), Font(String), + ThemeMode(ThemeMode), } #[derive(Debug, Clone)] diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 36d307e..1929b2a 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; +use iced::Border; use iced::border::Radius; use iced::widget::Slider; use iced::widget::Space; @@ -10,7 +11,6 @@ use iced::widget::button; use iced::widget::checkbox; use iced::widget::radio; use iced::widget::text_input; -use iced::Border; use crate::styles::tint; use crate::styles::with_alpha; @@ -24,6 +24,7 @@ use crate::app::SettingsTab; use crate::commands::Function; use crate::config::MainPage; use crate::config::Shelly; +use crate::config::ThemeMode; use crate::styles::delete_button_style; use crate::styles::settings_add_button_style; use crate::styles::settings_checkbox_style; @@ -48,8 +49,18 @@ pub fn settings_page(config: Config, settings_tab: SettingsTab) -> Element<'stat let tabs_row = Row::from_iter([ tab_button("General", SettingsTab::General, settings_tab, theme.clone()), - tab_button("Appearance", SettingsTab::Appearance, settings_tab, theme.clone()), - tab_button("Commands", SettingsTab::Commands, settings_tab, theme.clone()), + tab_button( + "Appearance", + SettingsTab::Appearance, + settings_tab, + theme.clone(), + ), + tab_button( + "Commands", + SettingsTab::Commands, + settings_tab, + theme.clone(), + ), ]) .spacing(2) .width(Length::Fill); @@ -113,20 +124,18 @@ fn reset_button(theme: crate::config::Theme, field: ResetField) -> Element<'stat .size(13) .font(theme.font()), ) - .style(move |_, _| { - button::Style { - text_color: theme_clone.text_color(0.5), - background: Some(Background::Color(with_alpha( - tint(theme_clone.bg_color(), 0.06), - 0.20, - ))), - border: Border { - color: theme_clone.text_color(0.15), - width: 0.5, - radius: Radius::new(4), - }, - ..Default::default() - } + .style(move |_, _| button::Style { + text_color: theme_clone.text_color(0.5), + background: Some(Background::Color(with_alpha( + tint(theme_clone.bg_color(), 0.06), + 0.20, + ))), + border: Border { + color: theme_clone.text_color(0.15), + width: 0.5, + radius: Radius::new(4), + }, + ..Default::default() }) .width(30) .height(26) @@ -174,9 +183,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat settings_item_column([ settings_hint_text(theme.clone(), "Set the rustcast placeholder"), text_input("Set Placeholder", &config.placeholder) - .on_input(|input| { - Message::SetConfig(SetConfigFields::PlaceHolder(input.clone())) - }) + .on_input(|input| Message::SetConfig(SetConfigFields::PlaceHolder(input.clone()))) .on_submit(Message::WriteConfig(false)) .width(Length::Fill) .style(move |_, _| settings_text_input_item_style(&theme_clone)) @@ -192,9 +199,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat settings_item_column([ settings_hint_text(theme.clone(), "Set the search URL"), text_input("Set Search URL", &config.search_url) - .on_input(|input| { - Message::SetConfig(SetConfigFields::SearchUrl(input.clone())) - }) + .on_input(|input| Message::SetConfig(SetConfigFields::SearchUrl(input.clone()))) .on_submit(Message::WriteConfig(false)) .width(Length::Fill) .style(move |_, _| settings_text_input_item_style(&theme_clone)) @@ -248,9 +253,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat settings_hint_text(theme.clone(), "Auto update"), checkbox(config.clone().auto_update) .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(move |input| { - Message::SetConfig(SetConfigFields::SetAutoUpdate(input)) - }) + .on_toggle(move |input| Message::SetConfig(SetConfigFields::SetAutoUpdate(input))) .into(), notice_item( theme.clone(), @@ -267,9 +270,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat settings_hint_text(theme.clone(), "Haptic feedback"), checkbox(config.clone().haptic_feedback) .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(|input| { - Message::SetConfig(SetConfigFields::HapticFeedback(input)) - }) + .on_toggle(|input| Message::SetConfig(SetConfigFields::HapticFeedback(input))) .into(), notice_item( theme.clone(), @@ -290,9 +291,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat settings_hint_text(theme.clone(), "Show menubar icon"), checkbox(config.clone().show_trayicon) .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(|input| { - Message::SetConfig(SetConfigFields::ShowMenubarIcon(input)) - }) + .on_toggle(|input| Message::SetConfig(SetConfigFields::ShowMenubarIcon(input))) .into(), notice_item( theme.clone(), @@ -309,9 +308,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat settings_hint_text(theme.clone(), "Enable Clipboard history"), checkbox(config.clone().cbhist) .style(move |_, _| settings_checkbox_style(&theme_clone)) - .on_toggle(|input| { - Message::SetConfig(SetConfigFields::ClipboardHistory(input)) - }) + .on_toggle(|input| Message::SetConfig(SetConfigFields::ClipboardHistory(input))) .into(), notice_item( theme.clone(), @@ -353,23 +350,17 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat move |_, _| settings_radio_button_style(&theme_clone.clone()) }) .into(), - radio( - "Events", - MainPage::Events, - Some(config.main_page), - |page| Message::SetConfig(SetConfigFields::SetPage(page)), - ) + radio("Events", MainPage::Events, Some(config.main_page), |page| { + Message::SetConfig(SetConfigFields::SetPage(page)) + }) .style({ let theme_clone = theme_clone.clone(); move |_, _| settings_radio_button_style(&theme_clone.clone()) }) .into(), - radio( - "Nothing", - MainPage::Blank, - Some(config.main_page), - |page| Message::SetConfig(SetConfigFields::SetPage(page)), - ) + radio("Nothing", MainPage::Blank, Some(config.main_page), |page| { + Message::SetConfig(SetConfigFields::SetPage(page)) + }) .style(move |_, _| settings_radio_button_style(&theme_clone.clone())) .into(), ]) @@ -382,13 +373,81 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat ); Column::from_iter([ - hotkey, cb_hotkey, placeholder_setting, search, debounce, start_at_login, auto_update, - haptic, tray_icon, clipboard_history, auto_suggest, + hotkey, + cb_hotkey, + placeholder_setting, + search, + debounce, + start_at_login, + auto_update, + haptic, + tray_icon, + clipboard_history, + auto_suggest, ]) .spacing(10) } fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'static, Message> { + let theme_clone = theme.clone(); + let theme_mode_setting = settings_row_with_reset( + settings_item_column([ + settings_hint_text(theme.clone(), "Theme mode"), + settings_item_row([ + radio( + "Dark", + ThemeMode::Dark, + Some(config.theme.theme_mode), + |mode| { + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::ThemeMode(mode), + )) + }, + ) + .style({ + let theme_clone = theme_clone.clone(); + move |_, _| settings_radio_button_style(&theme_clone.clone()) + }) + .into(), + radio( + "Light", + ThemeMode::Light, + Some(config.theme.theme_mode), + |mode| { + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::ThemeMode(mode), + )) + }, + ) + .style({ + let theme_clone = theme_clone.clone(); + move |_, _| settings_radio_button_style(&theme_clone.clone()) + }) + .into(), + radio( + "System", + ThemeMode::System, + Some(config.theme.theme_mode), + |mode| { + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::ThemeMode(mode), + )) + }, + ) + .style(move |_, _| settings_radio_button_style(&theme_clone.clone())) + .into(), + ]) + .spacing(30) + .into(), + notice_item( + theme.clone(), + "System follows the macOS appearance automatically", + ), + ]), + ResetField::ThemeMode, + theme.clone(), + ); + let theme_clone = theme.clone(); let show_scrollbar = settings_row_with_reset( settings_item_row([ @@ -476,9 +535,9 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s &config.theme.font.clone().unwrap_or("".to_string()), ) .on_input(move |input: String| { - Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::Font(input), - )) + Message::SetConfig(SetConfigFields::SetThemeFields(SetConfigThemeFields::Font( + input, + ))) }) .on_submit(Message::WriteConfig(false)) .width(Length::Fill) @@ -603,9 +662,7 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s let txt_clr = theme_clone.background_color; let change = change as f32 / 100.; Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::BackgroundColor( - change, txt_clr.1, txt_clr.2, - ), + SetConfigThemeFields::BackgroundColor(change, txt_clr.1, txt_clr.2), )) }, ) @@ -623,9 +680,7 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s let txt_clr = theme_clone.background_color; let change = change as f32 / 100.; Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::BackgroundColor( - txt_clr.0, change, txt_clr.2, - ), + SetConfigThemeFields::BackgroundColor(txt_clr.0, change, txt_clr.2), )) }, ) @@ -643,9 +698,7 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s let txt_clr = theme_clone.background_color; let change = change as f32 / 100.; Message::SetConfig(SetConfigFields::SetThemeFields( - SetConfigThemeFields::BackgroundColor( - txt_clr.0, txt_clr.1, change, - ), + SetConfigThemeFields::BackgroundColor(txt_clr.0, txt_clr.1, change), )) }, ) @@ -664,8 +717,15 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s ); Column::from_iter([ - show_scrollbar, clear_on_hide, clear_on_enter, show_icons, font_family, event_duration, - text_clr, bg_clr, + theme_mode_setting, + show_scrollbar, + clear_on_hide, + clear_on_enter, + show_icons, + font_family, + event_duration, + text_clr, + bg_clr, ]) .spacing(10) } diff --git a/src/app/tile.rs b/src/app/tile.rs index 7e128af..f4bbb32 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -268,6 +268,7 @@ impl Tile { Subscription::run(handle_recipient), Subscription::run(reload_events), Subscription::run(handle_version_and_rankings), + Subscription::run(handle_theme_mode), Subscription::run(check_event_tap), Subscription::run(handle_clipboard_history), Subscription::run(handle_file_search), @@ -806,6 +807,21 @@ fn check_event_tap() -> impl futures::Stream { }) } +/// Poll the system dark mode every 2 seconds and send a message when it changes. +fn handle_theme_mode() -> impl futures::Stream { + stream::channel(100, async |mut output| { + let mut prev_dark = crate::platform::macos::is_dark_mode(); + loop { + tokio::time::sleep(Duration::from_secs(2)).await; + let current = crate::platform::macos::is_dark_mode(); + if current != prev_dark { + prev_dark = current; + let _ = output.send(Message::ThemeModeChanged(current)).await; + } + } + }) +} + fn handle_version_and_rankings() -> impl futures::Stream { stream::channel(100, async |mut output| { loop { diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 45848e8..9f96a47 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -38,6 +38,7 @@ use crate::calculator::Expr; use crate::commands::Function; use crate::config::Config; use crate::config::MainPage; +use crate::config::ThemeMode; use crate::debounce::DebouncePolicy; use crate::platform::macos::events::Event; use crate::platform::macos::launching::Shortcut; @@ -769,11 +770,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { match handle { Some(file) => { let path_str = file.path().to_string_lossy().to_string(); - Message::FileDialogResult(Some(Box::new( - Message::SetConfig(SetConfigFields::Modes( - Editable::Create((mode_name, path_str)), - )), - ))) + Message::FileDialogResult(Some(Box::new(Message::SetConfig( + SetConfigFields::Modes(Editable::Create((mode_name, path_str))), + )))) } None => Message::FileDialogResult(None), } @@ -790,11 +789,12 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { match handle { Some(folder) => { let new = folder.path().to_string_lossy().to_string(); - Message::FileDialogResult(Some(Box::new( - Message::SetConfig(SetConfigFields::SearchDirs( - Editable::Update { old: old_dir, new }, - )), - ))) + Message::FileDialogResult(Some(Box::new(Message::SetConfig( + SetConfigFields::SearchDirs(Editable::Update { + old: old_dir, + new, + }), + )))) } None => Message::FileDialogResult(None), } @@ -811,11 +811,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { match handle { Some(folder) => { let new = folder.path().to_string_lossy().to_string(); - Message::FileDialogResult(Some(Box::new( - Message::SetConfig(SetConfigFields::SearchDirs( - Editable::Create(new), - )), - ))) + Message::FileDialogResult(Some(Box::new(Message::SetConfig( + SetConfigFields::SearchDirs(Editable::Create(new)), + )))) } None => Message::FileDialogResult(None), } @@ -948,6 +946,13 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { SetConfigFields::SetThemeFields(SetConfigThemeFields::Font(fnt)) => { final_config.theme.font = Some(fnt) } + SetConfigFields::SetThemeFields(SetConfigThemeFields::ThemeMode(mode)) => { + final_config.theme.theme_mode = mode; + let is_dark = crate::platform::macos::is_dark_mode(); + let (text, bg) = mode.presets(is_dark); + final_config.theme.text_color = text; + final_config.theme.background_color = bg; + } SetConfigFields::SetThemeFields(SetConfigThemeFields::TextColor(r, g, b)) => { final_config.theme.text_color = (r, g, b) } @@ -972,6 +977,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { }; tile.config = final_config; + tile.theme = tile.config.theme.clone().into(); Task::none() } @@ -987,35 +993,30 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ResetField::DebounceDelay => tile.config.debounce_delay = default.debounce_delay, ResetField::StartAtLogin => tile.config.start_at_login = default.start_at_login, ResetField::AutoUpdate => tile.config.auto_update = default.auto_update, - ResetField::HapticFeedback => { - tile.config.haptic_feedback = default.haptic_feedback - } - ResetField::ShowMenubarIcon => { - tile.config.show_trayicon = default.show_trayicon - } + ResetField::HapticFeedback => tile.config.haptic_feedback = default.haptic_feedback, + ResetField::ShowMenubarIcon => tile.config.show_trayicon = default.show_trayicon, ResetField::ClipboardHistory => tile.config.cbhist = default.cbhist, ResetField::MainPage => tile.config.main_page = default.main_page, + ResetField::ThemeMode => { + tile.config.theme.theme_mode = default.theme.theme_mode; + let is_dark = crate::platform::macos::is_dark_mode(); + let (text, bg) = default.theme.theme_mode.presets(is_dark); + tile.config.theme.text_color = text; + tile.config.theme.background_color = bg; + } ResetField::ShowScrollbar => { tile.config.theme.show_scroll_bar = default.theme.show_scroll_bar } ResetField::ClearOnHide => { - tile.config.buffer_rules.clear_on_hide = - default.buffer_rules.clear_on_hide + tile.config.buffer_rules.clear_on_hide = default.buffer_rules.clear_on_hide } ResetField::ClearOnEnter => { - tile.config.buffer_rules.clear_on_enter = - default.buffer_rules.clear_on_enter - } - ResetField::ShowIcons => { - tile.config.theme.show_icons = default.theme.show_icons + tile.config.buffer_rules.clear_on_enter = default.buffer_rules.clear_on_enter } + ResetField::ShowIcons => tile.config.theme.show_icons = default.theme.show_icons, ResetField::Font => tile.config.theme.font = default.theme.font, - ResetField::EventDuration => { - tile.config.event_duration = default.event_duration - } - ResetField::TextColor => { - tile.config.theme.text_color = default.theme.text_color - } + ResetField::EventDuration => tile.config.event_duration = default.event_duration, + ResetField::TextColor => tile.config.theme.text_color = default.theme.text_color, ResetField::BackgroundColor => { tile.config.theme.background_color = default.theme.background_color } @@ -1077,6 +1078,16 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } + Message::ThemeModeChanged(is_dark) => { + if tile.config.theme.theme_mode == ThemeMode::System { + let (text, bg) = ThemeMode::System.presets(is_dark); + tile.config.theme.text_color = text; + tile.config.theme.background_color = bg; + tile.theme = tile.config.theme.clone().into(); + } + Task::none() + } + Message::DebouncedSearch(id) => { // Only execute if this is still the most recent debounce timer if !tile.debouncer.is_ready() { diff --git a/src/config.rs b/src/config.rs index 2c295a4..366c80f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -86,6 +86,44 @@ impl std::fmt::Display for MainPage { } } +/// The mode for the theme (dark, light, or follow system) +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Copy)] +#[serde(rename_all = "lowercase")] +pub enum ThemeMode { + Dark, + Light, + System, +} + +impl Default for ThemeMode { + fn default() -> Self { + ThemeMode::Dark + } +} + +impl ThemeMode { + /// Return preset text and background colors for this mode. + pub fn presets(&self, is_system_dark: bool) -> ((f32, f32, f32), (f32, f32, f32)) { + match self { + ThemeMode::Dark => ( + (0.95, 0.95, 0.96), // light text + (0.0, 0.0, 0.0), // dark background + ), + ThemeMode::Light => ( + (0.05, 0.05, 0.05), // dark text + (0.95, 0.95, 0.96), // light background + ), + ThemeMode::System => { + if is_system_dark { + ((0.95, 0.95, 0.96), (0.0, 0.0, 0.0)) + } else { + ((0.05, 0.05, 0.05), (0.95, 0.95, 0.96)) + } + } + } + } +} + /// The settings you can set for the theme #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[serde(default)] @@ -96,17 +134,20 @@ pub struct Theme { pub show_icons: bool, pub show_scroll_bar: bool, pub font: Option, + pub theme_mode: ThemeMode, } impl Default for Theme { fn default() -> Self { + let (text, bg) = ThemeMode::Dark.presets(true); Self { - text_color: (0.95, 0.95, 0.96), - background_color: (0.0, 0.0, 0.0), + text_color: text, + background_color: bg, blur: false, show_icons: true, show_scroll_bar: false, font: None, + theme_mode: ThemeMode::Dark, } } } diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index ff8cc13..4bb7684 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -93,6 +93,16 @@ struct ProcessSerialNumber { hi: u32, } +/// Check whether the system is in dark mode via NSUserDefaults. +pub fn is_dark_mode() -> bool { + use objc2_foundation::{NSUserDefaults, ns_string}; + let defaults = NSUserDefaults::standardUserDefaults(); + defaults + .stringForKey(ns_string!("AppleInterfaceStyle")) + .map(|s| s.to_string() == "Dark") + .unwrap_or(false) +} + /// This is the function that transforms the process to a UI element, and hides the dock icon /// /// see mostly diff --git a/src/styles.rs b/src/styles.rs index c23bba0..b5041cc 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -301,7 +301,10 @@ pub fn settings_tab_style( /// Clean container style for the settings panel (non-glass, flat). pub fn settings_container_style(theme: &ConfigTheme) -> container::Style { container::Style { - background: Some(Background::Color(with_alpha(tint(theme.bg_color(), 0.04), 0.25))), + background: Some(Background::Color(with_alpha( + tint(theme.bg_color(), 0.04), + 0.25, + ))), border: Border { color: theme.text_color(0.15), width: 0.5,