popup fixes and clock gets nicer popup
This commit is contained in:
@@ -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"]
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
+44
-25
@@ -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());
|
||||
|
||||
column![
|
||||
text(format!("{} menu", module_id))
|
||||
.size(14)
|
||||
.width(Length::Fill)
|
||||
.align_x(Alignment::Center),
|
||||
content,
|
||||
mouse_area(text("close").size(12))
|
||||
.on_press(Message::Effect(ModuleEffect::ClosePopup(popup_id))),
|
||||
]
|
||||
.spacing(1)
|
||||
container(
|
||||
column![
|
||||
text(format!("{} menu", module_id))
|
||||
.size(14)
|
||||
.width(Length::Fill)
|
||||
.align_x(Alignment::Center),
|
||||
content,
|
||||
mouse_area(container(text("close").size(12)).style(pill))
|
||||
.on_press(Message::Effect(ModuleEffect::ClosePopup(popup_id))),
|
||||
]
|
||||
.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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
Reference in New Issue
Block a user