bread-theme: add libadwaita components + fix slider/chip theming gaps

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.
This commit is contained in:
Breadway 2026-07-29 22:45:18 +08:00
parent 6eb3479529
commit e898535bb4
5 changed files with 155 additions and 0 deletions

78
bread-theme/src/adw.rs Normal file
View file

@ -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: &gtk4::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()
}

View file

@ -114,6 +114,29 @@ pub fn apply_css(css: &str, provider: &RefCell<Option<CssProvider>>) {
}
}
/// 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>>) {

View file

@ -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\