bar: assemble modules from [bar.slots] instead of a fixed source order

Adds ModuleRegistry (src/bar/slots.rs), mapping each [bar.slots] module
name (workspaces/media/clock/volume/wifi/battery/control) to its
already-built widget. main.rs now registers the seven modules once
they're constructed, then walks ShellTheme::slots() to append them
into the left/centre/right containers in theme order instead of a
hardcoded sequence. An unknown module name in a theme manifest is
logged and skipped rather than panicking.

The Lua-declared widget_* containers keep their fixed interleave
(right-of-workspaces, left/right-of-clock, left-of-stats) — that's
Phase 3b, not this change.
This commit is contained in:
Breadway 2026-08-24 17:40:05 +08:00
parent bbb1a6c8d2
commit c5f7dd1ee3
3 changed files with 104 additions and 11 deletions

View file

@ -2,6 +2,7 @@ pub mod bluetooth;
pub mod clock; pub mod clock;
pub mod control; pub mod control;
pub mod media; pub mod media;
pub mod slots;
pub mod stats; pub mod stats;
pub mod tray; pub mod tray;
pub mod wifi; pub mod wifi;

53
src/bar/slots.rs Normal file
View file

@ -0,0 +1,53 @@
//! Module registry for the theme manifest's `[bar.slots]` (plan Phase 3a).
//!
//! Each bar module (`workspaces`, `media`, `clock`, `volume`, `wifi`,
//! `battery`, `control`, …) is still built exactly where it always was in
//! `main.rs` — this registry only decouples the ORDER in which those
//! already-constructed widgets get appended into the left/centre/right
//! containers from the fixed source-code order they were built in. main.rs
//! registers each widget by its manifest module name once construction is
//! 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.
use gtk4::prelude::*;
use std::collections::HashMap;
/// Maps a `[bar.slots]` module name to its already-built widget.
#[derive(Default)]
pub struct ModuleRegistry(HashMap<&'static str, gtk4::Widget>);
impl ModuleRegistry {
pub fn new() -> Self {
Self::default()
}
/// Registers `widget` under `name` (a `[bar.slots]` module name, e.g.
/// `"workspaces"` or `"clock"`).
pub fn register(&mut self, name: &'static str, widget: &impl IsA<gtk4::Widget>) {
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.
pub fn for_each_in_slot(
&self,
names: &[String],
mut on_widget: impl FnMut(&str, &gtk4::Widget),
) {
for name in names {
match self.0.get(name.as_str()) {
Some(widget) => on_widget(name, widget),
None => eprintln!("breadbar: [bar.slots] names unknown module '{name}' — skipping"),
}
}
}
}

View file

@ -233,7 +233,8 @@ impl SimpleComponent for App {
workspace_row.set_margin_start(8); workspace_row.set_margin_start(8);
workspace_row.set_valign(gtk4::Align::Center); workspace_row.set_valign(gtk4::Align::Center);
workspace_row.set_vexpand(false); workspace_row.set_vexpand(false);
workspace_row.append(&workspace_trail.overlay); // `workspace_trail.overlay` is appended in the "Assemble" section
// 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 // One per WidgetPlacement; positioned into the layout below as each
@ -243,7 +244,9 @@ impl SimpleComponent for App {
use bread_shared::widget::WidgetPlacement; use bread_shared::widget::WidgetPlacement;
let widget_right_of_workspaces = gtk4::Box::new(gtk4::Orientation::Horizontal, 6); let widget_right_of_workspaces = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
widget_right_of_workspaces.add_css_class("bread-widget-slot"); widget_right_of_workspaces.add_css_class("bread-widget-slot");
workspace_row.append(&widget_right_of_workspaces); // 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); let widget_left_of_clock = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
widget_left_of_clock.add_css_class("bread-widget-slot"); widget_left_of_clock.add_css_class("bread-widget-slot");
@ -396,10 +399,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);
center_area.append(&media_widget); // `media_widget`/`clock_box` and the `widget_left_of_clock`/
center_area.append(&widget_left_of_clock); // `widget_right_of_clock` interleave around the clock module are
center_area.append(&clock_box); // both appended in "Assemble" below, per `[bar.slots].centre`.
center_area.append(&widget_right_of_clock);
// ── Stats box (right side) ─────────────────────────────────────── // ── Stats box (right side) ───────────────────────────────────────
// Demo order: [vol 64] [wifi] [bat 83] [☰] // Demo order: [vol 64] [wifi] [bat 83] [☰]
@ -408,7 +410,9 @@ impl SimpleComponent for App {
stats_box.set_margin_end(2); stats_box.set_margin_end(2);
stats_box.set_valign(gtk4::Align::Center); stats_box.set_valign(gtk4::Align::Center);
stats_box.set_vexpand(false); stats_box.set_vexpand(false);
stats_box.append(&widget_left_of_stats); // Also appended in "Assemble" — before the right slot's modules,
// preserving its fixed position (left of the stats modules)
// whatever `[bar.slots].right` contains.
// CPU/RAM/power draw stay built (control panel + screenshots still // CPU/RAM/power draw stay built (control panel + screenshots still
// read the labels) but never mount on the island — the demo bar // read the labels) but never mount on the island — the demo bar
@ -442,7 +446,7 @@ impl SimpleComponent for App {
vol_lbl.add_css_class("stat-label"); vol_lbl.add_css_class("stat-label");
vol_box.append(&vol_img); vol_box.append(&vol_img);
vol_box.append(&vol_lbl); vol_box.append(&vol_lbl);
stats_box.append(&vol_box); // Appended in "Assemble" below, per `[bar.slots].right`.
let bat_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); let bat_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
bat_box.add_css_class("stat-pair"); bat_box.add_css_class("stat-pair");
@ -530,8 +534,8 @@ impl SimpleComponent for App {
wifi_img.set_halign(gtk4::Align::Center); wifi_img.set_halign(gtk4::Align::Center);
wifi_img.set_hexpand(false); wifi_img.set_hexpand(false);
connectivity_pair.append(&wifi_img); connectivity_pair.append(&wifi_img);
stats_box.append(&connectivity_pair); // `connectivity_pair` and `bat_box` are appended in "Assemble"
stats_box.append(&bat_box); // below, per `[bar.slots].right`.
// ── Control panel popover ──────────────────────────────────────── // ── Control panel popover ────────────────────────────────────────
// Liquid Motion chrome: CONTROL / vol / bl / lock·sleep·off. // Liquid Motion chrome: CONTROL / vol / bl / lock·sleep·off.
@ -652,7 +656,8 @@ impl SimpleComponent for App {
bar::control::spawn_set_brightness(s.value()); bar::control::spawn_set_brightness(s.value());
}); });
stats_box.append(&hamburger_btn); // `hamburger_btn` is appended in "Assemble" below, per
// `[bar.slots].right`.
// Standalone layer windows — below the island, slid in by Hyprland. // Standalone layer windows — below the island, slid in by Hyprland.
let panels = panel::PanelSet::new( let panels = panel::PanelSet::new(
@ -707,6 +712,40 @@ impl SimpleComponent for App {
media_widget.add_controller(mgesture); media_widget.add_controller(mgesture);
} }
// ── Assemble: slot-driven module order (plan §11 Phase 3a) ───────
// 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.
let bar_shell_theme = theme::shell_theme();
let bar_slots = bar_shell_theme.slots();
let mut bar_modules = bar::slots::ModuleRegistry::new();
bar_modules.register("workspaces", &workspace_trail.overlay);
bar_modules.register("media", &media_widget);
bar_modules.register("clock", &clock_box);
bar_modules.register("volume", &vol_box);
bar_modules.register("wifi", &connectivity_pair);
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);
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([ let widget_containers = std::collections::HashMap::from([
( (
WidgetPlacement::RightOfWorkspaces, WidgetPlacement::RightOfWorkspaces,