feat: add initial popup support

This commit is contained in:
drendog 2025-10-26 02:06:52 +01:00
parent 87fc46750c
commit 4ea4171eb7
Signed by: dwenya
GPG key ID: 8DD77074645332D0
11 changed files with 682 additions and 53 deletions

View file

@ -1,3 +1,4 @@
pub mod egl_context;
pub mod femtovg_window;
pub mod popup_window;
pub mod slint_platform;

View file

@ -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<RenderState>,
size: Cell<PhysicalSize>,
scale_factor: Cell<f32>,
}
#[allow(dead_code)]
impl PopupWindow {
pub fn new(renderer: FemtoVGRenderer) -> Rc<Self> {
Rc::new_cyclic(|weak_self| {
let window = Window::new(Weak::clone(weak_self) as Weak<dyn WindowAdapter>);
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
}
}

View file

@ -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<Rc<dyn WindowAdapter>, PlatformError>;
pub struct CustomSlintPlatform {
window: Weak<FemtoVGWindow>,
main_window: Weak<FemtoVGWindow>,
popup_creator: RefCell<Option<Rc<PopupCreator>>>,
first_call: Cell<bool>,
}
impl CustomSlintPlatform {
pub fn new(window: &Rc<FemtoVGWindow>) -> 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<F>(&self, creator: F)
where
F: Fn() -> Result<Rc<dyn WindowAdapter>, PlatformError> + 'static,
{
*self.popup_creator.borrow_mut() = Some(Rc::new(creator));
}
}
impl Platform for CustomSlintPlatform {
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter + 'static>, PlatformError> {
self.window
.upgrade()
.ok_or(PlatformError::NoPlatform)
.map(|w| w as Rc<dyn WindowAdapter>)
if self.first_call.get() {
self.first_call.set(false);
self.main_window
.upgrade()
.ok_or(PlatformError::NoPlatform)
.map(|w| w as Rc<dyn WindowAdapter>)
} else if let Some(creator) = self.popup_creator.borrow().as_ref() {
creator()
} else {
Err(PlatformError::NoPlatform)
}
}
}

View file

@ -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<XdgWmBase>,
pub fractional_scale_manager: Option<WpFractionalScaleManagerV1>,
pub viewporter: Option<WpViewporter>,
}
@ -38,6 +41,10 @@ impl GlobalCtx {
(WlSeat, seat, 1..=9)
)?;
let xdg_wm_base = global_list
.bind::<XdgWmBase, _, _>(queue_handle, 1..=6, ())
.ok();
let fractional_scale_manager = global_list
.bind::<WpFractionalScaleManagerV1, _, _>(queue_handle, 1..=1, ())
.ok();
@ -46,6 +53,10 @@ impl GlobalCtx {
.bind::<WpViewporter, _, _>(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,
})

View file

@ -2,6 +2,8 @@ pub mod builder;
mod config;
mod globals;
mod macros;
mod popup;
mod popup_manager;
mod proxies;
mod state;
mod surface;

119
src/windowing/popup.rs Normal file
View file

@ -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<WindowState>,
pub position: LogicalPosition,
pub size: PhysicalSize,
pub scale_factor: f32,
}
#[allow(dead_code)]
pub struct PopupSurface {
pub surface: Rc<WlSurface>,
pub xdg_surface: Rc<XdgSurface>,
pub xdg_popup: Rc<XdgPopup>,
pub fractional_scale: Option<Rc<WpFractionalScaleV1>>,
pub viewport: Option<Rc<WpViewport>>,
}
#[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);
}
}

View file

@ -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<XdgWmBase>,
seat: WlSeat,
fractional_scale_manager: Option<WpFractionalScaleManagerV1>,
viewporter: Option<WpViewporter>,
display: WlDisplay,
#[allow(dead_code)]
connection: Rc<Connection>,
}
impl PopupContext {
pub const fn new(
compositor: WlCompositor,
xdg_wm_base: Option<XdgWmBase>,
seat: WlSeat,
fractional_scale_manager: Option<WpFractionalScaleManagerV1>,
viewporter: Option<WpViewporter>,
display: WlDisplay,
connection: Rc<Connection>,
) -> Self {
Self {
compositor,
xdg_wm_base,
seat,
fractional_scale_manager,
viewporter,
display,
connection,
}
}
}
struct ActivePopup {
surface: PopupSurface,
window: Rc<PopupWindow>,
}
pub struct PopupManager {
context: PopupContext,
popups: RefCell<Vec<ActivePopup>>,
}
impl PopupManager {
pub const fn new(context: PopupContext) -> Self {
Self {
context,
popups: RefCell::new(Vec::new()),
}
}
pub fn create_popup(
&self,
queue_handle: &QueueHandle<WindowState>,
parent_layer_surface: &ZwlrLayerSurfaceV1,
last_pointer_serial: u32,
scale_factor: f32,
) -> Result<Rc<PopupWindow>> {
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<usize> {
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<Rc<PopupWindow>> {
self.popups
.borrow()
.get(index)
.map(|popup| Rc::clone(&popup.window))
}
}

View file

@ -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<CustomSlintPlatform>);
impl Platform for PlatformWrapper {
#[allow(clippy::absolute_paths)]
fn create_window_adapter(&self) -> std::result::Result<Rc<dyn WindowAdapter>, PlatformError> {
self.0.create_window_adapter()
}
}
pub struct WindowStateBuilder {
pub component_definition: Option<ComponentDefinition>,
pub surface: Option<Rc<WlSurface>>,
@ -116,17 +128,17 @@ impl WindowStateBuilder {
self
}
pub fn build(self) -> Result<WindowState> {
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<CustomSlintPlatform>)> {
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))
}
}

View file

@ -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<ZwlrLayerSurfaceV1, ()> for WindowState {
#[allow(clippy::cast_possible_truncation)]
@ -154,41 +159,56 @@ impl Dispatch<WlPointer, ()> 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<WpFractionalScaleV1, ()> for WindowState {
}
}
impl Dispatch<XdgWmBase, ()> for WindowState {
fn event(
_state: &mut Self,
xdg_wm_base: &XdgWmBase,
event: xdg_wm_base::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
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<XdgPopup, ()> for WindowState {
fn event(
_state: &mut Self,
_xdg_popup: &XdgPopup,
event: xdg_popup::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
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<XdgSurface, ()> for WindowState {
fn event(
state: &mut Self,
xdg_surface: &XdgSurface,
event: xdg_surface::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
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, ())
);

View file

@ -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<ManagedWpViewport>,
@ -38,9 +46,12 @@ pub struct WindowState {
output_size: PhysicalSize,
window: Rc<FemtoVGWindow>,
current_pointer_position: LogicalPosition,
last_pointer_serial: u32,
scale_factor: f32,
height: u32,
exclusive_zone: i32,
popup_manager: Option<Rc<PopupManager>>,
active_window: Option<ActiveWindow>,
}
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<PopupManager>) {
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 => {}
}
}
}

View file

@ -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<Connection>,
event_queue: EventQueue<WindowState>,
event_loop: EventLoop<'static, WindowState>,
popup_manager: Rc<PopupManager>,
}
impl WindowingSystem {
pub(super) fn new(config: WindowConfig) -> Result<Self> {
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<WindowState>,
) -> Result<WindowState> {
) -> Result<(WindowState, GlobalCtx, Rc<CustomSlintPlatform>)> {
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<PopupManager>,
platform: &Rc<CustomSlintPlatform>,
state: &WindowState,
event_queue: &EventQueue<WindowState>,
) {
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<dyn WindowAdapter>)
.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<WindowState>,
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()))?;