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:
parent
9761b85112
commit
dbeb693643
3 changed files with 312 additions and 84 deletions
137
src/main.rs
137
src/main.rs
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: >k4::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: >k4::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"
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
211
src/theme.rs
211
src/theme.rs
|
|
@ -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.)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue