//! barbar — a Wayland layer-shell status bar built on `iced` + `iced_layershell`. //! //! Renders a full-width bar pinned to the top of the screen via the //! `wlr-layer-shell` protocol. Optionally target a specific output by //! passing its name as the first CLI argument. //! //! Clicking any button spawns a separate LayerShell popup surface anchored //! to that button's position on the bar. use std::collections::BTreeMap; use iced::widget::{column, container, text}; use iced::{window, Alignment, Element, Length, Rectangle, Subscription, Task, Theme}; use iced_layershell::daemon; use iced_layershell::reexport::Anchor; use iced_layershell::settings::{LayerShellSettings, Settings, StartMode}; use iced_layershell::to_layer_message; use iced::futures::sink::SinkExt; use crate::module::{BarModule, ModuleMsg}; use crate::subscriptions::clock::ClockTicker; use crate::subscriptions::Subscription as ModuleSub; use crate::subscriptions::SubscriptionPayloadKind; mod module; mod popup; mod subscriptions; /// Height of the bar in logical pixels. const BAR_HEIGHT: u32 = 36; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /// Application state. Tracks which popup is open and owns the module registry. struct Bar { /// Which module's button triggered the open popup? None = closed. active_popup: Option, /// Module registry keyed by stable module id. modules: BTreeMap<&'static str, Box>, /// Surface ID of the currently open popup (for removal). popup_id: Option, /// Fan-out routing table: module id -> the subscription kinds it wants. /// Built once at boot from `BarModule::subscriptions()`. routes: BTreeMap<&'static str, Vec>, } impl Bar { /// Builds the module registry. The daemon calls this once at boot. fn new() -> Self { let mut modules: BTreeMap<&'static str, Box> = BTreeMap::new(); modules.insert("clock", Box::new(crate::module::clock::Clock::new())); let routes = modules .iter() .map(|(id, module)| (*id, module.subscriptions())) .collect(); Self { active_popup: None, modules, popup_id: None, routes, } } } /// Messages produced by user input, events and subscriptions. #[to_layer_message(multi)] #[derive(Debug, Clone)] pub(crate) enum Message { Noop, /// A routed module message; dispatch by `id` without naming its type. Module(ModuleMsg), /// A subscription event, pre-fanned by kind; `update` routes it. Subscription(SubscriptionPayloadKind), /// A bar module was clicked; requests a popup for that module. /// Carries the module to run `view` from, and the element to stick to. RequestPopup(String, String), /// Internal: the widget-tree layout pass reported the laid-out bounds of /// a module's clickable area; the popup is anchored there. BoundsFound(String, Rectangle), /// Closes the currently open popup. ClosePopup(window::Id), } // --------------------------------------------------------------------------- // Application wiring // --------------------------------------------------------------------------- fn namespace() -> String { String::from("barbar") } fn main() -> Result<(), iced_layershell::Error> { let start_mode = match std::env::args().nth(1) { Some(output) => StartMode::TargetScreen(output), None => StartMode::Active, }; daemon(Bar::new, namespace, update, view) .style(style) .subscription(gather_subscriptions) .settings(Settings { layer_settings: LayerShellSettings { size: Some((0, BAR_HEIGHT)), exclusive_zone: BAR_HEIGHT as i32, anchor: Anchor::Top | Anchor::Left | Anchor::Right, start_mode, ..Default::default() }, ..Default::default() }) .run() } fn gather_subscriptions(bar: &Bar) -> Subscription { //TODO: Get a vec of subscription enum 'bar_wants' and spawn a batch depending on what the //modules want. let wants_clock = bar .routes .values() .any(|kinds| kinds.iter().any(|k| matches!(k, ModuleSub::Clock(_)))); if !wants_clock { return Subscription::none(); } Subscription::run_with((), |_| { iced::stream::channel( 0, |mut sender: iced::futures::channel::mpsc::Sender| async move { let mut clock = ClockTicker::new(); let mut interval = tokio::time::interval(std::time::Duration::from_secs(1)); loop { interval.tick().await; for payload in clock.tick() { if sender .send(Message::Subscription(SubscriptionPayloadKind::Clock( payload, ))) .await .is_err() { return; } } } }, ) }) } // --------------------------------------------------------------------------- // Update // --------------------------------------------------------------------------- fn update(bar: &mut Bar, msg: Message) -> Task { match msg { Message::Module(m) => match bar.modules.get_mut(m.id) { // The module produces its own (app-level) tasks, e.g. opening a // popup for itself — nothing to map here. Some(module) => module.update(m), None => Task::none(), }, Message::Subscription(kind) => match kind { // Fan out by kind to every module whose route requests it. The // payload kind is typed, so no downcast needed. SubscriptionPayloadKind::Clock(payload) => bar .modules .iter_mut() .filter(|(id, _)| { bar.routes .get(*id) .is_some_and(|kinds| kinds.contains(&ModuleSub::Clock(payload.kind))) }) .map(|(_, module)| module.update(ModuleMsg::new(module.id(), payload.clone()))) .fold(Task::none(), |acc, t| acc.chain(t)), }, Message::RequestPopup(module_id, element_id) => { // Toggle: close if already open, else open. if bar.active_popup.as_deref() == Some(module_id.as_str()) { popup::close_popup(bar) } else { // Query the widget tree for the element's laid-out bounds; // the resulting `BoundsFound` opens the popup there. popup::capture_bounds(module_id, element_id) } } Message::BoundsFound(module_id, bounds) => popup::open_popup(bar, module_id, bounds), Message::ClosePopup(id) => { if bar.popup_id == Some(id) { bar.popup_id = None; bar.active_popup = None; } Task::done(Message::RemoveWindow(id)) } Message::Noop => Task::none(), // Forward multi-window mutations to their internal handlers. _ => Task::none(), } } // --------------------------------------------------------------------------- // View — routed per-surface by window ID // --------------------------------------------------------------------------- /// Dispatch view by which surface is being rendered. fn view(bar: &Bar, id: window::Id) -> Element<'_, Message> { // The bar is always created first. Any subsequently opened popup has a // different IcedId we can distinguish. We compare against the bar's // known ID (stored implicitly as "first window"). if bar.popup_id == Some(id) { // This is the popup surface. popup::view(bar) } else { // This is the main bar surface. bar_view(bar) } } /// Main bar widget tree. Each module renders itself; the app only lays the /// modules out in a row. Popup requests are the module's own business: its /// `update` returns the task that asks the app for a popup. fn bar_view(bar: &Bar) -> Element<'_, Message> { let mut children: Vec> = Vec::new(); for (i, (_, module)) in bar.modules.iter().enumerate() { children.push(module.view(None).map(Message::Module)); if i < bar.modules.len() - 1 { children.push(text("|").size(14).into()); } } container( column(children) .width(Length::Fill) .align_x(Alignment::Center), ) .padding(8) .align_x(Alignment::Center) .into() } // --------------------------------------------------------------------------- // Style // --------------------------------------------------------------------------- fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style { use iced::theme::Style; Style { background_color: theme.palette().background, text_color: theme.palette().text, } }