diff --git a/src/bar/workspaces.rs b/src/bar/workspaces.rs index f91ad67..86d8268 100644 --- a/src/bar/workspaces.rs +++ b/src/bar/workspaces.rs @@ -142,7 +142,13 @@ pub fn make_button( btn.set_halign(gtk4::Align::Center); btn.set_vexpand(false); btn.set_hexpand(false); - btn.set_size_request(-1, crate::theme::shell_theme().tokens().chip_height() as i32); + // `crate::theme::approved_chip_height`, not `tokens().chip_height()`: + // the latter is the stale pre-demo `breadbar::CHIP_HEIGHT` token (32 + // for this Trail/Pill style's theme) and, as a hard `set_size_request` + // minimum, would out-rank the CSS `min-height` the demo actually wants + // (26px Trail / 22px Pill) — see that function's doc comment. + let style = crate::theme::shell_theme().modules().workspaces.style; + btn.set_size_request(-1, crate::theme::approved_chip_height(style) as i32); if let Some(child) = btn.child() { child.set_halign(gtk4::Align::Center); child.set_valign(gtk4::Align::Center); @@ -156,21 +162,27 @@ pub fn make_button( } /// `style = "dots"` (theme 04/spotlight): a label-less pill whose WIDTH -/// encodes `windows` (0/1/2/3-or-more open) via `dot_widths` — see -/// `bread_theme::shell::WorkspacesModule::dot_widths`. Distinct from -/// [`make_button`] (Trail/Pill) rather than a variant of it because dots -/// carry no text at all (`04-spotlight.html`'s `.dots button` has no label); -/// reusing `Button::with_label("")` would still measure/lay out an empty -/// label box that a genuinely childless button doesn't. Width is a hard +/// encodes `windows` (0/1/2/3-or-more open). Distinct from [`make_button`] +/// (Trail/Pill) rather than a variant of it because dots carry no text at +/// all (`04-spotlight.html`'s `.dots button` has no label); reusing +/// `Button::with_label("")` would still measure/lay out an empty label box +/// that a genuinely childless button doesn't. Width is a hard /// `set_size_request` snap, not animated — GTK CSS min-width transitions /// don't participate in a directly-set size request the way an opacity/ /// background-color transition does, and the plan only calls out the /// capsule's own expand/collapse as worth the `anim::spring_to` treatment. +/// +/// `_dot_widths` (the manifest's `modules.workspaces.dot_widths`) is +/// accepted but deliberately unused — see `APPROVED_DOT_WIDTHS` below, +/// which overrides it with the approved Option B numbers the manifest's +/// own value predates. Kept in the signature rather than dropped so the +/// call site still documents where a real per-theme width would flow from +/// once `theme.toml` catches up. pub fn make_dot_button( id: WorkspaceId, active: WorkspaceId, windows: i32, - dot_widths: bread_theme::shell::DotWidths, + _dot_widths: bread_theme::shell::DotWidths, ) -> gtk4::Button { let btn = gtk4::Button::new(); btn.add_css_class("workspace-dot"); @@ -186,17 +198,26 @@ pub fn make_dot_button( btn.set_hexpand(false); // Height is a deliberate departure from `04-spotlight.html`'s own 6px // (see the demo's `.dots button { height: 6px }`): reported as "too - // small and hard to click" — the click-target enlargement is a - // separate, non-visual fix elsewhere, but 6px is also genuinely hard - // to *see* on a real display, not just hard to hit. 9px keeps the - // dots reading as slim pills rather than growing into little chips - // (which would fight the capsule's minimal, text-first look), while - // being clearly perceptible against the 36px-tall bar. Widths are left - // exactly as `dot_widths` (the manifest's own per-occupancy encoding, - // e.g. `[6, 10, 14, 18]`) specifies — only the height, which has no - // manifest token of its own, is breadbar's call to make. - const DOT_HEIGHT: i32 = 9; - btn.set_size_request(dot_widths[dot_width_index(windows)], DOT_HEIGHT); + // small and hard to click", and also genuinely hard to *see* on a real + // display, not just hard to hit. Option B (approved): 10px tall — up + // from an earlier 9px pass that undershot the approved number by 1px. + // Keeps the dots reading as slim pills rather than growing into little + // chips (which would fight the capsule's minimal, text-first look), + // while being clearly perceptible against the 36px-tall bar. + const DOT_HEIGHT: i32 = 10; + // Option B widths (approved): 8/13/17/22px for 0/1/2/3-or-more open + // windows — `[8, 13, 17, 22]`, not the `dot_widths` parameter's own + // manifest value. `theme.toml`'s `modules.workspaces.dot_widths = + // [6, 10, 14, 18]` predates this pass and was never updated to match; + // hardcoded here (ignoring the passed-in `dot_widths`) rather than + // edited upstream, since bread-ecosystem is a sibling agent's repo + // this pass. Flagged in the task report — `dot_widths` should become + // `[8, 13, 17, 22]` in `assets/shell/spotlight/theme.toml`. + const APPROVED_DOT_WIDTHS: bread_theme::shell::DotWidths = [8, 13, 17, 22]; + btn.set_size_request( + APPROVED_DOT_WIDTHS[dot_width_index(windows)], + DOT_HEIGHT, + ); btn.connect_clicked(move |_| { relm4::spawn(async move { switch_workspace(id).await; diff --git a/src/main.rs b/src/main.rs index 3a8618f..23ae426 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1995,6 +1995,13 @@ impl App { self.workspace_trail.cancel(); let prev: std::collections::HashSet = self.button_map.keys().copied().collect(); + // `button_map` only starts empty once — the very first call this + // App instance ever makes, before any workspace has ever been + // synced from Hyprland. A fully-emptied bar never happens after + // that (the active workspace's own row is always kept), so this + // doubles as a clean "is this the initial paint" signal without a + // dedicated flag — see its one use below. + let is_first_build = prev.is_empty(); while let Some(child) = self.workspace_box.first_child() { self.workspace_box.remove(&child); } @@ -2038,7 +2045,20 @@ impl App { bar::workspaces::make_button(ws.id, &ws.name, self.active_ws, ws.windows > 0) } }; - if !prev.contains(&ws.id) { + // Never on the very first build (bug: "the [Trail] row sits + // ~5px low on first paint and only corrects after the first + // switch"). Root cause: `ws-in`'s `row-in` keyframe animates + // `margin-top` 8px → 0 over 320ms; `WorkspaceTrail::place` + // (called once, synchronously-ish, right after this loop for + // the initial row) samples each button's geometry via a + // single-shot tick callback that can fire while that margin + // is still mid-animation, freezing the trail pill a few px + // low until the next `place`/`stretch` call (the first real + // workspace switch) re-samples the by-then-settled layout. + // The demo itself never animates the initial row in at all + // (`OCC.forEach` builds plainly, only `place(0)` runs) — only + // *subsequently added* workspaces should ever play this. + if !is_first_build && !prev.contains(&ws.id) { play_once(&btn, "ws-in", 360); } self.workspace_box.append(&btn); diff --git a/src/theme.rs b/src/theme.rs index f19dd57..a9731db 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -35,6 +35,21 @@ pub fn set_shell_theme(theme: ShellTheme) { SHELL_THEME.with(|cell| *cell.borrow_mut() = Rc::new(theme)); } +/// The one bar-chip height every chip in the row shares (vol/wifi/battery/ +/// menu/media, and the workspace pills for Trail/Pill styles) — see the +/// `chip_h` local in `load_css` for why this is a hardcoded per-` +/// WorkspaceStyle` override rather than `Tokens::chip_height()`. Used both +/// by that CSS and by `bar::workspaces::make_button`'s `set_size_request`, +/// which otherwise still forces the stale token value as a hard GTK +/// minimum that a CSS `min-height` alone cannot out-rank. +pub fn approved_chip_height(style: bread_theme::shell::WorkspaceStyle) -> i64 { + use bread_theme::shell::WorkspaceStyle::*; + match style { + Trail => 26, + Pill | Dots => 22, + } +} + fn load_css() -> String { // breadbar-specific rules only — fonts, base colours, and generic widgets // come from the shared ecosystem stylesheet (applied first in `apply()`). @@ -118,6 +133,24 @@ fn load_css() -> String { _ => radius_sm.clone(), }; + // ONE chip highlight height per bar, vertically centred — every chip + // (vol/wifi/battery/menu/media, icon-only and labelled alike) shares + // it so their fills align, instead of each sizing to its own content + // box (reported: battery sits high, wifi/menu are taller than their + // neighbours). Liquid Motion 26px / Glass Workbench 22px / Spotlight + // 22px, per the approved demo spec. + // + // HARDCODED, not `tokens.chip_height()`: that token is `breadbar:: + // CHIP_HEIGHT` (32) carried over from before this design pass and was + // never updated for the three builtin `theme.toml`s (32/20/36) — it + // predates and disagrees with the demo numbers above. bread-ecosystem + // is owned by a sibling agent this pass, so this stays a local + // override (same `WorkspaceStyle` this file already keys `chip_radius` + // off) rather than an edit to that repo's schema/manifests. Flagged in + // the task report: `chip_height` should become 26/22/22 upstream. + let chip_h = approved_chip_height(theme.modules().workspaces.style); + let chip_height_px = format!("{chip_h}px"); + // `modules.workspaces.style` (plan §11 Phase 5): "trail" (default, // liquid-motion) is exactly today's CSS, unchanged byte-for-byte — // dimmed/translucent buttons with the gradient trail overlay supplying @@ -140,7 +173,7 @@ fn load_css() -> String { background-color: @accent; border-radius: {radius_sm}; }}\ .workspace-btn {{ background: transparent; opacity: 0.36; color: @on-bg;\ border-radius: {radius_sm}; border: none; outline: none; box-shadow: none;\ - min-width: 28px; min-height: 26px; margin: 0; padding: 0 7px;\ + 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}; }}\ @@ -155,7 +188,7 @@ fn load_css() -> String { format!( ".workspace-btn {{ background: transparent; opacity: 1; color: alpha(@on-bg, 0.4);\ border-radius: {radius_sm}; border: none; outline: none; box-shadow: none;\ - min-width: 22px; min-height: 20px; margin: 0; padding: 0 6px;\ + 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}; }}\ @@ -253,7 +286,13 @@ fn load_css() -> String { radius_pill for spotlight, so every theme's stat chips round\ the way that theme's *other* rounded chrome already does,\ instead of all three sharing one borrowed hardcoded number. */\ - .stat-pair {{ margin: 0; border-radius: {chip_radius}; padding: 5px 9px; min-height: 0;\ + /* min-height (not the old `min-height: 0`): decision #1 — ONE\ + chip highlight height per bar, vertically centred, shared by\ + every chip so their fills align instead of each sizing to its\ + own content box (reported: battery sat high, wifi/menu were\ + taller than their neighbours). See `chip_height_px` above. */\ + .stat-pair {{ margin: 0; border-radius: {chip_radius}; padding: 5px 9px;\ + 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); }}\ @@ -271,7 +310,7 @@ fn load_css() -> String { out (spotlight has no icon-only chip today, but would get the\ same pill radius as its one `.stat-pair` sibling if it ever did). */\ .stat-pair.icon-only {{ padding: 4px;\ - min-width: 32px; min-height: 32px; }}\ + min-width: {chip_height_px}; min-height: {chip_height_px}; }}\ .stat-icon {{ margin-right: 6px; }}\ .stat-pair.icon-only .stat-icon {{ margin: 0; }}\ .bt-icon {{ margin-right: 8px; }} @@ -368,7 +407,10 @@ fn load_css() -> String { border-bottom: 1px solid alpha(@on-bg, 0.10); box-shadow: none; }}\ .confirm-button {{ background-color: @accent; color: @on-accent; }}\ .confirm-button:hover {{ background-color: alpha(@accent, 0.85); }}\ - .media-widget {{ border-radius: 10px; padding: 4px 8px; min-height: 0;\ + /* min-height: decision #1 — the media chip is a bar chip like\ + any other, so it shares the same row height instead of sizing\ + 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.media-in {{ animation: row-in 0.4s {spring} both; }}\ @@ -385,8 +427,20 @@ fn load_css() -> String { .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); }}\ - .control-panel-btn {{ padding: 5px 8px; margin: 0; border-radius: 10px;\ - opacity: 0.92; font-size: 18px; line-height: 1; min-width: 0; min-height: 0;\ + /* 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\ + `.stat-pair.icon-only` AND `.control-panel-btn`, and because\ + this rule sits later in the cascade its hardcoded 10px radius\ + and 0 min-size were silently winning over `.stat-pair`'s own\ + `chip_radius`/`chip_height_px` — the exact hamburger-corner-\ + mismatch bug decision #2 describes, and a second copy of\ + decision #1's height bug, both reintroduced by\ + this one class alone. Dropping the four properties lets\ + `.stat-pair`/`.stat-pair.icon-only` cascade through unchanged,\ + same fix shape as the icon-only border-radius removal above. */\ + .control-panel-btn {{ margin: 0;\ + opacity: 0.92; font-size: 18px; line-height: 1;\ background: transparent; border: none; outline: none; box-shadow: none;\ transition: background-color 0.22s {spring_settle},\ opacity 0.18s ease; }}\ @@ -508,6 +562,7 @@ fn load_css() -> String { 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,