diff --git a/src/style.css b/src/style.css index c463105..ac83217 100644 --- a/src/style.css +++ b/src/style.css @@ -230,6 +230,7 @@ label.network-poor { color: var(--error-color); } /* Loading spinner */ .loading-spinner { + color: var(--text-secondary); margin-top: 12px; margin-bottom: 12px; opacity: 0.6; @@ -454,4 +455,3 @@ popover row label { color: var(--text-secondary); font-size: 13px; } - diff --git a/src/themes/catppuccin.css b/src/themes/catppuccin.css index 5abf539..b468127 100644 --- a/src/themes/catppuccin.css +++ b/src/themes/catppuccin.css @@ -205,6 +205,7 @@ label.network-poor { color: var(--error-color); } .wifi-open { color: var(--text-primary); } .loading-spinner { + color: var(--text-secondary); margin-top: 12px; margin-bottom: 12px; opacity: 0.6; @@ -361,4 +362,3 @@ popover row label { background: var(--bg-secondary); border-color: var(--border-color-hover); } - diff --git a/src/themes/dracula.css b/src/themes/dracula.css index 363d5d7..e0bfbaa 100644 --- a/src/themes/dracula.css +++ b/src/themes/dracula.css @@ -205,6 +205,7 @@ label.network-poor { color: var(--error-color); } .wifi-open { color: var(--text-primary); } .loading-spinner { + color: var(--text-secondary); margin-top: 12px; margin-bottom: 12px; opacity: 0.6; @@ -361,4 +362,3 @@ popover row label { background: var(--bg-secondary); border-color: var(--border-color-hover); } - diff --git a/src/themes/gruvbox.css b/src/themes/gruvbox.css index 8b08bdd..51d8a71 100644 --- a/src/themes/gruvbox.css +++ b/src/themes/gruvbox.css @@ -236,6 +236,7 @@ label.network-poor { color: var(--error-color); } .wifi-open { color: var(--text-primary); } .loading-spinner { + color: var(--text-secondary); margin-top: 12px; margin-bottom: 12px; opacity: 0.6; @@ -392,4 +393,3 @@ popover row label { background: var(--bg-secondary); border-color: var(--border-color-hover); } - diff --git a/src/themes/nord.css b/src/themes/nord.css index 5ec7a32..a379098 100644 --- a/src/themes/nord.css +++ b/src/themes/nord.css @@ -205,6 +205,7 @@ label.network-poor { color: var(--error-color); } .wifi-open { color: var(--text-primary); } .loading-spinner { + color: var(--text-secondary); margin-top: 12px; margin-bottom: 12px; opacity: 0.6; @@ -361,4 +362,3 @@ popover row label { background: var(--bg-secondary); border-color: var(--border-color-hover); } - diff --git a/src/themes/tokyo.css b/src/themes/tokyo.css index b4df0be..7fbb346 100644 --- a/src/themes/tokyo.css +++ b/src/themes/tokyo.css @@ -205,6 +205,7 @@ label.network-poor { color: var(--error-color); } .wifi-open { color: var(--text-primary); } .loading-spinner { + color: var(--text-secondary); margin-top: 12px; margin-bottom: 12px; opacity: 0.6; @@ -361,4 +362,3 @@ popover row label { background: var(--bg-secondary); border-color: var(--border-color-hover); } - diff --git a/src/ui/connect.rs b/src/ui/connect.rs index bf36531..025e7fe 100644 --- a/src/ui/connect.rs +++ b/src/ui/connect.rs @@ -10,6 +10,8 @@ use nmrs::{ }; use std::rc::Rc; +use crate::ui::inherit_color_scheme; + pub fn connect_modal( nm: Rc, parent: &ApplicationWindow, @@ -45,6 +47,7 @@ fn draw_connect_modal( dialog.set_title(Some("Connect to Network")); dialog.set_transient_for(Some(parent)); dialog.set_modal(true); + inherit_color_scheme(&dialog, parent); dialog.add_css_class("diag-buttons"); let content_area = dialog.content_area(); @@ -124,6 +127,7 @@ fn draw_connect_modal( ("Open", ResponseType::Accept), ], ); + inherit_color_scheme(&file_dialog, &parent_dialog); let cert_entry = cert_entry_for_browse.clone(); file_dialog.connect_response(move |dialog, response| { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index fb5b766..868b1dd 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -22,6 +22,89 @@ use tokio::sync::Notify; type Callback = Rc; type CallbackCell = Rc>>; +const COLOR_SCHEME_PROPERTY: &str = "gtk-interface-color-scheme"; + +// Read the GTK 4.20 property dynamically for compatibility with older releases +fn system_prefers_dark(settings: >k::Settings) -> Option { + if !settings.has_property(COLOR_SCHEME_PROPERTY) { + return None; + } + + // Treat unknown values as GTK's default light scheme + glib::EnumValue::from_value(&settings.property_value(COLOR_SCHEME_PROPERTY)).and_then( + |(_, value)| match value.nick() { + "unsupported" => None, + "dark" => Some(true), + _ => Some(false), + }, + ) +} + +fn system_color_scheme() -> Option<(gtk::Settings, bool)> { + let settings = gtk::Settings::default()?; + system_prefers_dark(&settings).map(|prefers_dark| (settings, prefers_dark)) +} + +fn set_interface_color_scheme(settings: >k::Settings, prefers_dark: bool) { + settings.set_gtk_application_prefer_dark_theme(prefers_dark); + + let Some(property) = settings.find_property(COLOR_SCHEME_PROPERTY) else { + return; + }; + let Some(value) = glib::EnumClass::with_type(property.value_type()) + .and_then(|class| class.to_value_by_nick(if prefers_dark { "dark" } else { "light" })) + else { + return; + }; + + settings.set_property_from_value(COLOR_SCHEME_PROPERTY, &value); +} + +fn update_color_scheme(window: &impl IsA, prefers_dark: bool) { + window.remove_css_class("dark-theme"); + window.remove_css_class("light-theme"); + window.add_css_class(if prefers_dark { + "dark-theme" + } else { + "light-theme" + }); +} + +pub(crate) fn inherit_color_scheme(window: &impl IsA, parent: &impl IsA) { + update_color_scheme(window, parent.has_css_class("dark-theme")); +} + +fn sync_system_color_scheme(window: &ApplicationWindow) { + let Some((settings, prefers_dark)) = system_color_scheme() else { + return; + }; + + settings.set_gtk_application_prefer_dark_theme(prefers_dark); + update_color_scheme(window, prefers_dark); +} + +pub(crate) fn supports_system_color_scheme() -> bool { + system_color_scheme().is_some() +} + +pub(crate) fn apply_color_scheme_override(window: &ApplicationWindow, prefers_dark: bool) { + if let Some(settings) = gtk::Settings::default() { + set_interface_color_scheme(&settings, prefers_dark); + } + + update_color_scheme(window, prefers_dark); +} + +pub(crate) fn apply_system_color_scheme(window: &ApplicationWindow) { + if let Some(settings) = gtk::Settings::default() + && settings.has_property(COLOR_SCHEME_PROPERTY) + { + settings.reset_property(COLOR_SCHEME_PROPERTY); + } + + sync_system_color_scheme(window); +} + pub fn freq_to_band(freq: u32) -> Option<&'static str> { match freq { 2400..=2500 => Some("2.4GHz"), @@ -35,7 +118,23 @@ pub fn build_ui(app: &Application) { let win = ApplicationWindow::new(app); win.set_title(Some("")); win.set_default_size(450, 600); - win.add_css_class("dark-theme"); + + // Preserve the dark default when system preferences are unavailable + if let Some((settings, prefers_dark)) = system_color_scheme() { + win.add_css_class("system-theme"); + update_color_scheme(&win, prefers_dark); + + let win_weak = win.downgrade(); + settings.connect_notify_local(Some(COLOR_SCHEME_PROPERTY), move |_, _| { + if let Some(window) = win_weak.upgrade() + && window.has_css_class("system-theme") + { + sync_system_color_scheme(&window); + } + }); + } else { + apply_color_scheme_override(&win, true); + } let vbox = GtkBox::new(Orientation::Vertical, 0); let status = Label::new(None); diff --git a/src/ui/network_page.rs b/src/ui/network_page.rs index 0dccb3f..0ec673c 100644 --- a/src/ui/network_page.rs +++ b/src/ui/network_page.rs @@ -52,6 +52,7 @@ impl NetworkPage { let header = Box::new(Orientation::Horizontal, 6); let icon = Image::from_icon_name("network-wireless-signal-excellent-symbolic"); + icon.add_css_class("network-icon"); icon.set_pixel_size(24); let title = Label::new(None); diff --git a/src/ui/settings_page.rs b/src/ui/settings_page.rs index 9847829..958bab4 100644 --- a/src/ui/settings_page.rs +++ b/src/ui/settings_page.rs @@ -1,7 +1,10 @@ use gtk::prelude::*; use gtk::{Align, Box, Button, Label, Orientation}; +use crate::ui::apply_color_scheme_override; +use crate::ui::apply_system_color_scheme; use crate::ui::header::THEMES; +use crate::ui::supports_system_color_scheme; const CUSTOM_INDEX: u32 = 0; @@ -36,7 +39,7 @@ impl SettingsPage { root.append(&title); Self::build_theme_section(&root); - Self::build_light_dark_section(&root, window); + Self::build_appearance_section(&root, window); Self { root } } @@ -96,7 +99,7 @@ impl SettingsPage { root.append(§ion); } - fn build_light_dark_section(root: >k::Box, window: >k::ApplicationWindow) { + fn build_appearance_section(root: >k::Box, window: >k::ApplicationWindow) { let section = Box::new(Orientation::Vertical, 6); let label = Label::new(Some("Appearance")); @@ -106,6 +109,10 @@ impl SettingsPage { let toggle_box = Box::new(Orientation::Horizontal, 8); + let system_btn = Button::with_label("System"); + system_btn.add_css_class("appearance-btn"); + system_btn.set_visible(supports_system_color_scheme()); + let light_btn = Button::with_label("Light"); light_btn.add_css_class("appearance-btn"); @@ -114,12 +121,29 @@ impl SettingsPage { { let window_weak = window.downgrade(); + let light_btn_clone = light_btn.clone(); + let dark_btn_clone = dark_btn.clone(); + system_btn.connect_clicked(move |btn| { + if let Some(window) = window_weak.upgrade() { + window.add_css_class("system-theme"); + apply_system_color_scheme(&window); + btn.add_css_class("appearance-active"); + light_btn_clone.remove_css_class("appearance-active"); + dark_btn_clone.remove_css_class("appearance-active"); + } + }); + } + + { + let window_weak = window.downgrade(); + let system_btn_clone = system_btn.clone(); let dark_btn_clone = dark_btn.clone(); light_btn.connect_clicked(move |btn| { if let Some(window) = window_weak.upgrade() { - window.remove_css_class("dark-theme"); - window.add_css_class("light-theme"); + window.remove_css_class("system-theme"); + apply_color_scheme_override(&window, false); btn.add_css_class("appearance-active"); + system_btn_clone.remove_css_class("appearance-active"); dark_btn_clone.remove_css_class("appearance-active"); } }); @@ -127,23 +151,28 @@ impl SettingsPage { { let window_weak = window.downgrade(); + let system_btn_clone = system_btn.clone(); let light_btn_clone = light_btn.clone(); dark_btn.connect_clicked(move |btn| { if let Some(window) = window_weak.upgrade() { - window.remove_css_class("light-theme"); - window.add_css_class("dark-theme"); + window.remove_css_class("system-theme"); + apply_color_scheme_override(&window, true); btn.add_css_class("appearance-active"); + system_btn_clone.remove_css_class("appearance-active"); light_btn_clone.remove_css_class("appearance-active"); } }); } - if window.has_css_class("light-theme") { + if window.has_css_class("system-theme") { + system_btn.add_css_class("appearance-active"); + } else if window.has_css_class("light-theme") { light_btn.add_css_class("appearance-active"); } else { dark_btn.add_css_class("appearance-active"); } + toggle_box.append(&system_btn); toggle_box.append(&light_btn); toggle_box.append(&dark_btn); section.append(&toggle_box); diff --git a/src/ui/vpn_add_page.rs b/src/ui/vpn_add_page.rs index 881fcd9..c16dec8 100644 --- a/src/ui/vpn_add_page.rs +++ b/src/ui/vpn_add_page.rs @@ -8,6 +8,8 @@ use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer}; use std::cell::RefCell; use std::rc::Rc; +use crate::ui::inherit_color_scheme; + type OnSuccessCallback = Rc>>>; pub struct VpnAddPage { @@ -293,6 +295,7 @@ impl VpnAddPage { ("Open", ResponseType::Accept), ], ); + inherit_color_scheme(&dialog, &parent); dialog.connect_response(move |dialog, response| { if response == ResponseType::Accept && let Some(file) = dialog.file() diff --git a/src/ui/wired_page.rs b/src/ui/wired_page.rs index 92b6bbf..4153085 100644 --- a/src/ui/wired_page.rs +++ b/src/ui/wired_page.rs @@ -36,6 +36,7 @@ impl WiredPage { let header = Box::new(Orientation::Horizontal, 6); let icon = Image::from_icon_name("network-wired-symbolic"); + icon.add_css_class("wired-device-icon"); icon.set_pixel_size(24); let title = Label::new(None);