1//! The state of the window, which is shared with the event-loop.
2
3use std::num::NonZeroU32;
4use std::sync::{Arc, Weak};
5use std::time::Duration;
6
7use ahash::HashSet;
8use log::{info, warn};
9
10use sctk::reexports::client::backend::ObjectId;
11use sctk::reexports::client::protocol::wl_seat::WlSeat;
12use sctk::reexports::client::protocol::wl_shm::WlShm;
13use sctk::reexports::client::protocol::wl_surface::WlSurface;
14use sctk::reexports::client::{Connection, Proxy, QueueHandle};
15use sctk::reexports::csd_frame::{
16 DecorationsFrame, FrameAction, FrameClick, ResizeEdge, WindowState as XdgWindowState,
17};
18use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1;
19use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3;
20use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport;
21use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge;
22
23use sctk::compositor::{CompositorState, Region};
24use sctk::seat::pointer::ThemedPointer;
25use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure};
26use sctk::shell::xdg::XdgSurface;
27use sctk::shell::WaylandSurface;
28use sctk::shm::Shm;
29use sctk::subcompositor::SubcompositorState;
30use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
31
32use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size};
33use crate::error::{ExternalError, NotSupportedError};
34use crate::platform_impl::wayland::logical_to_physical_rounded;
35use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager;
36use crate::platform_impl::WindowId;
37use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme};
38
39use crate::platform_impl::wayland::seat::{
40 PointerConstraintsState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext,
41};
42use crate::platform_impl::wayland::state::{WindowCompositorUpdate, WinitState};
43
44#[cfg(feature = "sctk-adwaita")]
45pub type WinitFrame = sctk_adwaita::AdwaitaFrame<WinitState>;
46#[cfg(not(feature = "sctk-adwaita"))]
47pub type WinitFrame = sctk::shell::xdg::fallback_frame::FallbackFrame<WinitState>;
48
49// Minimum window inner size.
50const 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`].
53pub 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
155impl 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
1043impl 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)]
1064struct 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
1072impl 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)]
1083pub 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
1093impl 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")]
1110fn 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