Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/xdg_toplevel.rs (59.3K)
1 // SPDX-FileCopyrightText: © 2020 The River Developers
2 // SPDX-License-Identifier: GPL-3.0-only
3
4 use crate::ffi;
5 use crate::server::{Server, WlListener, WlList, wl_listener_remove, wl_signal_add};
6 use crate::window::Window;
7
8 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
9 pub enum ConfigureState {
10 Idle,
11 Inflight(u32),
12 Acked,
13 Committed,
14 TimedOut(u32),
15 TimedOutAcked,
16 }
17
18 pub struct XdgToplevel {
19 pub window: *mut Window,
20 pub wlr_toplevel: *mut ffi::wlr_xdg_toplevel,
21 pub decoration: *mut XdgDecoration,
22 pub geometry: ffi::wlr_box,
23 /// Surface extent as of the last commit — with `geometry`, the mapping
24 /// change detector for the deferred pointer refresh (see `handle_commit`).
25 pub last_surface_size: (i32, i32),
26 pub configure_state: ConfigureState,
27 /// Has the client ever answered a configure? Until it has,
28 /// `geometry` is the size the CLIENT asked for, not a response to one
29 /// — see `Window::render_start`, which refuses to adopt a wish over a
30 /// restored size.
31 pub acked_once: bool,
32
33 pub destroy: ffi::wl_listener,
34 pub ack_configure: ffi::wl_listener,
35 pub map: ffi::wl_listener,
36 pub unmap: ffi::wl_listener,
37 pub commit: ffi::wl_listener,
38 pub new_popup: ffi::wl_listener,
39 pub request_show_window_menu: ffi::wl_listener,
40 pub request_fullscreen: ffi::wl_listener,
41 pub request_maximize: ffi::wl_listener,
42 pub request_minimize: ffi::wl_listener,
43 pub request_move: ffi::wl_listener,
44 pub request_resize: ffi::wl_listener,
45 pub set_parent: ffi::wl_listener,
46 pub set_title: ffi::wl_listener,
47 pub set_app_id: ffi::wl_listener,
48 }
49
50 pub struct XdgDecoration {
51 pub wlr_decoration: *mut ffi::wlr_xdg_toplevel_decoration_v1,
52 pub destroy: ffi::wl_listener,
53 pub request_mode: ffi::wl_listener,
54 }
55
56 impl XdgToplevel {
57 pub unsafe fn create(
58 wlr_toplevel: *mut ffi::wlr_xdg_toplevel,
59 server: *mut Server,
60 ) -> Result<(), &'static str> {
61 log::debug!("new xdg_toplevel");
62
63 let window = Window::create(crate::window::WindowImpl::Toplevel(std::ptr::null_mut()), server)?;
64
65 let toplevel = Box::new(XdgToplevel {
66 window,
67 wlr_toplevel,
68 decoration: std::ptr::null_mut(),
69 geometry: std::mem::zeroed(),
70 last_surface_size: (0, 0),
71 configure_state: ConfigureState::Idle,
72 acked_once: false,
73
74 destroy: std::mem::zeroed(),
75 ack_configure: std::mem::zeroed(),
76 map: std::mem::zeroed(),
77 unmap: std::mem::zeroed(),
78 commit: std::mem::zeroed(),
79 new_popup: std::mem::zeroed(),
80 request_show_window_menu: std::mem::zeroed(),
81 request_fullscreen: std::mem::zeroed(),
82 request_maximize: std::mem::zeroed(),
83 request_minimize: std::mem::zeroed(),
84 request_move: std::mem::zeroed(),
85 request_resize: std::mem::zeroed(),
86 set_parent: std::mem::zeroed(),
87 set_title: std::mem::zeroed(),
88 set_app_id: std::mem::zeroed(),
89 });
90
91 let raw = Box::into_raw(toplevel);
92 (*window).set_impl(crate::window::WindowImpl::Toplevel(raw));
93
94 let base = ffi::river_wlr_xdg_toplevel_get_base(wlr_toplevel);
95 let surface = ffi::river_wlr_xdg_surface_get_surface(base);
96
97 let unmap_listener = &mut (*raw).unmap as *mut ffi::wl_listener as *mut WlListener;
98 (*unmap_listener).notify = Some(handle_unmap);
99 wl_signal_add(ffi::river_wlr_surface_get_unmap_signal(surface), &mut (*raw).unmap);
100
101 let surfaces_tree = (*window).surfaces.tree;
102 let capture_tree = &mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree;
103
104 let scene_xdg = ffi::wlr_scene_xdg_surface_create(surfaces_tree, base);
105 if scene_xdg.is_null() {
106 let _ = Box::from_raw(raw);
107 return Err("wlr_scene_xdg_surface_create failed");
108 }
109 let capture_xdg = ffi::wlr_scene_xdg_surface_create(capture_tree, base);
110 if capture_xdg.is_null() {
111 // Already added unmap listener, but let's at least try to free raw
112 let _ = Box::from_raw(raw);
113 return Err("wlr_scene_xdg_surface_create for capture tree failed");
114 }
115
116 ffi::river_wlr_xdg_surface_set_data(base, raw as *mut _);
117 ffi::river_wlr_surface_set_data(surface, (*window).tree as *mut ffi::wlr_scene_node as *mut _);
118
119 let destroy_listener = &mut (*raw).destroy as *mut ffi::wl_listener as *mut WlListener;
120 (*destroy_listener).notify = Some(handle_destroy);
121 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_destroy_signal(wlr_toplevel), &mut (*raw).destroy);
122
123 let ack_listener = &mut (*raw).ack_configure as *mut ffi::wl_listener as *mut WlListener;
124 (*ack_listener).notify = Some(handle_ack_configure);
125 wl_signal_add(ffi::river_wlr_xdg_surface_get_ack_configure_signal(base), &mut (*raw).ack_configure);
126
127 let map_listener = &mut (*raw).map as *mut ffi::wl_listener as *mut WlListener;
128 (*map_listener).notify = Some(handle_map);
129 wl_signal_add(ffi::river_wlr_surface_get_map_signal(surface), &mut (*raw).map);
130
131 let commit_listener = &mut (*raw).commit as *mut ffi::wl_listener as *mut WlListener;
132 (*commit_listener).notify = Some(handle_commit);
133 wl_signal_add(ffi::river_wlr_surface_get_commit_signal(surface), &mut (*raw).commit);
134
135 let popup_listener = &mut (*raw).new_popup as *mut ffi::wl_listener as *mut WlListener;
136 (*popup_listener).notify = Some(handle_new_popup);
137 wl_signal_add(ffi::river_wlr_xdg_surface_get_new_popup_signal(base), &mut (*raw).new_popup);
138
139 let menu_listener = &mut (*raw).request_show_window_menu as *mut ffi::wl_listener as *mut WlListener;
140 (*menu_listener).notify = Some(handle_request_show_window_menu);
141 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_show_window_menu_signal(wlr_toplevel), &mut (*raw).request_show_window_menu);
142
143 let fs_listener = &mut (*raw).request_fullscreen as *mut ffi::wl_listener as *mut WlListener;
144 (*fs_listener).notify = Some(handle_request_fullscreen);
145 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_fullscreen_signal(wlr_toplevel), &mut (*raw).request_fullscreen);
146
147 let max_listener = &mut (*raw).request_maximize as *mut ffi::wl_listener as *mut WlListener;
148 (*max_listener).notify = Some(handle_request_maximize);
149 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_maximize_signal(wlr_toplevel), &mut (*raw).request_maximize);
150
151 let min_listener = &mut (*raw).request_minimize as *mut ffi::wl_listener as *mut WlListener;
152 (*min_listener).notify = Some(handle_request_minimize);
153 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_minimize_signal(wlr_toplevel), &mut (*raw).request_minimize);
154
155 let move_listener = &mut (*raw).request_move as *mut ffi::wl_listener as *mut WlListener;
156 (*move_listener).notify = Some(handle_request_move);
157 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_move_signal(wlr_toplevel), &mut (*raw).request_move);
158
159 let resize_listener = &mut (*raw).request_resize as *mut ffi::wl_listener as *mut WlListener;
160 (*resize_listener).notify = Some(handle_request_resize);
161 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_resize_signal(wlr_toplevel), &mut (*raw).request_resize);
162
163 let parent_listener = &mut (*raw).set_parent as *mut ffi::wl_listener as *mut WlListener;
164 (*parent_listener).notify = Some(handle_set_parent);
165 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_set_parent_signal(wlr_toplevel), &mut (*raw).set_parent);
166
167 let title_listener = &mut (*raw).set_title as *mut ffi::wl_listener as *mut WlListener;
168 (*title_listener).notify = Some(handle_set_title);
169 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_set_title_signal(wlr_toplevel), &mut (*raw).set_title);
170
171 let app_listener = &mut (*raw).set_app_id as *mut ffi::wl_listener as *mut WlListener;
172 (*app_listener).notify = Some(handle_set_app_id);
173 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_set_app_id_signal(wlr_toplevel), &mut (*raw).set_app_id);
174
175 Ok(())
176 }
177
178 pub unsafe fn destroy_popups(&self) {
179 let base = ffi::river_wlr_xdg_toplevel_get_base(self.wlr_toplevel);
180 let list_head = ffi::river_wlr_xdg_surface_get_popups(base) as *mut WlList;
181 let mut curr = (*list_head).next;
182 while curr != list_head {
183 let next = (*curr).next;
184 let popup = crate::container_of!(curr, ffi::wlr_xdg_popup, link);
185 ffi::wl_resource_destroy((*popup).resource);
186 curr = next;
187 }
188 }
189
190 pub unsafe fn configure(&mut self) -> bool {
191 match self.configure_state {
192 ConfigureState::Idle
193 | ConfigureState::Inflight(..)
194 | ConfigureState::Acked
195 | ConfigureState::Committed
196 | ConfigureState::TimedOut(..)
197 | ConfigureState::TimedOutAcked => {}
198 }
199
200 let scheduled = &(*self.window).configure_scheduled;
201 let sent = &(*self.window).configure_sent;
202
203 if !self.needs_configure() {
204 match self.configure_state {
205 ConfigureState::Idle => return false,
206 ConfigureState::TimedOut(serial) => {
207 self.configure_state = ConfigureState::Inflight(serial);
208 return true;
209 }
210 ConfigureState::TimedOutAcked => {
211 self.configure_state = ConfigureState::Acked;
212 return true;
213 }
214 ConfigureState::Inflight(..) | ConfigureState::Acked | ConfigureState::Committed => {
215 return false;
216 }
217 }
218 }
219
220 // Absorb a size-only ECHO: the scheduled size merely restates what the
221 // client has already committed (its current geometry) and nothing else
222 // changed. Sending it anyway hands a self-sizing client a stale size
223 // one commit later — and when the client's content width flaps (the
224 // cpu module's text crossing 10%), that stale echo re-triggers a
225 // resize on both sides and the pair ping-pongs at frame rate (the
226 // status-bar jitter: ~3300 alternating 95/104 configures in 5min).
227 // Agree with reality instead and send nothing. A configure whose size
228 // DIFFERS from the committed geometry — a real compositor-driven
229 // resize — always goes through.
230 {
231 let echo_w = scheduled.width.or(sent.width);
232 let echo_h = scheduled.height.or(sent.height);
233 // Bounds are part of the echo, not a separate signal, when they
234 // merely track the echoed size: the arrange schedules a status
235 // window's bounds equal to its own box, so a self-resize ALWAYS
236 // carries a matching bounds delta — requiring bounds equality
237 // here would keep the absorb permanently disabled for exactly
238 // the windows that loop. A bounds change that differs from the
239 // echoed size (a real available-area change) still forces a
240 // configure.
241 let bounds_ok = (scheduled.bounds.width == sent.bounds.width
242 && scheduled.bounds.height == sent.bounds.height)
243 || (echo_w == Some(scheduled.bounds.width as u32)
244 && echo_h == Some(scheduled.bounds.height as u32));
245 let non_size_equal = scheduled.activated == sent.activated
246 && scheduled.ssd == sent.ssd
247 && scheduled.tiled == sent.tiled
248 && scheduled.capabilities == sent.capabilities
249 && scheduled.maximized == sent.maximized
250 && scheduled.inform_fullscreen == sent.inform_fullscreen
251 && scheduled.resizing == sent.resizing;
252 // Idle AND Committed: after any completed configure round-trip
253 // the state machine RESTS in Committed (Acked → Committed on
254 // commit; only the timeout path returns to Idle), so gating on
255 // Idle alone leaves this absorb dead in steady state — the exact
256 // moment the echo loop runs. Inflight/Acked stay excluded: a
257 // real configure is mid-flight and the scheduled size may need
258 // to supersede it.
259 let size_is_echo = self.geometry.width > 0
260 && self.geometry.height > 0
261 && echo_w == Some(self.geometry.width as u32)
262 && echo_h == Some(self.geometry.height as u32);
263 // Timeout recovery states absorb too: a hot echo loop drives the
264 // machine into TimedOut/TimedOutAcked, and an absorb that disarms
265 // there switches itself off at exactly the moment it exists for
266 // (observed live: a title-flapping window module sustained a
267 // 372↔456 storm at state=TimedOutAcked). Only Inflight/Acked stay
268 // excluded — a real configure is mid-flight there. Absorbing
269 // leaves the timeout recovery untouched: a late ack or the next
270 // commit still walks the state back to Idle.
271 if size_is_echo
272 && non_size_equal
273 && bounds_ok
274 && !matches!(
275 self.configure_state,
276 ConfigureState::Inflight(..) | ConfigureState::Acked
277 )
278 {
279 let absorbed_bounds = scheduled.bounds;
280 (*self.window).configure_sent.width = echo_w;
281 (*self.window).configure_sent.height = echo_h;
282 (*self.window).configure_sent.bounds = absorbed_bounds;
283 (*self.window).configure_scheduled.width = None;
284 (*self.window).configure_scheduled.height = None;
285 return false;
286 }
287 if size_is_echo && log::log_enabled!(log::Level::Debug) {
288 // The size restates committed geometry yet the absorb
289 // declined — name the blocker (the state, or which non-size
290 // field), so a live echo loop is diagnosable from the log.
291 log::debug!(
292 "XdgToplevel::configure: echo NOT absorbed: state={:?} non_size_equal={} \
293 (bounds {}x{}/{}x{} act {}/{} ssd {}/{} tiled {:?}/{:?} caps {:?}/{:?} max {}/{} fs {}/{} rsz {}/{})",
294 self.configure_state, non_size_equal,
295 scheduled.bounds.width, scheduled.bounds.height, sent.bounds.width, sent.bounds.height,
296 scheduled.activated, sent.activated,
297 scheduled.ssd, sent.ssd,
298 scheduled.tiled, sent.tiled,
299 scheduled.capabilities, sent.capabilities,
300 scheduled.maximized, sent.maximized,
301 scheduled.inform_fullscreen, sent.inform_fullscreen,
302 scheduled.resizing, sent.resizing,
303 );
304 }
305 }
306
307 ffi::wlr_xdg_toplevel_set_activated(self.wlr_toplevel, scheduled.activated);
308 ffi::wlr_xdg_toplevel_set_tiled(
309 self.wlr_toplevel,
310 scheduled.tiled,
311 );
312 ffi::wlr_xdg_toplevel_set_wm_capabilities(
313 self.wlr_toplevel,
314 scheduled.capabilities,
315 );
316 ffi::wlr_xdg_toplevel_set_maximized(self.wlr_toplevel, scheduled.maximized);
317 ffi::wlr_xdg_toplevel_set_fullscreen(self.wlr_toplevel, scheduled.inform_fullscreen);
318 ffi::wlr_xdg_toplevel_set_resizing(self.wlr_toplevel, scheduled.resizing);
319
320 if !self.decoration.is_null() {
321 let mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
322 ffi::wlr_xdg_toplevel_decoration_v1_set_mode((*self.decoration).wlr_decoration, mode);
323 }
324
325 if scheduled.bounds.width != sent.bounds.width || scheduled.bounds.height != sent.bounds.height {
326 ffi::wlr_xdg_toplevel_set_bounds(self.wlr_toplevel, scheduled.bounds.width as i32, scheduled.bounds.height as i32);
327 }
328
329 let width = if let Some(w) = scheduled.width {
330 w
331 } else if let Some(w) = (*self.window).configure_sent.width {
332 w
333 } else {
334 self.geometry.width as u32
335 };
336
337 let height = if let Some(h) = scheduled.height {
338 h
339 } else if let Some(h) = (*self.window).configure_sent.height {
340 h
341 } else {
342 self.geometry.height as u32
343 };
344
345 if log::log_enabled!(log::Level::Debug) {
346 log::debug!(
347 "XdgToplevel::configure: sending size {}x{} (scheduled={:?}, sent={:?}, geometry={:?}) to client '{}'",
348 width, height, scheduled.width, sent.width, (self.geometry.width, self.geometry.height), (*self.window).get_title_string().unwrap_or_else(|| "None".to_string())
349 );
350 }
351
352 let configure_serial = ffi::wlr_xdg_toplevel_set_size(self.wlr_toplevel, width as i32, height as i32);
353
354 (*self.window).configure_sent = (*self.window).configure_scheduled.clone();
355 (*self.window).configure_sent.width = Some(width);
356 (*self.window).configure_sent.height = Some(height);
357 (*self.window).configure_scheduled.width = None;
358 (*self.window).configure_scheduled.height = None;
359
360 if width != 0 && height != 0 &&
361 width == self.geometry.width as u32 && height == self.geometry.height as u32 &&
362 matches!(self.configure_state, ConfigureState::Idle) {
363 return false;
364 }
365
366 self.configure_state = ConfigureState::Inflight(configure_serial);
367 true
368 }
369
370 pub unsafe fn needs_configure(&self) -> bool {
371 let scheduled = &(*self.window).configure_scheduled;
372 let sent = &(*self.window).configure_sent;
373
374 if scheduled.width.is_some() && scheduled.width != sent.width {
375 return true;
376 }
377 if scheduled.height.is_some() && scheduled.height != sent.height {
378 return true;
379 }
380 if scheduled.bounds.width != sent.bounds.width || scheduled.bounds.height != sent.bounds.height {
381 return true;
382 }
383 if scheduled.activated != sent.activated {
384 return true;
385 }
386 if scheduled.ssd != sent.ssd {
387 return true;
388 }
389 if scheduled.tiled != sent.tiled {
390 return true;
391 }
392 if scheduled.capabilities != sent.capabilities {
393 return true;
394 }
395 if scheduled.maximized != sent.maximized {
396 return true;
397 }
398 if scheduled.inform_fullscreen != sent.inform_fullscreen {
399 return true;
400 }
401 if scheduled.resizing != sent.resizing {
402 return true;
403 }
404
405 false
406 }
407 }
408
409 unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
410 let toplevel = crate::container_of!(listener, XdgToplevel, destroy);
411
412 if !(*toplevel).decoration.is_null() {
413 XdgDecoration::deinit((*toplevel).decoration);
414 }
415
416 wl_listener_remove(&mut (*toplevel).destroy);
417 wl_listener_remove(&mut (*toplevel).ack_configure);
418 wl_listener_remove(&mut (*toplevel).map);
419 wl_listener_remove(&mut (*toplevel).unmap);
420 wl_listener_remove(&mut (*toplevel).commit);
421 wl_listener_remove(&mut (*toplevel).new_popup);
422 wl_listener_remove(&mut (*toplevel).request_show_window_menu);
423 wl_listener_remove(&mut (*toplevel).request_fullscreen);
424 wl_listener_remove(&mut (*toplevel).request_maximize);
425 wl_listener_remove(&mut (*toplevel).request_minimize);
426 wl_listener_remove(&mut (*toplevel).request_move);
427 wl_listener_remove(&mut (*toplevel).request_resize);
428 wl_listener_remove(&mut (*toplevel).set_parent);
429 wl_listener_remove(&mut (*toplevel).set_title);
430 wl_listener_remove(&mut (*toplevel).set_app_id);
431
432 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
433 ffi::river_wlr_xdg_surface_set_data(base, std::ptr::null_mut());
434 let surface = ffi::river_wlr_xdg_surface_get_surface(base);
435 ffi::river_wlr_surface_set_data(surface, std::ptr::null_mut());
436
437 let window = (*toplevel).window;
438 (*window).impl_destroying();
439 match (*window).state {
440 crate::window::WindowState::Init | crate::window::WindowState::Closing => {}
441 crate::window::WindowState::Ready | crate::window::WindowState::Initialized | crate::window::WindowState::Mapped => {
442 (*window).set_closing();
443 (*(*window).server).wm.dirty_windowing();
444 }
445 }
446
447 let _ = Box::from_raw(toplevel);
448 }
449
450 unsafe extern "C" fn handle_unmap(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
451 let toplevel = crate::container_of!(listener, XdgToplevel, unmap);
452 (*(*toplevel).window).unmap();
453 }
454
455 unsafe extern "C" fn handle_map(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
456 let toplevel = crate::container_of!(listener, XdgToplevel, map);
457 if let Err(e) = (*(*toplevel).window).map() {
458 log::error!("Window map failed: {}", e);
459 let client = ffi::wl_resource_get_client((*(*toplevel).wlr_toplevel).resource);
460 ffi::wl_client_post_no_memory(client);
461 return;
462 }
463
464 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
465 let mut new_geometry = std::mem::zeroed();
466 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
467 (*toplevel).geometry = new_geometry;
468
469 // A fullscreen state the client set before its first commit never raises
470 // request_fullscreen (wlroots only records it); honour it now that the
471 // window is mapped. A fresh floating spawn has no box yet — the arrange
472 // pass leaves it 0x0 so the acked-commit path adopts the natural size —
473 // but that next commit will be the fullscreen one, so seed the box with
474 // the size the window mapped at, or leaving fullscreen restores an
475 // output-sized window.
476 if ffi::river_wlr_xdg_toplevel_get_requested_fullscreen((*toplevel).wlr_toplevel) {
477 let window = (*toplevel).window;
478 if (*window).box_geom.width <= 0 && (*window).box_geom.height <= 0 && new_geometry.width > 0 && new_geometry.height > 0 {
479 (*window).box_geom.width = new_geometry.width;
480 (*window).box_geom.height = new_geometry.height;
481 }
482 (*(*window).server).wm.apply_client_fullscreen(window, true);
483 }
484 // Status segments and Utility windows are SELF-sizing: their bounds
485 // track their own box, so the committed geometry is adopted as the box.
486 let is_self_sized = matches!(
487 (*(*toplevel).window).tiling_mode,
488 crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Utility
489 ) || (*(*toplevel).window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
490 if is_self_sized {
491 (*(*toplevel).window).box_geom.width = new_geometry.width;
492 (*(*toplevel).window).box_geom.height = new_geometry.height;
493 // A view-centered modal that is ALSO self-sizing was centered at map
494 // against a size it had not committed yet; redo it now that it has.
495 (*(*toplevel).window).take_pending_view_center();
496 (*(*(*toplevel).window).server).wm.dirty_windowing();
497 } else if (*(*toplevel).window).pending_view_center
498 && new_geometry.width > 0
499 && new_geometry.height > 0
500 {
501 // A view-centered FLOATING window whose first size just arrived (the
502 // file chooser): adopt the geometry and redo the centering, or the
503 // map-time 400x400-floor placement stands for a 900x500 dialog.
504 (*(*toplevel).window).box_geom.width = new_geometry.width;
505 (*(*toplevel).window).box_geom.height = new_geometry.height;
506 (*(*toplevel).window).take_pending_view_center();
507 (*(*(*toplevel).window).server).wm.dirty_windowing();
508 }
509 }
510
511 unsafe extern "C" fn handle_new_popup(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
512 let toplevel = crate::container_of!(listener, XdgToplevel, new_popup);
513 let wlr_xdg_popup = data as *mut ffi::wlr_xdg_popup;
514
515 let window = (*toplevel).window;
516 let capture_node = &mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree;
517 if let Err(e) = crate::xdg_popup::XdgPopup::create(
518 wlr_xdg_popup,
519 (*window).popup_tree,
520 capture_node,
521 ) {
522 log::error!("Failed to create popup: {}", e);
523 ffi::wl_resource_post_no_memory((*wlr_xdg_popup).resource);
524 return;
525 }
526 // Opening a menu changes nothing the reorder pass's order hash can see,
527 // so it schedules no transaction: without this raise a tiled window's
528 // menu would stay under the floating plane until some unrelated restack
529 // came along. The pass re-applies it from then on.
530 (*(*window).server).wm.raise_focused_popups(window);
531 }
532
533 unsafe extern "C" fn handle_ack_configure(
534 listener: *mut ffi::wl_listener,
535 data: *mut std::ffi::c_void,
536 ) {
537 let toplevel = crate::container_of!(listener, XdgToplevel, ack_configure);
538 let acked_configure = data as *mut ffi::wlr_xdg_surface_configure;
539 let serial = (*acked_configure).serial;
540
541 // Any ack, matching serial or not, proves the client is answering
542 // configures: from here its geometry is a response, and `render_start`
543 // may adopt it again.
544 (*toplevel).acked_once = true;
545
546 match (*toplevel).configure_state {
547 ConfigureState::Inflight(s) => {
548 if serial == s {
549 (*toplevel).configure_state = ConfigureState::Acked;
550 }
551 }
552 ConfigureState::TimedOut(s) => {
553 if serial == s {
554 (*toplevel).configure_state = ConfigureState::TimedOutAcked;
555 }
556 }
557 _ => {}
558 }
559
560 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
561 let mut new_geometry = std::mem::zeroed();
562 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
563 (*toplevel).geometry = new_geometry;
564
565 // Status segments and Utility windows are SELF-sizing: their bounds
566 // track their own box, so the committed geometry is adopted as the box.
567 let is_self_sized = matches!(
568 (*(*toplevel).window).tiling_mode,
569 crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Utility
570 ) || (*(*toplevel).window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
571 if is_self_sized {
572 (*(*toplevel).window).box_geom.width = new_geometry.width;
573 (*(*toplevel).window).box_geom.height = new_geometry.height;
574 // A view-centered modal that is ALSO self-sizing was centered at map
575 // against a size it had not committed yet; redo it now that it has.
576 (*(*toplevel).window).take_pending_view_center();
577 (*(*(*toplevel).window).server).wm.dirty_windowing();
578 } else if (*(*toplevel).window).pending_view_center
579 && new_geometry.width > 0
580 && new_geometry.height > 0
581 {
582 // A view-centered FLOATING window whose first size just arrived (the
583 // file chooser): adopt the geometry and redo the centering, or the
584 // map-time 400x400-floor placement stands for a 900x500 dialog.
585 (*(*toplevel).window).box_geom.width = new_geometry.width;
586 (*(*toplevel).window).box_geom.height = new_geometry.height;
587 (*(*toplevel).window).take_pending_view_center();
588 (*(*(*toplevel).window).server).wm.dirty_windowing();
589 }
590 }
591
592 unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
593 let toplevel = crate::container_of!(listener, XdgToplevel, commit);
594 let window = (*toplevel).window;
595 // Grid-patch latch: the first commit after ack_grid_patch carries the
596 // buffer rendered for that patch — anchor to it from this commit on.
597 // Latching here (not at ack time) means an in-flight older buffer is
598 // never shown at the new anchor.
599 if let Some((serial, patch)) = (*window).grid_patch_acked.take() {
600 log::info!("[Grid] latched patch #{serial} on commit");
601 (*window).grid_patch_current = Some(patch);
602 // The commit swaps the BUFFER in the scene immediately, but the
603 // anchor (position/scale/box) otherwise waits for the next arrange
604 // pass — up to a frame of the new buffer drawn at the OLD patch's
605 // anchor, the occasional one-frame cell flicker at a patch swap.
606 // Apply the new anchor inline, mirroring the arrange Grid arm
607 // exactly (virtual_to_screen's truncating cast included); the next
608 // arrange re-affirms the same values.
609 {
610 let wm = &(*(*window).server).wm;
611 let zoom = crate::policy::background::sanitized_zoom(wm.desk_zoom);
612 let (mut ox, mut oy) = (0i32, 0i32);
613 let outputs_list = &(*(*window).server).om.outputs as *const ffi::wl_list
614 as *mut WlList;
615 let mut curr_out = (*outputs_list).next;
616 while curr_out != outputs_list {
617 let output = crate::container_of!(curr_out, crate::output::Output, link);
618 if (*output).sent.state == crate::output::OutputStateValue::Enabled {
619 let b = (*output).sent.box_layout();
620 ox = b.x;
621 oy = b.y;
622 break;
623 }
624 curr_out = (*curr_out).next;
625 }
626 if patch.scale > 0.0 {
627 // Rounded like every other virtual->screen placement (the
628 // arrange pass and the fallback lattice); truncation here put
629 // the latched patch a pixel off the lattice it replaces.
630 let (lcam, _, _) = wm.layout_camera();
631 let sx = ox + ((patch.x - lcam.pan_x) * zoom).round() as i32;
632 let sy = oy + ((patch.y - lcam.pan_y) * zoom).round() as i32;
633 (*window).rendering_requested.x = sx;
634 (*window).rendering_requested.y = sy;
635 (*window).scale = zoom / patch.scale;
636 (*window).box_geom.x = sx;
637 (*window).box_geom.y = sy;
638 (*window).box_geom.width = (patch.w * patch.scale).round() as i32;
639 (*window).box_geom.height = (patch.h * patch.scale).round() as i32;
640 }
641 }
642 // The grid sits in the optimized-blur capture set (backdrop layers):
643 // new patch content invalidates the shared blurred-backdrop cache,
644 // which nothing else re-bakes when the latch lands after the
645 // viewport has settled — translucent windows keep showing a blur of
646 // the pre-latch desktop.
647 ffi::river_scene_mark_optimized_blur_dirty((*(*window).server).scene.wlr_scene);
648 (*(*window).server).wm.dirty_windowing();
649 }
650 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
651 let old_geometry = (*toplevel).geometry;
652 let mut new_geometry = std::mem::zeroed();
653 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
654 (*toplevel).geometry = new_geometry;
655
656 // A commit that changes the window-geometry box or the surface extent
657 // changes the surface↔frame mapping under a STATIONARY cursor (the scene
658 // helper re-anchors the subtree by -geometry; a grown buffer adds
659 // hoverable area): pointer focus and surface-local coords go stale with
660 // no motion to fix them, and the next click is dispatched against the old
661 // mapping or dropped — the cce-ui overflow-rim popovers exposed this.
662 // Deferred to idle: wlroots' own scene commit listeners re-anchor AFTER
663 // this handler, so an inline refresh would query the stale scene.
664 {
665 let surface = ffi::river_wlr_xdg_surface_get_surface(base);
666 let surf_size = (
667 ffi::river_wlr_surface_get_width(surface),
668 ffi::river_wlr_surface_get_height(surface),
669 );
670 let mapping_changed = old_geometry.x != new_geometry.x
671 || old_geometry.y != new_geometry.y
672 || old_geometry.width != new_geometry.width
673 || old_geometry.height != new_geometry.height
674 || surf_size != (*toplevel).last_surface_size;
675 (*toplevel).last_surface_size = surf_size;
676 if mapping_changed
677 && matches!((*window).state, crate::window::WindowState::Mapped)
678 {
679 (*(*window).server).input_manager.schedule_pointer_refresh();
680 }
681 }
682
683 let app_id = (*window).get_app_id_string().unwrap_or_default();
684 let mut ignore_transparent = (*(*window).server).wm.layout.window_backdrop_blur_ignore_transparent;
685 if app_id.starts_with("cce-status") {
686 ignore_transparent = (*(*window).server).wm.layout.status_backdrop_blur_ignore_transparent;
687 }
688 let scale = (*window).scale;
689 let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status ||
690 app_id.starts_with("cce-status");
691 let is_decorated = (*(*window).server).wm.is_decorated_app(&app_id);
692 // Status segments are SELF-sizing (their bounds track their own box), so
693 // the geometry of the commit being handled is the truth. `rendering_sent`
694 // is a render-start snapshot that lags a contract commit by a render pass
695 // — sizing the blur from it left a menu-sized blur ghost hanging below
696 // the strip until the next commit re-ran this path.
697 let (actual_w, actual_h) = if is_status && (*toplevel).geometry.width > 0 && (*toplevel).geometry.height > 0 {
698 ((*toplevel).geometry.width as u32, (*toplevel).geometry.height as u32)
699 } else {
700 (
701 if (*window).rendering_sent.width > 0 { (*window).rendering_sent.width } else { (*toplevel).geometry.width as u32 },
702 if (*window).rendering_sent.height > 0 { (*window).rendering_sent.height } else { (*toplevel).geometry.height as u32 },
703 )
704 };
705 let geom_w = (actual_w as f64 * scale) as i32;
706 let geom_h = (actual_h as f64 * scale) as i32;
707 // Must mirror Window::set_rendering_state's radius exactly: both paths drive the same
708 // blur node, so if they disagree the corners flip between rounded and square depending
709 // on which one ran last.
710 let radius = if (*window).is_fullscreen() {
711 0
712 } else if (*window).rendering_requested.circular {
713 let w = (*window).rendering_sent.width as i32;
714 let h = (*window).rendering_sent.height as i32;
715 w.min(h) / 2
716 } else if is_status {
717 // Same status exemption as Window::set_rendering_state (part of the
718 // mirror): status segments draw their own module-box corners.
719 0
720 } else if (*window).wm_requested.ssd || is_decorated {
721 (*(*window).server).wm.layout.root_plate_corner_radius
722 } else {
723 0
724 };
725 // Same span widening as Window::set_rendering_state (part of the mirror).
726 let radius = if (*window).rendering_requested.circular {
727 radius
728 } else {
729 crate::window::widen_corner_radius(radius, actual_w as i32, actual_h as i32)
730 };
731 // Rounded corners do NOT require live blur: the corner shape is applied by the
732 // standard blur node's sampler (wlr_scene_blur_set_corner_radius) in both modes;
733 // the optimized node only re-bakes the shared offscreen cache
734 // (fx_render_pass_add_optimized_blur -> read_to_buffer) and never paints on
735 // screen. The old `radius > 0` opt-out silently disabled the optimization for
736 // every (rounded) window, forcing full-backdrop dual-kawase blur per frame per
737 // translucent window — the DE-wide hover-lag / constant-GPU-load root cause.
738 let use_optimized = if is_status {
739 false
740 } else {
741 (*(*window).server).wm.layout.scenefx_optimized_blur
742 };
743 let blur_enabled = (*window).rendering_requested.blur && ((*window).wm_requested.ssd || is_decorated || is_status);
744 ffi::river_scene_node_enable_blur(
745 (*window).tree as *mut ffi::wlr_scene_node,
746 blur_enabled,
747 use_optimized,
748 ignore_transparent,
749 0,
750 0,
751 geom_w,
752 geom_h,
753 // geom_w/h are already scaled to device px; the radius must match.
754 (radius as f64 * scale) as i32,
755 );
756
757 let capture_node = &mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node;
758 let mut geom = std::mem::zeroed();
759 ffi::river_wlr_xdg_surface_get_geometry(base, &mut geom);
760 ffi::wlr_scene_subsurface_tree_set_clip(capture_node, &geom);
761
762 let mut min_w = 0;
763 let mut min_h = 0;
764 let mut max_w = 0;
765 let mut max_h = 0;
766 ffi::river_wlr_xdg_toplevel_get_requested_min_max_size((*toplevel).wlr_toplevel, &mut min_w, &mut min_h, &mut max_w, &mut max_h);
767
768 (*window).set_dimensions_hint(crate::window::DimensionsHint {
769 min_width: min_w as u32,
770 min_height: min_h as u32,
771 max_width: max_w as u32,
772 max_height: max_h as u32,
773 });
774
775 if ffi::river_wlr_xdg_surface_get_initial_commit(base) {
776 assert!((*window).state != crate::window::WindowState::Ready);
777 if (*window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
778 log::debug!("[LinkDbg] initial commit -> ready app={:?} was_state={:?} linked={}",
779 (*window).get_app_id_string(), (*window).state, (*window).is_linked());
780 }
781 (*window).state = crate::window::WindowState::Ready;
782 let mut new_geometry = std::mem::zeroed();
783 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
784 (*toplevel).geometry = new_geometry;
785
786 let is_self_sized = matches!(
787 (*window).tiling_mode,
788 crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Utility
789 ) || (*window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
790 if is_self_sized {
791 (*window).box_geom.width = new_geometry.width;
792 (*window).box_geom.height = new_geometry.height;
793 }
794
795 (*(*window).server).wm.dirty_windowing();
796 return;
797 }
798
799 if (*window).state != crate::window::WindowState::Mapped && (*window).state != crate::window::WindowState::Ready {
800 return;
801 }
802
803 // A self-sizing overlay (cce-cloud) repaints at a new size on its own, with no
804 // configure round trip. The size-change branches below can't catch it: they
805 // compare against (*toplevel).geometry, which already holds the new value by
806 // the time they run, so size_changed is never true. Track the live geometry
807 // here instead and move the border with it, in the same commit that puts the
808 // new buffer on screen — waiting for the WM cycle (which round-trips out to
809 // the external window-manager client) leaves the border a size behind.
810 // Utility windows self-size the same way (the arrange pass only ever
811 // sends them the "you choose" 0x0, so every size change originates in a
812 // client commit like this one).
813 if matches!(
814 (*window).tiling_mode,
815 crate::tiling::TilingMode::Overlay | crate::tiling::TilingMode::Utility
816 ) {
817 let mut live = std::mem::zeroed();
818 ffi::river_wlr_xdg_surface_get_geometry(base, &mut live);
819 if live.width > 0 && live.height > 0
820 && (live.width != (*window).box_geom.width || live.height != (*window).box_geom.height)
821 {
822 (*window).box_geom.width = live.width;
823 (*window).box_geom.height = live.height;
824 // render_finish would otherwise reset box_geom from the render-start
825 // snapshot (rendering_sent) and snap the border back to the old size.
826 (*window).self_resized = true;
827 (*window).draw_borders();
828 (*window).set_dimensions(live.width as u32, live.height as u32);
829 (*(*window).server).wm.dirty_windowing();
830 }
831 }
832
833 // Status segments self-size the same way when an in-surface menu grows or
834 // contracts the surface (no configure round trip). Track the live
835 // geometry in the same commit: box_geom feeds the expanded-state order
836 // hash that restacks the segment into the popups layer, so a lagging
837 // box_geom left the freshly opened menu stacked UNDER its sibling
838 // segments (their text drew sharp over the menu's blur) for a beat.
839 // Unlike the overlay branch, no set_dimensions — the WM deliberately
840 // leaves status segment sizes to the client.
841 if is_status {
842 let mut live = std::mem::zeroed();
843 ffi::river_wlr_xdg_surface_get_geometry(base, &mut live);
844 if live.width > 0 && live.height > 0
845 && (live.width != (*window).box_geom.width || live.height != (*window).box_geom.height)
846 {
847 (*window).box_geom.width = live.width;
848 (*window).box_geom.height = live.height;
849 (*window).self_resized = true;
850 (*(*window).server).wm.dirty_windowing();
851 }
852 }
853
854 match (*toplevel).configure_state {
855 ConfigureState::Idle | ConfigureState::Committed | ConfigureState::TimedOut(..) => {
856 // Nothing to do: client-initiated size/position changes CANNOT
857 // be detected here. The top of handle_commit already refreshed
858 // (*toplevel).geometry from this commit, so any comparison
859 // against it never fires (the branch that used to live here was
860 // dead for that reason). Self-sizing overlays are handled by the
861 // live-geometry sync above; clients resizing through a configure
862 // round trip land in the Acked arm.
863 }
864 ConfigureState::Inflight(..) => {
865 (*window).send_frame_done();
866 }
867 ConfigureState::Acked | ConfigureState::TimedOutAcked => {
868 let mut new_geometry = std::mem::zeroed();
869 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
870 (*toplevel).geometry = new_geometry;
871
872 (*window).rendering_scheduled.width = new_geometry.width as u32;
873 (*window).rendering_scheduled.height = new_geometry.height as u32;
874
875 let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status ||
876 (*window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
877 // Overlay included so its scheduled size tracks the client's own; the
878 // border itself is handled by the live-geometry sync above.
879 let is_overlay = (*window).tiling_mode == crate::tiling::TilingMode::Overlay;
880 let is_utility = (*window).tiling_mode == crate::tiling::TilingMode::Utility;
881 if matches!((*window).tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup) || is_status || is_overlay || is_utility {
882 (*window).set_dimensions(new_geometry.width as u32, new_geometry.height as u32);
883 if is_status {
884 // Only a size that actually moved re-arranges. A status
885 // segment acks a configure and commits at its old size
886 // for every content refresh; each of those used to run
887 // a full manage/arrange/render transaction.
888 let (w, h) = (new_geometry.width, new_geometry.height);
889 if w != (*window).box_geom.width || h != (*window).box_geom.height {
890 (*window).box_geom.width = w;
891 (*window).box_geom.height = h;
892 (*(*window).server).wm.dirty_windowing();
893 }
894 }
895 }
896
897 let (dec_w, dec_h) = (*window).get_decorations_size();
898 if dec_w != (*window).last_decor_w || dec_h != (*window).last_decor_h {
899 (*window).last_decor_w = dec_w;
900 (*window).last_decor_h = dec_h;
901 (*(*window).server).wm.dirty_windowing();
902 }
903
904 if let Some(sent_w) = (*window).configure_sent.width {
905 if !(*window).wm_requested.ssd && dec_w > 0 && new_geometry.width as u32 == sent_w.saturating_sub(dec_w as u32) {
906 if !(*window).csd_buffer_size_bug {
907 (*window).csd_buffer_size_bug = true;
908 log::info!("Detected CSD buffer size bug for window '{}'. Activating workaround.", (*window).get_title_string().unwrap_or_default());
909 (*(*window).server).wm.dirty_windowing();
910 }
911 }
912 }
913
914 match (*toplevel).configure_state {
915 ConfigureState::Acked => {
916 (*toplevel).configure_state = ConfigureState::Committed;
917 (*(*window).server).wm.notify_configured();
918 }
919 ConfigureState::TimedOutAcked => {
920 (*toplevel).configure_state = ConfigureState::Idle;
921 (*(*window).server).wm.dirty_rendering();
922 }
923 _ => unreachable!(),
924 }
925 }
926 }
927
928 // Left/top-edge anchoring on the committed geometry — the shared
929 // Window::anchor_resize_commit; Xwayland windows take the same path from
930 // handle_window_commit.
931 let geometry = (*toplevel).geometry;
932 if let Some((final_x, final_y)) = (*window).anchor_resize_commit(geometry.width, geometry.height) {
933 let server = (*window).server;
934 // Keep the displayed buffer and the compensating position atomic.
935 // Live buffer (no configure in flight): the commit is already on
936 // screen, so move the scene tree in the same commit — waiting for
937 // the next render pass lets a frame composite the new size at the
938 // old position, jittering the anchored edges. Frozen buffer (saved
939 // for an in-flight configure): moving the tree now would shift the
940 // OLD-size buffer instead, so leave the position to the render pass
941 // (which restores the new buffer and applies it together) and make
942 // sure that pass runs promptly.
943 if !(*window).surfaces.saved {
944 ffi::river_scene_node_set_position_if_changed((*window).tree as *mut ffi::wlr_scene_node, final_x, final_y);
945 ffi::river_scene_node_set_position_if_changed((*window).popup_tree as *mut ffi::wlr_scene_node, final_x, final_y);
946 (*window).box_geom.width = geometry.width;
947 (*window).box_geom.height = geometry.height;
948 (*window).draw_borders();
949 } else {
950 (*server).wm.dirty_rendering();
951 }
952 }
953 }
954
955 unsafe extern "C" fn handle_request_show_window_menu(
956 listener: *mut ffi::wl_listener,
957 data: *mut std::ffi::c_void,
958 ) {
959 let toplevel = crate::container_of!(listener, XdgToplevel, request_show_window_menu);
960 let event = data as *mut ffi::wlr_xdg_toplevel_show_window_menu_event;
961 let window = (*toplevel).window;
962
963 (*window).wm_scheduled.show_window_menu_requested = Some(crate::window::ShowWindowMenuRequest {
964 x: (*event).x - (*toplevel).geometry.x,
965 y: (*event).y - (*toplevel).geometry.y,
966 });
967 (*(*window).server).wm.dirty_windowing();
968 }
969
970 unsafe extern "C" fn handle_request_fullscreen(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
971 let toplevel = crate::container_of!(listener, XdgToplevel, request_fullscreen);
972 let window = (*toplevel).window;
973
974 if ffi::river_wlr_xdg_toplevel_get_requested_fullscreen((*toplevel).wlr_toplevel) {
975 let wlr_output = ffi::river_wlr_xdg_toplevel_get_requested_fullscreen_output((*toplevel).wlr_toplevel);
976 if !wlr_output.is_null() {
977 let output = ffi::river_wlr_output_get_data(wlr_output) as *mut crate::output::Output;
978 (*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Fullscreen(output);
979 } else {
980 (*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut());
981 }
982 } else {
983 (*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Exit;
984 }
985 (*(*window).server).wm.dirty_windowing();
986 let enter = ffi::river_wlr_xdg_toplevel_get_requested_fullscreen((*toplevel).wlr_toplevel);
987 (*(*window).server).wm.apply_client_fullscreen(window, enter);
988 }
989
990 unsafe extern "C" fn handle_request_maximize(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
991 let toplevel = crate::container_of!(listener, XdgToplevel, request_maximize);
992 let window = (*toplevel).window;
993
994 // A Utility window declared itself content-shaped; a maximize would hand
995 // sizing back to the compositor. Explicitly ignored, not just unmapped
996 // from any affordance.
997 if (*window).tiling_mode == crate::tiling::TilingMode::Utility {
998 return;
999 }
1000
1001 if ffi::river_wlr_xdg_toplevel_get_requested_maximized((*toplevel).wlr_toplevel) {
1002 (*window).tiling_mode = crate::tiling::TilingMode::Tiled;
1003 (*window).mode_locked = true;
1004 (*window).wm_scheduled.maximize_requested = crate::window::MaximizeRequest::Maximize;
1005 } else {
1006 (*window).tiling_mode = crate::tiling::TilingMode::Floating;
1007 (*window).mode_locked = true;
1008 (*window).wm_scheduled.maximize_requested = crate::window::MaximizeRequest::Unmaximize;
1009 }
1010 (*(*window).server).wm.dirty_windowing();
1011 }
1012
1013 unsafe extern "C" fn handle_request_minimize(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1014 let toplevel = crate::container_of!(listener, XdgToplevel, request_minimize);
1015 let window = (*toplevel).window;
1016
1017 (*window).wm_scheduled.minimize_requested = true;
1018 (*(*window).server).wm.dirty_windowing();
1019 }
1020
1021 unsafe extern "C" fn handle_request_move(
1022 listener: *mut ffi::wl_listener,
1023 data: *mut std::ffi::c_void,
1024 ) {
1025 let toplevel = crate::container_of!(listener, XdgToplevel, request_move);
1026 let event = data as *mut ffi::wlr_xdg_toplevel_move_event;
1027 let window = (*toplevel).window;
1028 let seat = ffi::river_wlr_seat_get_data((*(*event).seat).seat) as *mut crate::seat::Seat;
1029
1030 if ffi::wlr_seat_validate_pointer_grab_serial((*seat).wlr_seat, std::ptr::null_mut(), (*event).serial) {
1031 let initial_mode = (*window).tiling_mode;
1032 if initial_mode != crate::tiling::TilingMode::Floating
1033 && initial_mode != crate::tiling::TilingMode::Popup
1034 && initial_mode != crate::tiling::TilingMode::Fullscreen
1035 && initial_mode != crate::tiling::TilingMode::Overlay
1036 // A move must not cost a window its Utility mode.
1037 && initial_mode != crate::tiling::TilingMode::Utility
1038 {
1039 (*window).tiling_mode = crate::tiling::TilingMode::Floating;
1040 (*window).mode_locked = true;
1041 }
1042
1043 (*seat).focus(crate::seat::Focus::Window(window));
1044 (*(*window).server).wm.stop_panning_animation();
1045 let cursor = &mut (*seat).cursor;
1046 let cursor_x = (*cursor.wlr_cursor).x;
1047 let cursor_y = (*cursor.wlr_cursor).y;
1048
1049 (*seat).op = Some(crate::seat::SeatOp {
1050 sent_release: false,
1051 input: crate::seat::SeatOpInput::Pointer,
1052 start_x: cursor_x as i32,
1053 start_y: cursor_y as i32,
1054 x: cursor_x as i32,
1055 y: cursor_y as i32,
1056 window_ptr: window,
1057 op_type: crate::seat::PointerOpType::Move,
1058 start_win_x: (*window).box_geom.x,
1059 start_win_y: (*window).box_geom.y,
1060 start_win_w: (*window).box_geom.width as u32,
1061 start_win_h: (*window).box_geom.height as u32,
1062 start_win_virtual_x: (*window).virtual_x,
1063 start_win_virtual_y: (*window).virtual_y,
1064 start_tiling_mode: (*window).tiling_mode,
1065 start_was_tiled: (*window).tiling_mode == crate::tiling::TilingMode::Tiled,
1066 start_mode_locked: (*window).mode_locked,
1067 start_pan_x: (*(*window).server).wm.desk_pan_x,
1068 start_pan_y: (*(*window).server).wm.desk_pan_y,
1069 started_in_overview: (*(*window).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
1070 });
1071 cursor.op_start_pointer();
1072 cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
1073
1074 (*window).wm_scheduled.pointer_move_requested = seat;
1075 (*(*window).server).wm.dirty_windowing();
1076 }
1077 }
1078
1079 unsafe extern "C" fn handle_request_resize(
1080 listener: *mut ffi::wl_listener,
1081 data: *mut std::ffi::c_void,
1082 ) {
1083 let toplevel = crate::container_of!(listener, XdgToplevel, request_resize);
1084 let event = data as *mut ffi::wlr_xdg_toplevel_resize_event;
1085 let window = (*toplevel).window;
1086
1087 // Nothing may interactively resize a Utility window — its size is the
1088 // client's `settings()` data, not a drag. Rejected at the request, not
1089 // just left without an affordance.
1090 if (*window).tiling_mode == crate::tiling::TilingMode::Utility {
1091 return;
1092 }
1093
1094 let seat = ffi::river_wlr_seat_get_data((*(*event).seat).seat) as *mut crate::seat::Seat;
1095
1096 if ffi::wlr_seat_validate_pointer_grab_serial((*seat).wlr_seat, std::ptr::null_mut(), (*event).serial) {
1097 let initial_mode = (*window).tiling_mode;
1098 if initial_mode != crate::tiling::TilingMode::Floating
1099 && initial_mode != crate::tiling::TilingMode::Popup
1100 && initial_mode != crate::tiling::TilingMode::Fullscreen
1101 {
1102 (*window).tiling_mode = crate::tiling::TilingMode::Floating;
1103 (*window).mode_locked = true;
1104 }
1105
1106 (*seat).focus(crate::seat::Focus::Window(window));
1107 (*(*window).server).wm.stop_panning_animation();
1108 let cursor = &mut (*seat).cursor;
1109 let cursor_x = (*cursor.wlr_cursor).x;
1110 let cursor_y = (*cursor.wlr_cursor).y;
1111
1112 let edges = crate::window::Edges::from_u32((*event).edges);
1113 (*seat).op = Some(crate::seat::SeatOp {
1114 sent_release: false,
1115 input: crate::seat::SeatOpInput::Pointer,
1116 start_x: cursor_x as i32,
1117 start_y: cursor_y as i32,
1118 x: cursor_x as i32,
1119 y: cursor_y as i32,
1120 window_ptr: window,
1121 op_type: crate::seat::PointerOpType::Resize {
1122 edges,
1123 },
1124 start_win_x: (*window).box_geom.x,
1125 start_win_y: (*window).box_geom.y,
1126 start_win_w: (*window).box_geom.width as u32,
1127 start_win_h: (*window).box_geom.height as u32,
1128 start_win_virtual_x: (*window).virtual_x,
1129 start_win_virtual_y: (*window).virtual_y,
1130 start_tiling_mode: (*window).tiling_mode,
1131 start_was_tiled: (*window).tiling_mode == crate::tiling::TilingMode::Tiled,
1132 start_mode_locked: (*window).mode_locked,
1133 start_pan_x: (*(*window).server).wm.desk_pan_x,
1134 start_pan_y: (*(*window).server).wm.desk_pan_y,
1135 started_in_overview: (*(*window).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
1136 });
1137 cursor.op_start_pointer();
1138 let cursor_name = crate::cursor::get_resize_cursor_name(edges);
1139 cursor.set_xcursor(cursor_name.as_ptr() as *const _);
1140
1141 (*window).wm_scheduled.pointer_resize_requested = Some(crate::window::PointerResizeRequest {
1142 seat,
1143 edges: (*event).edges,
1144 });
1145 (*(*window).server).wm.dirty_windowing();
1146 }
1147 }
1148
1149 unsafe extern "C" fn handle_set_parent(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1150 let toplevel = crate::container_of!(listener, XdgToplevel, set_parent);
1151 let window = (*toplevel).window;
1152 (*(*window).server).wm.dirty_windowing();
1153 }
1154
1155 unsafe extern "C" fn handle_set_title(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1156 let toplevel = crate::container_of!(listener, XdgToplevel, set_title);
1157 let window = (*toplevel).window;
1158 (*window).notify_title();
1159 }
1160
1161 unsafe extern "C" fn handle_set_app_id(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1162 let toplevel = crate::container_of!(listener, XdgToplevel, set_app_id);
1163 let window = (*toplevel).window;
1164 (*window).notify_app_id();
1165 }
1166
1167 unsafe extern "C" fn handle_decoration_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1168 let decoration = crate::container_of!(listener, XdgDecoration, destroy);
1169 XdgDecoration::deinit(decoration);
1170 }
1171
1172 unsafe extern "C" fn handle_decoration_request_mode(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1173 let decoration = crate::container_of!(listener, XdgDecoration, request_mode);
1174
1175 let base = ffi::river_wlr_xdg_toplevel_get_base((*(*decoration).wlr_decoration).toplevel);
1176 let toplevel = ffi::river_wlr_xdg_surface_get_data(base) as *mut XdgToplevel;
1177 let window = (*toplevel).window;
1178
1179 let hint = match (*(*decoration).wlr_decoration).requested_mode {
1180 ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_NONE => {
1181 ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_NO_PREFERENCE
1182 }
1183 ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE => {
1184 ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_PREFERS_CSD
1185 }
1186 ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE => {
1187 ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_PREFERS_SSD
1188 }
1189 _ => ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_NO_PREFERENCE,
1190 };
1191 (*window).set_decoration_hint(hint);
1192
1193 if ffi::river_wlr_xdg_surface_get_initialized(base) {
1194 let mut mode = (*(*decoration).wlr_decoration).requested_mode;
1195 if mode == ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_NONE {
1196 let server = (*window).server;
1197 let rule_ssd = (*server).wm.get_rule_for_window(window).and_then(|r| r.ssd);
1198 if let Some(true) = rule_ssd {
1199 mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
1200 } else {
1201 mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE;
1202 }
1203 }
1204 ffi::wlr_xdg_toplevel_decoration_v1_set_mode((*decoration).wlr_decoration, mode);
1205 (*window).wm_requested.ssd = mode == ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
1206 (*(*window).server).wm.dirty_windowing();
1207 }
1208 }
1209
1210 impl XdgDecoration {
1211 pub unsafe fn init(wlr_decoration: *mut ffi::wlr_xdg_toplevel_decoration_v1) -> *mut Self {
1212 let base = ffi::river_wlr_xdg_toplevel_get_base((*wlr_decoration).toplevel);
1213 let toplevel = ffi::river_wlr_xdg_surface_get_data(base) as *mut XdgToplevel;
1214
1215 let decoration = Box::into_raw(Box::new(XdgDecoration {
1216 wlr_decoration,
1217 destroy: std::mem::zeroed(),
1218 request_mode: std::mem::zeroed(),
1219 }));
1220
1221 (*toplevel).decoration = decoration;
1222
1223 let destroy_ptr = &mut (*decoration).destroy as *mut ffi::wl_listener as *mut WlListener;
1224 (*destroy_ptr).notify = Some(handle_decoration_destroy);
1225 wl_signal_add(&mut (*wlr_decoration).events.destroy, &mut (*decoration).destroy);
1226
1227 let req_mode_ptr = &mut (*decoration).request_mode as *mut ffi::wl_listener as *mut WlListener;
1228 (*req_mode_ptr).notify = Some(handle_decoration_request_mode);
1229 wl_signal_add(&mut (*wlr_decoration).events.request_mode, &mut (*decoration).request_mode);
1230
1231 if ffi::river_wlr_xdg_surface_get_initialized(base) {
1232 handle_decoration_request_mode(&mut (*decoration).request_mode, std::ptr::null_mut());
1233 }
1234
1235 decoration
1236 }
1237
1238 pub unsafe fn deinit(decoration: *mut XdgDecoration) {
1239 let base = ffi::river_wlr_xdg_toplevel_get_base((*(*decoration).wlr_decoration).toplevel);
1240 let toplevel = ffi::river_wlr_xdg_surface_get_data(base) as *mut XdgToplevel;
1241
1242 wl_listener_remove(&mut (*decoration).destroy);
1243 wl_listener_remove(&mut (*decoration).request_mode);
1244
1245 assert!(!(*toplevel).decoration.is_null());
1246 (*toplevel).decoration = std::ptr::null_mut();
1247
1248 let _ = Box::from_raw(decoration);
1249 }
1250 }
1251
1252