mirror of
https://codeberg.org/waydeer/layer-shika.git
synced 2025-12-23 10:25:54 +00:00
refactor: make popup full runtime configurable, add missing config
This commit is contained in:
parent
8d47f55ac5
commit
3e9e4cf4b4
24 changed files with 897 additions and 1739 deletions
|
|
@ -2,7 +2,7 @@ use super::renderable_window::{RenderState, RenderableWindow};
|
||||||
use crate::errors::{RenderingError, Result};
|
use crate::errors::{RenderingError, Result};
|
||||||
use crate::wayland::surfaces::popup_manager::OnCloseCallback;
|
use crate::wayland::surfaces::popup_manager::OnCloseCallback;
|
||||||
use core::ops::Deref;
|
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 log::info;
|
||||||
use slint::{
|
use slint::{
|
||||||
PhysicalSize, Window, WindowSize,
|
PhysicalSize, Window, WindowSize,
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,15 @@ use crate::rendering::femtovg::{popup_window::PopupWindow, renderable_window::Re
|
||||||
use crate::wayland::surfaces::display_metrics::{DisplayMetrics, SharedDisplayMetrics};
|
use crate::wayland::surfaces::display_metrics::{DisplayMetrics, SharedDisplayMetrics};
|
||||||
use layer_shika_domain::dimensions::LogicalSize as DomainLogicalSize;
|
use layer_shika_domain::dimensions::LogicalSize as DomainLogicalSize;
|
||||||
use layer_shika_domain::surface_dimensions::SurfaceDimensions;
|
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_config::PopupConfig;
|
||||||
use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode;
|
use layer_shika_domain::value_objects::popup_position::PopupPosition;
|
||||||
use layer_shika_domain::value_objects::popup_request::{PopupHandle, PopupRequest};
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use slint::{platform::femtovg_renderer::FemtoVGRenderer, PhysicalSize, WindowSize};
|
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 smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
backend::ObjectId,
|
backend::ObjectId,
|
||||||
|
|
@ -55,14 +56,13 @@ impl PopupId {
|
||||||
|
|
||||||
pub type OnCloseCallback = Box<dyn Fn(PopupHandle)>;
|
pub type OnCloseCallback = Box<dyn Fn(PopupHandle)>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CreatePopupParams {
|
pub struct CreatePopupParams {
|
||||||
pub last_pointer_serial: u32,
|
pub last_pointer_serial: u32,
|
||||||
pub reference_x: f32,
|
|
||||||
pub reference_y: f32,
|
|
||||||
pub width: f32,
|
pub width: f32,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub positioning_mode: PopupPositioningMode,
|
pub position: PopupPosition,
|
||||||
|
pub constraint_adjustment: ConstraintAdjustment,
|
||||||
pub grab: bool,
|
pub grab: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,8 +102,6 @@ impl PopupContext {
|
||||||
struct ActivePopup {
|
struct ActivePopup {
|
||||||
surface: PopupSurface,
|
surface: PopupSurface,
|
||||||
window: Rc<PopupWindow>,
|
window: Rc<PopupWindow>,
|
||||||
request: PopupRequest,
|
|
||||||
last_serial: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for ActivePopup {
|
impl Drop for ActivePopup {
|
||||||
|
|
@ -116,7 +114,7 @@ impl Drop for ActivePopup {
|
||||||
|
|
||||||
struct PendingPopup {
|
struct PendingPopup {
|
||||||
id: PopupId,
|
id: PopupId,
|
||||||
request: PopupRequest,
|
config: PopupConfig,
|
||||||
width: f32,
|
width: f32,
|
||||||
height: f32,
|
height: f32,
|
||||||
}
|
}
|
||||||
|
|
@ -124,9 +122,7 @@ struct PendingPopup {
|
||||||
struct PopupManagerState {
|
struct PopupManagerState {
|
||||||
popups: HashMap<PopupId, ActivePopup>,
|
popups: HashMap<PopupId, ActivePopup>,
|
||||||
display_metrics: SharedDisplayMetrics,
|
display_metrics: SharedDisplayMetrics,
|
||||||
current_popup_id: Option<PopupId>,
|
pending_popups: VecDeque<PendingPopup>,
|
||||||
pending_popup: Option<PendingPopup>,
|
|
||||||
id_generator: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupManagerState {
|
impl PopupManagerState {
|
||||||
|
|
@ -134,17 +130,9 @@ impl PopupManagerState {
|
||||||
Self {
|
Self {
|
||||||
popups: HashMap::new(),
|
popups: HashMap::new(),
|
||||||
display_metrics,
|
display_metrics,
|
||||||
current_popup_id: None,
|
pending_popups: VecDeque::new(),
|
||||||
pending_popup: None,
|
|
||||||
id_generator: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn allocate_id(&mut self) -> PopupId {
|
|
||||||
let id = PopupId(self.id_generator);
|
|
||||||
self.id_generator += 1;
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PopupManager {
|
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 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,
|
id,
|
||||||
request,
|
config,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
|
||||||
id.to_handle()
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[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
|
self.state
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.pending_popup
|
.pending_popups
|
||||||
.take()
|
.pop_front()
|
||||||
.map(|p| (p.id, p.request, p.width, p.height))
|
.map(|p| (p.id, p.config, p.width, p.height))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn has_pending_popup(&self) -> bool {
|
pub fn has_pending_popup(&self) -> bool {
|
||||||
self.state.borrow().pending_popup.is_some()
|
!self.state.borrow().pending_popups.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -220,25 +214,13 @@ impl PopupManager {
|
||||||
.update_output_size(output_size);
|
.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<usize> {
|
|
||||||
self.state.borrow().current_popup_id.map(PopupId::key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_pending_popup(
|
pub fn create_pending_popup(
|
||||||
self: &Rc<Self>,
|
self: &Rc<Self>,
|
||||||
queue_handle: &QueueHandle<AppState>,
|
queue_handle: &QueueHandle<AppState>,
|
||||||
parent_layer_surface: &ZwlrLayerSurfaceV1,
|
parent_layer_surface: &ZwlrLayerSurfaceV1,
|
||||||
last_pointer_serial: u32,
|
last_pointer_serial: u32,
|
||||||
) -> Result<Rc<PopupWindow>> {
|
) -> Result<Rc<PopupWindow>> {
|
||||||
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 {
|
LayerShikaError::WindowConfiguration {
|
||||||
message: "No pending popup request available".into(),
|
message: "No pending popup request available".into(),
|
||||||
}
|
}
|
||||||
|
|
@ -246,23 +228,21 @@ impl PopupManager {
|
||||||
|
|
||||||
let params = CreatePopupParams {
|
let params = CreatePopupParams {
|
||||||
last_pointer_serial,
|
last_pointer_serial,
|
||||||
reference_x: request.placement.position().0,
|
|
||||||
reference_y: request.placement.position().1,
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
positioning_mode: request.mode,
|
position: config.position.clone(),
|
||||||
grab: request.grab,
|
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(
|
fn create_popup_internal(
|
||||||
self: &Rc<Self>,
|
self: &Rc<Self>,
|
||||||
queue_handle: &QueueHandle<AppState>,
|
queue_handle: &QueueHandle<AppState>,
|
||||||
parent_layer_surface: &ZwlrLayerSurfaceV1,
|
parent_layer_surface: &ZwlrLayerSurfaceV1,
|
||||||
params: CreatePopupParams,
|
params: &CreatePopupParams,
|
||||||
request: PopupRequest,
|
|
||||||
popup_id: PopupId,
|
popup_id: PopupId,
|
||||||
) -> Result<Rc<PopupWindow>> {
|
) -> Result<Rc<PopupWindow>> {
|
||||||
let xdg_wm_base = self.context.xdg_wm_base.as_ref().ok_or_else(|| {
|
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();
|
let scale_factor = self.scale_factor();
|
||||||
info!(
|
info!(
|
||||||
"Creating popup window with scale factor {scale_factor}, reference=({}, {}), size=({} x {}), mode={:?}",
|
"Creating popup window with scale factor {scale_factor}, size=({} x {}), position={:?}",
|
||||||
params.reference_x,
|
params.width, params.height, params.position
|
||||||
params.reference_y,
|
|
||||||
params.width,
|
|
||||||
params.height,
|
|
||||||
params.positioning_mode
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let output_size = self.output_size();
|
let output_size = self.output_size();
|
||||||
|
|
@ -297,14 +273,6 @@ impl PopupManager {
|
||||||
.scale_factor_typed();
|
.scale_factor_typed();
|
||||||
let popup_dimensions = SurfaceDimensions::from_logical(popup_logical_size, domain_scale);
|
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(
|
let popup_size = PhysicalSize::new(
|
||||||
popup_dimensions.physical_width(),
|
popup_dimensions.physical_width(),
|
||||||
popup_dimensions.physical_height(),
|
popup_dimensions.physical_height(),
|
||||||
|
|
@ -320,7 +288,9 @@ impl PopupManager {
|
||||||
fractional_scale_manager: self.context.fractional_scale_manager.as_ref(),
|
fractional_scale_manager: self.context.fractional_scale_manager.as_ref(),
|
||||||
viewporter: self.context.viewporter.as_ref(),
|
viewporter: self.context.viewporter.as_ref(),
|
||||||
queue_handle,
|
queue_handle,
|
||||||
popup_config,
|
position: params.position.clone(),
|
||||||
|
output_bounds: output_logical_size,
|
||||||
|
constraint_adjustment: params.constraint_adjustment,
|
||||||
physical_size: popup_size,
|
physical_size: popup_size,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
});
|
});
|
||||||
|
|
@ -364,11 +334,8 @@ impl PopupManager {
|
||||||
ActivePopup {
|
ActivePopup {
|
||||||
surface: wayland_popup_surface,
|
surface: wayland_popup_surface,
|
||||||
window: Rc::clone(&popup_window),
|
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);
|
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) {
|
pub fn mark_popup_configured(&self, key: usize) {
|
||||||
let id = PopupId(key);
|
let id = PopupId(key);
|
||||||
if let Some(popup) = self.state.borrow().popups.get(&id) {
|
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<()> {
|
pub fn close(&self, handle: PopupHandle) -> Result<()> {
|
||||||
let id = PopupId::from_handle(handle);
|
let id = PopupId::from_handle(handle);
|
||||||
self.destroy_popup(id);
|
self.destroy_popup(id);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
use layer_shika_domain::dimensions::{LogicalSize as DomainLogicalSize, ScaleFactor};
|
use layer_shika_domain::dimensions::{LogicalRect, LogicalSize as DomainLogicalSize};
|
||||||
use layer_shika_domain::surface_dimensions::SurfaceDimensions;
|
use layer_shika_domain::value_objects::popup_behavior::ConstraintAdjustment as DomainConstraintAdjustment;
|
||||||
use layer_shika_domain::value_objects::popup_config::PopupConfig;
|
use layer_shika_domain::value_objects::popup_position::{Alignment, AnchorPoint, PopupPosition};
|
||||||
use log::info;
|
use log::info;
|
||||||
use slint::PhysicalSize;
|
use slint::PhysicalSize;
|
||||||
use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1;
|
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 std::rc::Rc;
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
protocol::{wl_compositor::WlCompositor, wl_seat::WlSeat, wl_surface::WlSurface},
|
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 fractional_scale_manager: Option<&'a WpFractionalScaleManagerV1>,
|
||||||
pub viewporter: Option<&'a WpViewporter>,
|
pub viewporter: Option<&'a WpViewporter>,
|
||||||
pub queue_handle: &'a QueueHandle<AppState>,
|
pub queue_handle: &'a QueueHandle<AppState>,
|
||||||
pub popup_config: PopupConfig,
|
pub position: PopupPosition,
|
||||||
|
pub output_bounds: DomainLogicalSize,
|
||||||
|
pub constraint_adjustment: DomainConstraintAdjustment,
|
||||||
pub physical_size: PhysicalSize,
|
pub physical_size: PhysicalSize,
|
||||||
pub scale_factor: f32,
|
pub scale_factor: f32,
|
||||||
}
|
}
|
||||||
|
|
@ -44,10 +45,11 @@ pub struct PopupSurface {
|
||||||
pub xdg_popup: Rc<XdgPopup>,
|
pub xdg_popup: Rc<XdgPopup>,
|
||||||
pub fractional_scale: Option<Rc<WpFractionalScaleV1>>,
|
pub fractional_scale: Option<Rc<WpFractionalScaleV1>>,
|
||||||
pub viewport: Option<Rc<WpViewport>>,
|
pub viewport: Option<Rc<WpViewport>>,
|
||||||
popup_config: PopupConfig,
|
position: PopupPosition,
|
||||||
|
output_bounds: DomainLogicalSize,
|
||||||
|
constraint_adjustment: DomainConstraintAdjustment,
|
||||||
xdg_wm_base: Rc<XdgWmBase>,
|
xdg_wm_base: Rc<XdgWmBase>,
|
||||||
queue_handle: QueueHandle<AppState>,
|
queue_handle: QueueHandle<AppState>,
|
||||||
scale_factor: Cell<f32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupSurface {
|
impl PopupSurface {
|
||||||
|
|
@ -102,10 +104,11 @@ impl PopupSurface {
|
||||||
xdg_popup,
|
xdg_popup,
|
||||||
fractional_scale,
|
fractional_scale,
|
||||||
viewport,
|
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()),
|
xdg_wm_base: Rc::new(params.xdg_wm_base.clone()),
|
||||||
queue_handle: params.queue_handle.clone(),
|
queue_handle: params.queue_handle.clone(),
|
||||||
scale_factor: Cell::new(params.scale_factor),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,26 +121,29 @@ impl PopupSurface {
|
||||||
.xdg_wm_base
|
.xdg_wm_base
|
||||||
.create_positioner(params.queue_handle, ());
|
.create_positioner(params.queue_handle, ());
|
||||||
|
|
||||||
let calculated_x = params.popup_config.calculated_top_left_x() as i32;
|
let logical_width = params.physical_size.width as f32 / params.scale_factor;
|
||||||
let calculated_y = params.popup_config.calculated_top_left_y() as i32;
|
let logical_height = params.physical_size.height as f32 / params.scale_factor;
|
||||||
|
|
||||||
info!(
|
let (calculated_x, calculated_y) = compute_top_left(
|
||||||
"Popup positioning: reference=({}, {}), mode={:?}, calculated_top_left=({}, {})",
|
¶ms.position,
|
||||||
params.popup_config.reference_x(),
|
DomainLogicalSize::from_raw(logical_width, logical_height),
|
||||||
params.popup_config.reference_y(),
|
params.output_bounds,
|
||||||
params.popup_config.positioning_mode(),
|
|
||||||
calculated_x,
|
|
||||||
calculated_y
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let logical_width = (params.physical_size.width as f32 / params.scale_factor) as i32;
|
info!(
|
||||||
let logical_height = (params.physical_size.height as f32 / params.scale_factor) as i32;
|
"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_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_anchor(Anchor::TopLeft);
|
||||||
positioner.set_gravity(Gravity::BottomRight);
|
positioner.set_gravity(Gravity::BottomRight);
|
||||||
positioner.set_constraint_adjustment(ConstraintAdjustment::None);
|
positioner
|
||||||
|
.set_constraint_adjustment(map_constraint_adjustment(params.constraint_adjustment));
|
||||||
|
|
||||||
positioner
|
positioner
|
||||||
}
|
}
|
||||||
|
|
@ -168,30 +174,15 @@ impl PopupSurface {
|
||||||
#[allow(clippy::cast_sign_loss)]
|
#[allow(clippy::cast_sign_loss)]
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
fn reposition_popup(&self, logical_width: i32, logical_height: i32) {
|
fn reposition_popup(&self, logical_width: i32, logical_height: i32) {
|
||||||
let scale_factor = self.scale_factor.get();
|
let (calculated_x, calculated_y) = compute_top_left(
|
||||||
|
&self.position,
|
||||||
let updated_config = PopupConfig::new(
|
DomainLogicalSize::from_raw(logical_width as f32, logical_height as f32),
|
||||||
self.popup_config.reference_x(),
|
self.output_bounds,
|
||||||
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 = updated_config.calculated_top_left_x() as i32;
|
|
||||||
let calculated_y = updated_config.calculated_top_left_y() as i32;
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Repositioning popup: reference=({}, {}), new_size=({}x{}), new_top_left=({}, {})",
|
"Repositioning popup: new_size=({}x{}), new_top_left=({}, {})",
|
||||||
self.popup_config.reference_x(),
|
logical_width, logical_height, calculated_x, calculated_y
|
||||||
self.popup_config.reference_y(),
|
|
||||||
logical_width,
|
|
||||||
logical_height,
|
|
||||||
calculated_x,
|
|
||||||
calculated_y
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let positioner = self.xdg_wm_base.create_positioner(&self.queue_handle, ());
|
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_size(logical_width, logical_height);
|
||||||
positioner.set_anchor(Anchor::TopLeft);
|
positioner.set_anchor(Anchor::TopLeft);
|
||||||
positioner.set_gravity(Gravity::BottomRight);
|
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);
|
self.xdg_popup.reposition(&positioner, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -211,3 +202,101 @@ impl PopupSurface {
|
||||||
self.surface.destroy();
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<CompilationResult>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ShellBuilder {
|
|
||||||
compilation: CompilationSource,
|
|
||||||
windows: Vec<SurfaceDefinition>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShellBuilder {
|
|
||||||
pub fn window(self, component: impl Into<String>) -> SurfaceConfigBuilder {
|
|
||||||
SurfaceConfigBuilder {
|
|
||||||
shell_builder: self,
|
|
||||||
component: component.into(),
|
|
||||||
config: WindowConfig::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn discover_surfaces(
|
|
||||||
mut self,
|
|
||||||
components: impl IntoIterator<Item = impl Into<String>>,
|
|
||||||
) -> Self {
|
|
||||||
for component in components {
|
|
||||||
self.surfaces.push(SurfaceDefinition {
|
|
||||||
component: component.into(),
|
|
||||||
config: WindowConfig::default(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(self) -> Result<Runtime> {
|
|
||||||
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<String> =
|
|
||||||
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<String> =
|
|
||||||
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<Margins>) -> 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<String>) -> Self {
|
|
||||||
self.config.namespace = namespace.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn scale_factor(mut self, sf: impl TryInto<ScaleFactor, Error = DomainError>) -> 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<String>) -> SurfaceConfigBuilder {
|
|
||||||
let shell_builder = self.complete();
|
|
||||||
shell_builder.window(component)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(self) -> Result<Runtime> {
|
|
||||||
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<Path>) -> 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<Path>, 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<String>) -> ShellBuilder {
|
|
||||||
ShellBuilder {
|
|
||||||
compilation: CompilationSource::Source {
|
|
||||||
code: code.into(),
|
|
||||||
compiler: Compiler::default(),
|
|
||||||
},
|
|
||||||
windows: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_source_with_compiler(code: impl Into<String>, compiler: Compiler) -> ShellBuilder {
|
|
||||||
ShellBuilder {
|
|
||||||
compilation: CompilationSource::Source {
|
|
||||||
code: code.into(),
|
|
||||||
compiler,
|
|
||||||
},
|
|
||||||
windows: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_compilation(result: Rc<CompilationResult>) -> ShellBuilder {
|
|
||||||
ShellBuilder {
|
|
||||||
compilation: CompilationSource::Compiled(result),
|
|
||||||
windows: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compile_file(path: impl AsRef<Path>) -> Result<Rc<CompilationResult>> {
|
|
||||||
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<String> = 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<String>) -> Result<Rc<CompilationResult>> {
|
|
||||||
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<String> = 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<RefCell<ShellSystemFacade>>,
|
|
||||||
windows: HashMap<String, SurfaceDefinition>,
|
|
||||||
compilation_result: Rc<CompilationResult>,
|
|
||||||
popup_command_sender: channel::Sender<PopupCommand>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Runtime {
|
|
||||||
pub(crate) fn new(
|
|
||||||
compilation_result: Rc<CompilationResult>,
|
|
||||||
definitions: Vec<SurfaceDefinition>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
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<CompilationResult>,
|
|
||||||
definition: SurfaceDefinition,
|
|
||||||
) -> Result<Self> {
|
|
||||||
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<CompilationResult>,
|
|
||||||
definitions: Vec<SurfaceDefinition>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let shell_configs: Vec<ShellWindowConfig> = 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::<Result<Vec<_>>>()?;
|
|
||||||
|
|
||||||
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<PopupCommand>) -> 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<F, R>(&self, name: &str, f: F) -> Result<R>
|
|
||||||
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<F>(&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<F, R>(&self, handle: OutputHandle, f: F) -> Result<R>
|
|
||||||
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<F>(&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<CompilationResult> {
|
|
||||||
&self.compilation_result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on<F, R>(&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<F, R>(
|
|
||||||
&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<F, R>(&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<F, R>(&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<F>(&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<F>(&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<OutputInfo> {
|
|
||||||
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<OutputInfo> {
|
|
||||||
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<F>(&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<F>(&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<Item = &ComponentInstance> {
|
|
||||||
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<OutputHandle> {
|
|
||||||
self.app_state.primary_output_handle()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn active_output_handle(&self) -> Option<OutputHandle> {
|
|
||||||
self.app_state.active_output_handle()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn output_registry(&self) -> &OutputRegistry {
|
|
||||||
self.app_state.output_registry()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn outputs(&self) -> impl Iterator<Item = (OutputHandle, &ComponentInstance)> {
|
|
||||||
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<Item = &OutputInfo> {
|
|
||||||
self.app_state.all_output_info()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn outputs_with_info(&self) -> impl Iterator<Item = (&OutputInfo, &ComponentInstance)> {
|
|
||||||
self.app_state
|
|
||||||
.outputs_with_info()
|
|
||||||
.map(|(info, surface)| (info, surface.component_instance()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn compilation_result(&self) -> Option<Rc<CompilationResult>> {
|
|
||||||
self.app_state
|
|
||||||
.primary_output()
|
|
||||||
.and_then(SurfaceState::compilation_result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
mod event_loop;
|
mod event_loop;
|
||||||
mod layer_surface;
|
mod layer_surface;
|
||||||
|
mod popup;
|
||||||
mod popup_builder;
|
mod popup_builder;
|
||||||
mod selection;
|
mod selection;
|
||||||
mod selector;
|
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::entities::output_registry::OutputRegistry;
|
||||||
pub use layer_shika_domain::prelude::AnchorStrategy;
|
pub use layer_shika_domain::prelude::AnchorStrategy;
|
||||||
pub use layer_shika_domain::value_objects::anchor::AnchorEdges;
|
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::keyboard_interactivity::KeyboardInteractivity;
|
||||||
pub use layer_shika_domain::value_objects::layer::Layer;
|
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_handle::OutputHandle;
|
||||||
pub use layer_shika_domain::value_objects::output_info::{OutputGeometry, OutputInfo};
|
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::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::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 layer_surface::{LayerSurfaceHandle, ShellSurfaceConfigHandler};
|
||||||
|
pub use popup::PopupShell;
|
||||||
pub use popup_builder::PopupBuilder;
|
pub use popup_builder::PopupBuilder;
|
||||||
pub use selection::Selection;
|
pub use selection::Selection;
|
||||||
pub use selector::{Output, Selector, Surface, SurfaceInfo};
|
pub use selector::{Output, Selector, Surface, SurfaceInfo};
|
||||||
|
|
@ -77,12 +82,12 @@ pub mod prelude {
|
||||||
AnchorEdges, AnchorStrategy, CompiledUiSource, DEFAULT_COMPONENT_NAME,
|
AnchorEdges, AnchorStrategy, CompiledUiSource, DEFAULT_COMPONENT_NAME,
|
||||||
DEFAULT_SURFACE_NAME, EventDispatchContext, EventLoopHandle, Handle, IntoValue,
|
DEFAULT_SURFACE_NAME, EventDispatchContext, EventLoopHandle, Handle, IntoValue,
|
||||||
KeyboardInteractivity, Layer, LayerSurfaceHandle, Output, OutputGeometry, OutputHandle,
|
KeyboardInteractivity, Layer, LayerSurfaceHandle, Output, OutputGeometry, OutputHandle,
|
||||||
OutputInfo, OutputPolicy, OutputRegistry, PopupBuilder, PopupHandle, PopupPlacement,
|
OutputInfo, OutputPolicy, OutputRegistry, PopupBuilder, PopupConfig, PopupHandle,
|
||||||
PopupPositioningMode, PopupRequest, PopupSize, PopupWindow, Result, Selection, Selector,
|
PopupPosition, PopupShell, PopupSize, PopupWindow, Result, Selection, Selector, Shell,
|
||||||
Shell, ShellBuilder, ShellConfig, ShellControl, ShellEventContext, ShellEventLoop,
|
ShellBuilder, ShellConfig, ShellControl, ShellEventContext, ShellEventLoop, ShellRuntime,
|
||||||
ShellRuntime, ShellSurfaceConfigHandler, Surface, SurfaceComponentConfig,
|
ShellSurfaceConfigHandler, Surface, SurfaceComponentConfig, SurfaceConfigBuilder,
|
||||||
SurfaceConfigBuilder, SurfaceControlHandle, SurfaceDefinition, SurfaceEntry, SurfaceHandle,
|
SurfaceControlHandle, SurfaceDefinition, SurfaceEntry, SurfaceHandle, SurfaceInfo,
|
||||||
SurfaceInfo, SurfaceMetadata, SurfaceRegistry,
|
SurfaceMetadata, SurfaceRegistry,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::calloop::{Generic, Interest, Mode, PostAction, RegistrationToken, Timer};
|
pub use crate::calloop::{Generic, Interest, Mode, PostAction, RegistrationToken, Timer};
|
||||||
|
|
|
||||||
44
crates/composition/src/popup/mod.rs
Normal file
44
crates/composition/src/popup/mod.rs
Normal file
|
|
@ -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<ShellCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PopupShell {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(sender: channel::Sender<ShellCommand>) -> Self {
|
||||||
|
Self { sender }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn builder(&self, component: impl Into<String>) -> PopupBuilder {
|
||||||
|
PopupBuilder::new(component).with_shell(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&self, config: PopupConfig) -> Result<PopupHandle> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,33 +1,33 @@
|
||||||
use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode;
|
use crate::popup::PopupShell;
|
||||||
use layer_shika_domain::value_objects::popup_request::{PopupPlacement, PopupRequest, PopupSize};
|
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
|
/// Produces a [`PopupConfig`] and can show it via [`PopupShell`].
|
||||||
/// a fluent API for configuring popups. Once built, pass the resulting `PopupRequest`
|
|
||||||
/// to `ShellControl::show_popup()` from within a callback.
|
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// shell.on("Main", "open_menu", |control| {
|
/// shell.on("Main", "open_menu", |control| {
|
||||||
/// let request = PopupBuilder::new("MenuPopup")
|
/// let popup_handle = control.popups().builder("MenuPopup")
|
||||||
/// .relative_to_cursor()
|
/// .at_cursor()
|
||||||
/// .anchor_top_left()
|
|
||||||
/// .grab(true)
|
/// .grab(true)
|
||||||
/// .close_on("menu_closed")
|
/// .close_on("menu_closed")
|
||||||
/// .build();
|
/// .show()?;
|
||||||
///
|
|
||||||
/// control.show_popup(&request)?;
|
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub struct PopupBuilder {
|
pub struct PopupBuilder {
|
||||||
component: String,
|
shell: Option<PopupShell>,
|
||||||
reference: PopupPlacement,
|
config: PopupConfig,
|
||||||
anchor: PopupPositioningMode,
|
|
||||||
size: PopupSize,
|
|
||||||
grab: bool,
|
|
||||||
close_callback: Option<String>,
|
|
||||||
resize_callback: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupBuilder {
|
impl PopupBuilder {
|
||||||
|
|
@ -35,170 +35,189 @@ impl PopupBuilder {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(component: impl Into<String>) -> Self {
|
pub fn new(component: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: component.into(),
|
shell: None,
|
||||||
reference: PopupPlacement::AtCursor,
|
config: PopupConfig::new(component),
|
||||||
anchor: PopupPositioningMode::TopLeft,
|
|
||||||
size: PopupSize::Content,
|
|
||||||
grab: false,
|
|
||||||
close_callback: None,
|
|
||||||
resize_callback: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Positions the popup at the current cursor location
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn relative_to_cursor(mut self) -> Self {
|
pub(crate) fn with_shell(mut self, shell: PopupShell) -> Self {
|
||||||
self.reference = PopupPlacement::AtCursor;
|
self.shell = Some(shell);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Positions the popup at the specified coordinates
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn relative_to_point(mut self, x: f32, y: f32) -> Self {
|
pub fn position(mut self, position: PopupPosition) -> Self {
|
||||||
self.reference = PopupPlacement::AtPosition { x, y };
|
self.config.position = position;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Positions the popup relative to a rectangular area
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn relative_to_rect(mut self, x: f32, y: f32, w: f32, h: f32) -> Self {
|
pub fn at_cursor(self) -> Self {
|
||||||
self.reference = PopupPlacement::AtRect { x, y, w, h };
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the anchor point for positioning the popup
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn anchor(mut self, anchor: PopupPositioningMode) -> Self {
|
pub fn size(mut self, size: PopupSize) -> Self {
|
||||||
self.anchor = anchor;
|
self.config.size = size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchors popup to top-left corner
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn anchor_top_left(mut self) -> Self {
|
pub fn fixed_size(self, width: f32, height: f32) -> Self {
|
||||||
self.anchor = PopupPositioningMode::TopLeft;
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchors popup to top-center
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn anchor_top_center(mut self) -> Self {
|
pub fn modal(mut self, enable: bool) -> Self {
|
||||||
self.anchor = PopupPositioningMode::TopCenter;
|
self.config.behavior.modal = enable;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchors popup to top-right corner
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn anchor_top_right(mut self) -> Self {
|
pub fn close_on_click_outside(mut self) -> Self {
|
||||||
self.anchor = PopupPositioningMode::TopRight;
|
self.config.behavior.close_on_click_outside = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchors popup to center-left
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn anchor_center_left(mut self) -> Self {
|
pub fn close_on_escape(mut self) -> Self {
|
||||||
self.anchor = PopupPositioningMode::CenterLeft;
|
self.config.behavior.close_on_escape = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchors popup to center
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn anchor_center(mut self) -> Self {
|
pub fn constraint_adjustment(mut self, adjustment: ConstraintAdjustment) -> Self {
|
||||||
self.anchor = PopupPositioningMode::Center;
|
self.config.behavior.constraint_adjustment = adjustment;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchors popup to center-right
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn anchor_center_right(mut self) -> Self {
|
pub fn on_output(mut self, target: OutputTarget) -> Self {
|
||||||
self.anchor = PopupPositioningMode::CenterRight;
|
self.config.output = target;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchors popup to bottom-left corner
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn anchor_bottom_left(mut self) -> Self {
|
pub fn on_primary(self) -> Self {
|
||||||
self.anchor = PopupPositioningMode::BottomLeft;
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchors popup to bottom-center
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn anchor_bottom_center(mut self) -> Self {
|
pub const fn z_index(mut self, index: i32) -> Self {
|
||||||
self.anchor = PopupPositioningMode::BottomCenter;
|
self.config.z_index = index;
|
||||||
self
|
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]
|
#[must_use]
|
||||||
pub fn close_on(mut self, callback_name: impl Into<String>) -> Self {
|
pub fn close_on(mut self, callback_name: impl Into<String>) -> Self {
|
||||||
self.close_callback = Some(callback_name.into());
|
self.config.close_callback = Some(callback_name.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a callback that will resize the popup when invoked
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn resize_on(mut self, callback_name: impl Into<String>) -> Self {
|
pub fn resize_on(mut self, callback_name: impl Into<String>) -> Self {
|
||||||
self.resize_callback = Some(callback_name.into());
|
self.config.resize_callback = Some(callback_name.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the popup request
|
|
||||||
///
|
|
||||||
/// After building, pass the request to `ShellControl::show_popup()` to display the popup.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build(self) -> PopupRequest {
|
pub fn build(self) -> PopupConfig {
|
||||||
let mut builder = PopupRequest::builder(self.component.clone())
|
self.config
|
||||||
.placement(self.reference)
|
}
|
||||||
.size(self.size)
|
|
||||||
.mode(self.anchor)
|
|
||||||
.grab(self.grab);
|
|
||||||
|
|
||||||
if let Some(ref close_cb) = self.close_callback {
|
pub fn show(self) -> Result<PopupHandle> {
|
||||||
builder = builder.close_on(close_cb.clone());
|
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 {
|
pub fn show_with_shell(self, shell: &PopupShell) -> Result<PopupHandle> {
|
||||||
builder = builder.resize_on(resize_cb.clone());
|
shell.show(self.build())
|
||||||
}
|
|
||||||
|
|
||||||
builder.build()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -582,8 +582,8 @@ impl Shell {
|
||||||
_control: &ShellControl,
|
_control: &ShellControl,
|
||||||
) {
|
) {
|
||||||
match command {
|
match command {
|
||||||
PopupCommand::Show(request) => {
|
PopupCommand::Show { handle, config } => {
|
||||||
if let Err(e) = ctx.show_popup(&request) {
|
if let Err(e) = ctx.show_popup(handle, &config) {
|
||||||
log::error!("Failed to show popup: {}", e);
|
log::error!("Failed to show popup: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -764,6 +764,12 @@ impl Shell {
|
||||||
ShellControl::new(self.command_sender.clone())
|
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
|
/// Returns the names of all registered surfaces
|
||||||
pub fn surface_names(&self) -> Vec<&str> {
|
pub fn surface_names(&self) -> Vec<&str> {
|
||||||
self.registry.surface_names()
|
self.registry.surface_names()
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,22 @@ use layer_shika_domain::prelude::{
|
||||||
AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor,
|
AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor,
|
||||||
};
|
};
|
||||||
use layer_shika_domain::value_objects::dimensions::{PopupDimensions, SurfaceDimension};
|
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::handle::SurfaceHandle;
|
||||||
use layer_shika_domain::value_objects::output_handle::OutputHandle;
|
use layer_shika_domain::value_objects::output_handle::OutputHandle;
|
||||||
use layer_shika_domain::value_objects::output_info::OutputInfo;
|
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_config::PopupConfig;
|
||||||
use layer_shika_domain::value_objects::popup_request::{
|
use layer_shika_domain::value_objects::popup_position::PopupPosition;
|
||||||
PopupHandle, PopupPlacement, PopupRequest, PopupSize,
|
use layer_shika_domain::value_objects::popup_size::PopupSize;
|
||||||
};
|
|
||||||
use layer_shika_domain::value_objects::surface_instance_id::SurfaceInstanceId;
|
use layer_shika_domain::value_objects::surface_instance_id::SurfaceInstanceId;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
pub enum PopupCommand {
|
pub enum PopupCommand {
|
||||||
Show(PopupRequest),
|
Show {
|
||||||
|
handle: PopupHandle,
|
||||||
|
config: PopupConfig,
|
||||||
|
},
|
||||||
Close(PopupHandle),
|
Close(PopupHandle),
|
||||||
Resize {
|
Resize {
|
||||||
handle: PopupHandle,
|
handle: PopupHandle,
|
||||||
|
|
@ -157,41 +160,9 @@ impl CallbackContext {
|
||||||
.surface_by_name_and_output(&self.surface_name, self.output_handle())
|
.surface_by_name_and_output(&self.surface_name, self.output_handle())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a popup from a popup request
|
#[must_use]
|
||||||
///
|
pub fn popups(&self) -> crate::PopupShell {
|
||||||
/// Convenience method that forwards to the underlying `ShellControl`.
|
self.control.popups()
|
||||||
/// 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<String>) -> 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<String>) -> 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<String>,
|
|
||||||
x: f32,
|
|
||||||
y: f32,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.control.show_popup_at_position(component, x, y)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Closes a specific popup by its handle
|
/// Closes a specific popup by its handle
|
||||||
|
|
@ -224,132 +195,21 @@ impl ShellControl {
|
||||||
Self { sender }
|
Self { sender }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a popup from a popup request
|
#[must_use]
|
||||||
///
|
pub fn popups(&self) -> crate::PopupShell {
|
||||||
/// This is the primary API for showing popups from Slint callbacks. Popups are
|
crate::PopupShell::new(self.sender.clone())
|
||||||
/// 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<String>) -> 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<String>) -> 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<String>,
|
|
||||||
x: f32,
|
|
||||||
y: f32,
|
|
||||||
) -> Result<()> {
|
|
||||||
let request = PopupRequest::builder(component.into())
|
|
||||||
.placement(PopupPlacement::AtPosition { x, y })
|
|
||||||
.build();
|
|
||||||
self.show_popup(&request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Closes a specific popup by its handle
|
/// Closes a specific popup by its handle
|
||||||
///
|
///
|
||||||
/// Use this when you need to close a specific popup that you opened previously.
|
/// 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.
|
/// The handle is returned by [`crate::PopupShell::show`].
|
||||||
///
|
|
||||||
/// For closing popups from within the popup itself, consider using the
|
|
||||||
/// `close_on` callback configuration in [`PopupRequest`] instead.
|
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// // Store handle when showing popup
|
/// // Store handle when showing popup
|
||||||
/// let handle = context.show_popup(&request)?;
|
/// let handle = context.popups().builder("MenuPopup").show()?;
|
||||||
///
|
///
|
||||||
/// // Later, close it
|
/// // Later, close it
|
||||||
/// control.close_popup(handle)?;
|
/// control.close_popup(handle)?;
|
||||||
|
|
@ -370,7 +230,7 @@ impl ShellControl {
|
||||||
/// in response to content changes or user interaction.
|
/// in response to content changes or user interaction.
|
||||||
///
|
///
|
||||||
/// For automatic content-based sizing, use `PopupSize::Content` with the
|
/// For automatic content-based sizing, use `PopupSize::Content` with the
|
||||||
/// `resize_on` callback configuration in [`PopupRequest`] instead.
|
/// `resize_on` callback configuration in [`PopupConfig`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # 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<'_> {
|
impl EventDispatchContext<'_> {
|
||||||
pub(crate) fn surface_by_instance_mut(
|
pub(crate) fn surface_by_instance_mut(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -883,8 +761,8 @@ impl EventDispatchContext<'_> {
|
||||||
/// Resize callbacks (if configured via `resize_on()`) will operate directly
|
/// Resize callbacks (if configured via `resize_on()`) will operate directly
|
||||||
/// on the popup manager for immediate updates.
|
/// on the popup manager for immediate updates.
|
||||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||||
pub fn show_popup(&mut self, req: &PopupRequest) -> Result<PopupHandle> {
|
pub fn show_popup(&mut self, handle: PopupHandle, config: &PopupConfig) -> Result<PopupHandle> {
|
||||||
log::info!("show_popup called for component '{}'", req.component);
|
log::info!("show_popup called for component '{}'", config.component);
|
||||||
|
|
||||||
let compilation_result = self.compilation_result().ok_or_else(|| {
|
let compilation_result = self.compilation_result().ok_or_else(|| {
|
||||||
log::error!("No compilation result available");
|
log::error!("No compilation result available");
|
||||||
|
|
@ -895,27 +773,25 @@ impl EventDispatchContext<'_> {
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Got compilation result, looking for component '{}'",
|
"Got compilation result, looking for component '{}'",
|
||||||
req.component
|
config.component
|
||||||
);
|
);
|
||||||
|
|
||||||
let definition = compilation_result
|
let definition = compilation_result
|
||||||
.component(&req.component)
|
.component(&config.component)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
log::error!(
|
log::error!(
|
||||||
"Component '{}' not found in compilation result",
|
"Component '{}' not found in compilation result",
|
||||||
req.component
|
config.component
|
||||||
);
|
);
|
||||||
Error::Domain(DomainError::Configuration {
|
Error::Domain(DomainError::Configuration {
|
||||||
message: format!(
|
message: format!(
|
||||||
"{} component not found in compilation result",
|
"{} component not found in compilation result",
|
||||||
req.component
|
config.component
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
log::debug!("Found component definition for '{}'", req.component);
|
log::debug!("Found component definition for '{}'", config.component);
|
||||||
|
|
||||||
self.close_current_popup()?;
|
|
||||||
|
|
||||||
let is_using_active = self.app_state.active_output().is_some();
|
let is_using_active = self.app_state.active_output().is_some();
|
||||||
let active_surface = self.active_or_primary_output().ok_or_else(|| {
|
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
|
// For content-based sizing, we need to query the component's preferred size first
|
||||||
let initial_dimensions = match req.size {
|
let initial_dimensions = initial_dimensions_from_size(&config.size);
|
||||||
PopupSize::Fixed { w, h } => {
|
let mut resolved_position = config.position.clone();
|
||||||
log::debug!("Using fixed popup size: {}x{}", w, h);
|
if let PopupPosition::Cursor { offset } = config.position {
|
||||||
(w, h)
|
let cursor_pos = active_surface.current_pointer_position();
|
||||||
}
|
resolved_position = PopupPosition::Absolute {
|
||||||
PopupSize::Content => {
|
x: cursor_pos.x + offset.x,
|
||||||
log::debug!("Using content-based sizing - starting at 2×2");
|
y: cursor_pos.y + offset.y,
|
||||||
// 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();
|
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Creating popup for '{}' with dimensions {}x{} at position ({}, {}), mode: {:?}",
|
"Creating popup for '{}' with dimensions {}x{} at position {:?}",
|
||||||
req.component,
|
config.component,
|
||||||
initial_dimensions.0,
|
initial_dimensions.0,
|
||||||
initial_dimensions.1,
|
initial_dimensions.1,
|
||||||
ref_x,
|
resolved_position
|
||||||
ref_y,
|
|
||||||
req.mode
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a new request with resolved placement
|
let resolved_config = PopupConfig {
|
||||||
let resolved_request = PopupRequest {
|
component: config.component.clone(),
|
||||||
component: req.component.clone(),
|
position: resolved_position,
|
||||||
placement: resolved_placement,
|
size: config.size.clone(),
|
||||||
size: req.size,
|
behavior: config.behavior.clone(),
|
||||||
mode: req.mode,
|
output: config.output.clone(),
|
||||||
grab: req.grab,
|
parent: config.parent,
|
||||||
close_callback: req.close_callback.clone(),
|
z_index: config.z_index,
|
||||||
resize_callback: req.resize_callback.clone(),
|
close_callback: config.close_callback.clone(),
|
||||||
|
resize_callback: config.resize_callback.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let popup_handle = popup_manager.request_popup(
|
popup_manager.request_popup(
|
||||||
resolved_request,
|
handle,
|
||||||
|
resolved_config,
|
||||||
initial_dimensions.0,
|
initial_dimensions.0,
|
||||||
initial_dimensions.1,
|
initial_dimensions.1,
|
||||||
);
|
);
|
||||||
|
|
||||||
let (instance, popup_key_cell) =
|
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);
|
popup_surface.set_component_instance(instance);
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::Domain(DomainError::Configuration {
|
return Err(Error::Domain(DomainError::Configuration {
|
||||||
|
|
@ -1008,7 +863,7 @@ impl EventDispatchContext<'_> {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(popup_handle)
|
Ok(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Closes a popup by its handle
|
/// Closes a popup by its handle
|
||||||
|
|
@ -1021,16 +876,6 @@ impl EventDispatchContext<'_> {
|
||||||
Ok(())
|
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
|
/// Resizes a popup to the specified dimensions
|
||||||
pub fn resize_popup(&mut self, handle: PopupHandle, width: f32, height: f32) -> Result<()> {
|
pub fn resize_popup(&mut self, handle: PopupHandle, width: f32, height: f32) -> Result<()> {
|
||||||
let active_surface = self.active_or_primary_output().ok_or_else(|| {
|
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!(
|
log::debug!(
|
||||||
"Ignoring resize request for non-existent popup with handle {:?}",
|
"Updated popup viewport to logical size: {}x{} (from resize to {}x{})",
|
||||||
handle
|
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(())
|
Ok(())
|
||||||
|
|
@ -1086,7 +917,7 @@ impl EventDispatchContext<'_> {
|
||||||
fn create_popup_instance(
|
fn create_popup_instance(
|
||||||
definition: &ComponentDefinition,
|
definition: &ComponentDefinition,
|
||||||
popup_manager: &Rc<PopupManager>,
|
popup_manager: &Rc<PopupManager>,
|
||||||
req: &PopupRequest,
|
config: &PopupConfig,
|
||||||
) -> Result<(ComponentInstance, Rc<Cell<usize>>)> {
|
) -> Result<(ComponentInstance, Rc<Cell<usize>>)> {
|
||||||
let instance = definition.create().map_err(|e| {
|
let instance = definition.create().map_err(|e| {
|
||||||
Error::Domain(DomainError::Configuration {
|
Error::Domain(DomainError::Configuration {
|
||||||
|
|
@ -1096,7 +927,7 @@ impl EventDispatchContext<'_> {
|
||||||
|
|
||||||
let popup_key_cell = Rc::new(Cell::new(0));
|
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| {
|
instance.show().map_err(|e| {
|
||||||
Error::Domain(DomainError::Configuration {
|
Error::Domain(DomainError::Configuration {
|
||||||
|
|
@ -1111,13 +942,18 @@ impl EventDispatchContext<'_> {
|
||||||
instance: &ComponentInstance,
|
instance: &ComponentInstance,
|
||||||
popup_manager: &Rc<PopupManager>,
|
popup_manager: &Rc<PopupManager>,
|
||||||
popup_key_cell: &Rc<Cell<usize>>,
|
popup_key_cell: &Rc<Cell<usize>>,
|
||||||
req: &PopupRequest,
|
config: &PopupConfig,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(close_callback_name) = &req.close_callback {
|
if let Some(close_callback_name) = &config.close_callback {
|
||||||
Self::register_close_callback(instance, popup_manager, close_callback_name)?;
|
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(
|
Self::register_resize_direct(
|
||||||
instance,
|
instance,
|
||||||
popup_manager,
|
popup_manager,
|
||||||
|
|
@ -1132,13 +968,16 @@ impl EventDispatchContext<'_> {
|
||||||
fn register_close_callback(
|
fn register_close_callback(
|
||||||
instance: &ComponentInstance,
|
instance: &ComponentInstance,
|
||||||
popup_manager: &Rc<PopupManager>,
|
popup_manager: &Rc<PopupManager>,
|
||||||
|
popup_key_cell: &Rc<Cell<usize>>,
|
||||||
callback_name: &str,
|
callback_name: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let popup_manager_weak = Rc::downgrade(popup_manager);
|
let popup_manager_weak = Rc::downgrade(popup_manager);
|
||||||
|
let key_cell = Rc::clone(popup_key_cell);
|
||||||
instance
|
instance
|
||||||
.set_callback(callback_name, move |_| {
|
.set_callback(callback_name, move |_| {
|
||||||
if let Some(popup_manager) = popup_manager_weak.upgrade() {
|
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
|
Value::Void
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,8 @@ pub struct LogicalPosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 }
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct PhysicalPosition {
|
pub struct PhysicalPosition {
|
||||||
x: i32,
|
x: i32,
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
pub mod output_registry;
|
pub mod output_registry;
|
||||||
|
pub mod popup_tree;
|
||||||
|
|
|
||||||
127
crates/domain/src/entities/popup_tree.rs
Normal file
127
crates/domain/src/entities/popup_tree.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
use crate::value_objects::handle::PopupHandle;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct PopupTree {
|
||||||
|
root_popups: Vec<PopupHandle>,
|
||||||
|
relationships: HashMap<PopupHandle, PopupNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PopupNode {
|
||||||
|
parent: Option<PopupHandle>,
|
||||||
|
children: Vec<PopupHandle>,
|
||||||
|
depth: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PopupTree {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_popup(&mut self, handle: PopupHandle, parent: Option<PopupHandle>) {
|
||||||
|
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<PopupHandle> {
|
||||||
|
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<PopupHandle> {
|
||||||
|
self.relationships.get(&handle).and_then(|n| n.parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_ancestors(&self, mut handle: PopupHandle) -> Vec<PopupHandle> {
|
||||||
|
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<PopupHandle>) {
|
||||||
|
out.push(handle);
|
||||||
|
for &child in self.get_children(handle) {
|
||||||
|
self.collect_descendants(child, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
pub use crate::config::SurfaceConfig;
|
pub use crate::config::SurfaceConfig;
|
||||||
pub use crate::dimensions::{
|
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::output_registry::OutputRegistry;
|
||||||
|
pub use crate::entities::popup_tree::PopupTree;
|
||||||
pub use crate::errors::{DomainError, Result};
|
pub use crate::errors::{DomainError, Result};
|
||||||
pub use crate::surface_dimensions::SurfaceDimensions;
|
pub use crate::surface_dimensions::SurfaceDimensions;
|
||||||
pub use crate::value_objects::anchor::AnchorEdges;
|
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::margins::Margins;
|
||||||
pub use crate::value_objects::output_info::{OutputGeometry, OutputInfo};
|
pub use crate::value_objects::output_info::{OutputGeometry, OutputInfo};
|
||||||
pub use crate::value_objects::output_policy::OutputPolicy;
|
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;
|
pub use crate::value_objects::ui_source::UiSource;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ pub mod margins;
|
||||||
pub mod output_handle;
|
pub mod output_handle;
|
||||||
pub mod output_info;
|
pub mod output_info;
|
||||||
pub mod output_policy;
|
pub mod output_policy;
|
||||||
|
pub mod output_target;
|
||||||
|
pub mod popup_behavior;
|
||||||
pub mod popup_config;
|
pub mod popup_config;
|
||||||
pub mod popup_positioning_mode;
|
pub mod popup_position;
|
||||||
pub mod popup_request;
|
pub mod popup_size;
|
||||||
pub mod surface_instance_id;
|
pub mod surface_instance_id;
|
||||||
pub mod ui_source;
|
pub mod ui_source;
|
||||||
|
|
|
||||||
23
crates/domain/src/value_objects/output_target.rs
Normal file
23
crates/domain/src/value_objects/output_target.rs
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
60
crates/domain/src/value_objects/popup_behavior.rs
Normal file
60
crates/domain/src/value_objects/popup_behavior.rs
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -1,94 +1,55 @@
|
||||||
use super::popup_positioning_mode::PopupPositioningMode;
|
use crate::value_objects::handle::PopupHandle;
|
||||||
use crate::dimensions::{LogicalPosition, LogicalSize};
|
use crate::value_objects::output_target::OutputTarget;
|
||||||
use crate::surface_dimensions::SurfaceDimensions;
|
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 {
|
pub struct PopupConfig {
|
||||||
reference_position: LogicalPosition,
|
/// Component name from compiled Slint file
|
||||||
dimensions: SurfaceDimensions,
|
pub component: String,
|
||||||
output_bounds: LogicalSize,
|
|
||||||
positioning_mode: PopupPositioningMode,
|
/// 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<PopupHandle>,
|
||||||
|
|
||||||
|
/// Z-order relative to siblings
|
||||||
|
pub z_index: i32,
|
||||||
|
|
||||||
|
/// Callback invoked by the component to request close
|
||||||
|
pub close_callback: Option<String>,
|
||||||
|
|
||||||
|
/// Callback invoked by the component to request resize (content-sizing)
|
||||||
|
pub resize_callback: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupConfig {
|
impl PopupConfig {
|
||||||
pub fn new(
|
#[must_use]
|
||||||
reference_x: f32,
|
pub fn new(component: impl Into<String>) -> Self {
|
||||||
reference_y: f32,
|
|
||||||
dimensions: SurfaceDimensions,
|
|
||||||
positioning_mode: PopupPositioningMode,
|
|
||||||
output_bounds: LogicalSize,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
reference_position: LogicalPosition::new(reference_x, reference_y),
|
component: component.into(),
|
||||||
dimensions,
|
position: PopupPosition::Cursor {
|
||||||
output_bounds,
|
offset: Offset::default(),
|
||||||
positioning_mode,
|
},
|
||||||
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
crates/domain/src/value_objects/popup_position.rs
Normal file
58
crates/domain/src/value_objects/popup_position.rs
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<String>,
|
|
||||||
pub resize_callback: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String>,
|
|
||||||
resize_callback: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String>) -> Self {
|
|
||||||
self.close_callback = Some(callback_name.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn resize_on(mut self, callback_name: impl Into<String>) -> 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
crates/domain/src/value_objects/popup_size.rs
Normal file
26
crates/domain/src/value_objects/popup_size.rs
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -143,8 +143,9 @@ pub use shell::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use window::{
|
pub use window::{
|
||||||
AnchorEdges, AnchorStrategy, KeyboardInteractivity, Layer, PopupBuilder, PopupHandle,
|
Alignment, AnchorEdges, AnchorPoint, AnchorStrategy, ConstraintAdjustment,
|
||||||
PopupPlacement, PopupPositioningMode, PopupRequest, PopupSize,
|
KeyboardInteractivity, Layer, Offset, OutputTarget, PopupBehavior, PopupBuilder, PopupConfig,
|
||||||
|
PopupHandle, PopupPosition, PopupShell, PopupSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use output::{OutputGeometry, OutputHandle, OutputInfo, OutputPolicy, OutputRegistry};
|
pub use output::{OutputGeometry, OutputHandle, OutputInfo, OutputPolicy, OutputRegistry};
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@ pub use crate::shell::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::window::{
|
pub use crate::window::{
|
||||||
AnchorEdges, AnchorStrategy, KeyboardInteractivity, Layer, PopupHandle, PopupPlacement,
|
Alignment, AnchorEdges, AnchorPoint, AnchorStrategy, ConstraintAdjustment,
|
||||||
PopupPositioningMode, PopupRequest, PopupSize,
|
KeyboardInteractivity, Layer, Offset, OutputTarget, PopupBehavior, PopupBuilder, PopupConfig,
|
||||||
|
PopupHandle, PopupPosition, PopupShell, PopupSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::output::{OutputGeometry, OutputHandle, OutputInfo, OutputPolicy, OutputRegistry};
|
pub use crate::output::{OutputGeometry, OutputHandle, OutputInfo, OutputPolicy, OutputRegistry};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub use layer_shika_composition::{
|
pub use layer_shika_composition::{
|
||||||
AnchorEdges, AnchorStrategy, KeyboardInteractivity, Layer, PopupBuilder, PopupHandle,
|
Alignment, AnchorEdges, AnchorPoint, AnchorStrategy, ConstraintAdjustment,
|
||||||
PopupPlacement, PopupPositioningMode, PopupRequest, PopupSize,
|
KeyboardInteractivity, Layer, Offset, OutputTarget, PopupBehavior, PopupBuilder, PopupConfig,
|
||||||
|
PopupHandle, PopupPosition, PopupShell, PopupSize,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue