shell: add spotlight builtin (theme 04), dots/placeholder-clock schema, and anim::spring_to
- bread-theme:🐚 WorkspacesModule.dot_widths and ClockModule.placeholder_clock,
resolved/validated in manifest.rs and modeled in types.rs, so a theme can
actually configure dot-pill widths and an entry-as-clock placeholder
instead of those staying schema-only.
- New compiled-in builtin "spotlight" (assets/shell/spotlight/), registered
in builtin.rs so list() returns three themes: centred capsule window spec
(anchors=[top] only, width=480, exclusive=none, keyboard=on_demand),
workspaces.style=dots, clock.style=none+placeholder_clock, launcher.mode=
embedded, drawer=[launcher_results]. Colours are palette token names
(pink), never hex.
- bread-theme::anim::spring_to: a reusable TickCallback size-interpolation
helper (GTK4 has no CSS width/height transition on a widget or layer-shell
surface), generalized from WorkspaceTrail's existing ease/tick pattern, for
breadbar's capsule-drawer expand/collapse.
- bread_launcher::LAUNCHER_APP: the launcher's one shared on-disk identity
(cache/config/history), so breadbar's embedded capsule and breadbox's
overlay window read and write the SAME cache and launch history rather
than forking into two rankings of a user's apps.
Tests: +6 spotlight builtin tests (loads/in list/capsule window shape/
embedded mode/dots widths/flat-pink-not-hex), bread-theme --lib 63->69,
bread-launcher --lib unchanged at 18.
This commit is contained in:
parent
6ffd689a4d
commit
8c5e42230a
9 changed files with 505 additions and 2 deletions
73
bread-theme/assets/shell/spotlight/spotlight.css
Normal file
73
bread-theme/assets/shell/spotlight/spotlight.css
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* CSS template for the spotlight builtin (bread-theme/src/shell/builtin.rs).
|
||||
* Same scope and substitution rules as liquid-motion.css/glass-workbench.css:
|
||||
* only the window/workspace/clock chrome the manifest's own concepts model.
|
||||
*
|
||||
* NOTE (unlike its two siblings): breadbar does not actually call
|
||||
* `ShellTheme::css()` today — its live stylesheet is the hand-written,
|
||||
* token-driven `breadbar::theme::load_css()`, which is where the capsule's
|
||||
* real chrome (dots, `.launcher-entry`, `.bread-drawer`/results rows) lives.
|
||||
* This file exists so the manifest's own template mechanism stays exercised
|
||||
* for all three builtins equally (and so a future `bread-theme` CLI/test
|
||||
* that DOES call `.css()` sees a real spotlight template) — see
|
||||
* `ShellTheme::css`'s own doc comment for why that gap between "what the
|
||||
* manifest models" and "what breadbar actually renders" is pre-existing,
|
||||
* not new to this file.
|
||||
*
|
||||
* Source: bos-ui-demos/04-spotlight.html's <style> block. No trail overlay
|
||||
* (dots never call `WorkspaceTrail::place`/`stretch`, same as pill) and no
|
||||
* per-digit flip (`style = "none"` — no clock label markup at all).
|
||||
*/
|
||||
|
||||
window.breadbar {
|
||||
background-color: alpha(@bg, {bg_alpha});
|
||||
color: @on-bg;
|
||||
border-radius: {radius_bar}px;
|
||||
border: 1px solid alpha(@on-bg, 0.08);
|
||||
}
|
||||
window.breadbar > box > centerbox { padding: 0 {pad}px; }
|
||||
window.breadbar button { min-height: 0; min-width: 0; }
|
||||
|
||||
/* Dots: colour/opacity/radius only — width is per-instance data
|
||||
* (`modules.workspaces.dot_widths`), set directly via `set_size_request` in
|
||||
* Rust (`bar::workspaces::make_dot_button`), since GTK CSS has no
|
||||
* per-instance variable width the way the demo's `[data-n]` attribute
|
||||
* selectors do.
|
||||
*/
|
||||
.workspace-dot {
|
||||
background-color: alpha(@on-bg, 0.35);
|
||||
border-radius: {radius_pill}px;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
min-height: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: background-color 0.25s {spring_settle}, opacity 0.25s {spring_settle};
|
||||
}
|
||||
.workspace-dot:hover { background-color: alpha(@on-bg, 0.55); }
|
||||
.workspace-dot:not(.occupied):not(.active) { opacity: 0.35; }
|
||||
.workspace-dot.active {
|
||||
background-color: @{accent_from};
|
||||
opacity: 1;
|
||||
}
|
||||
.workspace-dot.active:hover { background-color: @{accent_from}; }
|
||||
|
||||
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; }
|
||||
183
bread-theme/assets/shell/spotlight/theme.toml
Normal file
183
bread-theme/assets/shell/spotlight/theme.toml
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# The third compiled-in builtin (bread-theme/src/shell/builtin.rs), demo 04
|
||||
# in THEME_SYSTEM_PLAN.md — "Spotlight": breadbar's bar IS the launcher, a
|
||||
# centred capsule that expands into results (plan §7). Values are taken from
|
||||
# bos-ui-demos/04-spotlight.html's <style> block; where the demo is silent
|
||||
# (satellite offsets, bar-adjacent chip sizing) this file re-derives from the
|
||||
# capsule's own top edge the same way glass-workbench re-derives from its
|
||||
# flush bar's edge — see that file's header comment for the method.
|
||||
#
|
||||
# Two exceptions, same shape as glass-workbench's: font (Outfit is not
|
||||
# installed on the dev machine, so this renders in fallback until the user
|
||||
# installs ttf-outfit — same gap liquid-motion already has) and colour (the
|
||||
# demo's #e87898 pink is a *flat* accent — `--accent` doubling as both the
|
||||
# capsule's dot fill and its only gradient stop — so it maps to the
|
||||
# palette's `pink` token for both accent_from and accent_to, never a literal
|
||||
# hex value).
|
||||
#
|
||||
# What this builtin does NOT model (THEME_SYSTEM_PLAN.md phase 6 scope, task
|
||||
# "OUT OF SCOPE" list — deferred to phase 6c): query modes (`=` calc, `>`
|
||||
# command, `.` url) and result sections ("recent"/"apps") — `modes` stays
|
||||
# `["apps"]` and `sections` stays `false`, same trivial values every other
|
||||
# builtin already carries. The demo's `.searching .capsule { border-radius:
|
||||
# 20px }` (22px collapsed, 20px while actively searching) also has no
|
||||
# corresponding schema key — `[launcher].radius` is a single value, so this
|
||||
# builtin uses the collapsed 22px; the 2px searching-state shrink is an
|
||||
# unmodeled gap, not an oversight (see breadbar's launcher_entry/results
|
||||
# task notes for where this is implemented in Rust instead).
|
||||
|
||||
name = "Spotlight"
|
||||
id = "spotlight"
|
||||
|
||||
[tokens]
|
||||
font_family = "Outfit"
|
||||
font_fallback = "Varela Round, sans-serif"
|
||||
font_size_base = 13
|
||||
radius_bar = 22
|
||||
radius_card = 12
|
||||
radius_sm = 8
|
||||
radius_pill = 999
|
||||
pad = 14
|
||||
# Demo: `background: color-mix(in oklab, var(--glass) 82%, transparent)`.
|
||||
bg_alpha = 0.82
|
||||
# The demo's one curve (`--spring: cubic-bezier(.22,1.35,.36,1)`) — the same
|
||||
# overshoot liquid-motion calls `spring`. No separate flatter "settle" curve
|
||||
# is defined in 04's stylesheet, so both accessors point at it, same
|
||||
# reasoning as glass-workbench's single-curve tokens.
|
||||
spring = "cubic-bezier(0.22, 1.35, 0.36, 1)"
|
||||
spring_settle = "cubic-bezier(0.22, 1.35, 0.36, 1)"
|
||||
# Palette token NAMES, not hex — #e87898 is this palette's `pink`. Equal
|
||||
# from/to because the demo's accent here is flat, not liquid-motion's
|
||||
# accent→teal gradient.
|
||||
accent_from = "pink"
|
||||
accent_to = "pink"
|
||||
# Demo: `.barrow { height: 36px }` with no separate workspace-chip height —
|
||||
# the dots (`.dots button`) are sized entirely by `dot_widths` below, not by
|
||||
# chip_height, but this token still feeds any other chip-shaped element
|
||||
# (e.g. a future result-row affordance) at the bar's own row height.
|
||||
chip_height = 36
|
||||
# Demo: `.ico { width: 30px; height: 30px }` for a result row's icon
|
||||
# swatch — the icon glyph itself sits inside that box with implicit
|
||||
# padding, so the icon_px passed to bread-launcher's `ResultsList` is sized
|
||||
# a little under the swatch box, same margin glass-workbench leaves between
|
||||
# its 18px icon_px and larger chip_height.
|
||||
icon_px = 22
|
||||
# Floating centred capsule, full rounded border on every edge — same
|
||||
# reasoning as liquid-motion's island, not glass-workbench's flush bar.
|
||||
bar_border = "full"
|
||||
|
||||
[bar.window]
|
||||
# Anchored top ONLY: gtk4-layer-shell centres a surface anchored to
|
||||
# neither left nor right (THEME_SYSTEM_PLAN.md §7) — this is what makes the
|
||||
# capsule float centred instead of spanning the screen width.
|
||||
anchors = ["top"]
|
||||
width = 480
|
||||
height = 36
|
||||
margin = { top = 16 }
|
||||
# No exclusive zone: tiled clients run full-height behind the capsule,
|
||||
# unlike Island/Edge's reserved strip — the capsule floats over content.
|
||||
exclusive = "none"
|
||||
# Hands keyboard focus over only while `launcher_entry` holds it (plan §7)
|
||||
# — Island/Edge themes keep "none", unchanged.
|
||||
keyboard = "on_demand"
|
||||
layer = "top"
|
||||
|
||||
[bar.slots]
|
||||
left = ["workspaces"]
|
||||
centre = ["launcher_entry"]
|
||||
right = ["battery"]
|
||||
drawer = ["launcher_results"]
|
||||
|
||||
[modules.workspaces]
|
||||
style = "dots"
|
||||
show_empty = true
|
||||
# 0/1/2/3-or-more open windows → dot width in px
|
||||
# (`04-spotlight.html`'s `.dots button[data-n="N"]` rules).
|
||||
dot_widths = [6, 10, 14, 18]
|
||||
|
||||
[modules.clock]
|
||||
# No clock label at all — `launcher_entry`'s placeholder IS the clock
|
||||
# (`04-spotlight.html`: `q.placeholder = t` in `clock()`, replaced by the
|
||||
# search prompt only once focused/open).
|
||||
style = "none"
|
||||
format = "%H:%M"
|
||||
show_date = false
|
||||
placeholder_clock = true
|
||||
|
||||
[launcher]
|
||||
mode = "embedded"
|
||||
# Unread by an embedded launcher (breadbar's launcher_entry/launcher_results
|
||||
# read [bar.window].width and this table's radius/icon_px instead of
|
||||
# width/top — those two fields exist only for LauncherMode::Overlay, which
|
||||
# breadbox's own window still uses). Kept at the bar's own width/22px radius
|
||||
# for consistency with a reader who expects [launcher] to describe the
|
||||
# launcher's shape regardless of mode.
|
||||
width = 480
|
||||
top = "16px"
|
||||
radius = 22
|
||||
icon_px = 22
|
||||
row_anim = "none"
|
||||
rule = "none"
|
||||
footer = "count_apps"
|
||||
sections = false
|
||||
modes = ["apps"]
|
||||
|
||||
# Same five satellite namespaces as liquid-motion/glass-workbench, keyed
|
||||
# identically. This bar's edge sits at margin.top(16) + height(36) = 52px —
|
||||
# re-deriving glass-workbench's own edge+8/edge relationship (44 = 36+8,
|
||||
# 36 = 36) against 52 gives 60 (52+8) and 52.
|
||||
[surfaces."breadbar-notif"]
|
||||
anchor = "top_right"
|
||||
offset = [16, 60]
|
||||
width = 320
|
||||
layer = "overlay"
|
||||
|
||||
[surfaces."breadbar-osd"]
|
||||
# Independent of bar height — unchanged from liquid-motion/glass-workbench.
|
||||
anchor = "bottom_centre"
|
||||
offset = 80
|
||||
width = 180
|
||||
layer = "overlay"
|
||||
|
||||
[surfaces."breadbar-panel"]
|
||||
anchor = "top_right"
|
||||
offset = [16, 60]
|
||||
width = "auto"
|
||||
layer = "overlay"
|
||||
|
||||
[surfaces."breadbar-dismiss"]
|
||||
anchor = "fill"
|
||||
offset = 52
|
||||
width = "fill"
|
||||
layer = "overlay"
|
||||
|
||||
# Appearance-only, identical in shape to liquid-motion/glass-workbench's
|
||||
# rules — blur strength is still global (plan §9 known limit).
|
||||
[compositor."breadbar"]
|
||||
blur = true
|
||||
ignore_alpha = 0.18
|
||||
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 its two siblings.
|
||||
68
bread-theme/src/anim.rs
Normal file
68
bread-theme/src/anim.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//! `anim::spring_to` — THEME_SYSTEM_PLAN.md §7/§8: GTK4 has no CSS
|
||||
//! width/height transition on a widget or a layer-shell surface (unlike the
|
||||
//! demos' `transition: width .45s var(--spring)` /
|
||||
//! `transition: max-height .4s var(--spring)`), so a size change that wants
|
||||
//! to animate has to interpolate a plain integer over the frame clock
|
||||
//! instead and re-apply it (`set_size_request`, `set_default_width`, ...)
|
||||
//! every frame.
|
||||
//!
|
||||
//! This is the same house technique `breadbar::bar::workspaces::WorkspaceTrail`
|
||||
//! already uses for the workspace trail's stretch/snap (a local, un-exported
|
||||
//! `ease`/`ease_overshoot` pair driven by `add_tick_callback` and
|
||||
//! `Instant::elapsed`) — lifted here, generalized to a plain `i32 -> i32`
|
||||
//! interpolation with a caller-supplied frame callback, so theme 04's
|
||||
//! capsule-drawer expand/collapse (and any future popover that wants the
|
||||
//! same effect) doesn't have to reimplement it.
|
||||
|
||||
use gtk4::glib::ControlFlow;
|
||||
use gtk4::prelude::*;
|
||||
|
||||
/// Approximates `cubic-bezier(0.22, 1.35, 0.36, 1)` — the overshoot/"spring"
|
||||
/// curve every builtin theme's `tokens.spring` names (`Tokens::spring`'s own
|
||||
/// default). Not a literal bezier solve (same approximation
|
||||
/// `bar::workspaces::ease_overshoot` uses) — exact enough that the eye can't
|
||||
/// tell it apart from the real curve at animation speeds.
|
||||
fn spring_ease(t: f64) -> f64 {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
let c = 1.35;
|
||||
let t1 = t - 1.0;
|
||||
1.0 + t1 * t1 * ((c + 1.0) * t1 + c)
|
||||
}
|
||||
|
||||
/// Interpolates from `from` to `to` over `duration_ms`, calling `on_frame`
|
||||
/// with each intermediate value (and, on the final tick, the exact `to` —
|
||||
/// never an off-by-rounding near-miss) via `widget`'s frame clock.
|
||||
///
|
||||
/// Returns the [`gtk4::TickCallbackId`] so a caller that might need to
|
||||
/// interrupt an in-flight run (e.g. the drawer re-opening before its close
|
||||
/// animation finished) can `.remove()` it early; a run left to finish on its
|
||||
/// own needs no cleanup — the callback self-terminates by returning
|
||||
/// [`ControlFlow::Break`] once `duration_ms` has elapsed, same as
|
||||
/// `WorkspaceTrail`'s own tick callbacks.
|
||||
pub fn spring_to(
|
||||
widget: &impl IsA<gtk4::Widget>,
|
||||
from: i32,
|
||||
to: i32,
|
||||
duration_ms: f64,
|
||||
on_frame: impl FnMut(i32) + 'static,
|
||||
) -> gtk4::TickCallbackId {
|
||||
let started = std::time::Instant::now();
|
||||
// `add_tick_callback` requires `Fn`, not `FnMut` — the caller's frame
|
||||
// closure almost always needs to mutate captured state (a widget's size
|
||||
// request, an `Rc<Cell<..>>` flag), so it's boxed behind a `RefCell`
|
||||
// here rather than pushing `Cell`/`RefCell` plumbing onto every call
|
||||
// site (every current and future caller wants `FnMut`, none want `Fn`).
|
||||
let on_frame = std::cell::RefCell::new(on_frame);
|
||||
widget.add_tick_callback(move |_, _| {
|
||||
let elapsed = started.elapsed().as_secs_f64() * 1000.0;
|
||||
let mut on_frame = on_frame.borrow_mut();
|
||||
if elapsed >= duration_ms {
|
||||
on_frame(to);
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
let t = spring_ease(elapsed / duration_ms);
|
||||
let value = from as f64 + (to - from) as f64 * t;
|
||||
on_frame(value.round() as i32);
|
||||
ControlFlow::Continue
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
#[cfg(feature = "adw")]
|
||||
pub mod adw;
|
||||
#[cfg(feature = "gtk")]
|
||||
pub mod anim;
|
||||
#[cfg(feature = "gtk")]
|
||||
pub mod gtk;
|
||||
mod layerrules;
|
||||
mod output;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,18 @@ const GLASS_WORKBENCH_CSS: &str = include_str!(concat!(
|
|||
"/assets/shell/glass-workbench/glass-workbench.css"
|
||||
));
|
||||
|
||||
pub const SPOTLIGHT_ID: &str = "spotlight";
|
||||
|
||||
const SPOTLIGHT_TOML: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/assets/shell/spotlight/theme.toml"
|
||||
));
|
||||
|
||||
const SPOTLIGHT_CSS: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/assets/shell/spotlight/spotlight.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
|
||||
|
|
@ -68,6 +80,12 @@ pub const ALL: &[BuiltinTheme] = &[
|
|||
toml: GLASS_WORKBENCH_TOML,
|
||||
css: GLASS_WORKBENCH_CSS,
|
||||
},
|
||||
BuiltinTheme {
|
||||
id: SPOTLIGHT_ID,
|
||||
name: "Spotlight",
|
||||
toml: SPOTLIGHT_TOML,
|
||||
css: SPOTLIGHT_CSS,
|
||||
},
|
||||
];
|
||||
|
||||
/// Looks up a compiled-in theme by id — `None` means "not a builtin",
|
||||
|
|
|
|||
|
|
@ -156,6 +156,11 @@ pub(super) struct RawModules {
|
|||
pub(super) struct RawWorkspacesModule {
|
||||
pub(super) style: Option<String>,
|
||||
pub(super) show_empty: Option<bool>,
|
||||
/// `[6, 10, 14, 18]`-shaped — see [`DotWidths`]. A length other than 4
|
||||
/// is a hard error (validated in [`resolve_modules`]) rather than
|
||||
/// silently truncated/padded, matching this file's "typo'd key is a
|
||||
/// hard error" policy for enum-ish values.
|
||||
pub(super) dot_widths: Option<Vec<i64>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
|
|
@ -164,6 +169,7 @@ pub(super) struct RawClockModule {
|
|||
pub(super) style: Option<String>,
|
||||
pub(super) format: Option<String>,
|
||||
pub(super) show_date: Option<bool>,
|
||||
pub(super) placeholder_clock: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
|
|
@ -318,6 +324,15 @@ fn resolve_modules(theme_id: &str, m: Option<&RawModules>) -> anyhow::Result<Mod
|
|||
),
|
||||
};
|
||||
let show_empty = ws.and_then(|w| w.show_empty).unwrap_or(true);
|
||||
let dot_widths = match ws.and_then(|w| w.dot_widths.as_ref()) {
|
||||
None => DEFAULT_DOT_WIDTHS,
|
||||
Some(v) if v.len() == 4 => [v[0] as i32, v[1] as i32, v[2] as i32, v[3] as i32],
|
||||
Some(v) => bail!(
|
||||
"theme '{theme_id}': modules.workspaces.dot_widths has {} entries, expected 4 \
|
||||
(0/1/2/3-or-more open windows)",
|
||||
v.len()
|
||||
),
|
||||
};
|
||||
|
||||
let ck = m.and_then(|m| m.clock.as_ref());
|
||||
let cstyle = match ck.and_then(|c| c.style.as_deref()) {
|
||||
|
|
@ -333,13 +348,19 @@ fn resolve_modules(theme_id: &str, m: Option<&RawModules>) -> anyhow::Result<Mod
|
|||
.and_then(|c| c.format.clone())
|
||||
.unwrap_or_else(|| "%H:%M".to_string());
|
||||
let show_date = ck.and_then(|c| c.show_date).unwrap_or(false);
|
||||
let placeholder_clock = ck.and_then(|c| c.placeholder_clock).unwrap_or(false);
|
||||
|
||||
Ok(Modules {
|
||||
workspaces: WorkspacesModule { style, show_empty },
|
||||
workspaces: WorkspacesModule {
|
||||
style,
|
||||
show_empty,
|
||||
dot_widths,
|
||||
},
|
||||
clock: ClockModule {
|
||||
style: cstyle,
|
||||
format,
|
||||
show_date,
|
||||
placeholder_clock,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -721,6 +721,103 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// ---- spotlight builtin (plan §11 phase 6) ------------------------------
|
||||
|
||||
#[test]
|
||||
fn spotlight_loads_and_appears_in_list_alongside_the_other_two() {
|
||||
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight builtin should resolve");
|
||||
assert_eq!(theme.id(), "spotlight");
|
||||
assert_eq!(theme.name(), "Spotlight");
|
||||
|
||||
let summaries = list();
|
||||
assert!(
|
||||
summaries
|
||||
.iter()
|
||||
.any(|s| s.id == "spotlight" && s.source == ThemeSource::Builtin),
|
||||
"spotlight missing from list(): {summaries:?}"
|
||||
);
|
||||
// All three builtins must be listed side by side — adding the third
|
||||
// must not have displaced either of the first two.
|
||||
for id in ["liquid-motion", "glass-workbench"] {
|
||||
assert!(
|
||||
summaries
|
||||
.iter()
|
||||
.any(|s| s.id == id && s.source == ThemeSource::Builtin),
|
||||
"{id} missing from list() after adding spotlight: {summaries:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spotlight_window_is_a_centred_capsule_not_a_full_width_bar() {
|
||||
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||
let w = theme.window();
|
||||
// Anchored top ONLY — neither left nor right, which is what makes
|
||||
// gtk4-layer-shell centre the surface instead of stretching it
|
||||
// (THEME_SYSTEM_PLAN.md §7).
|
||||
assert_eq!(w.anchors, vec!["top"]);
|
||||
assert_eq!(w.width, Width::Px(480));
|
||||
assert_eq!(w.height, 36);
|
||||
assert_eq!(w.margin.top, 16);
|
||||
assert_eq!(
|
||||
w.exclusive,
|
||||
Exclusive::None,
|
||||
"the capsule floats over tiled content, unlike Island/Edge's reserved strip"
|
||||
);
|
||||
assert_eq!(
|
||||
w.keyboard,
|
||||
Keyboard::OnDemand,
|
||||
"keyboard focus hands over only while launcher_entry holds it"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spotlight_launcher_is_embedded_not_an_overlay_window() {
|
||||
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||
assert_eq!(theme.launcher().mode, LauncherMode::Embedded);
|
||||
assert_eq!(theme.slots().centre, vec!["launcher_entry"]);
|
||||
assert_eq!(theme.slots().drawer, vec!["launcher_results"]);
|
||||
assert!(
|
||||
!theme.slots().drawer.is_empty(),
|
||||
"spotlight is the one builtin that actually uses the drawer slot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spotlight_workspaces_are_dots_with_the_demos_own_widths() {
|
||||
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||
assert!(matches!(theme.modules().workspaces.style, WorkspaceStyle::Dots));
|
||||
assert_eq!(theme.modules().workspaces.dot_widths, [6, 10, 14, 18]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spotlight_clock_is_none_with_placeholder_clock_set() {
|
||||
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||
assert!(matches!(theme.modules().clock.style, ClockStyle::None));
|
||||
assert!(
|
||||
theme.modules().clock.placeholder_clock,
|
||||
"spotlight's entry placeholder stands in for a clock module entirely"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spotlight_accent_is_flat_pink_and_a_palette_token_not_hex() {
|
||||
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||
let t = theme.tokens();
|
||||
assert_eq!(t.accent_from(), "pink");
|
||||
assert_eq!(t.accent_to(), "pink");
|
||||
let css = theme.css(&crate::Palette::default());
|
||||
assert!(
|
||||
css.contains("@pink"),
|
||||
"accent_from/accent_to must resolve to the @pink palette token, not a hex literal:\n{css}"
|
||||
);
|
||||
assert!(
|
||||
!css.contains("#e87898"),
|
||||
"the demo's pink hex must never leak into the manifest — pywal theming depends on \
|
||||
this staying a palette token name:\n{css}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- extends merge ------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@ pub enum ClockStyle {
|
|||
None,
|
||||
}
|
||||
|
||||
/// Dot pill widths (px) for 0/1/2/3-or-more open windows, `style = "dots"`
|
||||
/// (theme 04/spotlight). Index 3 covers "3 or more" — the demo's own dots
|
||||
/// never grow past that fourth width. Unused by `Trail`/`Pill`.
|
||||
pub type DotWidths = [i32; 4];
|
||||
|
||||
/// The demo's own numbers (`04-spotlight.html`'s `.dots button[data-n="N"]`
|
||||
/// rules) — the default a theme gets if it sets `style = "dots"` but omits
|
||||
/// `dot_widths`.
|
||||
pub const DEFAULT_DOT_WIDTHS: DotWidths = [6, 10, 14, 18];
|
||||
|
||||
/// How the launcher attaches to the shell. Phase 1 ships only `Overlay`
|
||||
/// (breadbox's own window); `Embedded` is theme 04's bar-drawer launcher.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -133,6 +143,8 @@ pub struct Slots {
|
|||
pub struct WorkspacesModule {
|
||||
pub style: WorkspaceStyle,
|
||||
pub show_empty: bool,
|
||||
/// `style = "dots"` only — see [`DotWidths`]. Trail/Pill ignore this.
|
||||
pub dot_widths: DotWidths,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -140,6 +152,12 @@ pub struct ClockModule {
|
|||
pub style: ClockStyle,
|
||||
pub format: String,
|
||||
pub show_date: bool,
|
||||
/// `style = "none"` + this `true`: no module renders a clock label of
|
||||
/// its own — `launcher_entry`'s placeholder text becomes the time
|
||||
/// instead (theme 04/spotlight: the capsule's entry IS the clock until
|
||||
/// focused, per `04-spotlight.html`'s `q.placeholder = t`). Meaningless
|
||||
/// for `Flip`/`Plain`, which already show a time some other way.
|
||||
pub placeholder_clock: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue