1 | //! The state of the window, which is shared with the event-loop. |
2 | |
3 | use std::num::NonZeroU32; |
4 | use std::sync::{Arc, Weak}; |
5 | use std::time::Duration; |
6 | |
7 | use ahash::HashSet; |
8 | use log::{info, warn}; |
9 | |
10 | use sctk::reexports::client::backend::ObjectId; |
11 | use sctk::reexports::client::protocol::wl_seat::WlSeat; |
12 | use sctk::reexports::client::protocol::wl_shm::WlShm; |
13 | use sctk::reexports::client::protocol::wl_surface::WlSurface; |
14 | use sctk::reexports::client::{Connection, Proxy, QueueHandle}; |
15 | use sctk::reexports::csd_frame::{ |
16 | DecorationsFrame, FrameAction, FrameClick, ResizeEdge, WindowState as XdgWindowState, |
17 | }; |
18 | use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; |
19 | use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; |
20 | use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; |
21 | use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge; |
22 | |
23 | use sctk::compositor::{CompositorState, Region}; |
24 | use sctk::seat::pointer::ThemedPointer; |
25 | use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; |
26 | use sctk::shell::xdg::XdgSurface; |
27 | use sctk::shell::WaylandSurface; |
28 | use sctk::shm::Shm; |
29 | use sctk::subcompositor::SubcompositorState; |
30 | use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; |
31 | |
32 | use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size}; |
33 | use crate::error::{ExternalError, NotSupportedError}; |
34 | use crate::platform_impl::wayland::logical_to_physical_rounded; |
35 | use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; |
36 | use crate::platform_impl::WindowId; |
37 | use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme}; |
38 | |
39 | use crate::platform_impl::wayland::seat::{ |
40 | PointerConstraintsState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, |
41 | }; |
42 | use crate::platform_impl::wayland::state::{WindowCompositorUpdate, WinitState}; |
43 | |
44 | #[cfg (feature = "sctk-adwaita" )] |
45 | pub type WinitFrame = sctk_adwaita::AdwaitaFrame<WinitState>; |
46 | #[cfg (not(feature = "sctk-adwaita" ))] |
47 | pub type WinitFrame = sctk::shell::xdg::fallback_frame::FallbackFrame<WinitState>; |
48 | |
49 | // Minimum window inner size. |
50 | const MIN_WINDOW_SIZE: LogicalSize<u32> = LogicalSize::new(width:2, height:1); |
51 | |
52 | /// The state of the window which is being updated from the [`WinitState`]. |
53 | pub struct WindowState { |
54 | /// The connection to Wayland server. |
55 | pub connection: Connection, |
56 | |
57 | /// The window frame, which is created from the configure request. |
58 | frame: Option<WinitFrame>, |
59 | |
60 | /// The `Shm` to set cursor. |
61 | pub shm: WlShm, |
62 | |
63 | /// The last received configure. |
64 | pub last_configure: Option<WindowConfigure>, |
65 | |
66 | /// The pointers observed on the window. |
67 | pub pointers: Vec<Weak<ThemedPointer<WinitPointerData>>>, |
68 | |
69 | /// Cursor icon. |
70 | pub cursor_icon: CursorIcon, |
71 | |
72 | /// Wether the cursor is visible. |
73 | pub cursor_visible: bool, |
74 | |
75 | /// Pointer constraints to lock/confine pointer. |
76 | pub pointer_constraints: Option<Arc<PointerConstraintsState>>, |
77 | |
78 | /// Queue handle. |
79 | pub queue_handle: QueueHandle<WinitState>, |
80 | |
81 | /// Theme varaint. |
82 | theme: Option<Theme>, |
83 | |
84 | /// The current window title. |
85 | title: String, |
86 | |
87 | /// Whether the frame is resizable. |
88 | resizable: bool, |
89 | |
90 | // NOTE: we can't use simple counter, since it's racy when seat getting destroyed and new |
91 | // is created, since add/removed stuff could be delivered a bit out of order. |
92 | /// Seats that has keyboard focus on that window. |
93 | seat_focus: HashSet<ObjectId>, |
94 | |
95 | /// The scale factor of the window. |
96 | scale_factor: f64, |
97 | |
98 | /// Whether the window is transparent. |
99 | transparent: bool, |
100 | |
101 | /// The state of the compositor to create WlRegions. |
102 | compositor: Arc<CompositorState>, |
103 | |
104 | /// The current cursor grabbing mode. |
105 | cursor_grab_mode: GrabState, |
106 | |
107 | /// Whether the IME input is allowed for that window. |
108 | ime_allowed: bool, |
109 | |
110 | /// The current IME purpose. |
111 | ime_purpose: ImePurpose, |
112 | |
113 | /// The text inputs observed on the window. |
114 | text_inputs: Vec<ZwpTextInputV3>, |
115 | |
116 | /// The inner size of the window, as in without client side decorations. |
117 | size: LogicalSize<u32>, |
118 | |
119 | /// Whether the CSD fail to create, so we don't try to create them on each iteration. |
120 | csd_fails: bool, |
121 | |
122 | /// Whether we should decorate the frame. |
123 | decorate: bool, |
124 | |
125 | /// Min size. |
126 | min_inner_size: LogicalSize<u32>, |
127 | max_inner_size: Option<LogicalSize<u32>>, |
128 | |
129 | /// The size of the window when no states were applied to it. The primary use for it |
130 | /// is to fallback to original window size, before it was maximized, if the compositor |
131 | /// sends `None` for the new size in the configure. |
132 | stateless_size: LogicalSize<u32>, |
133 | |
134 | /// Initial window size provided by the user. Removed on the first |
135 | /// configure. |
136 | initial_size: Option<Size>, |
137 | |
138 | /// The state of the frame callback. |
139 | frame_callback_state: FrameCallbackState, |
140 | |
141 | viewport: Option<WpViewport>, |
142 | fractional_scale: Option<WpFractionalScaleV1>, |
143 | blur: Option<OrgKdeKwinBlur>, |
144 | blur_manager: Option<KWinBlurManager>, |
145 | |
146 | /// Whether the client side decorations have pending move operations. |
147 | /// |
148 | /// The value is the serial of the event triggered moved. |
149 | has_pending_move: Option<u32>, |
150 | |
151 | /// The underlying SCTK window. |
152 | pub window: Window, |
153 | } |
154 | |
155 | impl WindowState { |
156 | /// Create new window state. |
157 | pub fn new( |
158 | connection: Connection, |
159 | queue_handle: &QueueHandle<WinitState>, |
160 | winit_state: &WinitState, |
161 | initial_size: Size, |
162 | window: Window, |
163 | theme: Option<Theme>, |
164 | ) -> Self { |
165 | let compositor = winit_state.compositor_state.clone(); |
166 | let pointer_constraints = winit_state.pointer_constraints.clone(); |
167 | let viewport = winit_state |
168 | .viewporter_state |
169 | .as_ref() |
170 | .map(|state| state.get_viewport(window.wl_surface(), queue_handle)); |
171 | let fractional_scale = winit_state |
172 | .fractional_scaling_manager |
173 | .as_ref() |
174 | .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); |
175 | |
176 | Self { |
177 | blur: None, |
178 | blur_manager: winit_state.kwin_blur_manager.clone(), |
179 | compositor, |
180 | connection, |
181 | csd_fails: false, |
182 | cursor_grab_mode: GrabState::new(), |
183 | cursor_icon: CursorIcon::Default, |
184 | cursor_visible: true, |
185 | decorate: true, |
186 | fractional_scale, |
187 | frame: None, |
188 | frame_callback_state: FrameCallbackState::None, |
189 | seat_focus: Default::default(), |
190 | has_pending_move: None, |
191 | ime_allowed: false, |
192 | ime_purpose: ImePurpose::Normal, |
193 | last_configure: None, |
194 | max_inner_size: None, |
195 | min_inner_size: MIN_WINDOW_SIZE, |
196 | pointer_constraints, |
197 | pointers: Default::default(), |
198 | queue_handle: queue_handle.clone(), |
199 | resizable: true, |
200 | scale_factor: 1., |
201 | shm: winit_state.shm.wl_shm().clone(), |
202 | size: initial_size.to_logical(1.), |
203 | stateless_size: initial_size.to_logical(1.), |
204 | initial_size: Some(initial_size), |
205 | text_inputs: Vec::new(), |
206 | theme, |
207 | title: String::default(), |
208 | transparent: false, |
209 | viewport, |
210 | window, |
211 | } |
212 | } |
213 | |
214 | /// Apply closure on the given pointer. |
215 | fn apply_on_poiner<F: Fn(&ThemedPointer<WinitPointerData>, &WinitPointerData)>( |
216 | &self, |
217 | callback: F, |
218 | ) { |
219 | self.pointers |
220 | .iter() |
221 | .filter_map(Weak::upgrade) |
222 | .for_each(|pointer| { |
223 | let data = pointer.pointer().winit_data(); |
224 | callback(pointer.as_ref(), data); |
225 | }) |
226 | } |
227 | |
228 | /// Get the current state of the frame callback. |
229 | pub fn frame_callback_state(&self) -> FrameCallbackState { |
230 | self.frame_callback_state |
231 | } |
232 | |
233 | /// The frame callback was received, but not yet sent to the user. |
234 | pub fn frame_callback_received(&mut self) { |
235 | self.frame_callback_state = FrameCallbackState::Received; |
236 | } |
237 | |
238 | /// Reset the frame callbacks state. |
239 | pub fn frame_callback_reset(&mut self) { |
240 | self.frame_callback_state = FrameCallbackState::None; |
241 | } |
242 | |
243 | /// Request a frame callback if we don't have one for this window in flight. |
244 | pub fn request_frame_callback(&mut self) { |
245 | let surface = self.window.wl_surface(); |
246 | match self.frame_callback_state { |
247 | FrameCallbackState::None | FrameCallbackState::Received => { |
248 | self.frame_callback_state = FrameCallbackState::Requested; |
249 | surface.frame(&self.queue_handle, surface.clone()); |
250 | } |
251 | FrameCallbackState::Requested => (), |
252 | } |
253 | } |
254 | |
255 | pub fn configure( |
256 | &mut self, |
257 | configure: WindowConfigure, |
258 | shm: &Shm, |
259 | subcompositor: &Option<Arc<SubcompositorState>>, |
260 | ) -> bool { |
261 | // NOTE: when using fractional scaling or wl_compositor@v6 the scaling |
262 | // should be delivered before the first configure, thus apply it to |
263 | // properly scale the physical sizes provided by the users. |
264 | if let Some(initial_size) = self.initial_size.take() { |
265 | self.size = initial_size.to_logical(self.scale_factor()); |
266 | self.stateless_size = self.size; |
267 | } |
268 | |
269 | if let Some(subcompositor) = subcompositor.as_ref().filter(|_| { |
270 | configure.decoration_mode == DecorationMode::Client |
271 | && self.frame.is_none() |
272 | && !self.csd_fails |
273 | }) { |
274 | match WinitFrame::new( |
275 | &self.window, |
276 | shm, |
277 | #[cfg (feature = "sctk-adwaita" )] |
278 | self.compositor.clone(), |
279 | subcompositor.clone(), |
280 | self.queue_handle.clone(), |
281 | #[cfg (feature = "sctk-adwaita" )] |
282 | into_sctk_adwaita_config(self.theme), |
283 | ) { |
284 | Ok(mut frame) => { |
285 | frame.set_title(&self.title); |
286 | frame.set_scaling_factor(self.scale_factor); |
287 | // Hide the frame if we were asked to not decorate. |
288 | frame.set_hidden(!self.decorate); |
289 | self.frame = Some(frame); |
290 | } |
291 | Err(err) => { |
292 | warn!("Failed to create client side decorations frame: {err}" ); |
293 | self.csd_fails = true; |
294 | } |
295 | } |
296 | } else if configure.decoration_mode == DecorationMode::Server { |
297 | // Drop the frame for server side decorations to save resources. |
298 | self.frame = None; |
299 | } |
300 | |
301 | let stateless = Self::is_stateless(&configure); |
302 | |
303 | let (mut new_size, constrain) = if let Some(frame) = self.frame.as_mut() { |
304 | // Configure the window states. |
305 | frame.update_state(configure.state); |
306 | |
307 | match configure.new_size { |
308 | (Some(width), Some(height)) => { |
309 | let (width, height) = frame.subtract_borders(width, height); |
310 | let width = width.map(|w| w.get()).unwrap_or(1); |
311 | let height = height.map(|h| h.get()).unwrap_or(1); |
312 | ((width, height).into(), false) |
313 | } |
314 | (_, _) if stateless => (self.stateless_size, true), |
315 | _ => (self.size, true), |
316 | } |
317 | } else { |
318 | match configure.new_size { |
319 | (Some(width), Some(height)) => ((width.get(), height.get()).into(), false), |
320 | _ if stateless => (self.stateless_size, true), |
321 | _ => (self.size, true), |
322 | } |
323 | }; |
324 | |
325 | // Apply configure bounds only when compositor let the user decide what size to pick. |
326 | if constrain { |
327 | let bounds = self.inner_size_bounds(&configure); |
328 | new_size.width = bounds |
329 | .0 |
330 | .map(|bound_w| new_size.width.min(bound_w.get())) |
331 | .unwrap_or(new_size.width); |
332 | new_size.height = bounds |
333 | .1 |
334 | .map(|bound_h| new_size.height.min(bound_h.get())) |
335 | .unwrap_or(new_size.height); |
336 | } |
337 | |
338 | let new_state = configure.state; |
339 | let old_state = self |
340 | .last_configure |
341 | .as_ref() |
342 | .map(|configure| configure.state); |
343 | |
344 | let state_change_requires_resize = old_state |
345 | .map(|old_state| { |
346 | !old_state |
347 | .symmetric_difference(new_state) |
348 | .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) |
349 | .is_empty() |
350 | }) |
351 | // NOTE: `None` is present for the initial configure, thus we must always resize. |
352 | .unwrap_or(true); |
353 | |
354 | // NOTE: Set the configure before doing a resize, since we query it during it. |
355 | self.last_configure = Some(configure); |
356 | |
357 | if state_change_requires_resize || new_size != self.inner_size() { |
358 | self.resize(new_size); |
359 | true |
360 | } else { |
361 | false |
362 | } |
363 | } |
364 | |
365 | /// Compute the bounds for the inner size of the surface. |
366 | fn inner_size_bounds( |
367 | &self, |
368 | configure: &WindowConfigure, |
369 | ) -> (Option<NonZeroU32>, Option<NonZeroU32>) { |
370 | let configure_bounds = match configure.suggested_bounds { |
371 | Some((width, height)) => (NonZeroU32::new(width), NonZeroU32::new(height)), |
372 | None => (None, None), |
373 | }; |
374 | |
375 | if let Some(frame) = self.frame.as_ref() { |
376 | let (width, height) = frame.subtract_borders( |
377 | configure_bounds.0.unwrap_or(NonZeroU32::new(1).unwrap()), |
378 | configure_bounds.1.unwrap_or(NonZeroU32::new(1).unwrap()), |
379 | ); |
380 | ( |
381 | configure_bounds.0.and(width), |
382 | configure_bounds.1.and(height), |
383 | ) |
384 | } else { |
385 | configure_bounds |
386 | } |
387 | } |
388 | |
389 | #[inline ] |
390 | fn is_stateless(configure: &WindowConfigure) -> bool { |
391 | !(configure.is_maximized() || configure.is_fullscreen() || configure.is_tiled()) |
392 | } |
393 | |
394 | /// Start interacting drag resize. |
395 | pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { |
396 | let xdg_toplevel = self.window.xdg_toplevel(); |
397 | |
398 | // TODO(kchibisov) handle touch serials. |
399 | self.apply_on_poiner(|_, data| { |
400 | let serial = data.latest_button_serial(); |
401 | let seat = data.seat(); |
402 | xdg_toplevel.resize(seat, serial, direction.into()); |
403 | }); |
404 | |
405 | Ok(()) |
406 | } |
407 | |
408 | /// Start the window drag. |
409 | pub fn drag_window(&self) -> Result<(), ExternalError> { |
410 | let xdg_toplevel = self.window.xdg_toplevel(); |
411 | // TODO(kchibisov) handle touch serials. |
412 | self.apply_on_poiner(|_, data| { |
413 | let serial = data.latest_button_serial(); |
414 | let seat = data.seat(); |
415 | xdg_toplevel._move(seat, serial); |
416 | }); |
417 | |
418 | Ok(()) |
419 | } |
420 | |
421 | /// Tells whether the window should be closed. |
422 | #[allow (clippy::too_many_arguments)] |
423 | pub fn frame_click( |
424 | &mut self, |
425 | click: FrameClick, |
426 | pressed: bool, |
427 | seat: &WlSeat, |
428 | serial: u32, |
429 | timestamp: Duration, |
430 | window_id: WindowId, |
431 | updates: &mut Vec<WindowCompositorUpdate>, |
432 | ) -> Option<bool> { |
433 | match self.frame.as_mut()?.on_click(timestamp, click, pressed)? { |
434 | FrameAction::Minimize => self.window.set_minimized(), |
435 | FrameAction::Maximize => self.window.set_maximized(), |
436 | FrameAction::UnMaximize => self.window.unset_maximized(), |
437 | FrameAction::Close => WinitState::queue_close(updates, window_id), |
438 | FrameAction::Move => self.has_pending_move = Some(serial), |
439 | FrameAction::Resize(edge) => { |
440 | let edge = match edge { |
441 | ResizeEdge::None => XdgResizeEdge::None, |
442 | ResizeEdge::Top => XdgResizeEdge::Top, |
443 | ResizeEdge::Bottom => XdgResizeEdge::Bottom, |
444 | ResizeEdge::Left => XdgResizeEdge::Left, |
445 | ResizeEdge::TopLeft => XdgResizeEdge::TopLeft, |
446 | ResizeEdge::BottomLeft => XdgResizeEdge::BottomLeft, |
447 | ResizeEdge::Right => XdgResizeEdge::Right, |
448 | ResizeEdge::TopRight => XdgResizeEdge::TopRight, |
449 | ResizeEdge::BottomRight => XdgResizeEdge::BottomRight, |
450 | _ => return None, |
451 | }; |
452 | self.window.resize(seat, serial, edge); |
453 | } |
454 | FrameAction::ShowMenu(x, y) => self.window.show_window_menu(seat, serial, (x, y)), |
455 | _ => (), |
456 | }; |
457 | |
458 | Some(false) |
459 | } |
460 | |
461 | pub fn frame_point_left(&mut self) { |
462 | if let Some(frame) = self.frame.as_mut() { |
463 | frame.click_point_left(); |
464 | } |
465 | } |
466 | |
467 | // Move the point over decorations. |
468 | pub fn frame_point_moved( |
469 | &mut self, |
470 | seat: &WlSeat, |
471 | surface: &WlSurface, |
472 | timestamp: Duration, |
473 | x: f64, |
474 | y: f64, |
475 | ) -> Option<CursorIcon> { |
476 | // Take the serial if we had any, so it doesn't stick around. |
477 | let serial = self.has_pending_move.take(); |
478 | |
479 | if let Some(frame) = self.frame.as_mut() { |
480 | let cursor = frame.click_point_moved(timestamp, &surface.id(), x, y); |
481 | // If we have a cursor change, that means that cursor is over the decorations, |
482 | // so try to apply move. |
483 | if let Some(serial) = cursor.is_some().then_some(serial).flatten() { |
484 | self.window.move_(seat, serial); |
485 | None |
486 | } else { |
487 | cursor |
488 | } |
489 | } else { |
490 | None |
491 | } |
492 | } |
493 | |
494 | /// Get the stored resizable state. |
495 | #[inline ] |
496 | pub fn resizable(&self) -> bool { |
497 | self.resizable |
498 | } |
499 | |
500 | /// Set the resizable state on the window. |
501 | /// |
502 | /// Returns `true` when the state was applied. |
503 | #[inline ] |
504 | pub fn set_resizable(&mut self, resizable: bool) -> bool { |
505 | if self.resizable == resizable { |
506 | return false; |
507 | } |
508 | |
509 | self.resizable = resizable; |
510 | if resizable { |
511 | // Restore min/max sizes of the window. |
512 | self.reload_min_max_hints(); |
513 | } else { |
514 | self.set_min_inner_size(Some(self.size)); |
515 | self.set_max_inner_size(Some(self.size)); |
516 | } |
517 | |
518 | // Reload the state on the frame as well. |
519 | if let Some(frame) = self.frame.as_mut() { |
520 | frame.set_resizable(resizable); |
521 | } |
522 | |
523 | true |
524 | } |
525 | |
526 | /// Whether the window is focused by any seat. |
527 | #[inline ] |
528 | pub fn has_focus(&self) -> bool { |
529 | !self.seat_focus.is_empty() |
530 | } |
531 | |
532 | /// Whether the IME is allowed. |
533 | #[inline ] |
534 | pub fn ime_allowed(&self) -> bool { |
535 | self.ime_allowed |
536 | } |
537 | |
538 | /// Get the size of the window. |
539 | #[inline ] |
540 | pub fn inner_size(&self) -> LogicalSize<u32> { |
541 | self.size |
542 | } |
543 | |
544 | /// Whether the window received initial configure event from the compositor. |
545 | #[inline ] |
546 | pub fn is_configured(&self) -> bool { |
547 | self.last_configure.is_some() |
548 | } |
549 | |
550 | #[inline ] |
551 | pub fn is_decorated(&mut self) -> bool { |
552 | let csd = self |
553 | .last_configure |
554 | .as_ref() |
555 | .map(|configure| configure.decoration_mode == DecorationMode::Client) |
556 | .unwrap_or(false); |
557 | if let Some(frame) = csd.then_some(self.frame.as_ref()).flatten() { |
558 | !frame.is_hidden() |
559 | } else { |
560 | // Server side decorations. |
561 | true |
562 | } |
563 | } |
564 | |
565 | /// Get the outer size of the window. |
566 | #[inline ] |
567 | pub fn outer_size(&self) -> LogicalSize<u32> { |
568 | self.frame |
569 | .as_ref() |
570 | .map(|frame| frame.add_borders(self.size.width, self.size.height).into()) |
571 | .unwrap_or(self.size) |
572 | } |
573 | |
574 | /// Register pointer on the top-level. |
575 | pub fn pointer_entered(&mut self, added: Weak<ThemedPointer<WinitPointerData>>) { |
576 | self.pointers.push(added); |
577 | self.reload_cursor_style(); |
578 | |
579 | let mode = self.cursor_grab_mode.user_grab_mode; |
580 | let _ = self.set_cursor_grab_inner(mode); |
581 | } |
582 | |
583 | /// Pointer has left the top-level. |
584 | pub fn pointer_left(&mut self, removed: Weak<ThemedPointer<WinitPointerData>>) { |
585 | let mut new_pointers = Vec::new(); |
586 | for pointer in self.pointers.drain(..) { |
587 | if let Some(pointer) = pointer.upgrade() { |
588 | if pointer.pointer() != removed.upgrade().unwrap().pointer() { |
589 | new_pointers.push(Arc::downgrade(&pointer)); |
590 | } |
591 | } |
592 | } |
593 | |
594 | self.pointers = new_pointers; |
595 | } |
596 | |
597 | /// Refresh the decorations frame if it's present returning whether the client should redraw. |
598 | pub fn refresh_frame(&mut self) -> bool { |
599 | if let Some(frame) = self.frame.as_mut() { |
600 | if !frame.is_hidden() && frame.is_dirty() { |
601 | return frame.draw(); |
602 | } |
603 | } |
604 | |
605 | false |
606 | } |
607 | |
608 | /// Reload the cursor style on the given window. |
609 | pub fn reload_cursor_style(&mut self) { |
610 | if self.cursor_visible { |
611 | self.set_cursor(self.cursor_icon); |
612 | } else { |
613 | self.set_cursor_visible(self.cursor_visible); |
614 | } |
615 | } |
616 | |
617 | /// Reissue the transparency hint to the compositor. |
618 | pub fn reload_transparency_hint(&self) { |
619 | let surface = self.window.wl_surface(); |
620 | |
621 | if self.transparent { |
622 | surface.set_opaque_region(None); |
623 | } else if let Ok(region) = Region::new(&*self.compositor) { |
624 | region.add(0, 0, i32::MAX, i32::MAX); |
625 | surface.set_opaque_region(Some(region.wl_region())); |
626 | } else { |
627 | warn!("Failed to mark window opaque." ); |
628 | } |
629 | } |
630 | |
631 | /// Try to resize the window when the user can do so. |
632 | pub fn request_inner_size(&mut self, inner_size: Size) -> PhysicalSize<u32> { |
633 | if self |
634 | .last_configure |
635 | .as_ref() |
636 | .map(Self::is_stateless) |
637 | .unwrap_or(true) |
638 | { |
639 | self.resize(inner_size.to_logical(self.scale_factor())) |
640 | } |
641 | |
642 | logical_to_physical_rounded(self.inner_size(), self.scale_factor()) |
643 | } |
644 | |
645 | /// Resize the window to the new inner size. |
646 | fn resize(&mut self, inner_size: LogicalSize<u32>) { |
647 | self.size = inner_size; |
648 | |
649 | // Update the stateless size. |
650 | if Some(true) == self.last_configure.as_ref().map(Self::is_stateless) { |
651 | self.stateless_size = inner_size; |
652 | } |
653 | |
654 | // Update the inner frame. |
655 | let ((x, y), outer_size) = if let Some(frame) = self.frame.as_mut() { |
656 | // Resize only visible frame. |
657 | if !frame.is_hidden() { |
658 | frame.resize( |
659 | NonZeroU32::new(self.size.width).unwrap(), |
660 | NonZeroU32::new(self.size.height).unwrap(), |
661 | ); |
662 | } |
663 | |
664 | ( |
665 | frame.location(), |
666 | frame.add_borders(self.size.width, self.size.height).into(), |
667 | ) |
668 | } else { |
669 | ((0, 0), self.size) |
670 | }; |
671 | |
672 | // Reload the hint. |
673 | self.reload_transparency_hint(); |
674 | |
675 | // Set the window geometry. |
676 | self.window.xdg_surface().set_window_geometry( |
677 | x, |
678 | y, |
679 | outer_size.width as i32, |
680 | outer_size.height as i32, |
681 | ); |
682 | |
683 | // Update the target viewport, this is used if and only if fractional scaling is in use. |
684 | if let Some(viewport) = self.viewport.as_ref() { |
685 | // Set inner size without the borders. |
686 | viewport.set_destination(self.size.width as _, self.size.height as _); |
687 | } |
688 | } |
689 | |
690 | /// Get the scale factor of the window. |
691 | #[inline ] |
692 | pub fn scale_factor(&self) -> f64 { |
693 | self.scale_factor |
694 | } |
695 | |
696 | /// Set the cursor icon. |
697 | /// |
698 | /// Providing `None` will hide the cursor. |
699 | pub fn set_cursor(&mut self, cursor_icon: CursorIcon) { |
700 | self.cursor_icon = cursor_icon; |
701 | |
702 | if !self.cursor_visible { |
703 | return; |
704 | } |
705 | |
706 | self.apply_on_poiner(|pointer, _| { |
707 | if pointer.set_cursor(&self.connection, cursor_icon).is_err() { |
708 | warn!("Failed to set cursor to {:?}" , cursor_icon); |
709 | } |
710 | }) |
711 | } |
712 | |
713 | /// Set maximum inner window size. |
714 | pub fn set_min_inner_size(&mut self, size: Option<LogicalSize<u32>>) { |
715 | // Ensure that the window has the right minimum size. |
716 | let mut size = size.unwrap_or(MIN_WINDOW_SIZE); |
717 | size.width = size.width.max(MIN_WINDOW_SIZE.width); |
718 | size.height = size.height.max(MIN_WINDOW_SIZE.height); |
719 | |
720 | // Add the borders. |
721 | let size = self |
722 | .frame |
723 | .as_ref() |
724 | .map(|frame| frame.add_borders(size.width, size.height).into()) |
725 | .unwrap_or(size); |
726 | |
727 | self.min_inner_size = size; |
728 | self.window.set_min_size(Some(size.into())); |
729 | } |
730 | |
731 | /// Set maximum inner window size. |
732 | pub fn set_max_inner_size(&mut self, size: Option<LogicalSize<u32>>) { |
733 | let size = size.map(|size| { |
734 | self.frame |
735 | .as_ref() |
736 | .map(|frame| frame.add_borders(size.width, size.height).into()) |
737 | .unwrap_or(size) |
738 | }); |
739 | |
740 | self.max_inner_size = size; |
741 | self.window.set_max_size(size.map(Into::into)); |
742 | } |
743 | |
744 | /// Set the CSD theme. |
745 | pub fn set_theme(&mut self, theme: Option<Theme>) { |
746 | self.theme = theme; |
747 | #[cfg (feature = "sctk-adwaita" )] |
748 | if let Some(frame) = self.frame.as_mut() { |
749 | frame.set_config(into_sctk_adwaita_config(theme)) |
750 | } |
751 | } |
752 | |
753 | /// The current theme for CSD decorations. |
754 | #[inline ] |
755 | pub fn theme(&self) -> Option<Theme> { |
756 | self.theme |
757 | } |
758 | |
759 | /// Set the cursor grabbing state on the top-level. |
760 | pub fn set_cursor_grab(&mut self, mode: CursorGrabMode) -> Result<(), ExternalError> { |
761 | if self.cursor_grab_mode.user_grab_mode == mode { |
762 | return Ok(()); |
763 | } |
764 | |
765 | self.set_cursor_grab_inner(mode)?; |
766 | // Update user grab on success. |
767 | self.cursor_grab_mode.user_grab_mode = mode; |
768 | Ok(()) |
769 | } |
770 | |
771 | /// Reload the hints for minimum and maximum sizes. |
772 | pub fn reload_min_max_hints(&mut self) { |
773 | self.set_min_inner_size(Some(self.min_inner_size)); |
774 | self.set_max_inner_size(self.max_inner_size); |
775 | } |
776 | |
777 | /// Set the grabbing state on the surface. |
778 | fn set_cursor_grab_inner(&mut self, mode: CursorGrabMode) -> Result<(), ExternalError> { |
779 | let pointer_constraints = match self.pointer_constraints.as_ref() { |
780 | Some(pointer_constraints) => pointer_constraints, |
781 | None if mode == CursorGrabMode::None => return Ok(()), |
782 | None => return Err(ExternalError::NotSupported(NotSupportedError::new())), |
783 | }; |
784 | |
785 | // Replace the current mode. |
786 | let old_mode = std::mem::replace(&mut self.cursor_grab_mode.current_grab_mode, mode); |
787 | |
788 | match old_mode { |
789 | CursorGrabMode::None => (), |
790 | CursorGrabMode::Confined => self.apply_on_poiner(|_, data| { |
791 | data.unconfine_pointer(); |
792 | }), |
793 | CursorGrabMode::Locked => { |
794 | self.apply_on_poiner(|_, data| data.unlock_pointer()); |
795 | } |
796 | } |
797 | |
798 | let surface = self.window.wl_surface(); |
799 | match mode { |
800 | CursorGrabMode::Locked => self.apply_on_poiner(|pointer, data| { |
801 | let pointer = pointer.pointer(); |
802 | data.lock_pointer(pointer_constraints, surface, pointer, &self.queue_handle) |
803 | }), |
804 | CursorGrabMode::Confined => self.apply_on_poiner(|pointer, data| { |
805 | let pointer = pointer.pointer(); |
806 | data.confine_pointer(pointer_constraints, surface, pointer, &self.queue_handle) |
807 | }), |
808 | CursorGrabMode::None => { |
809 | // Current lock/confine was already removed. |
810 | } |
811 | } |
812 | |
813 | Ok(()) |
814 | } |
815 | |
816 | pub fn show_window_menu(&self, position: LogicalPosition<u32>) { |
817 | // TODO(kchibisov) handle touch serials. |
818 | self.apply_on_poiner(|_, data| { |
819 | let serial = data.latest_button_serial(); |
820 | let seat = data.seat(); |
821 | self.window.show_window_menu(seat, serial, position.into()); |
822 | }); |
823 | } |
824 | |
825 | /// Set the position of the cursor. |
826 | pub fn set_cursor_position(&self, position: LogicalPosition<f64>) -> Result<(), ExternalError> { |
827 | if self.pointer_constraints.is_none() { |
828 | return Err(ExternalError::NotSupported(NotSupportedError::new())); |
829 | } |
830 | |
831 | // Positon can be set only for locked cursor. |
832 | if self.cursor_grab_mode.current_grab_mode != CursorGrabMode::Locked { |
833 | return Err(ExternalError::Os(os_error!( |
834 | crate::platform_impl::OsError::Misc( |
835 | "cursor position can be set only for locked cursor." |
836 | ) |
837 | ))); |
838 | } |
839 | |
840 | self.apply_on_poiner(|_, data| { |
841 | data.set_locked_cursor_position(position.x, position.y); |
842 | }); |
843 | |
844 | Ok(()) |
845 | } |
846 | |
847 | /// Set the visibility state of the cursor. |
848 | pub fn set_cursor_visible(&mut self, cursor_visible: bool) { |
849 | self.cursor_visible = cursor_visible; |
850 | |
851 | if self.cursor_visible { |
852 | self.set_cursor(self.cursor_icon); |
853 | } else { |
854 | for pointer in self.pointers.iter().filter_map(|pointer| pointer.upgrade()) { |
855 | let latest_enter_serial = pointer.pointer().winit_data().latest_enter_serial(); |
856 | |
857 | pointer |
858 | .pointer() |
859 | .set_cursor(latest_enter_serial, None, 0, 0); |
860 | } |
861 | } |
862 | } |
863 | |
864 | /// Whether show or hide client side decorations. |
865 | #[inline ] |
866 | pub fn set_decorate(&mut self, decorate: bool) { |
867 | if decorate == self.decorate { |
868 | return; |
869 | } |
870 | |
871 | self.decorate = decorate; |
872 | |
873 | match self |
874 | .last_configure |
875 | .as_ref() |
876 | .map(|configure| configure.decoration_mode) |
877 | { |
878 | Some(DecorationMode::Server) if !self.decorate => { |
879 | // To disable decorations we should request client and hide the frame. |
880 | self.window |
881 | .request_decoration_mode(Some(DecorationMode::Client)) |
882 | } |
883 | _ if self.decorate => self |
884 | .window |
885 | .request_decoration_mode(Some(DecorationMode::Server)), |
886 | _ => (), |
887 | } |
888 | |
889 | if let Some(frame) = self.frame.as_mut() { |
890 | frame.set_hidden(!decorate); |
891 | // Force the resize. |
892 | self.resize(self.size); |
893 | } |
894 | } |
895 | |
896 | /// Add seat focus for the window. |
897 | #[inline ] |
898 | pub fn add_seat_focus(&mut self, seat: ObjectId) { |
899 | self.seat_focus.insert(seat); |
900 | } |
901 | |
902 | /// Remove seat focus from the window. |
903 | #[inline ] |
904 | pub fn remove_seat_focus(&mut self, seat: &ObjectId) { |
905 | self.seat_focus.remove(seat); |
906 | } |
907 | |
908 | /// Returns `true` if the requested state was applied. |
909 | pub fn set_ime_allowed(&mut self, allowed: bool) -> bool { |
910 | self.ime_allowed = allowed; |
911 | |
912 | let mut applied = false; |
913 | for text_input in &self.text_inputs { |
914 | applied = true; |
915 | if allowed { |
916 | text_input.enable(); |
917 | text_input.set_content_type_by_purpose(self.ime_purpose); |
918 | } else { |
919 | text_input.disable(); |
920 | } |
921 | text_input.commit(); |
922 | } |
923 | |
924 | applied |
925 | } |
926 | |
927 | /// Set the IME position. |
928 | pub fn set_ime_cursor_area(&self, position: LogicalPosition<u32>, size: LogicalSize<u32>) { |
929 | // FIXME: This won't fly unless user will have a way to request IME window per seat, since |
930 | // the ime windows will be overlapping, but winit doesn't expose API to specify for |
931 | // which seat we're setting IME position. |
932 | let (x, y) = (position.x as i32, position.y as i32); |
933 | let (width, height) = (size.width as i32, size.height as i32); |
934 | for text_input in self.text_inputs.iter() { |
935 | text_input.set_cursor_rectangle(x, y, width, height); |
936 | text_input.commit(); |
937 | } |
938 | } |
939 | |
940 | /// Set the IME purpose. |
941 | pub fn set_ime_purpose(&mut self, purpose: ImePurpose) { |
942 | self.ime_purpose = purpose; |
943 | |
944 | for text_input in &self.text_inputs { |
945 | text_input.set_content_type_by_purpose(purpose); |
946 | text_input.commit(); |
947 | } |
948 | } |
949 | |
950 | /// Get the IME purpose. |
951 | pub fn ime_purpose(&self) -> ImePurpose { |
952 | self.ime_purpose |
953 | } |
954 | |
955 | /// Set the scale factor for the given window. |
956 | #[inline ] |
957 | pub fn set_scale_factor(&mut self, scale_factor: f64) { |
958 | self.scale_factor = scale_factor; |
959 | |
960 | // NOTE: When fractional scaling is not used update the buffer scale. |
961 | if self.fractional_scale.is_none() { |
962 | let _ = self.window.set_buffer_scale(self.scale_factor as _); |
963 | } |
964 | |
965 | if let Some(frame) = self.frame.as_mut() { |
966 | frame.set_scaling_factor(scale_factor); |
967 | } |
968 | } |
969 | |
970 | /// Make window background blurred |
971 | #[inline ] |
972 | pub fn set_blur(&mut self, blurred: bool) { |
973 | if blurred && self.blur.is_none() { |
974 | if let Some(blur_manager) = self.blur_manager.as_ref() { |
975 | let blur = blur_manager.blur(self.window.wl_surface(), &self.queue_handle); |
976 | blur.commit(); |
977 | self.blur = Some(blur); |
978 | } else { |
979 | info!("Blur manager unavailable, unable to change blur" ) |
980 | } |
981 | } else if !blurred && self.blur.is_some() { |
982 | self.blur_manager |
983 | .as_ref() |
984 | .unwrap() |
985 | .unset(self.window.wl_surface()); |
986 | self.blur.take().unwrap().release(); |
987 | } |
988 | } |
989 | |
990 | /// Set the window title to a new value. |
991 | /// |
992 | /// This will autmatically truncate the title to something meaningfull. |
993 | pub fn set_title(&mut self, mut title: String) { |
994 | // Truncate the title to at most 1024 bytes, so that it does not blow up the protocol |
995 | // messages |
996 | if title.len() > 1024 { |
997 | let mut new_len = 1024; |
998 | while !title.is_char_boundary(new_len) { |
999 | new_len -= 1; |
1000 | } |
1001 | title.truncate(new_len); |
1002 | } |
1003 | |
1004 | // Update the CSD title. |
1005 | if let Some(frame) = self.frame.as_mut() { |
1006 | frame.set_title(&title); |
1007 | } |
1008 | |
1009 | self.window.set_title(&title); |
1010 | self.title = title; |
1011 | } |
1012 | |
1013 | /// Mark the window as transparent. |
1014 | #[inline ] |
1015 | pub fn set_transparent(&mut self, transparent: bool) { |
1016 | self.transparent = transparent; |
1017 | self.reload_transparency_hint(); |
1018 | } |
1019 | |
1020 | /// Register text input on the top-level. |
1021 | #[inline ] |
1022 | pub fn text_input_entered(&mut self, text_input: &ZwpTextInputV3) { |
1023 | if !self.text_inputs.iter().any(|t| t == text_input) { |
1024 | self.text_inputs.push(text_input.clone()); |
1025 | } |
1026 | } |
1027 | |
1028 | /// The text input left the top-level. |
1029 | #[inline ] |
1030 | pub fn text_input_left(&mut self, text_input: &ZwpTextInputV3) { |
1031 | if let Some(position) = self.text_inputs.iter().position(|t| t == text_input) { |
1032 | self.text_inputs.remove(position); |
1033 | } |
1034 | } |
1035 | |
1036 | /// Get the cached title. |
1037 | #[inline ] |
1038 | pub fn title(&self) -> &str { |
1039 | &self.title |
1040 | } |
1041 | } |
1042 | |
1043 | impl Drop for WindowState { |
1044 | fn drop(&mut self) { |
1045 | if let Some(blur) = self.blur.take() { |
1046 | blur.release(); |
1047 | } |
1048 | |
1049 | if let Some(fs) = self.fractional_scale.take() { |
1050 | fs.destroy(); |
1051 | } |
1052 | |
1053 | if let Some(viewport) = self.viewport.take() { |
1054 | viewport.destroy(); |
1055 | } |
1056 | |
1057 | // NOTE: the wl_surface used by the window is being cleaned up when |
1058 | // dropping SCTK `Window`. |
1059 | } |
1060 | } |
1061 | |
1062 | /// The state of the cursor grabs. |
1063 | #[derive (Clone, Copy)] |
1064 | struct GrabState { |
1065 | /// The grab mode requested by the user. |
1066 | user_grab_mode: CursorGrabMode, |
1067 | |
1068 | /// The current grab mode. |
1069 | current_grab_mode: CursorGrabMode, |
1070 | } |
1071 | |
1072 | impl GrabState { |
1073 | fn new() -> Self { |
1074 | Self { |
1075 | user_grab_mode: CursorGrabMode::None, |
1076 | current_grab_mode: CursorGrabMode::None, |
1077 | } |
1078 | } |
1079 | } |
1080 | |
1081 | /// The state of the frame callback. |
1082 | #[derive (Default, Debug, Clone, Copy, PartialEq, Eq)] |
1083 | pub enum FrameCallbackState { |
1084 | /// No frame callback was requsted. |
1085 | #[default] |
1086 | None, |
1087 | /// The frame callback was requested, but not yet arrived, the redraw events are throttled. |
1088 | Requested, |
1089 | /// The callback was marked as done, and user could receive redraw requested |
1090 | Received, |
1091 | } |
1092 | |
1093 | impl From<ResizeDirection> for XdgResizeEdge { |
1094 | fn from(value: ResizeDirection) -> Self { |
1095 | match value { |
1096 | ResizeDirection::North => XdgResizeEdge::Top, |
1097 | ResizeDirection::West => XdgResizeEdge::Left, |
1098 | ResizeDirection::NorthWest => XdgResizeEdge::TopLeft, |
1099 | ResizeDirection::NorthEast => XdgResizeEdge::TopRight, |
1100 | ResizeDirection::East => XdgResizeEdge::Right, |
1101 | ResizeDirection::SouthWest => XdgResizeEdge::BottomLeft, |
1102 | ResizeDirection::SouthEast => XdgResizeEdge::BottomRight, |
1103 | ResizeDirection::South => XdgResizeEdge::Bottom, |
1104 | } |
1105 | } |
1106 | } |
1107 | |
1108 | // NOTE: Rust doesn't allow `From<Option<Theme>>`. |
1109 | #[cfg (feature = "sctk-adwaita" )] |
1110 | fn into_sctk_adwaita_config(theme: Option<Theme>) -> sctk_adwaita::FrameConfig { |
1111 | match theme { |
1112 | Some(Theme::Light) => sctk_adwaita::FrameConfig::light(), |
1113 | Some(Theme::Dark) => sctk_adwaita::FrameConfig::dark(), |
1114 | None => sctk_adwaita::FrameConfig::auto(), |
1115 | } |
1116 | } |
1117 | |