diff --git a/nmrs-gui/src/lib.rs b/nmrs-gui/src/lib.rs index 23acb210..d9a87b88 100644 --- a/nmrs-gui/src/lib.rs +++ b/nmrs-gui/src/lib.rs @@ -9,7 +9,6 @@ use gtk::Application; use gtk::prelude::*; use crate::file_lock::acquire_app_lock; -use crate::style::load_css; use crate::ui::build_ui; #[derive(Parser, Debug)] @@ -44,7 +43,7 @@ pub fn run() -> anyhow::Result<()> { }; app.connect_activate(|app| { - load_css(); + crate::style::init(include_str!("style.css")); build_ui(app); }); diff --git a/nmrs-gui/src/style.rs b/nmrs-gui/src/style.rs index 75ebec04..d354e382 100644 --- a/nmrs-gui/src/style.rs +++ b/nmrs-gui/src/style.rs @@ -1,55 +1,96 @@ use gtk::gdk::Display; use gtk::gio::File; -use gtk::{CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION, STYLE_PROVIDER_PRIORITY_USER}; +use gtk::{CssProvider, STYLE_PROVIDER_PRIORITY_USER}; +use std::cell::RefCell; use std::fs; use std::io::Write; +use std::path::PathBuf; -/// Load and apply the user's custom ~/.config/nmrs/style.css. -/// If the file does not exist it is created with the bundled defaults. -/// This must be called after any theme provider is registered so that -/// same-priority (STYLE_PROVIDER_PRIORITY_USER) rules resolve in favour of -/// the user's stylesheet. -pub fn load_user_css() { - let path = dirs::config_dir() - .unwrap_or_default() - .join("nmrs/style.css"); +thread_local! { + static PROVIDER: RefCell = RefCell::new(CssProvider::new()); +} - let display = Display::default().expect("No display found"); +fn config_dir() -> PathBuf { + dirs::config_dir().unwrap_or_default().join("nmrs") +} - if path.exists() { - let provider = CssProvider::new(); - let file = File::for_path(&path); +fn style_path() -> PathBuf { + config_dir().join("style.css") +} - provider.load_from_file(&file); +fn custom_backup_path() -> PathBuf { + config_dir().join("style.custom.css") +} + +/// Register a single persistent CSS provider and load `~/.config/nmrs/style.css`. +/// If it doesn't exist, seeds it with the bundled default. +pub fn init(default_css: &str) { + let display = Display::default().expect("No display found"); + PROVIDER.with(|p| { gtk::style_context_add_provider_for_display( &display, - &provider, + &*p.borrow(), STYLE_PROVIDER_PRIORITY_USER, ); - } else { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).ok(); - } - - let default = include_str!("style.css"); - let mut f = fs::File::create(&path).expect("Failed to create CSS file"); - f.write_all(default.as_bytes()) - .expect("Failed to write default CSS"); + }); + + ensure_dir(); + if !style_path().exists() { + write_file(&style_path(), default_css); + } + reload(); +} + +/// Switch to a named theme: if the user was on "Custom", back up their +/// `style.css` to `style.custom.css` first. Then overwrite `style.css` +/// with the theme content and reload. +pub fn switch_to_theme(css: &str) { + let current = crate::theme_config::load_theme().unwrap_or_default(); + if current == "custom" { + backup_custom(); + } + write_file(&style_path(), css); + reload(); +} + +/// Switch to "Custom": restore `style.custom.css` back to `style.css` +/// if a backup exists, then reload. +pub fn switch_to_custom() { + let backup = custom_backup_path(); + if backup.exists() + && let Ok(contents) = fs::read_to_string(&backup) + { + write_file(&style_path(), &contents); } + reload(); } -pub fn load_css() { - let provider = CssProvider::new(); +/// Reload `~/.config/nmrs/style.css` into the persistent provider. +pub fn reload() { + let path = style_path(); + if path.exists() { + let file = File::for_path(&path); + PROVIDER.with(|p| { + p.borrow().load_from_file(&file); + }); + } +} - let css = include_str!("style.css"); - provider.load_from_data(css); +fn backup_custom() { + let src = style_path(); + if src.exists() { + fs::copy(&src, custom_backup_path()).ok(); + } +} - let display = Display::default().expect("No display found"); +fn write_file(path: &PathBuf, contents: &str) { + ensure_dir(); + let mut f = fs::File::create(path).expect("Failed to write CSS file"); + f.write_all(contents.as_bytes()) + .expect("Failed to write CSS file"); +} - gtk::style_context_add_provider_for_display( - &display, - &provider, - STYLE_PROVIDER_PRIORITY_APPLICATION, - ); +fn ensure_dir() { + fs::create_dir_all(config_dir()).ok(); } diff --git a/nmrs-gui/src/ui/mod.rs b/nmrs-gui/src/ui/mod.rs index 48c9ce95..dd5da190 100644 --- a/nmrs-gui/src/ui/mod.rs +++ b/nmrs-gui/src/ui/mod.rs @@ -8,16 +8,14 @@ pub mod wired_page; use gtk::prelude::*; use gtk::{ - Application, ApplicationWindow, Box as GtkBox, Label, Orientation, - STYLE_PROVIDER_PRIORITY_USER, ScrolledWindow, Spinner, Stack, pango::EllipsizeMode, + Application, ApplicationWindow, Box as GtkBox, Label, Orientation, ScrolledWindow, Spinner, + Stack, pango::EllipsizeMode, }; use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; use tokio::sync::Notify; -use crate::ui::header::THEMES; - type Callback = Rc; type CallbackCell = Rc>>; @@ -34,26 +32,7 @@ pub fn build_ui(app: &Application) { let win = ApplicationWindow::new(app); win.set_title(Some("")); win.set_default_size(100, 600); - - if let Some(key) = crate::theme_config::load_theme() - && let Some(theme) = THEMES.iter().find(|t| t.key == key.as_str()) - { - let provider = gtk::CssProvider::new(); - provider.load_from_data(theme.css); - - let display = gtk::prelude::RootExt::display(&win); - gtk::style_context_add_provider_for_display( - &display, - &provider, - STYLE_PROVIDER_PRIORITY_USER, - ); - - win.add_css_class("dark-theme"); - } - - // User's custom style.css must be registered after the theme so that it - // takes precedence when both run at STYLE_PROVIDER_PRIORITY_USER. - crate::style::load_user_css(); + win.add_css_class("dark-theme"); let vbox = GtkBox::new(Orientation::Vertical, 0); let status = Label::new(None); diff --git a/nmrs-gui/src/ui/settings_page.rs b/nmrs-gui/src/ui/settings_page.rs index 979e0eb2..98478298 100644 --- a/nmrs-gui/src/ui/settings_page.rs +++ b/nmrs-gui/src/ui/settings_page.rs @@ -1,7 +1,9 @@ use gtk::prelude::*; -use gtk::{Align, Box, Button, Label, Orientation, STYLE_PROVIDER_PRIORITY_USER}; +use gtk::{Align, Box, Button, Label, Orientation}; -use crate::ui::header::{THEMES, ThemeDef}; +use crate::ui::header::THEMES; + +const CUSTOM_INDEX: u32 = 0; pub struct SettingsPage { root: gtk::Box, @@ -33,13 +35,13 @@ impl SettingsPage { title.set_halign(Align::Start); root.append(&title); - Self::build_theme_section(&root, window); + Self::build_theme_section(&root); Self::build_light_dark_section(&root, window); Self { root } } - fn build_theme_section(root: >k::Box, window: >k::ApplicationWindow) { + fn build_theme_section(root: >k::Box) { let section = Box::new(Orientation::Vertical, 6); let label = Label::new(Some("Theme")); @@ -47,29 +49,47 @@ impl SettingsPage { label.set_halign(Align::Start); section.append(&label); - let names: Vec<&str> = THEMES.iter().map(|t| t.name).collect(); + let hint = Label::new(Some( + "Your overrides in ~/.config/nmrs/style.css are preserved", + )); + hint.add_css_class("info-value"); + hint.set_halign(Align::Start); + hint.set_opacity(0.6); + section.append(&hint); + + let mut names: Vec<&str> = vec!["Custom"]; + names.extend(THEMES.iter().map(|t| t.name)); let dropdown = gtk::DropDown::from_strings(&names); dropdown.set_halign(Align::Start); dropdown.set_hexpand(false); - if let Some(saved) = crate::theme_config::load_theme() - && let Some(idx) = THEMES.iter().position(|t| t.key == saved.as_str()) - { - dropdown.set_selected(idx as u32); + if let Some(saved) = crate::theme_config::load_theme() { + if let Some(idx) = THEMES.iter().position(|t| t.key == saved.as_str()) { + dropdown.set_selected(idx as u32 + 1); + } else { + dropdown.set_selected(CUSTOM_INDEX); + } + } else { + dropdown.set_selected(CUSTOM_INDEX); } - let window_weak = window.downgrade(); dropdown.connect_selected_notify(move |dd| { - let idx = dd.selected() as usize; - if idx >= THEMES.len() { + let idx = dd.selected(); + + if idx == CUSTOM_INDEX { + crate::style::switch_to_custom(); + crate::theme_config::save_theme("custom"); return; } - let theme = &THEMES[idx]; - - if let Some(window) = window_weak.upgrade() { - Self::apply_theme(theme, &window); + let theme_idx = (idx - 1) as usize; + if theme_idx >= THEMES.len() { + return; } + + let theme = &THEMES[theme_idx]; + crate::style::switch_to_theme(theme.css); + crate::theme_config::save_theme(theme.key); }); section.append(&dropdown); @@ -101,7 +121,6 @@ impl SettingsPage { window.add_css_class("light-theme"); btn.add_css_class("appearance-active"); dark_btn_clone.remove_css_class("appearance-active"); - crate::theme_config::save_theme("dark"); } }); } @@ -115,7 +134,6 @@ impl SettingsPage { window.add_css_class("dark-theme"); btn.add_css_class("appearance-active"); light_btn_clone.remove_css_class("appearance-active"); - crate::theme_config::save_theme("light"); } }); } @@ -132,22 +150,6 @@ impl SettingsPage { root.append(§ion); } - fn apply_theme(theme: &ThemeDef, window: >k::ApplicationWindow) { - let provider = gtk::CssProvider::new(); - provider.load_from_data(theme.css); - - let display = gtk::prelude::RootExt::display(window); - - gtk::style_context_add_provider_for_display( - &display, - &provider, - STYLE_PROVIDER_PRIORITY_USER, - ); - - crate::theme_config::save_theme(theme.key); - crate::style::load_user_css(); - } - pub fn widget(&self) -> >k::Box { &self.root } diff --git a/nmrs-gui/tests/style_test.rs b/nmrs-gui/tests/style_test.rs index 93b4144f..614a4e61 100644 --- a/nmrs-gui/tests/style_test.rs +++ b/nmrs-gui/tests/style_test.rs @@ -5,5 +5,5 @@ fn style_css_loads() { } gtk::init().unwrap(); - nmrs_gui::style::load_css(); + nmrs_gui::style::init(include_str!("../src/style.css")); }