bar: route Lua widgets to any [bar.slots] slot, not just four fixed spots
Phase 3b of the shell theme system. A slot list entry can now be `widget:<key>`, where `<key>` is either a WidgetPlacement alias (right_of_workspaces, left_of_clock, right_of_clock, left_of_stats, tray) or a Lua module name. ModuleRegistry::for_each_in_slot creates each widget container on demand at its slot position; reconcile_widgets routes each WidgetSpec by module name first, falling back to its placement alias, and logs+drops (never panics) a spec with no matching container. bread_shared::widget::WidgetPlacement stays untouched — it's still the wire type breadd sends, unmodified and unshadowed; only the container map that placement now resolves through is theme-driven.
This commit is contained in:
parent
c5f7dd1ee3
commit
8d80a05d9b
2 changed files with 150 additions and 86 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
//! Module registry for the theme manifest's `[bar.slots]` (plan Phase 3a).
|
//! Module registry for the theme manifest's `[bar.slots]` (plan Phase 3a),
|
||||||
|
//! extended in Phase 3b to also route `widget:<key>` entries.
|
||||||
//!
|
//!
|
||||||
//! Each bar module (`workspaces`, `media`, `clock`, `volume`, `wifi`,
|
//! Each bar module (`workspaces`, `media`, `clock`, `volume`, `wifi`,
|
||||||
//! `battery`, `control`, …) is still built exactly where it always was in
|
//! `battery`, `control`, …) is still built exactly where it always was in
|
||||||
|
|
@ -9,10 +10,14 @@
|
||||||
//! done, then walks `ShellTheme::slots()` to append them in the theme's
|
//! done, then walks `ShellTheme::slots()` to append them in the theme's
|
||||||
//! order instead of a hardcoded sequence.
|
//! order instead of a hardcoded sequence.
|
||||||
//!
|
//!
|
||||||
//! The Lua-declared `widget_*` containers (`WidgetPlacement`) are NOT part
|
//! A slot entry may also be `widget:<key>`, where `<key>` is either a
|
||||||
//! of this registry — their fixed interleave (right-of-workspaces,
|
//! `WidgetPlacement` alias (`right_of_workspaces`, `left_of_clock`,
|
||||||
//! left/right-of-clock, left-of-stats) stays exactly as it is today.
|
//! `right_of_clock`, `left_of_stats`, `tray`) or a Lua module name (see
|
||||||
//! Generalizing their placement is Phase 3b, not this task.
|
//! `bread_shared::widget::WidgetSpec::module`) — these route through
|
||||||
|
//! `for_each_in_slot`'s `on_widget` callback rather than this registry,
|
||||||
|
//! since their containers are Lua-widget slots created on demand by the
|
||||||
|
//! caller, not modules registered here. `WidgetPlacement` itself is a wire
|
||||||
|
//! type from `bread-shared` and is never referenced in this file.
|
||||||
|
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -32,22 +37,50 @@ impl ModuleRegistry {
|
||||||
self.0.insert(name, widget.clone().upcast());
|
self.0.insert(name, widget.clone().upcast());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Appends every module named in `names` (a manifest slot list, in
|
/// Walks every entry named in `names` (a manifest slot list, in theme
|
||||||
/// theme order) into `container` via `on_widget`, which lets the
|
/// order). A `widget:<key>` entry calls `on_widget(key)`, letting the
|
||||||
/// caller interleave fixed Lua widget containers around specific
|
/// caller create-or-fetch that Lua widget container and append it at
|
||||||
/// modules (e.g. the clock). A name with no registered widget is
|
/// this exact position. Anything else is looked up as a registered
|
||||||
/// logged and skipped — an unrecognized or unmapped module in a theme
|
/// module name and passed to `on_module`; a name with no registered
|
||||||
/// manifest must never crash the bar.
|
/// widget is logged and skipped — an unrecognized or unmapped module in
|
||||||
|
/// a theme manifest must never crash the bar.
|
||||||
pub fn for_each_in_slot(
|
pub fn for_each_in_slot(
|
||||||
&self,
|
&self,
|
||||||
names: &[String],
|
names: &[String],
|
||||||
mut on_widget: impl FnMut(&str, >k4::Widget),
|
mut on_module: impl FnMut(&str, >k4::Widget),
|
||||||
|
mut on_widget: impl FnMut(&str),
|
||||||
) {
|
) {
|
||||||
for name in names {
|
for name in names {
|
||||||
|
if let Some(key) = name.strip_prefix("widget:") {
|
||||||
|
on_widget(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match self.0.get(name.as_str()) {
|
match self.0.get(name.as_str()) {
|
||||||
Some(widget) => on_widget(name, widget),
|
Some(widget) => on_module(name, widget),
|
||||||
None => eprintln!("breadbar: [bar.slots] names unknown module '{name}' — skipping"),
|
None => eprintln!("breadbar: [bar.slots] names unknown module '{name}' — skipping"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the widget container keyed `key` in `containers`, creating it
|
||||||
|
/// (a plain horizontal box, styled like every other Lua widget slot) on
|
||||||
|
/// first use. Called from `for_each_in_slot`'s `on_widget` callback so a
|
||||||
|
/// `widget:<key>` slot entry gets a container the first time a theme
|
||||||
|
/// places one there, regardless of whether `key` is a `WidgetPlacement`
|
||||||
|
/// alias or a Lua module name — `reconcile_widgets` (main.rs) is what
|
||||||
|
/// gives that distinction meaning when it routes specs into these
|
||||||
|
/// containers.
|
||||||
|
pub fn widget_slot_container(
|
||||||
|
containers: &mut HashMap<String, gtk4::Box>,
|
||||||
|
key: &str,
|
||||||
|
) -> gtk4::Box {
|
||||||
|
containers
|
||||||
|
.entry(key.to_string())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
let b = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
||||||
|
b.add_css_class("bread-widget-slot");
|
||||||
|
b
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
|
||||||
177
src/main.rs
177
src/main.rs
|
|
@ -101,10 +101,16 @@ pub struct App {
|
||||||
tray_items: std::collections::HashMap<String, gtk4::Button>,
|
tray_items: std::collections::HashMap<String, gtk4::Button>,
|
||||||
|
|
||||||
// ── Lua-declared widgets ─────────────────────────────────────────────
|
// ── Lua-declared widgets ─────────────────────────────────────────────
|
||||||
// One container per WidgetPlacement (see bread_shared::widget), fully
|
// One container per `widget:<key>` slot entry (Phase 3b — see
|
||||||
// rebuilt on every AppInput::WidgetsUpdate — see widgets::client's
|
// bar::slots::ModuleRegistry and reconcile_widgets' routing below),
|
||||||
|
// fully rebuilt on every AppInput::WidgetsUpdate — see widgets::client's
|
||||||
// module doc for why that's simpler than incremental patching here.
|
// module doc for why that's simpler than incremental patching here.
|
||||||
widget_containers: std::collections::HashMap<bread_shared::widget::WidgetPlacement, gtk4::Box>,
|
// Keyed by the slot entry's key: either a WidgetPlacement alias
|
||||||
|
// (`right_of_workspaces`, `left_of_clock`, `right_of_clock`,
|
||||||
|
// `left_of_stats`, `tray`) or a Lua module name. `bread_shared::widget`'s
|
||||||
|
// `WidgetPlacement` itself never appears here — it's a wire type from
|
||||||
|
// the bread daemon API and stays untouched.
|
||||||
|
widget_containers: std::collections::HashMap<String, gtk4::Box>,
|
||||||
widget_tray_section: gtk4::Box,
|
widget_tray_section: gtk4::Box,
|
||||||
widget_tray_sep: gtk4::Separator,
|
widget_tray_sep: gtk4::Separator,
|
||||||
|
|
||||||
|
|
@ -223,9 +229,9 @@ impl SimpleComponent for App {
|
||||||
|
|
||||||
// ── Workspace row (left) ────────────────────────────────────────
|
// ── Workspace row (left) ────────────────────────────────────────
|
||||||
// Built imperatively (not via the view! macro) so a widget
|
// Built imperatively (not via the view! macro) so a widget
|
||||||
// container can sit as a plain sibling of workspace_box — see
|
// container can sit as a plain sibling of workspace_box — see the
|
||||||
// WidgetPlacement::RightOfWorkspaces below. The Overlay trail
|
// `widget:*` slot-entry handling in "Assemble" below. The Overlay
|
||||||
// lives behind the buttons; rebuild_buttons only touches the
|
// trail lives behind the buttons; rebuild_buttons only touches the
|
||||||
// button box, never the trail host.
|
// button box, never the trail host.
|
||||||
let workspace_trail = bar::workspaces::WorkspaceTrail::new();
|
let workspace_trail = bar::workspaces::WorkspaceTrail::new();
|
||||||
let workspace_box = workspace_trail.buttons.clone();
|
let workspace_box = workspace_trail.buttons.clone();
|
||||||
|
|
@ -237,24 +243,12 @@ impl SimpleComponent for App {
|
||||||
// below, in the order `[bar.slots].left` names it — not here.
|
// below, in the order `[bar.slots].left` names it — not here.
|
||||||
|
|
||||||
// ── Lua-declared widget containers ──────────────────────────────
|
// ── Lua-declared widget containers ──────────────────────────────
|
||||||
// One per WidgetPlacement; positioned into the layout below as each
|
// Phase 3b: a container per `widget:<key>` slot entry is created
|
||||||
// surrounding section (workspace row / center area / stats box /
|
// on demand while walking `[bar.slots]` in the "Assemble" section
|
||||||
// control popover) is built. Populated by widgets::client's
|
// below (see `bar::slots::widget_slot_container`), so ANY slot can
|
||||||
// events.subscribe-driven refresh loop, started at the end of init.
|
// host a Lua widget — not just the four fixed positions Phase 3a
|
||||||
use bread_shared::widget::WidgetPlacement;
|
// shipped with. Populated by widgets::client's events.subscribe-
|
||||||
let widget_right_of_workspaces = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
// driven refresh loop, started at the end of init.
|
||||||
widget_right_of_workspaces.add_css_class("bread-widget-slot");
|
|
||||||
// Also appended in "Assemble" — after the left slot's modules, so
|
|
||||||
// its fixed position (right of the workspace modules) is preserved
|
|
||||||
// whatever `[bar.slots].left` contains.
|
|
||||||
|
|
||||||
let widget_left_of_clock = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
|
||||||
widget_left_of_clock.add_css_class("bread-widget-slot");
|
|
||||||
let widget_right_of_clock = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
|
||||||
widget_right_of_clock.add_css_class("bread-widget-slot");
|
|
||||||
|
|
||||||
let widget_left_of_stats = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
|
||||||
widget_left_of_stats.add_css_class("bread-widget-slot");
|
|
||||||
|
|
||||||
// `tokens.icon_px` (plan §4) — bar-chrome icon pixel size; reused
|
// `tokens.icon_px` (plan §4) — bar-chrome icon pixel size; reused
|
||||||
// below for every `prepare_icon` call in this function.
|
// below for every `prepare_icon` call in this function.
|
||||||
|
|
@ -399,9 +393,9 @@ impl SimpleComponent for App {
|
||||||
center_area.add_css_class("center-area");
|
center_area.add_css_class("center-area");
|
||||||
center_area.set_valign(gtk4::Align::Center);
|
center_area.set_valign(gtk4::Align::Center);
|
||||||
center_area.set_vexpand(false);
|
center_area.set_vexpand(false);
|
||||||
// `media_widget`/`clock_box` and the `widget_left_of_clock`/
|
// `media_widget`/`clock_box` and any `widget:*` entries interleaved
|
||||||
// `widget_right_of_clock` interleave around the clock module are
|
// around them are all appended in "Assemble" below, in the exact
|
||||||
// both appended in "Assemble" below, per `[bar.slots].centre`.
|
// order `[bar.slots].centre` names them.
|
||||||
|
|
||||||
// ── Stats box (right side) ───────────────────────────────────────
|
// ── Stats box (right side) ───────────────────────────────────────
|
||||||
// Demo order: [vol 64] [wifi] [bat 83] [☰]
|
// Demo order: [vol 64] [wifi] [bat 83] [☰]
|
||||||
|
|
@ -712,13 +706,16 @@ impl SimpleComponent for App {
|
||||||
media_widget.add_controller(mgesture);
|
media_widget.add_controller(mgesture);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Assemble: slot-driven module order (plan §11 Phase 3a) ───────
|
// ── Assemble: slot-driven module + widget order (plan §11 Phase 3b) ──
|
||||||
// Every module widget above is already fully built; only the ORDER
|
// Every module widget above is already fully built; only the ORDER
|
||||||
// it lands in its container, and which of left/centre/right it
|
// it lands in its container, and which of left/centre/right it
|
||||||
// lands in, comes from the theme manifest's `[bar.slots]` now.
|
// lands in, comes from the theme manifest's `[bar.slots]` now. A
|
||||||
// The `widget_*` Lua containers keep today's fixed interleave
|
// `widget:<key>` slot entry gets (or creates) a Lua widget
|
||||||
// (right-of-workspaces, left/right-of-clock, left-of-stats) —
|
// container at that exact position — `<key>` is either a
|
||||||
// generalizing their placement is Phase 3b, not this task.
|
// `WidgetPlacement` alias or a Lua module name; see
|
||||||
|
// `bar::slots::widget_slot_container` and `reconcile_widgets`'
|
||||||
|
// routing below. This is how a Lua widget can land in ANY slot,
|
||||||
|
// not just the four fixed positions Phase 3a shipped with.
|
||||||
let bar_shell_theme = theme::shell_theme();
|
let bar_shell_theme = theme::shell_theme();
|
||||||
let bar_slots = bar_shell_theme.slots();
|
let bar_slots = bar_shell_theme.slots();
|
||||||
let mut bar_modules = bar::slots::ModuleRegistry::new();
|
let mut bar_modules = bar::slots::ModuleRegistry::new();
|
||||||
|
|
@ -730,32 +727,29 @@ impl SimpleComponent for App {
|
||||||
bar_modules.register("battery", &bat_box);
|
bar_modules.register("battery", &bat_box);
|
||||||
bar_modules.register("control", &hamburger_btn);
|
bar_modules.register("control", &hamburger_btn);
|
||||||
|
|
||||||
bar_modules.for_each_in_slot(&bar_slots.left, |_, widget| workspace_row.append(widget));
|
// `tray` never appears in a bar slot — it stays inside the
|
||||||
workspace_row.append(&widget_right_of_workspaces);
|
// control-panel popover (built above, next to the SNI tray) — but
|
||||||
|
// it's keyed here so `reconcile_widgets`' routing finds it the same
|
||||||
|
// way as any slot-driven widget container.
|
||||||
|
let mut widget_containers: std::collections::HashMap<String, gtk4::Box> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
widget_containers.insert("tray".to_string(), widget_tray_box);
|
||||||
|
|
||||||
bar_modules.for_each_in_slot(&bar_slots.centre, |name, widget| {
|
bar_modules.for_each_in_slot(
|
||||||
if name == "clock" {
|
&bar_slots.left,
|
||||||
center_area.append(&widget_left_of_clock);
|
|_, widget| workspace_row.append(widget),
|
||||||
}
|
|key| workspace_row.append(&bar::slots::widget_slot_container(&mut widget_containers, key)),
|
||||||
center_area.append(widget);
|
);
|
||||||
if name == "clock" {
|
bar_modules.for_each_in_slot(
|
||||||
center_area.append(&widget_right_of_clock);
|
&bar_slots.centre,
|
||||||
}
|
|_, widget| center_area.append(widget),
|
||||||
});
|
|key| center_area.append(&bar::slots::widget_slot_container(&mut widget_containers, key)),
|
||||||
|
);
|
||||||
stats_box.append(&widget_left_of_stats);
|
bar_modules.for_each_in_slot(
|
||||||
bar_modules.for_each_in_slot(&bar_slots.right, |_, widget| stats_box.append(widget));
|
&bar_slots.right,
|
||||||
|
|_, widget| stats_box.append(widget),
|
||||||
let widget_containers = std::collections::HashMap::from([
|
|key| stats_box.append(&bar::slots::widget_slot_container(&mut widget_containers, key)),
|
||||||
(
|
);
|
||||||
WidgetPlacement::RightOfWorkspaces,
|
|
||||||
widget_right_of_workspaces,
|
|
||||||
),
|
|
||||||
(WidgetPlacement::LeftOfClock, widget_left_of_clock),
|
|
||||||
(WidgetPlacement::RightOfClock, widget_right_of_clock),
|
|
||||||
(WidgetPlacement::LeftOfStats, widget_left_of_stats),
|
|
||||||
(WidgetPlacement::Tray, widget_tray_box),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ── Assemble ─────────────────────────────────────────────────────
|
// ── Assemble ─────────────────────────────────────────────────────
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
|
|
@ -1144,6 +1138,22 @@ impl SimpleComponent for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `widget:<key>` alias `for_each_in_slot` recognizes for each
|
||||||
|
/// `WidgetPlacement` variant — the fallback a `WidgetSpec` routes through
|
||||||
|
/// when no `widget:<module>` container claims its module name specifically.
|
||||||
|
/// Kept in one place since both the builtin manifest's slot lists and
|
||||||
|
/// `reconcile_widgets`' routing below must agree on these names.
|
||||||
|
fn placement_alias(placement: bread_shared::widget::WidgetPlacement) -> &'static str {
|
||||||
|
use bread_shared::widget::WidgetPlacement::*;
|
||||||
|
match placement {
|
||||||
|
Tray => "tray",
|
||||||
|
LeftOfClock => "left_of_clock",
|
||||||
|
RightOfClock => "right_of_clock",
|
||||||
|
RightOfWorkspaces => "right_of_workspaces",
|
||||||
|
LeftOfStats => "left_of_stats",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn reconcile_widgets(&mut self, specs: Vec<bread_shared::widget::WidgetSpec>) {
|
fn reconcile_widgets(&mut self, specs: Vec<bread_shared::widget::WidgetSpec>) {
|
||||||
for container in self.widget_containers.values() {
|
for container in self.widget_containers.values() {
|
||||||
|
|
@ -1152,23 +1162,47 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut by_placement: std::collections::HashMap<
|
// Route each spec to a widget_containers entry: a `widget:<module>`
|
||||||
bread_shared::widget::WidgetPlacement,
|
// slot entry (keyed by WidgetSpec::module) takes priority over the
|
||||||
|
// spec's placement alias, so a theme can retarget one Lua module's
|
||||||
|
// widgets without moving every widget that shares its placement.
|
||||||
|
// A spec whose module AND placement alias both lack a container
|
||||||
|
// (e.g. a theme's slots omit that placement's widget: entry
|
||||||
|
// entirely) is logged and dropped rather than silently vanishing —
|
||||||
|
// WidgetPlacement itself never changes; only which container (if
|
||||||
|
// any) each spec lands in does.
|
||||||
|
let mut by_container: std::collections::HashMap<
|
||||||
|
String,
|
||||||
Vec<&bread_shared::widget::WidgetSpec>,
|
Vec<&bread_shared::widget::WidgetSpec>,
|
||||||
> = std::collections::HashMap::new();
|
> = std::collections::HashMap::new();
|
||||||
for spec in &specs {
|
for spec in &specs {
|
||||||
by_placement.entry(spec.placement).or_default().push(spec);
|
let key = if self.widget_containers.contains_key(&spec.module) {
|
||||||
|
spec.module.clone()
|
||||||
|
} else {
|
||||||
|
placement_alias(spec.placement).to_string()
|
||||||
|
};
|
||||||
|
if self.widget_containers.contains_key(&key) {
|
||||||
|
by_container.entry(key).or_default().push(spec);
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"breadbar: widget '{}' (module '{}', placement {:?}) has no matching \
|
||||||
|
[bar.slots] widget: container — dropping",
|
||||||
|
spec.id, spec.module, spec.placement
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (placement, mut group) in by_placement {
|
let mut has_tray_widgets = false;
|
||||||
let Some(container) = self.widget_containers.get(&placement) else {
|
for (key, mut group) in by_container {
|
||||||
continue;
|
let container = &self.widget_containers[&key];
|
||||||
};
|
|
||||||
group.sort_by_key(|s| s.order);
|
group.sort_by_key(|s| s.order);
|
||||||
for spec in group {
|
for spec in group {
|
||||||
if !spec.visible {
|
if !spec.visible {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if key == "tray" {
|
||||||
|
has_tray_widgets = true;
|
||||||
|
}
|
||||||
let node = widgets::build_node(&spec.root, &spec.id);
|
let node = widgets::build_node(&spec.root, &spec.id);
|
||||||
if let Some(tooltip) = &spec.tooltip {
|
if let Some(tooltip) = &spec.tooltip {
|
||||||
node.set_tooltip_text(Some(tooltip));
|
node.set_tooltip_text(Some(tooltip));
|
||||||
|
|
@ -1177,20 +1211,17 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Tray placement has its own section/separator (handled below,
|
// The "tray" container has its own section/separator (handled
|
||||||
// same as the existing SNI tray items) — an empty inline slot has no
|
// below, same as the existing SNI tray items) — an empty inline
|
||||||
// such wrapper, so it must hide itself to stop contributing to
|
// slot has no such wrapper, so it must hide itself to stop
|
||||||
// center_area's `spacing` gap.
|
// contributing to its parent box's `spacing` gap.
|
||||||
for (placement, container) in &self.widget_containers {
|
for (key, container) in &self.widget_containers {
|
||||||
if *placement == bread_shared::widget::WidgetPlacement::Tray {
|
if key == "tray" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
container.set_visible(container.first_child().is_some());
|
container.set_visible(container.first_child().is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_tray_widgets = specs
|
|
||||||
.iter()
|
|
||||||
.any(|s| s.visible && s.placement == bread_shared::widget::WidgetPlacement::Tray);
|
|
||||||
self.widget_tray_section.set_visible(has_tray_widgets);
|
self.widget_tray_section.set_visible(has_tray_widgets);
|
||||||
self.widget_tray_sep.set_visible(has_tray_widgets);
|
self.widget_tray_sep.set_visible(has_tray_widgets);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue