From 4ea4171eb7e51a7e3405e8e86b8638bf8e774119 Mon Sep 17 00:00:00 2001 From: drendog Date: Sun, 26 Oct 2025 02:06:52 +0100 Subject: [PATCH] feat: add initial popup support --- src/rendering/mod.rs | 1 + src/rendering/popup_window.rs | 97 ++++++++++++++++++ src/rendering/slint_platform.rs | 34 +++++-- src/windowing/globals.rs | 12 +++ src/windowing/mod.rs | 2 + src/windowing/popup.rs | 119 ++++++++++++++++++++++ src/windowing/popup_manager.rs | 160 ++++++++++++++++++++++++++++++ src/windowing/state/builder.rs | 30 ++++-- src/windowing/state/dispatches.rs | 113 ++++++++++++++++++--- src/windowing/state/mod.rs | 62 +++++++++++- src/windowing/system.rs | 105 +++++++++++++++----- 11 files changed, 682 insertions(+), 53 deletions(-) create mode 100644 src/rendering/popup_window.rs create mode 100644 src/windowing/popup.rs create mode 100644 src/windowing/popup_manager.rs diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs index d4eef13..15b934b 100644 --- a/src/rendering/mod.rs +++ b/src/rendering/mod.rs @@ -1,3 +1,4 @@ pub mod egl_context; pub mod femtovg_window; +pub mod popup_window; pub mod slint_platform; diff --git a/src/rendering/popup_window.rs b/src/rendering/popup_window.rs new file mode 100644 index 0000000..0d376b1 --- /dev/null +++ b/src/rendering/popup_window.rs @@ -0,0 +1,97 @@ +use crate::errors::{LayerShikaError, Result}; +use core::ops::Deref; +use log::info; +use slint::{ + platform::{femtovg_renderer::FemtoVGRenderer, Renderer, WindowAdapter, WindowEvent}, + PhysicalSize, Window, WindowSize, +}; +use std::cell::Cell; +use std::rc::{Rc, Weak}; + +use super::femtovg_window::RenderState; + +#[allow(dead_code)] +pub struct PopupWindow { + window: Window, + renderer: FemtoVGRenderer, + render_state: Cell, + size: Cell, + scale_factor: Cell, +} + +#[allow(dead_code)] +impl PopupWindow { + pub fn new(renderer: FemtoVGRenderer) -> Rc { + Rc::new_cyclic(|weak_self| { + let window = Window::new(Weak::clone(weak_self) as Weak); + Self { + window, + renderer, + render_state: Cell::new(RenderState::Clean), + size: Cell::new(PhysicalSize::default()), + scale_factor: Cell::new(1.), + } + }) + } + + pub fn render_frame_if_dirty(&self) -> Result<()> { + if matches!( + self.render_state.replace(RenderState::Clean), + RenderState::Dirty + ) { + info!( + "Rendering popup frame (size: {:?}, scale: {})", + self.size.get(), + self.scale_factor.get() + ); + self.renderer.render().map_err(|e| { + LayerShikaError::Rendering(format!("Error rendering popup frame: {e}")) + })?; + info!("Popup frame rendered successfully"); + } + Ok(()) + } + + pub fn set_scale_factor(&self, scale_factor: f32) { + info!("Setting popup scale factor to {scale_factor}"); + self.scale_factor.set(scale_factor); + self.window() + .dispatch_event(WindowEvent::ScaleFactorChanged { scale_factor }); + } + + pub const fn scale_factor(&self) -> f32 { + self.scale_factor.get() + } +} + +impl WindowAdapter for PopupWindow { + fn window(&self) -> &Window { + &self.window + } + + fn renderer(&self) -> &dyn Renderer { + &self.renderer + } + + fn size(&self) -> PhysicalSize { + self.size.get() + } + + fn set_size(&self, size: WindowSize) { + self.size.set(size.to_physical(self.scale_factor())); + self.window.dispatch_event(WindowEvent::Resized { + size: size.to_logical(self.scale_factor()), + }); + } + + fn request_redraw(&self) { + self.render_state.set(RenderState::Dirty); + } +} + +impl Deref for PopupWindow { + type Target = Window; + fn deref(&self) -> &Self::Target { + &self.window + } +} diff --git a/src/rendering/slint_platform.rs b/src/rendering/slint_platform.rs index 5607a2f..572981b 100644 --- a/src/rendering/slint_platform.rs +++ b/src/rendering/slint_platform.rs @@ -2,27 +2,49 @@ use slint::{ platform::{Platform, WindowAdapter}, PlatformError, }; +use std::cell::{Cell, RefCell}; use std::rc::{Rc, Weak}; use super::femtovg_window::FemtoVGWindow; +type PopupCreator = dyn Fn() -> Result, PlatformError>; + pub struct CustomSlintPlatform { - window: Weak, + main_window: Weak, + popup_creator: RefCell>>, + first_call: Cell, } impl CustomSlintPlatform { pub fn new(window: &Rc) -> Self { Self { - window: Rc::downgrade(window), + main_window: Rc::downgrade(window), + popup_creator: RefCell::new(None), + first_call: Cell::new(true), } } + + #[allow(dead_code)] + pub fn set_popup_creator(&self, creator: F) + where + F: Fn() -> Result, PlatformError> + 'static, + { + *self.popup_creator.borrow_mut() = Some(Rc::new(creator)); + } } impl Platform for CustomSlintPlatform { fn create_window_adapter(&self) -> Result, PlatformError> { - self.window - .upgrade() - .ok_or(PlatformError::NoPlatform) - .map(|w| w as Rc) + if self.first_call.get() { + self.first_call.set(false); + self.main_window + .upgrade() + .ok_or(PlatformError::NoPlatform) + .map(|w| w as Rc) + } else if let Some(creator) = self.popup_creator.borrow().as_ref() { + creator() + } else { + Err(PlatformError::NoPlatform) + } } } diff --git a/src/windowing/globals.rs b/src/windowing/globals.rs index 7199e9f..d314e4e 100644 --- a/src/windowing/globals.rs +++ b/src/windowing/globals.rs @@ -8,6 +8,7 @@ use wayland_client::{ }; use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; use wayland_protocols::wp::viewporter::client::wp_viewporter::WpViewporter; +use wayland_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase; use super::state::WindowState; @@ -16,6 +17,8 @@ pub struct GlobalCtx { pub output: WlOutput, pub layer_shell: ZwlrLayerShellV1, pub seat: WlSeat, + #[allow(dead_code)] + pub xdg_wm_base: Option, pub fractional_scale_manager: Option, pub viewporter: Option, } @@ -38,6 +41,10 @@ impl GlobalCtx { (WlSeat, seat, 1..=9) )?; + let xdg_wm_base = global_list + .bind::(queue_handle, 1..=6, ()) + .ok(); + let fractional_scale_manager = global_list .bind::(queue_handle, 1..=1, ()) .ok(); @@ -46,6 +53,10 @@ impl GlobalCtx { .bind::(queue_handle, 1..=1, ()) .ok(); + if xdg_wm_base.is_none() { + info!("xdg-shell protocol not available, popup support disabled"); + } + if fractional_scale_manager.is_none() { info!("Fractional scale protocol not available, using integer scaling"); } @@ -59,6 +70,7 @@ impl GlobalCtx { output, layer_shell, seat, + xdg_wm_base, fractional_scale_manager, viewporter, }) diff --git a/src/windowing/mod.rs b/src/windowing/mod.rs index 93c76f8..1c60b47 100644 --- a/src/windowing/mod.rs +++ b/src/windowing/mod.rs @@ -2,6 +2,8 @@ pub mod builder; mod config; mod globals; mod macros; +mod popup; +mod popup_manager; mod proxies; mod state; mod surface; diff --git a/src/windowing/popup.rs b/src/windowing/popup.rs new file mode 100644 index 0000000..d8bcf09 --- /dev/null +++ b/src/windowing/popup.rs @@ -0,0 +1,119 @@ +use log::info; +use slint::{LogicalPosition, PhysicalSize}; +use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; +use std::rc::Rc; +use wayland_client::{ + protocol::{wl_compositor::WlCompositor, wl_seat::WlSeat, wl_surface::WlSurface}, + QueueHandle, +}; +use wayland_protocols::wp::fractional_scale::v1::client::{ + wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, + wp_fractional_scale_v1::WpFractionalScaleV1, +}; +use wayland_protocols::wp::viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, +}; +use wayland_protocols::xdg::shell::client::{ + xdg_popup::XdgPopup, + xdg_positioner::{Anchor, ConstraintAdjustment, Gravity, XdgPositioner}, + xdg_surface::XdgSurface, + xdg_wm_base::XdgWmBase, +}; + +use super::state::WindowState; + +#[allow(dead_code)] +pub struct PopupSurfaceParams<'a> { + pub compositor: &'a WlCompositor, + pub xdg_wm_base: &'a XdgWmBase, + pub parent_layer_surface: &'a ZwlrLayerSurfaceV1, + pub fractional_scale_manager: Option<&'a WpFractionalScaleManagerV1>, + pub viewporter: Option<&'a WpViewporter>, + pub queue_handle: &'a QueueHandle, + pub position: LogicalPosition, + pub size: PhysicalSize, + pub scale_factor: f32, +} + +#[allow(dead_code)] +pub struct PopupSurface { + pub surface: Rc, + pub xdg_surface: Rc, + pub xdg_popup: Rc, + pub fractional_scale: Option>, + pub viewport: Option>, +} + +#[allow(dead_code)] +impl PopupSurface { + pub fn create(params: &PopupSurfaceParams<'_>) -> Self { + let surface = Rc::new(params.compositor.create_surface(params.queue_handle, ())); + + let xdg_surface = Rc::new(params.xdg_wm_base.get_xdg_surface( + &surface, + params.queue_handle, + (), + )); + + let positioner = Self::create_positioner(params); + + let xdg_popup = Rc::new(xdg_surface.get_popup(None, &positioner, params.queue_handle, ())); + + info!("Attaching popup to layer surface via get_popup"); + params.parent_layer_surface.get_popup(&xdg_popup); + + let fractional_scale = params.fractional_scale_manager.map(|manager| { + info!("Creating fractional scale object for popup surface"); + Rc::new(manager.get_fractional_scale(&surface, params.queue_handle, ())) + }); + + let viewport = params.viewporter.map(|vp| { + info!("Creating viewport for popup surface"); + Rc::new(vp.get_viewport(&surface, params.queue_handle, ())) + }); + + surface.set_buffer_scale(1); + surface.commit(); + + Self { + surface, + xdg_surface, + xdg_popup, + fractional_scale, + viewport, + } + } + + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_possible_wrap)] + fn create_positioner(params: &PopupSurfaceParams<'_>) -> XdgPositioner { + let positioner = params + .xdg_wm_base + .create_positioner(params.queue_handle, ()); + + let x = (params.position.x * params.scale_factor) as i32; + let y = (params.position.y * params.scale_factor) as i32; + let width = params.size.width as i32; + let height = params.size.height as i32; + + positioner.set_anchor_rect(x, y, 1, 1); + positioner.set_size(width, height); + positioner.set_anchor(Anchor::TopLeft); + positioner.set_gravity(Gravity::BottomRight); + positioner.set_constraint_adjustment( + ConstraintAdjustment::SlideX + | ConstraintAdjustment::SlideY + | ConstraintAdjustment::FlipX + | ConstraintAdjustment::FlipY + | ConstraintAdjustment::ResizeX + | ConstraintAdjustment::ResizeY, + ); + + positioner + } + + pub fn grab(&self, seat: &WlSeat, serial: u32) { + info!("Grabbing popup with serial {serial}"); + self.xdg_popup.grab(seat, serial); + } +} diff --git a/src/windowing/popup_manager.rs b/src/windowing/popup_manager.rs new file mode 100644 index 0000000..5d680bc --- /dev/null +++ b/src/windowing/popup_manager.rs @@ -0,0 +1,160 @@ +use crate::errors::{LayerShikaError, Result}; +use crate::rendering::{egl_context::EGLContext, popup_window::PopupWindow}; +use log::info; +use slint::{platform::femtovg_renderer::FemtoVGRenderer, PhysicalSize, WindowSize}; +use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; +use std::cell::RefCell; +use std::rc::Rc; +use wayland_client::{ + backend::ObjectId, + protocol::{wl_compositor::WlCompositor, wl_display::WlDisplay, wl_seat::WlSeat}, + Connection, Proxy, QueueHandle, +}; +use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use wayland_protocols::wp::viewporter::client::wp_viewporter::WpViewporter; +use wayland_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase; + +use super::{popup::PopupSurface, state::WindowState}; + +pub struct PopupContext { + compositor: WlCompositor, + xdg_wm_base: Option, + seat: WlSeat, + fractional_scale_manager: Option, + viewporter: Option, + display: WlDisplay, + #[allow(dead_code)] + connection: Rc, +} + +impl PopupContext { + pub const fn new( + compositor: WlCompositor, + xdg_wm_base: Option, + seat: WlSeat, + fractional_scale_manager: Option, + viewporter: Option, + display: WlDisplay, + connection: Rc, + ) -> Self { + Self { + compositor, + xdg_wm_base, + seat, + fractional_scale_manager, + viewporter, + display, + connection, + } + } +} + +struct ActivePopup { + surface: PopupSurface, + window: Rc, +} + +pub struct PopupManager { + context: PopupContext, + popups: RefCell>, +} + +impl PopupManager { + pub const fn new(context: PopupContext) -> Self { + Self { + context, + popups: RefCell::new(Vec::new()), + } + } + + pub fn create_popup( + &self, + queue_handle: &QueueHandle, + parent_layer_surface: &ZwlrLayerSurfaceV1, + last_pointer_serial: u32, + scale_factor: f32, + ) -> Result> { + let xdg_wm_base = self.context.xdg_wm_base.as_ref().ok_or_else(|| { + LayerShikaError::WaylandProtocol("xdg-shell not available for popups".into()) + })?; + + info!("Creating popup window"); + + let popup_size = PhysicalSize::new(360, 524); + + let popup_surface = PopupSurface::create(&super::popup::PopupSurfaceParams { + compositor: &self.context.compositor, + xdg_wm_base, + parent_layer_surface, + fractional_scale_manager: self.context.fractional_scale_manager.as_ref(), + viewporter: self.context.viewporter.as_ref(), + queue_handle, + position: slint::LogicalPosition::new(0.0, 0.0), + size: popup_size, + scale_factor, + }); + + popup_surface.grab(&self.context.seat, last_pointer_serial); + + let context = EGLContext::builder() + .with_display_id(self.context.display.id()) + .with_surface_id(popup_surface.surface.id()) + .with_size(popup_size) + .build() + .map_err(|e| LayerShikaError::EGLContextCreation(e.to_string()))?; + + let renderer = FemtoVGRenderer::new(context) + .map_err(|e| LayerShikaError::FemtoVGRendererCreation(e.to_string()))?; + + let popup_window = PopupWindow::new(renderer); + popup_window.set_size(WindowSize::Physical(popup_size)); + popup_window.set_scale_factor(scale_factor); + + info!("Popup window created successfully"); + + self.popups.borrow_mut().push(ActivePopup { + surface: popup_surface, + window: Rc::clone(&popup_window), + }); + + Ok(popup_window) + } + + pub fn render_popups(&self) -> Result<()> { + for popup in self.popups.borrow().iter() { + popup.window.render_frame_if_dirty()?; + } + Ok(()) + } + + pub const fn has_xdg_shell(&self) -> bool { + self.context.xdg_wm_base.is_some() + } + + #[allow(dead_code)] + pub fn popup_count(&self) -> usize { + self.popups.borrow().len() + } + + pub fn mark_all_popups_dirty(&self) { + for popup in self.popups.borrow().iter() { + popup.window.request_redraw(); + } + } + + pub fn find_popup_index_by_surface_id(&self, surface_id: &ObjectId) -> Option { + for (index, popup) in self.popups.borrow().iter().enumerate() { + if popup.surface.surface.id() == *surface_id { + return Some(index); + } + } + None + } + + pub fn get_popup_window(&self, index: usize) -> Option> { + self.popups + .borrow() + .get(index) + .map(|popup| Rc::clone(&popup.window)) + } +} diff --git a/src/windowing/state/builder.rs b/src/windowing/state/builder.rs index e8eee1c..58d5d6d 100644 --- a/src/windowing/state/builder.rs +++ b/src/windowing/state/builder.rs @@ -1,5 +1,8 @@ use std::rc::Rc; -use slint::{platform::set_platform, PhysicalSize}; +use slint::{ + platform::{set_platform, Platform, WindowAdapter}, + PhysicalSize, PlatformError, +}; use slint_interpreter::ComponentDefinition; use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; use wayland_client::{protocol::{wl_output::WlOutput, wl_pointer::WlPointer, wl_surface::WlSurface}, Connection}; @@ -9,6 +12,15 @@ use crate::{errors::{LayerShikaError, Result}, rendering::{femtovg_window::Femto use super::WindowState; +struct PlatformWrapper(Rc); + +impl Platform for PlatformWrapper { + #[allow(clippy::absolute_paths)] + fn create_window_adapter(&self) -> std::result::Result, PlatformError> { + self.0.create_window_adapter() + } +} + pub struct WindowStateBuilder { pub component_definition: Option, pub surface: Option>, @@ -116,17 +128,17 @@ impl WindowStateBuilder { self } - pub fn build(self) -> Result { - let platform = CustomSlintPlatform::new( - self.window - .as_ref() - .ok_or_else(|| LayerShikaError::InvalidInput("Window is required".into()))?, - ); - set_platform(Box::new(platform)).map_err(|e| { + pub fn build(self) -> Result<(WindowState, Rc)> { + let platform = + Rc::new(CustomSlintPlatform::new(self.window.as_ref().ok_or_else( + || LayerShikaError::InvalidInput("Window is required".into()), + )?)); + set_platform(Box::new(PlatformWrapper(Rc::clone(&platform)))).map_err(|e| { LayerShikaError::PlatformSetup(format!("Failed to set platform: {e:?}")) })?; - WindowState::new(self) + let state = WindowState::new(self)?; + Ok((state, platform)) } } diff --git a/src/windowing/state/dispatches.rs b/src/windowing/state/dispatches.rs index 267d8e2..6f4a747 100644 --- a/src/windowing/state/dispatches.rs +++ b/src/windowing/state/dispatches.rs @@ -1,3 +1,4 @@ +use super::WindowState; use crate::impl_empty_dispatch; use log::info; use slint::{ @@ -28,8 +29,12 @@ use wayland_protocols::wp::fractional_scale::v1::client::{ use wayland_protocols::wp::viewporter::client::{ wp_viewport::WpViewport, wp_viewporter::WpViewporter, }; - -use super::WindowState; +use wayland_protocols::xdg::shell::client::{ + xdg_popup::{self, XdgPopup}, + xdg_positioner::XdgPositioner, + xdg_surface::{self, XdgSurface}, + xdg_wm_base::{self, XdgWmBase}, +}; impl Dispatch for WindowState { #[allow(clippy::cast_possible_truncation)] @@ -154,41 +159,56 @@ impl Dispatch for WindowState { ) { match event { wl_pointer::Event::Enter { + serial, + surface, surface_x, surface_y, - .. + } => { + info!("Pointer entered surface {:?}", surface.id()); + state.set_last_pointer_serial(serial); + state.set_current_pointer_position(surface_x, surface_y); + + state.find_window_for_surface(&surface); + let position = *state.current_pointer_position(); + + state.dispatch_to_active_window(WindowEvent::PointerMoved { position }); } - | wl_pointer::Event::Motion { + + wl_pointer::Event::Motion { surface_x, surface_y, .. } => { state.set_current_pointer_position(surface_x, surface_y); - let logical_position = state.current_pointer_position(); - state.window().dispatch_event(WindowEvent::PointerMoved { - position: *logical_position, - }); + let position = *state.current_pointer_position(); + + state.dispatch_to_active_window(WindowEvent::PointerMoved { position }); } wl_pointer::Event::Leave { .. } => { - state.window().dispatch_event(WindowEvent::PointerExited); + state.dispatch_to_active_window(WindowEvent::PointerExited); + state.active_window = None; } wl_pointer::Event::Button { + serial, state: button_state, .. } => { + state.set_last_pointer_serial(serial); + let position = *state.current_pointer_position(); let event = match button_state { WEnum::Value(wl_pointer::ButtonState::Pressed) => WindowEvent::PointerPressed { button: PointerEventButton::Left, - position: *state.current_pointer_position(), + position, }, _ => WindowEvent::PointerReleased { button: PointerEventButton::Left, - position: *state.current_pointer_position(), + position, }, }; - state.window().dispatch_event(event); + + state.dispatch_to_active_window(event); } _ => {} } @@ -213,6 +233,72 @@ impl Dispatch for WindowState { } } +impl Dispatch for WindowState { + fn event( + _state: &mut Self, + xdg_wm_base: &XdgWmBase, + event: xdg_wm_base::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + info!("XdgWmBase ping received, sending pong with serial {serial}"); + xdg_wm_base.pong(serial); + } + } +} + +impl Dispatch for WindowState { + fn event( + _state: &mut Self, + _xdg_popup: &XdgPopup, + event: xdg_popup::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + match event { + xdg_popup::Event::Configure { + x, + y, + width, + height, + } => { + info!("XdgPopup Configure: position=({x}, {y}), size=({width}x{height})"); + } + xdg_popup::Event::PopupDone => { + info!("XdgPopup dismissed"); + } + xdg_popup::Event::Repositioned { token } => { + info!("XdgPopup repositioned with token {token}"); + } + _ => {} + } + } +} + +impl Dispatch for WindowState { + fn event( + state: &mut Self, + xdg_surface: &XdgSurface, + event: xdg_surface::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial } = event { + info!("XdgSurface Configure received, sending ack with serial {serial}"); + xdg_surface.ack_configure(serial); + + if let Some(popup_manager) = &state.popup_manager { + info!("Marking all popups as dirty after Configure"); + popup_manager.mark_all_popups_dirty(); + } + } + } +} + impl_empty_dispatch!( (WlRegistry, GlobalListContents), (WlCompositor, ()), @@ -221,5 +307,6 @@ impl_empty_dispatch!( (WlSeat, ()), (WpFractionalScaleManagerV1, ()), (WpViewporter, ()), - (WpViewport, ()) + (WpViewport, ()), + (XdgPositioner, ()) ); diff --git a/src/windowing/state/mod.rs b/src/windowing/state/mod.rs index 16a9b8f..44545f1 100644 --- a/src/windowing/state/mod.rs +++ b/src/windowing/state/mod.rs @@ -2,12 +2,14 @@ use std::rc::Rc; use builder::WindowStateBuilder; use log::info; use slint::{LogicalPosition, PhysicalSize, ComponentHandle}; +use slint::platform::{WindowAdapter, WindowEvent}; use slint_interpreter::ComponentInstance; use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; -use wayland_client::protocol::{wl_output::WlOutput, wl_surface::WlSurface}; +use wayland_client::{protocol::{wl_output::WlOutput, wl_surface::WlSurface}, Proxy}; use crate::rendering::femtovg_window::FemtoVGWindow; use crate::errors::{LayerShikaError, Result}; use crate::windowing::surface_dimensions::SurfaceDimensions; +use crate::windowing::popup_manager::PopupManager; use crate::windowing::proxies::{ ManagedWlPointer, ManagedWlSurface, ManagedZwlrLayerSurfaceV1, ManagedWpFractionalScaleV1, ManagedWpViewport, @@ -23,6 +25,12 @@ enum ScalingMode { Integer, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ActiveWindow { + Main, + Popup(usize), +} + pub struct WindowState { component_instance: ComponentInstance, viewport: Option, @@ -38,9 +46,12 @@ pub struct WindowState { output_size: PhysicalSize, window: Rc, current_pointer_position: LogicalPosition, + last_pointer_serial: u32, scale_factor: f32, height: u32, exclusive_zone: i32, + popup_manager: Option>, + active_window: Option, } impl WindowState { @@ -100,9 +111,12 @@ impl WindowState { output_size: builder.output_size.unwrap_or_default(), window, current_pointer_position: LogicalPosition::default(), + last_pointer_serial: 0, scale_factor: builder.scale_factor, height: builder.height, exclusive_zone: builder.exclusive_zone, + popup_manager: None, + active_window: None, }) } @@ -264,4 +278,50 @@ impl WindowState { pub const fn scale_factor(&self) -> f32 { self.scale_factor } + + pub const fn last_pointer_serial(&self) -> u32 { + self.last_pointer_serial + } + + pub const fn set_last_pointer_serial(&mut self, serial: u32) { + self.last_pointer_serial = serial; + } + + pub fn set_popup_manager(&mut self, popup_manager: Rc) { + self.popup_manager = Some(popup_manager); + } + + pub fn find_window_for_surface(&mut self, surface: &WlSurface) { + let surface_id = surface.id(); + + if (**self.surface.inner()).id() == surface_id { + self.active_window = Some(ActiveWindow::Main); + return; + } + + if let Some(popup_manager) = &self.popup_manager { + if let Some(popup_index) = popup_manager.find_popup_index_by_surface_id(&surface_id) { + self.active_window = Some(ActiveWindow::Popup(popup_index)); + return; + } + } + + self.active_window = None; + } + + pub fn dispatch_to_active_window(&self, event: WindowEvent) { + match self.active_window { + Some(ActiveWindow::Main) => { + self.window.window().dispatch_event(event); + } + Some(ActiveWindow::Popup(index)) => { + if let Some(popup_manager) = &self.popup_manager { + if let Some(popup_window) = popup_manager.get_popup_window(index) { + popup_window.dispatch_event(event); + } + } + } + None => {} + } + } } diff --git a/src/windowing/system.rs b/src/windowing/system.rs index 177f1c4..c24483e 100644 --- a/src/windowing/system.rs +++ b/src/windowing/system.rs @@ -1,17 +1,20 @@ use super::{ config::{LayerSurfaceParams, WindowConfig}, globals::GlobalCtx, + popup_manager::{PopupContext, PopupManager}, state::{builder::WindowStateBuilder, WindowState}, surface::{SurfaceCtx, SurfaceSetupParams}, }; use crate::{ errors::{LayerShikaError, Result}, - rendering::{egl_context::EGLContext, femtovg_window::FemtoVGWindow}, + rendering::{ + egl_context::EGLContext, femtovg_window::FemtoVGWindow, slint_platform::CustomSlintPlatform, + }, }; use log::{error, info}; use slint::{ - platform::{femtovg_renderer::FemtoVGRenderer, update_timers_and_animations}, - LogicalPosition, PhysicalSize, + platform::{femtovg_renderer::FemtoVGRenderer, update_timers_and_animations, WindowAdapter}, + LogicalPosition, PhysicalSize, PlatformError, WindowPosition, }; use slint_interpreter::ComponentInstance; use smithay_client_toolkit::reexports::calloop::{ @@ -28,21 +31,43 @@ pub struct WindowingSystem { connection: Rc, event_queue: EventQueue, event_loop: EventLoop<'static, WindowState>, + popup_manager: Rc, } impl WindowingSystem { pub(super) fn new(config: WindowConfig) -> Result { info!("Initializing WindowingSystem"); let (connection, event_queue) = Self::init_wayland_connection()?; - let state = Self::init_state(config, &connection, &event_queue)?; + let (state, global_ctx, platform) = Self::init_state(config, &connection, &event_queue)?; let event_loop = EventLoop::try_new().map_err(|e| LayerShikaError::EventLoop(e.to_string()))?; + let popup_context = PopupContext::new( + global_ctx.compositor, + global_ctx.xdg_wm_base, + global_ctx.seat, + global_ctx.fractional_scale_manager, + global_ctx.viewporter, + connection.display(), + Rc::clone(&connection), + ); + + let popup_manager = Rc::new(PopupManager::new(popup_context)); + + Self::setup_popup_creator(&popup_manager, &platform, &state, &event_queue); + Ok(Self { state, connection, event_queue, event_loop, + popup_manager, + }) + .map(|mut system| { + system + .state + .set_popup_manager(Rc::clone(&system.popup_manager)); + system }) } @@ -57,7 +82,7 @@ impl WindowingSystem { config: WindowConfig, connection: &Connection, event_queue: &EventQueue, - ) -> Result { + ) -> Result<(WindowState, GlobalCtx, Rc)> { let global_ctx = GlobalCtx::initialize(connection, &event_queue.handle()) .map_err(|e| LayerShikaError::GlobalInitialization(e.to_string()))?; @@ -83,7 +108,7 @@ impl WindowingSystem { let surface_ctx = SurfaceCtx::setup(&setup_params, &layer_surface_params); let pointer = Rc::new(global_ctx.seat.get_pointer(&event_queue.handle(), ())); - let output = Rc::new(global_ctx.output); + let output = Rc::new(global_ctx.output.clone()); let window = Self::initialize_renderer(&surface_ctx.surface, &connection.display(), &config) .map_err(|e| LayerShikaError::EGLContextCreation(e.to_string()))?; @@ -108,9 +133,47 @@ impl WindowingSystem { builder = builder.with_viewport(Rc::clone(vp)); } - builder + let (state, platform) = builder .build() - .map_err(|e| LayerShikaError::WindowConfiguration(e.to_string())) + .map_err(|e| LayerShikaError::WindowConfiguration(e.to_string()))?; + + Ok((state, global_ctx, platform)) + } + + fn setup_popup_creator( + popup_manager: &Rc, + platform: &Rc, + state: &WindowState, + event_queue: &EventQueue, + ) { + if !popup_manager.has_xdg_shell() { + info!("xdg-shell not available, popups will not be supported"); + return; + } + + info!("Setting up popup creator with xdg-shell support"); + + let popup_manager_clone = Rc::clone(popup_manager); + let layer_surface = state.layer_surface(); + let queue_handle = event_queue.handle(); + let scale_factor = state.scale_factor(); + let last_serial = state.last_pointer_serial(); + + platform.set_popup_creator(move || { + info!("Popup creator called! Creating popup window..."); + + let result = popup_manager_clone + .create_popup(&queue_handle, &layer_surface, last_serial, scale_factor) + .map(|w| w as Rc) + .map_err(|e| PlatformError::Other(format!("Failed to create popup: {e}"))); + + match &result { + Ok(_) => info!("Popup created successfully"), + Err(e) => info!("Popup creation failed: {e:?}"), + } + + result + }); } fn initialize_renderer( @@ -133,7 +196,7 @@ impl WindowingSystem { let femtovg_window = FemtoVGWindow::new(renderer); femtovg_window.set_size(slint::WindowSize::Physical(init_size)); femtovg_window.set_scale_factor(config.scale_factor); - femtovg_window.set_position(LogicalPosition::new(0., 0.)); + femtovg_window.set_position(WindowPosition::Logical(LogicalPosition::new(0., 0.))); Ok(femtovg_window) } @@ -145,10 +208,6 @@ impl WindowingSystem { pub fn run(&mut self) -> Result<()> { info!("Starting WindowingSystem main loop"); - // Process all initial configuration events by repeatedly dispatching until queue is empty. - // After each batch, flush and render to allow compositor to respond with additional - // configure events (e.g., after fractional scale is applied). - // This ensures proper initialization. info!("Processing initial Wayland configuration events"); while self .event_queue @@ -156,12 +215,10 @@ impl WindowingSystem { .map_err(|e| LayerShikaError::WaylandProtocol(e.to_string()))? > 0 { - // Flush requests to compositor after processing each event batch self.connection .flush() .map_err(|e| LayerShikaError::WaylandProtocol(e.to_string()))?; - // Render if window was marked dirty by the configure events update_timers_and_animations(); self.state .window() @@ -169,17 +226,17 @@ impl WindowingSystem { .map_err(|e| LayerShikaError::Rendering(e.to_string()))?; } - // Setup Wayland file descriptor as event source for ongoing events self.setup_wayland_event_source()?; let event_queue = &mut self.event_queue; let connection = &self.connection; + let popup_manager = Rc::clone(&self.popup_manager); - // Enter the main event loop with consistent event processing: - // read -> dispatch -> animate -> render -> flush self.event_loop .run(None, &mut self.state, move |shared_data| { - if let Err(e) = Self::process_events(connection, event_queue, shared_data) { + if let Err(e) = + Self::process_events(connection, event_queue, shared_data, &popup_manager) + { error!("Error processing events: {e}"); } }) @@ -204,29 +261,29 @@ impl WindowingSystem { connection: &Connection, event_queue: &mut EventQueue, shared_data: &mut WindowState, + popup_manager: &PopupManager, ) -> Result<()> { - // 1. READ: Read pending events from Wayland socket if available if let Some(guard) = event_queue.prepare_read() { guard .read() .map_err(|e| LayerShikaError::WaylandProtocol(e.to_string()))?; } - // 2. DISPATCH: Process all queued Wayland events event_queue .dispatch_pending(shared_data) .map_err(|e| LayerShikaError::WaylandProtocol(e.to_string()))?; - // 3. ANIMATE: Update Slint timers and animations update_timers_and_animations(); - // 4. RENDER: Render frame if window was marked dirty shared_data .window() .render_frame_if_dirty() .map_err(|e| LayerShikaError::Rendering(e.to_string()))?; - // 5. FLUSH: Send buffered Wayland requests to compositor + popup_manager + .render_popups() + .map_err(|e| LayerShikaError::Rendering(e.to_string()))?; + connection .flush() .map_err(|e| LayerShikaError::WaylandProtocol(e.to_string()))?;