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`,
|
||||
//! `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
|
||||
//! order instead of a hardcoded sequence.
|
||||
//!
|
||||
//! The Lua-declared `widget_*` containers (`WidgetPlacement`) are NOT part
|
||||
//! of this registry — their fixed interleave (right-of-workspaces,
|
||||
//! left/right-of-clock, left-of-stats) stays exactly as it is today.
|
||||
//! Generalizing their placement is Phase 3b, not this task.
|
||||
//! A slot entry may also 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 (see
|
||||
//! `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 std::collections::HashMap;
|
||||
|
|
@ -32,22 +37,50 @@ impl ModuleRegistry {
|
|||
self.0.insert(name, widget.clone().upcast());
|
||||
}
|
||||
|
||||
/// Appends every module named in `names` (a manifest slot list, in
|
||||
/// theme order) into `container` via `on_widget`, which lets the
|
||||
/// caller interleave fixed Lua widget containers around specific
|
||||
/// modules (e.g. the clock). A name with no registered widget is
|
||||
/// logged and skipped — an unrecognized or unmapped module in a theme
|
||||
/// manifest must never crash the bar.
|
||||
/// Walks every entry named in `names` (a manifest slot list, in theme
|
||||
/// order). A `widget:<key>` entry calls `on_widget(key)`, letting the
|
||||
/// caller create-or-fetch that Lua widget container and append it at
|
||||
/// this exact position. Anything else is looked up as a registered
|
||||
/// module name and passed to `on_module`; a name with no registered
|
||||
/// 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(
|
||||
&self,
|
||||
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 {
|
||||
if let Some(key) = name.strip_prefix("widget:") {
|
||||
on_widget(key);
|
||||
continue;
|
||||
}
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
|
||||
// ── Lua-declared widgets ─────────────────────────────────────────────
|
||||
// One container per WidgetPlacement (see bread_shared::widget), fully
|
||||
// rebuilt on every AppInput::WidgetsUpdate — see widgets::client's
|
||||
// One container per `widget:<key>` slot entry (Phase 3b — see
|
||||
// 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.
|
||||
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_sep: gtk4::Separator,
|
||||
|
||||
|
|
@ -223,9 +229,9 @@ impl SimpleComponent for App {
|
|||
|
||||
// ── Workspace row (left) ────────────────────────────────────────
|
||||
// Built imperatively (not via the view! macro) so a widget
|
||||
// container can sit as a plain sibling of workspace_box — see
|
||||
// WidgetPlacement::RightOfWorkspaces below. The Overlay trail
|
||||
// lives behind the buttons; rebuild_buttons only touches the
|
||||
// container can sit as a plain sibling of workspace_box — see the
|
||||
// `widget:*` slot-entry handling in "Assemble" below. The Overlay
|
||||
// trail lives behind the buttons; rebuild_buttons only touches the
|
||||
// button box, never the trail host.
|
||||
let workspace_trail = bar::workspaces::WorkspaceTrail::new();
|
||||
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.
|
||||
|
||||
// ── Lua-declared widget containers ──────────────────────────────
|
||||
// One per WidgetPlacement; positioned into the layout below as each
|
||||
// surrounding section (workspace row / center area / stats box /
|
||||
// control popover) is built. Populated by widgets::client's
|
||||
// events.subscribe-driven refresh loop, started at the end of init.
|
||||
use bread_shared::widget::WidgetPlacement;
|
||||
let widget_right_of_workspaces = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
||||
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");
|
||||
// Phase 3b: a container per `widget:<key>` slot entry is created
|
||||
// on demand while walking `[bar.slots]` in the "Assemble" section
|
||||
// below (see `bar::slots::widget_slot_container`), so ANY slot can
|
||||
// host a Lua widget — not just the four fixed positions Phase 3a
|
||||
// shipped with. Populated by widgets::client's events.subscribe-
|
||||
// driven refresh loop, started at the end of init.
|
||||
|
||||
// `tokens.icon_px` (plan §4) — bar-chrome icon pixel size; reused
|
||||
// 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.set_valign(gtk4::Align::Center);
|
||||
center_area.set_vexpand(false);
|
||||
// `media_widget`/`clock_box` and the `widget_left_of_clock`/
|
||||
// `widget_right_of_clock` interleave around the clock module are
|
||||
// both appended in "Assemble" below, per `[bar.slots].centre`.
|
||||
// `media_widget`/`clock_box` and any `widget:*` entries interleaved
|
||||
// around them are all appended in "Assemble" below, in the exact
|
||||
// order `[bar.slots].centre` names them.
|
||||
|
||||
// ── Stats box (right side) ───────────────────────────────────────
|
||||
// Demo order: [vol 64] [wifi] [bat 83] [☰]
|
||||
|
|
@ -712,13 +706,16 @@ impl SimpleComponent for App {
|
|||
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
|
||||
// it lands in its container, and which of left/centre/right it
|
||||
// lands in, comes from the theme manifest's `[bar.slots]` now.
|
||||
// The `widget_*` Lua containers keep today's fixed interleave
|
||||
// (right-of-workspaces, left/right-of-clock, left-of-stats) —
|
||||
// generalizing their placement is Phase 3b, not this task.
|
||||
// lands in, comes from the theme manifest's `[bar.slots]` now. A
|
||||
// `widget:<key>` slot entry gets (or creates) a Lua widget
|
||||
// container at that exact position — `<key>` is either a
|
||||
// `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_slots = bar_shell_theme.slots();
|
||||
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("control", &hamburger_btn);
|
||||
|
||||
bar_modules.for_each_in_slot(&bar_slots.left, |_, widget| workspace_row.append(widget));
|
||||
workspace_row.append(&widget_right_of_workspaces);
|
||||
// `tray` never appears in a bar slot — it stays inside the
|
||||
// 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| {
|
||||
if name == "clock" {
|
||||
center_area.append(&widget_left_of_clock);
|
||||
}
|
||||
center_area.append(widget);
|
||||
if name == "clock" {
|
||||
center_area.append(&widget_right_of_clock);
|
||||
}
|
||||
});
|
||||
|
||||
stats_box.append(&widget_left_of_stats);
|
||||
bar_modules.for_each_in_slot(&bar_slots.right, |_, widget| stats_box.append(widget));
|
||||
|
||||
let widget_containers = std::collections::HashMap::from([
|
||||
(
|
||||
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),
|
||||
]);
|
||||
bar_modules.for_each_in_slot(
|
||||
&bar_slots.left,
|
||||
|_, widget| workspace_row.append(widget),
|
||||
|key| workspace_row.append(&bar::slots::widget_slot_container(&mut widget_containers, key)),
|
||||
);
|
||||
bar_modules.for_each_in_slot(
|
||||
&bar_slots.centre,
|
||||
|_, widget| center_area.append(widget),
|
||||
|key| center_area.append(&bar::slots::widget_slot_container(&mut widget_containers, key)),
|
||||
);
|
||||
bar_modules.for_each_in_slot(
|
||||
&bar_slots.right,
|
||||
|_, widget| stats_box.append(widget),
|
||||
|key| stats_box.append(&bar::slots::widget_slot_container(&mut widget_containers, key)),
|
||||
);
|
||||
|
||||
// ── Assemble ─────────────────────────────────────────────────────
|
||||
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 {
|
||||
fn reconcile_widgets(&mut self, specs: Vec<bread_shared::widget::WidgetSpec>) {
|
||||
for container in self.widget_containers.values() {
|
||||
|
|
@ -1152,23 +1162,47 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
let mut by_placement: std::collections::HashMap<
|
||||
bread_shared::widget::WidgetPlacement,
|
||||
// Route each spec to a widget_containers entry: a `widget:<module>`
|
||||
// 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>,
|
||||
> = std::collections::HashMap::new();
|
||||
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 Some(container) = self.widget_containers.get(&placement) else {
|
||||
continue;
|
||||
};
|
||||
let mut has_tray_widgets = false;
|
||||
for (key, mut group) in by_container {
|
||||
let container = &self.widget_containers[&key];
|
||||
group.sort_by_key(|s| s.order);
|
||||
for spec in group {
|
||||
if !spec.visible {
|
||||
continue;
|
||||
}
|
||||
if key == "tray" {
|
||||
has_tray_widgets = true;
|
||||
}
|
||||
let node = widgets::build_node(&spec.root, &spec.id);
|
||||
if let Some(tooltip) = &spec.tooltip {
|
||||
node.set_tooltip_text(Some(tooltip));
|
||||
|
|
@ -1177,20 +1211,17 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
// The Tray placement has its own section/separator (handled below,
|
||||
// same as the existing SNI tray items) — an empty inline slot has no
|
||||
// such wrapper, so it must hide itself to stop contributing to
|
||||
// center_area's `spacing` gap.
|
||||
for (placement, container) in &self.widget_containers {
|
||||
if *placement == bread_shared::widget::WidgetPlacement::Tray {
|
||||
// The "tray" container has its own section/separator (handled
|
||||
// below, same as the existing SNI tray items) — an empty inline
|
||||
// slot has no such wrapper, so it must hide itself to stop
|
||||
// contributing to its parent box's `spacing` gap.
|
||||
for (key, container) in &self.widget_containers {
|
||||
if key == "tray" {
|
||||
continue;
|
||||
}
|
||||
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_sep.set_visible(has_tray_widgets);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue