diff --git a/src/app.rs b/src/app.rs index 1377643..ba74701 100644 --- a/src/app.rs +++ b/src/app.rs @@ -31,14 +31,14 @@ pub const FILE_SEARCH_BATCH_SIZE: u32 = 10; /// The rustcast descriptor name to be put for all rustcast commands pub const RUSTCAST_DESC_NAME: &str = "Utility"; -/// The different pages that rustcast can have / has +/// The different pages that rustcast can have / has within the launcher +/// Settings is notably missing since this opens as a new window outside the launcher #[derive(Debug, Clone, PartialEq)] pub enum Page { Main, FileSearch, ClipboardHistory, EmojiSearch, - Settings, } /// The settings panel tabs @@ -94,7 +94,6 @@ impl std::fmt::Display for Page { Page::FileSearch => "File search", Page::EmojiSearch => "Emoji search", Page::ClipboardHistory => "Clipboard history", - Page::Settings => "Settings", }) } } @@ -127,7 +126,7 @@ pub enum Editable { #[derive(Debug, Clone)] pub enum Message { UriReceived(String), - WriteConfig(bool), + WriteConfig, SaveRanking, ToggleAutoStartup(bool), LoadRanking, @@ -136,7 +135,6 @@ pub enum Message { ResizeWindow(Id, f32), OpenWindow, OpenResult(u32), - OpenToSettings, SearchQueryChanged(String, Id), KeyPressed(Shortcut), FocusTextInput(Move), @@ -169,6 +167,8 @@ pub enum Message { DebouncedSearch(Id), ThemeModeChanged(bool), SimulatePaste(i32), + OpenSettingsWindow, + SettingsWindowOpened(window::Id), } #[derive(Debug, Clone)] @@ -228,6 +228,23 @@ pub fn default_settings() -> Settings { } } +pub fn settings_window_settings() -> window::Settings { + Settings { + resizable: false, + decorations: true, + minimizable: false, + level: window::Level::AlwaysOnTop, + transparent: false, + blur: false, + size: iced::Size { + width: 900., + height: 600., + }, + position: window::Position::Centered, + ..Default::default() + } +} + /// A Trait to define that a struct can be converted to an app pub trait ToApp { /// Convert self into an app @@ -287,7 +304,7 @@ impl ToApps for HashMap { impl DebouncePolicy for Page { fn debounce_delay(&self, config: &Config) -> Option { match self { - Page::Main | Page::ClipboardHistory | Page::Settings => None, + Page::Main | Page::ClipboardHistory => None, Page::FileSearch | Page::EmojiSearch => { Some(Duration::from_millis(config.debounce_delay)) } @@ -306,7 +323,6 @@ mod tests { assert_eq!(Page::FileSearch.to_string(), "File search"); assert_eq!(Page::ClipboardHistory.to_string(), "Clipboard history"); assert_eq!(Page::EmojiSearch.to_string(), "Emoji search"); - assert_eq!(Page::Settings.to_string(), "Settings"); } #[test] @@ -318,7 +334,6 @@ mod tests { assert_eq!(Page::Main.debounce_delay(&config), None); assert_eq!(Page::ClipboardHistory.debounce_delay(&config), None); - assert_eq!(Page::Settings.debounce_delay(&config), None); assert_eq!( Page::FileSearch.debounce_delay(&config), Some(Duration::from_millis(123)) diff --git a/src/app/apps.rs b/src/app/apps.rs index 2b1539d..d955562 100644 --- a/src/app/apps.rs +++ b/src/app/apps.rs @@ -128,7 +128,7 @@ impl App { }, App { ranking: 0, - open_command: AppCommand::Message(Message::SwitchToPage(Page::Settings)), + open_command: AppCommand::Message(Message::OpenSettingsWindow), desc: RUSTCAST_DESC_NAME.to_string(), icons: icons.clone(), display_name: "Open RustCast Preferences".to_string(), diff --git a/src/app/menubar.rs b/src/app/menubar.rs index 7b82e88..cc86b32 100644 --- a/src/app/menubar.rs +++ b/src/app/menubar.rs @@ -128,7 +128,10 @@ fn init_event_handler(sender: ExtSender, shortcut: Shortcut) { } "open_preferences" => { runtime.spawn(async move { - sender.clone().try_send(Message::OpenToSettings).unwrap(); + sender + .clone() + .try_send(Message::OpenSettingsWindow) + .unwrap(); }); } "open_github_page" => { diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 58d25d8..203d21f 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -4,14 +4,21 @@ use std::collections::HashMap; use iced::Border; use iced::border::Radius; +use iced::widget::Container; +use iced::widget::Scrollable; 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::scrollable::Direction; +use iced::widget::scrollable::Scrollbar; use iced::widget::text_input; +use iced::widget::toggler; +use crate::styles::settings_contents_container_style; +use crate::styles::settings_tabs_container_style; +use crate::styles::settings_toggle_style; use crate::styles::tint; use crate::styles::with_alpha; @@ -27,8 +34,6 @@ 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; -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; @@ -42,12 +47,13 @@ use crate::{ const SETTINGS_ITEM_PADDING: u16 = 4; const SETTINGS_ITEM_HEIGHT: u32 = 55; const SETTINGS_ITEM_COL_SPACING: u32 = 3; +const SETTINGS_INPUT_WIDTH: f32 = 250.0; pub fn settings_page(config: Config, settings_tab: SettingsTab) -> Element<'static, Message> { let config = Box::new(config.clone()); let theme = config.theme.clone(); - let tabs_row = Row::from_iter([ + let tabs_column = Column::from_iter([ tab_button("General", SettingsTab::General, settings_tab, theme.clone()), tab_button( "Appearance", @@ -62,8 +68,15 @@ pub fn settings_page(config: Config, settings_tab: SettingsTab) -> Element<'stat theme.clone(), ), ]) - .spacing(2) - .width(Length::Fill); + .spacing(2); + + let theme_clone = theme.clone(); + let tabs_container = Container::new(tabs_column) + .style(move |_| settings_tabs_container_style(&theme_clone)) + .height(Length::Fill) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) + .padding(12) + .align_x(Alignment::Center); let tab_content: Column<'static, Message> = match settings_tab { SettingsTab::General => general_tab(config.clone(), theme.clone()), @@ -71,8 +84,7 @@ pub fn settings_page(config: Config, settings_tab: SettingsTab) -> Element<'stat SettingsTab::Commands => commands_tab(config.clone(), theme.clone()), }; - let items = Column::from_iter([ - tabs_row.into(), + let contents_column = Column::from_iter([ tab_content.into(), Space::new().height(10).into(), Row::from_iter([ @@ -80,18 +92,25 @@ pub fn settings_page(config: Config, settings_tab: SettingsTab) -> Element<'stat copy_config_button(config), wiki_button(theme.clone()), ]) - .spacing(5) .width(Length::Fill) .into(), - ]) - .spacing(10); + ]); + + let contents_container = Container::new(Scrollable::with_direction( + contents_column, + Direction::Vertical(Scrollbar::hidden()), + )) + .style(move |_| settings_contents_container_style(&theme)) + .height(Length::Fill) + .width(Length::Fill) + .padding(12) + .align_x(Alignment::Center); + + let items = Row::from_iter([tabs_container.into(), contents_container.into()]); container(items) - .style(move |_| settings_container_style(&theme)) .height(Length::Fill) .width(Length::Fill) - .padding(12) - .align_x(Alignment::Center) .into() } @@ -118,7 +137,7 @@ fn tab_button( fn reset_button(theme: crate::config::Theme, field: ResetField) -> Element<'static, Message> { let theme_clone = theme.clone(); Button::new( - Text::new("R") + Text::new("⟳") .align_x(Alignment::Center) .align_y(Alignment::Center) .size(13) @@ -146,15 +165,19 @@ fn reset_button(theme: crate::config::Theme, field: ResetField) -> Element<'stat fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'static, Message> { let theme_clone = theme.clone(); let hotkey = settings_row_with_reset( - settings_item_column([ - settings_hint_text(theme.clone(), "Toggle hotkey"), + settings_item_row([ + settings_hint_text( + theme.clone(), + "Toggle hotkey", + Some("Use \"+\" as a seperator"), + ), + Space::new().width(Length::Fill).into(), text_input("Toggle Hotkey", &config.toggle_hotkey) .on_input(|input| Message::SetConfig(SetConfigFields::ToggleHotkey(input.clone()))) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) + .on_submit(Message::WriteConfig) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) .style(move |_, _| settings_text_input_item_style(&theme_clone)) .into(), - notice_item(theme.clone(), "Use \"+\" as a seperator"), ]), ResetField::ToggleHotkey, theme.clone(), @@ -162,17 +185,21 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat let theme_clone = theme.clone(); let cb_hotkey = settings_row_with_reset( - settings_item_column([ - settings_hint_text(theme.clone(), "Clipboard hotkey"), + settings_item_row([ + settings_hint_text( + theme.clone(), + "Clipboard hotkey", + Some("Use \"+\" as a seperator"), + ), + Space::new().width(Length::Fill).into(), text_input("Clipboard Hotkey", &config.clipboard_hotkey) .on_input(|input| { Message::SetConfig(SetConfigFields::ClipboardHotkey(input.clone())) }) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) + .on_submit(Message::WriteConfig) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) .style(move |_, _| settings_text_input_item_style(&theme_clone)) .into(), - notice_item(theme.clone(), "Use \"+\" as a seperator"), ]), ResetField::ClipboardHotkey, theme.clone(), @@ -180,15 +207,19 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat let theme_clone = theme.clone(); let placeholder_setting = settings_row_with_reset( - settings_item_column([ - settings_hint_text(theme.clone(), "Set the rustcast placeholder"), + settings_item_row([ + settings_hint_text( + theme.clone(), + "Set the rustcast placeholder", + Some("Welcome text on open"), + ), + Space::new().width(Length::Fill).into(), text_input("Set Placeholder", &config.placeholder) .on_input(|input| Message::SetConfig(SetConfigFields::PlaceHolder(input.clone()))) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) + .on_submit(Message::WriteConfig) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) .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(), @@ -196,15 +227,19 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat let theme_clone = theme.clone(); let search = settings_row_with_reset( - settings_item_column([ - settings_hint_text(theme.clone(), "Set the search URL"), + settings_item_row([ + settings_hint_text( + theme.clone(), + "Set the search URL", + Some("Search engine to use (%s = query)"), + ), + Space::new().width(Length::Fill).into(), 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) + .on_submit(Message::WriteConfig) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) .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(), @@ -213,184 +248,145 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat let theme_clone = theme.clone(); let current_delay = config.debounce_delay; let debounce = settings_row_with_reset( - settings_item_column([ - settings_hint_text(theme.clone(), "Set the debounce time"), + settings_item_row([ + settings_hint_text( + theme.clone(), + "Set the debounce time (ms)", + Some("File search response time"), + ), + Space::new().width(Length::Fill).into(), 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) + .on_submit(Message::WriteConfig) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) .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 start_at_login = settings_row_with_reset( - 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"), - ]), - ResetField::StartAtLogin, - theme.clone(), - ); + let start_at_login = settings_row_without_reset(settings_item_row([ + settings_hint_text(theme.clone(), "Start at login", None::), + Space::new().width(Length::Fill).into(), + toggler(config.clone().start_at_login) + .style(move |_, status| settings_toggle_style(status)) + .on_toggle(Message::ToggleAutoStartup) + .into(), + ])); - 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 auto_update = settings_row_without_reset(settings_item_row([ + settings_hint_text(theme.clone(), "Auto update", None::), + Space::new().width(Length::Fill).into(), + toggler(config.clone().auto_update) + .style(move |_, status| settings_toggle_style(status)) + .on_toggle(move |input| Message::SetConfig(SetConfigFields::SetAutoUpdate(input))) + .into(), + ])); - let theme_clone = theme.clone(); - let haptic = settings_row_with_reset( + let haptic = settings_row_without_reset( Row::from_iter([ - settings_hint_text(theme.clone(), "Haptic feedback"), - checkbox(config.clone().haptic_feedback) - .style(move |_, _| settings_checkbox_style(&theme_clone)) + settings_hint_text(theme.clone(), "Haptic feedback", None::), + Space::new().width(Length::Fill).into(), + toggler(config.clone().haptic_feedback) + .style(move |_, status| settings_toggle_style(status)) .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), - ResetField::HapticFeedback, - theme.clone(), ); - let theme_clone = theme.clone(); - 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 tray_icon = settings_row_without_reset(settings_item_row([ + settings_hint_text(theme.clone(), "Show menubar icon", None::), + Space::new().width(Length::Fill).into(), + toggler(config.clone().show_trayicon) + .style(move |_, status| settings_toggle_style(status)) + .on_toggle(|input| Message::SetConfig(SetConfigFields::ShowMenubarIcon(input))) + .into(), + ])); - let theme_clone = theme.clone(); - let clipboard_history = settings_row_with_reset( + let clipboard_history = settings_row_without_reset( Row::from_iter([ - settings_hint_text(theme.clone(), "Enable Clipboard history"), - checkbox(config.clone().cbhist) - .style(move |_, _| settings_checkbox_style(&theme_clone)) + settings_hint_text(theme.clone(), "Enable clipboard history", None::), + Space::new().width(Length::Fill).into(), + toggler(config.clone().cbhist) + .style(move |_, status| settings_toggle_style(status)) .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 cbhist_paste_on_select = settings_row_with_reset( + let cbhist_paste_on_select = settings_row_without_reset( Row::from_iter([ - settings_hint_text(theme.clone(), "Paste on select"), - checkbox(config.clone().cbhist_paste_on_select) - .style(move |_, _| settings_checkbox_style(&theme_clone)) + settings_hint_text(theme.clone(), "Paste on select", None::), + Space::new().width(Length::Fill).into(), + toggler(config.clone().cbhist_paste_on_select) + .style(move |_, status| settings_toggle_style(status)) .on_toggle(|input| { Message::SetConfig(SetConfigFields::ClipboardPasteOnSelect(input)) }) .into(), - notice_item(theme.clone(), "Auto-paste clipboard item after selecting"), ]) .align_y(Alignment::Center) .spacing(SETTINGS_ITEM_COL_SPACING * 2) .padding(SETTINGS_ITEM_PADDING) .height(SETTINGS_ITEM_HEIGHT), - ResetField::ClipboardPasteOnSelect, - theme.clone(), ); let theme_clone = theme.clone(); - 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) + let auto_suggest = settings_row_without_reset(settings_item_row([ + settings_hint_text(theme.clone(), "Suggestions on open", None::), + Space::new().width(Length::Fill).into(), + 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(), - notice_item(theme.clone(), "What an empty query should show"), - ]), - ResetField::MainPage, - theme.clone(), - ); + 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(), + ])); Column::from_iter([ hotkey, @@ -411,146 +407,118 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat 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( + let theme_mode_setting = settings_row_without_reset(settings_item_row([ + settings_hint_text(theme.clone(), "Theme mode", None::), + Space::new().width(Length::Fill).into(), 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| { + radio( + "Dark", + ThemeMode::Dark, + Some(config.theme.theme_mode), + |mode| { 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 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), + SetConfigThemeFields::ThemeMode(mode), )) - }) - .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 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), + }, + ) + .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), )) - }) - .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| { + }, + ) + .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::ShowIcons(input), + SetConfigThemeFields::ThemeMode(mode), )) - }) - .into(), - notice_item(theme.clone(), "If you want app icons to be visible"), - ]), - ResetField::ShowIcons, - theme.clone(), - ); + }, + ) + .style(move |_, _| settings_radio_button_style(&theme_clone.clone())) + .into(), + ]) + .spacing(30) + .into(), + ])); + + let show_scrollbar = settings_row_without_reset(settings_item_row([ + settings_hint_text(theme.clone(), "Show scrollbar", None::), + Space::new().width(Length::Fill).into(), + toggler(config.theme.show_scroll_bar) + .style(move |_, status| settings_toggle_style(status)) + .on_toggle(|input| { + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::ShowScrollBar(input), + )) + }) + .into(), + ])); + + let clear_on_hide = settings_row_without_reset(settings_item_row([ + settings_hint_text( + theme.clone(), + "Clear on hide", + Some("Clear query when rustcast is hidden"), + ), + Space::new().width(Length::Fill).into(), + toggler(config.clone().buffer_rules.clear_on_hide) + .style(move |_, status| settings_toggle_style(status)) + .on_toggle(move |input| { + Message::SetConfig(SetConfigFields::SetBufferFields( + SetConfigBufferFields::ClearOnHide(input), + )) + }) + .into(), + ])); + + let clear_on_enter = settings_row_without_reset(settings_item_row([ + settings_hint_text(theme.clone(), "Clear on enter", None::), + Space::new().width(Length::Fill).into(), + toggler(config.clone().buffer_rules.clear_on_enter) + .style(move |_, status| settings_toggle_style(status)) + .on_toggle(move |input| { + Message::SetConfig(SetConfigFields::SetBufferFields( + SetConfigBufferFields::ClearOnEnter(input), + )) + }) + .into(), + ])); + + let show_icons = settings_row_without_reset(settings_item_row([ + settings_hint_text(theme.clone(), "Show icons", None::), + Space::new().width(Length::Fill).into(), + toggler(config.clone().theme.show_icons) + .style(move |_, status| settings_toggle_style(status)) + .on_toggle(move |input| { + Message::SetConfig(SetConfigFields::SetThemeFields( + SetConfigThemeFields::ShowIcons(input), + )) + }) + .into(), + ])); let theme_clone = theme.clone(); let font_family = settings_row_with_reset( - settings_item_column([ - settings_hint_text(theme.clone(), "Set Font family"), + settings_item_row([ + settings_hint_text(theme.clone(), "Font family", None::), + Space::new().width(Length::Fill).into(), text_input( "Font family", &config.theme.font.clone().unwrap_or("".to_string()), @@ -560,11 +528,10 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s input, ))) }) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) + .on_submit(Message::WriteConfig) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) .style(move |_, _| settings_text_input_item_style(&theme_clone)) .into(), - notice_item(theme.clone(), "What font rustcast should use"), ]), ResetField::Font, theme.clone(), @@ -572,20 +539,21 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s let theme_clone = theme.clone(); let event_duration = settings_row_with_reset( - settings_item_column([ - settings_hint_text(theme.clone(), "Set Event duration"), + settings_item_row([ + settings_hint_text( + theme.clone(), + "Event duration", + Some("Minutes from now events should be displayed"), + ), + Space::new().width(Length::Fill).into(), 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) + .on_submit(Message::WriteConfig) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) .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(), @@ -597,11 +565,12 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s let theme_clone_3 = theme.clone(); let text_clr = settings_row_with_reset( Column::from_iter([ - settings_hint_text(theme.clone(), "Set text colour"), + settings_hint_text(theme.clone(), "Set text colour", None::), Column::from_iter([ settings_hint_text( theme.clone(), format!("R value: {}", theme_clone.text_color.0), + None::, ), Slider::new( 0..=100, @@ -620,6 +589,7 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s settings_hint_text( theme.clone(), format!("G value: {}", theme_clone.text_color.1), + None::, ), Slider::new( 0..=100, @@ -638,6 +608,7 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s settings_hint_text( theme.clone(), format!("B value: {}", theme_clone.text_color.2), + None::, ), Slider::new( 0..=100, @@ -653,7 +624,6 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s .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) @@ -670,11 +640,12 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s let theme_clone_3 = theme.clone(); let bg_clr = settings_row_with_reset( Column::from_iter([ - settings_hint_text(theme.clone(), "Set background colour"), + settings_hint_text(theme.clone(), "Set background colour", None::), Column::from_iter([ settings_hint_text( theme.clone(), format!("R value: {}", theme_clone.background_color.0), + None::, ), Slider::new( 0..=100, @@ -693,6 +664,7 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s settings_hint_text( theme.clone(), format!("G value: {}", theme_clone.background_color.1), + None::, ), Slider::new( 0..=100, @@ -711,6 +683,7 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s settings_hint_text( theme.clone(), format!("B value: {}", theme_clone.background_color.2), + None::, ), Slider::new( 0..=100, @@ -726,7 +699,6 @@ fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'s .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) @@ -772,7 +744,8 @@ fn section_header_with_reset( theme: crate::config::Theme, ) -> Element<'static, Message> { Row::from_iter([ - settings_hint_text(theme.clone(), label), + settings_hint_text(theme.clone(), label, None::), + Space::new().width(Length::Fill).into(), reset_button(theme, field), ]) .align_y(Alignment::Center) @@ -793,6 +766,16 @@ fn settings_row_with_reset( .into() } +fn settings_row_without_reset( + content: impl Into>, +) -> Element<'static, Message> { + Row::from_iter([content.into()]) + .align_y(Alignment::Center) + .spacing(5) + .width(Length::Fill) + .into() +} + fn savebutton(theme: Theme) -> Element<'static, Message> { Button::new( Text::new("Save") @@ -802,7 +785,7 @@ fn savebutton(theme: Theme) -> Element<'static, Message> { ) .style(move |_, _| settings_save_button_style(&theme)) .width(Length::Fill) - .on_press(Message::WriteConfig(true)) + .on_press(Message::WriteConfig) .into() } @@ -839,21 +822,25 @@ fn copy_config_button(config: Box) -> Element<'static, Message> { .into() } -fn settings_hint_text(theme: Theme, text: impl ToString) -> Element<'static, Message> { - let text = text.to_string(); - - Text::new(text) +fn settings_hint_text( + theme: Theme, + text: impl ToString, + subtitle: Option, +) -> Element<'static, Message> { + let title = Text::new(text.to_string()) .font(theme.font()) - .color(theme.text_color(0.7)) - .into() -} + .color(theme.text_color(0.7)); -fn settings_item_column( - elems: impl IntoIterator>, -) -> Column<'static, Message> { - Column::from_iter(elems) - .spacing(SETTINGS_ITEM_COL_SPACING) - .padding(SETTINGS_ITEM_PADDING) + let mut content = Column::new().push(title); + + if let Some(subtitle) = subtitle { + let subtitle = Text::new(subtitle.to_string()) + .font(theme.font()) + .color(theme.text_color(0.3)); + content = content.push(subtitle); + } + + container(content).into() } fn settings_item_row( @@ -985,7 +972,7 @@ fn text_input_cell(text: String, theme: &Theme, placeholder: &str) -> TextInput< text_input(placeholder, &text) .font(theme.font()) .padding(5) - .on_submit(Message::WriteConfig(false)) + .on_submit(Message::WriteConfig) } fn modes_item(modes: HashMap, theme: &Theme) -> Element<'static, Message> { diff --git a/src/app/tile.rs b/src/app/tile.rs index e07e5cb..fee0de4 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -175,6 +175,7 @@ fn build_mdfind_args(query: &str, dirs: &[String], home_dir: &str) -> Option`) all of the cliboard contents /// - Page ([`Page`]) the current page of the window (main or clipboard history) /// - RustCast's height: to figure out which height to resize to +/// - Settings Window: the ID of the window if it is open #[derive(Clone)] pub struct Tile { pub theme: iced::Theme, @@ -202,6 +203,7 @@ pub struct Tile { pub file_dialog_open: bool, pub settings_tab: crate::app::SettingsTab, debouncer: Debouncer, + pub settings_window: Option, } /// A struct to store all the hotkeys @@ -255,7 +257,7 @@ impl Tile { .. }) => { if cha.to_string() == "," { - return Some(Message::SwitchToPage(Page::Settings)); + return Some(Message::OpenSettingsWindow); } None } diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 209fda5..07f9356 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -100,6 +100,7 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) { file_dialog_open: false, settings_tab: SettingsTab::General, debouncer: Debouncer::new(config.debounce_delay), + settings_window: None, }, Task::batch([open.map(|_| Message::OpenWindow)]), ) @@ -107,6 +108,10 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) { /// The elm View function that renders the entire rustcast window pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { + if tile.settings_window == Some(wid) { + return settings_page(tile.config.clone(), tile.settings_tab); + }; + if tile.visible { let title_input = text_input(tile.config.placeholder.as_str(), &tile.query) .on_input(move |a| Message::SearchQueryChanged(a, wid)) @@ -119,17 +124,16 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { .style(move |_, _| rustcast_text_input_style(&tile.config.theme)) .padding(20); - let scrollbar_direction = - if !tile.config.theme.show_scroll_bar || tile.page == Page::Settings { - Direction::Vertical(Scrollbar::hidden()) - } else { - Direction::Vertical( - Scrollbar::new() - .width(1) - .scroller_width(1.1) - .anchor(Anchor::Start), - ) - }; + let scrollbar_direction = if !tile.config.theme.show_scroll_bar { + Direction::Vertical(Scrollbar::hidden()) + } else { + Direction::Vertical( + Scrollbar::new() + .width(1) + .scroller_width(1.1) + .anchor(Anchor::Start), + ) + }; let results = match tile.page { Page::ClipboardHistory => clipboard_view( @@ -145,7 +149,6 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { .collect(), tile.focus_id, ), - 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( @@ -162,12 +165,11 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { let results_count = match &tile.page { Page::Main | Page::EmojiSearch | Page::FileSearch => tile.results.len(), Page::ClipboardHistory => tile.clipboard_content.len(), - Page::Settings => 0, }; // This determines the height of the scrollable window let height = match tile.page { - Page::ClipboardHistory | Page::Settings => 385, + Page::ClipboardHistory => 385, // Height of each emoji is EMOJI_HEIGHT + 20 for padding Page::EmojiSearch => std::cmp::min(tile.results.len().div_ceil(6) * 90, 290), _ => std::cmp::min(tile.results.len() * 60, 290), diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 8824c9b..6c29cec 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -31,6 +31,7 @@ use crate::app::apps::AppCommand; use crate::app::default_settings; use crate::app::menubar::menu_builder; use crate::app::menubar::menu_icon; +use crate::app::settings_window_settings; use crate::app::tile::AppIndex; use crate::app::{Message, Page, tile::Tile}; use crate::autoupdate::download_latest_app; @@ -121,12 +122,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } } - Message::UpdateEvents => { tile.events = Event::get_events(tile.config.event_duration); Task::none() } - Message::UriReceived(uri) => { let Ok(url) = Url::parse(&uri) else { return Task::none(); @@ -149,7 +148,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { _ => Task::none(), } } - Message::UpdateAvailable => { tile.update_available = true; @@ -160,7 +158,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Task::done(Message::ReloadConfig) } - Message::SwitchMode(mode) => { if let Some(command) = tile.config.modes.get(mode.trim()) { tile.current_mode = mode.clone(); @@ -174,7 +171,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } } - Message::HideTrayIcon => { tile.tray_icon = None; tile.config.show_trayicon = false; @@ -183,7 +179,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { thread::spawn(move || fs::write(home + "/.config/rustcast/config.toml", confg_str)); Task::none() } - Message::SetSender(sender) => { tile.sender = Some(sender.clone()); match global_handler(sender.clone(), tile.hotkeys.all_hotkeys()) { @@ -198,7 +193,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Task::none() } - Message::ToggleAutoStartup(set_to) => { if set_to { start_at_login(); @@ -209,7 +203,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Task::none() } - Message::EscKeyPressed(id) => { if !tile.query_lc.is_empty() { return Task::batch([ @@ -220,9 +213,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { match tile.page { Page::Main => {} - Page::Settings => { - return Task::done(Message::WriteConfig(true)); - } _ => { return Task::done(Message::SwitchToPage(Page::Main)); } @@ -249,13 +239,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ]) } } - Message::ClearSearchQuery => { tile.query_lc = String::new(); tile.query = String::new(); Task::none() } - Message::ChangeFocus(key, amount) => { let mut return_task = Task::none(); for _ in 0..amount { @@ -301,7 +289,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { let quantity = match tile.page { Page::Main | Page::FileSearch | Page::ClipboardHistory => 66.5, Page::EmojiSearch => 5., - Page::Settings => 0., }; let (wrapped_up, wrapped_down) = match &key { @@ -331,7 +318,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } return_task } - Message::ResizeWindow(id, height) => { info!("Resizing rustcast window"); tile.height = height; @@ -350,7 +336,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } - Message::SaveRanking => { tile.ranking = tile.options.get_rankings(); let string_rep = toml::to_string(&tile.ranking).unwrap_or("".to_string()); @@ -359,10 +344,8 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { fs::write(ranking_file_path, string_rep).ok(); Task::none() } - Message::OpenFocused => Task::done(Message::OpenResult(tile.focus_id)), Message::OpenResult(id) => open_result(tile, id as usize), - Message::ReloadConfig => { info!("Reloading config"); let new_config: Config = match toml::from_str( @@ -426,7 +409,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::done(Message::SetSender(tile.sender.clone().unwrap())), ]) } - Message::KeyPressed(shortcut) => { if let Some(cmd) = tile.hotkeys.shells.get(&shortcut) { return Task::done(Message::RunFunction(Function::RunShellCommand( @@ -475,20 +457,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } } - - Message::OpenToSettings => { - tile.page = Page::Settings; - Task::batch([ - Task::done(Message::OpenWindow), - open_window(((7 * 55) + 35 + DEFAULT_WINDOW_HEIGHT as usize) as f32), - ]) - } - Message::SwitchSettingsTab(tab) => { tile.settings_tab = tab; Task::none() } - Message::SwitchToPage(page) => { let task = match &page { Page::ClipboardHistory => { @@ -503,13 +475,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ) }) } - Page::Settings => window::latest().map(|x| { - let id = x.unwrap(); - Message::ResizeWindow( - id, - ((7 * 55) + 35 + DEFAULT_WINDOW_HEIGHT as usize) as f32, - ) - }), _ => Task::none(), }; @@ -530,7 +495,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { refresh_empty_main_query, ]) } - Message::RunFunction(command) => { if let Function::TileWindow(pos) = &command { if let Some(pid) = tile.frontmost.as_ref().map(|a| a.processIdentifier()) { @@ -541,10 +505,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } } command.execute(&tile.config); - let page_task = match tile.page { - Page::Settings => Task::done(Message::SwitchToPage(Page::Main)), - _ => Task::none(), - }; let return_focus_task = match &command { Function::OpenApp(_) | Function::GoogleSearch(_) => Task::none(), @@ -574,16 +534,18 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { window::latest() .map(|x| x.unwrap()) .map(Message::HideWindow) - .chain(page_task) .chain(Task::done(Message::ClearSearchQuery)) .chain(return_focus_task) .chain(paste_task) } - Message::HideWindow(a) => { if tile.file_dialog_open { return Task::none(); } + if tile.settings_window == Some(a) { + tile.settings_window = None; + return Task::none(); + } info!("Hiding RustCast window"); tile.visible = false; tile.focused = false; @@ -592,13 +554,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::batch([window::close(a), Task::done(Message::ClearSearchResults)]) } - Message::ReturnFocus => { info!("Restoring frontmost app"); tile.restore_frontmost(); Task::none() } - Message::FocusTextInput(update_query_char) => { match update_query_char { Move::Forwards(query_char) => { @@ -619,7 +579,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { .map(move |x| Message::SearchQueryChanged(updated_query.clone(), x)), ]) } - Message::ToggleFavouriteApp(app_name) => { let ranking = match tile.options.by_name.get(&app_name) { None => return Task::none(), @@ -634,7 +593,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.options.set_ranking(&app_name, ranking); Task::none() } - Message::UpdateApps => { let mut new_options = get_installed_apps(tile.config.theme.show_icons); new_options.extend(tile.config.shells.iter().map(|x| x.to_app())); @@ -658,20 +616,22 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } - Message::ClearSearchResults => { tile.results = Vec::new(); Task::none() } Message::WindowFocusChanged(wid, focused) => { + if Some(wid) == tile.settings_window { + return Task::none(); + } tile.focused = focused; + if !focused { Task::done(Message::HideWindow(wid)).chain(Task::done(Message::ClearSearchQuery)) } else { Task::none() } } - Message::EditClipboardHistory(action) => { if !tile.config.cbhist { return Task::none(); @@ -721,12 +681,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Task::none() } - Message::SetFileSearchSender(sender) => { tile.file_search_sender = Some(sender); Task::none() } - Message::FileSearchResult(apps) => { assert!(apps.len() <= 50, "Batch must not exceed 50 results."); if tile.page == Page::FileSearch { @@ -745,14 +703,12 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Task::none() } - Message::FileSearchClear => { if tile.page == Page::FileSearch { tile.results.clear(); } Task::none() } - Message::SearchQueryChanged(input, id) => { tile.focus_id = 0; @@ -781,7 +737,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { execute_query(tile, id) } } - Message::OpenFileDialog(action) => { tile.file_dialog_open = true; let home = std::env::var("HOME").unwrap_or("/".to_string()); @@ -848,7 +803,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } } } - Message::FileDialogResult(inner) => { tile.file_dialog_open = false; match inner { @@ -856,7 +810,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { None => Task::none(), } } - Message::SetConfig(config) => { let mut final_config = tile.config.clone(); match config.clone() { @@ -975,9 +928,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { 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); + let (text, bg, secondary) = mode.presets(is_dark); final_config.theme.text_color = text; final_config.theme.background_color = bg; + final_config.theme.secondary_bg_color = secondary; } SetConfigFields::SetThemeFields(SetConfigThemeFields::TextColor(r, g, b)) => { final_config.theme.text_color = (r, g, b) @@ -1009,7 +963,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.theme = tile.config.theme.clone().into(); Task::none() } - Message::ResetField(field) => { let default = Config::default(); match field { @@ -1029,9 +982,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { 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); + let (text, bg, secondary) = default.theme.theme_mode.presets(is_dark); tile.config.theme.text_color = text; tile.config.theme.background_color = bg; + tile.config.theme.secondary_bg_color = secondary; } ResetField::ShowScrollbar => { tile.config.theme.show_scroll_bar = default.theme.show_scroll_bar @@ -1057,10 +1011,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.config.cbhist_paste_on_select = default.cbhist_paste_on_select } } + tile.theme = tile.config.theme.clone().into(); Task::none() } - - Message::WriteConfig(page_switch) => { + Message::WriteConfig => { let config_file_path = std::env::var("HOME").unwrap_or("".to_string()) + "/.config/rustcast/config.toml"; @@ -1083,36 +1037,26 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { }) .ok(); - Task::batch([ - Task::done(Message::ReloadConfig), - if page_switch { - Task::done(Message::SwitchToPage(Page::Main)) - } else { - Task::none() - }, - ]) + Task::batch([Task::done(Message::ReloadConfig), Task::none()]) } - Message::ClearClipboardHistory => { tile.clipboard_content.clear(); Task::none() } - Message::SimulatePaste(pid) => { crate::platform::simulate_paste(pid); Task::none() } - Message::ThemeModeChanged(is_dark) => { if tile.config.theme.theme_mode == ThemeMode::System { - let (text, bg) = ThemeMode::System.presets(is_dark); + let (text, bg, secondary) = ThemeMode::System.presets(is_dark); tile.config.theme.text_color = text; tile.config.theme.background_color = bg; + tile.config.theme.secondary_bg_color = secondary; 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() { @@ -1121,6 +1065,16 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { execute_query(tile, id) } + Message::OpenSettingsWindow => { + if let Some(id) = tile.settings_window { + window::gain_focus(id) + } else { + let (id, task) = window::open(settings_window_settings()); + tile.settings_window = Some(id); + task.map(move |_| Message::SettingsWindowOpened(id)) + } + } + Message::SettingsWindowOpened(_id) => Task::none(), } } @@ -1212,13 +1166,8 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task { let mut task = Task::none(); let prev_size = tile.results.len(); - match tile.page { - Page::ClipboardHistory | Page::Settings => { - if tile.query_lc != "main" { - return Task::none(); - } - } - _ => {} + if tile.page == Page::ClipboardHistory && tile.query_lc != "main" { + return Task::none(); } if tile.page == Page::Main && tile.query_lc.is_empty() { @@ -1452,11 +1401,6 @@ mod tests { )), 0, ), - test_app( - "message", - AppCommand::Message(Message::SwitchToPage(Page::Settings)), - 0, - ), test_app("display", AppCommand::Display, 0), ]), emoji_apps: AppIndex::empty(), @@ -1486,6 +1430,7 @@ mod tests { file_dialog_open: false, settings_tab: crate::app::SettingsTab::General, debouncer: crate::debounce::Debouncer::new(10), + settings_window: None, } } @@ -1521,10 +1466,6 @@ mod tests { classify_query_action(&Page::Main, "fav", "fav"), Some(QueryAction::ShowFavourites) ); - assert_eq!( - classify_query_action(&Page::Settings, "main", "main"), - Some(QueryAction::SwitchToPage(Page::Main)) - ); } #[test] @@ -1551,20 +1492,13 @@ mod tests { AppCommand::Function(Function::OpenApp("/Applications/Openable.app".to_string())), 0, ), - test_app( - "message", - AppCommand::Message(Message::SwitchToPage(Page::Settings)), - 0, - ), test_app("display", AppCommand::Display, 0), ]); let _ = open_result(&mut tile, 0); let _ = open_result(&mut tile, 1); - let _ = open_result(&mut tile, 2); assert_eq!(tile.options.get_rankings().get("openable"), Some(&1)); - assert_eq!(tile.options.get_rankings().get("message"), Some(&1)); assert_eq!(tile.options.get_rankings().get("display"), None); } } diff --git a/src/config.rs b/src/config.rs index d255806..1f74021 100644 --- a/src/config.rs +++ b/src/config.rs @@ -105,21 +105,26 @@ impl Default for ThemeMode { 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)) { + pub fn presets( + &self, + is_system_dark: bool, + ) -> ((f32, f32, f32), (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 + (0.1, 0.1, 0.1), // dark secondary background ), ThemeMode::Light => ( (0.05, 0.05, 0.05), // dark text (0.95, 0.95, 0.96), // light background + (0.9, 0.9, 0.9), // light secondary background ), ThemeMode::System => { if is_system_dark { - ((0.95, 0.95, 0.96), (0.0, 0.0, 0.0)) + ((0.95, 0.95, 0.96), (0.0, 0.0, 0.0), (0.1, 0.1, 0.1)) } else { - ((0.05, 0.05, 0.05), (0.95, 0.95, 0.96)) + ((0.05, 0.05, 0.05), (0.95, 0.95, 0.96), (0.9, 0.9, 0.9)) } } } @@ -132,6 +137,7 @@ impl ThemeMode { pub struct Theme { pub text_color: (f32, f32, f32), pub background_color: (f32, f32, f32), + pub secondary_bg_color: (f32, f32, f32), pub blur: bool, pub show_icons: bool, pub show_scroll_bar: bool, @@ -141,10 +147,11 @@ pub struct Theme { impl Default for Theme { fn default() -> Self { - let (text, bg) = ThemeMode::Dark.presets(true); + let (text, bg, secondary) = ThemeMode::Dark.presets(true); Self { text_color: text, background_color: bg, + secondary_bg_color: secondary, blur: false, show_icons: true, show_scroll_bar: false, @@ -210,6 +217,16 @@ impl Theme { } } + /// Return the secondary background color in the theme config of type [`iced::Color`] + pub fn secondary_bg_color(&self) -> iced::Color { + iced::Color { + r: self.secondary_bg_color.0, + g: self.secondary_bg_color.1, + b: self.secondary_bg_color.2, + a: 0., + } + } + /// Return the font in the theme config of type [`iced::Font`] pub fn font(&self) -> Font { let opt_font_name = self.font.clone(); diff --git a/src/styles.rs b/src/styles.rs index aa942a8..e4b8f01 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -2,7 +2,8 @@ use crate::config::Theme as ConfigTheme; use iced::Shadow; use iced::border::Radius; -use iced::widget::{button, checkbox, container, radio, scrollable, slider}; +use iced::widget::toggler::Status; +use iced::widget::{button, container, radio, scrollable, slider, toggler}; use iced::{Background, Border, Color, widget::text_input}; /// Helper: mix base color with white (simple “tint”) @@ -298,13 +299,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 { +/// Clean container style for the tabs in the settings panel (non-glass, flat). +pub fn settings_tabs_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(settings_surface(theme.bg_color(), 0.12))), border: Border { color: theme.text_color(0.15), width: 0.5, @@ -315,16 +313,67 @@ pub fn settings_container_style(theme: &ConfigTheme) -> container::Style { } } -pub fn settings_checkbox_style(theme: &ConfigTheme) -> checkbox::Style { - checkbox::Style { - background: Background::Color(Color::TRANSPARENT), - icon_color: theme.text_color(1.), - border: iced::Border { - color: theme.text_color(1.), - width: 1., - radius: Radius::new(2.), +/// Clean container style for the contents in settings panel (non-glass, flat). +pub fn settings_contents_container_style(theme: &ConfigTheme) -> container::Style { + container::Style { + background: Some(Background::Color(settings_surface(theme.bg_color(), 0.10))), + 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_toggle_style(status: toggler::Status) -> toggler::Style { + match status { + Status::Active { is_toggled } => toggler::Style { + background: if is_toggled { + Background::Color(Color::from_rgb8(52, 199, 89)) // iOS System Green + } else { + Background::Color(Color::from_rgb8(174, 174, 178)) // iOS Gray + }, + background_border_width: 0.0, + background_border_color: Color::TRANSPARENT, + foreground: Background::Color(Color::WHITE), + foreground_border_width: 0.0, + foreground_border_color: Color::TRANSPARENT, + text_color: Some(Color::BLACK), + border_radius: None, + padding_ratio: 0.05, + }, + Status::Hovered { is_toggled } => toggler::Style { + background: if is_toggled { + Background::Color(Color::from_rgb8(48, 183, 82)) // Slightly deeper green + } else { + Background::Color(Color::from_rgb8(218, 218, 219)) // Slightly darker track gray + }, + background_border_width: 0.0, + background_border_color: Color::TRANSPARENT, + foreground: Background::Color(Color::WHITE), + foreground_border_width: 0.0, + foreground_border_color: Color::TRANSPARENT, + text_color: Some(Color::BLACK), + border_radius: None, + padding_ratio: 0.05, + }, + Status::Disabled { is_toggled } => toggler::Style { + background: if is_toggled { + Background::Color(Color::from_rgb8(179, 238, 194)) // Desaturated translucent green + } else { + Background::Color(Color::from_rgb8(242, 242, 247)) // Very faint gray + }, + background_border_width: 0.0, + background_border_color: Color::TRANSPARENT, + foreground: Background::Color(Color::from_rgb8(250, 250, 250)), // Off-white handle + foreground_border_width: 0.0, + foreground_border_color: Color::TRANSPARENT, + text_color: Some(Color::from_rgb8(174, 174, 178)), // iOS Gray + border_radius: None, + padding_ratio: 0.05, }, - text_color: None, } } @@ -358,6 +407,13 @@ pub fn glass_surface(base: Color, focused: bool) -> Color { with_alpha(tint(base, t), a) } +/// The settings window is opaque, but `theme.bg_color()` has an alpha of 0 so it +/// would let the window background bleed through. This returns a fully opaque, +/// slightly tinted surface so panels read as distinct from the window background. +pub fn settings_surface(base: Color, amount: f32) -> Color { + with_alpha(tint(base, amount), 1.0) +} + /// Helper fn for making a borders color look like its glassy pub fn glass_border(base_text: Color, focused: bool) -> Color { let a = if focused { 0.35 } else { 0.22 };