shell: fix icon/button/card legibility and screenshot capture for a light theme
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.
This commit is contained in:
parent
dbeb693643
commit
31138da46e
2 changed files with 127 additions and 16 deletions
|
|
@ -152,17 +152,40 @@ fn bar_capture_height() -> i32 {
|
|||
window.height + window.margin.top
|
||||
}
|
||||
|
||||
/// Y-origin for the `bar`/`capsule-collapsed` capture rectangle (axis 2,
|
||||
/// daylight). Every theme through spotlight anchors top, so the bar's own
|
||||
/// pixels always start at the canvas's own top edge (`y = 0`) — that's what
|
||||
/// every existing call site here hardcoded. A bottom-anchored bar's real
|
||||
/// on-screen footprint is instead the LAST `bar_capture_height()` pixels of
|
||||
/// the canvas; capturing from `y = 0` unchanged grabs the isolated
|
||||
/// compositor's empty background/gradient near the top of the output and
|
||||
/// misses the actual bar entirely (confirmed empirically while verifying
|
||||
/// this task — `bread-capture --app breadbar --view bar` under
|
||||
/// `BREAD_SHELL_THEME=daylight` returned a plain gradient with no dock in
|
||||
/// frame at all, before this fix). `canvas_height` is the capture canvas's
|
||||
/// own height (`req.height`, i.e. `--height`/`isolate_height`), not this
|
||||
/// crate's own bar height.
|
||||
fn bar_capture_y(canvas_height: i32, bar_height: i32) -> i32 {
|
||||
let window = crate::theme::shell_theme().window().clone();
|
||||
if window.anchors.iter().any(|a| a == "bottom") {
|
||||
(canvas_height - bar_height).max(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(root: >k4::ApplicationWindow, req: ScreenshotRequest, handles: Handles) {
|
||||
let output = req.output;
|
||||
let (width, height) = (req.width as i32, req.height as i32);
|
||||
let bar_height = bar_capture_height();
|
||||
let bar_y = bar_capture_y(height, bar_height);
|
||||
|
||||
match req.view.as_str() {
|
||||
"bar" => {
|
||||
root.connect_map(move |_| {
|
||||
let output = output.clone();
|
||||
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||
finish(bread_screenshots::capture_region(0, 0, width, bar_height, &output));
|
||||
finish(bread_screenshots::capture_region(0, bar_y, width, bar_height, &output));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -207,7 +230,7 @@ pub fn dispatch(root: >k4::ApplicationWindow, req: ScreenshotRequest, handles:
|
|||
root.connect_map(move |_| {
|
||||
let output = output.clone();
|
||||
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||
finish(bread_screenshots::capture_region(0, 0, width, bar_height, &output));
|
||||
finish(bread_screenshots::capture_region(0, bar_y, width, bar_height, &output));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
116
src/theme.rs
116
src/theme.rs
|
|
@ -112,6 +112,37 @@ fn load_css() -> String {
|
|||
// 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") };
|
||||
// The notification/history/OSD/wifi-add-dialog cards (0.70) and the
|
||||
// control/wifi/media popover window (0.72) hardcode their own alpha
|
||||
// literals independent of `tokens.bg_alpha()` — reasonable for a dark
|
||||
// "glass" surface (0.70-0.72 alpha over a dark fill still reads as
|
||||
// deliberately translucent glass), but daylight's demo draws these as
|
||||
// fully OPAQUE paper (`.note`/`.osd { background: var(--paper) }`, no
|
||||
// alpha at all) — at the old 0.70 literal, daylight's near-white
|
||||
// `{panel}` fill instead reads as pale, background-tinted glass, which
|
||||
// is exactly the "physical object" read the design brief calls for
|
||||
// NOT doing. Swapping these two literals wholesale to `{bg_alpha}`
|
||||
// would also move liquid-motion/glass-workbench/spotlight's own
|
||||
// rendering (0.70/0.72 -> 0.72/0.82 for spotlight) — a real, visible
|
||||
// regression the task rules out — so this only substitutes `bg_alpha`
|
||||
// (0.94 for daylight) in for `light` themes and keeps the exact prior
|
||||
// literal for every other theme.
|
||||
let card_alpha = if light { bg_alpha } else { 0.70 };
|
||||
let panel_surface_alpha = if light { bg_alpha } else { 0.72 };
|
||||
// The OSD/widget-node progress troughs' UNFILLED track is
|
||||
// `alpha(@accent, 0.25)` — a teal-at-25%-alpha tint that reads as a
|
||||
// faint, visible track against a dark pill (every theme through
|
||||
// spotlight), but is nearly indistinguishable from daylight's own
|
||||
// near-white pill (confirmed empirically: the OSD volume slider's
|
||||
// empty track was barely visible against its own paper background).
|
||||
// `{ink}`-based for light themes gives a neutral faint-dark track
|
||||
// instead, independent of whatever hue the accent happens to be;
|
||||
// every other theme's literal `alpha(@accent, 0.25)` is unchanged.
|
||||
let trough_bg = if light {
|
||||
format!("alpha({ink}, 0.14)")
|
||||
} else {
|
||||
"alpha(@accent, 0.25)".to_string()
|
||||
};
|
||||
// `[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),
|
||||
|
|
@ -343,7 +374,13 @@ fn load_css() -> String {
|
|||
on nesting depth, liquid-motion/glass-workbench render byte-\
|
||||
identical CSS either way. */\
|
||||
window.breadbar > box > centerbox {{ padding: {centerbox_padding}; }}\
|
||||
window.breadbar button {{ min-height: 0; min-width: 0; }}\
|
||||
/* `color` here for the same reason `window.breadbar-panel button`\
|
||||
(below) needs it: a real GtkButton's own text (the bar's\
|
||||
hamburger, `.control-panel-btn`) doesn't inherit `color` from\
|
||||
this window — the shared, ecosystem-wide `button {{ color:\
|
||||
@on-surface }}` rule (lib.rs) matches it directly first. See\
|
||||
that rule's own comment for the full explanation. */\
|
||||
window.breadbar button {{ min-height: 0; min-width: 0; color: {ink}; }}\
|
||||
{segment_css}\
|
||||
{workspace_css}\
|
||||
.clock-box {{ padding: 0 4px; }}\
|
||||
|
|
@ -413,10 +450,10 @@ fn load_css() -> String {
|
|||
separator.bar-sep {{ min-height: 12px; min-width: 1px; margin: 0 10px 0 2px;\
|
||||
background: alpha({ink}, 0.10); }}\
|
||||
window.breadbar-notification {{ background-color: transparent; color: {ink}; }}\
|
||||
window.breadbar-history {{ background-color: alpha({panel}, 0.70); color: {ink};\
|
||||
window.breadbar-history {{ background-color: alpha({panel}, {card_alpha}); color: {ink};\
|
||||
border-radius: {radius}; border: 1px solid alpha({ink}, 0.10);\
|
||||
animation: pop-in 0.45s {spring} both; }}\
|
||||
.notification-card {{ background: alpha({panel}, 0.70); color: {ink}; border-radius: {radius};\
|
||||
.notification-card {{ background: alpha({panel}, {card_alpha}); 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; }}\
|
||||
|
|
@ -445,18 +482,38 @@ fn load_css() -> String {
|
|||
.history-time {{ opacity: 0.5; font-size: 11px; }}\
|
||||
.history-body {{ opacity: 0.75; }}\
|
||||
.history-card {{ margin-bottom: 6px; }}\
|
||||
window.breadbar-osd {{ background-color: alpha({panel}, 0.70); color: {ink};\
|
||||
window.breadbar-osd {{ background-color: alpha({panel}, {card_alpha}); 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; }}\
|
||||
progressbar.osd-bar {{ min-height: 6px; }}\
|
||||
progressbar.osd-bar trough {{ background-image: none; background-color: alpha(@accent, 0.25);\
|
||||
progressbar.osd-bar trough {{ background-image: none; background-color: {trough_bg};\
|
||||
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({panel}, 0.72); color: {ink};\
|
||||
window.breadbar-panel {{ background-color: alpha({panel}, {panel_surface_alpha}); color: {ink};\
|
||||
border-radius: 14px; border: 1px solid alpha({ink}, 0.12); }}\
|
||||
/* A real GtkButton's own label text does NOT inherit `color` from\
|
||||
an ancestor window: `bread_theme::stylesheet()`'s shared,\
|
||||
ecosystem-wide `button {{ color: @on-surface }}` rule (lib.rs,\
|
||||
applied to every bread app before this file's CSS layers on\
|
||||
top) matches the button element directly, and a direct match\
|
||||
always beats inheritance regardless of specificity. `@on-surface`\
|
||||
is `ink_on(@surface)`, and `@surface` is `bread_theme::palette`'s\
|
||||
FIXED_SURFACE constant — pinned dark, same as `@bg` — so every\
|
||||
button's label (power row, hamburger, wifi/bluetooth popover\
|
||||
rows, the add-network dialog's Cancel/Connect) rendered\
|
||||
near-white regardless of the active shell theme. Invisible-but-\
|
||||
correct on every dark theme through spotlight; confirmed\
|
||||
empirically as near-invisible ghost text under daylight (isolated\
|
||||
`bread-capture` control-panel screenshot, pre-fix). One rule,\
|
||||
scoped by ancestor class so its specificity beats the shared\
|
||||
unscoped `button` rule, instead of patching each `.power-btn`/\
|
||||
`.control-panel-btn`/`.wifi-popover-row`/etc. class individually. */\
|
||||
window.breadbar-panel button, window.wifi-popover button,\
|
||||
window.wifi-add-dialog button, window.breadbar-notification button,\
|
||||
window.breadbar-history button {{ color: {ink}; }}\
|
||||
window.breadbar-dismiss, .breadbar-dismiss-hit {{\
|
||||
background-color: alpha(#000000, 0.02); }}\
|
||||
.popover-caret {{ min-height: 2px; margin: 2px 4px 10px; border-radius: 2px;\
|
||||
|
|
@ -506,10 +563,10 @@ fn load_css() -> String {
|
|||
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: {ink}; }}\
|
||||
window.wifi-add-dialog {{ background-color: alpha({panel}, 0.70); color: {ink}; min-width: 240px;\
|
||||
window.wifi-add-dialog {{ background-color: alpha({panel}, {card_alpha}); 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({panel}, 0.70); color: {ink};\
|
||||
window.wifi-add-dialog headerbar {{ background-color: alpha({panel}, {card_alpha}); color: {ink};\
|
||||
border-top-left-radius: {radius}; border-top-right-radius: {radius};\
|
||||
border-bottom: 1px solid alpha({ink}, 0.10); box-shadow: none; }}\
|
||||
.confirm-button {{ background-color: @accent; color: @on-accent; }}\
|
||||
|
|
@ -595,7 +652,7 @@ fn load_css() -> String {
|
|||
of Adwaita's default blue-on-gray, and let `style.color` retint\
|
||||
that fill the same way it retints label/icon text. */\
|
||||
.bread-widget-slot {{ margin-right: 12px; }}\
|
||||
progressbar.bread-widget-node trough {{ background-image: none; background-color: alpha(@accent, 0.25); border-radius: 3px; min-height: 6px; }}\
|
||||
progressbar.bread-widget-node trough {{ background-image: none; background-color: {trough_bg}; border-radius: 3px; min-height: 6px; }}\
|
||||
progressbar.bread-widget-node trough progress {{ background-image: none; background-color: @accent; border-radius: 3px; min-height: 6px; }}\
|
||||
progressbar.bread-widget-node.bread-color-fg trough progress {{ background-color: @fg; }}\
|
||||
progressbar.bread-widget-node.bread-color-dim trough progress {{ background-color: alpha(@fg, 0.6); }}\
|
||||
|
|
@ -681,16 +738,47 @@ fn load_css() -> String {
|
|||
}
|
||||
|
||||
/// Returns the ink colour for icon tinting in the stats bar — the same
|
||||
/// luminance-picked colour the bar's text uses, so icons stay legible on the bar
|
||||
/// whatever lightness pywal gives the background.
|
||||
/// luminance-picked colour the bar's text uses, so icons stay legible on the
|
||||
/// bar whatever lightness pywal gives the background.
|
||||
///
|
||||
/// FIXED — axis 1, daylight: this used to be unconditionally
|
||||
/// `ink_on(&load_palette().background)`. `load_palette().background` is
|
||||
/// `bread_theme::palette::FIXED_BACKGROUND` (`"#0c0c0c"`), pinned dark
|
||||
/// regardless of pywal OR the active shell theme (see that constant's own
|
||||
/// doc comment) — so this always resolved to the SAME near-white value,
|
||||
/// baked directly into a rasterised SVG texture at icon-build time
|
||||
/// (`svg_texture_sized`, the only call site), completely outside CSS and
|
||||
/// therefore untouched by `load_css`'s own `panel`/`ink` swap. Every icon
|
||||
/// built through [`crate::svg_image`]/[`crate::svg_texture`] (volume, wifi,
|
||||
/// battery, hamburger, media transport, the OSD glyph) was near-white on
|
||||
/// every existing (dark) theme, which read as correct by construction —
|
||||
/// until daylight's near-white paper pills made the SAME near-white glyph
|
||||
/// nearly invisible against its own background. Confirmed empirically
|
||||
/// (isolated `bread-capture` OSD-volume screenshot, pre-fix) before this
|
||||
/// fix. Mirrors `load_css`'s own `ink` local exactly: the dark theme's
|
||||
/// unchanged `ink_on(background)` (near-white), or — for a light theme —
|
||||
/// `background` itself (the fixed dark hex IS the correct dark ink, the
|
||||
/// same identity `load_css`'s `ink = "@bg"` case relies on).
|
||||
pub fn fg_color() -> String {
|
||||
ink_on(&load_palette().background).to_string()
|
||||
let p = load_palette();
|
||||
if shell_theme().tokens().light() {
|
||||
p.background.clone()
|
||||
} else {
|
||||
ink_on(&p.background).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Ink colour for the given Hyprland output's wallpaper palette.
|
||||
/// Ink colour for the given Hyprland output's wallpaper palette. See
|
||||
/// [`fg_color`]'s doc comment for the same light-theme fix; kept in step
|
||||
/// even though this accessor has no call site today.
|
||||
#[allow(dead_code)]
|
||||
pub fn fg_color_for(output: &str) -> String {
|
||||
ink_on(&load_palette_for(output).background).to_string()
|
||||
let p = load_palette_for(output);
|
||||
if shell_theme().tokens().light() {
|
||||
p.background.clone()
|
||||
} else {
|
||||
ink_on(&p.background).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind this window (and its popover children) to `output`'s palette.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue