From 3e9e4cf4b49175499a4a57580bd8c699433b297e Mon Sep 17 00:00:00 2001 From: drendog Date: Mon, 15 Dec 2025 08:14:25 +0100 Subject: [PATCH] refactor: make popup full runtime configurable, add missing config --- .../src/rendering/femtovg/popup_window.rs | 2 +- .../src/wayland/surfaces/popup_manager.rs | 120 +-- .../src/wayland/surfaces/popup_surface.rs | 177 +++- crates/composition/src/layer_shika.rs | 890 ------------------ crates/composition/src/lib.rs | 27 +- crates/composition/src/popup/mod.rs | 44 + crates/composition/src/popup_builder.rs | 259 ++--- crates/composition/src/shell.rs | 10 +- crates/composition/src/system.rs | 367 ++------ crates/domain/src/dimensions.rs | 54 +- crates/domain/src/entities/mod.rs | 1 + crates/domain/src/entities/popup_tree.rs | 127 +++ crates/domain/src/prelude.rs | 10 +- crates/domain/src/value_objects/mod.rs | 6 +- .../domain/src/value_objects/output_target.rs | 23 + .../src/value_objects/popup_behavior.rs | 60 ++ .../domain/src/value_objects/popup_config.rs | 131 +-- .../src/value_objects/popup_position.rs | 58 ++ .../value_objects/popup_positioning_mode.rs | 47 - .../domain/src/value_objects/popup_request.rs | 182 ---- crates/domain/src/value_objects/popup_size.rs | 26 + src/lib.rs | 5 +- src/prelude.rs | 5 +- src/window.rs | 5 +- 24 files changed, 897 insertions(+), 1739 deletions(-) delete mode 100644 crates/composition/src/layer_shika.rs create mode 100644 crates/composition/src/popup/mod.rs create mode 100644 crates/domain/src/entities/popup_tree.rs create mode 100644 crates/domain/src/value_objects/output_target.rs create mode 100644 crates/domain/src/value_objects/popup_behavior.rs create mode 100644 crates/domain/src/value_objects/popup_position.rs delete mode 100644 crates/domain/src/value_objects/popup_positioning_mode.rs delete mode 100644 crates/domain/src/value_objects/popup_request.rs create mode 100644 crates/domain/src/value_objects/popup_size.rs diff --git a/crates/adapters/src/rendering/femtovg/popup_window.rs b/crates/adapters/src/rendering/femtovg/popup_window.rs index 06eac40..8dda607 100644 --- a/crates/adapters/src/rendering/femtovg/popup_window.rs +++ b/crates/adapters/src/rendering/femtovg/popup_window.rs @@ -2,7 +2,7 @@ use super::renderable_window::{RenderState, RenderableWindow}; use crate::errors::{RenderingError, Result}; use crate::wayland::surfaces::popup_manager::OnCloseCallback; use core::ops::Deref; -use layer_shika_domain::value_objects::popup_request::PopupHandle; +use layer_shika_domain::value_objects::handle::PopupHandle; use log::info; use slint::{ PhysicalSize, Window, WindowSize, diff --git a/crates/adapters/src/wayland/surfaces/popup_manager.rs b/crates/adapters/src/wayland/surfaces/popup_manager.rs index 0a6d465..15b3e18 100644 --- a/crates/adapters/src/wayland/surfaces/popup_manager.rs +++ b/crates/adapters/src/wayland/surfaces/popup_manager.rs @@ -4,14 +4,15 @@ use crate::rendering::femtovg::{popup_window::PopupWindow, renderable_window::Re use crate::wayland::surfaces::display_metrics::{DisplayMetrics, SharedDisplayMetrics}; use layer_shika_domain::dimensions::LogicalSize as DomainLogicalSize; use layer_shika_domain::surface_dimensions::SurfaceDimensions; +use layer_shika_domain::value_objects::handle::PopupHandle; +use layer_shika_domain::value_objects::popup_behavior::ConstraintAdjustment; use layer_shika_domain::value_objects::popup_config::PopupConfig; -use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; -use layer_shika_domain::value_objects::popup_request::{PopupHandle, PopupRequest}; +use layer_shika_domain::value_objects::popup_position::PopupPosition; use log::info; use slint::{platform::femtovg_renderer::FemtoVGRenderer, PhysicalSize, WindowSize}; use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; use std::cell::{Cell, RefCell}; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use wayland_client::{ backend::ObjectId, @@ -55,14 +56,13 @@ impl PopupId { pub type OnCloseCallback = Box; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] 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 position: PopupPosition, + pub constraint_adjustment: ConstraintAdjustment, pub grab: bool, } @@ -102,8 +102,6 @@ impl PopupContext { struct ActivePopup { surface: PopupSurface, window: Rc, - request: PopupRequest, - last_serial: u32, } impl Drop for ActivePopup { @@ -116,7 +114,7 @@ impl Drop for ActivePopup { struct PendingPopup { id: PopupId, - request: PopupRequest, + config: PopupConfig, width: f32, height: f32, } @@ -124,9 +122,7 @@ struct PendingPopup { struct PopupManagerState { popups: HashMap, display_metrics: SharedDisplayMetrics, - current_popup_id: Option, - pending_popup: Option, - id_generator: usize, + pending_popups: VecDeque, } impl PopupManagerState { @@ -134,17 +130,9 @@ impl PopupManagerState { Self { popups: HashMap::new(), display_metrics, - current_popup_id: None, - pending_popup: None, - id_generator: 0, + pending_popups: VecDeque::new(), } } - - fn allocate_id(&mut self) -> PopupId { - let id = PopupId(self.id_generator); - self.id_generator += 1; - id - } } pub struct PopupManager { @@ -164,33 +152,39 @@ impl PopupManager { } } - pub fn request_popup(&self, request: PopupRequest, width: f32, height: f32) -> PopupHandle { + pub fn request_popup( + &self, + handle: PopupHandle, + config: PopupConfig, + width: f32, + height: f32, + ) -> PopupHandle { let mut state = self.state.borrow_mut(); - let id = state.allocate_id(); + let id = PopupId::from_handle(handle); - state.pending_popup = Some(PendingPopup { + state.pending_popups.push_back(PendingPopup { id, - request, + config, width, height, }); - id.to_handle() + handle } #[must_use] - pub(crate) fn take_pending_popup_params(&self) -> Option<(PopupId, PopupRequest, f32, f32)> { + pub(crate) fn take_pending_popup_params(&self) -> Option<(PopupId, PopupConfig, f32, f32)> { self.state .borrow_mut() - .pending_popup - .take() - .map(|p| (p.id, p.request, p.width, p.height)) + .pending_popups + .pop_front() + .map(|p| (p.id, p.config, p.width, p.height)) } #[must_use] pub fn has_pending_popup(&self) -> bool { - self.state.borrow().pending_popup.is_some() + !self.state.borrow().pending_popups.is_empty() } #[must_use] @@ -220,25 +214,13 @@ impl PopupManager { .update_output_size(output_size); } - pub fn close_current_popup(&self) { - let id = self.state.borrow_mut().current_popup_id.take(); - if let Some(id) = id { - self.destroy_popup(id); - } - } - - #[must_use] - pub fn current_popup_key(&self) -> Option { - self.state.borrow().current_popup_id.map(PopupId::key) - } - pub fn create_pending_popup( self: &Rc, queue_handle: &QueueHandle, parent_layer_surface: &ZwlrLayerSurfaceV1, last_pointer_serial: u32, ) -> Result> { - let (id, request, width, height) = self.take_pending_popup_params().ok_or_else(|| { + let (id, config, width, height) = self.take_pending_popup_params().ok_or_else(|| { LayerShikaError::WindowConfiguration { message: "No pending popup request available".into(), } @@ -246,23 +228,21 @@ impl PopupManager { let params = CreatePopupParams { last_pointer_serial, - reference_x: request.placement.position().0, - reference_y: request.placement.position().1, width, height, - positioning_mode: request.mode, - grab: request.grab, + position: config.position.clone(), + constraint_adjustment: config.behavior.constraint_adjustment, + grab: config.behavior.grab, }; - self.create_popup_internal(queue_handle, parent_layer_surface, params, request, id) + self.create_popup_internal(queue_handle, parent_layer_surface, ¶ms, id) } fn create_popup_internal( self: &Rc, queue_handle: &QueueHandle, parent_layer_surface: &ZwlrLayerSurfaceV1, - params: CreatePopupParams, - request: PopupRequest, + params: &CreatePopupParams, popup_id: PopupId, ) -> Result> { let xdg_wm_base = self.context.xdg_wm_base.as_ref().ok_or_else(|| { @@ -273,12 +253,8 @@ impl PopupManager { let scale_factor = self.scale_factor(); info!( - "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 + "Creating popup window with scale factor {scale_factor}, size=({} x {}), position={:?}", + params.width, params.height, params.position ); let output_size = self.output_size(); @@ -297,14 +273,6 @@ impl PopupManager { .scale_factor_typed(); let popup_dimensions = SurfaceDimensions::from_logical(popup_logical_size, domain_scale); - let popup_config = PopupConfig::new( - params.reference_x, - params.reference_y, - popup_dimensions, - params.positioning_mode, - output_logical_size, - ); - let popup_size = PhysicalSize::new( popup_dimensions.physical_width(), popup_dimensions.physical_height(), @@ -320,7 +288,9 @@ impl PopupManager { fractional_scale_manager: self.context.fractional_scale_manager.as_ref(), viewporter: self.context.viewporter.as_ref(), queue_handle, - popup_config, + position: params.position.clone(), + output_bounds: output_logical_size, + constraint_adjustment: params.constraint_adjustment, physical_size: popup_size, scale_factor, }); @@ -364,11 +334,8 @@ impl PopupManager { ActivePopup { surface: wayland_popup_surface, window: Rc::clone(&popup_window), - request, - last_serial: params.last_pointer_serial, }, ); - state.current_popup_id = Some(popup_id); info!("Popup window created successfully with id {:?}", popup_id); @@ -463,15 +430,6 @@ impl PopupManager { } } - pub fn get_popup_info(&self, key: usize) -> Option<(PopupRequest, u32)> { - let id = PopupId(key); - self.state - .borrow() - .popups - .get(&id) - .map(|popup| (popup.request.clone(), popup.last_serial)) - } - pub fn mark_popup_configured(&self, key: usize) { let id = PopupId(key); if let Some(popup) = self.state.borrow().popups.get(&id) { @@ -479,10 +437,6 @@ impl PopupManager { } } - pub fn show(&self, request: PopupRequest, width: f32, height: f32) -> PopupHandle { - self.request_popup(request, width, height) - } - pub fn close(&self, handle: PopupHandle) -> Result<()> { let id = PopupId::from_handle(handle); self.destroy_popup(id); diff --git a/crates/adapters/src/wayland/surfaces/popup_surface.rs b/crates/adapters/src/wayland/surfaces/popup_surface.rs index 243028f..2b0fac7 100644 --- a/crates/adapters/src/wayland/surfaces/popup_surface.rs +++ b/crates/adapters/src/wayland/surfaces/popup_surface.rs @@ -1,10 +1,9 @@ -use layer_shika_domain::dimensions::{LogicalSize as DomainLogicalSize, ScaleFactor}; -use layer_shika_domain::surface_dimensions::SurfaceDimensions; -use layer_shika_domain::value_objects::popup_config::PopupConfig; +use layer_shika_domain::dimensions::{LogicalRect, LogicalSize as DomainLogicalSize}; +use layer_shika_domain::value_objects::popup_behavior::ConstraintAdjustment as DomainConstraintAdjustment; +use layer_shika_domain::value_objects::popup_position::{Alignment, AnchorPoint, PopupPosition}; use log::info; use slint::PhysicalSize; use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; -use std::cell::Cell; use std::rc::Rc; use wayland_client::{ protocol::{wl_compositor::WlCompositor, wl_seat::WlSeat, wl_surface::WlSurface}, @@ -33,7 +32,9 @@ pub struct PopupSurfaceParams<'a> { pub fractional_scale_manager: Option<&'a WpFractionalScaleManagerV1>, pub viewporter: Option<&'a WpViewporter>, pub queue_handle: &'a QueueHandle, - pub popup_config: PopupConfig, + pub position: PopupPosition, + pub output_bounds: DomainLogicalSize, + pub constraint_adjustment: DomainConstraintAdjustment, pub physical_size: PhysicalSize, pub scale_factor: f32, } @@ -44,10 +45,11 @@ pub struct PopupSurface { pub xdg_popup: Rc, pub fractional_scale: Option>, pub viewport: Option>, - popup_config: PopupConfig, + position: PopupPosition, + output_bounds: DomainLogicalSize, + constraint_adjustment: DomainConstraintAdjustment, xdg_wm_base: Rc, queue_handle: QueueHandle, - scale_factor: Cell, } impl PopupSurface { @@ -102,10 +104,11 @@ impl PopupSurface { xdg_popup, fractional_scale, viewport, - popup_config: params.popup_config, + position: params.position.clone(), + output_bounds: params.output_bounds, + constraint_adjustment: params.constraint_adjustment, xdg_wm_base: Rc::new(params.xdg_wm_base.clone()), queue_handle: params.queue_handle.clone(), - scale_factor: Cell::new(params.scale_factor), } } @@ -118,26 +121,29 @@ impl PopupSurface { .xdg_wm_base .create_positioner(params.queue_handle, ()); - let calculated_x = params.popup_config.calculated_top_left_x() as i32; - let calculated_y = params.popup_config.calculated_top_left_y() as i32; + let logical_width = params.physical_size.width as f32 / params.scale_factor; + let logical_height = params.physical_size.height as f32 / params.scale_factor; - 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 + let (calculated_x, calculated_y) = compute_top_left( + ¶ms.position, + DomainLogicalSize::from_raw(logical_width, logical_height), + params.output_bounds, ); - 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!( + "Popup positioning: position={:?}, calculated_top_left=({}, {})", + params.position, calculated_x, calculated_y + ); + + let logical_width_i32 = logical_width as i32; + let logical_height_i32 = logical_height as i32; positioner.set_anchor_rect(calculated_x, calculated_y, 1, 1); - positioner.set_size(logical_width, logical_height); + positioner.set_size(logical_width_i32, logical_height_i32); positioner.set_anchor(Anchor::TopLeft); positioner.set_gravity(Gravity::BottomRight); - positioner.set_constraint_adjustment(ConstraintAdjustment::None); + positioner + .set_constraint_adjustment(map_constraint_adjustment(params.constraint_adjustment)); positioner } @@ -168,30 +174,15 @@ impl PopupSurface { #[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_precision_loss)] fn reposition_popup(&self, logical_width: i32, logical_height: i32) { - let scale_factor = self.scale_factor.get(); - - let updated_config = PopupConfig::new( - self.popup_config.reference_x(), - self.popup_config.reference_y(), - SurfaceDimensions::from_logical( - DomainLogicalSize::from_raw(logical_width as f32, logical_height as f32), - ScaleFactor::from_raw(scale_factor), - ), - self.popup_config.positioning_mode(), - self.popup_config.output_bounds(), + let (calculated_x, calculated_y) = compute_top_left( + &self.position, + DomainLogicalSize::from_raw(logical_width as f32, logical_height as f32), + self.output_bounds, ); - let calculated_x = updated_config.calculated_top_left_x() as i32; - let calculated_y = updated_config.calculated_top_left_y() as i32; - info!( - "Repositioning popup: reference=({}, {}), new_size=({}x{}), new_top_left=({}, {})", - self.popup_config.reference_x(), - self.popup_config.reference_y(), - logical_width, - logical_height, - calculated_x, - calculated_y + "Repositioning popup: new_size=({}x{}), new_top_left=({}, {})", + logical_width, logical_height, calculated_x, calculated_y ); let positioner = self.xdg_wm_base.create_positioner(&self.queue_handle, ()); @@ -199,7 +190,7 @@ impl PopupSurface { positioner.set_size(logical_width, logical_height); positioner.set_anchor(Anchor::TopLeft); positioner.set_gravity(Gravity::BottomRight); - positioner.set_constraint_adjustment(ConstraintAdjustment::None); + positioner.set_constraint_adjustment(map_constraint_adjustment(self.constraint_adjustment)); self.xdg_popup.reposition(&positioner, 0); } @@ -211,3 +202,101 @@ impl PopupSurface { self.surface.destroy(); } } + +fn compute_top_left( + position: &PopupPosition, + popup_size: DomainLogicalSize, + output_bounds: DomainLogicalSize, +) -> (i32, i32) { + let (mut x, mut y) = match position { + PopupPosition::Absolute { x, y } => (*x, *y), + PopupPosition::Centered { offset } => ( + (output_bounds.width() / 2.0) - (popup_size.width() / 2.0) + offset.x, + (output_bounds.height() / 2.0) - (popup_size.height() / 2.0) + offset.y, + ), + PopupPosition::Element { + rect, + anchor, + alignment, + } => { + let (anchor_x, anchor_y) = anchor_point_in_rect(*rect, *anchor); + let (ax, ay) = alignment_offsets(*alignment, popup_size); + (anchor_x - ax, anchor_y - ay) + } + PopupPosition::Cursor { .. } | PopupPosition::RelativeToParent { .. } => { + log::warn!("PopupPosition variant not supported by current backend: {position:?}"); + (0.0, 0.0) + } + }; + + let max_x = (output_bounds.width() - popup_size.width()).max(0.0); + let max_y = (output_bounds.height() - popup_size.height()).max(0.0); + + x = x.clamp(0.0, max_x); + y = y.clamp(0.0, max_y); + + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_possible_wrap)] + (x as i32, y as i32) +} + +fn anchor_point_in_rect(rect: LogicalRect, anchor: AnchorPoint) -> (f32, f32) { + let x = rect.x(); + let y = rect.y(); + let w = rect.width(); + let h = rect.height(); + + match anchor { + AnchorPoint::TopLeft => (x, y), + AnchorPoint::TopCenter => (x + w / 2.0, y), + AnchorPoint::TopRight => (x + w, y), + AnchorPoint::CenterLeft => (x, y + h / 2.0), + AnchorPoint::Center => (x + w / 2.0, y + h / 2.0), + AnchorPoint::CenterRight => (x + w, y + h / 2.0), + AnchorPoint::BottomLeft => (x, y + h), + AnchorPoint::BottomCenter => (x + w / 2.0, y + h), + AnchorPoint::BottomRight => (x + w, y + h), + } +} + +fn alignment_offsets(alignment: Alignment, popup_size: DomainLogicalSize) -> (f32, f32) { + let x = match alignment { + Alignment::Start => 0.0, + Alignment::Center => popup_size.width() / 2.0, + Alignment::End => popup_size.width(), + }; + let y = match alignment { + Alignment::Start => 0.0, + Alignment::Center => popup_size.height() / 2.0, + Alignment::End => popup_size.height(), + }; + (x, y) +} + +fn map_constraint_adjustment(adjustment: DomainConstraintAdjustment) -> ConstraintAdjustment { + match adjustment { + DomainConstraintAdjustment::None => ConstraintAdjustment::None, + DomainConstraintAdjustment::Slide => { + ConstraintAdjustment::SlideX | ConstraintAdjustment::SlideY + } + DomainConstraintAdjustment::Flip => { + ConstraintAdjustment::FlipX | ConstraintAdjustment::FlipY + } + DomainConstraintAdjustment::Resize => { + ConstraintAdjustment::ResizeX | ConstraintAdjustment::ResizeY + } + DomainConstraintAdjustment::SlideAndResize => { + ConstraintAdjustment::SlideX + | ConstraintAdjustment::SlideY + | ConstraintAdjustment::ResizeX + | ConstraintAdjustment::ResizeY + } + DomainConstraintAdjustment::FlipAndSlide => { + ConstraintAdjustment::FlipX + | ConstraintAdjustment::FlipY + | ConstraintAdjustment::SlideX + | ConstraintAdjustment::SlideY + } + DomainConstraintAdjustment::All => ConstraintAdjustment::all(), + } +} diff --git a/crates/composition/src/layer_shika.rs b/crates/composition/src/layer_shika.rs deleted file mode 100644 index ff34c40..0000000 --- a/crates/composition/src/layer_shika.rs +++ /dev/null @@ -1,890 +0,0 @@ -use crate::event_loop::{EventLoopHandle, FromAppState}; -use crate::popup_builder::PopupBuilder; -use crate::shell::LayerSurfaceHandle; -use crate::shell_runtime::ShellRuntime; -use crate::system::{PopupCommand, ShellControl}; -use crate::value_conversion::IntoValue; -use crate::{Error, Result}; -use layer_shika_adapters::errors::EventLoopError; -use layer_shika_adapters::platform::calloop::channel; -use layer_shika_adapters::platform::slint_interpreter::{ - CompilationResult, Compiler, ComponentInstance, Value, -}; -use layer_shika_adapters::{ - AppState, ShellWindowConfig, WaylandWindowConfig, SurfaceState, ShellSystemFacade, -}; -use layer_shika_domain::config::WindowConfig; -use layer_shika_domain::entities::output_registry::OutputRegistry; -use layer_shika_domain::errors::DomainError; -use layer_shika_domain::prelude::{ - AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor, WindowDimension, -}; -use layer_shika_domain::value_objects::output_handle::OutputHandle; -use layer_shika_domain::value_objects::output_info::OutputInfo; -use spin_on::spin_on; -use std::cell::RefCell; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::rc::Rc; - -pub const DEFAULT_COMPONENT_NAME: &str = "Main"; - -#[derive(Debug, Clone)] -pub struct SurfaceDefinition { - pub component: String, - pub config: WindowConfig, -} - -enum CompilationSource { - File { path: PathBuf, compiler: Compiler }, - Source { code: String, compiler: Compiler }, - Compiled(Rc), -} - -pub struct ShellBuilder { - compilation: CompilationSource, - windows: Vec, -} - -impl ShellBuilder { - pub fn window(self, component: impl Into) -> SurfaceConfigBuilder { - SurfaceConfigBuilder { - shell_builder: self, - component: component.into(), - config: WindowConfig::default(), - } - } - - #[must_use] - pub fn discover_surfaces( - mut self, - components: impl IntoIterator>, - ) -> Self { - for component in components { - self.surfaces.push(SurfaceDefinition { - component: component.into(), - config: WindowConfig::default(), - }); - } - self - } - - pub fn build(self) -> Result { - let windows = if self.surfaces.is_empty() { - vec![SurfaceDefinition { - component: DEFAULT_COMPONENT_NAME.to_string(), - config: WindowConfig::default(), - }] - } else { - self.surfaces - }; - - let compilation_result = match self.compilation { - CompilationSource::File { path, compiler } => { - let result = spin_on(compiler.build_from_path(&path)); - let diagnostics: Vec<_> = result.diagnostics().collect(); - if !diagnostics.is_empty() { - let messages: Vec = - diagnostics.iter().map(ToString::to_string).collect(); - return Err(DomainError::Configuration { - message: format!( - "Failed to compile Slint file '{}':\n{}", - path.display(), - messages.join("\n") - ), - } - .into()); - } - Rc::new(result) - } - CompilationSource::Source { code, compiler } => { - let result = spin_on(compiler.build_from_source(code, PathBuf::default())); - let diagnostics: Vec<_> = result.diagnostics().collect(); - if !diagnostics.is_empty() { - let messages: Vec = - diagnostics.iter().map(ToString::to_string).collect(); - return Err(DomainError::Configuration { - message: format!( - "Failed to compile Slint source:\n{}", - messages.join("\n") - ), - } - .into()); - } - Rc::new(result) - } - CompilationSource::Compiled(result) => result, - }; - - Runtime::new(compilation_result, windows) - } -} - -pub struct SurfaceConfigBuilder { - shell_builder: ShellBuilder, - component: String, - config: WindowConfig, -} - -impl SurfaceConfigBuilder { - #[must_use] - pub fn size(mut self, width: u32, height: u32) -> Self { - self.config.dimensions = WindowDimension::new(width, height); - self - } - - #[must_use] - pub fn height(mut self, height: u32) -> Self { - self.config.dimensions = WindowDimension::new(self.config.dimensions.width(), height); - self - } - - #[must_use] - pub fn width(mut self, width: u32) -> Self { - self.config.dimensions = WindowDimension::new(width, self.config.dimensions.height()); - self - } - - #[must_use] - pub const fn layer(mut self, layer: Layer) -> Self { - self.config.layer = layer; - self - } - - #[must_use] - pub fn margin(mut self, margin: impl Into) -> Self { - self.config.margin = margin.into(); - self - } - - #[must_use] - pub const fn anchor(mut self, anchor: AnchorEdges) -> Self { - self.config.anchor = anchor; - self - } - - #[must_use] - pub const fn exclusive_zone(mut self, zone: i32) -> Self { - self.config.exclusive_zone = zone; - self - } - - #[must_use] - pub fn namespace(mut self, namespace: impl Into) -> Self { - self.config.namespace = namespace.into(); - self - } - - #[must_use] - pub fn scale_factor(mut self, sf: impl TryInto) -> Self { - self.config.scale_factor = sf.try_into().unwrap_or_default(); - self - } - - #[must_use] - pub const fn keyboard_interactivity(mut self, mode: KeyboardInteractivity) -> Self { - self.config.keyboard_interactivity = mode; - self - } - - #[must_use] - pub fn output_policy(mut self, policy: OutputPolicy) -> Self { - self.config.output_policy = policy; - self - } - - #[must_use] - pub fn window(self, component: impl Into) -> SurfaceConfigBuilder { - let shell_builder = self.complete(); - shell_builder.window(component) - } - - pub fn build(self) -> Result { - self.complete().build() - } - - pub fn run(self) -> Result<()> { - let mut runtime = self.build()?; - runtime.run() - } - - fn complete(mut self) -> ShellBuilder { - self.shell_builder.surfaces.push(SurfaceDefinition { - component: self.component, - config: self.config, - }); - self.shell_builder - } -} - -pub struct LayerShika; - -impl LayerShika { - pub fn from_file(path: impl AsRef) -> ShellBuilder { - ShellBuilder { - compilation: CompilationSource::File { - path: path.as_ref().to_path_buf(), - compiler: Compiler::default(), - }, - windows: Vec::new(), - } - } - - pub fn from_file_with_compiler(path: impl AsRef, compiler: Compiler) -> ShellBuilder { - ShellBuilder { - compilation: CompilationSource::File { - path: path.as_ref().to_path_buf(), - compiler, - }, - windows: Vec::new(), - } - } - - pub fn from_source(code: impl Into) -> ShellBuilder { - ShellBuilder { - compilation: CompilationSource::Source { - code: code.into(), - compiler: Compiler::default(), - }, - windows: Vec::new(), - } - } - - pub fn from_source_with_compiler(code: impl Into, compiler: Compiler) -> ShellBuilder { - ShellBuilder { - compilation: CompilationSource::Source { - code: code.into(), - compiler, - }, - windows: Vec::new(), - } - } - - pub fn from_compilation(result: Rc) -> ShellBuilder { - ShellBuilder { - compilation: CompilationSource::Compiled(result), - windows: Vec::new(), - } - } - - pub fn compile_file(path: impl AsRef) -> Result> { - let compiler = Compiler::default(); - let result = spin_on(compiler.build_from_path(path.as_ref())); - let diagnostics: Vec<_> = result.diagnostics().collect(); - if !diagnostics.is_empty() { - let messages: Vec = diagnostics.iter().map(ToString::to_string).collect(); - return Err(DomainError::Configuration { - message: format!( - "Failed to compile Slint file '{}':\n{}", - path.as_ref().display(), - messages.join("\n") - ), - } - .into()); - } - Ok(Rc::new(result)) - } - - pub fn compile_source(code: impl Into) -> Result> { - let compiler = Compiler::default(); - let result = spin_on(compiler.build_from_source(code.into(), PathBuf::default())); - let diagnostics: Vec<_> = result.diagnostics().collect(); - if !diagnostics.is_empty() { - let messages: Vec = diagnostics.iter().map(ToString::to_string).collect(); - return Err(DomainError::Configuration { - message: format!("Failed to compile Slint source:\n{}", messages.join("\n")), - } - .into()); - } - Ok(Rc::new(result)) - } -} - -pub struct Runtime { - inner: Rc>, - windows: HashMap, - compilation_result: Rc, - popup_command_sender: channel::Sender, -} - -impl Runtime { - pub(crate) fn new( - compilation_result: Rc, - definitions: Vec, - ) -> Result { - log::info!( - "Creating LayerShika runtime with {} windows", - definitions.len() - ); - - if definitions.is_empty() { - return Err(Error::Domain(DomainError::Configuration { - message: "At least one window definition is required".to_string(), - })); - } - - let is_single_window = definitions.len() == 1; - - if is_single_window { - let definition = definitions.into_iter().next().ok_or_else(|| { - Error::Domain(DomainError::Configuration { - message: "Expected at least one window definition".to_string(), - }) - })?; - Self::new_single_window(compilation_result, definition) - } else { - Self::new_multi_window(compilation_result, definitions) - } - } - - fn new_single_window( - compilation_result: Rc, - definition: SurfaceDefinition, - ) -> Result { - let component_definition = compilation_result - .component(&definition.component) - .ok_or_else(|| { - Error::Domain(DomainError::Configuration { - message: format!( - "Component '{}' not found in compilation result", - definition.component - ), - }) - })?; - - let wayland_config = WaylandWindowConfig::from_domain_config( - component_definition, - Some(Rc::clone(&compilation_result)), - definition.config.clone(), - ); - - let inner = layer_shika_adapters::WaylandShellSystem::new(&wayland_config)?; - let facade = ShellSystemFacade::new(inner); - let inner_rc = Rc::new(RefCell::new(facade)); - - let (sender, receiver) = channel::channel(); - - let mut windows = HashMap::new(); - windows.insert(definition.component.clone(), definition); - - let shell = Self { - inner: Rc::clone(&inner_rc), - windows, - compilation_result, - popup_command_sender: sender, - }; - - shell.setup_popup_command_handler(receiver)?; - - log::info!("LayerShika runtime created (single-window mode)"); - - Ok(shell) - } - - fn new_multi_window( - compilation_result: Rc, - definitions: Vec, - ) -> Result { - let shell_configs: Vec = definitions - .iter() - .map(|def| { - let component_definition = compilation_result - .component(&def.component) - .ok_or_else(|| { - Error::Domain(DomainError::Configuration { - message: format!( - "Component '{}' not found in compilation result", - def.component - ), - }) - })?; - - let wayland_config = WaylandWindowConfig::from_domain_config( - component_definition, - Some(Rc::clone(&compilation_result)), - def.config.clone(), - ); - - Ok(ShellWindowConfig { - name: def.component.clone(), - config: wayland_config, - }) - }) - .collect::>>()?; - - let inner = layer_shika_adapters::WaylandShellSystem::new_multi(&shell_configs)?; - let facade = ShellSystemFacade::new(inner); - let inner_rc = Rc::new(RefCell::new(facade)); - - let (sender, receiver) = channel::channel(); - - let mut windows = HashMap::new(); - for definition in definitions { - windows.insert(definition.component.clone(), definition); - } - - let shell = Self { - inner: Rc::clone(&inner_rc), - windows, - compilation_result, - popup_command_sender: sender, - }; - - shell.setup_popup_command_handler(receiver)?; - - log::info!( - "LayerShika runtime created (multi-window mode) with windows: {:?}", - shell.surface_names() - ); - - Ok(shell) - } - - fn setup_popup_command_handler(&self, receiver: channel::Channel) -> Result<()> { - let loop_handle = self.inner.borrow().inner_ref().event_loop_handle(); - let control = self.control(); - - loop_handle - .insert_source(receiver, move |event, (), app_state| { - if let channel::Event::Msg(command) = event { - let mut ctx = crate::system::EventDispatchContext::from_app_state(app_state); - - match command { - PopupCommand::Show(request) => { - if let Err(e) = ctx.show_popup(&request) { - log::error!("Failed to show popup: {}", e); - } - } - PopupCommand::Close(handle) => { - if let Err(e) = ctx.close_popup(handle) { - log::error!("Failed to close popup: {}", e); - } - } - PopupCommand::Resize { - handle, - width, - height, - } => { - if let Err(e) = ctx.resize_popup(handle, width, height) { - log::error!("Failed to resize popup: {}", e); - } - } - } - } - }) - .map_err(|e| { - Error::Adapter( - EventLoopError::InsertSource { - message: format!("Failed to setup popup command handler: {e:?}"), - } - .into(), - ) - })?; - - Ok(()) - } - - #[must_use] - pub fn control(&self) -> ShellControl { - ShellControl::new(self.popup_command_sender.clone()) - } - - pub fn surface_names(&self) -> Vec<&str> { - self.surfaces.keys().map(String::as_str).collect() - } - - pub fn has_surface(&self, name: &str) -> bool { - self.surfaces.contains_key(name) - } - - pub fn event_loop_handle(&self) -> EventLoopHandle { - EventLoopHandle::new(Rc::downgrade(&self.inner)) - } - - pub fn run(&mut self) -> Result<()> { - log::info!( - "Starting LayerShika event loop with {} windows", - self.surfaces.len() - ); - self.inner.borrow_mut().run()?; - Ok(()) - } - - pub fn with_surface(&self, name: &str, f: F) -> Result - where - F: FnOnce(&ComponentInstance) -> R, - { - if !self.surfaces.contains_key(name) { - return Err(Error::Domain(DomainError::Configuration { - message: format!("Window '{}' not found", name), - })); - } - - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - system - .app_state() - .windows_by_shell_name(name) - .next() - .map(|surface| f(surface.component_instance())) - .ok_or_else(|| { - Error::Domain(DomainError::Configuration { - message: format!("No instance found for window '{}'", name), - }) - }) - } - - pub fn with_all_surfaces(&self, mut f: F) - where - F: FnMut(&str, &ComponentInstance), - { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - for name in self.surfaces.keys() { - for surface in system.app_state().windows_by_shell_name(name) { - f(name, surface.component_instance()); - } - } - } - - pub fn with_output(&self, handle: OutputHandle, f: F) -> Result - where - F: FnOnce(&ComponentInstance) -> R, - { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - let window = system - .app_state() - .get_output_by_handle(handle) - .ok_or_else(|| { - Error::Domain(DomainError::Configuration { - message: format!("Output with handle {:?} not found", handle), - }) - })?; - Ok(f(surface.component_instance())) - } - - pub fn with_all_outputs(&self, mut f: F) - where - F: FnMut(OutputHandle, &ComponentInstance), - { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - for (handle, surface) in system.app_state().outputs_with_handles() { - f(handle, surface.component_instance()); - } - } - - #[must_use] - pub fn compilation_result(&self) -> &Rc { - &self.compilation_result - } - - pub fn on(&self, surface_name: &str, callback_name: &str, handler: F) -> Result<()> - where - F: Fn(ShellControl) -> R + 'static, - R: IntoValue, - { - if !self.surfaces.contains_key(surface_name) { - return Err(Error::Domain(DomainError::Configuration { - message: format!("Window '{}' not found", surface_name), - })); - } - - let control = self.control(); - let handler = Rc::new(handler); - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - for surface in system.app_state().windows_by_shell_name(surface_name) { - let handler_rc = Rc::clone(&handler); - let control_clone = control.clone(); - if let Err(e) = window - .component_instance() - .set_callback(callback_name, move |_args| { - handler_rc(control_clone.clone()).into_value() - }) - { - log::error!( - "Failed to register callback '{}' on window '{}': {}", - callback_name, - surface_name, - e - ); - } - } - - Ok(()) - } - - pub fn on_with_args( - &self, - surface_name: &str, - callback_name: &str, - handler: F, - ) -> Result<()> - where - F: Fn(&[Value], ShellControl) -> R + 'static, - R: IntoValue, - { - if !self.surfaces.contains_key(surface_name) { - return Err(Error::Domain(DomainError::Configuration { - message: format!("Window '{}' not found", surface_name), - })); - } - - let control = self.control(); - let handler = Rc::new(handler); - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - for surface in system.app_state().windows_by_shell_name(surface_name) { - let handler_rc = Rc::clone(&handler); - let control_clone = control.clone(); - if let Err(e) = window - .component_instance() - .set_callback(callback_name, move |args| { - handler_rc(args, control_clone.clone()).into_value() - }) - { - log::error!( - "Failed to register callback '{}' on window '{}': {}", - callback_name, - surface_name, - e - ); - } - } - - Ok(()) - } - - pub fn on_global(&self, callback_name: &str, handler: F) -> Result<()> - where - F: Fn(ShellControl) -> R + 'static, - R: IntoValue, - { - let control = self.control(); - let handler = Rc::new(handler); - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - for surface in system.app_state().all_outputs() { - let handler_rc = Rc::clone(&handler); - let control_clone = control.clone(); - if let Err(e) = window - .component_instance() - .set_callback(callback_name, move |_args| { - handler_rc(control_clone.clone()).into_value() - }) - { - log::error!( - "Failed to register global callback '{}': {}", - callback_name, - e - ); - } - } - - Ok(()) - } - - pub fn on_global_with_args(&self, callback_name: &str, handler: F) -> Result<()> - where - F: Fn(&[Value], ShellControl) -> R + 'static, - R: IntoValue, - { - let control = self.control(); - let handler = Rc::new(handler); - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - for surface in system.app_state().all_outputs() { - let handler_rc = Rc::clone(&handler); - let control_clone = control.clone(); - if let Err(e) = window - .component_instance() - .set_callback(callback_name, move |args| { - handler_rc(args, control_clone.clone()).into_value() - }) - { - log::error!( - "Failed to register global callback '{}': {}", - callback_name, - e - ); - } - } - - Ok(()) - } - - pub fn apply_surface_config(&self, surface_name: &str, f: F) - where - F: Fn(&ComponentInstance, LayerSurfaceHandle<'_>), - { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - if self.surfaces.contains_key(surface_name) { - for surface in system.app_state().windows_by_shell_name(surface_name) { - let surface_handle = LayerSurfaceHandle::from_window_state(surface); - f(surface.component_instance(), surface_handle); - } - } - } - - pub fn apply_global_config(&self, f: F) - where - F: Fn(&ComponentInstance, LayerSurfaceHandle<'_>), - { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - for surface in system.app_state().all_outputs() { - let surface_handle = LayerSurfaceHandle::from_window_state(surface); - f(surface.component_instance(), surface_handle); - } - } - - pub fn output_registry(&self) -> OutputRegistry { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - system.app_state().output_registry().clone() - } - - pub fn get_output_info(&self, handle: OutputHandle) -> Option { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - system.app_state().get_output_info(handle).cloned() - } - - pub fn all_output_info(&self) -> Vec { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - system.app_state().all_output_info().cloned().collect() - } -} - -impl ShellRuntime for Runtime { - type LoopHandle = EventLoopHandle; - type Context<'a> = EventContext<'a>; - - fn event_loop_handle(&self) -> Self::LoopHandle { - EventLoopHandle::new(Rc::downgrade(&self.inner)) - } - - fn with_component(&self, name: &str, mut f: F) - where - F: FnMut(&ComponentInstance), - { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - if self.surfaces.contains_key(name) { - for surface in system.app_state().windows_by_shell_name(name) { - f(surface.component_instance()); - } - } - } - - fn with_all_components(&self, mut f: F) - where - F: FnMut(&str, &ComponentInstance), - { - let facade = self.inner.borrow(); - let system = facade.inner_ref(); - - for name in self.surfaces.keys() { - for surface in system.app_state().windows_by_shell_name(name) { - f(name, surface.component_instance()); - } - } - } - - fn run(&mut self) -> Result<()> { - self.inner.borrow_mut().run()?; - Ok(()) - } -} - -pub struct EventContext<'a> { - app_state: &'a mut AppState, -} - -impl<'a> FromAppState<'a> for EventContext<'a> { - fn from_app_state(app_state: &'a mut AppState) -> Self { - Self { app_state } - } -} - -impl EventContext<'_> { - pub fn get_surface_component(&self, name: &str) -> Option<&ComponentInstance> { - self.app_state - .windows_by_shell_name(name) - .next() - .map(SurfaceState::component_instance) - } - - pub fn all_surface_components(&self) -> impl Iterator { - self.app_state - .all_outputs() - .map(SurfaceState::component_instance) - } - - pub fn render_frame_if_dirty(&mut self) -> Result<()> { - for surface in self.app_state.all_outputs() { - surface.render_frame_if_dirty()?; - } - Ok(()) - } - - #[must_use] - pub fn primary_output_handle(&self) -> Option { - self.app_state.primary_output_handle() - } - - #[must_use] - pub fn active_output_handle(&self) -> Option { - self.app_state.active_output_handle() - } - - pub fn output_registry(&self) -> &OutputRegistry { - self.app_state.output_registry() - } - - pub fn outputs(&self) -> impl Iterator { - self.app_state - .outputs_with_handles() - .map(|(handle, surface)| (handle, surface.component_instance())) - } - - pub fn get_output_component(&self, handle: OutputHandle) -> Option<&ComponentInstance> { - self.app_state - .get_output_by_handle(handle) - .map(SurfaceState::component_instance) - } - - pub fn get_output_info(&self, handle: OutputHandle) -> Option<&OutputInfo> { - self.app_state.get_output_info(handle) - } - - pub fn all_output_info(&self) -> impl Iterator { - self.app_state.all_output_info() - } - - pub fn outputs_with_info(&self) -> impl Iterator { - self.app_state - .outputs_with_info() - .map(|(info, surface)| (info, surface.component_instance())) - } - - #[must_use] - pub fn compilation_result(&self) -> Option> { - self.app_state - .primary_output() - .and_then(SurfaceState::compilation_result) - } -} diff --git a/crates/composition/src/lib.rs b/crates/composition/src/lib.rs index e1f96d0..8527614 100644 --- a/crates/composition/src/lib.rs +++ b/crates/composition/src/lib.rs @@ -2,6 +2,7 @@ mod event_loop; mod layer_surface; +mod popup; mod popup_builder; mod selection; mod selector; @@ -22,18 +23,22 @@ pub use layer_shika_adapters::platform::{slint, slint_interpreter}; pub use layer_shika_domain::entities::output_registry::OutputRegistry; pub use layer_shika_domain::prelude::AnchorStrategy; pub use layer_shika_domain::value_objects::anchor::AnchorEdges; -pub use layer_shika_domain::value_objects::handle::{Handle, SurfaceHandle}; +pub use layer_shika_domain::value_objects::handle::{Handle, PopupHandle, SurfaceHandle}; pub use layer_shika_domain::value_objects::keyboard_interactivity::KeyboardInteractivity; pub use layer_shika_domain::value_objects::layer::Layer; pub use layer_shika_domain::value_objects::output_handle::OutputHandle; pub use layer_shika_domain::value_objects::output_info::{OutputGeometry, OutputInfo}; pub use layer_shika_domain::value_objects::output_policy::OutputPolicy; -pub use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; -pub use layer_shika_domain::value_objects::popup_request::{ - PopupHandle, PopupPlacement, PopupRequest, PopupSize, -}; pub use layer_shika_domain::value_objects::surface_instance_id::SurfaceInstanceId; +pub use layer_shika_domain::value_objects::{ + output_target::OutputTarget, + popup_behavior::{ConstraintAdjustment, OutputMigrationPolicy, PopupBehavior}, + popup_config::PopupConfig, + popup_position::{Alignment, AnchorPoint, Offset, PopupPosition}, + popup_size::PopupSize, +}; pub use layer_surface::{LayerSurfaceHandle, ShellSurfaceConfigHandler}; +pub use popup::PopupShell; pub use popup_builder::PopupBuilder; pub use selection::Selection; pub use selector::{Output, Selector, Surface, SurfaceInfo}; @@ -77,12 +82,12 @@ pub mod prelude { AnchorEdges, AnchorStrategy, CompiledUiSource, DEFAULT_COMPONENT_NAME, DEFAULT_SURFACE_NAME, EventDispatchContext, EventLoopHandle, Handle, IntoValue, KeyboardInteractivity, Layer, LayerSurfaceHandle, Output, OutputGeometry, OutputHandle, - OutputInfo, OutputPolicy, OutputRegistry, PopupBuilder, PopupHandle, PopupPlacement, - PopupPositioningMode, PopupRequest, PopupSize, PopupWindow, Result, Selection, Selector, - Shell, ShellBuilder, ShellConfig, ShellControl, ShellEventContext, ShellEventLoop, - ShellRuntime, ShellSurfaceConfigHandler, Surface, SurfaceComponentConfig, - SurfaceConfigBuilder, SurfaceControlHandle, SurfaceDefinition, SurfaceEntry, SurfaceHandle, - SurfaceInfo, SurfaceMetadata, SurfaceRegistry, + OutputInfo, OutputPolicy, OutputRegistry, PopupBuilder, PopupConfig, PopupHandle, + PopupPosition, PopupShell, PopupSize, PopupWindow, Result, Selection, Selector, Shell, + ShellBuilder, ShellConfig, ShellControl, ShellEventContext, ShellEventLoop, ShellRuntime, + ShellSurfaceConfigHandler, Surface, SurfaceComponentConfig, SurfaceConfigBuilder, + SurfaceControlHandle, SurfaceDefinition, SurfaceEntry, SurfaceHandle, SurfaceInfo, + SurfaceMetadata, SurfaceRegistry, }; pub use crate::calloop::{Generic, Interest, Mode, PostAction, RegistrationToken, Timer}; diff --git a/crates/composition/src/popup/mod.rs b/crates/composition/src/popup/mod.rs new file mode 100644 index 0000000..b796f2e --- /dev/null +++ b/crates/composition/src/popup/mod.rs @@ -0,0 +1,44 @@ +use crate::popup_builder::PopupBuilder; +use crate::system::{PopupCommand, ShellCommand, ShellControl}; +use crate::{Error, Result}; +use layer_shika_adapters::platform::calloop::channel; +use layer_shika_domain::errors::DomainError; +use layer_shika_domain::value_objects::handle::PopupHandle; +use layer_shika_domain::value_objects::popup_config::PopupConfig; + +#[derive(Clone)] +pub struct PopupShell { + sender: channel::Sender, +} + +impl PopupShell { + #[must_use] + pub const fn new(sender: channel::Sender) -> Self { + Self { sender } + } + + #[must_use] + pub fn builder(&self, component: impl Into) -> PopupBuilder { + PopupBuilder::new(component).with_shell(self.clone()) + } + + pub fn show(&self, config: PopupConfig) -> Result { + let handle = PopupHandle::new(); + self.sender + .send(ShellCommand::Popup(PopupCommand::Show { handle, config })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send popup show command: channel closed".to_string(), + }) + })?; + Ok(handle) + } + + pub fn close(&self, handle: PopupHandle) -> Result<()> { + ShellControl::new(self.sender.clone()).close_popup(handle) + } + + pub fn resize_fixed(&self, handle: PopupHandle, width: f32, height: f32) -> Result<()> { + ShellControl::new(self.sender.clone()).resize_popup(handle, width, height) + } +} diff --git a/crates/composition/src/popup_builder.rs b/crates/composition/src/popup_builder.rs index 19949ae..5b9f7e3 100644 --- a/crates/composition/src/popup_builder.rs +++ b/crates/composition/src/popup_builder.rs @@ -1,33 +1,33 @@ -use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; -use layer_shika_domain::value_objects::popup_request::{PopupPlacement, PopupRequest, PopupSize}; +use crate::popup::PopupShell; +use crate::{Error, Result}; +use layer_shika_domain::dimensions::LogicalRect; +use layer_shika_domain::errors::DomainError; +use layer_shika_domain::value_objects::handle::PopupHandle; +use layer_shika_domain::value_objects::output_target::OutputTarget; +use layer_shika_domain::value_objects::popup_behavior::ConstraintAdjustment; +use layer_shika_domain::value_objects::popup_config::PopupConfig; +use layer_shika_domain::value_objects::popup_position::{ + Alignment, AnchorPoint, Offset, PopupPosition, +}; +use layer_shika_domain::value_objects::popup_size::PopupSize; -/// Builder for configuring popup windows +/// Builder for configuring popups /// -/// This is a convenience wrapper around `PopupRequest::builder()` that provides -/// a fluent API for configuring popups. Once built, pass the resulting `PopupRequest` -/// to `ShellControl::show_popup()` from within a callback. +/// Produces a [`PopupConfig`] and can show it via [`PopupShell`]. /// /// # Example /// ```rust,ignore /// shell.on("Main", "open_menu", |control| { -/// let request = PopupBuilder::new("MenuPopup") -/// .relative_to_cursor() -/// .anchor_top_left() +/// let popup_handle = control.popups().builder("MenuPopup") +/// .at_cursor() /// .grab(true) /// .close_on("menu_closed") -/// .build(); -/// -/// control.show_popup(&request)?; +/// .show()?; /// }); /// ``` pub struct PopupBuilder { - component: String, - reference: PopupPlacement, - anchor: PopupPositioningMode, - size: PopupSize, - grab: bool, - close_callback: Option, - resize_callback: Option, + shell: Option, + config: PopupConfig, } impl PopupBuilder { @@ -35,170 +35,189 @@ impl PopupBuilder { #[must_use] pub fn new(component: impl Into) -> Self { Self { - component: component.into(), - reference: PopupPlacement::AtCursor, - anchor: PopupPositioningMode::TopLeft, - size: PopupSize::Content, - grab: false, - close_callback: None, - resize_callback: None, + shell: None, + config: PopupConfig::new(component), } } - /// Positions the popup at the current cursor location #[must_use] - pub fn relative_to_cursor(mut self) -> Self { - self.reference = PopupPlacement::AtCursor; + pub(crate) fn with_shell(mut self, shell: PopupShell) -> Self { + self.shell = Some(shell); self } - /// Positions the popup at the specified coordinates #[must_use] - pub fn relative_to_point(mut self, x: f32, y: f32) -> Self { - self.reference = PopupPlacement::AtPosition { x, y }; + pub fn position(mut self, position: PopupPosition) -> Self { + self.config.position = position; self } - /// Positions the popup relative to a rectangular area #[must_use] - pub fn relative_to_rect(mut self, x: f32, y: f32, w: f32, h: f32) -> Self { - self.reference = PopupPlacement::AtRect { x, y, w, h }; + pub fn at_cursor(self) -> Self { + self.position(PopupPosition::Cursor { + offset: Offset::default(), + }) + } + + #[must_use] + pub fn at_position(self, x: f32, y: f32) -> Self { + self.position(PopupPosition::Absolute { x, y }) + } + + #[must_use] + pub fn centered(self) -> Self { + self.position(PopupPosition::Centered { + offset: Offset::default(), + }) + } + + #[must_use] + pub fn relative_to_rect( + self, + rect: LogicalRect, + anchor: AnchorPoint, + alignment: Alignment, + ) -> Self { + self.position(PopupPosition::Element { + rect, + anchor, + alignment, + }) + } + + #[must_use] + pub fn offset(mut self, x: f32, y: f32) -> Self { + match &mut self.config.position { + PopupPosition::Absolute { x: abs_x, y: abs_y } => { + *abs_x += x; + *abs_y += y; + } + PopupPosition::Cursor { offset } + | PopupPosition::Centered { offset } + | PopupPosition::RelativeToParent { offset, .. } => { + offset.x += x; + offset.y += y; + } + PopupPosition::Element { .. } => { + self.config.position = PopupPosition::Cursor { + offset: Offset { x, y }, + }; + } + } self } - /// Sets the anchor point for positioning the popup #[must_use] - pub const fn anchor(mut self, anchor: PopupPositioningMode) -> Self { - self.anchor = anchor; + pub fn size(mut self, size: PopupSize) -> Self { + self.config.size = size; self } - /// Anchors popup to top-left corner #[must_use] - pub fn anchor_top_left(mut self) -> Self { - self.anchor = PopupPositioningMode::TopLeft; + pub fn fixed_size(self, width: f32, height: f32) -> Self { + self.size(PopupSize::Fixed { width, height }) + } + + #[must_use] + pub fn min_size(self, width: f32, height: f32) -> Self { + self.size(PopupSize::Minimum { width, height }) + } + + #[must_use] + pub fn max_size(self, width: f32, height: f32) -> Self { + self.size(PopupSize::Maximum { width, height }) + } + + #[must_use] + pub fn content_sized(self) -> Self { + self.size(PopupSize::Content) + } + + #[must_use] + pub fn grab(mut self, enable: bool) -> Self { + self.config.behavior.grab = enable; self } - /// Anchors popup to top-center #[must_use] - pub fn anchor_top_center(mut self) -> Self { - self.anchor = PopupPositioningMode::TopCenter; + pub fn modal(mut self, enable: bool) -> Self { + self.config.behavior.modal = enable; self } - /// Anchors popup to top-right corner #[must_use] - pub fn anchor_top_right(mut self) -> Self { - self.anchor = PopupPositioningMode::TopRight; + pub fn close_on_click_outside(mut self) -> Self { + self.config.behavior.close_on_click_outside = true; self } - /// Anchors popup to center-left #[must_use] - pub fn anchor_center_left(mut self) -> Self { - self.anchor = PopupPositioningMode::CenterLeft; + pub fn close_on_escape(mut self) -> Self { + self.config.behavior.close_on_escape = true; self } - /// Anchors popup to center #[must_use] - pub fn anchor_center(mut self) -> Self { - self.anchor = PopupPositioningMode::Center; + pub fn constraint_adjustment(mut self, adjustment: ConstraintAdjustment) -> Self { + self.config.behavior.constraint_adjustment = adjustment; self } - /// Anchors popup to center-right #[must_use] - pub fn anchor_center_right(mut self) -> Self { - self.anchor = PopupPositioningMode::CenterRight; + pub fn on_output(mut self, target: OutputTarget) -> Self { + self.config.output = target; self } - /// Anchors popup to bottom-left corner #[must_use] - pub fn anchor_bottom_left(mut self) -> Self { - self.anchor = PopupPositioningMode::BottomLeft; + pub fn on_primary(self) -> Self { + self.on_output(OutputTarget::Primary) + } + + #[must_use] + pub fn on_active(self) -> Self { + self.on_output(OutputTarget::Active) + } + + #[must_use] + pub fn parent(mut self, parent: PopupHandle) -> Self { + self.config.parent = Some(parent); self } - /// Anchors popup to bottom-center #[must_use] - pub fn anchor_bottom_center(mut self) -> Self { - self.anchor = PopupPositioningMode::BottomCenter; + pub const fn z_index(mut self, index: i32) -> Self { + self.config.z_index = index; self } - /// Anchors popup to bottom-right corner - #[must_use] - pub fn anchor_bottom_right(mut self) -> Self { - self.anchor = PopupPositioningMode::BottomRight; - self - } - - /// Sets the popup size strategy - /// - /// Use `PopupSize::Content` for auto-sizing or `PopupSize::Fixed { w, h }` for explicit dimensions. - #[must_use] - pub const fn size(mut self, size: PopupSize) -> Self { - self.size = size; - self - } - - /// Sets a fixed size for the popup - #[must_use] - pub fn fixed_size(mut self, w: f32, h: f32) -> Self { - self.size = PopupSize::Fixed { w, h }; - self - } - - /// Uses content-based sizing for the popup - #[must_use] - pub fn content_size(mut self) -> Self { - self.size = PopupSize::Content; - self - } - - /// Enables or disables keyboard/pointer grab for modal behavior - #[must_use] - pub const fn grab(mut self, enable: bool) -> Self { - self.grab = enable; - self - } - - /// Registers a callback that will close the popup when invoked #[must_use] pub fn close_on(mut self, callback_name: impl Into) -> Self { - self.close_callback = Some(callback_name.into()); + self.config.close_callback = Some(callback_name.into()); self } - /// Registers a callback that will resize the popup when invoked #[must_use] pub fn resize_on(mut self, callback_name: impl Into) -> Self { - self.resize_callback = Some(callback_name.into()); + self.config.resize_callback = Some(callback_name.into()); self } - /// Builds the popup request - /// - /// After building, pass the request to `ShellControl::show_popup()` to display the popup. #[must_use] - pub fn build(self) -> PopupRequest { - let mut builder = PopupRequest::builder(self.component.clone()) - .placement(self.reference) - .size(self.size) - .mode(self.anchor) - .grab(self.grab); + pub fn build(self) -> PopupConfig { + self.config + } - if let Some(ref close_cb) = self.close_callback { - builder = builder.close_on(close_cb.clone()); - } + pub fn show(self) -> Result { + let Some(shell) = self.shell else { + return Err(Error::Domain(DomainError::Configuration { + message: "PopupBuilder::show() requires a builder created via `shell.popups().builder(...)`".to_string(), + })); + }; + shell.show(self.config) + } - if let Some(ref resize_cb) = self.resize_callback { - builder = builder.resize_on(resize_cb.clone()); - } - - builder.build() + pub fn show_with_shell(self, shell: &PopupShell) -> Result { + shell.show(self.build()) } } diff --git a/crates/composition/src/shell.rs b/crates/composition/src/shell.rs index f001762..fe859db 100644 --- a/crates/composition/src/shell.rs +++ b/crates/composition/src/shell.rs @@ -582,8 +582,8 @@ impl Shell { _control: &ShellControl, ) { match command { - PopupCommand::Show(request) => { - if let Err(e) = ctx.show_popup(&request) { + PopupCommand::Show { handle, config } => { + if let Err(e) = ctx.show_popup(handle, &config) { log::error!("Failed to show popup: {}", e); } } @@ -764,6 +764,12 @@ impl Shell { ShellControl::new(self.command_sender.clone()) } + /// Access popup management API + #[must_use] + pub fn popups(&self) -> crate::PopupShell { + self.control().popups() + } + /// Returns the names of all registered surfaces pub fn surface_names(&self) -> Vec<&str> { self.registry.surface_names() diff --git a/crates/composition/src/system.rs b/crates/composition/src/system.rs index e9990d5..f91c8eb 100644 --- a/crates/composition/src/system.rs +++ b/crates/composition/src/system.rs @@ -14,19 +14,22 @@ use layer_shika_domain::prelude::{ AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor, }; use layer_shika_domain::value_objects::dimensions::{PopupDimensions, SurfaceDimension}; +use layer_shika_domain::value_objects::handle::PopupHandle; use layer_shika_domain::value_objects::handle::SurfaceHandle; use layer_shika_domain::value_objects::output_handle::OutputHandle; use layer_shika_domain::value_objects::output_info::OutputInfo; -use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; -use layer_shika_domain::value_objects::popup_request::{ - PopupHandle, PopupPlacement, PopupRequest, PopupSize, -}; +use layer_shika_domain::value_objects::popup_config::PopupConfig; +use layer_shika_domain::value_objects::popup_position::PopupPosition; +use layer_shika_domain::value_objects::popup_size::PopupSize; use layer_shika_domain::value_objects::surface_instance_id::SurfaceInstanceId; use std::cell::Cell; use std::rc::Rc; pub enum PopupCommand { - Show(PopupRequest), + Show { + handle: PopupHandle, + config: PopupConfig, + }, Close(PopupHandle), Resize { handle: PopupHandle, @@ -157,41 +160,9 @@ impl CallbackContext { .surface_by_name_and_output(&self.surface_name, self.output_handle()) } - /// Shows a popup from a popup request - /// - /// Convenience method that forwards to the underlying `ShellControl`. - /// See [`ShellControl::show_popup`] for full documentation. - pub fn show_popup(&self, request: &PopupRequest) -> Result<()> { - self.control.show_popup(request) - } - - /// Shows a popup at the current cursor position - /// - /// Convenience method that forwards to the underlying `ShellControl`. - /// See [`ShellControl::show_popup_at_cursor`] for full documentation. - pub fn show_popup_at_cursor(&self, component: impl Into) -> Result<()> { - self.control.show_popup_at_cursor(component) - } - - /// Shows a popup centered on screen - /// - /// Convenience method that forwards to the underlying `ShellControl`. - /// See [`ShellControl::show_popup_centered`] for full documentation. - pub fn show_popup_centered(&self, component: impl Into) -> Result<()> { - self.control.show_popup_centered(component) - } - - /// Shows a popup at the specified absolute position - /// - /// Convenience method that forwards to the underlying `ShellControl`. - /// See [`ShellControl::show_popup_at_position`] for full documentation. - pub fn show_popup_at_position( - &self, - component: impl Into, - x: f32, - y: f32, - ) -> Result<()> { - self.control.show_popup_at_position(component, x, y) + #[must_use] + pub fn popups(&self) -> crate::PopupShell { + self.control.popups() } /// Closes a specific popup by its handle @@ -224,132 +195,21 @@ impl ShellControl { Self { sender } } - /// Shows a popup from a popup request - /// - /// This is the primary API for showing popups from Slint callbacks. Popups are - /// transient windows that appear above the main surface, commonly used for menus, - /// tooltips, dropdowns, and other temporary UI elements. - /// - /// # Content-Based Sizing - /// - /// When using `PopupSize::Content`, you must configure a resize callback via - /// `resize_on()` to enable automatic resizing. The popup component should use a - /// `Timer` with `interval: 1ms` to invoke the resize callback after initialization, - /// ensuring the component is initialized before callback invocation. This allows the - /// popup to reposition itself to fit the content. See the `popup-demo` example. - /// - /// # Example - /// - /// ```rust,ignore - /// shell.on("Main", "open_menu", |control| { - /// let request = PopupRequest::builder("MenuPopup") - /// .placement(PopupPlacement::at_cursor()) - /// .grab(true) - /// .close_on("menu_closed") - /// .build(); - /// - /// control.show_popup(&request)?; - /// Value::Void - /// }); - /// ``` - /// - /// # See Also - /// - /// - [`show_popup_at_cursor`](Self::show_popup_at_cursor) - Convenience method for cursor-positioned popups - /// - [`show_popup_centered`](Self::show_popup_centered) - Convenience method for centered popups - /// - [`show_popup_at_position`](Self::show_popup_at_position) - Convenience method for absolute positioning - /// - [`PopupRequest`] - Full popup configuration options - /// - [`PopupBuilder`] - Fluent API for building popup requests - pub fn show_popup(&self, request: &PopupRequest) -> Result<()> { - self.sender - .send(ShellCommand::Popup(PopupCommand::Show(request.clone()))) - .map_err(|_| { - Error::Domain(DomainError::Configuration { - message: "Failed to send popup show command: channel closed".to_string(), - }) - }) - } - - /// Shows a popup at the current cursor position - /// - /// Convenience method for showing a popup at the cursor with default settings. - /// For more control over popup positioning, sizing, and behavior, use - /// [`show_popup`](Self::show_popup) with a [`PopupRequest`]. - /// - /// # Example - /// - /// ```rust,ignore - /// shell.on("Main", "context_menu", |control| { - /// control.show_popup_at_cursor("ContextMenu")?; - /// Value::Void - /// }); - /// ``` - pub fn show_popup_at_cursor(&self, component: impl Into) -> Result<()> { - let request = PopupRequest::builder(component.into()) - .placement(PopupPlacement::AtCursor) - .build(); - self.show_popup(&request) - } - - /// Shows a popup centered on screen - /// - /// Convenience method for showing a centered popup. Useful for dialogs - /// and modal content that should appear in the middle of the screen. - /// - /// # Example - /// - /// ```rust,ignore - /// shell.on("Main", "show_dialog", |control| { - /// control.show_popup_centered("ConfirmDialog")?; - /// Value::Void - /// }); - /// ``` - pub fn show_popup_centered(&self, component: impl Into) -> Result<()> { - let request = PopupRequest::builder(component.into()) - .placement(PopupPlacement::AtCursor) - .mode(PopupPositioningMode::Center) - .build(); - self.show_popup(&request) - } - - /// Shows a popup at the specified absolute position - /// - /// Convenience method for showing a popup at an exact screen coordinate. - /// The position is in logical pixels relative to the surface origin. - /// - /// # Example - /// - /// ```rust,ignore - /// shell.on("Main", "show_tooltip", |control| { - /// control.show_popup_at_position("Tooltip", 100.0, 50.0)?; - /// Value::Void - /// }); - /// ``` - pub fn show_popup_at_position( - &self, - component: impl Into, - x: f32, - y: f32, - ) -> Result<()> { - let request = PopupRequest::builder(component.into()) - .placement(PopupPlacement::AtPosition { x, y }) - .build(); - self.show_popup(&request) + #[must_use] + pub fn popups(&self) -> crate::PopupShell { + crate::PopupShell::new(self.sender.clone()) } /// Closes a specific popup by its handle /// /// Use this when you need to close a specific popup that you opened previously. - /// The handle is returned by [`show_popup`](Self::show_popup) and related methods. - /// - /// For closing popups from within the popup itself, consider using the - /// `close_on` callback configuration in [`PopupRequest`] instead. + /// The handle is returned by [`crate::PopupShell::show`]. /// /// # Example /// /// ```rust,ignore /// // Store handle when showing popup - /// let handle = context.show_popup(&request)?; + /// let handle = context.popups().builder("MenuPopup").show()?; /// /// // Later, close it /// control.close_popup(handle)?; @@ -370,7 +230,7 @@ impl ShellControl { /// in response to content changes or user interaction. /// /// For automatic content-based sizing, use `PopupSize::Content` with the - /// `resize_on` callback configuration in [`PopupRequest`] instead. + /// `resize_on` callback configuration in [`PopupConfig`]. /// /// # Example /// @@ -733,6 +593,24 @@ fn extract_dimensions_from_callback(args: &[Value]) -> PopupDimensions { ) } +fn initial_dimensions_from_size(size: &PopupSize) -> (f32, f32) { + match *size { + PopupSize::Fixed { width, height } | PopupSize::Minimum { width, height } => { + (width, height) + } + PopupSize::Range { + min_width, + min_height, + .. + } => (min_width, min_height), + PopupSize::Content | PopupSize::Maximum { .. } | PopupSize::MatchParent => { + // Start with minimal size. Consumer app should register a callback to + // call resize_popup() with the desired dimensions. + (2.0, 2.0) + } + } +} + impl EventDispatchContext<'_> { pub(crate) fn surface_by_instance_mut( &mut self, @@ -883,8 +761,8 @@ impl EventDispatchContext<'_> { /// Resize callbacks (if configured via `resize_on()`) will operate directly /// on the popup manager for immediate updates. #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] - pub fn show_popup(&mut self, req: &PopupRequest) -> Result { - log::info!("show_popup called for component '{}'", req.component); + pub fn show_popup(&mut self, handle: PopupHandle, config: &PopupConfig) -> Result { + log::info!("show_popup called for component '{}'", config.component); let compilation_result = self.compilation_result().ok_or_else(|| { log::error!("No compilation result available"); @@ -895,27 +773,25 @@ impl EventDispatchContext<'_> { log::debug!( "Got compilation result, looking for component '{}'", - req.component + config.component ); let definition = compilation_result - .component(&req.component) + .component(&config.component) .ok_or_else(|| { log::error!( "Component '{}' not found in compilation result", - req.component + config.component ); Error::Domain(DomainError::Configuration { message: format!( "{} component not found in compilation result", - req.component + config.component ), }) })?; - log::debug!("Found component definition for '{}'", req.component); - - self.close_current_popup()?; + log::debug!("Found component definition for '{}'", config.component); let is_using_active = self.app_state.active_output().is_some(); let active_surface = self.active_or_primary_output().ok_or_else(|| { @@ -937,70 +813,49 @@ impl EventDispatchContext<'_> { })?; // For content-based sizing, we need to query the component's preferred size first - let initial_dimensions = match req.size { - PopupSize::Fixed { w, h } => { - log::debug!("Using fixed popup size: {}x{}", w, h); - (w, h) - } - PopupSize::Content => { - log::debug!("Using content-based sizing - starting at 2×2"); - // Start with minimal size. Consumer app should register a callback to - // call resize_popup() with the desired dimensions. - (2.0, 2.0) - } - }; - - let resolved_placement = match req.placement { - PopupPlacement::AtCursor => { - let cursor_pos = active_surface.current_pointer_position(); - log::debug!( - "Resolving AtCursor placement to actual cursor position: ({}, {})", - cursor_pos.x, - cursor_pos.y - ); - PopupPlacement::AtPosition { - x: cursor_pos.x, - y: cursor_pos.y, - } - } - other => other, - }; - - let (ref_x, ref_y) = resolved_placement.position(); + let initial_dimensions = initial_dimensions_from_size(&config.size); + let mut resolved_position = config.position.clone(); + if let PopupPosition::Cursor { offset } = config.position { + let cursor_pos = active_surface.current_pointer_position(); + resolved_position = PopupPosition::Absolute { + x: cursor_pos.x + offset.x, + y: cursor_pos.y + offset.y, + }; + } log::debug!( - "Creating popup for '{}' with dimensions {}x{} at position ({}, {}), mode: {:?}", - req.component, + "Creating popup for '{}' with dimensions {}x{} at position {:?}", + config.component, initial_dimensions.0, initial_dimensions.1, - ref_x, - ref_y, - req.mode + resolved_position ); - // Create a new request with resolved placement - let resolved_request = PopupRequest { - component: req.component.clone(), - placement: resolved_placement, - size: req.size, - mode: req.mode, - grab: req.grab, - close_callback: req.close_callback.clone(), - resize_callback: req.resize_callback.clone(), + let resolved_config = PopupConfig { + component: config.component.clone(), + position: resolved_position, + size: config.size.clone(), + behavior: config.behavior.clone(), + output: config.output.clone(), + parent: config.parent, + z_index: config.z_index, + close_callback: config.close_callback.clone(), + resize_callback: config.resize_callback.clone(), }; - let popup_handle = popup_manager.request_popup( - resolved_request, + popup_manager.request_popup( + handle, + resolved_config, initial_dimensions.0, initial_dimensions.1, ); let (instance, popup_key_cell) = - Self::create_popup_instance(&definition, &popup_manager, req)?; + Self::create_popup_instance(&definition, &popup_manager, config)?; - popup_key_cell.set(popup_handle.key()); + popup_key_cell.set(handle.key()); - if let Some(popup_surface) = popup_manager.get_popup_window(popup_handle.key()) { + if let Some(popup_surface) = popup_manager.get_popup_window(handle.key()) { popup_surface.set_component_instance(instance); } else { return Err(Error::Domain(DomainError::Configuration { @@ -1008,7 +863,7 @@ impl EventDispatchContext<'_> { })); } - Ok(popup_handle) + Ok(handle) } /// Closes a popup by its handle @@ -1021,16 +876,6 @@ impl EventDispatchContext<'_> { Ok(()) } - /// Closes the currently active popup - pub fn close_current_popup(&mut self) -> Result<()> { - if let Some(active_surface) = self.active_or_primary_output() { - if let Some(popup_manager) = active_surface.popup_manager() { - popup_manager.close_current_popup(); - } - } - Ok(()) - } - /// Resizes a popup to the specified dimensions pub fn resize_popup(&mut self, handle: PopupHandle, width: f32, height: f32) -> Result<()> { let active_surface = self.active_or_primary_output().ok_or_else(|| { @@ -1045,39 +890,25 @@ impl EventDispatchContext<'_> { }) })?; - let Some((request, _serial)) = popup_manager.get_popup_info(handle.key()) else { + if let Some(popup_surface) = popup_manager.get_popup_window(handle.key()) { + popup_surface.request_resize(width, height); + + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_possible_wrap)] + let logical_width = width as i32; + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_possible_wrap)] + let logical_height = height as i32; + + popup_manager.update_popup_viewport(handle.key(), logical_width, logical_height); + popup_manager.commit_popup_surface(handle.key()); log::debug!( - "Ignoring resize request for non-existent popup with handle {:?}", - handle + "Updated popup viewport to logical size: {}x{} (from resize to {}x{})", + logical_width, + logical_height, + width, + height ); - return Ok(()); - }; - - let current_size = request.size.dimensions(); - let size_changed = - current_size.is_none_or(|(w, h)| (w - width).abs() > 0.01 || (h - height).abs() > 0.01); - - if size_changed { - if let Some(popup_surface) = popup_manager.get_popup_window(handle.key()) { - popup_surface.request_resize(width, height); - - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_possible_wrap)] - let logical_width = width as i32; - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_possible_wrap)] - let logical_height = height as i32; - - popup_manager.update_popup_viewport(handle.key(), logical_width, logical_height); - popup_manager.commit_popup_surface(handle.key()); - log::debug!( - "Updated popup viewport to logical size: {}x{} (from resize to {}x{})", - logical_width, - logical_height, - width, - height - ); - } } Ok(()) @@ -1086,7 +917,7 @@ impl EventDispatchContext<'_> { fn create_popup_instance( definition: &ComponentDefinition, popup_manager: &Rc, - req: &PopupRequest, + config: &PopupConfig, ) -> Result<(ComponentInstance, Rc>)> { let instance = definition.create().map_err(|e| { Error::Domain(DomainError::Configuration { @@ -1096,7 +927,7 @@ impl EventDispatchContext<'_> { let popup_key_cell = Rc::new(Cell::new(0)); - Self::register_popup_callbacks(&instance, popup_manager, &popup_key_cell, req)?; + Self::register_popup_callbacks(&instance, popup_manager, &popup_key_cell, config)?; instance.show().map_err(|e| { Error::Domain(DomainError::Configuration { @@ -1111,13 +942,18 @@ impl EventDispatchContext<'_> { instance: &ComponentInstance, popup_manager: &Rc, popup_key_cell: &Rc>, - req: &PopupRequest, + config: &PopupConfig, ) -> Result<()> { - if let Some(close_callback_name) = &req.close_callback { - Self::register_close_callback(instance, popup_manager, close_callback_name)?; + if let Some(close_callback_name) = &config.close_callback { + Self::register_close_callback( + instance, + popup_manager, + popup_key_cell, + close_callback_name, + )?; } - if let Some(resize_callback_name) = &req.resize_callback { + if let Some(resize_callback_name) = &config.resize_callback { Self::register_resize_direct( instance, popup_manager, @@ -1132,13 +968,16 @@ impl EventDispatchContext<'_> { fn register_close_callback( instance: &ComponentInstance, popup_manager: &Rc, + popup_key_cell: &Rc>, callback_name: &str, ) -> Result<()> { let popup_manager_weak = Rc::downgrade(popup_manager); + let key_cell = Rc::clone(popup_key_cell); instance .set_callback(callback_name, move |_| { if let Some(popup_manager) = popup_manager_weak.upgrade() { - popup_manager.close_current_popup(); + let key = key_cell.get(); + popup_manager.close(PopupHandle::from_raw(key)).ok(); } Value::Void }) diff --git a/crates/domain/src/dimensions.rs b/crates/domain/src/dimensions.rs index 569c5d4..545912c 100644 --- a/crates/domain/src/dimensions.rs +++ b/crates/domain/src/dimensions.rs @@ -193,7 +193,8 @@ pub struct LogicalPosition { } impl LogicalPosition { - pub fn new(x: f32, y: f32) -> Self { + #[must_use] + pub const fn new(x: f32, y: f32) -> Self { Self { x, y } } @@ -224,6 +225,57 @@ impl Default for LogicalPosition { } } +/// Rectangle in logical pixels +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct LogicalRect { + x: f32, + y: f32, + width: f32, + height: f32, +} + +impl LogicalRect { + #[must_use] + pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self { + Self { + x, + y, + width, + height, + } + } + + #[must_use] + pub const fn x(&self) -> f32 { + self.x + } + + #[must_use] + pub const fn y(&self) -> f32 { + self.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 origin(&self) -> LogicalPosition { + LogicalPosition::new(self.x, self.y) + } + + #[must_use] + pub const fn size(&self) -> LogicalSize { + LogicalSize::from_raw(self.width, self.height) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PhysicalPosition { x: i32, diff --git a/crates/domain/src/entities/mod.rs b/crates/domain/src/entities/mod.rs index 1589402..6f59086 100644 --- a/crates/domain/src/entities/mod.rs +++ b/crates/domain/src/entities/mod.rs @@ -1 +1,2 @@ pub mod output_registry; +pub mod popup_tree; diff --git a/crates/domain/src/entities/popup_tree.rs b/crates/domain/src/entities/popup_tree.rs new file mode 100644 index 0000000..acad6be --- /dev/null +++ b/crates/domain/src/entities/popup_tree.rs @@ -0,0 +1,127 @@ +use crate::value_objects::handle::PopupHandle; +use std::collections::HashMap; + +#[derive(Debug, Default, Clone)] +pub struct PopupTree { + root_popups: Vec, + relationships: HashMap, +} + +#[derive(Debug, Clone)] +pub struct PopupNode { + parent: Option, + children: Vec, + depth: usize, +} + +impl PopupTree { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + pub fn add_popup(&mut self, handle: PopupHandle, parent: Option) { + if self.relationships.contains_key(&handle) { + return; + } + + let depth = parent + .and_then(|p| self.relationships.get(&p).map(|n| n.depth + 1)) + .unwrap_or(0); + + self.relationships.insert( + handle, + PopupNode { + parent, + children: Vec::new(), + depth, + }, + ); + + if let Some(parent) = parent { + if let Some(parent_node) = self.relationships.get_mut(&parent) { + parent_node.children.push(handle); + } + } else { + self.root_popups.push(handle); + } + } + + /// Removes a popup and returns the handles that should be cascade-removed. + /// + /// The returned list contains `handle` and all descendants in a stable order + /// (parents before children). + pub fn remove_popup(&mut self, handle: PopupHandle) -> Vec { + let mut cascade = Vec::new(); + self.collect_descendants(handle, &mut cascade); + + if let Some(node) = self.relationships.remove(&handle) { + if let Some(parent) = node.parent { + if let Some(parent_node) = self.relationships.get_mut(&parent) { + parent_node.children.retain(|&c| c != handle); + } + } else { + self.root_popups.retain(|&h| h != handle); + } + } + + for removed in cascade.iter().copied().filter(|&h| h != handle) { + if let Some(node) = self.relationships.remove(&removed) { + if let Some(parent) = node.parent { + if let Some(parent_node) = self.relationships.get_mut(&parent) { + parent_node.children.retain(|&c| c != removed); + } + } else { + self.root_popups.retain(|&h| h != removed); + } + } + } + + cascade + } + + #[must_use] + pub fn get_children(&self, handle: PopupHandle) -> &[PopupHandle] { + self.relationships + .get(&handle) + .map_or(&[], |n| n.children.as_slice()) + } + + #[must_use] + pub fn get_parent(&self, handle: PopupHandle) -> Option { + self.relationships.get(&handle).and_then(|n| n.parent) + } + + #[must_use] + pub fn get_ancestors(&self, mut handle: PopupHandle) -> Vec { + let mut ancestors = Vec::new(); + while let Some(parent) = self.get_parent(handle) { + ancestors.push(parent); + handle = parent; + } + ancestors + } + + #[must_use] + pub fn is_descendant(&self, mut child: PopupHandle, ancestor: PopupHandle) -> bool { + while let Some(parent) = self.get_parent(child) { + if parent == ancestor { + return true; + } + child = parent; + } + false + } + + #[must_use] + pub fn roots(&self) -> &[PopupHandle] { + &self.root_popups + } + + fn collect_descendants(&self, handle: PopupHandle, out: &mut Vec) { + out.push(handle); + for &child in self.get_children(handle) { + self.collect_descendants(child, out); + } + } +} diff --git a/crates/domain/src/prelude.rs b/crates/domain/src/prelude.rs index f29ce10..df891b5 100644 --- a/crates/domain/src/prelude.rs +++ b/crates/domain/src/prelude.rs @@ -2,9 +2,10 @@ pub use crate::config::SurfaceConfig; pub use crate::dimensions::{ - LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, ScaleFactor, + LogicalPosition, LogicalRect, LogicalSize, PhysicalPosition, PhysicalSize, ScaleFactor, }; pub use crate::entities::output_registry::OutputRegistry; +pub use crate::entities::popup_tree::PopupTree; pub use crate::errors::{DomainError, Result}; pub use crate::surface_dimensions::SurfaceDimensions; pub use crate::value_objects::anchor::AnchorEdges; @@ -16,4 +17,11 @@ pub use crate::value_objects::layer::Layer; pub use crate::value_objects::margins::Margins; pub use crate::value_objects::output_info::{OutputGeometry, OutputInfo}; pub use crate::value_objects::output_policy::OutputPolicy; +pub use crate::value_objects::output_target::OutputTarget; +pub use crate::value_objects::popup_behavior::{ + ConstraintAdjustment, OutputMigrationPolicy, PopupBehavior, +}; +pub use crate::value_objects::popup_config::PopupConfig; +pub use crate::value_objects::popup_position::{Alignment, AnchorPoint, Offset, PopupPosition}; +pub use crate::value_objects::popup_size::PopupSize; pub use crate::value_objects::ui_source::UiSource; diff --git a/crates/domain/src/value_objects/mod.rs b/crates/domain/src/value_objects/mod.rs index 7fcb45e..f7da72f 100644 --- a/crates/domain/src/value_objects/mod.rs +++ b/crates/domain/src/value_objects/mod.rs @@ -8,8 +8,10 @@ pub mod margins; pub mod output_handle; pub mod output_info; pub mod output_policy; +pub mod output_target; +pub mod popup_behavior; pub mod popup_config; -pub mod popup_positioning_mode; -pub mod popup_request; +pub mod popup_position; +pub mod popup_size; pub mod surface_instance_id; pub mod ui_source; diff --git a/crates/domain/src/value_objects/output_target.rs b/crates/domain/src/value_objects/output_target.rs new file mode 100644 index 0000000..e3b6895 --- /dev/null +++ b/crates/domain/src/value_objects/output_target.rs @@ -0,0 +1,23 @@ +use crate::value_objects::output_handle::OutputHandle; + +/// Explicit output targeting +#[derive(Debug, Clone)] +pub enum OutputTarget { + /// Use primary output + Primary, + + /// Use currently active output + Active, + + /// Use specific output by handle + Handle(OutputHandle), + + /// Use output by name + Named(String), + + /// Inherit from parent (for child popups) + InheritFromParent, + + /// Use output containing cursor + ContainingCursor, +} diff --git a/crates/domain/src/value_objects/popup_behavior.rs b/crates/domain/src/value_objects/popup_behavior.rs new file mode 100644 index 0000000..46a9bc9 --- /dev/null +++ b/crates/domain/src/value_objects/popup_behavior.rs @@ -0,0 +1,60 @@ +/// Wayland-compatible constraint adjustment +/// +/// Maps directly to `XdgPositioner` `constraint_adjustment` flags. Compositor +/// handles the actual repositioning logic. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ConstraintAdjustment { + /// No adjustment (manual clamping) + #[default] + None, + + /// Slide along axis to fit + Slide, + + /// Flip to opposite side if doesn't fit + Flip, + + /// Resize to fit + Resize, + + /// Combination strategies + SlideAndResize, + FlipAndSlide, + All, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum OutputMigrationPolicy { + /// Move to primary when output disconnects + #[default] + MigrateToPrimary, + + /// Move to currently active output + MigrateToActive, + + /// Close when output disconnects + Close, +} + +/// Behavioral configuration +#[derive(Debug, Clone, Default)] +#[allow(clippy::struct_excessive_bools)] +pub struct PopupBehavior { + /// Grab keyboard and pointer input + pub grab: bool, + + /// Modal (blocks interaction with parent) + pub modal: bool, + + /// Auto-close on outside click + pub close_on_click_outside: bool, + + /// Auto-close on escape key + pub close_on_escape: bool, + + /// How to handle screen edge constraints + pub constraint_adjustment: ConstraintAdjustment, + + /// How to handle output disconnect for this popup + pub output_migration: OutputMigrationPolicy, +} diff --git a/crates/domain/src/value_objects/popup_config.rs b/crates/domain/src/value_objects/popup_config.rs index 6c62631..d0c3822 100644 --- a/crates/domain/src/value_objects/popup_config.rs +++ b/crates/domain/src/value_objects/popup_config.rs @@ -1,94 +1,55 @@ -use super::popup_positioning_mode::PopupPositioningMode; -use crate::dimensions::{LogicalPosition, LogicalSize}; -use crate::surface_dimensions::SurfaceDimensions; +use crate::value_objects::handle::PopupHandle; +use crate::value_objects::output_target::OutputTarget; +use crate::value_objects::popup_behavior::PopupBehavior; +use crate::value_objects::popup_position::{Offset, PopupPosition}; +use crate::value_objects::popup_size::PopupSize; -#[derive(Debug, Clone, Copy)] +/// Declarative popup configuration (runtime modifiable) +#[derive(Debug, Clone)] pub struct PopupConfig { - reference_position: LogicalPosition, - dimensions: SurfaceDimensions, - output_bounds: LogicalSize, - positioning_mode: PopupPositioningMode, + /// Component name from compiled Slint file + pub component: String, + + /// Positioning configuration + pub position: PopupPosition, + + /// Size configuration + pub size: PopupSize, + + /// Popup behavior flags + pub behavior: PopupBehavior, + + /// Output targeting + pub output: OutputTarget, + + /// Parent popup (for hierarchical popups) + pub parent: Option, + + /// Z-order relative to siblings + pub z_index: i32, + + /// Callback invoked by the component to request close + pub close_callback: Option, + + /// Callback invoked by the component to request resize (content-sizing) + pub resize_callback: Option, } impl PopupConfig { - pub fn new( - reference_x: f32, - reference_y: f32, - dimensions: SurfaceDimensions, - positioning_mode: PopupPositioningMode, - output_bounds: LogicalSize, - ) -> Self { + #[must_use] + pub fn new(component: impl Into) -> Self { Self { - reference_position: LogicalPosition::new(reference_x, reference_y), - dimensions, - output_bounds, - positioning_mode, + component: component.into(), + position: PopupPosition::Cursor { + offset: Offset::default(), + }, + size: PopupSize::default(), + behavior: PopupBehavior::default(), + output: OutputTarget::Active, + parent: None, + z_index: 0, + close_callback: None, + resize_callback: None, } } - - pub const fn reference_position(&self) -> LogicalPosition { - self.reference_position - } - - pub const fn reference_x(&self) -> f32 { - self.reference_position.x() - } - - pub const fn reference_y(&self) -> f32 { - self.reference_position.y() - } - - pub const fn dimensions(&self) -> SurfaceDimensions { - self.dimensions - } - - pub fn popup_size(&self) -> LogicalSize { - self.dimensions.logical_size() - } - - pub fn width(&self) -> f32 { - self.dimensions.logical_size().width() - } - - pub fn height(&self) -> f32 { - self.dimensions.logical_size().height() - } - - pub const fn output_bounds(&self) -> LogicalSize { - self.output_bounds - } - - pub const fn positioning_mode(&self) -> PopupPositioningMode { - self.positioning_mode - } - - pub fn calculated_top_left_position(&self) -> LogicalPosition { - let unclamped = self.calculate_unclamped_position(); - self.popup_size() - .clamp_position(unclamped, self.output_bounds) - } - - fn calculate_unclamped_position(&self) -> LogicalPosition { - let x = if self.positioning_mode.center_x() { - self.reference_x() - (self.width() / 2.0) - } else { - self.reference_x() - }; - - let y = if self.positioning_mode.center_y() { - self.reference_y() - (self.height() / 2.0) - } else { - self.reference_y() - }; - - LogicalPosition::new(x, y) - } - - pub fn calculated_top_left_x(&self) -> f32 { - self.calculated_top_left_position().x() - } - - pub fn calculated_top_left_y(&self) -> f32 { - self.calculated_top_left_position().y() - } } diff --git a/crates/domain/src/value_objects/popup_position.rs b/crates/domain/src/value_objects/popup_position.rs new file mode 100644 index 0000000..36435f7 --- /dev/null +++ b/crates/domain/src/value_objects/popup_position.rs @@ -0,0 +1,58 @@ +use crate::dimensions::LogicalRect; + +#[derive(Debug, Clone)] +pub enum PopupPosition { + /// Absolute position in surface coordinates + Absolute { x: f32, y: f32 }, + + /// Relative to cursor position + Cursor { offset: Offset }, + + /// Relative to a UI element (rect) + Element { + rect: LogicalRect, + anchor: AnchorPoint, + alignment: Alignment, + }, + + /// Relative to parent popup + RelativeToParent { + anchor: AnchorPoint, + alignment: Alignment, + offset: Offset, + }, + + /// Centered on output + Centered { offset: Offset }, +} + +/// 9-point anchor system +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnchorPoint { + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + CenterRight, + BottomLeft, + BottomCenter, + BottomRight, +} + +/// How popup aligns relative to anchor +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Alignment { + /// Popup starts at anchor + Start, + /// Popup centers on anchor + Center, + /// Popup ends at anchor + End, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub struct Offset { + pub x: f32, + pub y: f32, +} diff --git a/crates/domain/src/value_objects/popup_positioning_mode.rs b/crates/domain/src/value_objects/popup_positioning_mode.rs deleted file mode 100644 index aa3487e..0000000 --- a/crates/domain/src/value_objects/popup_positioning_mode.rs +++ /dev/null @@ -1,47 +0,0 @@ -/// Alignment mode for popup positioning -/// -/// Determines how a popup is aligned relative to its placement point. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] -pub enum PopupPositioningMode { - /// Align popup's top-left corner to placement point - #[default] - TopLeft, - /// Center popup horizontally at placement point, top edge aligned - TopCenter, - /// Align popup's top-right corner to placement point - TopRight, - /// Center popup vertically at placement point, left edge aligned - CenterLeft, - /// Center popup both horizontally and vertically at placement point - Center, - /// Center popup vertically at placement point, right edge aligned - CenterRight, - /// Align popup's bottom-left corner to placement point - BottomLeft, - /// Center popup horizontally at placement point, bottom edge aligned - BottomCenter, - /// Align popup's bottom-right corner to placement point - 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, - } - } -} diff --git a/crates/domain/src/value_objects/popup_request.rs b/crates/domain/src/value_objects/popup_request.rs deleted file mode 100644 index 1868582..0000000 --- a/crates/domain/src/value_objects/popup_request.rs +++ /dev/null @@ -1,182 +0,0 @@ -#![allow(clippy::pub_use)] - -pub use super::handle::PopupHandle; -use super::popup_positioning_mode::PopupPositioningMode; - -/// Configuration for showing a popup window -/// -/// Use `PopupRequest::builder()` to construct with fluent API. -#[derive(Debug, Clone)] -pub struct PopupRequest { - pub component: String, - pub placement: PopupPlacement, - pub size: PopupSize, - pub mode: PopupPositioningMode, - pub grab: bool, - pub close_callback: Option, - pub resize_callback: Option, -} - -impl PopupRequest { - #[must_use] - pub fn new( - component: String, - placement: PopupPlacement, - size: PopupSize, - mode: PopupPositioningMode, - ) -> Self { - Self { - component, - placement, - size, - mode, - grab: false, - close_callback: None, - resize_callback: None, - } - } - - #[must_use] - pub fn builder(component: String) -> PopupRequestBuilder { - PopupRequestBuilder::new(component) - } -} - -/// Where to position a popup relative to the surface -#[derive(Debug, Clone, Copy)] -pub enum PopupPlacement { - /// At absolute logical position - AtPosition { x: f32, y: f32 }, - /// At current cursor position - AtCursor, - /// Relative to a logical rectangle - AtRect { x: f32, y: f32, w: f32, h: f32 }, -} - -impl PopupPlacement { - #[must_use] - pub const fn at_position(x: f32, y: f32) -> Self { - Self::AtPosition { x, y } - } - - #[must_use] - pub const fn at_cursor() -> Self { - Self::AtCursor - } - - #[must_use] - pub const fn at_rect(x: f32, y: f32, w: f32, h: f32) -> Self { - Self::AtRect { x, y, w, h } - } - - #[must_use] - pub const fn position(&self) -> (f32, f32) { - match *self { - Self::AtPosition { x, y } | Self::AtRect { x, y, .. } => (x, y), - Self::AtCursor => (0.0, 0.0), - } - } -} - -/// How to size a popup window -#[derive(Debug, Clone, Copy)] -pub enum PopupSize { - /// Fixed width and height in logical pixels - Fixed { w: f32, h: f32 }, - /// Size automatically based on content - Content, -} - -impl PopupSize { - #[must_use] - pub const fn fixed(w: f32, h: f32) -> Self { - Self::Fixed { w, h } - } - - #[must_use] - pub const fn content() -> Self { - Self::Content - } - - #[must_use] - pub const fn dimensions(&self) -> Option<(f32, f32)> { - match *self { - Self::Fixed { w, h } => Some((w, h)), - Self::Content => None, - } - } -} - -pub struct PopupRequestBuilder { - component: String, - placement: PopupPlacement, - size: PopupSize, - mode: PopupPositioningMode, - grab: bool, - close_callback: Option, - resize_callback: Option, -} - -impl PopupRequestBuilder { - #[must_use] - pub fn new(component: String) -> Self { - Self { - component, - placement: PopupPlacement::AtCursor, - size: PopupSize::Content, - mode: PopupPositioningMode::default(), - grab: false, - close_callback: None, - resize_callback: None, - } - } - - #[must_use] - pub const fn placement(mut self, placement: PopupPlacement) -> Self { - self.placement = placement; - self - } - - #[must_use] - pub const fn size(mut self, size: PopupSize) -> Self { - self.size = size; - self - } - - #[must_use] - pub const fn mode(mut self, mode: PopupPositioningMode) -> Self { - self.mode = mode; - self - } - - #[must_use] - pub const fn grab(mut self, grab: bool) -> Self { - self.grab = grab; - self - } - - #[must_use] - pub fn close_on(mut self, callback_name: impl Into) -> Self { - self.close_callback = Some(callback_name.into()); - self - } - - #[must_use] - pub fn resize_on(mut self, callback_name: impl Into) -> Self { - self.resize_callback = Some(callback_name.into()); - self - } - - #[must_use] - pub fn build(self) -> PopupRequest { - PopupRequest { - component: self.component, - placement: self.placement, - size: self.size, - mode: self.mode, - grab: self.grab, - close_callback: self.close_callback, - resize_callback: self.resize_callback, - } - } -} diff --git a/crates/domain/src/value_objects/popup_size.rs b/crates/domain/src/value_objects/popup_size.rs new file mode 100644 index 0000000..ea63141 --- /dev/null +++ b/crates/domain/src/value_objects/popup_size.rs @@ -0,0 +1,26 @@ +#[derive(Debug, Clone, Default)] +pub enum PopupSize { + /// Fixed logical size + Fixed { width: f32, height: f32 }, + + /// Minimum size (can grow with content) + Minimum { width: f32, height: f32 }, + + /// Maximum size (can shrink below content) + Maximum { width: f32, height: f32 }, + + /// Constrained range + Range { + min_width: f32, + min_height: f32, + max_width: f32, + max_height: f32, + }, + + /// Automatic based on content (default: use 2×2 initialization) + #[default] + Content, + + /// Match parent popup size + MatchParent, +} diff --git a/src/lib.rs b/src/lib.rs index c43e234..2b820ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,8 +143,9 @@ pub use shell::{ }; pub use window::{ - AnchorEdges, AnchorStrategy, KeyboardInteractivity, Layer, PopupBuilder, PopupHandle, - PopupPlacement, PopupPositioningMode, PopupRequest, PopupSize, + Alignment, AnchorEdges, AnchorPoint, AnchorStrategy, ConstraintAdjustment, + KeyboardInteractivity, Layer, Offset, OutputTarget, PopupBehavior, PopupBuilder, PopupConfig, + PopupHandle, PopupPosition, PopupShell, PopupSize, }; pub use output::{OutputGeometry, OutputHandle, OutputInfo, OutputPolicy, OutputRegistry}; diff --git a/src/prelude.rs b/src/prelude.rs index 1005e7c..2ecd843 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -16,8 +16,9 @@ pub use crate::shell::{ }; pub use crate::window::{ - AnchorEdges, AnchorStrategy, KeyboardInteractivity, Layer, PopupHandle, PopupPlacement, - PopupPositioningMode, PopupRequest, PopupSize, + Alignment, AnchorEdges, AnchorPoint, AnchorStrategy, ConstraintAdjustment, + KeyboardInteractivity, Layer, Offset, OutputTarget, PopupBehavior, PopupBuilder, PopupConfig, + PopupHandle, PopupPosition, PopupShell, PopupSize, }; pub use crate::output::{OutputGeometry, OutputHandle, OutputInfo, OutputPolicy, OutputRegistry}; diff --git a/src/window.rs b/src/window.rs index 3e6472e..062ad40 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,4 +1,5 @@ pub use layer_shika_composition::{ - AnchorEdges, AnchorStrategy, KeyboardInteractivity, Layer, PopupBuilder, PopupHandle, - PopupPlacement, PopupPositioningMode, PopupRequest, PopupSize, + Alignment, AnchorEdges, AnchorPoint, AnchorStrategy, ConstraintAdjustment, + KeyboardInteractivity, Layer, Offset, OutputTarget, PopupBehavior, PopupBuilder, PopupConfig, + PopupHandle, PopupPosition, PopupShell, PopupSize, };