shell themes: add Daylight — bottom-anchored, segmented, light builtin
Fourth compiled-in shell theme (bread-theme/src/shell/builtin.rs). Stresses four axes no existing theme touched: - light surfaces (new tokens.light flag, swaps which of the fixed @bg/ @on-bg pair plays paper-surface vs ink — see Tokens::light's doc comment) - bottom anchoring (bar.window.anchors = ["bottom", ...]) - segmented bar chrome (new bar_border = "segmented" value: the bar window itself draws no fill/border, three slot-group pills draw their own) - blur disabled per-theme ([compositor.*] blur = false everywhere) Also: a new bottom_right surface anchor (manifest.rs + types.rs), since the three existing anchor shapes all assume a top-anchored bar's satellites belong in the top-right corner. tokens.accent2() gives the equaliser its own accent independent of accent_from/accent_to.
This commit is contained in:
parent
80e25f23b3
commit
6d9912af56
6 changed files with 665 additions and 22 deletions
117
bread-theme/assets/shell/daylight/daylight.css
Normal file
117
bread-theme/assets/shell/daylight/daylight.css
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/* CSS template for the daylight builtin (bread-theme/src/shell/builtin.rs).
|
||||
* Same scope and substitution rules as its three siblings: only the
|
||||
* window/workspace/clock chrome the manifest's own concepts model, `{name}`
|
||||
* tokens substituted, `@name` palette references passed through untouched.
|
||||
* Declared-but-not-yet-consumed in production, same as every sibling
|
||||
* template — see `ShellTheme::css`'s doc comment for why (breadbar hand-
|
||||
* rolls its own CSS in `breadbar::theme::load_css` instead of calling this
|
||||
* method). Exercised by this crate's own tests.
|
||||
*
|
||||
* Source: bos-ui-demos/proposed/daylight.html's <style> block.
|
||||
*
|
||||
* Unlike its three siblings, this template hardcodes which of `@bg`/`@on-bg`
|
||||
* plays "paper surface" versus "ink" directly (`@on-bg` as the fill, `@bg`
|
||||
* as the text) rather than branching on `{light}` at substitution time —
|
||||
* `Tokens::substitute` only does `{name}` replacement, not conditionals, and
|
||||
* this template is Daylight's alone, so there is nothing to branch FOR here.
|
||||
* `breadbar::theme::load_css` is the one place that genuinely needs to pick
|
||||
* between the two directions at runtime (it renders all four themes from
|
||||
* one function) — see its own `panel`/`ink` locals and Tokens::light's doc
|
||||
* comment for the general mechanism this template only needs one side of.
|
||||
*/
|
||||
|
||||
window.breadbar {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: @bg;
|
||||
}
|
||||
window.breadbar > box > centerbox { padding: 0 14px; }
|
||||
window.breadbar button { min-height: 0; min-width: 0; }
|
||||
|
||||
/* The three detached pills (axis 3, segmented) — see
|
||||
* breadbar::theme::load_css's `.bar-segment` rule for the identical
|
||||
* production version of this block.
|
||||
*/
|
||||
.bar-segment {
|
||||
background-color: alpha(@on-bg, {bg_alpha});
|
||||
border: 1px solid alpha(@bg, 0.10);
|
||||
border-radius: {radius_bar}px;
|
||||
box-shadow: 0 2px 10px alpha(@bg, 0.13);
|
||||
}
|
||||
|
||||
.workspace-trail {
|
||||
background-image: linear-gradient(90deg, @{accent_from}, @{accent_to});
|
||||
background-color: @{accent_from};
|
||||
border-radius: {radius_sm}px;
|
||||
}
|
||||
.workspace-btn {
|
||||
background: transparent;
|
||||
opacity: 0.36;
|
||||
color: @bg;
|
||||
border-radius: {radius_sm}px;
|
||||
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(@bg, 0.08); }
|
||||
.workspace-btn.occupied { opacity: 0.78; }
|
||||
.workspace-btn.active { background: transparent; color: @on-accent; opacity: 1; }
|
||||
.workspace-btn.active:hover { background: transparent; }
|
||||
|
||||
.clock-plain { padding: 0 4px; }
|
||||
.clock-plain-time {
|
||||
font-size: {font_size_base}px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.date-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.48;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* Warm amber equaliser — the second accent (axis 1's colour-mapping note):
|
||||
* distinct from the teal workspace-trail accent above.
|
||||
*/
|
||||
.media-eq-bar {
|
||||
background-color: @{accent2};
|
||||
}
|
||||
|
||||
/* Light-theme overrides of the notification/OSD surfaces — the demo's own
|
||||
* `.notifs`/`.note`/`.osd` block, which explicitly replaces shared.css's
|
||||
* dark defaults. `@on-bg` (paper) / `@bg` (ink), not `@bg` (glass) / `@on-bg`
|
||||
* (light ink) the way every sibling theme's identical rule reads — this is
|
||||
* the one deliberate inversion this template makes relative to its
|
||||
* siblings, and it is exactly what `{light}` conditions in the shared Rust
|
||||
* implementation (breadbar::theme::load_css). See axis 1 in the task
|
||||
* report for the full inventory this one inversion stands in for.
|
||||
*/
|
||||
window.breadbar-osd {
|
||||
background-color: alpha(@on-bg, {bg_alpha});
|
||||
color: @bg;
|
||||
border-radius: {radius_pill}px;
|
||||
border: 1px solid alpha(@bg, 0.10);
|
||||
box-shadow: 0 8px 22px alpha(@bg, 0.16);
|
||||
}
|
||||
|
||||
window.breadbar-panel {
|
||||
background-color: alpha(@on-bg, {bg_alpha});
|
||||
color: @bg;
|
||||
border-radius: {radius_card}px;
|
||||
border: 1px solid alpha(@bg, 0.10);
|
||||
}
|
||||
|
||||
window.breadbar-dismiss {
|
||||
background-color: alpha(#000000, 0.02);
|
||||
}
|
||||
|
||||
.bread-widget-slot { margin-right: {pad}px; }
|
||||
281
bread-theme/assets/shell/daylight/theme.toml
Normal file
281
bread-theme/assets/shell/daylight/theme.toml
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# The fourth compiled-in builtin (bread-theme/src/shell/builtin.rs), demo
|
||||
# "Daylight" in bos-ui-demos/proposed/daylight.html — a bottom-anchored dock
|
||||
# split into three DETACHED pills (workspaces | clock+media | stats)
|
||||
# floating on an otherwise fully transparent bar, ink-on-paper: near-opaque
|
||||
# white surfaces, soft drop shadows (blur is OFF — see [compositor] below),
|
||||
# deep teal accent, warm amber equaliser. This is the first built-in that is
|
||||
# LIGHT, the first anchored BOTTOM, the first SEGMENTED (three pills instead
|
||||
# of one bar surface), and the first with blur deliberately disabled — see
|
||||
# the task report for what each of those four axes required.
|
||||
#
|
||||
# Values are taken from bos-ui-demos/proposed/daylight.html's <style> block;
|
||||
# where the demo is silent (satellite offsets, bar-adjacent chip sizing)
|
||||
# this file re-derives from the dock's own edge, same method every sibling
|
||||
# theme.toml already documents.
|
||||
#
|
||||
# Font (Outfit is not installed on the dev machine, same gap liquid-motion/
|
||||
# spotlight already have — renders in fallback until ttf-outfit is
|
||||
# installed) and colour (the demo's #2f6d7a teal / #c2683c amber are *flat*
|
||||
# accents — mapped to this palette's `teal` / `yellow` tokens, see [tokens]'s
|
||||
# own note — never a literal hex value, or pywal theming breaks).
|
||||
|
||||
name = "Daylight"
|
||||
id = "daylight"
|
||||
|
||||
[tokens]
|
||||
font_family = "Outfit, sans-serif"
|
||||
font_fallback = "sans-serif"
|
||||
font_size_base = 14
|
||||
# `.bar-segment`'s radius (see `bar_border = "segmented"` below) — the
|
||||
# window itself (`window.breadbar`) draws no radius at all, since it draws
|
||||
# no fill/border either. Demo: `.seg { border-radius: 14px }`.
|
||||
radius_bar = 14
|
||||
# Notification/history card radius. Demo has no explicit `.note` radius of
|
||||
# its own (its override block only touches background/border/shadow/colour
|
||||
# — see shared.css's `.note { border-radius: 12px }` default) — 14px keeps
|
||||
# it a close, deliberate match to this theme's own popover/segment radius
|
||||
# instead of inheriting an unrelated demo's 12px.
|
||||
radius_card = 14
|
||||
# Chip/workspace-pill radius. Demo: `.chip`/`.wp`/`.trail` all draw a 9px
|
||||
# pill (concentric inside the 14px segment with a 7px inset, per the demo's
|
||||
# own comment next to `.chip`).
|
||||
radius_sm = 9
|
||||
radius_pill = 999
|
||||
# Demo: `.pop { padding: 13px }`.
|
||||
pad = 13
|
||||
# `.bar-segment` fill alpha — demo: `.seg { background: rgba(255,255,255,.94) }`,
|
||||
# a near-OPAQUE paper pill, not liquid-motion/glass-workbench's translucent
|
||||
# glass (0.72). Also reused for the notification/OSD/panel surfaces below
|
||||
# (the same `bg_alpha` token every sibling theme already reuses for more
|
||||
# than just the bar).
|
||||
bg_alpha = 0.94
|
||||
# The demo defines both curves distinctly (`--spring`/`--settle`), like
|
||||
# liquid-motion — not glass-workbench/spotlight's single shared curve.
|
||||
spring = "cubic-bezier(0.22, 1.35, 0.36, 1)"
|
||||
spring_settle = "cubic-bezier(0.22, 1.2, 0.36, 1)"
|
||||
# Palette token NAMES, not hex — #2f6d7a is this palette's `teal`. Equal
|
||||
# from/to because the demo's workspace-trail fill is flat
|
||||
# (`.trail { background: var(--accent) }`), not a gradient — same reasoning
|
||||
# as glass-workbench/spotlight's own flat accents. accent_from IS read (the
|
||||
# Trail gradient's start stop, and now — this theme is the reason — its end
|
||||
# stop too, see accent_to's own doc comment in types.rs).
|
||||
accent_from = "teal"
|
||||
accent_to = "teal"
|
||||
# A SECOND accent, independent of accent_from/accent_to: the demo's media
|
||||
# equaliser bars are warm amber (`--accent2: #c2683c`), not the workspace
|
||||
# trail's deep teal — one accent value can no longer describe both, which is
|
||||
# why this key exists at all (see Tokens::accent2's doc comment). Mapped to
|
||||
# `yellow` (pywal ANSI color3): the demo's amber reads warmer/more orange
|
||||
# than a pure hue, and `yellow` is the only remaining named accent slot not
|
||||
# already claimed by `accent`/`teal`/`green`/`pink` across the four built-in
|
||||
# themes that reads plausibly warm regardless of wallpaper. Consumed by
|
||||
# breadbar::theme::load_css's `.media-eq-bar` rule.
|
||||
accent2 = "yellow"
|
||||
# Demo: `.chip { height: 26px }` / `.wp { height: 26px }` on the 40px dock —
|
||||
# the same 26px liquid-motion's own Trail-style bar already uses (see
|
||||
# `breadbar::theme::approved_chip_height`, which is what actually governs
|
||||
# this in practice — see that function's own note on why this token is
|
||||
# otherwise a restated truth, not a live input).
|
||||
chip_height = 26
|
||||
# No single demo number for bar-icon glyph size (the demo's `.gl` box is
|
||||
# 16x16, but that is the icon's *swatch*, not the glyph-drawing size other
|
||||
# themes' icon_px models) — chosen proportionally between liquid-motion's
|
||||
# 24px (44px bar) and glass-workbench's 18px (36px bar) for this theme's
|
||||
# 40px dock.
|
||||
icon_px = 20
|
||||
# THE axis-3 (segmented) knob: `window.breadbar` itself gets no fill/
|
||||
# border/radius at all (drawn fully transparent); the bar's three slot-group
|
||||
# containers each get their own `.bar-segment` pill surface instead. See
|
||||
# Tokens::bar_border's doc comment and breadbar::theme::load_css's
|
||||
# `segmented` local for exactly what this changes.
|
||||
bar_border = "segmented"
|
||||
# THE axis-1 (light) knob — see Tokens::light's doc comment for why this
|
||||
# exists and exactly what it swaps. Every dark-assuming hardcode this flag
|
||||
# fixes is listed in the task report.
|
||||
light = true
|
||||
|
||||
[bar.window]
|
||||
# THE axis-2 (bottom-anchored) knob. Full-bleed left/right (`window.
|
||||
# breadbar`'s own bounds touch both screen edges, like every sibling
|
||||
# theme's top-anchored bar), with `bottom_margin` floating it 12px off the
|
||||
# bottom edge — the demo's own `.dock { left: 0; right: 0; bottom: 12px }`.
|
||||
# The 14px horizontal inset the demo's `.dock` draws via CSS `padding`
|
||||
# (not a window margin) is reproduced the same way: see `centerbox_padding`
|
||||
# in breadbar::theme::load_css, keyed off `bar_border == "segmented"`
|
||||
# exactly like glass-workbench's flush bar already keys its own padding off
|
||||
# `bar_border == "bottom"`.
|
||||
anchors = ["bottom", "left", "right"]
|
||||
width = "fill"
|
||||
height = 40
|
||||
margin = { top = 0, left = 0, right = 0, bottom = 12 }
|
||||
exclusive = "auto"
|
||||
keyboard = "none"
|
||||
layer = "top"
|
||||
|
||||
[bar.slots]
|
||||
# Same widget-alias shape as liquid-motion (plan §11 phase 1's fixed
|
||||
# interleave points), even though the demo itself shows no extra Lua
|
||||
# widget — so a widget requesting one of these placements has a home under
|
||||
# Daylight too, the same reasoning spotlight's own `widget:left_of_stats`
|
||||
# documents for its slimmer slot set.
|
||||
left = ["workspaces", "widget:right_of_workspaces"]
|
||||
centre = ["media", "widget:left_of_clock", "clock", "widget:right_of_clock"]
|
||||
right = ["widget:left_of_stats", "volume", "wifi", "battery", "control"]
|
||||
drawer = []
|
||||
|
||||
[modules.workspaces]
|
||||
style = "trail"
|
||||
show_empty = true
|
||||
|
||||
[modules.clock]
|
||||
# Demo: one time label + one date label (`.clock`/`.date`), not liquid-
|
||||
# motion's per-digit flip — same reasoning as glass-workbench's own "plain"
|
||||
# choice.
|
||||
style = "plain"
|
||||
format = "%H:%M"
|
||||
show_date = true
|
||||
|
||||
[launcher]
|
||||
mode = "overlay"
|
||||
# Demo: `.bx { width: min(430px, 90%) }`.
|
||||
width = 430
|
||||
# NOTE — known gap, see task report: `breadbox` (the process that actually
|
||||
# reads this field) is not one of this task's two repos, and `Launcher` has
|
||||
# no "anchor from bottom" concept at all — `top` is unconditionally a
|
||||
# distance from the TOP of the screen in every existing consumer. The demo's
|
||||
# launcher rises from just above the dock (`.bx { bottom: 62px }`), which
|
||||
# this schema/consumer pairing cannot express without a breadbox-side change
|
||||
# this task cannot make. 55% is a best-effort placement (roughly the lower
|
||||
# half of the screen, not flush against either edge) documented here as a
|
||||
# stopgap, not a real fix.
|
||||
top = "55%"
|
||||
# Demo: `.bx { border-radius: 18px }`.
|
||||
radius = 18
|
||||
# Demo: `.bx .ico { width: 26px; height: 26px }`.
|
||||
icon_px = 26
|
||||
# Demo's rows all fade/slide in together with a small per-index stagger
|
||||
# (`animation-delay: min(i,10)*20ms`) — closest existing `row_anim` value is
|
||||
# glass-workbench's "stagger" (the CSS classes it reuses are already 28ms
|
||||
# apart, not 20ms, but this schema has no numeric stagger-interval knob of
|
||||
# its own to match the demo's exactly).
|
||||
row_anim = "stagger"
|
||||
# Demo: a plain 1px hairline under the search field
|
||||
# (`.bx .q { border-bottom: 1px solid rgba(26,29,34,.09) }`), not liquid-
|
||||
# motion's gradient rule.
|
||||
rule = "hairline"
|
||||
footer = "count_apps"
|
||||
sections = true
|
||||
# Demo supports all four query prefixes (bare/`=`/`>`/`.`), same as
|
||||
# spotlight — its `<script>` reuses the identical shared.js query parser.
|
||||
modes = ["apps", "calc", "cmd", "url"]
|
||||
# `.bx .r { border-radius: 10px; margin: 0 7px; padding: 9px 11px }`
|
||||
row_radius = 10
|
||||
row_inset = 7
|
||||
row_padding_v = 9
|
||||
row_padding_h = 11
|
||||
# `.bx .ico { border-radius: 8px }` (paired with icon_px = 26 above)
|
||||
icon_radius = 8
|
||||
# `.bx .q { padding: 15px 17px; font-size: 15px }`
|
||||
search_font_size = 15
|
||||
search_padding_v = 15
|
||||
search_padding_h = 17
|
||||
# Demo: `.bx { background: var(--paper) }` — fully OPAQUE white, not
|
||||
# liquid-motion/glass-workbench's translucent glass. 0.97, not a bare 1.0,
|
||||
# to read as a deliberate "near-opaque paper" choice rather than a rounding
|
||||
# accident, matching this theme's own `bg_alpha`/pill-surface language.
|
||||
panel_alpha = 0.97
|
||||
# Demo: `.bx .r.sel { background: rgba(47,109,122,.15) }` — teal at 15%.
|
||||
selection_alpha = 0.15
|
||||
|
||||
# Keyed by layer-shell namespace, matching [compositor.*] below.
|
||||
#
|
||||
# `bottom_right`, not `top_right`: THE OTHER axis-2 gap this theme exposed
|
||||
# and fixed. Every sibling theme's bar sits at the TOP, so `top_right`
|
||||
# always put breadbar-notif/breadbar-panel naturally close to the bar; a
|
||||
# bottom-anchored dock has nothing in the original three-shape anchor set
|
||||
# that keeps its satellites near it. `bottom_right` is a new fourth anchor
|
||||
# shape (bread-theme/src/shell/manifest.rs + breadbar/src/surface.rs),
|
||||
# validated the same "typo'd key is a hard error" way as its siblings.
|
||||
# `offset` is `[right, bottom]`, mirroring `top_right`'s `[right, top]`
|
||||
# convention. This bar's own bottom edge sits at margin.bottom(12) +
|
||||
# height(40) = 52px, so 64 (52 + a 12px gap, the same gap-beyond-the-bar-
|
||||
# edge relationship every sibling theme's own top_right offset already
|
||||
# uses) keeps the popup just clear of the dock.
|
||||
[surfaces."breadbar-notif"]
|
||||
anchor = "bottom_right"
|
||||
offset = [16, 64]
|
||||
# 320, not the demo's own shared.css `.notifs { width: 250px }` — every
|
||||
# sibling theme's breadbar-notif width already follows breadbar's real
|
||||
# `set_default_width(320)` live-toast constant instead of the demo markup's
|
||||
# arbitrary number (see liquid-motion/theme.toml's own note on this), and
|
||||
# this theme follows that same real-app-fidelity precedent rather than the
|
||||
# demo's number.
|
||||
width = 320
|
||||
layer = "overlay"
|
||||
|
||||
[surfaces."breadbar-osd"]
|
||||
# Demo: `.osd { bottom: 64px }` — independent of `anchor`, this already
|
||||
# matches the notif/panel offset above (both landed on 64 from the same
|
||||
# "12px clear of the dock's 52px edge" reasoning, coincidentally the same
|
||||
# number as the notif/panel `offset[1]`).
|
||||
anchor = "bottom_centre"
|
||||
offset = 64
|
||||
width = 180
|
||||
layer = "overlay"
|
||||
|
||||
[surfaces."breadbar-panel"]
|
||||
anchor = "bottom_right"
|
||||
offset = [16, 64]
|
||||
width = "auto"
|
||||
layer = "overlay"
|
||||
|
||||
[surfaces."breadbar-dismiss"]
|
||||
# THE THIRD axis-2 consequence: `fill`'s `offset` was single-value
|
||||
# (top-margin-only) before this theme — a bottom-anchored bar needs the
|
||||
# scrim's GAP at the BOTTOM instead, so `breadbar/src/surface.rs::apply`'s
|
||||
# "fill" arm now reads a `[top, bottom]` pair (falling back to `[value, 0]`
|
||||
# for every existing single-value theme, so liquid-motion/glass-workbench/
|
||||
# spotlight are unaffected). `0` here means the scrim covers all the way to
|
||||
# the true top of the screen (nothing to clear up there); `52` is this bar's
|
||||
# own edge (margin.bottom(12) + height(40)), leaving the dock's own
|
||||
# footprint click-through the way every sibling theme's single top offset
|
||||
# already leaves its own bar's footprint alone.
|
||||
anchor = "fill"
|
||||
offset = [0, 52]
|
||||
width = "fill"
|
||||
layer = "overlay"
|
||||
|
||||
# Appearance-only, never placement/workspace/focus (plan §12 Layer B
|
||||
# boundary). THE axis-4 knob: `blur = false` everywhere, and `blur_popups`
|
||||
# dropped too — the demo draws depth with `box-shadow`, not
|
||||
# `backdrop-filter`, and per the task brief this also sidesteps the
|
||||
# shadow-halo trap a blurred `ignore_alpha` surface has (fixed in breadbox
|
||||
# by removing the shadow entirely — see that fix's history) since there is
|
||||
# no blur here for a shadow to get caught inside. No `ignore_alpha`
|
||||
# anywhere below either: that key only means anything to a BLURRED surface,
|
||||
# and setting it on an unblurred one would be exactly the kind of
|
||||
# never-consumed key this project's rules forbid.
|
||||
[compositor."breadbar"]
|
||||
blur = false
|
||||
blur_popups = false
|
||||
animation = "slide bottom"
|
||||
|
||||
[compositor."breadbar-osd"]
|
||||
blur = false
|
||||
animation = "slide bottom"
|
||||
|
||||
[compositor."breadbar-notif"]
|
||||
blur = false
|
||||
animation = "slide right"
|
||||
|
||||
[compositor."breadbar-panel"]
|
||||
blur = false
|
||||
animation = "slide right"
|
||||
|
||||
[compositor."breadbar-dismiss"]
|
||||
no_anim = true
|
||||
|
||||
[compositor."breadbox"]
|
||||
blur = false
|
||||
|
||||
# No `css = "..."` overlay — compiled-in builtin, same as its three siblings.
|
||||
|
|
@ -47,6 +47,18 @@ const SPOTLIGHT_CSS: &str = include_str!(concat!(
|
|||
"/assets/shell/spotlight/spotlight.css"
|
||||
));
|
||||
|
||||
pub const DAYLIGHT_ID: &str = "daylight";
|
||||
|
||||
const DAYLIGHT_TOML: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/assets/shell/daylight/theme.toml"
|
||||
));
|
||||
|
||||
const DAYLIGHT_CSS: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/assets/shell/daylight/daylight.css"
|
||||
));
|
||||
|
||||
/// One compiled-in theme's identity plus its two `include_str!`ed assets.
|
||||
/// `id`/`name` are also duplicated inside `toml`'s own `id =`/`name =`
|
||||
/// fields — kept here too so [`all`]/[`find`] can list/look up a builtin
|
||||
|
|
@ -86,6 +98,12 @@ pub const ALL: &[BuiltinTheme] = &[
|
|||
toml: SPOTLIGHT_TOML,
|
||||
css: SPOTLIGHT_CSS,
|
||||
},
|
||||
BuiltinTheme {
|
||||
id: DAYLIGHT_ID,
|
||||
name: "Daylight",
|
||||
toml: DAYLIGHT_TOML,
|
||||
css: DAYLIGHT_CSS,
|
||||
},
|
||||
];
|
||||
|
||||
/// Looks up a compiled-in theme by id — `None` means "not a builtin",
|
||||
|
|
|
|||
|
|
@ -451,11 +451,11 @@ fn resolve_surfaces(
|
|||
let Some(raw) = raw else { return Ok(out) };
|
||||
for (namespace, s) in raw {
|
||||
let anchor = match s.anchor.as_deref() {
|
||||
Some(a @ ("top_right" | "bottom_centre" | "fill")) => a.to_string(),
|
||||
Some(a @ ("top_right" | "bottom_right" | "bottom_centre" | "fill")) => a.to_string(),
|
||||
Some(other) => bail!(
|
||||
"theme '{theme_id}': surfaces.{namespace}.anchor = \"{other}\" is not \
|
||||
top_right|bottom_centre|fill (the only shapes breadbar's satellite \
|
||||
windows implement — see breadbar/src/surface.rs)"
|
||||
top_right|bottom_right|bottom_centre|fill (the only shapes breadbar's \
|
||||
satellite windows implement — see breadbar/src/surface.rs)"
|
||||
),
|
||||
None => bail!(
|
||||
"theme '{theme_id}': surfaces.{namespace} has no anchor set \
|
||||
|
|
|
|||
|
|
@ -938,6 +938,166 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// ---- daylight builtin (plan §11 phase 7) -------------------------
|
||||
|
||||
#[test]
|
||||
fn daylight_loads_and_appears_in_list_alongside_the_other_three() {
|
||||
let theme = load_named(builtin::DAYLIGHT_ID).expect("daylight builtin should resolve");
|
||||
assert_eq!(theme.id(), "daylight");
|
||||
assert_eq!(theme.name(), "Daylight");
|
||||
|
||||
let summaries = list();
|
||||
assert!(
|
||||
summaries
|
||||
.iter()
|
||||
.any(|s| s.id == "daylight" && s.source == ThemeSource::Builtin),
|
||||
"daylight missing from list(): {summaries:?}"
|
||||
);
|
||||
// All four builtins must be listed side by side.
|
||||
for id in ["liquid-motion", "glass-workbench", "spotlight"] {
|
||||
assert!(
|
||||
summaries
|
||||
.iter()
|
||||
.any(|s| s.id == id && s.source == ThemeSource::Builtin),
|
||||
"{id} missing from list() after adding daylight: {summaries:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daylight_is_bottom_anchored_unlike_every_other_builtin() {
|
||||
let theme = load_named(builtin::DAYLIGHT_ID).expect("daylight should resolve");
|
||||
let w = theme.window();
|
||||
assert_eq!(
|
||||
w.anchors,
|
||||
vec!["bottom", "left", "right"],
|
||||
"daylight must anchor bottom, not top like its three siblings"
|
||||
);
|
||||
assert_eq!(w.height, 40);
|
||||
assert_eq!(
|
||||
w.margin,
|
||||
Margin {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 12,
|
||||
},
|
||||
"the floating gap must be a BOTTOM margin, not a top one"
|
||||
);
|
||||
assert!(matches!(w.width, Width::Fill));
|
||||
assert!(matches!(w.exclusive, Exclusive::Auto));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daylight_compositor_rules_disable_blur_everywhere() {
|
||||
let theme = load_named(builtin::DAYLIGHT_ID).expect("daylight should resolve");
|
||||
let rules = theme.compositor_rules();
|
||||
for ns in [
|
||||
"breadbar",
|
||||
"breadbar-osd",
|
||||
"breadbar-notif",
|
||||
"breadbar-panel",
|
||||
"breadbox",
|
||||
] {
|
||||
assert!(
|
||||
!rules[ns].blur,
|
||||
"compositor.{ns}.blur must be false under daylight — a theme must be able \
|
||||
to DISABLE a compositor rule, not just tune it"
|
||||
);
|
||||
}
|
||||
assert!(!rules["breadbar"].blur_popups);
|
||||
assert_eq!(rules["breadbar"].animation.as_deref(), Some("slide bottom"));
|
||||
// No ignore_alpha anywhere: that key only means something to a
|
||||
// blurred surface, and setting it on an unblurred one would be a
|
||||
// never-consumed value.
|
||||
for ns in ["breadbar", "breadbar-osd", "breadbar-notif", "breadbar-panel"] {
|
||||
assert!(
|
||||
rules[ns].ignore_alpha.is_none(),
|
||||
"compositor.{ns}.ignore_alpha should be unset when blur is off"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daylight_satellites_anchor_bottom_right_not_top_right() {
|
||||
let theme = load_named(builtin::DAYLIGHT_ID).expect("daylight should resolve");
|
||||
let surfaces = theme.surfaces();
|
||||
for ns in ["breadbar-notif", "breadbar-panel"] {
|
||||
assert_eq!(
|
||||
surfaces[ns].anchor, "bottom_right",
|
||||
"{ns} must sit near the bottom-anchored dock, not the top-right corner \
|
||||
every top-anchored sibling theme uses"
|
||||
);
|
||||
}
|
||||
assert_eq!(surfaces["breadbar-osd"].anchor, "bottom_centre");
|
||||
// "fill" (breadbar-dismiss) now carries a [top, bottom] pair: 0 at
|
||||
// the top (nothing to clear there) and 52 at the bottom (the dock's
|
||||
// own edge), the inverse of every top-anchored sibling's single
|
||||
// top-only offset.
|
||||
assert_eq!(surfaces["breadbar-dismiss"].offset, vec![0.0, 52.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daylight_is_light_and_segmented() {
|
||||
let theme = load_named(builtin::DAYLIGHT_ID).expect("daylight should resolve");
|
||||
assert!(theme.tokens().light(), "daylight must set tokens.light = true");
|
||||
assert_eq!(theme.tokens().bar_border(), "segmented");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daylight_accent_is_teal_and_a_palette_token_not_hex_with_a_distinct_amber_accent2() {
|
||||
let theme = load_named(builtin::DAYLIGHT_ID).expect("daylight should resolve");
|
||||
let t = theme.tokens();
|
||||
assert_eq!(t.accent_from(), "teal");
|
||||
assert_eq!(t.accent_to(), "teal");
|
||||
assert_eq!(t.accent2(), "yellow");
|
||||
let css = theme.css(&crate::Palette::default());
|
||||
assert!(
|
||||
css.contains("@teal"),
|
||||
"accent_from/accent_to must resolve to the @teal palette token, not a hex literal:\n{css}"
|
||||
);
|
||||
assert!(
|
||||
css.contains("@yellow"),
|
||||
"accent2 must resolve to the @yellow palette token, not a hex literal:\n{css}"
|
||||
);
|
||||
assert!(
|
||||
!css.contains("#2f6d7a") && !css.contains("#c2683c"),
|
||||
"the demo's teal/amber hex must never leak into the manifest — pywal theming \
|
||||
depends on this staying palette token names:\n{css}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_token_defaults_false_for_every_other_builtin() {
|
||||
// The other three builtins must render unchanged — this flag must
|
||||
// not flip anything unless a theme explicitly opts in.
|
||||
for id in [
|
||||
builtin::LIQUID_MOTION_ID,
|
||||
builtin::GLASS_WORKBENCH_ID,
|
||||
builtin::SPOTLIGHT_ID,
|
||||
] {
|
||||
let theme = load_named(id).unwrap_or_else(|e| panic!("{id} should resolve: {e:#}"));
|
||||
assert!(!theme.tokens().light(), "{id} must default tokens.light to false");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_right_surface_anchor_resolves_like_its_siblings() {
|
||||
let xdg = isolated_xdg();
|
||||
write_theme(
|
||||
&xdg,
|
||||
"bottomright",
|
||||
r#"
|
||||
id = "bottomright"
|
||||
[surfaces."breadbar-notif"]
|
||||
anchor = "bottom_right"
|
||||
offset = [16, 64]
|
||||
"#,
|
||||
);
|
||||
let theme = load_named("bottomright").expect("bottom_right anchor should resolve");
|
||||
assert_eq!(theme.surfaces()["breadbar-notif"].anchor, "bottom_right");
|
||||
}
|
||||
|
||||
// ---- extends merge ------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
|
@ -1147,7 +1307,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn every_known_surface_anchor_shape_resolves() {
|
||||
for anchor in ["top_right", "bottom_centre", "fill"] {
|
||||
for anchor in ["top_right", "bottom_right", "bottom_centre", "fill"] {
|
||||
let xdg = isolated_xdg();
|
||||
write_theme(
|
||||
&xdg,
|
||||
|
|
|
|||
|
|
@ -277,13 +277,23 @@ pub struct Launcher {
|
|||
/// one lookup.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Surface {
|
||||
/// One of `top_right`/`bottom_centre`/`fill` — validated in
|
||||
/// `manifest.rs::resolve_surfaces` against the shapes
|
||||
/// One of `top_right`/`bottom_right`/`bottom_centre`/`fill` — validated
|
||||
/// in `manifest.rs::resolve_surfaces` against the shapes
|
||||
/// `breadbar/src/surface.rs::apply` actually implements, the same
|
||||
/// "typo'd key is a hard error" policy this field's siblings
|
||||
/// (`width`, `layer`) already got. Required, not defaulted: a surface
|
||||
/// entry with no anchor at all is as much a hard `theme.toml` error as
|
||||
/// an unrecognized one.
|
||||
///
|
||||
/// `bottom_right` (daylight, plan §11 phase 7) added alongside the
|
||||
/// original three: every built-in theme before it anchored its bar to
|
||||
/// the TOP, so `breadbar-notif`/`breadbar-panel` popping up from
|
||||
/// `top_right` always sat naturally close to the bar. A bottom-anchored
|
||||
/// bar has no shape in the original three that keeps those satellites
|
||||
/// near it — `top_right` would put them at the opposite corner of the
|
||||
/// screen from the dock they visually belong to. `offset` is
|
||||
/// `[right, bottom]` for this anchor, the same two-element convention
|
||||
/// `top_right`'s `[right, top]` already uses.
|
||||
pub anchor: String,
|
||||
pub offset: Vec<f64>,
|
||||
pub width: SurfaceWidth,
|
||||
|
|
@ -404,6 +414,19 @@ impl Tokens {
|
|||
}
|
||||
}
|
||||
|
||||
/// Same fallback shape as [`Self::str_or`]/[`Self::int_or`]/
|
||||
/// [`Self::float_or`] for a `TokenValue::Bool`. First consumer: `light`
|
||||
/// below (theme 05/daylight, plan §11 phase 7) — every existing token is
|
||||
/// a string/number, this is the first bool-shaped one, hence the new
|
||||
/// helper rather than reusing one of the three above.
|
||||
fn bool_or(&self, key: &str, default: bool) -> bool {
|
||||
match self.map.get(key) {
|
||||
Some(TokenValue::Bool(b)) => *b,
|
||||
Some(TokenValue::Str(s)) => s.parse().unwrap_or(default),
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumed by `breadbox::main::build_css` for the launcher panel's
|
||||
/// `font-family` (the `entry.search`/`row` rule) as of the
|
||||
/// liquid-motion/glass-workbench redesign — combined with
|
||||
|
|
@ -465,24 +488,62 @@ impl Tokens {
|
|||
pub fn accent_from(&self) -> String {
|
||||
self.str_or("accent_from", "accent")
|
||||
}
|
||||
/// Declared-but-not-yet-consumed in production, for a subtler reason
|
||||
/// than most of this file's other "never read" fields: this crate's own
|
||||
/// CSS templates DO read `{accent_to}` (the `WorkspaceStyle::Trail`
|
||||
/// gradient stop in `assets/shell/liquid-motion/liquid-motion.css`,
|
||||
/// exercised by `super::ShellTheme::css` and this module's own tests),
|
||||
/// but breadbar
|
||||
/// never calls that method — its hand-rolled Trail CSS
|
||||
/// (`breadbar::theme::load_css`) hardcodes the literal gradient
|
||||
/// `linear-gradient(90deg, @accent, @teal)` instead of substituting
|
||||
/// `accent_from`/`accent_to`, so a theme that set a *different*
|
||||
/// `accent_to` than liquid-motion's "teal" would see no change in the
|
||||
/// running bar. Every built-in theme still sets this key so the
|
||||
/// manifest states its actual design intent; see each `theme.toml`'s
|
||||
/// own note next to it.
|
||||
/// Was declared-but-not-yet-consumed in production through theme 04
|
||||
/// (spotlight): breadbar's hand-rolled Trail CSS hardcoded the literal
|
||||
/// gradient `linear-gradient(90deg, @accent, @teal)` instead of
|
||||
/// substituting `accent_from`/`accent_to`, silently correct only because
|
||||
/// liquid-motion's own `accent_from`/`accent_to` happened to be
|
||||
/// `"accent"`/`"teal"` — the exact two names the hardcode already spelled
|
||||
/// out. Theme 05 (daylight, plan §11 phase 7) is the first Trail-style
|
||||
/// theme to set a *different* pair (`accent_from = accent_to = "teal"`,
|
||||
/// a flat fill, not a gradient), which is what finally forced
|
||||
/// `breadbar::theme::load_css`'s Trail branch to read this method for
|
||||
/// real instead of the two literal names — see that function's own note
|
||||
/// next to `.workspace-trail`.
|
||||
pub fn accent_to(&self) -> String {
|
||||
let from = self.accent_from();
|
||||
self.str_or("accent_to", &from)
|
||||
}
|
||||
/// A second, independent accent for chrome that shouldn't track the
|
||||
/// primary `accent_from`/`accent_to` pair — daylight (plan §11 phase 7)
|
||||
/// is the first theme that needs one: its media-widget equaliser bars
|
||||
/// are warm amber while the workspace trail/active-fill accent is deep
|
||||
/// teal, so one `accent_from` value can no longer describe both.
|
||||
/// Defaults to `accent_from` itself, which reproduces every earlier
|
||||
/// theme's actual rendering byte-for-byte (liquid-motion/glass-
|
||||
/// workbench/spotlight all paint their equaliser with the same accent
|
||||
/// as everything else, and none of them sets this key). Consumed by
|
||||
/// `breadbar::theme::load_css`'s `.media-eq-bar` rule.
|
||||
pub fn accent2(&self) -> String {
|
||||
let from = self.accent_from();
|
||||
self.str_or("accent2", &from)
|
||||
}
|
||||
/// Whether this theme's surfaces are painted ink-on-paper (near-opaque
|
||||
/// LIGHT fills with dark ink) rather than the glass-on-dark look every
|
||||
/// earlier theme assumed — daylight (plan §11 phase 7) is the first
|
||||
/// theme to set this `true`. Defaults `false`, reproducing every
|
||||
/// existing theme's rendering unchanged.
|
||||
///
|
||||
/// Exists because the palette's `bg`/`surface`/`overlay`/`fg` slots are
|
||||
/// NEVER pywal-derived (`bread_theme::palette`'s `FIXED_*` constants —
|
||||
/// deliberately, so a bright wallpaper can't turn the whole UI an
|
||||
/// unreadable light colour by accident) — only the six accent slots
|
||||
/// vary. That means there is no palette token whose value is a light
|
||||
/// "paper" surface for a theme to reference by name; `@bg` is always
|
||||
/// dark, `@on-bg` is always its computed-legible near-white ink.
|
||||
/// `breadbar::theme::load_css` reads this flag to swap which of those
|
||||
/// two *fixed, anti-correlated* tokens plays "surface fill" versus
|
||||
/// "ink" for every translucent card/panel/hover-wash in the stylesheet
|
||||
/// (`@on-bg` as the near-white fill, `@bg` as the near-black ink,
|
||||
/// otherwise the reverse) — see that function's own `panel`/`ink`
|
||||
/// locals. This works only because `@bg`/`@on-bg` are pinned opposite
|
||||
/// constants by construction; it is not a general "pick any light
|
||||
/// surface colour" mechanism, and this doc comment is the canonical
|
||||
/// place that fact is written down (see the task report for the full
|
||||
/// reasoning: the palette schema has no dedicated light-surface token).
|
||||
pub fn light(&self) -> bool {
|
||||
self.bool_or("light", false)
|
||||
}
|
||||
/// Workspace-pill / chip height. Not in the plan §4 schema, but
|
||||
/// `breadbar::CHIP_HEIGHT` (32) today. See [`Tokens`] doc.
|
||||
pub fn chip_height(&self) -> i64 {
|
||||
|
|
@ -498,8 +559,14 @@ impl Tokens {
|
|||
/// edge-to-edge bar, plan §1) draws only the bottom hairline the demo's
|
||||
/// `.bar { border-bottom: 1px solid #ffffff12 }` calls for — a floating
|
||||
/// island's full border would otherwise render as a stray top/side line
|
||||
/// flush against the screen edge. See [`Tokens`] doc; consumed by
|
||||
/// `breadbar::theme::load_css`, not by [`crate::shell::ShellTheme::css`].
|
||||
/// flush against the screen edge. `"segmented"` (daylight, plan §11
|
||||
/// phase 7) is a third value: `window.breadbar` itself gets NO fill,
|
||||
/// border, or radius at all (fully transparent), and the bar's three
|
||||
/// slot-group containers each carry their own pill surface instead (see
|
||||
/// `breadbar::theme::load_css`'s `segmented`/`.bar-segment` locals) —
|
||||
/// this is what lets one bar surface look like three detached floating
|
||||
/// pills rather than one continuous strip. See [`Tokens`] doc; consumed
|
||||
/// by `breadbar::theme::load_css`, not by [`crate::shell::ShellTheme::css`].
|
||||
pub fn bar_border(&self) -> String {
|
||||
self.str_or("bar_border", "full")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue