feat: add popup positioning mode

This commit is contained in:
drendog 2025-11-02 05:21:26 +01:00
parent d646306a7a
commit 47487b7062
Signed by: dwenya
GPG key ID: 8DD77074645332D0
9 changed files with 251 additions and 122 deletions

View file

@ -6,9 +6,7 @@ pub mod wayland;
pub use rendering::femtovg::popup_window::PopupWindow; pub use rendering::femtovg::popup_window::PopupWindow;
pub use rendering::slint_integration::platform::{ pub use rendering::slint_integration::platform::{
clear_popup_position_override, close_current_popup, get_popup_position_override, clear_popup_config, close_current_popup, get_popup_config, set_popup_config,
set_popup_position_override, clear_popup_size_override, get_popup_size_override,
set_popup_size_override,
}; };
pub mod platform { pub mod platform {

View file

@ -1,3 +1,4 @@
use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode;
use slint::{ use slint::{
PlatformError, PlatformError,
platform::{Platform, WindowAdapter}, platform::{Platform, WindowAdapter},
@ -9,6 +10,7 @@ use crate::rendering::femtovg::main_window::FemtoVGWindow;
use crate::rendering::femtovg::popup_window::PopupWindow; use crate::rendering::femtovg::popup_window::PopupWindow;
type PopupCreator = dyn Fn() -> Result<Rc<dyn WindowAdapter>, PlatformError>; type PopupCreator = dyn Fn() -> Result<Rc<dyn WindowAdapter>, PlatformError>;
type PopupConfigData = (f32, f32, f32, f32, PopupPositioningMode);
thread_local! { thread_local! {
static CURRENT_PLATFORM: RefCell<Option<Weak<CustomSlintPlatform>>> = const { RefCell::new(None) }; static CURRENT_PLATFORM: RefCell<Option<Weak<CustomSlintPlatform>>> = const { RefCell::new(None) };
@ -24,61 +26,43 @@ pub fn close_current_popup() {
}); });
} }
pub fn set_popup_position_override(x: f32, y: f32) { pub fn set_popup_config(
reference_x: f32,
reference_y: f32,
width: f32,
height: f32,
positioning_mode: PopupPositioningMode,
) {
CURRENT_PLATFORM.with(|platform| { CURRENT_PLATFORM.with(|platform| {
if let Some(weak_platform) = platform.borrow().as_ref() { if let Some(weak_platform) = platform.borrow().as_ref() {
if let Some(strong_platform) = weak_platform.upgrade() { if let Some(strong_platform) = weak_platform.upgrade() {
strong_platform.set_popup_position(x, y); strong_platform.set_popup_config(
reference_x,
reference_y,
width,
height,
positioning_mode,
);
} }
} }
}); });
} }
pub fn get_popup_position_override() -> Option<(f32, f32)> { pub fn get_popup_config() -> Option<PopupConfigData> {
CURRENT_PLATFORM.with(|platform| { CURRENT_PLATFORM.with(|platform| {
platform platform
.borrow() .borrow()
.as_ref() .as_ref()
.and_then(Weak::upgrade) .and_then(Weak::upgrade)
.and_then(|strong| strong.get_popup_position()) .and_then(|strong| strong.get_popup_config())
}) })
} }
pub fn clear_popup_position_override() { pub fn clear_popup_config() {
CURRENT_PLATFORM.with(|platform| { CURRENT_PLATFORM.with(|platform| {
if let Some(weak_platform) = platform.borrow().as_ref() { if let Some(weak_platform) = platform.borrow().as_ref() {
if let Some(strong_platform) = weak_platform.upgrade() { if let Some(strong_platform) = weak_platform.upgrade() {
strong_platform.clear_popup_position(); strong_platform.clear_popup_config();
}
}
});
}
pub fn set_popup_size_override(width: f32, height: f32) {
CURRENT_PLATFORM.with(|platform| {
if let Some(weak_platform) = platform.borrow().as_ref() {
if let Some(strong_platform) = weak_platform.upgrade() {
strong_platform.set_popup_size(width, height);
}
}
});
}
pub fn get_popup_size_override() -> Option<(f32, f32)> {
CURRENT_PLATFORM.with(|platform| {
platform
.borrow()
.as_ref()
.and_then(Weak::upgrade)
.and_then(|strong| strong.get_popup_size())
})
}
pub fn clear_popup_size_override() {
CURRENT_PLATFORM.with(|platform| {
if let Some(weak_platform) = platform.borrow().as_ref() {
if let Some(strong_platform) = weak_platform.upgrade() {
strong_platform.clear_popup_size();
} }
} }
}); });
@ -89,8 +73,7 @@ pub struct CustomSlintPlatform {
popup_creator: RefCell<Option<Rc<PopupCreator>>>, popup_creator: RefCell<Option<Rc<PopupCreator>>>,
first_call: Cell<bool>, first_call: Cell<bool>,
last_popup: RefCell<Option<Weak<PopupWindow>>>, last_popup: RefCell<Option<Weak<PopupWindow>>>,
popup_position: RefCell<Option<(f32, f32)>>, popup_config: RefCell<Option<PopupConfigData>>,
popup_size: RefCell<Option<(f32, f32)>>,
} }
impl CustomSlintPlatform { impl CustomSlintPlatform {
@ -101,8 +84,7 @@ impl CustomSlintPlatform {
popup_creator: RefCell::new(None), popup_creator: RefCell::new(None),
first_call: Cell::new(true), first_call: Cell::new(true),
last_popup: RefCell::new(None), last_popup: RefCell::new(None),
popup_position: RefCell::new(None), popup_config: RefCell::new(None),
popup_size: RefCell::new(None),
}); });
CURRENT_PLATFORM.with(|current| { CURRENT_PLATFORM.with(|current| {
@ -133,29 +115,25 @@ impl CustomSlintPlatform {
*self.last_popup.borrow_mut() = None; *self.last_popup.borrow_mut() = None;
} }
pub fn set_popup_position(&self, x: f32, y: f32) { pub fn set_popup_config(
*self.popup_position.borrow_mut() = Some((x, y)); &self,
reference_x: f32,
reference_y: f32,
width: f32,
height: f32,
positioning_mode: PopupPositioningMode,
) {
*self.popup_config.borrow_mut() =
Some((reference_x, reference_y, width, height, positioning_mode));
} }
#[must_use] #[must_use]
pub fn get_popup_position(&self) -> Option<(f32, f32)> { pub fn get_popup_config(&self) -> Option<PopupConfigData> {
*self.popup_position.borrow() *self.popup_config.borrow()
} }
pub fn clear_popup_position(&self) { pub fn clear_popup_config(&self) {
*self.popup_position.borrow_mut() = None; *self.popup_config.borrow_mut() = None;
}
pub fn set_popup_size(&self, width: f32, height: f32) {
*self.popup_size.borrow_mut() = Some((width, height));
}
pub fn get_popup_size(&self) -> Option<(f32, f32)> {
*self.popup_size.borrow()
}
pub fn clear_popup_size(&self) {
*self.popup_size.borrow_mut() = None;
} }
} }

View file

@ -2,7 +2,7 @@ use crate::wayland::{
config::{LayerSurfaceParams, WaylandWindowConfig}, config::{LayerSurfaceParams, WaylandWindowConfig},
globals::context::GlobalContext, globals::context::GlobalContext,
surfaces::layer_surface::{SurfaceCtx, SurfaceSetupParams}, surfaces::layer_surface::{SurfaceCtx, SurfaceSetupParams},
surfaces::popup_manager::{PopupContext, PopupManager}, surfaces::popup_manager::{CreatePopupParams, PopupContext, PopupManager},
surfaces::{ surfaces::{
surface_builder::WindowStateBuilder, surface_builder::WindowStateBuilder,
surface_state::{SharedPointerSerial, WindowState}, surface_state::{SharedPointerSerial, WindowState},
@ -11,13 +11,15 @@ use crate::wayland::{
use crate::{ use crate::{
errors::{EventLoopError, LayerShikaError, RenderingError, Result}, errors::{EventLoopError, LayerShikaError, RenderingError, Result},
rendering::{ rendering::{
egl::context::EGLContext, femtovg::main_window::FemtoVGWindow, egl::context::EGLContext,
slint_integration::platform::CustomSlintPlatform, femtovg::main_window::FemtoVGWindow,
slint_integration::platform::{CustomSlintPlatform, clear_popup_config, get_popup_config},
}, },
}; };
use core::result::Result as CoreResult; use core::result::Result as CoreResult;
use layer_shika_domain::errors::DomainError; use layer_shika_domain::errors::DomainError;
use layer_shika_domain::ports::windowing::WindowingSystemPort; use layer_shika_domain::ports::windowing::WindowingSystemPort;
use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode;
use log::{error, info}; use log::{error, info};
use slint::{ use slint::{
LogicalPosition, PhysicalSize, PlatformError, WindowPosition, LogicalPosition, PhysicalSize, PlatformError, WindowPosition,
@ -173,14 +175,45 @@ impl WaylandWindowingSystem {
let layer_surface = state.layer_surface(); let layer_surface = state.layer_surface();
let queue_handle = event_queue.handle(); let queue_handle = event_queue.handle();
let serial_holder = Rc::clone(shared_serial); let serial_holder = Rc::clone(shared_serial);
let output_size = *state.output_size();
#[allow(clippy::cast_precision_loss)]
let default_width = output_size.width as f32;
#[allow(clippy::cast_precision_loss)]
let default_height = output_size.height as f32;
platform.set_popup_creator(move || { platform.set_popup_creator(move || {
info!("Popup creator called! Creating popup window..."); info!("Popup creator called! Creating popup window...");
let serial = serial_holder.get(); let serial = serial_holder.get();
let (reference_x, reference_y, width, height, positioning_mode) = get_popup_config()
.unwrap_or_else(|| {
log::warn!("No popup config provided, using output size as defaults");
(
0.0,
0.0,
default_width,
default_height,
PopupPositioningMode::TopLeft,
)
});
clear_popup_config();
let popup_window = popup_manager_clone let popup_window = popup_manager_clone
.create_popup(&queue_handle, &layer_surface, serial) .create_popup(
&queue_handle,
&layer_surface,
CreatePopupParams {
last_pointer_serial: serial,
reference_x,
reference_y,
width,
height,
positioning_mode,
},
)
.map_err(|e| PlatformError::Other(format!("Failed to create popup: {e}")))?; .map_err(|e| PlatformError::Other(format!("Failed to create popup: {e}")))?;
if let Some(platform) = platform_weak.upgrade() { if let Some(platform) = platform_weak.upgrade() {

View file

@ -1,10 +1,8 @@
use crate::errors::{LayerShikaError, Result}; use crate::errors::{LayerShikaError, Result};
use crate::rendering::egl::context::EGLContext; use crate::rendering::egl::context::EGLContext;
use crate::rendering::femtovg::popup_window::PopupWindow; use crate::rendering::femtovg::popup_window::PopupWindow;
use crate::rendering::slint_integration::platform::{ use layer_shika_domain::value_objects::popup_config::PopupConfig;
clear_popup_position_override, get_popup_position_override, use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode;
clear_popup_size_override, get_popup_size_override,
};
use log::info; use log::info;
use slab::Slab; use slab::Slab;
use slint::{platform::femtovg_renderer::FemtoVGRenderer, PhysicalSize, WindowSize}; use slint::{platform::femtovg_renderer::FemtoVGRenderer, PhysicalSize, WindowSize};
@ -23,6 +21,16 @@ use wayland_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase;
use super::popup_surface::PopupSurface; use super::popup_surface::PopupSurface;
use super::surface_state::WindowState; use super::surface_state::WindowState;
#[derive(Debug, Clone, Copy)]
pub struct CreatePopupParams {
pub last_pointer_serial: u32,
pub reference_x: f32,
pub reference_y: f32,
pub width: f32,
pub height: f32,
pub positioning_mode: PopupPositioningMode,
}
pub struct PopupContext { pub struct PopupContext {
compositor: WlCompositor, compositor: WlCompositor,
xdg_wm_base: Option<XdgWmBase>, xdg_wm_base: Option<XdgWmBase>,
@ -89,7 +97,7 @@ impl PopupManager {
self: &Rc<Self>, self: &Rc<Self>,
queue_handle: &QueueHandle<WindowState>, queue_handle: &QueueHandle<WindowState>,
parent_layer_surface: &ZwlrLayerSurfaceV1, parent_layer_surface: &ZwlrLayerSurfaceV1,
last_pointer_serial: u32, params: CreatePopupParams,
) -> 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(|| {
LayerShikaError::WindowConfiguration { LayerShikaError::WindowConfiguration {
@ -97,41 +105,32 @@ impl PopupManager {
} }
})?; })?;
let pointer_position = if let Some((x, y)) = get_popup_position_override() {
info!("Using explicit popup position: ({}, {})", x, y);
clear_popup_position_override();
slint::LogicalPosition::new(x, y)
} else {
log::error!("No popup position provided - using (0, 0) as fallback");
slint::LogicalPosition::new(0.0, 0.0)
};
let scale_factor = *self.current_scale_factor.borrow(); let scale_factor = *self.current_scale_factor.borrow();
let output_size = *self.current_output_size.borrow();
info!( info!(
"Creating popup window with scale factor {scale_factor} and output size {output_size:?}" "Creating popup window with scale factor {scale_factor}, reference=({}, {}), size=({} x {}), mode={:?}",
params.reference_x,
params.reference_y,
params.width,
params.height,
params.positioning_mode
);
let popup_config = PopupConfig::new(
params.reference_x,
params.reference_y,
params.width,
params.height,
params.positioning_mode,
); );
#[allow(clippy::cast_precision_loss)]
let logical_size = if let Some((width, height)) = get_popup_size_override() {
info!("Using explicit popup size: ({}, {})", width, height);
clear_popup_size_override();
slint::LogicalSize::new(width, height)
} else {
info!("No popup size override - using full output size");
slint::LogicalSize::new(
output_size.width as f32 / scale_factor,
output_size.height as f32 / scale_factor,
)
};
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_sign_loss)]
let popup_size = PhysicalSize::new( let popup_size = PhysicalSize::new(
(logical_size.width * scale_factor) as u32, (params.width * scale_factor) as u32,
(logical_size.height * scale_factor) as u32, (params.height * scale_factor) as u32,
); );
info!("Popup logical size: {logical_size:?}, physical size: {popup_size:?}"); info!("Popup physical size: {popup_size:?}");
let popup_surface = PopupSurface::create(&super::popup_surface::PopupSurfaceParams { let popup_surface = PopupSurface::create(&super::popup_surface::PopupSurfaceParams {
compositor: &self.context.compositor, compositor: &self.context.compositor,
@ -140,12 +139,12 @@ 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,
position: pointer_position, popup_config,
size: popup_size, physical_size: popup_size,
scale_factor, scale_factor,
}); });
popup_surface.grab(&self.context.seat, last_pointer_serial); popup_surface.grab(&self.context.seat, params.last_pointer_serial);
let context = EGLContext::builder() let context = EGLContext::builder()
.with_display_id(self.context.display.id()) .with_display_id(self.context.display.id())
@ -158,7 +157,10 @@ impl PopupManager {
let popup_window = PopupWindow::new(renderer); let popup_window = PopupWindow::new(renderer);
popup_window.set_scale_factor(scale_factor); popup_window.set_scale_factor(scale_factor);
popup_window.set_size(WindowSize::Logical(logical_size)); popup_window.set_size(WindowSize::Logical(slint::LogicalSize::new(
params.width,
params.height,
)));
let key = self.popups.borrow_mut().insert(ActivePopup { let key = self.popups.borrow_mut().insert(ActivePopup {
surface: popup_surface, surface: popup_surface,

View file

@ -1,5 +1,6 @@
use layer_shika_domain::value_objects::popup_config::PopupConfig;
use log::info; use log::info;
use slint::{LogicalPosition, PhysicalSize}; use slint::PhysicalSize;
use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1;
use std::rc::Rc; use std::rc::Rc;
use wayland_client::{ use wayland_client::{
@ -30,8 +31,8 @@ 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<WindowState>, pub queue_handle: &'a QueueHandle<WindowState>,
pub position: LogicalPosition, pub popup_config: PopupConfig,
pub size: PhysicalSize, pub physical_size: PhysicalSize,
pub scale_factor: f32, pub scale_factor: f32,
} }
@ -76,14 +77,14 @@ impl PopupSurface {
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_possible_truncation)]
if let Some(ref vp) = viewport { if let Some(ref vp) = viewport {
let logical_width = (params.size.width as f32 / params.scale_factor) as i32; let logical_width = (params.physical_size.width as f32 / params.scale_factor) as i32;
let logical_height = (params.size.height as f32 / params.scale_factor) as i32; let logical_height = (params.physical_size.height as f32 / params.scale_factor) as i32;
info!( info!(
"Setting viewport destination to logical size: {}x{} (physical: {}x{}, scale: {})", "Setting viewport destination to logical size: {}x{} (physical: {}x{}, scale: {})",
logical_width, logical_width,
logical_height, logical_height,
params.size.width, params.physical_size.width,
params.size.height, params.physical_size.height,
params.scale_factor params.scale_factor
); );
vp.set_destination(logical_width, logical_height); vp.set_destination(logical_width, logical_height);
@ -102,24 +103,29 @@ impl PopupSurface {
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_wrap)] #[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
fn create_positioner(params: &PopupSurfaceParams<'_>) -> XdgPositioner { fn create_positioner(params: &PopupSurfaceParams<'_>) -> XdgPositioner {
let positioner = params let positioner = params
.xdg_wm_base .xdg_wm_base
.create_positioner(params.queue_handle, ()); .create_positioner(params.queue_handle, ());
let x = params.position.x as i32; let calculated_x = params.popup_config.calculated_top_left_x() as i32;
let y = params.position.y as i32; let calculated_y = params.popup_config.calculated_top_left_y() as i32;
#[allow(clippy::cast_possible_truncation)] info!(
#[allow(clippy::cast_sign_loss)] "Popup positioning: reference=({}, {}), mode={:?}, calculated_top_left=({}, {})",
#[allow(clippy::cast_precision_loss)] params.popup_config.reference_x(),
let logical_width = (params.size.width as f32 / params.scale_factor) as i32; params.popup_config.reference_y(),
#[allow(clippy::cast_possible_truncation)] params.popup_config.positioning_mode(),
#[allow(clippy::cast_sign_loss)] calculated_x,
#[allow(clippy::cast_precision_loss)] calculated_y
let logical_height = (params.size.height as f32 / params.scale_factor) as i32; );
positioner.set_anchor_rect(x, y, 1, 1); let logical_width = (params.physical_size.width as f32 / params.scale_factor) as i32;
let logical_height = (params.physical_size.height as f32 / params.scale_factor) as i32;
positioner.set_anchor_rect(calculated_x, calculated_y, 1, 1);
positioner.set_size(logical_width, logical_height); positioner.set_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);

View file

@ -11,11 +11,9 @@ pub use builder::LayerShika;
pub use layer_shika_adapters::PopupWindow; pub use layer_shika_adapters::PopupWindow;
pub use layer_shika_adapters::close_current_popup; pub use layer_shika_adapters::close_current_popup;
pub use layer_shika_adapters::platform::{calloop, slint, slint_interpreter}; pub use layer_shika_adapters::platform::{calloop, slint, slint_interpreter};
pub use layer_shika_adapters::{ pub use layer_shika_adapters::{clear_popup_config, get_popup_config, set_popup_config};
clear_popup_position_override, get_popup_position_override, set_popup_position_override,
clear_popup_size_override, get_popup_size_override, set_popup_size_override,
};
pub use layer_shika_domain::value_objects::anchor::AnchorEdges; pub use layer_shika_domain::value_objects::anchor::AnchorEdges;
pub use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode;
pub type Result<T> = StdResult<T, Error>; pub type Result<T> = StdResult<T, Error>;

View file

@ -2,3 +2,5 @@ pub mod anchor;
pub mod dimensions; pub mod dimensions;
pub mod layer; pub mod layer;
pub mod margins; pub mod margins;
pub mod popup_config;
pub mod popup_positioning_mode;

View file

@ -0,0 +1,72 @@
use super::popup_positioning_mode::PopupPositioningMode;
#[derive(Debug, Clone, Copy)]
pub struct PopupConfig {
reference_x: f32,
reference_y: f32,
width: f32,
height: f32,
positioning_mode: PopupPositioningMode,
}
impl PopupConfig {
#[must_use]
pub const fn new(
reference_x: f32,
reference_y: f32,
width: f32,
height: f32,
positioning_mode: PopupPositioningMode,
) -> Self {
Self {
reference_x,
reference_y,
width,
height,
positioning_mode,
}
}
#[must_use]
pub const fn reference_x(&self) -> f32 {
self.reference_x
}
#[must_use]
pub const fn reference_y(&self) -> f32 {
self.reference_y
}
#[must_use]
pub const fn width(&self) -> f32 {
self.width
}
#[must_use]
pub const fn height(&self) -> f32 {
self.height
}
#[must_use]
pub const fn positioning_mode(&self) -> PopupPositioningMode {
self.positioning_mode
}
#[must_use]
pub fn calculated_top_left_x(&self) -> f32 {
if self.positioning_mode.center_x() {
self.reference_x - (self.width / 2.0)
} else {
self.reference_x
}
}
#[must_use]
pub fn calculated_top_left_y(&self) -> f32 {
if self.positioning_mode.center_y() {
self.reference_y - (self.height / 2.0)
} else {
self.reference_y
}
}
}

View file

@ -0,0 +1,40 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PopupPositioningMode {
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl PopupPositioningMode {
#[must_use]
pub const fn center_x(self) -> bool {
matches!(self, Self::TopCenter | Self::Center | Self::BottomCenter)
}
#[must_use]
pub const fn center_y(self) -> bool {
matches!(self, Self::CenterLeft | Self::Center | Self::CenterRight)
}
#[must_use]
pub const fn from_flags(center_x: bool, center_y: bool) -> Self {
match (center_x, center_y) {
(false, false) => Self::TopLeft,
(true, false) => Self::TopCenter,
(false, true) => Self::CenterLeft,
(true, true) => Self::Center,
}
}
}
impl Default for PopupPositioningMode {
fn default() -> Self {
Self::TopLeft
}
}