The libadwaita-based settings screen (previous commit) had two real bugs a closer look (and a screenshot from the real running app) caught: - AdwSpinRow's internal GtkSpinButton has no width constraint of its own, so once the row was widened to 900px, the spin button stretched to fill it - the digits and +/- buttons ended up stranded behind a huge empty bordered box, the exact bug the design review flagged, just worse. - bread-theme's shared `entry, spinbutton` rule outlines every field with an always-visible @overlay border, which on a light-cream overlay colour reads as a stark white outline against the dark theme. Rather than keep fighting libadwaita's internal row/spin-button opinions (there's no supported way to reach in and constrain them), settings.rs is now plain GTK4 mirroring bos-settings' own Row.svelte/NumberField.svelte/ TextField.svelte design exactly: same tokens (12/16px row padding, ch-width inputs, transparent-at-rest border, accent border only on focus), built on a plain `list.boxed-list` for the native rounded-corner-run + divider styling. Full control over sizing, no internal widget to hunt for. Also fixed a real ordering bug in the editor AdwDialog conversion: `open_editor` used to call `dialog.present()` internally before returning, so callers that connected `dialog.connect_map` afterward (screenshot mode) missed the signal entirely - it can fire synchronously inside `present`. Presentation now happens at each call site, after wiring `connect_map`. Also gave the dialog's content an explicit height/vexpand + min-content- height, since the ScrolledWindow had none and the whole dialog was collapsing to just its header bar. breadpad-shared's bread-theme dependency also gets fixed here: it was still pinned to an old GitHub-mirror tag (v0.2.8) while breadman pinned the same crate to git.breadway.dev's dev branch - two different copies of bread-theme compiled into the same binary, so breadman's actual runtime CSS (built through breadpad_shared::theme) never saw any of the shared stylesheet fixes above regardless of what breadman's own direct dependency resolved to.
333 lines
9.4 KiB
Rust
333 lines
9.4 KiB
Rust
pub use bread_theme::{load_palette, Palette};
|
|
|
|
/// Apply breadpad/breadman's stylesheet and keep it live across palette changes.
|
|
/// [`build_css`] bundles the shared component sheet with the app's own rules from
|
|
/// the current pywal palette; `bread_theme::gtk::apply_app_css` re-runs this
|
|
/// whenever `bread-theme reload` rewrites the shared theme file, so the UI
|
|
/// recolours in place (and re-reads the user's `style.css` override too).
|
|
pub fn apply_live() {
|
|
bread_theme::gtk::apply_app_css(|| {
|
|
let palette = load_palette();
|
|
let user_css = std::fs::read_to_string(crate::config::style_css_path()).ok();
|
|
build_css(&palette, user_css.as_deref())
|
|
});
|
|
}
|
|
|
|
/// Generate the full breadpad/breadman CSS string. The base — `@define-color`
|
|
/// palette, fonts, and generic widget styling — comes from the shared
|
|
/// `bread_theme::stylesheet`, so breadpad and breadman look identical to the
|
|
/// rest of the ecosystem. Only breadpad-specific component rules are appended.
|
|
pub fn build_css(palette: &Palette, user_css: Option<&str>) -> String {
|
|
// Shared ecosystem base (define-colors incl. accent, font, buttons, entries,
|
|
// switches, lists, cards, scrollbars). `overlay` here is color7 — consistent
|
|
// with every other bread app (breadpad previously mapped it to color0).
|
|
let mut css = bread_theme::stylesheet(palette);
|
|
|
|
css.push_str(
|
|
r#"
|
|
/* breadpad/breadman-specific components */
|
|
window { border-radius: 8px; }
|
|
|
|
/* breadman/views/settings.rs — matches bos-settings' Row.svelte/
|
|
NumberField.svelte/TextField.svelte exactly (same design tokens: 12/16px
|
|
row padding, transparent-at-rest input border, ch-width inputs) rather
|
|
than libadwaita's own AdwActionRow/AdwSpinRow padding and internal
|
|
spin-button sizing, which run noticeably taller/wider and don't expose a
|
|
way to constrain from the outside. `list.boxed-list` already gets its
|
|
surface fill + radius from the shared stylesheet; this only adds the
|
|
compact row padding and the divider between rows. */
|
|
list.boxed-list row.field-row { padding: 12px 16px; min-height: 0; }
|
|
list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha(@on-surface, 0.08); }
|
|
.field-row-subtitle { opacity: 0.6; font-size: 12px; }
|
|
|
|
.field-input {
|
|
background-color: @bg;
|
|
color: @on-surface;
|
|
border: 1px solid transparent;
|
|
border-radius: 6px;
|
|
padding: 4px 8px;
|
|
}
|
|
.field-input:focus-within { border-color: @accent; outline: none; }
|
|
|
|
.popup-entry {
|
|
background: @bg;
|
|
color: @fg;
|
|
border: 2px solid @blue;
|
|
border-radius: 6px;
|
|
padding: 14px;
|
|
font-size: 14px;
|
|
caret-color: @fg;
|
|
}
|
|
|
|
.popup-entry:focus {
|
|
outline: none;
|
|
border-color: @teal;
|
|
}
|
|
|
|
.type-chip {
|
|
background: @overlay;
|
|
color: @on-overlay;
|
|
border-radius: 999px;
|
|
padding: 4px 12px;
|
|
font-size: 12px;
|
|
margin: 4px;
|
|
}
|
|
|
|
.type-chip.active {
|
|
background: @blue;
|
|
color: @on-accent;
|
|
}
|
|
|
|
/* Per-type tint, matching the note-card-{type} accent-bar colors below -
|
|
the flat cream badge was the same high-contrast fill for every type,
|
|
out-shouting note body text while telling you nothing extra. */
|
|
.type-chip-todo { background: alpha(@green, 0.18); color: @green; }
|
|
.type-chip-reminder { background: alpha(@yellow, 0.18); color: @yellow; }
|
|
.type-chip-idea { background: alpha(@pink, 0.18); color: @pink; }
|
|
.type-chip-question { background: alpha(@teal, 0.18); color: @teal; }
|
|
.type-chip-note { background: alpha(@blue, 0.18); color: @blue; }
|
|
|
|
.confirm-button {
|
|
background: @blue;
|
|
color: @on-accent;
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 8px 16px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.note-card {
|
|
background: shade(@bg, 1.1);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin: 8px;
|
|
border-left: 3px solid @blue;
|
|
}
|
|
|
|
.note-card:hover {
|
|
background: shade(@bg, 1.2);
|
|
}
|
|
|
|
.search-entry {
|
|
background: shade(@bg, 1.1);
|
|
color: @fg;
|
|
border: 1px solid @overlay;
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
.search-entry:focus {
|
|
border-color: @blue;
|
|
outline: none;
|
|
}
|
|
|
|
.sidebar-row {
|
|
padding: 8px 12px;
|
|
font-size: 14px;
|
|
transition: background 100ms ease;
|
|
}
|
|
|
|
.sidebar-row:hover:not(:selected) {
|
|
background: shade(@bg, 1.1);
|
|
}
|
|
|
|
.sidebar-row:selected {
|
|
background: @blue;
|
|
color: @on-accent;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.sidebar-section-label {
|
|
color: alpha(@fg, 0.5);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 12px 12px 8px 12px;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.action-btn {
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 2px 7px;
|
|
min-width: 28px;
|
|
min-height: 28px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
background: shade(@bg, 1.3);
|
|
}
|
|
|
|
.done-btn { color: @green; }
|
|
.done-btn:hover { background: alpha(@green, 0.15); }
|
|
|
|
.edit-btn { color: @blue; }
|
|
.edit-btn:hover { background: alpha(@blue, 0.15); }
|
|
|
|
/* Fixed red, not @red - pywal can hand `red` any hue depending on the
|
|
wallpaper (see bread-theme's button.destructive-action for the same
|
|
reasoning), which would make delete indistinguishable from a normal
|
|
accent action. */
|
|
.danger-btn { color: #e01b24; }
|
|
.danger-btn:hover { background: alpha(#e01b24, 0.15); }
|
|
|
|
.note-card-todo { border-left-color: @green; }
|
|
.note-card-reminder { border-left-color: @yellow; }
|
|
.note-card-idea { border-left-color: @pink; }
|
|
.note-card-question { border-left-color: @teal; }
|
|
.note-card-note { border-left-color: @blue; }
|
|
|
|
.reminder-window {
|
|
background: @bg;
|
|
border: 1px solid @overlay;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.reminder-emoji { font-size: 20px; }
|
|
|
|
.reminder-title {
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
color: alpha(@fg, 0.6);
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.reminder-time {
|
|
font-size: 12px;
|
|
color: alpha(@fg, 0.5);
|
|
}
|
|
|
|
.reminder-body {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: @fg;
|
|
}
|
|
|
|
/* Dismiss and Snooze are both secondary/outline actions and should read at
|
|
equal weight - Dismiss used to sit at 0.6 alpha next to Snooze's full
|
|
opacity, which made the button that closes the reminder look disabled. */
|
|
.reminder-dismiss {
|
|
background: transparent;
|
|
border: 1px solid @overlay;
|
|
border-radius: 8px;
|
|
padding: 8px 16px;
|
|
color: @fg;
|
|
}
|
|
|
|
.reminder-dismiss:hover { background: shade(@bg, 1.1); }
|
|
|
|
.reminder-snooze {
|
|
background: transparent;
|
|
border: 1px solid @overlay;
|
|
border-radius: 8px;
|
|
padding: 8px 16px;
|
|
color: @fg;
|
|
}
|
|
|
|
.reminder-snooze:hover { background: shade(@bg, 1.1); }
|
|
|
|
/* Left-aligned (the button's child label sets xalign itself), full-width
|
|
row with a hairline divider so the list reads as distinct clickable rows
|
|
even before hover - a hover tint alone doesn't show up in a static
|
|
reading of the popover's default state. */
|
|
.snooze-option {
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 10px 12px;
|
|
color: @fg;
|
|
border-bottom: 1px solid alpha(@overlay, 0.15);
|
|
}
|
|
|
|
.snooze-option:hover { background: shade(@bg, 1.2); }
|
|
|
|
.snooze-custom-entry {
|
|
background: @bg;
|
|
color: @fg;
|
|
border: 1px solid @overlay;
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
margin: 4px;
|
|
}
|
|
|
|
.snooze-custom-entry:focus-within { border-color: @blue; outline: none; }
|
|
|
|
/* Matches the reminder alert card's flat-bordered elevation (1px border,
|
|
8px radius, no shadow) instead of GTK's default arrow+drop-shadow popover
|
|
chrome - the two surfaces used to speak two different elevation
|
|
languages. */
|
|
popover.snooze-popover > contents {
|
|
border: 1px solid @overlay;
|
|
box-shadow: none;
|
|
}
|
|
"#);
|
|
|
|
if let Some(extra) = user_css {
|
|
css.push('\n');
|
|
css.push_str(extra);
|
|
}
|
|
|
|
css
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn css_defines_bg_color() {
|
|
let css = build_css(&Palette::default(), None);
|
|
assert!(css.contains("@define-color bg #1e1e2e"), "css missing bg: {}", &css[..300]);
|
|
}
|
|
|
|
#[test]
|
|
fn css_defines_all_named_colors() {
|
|
let css = build_css(&Palette::default(), None);
|
|
for name in &["red", "green", "yellow", "blue", "pink", "teal", "overlay"] {
|
|
assert!(css.contains(&format!("@define-color {name} ")), "missing @define-color {name}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn css_contains_window_rule() {
|
|
let css = build_css(&Palette::default(), None);
|
|
assert!(css.contains("window {"));
|
|
assert!(css.contains("background-color: @bg"));
|
|
}
|
|
|
|
#[test]
|
|
fn css_contains_popup_entry_class() {
|
|
let css = build_css(&Palette::default(), None);
|
|
assert!(css.contains(".popup-entry {"), "css: {}", &css[300..600]);
|
|
}
|
|
|
|
#[test]
|
|
fn css_contains_note_card_class() {
|
|
let css = build_css(&Palette::default(), None);
|
|
assert!(css.contains(".note-card {"));
|
|
}
|
|
|
|
#[test]
|
|
fn css_appends_user_css() {
|
|
let user = ".my-override { color: hotpink; }";
|
|
let css = build_css(&Palette::default(), Some(user));
|
|
assert!(css.contains(".my-override { color: hotpink; }"));
|
|
}
|
|
|
|
#[test]
|
|
fn css_without_user_css_omits_user_rules() {
|
|
let css = build_css(&Palette::default(), None);
|
|
assert!(!css.contains(".my-override"));
|
|
}
|
|
|
|
#[test]
|
|
fn css_reflects_custom_palette_colors() {
|
|
let mut p = Palette::default();
|
|
p.background = "#deadbe".into();
|
|
p.color4 = "#cafe00".into();
|
|
let css = build_css(&p, None);
|
|
assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]);
|
|
assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]);
|
|
}
|
|
}
|