diff --git a/Cargo.lock b/Cargo.lock index b0ebcac..e99d7bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,7 @@ version = "0.3.1" dependencies = [ "dirs", "gtk4", + "libadwaita", "serde", "serde_json", ] @@ -1279,6 +1280,36 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "libadwaita" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b9900e67182a4b5b1f157b448d94f0715c8b9770cce21cf000801917f53bfa" +dependencies = [ + "gdk4", + "gio", + "glib", + "gtk4", + "libadwaita-sys", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3c27642b389852aa99341bd4a4c19ec6f8a2b63ebdd7f5ba1952198079ccd" +dependencies = [ + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "libc" version = "0.2.186" diff --git a/bread-theme/Cargo.toml b/bread-theme/Cargo.toml index 43c3952..f5e1cda 100644 --- a/bread-theme/Cargo.toml +++ b/bread-theme/Cargo.toml @@ -13,11 +13,24 @@ serde = { workspace = true } serde_json = { workspace = true } dirs = { workspace = true } gtk4 = { version = "0.11", features = ["v4_12"], optional = true } +# Rust bindings for libadwaita (GNOME's widget library on top of GTK4) — the +# actual source of the modern GNOME look (grouped preference rows, real +# toggle/spin rows, view switchers), not just a CSS reskin of plain GTK4 +# widgets. `v1_7` for ToggleGroup (used for tab-row-style pickers); the +# system library only needs to be >= that (this machine has 1.9.2). +libadwaita = { version = "0.9", features = ["v1_7"], optional = true } [features] # Enable GTK4 CSS provider helpers (breadbar, breadbox, breadpad use this). # bread (daemon) and breadcrumbs (CLI) depend on this crate without the feature. gtk = ["dep:gtk4"] +# Composite libadwaita-based widgets (bread_theme::adw) — separate from `gtk` +# because libadwaita's own top-level window chrome (AdwApplicationWindow) +# isn't compatible with gtk4-layer-shell surfaces, so the five layer-shell +# apps (breadbar, breadbox, breadclip, breadsearch, breadpad) only want +# plain CSS, not this. Apps with an ordinary top-level window (breadman, +# breadhelp) want both. +adw = ["gtk", "dep:libadwaita"] # The generator CLI. It only touches the gtk-free lib API (render + write), so # it builds without the gtk feature and stays light. diff --git a/bread-theme/src/adw.rs b/bread-theme/src/adw.rs new file mode 100644 index 0000000..118e528 --- /dev/null +++ b/bread-theme/src/adw.rs @@ -0,0 +1,78 @@ +//! Composite libadwaita widgets for the bread ecosystem's design system — +//! the actual mechanism (real GNOME-style widgets, not more hand-rolled CSS) +//! behind why bos-settings' sidebar/section/toggle rows read as more polished +//! than the plain-GTK4 apps'. An app calls these instead of assembling boxes +//! and labels and raw widgets from scratch each time, so spacing/sizing/ +//! grouping decisions get made once, correctly, here — not re-derived per +//! screen. +//! +//! Not usable from the five `gtk4-layer-shell` apps (breadbar, breadbox, +//! breadclip, breadsearch, breadpad): `AdwApplicationWindow`'s own chrome +//! isn't compatible with a layer-shell surface, and these helpers assume an +//! ordinary top-level window. Apps with a plain top-level window (breadman, +//! breadhelp) can use the full set. + +use libadwaita as adw; +use adw::prelude::*; + +/// Call once at startup, before building any widgets from this module — +/// initializes libadwaita's style manager and forces dark mode regardless of +/// the system GTK theme preference. bread-theme's whole design is a *fixed* +/// dark base (only the accent tracks pywal — see `palette::FIXED_BACKGROUND` +/// etc.) so an app respecting a light system preference here would silently +/// break that contract the moment someone's GNOME settings say "light". +pub fn init() { + adw::init().expect("failed to initialize libadwaita"); + adw::StyleManager::default().set_color_scheme(adw::ColorScheme::ForceDark); +} + +/// A titled, optionally-described group of setting rows — the +/// title-then-description-then-rows rhythm bos-settings already uses per +/// section, now available to native GTK4/relm4 apps instead of a hand-rolled +/// vbox with a bold label glued to the top. +pub fn preferences_group(title: &str, description: Option<&str>) -> adw::PreferencesGroup { + let group = adw::PreferencesGroup::builder().title(title).build(); + if let Some(desc) = description { + group.set_description(Some(desc)); + } + group +} + +/// A single on/off setting row with a correctly-sized, correctly-positioned +/// switch — the direct fix for the ~1400px-wide stretched-switch bug +/// (breadman/settings had no intrinsic width on its hand-rolled switch, so +/// it filled the row like a progress bar). +pub fn toggle_row(title: &str, subtitle: Option<&str>, active: bool) -> adw::SwitchRow { + let row = adw::SwitchRow::builder().title(title).active(active).build(); + if let Some(sub) = subtitle { + row.set_subtitle(sub); + } + row +} + +/// A single numeric setting row (spin button docked to its own label, +/// instead of stranded ~1300px away at the window's far edge). +pub fn spin_row(title: &str, subtitle: Option<&str>, adjustment: >k4::Adjustment) -> adw::SpinRow { + let row = adw::SpinRow::builder().title(title).adjustment(adjustment).build(); + if let Some(sub) = subtitle { + row.set_subtitle(sub); + } + row +} + +/// A general label(+subtitle) row with room for a trailing widget +/// (`row.add_suffix(&widget)`) — for settings that don't fit switch/spin +/// (text entries, buttons, dropdowns, a raw value display). +pub fn action_row(title: &str, subtitle: Option<&str>) -> adw::ActionRow { + let row = adw::ActionRow::builder().title(title).build(); + if let Some(sub) = subtitle { + row.set_subtitle(sub); + } + row +} + +/// A page of one or more `preferences_group`s, with correct margins and +/// scroll handling — the top-level content container for a settings screen. +pub fn preferences_page() -> adw::PreferencesPage { + adw::PreferencesPage::new() +} diff --git a/bread-theme/src/gtk.rs b/bread-theme/src/gtk.rs index aab7d01..fb759c7 100644 --- a/bread-theme/src/gtk.rs +++ b/bread-theme/src/gtk.rs @@ -114,6 +114,29 @@ pub fn apply_css(css: &str, provider: &RefCell>) { } } +/// 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, 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>) { diff --git a/bread-theme/src/lib.rs b/bread-theme/src/lib.rs index a97588b..5d1ff7f 100644 --- a/bread-theme/src/lib.rs +++ b/bread-theme/src/lib.rs @@ -1,6 +1,8 @@ pub mod palette; #[cfg(feature = "gtk")] pub mod gtk; +#[cfg(feature = "adw")] +pub mod adw; pub use palette::{load_palette, Palette}; @@ -187,6 +189,14 @@ pub fn stylesheet(p: &Palette) -> String { switch {{ background-color: @overlay; border-radius: {pill}px; }}\n\ switch:checked {{ background-color: @accent; }}\n\ switch slider {{ background-color: @on-surface; border-radius: {pill}px; }}\n\ + /* GtkScale (sliders) render with GTK's own default accent (a fixed\ + blue, independent of the app's theme) unless styled explicitly —\ + every app with a volume/brightness slider was silently showing\ + that default instead of the palette's accent until this rule\ + existed. */\n\ + scale trough {{ background-color: @overlay; border-radius: {pill}px; min-height: 6px; }}\n\ + scale trough highlight {{ background-color: @accent; border-radius: {pill}px; min-height: 6px; }}\n\ + scale slider {{ background-color: @on-bg; border-radius: {pill}px; }}\n\ list, listbox {{ background-color: transparent; }}\n\ row {{ border-radius: {r2}px; }}\n\ row:selected, list row:selected {{ background-color: @accent; color: @on-accent; }}\n\