Found via isolated bread-capture screenshots of daylight (BREAD_SHELL_THEME=
daylight), not guesswork:
- theme::fg_color() computed icon tint from load_palette().background, which
is bread-theme's FIXED (never-pywal, never-theme-aware) dark constant —
every SVG-rasterised bar/OSD icon was baked near-white regardless of the
active theme: invisible-but-correct on every prior dark theme, nearly
invisible on daylight's own near-white pills. Now theme-aware via
tokens.light().
- Real GtkButtons (power row, hamburger, wifi/bluetooth popover rows, the
add-network dialog) don't inherit the window's ink colour: the shared
ecosystem stylesheet's unscoped button{color:@on-surface} rule matches
them directly first, and @on-surface is ink computed against the same
kind of fixed dark constant. One ancestor-scoped override per
breadbar-owned window fixes all of them without touching the shared rule.
- The notification/history/OSD/wifi-add-dialog cards and the popover window
hardcoded their own 0.70/0.72 alpha literals independent of bg_alpha —
fine as translucent glass on a dark fill, but reads as pale glass instead
of opaque paper on daylight. Now uses bg_alpha for light themes only,
unchanged literal for the other three.
- Same story for the OSD/widget-node progress trough's accent-tinted empty
track, barely visible against a light pill.
- screenshot.rs: the "bar"/"capsule-collapsed" capture views hardcoded a
top-left capture rectangle — correct for every top-anchored theme, but a
bottom-anchored bar's real footprint is the last bar_capture_height()
pixels of the canvas. Confirmed empirically: a daylight bar capture
returned a plain background gradient with no dock in frame at all before
this fix.
- 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.
ANIMATION WORK #3. Liquid Motion only: the island's layer-shell top
margin springs (anim::spring_to) from just above its resting position
up to the theme's configured margin.top over 420ms on first map, plus
an opacity-only fade (the new bar-entrance CSS class / bar-in
keyframe). Deliberately animates the surface's own layer-shell margin
in Rust rather than a CSS margin on an inner widget: that would touch
box-model geometry, and WorkspaceTrail::place()'s single-shot initial
sample is a documented previous crash site for exactly that kind of
still-moving-on-first-paint bug (see the ws-in/row-in guard's own
comment further down). A layer-shell margin change repositions the
whole surface without touching any widget's own measured size, so it's
invisible to compute_bounds(host) by construction.
glass-workbench (flush edge-to-edge bar, no floating margin to slide
from) gets nothing. spotlight gets the opacity fade only -- its
capsule already has its own width/drawer motion in flight during a
real search, so a margin spring felt like piling onto that rather than
complementing it.
surface::click_through set a completely empty input region on the toast
so every pointer event passed through -- including to the toast's own
action buttons and inline-reply entry. Replace it with
surface::set_hit_region(window, widgets), which sets the input region to
the union of the given widgets' rectangles instead of empty; everywhere
else on the surface stays click-through exactly as before.
popup.rs recomputes this via refresh_hit_region every time the card set
could have changed (shown, dismissed, expired -- routed through the one
dismiss() function) and keeps recomputing every frame for HIT_TRACK_MS
afterward, since a card's own entrance animation or the stack's
push-down reflow can still be moving a button on the frame the change
happens. collect_interactive walks the real widget tree for
GtkButton/GtkEntry rather than tracking a flat list, so it can't drift
out of sync with make_card's structure. build_window's connect_map
handles the first-map race the same way surface::click_through used to.
Also: spring_in_card grows a newly shown card's height from 0 to its
natural size via bread_theme::anim::spring_to (same technique as
main.rs's animate_drawer_height), so the existing stack gets pushed
down smoothly instead of jumping.
KeyboardMode::None is unchanged on the toast; history.rs's OnDemand
mode is untouched.
Still to do: the actual dismiss button in make_card (this commit wires
the mechanism that will hit-test it, but no card has one yet).
Reuse the clock's per-digit flip machinery (make_clock_digits /
flip_clock_digits) for the volume and battery stat chips: vol_lbl/bat_lbl
become digit-chip boxes (one .stat-digit label per character) instead of
a single gtk4::Label, and only the characters that actually changed play
the digit-flip keyframe on a StatsUpdate. The volume slider's live drag
feedback uses a new no-animation set_digit_chip instead, since it fires
on every pointer-move tick and replaying the flip that fast would just
flicker.
Drops the now-unused tick_label (both call sites moved to
flip_digit_chip).
Decision #1 (one chip highlight height per bar, vertically centred): the
.stat-pair/.stat-pair.icon-only/.media-widget/workspace-btn CSS all sized
to their own content box before this (min-height: 0, or a stale 32/20px
figure), which is why battery sat high and wifi/menu ran taller than their
neighbours. Added a hardcoded approved_chip_height() (26/22/22 by
WorkspaceStyle, matching the demo spec) since bread-ecosystem's own
chip_height token (32/20/36) predates this pass and disagrees with it, and
that repo is a sibling agent's this cycle.
Also found and fixed a second copy of the hamburger-corner-mismatch bug:
.control-panel-btn (hamburger only) hardcoded its own border-radius/
min-width/min-height *later* in the cascade than .stat-pair, silently
winning over chip_radius/chip_height regardless of the icon-only fix
already in place. Dropped the four conflicting properties so .stat-pair
cascades through unchanged.
Spotlight workspace dots: bumped to the approved Option B spec (10px
tall, widths 8/13/17/22 by open-window count) — the prior pass landed
9px/[6,10,14,18], one generation behind. dot_widths is likewise
theme.toml-derived and stale, so overridden locally with a comment
rather than edited upstream.
Workspace row "sits low on first paint" bug: root cause was the ws-in
entrance animation (row-in keyframe, margin-top 8px -> 0) playing on
every button during the very first rebuild_buttons call, racing
WorkspaceTrail::place()'s single-shot geometry sample and freezing the
trail pill a few px low until the next real switch re-measured settled
layout. The demo never animates the initial row in at all — only
subsequently-added workspaces should. Suppressed ws-in specifically on
the first build (button_map starts empty exactly once).
Also fixed: make_button's set_size_request was still pinning the stale
tokens().chip_height() as a hard GTK minimum, which would have out-ranked
the CSS min-height fix above for Trail/Pill workspace pills.
04-spotlight.html's .dots button is 6px tall — reported as "too small
and hard to click." The click-target side of that is a separate,
non-visual fix elsewhere; this is the visual side only. 6px is also
genuinely hard to see on a real display, not just hard to hit, so
DOT_HEIGHT bumps it to 9px (bar::workspaces::make_dot_button) — enough
to read clearly without growing into a little chip that would fight
the capsule's minimal look. Widths are untouched: dot_widths is the
manifest's own per-occupancy encoding ([6, 10, 14, 18] in the builtin
spotlight theme.toml) and stays exactly as specified; only the height,
which has no manifest token of its own, is breadbar's call to make.
The CSS .workspace-dot min-height is updated to match (9px) so it
doesn't silently drift from the value that actually governs the
rendered size (a direct set_size_request, not normal CSS layout).
Not visually verified via capture — the isolated headless-Sway harness
has no Hyprland IPC, so workspace dots render empty there regardless
of theme (pre-existing harness limit, unrelated to this change).
Verified by reading the CSS/Rust against 04-spotlight.html's <style>
block instead, and by cargo build/clippy/test passing clean.
.workspace-trail/.workspace-btn hardcoded border-radius: 12px and
min-height: 28px — neither matches this theme's own radius_sm token
(9px) nor the 01-liquid-motion.html demo's .ws-btn/.trail rules (26px
tall, 9px radius). radius_sm happens to be an exact match for the
demo's 9px here. Reported: "the pills on liquid motion just look
off".
Not visually verified via capture — the isolated headless-Sway
harness has no Hyprland IPC, so workspace buttons render empty there
regardless of theme (pre-existing harness limit, unrelated to this
change). Verified by reading the CSS against 01-liquid-motion.html's
<style> block instead, and by `cargo build`/clippy/test passing clean.
.stat-pair's border-radius was a hardcoded 10px shared identically by
all three themes: coincidentally close for liquid-motion (radius_sm
9px, matching its demo's .chip radius almost exactly), flatly wrong
for glass-workbench (demo's .chip is 6px, exactly this theme's own
radius_sm), and disconnected from spotlight's much rounder capsule
language. Chips now round by chip_radius: radius_sm for liquid-motion/
glass-workbench, radius_pill for spotlight (its bar/dots are already
that round, so its lone .stat-pair occupant, the battery chip, now
reads as part of that same family instead of a stray rounded rect
inside a much-rounder capsule).
Also drops .stat-pair.icon-only's own 999px radius override. That
made wifi and the liquid-motion hamburger — the only two icon-only
chips — fully circular while their row neighbours (vol, battery)
stayed a rounded rect at .stat-pair's radius: a visible mismatch
inside one row, called out against the hamburger specifically.
Every demo's .chip class draws vol/wifi/battery/menu identically and
none of them circular, so dropping the override just lets the shared
radius cascade through unchanged.
Verified via bread-capture's isolated headless-Sway harness with a
temporary (not committed) debug outline on .stat-pair: liquid-motion's
four right-side chips now share one visible corner radius instead of
two circular + two rounded-rect, and spotlight's battery chip reads as
a rounded pill matching its capsule/dots instead of a sharper rect.
wifi_img.set_hexpand(false) meant the icon packed at the start of the
32px icon-only chip with no leftover space for halign:Center to work
with, sitting visibly left of center on glass-workbench/liquid-motion.
hexpand(true) fixes the centering, but that alone silently bubbles the
expand flag up through connectivity_pair into the shared right-hand
stats box and the centerbox's end slot, blowing the wifi/vol cluster's
layout apart (confirmed via a --screenshot bar capture: the whole
cluster jumped left against the clock with a huge gap before
battery/hamburger). connectivity_pair.set_hexpand(false) pins the
box's own expand explicitly so the fix stays contained to this chip.
Also drops the dead .wifi-pair { padding: 6px } rule: it can never
apply since .stat-pair.icon-only's two-class selector always beats
its one-class specificity regardless of source order, so the intended
padding never actually rendered.
bread_theme:🐚:watch() now returns an opaque ThemeWatch (it re-arms
itself onto a new theme's directory when the active theme id changes,
rather than staying pinned to whichever directory was active at call
time) instead of a bare gio::FileMonitor. Update the stored handle's
type to match.
Completes the spotlight capsule (theme 04), THEME_SYSTEM_PLAN.md §7:
- Keyboard open (item A): a new launcher_command module subscribes to
bread.command.box.open — only when the active theme's launcher is
Embedded — and focuses launcher_entry on receipt, which the existing
EventControllerFocus::connect_enter already turns into an open_fn()
call, same path a mouse click into the entry takes. breadbox's own
keybind-triggered launch now redirects to this same event under an
embedded theme instead of stacking its overlay window on top (see the
breadbox commit).
- Click-away scrim (item B): open_fn/close_fn now show/hide panel.rs's
dismiss surface via the new show_capsule_dismiss/hide_dismiss, and
panels.set_on_dismiss(close_fn) wires a click-away into the capsule's
own close path. The scrim's clickable region starts at a fixed offset
(capsule row height + the drawer's own maximum content height,
DRAWER_MAX_HEIGHT_PX) rather than the drawer's live height, so it can
never geometrically overlap a rendered result row regardless of
breadbar-dismiss's overlay layer always rendering above the bar's own.
- Query modes (item C): connect_changed now runs bread_launcher::parse_query
against the entry text and, when the parsed prefix is listed in
[launcher].modes, swaps launcher_results.scroller for a new mode_list
populated by populate_mode_list (calc result / filtered commands / an
"open this URL" prompt). key_ctrl routes Up/Down/Return at whichever
list is active; a `>`/`.` row's action runs through the new ModeAction
enum (RunShell for a command's own fixed exec string, OpenUrl straight
to xdg-open) so arbitrary typed text is never passed through a shell.
- Sections (item D): ResultsList::new's new `sections` bool is threaded
from [launcher].sections.
- Search-state geometry (item E): root's own width now spring-animates
between [launcher].width and .search_width (animate_capsule_width, same
set_size_request-driven technique animate_drawer_height already uses),
and a `.searching` class on root switches border-radius to
.search_radius via a new CSS rule/transition in theme.rs.
Also fixes a real bug this surfaced: drawer_target_height was measuring
drawer_box itself, whose size_request is continuously overwritten by
animate_drawer_height's own tick callback — GTK clamps a widget's
measure() up to at least its own explicit size request, so switching from
the (tall) app list to a one-row calc result measured the PREVIOUS
frame's forced height instead of the new content's actual size. It now
sums each visible child's own natural height directly, which also fixes
the same latent issue for narrowing app-search results.
screenshot.rs gains two new views (capsule-sections, capsule-calc) to
exercise D and C's calc path.
- New bread-launcher dep (gtk feature), pinned tag v0.7.4 with a dev-only
[patch] entry alongside bread-theme's.
- Wire the drawer slot (plan §2/§7): root becomes a vbox whose first row is
the existing CenterBox and whose second is drawer_box, appended via the
same ModuleRegistry::for_each_in_slot pattern left/centre/right already
use. Empty for liquid-motion/glass-workbench (zero children, zero size);
theme.rs's window.breadbar > centerbox selector becomes > box > centerbox
to follow the new nesting, byte-identical CSS for both existing themes.
- modules.workspaces.style = dots: bar::workspaces::make_dot_button, width
from modules.workspaces.dot_widths, wired as a third rebuild_buttons arm.
- New launcher_entry (plain GtkEntry) and launcher_results
(bread_launcher::gtk::ResultsList, sharing breadbox's cache/history via
bread_launcher::LAUNCHER_APP) modules, built unconditionally and placed
only when a theme's [bar.slots] names them (spotlight today).
- Capsule expand/collapse: typing/focus opens the drawer and animates its
height via bread_theme::anim::spring_to; Up/Down move the selection,
Enter launches (do_launch + record_launch) and collapses, Escape collapses
and releases keyboard focus (on_demand ties to GTK's own focus-widget
state, so releasing GTK focus hands the compositor keyboard back).
- window.width now wires Width::Px into set_default_width (previously
unconsumed — Fill-anchored themes never needed it, the capsule does).
- Two new --screenshot views: capsule-collapsed, capsule-expanded (the
latter focuses launcher_entry, types a query, and captures bar height +
the drawer's actual settled height rather than a guessed constant).
cargo check/clippy: no new warnings (same two pre-existing: dead_code on
system_stats_box/cpu_pair/mem_pair/pwr_pair, from_* on workspaces.rs).
cargo test: 18/18 unchanged.
Wires modules.workspaces.style/modules.clock.style into the bar for real
(Phase 3 shipped the schema but only ever consumed trail/flip), and adds
cpu/ram as bar modules, so glass-workbench (bread-theme) renders correctly
while liquid-motion's default path is untouched:
- workspaces: "pill" never calls WorkspaceTrail::place/stretch (the trail
overlay stays invisible) and honours modules.workspaces.show_empty for
real, rendering unoccupied non-active workspaces dimmed via CSS instead
of filtering them out of the row. "trail" keeps the exact pre-existing
filter/place/stretch behaviour regardless of show_empty.
- clock: "plain" registers a plain date+time label (date_lbl, built but
never parented until now, plus a new clock_plain_lbl) instead of the
per-digit flip box. "flip" is untouched.
- cpu/ram: two new bar chips (bar_cpu_pair/bar_ram_pair, bar_cpu_lbl/
bar_ram_lbl) fed by the same AppInput::StatsUpdate data as the control
panel's cpu_pair/mem_pair. Separate instances rather than reparenting
the panel's own pair — a GTK widget can only have one parent, and
reparenting would pull them out of the control panel's sys-grid, which
no theme asked to change.
theme.rs::load_css() now branches on tokens.bar_border() (full-border
island vs. flush bar's bottom-only hairline) and modules.workspaces.style
(dimmed/translucent trail-fill buttons vs. solid-accent-fill pills), and
unconditionally gains .clock-plain/.clock-plain-time rules. The trail/flip
branches reproduce today's CSS byte-for-byte.
Verification (bread-capture, isolated headless-Sway harness):
- Noise floor: same baseline binary against itself varies from AE=0 (same
capture minute) up to ~1900px (an in-flight digit-flip/clock-tick or a
live Wi-Fi scan straddling two captures) and ~39000px on control-panel
(live hardware sensors) — all pre-existing, not introduced by this change.
- Regression: this binary vs. the pre-Phase-5 baseline, captured in the
same clock-minute to remove the dominant noise source, is AE=0 (bit-for-
bit identical) on the bar view under the default liquid-motion theme.
- glass-workbench (BREAD_SHELL_THEME=glass-workbench): bar view is
1920x36 (flush, no margin) vs. liquid-motion's 1920x56 (12px margin +
44px island), square corners vs. liquid-motion's rounded island, plain
"Mon 24/08 23:24" clock, and cpu/ram/wifi/battery/control chips with no
media widget — matches demo 02. The isolated Sway capture harness has no
live Hyprland IPC, so it reports zero workspaces under both themes
(pre-existing, unrelated to this change) — the pill fill/dim CSS itself
is exercised by code review and the trail-untouched regression result,
not by a captured pixel with visible buttons.
cargo test: 18/18 passing (breadbar), no new clippy/cargo-check warnings
beyond the two pre-existing ones this task named up front.
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.
The control panel lists PipeWire sinks and sets the default, moving
playing streams so the change is immediate. The workspace pill starts
on switch without a layout wait, measures against the Fixed host so it
stays centered on the digit, and ignores row-wide bounds that used to
stretch it across several chips.
One layer-shell window per Hyprland output now loads that output's
bread-theme palette. Notifications, history, and OSD follow the
monitor they appear on. Pin bread-theme to v0.7.4.
Parse FDO action pairs onto popup buttons, emit ActionInvoked on
click (default action on the body), and show an inline-reply field
when senders request it. Closing still emits NotificationClosed.
History persist is unchanged.
Keep a bounded in-memory history (last 50) and a layer-shell window
listing app, summary, truncated body, and time. Toggle with
`breadbar --history` or D-Bus `dev.breadway.Bar.ToggleHistory`.
Pin bread-theme, bread-utils, and bread-screenshots to tag v0.7.2.
bread-shared stays on bread v0.8.0-rc.1.
Was a bare gtk4::Window with no titlebar — GTK4's own minimal CSD
fallback for that case is a flat bar with plain system-font title text
and square corners, which is what actually made this look like a stray
window from a different decade next to the rest of the (rounded,
borderless) ecosystem. Now sets a real HeaderBar (which picks up the
window's existing title automatically) and gives the dialog + headerbar
matching rounded corners and dark theming via the existing
.wifi-add-dialog CSS class.
Also switched the Connect button off "suggested-action": GTK4's bundled
theme special-cases that exact class name for a native OS-accent-colour
feature that isn't a normal CSS rule and doesn't yield to any
background-color override this stylesheet adds, confirmed empirically
(a much more specific selector had zero effect). Moved to
"confirm-button", the same accent-button convention breadman/breadpad
already use.
Known residual issue, not resolved here: the Connect button still
doesn't render the accent colour even via "confirm-button" — narrowed
down to *some* button-specific styling quirk in this one dialog (window-
level rules like the rounded corners and dark background apply
correctly; a maximally-obvious magenta test rule on the same selector
also failed to render, ruling out a color-value mistake specifically),
but not root-caused further. Left as a known follow-up rather than
blocking the CSD fix, which was the actual ask.
Also removed two `cursor: pointer` CSS properties (on .clickable and
.media-widget) that GTK4 doesn't support as a CSS property at all — was
logging "Theme parser error: No property named cursor" on every
startup, doing nothing functionally either way.
Adds real SVG icon assets replacing the icons-needed.txt checklist,
a bluetooth popover module, per-notification urgency (CSS-styled),
and app-name/summary dedup in notification cards. Rounds out the
control panel's stats section with RAM and power draw alongside the
existing CPU/GPU/net rows.
Rows for scanned networks not yet in breadcrumbs' saved config were
both dimmed and disabled (set_sensitive(false)), so there was no way
to add them from the popover. They now stay visually dimmed via the
existing wifi-popover-row-unsaved style but remain clickable, opening
a small password dialog that saves + joins the network via
`breadcrumbs add` + `breadcrumbs join`.
GitHub Actions self-hosted runners need per-repo registration on a
personal account; Forgejo Actions' runner already serves every repo
with zero setup. Moves release publishing there (dl.breadway.dev stays
the primary bakery target; GitHub release upload is kept as the
fallback via an explicit token, since Forgejo Actions has no ambient
GITHUB_TOKEN) and adds a mirror workflow to keep GitHub in sync
automatically.
Use bread-theme 0.2.7's luminance-picked ink (@on-*) for text on coloured
backgrounds: the active workspace pill and notification cards previously kept the
pywal foreground, which vanished when those slots came out light. Drop the
blanket label colour rule (it overrode the per-surface ink on child labels).
Switch to bread_theme::gtk::apply_app_css so the bar recolours live on
`bread-theme reload` instead of only at startup.
Call bread_theme::gtk::apply_shared() before breadbar's own rules so fonts,
palette, and generic widgets come from the one ecosystem stylesheet (and
recolour live). Keep only breadbar-specific CSS (bar window, workspace
buttons, stats, notifications, OSD). Bump bread-theme dep to v0.2.6.
- Cargo.toml: depend on bread-theme (path dep for local dev, git dep for
production) with gtk feature; remove local theme dependencies
- src/theme.rs: replace local pywal/Catppuccin impl with bread_theme::gtk
helpers; local bar-specific CSS is preserved
- bakery.toml: describes breadbar for bakery install
- release.yml: builds on hestia self-hosted runner, publishes binary to
dl.breadway.dev and GitHub Releases on v* tags