shell theme: add glass-workbench as a second compiled-in theme
Phase 5 of the shell theme system (THEME_SYSTEM_PLAN.md §11): a second
builtin, demo 02's flush edge-to-edge bar with pill workspaces, a plain
date+time clock, and cpu/ram chips instead of the media widget.
- bread-theme/assets/shell/glass-workbench/: theme.toml + CSS template,
faithful to bos-ui-demos/02-glass-workbench.html. Accent maps to the
`green` palette token (flat, not a gradient) rather than a hex literal,
so pywal theming still works.
- builtin.rs: generalized from a single hardcoded liquid-motion constant
pair to a small BuiltinTheme registry (builtin::ALL / builtin::find),
so mod.rs's discovery/list()/resolve_builtin no longer special-case one
id. liquid-motion stays the pinned fallback in resolve_builtin().
- manifest.rs: KNOWN_MODULES gains "cpu"/"ram".
- types.rs: new Tokens::bar_border() ("full" default vs "bottom") so a
flush bar can ask for a single hairline instead of an island's full
border.
- Tests: builtin loads, appears in list() alongside liquid-motion, and its
window spec is the flush/edge shape (36px, zero margin, radius 0).
cargo test -p bread-theme --lib: 63 passing (59 prior + 4 new).
This commit is contained in:
parent
53a6c59f2d
commit
96fa79c1d4
6 changed files with 467 additions and 40 deletions
86
bread-theme/assets/shell/glass-workbench/glass-workbench.css
Normal file
86
bread-theme/assets/shell/glass-workbench/glass-workbench.css
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/* CSS template for the glass-workbench builtin (bread-theme/src/shell/
|
||||||
|
* builtin.rs). Same scope and substitution rules as liquid-motion.css: only
|
||||||
|
* the window/workspace/clock chrome the manifest's own concepts model, `{name}`
|
||||||
|
* tokens substituted, `@name` palette references passed through untouched.
|
||||||
|
*
|
||||||
|
* Source: bos-ui-demos/02-glass-workbench.html's <style> block. No
|
||||||
|
* per-digit flip (this theme has no `.clock-digit` markup at all — plain
|
||||||
|
* style is a date label + one time label) and no gradient trail (pill style
|
||||||
|
* never makes `.workspace-trail` visible — see breadbar's
|
||||||
|
* `WorkspaceTrail::place`/`stretch`, which this theme's slot config simply
|
||||||
|
* never calls).
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.breadbar {
|
||||||
|
background-color: alpha(@bg, {bg_alpha});
|
||||||
|
color: @on-bg;
|
||||||
|
border-radius: {radius_bar}px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid alpha(@on-bg, 0.07);
|
||||||
|
}
|
||||||
|
window.breadbar > centerbox { padding: 0 {pad}px; }
|
||||||
|
window.breadbar button { min-height: 0; min-width: 0; }
|
||||||
|
|
||||||
|
/* Pill workspaces: solid accent fill when active, dimmed when empty — no
|
||||||
|
* trail overlay is ever shown (place()/stretch() are never called for this
|
||||||
|
* style), so `.workspace-trail` itself needs no rule here.
|
||||||
|
*/
|
||||||
|
.workspace-btn {
|
||||||
|
background: transparent;
|
||||||
|
opacity: 1;
|
||||||
|
color: alpha(@on-bg, 0.4);
|
||||||
|
border-radius: {radius_sm}px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
min-width: 22px;
|
||||||
|
min-height: {chip_height}px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 0.22s {spring_settle}, color 0.22s {spring_settle},
|
||||||
|
opacity 0.22s {spring_settle};
|
||||||
|
}
|
||||||
|
.workspace-btn:hover { background: alpha(@on-bg, 0.08); }
|
||||||
|
.workspace-btn.occupied { color: alpha(@on-bg, 0.8); }
|
||||||
|
.workspace-btn:not(.occupied):not(.active) { opacity: 0.35; }
|
||||||
|
.workspace-btn.active {
|
||||||
|
background: @{accent_from};
|
||||||
|
color: @on-accent;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.workspace-btn.active:hover { background: @{accent_from}; }
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.breadbar-osd {
|
||||||
|
background-color: alpha(@bg, 0.72);
|
||||||
|
color: @on-bg;
|
||||||
|
border-radius: {radius_pill}px;
|
||||||
|
border: 1px solid alpha(@on-bg, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.breadbar-panel {
|
||||||
|
background-color: alpha(@bg, 0.86);
|
||||||
|
color: @on-bg;
|
||||||
|
border-radius: {radius_card}px;
|
||||||
|
border: 1px solid alpha(@on-bg, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.breadbar-dismiss {
|
||||||
|
background-color: alpha(#000000, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bread-widget-slot { margin-right: {pad}px; }
|
||||||
169
bread-theme/assets/shell/glass-workbench/theme.toml
Normal file
169
bread-theme/assets/shell/glass-workbench/theme.toml
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
# The second compiled-in builtin (bread-theme/src/shell/builtin.rs), demo 02
|
||||||
|
# in THEME_SYSTEM_PLAN.md — "Glass Workbench": a flush edge-to-edge bar with
|
||||||
|
# plain pill workspaces, a plain date+time clock, and cpu/ram chips instead
|
||||||
|
# of the media widget liquid-motion carries. Values are taken from
|
||||||
|
# bos-ui-demos/02-glass-workbench.html's <style> block; where the demo is
|
||||||
|
# silent on a number (bar-adjacent chip sizing, satellite offsets) this file
|
||||||
|
# scales liquid-motion's own numbers down by the same 36/44 bar-height ratio
|
||||||
|
# rather than inventing something unrelated to either source.
|
||||||
|
#
|
||||||
|
# Unlike liquid-motion, this builtin has no "as the code exists today" tether
|
||||||
|
# — it is new, so demo fidelity is the only source of truth. See Phase 5's
|
||||||
|
# task notes for the two exceptions: font (IBM Plex Sans is not installed on
|
||||||
|
# the dev machine, so this renders in fallback until the user installs
|
||||||
|
# ttf-ibm-plex) and colour (the demo's #7a9a88 sage is a *flat* accent, so it
|
||||||
|
# maps to the palette's `green` token for both accent_from and accent_to —
|
||||||
|
# never a literal hex value, or pywal theming breaks).
|
||||||
|
|
||||||
|
name = "Glass Workbench"
|
||||||
|
id = "glass-workbench"
|
||||||
|
|
||||||
|
[tokens]
|
||||||
|
font_family = "IBM Plex Sans"
|
||||||
|
font_fallback = "Inter, Noto Sans, sans-serif"
|
||||||
|
font_size_base = 13
|
||||||
|
radius_bar = 0
|
||||||
|
radius_card = 10
|
||||||
|
radius_sm = 6
|
||||||
|
radius_pill = 999
|
||||||
|
pad = 12
|
||||||
|
bg_alpha = 0.72
|
||||||
|
# The demo defines exactly one easing curve (`--spring: cubic-bezier(.22,
|
||||||
|
# 1.2, .36, 1)`), not liquid-motion's overshoot/settle pair — there's no
|
||||||
|
# digit-flip or trail-stretch here to want a bounce for. Both `spring` and
|
||||||
|
# `spring_settle` point at the demo's one curve rather than inventing a
|
||||||
|
# second value the demo never specifies.
|
||||||
|
spring = "cubic-bezier(0.22, 1.2, 0.36, 1)"
|
||||||
|
spring_settle = "cubic-bezier(0.22, 1.2, 0.36, 1)"
|
||||||
|
# Palette token NAMES, not hex — #7a9a88 is this palette's `green`. Equal
|
||||||
|
# from/to because the demo's accent is flat, not a gradient.
|
||||||
|
accent_from = "green"
|
||||||
|
accent_to = "green"
|
||||||
|
# Demo: `.ws button { height: 20px }`. Liquid-motion's chip_height (32) is
|
||||||
|
# tied to its 44px island; 20px is the demo's own number for this bar's
|
||||||
|
# workspace pills, and is reused for the bar's other small chips (cpu/ram/
|
||||||
|
# wifi/battery/control) so they all sit at the same height on the thinner
|
||||||
|
# 36px flush bar.
|
||||||
|
chip_height = 20
|
||||||
|
# No demo number for bar-icon size; scaled from liquid-motion's 24px by the
|
||||||
|
# same 36/44 bar-height ratio liquid-motion itself uses for its 44px bar.
|
||||||
|
icon_px = 18
|
||||||
|
# Flush edge-to-edge bar: a full border would draw a stray line along the
|
||||||
|
# top/side screen edges a floating island doesn't have to worry about. See
|
||||||
|
# Tokens::bar_border's doc comment.
|
||||||
|
bar_border = "bottom"
|
||||||
|
|
||||||
|
[bar.window]
|
||||||
|
anchors = ["top", "left", "right"]
|
||||||
|
width = "fill"
|
||||||
|
height = 36
|
||||||
|
margin = { top = 0, left = 0, right = 0 }
|
||||||
|
exclusive = "auto"
|
||||||
|
keyboard = "none"
|
||||||
|
layer = "top"
|
||||||
|
|
||||||
|
[bar.slots]
|
||||||
|
# No media module — demo 02 has no media widget at all (plan §1 table).
|
||||||
|
# breadbar must tolerate the omission: `media_widget` is still built and
|
||||||
|
# registered under the "media" module name (main.rs), it just never appears
|
||||||
|
# in any of this theme's slot lists, so it's never appended anywhere.
|
||||||
|
left = ["workspaces"]
|
||||||
|
centre = ["clock"]
|
||||||
|
right = ["cpu", "ram", "wifi", "battery", "control"]
|
||||||
|
drawer = []
|
||||||
|
|
||||||
|
[modules.workspaces]
|
||||||
|
style = "pill"
|
||||||
|
show_empty = true
|
||||||
|
|
||||||
|
[modules.clock]
|
||||||
|
style = "plain"
|
||||||
|
format = "%H:%M"
|
||||||
|
show_date = true
|
||||||
|
|
||||||
|
[launcher]
|
||||||
|
mode = "overlay"
|
||||||
|
width = 560
|
||||||
|
top = "64px"
|
||||||
|
radius = 12
|
||||||
|
icon_px = 32
|
||||||
|
row_anim = "stagger"
|
||||||
|
rule = "hairline"
|
||||||
|
footer = "count_results"
|
||||||
|
# Declared-but-not-consumed, exactly like liquid-motion's own launcher block:
|
||||||
|
# breadbox's Phase 4b-i wiring only reads mode/width/top/radius/icon_px.
|
||||||
|
# row_anim/rule/footer are this theme's demo-derived aspirations breadbox
|
||||||
|
# does not implement yet; sections/modes describe today's actual (trivial,
|
||||||
|
# apps-only) behaviour but are equally unread by any code path.
|
||||||
|
sections = false
|
||||||
|
modes = ["apps"]
|
||||||
|
|
||||||
|
# Same four satellite namespaces as liquid-motion, keyed identically so the
|
||||||
|
# two manifests validate against the same [compositor.*] keyspace. Offsets
|
||||||
|
# are liquid-motion's own numbers re-derived from this bar's actual edge
|
||||||
|
# instead of the island's: liquid-motion's bar bottom edge sits at
|
||||||
|
# margin.top(12) + height(44) = 56px, and its breadbar-notif/-panel top
|
||||||
|
# offset (64) is that plus an 8px gap, while breadbar-dismiss (56) sits
|
||||||
|
# flush against it with no gap. This bar's edge is margin.top(0) +
|
||||||
|
# height(36) = 36px, so the same two relationships give 44 (36 + 8) and 36.
|
||||||
|
[surfaces."breadbar-notif"]
|
||||||
|
anchor = "top_right"
|
||||||
|
offset = [16, 44]
|
||||||
|
width = 320
|
||||||
|
layer = "overlay"
|
||||||
|
|
||||||
|
[surfaces."breadbar-osd"]
|
||||||
|
# Independent of bar height (an OSD pill near the bottom of the screen) —
|
||||||
|
# unchanged from liquid-motion.
|
||||||
|
anchor = "bottom_centre"
|
||||||
|
offset = 80
|
||||||
|
width = 180
|
||||||
|
layer = "overlay"
|
||||||
|
|
||||||
|
[surfaces."breadbar-panel"]
|
||||||
|
anchor = "top_right"
|
||||||
|
offset = [16, 44]
|
||||||
|
width = "auto"
|
||||||
|
layer = "overlay"
|
||||||
|
|
||||||
|
[surfaces."breadbar-dismiss"]
|
||||||
|
anchor = "fill"
|
||||||
|
offset = 36
|
||||||
|
width = "fill"
|
||||||
|
layer = "overlay"
|
||||||
|
|
||||||
|
# Appearance-only, never placement/workspace/focus (plan §12 Layer B
|
||||||
|
# boundary) — identical to liquid-motion's rules. Blur *strength* is global
|
||||||
|
# (plan §9 known limit), so this theme cannot independently match the demo's
|
||||||
|
# per-surface blur radii (20px bar / 16px launcher / 22px popovers / 6px
|
||||||
|
# window borders); only on/off, ignore_alpha and slide direction are
|
||||||
|
# per-namespace knobs this system actually has.
|
||||||
|
[compositor."breadbar"]
|
||||||
|
blur = true
|
||||||
|
ignore_alpha = 0.2
|
||||||
|
blur_popups = true
|
||||||
|
animation = "slide top"
|
||||||
|
|
||||||
|
[compositor."breadbar-osd"]
|
||||||
|
blur = true
|
||||||
|
ignore_alpha = 0.2
|
||||||
|
animation = "slide bottom"
|
||||||
|
|
||||||
|
[compositor."breadbar-notif"]
|
||||||
|
blur = true
|
||||||
|
ignore_alpha = 0.2
|
||||||
|
animation = "slide right"
|
||||||
|
|
||||||
|
[compositor."breadbar-panel"]
|
||||||
|
blur = true
|
||||||
|
ignore_alpha = 0.2
|
||||||
|
animation = "slide right"
|
||||||
|
|
||||||
|
[compositor."breadbar-dismiss"]
|
||||||
|
no_anim = true
|
||||||
|
|
||||||
|
[compositor."breadbox"]
|
||||||
|
blur = true
|
||||||
|
ignore_alpha = 0.2
|
||||||
|
|
||||||
|
# No `css = "..."` overlay — compiled-in builtin, same as liquid-motion.
|
||||||
|
|
@ -1,23 +1,77 @@
|
||||||
//! The one compiled-in theme (plan §11 phase 1: "**One** built-in manifest
|
//! The compiled-in themes (plan §11 phase 1/5: "**One** built-in manifest
|
||||||
//! (`liquid-motion`) describing the bar as it exists today"). Both files are
|
//! (`liquid-motion`) describing the bar as it exists today", extended in
|
||||||
//! plain data, not Rust — `theme.toml` is the manifest text a user override
|
//! Phase 5 with `glass-workbench`, demo 02). Every file here is plain data,
|
||||||
//! would otherwise supply, and `liquid-motion.css` is the CSS template
|
//! not Rust — each `theme.toml` is the manifest text a user override would
|
||||||
//! `ShellTheme::css` substitutes tokens into (see that method's doc comment
|
//! otherwise supply, and each `<id>.css` is the CSS template `ShellTheme::css`
|
||||||
//! for why this template is a representative subset of `breadbar::theme::
|
//! substitutes tokens into (see that method's doc comment for why this
|
||||||
//! load_css`'s full stylesheet rather than a byte-for-byte copy of it).
|
//! template is a representative subset of `breadbar::theme::load_css`'s
|
||||||
|
//! full stylesheet rather than a byte-for-byte copy of it).
|
||||||
//!
|
//!
|
||||||
//! Both are read with `include_str!` so a broken build can't ship without
|
//! All are read with `include_str!` so a broken build can't ship without
|
||||||
//! them, and so [`super::builtin`] never touches the filesystem — it must
|
//! them, and so [`super`] never touches the filesystem for a builtin — it
|
||||||
//! work identically whether or not `$XDG_CONFIG_HOME` exists at all.
|
//! must work identically whether or not `$XDG_CONFIG_HOME` exists at all.
|
||||||
|
|
||||||
pub const LIQUID_MOTION_ID: &str = "liquid-motion";
|
pub const LIQUID_MOTION_ID: &str = "liquid-motion";
|
||||||
|
|
||||||
pub const LIQUID_MOTION_TOML: &str = include_str!(concat!(
|
const LIQUID_MOTION_TOML: &str = include_str!(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/assets/shell/liquid-motion/theme.toml"
|
"/assets/shell/liquid-motion/theme.toml"
|
||||||
));
|
));
|
||||||
|
|
||||||
pub const LIQUID_MOTION_CSS: &str = include_str!(concat!(
|
const LIQUID_MOTION_CSS: &str = include_str!(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/assets/shell/liquid-motion/liquid-motion.css"
|
"/assets/shell/liquid-motion/liquid-motion.css"
|
||||||
));
|
));
|
||||||
|
|
||||||
|
pub const GLASS_WORKBENCH_ID: &str = "glass-workbench";
|
||||||
|
|
||||||
|
const GLASS_WORKBENCH_TOML: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/assets/shell/glass-workbench/theme.toml"
|
||||||
|
));
|
||||||
|
|
||||||
|
const GLASS_WORKBENCH_CSS: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/assets/shell/glass-workbench/glass-workbench.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
|
||||||
|
/// without parsing TOML first (`super::list`'s builtin fallback entry, and
|
||||||
|
/// `super::find_source`'s existence check, both run before any manifest
|
||||||
|
/// parsing happens).
|
||||||
|
pub struct BuiltinTheme {
|
||||||
|
pub id: &'static str,
|
||||||
|
pub name: &'static str,
|
||||||
|
pub toml: &'static str,
|
||||||
|
pub css: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every compiled-in theme, in the order [`super::list`] should present
|
||||||
|
/// them. Adding a third builtin is one entry here plus its two asset files
|
||||||
|
/// — nothing else in `mod.rs` names a specific builtin id except the always-
|
||||||
|
/// -safe fallback ([`LIQUID_MOTION_ID`], deliberately still hardcoded at
|
||||||
|
/// its one call site in `super::resolve_builtin` — see that function's doc
|
||||||
|
/// comment for why that one reference must NOT become "whichever builtin is
|
||||||
|
/// listed first").
|
||||||
|
pub const ALL: &[BuiltinTheme] = &[
|
||||||
|
BuiltinTheme {
|
||||||
|
id: LIQUID_MOTION_ID,
|
||||||
|
name: "Liquid Motion",
|
||||||
|
toml: LIQUID_MOTION_TOML,
|
||||||
|
css: LIQUID_MOTION_CSS,
|
||||||
|
},
|
||||||
|
BuiltinTheme {
|
||||||
|
id: GLASS_WORKBENCH_ID,
|
||||||
|
name: "Glass Workbench",
|
||||||
|
toml: GLASS_WORKBENCH_TOML,
|
||||||
|
css: GLASS_WORKBENCH_CSS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Looks up a compiled-in theme by id — `None` means "not a builtin",
|
||||||
|
/// exactly like a miss in the user/system theme directories.
|
||||||
|
pub fn find(id: &str) -> Option<&'static BuiltinTheme> {
|
||||||
|
ALL.iter().find(|t| t.id == id)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ const KNOWN_MODULES: &[&str] = &[
|
||||||
"control",
|
"control",
|
||||||
"launcher_entry",
|
"launcher_entry",
|
||||||
"launcher_results",
|
"launcher_results",
|
||||||
|
// `02-glass-workbench` (plan §11 phase 5): plain right-side stat chips,
|
||||||
|
// reusing the same `AppInput::StatsUpdate` data the control panel's
|
||||||
|
// sys-grid already receives — see breadbar's `bar::slots` module docs.
|
||||||
|
"cpu",
|
||||||
|
"ram",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub(super) fn validate_module_name(theme_id: &str, slot: &str, module: &str) -> anyhow::Result<()> {
|
pub(super) fn validate_module_name(theme_id: &str, slot: &str, module: &str) -> anyhow::Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,10 @@ fn system_theme_path(id: &str) -> PathBuf {
|
||||||
enum Source {
|
enum Source {
|
||||||
User(PathBuf),
|
User(PathBuf),
|
||||||
System(PathBuf),
|
System(PathBuf),
|
||||||
Builtin,
|
/// Carries the id rather than being a bare unit variant now that there's
|
||||||
|
/// more than one compiled-in theme — [`read_source`]/[`css_template_for`]
|
||||||
|
/// need to know *which* builtin's assets to hand back.
|
||||||
|
Builtin(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_source(id: &str) -> Option<Source> {
|
fn find_source(id: &str) -> Option<Source> {
|
||||||
|
|
@ -187,8 +190,8 @@ fn find_source(id: &str) -> Option<Source> {
|
||||||
if system.is_file() {
|
if system.is_file() {
|
||||||
return Some(Source::System(system));
|
return Some(Source::System(system));
|
||||||
}
|
}
|
||||||
if id == builtin::LIQUID_MOTION_ID {
|
if builtin::find(id).is_some() {
|
||||||
return Some(Source::Builtin);
|
return Some(Source::Builtin(id.to_string()));
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -202,16 +205,19 @@ fn read_source(src: &Source) -> anyhow::Result<(String, Option<PathBuf>)> {
|
||||||
std::fs::read_to_string(p).with_context(|| format!("reading {}", p.display()))?;
|
std::fs::read_to_string(p).with_context(|| format!("reading {}", p.display()))?;
|
||||||
Ok((text, p.parent().map(|d| d.to_path_buf())))
|
Ok((text, p.parent().map(|d| d.to_path_buf())))
|
||||||
}
|
}
|
||||||
Source::Builtin => Ok((builtin::LIQUID_MOTION_TOML.to_string(), None)),
|
Source::Builtin(id) => {
|
||||||
|
let toml = builtin::find(id)
|
||||||
|
.map(|t| t.toml.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok((toml, None))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn css_template_for(id: &str) -> String {
|
fn css_template_for(id: &str) -> String {
|
||||||
if id == builtin::LIQUID_MOTION_ID {
|
builtin::find(id)
|
||||||
builtin::LIQUID_MOTION_CSS.to_string()
|
.map(|t| t.css.to_string())
|
||||||
} else {
|
.unwrap_or_default()
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The fallible primitive: look up `id` through discovery, apply one level
|
/// The fallible primitive: look up `id` through discovery, apply one level
|
||||||
|
|
@ -278,25 +284,27 @@ fn resolve_theme(id: &str, extends_depth: u8) -> anyhow::Result<ShellTheme> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bypasses discovery entirely and resolves straight from the compiled-in
|
/// Bypasses discovery entirely and resolves straight from the compiled-in
|
||||||
/// `LIQUID_MOTION_TOML`/`LIQUID_MOTION_CSS` constants — used as [`load`]'s
|
/// `liquid-motion` assets — used as [`load`]'s fallback specifically
|
||||||
/// fallback specifically *because* it cannot be affected by a broken user
|
/// *because* it cannot be affected by a broken user override file at the
|
||||||
/// override file at the same id (unlike calling `load_named("liquid-motion")`
|
/// same id (unlike calling `load_named("liquid-motion")` again, which would
|
||||||
/// again, which would hit that same broken file first via discovery and
|
/// hit that same broken file first via discovery and fail identically).
|
||||||
/// fail identically).
|
/// Deliberately always `liquid-motion`, not "whichever theme was active" or
|
||||||
|
/// "the first entry in `builtin::ALL`" — this is the one theme every other
|
||||||
|
/// fallback path in this module bottoms out at, so its identity has to stay
|
||||||
|
/// pinned regardless of how many more builtins `glass-workbench` grows
|
||||||
|
/// siblings.
|
||||||
fn resolve_builtin() -> ShellTheme {
|
fn resolve_builtin() -> ShellTheme {
|
||||||
let value: toml::Value = toml::from_str(builtin::LIQUID_MOTION_TOML)
|
let asset = builtin::find(builtin::LIQUID_MOTION_ID)
|
||||||
.expect("compiled-in builtin theme.toml must parse");
|
.expect("liquid-motion must always be present in builtin::ALL");
|
||||||
|
let value: toml::Value =
|
||||||
|
toml::from_str(asset.toml).expect("compiled-in builtin theme.toml must parse");
|
||||||
let raw: RawManifest = value
|
let raw: RawManifest = value
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("compiled-in builtin theme.toml must satisfy the manifest schema");
|
.expect("compiled-in builtin theme.toml must satisfy the manifest schema");
|
||||||
manifest::validate_slots(&raw, builtin::LIQUID_MOTION_ID)
|
manifest::validate_slots(&raw, builtin::LIQUID_MOTION_ID)
|
||||||
.expect("compiled-in builtin theme.toml must use only known module names");
|
.expect("compiled-in builtin theme.toml must use only known module names");
|
||||||
raw.resolve(
|
raw.resolve(builtin::LIQUID_MOTION_ID, asset.css.to_string(), None)
|
||||||
builtin::LIQUID_MOTION_ID,
|
.expect("compiled-in builtin theme.toml must resolve")
|
||||||
builtin::LIQUID_MOTION_CSS.to_string(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.expect("compiled-in builtin theme.toml must resolve")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static FALLBACK_LOGGED: std::sync::Once = std::sync::Once::new();
|
static FALLBACK_LOGGED: std::sync::Once = std::sync::Once::new();
|
||||||
|
|
@ -390,12 +398,14 @@ pub fn list() -> Vec<ThemeSummary> {
|
||||||
&mut out,
|
&mut out,
|
||||||
&mut seen,
|
&mut seen,
|
||||||
);
|
);
|
||||||
if seen.insert(builtin::LIQUID_MOTION_ID.to_string()) {
|
for b in builtin::ALL {
|
||||||
out.push(ThemeSummary {
|
if seen.insert(b.id.to_string()) {
|
||||||
id: builtin::LIQUID_MOTION_ID.to_string(),
|
out.push(ThemeSummary {
|
||||||
name: "Liquid Motion".to_string(),
|
id: b.id.to_string(),
|
||||||
source: ThemeSource::Builtin,
|
name: b.name.to_string(),
|
||||||
});
|
source: ThemeSource::Builtin,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
@ -618,6 +628,99 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- glass-workbench builtin (plan §11 phase 5) -----------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glass_workbench_loads_and_appears_in_list() {
|
||||||
|
let theme = load_named(builtin::GLASS_WORKBENCH_ID)
|
||||||
|
.expect("glass-workbench builtin should resolve");
|
||||||
|
assert_eq!(theme.id(), "glass-workbench");
|
||||||
|
assert_eq!(theme.name(), "Glass Workbench");
|
||||||
|
|
||||||
|
let summaries = list();
|
||||||
|
assert!(
|
||||||
|
summaries
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.id == "glass-workbench" && s.source == ThemeSource::Builtin),
|
||||||
|
"glass-workbench missing from list(): {summaries:?}"
|
||||||
|
);
|
||||||
|
// Both builtins must be listed side by side — Phase 5 must not have
|
||||||
|
// dropped liquid-motion in the process of adding a second theme.
|
||||||
|
assert!(summaries
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.id == "liquid-motion" && s.source == ThemeSource::Builtin));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glass_workbench_window_is_flush_edge_to_edge_not_a_floating_island() {
|
||||||
|
let theme =
|
||||||
|
load_named(builtin::GLASS_WORKBENCH_ID).expect("glass-workbench should resolve");
|
||||||
|
let w = theme.window();
|
||||||
|
assert_eq!(w.anchors, vec!["top", "left", "right"]);
|
||||||
|
assert!(matches!(w.width, Width::Fill));
|
||||||
|
assert_eq!(w.height, 36);
|
||||||
|
assert_eq!(
|
||||||
|
w.margin,
|
||||||
|
Margin {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0
|
||||||
|
},
|
||||||
|
"flush bar must have no margin on any edge"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
theme.tokens().radius_bar(),
|
||||||
|
0,
|
||||||
|
"flush edge-to-edge bar must have square corners"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
theme.tokens().bar_border(),
|
||||||
|
"bottom",
|
||||||
|
"flush bar draws only a bottom hairline, not liquid-motion's full border"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glass_workbench_modules_are_pill_and_plain_with_no_media_slot() {
|
||||||
|
let theme =
|
||||||
|
load_named(builtin::GLASS_WORKBENCH_ID).expect("glass-workbench should resolve");
|
||||||
|
assert!(matches!(
|
||||||
|
theme.modules().workspaces.style,
|
||||||
|
WorkspaceStyle::Pill
|
||||||
|
));
|
||||||
|
assert!(matches!(theme.modules().clock.style, ClockStyle::Plain));
|
||||||
|
assert!(theme.modules().clock.show_date);
|
||||||
|
assert_eq!(theme.slots().centre, vec!["clock"]);
|
||||||
|
assert!(
|
||||||
|
!theme.slots().centre.contains(&"media".to_string()),
|
||||||
|
"demo 02 has no media widget"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
theme.slots().right,
|
||||||
|
vec!["cpu", "ram", "wifi", "battery", "control"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glass_workbench_accent_is_flat_and_a_palette_token_not_hex() {
|
||||||
|
let theme =
|
||||||
|
load_named(builtin::GLASS_WORKBENCH_ID).expect("glass-workbench should resolve");
|
||||||
|
let t = theme.tokens();
|
||||||
|
assert_eq!(t.accent_from(), "green");
|
||||||
|
assert_eq!(t.accent_to(), "green");
|
||||||
|
let css = theme.css(&crate::Palette::default());
|
||||||
|
assert!(
|
||||||
|
css.contains("@green"),
|
||||||
|
"accent_from/accent_to must resolve to the @green palette token, not a hex literal:\n{css}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!css.contains("#7a9a88"),
|
||||||
|
"the demo's sage hex must never leak into the manifest — pywal theming depends on \
|
||||||
|
this staying a palette token name:\n{css}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- extends merge ------------------------------------------------
|
// ---- extends merge ------------------------------------------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,16 @@ impl Tokens {
|
||||||
pub fn icon_px(&self) -> i64 {
|
pub fn icon_px(&self) -> i64 {
|
||||||
self.int_or("icon_px", 24)
|
self.int_or("icon_px", 24)
|
||||||
}
|
}
|
||||||
|
/// Not in the plan §4 schema. `"full"` (default, liquid-motion's island)
|
||||||
|
/// draws a border on all four edges; `"bottom"` (glass-workbench's flush
|
||||||
|
/// 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`].
|
||||||
|
pub fn bar_border(&self) -> String {
|
||||||
|
self.str_or("bar_border", "full")
|
||||||
|
}
|
||||||
|
|
||||||
/// Replace every `{name}` occurrence in `template` with that token's
|
/// Replace every `{name}` occurrence in `template` with that token's
|
||||||
/// [`TokenValue::as_css`] form. Longest names are substituted first
|
/// [`TokenValue::as_css`] form. Longest names are substituted first
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue