layer_shika_adapters/wayland/surfaces/
popup_manager.rs

1use crate::errors::{LayerShikaError, Result};
2use crate::rendering::egl::context_factory::RenderContextFactory;
3use crate::rendering::femtovg::popup_window::PopupWindow;
4use crate::rendering::femtovg::renderable_window::{FractionalScaleConfig, RenderableWindow};
5use crate::wayland::surfaces::display_metrics::{DisplayMetrics, SharedDisplayMetrics};
6use layer_shika_domain::dimensions::LogicalSize as DomainLogicalSize;
7use layer_shika_domain::surface_dimensions::SurfaceDimensions;
8use layer_shika_domain::value_objects::handle::PopupHandle;
9use layer_shika_domain::value_objects::popup_behavior::ConstraintAdjustment;
10use layer_shika_domain::value_objects::popup_config::PopupConfig;
11use layer_shika_domain::value_objects::popup_position::PopupPosition;
12use log::info;
13use slint::{platform::femtovg_renderer::FemtoVGRenderer, PhysicalSize};
14use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1;
15use std::cell::{Cell, RefCell};
16use std::collections::{HashMap, VecDeque};
17use std::rc::Rc;
18use wayland_client::{
19    backend::ObjectId,
20    protocol::{wl_compositor::WlCompositor, wl_seat::WlSeat, wl_surface::WlSurface},
21    Connection, Proxy, QueueHandle,
22};
23use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1;
24use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1;
25use wayland_protocols::wp::viewporter::client::wp_viewporter::WpViewporter;
26use wayland_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase;
27
28use super::app_state::AppState;
29use super::popup_surface::PopupSurface;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ActiveWindow {
33    Main,
34    Popup(PopupHandle),
35    None,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub(crate) struct PopupId(usize);
40
41impl PopupId {
42    #[must_use]
43    const fn key(self) -> usize {
44        self.0
45    }
46
47    #[must_use]
48    const fn from_handle(handle: PopupHandle) -> Self {
49        Self(handle.key())
50    }
51
52    #[must_use]
53    const fn to_handle(self) -> PopupHandle {
54        PopupHandle::from_raw(self.0)
55    }
56}
57
58pub type OnCloseCallback = Box<dyn Fn(PopupHandle)>;
59
60#[derive(Debug, Clone)]
61pub struct CreatePopupParams {
62    pub last_pointer_serial: u32,
63    pub width: f32,
64    pub height: f32,
65    pub position: PopupPosition,
66    pub constraint_adjustment: ConstraintAdjustment,
67    pub grab: bool,
68}
69
70#[derive(Clone)]
71pub struct PopupContext {
72    compositor: WlCompositor,
73    xdg_wm_base: Option<XdgWmBase>,
74    seat: WlSeat,
75    fractional_scale_manager: Option<WpFractionalScaleManagerV1>,
76    viewporter: Option<WpViewporter>,
77    render_factory: Rc<RenderContextFactory>,
78}
79
80impl PopupContext {
81    #[must_use]
82    #[allow(clippy::too_many_arguments)]
83    pub fn new(
84        compositor: WlCompositor,
85        xdg_wm_base: Option<XdgWmBase>,
86        seat: WlSeat,
87        fractional_scale_manager: Option<WpFractionalScaleManagerV1>,
88        viewporter: Option<WpViewporter>,
89        _connection: Rc<Connection>,
90        render_factory: Rc<RenderContextFactory>,
91    ) -> Self {
92        Self {
93            compositor,
94            xdg_wm_base,
95            seat,
96            fractional_scale_manager,
97            viewporter,
98            render_factory,
99        }
100    }
101}
102
103struct ActivePopup {
104    surface: PopupSurface,
105    window: Rc<PopupWindow>,
106}
107
108impl Drop for ActivePopup {
109    fn drop(&mut self) {
110        info!("ActivePopup being dropped - cleaning up resources");
111        self.window.cleanup_resources();
112        self.surface.destroy();
113    }
114}
115
116struct PendingPopup {
117    id: PopupId,
118    config: PopupConfig,
119    width: f32,
120    height: f32,
121}
122
123struct PopupManagerState {
124    popups: HashMap<PopupId, ActivePopup>,
125    display_metrics: SharedDisplayMetrics,
126    pending_popups: VecDeque<PendingPopup>,
127}
128
129impl PopupManagerState {
130    fn new(display_metrics: SharedDisplayMetrics) -> Self {
131        Self {
132            popups: HashMap::new(),
133            display_metrics,
134            pending_popups: VecDeque::new(),
135        }
136    }
137}
138
139pub struct PopupManager {
140    context: PopupContext,
141    state: RefCell<PopupManagerState>,
142    scale_factor: Cell<f32>,
143}
144
145impl PopupManager {
146    #[must_use]
147    pub fn new(context: PopupContext, display_metrics: SharedDisplayMetrics) -> Self {
148        let scale_factor = display_metrics.borrow().scale_factor();
149        Self {
150            context,
151            state: RefCell::new(PopupManagerState::new(display_metrics)),
152            scale_factor: Cell::new(scale_factor),
153        }
154    }
155
156    pub fn request_popup(
157        &self,
158        handle: PopupHandle,
159        config: PopupConfig,
160        width: f32,
161        height: f32,
162    ) -> PopupHandle {
163        let mut state = self.state.borrow_mut();
164
165        let id = PopupId::from_handle(handle);
166
167        state.pending_popups.push_back(PendingPopup {
168            id,
169            config,
170            width,
171            height,
172        });
173
174        handle
175    }
176
177    #[must_use]
178    pub(crate) fn take_pending_popup_params(&self) -> Option<(PopupId, PopupConfig, f32, f32)> {
179        self.state
180            .borrow_mut()
181            .pending_popups
182            .pop_front()
183            .map(|p| (p.id, p.config, p.width, p.height))
184    }
185
186    #[must_use]
187    pub fn has_pending_popup(&self) -> bool {
188        !self.state.borrow().pending_popups.is_empty()
189    }
190
191    #[must_use]
192    pub fn scale_factor(&self) -> f32 {
193        self.scale_factor.get()
194    }
195
196    #[must_use]
197    pub fn output_size(&self) -> PhysicalSize {
198        self.state.borrow().display_metrics.borrow().output_size()
199    }
200
201    pub fn update_scale_factor(&self, scale_factor: f32) {
202        self.scale_factor.set(scale_factor);
203
204        let render_scale = FractionalScaleConfig::render_scale(scale_factor);
205        for popup in self.state.borrow().popups.values() {
206            popup.window.set_scale_factor(render_scale);
207        }
208        self.mark_all_popups_dirty();
209    }
210
211    pub fn update_output_size(&self, output_size: PhysicalSize) {
212        self.state
213            .borrow()
214            .display_metrics
215            .borrow_mut()
216            .update_output_size(output_size);
217    }
218
219    pub fn create_pending_popup(
220        self: &Rc<Self>,
221        queue_handle: &QueueHandle<AppState>,
222        parent_layer_surface: &ZwlrLayerSurfaceV1,
223        last_pointer_serial: u32,
224    ) -> Result<Rc<PopupWindow>> {
225        let (id, config, width, height) = self.take_pending_popup_params().ok_or_else(|| {
226            LayerShikaError::WindowConfiguration {
227                message: "No pending popup request available".into(),
228            }
229        })?;
230
231        let params = CreatePopupParams {
232            last_pointer_serial,
233            width,
234            height,
235            position: config.position.clone(),
236            constraint_adjustment: config.behavior.constraint_adjustment,
237            grab: config.behavior.grab,
238        };
239
240        self.create_popup_internal(queue_handle, parent_layer_surface, &params, id)
241    }
242
243    fn create_popup_internal(
244        self: &Rc<Self>,
245        queue_handle: &QueueHandle<AppState>,
246        parent_layer_surface: &ZwlrLayerSurfaceV1,
247        params: &CreatePopupParams,
248        popup_id: PopupId,
249    ) -> Result<Rc<PopupWindow>> {
250        let xdg_wm_base = self.context.xdg_wm_base.as_ref().ok_or_else(|| {
251            LayerShikaError::WindowConfiguration {
252                message: "xdg-shell not available for popups".into(),
253            }
254        })?;
255
256        let scale_factor = self.scale_factor();
257        info!(
258            "Creating popup window with scale factor {scale_factor}, size=({} x {}), position={:?}",
259            params.width, params.height, params.position
260        );
261
262        let output_size = self.output_size();
263        #[allow(clippy::cast_precision_loss)]
264        let output_logical_size = DomainLogicalSize::from_raw(
265            output_size.width as f32 / scale_factor,
266            output_size.height as f32 / scale_factor,
267        );
268
269        let popup_logical_size = DomainLogicalSize::from_raw(params.width, params.height);
270        let domain_scale = self
271            .state
272            .borrow()
273            .display_metrics
274            .borrow()
275            .scale_factor_typed();
276        let popup_dimensions = SurfaceDimensions::from_logical(popup_logical_size, domain_scale);
277
278        let popup_size = PhysicalSize::new(
279            popup_dimensions.physical_width(),
280            popup_dimensions.physical_height(),
281        );
282
283        info!("Popup physical size: {popup_size:?}");
284
285        let wayland_popup_surface =
286            PopupSurface::create(&super::popup_surface::PopupSurfaceParams {
287                compositor: &self.context.compositor,
288                xdg_wm_base,
289                parent_layer_surface,
290                fractional_scale_manager: self.context.fractional_scale_manager.as_ref(),
291                viewporter: self.context.viewporter.as_ref(),
292                queue_handle,
293                position: params.position.clone(),
294                output_bounds: output_logical_size,
295                constraint_adjustment: params.constraint_adjustment,
296                physical_size: popup_size,
297                scale_factor,
298            });
299
300        if params.grab {
301            wayland_popup_surface.grab(&self.context.seat, params.last_pointer_serial);
302        } else {
303            info!("Skipping popup grab (grab disabled in request)");
304            wayland_popup_surface.surface.commit();
305        }
306
307        let context = self
308            .context
309            .render_factory
310            .create_context(&wayland_popup_surface.surface.id(), popup_size)?;
311
312        let renderer = FemtoVGRenderer::new(context)
313            .map_err(|e| LayerShikaError::FemtoVGRendererCreation { source: e })?;
314
315        let on_close: OnCloseCallback = {
316            let weak_self = Rc::downgrade(self);
317            Box::new(move |handle: PopupHandle| {
318                if let Some(manager) = weak_self.upgrade() {
319                    let id = PopupId::from_handle(handle);
320                    manager.destroy_popup(id);
321                }
322            })
323        };
324
325        let popup_window = PopupWindow::new_with_callback(renderer, on_close);
326        popup_window.set_popup_id(popup_id.to_handle());
327
328        let config = FractionalScaleConfig::new(params.width, params.height, scale_factor);
329        info!(
330            "Popup using render scale {} (from {}), render_physical {}x{}",
331            config.render_scale,
332            scale_factor,
333            config.render_physical_size.width,
334            config.render_physical_size.height
335        );
336        config.apply_to(popup_window.as_ref());
337
338        let mut state = self.state.borrow_mut();
339        state.popups.insert(
340            popup_id,
341            ActivePopup {
342                surface: wayland_popup_surface,
343                window: Rc::clone(&popup_window),
344            },
345        );
346
347        info!("Popup window created successfully with id {:?}", popup_id);
348
349        Ok(popup_window)
350    }
351
352    pub fn render_popups(&self) -> Result<()> {
353        let state = self.state.borrow();
354        for popup in state.popups.values() {
355            popup.window.render_frame_if_dirty()?;
356        }
357        Ok(())
358    }
359
360    pub const fn has_xdg_shell(&self) -> bool {
361        self.context.xdg_wm_base.is_some()
362    }
363
364    pub fn mark_all_popups_dirty(&self) {
365        let state = self.state.borrow();
366        for popup in state.popups.values() {
367            popup.window.request_redraw();
368        }
369    }
370
371    pub fn find_popup_key_by_surface_id(&self, surface_id: &ObjectId) -> Option<usize> {
372        self.state
373            .borrow()
374            .popups
375            .iter()
376            .find_map(|(id, popup)| (popup.surface.surface.id() == *surface_id).then_some(id.key()))
377    }
378
379    pub fn find_popup_key_by_fractional_scale_id(
380        &self,
381        fractional_scale_id: &ObjectId,
382    ) -> Option<usize> {
383        self.state.borrow().popups.iter().find_map(|(id, popup)| {
384            popup
385                .surface
386                .fractional_scale
387                .as_ref()
388                .filter(|fs| fs.id() == *fractional_scale_id)
389                .map(|_| id.key())
390        })
391    }
392
393    pub fn get_popup_window(&self, key: usize) -> Option<Rc<PopupWindow>> {
394        let id = PopupId(key);
395        self.state
396            .borrow()
397            .popups
398            .get(&id)
399            .map(|popup| Rc::clone(&popup.window))
400    }
401
402    fn destroy_popup(&self, id: PopupId) {
403        if let Some(_popup) = self.state.borrow_mut().popups.remove(&id) {
404            info!("Destroying popup with id {:?}", id);
405            // cleanup happens automatically via ActivePopup::drop()
406        }
407    }
408
409    pub fn find_popup_key_by_xdg_popup_id(&self, xdg_popup_id: &ObjectId) -> Option<usize> {
410        self.state.borrow().popups.iter().find_map(|(id, popup)| {
411            (popup.surface.xdg_popup.id() == *xdg_popup_id).then_some(id.key())
412        })
413    }
414
415    pub fn find_popup_key_by_xdg_surface_id(&self, xdg_surface_id: &ObjectId) -> Option<usize> {
416        self.state.borrow().popups.iter().find_map(|(id, popup)| {
417            (popup.surface.xdg_surface.id() == *xdg_surface_id).then_some(id.key())
418        })
419    }
420
421    pub fn update_popup_viewport(&self, key: usize, logical_width: i32, logical_height: i32) {
422        let id = PopupId(key);
423        if let Some(popup) = self.state.borrow().popups.get(&id) {
424            popup.window.begin_repositioning();
425            popup
426                .surface
427                .update_viewport_size(logical_width, logical_height);
428        }
429    }
430
431    pub fn commit_popup_surface(&self, key: usize) {
432        let id = PopupId(key);
433        if let Some(popup) = self.state.borrow().popups.get(&id) {
434            popup.surface.surface.commit();
435            popup.window.end_repositioning();
436            popup.window.request_redraw();
437        }
438    }
439
440    pub fn mark_popup_configured(&self, key: usize) {
441        let id = PopupId(key);
442        if let Some(popup) = self.state.borrow().popups.get(&id) {
443            popup.window.mark_configured();
444        }
445    }
446
447    pub fn close(&self, handle: PopupHandle) -> Result<()> {
448        let id = PopupId::from_handle(handle);
449        self.destroy_popup(id);
450        Ok(())
451    }
452
453    #[must_use]
454    pub fn find_by_surface(&self, surface_id: &ObjectId) -> Option<PopupHandle> {
455        self.find_popup_key_by_surface_id(surface_id)
456            .map(PopupHandle::from_raw)
457    }
458
459    #[must_use]
460    pub fn find_by_fractional_scale(&self, fractional_scale_id: &ObjectId) -> Option<PopupHandle> {
461        self.find_popup_key_by_fractional_scale_id(fractional_scale_id)
462            .map(PopupHandle::from_raw)
463    }
464
465    #[must_use]
466    pub fn find_by_xdg_popup(&self, xdg_popup_id: &ObjectId) -> Option<PopupHandle> {
467        self.find_popup_key_by_xdg_popup_id(xdg_popup_id)
468            .map(PopupHandle::from_raw)
469    }
470
471    #[must_use]
472    pub fn find_by_xdg_surface(&self, xdg_surface_id: &ObjectId) -> Option<PopupHandle> {
473        self.find_popup_key_by_xdg_surface_id(xdg_surface_id)
474            .map(PopupHandle::from_raw)
475    }
476
477    #[must_use]
478    pub fn get_active_window(
479        &self,
480        surface: &WlSurface,
481        main_surface_id: &ObjectId,
482    ) -> ActiveWindow {
483        let surface_id = surface.id();
484
485        if *main_surface_id == surface_id {
486            return ActiveWindow::Main;
487        }
488
489        if let Some(popup_handle) = self
490            .find_popup_key_by_surface_id(&surface_id)
491            .map(PopupHandle::from_raw)
492        {
493            return ActiveWindow::Popup(popup_handle);
494        }
495
496        ActiveWindow::None
497    }
498
499    pub fn update_scale_for_fractional_scale_object(
500        &self,
501        fractional_scale_proxy: &WpFractionalScaleV1,
502        scale_120ths: u32,
503    ) {
504        let fractional_scale_id = fractional_scale_proxy.id();
505
506        if let Some(popup_key) = self.find_popup_key_by_fractional_scale_id(&fractional_scale_id) {
507            if let Some(popup_surface) = self.get_popup_window(popup_key) {
508                let new_scale_factor = DisplayMetrics::scale_factor_from_120ths(scale_120ths);
509                let render_scale = FractionalScaleConfig::render_scale(new_scale_factor);
510                info!(
511                    "Updating popup scale factor to {} (render scale {}, from {}x)",
512                    new_scale_factor, render_scale, scale_120ths
513                );
514                popup_surface.set_scale_factor(render_scale);
515                popup_surface.request_redraw();
516            }
517        }
518    }
519}