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, ¶ms, 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 }
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}