From 3573b7bc1f5d47af877c7ac0bd930aff827959ab Mon Sep 17 00:00:00 2001 From: Doloro1978 Date: Mon, 31 Aug 2026 13:22:56 +0100 Subject: [PATCH] slop --- Cargo.toml | 2 +- src/main.rs | 327 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 290 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad3b08d..87e70ba 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"] } +iced = { version = "0.14", default-features = false, features = ["wgpu", "wayland", "crisp", "web-colors", "thread-pool", "advanced"] } iced_layershell = { version = "0.19", default-features = false } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 161b63a..9c904e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,32 +3,102 @@ //! 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 iced::widget::{row, text}; -use iced::{Alignment, Element, Length, Task, Theme}; -use iced_layershell::application; -use iced_layershell::reexport::Anchor; +use iced::advanced::widget as advanced_widget; +use iced::widget::{column, container, mouse_area, text}; +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, StartMode, Settings}; use iced_layershell::to_layer_message; /// Height of the bar in logical pixels. const BAR_HEIGHT: u32 = 36; -/// Optional width of the bar; `None` means "as wide as the output". -const BAR_WIDTH: Option = None; +/// Popup dimensions. +const POPUP_W: u32 = 150; +const POPUP_H: u32 = 100; -pub fn main() -> Result<(), iced_layershell::Error> { - // Target a specific output if one was given, otherwise the active one. +/// Vertical gap (logical px) between the bar's bottom edge and the popup. +const POPUP_GAP: i32 = 32; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +/// Identifies which bar module triggered an action. +#[derive(Debug, Clone, Copy, PartialEq)] +enum Module { + Workspace, + Clock, + System, +} + +impl Module { + const ALL: &'static [Self] = &[Self::Workspace, Self::Clock, Self::System]; + + fn label(&self) -> &'static str { + match self { + Self::Workspace => "workspaces", + Self::Clock => "clock", + Self::System => "system", + } + } + + /// Stable widget-tree id used to look up the laid-out bounds of the + /// module's clickable area. + fn id(&self) -> iced::widget::Id { + iced::widget::Id::new(self.label()) + } +} + +/// Application state. Tracks which popup is open and shared module data. +#[derive(Debug, Default)] +struct Bar { + /// Which button triggered the open popup? None = closed. + active_popup: Option, + /// Surface ID of the currently open popup (for removal). + popup_id: Option, + /// Example workspace list populated when workspace menu opens. + workspaces: Vec, +} + +/// Messages produced by user input, events and subscriptions. +#[to_layer_message(multi)] +#[derive(Debug, Clone)] +enum Message { + Noop, + /// A bar module was clicked; opens or toggles a popup. + ModuleClicked(Module), + /// Internal: the widget-tree layout pass reported the laid-out bounds of + /// a module's clickable area; the popup is anchored there. + BoundsFound(Module, 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, }; - application(Bar::default, namespace, update, view) + daemon(Bar::default, namespace, update, view) .style(style) .settings(Settings { layer_settings: LayerShellSettings { - size: Some((BAR_WIDTH.unwrap_or(0), BAR_HEIGHT)), + size: Some((0, BAR_HEIGHT)), exclusive_zone: BAR_HEIGHT as i32, anchor: Anchor::Top | Anchor::Left | Anchor::Right, start_mode, @@ -39,47 +109,228 @@ pub fn main() -> Result<(), iced_layershell::Error> { .run() } -/// Application state. Empty for now; placeholders will evolve into -/// workspace/clock/system modules. -#[derive(Debug, Default)] -struct Bar; +// --------------------------------------------------------------------------- +// Update +// --------------------------------------------------------------------------- -/// 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(module) => { + // Toggle: close if already open, else open. + if bar.active_popup == Some(module) { + 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. + capture_bounds(module) + } + + Message::BoundsFound(module, bounds) => { + bar.active_popup = Some(module); + + // 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); + + // Seed example data for the workspace menu. + if module == Module::Workspace { + bar.workspaces = vec!["1".into(), "2".into(), "3".into(), "4".into()]; + } + + task + } + + 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 update(_bar: &mut Bar, _message: Message) -> Task { +/// Helper: closes the popup if one is open. +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)); + } Task::none() } -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), +/// Runs a widget-tree [`Operation`] that captures the laid-out bounds of the +/// given module's clickable area, then reports them via `BoundsFound`. +fn capture_bounds(module: Module) -> Task { + let target = module.id(); + + struct FindBounds { + target: iced::widget::Id, + found: Option, + } + + impl advanced_widget::Operation for FindBounds { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn advanced_widget::Operation)) { + // Delegate traversal so widgets can visit their children; the + // container id is matched in `container` above. + operate(self); + } + + fn container(&mut self, id: Option<&iced::widget::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| Message::BoundsFound(module, bounds)) +} + +// --------------------------------------------------------------------------- +// 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. +fn bar_view(_bar: &Bar) -> Element<'static, Message> { + let mut children: Vec> = Vec::new(); + + for (i, module) in Module::ALL.iter().enumerate() { + children.push(button(*module)); + if i < Module::ALL.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() +} + +/// Standalone button wrapped in MouseArea for click capture. The container +/// carries the module's id so a widget-tree [`Operation`] can find its +/// laid-out bounds for popup anchoring. +fn button(module: Module) -> Element<'static, Message> { + container(mouse_area(text(module.label()).size(14)).on_press(Message::ModuleClicked(module))) + .id(module.id()) + .into() +} + +/// Popup widget tree. Rendered on its own floating LayerShell surface. +fn popup_view<'a>(bar: &'a Bar) -> Element<'a, Message> { + let module = bar.active_popup.unwrap(); + let popup_id = bar.popup_id.unwrap(); + let label = module.label(); + + // Build list items based on which module was clicked. + let items: Vec> = match module { + Module::Workspace => bar + .workspaces + .iter() + .map(|ws| popup_item(ws.clone(), popup_id)) + .collect(), + Module::Clock => vec![ + popup_item("Time format", popup_id), + popup_item("Date display", popup_id), + ], + Module::System => vec![ + popup_item("Brightness", popup_id), + popup_item("Volume", popup_id), + popup_item("Power", popup_id), + ], + }; + + column![ + text(format!("{} menu", label)) + .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(12) - .padding(8) - .align_y(Alignment::Center) - .width(Length::Fill) - .height(Length::Fill) + .spacing(1) .into() } +/// Generic clickable item inside a popup. +fn popup_item(label: impl Into, popup_id: window::Id) -> Element<'static, Message> { + mouse_area(text(label.into()).size(13)) + .on_press(Message::ClosePopup(popup_id)) + .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, } -} \ No newline at end of file +}