New `adw` feature (gated separately from `gtk`, since AdwApplicationWindow isn't compatible with gtk4-layer-shell — the five panel/launcher apps stay on plain `gtk`, only breadman/breadhelp-style plain-window apps want this): preferences_group/toggle_row/spin_row/action_row/preferences_page, wrapping libadwaita's PreferencesGroup/SwitchRow/SpinRow/ActionRow/PreferencesPage. adw::init() also forces dark color-scheme, since bread-theme's whole design is a fixed dark base regardless of system GTK preference. These directly target defects a design critique found: hand-rolled switch+label rows with no intrinsic width (breadman/settings' ~1400px stretched toggles) and spinners stranded far from their label — both just don't happen when the row is a real AdwSwitchRow/AdwSpinRow instead of a box assembled from scratch. Also, two shared-stylesheet fixes usable by every app immediately, gtk feature only: - `scale` (slider) had no rule at all, so every volume/brightness slider showed GTK's own default blue instead of the palette accent — the same critique flagged breadbar's control-panel sliders contradicting its own on-brand OSD fill two clicks away. - A new `chip()`/`set_chip_active()` helper in gtk.rs uses the existing (already-tokenized, already-defined) `.chip`/`.pill` stylesheet rule instead of each app hand-rolling its own filter-chip CSS — which is how breadclip/breadpad/breadman ended up with three different, mutually disagreeing pill fills for what's supposed to be one shared component.
166 lines
6.8 KiB
Rust
166 lines
6.8 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) };
|
|
static APP_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
|
static APP_MONITOR: RefCell<Option<gio::FileMonitor>> = const { RefCell::new(None) };
|
|
#[allow(clippy::type_complexity)]
|
|
static APP_BUILDER: RefCell<Option<Box<dyn Fn() -> String>>> = 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));
|
|
}
|
|
|
|
fn reload_app() {
|
|
let css = APP_BUILDER.with(|b| b.borrow().as_ref().map(|f| f()));
|
|
if let Some(css) = css {
|
|
APP_PROVIDER.with(|cell| apply_css(&css, cell));
|
|
}
|
|
}
|
|
|
|
/// Watch the shared stylesheet for changes and run `reload` when it's rewritten.
|
|
///
|
|
/// `bread-theme` writes the file with write-tmp-then-rename (atomic), which
|
|
/// *replaces the inode*. A monitor on the file itself dies after the first
|
|
/// replace (inotify reports DELETE_SELF and never re-arms), so we monitor the
|
|
/// parent *directory* and filter for the stylesheet's filename — that fires
|
|
/// reliably on every reload. Returns the monitor (keep it alive to stay armed).
|
|
fn watch_theme_file(reload: fn()) -> Option<gio::FileMonitor> {
|
|
let target = crate::shared_css_path();
|
|
let dir = target.parent()?;
|
|
// The dir must exist to be monitored; `bread-theme generate` makes it at
|
|
// login, but create it here too so a GUI started first still arms the watch.
|
|
let _ = std::fs::create_dir_all(dir);
|
|
let monitor = gio::File::for_path(dir)
|
|
.monitor_directory(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE)
|
|
.ok()?;
|
|
monitor.connect_changed(move |_, file, other, _event| {
|
|
// The rename lands as an event whose file (or move destination) is the
|
|
// stylesheet. Match either to catch both CREATED/CHANGED and MOVED_IN.
|
|
let is_target = |f: &gio::File| f.path().as_deref() == Some(target.as_path());
|
|
if is_target(file) || other.is_some_and(is_target) {
|
|
reload();
|
|
}
|
|
});
|
|
Some(monitor)
|
|
}
|
|
|
|
/// Apply an app's *own* stylesheet and keep it live across palette changes.
|
|
///
|
|
/// `build` is called now to produce the app-specific CSS, and again every time
|
|
/// the shared theme file is rewritten — i.e. whenever `bread-theme reload` (or
|
|
/// `generate`) runs after pywal changes. The app recolours in place, no restart.
|
|
///
|
|
/// This is the counterpart to [`apply_shared`]: that hot-reloads the *shared*
|
|
/// component sheet; this hot-reloads the app's *own* rules (which are built from
|
|
/// the palette, so they'd otherwise be frozen at startup). Apps that build their
|
|
/// CSS from [`crate::stylesheet`] themselves can use this alone; apps that layer
|
|
/// on top of [`apply_shared`] call both.
|
|
///
|
|
/// Call once at startup. The closure should read the current palette
|
|
/// ([`crate::load_palette`]) each time so it picks up the new colours.
|
|
pub fn apply_app_css<F: Fn() -> String + 'static>(build: F) {
|
|
APP_BUILDER.with(|b| *b.borrow_mut() = Some(Box::new(build)));
|
|
reload_app();
|
|
APP_MONITOR.with(|cell| {
|
|
if cell.borrow().is_some() {
|
|
return;
|
|
}
|
|
*cell.borrow_mut() = watch_theme_file(reload_app);
|
|
});
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
*cell.borrow_mut() = watch_theme_file(reload_shared);
|
|
});
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
}
|
|
|
|
/// A filter/tag chip using the shared `.chip` stylesheet rule (an
|
|
/// `@overlay`-filled pill, `@accent`-filled when the `active` CSS class is
|
|
/// set) instead of a fresh literal color — this is the fix for the same
|
|
/// component drifting to three different fills across breadclip (grey),
|
|
/// breadpad, and breadman (both cream), none of which agreed with each
|
|
/// other or with the shared token.
|
|
pub fn chip(label: &str) -> gtk4::Button {
|
|
gtk4::Button::builder().label(label).css_classes(["chip"]).build()
|
|
}
|
|
|
|
/// Toggles a chip's (or any widget's) `active` CSS class — the `.chip.active`
|
|
/// stylesheet rule fills it with the accent instead of the neutral overlay.
|
|
/// Wiring *when* a chip becomes active (single-select filter, multi-select
|
|
/// tags, etc.) is genuinely per-app, so that stays the caller's job; this is
|
|
/// just the one-line visual toggle every case needs.
|
|
pub fn set_chip_active(chip: &impl IsA<gtk4::Widget>, active: bool) {
|
|
if active {
|
|
chip.add_css_class("active");
|
|
} else {
|
|
chip.remove_css_class("active");
|
|
}
|
|
}
|
|
|
|
/// 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("");
|
|
}
|
|
}
|
|
}
|
|
}
|