popup fixes and clock gets nicer popup

This commit is contained in:
2026-09-15 11:46:16 +01:00
parent fb6b03b369
commit ac70a9133a
7 changed files with 87 additions and 63 deletions
+7
View File
@@ -0,0 +1,7 @@
# Link with mold from the repo itself, so `cargo build`/`cargo run` do not
# depend on a machine-local ~/.cargo/config.toml. -fuse-ld is understood by
# both the clang and gcc drivers, so this works with whatever `linker` the
# caller has configured (mold is in the devShell PATH; the nix build gets it
# via flake.nix).
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
+10 -2
View File
@@ -2,16 +2,24 @@ use iced::{window, Rectangle};
use crate::Wire;
#[derive(Clone, Debug)]
pub struct PopupSettings {
// pub name: String,
pub module_id: String,
pub element_id: String,
pub gap: i32,
}
/// What a module asks the app to do — the single effect vocabulary shared by
/// modules and app, so mapping module output into a `Message` is a plain
/// `.map(Message::Effect)` with no mirror enum.
#[derive(Debug, Clone)]
pub enum ModuleEffect {
/// Toggle a popup for the given module id (element id anchors it).
RequestPopup(String, String),
RequestPopup(PopupSettings),
/// A widget-tree pass reported a module's laid-out bounds; anchor the
/// popup there. App-internal, but one effect type keeps routing trivial.
BoundsFound(String, Rectangle),
BoundsFound(Rectangle, PopupSettings),
/// Request removal of the popup surface.
ClosePopup(window::Id),
/// Route a wire to its target (a module, a service inbox, or a topic).
+1 -1
View File
@@ -15,7 +15,7 @@ mod wire;
pub use config::{BarbarConfig, Modules};
pub use display::Display;
pub use effect::ModuleEffect;
pub use effect::{ModuleEffect, PopupSettings};
pub use messages::*;
pub use module::BarModule;
pub use reusable_elements::pill;
+36 -17
View File
@@ -5,12 +5,12 @@
//! then opens the popup there; the active module renders its content.
use iced::advanced::widget as advanced_widget;
use iced::widget::{column, mouse_area, text};
use iced::{Alignment, Element, Length, Rectangle, Task};
use iced::widget::{column, container, mouse_area, text};
use iced::{Alignment, Border, Element, Length, Padding, Rectangle, Task};
use iced_layershell::actions::IcedNewPopupSettings;
use iced_layershell::reexport::{PopupAnchor, PopupGravity};
use common::ModuleEffect;
use common::{pill, ModuleEffect, PopupSettings};
use crate::msg::popup_open;
use crate::{Bar, Message, Msg};
@@ -19,17 +19,17 @@ use crate::{Bar, Message, Msg};
const POPUP_W: u32 = 150;
const POPUP_H: u32 = 100;
/// Gap (logical px) between the bar's bottom edge and the popup.
const POPUP_GAP: i32 = 32;
// Gap (logical px) between the bar's bottom edge and the popup.
// const POPUP_GAP: i32 = 32;
/// Opens a popup for `module_id` below its laid-out `bounds`. Size comes
/// from the module's `popup_size()` (default: small menu).
pub fn open_popup(bar: &mut Bar, module_id: String, bounds: Rectangle) -> Task<Message> {
bar.active_popup = Some(module_id.clone());
pub fn open_popup(bar: &mut Bar, bounds: Rectangle, settings: PopupSettings) -> Task<Message> {
bar.active_popup = Some(settings.module_id.clone());
let (w, h) = bar
.modules
.get(module_id.as_str())
.get(settings.module_id.as_str())
.and_then(|m| m.popup_size())
.unwrap_or((POPUP_W, POPUP_H));
@@ -41,14 +41,14 @@ pub fn open_popup(bar: &mut Bar, module_id: String, bounds: Rectangle) -> Task<M
bounds.width.round() as i32,
bounds.height.round() as i32,
);
let anchor_rect = (bx, by, bw, bh + POPUP_GAP);
let settings = IcedNewPopupSettings::on_current_surface((w, h), anchor_rect)
let anchor_rect = (bx, by, bw, bh + settings.gap);
let settings_popup = IcedNewPopupSettings::on_current_surface((w, h), anchor_rect)
.anchor(PopupAnchor::Bottom)
.gravity(PopupGravity::Bottom);
let (id, task) = popup_open(settings);
let (id, task) = popup_open(settings_popup);
bar.popup_id = Some(id);
tracing::debug!(module = %module_id, ?id, size = ?(w, h), "popup opened");
tracing::debug!(module = %settings.module_id, ?id, size = ?(w, h), "popup opened");
task
}
@@ -64,8 +64,8 @@ pub fn close_popup(bar: &mut Bar) -> Task<Message> {
/// Runs a widget-tree [`Operation`] that captures the laid-out bounds of the
/// given element, then reports them via `BoundsFound` for the module.
pub fn capture_bounds(module_id: String, element_id: String) -> Task<Message> {
let target = iced::widget::Id::from(element_id);
pub fn capture_bounds(settings: PopupSettings) -> Task<Message> {
let target = iced::widget::Id::from(settings.element_id.clone());
struct FindBounds {
target: iced::widget::Id,
@@ -98,7 +98,9 @@ pub fn capture_bounds(module_id: String, element_id: String) -> Task<Message> {
target,
found: None,
})
.map(move |bounds| Message::Effect(ModuleEffect::BoundsFound(module_id.clone(), bounds)))
.map(move |bounds| -> Message {
Message::Effect(ModuleEffect::BoundsFound(bounds, settings.clone()))
})
}
/// Popup widget tree on its own LayerShell surface; the active module
@@ -116,15 +118,32 @@ pub fn view(bar: &Bar) -> Element<'_, Message> {
})
.unwrap_or_else(|| text("unknown module").into());
container(
column![
text(format!("{} menu", module_id))
.size(14)
.width(Length::Fill)
.align_x(Alignment::Center),
content,
mouse_area(text("close").size(12))
mouse_area(container(text("close").size(12)).style(pill))
.on_press(Message::Effect(ModuleEffect::ClosePopup(popup_id))),
]
.spacing(1)
.spacing(1),
)
.padding(Padding::from(4))
.style(|theme| container::Style {
text_color: None,
background: Some(
iced::Background::Color(theme.extended_palette().background.base.color)
.scale_alpha(0.4),
),
border: Border {
width: 3.0,
radius: 10.into(),
color: theme.extended_palette().secondary.strong.color,
},
snap: true,
..Default::default()
})
.into()
}
+6 -6
View File
@@ -78,18 +78,18 @@ fn route(bar: &mut Bar, wire: Wire) -> Task<Message> {
/// Runs an effect; may set up bar state for it (e.g. which popup is open).
fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task<Message> {
match effect {
ModuleEffect::RequestPopup(module_id, element_id) => {
ModuleEffect::RequestPopup(settings) => {
// Toggle: close if already open for this module, else open.
if bar.active_popup.as_deref() == Some(module_id.as_str()) {
tracing::debug!(module = %module_id, "closing popup");
if bar.active_popup.as_deref() == Some(settings.module_id.as_str()) {
tracing::debug!(module = settings.module_id, "closing popup");
popup::close_popup(bar)
} else {
tracing::debug!(module = %module_id, "opening popup");
popup::capture_bounds(module_id, element_id)
tracing::debug!(module = settings.module_id, "opening popup");
popup::capture_bounds(settings)
}
}
ModuleEffect::BoundsFound(module_id, bounds) => popup::open_popup(bar, module_id, bounds),
ModuleEffect::BoundsFound(bounds, settings) => popup::open_popup(bar, bounds, settings),
ModuleEffect::ClosePopup(id) => {
// Request removal only; keep popup state so the popup content
+16 -21
View File
@@ -3,10 +3,12 @@
//! service to switch which kind it publishes; right-click opens a popup.
use iced::widget::{container, mouse_area, text};
use iced::{Element, Padding, Task};
use iced::Length::Fill;
use iced::{alignment, Element, Padding, Task};
use common::{
pill, BarModule, ClockKind, ClockMsg, ClockPayload, Endpoint, ModuleEffect, Service, Wire,
pill, BarModule, ClockKind, ClockMsg, ClockPayload, Endpoint, ModuleEffect, PopupSettings,
Service, Wire,
};
use serde::Deserialize;
use toml::Table;
@@ -82,7 +84,12 @@ impl BarModule for Clock {
let me = Endpoint::module(self.id());
match window_id {
// Popup surface: current time in large text.
Some(_) => container(text(&self.value).size(56)).padding(20).into(),
Some(_) => container(text(&self.value).size(56))
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center)
.padding(20)
.width(Fill)
.into(),
// Bar surface: clickable time. Container carries the module id
// so the popup can anchor to these bounds.
None => container(
@@ -125,10 +132,12 @@ impl BarModule for Clock {
want,
)))
}
Some(ClockMsg::OpenPopup) => Task::done(ModuleEffect::RequestPopup(
self.id().to_string(),
self.id().to_string(),
)),
Some(ClockMsg::OpenPopup) => Task::done(ModuleEffect::RequestPopup(PopupSettings {
// name: "barbar".into(),
module_id: self.id().into(),
element_id: "clock".into(),
gap: 8,
})),
None => Task::none(),
}
}
@@ -141,17 +150,3 @@ impl BarModule for Clock {
Some((POPUP_W, POPUP_H))
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Regression: `new(None)` used to call `default()`, which called
/// `new(None)` — infinite recursion (hard stack overflow) whenever the
/// clock config was absent or failed to parse.
#[test]
fn no_config_terminates() {
assert!(!Clock::new(None).show_seconds);
assert!(!Clock::default().show_seconds);
}
}
+3 -8
View File
@@ -79,17 +79,12 @@
# from it, and loads libspa-support from there at runtime.
pkgs.pipewire.dev
]
# mold for the final link; the flag itself comes from the
# repo-wide .cargo/config.toml so dev and nix builds share one.
++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
pkgs.mold
];
# Link with mold (fast linker). Lives in commonArgs so deps and the
# final crate share one flag set; note this rebuilds all dependency
# crates once when first added.
env = pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
RUSTFLAGS = "-C link-arg=-fuse-ld=mold";
};
buildInputs =
guiLibs
++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isDarwin [
@@ -144,7 +139,7 @@
# Put the GUI shared libraries on the runtime library path so winit
# can dlopen libwayland and wgpu can find the vulkan loader.
# No RUSTFLAGS here: an env RUSTFLAGS would override (not merge
# with) ~/.cargo/config.toml's rustflags, giving devShell and
# with) .cargo/config.toml's rustflags, giving devShell and
# non-devShell builds different fingerprints and force-rebuilding
# every dependency on each switch.
shellHook = ''