diff --git a/Cargo.toml b/Cargo.toml index 87e70ba..ad3b08d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] -iced = { version = "0.14", default-features = false, features = ["wgpu", "wayland", "crisp", "web-colors", "thread-pool", "advanced"] } +iced = { version = "0.14", default-features = false, features = ["wgpu", "wayland", "crisp", "web-colors", "thread-pool"] } iced_layershell = { version = "0.19", default-features = false } \ No newline at end of file diff --git a/src/bounds.rs b/src/bounds.rs deleted file mode 100644 index 7894bce..0000000 --- a/src/bounds.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Widget-tree operation that captures the laid-out bounds of a clickable -//! bar module so a popup can be anchored to it. - -use iced::advanced::widget as advanced_widget; -use iced::widget::Id; -use iced::{Rectangle, Task}; - -/// Captures the laid-out bounds of the module with the given id, reported via -/// `BoundsFound`. -pub fn capture_bounds(label: String) -> Task { - let target = Id::from(label.clone()); - - struct FindBounds { - target: Id, - found: Option, - } - - impl advanced_widget::Operation for FindBounds { - fn traverse( - &mut self, - operate: &mut dyn FnMut(&mut dyn advanced_widget::Operation), - ) { - operate(self); - } - - fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { - if self.found.is_none() && id == Some(&self.target) { - self.found = Some(bounds); - } - } - - fn finish(&self) -> advanced_widget::operation::Outcome { - match self.found { - Some(bounds) => advanced_widget::operation::Outcome::Some(bounds), - None => advanced_widget::operation::Outcome::None, - } - } - } - - advanced_widget::operate(FindBounds { - target, - found: None, - }) - .map(move |bounds| crate::Message::BoundsFound(label.clone(), bounds)) -} diff --git a/src/main.rs b/src/main.rs index 7335447..161b63a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,72 +1,34 @@ -//! Wayland layer-shell status bar built on `iced` + `iced_layershell`. +//! 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. -mod bounds; -mod module; -mod modules; - -use iced::widget::{container, text, Row}; -use iced::{window, Alignment, Element, Length, Rectangle, Task, Theme}; -use iced_layershell::daemon; -use iced_layershell::reexport::{Anchor, PopupAnchor, PopupGravity}; -use iced_layershell::settings::{LayerShellSettings, Settings, StartMode}; +use iced::widget::{row, text}; +use iced::{Alignment, Element, Length, Task, Theme}; +use iced_layershell::application; +use iced_layershell::reexport::Anchor; +use iced_layershell::settings::{LayerShellSettings, StartMode, Settings}; use iced_layershell::to_layer_message; -use crate::module::{ModuleMsg, ModuleState, MODULES}; - /// Height of the bar in logical pixels. const BAR_HEIGHT: u32 = 36; -const POPUP_W: u32 = 150; -const POPUP_H: u32 = 100; +/// Optional width of the bar; `None` means "as wide as the output". +const BAR_WIDTH: Option = None; -/// Vertical gap (logical px) between the bar's bottom edge and the popup. -const POPUP_GAP: i32 = 32; - -struct Bar { - /// Which button triggered the open popup? None = closed. Keyed by module label. - active_popup: Option, - /// Surface ID of the currently open popup (for removal). - popup_id: Option, - /// Live module states, in registry order. - modules: Vec>, -} - -impl Default for Bar { - fn default() -> Self { - Self { - active_popup: None, - popup_id: None, - modules: MODULES.iter().map(|m| m.new_state()).collect(), - } - } -} - -#[to_layer_message(multi)] -#[derive(Debug, Clone)] -enum Message { - Noop, - ModuleClicked(String), - BoundsFound(String, Rectangle), - ClosePopup(window::Id), - Module(String, Box), -} - -fn namespace() -> String { - String::from("barbar") -} - -fn main() -> Result<(), iced_layershell::Error> { +pub fn main() -> Result<(), iced_layershell::Error> { + // Target a specific output if one was given, otherwise the active one. let start_mode = match std::env::args().nth(1) { Some(output) => StartMode::TargetScreen(output), None => StartMode::Active, }; - daemon(Bar::default, namespace, update, view) + application(Bar::default, namespace, update, view) .style(style) - .subscription(subscription) .settings(Settings { layer_settings: LayerShellSettings { - size: Some((0, BAR_HEIGHT)), + size: Some((BAR_WIDTH.unwrap_or(0), BAR_HEIGHT)), exclusive_zone: BAR_HEIGHT as i32, anchor: Anchor::Top | Anchor::Left | Anchor::Right, start_mode, @@ -77,149 +39,47 @@ fn main() -> Result<(), iced_layershell::Error> { .run() } -/// Folds module subscriptions into one daemon subscription. -fn subscription(bar: &Bar) -> iced::Subscription { - iced::Subscription::batch(bar.modules.iter().flat_map(|m| m.subscriptions())) +/// Application state. Empty for now; placeholders will evolve into +/// workspace/clock/system modules. +#[derive(Debug, Default)] +struct Bar; + +/// Messages produced by user input, events and subscriptions. +#[to_layer_message] +#[derive(Debug, Clone)] +enum Message { + /// No-op used as a fallback target for pending subscriptions. + Noop, } -fn update(bar: &mut Bar, msg: Message) -> Task { - match msg { - Message::ModuleClicked(label) => { - // Toggle: close if already open, else open. - if bar.active_popup.as_deref() == Some(label.as_str()) { - return maybe_close_popup(bar); - } - - // Query the widget tree for the module's actual laid-out bounds. - // The resulting `BoundsFound` message opens the popup there. - bounds::capture_bounds(label) - } - - Message::BoundsFound(label, bounds) => { - bar.active_popup = Some(label.clone()); - - // Anchor rect = the button's bounds, extended below the bar so the - // `Bottom` anchor point (bottom-center) sits POPUP_GAP below the - // button's bottom edge. With `Bottom` gravity the popup grows - // downward from there, leaving a gap under the bar. - let (x, y, w, h) = ( - bounds.x.round() as i32, - bounds.y.round() as i32, - bounds.width.round() as i32, - bounds.height.round() as i32, - ); - let anchor_rect = (x, y, w, h + POPUP_GAP); - let settings = iced_layershell::actions::IcedNewPopupSettings::on_current_surface( - (POPUP_W, POPUP_H), - anchor_rect, - ) - .anchor(PopupAnchor::Bottom) - .gravity(PopupGravity::Bottom); - - let (id, task) = Message::popup_open(settings); - bar.popup_id = Some(id); - - task - } - - Message::Module(label, msg) => { - let state = bar - .modules - .iter_mut() - .find(|m| m.label() == label) - .expect("unknown module label"); - state.handle(msg) - } - - 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. - Message::AnchorChange { .. } - | Message::SetInputRegion { .. } - | Message::AnchorSizeChange { .. } - | Message::LayerChange { .. } - | Message::MarginChange { .. } - | Message::SizeChange { .. } - | Message::ExclusiveZoneChange { .. } - | Message::KeyboardInteractivityChange { .. } - | Message::NewBaseWindow { .. } - | Message::NewInputPanel { .. } - | Message::NewLayerShell { .. } - | Message::NewMenu { .. } - | Message::NewPopUp { .. } - | Message::RemoveWindow(_) - | Message::ForgetLastOutput - | Message::VirtualKeyboardPressed { .. } => Task::none(), - } +fn namespace() -> String { + String::from("barbar") } -fn maybe_close_popup(bar: &mut Bar) -> Task { - if let Some(id) = bar.popup_id.take() { - bar.active_popup = None; - return Task::done(Message::RemoveWindow(id)); - } +fn update(_bar: &mut Bar, _message: Message) -> Task { Task::none() } -/// Dispatch view by which surface is being rendered. -fn view(bar: &Bar, id: window::Id) -> Element<'_, Message> { - if bar.popup_id == Some(id) { - popup_view(bar) - } else { - bar_view(bar) - } -} - -/// Main bar widget tree. -fn bar_view(bar: &Bar) -> Element<'static, Message> { - let mut children: Vec> = Vec::new(); - - for (i, module) in bar.modules.iter().enumerate() { - children.push(module.button()); - if i < bar.modules.len() - 1 { - children.push(text("|").size(14).into()); - } - } - - container( - Row::from_vec(children) - .width(Length::Fill) - .align_y(Alignment::Center), - ) +fn view(_bar: &Bar) -> Element<'_, Message> { + row![ + text("workspaces").size(14), + text("|").size(14), + text("clock").size(14), + text("|").size(14), + text("system").size(14), + ] + .spacing(12) .padding(8) - .align_x(Alignment::Center) + .align_y(Alignment::Center) + .width(Length::Fill) + .height(Length::Fill) .into() } -/// Popup widget tree. Rendered on its own floating LayerShell surface. -fn popup_view(bar: &Bar) -> Element<'static, Message> { - let label = bar - .active_popup - .as_ref() - .expect("popup open without active module"); - let popup_id = bar.popup_id.expect("popup open without surface id"); - - let module = bar - .modules - .iter() - .find(|m| m.label() == label) - .expect("unknown module label"); - - module.popup(popup_id) -} - fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style { use iced::theme::Style; Style { background_color: theme.palette().background, text_color: theme.palette().text, } -} +} \ No newline at end of file diff --git a/src/module.rs b/src/module.rs deleted file mode 100644 index eb88920..0000000 --- a/src/module.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::any::Any; -use std::fmt::Debug; - -use iced::widget::{container, mouse_area, text, Id}; -use iced::{window, Alignment, Element, Length, Subscription, Task}; - -use crate::Message; - -/// A module's own message type, boxed for transport through [`Message::Module`]. -pub trait ModuleMsg: Any + Debug + Send + 'static { - fn clone_box(&self) -> Box; -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.clone_box() - } -} - -impl ModuleMsg for T { - fn clone_box(&self) -> Box { - Box::new((*self).clone()) - } -} - -/// Downcast a boxed module message to a concrete type. -pub fn downcast(boxed: Box) -> Result, ()> { - let erased: Box = boxed; - match erased.downcast::() { - Ok(t) => Ok(t), - Err(_) => Err(()), - } -} - -/// Static module definition: how to construct the live state. -pub trait ModuleDef: Send + Sync { - fn new_state(&self) -> Box; -} - -/// Live, object-safe module state held by the bar. -pub trait ModuleState: Send + 'static { - /// Stable label used as widget-tree id and message routing key. - fn label(&self) -> &'static str; - - /// Stable widget-tree id used for popup bounds capture. - fn id(&self) -> Id { - Id::from(self.label().to_string()) - } - - /// Render the module's clickable bar segment. - fn button(&self) -> Element<'static, Message> { - container( - mouse_area(text(self.label()).size(14)) - .on_press(Message::ModuleClicked(self.label().to_string())), - ) - .id(self.id()) - .into() - } - - /// Handle a message routed to this module. - fn handle(&mut self, msg: Box) -> Task; - - /// Render the popup body when this module's popup is open. - fn popup(&self, popup_id: window::Id) -> Element<'static, Message> { - let _ = popup_id; - text(format!("{} menu", self.label())) - .size(14) - .width(Length::Fill) - .align_x(Alignment::Center) - .into() - } - - /// Subscriptions this module wants to run. - fn subscriptions(&self) -> Vec> { - Vec::new() - } -} - -/// Registry of all bar modules, in display order. -pub static MODULES: &[&'static dyn ModuleDef] = &[ - &crate::modules::workspaces::Workspaces, - &crate::modules::clock::Clock, - &crate::modules::system::System, -]; diff --git a/src/modules/clock.rs b/src/modules/clock.rs deleted file mode 100644 index 57ede6e..0000000 --- a/src/modules/clock.rs +++ /dev/null @@ -1,50 +0,0 @@ -use iced::widget::{column, mouse_area, text}; -use iced::{window, Alignment, Element, Length}; - -use crate::module::{ModuleDef, ModuleMsg, ModuleState}; -use crate::Message; - -pub struct Clock; - -pub struct State; - -impl ModuleDef for Clock { - fn new_state(&self) -> Box { - Box::new(State) - } -} - -impl ModuleState for State { - fn label(&self) -> &'static str { - "clock" - } - - // ponytail: clock has no internal messages yet; the tick subscription - // that gives it live time is the upgrade path (add `Msg` + `update` - // here when needed). - fn handle(&mut self, _msg: Box) -> iced::Task { - iced::Task::none() - } - - fn popup(&self, popup_id: window::Id) -> Element<'static, Message> { - let items: Vec> = ["Time format", "Date display"] - .into_iter() - .map(|label| { - mouse_area(text(label).size(13)) - .on_press(Message::ClosePopup(popup_id)) - .into() - }) - .collect(); - - column![ - text("clock menu") - .size(14) - .width(Length::Fill) - .align_x(Alignment::Center), - column(items).spacing(1), - mouse_area(text("close").size(12)).on_press(Message::ClosePopup(popup_id)), - ] - .spacing(1) - .into() - } -} diff --git a/src/modules/mod.rs b/src/modules/mod.rs deleted file mode 100644 index 308b27a..0000000 --- a/src/modules/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod clock; -pub mod system; -pub mod workspaces; diff --git a/src/modules/system.rs b/src/modules/system.rs deleted file mode 100644 index bc647bc..0000000 --- a/src/modules/system.rs +++ /dev/null @@ -1,49 +0,0 @@ -use iced::widget::{column, mouse_area, text}; -use iced::{window, Alignment, Element, Length}; - -use crate::module::{ModuleDef, ModuleMsg, ModuleState}; -use crate::Message; - -pub struct System; - -pub struct State; - -impl ModuleDef for System { - fn new_state(&self) -> Box { - Box::new(State) - } -} - -impl ModuleState for State { - fn label(&self) -> &'static str { - "system" - } - - // ponytail: no internal messages yet; real brightness/volume/power - // actions (each with its own `Msg` variant) are the upgrade path. - fn handle(&mut self, _msg: Box) -> iced::Task { - iced::Task::none() - } - - fn popup(&self, popup_id: window::Id) -> Element<'static, Message> { - let items: Vec> = ["Brightness", "Volume", "Power"] - .into_iter() - .map(|label| { - mouse_area(text(label).size(13)) - .on_press(Message::ClosePopup(popup_id)) - .into() - }) - .collect(); - - column![ - text("system menu") - .size(14) - .width(Length::Fill) - .align_x(Alignment::Center), - column(items).spacing(1), - mouse_area(text("close").size(12)).on_press(Message::ClosePopup(popup_id)), - ] - .spacing(1) - .into() - } -} diff --git a/src/modules/workspaces.rs b/src/modules/workspaces.rs deleted file mode 100644 index ce17002..0000000 --- a/src/modules/workspaces.rs +++ /dev/null @@ -1,66 +0,0 @@ -use iced::widget::{column, mouse_area, text, Row}; -use iced::{window, Alignment, Element, Length, Task}; - -use crate::module::{downcast, ModuleDef, ModuleMsg, ModuleState}; -use crate::Message; - -pub struct Workspaces; - -pub struct State { - workspaces: Vec, -} - -#[derive(Debug, Clone)] -pub enum Msg { - Select(String), -} - -impl ModuleDef for Workspaces { - fn new_state(&self) -> Box { - Box::new(State { - workspaces: vec!["1".into(), "2".into(), "3".into(), "4".into()], - }) - } -} - -impl ModuleState for State { - fn label(&self) -> &'static str { - "workspaces" - } - - fn handle(&mut self, msg: Box) -> Task { - let msg = *downcast::(msg).expect("workspaces module received foreign message"); - match msg { - Msg::Select(ws) => { - self.workspaces.sort_by_key(|w| w != &ws); - Task::none() - } - } - } - - fn popup(&self, popup_id: window::Id) -> Element<'static, Message> { - let items: Vec> = self - .workspaces - .iter() - .map(|ws| { - mouse_area(text(ws.clone()).size(13)) - .on_press(Message::Module( - "workspaces".into(), - Box::new(Msg::Select(ws.clone())), - )) - .into() - }) - .collect(); - - column![ - text("workspaces menu") - .size(14) - .width(Length::Fill) - .align_x(Alignment::Center), - Row::from_vec(items).spacing(1), - mouse_area(text("close").size(12)).on_press(Message::ClosePopup(popup_id)), - ] - .spacing(1) - .into() - } -}