shell: widen spotlight's launcher schema for phase 6c (modes/sections/search geometry)
[launcher].modes now lists calc/cmd/url alongside apps, sections is enabled, and two new keys (search_width/search_radius) let an embedded launcher theme declare its search-state capsule geometry, defaulting to the idle value when a theme omits them. Also gives spotlight's [bar.slots] a widget:left_of_stats entry so a Lua widget requesting that placement (e.g. git-branch-widget.lua) has a home instead of being silently dropped.
This commit is contained in:
parent
95e676e909
commit
d2c68f18b0
4 changed files with 109 additions and 15 deletions
|
|
@ -14,16 +14,13 @@
|
||||||
# palette's `pink` token for both accent_from and accent_to, never a literal
|
# palette's `pink` token for both accent_from and accent_to, never a literal
|
||||||
# hex value).
|
# hex value).
|
||||||
#
|
#
|
||||||
# What this builtin does NOT model (THEME_SYSTEM_PLAN.md phase 6 scope, task
|
# Phase 6c (this pass): query modes (`=` calc, `>` command, `.` url) and
|
||||||
# "OUT OF SCOPE" list — deferred to phase 6c): query modes (`=` calc, `>`
|
# result sections ("recent"/"apps") are now implemented — `modes` lists all
|
||||||
# command, `.` url) and result sections ("recent"/"apps") — `modes` stays
|
# four and `sections` is `true`. The demo's `.searching .capsule` state
|
||||||
# `["apps"]` and `sections` stays `false`, same trivial values every other
|
# (480px -> 520px width, 22px -> 20px radius) is modeled via two new
|
||||||
# builtin already carries. The demo's `.searching .capsule { border-radius:
|
# `[launcher]` keys, `search_width`/`search_radius`, read only by
|
||||||
# 20px }` (22px collapsed, 20px while actively searching) also has no
|
# `LauncherMode::Embedded` hosts (breadbar's capsule; unread by breadbox's
|
||||||
# corresponding schema key — `[launcher].radius` is a single value, so this
|
# overlay window, which stays `mode = "overlay"`).
|
||||||
# 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"
|
name = "Spotlight"
|
||||||
id = "spotlight"
|
id = "spotlight"
|
||||||
|
|
@ -84,7 +81,19 @@ layer = "top"
|
||||||
[bar.slots]
|
[bar.slots]
|
||||||
left = ["workspaces"]
|
left = ["workspaces"]
|
||||||
centre = ["launcher_entry"]
|
centre = ["launcher_entry"]
|
||||||
right = ["battery"]
|
# `widget:left_of_stats` (Phase 6c decision, see task notes): a Lua widget
|
||||||
|
# that requests this same placement (`bread-shared`'s `WidgetPlacement::
|
||||||
|
# LeftOfStats`, e.g. a real user's `git-branch-widget.lua`) would otherwise
|
||||||
|
# be silently undeliverable under spotlight — no `[bar.slots]` entry named
|
||||||
|
# any `widget:*` key at all, so `reconcile_widgets` drops it every time
|
||||||
|
# (quietly, since the Phase 6b fix, but still dropped). Deliberately just
|
||||||
|
# this one alias, not all five liquid-motion carries: spotlight has no
|
||||||
|
# clock module to anchor `left_of_clock`/`right_of_clock` against and no
|
||||||
|
# workspace-adjacent affordance next to its compact dots, so adding those
|
||||||
|
# would clutter a launcher-focused capsule with slots nothing here asks
|
||||||
|
# for. `left_of_stats` sits right where `battery` already does, matching
|
||||||
|
# liquid-motion's own ordering (`widget:left_of_stats` before its module).
|
||||||
|
right = ["widget:left_of_stats", "battery"]
|
||||||
drawer = ["launcher_results"]
|
drawer = ["launcher_results"]
|
||||||
|
|
||||||
[modules.workspaces]
|
[modules.workspaces]
|
||||||
|
|
@ -118,8 +127,21 @@ icon_px = 22
|
||||||
row_anim = "none"
|
row_anim = "none"
|
||||||
rule = "none"
|
rule = "none"
|
||||||
footer = "count_apps"
|
footer = "count_apps"
|
||||||
sections = false
|
# Phase 6c: "recent" / "apps" headers in the idle (empty-query) drawer view
|
||||||
modes = ["apps"]
|
# — `bread_launcher::split_sections` groups the same already-loaded/sorted
|
||||||
|
# entries `LaunchHistory`'s counts already rank, no new tracking needed.
|
||||||
|
sections = true
|
||||||
|
# `04-spotlight.html`'s three prefixes: `=` calc, `>` command, `.` url —
|
||||||
|
# `bread_launcher::parse_query`/`eval_calc`/`filter_commands` implement the
|
||||||
|
# pure logic; breadbar's capsule wiring (main.rs) gates on this list so an
|
||||||
|
# unlisted prefix character just falls through to a literal "apps" query.
|
||||||
|
modes = ["apps", "calc", "cmd", "url"]
|
||||||
|
# `04-spotlight.html`: `.searching .capsule { width: 520px; border-radius:
|
||||||
|
# 20px }` vs the idle 480px/22px above — animated via
|
||||||
|
# `bread_theme::anim::spring_to` (width) and a `.searching` CSS class
|
||||||
|
# (radius) in breadbar's capsule wiring.
|
||||||
|
search_width = 520
|
||||||
|
search_radius = 20
|
||||||
|
|
||||||
# Same five satellite namespaces as liquid-motion/glass-workbench, keyed
|
# Same five satellite namespaces as liquid-motion/glass-workbench, keyed
|
||||||
# identically. This bar's edge sits at margin.top(16) + height(36) = 52px —
|
# identically. This bar's edge sits at margin.top(16) + height(36) = 52px —
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,8 @@ pub(super) struct RawLauncher {
|
||||||
pub(super) footer: Option<String>,
|
pub(super) footer: Option<String>,
|
||||||
pub(super) sections: Option<bool>,
|
pub(super) sections: Option<bool>,
|
||||||
pub(super) modes: Option<Vec<String>>,
|
pub(super) modes: Option<Vec<String>>,
|
||||||
|
pub(super) search_width: Option<i64>,
|
||||||
|
pub(super) search_radius: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
|
@ -374,13 +376,15 @@ fn resolve_launcher(theme_id: &str, l: Option<&RawLauncher>) -> anyhow::Result<L
|
||||||
bail!("theme '{theme_id}': launcher.mode = \"{other}\" is not overlay|embedded")
|
bail!("theme '{theme_id}': launcher.mode = \"{other}\" is not overlay|embedded")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let width = l.and_then(|l| l.width).unwrap_or(540) as i32;
|
||||||
|
let radius = l.and_then(|l| l.radius).unwrap_or(20) as i32;
|
||||||
Ok(Launcher {
|
Ok(Launcher {
|
||||||
mode,
|
mode,
|
||||||
width: l.and_then(|l| l.width).unwrap_or(540) as i32,
|
width,
|
||||||
top: l
|
top: l
|
||||||
.and_then(|l| l.top.clone())
|
.and_then(|l| l.top.clone())
|
||||||
.unwrap_or_else(|| "16%".to_string()),
|
.unwrap_or_else(|| "16%".to_string()),
|
||||||
radius: l.and_then(|l| l.radius).unwrap_or(20) as i32,
|
radius,
|
||||||
icon_px: l.and_then(|l| l.icon_px).unwrap_or(36) as i32,
|
icon_px: l.and_then(|l| l.icon_px).unwrap_or(36) as i32,
|
||||||
row_anim: l
|
row_anim: l
|
||||||
.and_then(|l| l.row_anim.clone())
|
.and_then(|l| l.row_anim.clone())
|
||||||
|
|
@ -395,6 +399,11 @@ fn resolve_launcher(theme_id: &str, l: Option<&RawLauncher>) -> anyhow::Result<L
|
||||||
modes: l
|
modes: l
|
||||||
.and_then(|l| l.modes.clone())
|
.and_then(|l| l.modes.clone())
|
||||||
.unwrap_or_else(|| vec!["apps".to_string()]),
|
.unwrap_or_else(|| vec!["apps".to_string()]),
|
||||||
|
// Default to the idle value — a theme that never sets these gets
|
||||||
|
// "no change while searching", not a hardcoded widen/shrink it
|
||||||
|
// never asked for (see the field docs on `Launcher`).
|
||||||
|
search_width: l.and_then(|l| l.search_width).map(|v| v as i32).unwrap_or(width),
|
||||||
|
search_radius: l.and_then(|l| l.search_radius).map(|v| v as i32).unwrap_or(radius),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -783,6 +783,56 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spotlight_launcher_widget_slot_gives_left_of_stats_widgets_a_home() {
|
||||||
|
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||||
|
assert!(
|
||||||
|
theme
|
||||||
|
.slots()
|
||||||
|
.right
|
||||||
|
.iter()
|
||||||
|
.any(|s| s == "widget:left_of_stats"),
|
||||||
|
"a Lua widget requesting WidgetPlacement::LeftOfStats (e.g. \
|
||||||
|
git-branch-widget.lua) must have a home under spotlight: {:?}",
|
||||||
|
theme.slots().right
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spotlight_query_modes_and_sections_are_enabled_phase_6c() {
|
||||||
|
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||||
|
let l = theme.launcher();
|
||||||
|
assert!(l.sections, "phase 6c: recent/apps headers are implemented");
|
||||||
|
for mode in ["apps", "calc", "cmd", "url"] {
|
||||||
|
assert!(
|
||||||
|
l.modes.iter().any(|m| m == mode),
|
||||||
|
"spotlight.modes should list \"{mode}\": {:?}",
|
||||||
|
l.modes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spotlight_launcher_search_state_geometry_differs_from_idle() {
|
||||||
|
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||||
|
let l = theme.launcher();
|
||||||
|
assert_eq!(l.width, 480);
|
||||||
|
assert_eq!(l.search_width, 520);
|
||||||
|
assert_eq!(l.radius, 22);
|
||||||
|
assert_eq!(l.search_radius, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn launcher_search_geometry_defaults_to_idle_values_when_unset() {
|
||||||
|
// liquid-motion's own [launcher] table never sets search_width/
|
||||||
|
// search_radius — a theme that omits them must fall back to "no
|
||||||
|
// change while searching", not some hardcoded magic number.
|
||||||
|
let theme = resolve_builtin();
|
||||||
|
let l = theme.launcher();
|
||||||
|
assert_eq!(l.search_width, l.width);
|
||||||
|
assert_eq!(l.search_radius, l.radius);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spotlight_workspaces_are_dots_with_the_demos_own_widths() {
|
fn spotlight_workspaces_are_dots_with_the_demos_own_widths() {
|
||||||
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
let theme = load_named(builtin::SPOTLIGHT_ID).expect("spotlight should resolve");
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,19 @@ pub struct Launcher {
|
||||||
pub footer: String,
|
pub footer: String,
|
||||||
pub sections: bool,
|
pub sections: bool,
|
||||||
pub modes: Vec<String>,
|
pub modes: Vec<String>,
|
||||||
|
/// `LauncherMode::Embedded` only (theme 04/spotlight, plan §7 phase 6c):
|
||||||
|
/// the capsule's own width while a search is in progress
|
||||||
|
/// (`04-spotlight.html`: `.searching .capsule { width: 520px }` vs the
|
||||||
|
/// idle 480px). Defaults to `width` (no widen) for a theme that omits
|
||||||
|
/// it, so an `Overlay`-mode theme — which never reads this field at all
|
||||||
|
/// — and an `Embedded` theme that just doesn't want the widen both fall
|
||||||
|
/// back to "no change" rather than a hardcoded magic number.
|
||||||
|
pub search_width: i32,
|
||||||
|
/// `LauncherMode::Embedded` only: `border-radius` while searching
|
||||||
|
/// (`04-spotlight.html`: `.searching .capsule { border-radius: 20px }`
|
||||||
|
/// vs the collapsed 22px `radius`). Same default-to-`radius` fallback
|
||||||
|
/// reasoning as `search_width`.
|
||||||
|
pub search_radius: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A satellite surface, keyed by layer-shell namespace in `[surfaces.*]` —
|
/// A satellite surface, keyed by layer-shell namespace in `[surfaces.*]` —
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue