Adds the single source of truth for bread GUI styling so the apps stop each re-implementing (and drifting on) component CSS: - stylesheet(&Palette): full component sheet (buttons, entries, switches, dropdowns, lists/rows/sidebars, cards, chips, scrollbars, headings) built from the design tokens + a canonical @define-color block (surface=color0, overlay=color7, accent=color4). - render() / shared_css_path() / write_shared_css(): render for the current pywal palette and write to $XDG_RUNTIME_DIR/bread/theme.css. - gtk::apply_shared(): load that file (or a rendered fallback) at APPLICATION priority and watch it, so every app recolours live with no rebuild. - new `bread-theme` CLI (generate|path|print) — gtk-free, light. Run at session start and on palette change; apps pick it up via the file watch. The contract is a CSS *file*, so apps stay decoupled from this crate's gtk4 version. Tests cover the stylesheet, path, and render helpers.
84 lines
3 KiB
Rust
84 lines
3 KiB
Rust
use gtk4::gio;
|
|
use gtk4::prelude::*;
|
|
use gtk4::CssProvider;
|
|
use std::cell::RefCell;
|
|
use std::path::Path;
|
|
|
|
thread_local! {
|
|
static SHARED_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
|
static SHARED_MONITOR: RefCell<Option<gio::FileMonitor>> = const { RefCell::new(None) };
|
|
}
|
|
|
|
fn reload_shared() {
|
|
let css = std::fs::read_to_string(crate::shared_css_path())
|
|
.unwrap_or_else(|_| crate::render());
|
|
SHARED_PROVIDER.with(|cell| apply_css(&css, cell));
|
|
}
|
|
|
|
/// Load the ecosystem's shared stylesheet (the file written by
|
|
/// `bread-theme generate`, or a freshly rendered fallback if absent) at
|
|
/// APPLICATION priority, and watch the file so the whole UI recolours live when
|
|
/// the palette changes — no app rebuild or restart needed.
|
|
///
|
|
/// Call once at startup; then add the app's own CSS provider *after* this so
|
|
/// app-specific rules win on equal specificity.
|
|
pub fn apply_shared() {
|
|
reload_shared();
|
|
SHARED_MONITOR.with(|cell| {
|
|
if cell.borrow().is_some() {
|
|
return;
|
|
}
|
|
let file = gio::File::for_path(crate::shared_css_path());
|
|
if let Ok(monitor) = file.monitor_file(gio::FileMonitorFlags::NONE, gio::Cancellable::NONE) {
|
|
monitor.connect_changed(|_, _, _, _| reload_shared());
|
|
*cell.borrow_mut() = Some(monitor);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Apply a CSS string to the default display at APPLICATION priority.
|
|
/// Re-uses an existing provider if one is passed in (for SIGHUP reloads).
|
|
pub fn apply_css(css: &str, provider: &RefCell<Option<CssProvider>>) {
|
|
let display = gtk4::gdk::Display::default().expect("no display");
|
|
let mut guard = provider.borrow_mut();
|
|
if let Some(p) = guard.as_ref() {
|
|
p.load_from_string(css);
|
|
} else {
|
|
let p = CssProvider::new();
|
|
p.load_from_string(css);
|
|
gtk4::style_context_add_provider_for_display(
|
|
&display,
|
|
&p,
|
|
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
);
|
|
*guard = Some(p);
|
|
}
|
|
}
|
|
|
|
/// Apply a user CSS override file at USER priority. Clears the provider if the
|
|
/// file is absent so stale overrides don't persist across SIGHUP reloads.
|
|
pub fn apply_user_css(path: &Path, provider: &RefCell<Option<CssProvider>>) {
|
|
let display = gtk4::gdk::Display::default().expect("no display");
|
|
let mut guard = provider.borrow_mut();
|
|
match std::fs::read_to_string(path) {
|
|
Ok(css) => {
|
|
if let Some(p) = guard.as_ref() {
|
|
p.load_from_string(&css);
|
|
} else {
|
|
let p = CssProvider::new();
|
|
p.load_from_string(&css);
|
|
gtk4::style_context_add_provider_for_display(
|
|
&display,
|
|
&p,
|
|
gtk4::STYLE_PROVIDER_PRIORITY_USER,
|
|
);
|
|
*guard = Some(p);
|
|
}
|
|
}
|
|
Err(_) => {
|
|
if let Some(p) = guard.as_ref() {
|
|
p.load_from_string("");
|
|
}
|
|
}
|
|
}
|
|
}
|