Drive bar/window geometry and CSS tokens from the shell theme manifest

breadbar no longer hardcodes BAR_HEIGHT/BAR_MARGIN_TOP/BAR_MARGIN_SIDES/
CHIP_HEIGHT/ICON_PX or the root window's layer-shell setup: it loads
bread_theme:🐚:ShellTheme once (theme::shell_theme(), cached in a
thread-local) and reads bar.window (anchors, margin, exclusive zone,
keyboard mode - now set explicitly instead of relying on the library
default) plus tokens.{icon_px,chip_height} from it everywhere those used
to be literals.

The four satellite surfaces (breadbar-osd, breadbar-notif, breadbar-panel,
breadbar-dismiss) now get their anchor/margin/width/layer from the
manifest's [surfaces.*] table via a small new surface::apply() helper,
narrowly scoped to the three anchor shapes those surfaces actually use.
The pre-existing 8px gap between the panel's top margin and the dismiss
scrim's is preserved exactly (and now commented) rather than "fixed".

theme.rs's load_css() keeps its ~250 lines of hand-written breadbar CSS
(notifications, wifi popover, control panel, media widget) but now reads
its five radius/pad locals and the two easing curves (spring vs
spring_settle - hover/settle transitions were previously miscategorized
as the overshoot curve in the constant audit; the actual code already
used the settle curve there, confirmed against src) from theme tokens
instead of hardcoding them.

Also wires bread_theme:🐚:watch() so editing the active theme's
theme.toml/extra.css hot-reloads CSS tokens without a restart, same as
a pywal palette change already does. Window-spec values still need a
restart per the plan (read once at window-construction time).

Verified pixel-identical: captured all 10 breadbar --screenshot views
(bar, control-panel, connectivity-wifi/bluetooth, media-popover,
notification/-critical, osd-volume/-brightness, wifi-add-dialog) via
bread-capture's isolated headless-Sway harness against both the
pre-change and post-change binaries; every view diffs byte-identical
at the decoded-pixel level.
This commit is contained in:
Breadway 2026-08-23 15:58:25 +08:00
parent 297207a7aa
commit bbb1a6c8d2
11 changed files with 278 additions and 99 deletions

78
src/surface.rs Normal file
View file

@ -0,0 +1,78 @@
//! Applies a `[surfaces.<namespace>]` entry (plan §4/§6, Phase 2) to a
//! satellite layer-shell window: anchor, margin (from `offset`), width and
//! layer. Deliberately narrow — it only understands the three anchor shapes
//! breadbar's four built-in surfaces actually use today ("breadbar-notif",
//! "breadbar-osd", "breadbar-panel", "breadbar-dismiss": `top_right`,
//! `bottom_centre`, `fill`), not a general anchor DSL (the plan's own
//! anti-goal, §2). `exclusive` zone and `keyboard` mode aren't part of the
//! `[surfaces.*]` schema (`bread_theme::shell::Surface` has no such fields)
//! and stay hardcoded at each call site, same as before this refactor.
use bread_theme::shell::SurfaceWidth;
use gtk4::prelude::*;
use gtk4_layer_shell::{Edge, Layer, LayerShell};
/// `namespace` should be a key in the active theme's `[surfaces.*]` table —
/// every call site in this crate passes one of breadbar's own namespace
/// literals, so a miss here means the active theme fell out of sync with
/// the Rust source, not a bad runtime value. Logs and leaves the window at
/// gtk4-layer-shell's own defaults rather than panicking, matching every
/// other "malformed/incomplete theme" fallback in this system.
///
/// Does not set `set_default_width` for a namespace shared by more than one
/// window with genuinely different widths (`breadbar-notif`'s live toast is
/// 320px, its history sibling is 360px, and only the toast's width is
/// modeled in `[surfaces.*]` — see the Phase 0 constant inventory); callers
/// that need a different width than the theme's own set it explicitly
/// afterward.
pub fn apply(window: &gtk4::Window, namespace: &str) {
let theme = crate::theme::shell_theme();
let Some(surf) = theme.surfaces().get(namespace) else {
eprintln!(
"breadbar: no [surfaces.{namespace}] entry in the active theme; \
window left at layer-shell defaults"
);
return;
};
window.set_layer(if surf.layer == "top" {
Layer::Top
} else {
Layer::Overlay
});
match surf.anchor.as_str() {
"top_right" => {
window.set_anchor(Edge::Top, true);
window.set_anchor(Edge::Right, true);
// offset = [right, top] for this anchor shape.
let right = surf.offset.first().copied().unwrap_or(0.0) as i32;
let top = surf.offset.get(1).copied().unwrap_or(0.0) as i32;
window.set_margin(Edge::Right, right);
window.set_margin(Edge::Top, top);
}
"bottom_centre" => {
window.set_anchor(Edge::Bottom, true);
let bottom = surf.offset.first().copied().unwrap_or(0.0) as i32;
window.set_margin(Edge::Bottom, bottom);
}
"fill" => {
for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] {
window.set_anchor(edge, true);
}
// Only a top margin is meaningful here — a fullscreen click-away
// scrim that starts below the bar rather than covering it.
let top = surf.offset.first().copied().unwrap_or(0.0) as i32;
window.set_margin(Edge::Top, top);
}
other => eprintln!(
"breadbar: surfaces.{namespace}.anchor = \"{other}\" is not one of \
top_right|bottom_centre|fill breadbar's satellite windows don't \
understand any other shape yet, leaving this window unanchored"
),
}
if let SurfaceWidth::Px(px) = surf.width {
window.set_default_width(px);
}
}