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
|
|
@ -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