From 47487b70621e2538bb8ea69db229e466b44dd9f1 Mon Sep 17 00:00:00 2001 From: drendog Date: Sun, 2 Nov 2025 05:21:26 +0100 Subject: [PATCH] feat: add popup positioning mode --- adapters/src/lib.rs | 4 +- .../rendering/slint_integration/platform.rs | 94 +++++++------------ adapters/src/wayland/shell_adapter.rs | 41 +++++++- .../src/wayland/surfaces/popup_manager.rs | 72 +++++++------- .../src/wayland/surfaces/popup_surface.rs | 42 +++++---- composition/src/lib.rs | 6 +- domain/src/value_objects/mod.rs | 2 + domain/src/value_objects/popup_config.rs | 72 ++++++++++++++ .../value_objects/popup_positioning_mode.rs | 40 ++++++++ 9 files changed, 251 insertions(+), 122 deletions(-) create mode 100644 domain/src/value_objects/popup_config.rs create mode 100644 domain/src/value_objects/popup_positioning_mode.rs diff --git a/adapters/src/lib.rs b/adapters/src/lib.rs index e1f2f7a..7625487 100644 --- a/adapters/src/lib.rs +++ b/adapters/src/lib.rs @@ -6,9 +6,7 @@ pub mod wayland; pub use rendering::femtovg::popup_window::PopupWindow; pub use rendering::slint_integration::platform::{ - clear_popup_position_override, close_current_popup, get_popup_position_override, - set_popup_position_override, clear_popup_size_override, get_popup_size_override, - set_popup_size_override, + clear_popup_config, close_current_popup, get_popup_config, set_popup_config, }; pub mod platform { diff --git a/adapters/src/rendering/slint_integration/platform.rs b/adapters/src/rendering/slint_integration/platform.rs index 8ab4abe..97d5b7a 100644 --- a/adapters/src/rendering/slint_integration/platform.rs +++ b/adapters/src/rendering/slint_integration/platform.rs @@ -1,3 +1,4 @@ +use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; use slint::{ PlatformError, platform::{Platform, WindowAdapter}, @@ -9,6 +10,7 @@ use crate::rendering::femtovg::main_window::FemtoVGWindow; use crate::rendering::femtovg::popup_window::PopupWindow; type PopupCreator = dyn Fn() -> Result, PlatformError>; +type PopupConfigData = (f32, f32, f32, f32, PopupPositioningMode); thread_local! { static CURRENT_PLATFORM: RefCell>> = const { RefCell::new(None) }; @@ -24,61 +26,43 @@ pub fn close_current_popup() { }); } -pub fn set_popup_position_override(x: f32, y: f32) { +pub fn set_popup_config( + reference_x: f32, + reference_y: f32, + width: f32, + height: f32, + positioning_mode: PopupPositioningMode, +) { CURRENT_PLATFORM.with(|platform| { if let Some(weak_platform) = platform.borrow().as_ref() { if let Some(strong_platform) = weak_platform.upgrade() { - strong_platform.set_popup_position(x, y); + strong_platform.set_popup_config( + reference_x, + reference_y, + width, + height, + positioning_mode, + ); } } }); } -pub fn get_popup_position_override() -> Option<(f32, f32)> { +pub fn get_popup_config() -> Option { CURRENT_PLATFORM.with(|platform| { platform .borrow() .as_ref() .and_then(Weak::upgrade) - .and_then(|strong| strong.get_popup_position()) + .and_then(|strong| strong.get_popup_config()) }) } -pub fn clear_popup_position_override() { +pub fn clear_popup_config() { CURRENT_PLATFORM.with(|platform| { if let Some(weak_platform) = platform.borrow().as_ref() { if let Some(strong_platform) = weak_platform.upgrade() { - strong_platform.clear_popup_position(); - } - } - }); -} - -pub fn set_popup_size_override(width: f32, height: f32) { - CURRENT_PLATFORM.with(|platform| { - if let Some(weak_platform) = platform.borrow().as_ref() { - if let Some(strong_platform) = weak_platform.upgrade() { - strong_platform.set_popup_size(width, height); - } - } - }); -} - -pub fn get_popup_size_override() -> Option<(f32, f32)> { - CURRENT_PLATFORM.with(|platform| { - platform - .borrow() - .as_ref() - .and_then(Weak::upgrade) - .and_then(|strong| strong.get_popup_size()) - }) -} - -pub fn clear_popup_size_override() { - CURRENT_PLATFORM.with(|platform| { - if let Some(weak_platform) = platform.borrow().as_ref() { - if let Some(strong_platform) = weak_platform.upgrade() { - strong_platform.clear_popup_size(); + strong_platform.clear_popup_config(); } } }); @@ -89,8 +73,7 @@ pub struct CustomSlintPlatform { popup_creator: RefCell>>, first_call: Cell, last_popup: RefCell>>, - popup_position: RefCell>, - popup_size: RefCell>, + popup_config: RefCell>, } impl CustomSlintPlatform { @@ -101,8 +84,7 @@ impl CustomSlintPlatform { popup_creator: RefCell::new(None), first_call: Cell::new(true), last_popup: RefCell::new(None), - popup_position: RefCell::new(None), - popup_size: RefCell::new(None), + popup_config: RefCell::new(None), }); CURRENT_PLATFORM.with(|current| { @@ -133,29 +115,25 @@ impl CustomSlintPlatform { *self.last_popup.borrow_mut() = None; } - pub fn set_popup_position(&self, x: f32, y: f32) { - *self.popup_position.borrow_mut() = Some((x, y)); + pub fn set_popup_config( + &self, + reference_x: f32, + reference_y: f32, + width: f32, + height: f32, + positioning_mode: PopupPositioningMode, + ) { + *self.popup_config.borrow_mut() = + Some((reference_x, reference_y, width, height, positioning_mode)); } #[must_use] - pub fn get_popup_position(&self) -> Option<(f32, f32)> { - *self.popup_position.borrow() + pub fn get_popup_config(&self) -> Option { + *self.popup_config.borrow() } - pub fn clear_popup_position(&self) { - *self.popup_position.borrow_mut() = None; - } - - pub fn set_popup_size(&self, width: f32, height: f32) { - *self.popup_size.borrow_mut() = Some((width, height)); - } - - pub fn get_popup_size(&self) -> Option<(f32, f32)> { - *self.popup_size.borrow() - } - - pub fn clear_popup_size(&self) { - *self.popup_size.borrow_mut() = None; + pub fn clear_popup_config(&self) { + *self.popup_config.borrow_mut() = None; } } diff --git a/adapters/src/wayland/shell_adapter.rs b/adapters/src/wayland/shell_adapter.rs index cb1109e..c5856c5 100644 --- a/adapters/src/wayland/shell_adapter.rs +++ b/adapters/src/wayland/shell_adapter.rs @@ -2,7 +2,7 @@ use crate::wayland::{ config::{LayerSurfaceParams, WaylandWindowConfig}, globals::context::GlobalContext, surfaces::layer_surface::{SurfaceCtx, SurfaceSetupParams}, - surfaces::popup_manager::{PopupContext, PopupManager}, + surfaces::popup_manager::{CreatePopupParams, PopupContext, PopupManager}, surfaces::{ surface_builder::WindowStateBuilder, surface_state::{SharedPointerSerial, WindowState}, @@ -11,13 +11,15 @@ use crate::wayland::{ use crate::{ errors::{EventLoopError, LayerShikaError, RenderingError, Result}, rendering::{ - egl::context::EGLContext, femtovg::main_window::FemtoVGWindow, - slint_integration::platform::CustomSlintPlatform, + egl::context::EGLContext, + femtovg::main_window::FemtoVGWindow, + slint_integration::platform::{CustomSlintPlatform, clear_popup_config, get_popup_config}, }, }; use core::result::Result as CoreResult; use layer_shika_domain::errors::DomainError; use layer_shika_domain::ports::windowing::WindowingSystemPort; +use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; use log::{error, info}; use slint::{ LogicalPosition, PhysicalSize, PlatformError, WindowPosition, @@ -173,14 +175,45 @@ impl WaylandWindowingSystem { let layer_surface = state.layer_surface(); let queue_handle = event_queue.handle(); let serial_holder = Rc::clone(shared_serial); + let output_size = *state.output_size(); + + #[allow(clippy::cast_precision_loss)] + let default_width = output_size.width as f32; + #[allow(clippy::cast_precision_loss)] + let default_height = output_size.height as f32; platform.set_popup_creator(move || { info!("Popup creator called! Creating popup window..."); let serial = serial_holder.get(); + let (reference_x, reference_y, width, height, positioning_mode) = get_popup_config() + .unwrap_or_else(|| { + log::warn!("No popup config provided, using output size as defaults"); + ( + 0.0, + 0.0, + default_width, + default_height, + PopupPositioningMode::TopLeft, + ) + }); + + clear_popup_config(); + let popup_window = popup_manager_clone - .create_popup(&queue_handle, &layer_surface, serial) + .create_popup( + &queue_handle, + &layer_surface, + CreatePopupParams { + last_pointer_serial: serial, + reference_x, + reference_y, + width, + height, + positioning_mode, + }, + ) .map_err(|e| PlatformError::Other(format!("Failed to create popup: {e}")))?; if let Some(platform) = platform_weak.upgrade() { diff --git a/adapters/src/wayland/surfaces/popup_manager.rs b/adapters/src/wayland/surfaces/popup_manager.rs index e031fa6..c554240 100644 --- a/adapters/src/wayland/surfaces/popup_manager.rs +++ b/adapters/src/wayland/surfaces/popup_manager.rs @@ -1,10 +1,8 @@ use crate::errors::{LayerShikaError, Result}; use crate::rendering::egl::context::EGLContext; use crate::rendering::femtovg::popup_window::PopupWindow; -use crate::rendering::slint_integration::platform::{ - clear_popup_position_override, get_popup_position_override, - clear_popup_size_override, get_popup_size_override, -}; +use layer_shika_domain::value_objects::popup_config::PopupConfig; +use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; use log::info; use slab::Slab; use slint::{platform::femtovg_renderer::FemtoVGRenderer, PhysicalSize, WindowSize}; @@ -23,6 +21,16 @@ use wayland_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase; use super::popup_surface::PopupSurface; use super::surface_state::WindowState; +#[derive(Debug, Clone, Copy)] +pub struct CreatePopupParams { + pub last_pointer_serial: u32, + pub reference_x: f32, + pub reference_y: f32, + pub width: f32, + pub height: f32, + pub positioning_mode: PopupPositioningMode, +} + pub struct PopupContext { compositor: WlCompositor, xdg_wm_base: Option, @@ -89,7 +97,7 @@ impl PopupManager { self: &Rc, queue_handle: &QueueHandle, parent_layer_surface: &ZwlrLayerSurfaceV1, - last_pointer_serial: u32, + params: CreatePopupParams, ) -> Result> { let xdg_wm_base = self.context.xdg_wm_base.as_ref().ok_or_else(|| { LayerShikaError::WindowConfiguration { @@ -97,41 +105,32 @@ impl PopupManager { } })?; - let pointer_position = if let Some((x, y)) = get_popup_position_override() { - info!("Using explicit popup position: ({}, {})", x, y); - clear_popup_position_override(); - slint::LogicalPosition::new(x, y) - } else { - log::error!("No popup position provided - using (0, 0) as fallback"); - slint::LogicalPosition::new(0.0, 0.0) - }; - let scale_factor = *self.current_scale_factor.borrow(); - let output_size = *self.current_output_size.borrow(); info!( - "Creating popup window with scale factor {scale_factor} and output size {output_size:?}" + "Creating popup window with scale factor {scale_factor}, reference=({}, {}), size=({} x {}), mode={:?}", + params.reference_x, + params.reference_y, + params.width, + params.height, + params.positioning_mode + ); + + let popup_config = PopupConfig::new( + params.reference_x, + params.reference_y, + params.width, + params.height, + params.positioning_mode, ); - #[allow(clippy::cast_precision_loss)] - let logical_size = if let Some((width, height)) = get_popup_size_override() { - info!("Using explicit popup size: ({}, {})", width, height); - clear_popup_size_override(); - slint::LogicalSize::new(width, height) - } else { - info!("No popup size override - using full output size"); - slint::LogicalSize::new( - output_size.width as f32 / scale_factor, - output_size.height as f32 / scale_factor, - ) - }; #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] let popup_size = PhysicalSize::new( - (logical_size.width * scale_factor) as u32, - (logical_size.height * scale_factor) as u32, + (params.width * scale_factor) as u32, + (params.height * scale_factor) as u32, ); - info!("Popup logical size: {logical_size:?}, physical size: {popup_size:?}"); + info!("Popup physical size: {popup_size:?}"); let popup_surface = PopupSurface::create(&super::popup_surface::PopupSurfaceParams { compositor: &self.context.compositor, @@ -140,12 +139,12 @@ impl PopupManager { fractional_scale_manager: self.context.fractional_scale_manager.as_ref(), viewporter: self.context.viewporter.as_ref(), queue_handle, - position: pointer_position, - size: popup_size, + popup_config, + physical_size: popup_size, scale_factor, }); - popup_surface.grab(&self.context.seat, last_pointer_serial); + popup_surface.grab(&self.context.seat, params.last_pointer_serial); let context = EGLContext::builder() .with_display_id(self.context.display.id()) @@ -158,7 +157,10 @@ impl PopupManager { let popup_window = PopupWindow::new(renderer); popup_window.set_scale_factor(scale_factor); - popup_window.set_size(WindowSize::Logical(logical_size)); + popup_window.set_size(WindowSize::Logical(slint::LogicalSize::new( + params.width, + params.height, + ))); let key = self.popups.borrow_mut().insert(ActivePopup { surface: popup_surface, diff --git a/adapters/src/wayland/surfaces/popup_surface.rs b/adapters/src/wayland/surfaces/popup_surface.rs index ed64680..acc5fb6 100644 --- a/adapters/src/wayland/surfaces/popup_surface.rs +++ b/adapters/src/wayland/surfaces/popup_surface.rs @@ -1,5 +1,6 @@ +use layer_shika_domain::value_objects::popup_config::PopupConfig; use log::info; -use slint::{LogicalPosition, PhysicalSize}; +use slint::PhysicalSize; use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; use std::rc::Rc; use wayland_client::{ @@ -30,8 +31,8 @@ pub struct PopupSurfaceParams<'a> { pub fractional_scale_manager: Option<&'a WpFractionalScaleManagerV1>, pub viewporter: Option<&'a WpViewporter>, pub queue_handle: &'a QueueHandle, - pub position: LogicalPosition, - pub size: PhysicalSize, + pub popup_config: PopupConfig, + pub physical_size: PhysicalSize, pub scale_factor: f32, } @@ -76,14 +77,14 @@ impl PopupSurface { #[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation)] if let Some(ref vp) = viewport { - let logical_width = (params.size.width as f32 / params.scale_factor) as i32; - let logical_height = (params.size.height as f32 / params.scale_factor) as i32; + let logical_width = (params.physical_size.width as f32 / params.scale_factor) as i32; + let logical_height = (params.physical_size.height as f32 / params.scale_factor) as i32; info!( "Setting viewport destination to logical size: {}x{} (physical: {}x{}, scale: {})", logical_width, logical_height, - params.size.width, - params.size.height, + params.physical_size.width, + params.physical_size.height, params.scale_factor ); vp.set_destination(logical_width, logical_height); @@ -102,24 +103,29 @@ impl PopupSurface { #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_possible_wrap)] + #[allow(clippy::cast_sign_loss)] + #[allow(clippy::cast_precision_loss)] fn create_positioner(params: &PopupSurfaceParams<'_>) -> XdgPositioner { let positioner = params .xdg_wm_base .create_positioner(params.queue_handle, ()); - let x = params.position.x as i32; - let y = params.position.y as i32; + let calculated_x = params.popup_config.calculated_top_left_x() as i32; + let calculated_y = params.popup_config.calculated_top_left_y() as i32; - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_sign_loss)] - #[allow(clippy::cast_precision_loss)] - let logical_width = (params.size.width as f32 / params.scale_factor) as i32; - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_sign_loss)] - #[allow(clippy::cast_precision_loss)] - let logical_height = (params.size.height as f32 / params.scale_factor) as i32; + info!( + "Popup positioning: reference=({}, {}), mode={:?}, calculated_top_left=({}, {})", + params.popup_config.reference_x(), + params.popup_config.reference_y(), + params.popup_config.positioning_mode(), + calculated_x, + calculated_y + ); - positioner.set_anchor_rect(x, y, 1, 1); + let logical_width = (params.physical_size.width as f32 / params.scale_factor) as i32; + let logical_height = (params.physical_size.height as f32 / params.scale_factor) as i32; + + positioner.set_anchor_rect(calculated_x, calculated_y, 1, 1); positioner.set_size(logical_width, logical_height); positioner.set_anchor(Anchor::TopLeft); positioner.set_gravity(Gravity::BottomRight); diff --git a/composition/src/lib.rs b/composition/src/lib.rs index 45e162d..d6432d3 100644 --- a/composition/src/lib.rs +++ b/composition/src/lib.rs @@ -11,11 +11,9 @@ pub use builder::LayerShika; pub use layer_shika_adapters::PopupWindow; pub use layer_shika_adapters::close_current_popup; pub use layer_shika_adapters::platform::{calloop, slint, slint_interpreter}; -pub use layer_shika_adapters::{ - clear_popup_position_override, get_popup_position_override, set_popup_position_override, - clear_popup_size_override, get_popup_size_override, set_popup_size_override, -}; +pub use layer_shika_adapters::{clear_popup_config, get_popup_config, set_popup_config}; pub use layer_shika_domain::value_objects::anchor::AnchorEdges; +pub use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; pub type Result = StdResult; diff --git a/domain/src/value_objects/mod.rs b/domain/src/value_objects/mod.rs index c830a20..b66ca90 100644 --- a/domain/src/value_objects/mod.rs +++ b/domain/src/value_objects/mod.rs @@ -2,3 +2,5 @@ pub mod anchor; pub mod dimensions; pub mod layer; pub mod margins; +pub mod popup_config; +pub mod popup_positioning_mode; diff --git a/domain/src/value_objects/popup_config.rs b/domain/src/value_objects/popup_config.rs new file mode 100644 index 0000000..ddcaf38 --- /dev/null +++ b/domain/src/value_objects/popup_config.rs @@ -0,0 +1,72 @@ +use super::popup_positioning_mode::PopupPositioningMode; + +#[derive(Debug, Clone, Copy)] +pub struct PopupConfig { + reference_x: f32, + reference_y: f32, + width: f32, + height: f32, + positioning_mode: PopupPositioningMode, +} + +impl PopupConfig { + #[must_use] + pub const fn new( + reference_x: f32, + reference_y: f32, + width: f32, + height: f32, + positioning_mode: PopupPositioningMode, + ) -> Self { + Self { + reference_x, + reference_y, + width, + height, + positioning_mode, + } + } + + #[must_use] + pub const fn reference_x(&self) -> f32 { + self.reference_x + } + + #[must_use] + pub const fn reference_y(&self) -> f32 { + self.reference_y + } + + #[must_use] + pub const fn width(&self) -> f32 { + self.width + } + + #[must_use] + pub const fn height(&self) -> f32 { + self.height + } + + #[must_use] + pub const fn positioning_mode(&self) -> PopupPositioningMode { + self.positioning_mode + } + + #[must_use] + pub fn calculated_top_left_x(&self) -> f32 { + if self.positioning_mode.center_x() { + self.reference_x - (self.width / 2.0) + } else { + self.reference_x + } + } + + #[must_use] + pub fn calculated_top_left_y(&self) -> f32 { + if self.positioning_mode.center_y() { + self.reference_y - (self.height / 2.0) + } else { + self.reference_y + } + } +} diff --git a/domain/src/value_objects/popup_positioning_mode.rs b/domain/src/value_objects/popup_positioning_mode.rs new file mode 100644 index 0000000..e34643c --- /dev/null +++ b/domain/src/value_objects/popup_positioning_mode.rs @@ -0,0 +1,40 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PopupPositioningMode { + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + CenterRight, + BottomLeft, + BottomCenter, + BottomRight, +} + +impl PopupPositioningMode { + #[must_use] + pub const fn center_x(self) -> bool { + matches!(self, Self::TopCenter | Self::Center | Self::BottomCenter) + } + + #[must_use] + pub const fn center_y(self) -> bool { + matches!(self, Self::CenterLeft | Self::Center | Self::CenterRight) + } + + #[must_use] + pub const fn from_flags(center_x: bool, center_y: bool) -> Self { + match (center_x, center_y) { + (false, false) => Self::TopLeft, + (true, false) => Self::TopCenter, + (false, true) => Self::CenterLeft, + (true, true) => Self::Center, + } + } +} + +impl Default for PopupPositioningMode { + fn default() -> Self { + Self::TopLeft + } +}