shell: wire up bottom-anchored, segmented, light bar chrome for Daylight

- main.rs: apply bar.window.margin.bottom to the layer-shell surface (was
  parsed but never set — WindowSpec::margin.bottom's own doc comment
  flagged this gap); fix the exclusive-zone calc to reserve the ANCHORED
  edge's margin instead of always margin.top (exclusive_zone_for, unit
  tested); tag the three bar slot-group boxes with an unconditional
  .bar-segment CSS class (a no-op unless the active theme sets
  bar_border = "segmented").
- surface.rs: add the bottom_right satellite anchor (mirrors top_right on
  the bottom edge) and extend the fill anchor's offset to a [top, bottom]
  pair so the dismiss scrim can leave its gap at the bottom of the screen
  instead of only the top.
- theme.rs: tokens.light() swaps which of the fixed @bg/@on-bg pair plays
  paper-surface vs ink across every translucent card/panel/hover-wash in
  the stylesheet; bar_border = "segmented" makes window.breadbar fully
  transparent and gives .bar-segment its own pill surface/border/shadow;
  the Trail workspace style now actually substitutes accent_from/accent_to
  instead of a hardcoded @accent/@teal gradient; accent2 gives the media
  equaliser an accent independent of the workspace-trail one.
This commit is contained in:
Breadway 2026-08-26 20:19:28 +08:00
parent 9761b85112
commit dbeb693643
3 changed files with 312 additions and 84 deletions

View file

@ -66,6 +66,108 @@ macro_rules! capsule_trace {
/// how tall the drawer currently is.
const DRAWER_MAX_HEIGHT_PX: i32 = 420;
/// `bar.window.exclusive` resolved to a layer-shell exclusive-zone pixel
/// value (axis 2, daylight). `Exclusive::Auto` reserves `height` plus the
/// margin on whichever edge the bar is actually anchored to — `margin.top`
/// for every theme through spotlight (all top-anchored), `margin.bottom`
/// for daylight's bottom-anchored dock. Before daylight this always read
/// `margin.top` unconditionally, which happened to be correct for every
/// existing theme (a bottom-anchored bar had never shipped, so
/// `margin.bottom` was always 0 and never observed) but reserved 12px too
/// little for daylight (`height` alone, not `height + margin.bottom`) —
/// tiled windows would have run up under the last 12px of the floating
/// dock. Extracted to a pure function so this is unit-testable without a
/// live layer-shell surface.
fn exclusive_zone_for(spec: &bread_theme::shell::WindowSpec) -> i32 {
let anchor_margin = if spec.anchors.iter().any(|a| a == "bottom") {
spec.margin.bottom
} else {
spec.margin.top
};
match spec.exclusive {
Exclusive::Auto => spec.height + anchor_margin,
Exclusive::None => -1,
Exclusive::Px(px) => px,
}
}
#[cfg(test)]
mod exclusive_zone_tests {
use super::exclusive_zone_for;
use bread_theme::shell::{Exclusive, Margin, Width, WindowSpec};
fn spec(anchors: &[&str], height: i32, margin: Margin, exclusive: Exclusive) -> WindowSpec {
WindowSpec {
anchors: anchors.iter().map(|s| s.to_string()).collect(),
width: Width::Fill,
height,
margin,
exclusive,
keyboard: bread_theme::shell::Keyboard::None,
layer: "top".to_string(),
}
}
#[test]
fn top_anchored_auto_reserves_height_plus_top_margin() {
let s = spec(
&["top", "left", "right"],
44,
Margin {
top: 12,
left: 16,
right: 16,
bottom: 0,
},
Exclusive::Auto,
);
assert_eq!(exclusive_zone_for(&s), 56);
}
#[test]
fn bottom_anchored_auto_reserves_height_plus_bottom_margin_not_top() {
// The daylight case: margin.top is 0 (nothing reserved there), the
// real floating gap is margin.bottom.
let s = spec(
&["bottom", "left", "right"],
40,
Margin {
top: 0,
left: 0,
right: 0,
bottom: 12,
},
Exclusive::Auto,
);
assert_eq!(exclusive_zone_for(&s), 52);
}
#[test]
fn none_is_no_reservation_regardless_of_anchor() {
let s = spec(
&["bottom"],
40,
Margin::default(),
Exclusive::None,
);
assert_eq!(exclusive_zone_for(&s), -1);
}
#[test]
fn explicit_px_wins_over_computed_auto_value() {
let s = spec(
&["bottom", "left", "right"],
40,
Margin {
bottom: 12,
..Margin::default()
},
Exclusive::Px(99),
);
assert_eq!(exclusive_zone_for(&s), 99);
}
}
pub struct BarInit {
pub screenshot: Option<screenshot::ScreenshotRequest>,
pub monitor: Option<String>,
@ -324,6 +426,13 @@ impl SimpleComponent for App {
root.set_margin(Edge::Top, window_spec.margin.top);
root.set_margin(Edge::Left, window_spec.margin.left);
root.set_margin(Edge::Right, window_spec.margin.right);
// `Margin::bottom` (axis 2, daylight): every builtin before daylight
// anchored top and left this at its struct default of 0, which is
// exactly why this call was missing entirely — see that field's own
// doc comment (bread-theme) for the gap this closes. A bottom-
// anchored theme's floating gap off the screen edge IS this margin,
// the same way a top-anchored theme's is `margin.top`.
root.set_margin(Edge::Bottom, window_spec.margin.bottom);
// `Width::Fill` (Island/Edge): unset, exactly as before this
// change — the surface stretches to the anchored left/right edges
// on its own, with no explicit width request needed. `Width::Px`
@ -342,13 +451,18 @@ impl SimpleComponent for App {
// regardless of what the app catalog contains.
root.set_size_request(px, -1);
}
// "auto" reserves height + top margin so tiled clients sit below the
// gap — see WindowSpec::exclusive's doc comment (bread-theme).
let exclusive_zone = match window_spec.exclusive {
Exclusive::Auto => window_spec.height + window_spec.margin.top,
Exclusive::None => -1,
Exclusive::Px(px) => px,
};
// "auto" reserves height + the margin on the ANCHORED edge, so
// tiled clients sit clear of the gap — see WindowSpec::exclusive's
// doc comment (bread-theme). Every theme before daylight anchored
// top, so `margin.top` was the only edge that ever mattered here;
// reading it unconditionally under a bottom-anchored bar reserved
// only `height` (40px), 12px short of the dock's real footprint
// (`height` + `margin.bottom`) — tiled windows would have run up
// into the last 12px of the floating dock. `margin_for_exclusive`
// picks whichever edge's margin actually borders the reserved
// strip, same reasoning `exclusive_zone_for`'s own doc comment
// (tested standalone) spells out.
let exclusive_zone = exclusive_zone_for(&window_spec);
root.set_exclusive_zone(exclusive_zone);
// ANIMATION WORK #3: bar entrance on first map, Liquid Motion
// only. The flush glass-workbench bar has no floating margin to
@ -433,6 +547,13 @@ impl SimpleComponent for App {
let workspace_trail = bar::workspaces::WorkspaceTrail::new();
let workspace_box = workspace_trail.buttons.clone();
let workspace_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
// `.bar-segment` (axis 3, daylight): a no-op class under every other
// theme (theme.rs only ever emits a `.bar-segment` CSS rule when
// `bar_border == "segmented"` — see its `segment_css` local) —
// added unconditionally here, same as `center_area`/`stats_box`
// below, so the Rust side never has to branch on the active theme
// id to know which boxes are "the three segments".
workspace_row.add_css_class("bar-segment");
workspace_row.set_margin_start(8);
workspace_row.set_valign(gtk4::Align::Center);
workspace_row.set_vexpand(false);
@ -685,6 +806,7 @@ impl SimpleComponent for App {
// Center area: [media_widget · widgets · clock · widgets]
let center_area = gtk4::Box::new(gtk4::Orientation::Horizontal, 12);
center_area.add_css_class("center-area");
center_area.add_css_class("bar-segment");
center_area.set_valign(gtk4::Align::Center);
center_area.set_vexpand(false);
// `media_widget`/`clock_box` and any `widget:*` entries interleaved
@ -695,6 +817,7 @@ impl SimpleComponent for App {
// Demo order: [vol 64] [wifi] [bat 83] [☰]
let stats_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 2);
stats_box.add_css_class("stats-box");
stats_box.add_css_class("bar-segment");
stats_box.set_margin_end(2);
stats_box.set_valign(gtk4::Align::Center);
stats_box.set_vexpand(false);

View file

@ -1,12 +1,16 @@
//! 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
//! layer. Deliberately narrow — it only understands the four 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.
//! `bottom_right`, `bottom_centre`, `fill`), not a general anchor DSL (the
//! plan's own anti-goal, §2). `bottom_right` was added for daylight (plan
//! §11 phase 7, axis 2) — the first bottom-anchored bar, whose satellites
//! need to hug the bottom-right corner the way every top-anchored theme's
//! already hug the top-right one. `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::*;
@ -54,6 +58,23 @@ pub fn apply(window: &gtk4::Window, namespace: &str) {
window.set_margin(Edge::Right, right);
window.set_margin(Edge::Top, top);
}
// Axis 2 (daylight): every theme through spotlight anchors its bar
// to the TOP, so `top_right` always put breadbar-notif/
// breadbar-panel naturally close to the bar. A bottom-anchored bar
// has nothing in the original three shapes that keeps its
// satellites near it — `top_right` would land them at the opposite
// corner of the screen from the dock they belong to. Mirrors
// `top_right` exactly, just on the bottom edge.
"bottom_right" => {
window.set_anchor(Edge::Bottom, true);
window.set_anchor(Edge::Right, true);
// offset = [right, bottom], same convention as top_right's
// [right, top].
let right = surf.offset.first().copied().unwrap_or(0.0) as i32;
let bottom = surf.offset.get(1).copied().unwrap_or(0.0) as i32;
window.set_margin(Edge::Right, right);
window.set_margin(Edge::Bottom, bottom);
}
"bottom_centre" => {
window.set_anchor(Edge::Bottom, true);
let bottom = surf.offset.first().copied().unwrap_or(0.0) as i32;
@ -63,15 +84,24 @@ pub fn apply(window: &gtk4::Window, namespace: &str) {
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.
// offset = [top, bottom] — a fullscreen click-away scrim that
// leaves a gap clear of the bar on whichever edge the bar
// actually anchors to. Every theme through spotlight anchors
// top, so only `offset[0]` (top) was ever meaningful before
// daylight; a single-value `offset` (every existing theme's
// manifest) still means exactly what it always did, since
// `offset.get(1)` falls back to 0 — a bottom-anchored theme is
// the first to give this a real, nonzero bottom value instead.
let top = surf.offset.first().copied().unwrap_or(0.0) as i32;
let bottom = surf.offset.get(1).copied().unwrap_or(0.0) as i32;
window.set_margin(Edge::Top, top);
window.set_margin(Edge::Bottom, bottom);
}
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"
top_right|bottom_right|bottom_centre|fill breadbar's satellite \
windows don't understand any other shape yet, leaving this window \
unanchored"
),
}

View file

@ -90,6 +90,28 @@ fn load_css() -> String {
// and unconditional-but-unused block below), so this being "accent" vs
// "green" vs "pink" per theme has no visible effect on them.
let accent_from = tokens.accent_from();
// `accent_to` (Trail's own gradient end stop) and `accent2` (a second,
// independent accent — daylight's amber equaliser, distinct from its
// teal workspace-trail) — both palette token names, never hex, same
// reasoning as `accent_from` above.
let accent_to = tokens.accent_to();
let accent2 = tokens.accent2();
// Axis 1 (daylight, plan §11 phase 7): `tokens.light()` — see that
// method's doc comment (bread-theme) for the full reasoning. Every
// surface/ink pair in this stylesheet through spotlight hardcoded
// `@bg` (a FIXED, never-pywal-derived dark hex — see
// `bread_theme::palette`'s `FIXED_BACKGROUND`) as the translucent
// surface fill and `@on-bg` (that fixed dark colour's computed-legible,
// therefore always near-white, ink) as the text/wash colour. That is
// exactly backwards for an ink-on-paper theme: `panel`/`ink` swap which
// of those two FIXED, anti-correlated tokens plays which role. This is
// NOT a general "pick any light surface colour" mechanism — it works
// only because `@bg` is pinned dark and `@on-bg` is its computed
// opposite, by construction, regardless of pywal. See the task report
// for the full inventory of every site this swap had to reach.
let light = tokens.light();
let (panel, ink): (&str, &str) = if light { ("@on-bg", "@bg") } else { ("@bg", "@on-bg") };
// `[launcher].search_radius` (plan §7 phase 6c) — `LauncherMode::
// Embedded` only (spotlight); `.launcher().radius` itself already
// equals `radius_bar` for that theme (see its own theme.toml comment),
@ -108,12 +130,56 @@ fn load_css() -> String {
// horizontal padding too: the flush bar's demo padding (`0 12px`,
// symmetric) differs from the island's own asymmetric `0 8px 0 6px`.
let flush = tokens.bar_border() == "bottom";
let window_border = if flush {
"border: none; border-bottom: 1px solid alpha(@on-bg, 0.07);".to_string()
// Axis 3 (daylight, plan §11 phase 7): `bar_border == "segmented"` —
// `window.breadbar` itself draws NO fill/border/radius/shadow at all
// (fully transparent); the bar's three slot-group containers
// (`workspace_row`/`center_area`/`stats_box`, each carrying a
// `.bar-segment` class added unconditionally in main.rs) draw their own
// pill surfaces instead — see `segment_css` below. This is what lets
// one GTK window read as three detached floating pills rather than one
// continuous strip; see the task report for exactly what this can and
// can't express (the window is still ONE input-hit-testable surface —
// clicking in a transparent gap between pills does not click through to
// whatever's behind the bar, only true multi-window segmentation would
// do that).
let segmented = tokens.bar_border() == "segmented";
let window_chrome = if segmented {
"background-color: transparent; border: none; box-shadow: none;".to_string()
} else if flush {
format!(
"background-color: alpha({panel}, {bg_alpha}); border: none; \
border-bottom: 1px solid alpha({ink}, 0.07);"
)
} else {
"border: 1px solid alpha(@on-bg, 0.08);".to_string()
format!("background-color: alpha({panel}, {bg_alpha}); border: 1px solid alpha({ink}, 0.08);")
};
let bar_radius = if segmented {
"0px".to_string()
} else {
radius_bar.clone()
};
let centerbox_padding = if flush || segmented { "0 14px" } else { "0 8px 0 6px" };
// The three detached pills themselves — a no-op empty rule under every
// non-segmented theme, so `.bar-segment` (added unconditionally to all
// three slot-group boxes in main.rs) renders nothing extra for them.
// Demo: `.seg { background: rgba(255,255,255,.94); border: 1px solid
// rgba(26,29,34,.10); border-radius: 14px;
// box-shadow: 0 2px 10px rgba(26,29,34,.13), 0 0 0 .5px rgba(255,255,255,.7) inset }`.
// Safe to pair a real box-shadow with an unblurred surface (axis 4:
// daylight's own `[compositor.breadbar].blur = false`) — a box-shadow
// above a BLURRED, `ignore_alpha`d surface is the shadow-halo bug fixed
// in breadbox by removing the shadow outright; with blur off there is
// no blur pass to catch this rectangle inside.
let segment_css = if segmented {
format!(
".bar-segment {{ background-color: alpha({panel}, {bg_alpha}); \
border: 1px solid alpha({ink}, 0.10); border-radius: {radius_bar}; \
box-shadow: 0 2px 10px alpha({ink}, 0.13); }}\
"
)
} else {
String::new()
};
let centerbox_padding = if flush { "0 12px" } else { "0 8px 0 6px" };
// Radius for `.stat-pair` (vol/wifi/battery/hamburger chips): radius_sm
// for liquid-motion (9px) and glass-workbench (6px, exact match to that
@ -169,15 +235,25 @@ fn load_css() -> String {
// it's also used but for a different, already-correct reason.
// Reported: "the pills on liquid motion just look off".
bread_theme::shell::WorkspaceStyle::Trail => format!(
".workspace-trail {{ background-image: linear-gradient(90deg, @accent, @teal);\
background-color: @accent; border-radius: {radius_sm}; }}\
.workspace-btn {{ background: transparent; opacity: 0.36; color: @on-bg;\
// `@{{accent_from}}`/`@{{accent_to}}`, not the literal
// `@accent`/`@teal` this hardcoded through spotlight: harmless
// while every Trail-style theme's own accent_from/accent_to
// happened to BE "accent"/"teal" (liquid-motion — the only
// other Trail theme so far), but daylight sets
// accent_from = accent_to = "teal" for a FLAT fill, and the old
// hardcode would have painted a stray blue-to-teal gradient
// over it regardless of that setting. See Tokens::accent_to's
// doc comment (bread-theme) — this is the fix that finally
// consumes it for real.
".workspace-trail {{ background-image: linear-gradient(90deg, @{accent_from}, @{accent_to});\
background-color: @{accent_from}; border-radius: {radius_sm}; }}\
.workspace-btn {{ background: transparent; opacity: 0.36; color: {ink};\
border-radius: {radius_sm}; border: none; outline: none; box-shadow: none;\
min-width: 28px; min-height: {chip_height_px}; margin: 0; padding: 0 7px;\
font-size: 22px; font-weight: bold;\
transition: opacity 0.22s {spring_settle},\
background-color 0.22s {spring_settle}; }}\
.workspace-btn:hover {{ opacity: 0.85; background: alpha(@on-bg, 0.08); }}\
.workspace-btn:hover {{ opacity: 0.85; background: alpha({ink}, 0.08); }}\
.workspace-btn.occupied {{ opacity: 0.78; }}\
.workspace-btn.active {{ background: transparent; color: @on-accent; opacity: 1; }}\
.workspace-btn.active:hover {{ background: transparent; }}\
@ -186,14 +262,14 @@ fn load_css() -> String {
bread_theme::shell::WorkspaceStyle::Pill => {
let accent = theme.tokens().accent_from();
format!(
".workspace-btn {{ background: transparent; opacity: 1; color: alpha(@on-bg, 0.4);\
".workspace-btn {{ background: transparent; opacity: 1; color: alpha({ink}, 0.4);\
border-radius: {radius_sm}; border: none; outline: none; box-shadow: none;\
min-width: 22px; min-height: {chip_height_px}; margin: 0; padding: 0 6px;\
font-size: 12px; font-weight: 600;\
transition: background-color 0.22s {spring_settle},\
color 0.22s {spring_settle}, opacity 0.22s {spring_settle}; }}\
.workspace-btn:hover {{ background: alpha(@on-bg, 0.08); }}\
.workspace-btn.occupied {{ color: alpha(@on-bg, 0.8); }}\
.workspace-btn:hover {{ background: alpha({ink}, 0.08); }}\
.workspace-btn.occupied {{ color: alpha({ink}, 0.8); }}\
.workspace-btn:not(.occupied):not(.active) {{ opacity: 0.35; }}\
.workspace-btn.active {{ background: @{accent}; color: @on-accent; opacity: 1; }}\
.workspace-btn.active:hover {{ background: @{accent}; }}\
@ -221,12 +297,12 @@ fn load_css() -> String {
// constant's own doc comment for why. This min-height
// exists mainly so the property isn't silently absent from
// the stylesheet a reader would expect to define it.
".workspace-dot {{ background-color: alpha(@on-bg, 0.35); color: transparent;\
".workspace-dot {{ background-color: alpha({ink}, 0.35); color: transparent;\
border-radius: {radius_pill}; border: none; outline: none; box-shadow: none;\
min-height: 9px; margin: 0; padding: 0;\
transition: background-color 0.25s {spring_settle},\
opacity 0.25s {spring_settle}; }}\
.workspace-dot:hover {{ background-color: alpha(@on-bg, 0.55); }}\
.workspace-dot:hover {{ background-color: alpha({ink}, 0.55); }}\
.workspace-dot:not(.occupied):not(.active) {{ opacity: 0.35; }}\
.workspace-dot.active {{ background-color: @{accent}; opacity: 1; }}\
.workspace-dot.active:hover {{ background-color: @{accent}; }}",
@ -254,8 +330,7 @@ fn load_css() -> String {
this class at all. */\
@keyframes bar-in {{ from {{ opacity: 0; }} }}\
.bar-entrance {{ animation: bar-in 0.4s {spring_settle} both; }}\
window.breadbar {{ background-color: alpha(@bg, {bg_alpha}); color: @on-bg;\
border-radius: {radius_bar}; {window_border}\
window.breadbar {{ color: {ink}; border-radius: {bar_radius}; {window_chrome}\
transition: border-radius 0.3s {spring_settle}; }}\
/* `[launcher].search_radius` (plan §7 phase 6c, spotlight only —\
`launcher_entry` never gets focus under any other theme, so\
@ -269,6 +344,7 @@ fn load_css() -> String {
identical CSS either way. */\
window.breadbar > box > centerbox {{ padding: {centerbox_padding}; }}\
window.breadbar button {{ min-height: 0; min-width: 0; }}\
{segment_css}\
{workspace_css}\
.clock-box {{ padding: 0 4px; }}\
.clock-label {{ font-size: 24px; font-weight: bold; letter-spacing: 0.04em;\
@ -315,8 +391,8 @@ fn load_css() -> String {
min-height: {chip_height_px};\
transition: background-color 0.22s {spring_settle},\
opacity 0.18s ease; }}\
.stat-pair:hover {{ background: alpha(@on-bg, 0.12); }}\
.stat-pair:active {{ background: alpha(@on-bg, 0.18); }}\
.stat-pair:hover {{ background: alpha({ink}, 0.12); }}\
.stat-pair:active {{ background: alpha({ink}, 0.18); }}\
/* No border-radius override here (was a hardcoded 999px, making\
wifi/hamburger the only two `.icon-only` chips fully\
circular while their row neighbours vol/battery stayed a\
@ -335,13 +411,13 @@ fn load_css() -> String {
.stat-pair.icon-only .stat-icon {{ margin: 0; }}\
.bt-icon {{ margin-right: 8px; }}
separator.bar-sep {{ min-height: 12px; min-width: 1px; margin: 0 10px 0 2px;\
background: alpha(@on-bg, 0.10); }}\
window.breadbar-notification {{ background-color: transparent; color: @on-bg; }}\
window.breadbar-history {{ background-color: alpha(@bg, 0.70); color: @on-bg;\
border-radius: {radius}; border: 1px solid alpha(@on-bg, 0.10);\
background: alpha({ink}, 0.10); }}\
window.breadbar-notification {{ background-color: transparent; color: {ink}; }}\
window.breadbar-history {{ background-color: alpha({panel}, 0.70); color: {ink};\
border-radius: {radius}; border: 1px solid alpha({ink}, 0.10);\
animation: pop-in 0.45s {spring} both; }}\
.notification-card {{ background: alpha(@bg, 0.70); color: @on-bg; border-radius: {radius};\
padding: {pad}; margin-bottom: 8px; border: 1px solid alpha(@on-bg, 0.10);\
.notification-card {{ background: alpha({panel}, 0.70); color: {ink}; border-radius: {radius};\
padding: {pad}; margin-bottom: 8px; border: 1px solid alpha({ink}, 0.10);\
border-left: 3px solid transparent;\
animation: notif-in 0.45s {spring_settle} both; }}\
.notification-card.urgency-critical {{ border-left-color: @red; }}\
@ -358,19 +434,19 @@ fn load_css() -> String {
add vertical bulk the approved demo's own card never has. */\
.notification-dismiss {{ min-width: 18px; min-height: 18px; padding: 0;\
margin: 2px; border-radius: {radius_pill}; background: transparent;\
color: @on-bg; opacity: 0.45; font-size: 12px; font-weight: bold;\
color: {ink}; opacity: 0.45; font-size: 12px; font-weight: bold;\
border: none; outline: none; box-shadow: none;\
transition: background-color 0.18s {spring_settle}, opacity 0.18s ease; }}\
.notification-dismiss:hover {{ opacity: 1; background: alpha(@on-bg, 0.16); }}\
.notification-dismiss:active {{ background: alpha(@on-bg, 0.24); }}\
.notification-dismiss:hover {{ opacity: 1; background: alpha({ink}, 0.16); }}\
.notification-dismiss:active {{ background: alpha({ink}, 0.24); }}\
.history-title {{ font-weight: bold; font-size: 13px; }}\
.history-close {{ padding: 2px 8px; }}\
.history-empty {{ opacity: 0.5; padding: 8px 0; }}\
.history-time {{ opacity: 0.5; font-size: 11px; }}\
.history-body {{ opacity: 0.75; }}\
.history-card {{ margin-bottom: 6px; }}\
window.breadbar-osd {{ background-color: alpha(@bg, 0.70); color: @on-bg;\
border-radius: {radius_pill}; border: 1px solid alpha(@on-bg, 0.10);\
window.breadbar-osd {{ background-color: alpha({panel}, 0.70); color: {ink};\
border-radius: {radius_pill}; border: 1px solid alpha({ink}, 0.10);\
animation: osd-in 0.4s {spring_settle} both; }}\
.osd-icon {{ opacity: 0.85; margin-right: 8px; }}\
.osd-icon-muted {{ opacity: 0.35; }}\
@ -379,8 +455,8 @@ fn load_css() -> String {
border-radius: 3px; min-height: 6px; }}\
progressbar.osd-bar trough progress {{ background-image: none; background-color: @accent;\
border-radius: 3px; min-height: 6px; }}\
window.breadbar-panel {{ background-color: alpha(@bg, 0.72); color: @on-bg;\
border-radius: 14px; border: 1px solid alpha(@on-bg, 0.12); }}\
window.breadbar-panel {{ background-color: alpha({panel}, 0.72); color: {ink};\
border-radius: 14px; border: 1px solid alpha({ink}, 0.12); }}\
window.breadbar-dismiss, .breadbar-dismiss-hit {{\
background-color: alpha(#000000, 0.02); }}\
.popover-caret {{ min-height: 2px; margin: 2px 4px 10px; border-radius: 2px;\
@ -389,9 +465,9 @@ fn load_css() -> String {
animation: caret-draw 0.45s {spring} both; }}\
.wifi-popover-inner {{ min-width: 228px; padding: {pad}; }}\
window.wifi-popover button {{ min-height: 0; min-width: 0; }}\
.popover-tab-row {{ background: alpha(@on-bg, 0.06); border-radius: 10px;\
.popover-tab-row {{ background: alpha({ink}, 0.06); border-radius: 10px;\
padding: 3px; margin-bottom: 10px; }}\
.popover-tab {{ background: transparent; color: @on-bg; border: none; box-shadow: none;\
.popover-tab {{ background: transparent; color: {ink}; border: none; box-shadow: none;\
outline: none; border-radius: 999px; padding: 0 14px; min-height: 32px;\
font-size: 17px; font-weight: bold; opacity: 0.55;\
transition: background-color 0.22s {spring_settle},\
@ -408,7 +484,7 @@ fn load_css() -> String {
outline: none; border-radius: 10px; padding: 0 12px; min-height: 42px;\
transition: background-color 0.18s {spring_settle}; }}\
.wifi-popover-row label {{ font-size: 18px; }}\
.wifi-popover-row:hover {{ background: alpha(@on-bg, 0.08); }}\
.wifi-popover-row:hover {{ background: alpha({ink}, 0.08); }}\
.wifi-popover-row-active {{ background: alpha(@accent, 0.14); color: @accent; }}\
.wifi-popover-row-active:hover {{ background: alpha(@accent, 0.20); }}\
.row-in {{ animation: row-in 0.32s {spring} both; }}\
@ -424,18 +500,18 @@ fn load_css() -> String {
switch.bt-switch:checked:hover {{ min-width: 42px; min-height: 24px; padding: 2px;\
border: none; outline: none; box-shadow: none; background-image: none;\
border-radius: 99px; }}\
switch.bt-switch {{ background-color: alpha(@on-bg, 0.14);\
switch.bt-switch {{ background-color: alpha({ink}, 0.14);\
transition: background-color 0.25s {spring_settle}; }}\
switch.bt-switch:checked {{ background-color: @accent; }}\
switch.bt-switch slider {{ min-width: 20px; min-height: 20px; margin: 0;\
border-radius: 99px; border: none; outline: none; box-shadow: none;\
background-image: none; background-color: @on-bg; }}\
window.wifi-add-dialog {{ background-color: alpha(@bg, 0.70); color: @on-bg; min-width: 240px;\
border-radius: {radius}; border: 1px solid alpha(@on-bg, 0.10);\
background-image: none; background-color: {ink}; }}\
window.wifi-add-dialog {{ background-color: alpha({panel}, 0.70); color: {ink}; min-width: 240px;\
border-radius: {radius}; border: 1px solid alpha({ink}, 0.10);\
animation: pop-in 0.45s {spring} both; }}\
window.wifi-add-dialog headerbar {{ background-color: alpha(@bg, 0.70); color: @on-bg;\
window.wifi-add-dialog headerbar {{ background-color: alpha({panel}, 0.70); color: {ink};\
border-top-left-radius: {radius}; border-top-right-radius: {radius};\
border-bottom: 1px solid alpha(@on-bg, 0.10); box-shadow: none; }}\
border-bottom: 1px solid alpha({ink}, 0.10); box-shadow: none; }}\
.confirm-button {{ background-color: @accent; color: @on-accent; }}\
.confirm-button:hover {{ background-color: alpha(@accent, 0.85); }}\
/* min-height: decision #1 — the media chip is a bar chip like\
@ -443,10 +519,10 @@ fn load_css() -> String {
to its own eq-bar/label content. */\
.media-widget {{ border-radius: 10px; padding: 4px 8px; min-height: {chip_height_px};\
transition: background-color 0.22s {spring_settle}; }}\
.media-widget:hover {{ background: alpha(@on-bg, 0.08); }}\
.media-widget:hover {{ background: alpha({ink}, 0.08); }}\
.media-widget.media-in {{ animation: row-in 0.4s {spring} both; }}\
.media-eq {{ min-height: 14px; margin-right: 4px; }}\
.media-eq-bar {{ min-width: 3px; min-height: 5px; background-color: @accent;\
.media-eq-bar {{ min-width: 3px; min-height: 5px; background-color: @{accent2};\
border-radius: 2px; }}\
.media-widget.playing .media-eq-bar {{\
animation: media-eq 0.85s ease-in-out infinite alternate; }}\
@ -457,7 +533,7 @@ fn load_css() -> String {
.media-controls {{ padding: 4px; }}\
.media-btn {{ min-width: 32px; padding: 4px 8px; border-radius: {radius_sm};\
transition: background-color 0.18s ease; }}\
.media-btn:hover {{ background: alpha(@on-bg, 0.10); }}\
.media-btn:hover {{ background: alpha({ink}, 0.10); }}\
/* No padding/border-radius/min-width/min-height here (was\
`padding: 5px 8px; border-radius: 10px; min-width: 0;\
min-height: 0`): the hamburger is the only button carrying both\
@ -475,8 +551,8 @@ fn load_css() -> String {
background: transparent; border: none; outline: none; box-shadow: none;\
transition: background-color 0.22s {spring_settle},\
opacity 0.18s ease; }}\
.control-panel-btn:hover {{ opacity: 1; background: alpha(@on-bg, 0.10); }}\
.control-panel-btn:active {{ background: alpha(@on-bg, 0.16); }}\
.control-panel-btn:hover {{ opacity: 1; background: alpha({ink}, 0.10); }}\
.control-panel-btn:active {{ background: alpha({ink}, 0.16); }}\
.control-panel {{ }}\
.control-panel-inner {{ min-width: 248px; padding: {pad}; }}\
.sys-grid {{ margin: 2px 0 6px; }}\
@ -488,7 +564,7 @@ fn load_css() -> String {
.control-panel-row-label {{ font-size: 16px; opacity: 0.78; }}\
.control-panel-slider {{ margin: 0; padding: 0; min-height: 18px; }}\
scale.control-panel-slider trough {{ min-height: 6px; border-radius: 99px;\
background-image: none; background-color: alpha(@on-bg, 0.12);\
background-image: none; background-color: alpha({ink}, 0.12);\
border: none; outline: none; box-shadow: none; }}\
scale.control-panel-slider highlight {{ min-height: 6px; border-radius: 99px;\
background-image: none; background-color: @accent; }}\
@ -499,14 +575,14 @@ fn load_css() -> String {
.sink-row label {{ font-size: 15px; }}\
.power-row {{ margin-top: 8px; }}\
.power-btn {{ min-width: 0; min-height: 0; padding: 8px 10px; border-radius: 8px;\
background: alpha(@on-bg, 0.08); font-size: 13px; border: none;\
background: alpha({ink}, 0.08); font-size: 13px; border: none;\
outline: none; box-shadow: none;\
transition: background-color 0.2s {spring_settle}; }}\
.power-btn:hover {{ background: alpha(@on-bg, 0.14); }}\
.power-btn:hover {{ background: alpha({ink}, 0.14); }}\
.power-btn:active {{ background: alpha(@accent, 0.22); }}\
.notification-action {{ transition: background-color 0.18s ease; }}\
.tray-btn {{ transition: opacity 0.2s ease, background-color 0.2s ease; }}\
separator {{ margin: 4px 0; background: alpha(@on-bg, 0.10); }}\
separator {{ margin: 4px 0; background: alpha({ink}, 0.10); }}\
/* Lua-declared widgets (see Documentation.md's Widgets §style): the\
slot rule below is what the four inline `.bread-widget-slot`\
containers in main.rs rely on for the same 12px stat-pair rhythm\
@ -562,20 +638,20 @@ fn load_css() -> String {
are built regardless of the active theme (see main.rs's \"Assemble\"\
section), just never placed in a slot outside spotlight, so these\
rules render nothing on liquid-motion/glass-workbench. */\
.launcher-entry {{ background: transparent; color: @on-bg; border: none;\
.launcher-entry {{ background: transparent; color: {ink}; border: none;\
outline: none; box-shadow: none; caret-color: @{accent_from};\
font-size: 13px; font-weight: 500; letter-spacing: 0.06em;\
padding: 0; margin: 0; min-height: 0; }}\
.launcher-entry.searching {{ font-size: 15px; letter-spacing: 0; }}\
.bread-drawer {{ min-height: 0; }}\
.bread-drawer.open {{ border-top: 1px solid alpha(@on-bg, 0.08);\
.bread-drawer.open {{ border-top: 1px solid alpha({ink}, 0.08);\
margin-top: 6px; padding-top: 2px; }}\
.bread-drawer listbox {{ background: transparent; padding: 2px 0; }}\
.bread-drawer row {{ padding: 8px 14px; border-radius: {radius_sm};\
color: @on-bg; background-color: transparent; }}\
.bread-drawer row:hover {{ background-color: alpha(@on-bg, 0.08); }}\
color: {ink}; background-color: transparent; }}\
.bread-drawer row:hover {{ background-color: alpha({ink}, 0.08); }}\
.bread-drawer row:selected {{ background-color: alpha(@{accent_from}, 0.18);\
color: @on-bg; }}\
color: {ink}; }}\
.bread-drawer .app-name {{ font-size: 14px; font-weight: 500; }}\
.bread-drawer .app-muted {{ opacity: 0.45; font-size: 11px; }}\
/* `[launcher].sections` (plan §7 phase 6c) — the idle drawer's\
@ -587,21 +663,20 @@ fn load_css() -> String {
.section-header-label {{ font-size: 11px; font-weight: 600;\
letter-spacing: 0.08em; text-transform: uppercase;\
opacity: 0.45; }}",
radius = radius,
radius_search = radius_search,
radius_bar = radius_bar,
radius_sm = radius_sm,
radius_pill = radius_pill,
chip_radius = chip_radius,
chip_height_px = chip_height_px,
pad = pad,
spring = spring,
spring_settle = spring_settle,
window_border = window_border,
centerbox_padding = centerbox_padding,
workspace_css = workspace_css,
bg_alpha = bg_alpha,
accent_from = accent_from,
// Implicit capture (2021 edition) for every `{name}` above: each
// matches an in-scope `let` binding of the same name (`radius`,
// `spring`, `ink`, `panel`, `bar_radius`, `window_chrome`,
// `segment_css`, `accent_from`, `accent2`, ...) rather than a hand-
// maintained `name = name,` list — axis 1's `panel`/`ink` swap and
// axis 3's `segment_css`/`window_chrome`/`bar_radius` locals both
// needed a growing, easy-to-desync explicit list to stay in step
// with the string body above; switching the whole call to implicit
// capture removes that failure mode instead of extending it further.
// (`radius_bar`, `flush`, `light`, `segmented`, `window_border`
// itself, and `accent_to` are each read by name ABOVE this literal,
// not inside it, so they're deliberately absent here — an unused
// implicit-capture name is a hard compile error, same discipline
// this crate already applies to unread `theme.toml` keys.)
)
}