Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/window.rs (260.2K)
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, WlList, wl_list_insert, wl_list_remove, wl_list_remove_and_reinit, WlListener, wl_signal_add};
6 use crate::wm_node::WmNode;
7 use crate::xdg_toplevel::ConfigureState;
8
9 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
10 pub enum WindowState {
11 Init,
12 Ready,
13 Initialized,
14 Mapped,
15 Closing,
16 }
17
18 #[derive(Clone, Copy)]
19 pub enum WindowImpl {
20 Toplevel(*mut crate::xdg_toplevel::XdgToplevel),
21 Xwayland(*mut crate::xwayland_window::XwaylandWindow),
22 Destroying,
23 }
24
25 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
26 pub enum FullscreenRequest {
27 NoRequest,
28 Fullscreen(*mut crate::output::Output),
29 Exit,
30 }
31
32 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
33 pub enum MaximizeRequest {
34 NoRequest,
35 Maximize,
36 Unmaximize,
37 }
38
39 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
40 pub struct Dimensions {
41 pub width: u32,
42 pub height: u32,
43 }
44
45 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
46 pub struct DimensionsHint {
47 pub min_width: u32,
48 pub min_height: u32,
49 pub max_width: u32,
50 pub max_height: u32,
51 }
52
53 impl DimensionsHint {
54 /// Clamp a requested content size to the client's declared range; a
55 /// zero bound is "unset" (xdg-shell's convention) and leaves that side
56 /// alone. A max below the min is the client's own contradiction and
57 /// the min wins.
58 pub fn clamp(&self, width: u32, height: u32) -> (u32, u32) {
59 let mut w = width;
60 let mut h = height;
61 if self.max_width > 0 {
62 w = w.min(self.max_width);
63 }
64 if self.max_height > 0 {
65 h = h.min(self.max_height);
66 }
67 if self.min_width > 0 {
68 w = w.max(self.min_width);
69 }
70 if self.min_height > 0 {
71 h = h.max(self.min_height);
72 }
73 (w, h)
74 }
75 }
76
77 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
78 pub struct Edges {
79 pub top: bool,
80 pub bottom: bool,
81 pub left: bool,
82 pub right: bool,
83 }
84
85 impl Edges {
86 pub fn new() -> Self {
87 Self { top: false, bottom: false, left: false, right: false }
88 }
89 pub fn from_u32(val: u32) -> Self {
90 Self {
91 top: (val & 1) != 0,
92 bottom: (val & 2) != 0,
93 left: (val & 4) != 0,
94 right: (val & 8) != 0,
95 }
96 }
97 }
98
99 #[derive(Clone, Copy, Debug, PartialEq)]
100 pub struct Border {
101 pub edges: Edges,
102 pub width: u32,
103 /// Premultiplied-alpha RGBA, 0.0–1.0 per channel (scenefx convention).
104 pub color: [f32; 4],
105 /// Color while the pointer hovers the border (the grab surface).
106 pub hover_color: [f32; 4],
107 pub corner_radius: i32,
108 }
109
110 impl Border {
111 pub fn none() -> Self {
112 Self { edges: Edges::new(), width: 0, color: [0.0; 4], hover_color: [0.0; 4], corner_radius: 0 }
113 }
114 }
115
116 /// A window-scale corner radius as scenefx should consume it: the configured
117 /// nominal (circle-equivalent) radius widened by the curvature-match span
118 /// factor, capped at half the smaller content extent so opposite corners
119 /// can't overlap — the exact counterpart of cce-ui's
120 /// `VkRenderer::clip_corner_radius`, which widens the clients' plate/clip
121 /// corners the same way. `width`/`height` and the returned radius are in
122 /// logical px; callers scale to device px where they already do.
123 pub fn widen_corner_radius(nominal: i32, width: i32, height: i32) -> i32 {
124 if nominal <= 0 {
125 return nominal;
126 }
127 let widened = (nominal as f64 * crate::config::corner_span_factor()).round() as i32;
128 widened.min(width.min(height) / 2)
129 }
130
131 /// One of the 8 interactive border zones. Each draws as its own visual
132 /// element (corners as two-rect Ls) and highlights independently on hover.
133 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
134 pub enum BorderElement {
135 Top,
136 Bottom,
137 Left,
138 Right,
139 TopLeft,
140 TopRight,
141 BottomLeft,
142 BottomRight,
143 }
144
145 impl BorderElement {
146 /// Every zone, in `index()` order.
147 pub const ALL: [BorderElement; 8] = [
148 BorderElement::Top,
149 BorderElement::Bottom,
150 BorderElement::Left,
151 BorderElement::Right,
152 BorderElement::TopLeft,
153 BorderElement::TopRight,
154 BorderElement::BottomLeft,
155 BorderElement::BottomRight,
156 ];
157
158 /// Index into `Window::border_reveal`. Declaration order; kept in one
159 /// place so the reveal array and the enum can't drift apart.
160 pub fn index(self) -> usize {
161 match self {
162 BorderElement::Top => 0,
163 BorderElement::Bottom => 1,
164 BorderElement::Left => 2,
165 BorderElement::Right => 3,
166 BorderElement::TopLeft => 4,
167 BorderElement::TopRight => 5,
168 BorderElement::BottomLeft => 6,
169 BorderElement::BottomRight => 7,
170 }
171 }
172 }
173
174 /// Per-frame step of the hover fade, as a fraction of the remaining distance
175 /// to the target (the same exponential-approach shape the viewport pan uses).
176 pub const BORDER_FADE_STEP: f32 = 0.15;
177 /// Below this the fade is treated as finished and snapped to its target.
178 pub const BORDER_FADE_EPSILON: f32 = 0.004;
179
180 /// Per-tick step of the fullscreen-toggle animation, as a fraction of the
181 /// remaining distance to the target rect (the pan/border-fade shape).
182 pub const FS_ANIM_STEP: f64 = 0.22;
183 /// A channel within this many screen px of its target counts as settled.
184 pub const FS_ANIM_EPSILON: f64 = 0.5;
185 /// Hard cap on animation lifetime (~3s at 16ms) so a client that never
186 /// commits its new size can't leave the window stuck mid-stretch.
187 pub const FS_ANIM_MAX_TICKS: u32 = 180;
188
189 /// State of an in-flight fullscreen-toggle animation, in screen px.
190 #[derive(Clone, Copy)]
191 pub struct FsAnim {
192 pub x: f64,
193 pub y: f64,
194 pub w: f64,
195 pub h: f64,
196 /// The target only becomes real once the next arrange/configure lands;
197 /// until the target has moved off the start rect the animation must not
198 /// declare itself settled (start == target on the first ticks).
199 pub moved: bool,
200 pub ticks: u32,
201 }
202
203 /// Length of a corner zone, measured from the outer corner along each band.
204 /// Shared by the visual segments (draw_borders) and the pointer zones
205 /// (cursor.rs get_border_zone) so they always agree. `configured` comes from
206 /// `border { corner_length= }`; 0 picks the auto formula. Never shorter than
207 /// the band width, so a corner is at least its diagonal square. `r_out` is
208 /// the corner ring's OUTER arc radius (the window silhouette radius plus the
209 /// band; 0 for square windows): the zone must reach past the arc plus half a
210 /// band of straight arm, or the widened window corners (~35 logical px)
211 /// overflow the corner piece and the arc gets truncated mid-sweep.
212 pub fn border_corner_len(bw: f64, configured: i32, r_out: f64) -> f64 {
213 let cl = if configured > 0 {
214 // Configured lengths predate the band doubling — scale them the same
215 // way, and keep at least half a band of straight arm (arm = cl − bw)
216 // so a corner can never collapse to a bare square. (corner_length=16
217 // with the doubled 16px band used to yield arm = 0: corners vanished.)
218 (configured as f64 * 2.0).max(1.5 * bw)
219 } else {
220 // 3× band: the corner arms reach well down each edge (they also
221 // carry the rounded-corner arc, which eats into the straight run).
222 (3.0 * bw).max(24.0)
223 };
224 cl.max(r_out + 0.5 * bw)
225 }
226
227 /// Floor on the border grab/reveal band, in unscaled layout pixels. Borders
228 /// rest invisible until hovered, so the band is the only thing to aim at; a
229 /// narrow target would be unusable.
230 pub const HOVER_BAND_MIN: f64 = 16.0;
231
232 /// Effective interactive border band width (unscaled): the configured border
233 /// width DOUBLED — the hover/grab band runs twice the classic border — with
234 /// the HOVER_BAND_MIN floor. Shared by the visual segments (draw_borders),
235 /// the hit catchers, and the pointer zones (cursor::get_border_zone) so they
236 /// can never drift apart.
237 pub fn border_band_width(configured_width: u32) -> f64 {
238 (configured_width as f64 * 2.0).max(HOVER_BAND_MIN)
239 }
240
241 /// Where the eight resize handles sit — one disc per zone, in
242 /// `BorderElement::index()` order — for a window whose content is `w`×`h`
243 /// ON SCREEN, with silhouette corner radius `r_in` and handle diameter `d`
244 /// (all screen px). Returns the centres and the disc radius. The side discs
245 /// are tangent to their side; a corner disc sits on the corner's diagonal,
246 /// tangent to the rounded corner arc when that arc is wider than the disc
247 /// and tucked into the two straight edges otherwise. The frame shader
248 /// (scenefx `frame.frag`) lays out the same discs from the same inputs;
249 /// `draw_borders` (the catchers) and `cursor::get_border_zone` (the hit
250 /// test) both call this, so what is drawn is what grabs. Keep the shader
251 /// and this in step.
252 pub fn handle_disc_layout(w: f64, h: f64, r_in: f64, d: f64) -> ([(f64, f64); 8], f64) {
253 let r = 0.5 * d;
254 let t = if r_in > r { r_in - (r_in - r) / std::f64::consts::SQRT_2 } else { r };
255 let centres = [
256 (0.5 * w, r), // Top
257 (0.5 * w, h - r), // Bottom
258 (r, 0.5 * h), // Left
259 (w - r, 0.5 * h), // Right
260 (t, t), // TopLeft
261 (w - t, t), // TopRight
262 (t, h - t), // BottomLeft
263 (w - t, h - t), // BottomRight
264 ];
265 (centres, r)
266 }
267
268 pub struct BorderRects {
269 /// The old full-band hit catchers. Retired by the disc handles — the
270 /// pointer between two discs must reach the app, not a catcher — and
271 /// kept disabled.
272 pub left: *mut ffi::wlr_scene_rect,
273 pub right: *mut ffi::wlr_scene_rect,
274 pub top: *mut ffi::wlr_scene_rect,
275 pub bottom: *mut ffi::wlr_scene_rect,
276 /// Invisible square catchers, one per handle disc, indexed by
277 /// `BorderElement::index()`: they make a scene hit on a disc resolve to
278 /// this window even where the client's input region does not cover it.
279 pub segments: [*mut ffi::wlr_scene_rect; 8],
280 /// The resize handles: all eight discs in one shader-drawn node.
281 pub frame: *mut ffi::wlr_scene_frame,
282 /// Parent of `segments`, living in the global border overlay layer rather
283 /// than in the window tree. Tracks the window tree's position so the
284 /// segments keep their window-local coordinates.
285 pub tree: *mut ffi::wlr_scene_tree,
286 }
287
288 pub struct ShowWindowMenuRequest {
289 pub x: i32,
290 pub y: i32,
291 }
292
293 pub struct PointerResizeRequest {
294 pub seat: *mut crate::seat::Seat,
295 pub edges: u32,
296 }
297
298 pub struct WmScheduledState {
299 pub dimensions_hint: DimensionsHint,
300 pub decoration_hint: ffi::zcce_window_v1_decoration_hint,
301 pub show_window_menu_requested: Option<ShowWindowMenuRequest>,
302 pub fullscreen_requested: FullscreenRequest,
303 pub maximize_requested: MaximizeRequest,
304 pub minimize_requested: bool,
305 pub dirty_app_id: bool,
306 pub dirty_title: bool,
307 pub pointer_move_requested: *mut crate::seat::Seat,
308 pub pointer_resize_requested: Option<PointerResizeRequest>,
309 }
310
311 pub struct WmSentState {
312 pub dimensions_hint: DimensionsHint,
313 pub decoration_hint: ffi::zcce_window_v1_decoration_hint,
314 pub parent: Option<crate::slotmap::Key>,
315 }
316
317 pub struct WmRequestedState {
318 pub dimensions: Option<Dimensions>,
319 pub bounds: Dimensions,
320 pub ssd: bool,
321 pub tiled: u32,
322 pub capabilities: u32,
323 pub resizing: bool,
324 pub maximized: bool,
325 pub fullscreen: *mut crate::output::Output,
326 pub inform_fullscreen: bool,
327 pub close: bool,
328 }
329
330 #[derive(Clone, Debug, PartialEq, Eq)]
331 pub struct Configure {
332 pub width: Option<u32>,
333 pub height: Option<u32>,
334 pub bounds: Dimensions,
335 pub activated: bool,
336 pub ssd: bool,
337 pub tiled: u32,
338 pub capabilities: u32,
339 pub maximized: bool,
340 pub inform_fullscreen: bool,
341 pub resizing: bool,
342 }
343
344 impl Configure {
345 pub fn new() -> Self {
346 Self {
347 width: None,
348 height: None,
349 bounds: Dimensions { width: 0, height: 0 },
350 activated: false,
351 ssd: false,
352 tiled: 0,
353 capabilities: 0,
354 maximized: false,
355 inform_fullscreen: false,
356 resizing: false,
357 }
358 }
359 }
360
361 pub struct WindowRenderingScheduled {
362 pub width: u32,
363 pub height: u32,
364 pub resend_dimensions: bool,
365 }
366
367 pub struct WindowRenderingSent {
368 pub width: u32,
369 pub height: u32,
370 pub presentation_hint: ffi::zcce_output_v1_presentation_mode,
371 }
372
373 pub struct WindowRenderingRequested {
374 pub x: i32,
375 pub y: i32,
376 pub hidden: bool,
377 pub border: Border,
378 pub clip: ffi::wlr_box,
379 pub content_clip: ffi::wlr_box,
380 pub opacity: f32,
381 pub circular: bool,
382 pub blur: bool,
383 }
384
385 pub struct Window {
386 pub ref_key: crate::slotmap::Key,
387 pub server: *mut Server,
388 pub object: *mut ffi::wl_resource, // zcce_window_v1
389 pub node: WmNode,
390 pub state: WindowState,
391 pub impl_type: WindowImpl,
392 /// Where a two-finger scroll over this window becomes an emulated
393 /// view drag (see `cursor::ViewDrag`), when the app has said so through
394 /// `touchpad-view-regions`: rectangles in surface-local pixels, `[x, y,
395 /// w, h]`. `None` means the whole window, which is what an app that
396 /// never sends any gets. Outside the rectangles the scroll reaches the
397 /// client untouched — Houdini's parameter editor scrolls, its 3D
398 /// viewports tumble.
399 pub view_regions: Option<Vec<[f64; 4]>>,
400
401 pub tree: *mut ffi::wlr_scene_tree,
402 pub fullscreen_background: *mut ffi::wlr_scene_rect,
403 pub window_background: *mut ffi::wlr_scene_rect,
404 /// scenefx drop shadow, first child of `tree` so it renders beneath
405 /// everything else in the window; null if creation failed (shadow skipped).
406 pub shadow: *mut ffi::wlr_scene_shadow,
407 /// scenefx bevel node: the lit chamfer around the inside of the window's
408 /// edge. Created LAST in the window tree so it draws over the surface —
409 /// the rim overlays the client's outermost pixels. Null if creation
410 /// failed (the effect is then simply absent).
411 pub bevel: *mut ffi::wlr_scene_bevel,
412 /// scenefx droplet node: for droplet-styled status segments, the
413 /// backdrop refracted through the drop's lens. Created BEFORE the
414 /// surfaces so it draws beneath the client's translucent drop. Null if
415 /// creation failed (the effect is then simply absent).
416 pub droplet: *mut ffi::wlr_scene_droplet,
417 pub decorations_below: ffi::wl_list,
418 pub decorations_below_tree: *mut ffi::wlr_scene_tree,
419 pub surfaces: crate::scene::SaveableSurfaces,
420 pub border: BorderRects,
421 /// The border zone the pointer is over (set by cursor.rs); that segment
422 /// draws in `hover_color` while set.
423 pub hovered_border_element: Option<BorderElement>,
424 /// The zone the ring was last DRAWN with, so `step_border_fade` can tell
425 /// a hover change from a settled ring. In overview every zone already
426 /// sits at full reveal, so a hover swap moves no reveal value at all —
427 /// and a step keyed on reveal alone never repainted, leaving the shader
428 /// on whatever zone the last unrelated commit happened to push. The
429 /// highlight lagged one hover behind: "the wrong handle lights up".
430 pub border_hover_drawn: Option<BorderElement>,
431 /// Per-zone reveal factor, 0.0 (fully hidden) to 1.0 (fully drawn),
432 /// indexed by `BorderElement::index`. Borders rest invisible and only the
433 /// zone under the pointer fades in. Deliberately NOT part of
434 /// `rendering_requested.border`, which the arrange pass rewrites wholesale
435 /// every pass and would otherwise clobber.
436 pub border_reveal: [f32; 8],
437 /// How far this window is dimmed for lying OVER the adjust target, 0.0
438 /// (full opacity) to 1.0 (`border.overlap_opacity`): a Floating window
439 /// overlapping the window whose handles are up would hide them, so it
440 /// eases down while the mode is on and back up when it ends. Stepped by
441 /// `step_adjust_dim` on the border-fade timer; applied through
442 /// `effective_opacity`.
443 pub adjust_dim: f32,
444 /// The map/close fade, 0.0 (invisible) to 1.0 (fully drawn). A window
445 /// starts at 0 when it maps and eases to 1; a client that asks to close
446 /// (`fade-out` on the control socket) eases it back to 0 and then exits.
447 /// Applied through `effective_opacity`, so it MULTIPLIES the arrange
448 /// pass's own opacity and the adjust-mode dim rather than fighting them.
449 /// Stepped by `step_map_fade` on the border-fade timer.
450 pub map_fade: f32,
451 /// Where `map_fade` is easing to: 1.0 while the window lives, 0.0 once a
452 /// close fade has been asked for.
453 pub map_fade_target: f32,
454 /// Linear per-tick step for `map_fade`, derived from the configured
455 /// duration at the moment the fade starts. Linear, not the borders'
456 /// exponential approach: an exponential close fade never actually
457 /// reaches zero, and the client is waiting on a deadline to exit.
458 pub map_fade_step: f32,
459 pub decorations_above: ffi::wl_list,
460 pub decorations_above_tree: *mut ffi::wlr_scene_tree,
461 pub popup_tree: *mut ffi::wlr_scene_tree,
462 pub capture_scene: *mut ffi::wlr_scene,
463 pub capture_source: *mut ffi::wlr_ext_image_capture_source_v1,
464 pub tiling_mode: crate::tiling::TilingMode,
465 pub mode_locked: bool,
466 pub is_new: bool,
467 pub restored: bool,
468 /// Position was decided at map time rather than by history: a one-shot
469 /// `place-next` hint (widget-spawned picker opening at its control) or a
470 /// view-centered session modal. Either way it suppresses the spawn
471 /// viewport pan — the window is already where the user is looking.
472 pub hint_placed: bool,
473 /// A view-centering that ran before the window's real size was known and
474 /// must be redone once it lands. Only self-sizing modals set it: their
475 /// geometry arrives on a commit, well after `map()`, so the centering at
476 /// map sees `mapped_size_hint`'s fallback and misses by half the
477 /// difference between that and the truth.
478 pub pending_view_center: bool,
479 /// True only when the restored geometry came out of the startup restore queue
480 /// (`state.json`'s window list). A window reopened later in the session matches
481 /// `last_window_states` instead and leaves this false, so it still counts as a
482 /// fresh spawn for `center_on_spawn`.
483 pub session_restored: bool,
484 pub restored_focused: bool,
485 pub closed: bool,
486 /// Set when the compositor asks this window to close, so `unmap` can tell
487 /// a departure someone requested from a client that simply vanished.
488 pub close_requested: bool,
489 pub has_parent: bool,
490 pub minimized: bool,
491 /// While Some, the window is mid fullscreen-toggle: its on-screen rect is
492 /// this box, eased toward the arranged geometry by `step_fs_anim` on the
493 /// border-fade tick. `render_finish` draws at this rect (position, buffer
494 /// stretch, backdrop, clip) instead of the settled geometry.
495 pub fs_anim: Option<FsAnim>,
496 /// Mode and lock this window had when a `SetWindowMode` made it
497 /// Fullscreen; the policy's fullscreen toggle restores both on exit.
498 /// Cleared by any `SetWindowMode` to another mode.
499 pub pre_fullscreen: Option<(crate::tiling::TilingMode, bool)>,
500 pub circular: bool,
501 pub blur: bool,
502 pub scale: f64,
503 pub last_applied_scale: f64,
504 pub virtual_x: f64,
505 pub virtual_y: f64,
506 pub resize_start_vx: f64,
507 pub resize_start_vy: f64,
508 pub resize_start_w: u32,
509 pub resize_start_h: u32,
510 pub resize_edges: Option<Edges>,
511 /// Client hint: an in-surface popover (menu/dropdown) covers this rect,
512 /// surface-local logical px (zcce set_popover_region). The overview
513 /// resize ring is clipped away beneath it and its band does not grab
514 /// there — the menu reads as in front of the chrome.
515 pub popover_region: Option<ffi::wlr_box>,
516 /// The client resized itself and the new-size buffer is already on screen, so
517 /// `render_finish` must take the size from the live commit rather than the
518 /// render-start snapshot (`rendering_sent`), which still holds the previous
519 /// size and would snap the border back. Cleared once consumed.
520 pub self_resized: bool,
521 /// Status segments: the along-bar length last seen while the segment was
522 /// at bar thickness. Feeds WindowSnapshot::status_collapsed_len so an
523 /// EXPANDED segment (surface grown into an in-surface menu) keeps its
524 /// frozen slot in the arrange pass.
525 pub status_collapsed_len: i32,
526 /// Set by the commit listener, cleared by the window-manager stream
527 /// timer after a capture: the damage gate for `stream_server` frames.
528 /// Starts true so a fresh subscriber gets an immediate first frame.
529 pub stream_dirty: bool,
530 /// Surface size at the last commit of a status segment, so
531 /// `handle_window_commit` re-arranges only when the segment actually
532 /// changed size rather than on every content refresh.
533 pub status_commit_size: (i32, i32),
534 pub commit: ffi::wl_listener,
535 pub was_fullscreen: bool,
536 pub saved_width: i32,
537 pub saved_height: i32,
538 pub saved_virtual_x: f64,
539 pub saved_virtual_y: f64,
540 pub was_tiled: bool,
541 /// Declared the desktop-grid layer via zcce_toplevel_v1.set_grid (the
542 /// app_id "cce-grid" convention also maps the role; the flag makes the
543 /// declaration explicit and app_id-independent).
544 pub grid_declared: bool,
545 /// Grid windows: patch sent to the client, awaiting ack_grid_patch.
546 pub grid_patch_pending: Option<(u32, crate::policy::api::GridPatch)>,
547 /// Acked patch awaiting the client's next commit (the rendered buffer).
548 pub grid_patch_acked: Option<(u32, crate::policy::api::GridPatch)>,
549 /// The patch the CURRENT buffer covers — what arrange anchors to.
550 pub grid_patch_current: Option<crate::policy::api::GridPatch>,
551 pub grid_patch_serial: u32,
552 /// The current patch was rendered under a style config that has since
553 /// changed (reload, or a `layout` change to the desktop keys): re-issue
554 /// it on the next arrange even though its coverage is still fine. See
555 /// `WindowManager::invalidate_grid_patches`.
556 pub grid_patch_stale: bool,
557 /// The patch last issued was sized for a camera FLIGHT's destination —
558 /// small enough for the client to render before the ramp lands, not the
559 /// roomy cap-filling rect a resting camera wants for pan headroom. Once
560 /// the camera is at rest with that patch latched, `update_grid_patches`
561 /// re-issues the roomy one and clears this.
562 pub grid_patch_flight: bool,
563 pub saved_floating_width: i32,
564 pub saved_floating_height: i32,
565 pub saved_floating_virtual_x: f64,
566 pub saved_floating_virtual_y: f64,
567
568 pub wm_scheduled: WmScheduledState,
569 pub wm_sent: WmSentState,
570 pub wm_requested: WmRequestedState,
571 pub configure_scheduled: Configure,
572 pub configure_sent: Configure,
573 pub rendering_scheduled: WindowRenderingScheduled,
574 pub rendering_sent: WindowRenderingSent,
575 pub rendering_requested: WindowRenderingRequested,
576 pub box_geom: ffi::wlr_box,
577 pub margin_x: i32,
578 pub margin_y: i32,
579 pub last_decor_w: i32,
580 pub last_decor_h: i32,
581 pub foreign_toplevel_handle: *mut ffi::wlr_ext_foreign_toplevel_handle_v1,
582 pub wlr_toplevel_handle: *mut ffi::wlr_foreign_toplevel_handle_v1,
583 pub csd_buffer_size_bug: bool,
584 pub status_edge: StatusEdge,
585 }
586
587 pub use crate::policy::arrange::StatusEdge;
588
589 impl Window {
590 pub unsafe fn is_wine(&self) -> bool {
591 false
592 }
593
594 /// The `surface { shadow tiled=false }` switch: a Tiled window (which is
595 /// also what Maximized resolves to) casts no drop shadow when it is off.
596 /// Floating, popup and every other mode are unaffected. Evaluated on both
597 /// render paths, so a float/tile toggle restyles on the next arrange.
598 pub unsafe fn wants_tiled_shadow(&self) -> bool {
599 (*self.server).wm.layout.shadow_tiled
600 || self.tiling_mode != crate::tiling::TilingMode::Tiled
601 }
602
603 pub unsafe fn is_fullscreen(&self) -> bool {
604 self.tiling_mode == crate::tiling::TilingMode::Fullscreen
605 || !self.wm_requested.fullscreen.is_null()
606 }
607
608 pub unsafe fn role(&self) -> crate::policy::api::WindowRole {
609 if self.grid_declared {
610 return crate::policy::api::WindowRole::Grid;
611 }
612 // Borrowed, not `get_app_id_string()`: this runs several times per
613 // pointer-motion event (`is_status_bar`/`is_grid`/`is_wallpaper` in
614 // the cursor passthrough) and per window per transaction, and each
615 // call used to heap-allocate a String just to prefix-match it.
616 let ptr = self.get_app_id();
617 let app_id = if ptr.is_null() { None } else { std::ffi::CStr::from_ptr(ptr).to_str().ok() };
618 crate::policy::api::WindowRole::from_app_id(app_id)
619 }
620
621 pub unsafe fn is_grid(&self) -> bool {
622 self.role() == crate::policy::api::WindowRole::Grid
623 }
624
625 pub unsafe fn is_status_bar(&self) -> bool {
626 self.role() == crate::policy::api::WindowRole::StatusBar
627 }
628
629 pub unsafe fn is_wallpaper(&self) -> bool {
630 self.role() == crate::policy::api::WindowRole::Background
631 }
632
633 pub unsafe fn is_linked(&self) -> bool {
634 let prev = self.node.link.prev;
635 let next = self.node.link.next;
636 if prev.is_null() || next.is_null() {
637 return false;
638 }
639 let self_ptr = &self.node.link as *const ffi::wl_list as *mut ffi::wl_list;
640 prev != self_ptr
641 }
642
643
644 pub unsafe fn create(impl_type: WindowImpl, server: *mut Server) -> Result<*mut Self, &'static str> {
645 let hidden_tree = (*server).scene.hidden_tree;
646 let tree = ffi::wlr_scene_tree_create(hidden_tree);
647 if tree.is_null() {
648 return Err("Failed to create tree");
649 }
650
651 let popup_tree = ffi::wlr_scene_tree_create(hidden_tree);
652 if popup_tree.is_null() {
653 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
654 return Err("Failed to create popup_tree");
655 }
656
657 let capture_scene = ffi::wlr_scene_create();
658 if capture_scene.is_null() {
659 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
660 ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
661 return Err("Failed to create capture_scene");
662 }
663 // SceneFX 0.4 does not support restack_xwayland_surfaces
664 // (*capture_scene).restack_xwayland_surfaces = false;
665
666 // Created first so it is the bottom-most child: the cast shadow must render
667 // beneath the (translucent) window content and its backgrounds. Geometry and
668 // color are synced per-frame in update_shadow; a null pointer just disables
669 // the effect rather than failing window creation.
670 let shadow_color = [0.0f32, 0.0f32, 0.0f32, 0.55f32];
671 let shadow = ffi::wlr_scene_shadow_create(tree, 0, 0, 0, 22.0, shadow_color.as_ptr());
672 if !shadow.is_null() {
673 ffi::wlr_scene_node_set_enabled(&mut (*shadow).node, false);
674 }
675
676 // Beneath the surfaces like the shadow: the refracted backdrop must
677 // render under the client's translucent drop, not over it. Synced in
678 // update_droplet; enabled only for droplet-styled status segments.
679 let droplet = ffi::wlr_scene_droplet_create(tree, 0, 0);
680 if !droplet.is_null() {
681 ffi::wlr_scene_node_set_enabled(&mut (*droplet).node, false);
682 }
683
684 let black_color = [0.0f32, 0.0f32, 0.0f32, 1.0f32];
685 let fullscreen_background = ffi::wlr_scene_rect_create(tree, 0, 0, black_color.as_ptr());
686 if fullscreen_background.is_null() {
687 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
688 ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
689 ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
690 return Err("Failed to create fullscreen rect");
691 }
692
693 let decorations_below_tree = ffi::wlr_scene_tree_create(tree);
694
695 let clear_color = [0.0f32, 0.0f32, 0.0f32, 0.0f32];
696 let window_background = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
697 if window_background.is_null() {
698 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
699 ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
700 ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
701 return Err("Failed to create window background rect");
702 }
703
704 let surfaces = match crate::scene::SaveableSurfaces::init(tree) {
705 Ok(s) => s,
706 Err(e) => {
707 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
708 ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
709 ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
710 return Err(e);
711 }
712 };
713
714 // Created after the surfaces so it is ABOVE them in the window tree:
715 // the bevel is an inner rim drawn over the client's outermost pixels,
716 // not something tucked behind them. Geometry, light and colour are
717 // synced per frame in update_bevel; a null pointer disables the
718 // effect rather than failing window creation.
719 let bevel_color = [1.0f32, 1.0f32, 1.0f32, 1.0f32];
720 let bevel = ffi::wlr_scene_bevel_create(tree, 0, 0, 0, 0.0, bevel_color.as_ptr());
721 if !bevel.is_null() {
722 ffi::wlr_scene_node_set_enabled(&mut (*bevel).node, false);
723 }
724
725 // The invisible hit catchers stay in the window tree so pointer
726 // hit-testing and z-order are unchanged. The visible segments live in
727 // a sibling tree parented to the global border overlay layer, so a
728 // revealed edge draws over the neighbouring window it overhangs.
729 let border_left = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
730 let border_right = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
731 let border_top = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
732 let border_bottom = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
733
734 let border_tree = ffi::wlr_scene_tree_create((*server).scene.layers.border_overlay);
735 if border_tree.is_null() {
736 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
737 ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
738 ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
739 return Err("Failed to create window border tree");
740 }
741 let mut border_segments = [std::ptr::null_mut(); 8];
742 for seg in border_segments.iter_mut() {
743 *seg = ffi::wlr_scene_rect_create(border_tree, 0, 0, clear_color.as_ptr());
744 }
745 // The resize handles: one node draws all eight discs (scenefx
746 // frame.frag). Not eight rounded scene rects, because a scene rect
747 // takes the renderer's global corner shape — a squircle — so a rect
748 // with radius half its size would not be a circle. The eight rects
749 // above are the discs' invisible hit catchers.
750 let border_frame = ffi::wlr_scene_frame_create(border_tree, 0, 0, 0, clear_color.as_ptr());
751
752 let decorations_above_tree = ffi::wlr_scene_tree_create(tree);
753
754 let mut window = Box::new(Window {
755 view_regions: None,
756 ref_key: crate::slotmap::Key { generation: 0, index: 0 },
757 server,
758 object: std::ptr::null_mut(),
759 node: std::mem::zeroed(),
760 state: WindowState::Init,
761 impl_type,
762 tree,
763 fullscreen_background,
764 window_background,
765 shadow,
766 bevel,
767 droplet,
768 decorations_below: std::mem::zeroed(),
769 decorations_below_tree,
770 surfaces,
771 border: BorderRects {
772 left: border_left,
773 right: border_right,
774 top: border_top,
775 bottom: border_bottom,
776 segments: border_segments,
777 frame: border_frame,
778 tree: border_tree,
779 },
780 hovered_border_element: None,
781 border_hover_drawn: None,
782 border_reveal: [0.0; 8],
783 adjust_dim: 0.0,
784 // 1.0, not 0.0: a window only starts its fade in `map()`, and
785 // one that never fades (fading disabled, a status segment) must
786 // render at full strength from its first frame.
787 map_fade: 1.0,
788 map_fade_target: 1.0,
789 map_fade_step: 1.0,
790 decorations_above: std::mem::zeroed(),
791 decorations_above_tree,
792 popup_tree,
793 capture_scene,
794 capture_source: std::ptr::null_mut(),
795 tiling_mode: crate::tiling::TilingMode::Floating,
796 mode_locked: false,
797 is_new: true,
798 restored: false,
799 hint_placed: false,
800 pending_view_center: false,
801 session_restored: false,
802 restored_focused: false,
803 closed: false,
804 close_requested: false,
805 has_parent: false,
806 minimized: false,
807 fs_anim: None,
808 pre_fullscreen: None,
809 circular: false,
810 blur: false,
811 scale: 1.0,
812 last_applied_scale: 1.0,
813 virtual_x: unsafe { (*server).wm.desk_pan_x + 100.0 },
814 virtual_y: unsafe { (*server).wm.desk_pan_y + 100.0 },
815 resize_start_vx: 0.0,
816 resize_start_vy: 0.0,
817 resize_start_w: 0,
818 resize_start_h: 0,
819 resize_edges: None,
820 popover_region: None,
821 self_resized: false,
822 status_collapsed_len: 0,
823 stream_dirty: true,
824 status_commit_size: (0, 0),
825 commit: std::mem::zeroed(),
826 was_fullscreen: false,
827 saved_width: 0,
828 saved_height: 0,
829 saved_virtual_x: 0.0,
830 saved_virtual_y: 0.0,
831 was_tiled: false,
832 grid_declared: false,
833 grid_patch_pending: None,
834 grid_patch_acked: None,
835 grid_patch_current: None,
836 grid_patch_serial: 0,
837 grid_patch_stale: false,
838 grid_patch_flight: false,
839 saved_floating_width: 0,
840 saved_floating_height: 0,
841 saved_floating_virtual_x: 0.0,
842 saved_floating_virtual_y: 0.0,
843 wm_scheduled: WmScheduledState {
844 dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
845 decoration_hint: ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
846 show_window_menu_requested: None,
847 fullscreen_requested: FullscreenRequest::NoRequest,
848 maximize_requested: MaximizeRequest::NoRequest,
849 minimize_requested: false,
850 dirty_app_id: false,
851 dirty_title: false,
852 pointer_move_requested: std::ptr::null_mut(),
853 pointer_resize_requested: None,
854 },
855 wm_sent: WmSentState {
856 dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
857 decoration_hint: ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
858 parent: None,
859 },
860 wm_requested: WmRequestedState {
861 dimensions: None,
862 bounds: Dimensions { width: 0, height: 0 },
863 ssd: false,
864 tiled: 0,
865 capabilities: 1 | 2 | 4 | 8,
866 resizing: false,
867 maximized: false,
868 fullscreen: std::ptr::null_mut(),
869 inform_fullscreen: false,
870 close: false,
871 },
872 configure_scheduled: Configure::new(),
873 configure_sent: Configure::new(),
874 rendering_scheduled: WindowRenderingScheduled {
875 width: 0,
876 height: 0,
877 resend_dimensions: false,
878 },
879 rendering_sent: WindowRenderingSent {
880 width: 0,
881 height: 0,
882 presentation_hint: ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC,
883 },
884 rendering_requested: WindowRenderingRequested {
885 x: 0,
886 y: 0,
887 hidden: false,
888 border: Border::none(),
889 clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
890 content_clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
891 opacity: 1.0f32,
892 circular: false,
893 blur: false,
894 },
895 box_geom: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
896 margin_x: 0,
897 margin_y: 0,
898 last_decor_w: 0,
899 last_decor_h: 0,
900 foreign_toplevel_handle: std::ptr::null_mut(),
901 wlr_toplevel_handle: std::ptr::null_mut(),
902 csd_buffer_size_bug: false,
903 status_edge: StatusEdge::Unspecified,
904 });
905
906 ffi::wl_list_init(&mut window.decorations_below);
907 ffi::wl_list_init(&mut window.decorations_above);
908
909 let raw = Box::into_raw(window);
910 let key = (*(*raw).server).wm.windows.put(raw);
911 (*raw).ref_key = key;
912 (*raw).node.init(crate::wm_node::WmNodeTag::Window);
913
914 ffi::wlr_scene_node_set_enabled(tree as *mut ffi::wlr_scene_node, false);
915 ffi::wlr_scene_node_set_enabled(popup_tree as *mut ffi::wlr_scene_node, false);
916 ffi::wlr_scene_node_set_enabled(fullscreen_background as *mut ffi::wlr_scene_node, false);
917
918 crate::scene_node_data::SceneNodeData::attach(
919 tree as *mut ffi::wlr_scene_node,
920 crate::scene_node_data::SceneNodeDataVal::Window(raw),
921 );
922 crate::scene_node_data::SceneNodeData::attach(
923 popup_tree as *mut ffi::wlr_scene_node,
924 crate::scene_node_data::SceneNodeDataVal::Window(raw),
925 );
926 // The border segments sit outside the window tree; without data of
927 // their own a hit on a revealed segment would resolve to no window at
928 // all, so tag them with the window they belong to.
929 crate::scene_node_data::SceneNodeData::attach(
930 border_tree as *mut ffi::wlr_scene_node,
931 crate::scene_node_data::SceneNodeDataVal::Window(raw),
932 );
933 ffi::wlr_scene_node_set_enabled(border_tree as *mut ffi::wlr_scene_node, false);
934
935 Ok(raw)
936 }
937
938 pub unsafe fn set_impl(&mut self, impl_type: WindowImpl) {
939 self.impl_type = impl_type;
940 }
941
942 pub unsafe fn impl_destroying(&mut self) {
943 self.impl_type = WindowImpl::Destroying;
944 }
945
946 pub unsafe fn get_title(&self) -> *const libc::c_char {
947 match self.impl_type {
948 WindowImpl::Toplevel(toplevel) => {
949 if toplevel.is_null() {
950 std::ptr::null()
951 } else {
952 ffi::river_wlr_xdg_toplevel_get_title((*toplevel).wlr_toplevel)
953 }
954 }
955 WindowImpl::Xwayland(xwindow) => {
956 if xwindow.is_null() {
957 std::ptr::null()
958 } else {
959 (*(*xwindow).xsurface).title
960 }
961 }
962 WindowImpl::Destroying => std::ptr::null(),
963 }
964 }
965
966 pub unsafe fn get_app_id(&self) -> *const libc::c_char {
967 match self.impl_type {
968 WindowImpl::Toplevel(toplevel) => {
969 if toplevel.is_null() {
970 std::ptr::null()
971 } else {
972 ffi::river_wlr_xdg_toplevel_get_app_id((*toplevel).wlr_toplevel)
973 }
974 }
975 WindowImpl::Xwayland(xwindow) => {
976 if xwindow.is_null() {
977 std::ptr::null()
978 } else {
979 (*(*xwindow).xsurface).class
980 }
981 }
982 WindowImpl::Destroying => std::ptr::null(),
983 }
984 }
985
986 pub unsafe fn get_app_id_string(&self) -> Option<String> {
987 let ptr = self.get_app_id();
988 if ptr.is_null() {
989 None
990 } else {
991 Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned())
992 }
993 }
994
995 pub unsafe fn get_title_string(&self) -> Option<String> {
996 let ptr = self.get_title();
997 if ptr.is_null() {
998 None
999 } else {
1000 Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned())
1001 }
1002 }
1003
1004 /// Dest-size factor for this window's surface buffers on top of the
1005 /// overview zoom: 1/output-scale for an X11 window under
1006 /// `xwayland_hidpi`, whose buffer is physical pixels (see
1007 /// `xwayland_window::x11_scale_for`); 1 for everything else, including
1008 /// an X11 window named in `xwayland_hidpi_except`.
1009 pub unsafe fn x11_buffer_scale(&self) -> f64 {
1010 if let WindowImpl::Xwayland(xwindow) = self.impl_type {
1011 let xsurface = if xwindow.is_null() { std::ptr::null() } else { (*xwindow).xsurface as *const _ };
1012 1.0 / crate::xwayland_window::x11_scale_for(self.server, xsurface) as f64
1013 } else {
1014 1.0
1015 }
1016 }
1017
1018 pub unsafe fn get_parent(&self) -> *mut Window {
1019 match self.impl_type {
1020 WindowImpl::Toplevel(toplevel) => {
1021 if toplevel.is_null() {
1022 std::ptr::null_mut()
1023 } else {
1024 let wlr_parent = ffi::river_wlr_xdg_toplevel_get_parent((*toplevel).wlr_toplevel);
1025 if wlr_parent.is_null() {
1026 std::ptr::null_mut()
1027 } else {
1028 let base = ffi::river_wlr_xdg_toplevel_get_base(wlr_parent);
1029 let parent_xdg = ffi::river_wlr_xdg_surface_get_data(base) as *mut crate::xdg_toplevel::XdgToplevel;
1030 if parent_xdg.is_null() {
1031 std::ptr::null_mut()
1032 } else {
1033 (*parent_xdg).window
1034 }
1035 }
1036 }
1037 }
1038 WindowImpl::Xwayland(xwindow) => {
1039 if xwindow.is_null() {
1040 std::ptr::null_mut()
1041 } else {
1042 let parent_xsurface = (*(*xwindow).xsurface).parent;
1043 if parent_xsurface.is_null() {
1044 std::ptr::null_mut()
1045 } else {
1046 let parent_data = (*parent_xsurface).data;
1047 if parent_data.is_null() {
1048 std::ptr::null_mut()
1049 } else {
1050 let parent_xwindow = parent_data as *mut crate::xwayland_window::XwaylandWindow;
1051 (*parent_xwindow).window
1052 }
1053 }
1054 }
1055 }
1056 WindowImpl::Destroying => std::ptr::null_mut(),
1057 }
1058 }
1059
1060 pub unsafe fn unreliable_pid(&self) -> i32 {
1061 match self.impl_type {
1062 WindowImpl::Toplevel(toplevel) => {
1063 if toplevel.is_null() {
1064 0
1065 } else {
1066 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
1067 let surface = ffi::river_wlr_xdg_surface_get_surface(base);
1068 if surface.is_null() {
1069 0
1070 } else {
1071 let res = ffi::river_wlr_surface_get_resource(surface);
1072 if res.is_null() {
1073 0
1074 } else {
1075 let client = ffi::wl_resource_get_client(res);
1076 if client.is_null() {
1077 0
1078 } else {
1079 let mut pid = 0;
1080 let mut uid = 0;
1081 let mut gid = 0;
1082 ffi::wl_client_get_credentials(client, &mut pid, &mut uid, &mut gid);
1083 pid
1084 }
1085 }
1086 }
1087 }
1088 }
1089 WindowImpl::Xwayland(xwindow) => {
1090 if xwindow.is_null() {
1091 0
1092 } else {
1093 (*(*xwindow).xsurface).pid
1094 }
1095 }
1096 WindowImpl::Destroying => 0,
1097 }
1098 }
1099
1100 /// Overlay-mode UI (cce-cloud menus and the like): takes keyboard input
1101 /// while open, but is invisible to the window manager's notion of "the
1102 /// focused window" — persistence, camera follow, arrange focus styling
1103 /// and refocus rules all look through it to the real window underneath.
1104 pub unsafe fn is_overlay_ui(&self) -> bool {
1105 self.tiling_mode == crate::tiling::TilingMode::Overlay
1106 || self.get_app_id_string().as_deref() == Some("cce-cloud")
1107 }
1108
1109 /// A "shy" X11 window: a top-level that declines input focus
1110 /// (WM_HINTS input = False) and asks to be skipped by the taskbar —
1111 /// what Wine emits for a WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW window.
1112 /// Apps use those as helpers they place themselves: Ubisoft Connect
1113 /// keeps an untitled one exactly behind its borderless main window
1114 /// (the shadow-window trick), where Windows never shows it. Managed
1115 /// like an app window it was restored to a saved spot, pulled on-desk
1116 /// and raised — a blank white window with the app icon, over
1117 /// everything. So it is left to the client: no saved-state restore, its
1118 /// own position honoured at map and on request, never focused, never
1119 /// raised, stacked at the bottom.
1120 pub unsafe fn is_shy(&self) -> bool {
1121 let WindowImpl::Xwayland(xwindow) = self.impl_type else {
1122 return false;
1123 };
1124 if xwindow.is_null() || (*xwindow).xsurface.is_null() {
1125 return false;
1126 }
1127 let xs = (*xwindow).xsurface;
1128 if !(*xs).parent.is_null() || !(*xs).skip_taskbar || (*xs).hints.is_null() {
1129 return false;
1130 }
1131 let hints = (*xs).hints;
1132 let input_flag = ffi::xcb_icccm_wm_t_XCB_ICCCM_WM_HINT_INPUT as i32;
1133 (*hints).flags & input_flag != 0 && (*hints).input == 0
1134 }
1135
1136 pub unsafe fn try_restore(&mut self) {
1137 if self.restored {
1138 return;
1139 }
1140 // No geometry is ever saved for a Utility window, so none may be
1141 // restored over it — a pre-Utility state.json entry for the same
1142 // app_id would otherwise dictate a stale size to a self-sizing
1143 // client. (Belt over suspenders: the arrange pass restates the
1144 // "you choose" 0x0 for Utility anyway, so even a slipped-through
1145 // restore heals on the client's next commit.)
1146 if self.tiling_mode == crate::tiling::TilingMode::Utility {
1147 return;
1148 }
1149 // A transient — an xdg toplevel with a parent, or an X11 window with
1150 // WM_TRANSIENT_FOR — is a dialog of the window it hangs off, and is
1151 // never what a saved entry describes. It shares its app_id with the
1152 // main window, so the app_id-only third pass of the state matchers
1153 // (kept for a relaunched main window whose title has changed) would
1154 // hand it the MAIN window's geometry: Houdini's Preferences opened at
1155 // the full 1856x1141 of the session it belongs to, and hkey's
1156 // "Redeem Result" at the administrator's size. The save pass skips
1157 // transients for the same reason, so there is nothing of their own to
1158 // restore either; they size themselves.
1159 if !self.get_parent().is_null() {
1160 return;
1161 }
1162 // For an X11 window that check is only meaningful once its properties
1163 // are all in: they arrive one PropertyNotify at a time, and WM_CLASS
1164 // (the app_id) lands before WM_TRANSIENT_FOR, so on the app_id notify
1165 // a dialog still looks parentless and the app_id-only match below
1166 // restored it anyway — first match wins, and the restore overwrites
1167 // the client's own requested size, so it cannot be undone when the
1168 // parent turns up. (Waiting for the wl_surface was not enough: GTK's
1169 // dialog was still title-less and parentless at association.) Wait
1170 // for `map`, which calls back in here; by then every property the
1171 // client set before mapping has been read.
1172 if matches!(self.impl_type, WindowImpl::Xwayland(_)) && self.state != WindowState::Mapped {
1173 return;
1174 }
1175 // A full-screen X11 game (`xwayland_hidpi_except`) sizes itself to
1176 // the screen; restoring a saved size onto it is what shrank
1177 // Trackmania to the launcher's 1214x689 — the game then pinned that
1178 // size in its hints and no fullscreen could take. Mark it restored
1179 // so nothing else tries.
1180 if crate::xwayland_window::window_is_hidpi_exempt(self as *const Window) {
1181 log::info!(
1182 "Not restoring saved state for {:?}: named in xwayland_hidpi_except, it places itself",
1183 self.get_title_string().unwrap_or_default()
1184 );
1185 self.restored = true;
1186 return;
1187 }
1188 // A shy helper window (no-activate, skip-taskbar) is placed by its
1189 // app, relative to the app's own windows — see `is_shy`.
1190 if self.is_shy() {
1191 log::info!(
1192 "Not restoring saved state for {:?} ({}): a no-activate helper window, its app places it",
1193 self.get_title_string().unwrap_or_default(),
1194 self.get_app_id_string().unwrap_or_default()
1195 );
1196 self.restored = true;
1197 return;
1198 }
1199 let app_id_str = self.get_app_id_string().unwrap_or_default();
1200 if app_id_str.is_empty()
1201 || app_id_str.starts_with("cce-status")
1202 || app_id_str == "cce-wallpaper"
1203 || app_id_str == "cce-grid"
1204 {
1205 return;
1206 }
1207 let title_str = self.get_title_string().unwrap_or_default();
1208 let mut saved_opt = (*self.server).wm.match_and_remove_restore_state(&app_id_str, &title_str);
1209 let from_session = saved_opt.is_some();
1210 if saved_opt.is_none() {
1211 saved_opt = (*self.server).wm.match_last_window_state(&app_id_str, &title_str);
1212 }
1213 if let Some(saved) = saved_opt {
1214 log::info!("Restoring saved state for window: app_id={}, title={}. Position: ({}, {}), Size: {}x{}", app_id_str, title_str, saved.virtual_x, saved.virtual_y, saved.width, saved.height);
1215 self.tiling_mode = saved.tiling_mode;
1216 // `minimized` is session state, not app memory: a window the
1217 // user just opened must never be born hidden. On a
1218 // `last_window_states` borrow the flag is whatever the sibling
1219 // (or the app's last incarnation) happened to be doing — and a
1220 // parentless dialog matched by app_id alone inherits it from
1221 // the LIVE main window, which the user may well have minimized
1222 // to get it out of the way. Focused, listed, and invisible.
1223 if from_session {
1224 self.minimized = saved.minimized;
1225 }
1226 self.virtual_x = saved.virtual_x;
1227 self.virtual_y = saved.virtual_y;
1228 self.scale = saved.scale;
1229 self.box_geom.width = saved.width as i32;
1230 self.box_geom.height = saved.height as i32;
1231
1232 self.wm_requested.dimensions = Some(crate::window::Dimensions {
1233 width: saved.width,
1234 height: saved.height,
1235 });
1236 self.wm_requested.bounds = crate::window::Dimensions {
1237 width: saved.width,
1238 height: saved.height,
1239 };
1240
1241 self.rendering_scheduled.width = saved.width;
1242 self.rendering_scheduled.height = saved.height;
1243 self.rendering_sent.width = saved.width;
1244 self.rendering_sent.height = saved.height;
1245
1246 match self.impl_type {
1247 WindowImpl::Toplevel(toplevel) => {
1248 if !toplevel.is_null() {
1249 (*toplevel).geometry.width = saved.width as i32;
1250 (*toplevel).geometry.height = saved.height as i32;
1251 }
1252 }
1253 WindowImpl::Xwayland(xwindow) => {
1254 // This pre-writes the wlroots mirror so `render_finish`
1255 // reports the saved size from the first frame; X itself
1256 // is still at the window's natural size until the
1257 // arrange pass configures it. That configure must not
1258 // be deduplicated against this mirror — see
1259 // `xwayland_window::needs_configure`, which also checks
1260 // the geometry the compositor has actually sent.
1261 if !xwindow.is_null() && !(*xwindow).xsurface.is_null() {
1262 let s = crate::xwayland_window::x11_scale_for(self.server, (*xwindow).xsurface);
1263 (*(*xwindow).xsurface).width = crate::xwayland_window::to_x11(saved.width as i32, s) as u16;
1264 (*(*xwindow).xsurface).height = crate::xwayland_window::to_x11(saved.height as i32, s) as u16;
1265 }
1266 }
1267 _ => {}
1268 }
1269
1270 // A restored non-Floating mode is EXPLICIT state, and has to be
1271 // latched to survive. `get_mode_for_window` returns the window's own
1272 // mode only when `mode_locked`; unlocked, it resolves from the config
1273 // rules and falls through to Floating — and the arrange pass writes
1274 // that resolution straight back into `tiling_mode`
1275 // (`window_manager.rs`, the `wp.tiling_mode` apply). So a window
1276 // restored Tiled but unlocked was demoted by the very next arrange,
1277 // which is why a relaunched app came back floating however exactly
1278 // its geometry had been restored: position, size and cell were all
1279 // right, and the mode was gone before the first frame.
1280 //
1281 // Both sibling promotions already pair the mode with the lock — the
1282 // seat's op_end detection, and the geometric one just below, which is
1283 // why a window saved Floating-but-aligned survived while one saved
1284 // Tiled did not. Only Floating is left unlatched here, so a window
1285 // with no explicit mode still resolves from the rules as before.
1286 if saved.tiling_mode != crate::tiling::TilingMode::Floating {
1287 self.mode_locked = true;
1288 }
1289
1290 // Geometric promotion at restore time: a window whose saved
1291 // geometry sits cell-aligned IS tiled, even if an older session
1292 // saved it as Floating (pre-rework state, or a session that
1293 // never touched it after it landed on the grid). Same test and
1294 // lock as the op_end detection. No demotion here — a saved
1295 // Tiled window off the current grid is re-snapped by the Tiled
1296 // arrange arm instead.
1297 if self.tiling_mode == crate::tiling::TilingMode::Floating {
1298 let sp = (*self.server).wm.layout.snap_params();
1299 if crate::policy::snap::is_cell_aligned(
1300 self.virtual_x,
1301 self.virtual_y,
1302 saved.width as f64,
1303 saved.height as f64,
1304 &sp,
1305 1.0,
1306 ) {
1307 self.tiling_mode = crate::tiling::TilingMode::Tiled;
1308 self.mode_locked = true;
1309 }
1310 }
1311
1312 // A remembered FLOATING position is only worth keeping if it is
1313 // where the user can see it. The camera at restore is wherever
1314 // the session left it (or wherever the user has panned since a
1315 // relaunch), and a floating window a screen away from that is
1316 // lost, not remembered: Inkscape's start screen came back a full
1317 // viewport above the desk every login, at the cell its previous
1318 // incarnation had been saved in, with nothing on screen to say
1319 // it existed. Tiled windows are the grid's and stay put.
1320 //
1321 // Unless it is on the tiled desk: a window within a viewport of
1322 // the tiled windows' bounding box (`tiled_desk_bounds`, the
1323 // session's tiled entries still to restore plus the tiled
1324 // windows already up) is placed beside content the user pans
1325 // along, and stays where it was put — cce-data-editor parked
1326 // left of the first column came back mid-view every login.
1327 if self.tiling_mode == crate::tiling::TilingMode::Floating && !self.minimized {
1328 let (_, _, vp_w, vp_h) = self.first_enabled_output_box();
1329 let wm = &(*self.server).wm;
1330 let cam = crate::policy::camera::Camera {
1331 pan_x: wm.desk_pan_x,
1332 pan_y: wm.desk_pan_y,
1333 zoom: wm.desk_zoom,
1334 };
1335 let desk = wm.tiled_desk_bounds();
1336 if let Some((nx, ny)) = crate::policy::camera::recalled_origin(
1337 self.virtual_x,
1338 self.virtual_y,
1339 saved.width as f64,
1340 saved.height as f64,
1341 cam,
1342 vp_w,
1343 vp_h,
1344 desk,
1345 ) {
1346 log::info!(
1347 "Recalling off-view floating window into view: app_id={} remembered=({:.0},{:.0}) -> ({:.0},{:.0}) (tiled desk: {:?})",
1348 app_id_str, self.virtual_x, self.virtual_y, nx, ny, desk
1349 );
1350 self.virtual_x = nx;
1351 self.virtual_y = ny;
1352 }
1353 }
1354
1355 // A borrowed origin is a live sibling's origin whenever the
1356 // app_id-only pass matched a window of an app that is still
1357 // running: a parentless dialog (1Password's CLI "Authorize"
1358 // prompt, a second browser window) lands exactly on the main
1359 // window's top-left corner, where it reads as part of that
1360 // window rather than a new one. Cascade it off any mapped
1361 // sibling already sitting there, the way every stacking WM
1362 // offsets a new window from the last. Session entries are
1363 // exempt: a restored layout is where the user left it.
1364 if !from_session && self.tiling_mode == crate::tiling::TilingMode::Floating {
1365 let (nx, ny) = self.cascade_off_siblings(&app_id_str, self.virtual_x, self.virtual_y);
1366 if (nx, ny) != (self.virtual_x, self.virtual_y) {
1367 log::info!(
1368 "Cascading new {} window off a sibling at ({:.0},{:.0}) -> ({:.0},{:.0})",
1369 app_id_str, self.virtual_x, self.virtual_y, nx, ny
1370 );
1371 self.virtual_x = nx;
1372 self.virtual_y = ny;
1373 }
1374 }
1375
1376 self.restored = true;
1377 self.session_restored = from_session;
1378 // The saved `focused` flag only means something for the startup
1379 // restore queue; on a `last_window_states` borrow it is stale
1380 // (whether the app happened to be focused when last closed) and
1381 // must not feed the settle-phase focus gates.
1382 self.restored_focused = from_session && saved.focused;
1383 }
1384 }
1385
1386 /// Step an origin diagonally until no mapped sibling of `app_id` (any
1387 /// window but this one) has its top-left within a few pixels of it.
1388 /// Bounded, so a pathological pile of siblings cannot walk a window off
1389 /// the desk: after `MAX_STEPS` the last candidate is taken as is.
1390 unsafe fn cascade_off_siblings(&self, app_id: &str, x: f64, y: f64) -> (f64, f64) {
1391 const STEP: f64 = 40.0;
1392 const NEAR: f64 = 4.0;
1393 const MAX_STEPS: usize = 8;
1394 let me = self as *const Window;
1395 let origins: Vec<(f64, f64)> = (*self.server)
1396 .wm
1397 .windows
1398 .iter()
1399 .copied()
1400 .filter(|&w| !w.is_null() && w as *const Window != me && !(*w).closed)
1401 .filter(|&w| matches!((*w).state, WindowState::Mapped))
1402 .filter(|&w| (*w).get_app_id_string().as_deref() == Some(app_id))
1403 .map(|w| ((*w).virtual_x, (*w).virtual_y))
1404 .collect();
1405 let taken = |cx: f64, cy: f64| {
1406 origins
1407 .iter()
1408 .any(|&(ox, oy)| (ox - cx).abs() <= NEAR && (oy - cy).abs() <= NEAR)
1409 };
1410 let (mut cx, mut cy) = (x, y);
1411 for _ in 0..MAX_STEPS {
1412 if !taken(cx, cy) {
1413 break;
1414 }
1415 cx += STEP;
1416 cy += STEP;
1417 }
1418 (cx, cy)
1419 }
1420
1421 /// Apply a one-shot `place-next` hint: land the window's top-left just
1422 /// below-right of the hinted layout position (the control that spawned
1423 /// it), clamped to the output so it stays fully on-screen. Runs after
1424 /// `try_restore` so the remembered SIZE is kept — only the position is
1425 /// overridden — and marks `hint_placed` so the spawn viewport pan is
1426 /// skipped (the window is already under the user's pointer).
1427 /// Layout box of the first enabled output — `(phys_x, phys_y, width,
1428 /// height)`, the viewport every placement decision is measured against.
1429 /// Falls back to a 1920x1080 box at the origin before any output is up.
1430 unsafe fn first_enabled_output_box(&self) -> (f64, f64, f64, f64) {
1431 let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
1432 let mut curr_out = (*outputs_list).next;
1433 while curr_out != outputs_list {
1434 let output = crate::container_of!(curr_out, crate::output::Output, link);
1435 if (*output).sent.state == crate::output::OutputStateValue::Enabled {
1436 let b = (*output).sent.box_layout();
1437 return (b.x as f64, b.y as f64, b.width as f64, b.height as f64);
1438 }
1439 curr_out = (*curr_out).next;
1440 }
1441 (0.0, 0.0, 1920.0, 1080.0)
1442 }
1443
1444 /// Virtual position to layout (screen) position, ROUNDED — the same
1445 /// conversion the arrange pass makes (`PlacementCtx::virtual_to_screen`).
1446 /// Every writer of a window's screen origin has to agree on the
1447 /// rounding: the seat op and the resize-commit anchoring truncated while
1448 /// the arrange pass rounds, so whenever the fractional part was .5 or
1449 /// more the window stepped a pixel back and forth between a commit and
1450 /// the next arrange — a twitch on every resize step at overview zoom,
1451 /// and a one-pixel hop on grab and release.
1452 pub unsafe fn virtual_to_screen(&self, vx: f64, vy: f64) -> (i32, i32) {
1453 let wm = &(*self.server).wm;
1454 let (cam, _, _) = wm.layout_camera();
1455 let (out_x, out_y, _, _) = self.first_enabled_output_box();
1456 (
1457 out_x as i32 + ((vx - cam.pan_x) * cam.zoom).round() as i32,
1458 out_y as i32 + ((vy - cam.pan_y) * cam.zoom).round() as i32,
1459 )
1460 }
1461
1462 /// Layout (screen) position back to a virtual position — the inverse of
1463 /// `virtual_to_screen`. A client that repositions itself hands us a
1464 /// SCREEN origin, but the arrange pass places a floating window from its
1465 /// VIRTUAL one, so a screen origin written on its own survives exactly
1466 /// until the next transaction and is then recomputed away.
1467 pub unsafe fn screen_to_virtual(&self, sx: i32, sy: i32) -> (f64, f64) {
1468 let wm = &(*self.server).wm;
1469 let (cam, _, _) = wm.layout_camera();
1470 let zoom = cam.zoom.max(0.01);
1471 let (out_x, out_y, _, _) = self.first_enabled_output_box();
1472 (
1473 cam.pan_x + (sx as f64 - out_x) / zoom,
1474 cam.pan_y + (sy as f64 - out_y) / zoom,
1475 )
1476 }
1477
1478 /// Best-known window size in VIRTUAL units at map time. `box_geom` is the
1479 /// render pass's size and is only filled in once a frame has been drawn
1480 /// (or by `try_restore` from the saved geometry), so a first-ever launch
1481 /// falls back to the client's committed toplevel geometry.
1482 unsafe fn mapped_size_hint(&self) -> (f64, f64) {
1483 if self.box_geom.width > 0 && self.box_geom.height > 0 {
1484 return (self.box_geom.width as f64, self.box_geom.height as f64);
1485 }
1486 if let WindowImpl::Toplevel(toplevel) = self.impl_type {
1487 if !toplevel.is_null() {
1488 let g = (*toplevel).geometry;
1489 if g.width > 0 && g.height > 0 {
1490 return (g.width as f64, g.height as f64);
1491 }
1492 }
1493 }
1494 (400.0, 400.0)
1495 }
1496
1497 unsafe fn try_hint_placement(&mut self) {
1498 let app_id = self.get_app_id_string().unwrap_or_default();
1499 if app_id.is_empty() {
1500 return;
1501 }
1502 // Claimed before the mode is judged, so a hint aimed at this window
1503 // does not linger and land on the next one to open.
1504 let Some((hx, hy, cell_anchored)) = (*self.server).wm.take_pending_placement(&app_id)
1505 else {
1506 return;
1507 };
1508 if cell_anchored {
1509 // TILED IS THE POINT here, unlike the position-only hint below: a
1510 // window that reopens filling four squares is exactly the case
1511 // this exists for. Only the modes that do not own a position at
1512 // all are excluded.
1513 if matches!(
1514 self.tiling_mode,
1515 crate::tiling::TilingMode::Fullscreen
1516 | crate::tiling::TilingMode::Popup
1517 | crate::tiling::TilingMode::Overlay
1518 | crate::tiling::TilingMode::Status
1519 ) {
1520 return;
1521 }
1522 self.place_on_invocation_cell(&app_id, hx, hy);
1523 return;
1524 }
1525 // Utility included: the hint moves only the POSITION, which a utility
1526 // window does not own — only its size is the client's.
1527 if !matches!(
1528 self.tiling_mode,
1529 crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Utility
1530 ) {
1531 return;
1532 }
1533
1534 let (phys_x, phys_y, vp_w, vp_h) = self.first_enabled_output_box();
1535
1536 let wm = &(*self.server).wm;
1537 let zoom = wm.desk_zoom.max(0.01);
1538 let (vw, vh) = self.mapped_size_hint();
1539 let (w, h) = (vw * zoom, vh * zoom);
1540
1541 const OFFSET: f64 = 12.0; // context-menu-style drop below-right of the control
1542 const MARGIN: f64 = 8.0;
1543 let sx = (hx + OFFSET)
1544 .min(phys_x + vp_w - w - MARGIN)
1545 .max(phys_x + MARGIN);
1546 let sy = (hy + OFFSET)
1547 .min(phys_y + vp_h - h - MARGIN)
1548 .max(phys_y + MARGIN);
1549
1550 // screen = phys + (virtual - desk_pan) * zoom → invert for virtual.
1551 self.virtual_x = wm.desk_pan_x + (sx - phys_x) / zoom;
1552 self.virtual_y = wm.desk_pan_y + (sy - phys_y) / zoom;
1553 self.hint_placed = true;
1554 log::info!(
1555 "place-next hint applied: app_id={} screen=({:.0},{:.0}) virtual=({:.1},{:.1})",
1556 app_id, sx, sy, self.virtual_x, self.virtual_y
1557 );
1558 }
1559
1560 /// Step a freshly-spawned TILED window off any tiled window it would open
1561 /// on top of, keeping its size and staying as close to its intended spot
1562 /// as possible (`policy::spawn::nearest_free`).
1563 ///
1564 /// The remembered-position path has no idea whether that position is still
1565 /// free — it was when the window closed, and something else may have taken
1566 /// it since. Two tiled windows stacked on the same squares is never what
1567 /// was meant: tiled windows are the ones laid out to sit side by side.
1568 ///
1569 /// Deliberately narrow:
1570 /// - Only TILED windows are moved, and only tiled windows count as
1571 /// obstacles. Floating windows overlap by nature; that is the difference
1572 /// between the two modes, not a fault to correct.
1573 /// - Session restore is exempt. A restored layout is a layout the user
1574 /// arranged and saved, and mapping order is arbitrary, so nudging there
1575 /// would rearrange a deliberate desktop at every login.
1576 unsafe fn avoid_tiled_overlap(&mut self) {
1577 if self.session_restored || self.tiling_mode != crate::tiling::TilingMode::Tiled {
1578 return;
1579 }
1580 let wm = &(*self.server).wm;
1581 let sp = wm.layout.snap_params();
1582 if sp.cell_w <= 0.5 || sp.cell_h <= 0.5 {
1583 return;
1584 }
1585 let (vw, vh) = self.mapped_size_hint();
1586 if vw <= 0.0 || vh <= 0.0 {
1587 return;
1588 }
1589 let (c0, r0, c1, r1) = crate::policy::cells::window_span(
1590 self.virtual_x, self.virtual_y, vw, vh, sp.cell_w, sp.cell_h, sp.gap_width,
1591 );
1592 let want = crate::policy::spawn::CellBlock::new(c0, r0, c1, r1);
1593
1594 let mut occupied = Vec::new();
1595 for &w in wm.windows.iter() {
1596 if w.is_null() || w == (self as *mut Window) || (*w).closed || (*w).minimized {
1597 continue;
1598 }
1599 if !matches!((*w).state, WindowState::Mapped) {
1600 continue;
1601 }
1602 if (*w).tiling_mode != crate::tiling::TilingMode::Tiled {
1603 continue;
1604 }
1605 let (ow, oh) = ((*w).box_geom.width as f64, (*w).box_geom.height as f64);
1606 if ow <= 0.0 || oh <= 0.0 {
1607 continue;
1608 }
1609 let (oc0, or0, oc1, or1) = crate::policy::cells::window_span(
1610 (*w).virtual_x, (*w).virtual_y, ow, oh, sp.cell_w, sp.cell_h, sp.gap_width,
1611 );
1612 occupied.push(crate::policy::spawn::CellBlock::new(oc0, or0, oc1, or1));
1613 }
1614 if occupied.is_empty() {
1615 return;
1616 }
1617
1618 // Bounded: a window that cannot find room nearby stays put rather than
1619 // being flung to an empty region of a desktop that has no edges.
1620 const SEARCH_SQUARES: i32 = 12;
1621 let free = crate::policy::spawn::nearest_free(want, &occupied, SEARCH_SQUARES);
1622 if free == want {
1623 return;
1624 }
1625 let (bx, by, _, _) = crate::policy::cells::block_rect(
1626 free.col0, free.row0, free.col1, free.row1,
1627 sp.cell_w, sp.cell_h, sp.gap_width, sp.cell_inset,
1628 );
1629 log::info!(
1630 "spawn overlap: {} would open on a tiled window at {} -> moved to {}",
1631 self.get_app_id_string().unwrap_or_default(),
1632 crate::policy::cells::span_label(want.col0, want.row0, want.col1, want.row1),
1633 crate::policy::cells::span_label(free.col0, free.row0, free.col1, free.row1),
1634 );
1635 self.virtual_x = bx;
1636 self.virtual_y = by;
1637 }
1638
1639 /// Place this window on the grid square the user invoked it from, keeping
1640 /// its remembered SIZE and growing away from the windows already there
1641 /// (`policy::spawn::place_at_cell`).
1642 ///
1643 /// The size comes from the remembered geometry `try_restore` just applied,
1644 /// measured in whole squares: a window last seen filling four squares
1645 /// opens filling four squares, at the corner of the invocation square that
1646 /// leaves it clear of its neighbours.
1647 unsafe fn place_on_invocation_cell(&mut self, app_id: &str, hx: f64, hy: f64) {
1648 let wm = &(*self.server).wm;
1649 let sp = wm.layout.snap_params();
1650 if sp.cell_w <= 0.5 || sp.cell_h <= 0.5 {
1651 return;
1652 }
1653 let (phys_x, phys_y, vp_w, vp_h) = self.first_enabled_output_box();
1654 let zoom = wm.desk_zoom.max(0.01);
1655 // The hint is a layout point; the grid is in virtual coordinates.
1656 let inv_vx = wm.desk_pan_x + (hx - phys_x) / zoom;
1657 let inv_vy = wm.desk_pan_y + (hy - phys_y) / zoom;
1658 let col = crate::policy::cells::cell_index(inv_vx, sp.cell_w, sp.gap_width);
1659 let row = crate::policy::cells::cell_index(inv_vy, sp.cell_h, sp.gap_width);
1660
1661 // Size in squares, from the geometry `try_restore` left in place.
1662 let (vw, vh) = self.mapped_size_hint();
1663 let (c0, r0, c1, r1) = crate::policy::cells::window_span(
1664 0.0, 0.0, vw, vh, sp.cell_w, sp.cell_h, sp.gap_width,
1665 );
1666 let (cols, rows) = (c1 - c0 + 1, r1 - r0 + 1);
1667
1668 // Everything else already on the desktop, in squares. Chrome and the
1669 // canvas itself are not obstacles.
1670 let mut occupied = Vec::new();
1671 for &w in wm.windows.iter() {
1672 if w.is_null() || w == (self as *mut Window) || (*w).closed || (*w).minimized {
1673 continue;
1674 }
1675 if !matches!((*w).state, WindowState::Mapped) {
1676 continue;
1677 }
1678 if (*w).is_status_bar() || (*w).is_wallpaper() || (*w).is_grid() {
1679 continue;
1680 }
1681 let (ow, oh) = ((*w).box_geom.width as f64, (*w).box_geom.height as f64);
1682 if ow <= 0.0 || oh <= 0.0 {
1683 continue;
1684 }
1685 let (oc0, or0, oc1, or1) = crate::policy::cells::window_span(
1686 (*w).virtual_x, (*w).virtual_y, ow, oh, sp.cell_w, sp.cell_h, sp.gap_width,
1687 );
1688 occupied.push(crate::policy::spawn::CellBlock::new(oc0, or0, oc1, or1));
1689 }
1690
1691 // Visible squares, so a tie between two clear corners goes to the one
1692 // on screen.
1693 let view = {
1694 let (vx0, vy0) = (wm.desk_pan_x, wm.desk_pan_y);
1695 let (vx1, vy1) = (vx0 + vp_w / zoom, vy0 + vp_h / zoom);
1696 let c0 = crate::policy::cells::cell_index(vx0, sp.cell_w, sp.gap_width);
1697 let r0 = crate::policy::cells::cell_index(vy0, sp.cell_h, sp.gap_width);
1698 let c1 = crate::policy::cells::cell_index(vx1, sp.cell_w, sp.gap_width);
1699 let r1 = crate::policy::cells::cell_index(vy1, sp.cell_h, sp.gap_width);
1700 crate::policy::spawn::CellBlock::new(c0, r0, c1, r1)
1701 };
1702
1703 let block = crate::policy::spawn::place_at_cell(col, row, cols, rows, &occupied, Some(view));
1704 let (bx, by, bw, bh) = crate::policy::cells::block_rect(
1705 block.col0, block.row0, block.col1, block.row1,
1706 sp.cell_w, sp.cell_h, sp.gap_width, sp.cell_inset,
1707 );
1708 self.virtual_x = bx;
1709 self.virtual_y = by;
1710 // A window that was filling whole squares keeps doing so — it is the
1711 // same window, in the same shape, somewhere else. One that was not
1712 // keeps its own size and simply starts at the square's corner.
1713 if self.tiling_mode == crate::tiling::TilingMode::Tiled {
1714 self.box_geom.width = bw.round() as i32;
1715 self.box_geom.height = bh.round() as i32;
1716 self.wm_requested.dimensions = Some(crate::window::Dimensions {
1717 width: bw.round() as u32,
1718 height: bh.round() as u32,
1719 });
1720 }
1721 self.hint_placed = true;
1722 log::info!(
1723 "place-next-cell: {} -> {} ({}x{} squares) at virtual ({:.0}, {:.0})",
1724 app_id,
1725 crate::policy::cells::span_label(block.col0, block.row0, block.col1, block.row1),
1726 cols, rows, bx, by
1727 );
1728 }
1729
1730 /// Open a session modal in the middle of what the user is looking at,
1731 /// ignoring wherever it last sat.
1732 ///
1733 /// On a panning desktop a remembered position is actively wrong for these
1734 /// windows: the camera has almost always moved since the last time, so
1735 /// the window maps somewhere off-view and the prompt reads as never
1736 /// having appeared — which for the polkit agent means the privileged
1737 /// action silently times out.
1738 ///
1739 /// Runs after `try_restore`, so the remembered SIZE is still available
1740 /// and only the position is overridden — the same split
1741 /// `try_hint_placement` uses — and marks `hint_placed` so the spawn
1742 /// viewport pan is skipped: the window is already centered in view, and
1743 /// panning the camera to it would move the desktop out from under the
1744 /// user for a dialog that is about to close again.
1745 /// Windows that open centered on the current view rather than wherever
1746 /// they last were: DE session modals whose whole job is to interrupt, and
1747 /// which the user must be able to answer immediately.
1748 ///
1749 /// Hardcoded by app_id like the compositor's other DE-internal window
1750 /// classes (`cce-status*`/`cce-wallpaper`/`cce-grid` in `try_restore`,
1751 /// `cce-notifier`/`cce-cloud` in `get_mode_for_window`). The config's
1752 /// per-app window rules assign a tiling MODE, not a placement, so there
1753 /// is nothing there to hang this off yet.
1754 fn is_view_centered_modal(app_id: &str) -> bool {
1755 // The polkit prompt, and the file chooser cce-files runs in --select/
1756 // --save mode: both are spawned BY an action in the current view and
1757 // must be answered immediately — a remembered position is actively
1758 // wrong for them (the chooser used to map wherever the file manager
1759 // was last used, squares away from the app that opened it).
1760 app_id == "cce-authenticator" || app_id == "cce-filesystem-chooser"
1761 }
1762
1763 unsafe fn try_center_on_view(&mut self) {
1764 let app_id = self.get_app_id_string().unwrap_or_default();
1765 if !Self::is_view_centered_modal(&app_id) {
1766 return;
1767 }
1768
1769 // Whatever history says, a modal has to be visible and free-floating:
1770 // a restored Tiled mode would re-snap it onto a grid cell (undoing
1771 // the centering) and a restored `minimized` would hide the prompt
1772 // outright. `mode_locked` is the "explicit beats heuristic" latch, so
1773 // the arrange pass cannot geometrically re-promote it either.
1774 //
1775 // Utility is exempt from the mode forcing ONLY — like
1776 // `try_hint_placement`, this owns the window's POSITION, never its
1777 // size. A Utility window already satisfies everything the forcing is
1778 // for: it always floats, never tiles, and both the grid snap and the
1779 // overview displacement skip it. Overwriting the field would silently
1780 // strip the mode — `set_utility` arrives before map, and every Utility
1781 // gate reads `tiling_mode` RAW — leaving the modal resizable, its
1782 // geometry saved, and a stale size restored over it next time.
1783 if self.tiling_mode != crate::tiling::TilingMode::Utility {
1784 self.tiling_mode = crate::tiling::TilingMode::Floating;
1785 }
1786 self.mode_locked = true;
1787 self.minimized = false;
1788
1789 // A self-sizing modal has not committed its geometry yet, so
1790 // `mapped_size_hint` here is still the 400x400 floor — centering
1791 // against that misses by half the difference from the real size (a
1792 // 640x360 prompt landed 120px right and 20px high). Center anyway so
1793 // the first frame is not wildly off, and latch a redo for the commit
1794 // that brings the truth.
1795 //
1796 // Any mode with unknown geometry latches the redo — not Utility only.
1797 // The file chooser disproved the old Utility-only reasoning: a
1798 // FLOATING self-sizer on its first ever run has no restored geometry
1799 // and no arrange-given size either, so it was centered against the
1800 // 400x400 floor and stuck there, ~250px off for a 900x500 dialog.
1801 // A Floating modal with restored geometry still skips the latch
1802 // (box_geom is already filled by the time we run).
1803 self.pending_view_center = self.box_geom.width <= 0 || self.box_geom.height <= 0;
1804 self.apply_view_centering();
1805 }
1806
1807 /// The centering itself, split out so the self-sizing commit path can redo
1808 /// it once the client's real size lands.
1809 unsafe fn apply_view_centering(&mut self) {
1810 let (_, _, vp_w, vp_h) = self.first_enabled_output_box();
1811 let wm = &(*self.server).wm;
1812 let zoom = wm.desk_zoom.max(0.01);
1813 let (w, h) = self.mapped_size_hint();
1814
1815 // Policy owns the camera math; the output's origin cancels out of the
1816 // centering, so only the extent is needed per axis.
1817 self.virtual_x = crate::policy::camera::centered_window_origin(wm.desk_pan_x, vp_w, zoom, w);
1818 self.virtual_y = crate::policy::camera::centered_window_origin(wm.desk_pan_y, vp_h, zoom, h);
1819 self.hint_placed = true;
1820 log::info!(
1821 "view-centered modal: app_id={} size=({:.0}x{:.0}) zoom={:.2} virtual=({:.1},{:.1})",
1822 self.get_app_id_string().unwrap_or_default(),
1823 w, h, zoom, self.virtual_x, self.virtual_y
1824 );
1825 }
1826
1827 /// Redo a latched view-centering now that a self-sizing modal's real
1828 /// geometry has arrived. One-shot: a later commit (or a user dragging the
1829 /// window) must not snap it back to the middle.
1830 pub unsafe fn take_pending_view_center(&mut self) {
1831 if !self.pending_view_center || self.box_geom.width <= 0 || self.box_geom.height <= 0 {
1832 return;
1833 }
1834 self.pending_view_center = false;
1835 self.apply_view_centering();
1836 }
1837
1838 pub unsafe fn map(&mut self) -> Result<(), &'static str> {
1839 log::debug!("window '{:?}' mapped", self.get_title());
1840 if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
1841 log::debug!("[LinkDbg] map app={:?} was_state={:?} linked={}",
1842 self.get_app_id_string(), self.state, self.is_linked());
1843 }
1844 assert!(!matches!(self.impl_type, WindowImpl::Destroying));
1845 assert_eq!(self.state, WindowState::Initialized);
1846 self.state = WindowState::Mapped;
1847
1848 self.try_restore();
1849 self.try_hint_placement();
1850 // Last: a session modal's placement is not negotiable, so it wins
1851 // over both the remembered geometry and any stale place-next hint.
1852 self.try_center_on_view();
1853 // After every placement decision, including the invocation-square one:
1854 // whichever chose this spot, a tiled window must not open stacked on
1855 // another. The anchor rule already avoids that when any corner is
1856 // clear, so this only acts when none was.
1857 self.avoid_tiled_overlap();
1858
1859 let surface = self.root_surface();
1860 if !surface.is_null() {
1861 let commit_listener = &mut self.commit as *mut ffi::wl_listener as *mut WlListener;
1862 (*commit_listener).notify = Some(handle_window_commit);
1863 wl_signal_add(ffi::river_wlr_surface_get_commit_signal(surface), &mut self.commit);
1864 }
1865
1866 let app_id_ptr = self.get_app_id();
1867 let (is_status_bar, is_wallpaper) = if !app_id_ptr.is_null() {
1868 let app_id = std::ffi::CStr::from_ptr(app_id_ptr).to_string_lossy();
1869 (app_id.starts_with("cce-status"), app_id.as_ref() == "cce-wallpaper")
1870 } else {
1871 (false, false)
1872 };
1873
1874 if is_status_bar || is_wallpaper {
1875 self.tiling_mode = crate::tiling::TilingMode::Status;
1876 if is_status_bar && self.status_edge == StatusEdge::Unspecified {
1877 let app_id = std::ffi::CStr::from_ptr(app_id_ptr).to_string_lossy();
1878 let name = if let Some(stripped) = app_id.strip_prefix("cce-status-interface-left-").or_else(|| app_id.strip_prefix("cce-status-left-")) {
1879 stripped
1880 } else if let Some(stripped) = app_id.strip_prefix("cce-status-interface-right-").or_else(|| app_id.strip_prefix("cce-status-right-")) {
1881 stripped
1882 } else {
1883 &app_id
1884 };
1885 let mut loaded_edge = None;
1886 if name == "light_source" {
1887 let mut light_pos = 2.356194490192345_f32; // Default 135 deg in rad
1888 if let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) {
1889 let val = cce_ui::config::parse_kdl_to_json(&content);
1890 if let Some(wm_obj) = val.get("window_manager") {
1891 if let Some(pos_val) = wm_obj.get("light_source_position") {
1892 if let Some(f) = pos_val.as_f64() {
1893 light_pos = f as f32;
1894 } else if let Some(i) = pos_val.as_i64() {
1895 let deg = i as f32;
1896 if deg > 2.0 * std::f32::consts::PI {
1897 light_pos = deg.to_radians();
1898 } else {
1899 light_pos = deg;
1900 }
1901 }
1902 }
1903 }
1904 }
1905
1906 let two_pi = 2.0 * std::f32::consts::PI;
1907 let mut angle = light_pos % two_pi;
1908 if angle < 0.0 {
1909 angle += two_pi;
1910 }
1911
1912 let pi = std::f32::consts::PI;
1913 let edge = if angle < pi / 8.0 || angle >= 15.0 * pi / 8.0 {
1914 StatusEdge::Right
1915 } else if angle < 3.0 * pi / 8.0 {
1916 StatusEdge::TopRight
1917 } else if angle < 5.0 * pi / 8.0 {
1918 StatusEdge::TopCenter
1919 } else if angle < 7.0 * pi / 8.0 {
1920 StatusEdge::TopLeft
1921 } else if angle < 9.0 * pi / 8.0 {
1922 StatusEdge::Left
1923 } else if angle < 11.0 * pi / 8.0 {
1924 StatusEdge::BottomLeft
1925 } else if angle < 13.0 * pi / 8.0 {
1926 StatusEdge::BottomCenter
1927 } else {
1928 StatusEdge::BottomRight
1929 };
1930 loaded_edge = Some(edge);
1931 } else if let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) {
1932 let val = cce_ui::config::parse_kdl_to_json(&content);
1933 if let Some(layout_obj) = val.get("layout") {
1934 if let Some(status_bar_obj) = layout_obj.get("status_bar") {
1935 if let Some(edge_val) = status_bar_obj.get(name) {
1936 if let Some(edge_str) = edge_val.as_str() {
1937 loaded_edge = match edge_str.to_lowercase().as_str() {
1938 "left" => Some(StatusEdge::Left),
1939 "right" => Some(StatusEdge::Right),
1940 "top-left" => Some(StatusEdge::TopLeft),
1941 "top-center" => Some(StatusEdge::TopCenter),
1942 "top-right" => Some(StatusEdge::TopRight),
1943 "bottom-left" => Some(StatusEdge::BottomLeft),
1944 "bottom-center" => Some(StatusEdge::BottomCenter),
1945 "bottom-right" => Some(StatusEdge::BottomRight),
1946 _ => None,
1947 };
1948 }
1949 }
1950 }
1951 }
1952 }
1953 self.status_edge = if let Some(edge) = loaded_edge {
1954 edge
1955 } else {
1956 if app_id.contains("viewport") {
1957 StatusEdge::TopLeft
1958 } else if app_id.contains("window") {
1959 StatusEdge::TopCenter
1960 } else {
1961 StatusEdge::TopRight
1962 }
1963 };
1964 }
1965 } else {
1966 let mut should_focus = true;
1967 if self.session_restored && self.restored_focused {
1968 (*self.server).wm.restored_focused_window_mapped = true;
1969 if (*self.server).wm.startup_input_seen {
1970 // The user already typed/clicked somewhere (e.g. into
1971 // the keepassxc unlock dialog) while this window was
1972 // still loading — mapping now must not yank focus out
1973 // from under them.
1974 log::info!("[FocusRestore] Restored focused window {:?} mapped after user input; leaving focus alone", self.get_title());
1975 should_focus = false;
1976 } else {
1977 log::info!("[FocusRestore] Restored focused window mapped: {:?}", self.get_title());
1978 }
1979 } else if (*self.server).wm.has_restored_focused_window
1980 && !(*self.server).wm.restored_focused_window_mapped
1981 && !(*self.server).wm.startup_input_seen
1982 {
1983 // Strict settle phase: until the session's focused window
1984 // maps (or the user intervenes), NOTHING else auto-focuses —
1985 // neither restored siblings mapping first nor autostarts.
1986 // This also keeps the focus-follow pan parked at the saved
1987 // camera instead of wandering to whichever window loads
1988 // fastest.
1989 log::info!("[FocusRestore] Holding focus for the session's focused window; {:?} maps unfocused", self.get_title());
1990 should_focus = false;
1991 } else if (*self.server).wm.has_restored_focused_window
1992 && (*self.server).wm.restored_focused_window_mapped
1993 {
1994 if self.session_restored {
1995 // A restored sibling mapping after the session's focused
1996 // window: never steal back. Only true session restores —
1997 // a mid-session spawn that borrowed geometry from
1998 // last_window_states is a fresh launch and must focus
1999 // (and spawn-pan) normally, else it maps invisible at
2000 // its remembered off-viewport spot for the whole session.
2001 log::info!("[FocusRestore] Blocking focus to non-focused restored window {:?} because restored focused window is already mapped", self.get_title());
2002 should_focus = false;
2003 } else if !(*self.server).wm.startup_input_seen {
2004 // A window mapping unbidden while the session is still
2005 // settling (no key/button pressed yet) — an autostart
2006 // like keepassxc popping up after the restored windows.
2007 // It must not steal focus (or drag the focus-follow pan
2008 // over to itself) from the session's focused window.
2009 log::info!("[FocusRestore] Blocking focus steal by unrestored window {:?} mapping before first input", self.get_title());
2010 should_focus = false;
2011 }
2012 }
2013
2014 // A client whose connection broke rebuilds its surface from
2015 // scratch (cce-ui window_runner::run) and maps again seconds
2016 // later. The user never asked for that window, so it must not
2017 // take focus from whatever they moved on to.
2018 //
2019 // Keyed on the previous window vanishing WITHOUT a requested
2020 // close — not on matching saved state, which a mid-session spawn
2021 // does too and which must still focus and spawn-pan normally.
2022 if should_focus {
2023 if let Some(app_id) = self.get_app_id_string() {
2024 if (*self.server).wm.take_recent_vanish(&app_id) {
2025 log::info!("[FocusRestore] Blocking focus steal by reconnecting client {:?} ({})", self.get_title(), app_id);
2026 should_focus = false;
2027 }
2028 }
2029 }
2030
2031 // A WORLD window spawning during overview pulls the session
2032 // out of it, landing at zoom 1 on the new window — the user
2033 // asked for it (launcher pick, spawn keybind). Chrome
2034 // (Popup/Overlay), status, wallpaper and the grid spawn without
2035 // disturbing the overview. Before the focus loop, so the
2036 // focus-follow pan sees the settled zoom-1 camera and no-ops.
2037 if should_focus
2038 && (*self.server).wm.mode == crate::window_manager::WindowManagerMode::Overview
2039 && !self.is_grid()
2040 && !self.is_status_bar()
2041 && !self.is_wallpaper()
2042 {
2043 let resolved = (*self.server).wm.get_mode_for_window(self as *mut Window);
2044 if !matches!(
2045 resolved,
2046 crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
2047 ) {
2048 (*self.server).wm.exit_overview_to_window(self as *mut Window);
2049 }
2050 }
2051
2052 // The grid layer never takes focus — it is desktop furniture,
2053 // not a window (it is also input-transparent, so focus here
2054 // would be unreachable-by-click and unswitchable-away for
2055 // keyboard input).
2056 if should_focus && !self.is_grid() {
2057 let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2058 let mut curr = (*seats).next;
2059 while curr != seats {
2060 let next = (*curr).next;
2061 let seat = crate::container_of!(curr, crate::seat::Seat, link);
2062 (*seat).focus(crate::seat::Focus::Window(self as *mut Window));
2063 curr = next;
2064 }
2065 }
2066 }
2067
2068 // The open dissolve. Last in `map`, so the window is fully placed and
2069 // its scene tree built before the ramp touches it — and so a window
2070 // that failed to map never starts one. `start_map_fade` snaps rather
2071 // than ramps when fading is off or this surface opts out (status
2072 // segments, wallpaper), so there is no second branch here.
2073 let fade_ms = (*self.server).wm.layout.fade_in_ms;
2074 if self.wants_map_fade() && fade_ms > 0 {
2075 self.map_fade = 0.0;
2076 }
2077 self.start_map_fade(1.0, fade_ms);
2078
2079 (*self.server).wm.dirty_windowing();
2080 Ok(())
2081 }
2082
2083 pub unsafe fn set_closing(&mut self) {
2084 if self.state != WindowState::Closing {
2085 if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2086 log::debug!("[LinkDbg] set_closing app={:?} was_state={:?} was_linked={}",
2087 self.get_app_id_string(), self.state, self.is_linked());
2088 }
2089 self.state = WindowState::Closing;
2090 if self.is_linked() {
2091 wl_list_remove_and_reinit(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
2092 }
2093 }
2094 }
2095
2096 pub unsafe fn unmap(&mut self) {
2097 log::debug!("window '{:?}' unmapped", self.get_title());
2098 if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2099 log::debug!("[LinkDbg] unmap app={:?} state={:?} linked={}",
2100 self.get_app_id_string(), self.state, self.is_linked());
2101 }
2102 if self.state != WindowState::Mapped {
2103 return;
2104 }
2105 // Nobody asked this window to go: either its program exited on its
2106 // own or — the case this feeds — its Wayland connection broke and
2107 // cce-ui is about to rebuild the surface on a fresh one. Chrome is
2108 // the exception: a Popup (the cce-cloud launcher) or an Overlay dock
2109 // closes itself as part of being used — Escape, a pick, a click-away,
2110 // a keyboard leave — and the next super+d inside the grace is a
2111 // deliberate relaunch that must focus, not a crashed client
2112 // reconnecting. Counting it left the reopened launcher unfocused.
2113 if !self.close_requested
2114 && !matches!(
2115 self.tiling_mode,
2116 crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
2117 )
2118 {
2119 if let Some(app_id) = self.get_app_id_string() {
2120 (*self.server).wm.note_vanished(app_id);
2121 }
2122 }
2123 wl_listener_remove_safe(&mut self.commit);
2124 self.surfaces.save();
2125 assert!(!matches!(self.impl_type, WindowImpl::Destroying));
2126 self.set_closing();
2127 (*self.server).wm.dirty_windowing();
2128
2129 if !self.foreign_toplevel_handle.is_null() {
2130 ffi::wlr_ext_foreign_toplevel_handle_v1_destroy(self.foreign_toplevel_handle);
2131 self.foreign_toplevel_handle = std::ptr::null_mut();
2132 }
2133 if !self.wlr_toplevel_handle.is_null() {
2134 ffi::wlr_foreign_toplevel_handle_v1_destroy(self.wlr_toplevel_handle);
2135 self.wlr_toplevel_handle = std::ptr::null_mut();
2136 }
2137
2138
2139 }
2140
2141 pub unsafe fn close(&mut self) {
2142 self.close_requested = true;
2143 match self.impl_type {
2144 WindowImpl::Toplevel(toplevel) => {
2145 if !toplevel.is_null() {
2146 ffi::wlr_xdg_toplevel_send_close((*toplevel).wlr_toplevel);
2147 }
2148 }
2149 WindowImpl::Xwayland(xwindow) => {
2150 if !xwindow.is_null() {
2151 ffi::wlr_xwayland_surface_close((*xwindow).xsurface);
2152 }
2153 }
2154 WindowImpl::Destroying => {}
2155 }
2156 }
2157
2158 pub unsafe fn destroy(window: *mut Window) {
2159 assert!(matches!((*window).impl_type, WindowImpl::Destroying));
2160 match (*window).state {
2161 WindowState::Init => {}
2162 WindowState::Closing => {
2163 (*(*window).server).wm.dirty_windowing();
2164 return;
2165 }
2166 _ => unreachable!(),
2167 }
2168 assert!((*window).object.is_null());
2169
2170 let seats = &mut (*(*window).server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2171 let mut curr = (*seats).next;
2172 while curr != seats {
2173 let next = (*curr).next;
2174 let seat = crate::container_of!(curr, crate::seat::Seat, link);
2175 if let crate::seat::Focus::Window(w) = (*seat).focused {
2176 if w == window {
2177 (*seat).focus(crate::seat::Focus::None);
2178 (*(*window).server).wm.focus_next_visible_window(seat);
2179 }
2180 }
2181 if let Some(ref op) = (*seat).op {
2182 if op.window_ptr == window {
2183 (*seat).op = None;
2184 }
2185 }
2186 curr = next;
2187 }
2188
2189
2190
2191 // Destroy decorations
2192 for decorations in [&mut (*window).decorations_above as *mut ffi::wl_list, &mut (*window).decorations_below as *mut ffi::wl_list] {
2193 let list_head = decorations as *mut WlList;
2194 let mut curr = (*list_head).next;
2195 while curr != list_head {
2196 let next = (*curr).next;
2197 let dec = crate::container_of!(curr, Decoration, link);
2198 (*dec).destroy();
2199 curr = next;
2200 }
2201 }
2202
2203 wl_listener_remove_safe(&mut (*window).commit);
2204 ffi::wlr_scene_node_destroy((*window).tree as *mut ffi::wlr_scene_node);
2205 ffi::wlr_scene_node_destroy((*window).popup_tree as *mut ffi::wlr_scene_node);
2206 // The border segments hang off the global overlay layer, not off
2207 // `tree`, so destroying the window tree does not take them with it.
2208 // Left behind they would both leak and keep a SceneNodeData pointing
2209 // at this freed window for the next hit test to find.
2210 ffi::wlr_scene_node_destroy((*window).border.tree as *mut ffi::wlr_scene_node);
2211 ffi::wlr_scene_node_destroy(&mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
2212
2213 (*window).node.deinit();
2214
2215 (*(*window).server).wm.remove_from_history(window);
2216 // A seat cursor may still name this window as its adjust target.
2217 // The next hover evaluation would replace it, but a window allocated
2218 // at the same address in the meantime must not inherit the ring.
2219 {
2220 let seats = &mut (*(*window).server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2221 let mut curr = (*seats).next;
2222 while curr != seats {
2223 let seat = crate::container_of!(curr, crate::seat::Seat, link);
2224 if (*seat).cursor.adjust_hover == window {
2225 (*seat).cursor.adjust_hover = std::ptr::null_mut();
2226 }
2227 curr = (*curr).next;
2228 }
2229 }
2230 (*(*window).server).wm.windows.remove((*window).ref_key);
2231 (*(*window).server).wm.check_clean_exit_progress();
2232
2233 let _ = Box::from_raw(window);
2234 }
2235
2236 pub unsafe fn set_dimensions_hint(&mut self, hint: DimensionsHint) {
2237 self.wm_scheduled.dimensions_hint = hint;
2238 if self.wm_sent.dimensions_hint != hint {
2239 // Overlay included: a self-sizing overlay (cce-cloud) changes its hint
2240 // on every resize, and skipping it meant no arrange pass was scheduled.
2241 // Utility for the same reason: it is self-sizing by definition.
2242 if matches!(self.tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Overlay | crate::tiling::TilingMode::Utility) {
2243 (*self.server).wm.dirty_windowing();
2244 }
2245 self.wm_sent.dimensions_hint = hint;
2246 }
2247 }
2248
2249 pub unsafe fn set_dimensions(&mut self, width: u32, height: u32) {
2250 self.rendering_scheduled.width = width;
2251 self.rendering_scheduled.height = height;
2252
2253 if self.rendering_scheduled.resend_dimensions ||
2254 self.rendering_scheduled.width != self.rendering_sent.width ||
2255 self.rendering_scheduled.height != self.rendering_sent.height {
2256 (*self.server).wm.dirty_rendering();
2257 }
2258 }
2259
2260 /// Is a pointer resize op on this window still in progress on any seat?
2261 pub unsafe fn resize_op_active(&self) -> bool {
2262 let seats_list = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2263 let mut curr_seat = (*seats_list).next;
2264 while curr_seat != seats_list {
2265 let seat = crate::container_of!(curr_seat, crate::seat::Seat, link);
2266 if let Some(ref op) = (*seat).op {
2267 if op.window_ptr == self as *const Window as *mut Window {
2268 if let crate::seat::PointerOpType::Resize { .. } = op.op_type {
2269 return true;
2270 }
2271 }
2272 }
2273 curr_seat = (*curr_seat).next;
2274 }
2275 false
2276 }
2277
2278 /// Interactive-resize anchoring for the commit path, shared by every
2279 /// surface kind. While a LEFT/TOP edge is being dragged, the client's
2280 /// committed size decides where the window's origin goes: the opposite
2281 /// edge stays where the drag found it, so the dragged edge is the one
2282 /// that appears to move. Without this the origin holds still and the
2283 /// window grows away from the grabbed edge — which is what Xwayland
2284 /// windows did until they were routed through here: only the
2285 /// xdg-toplevel commit handler had the math.
2286 ///
2287 /// `committed_w`/`committed_h` are the size the client just committed,
2288 /// in `box_geom` units (content size; for wine X11 windows the caller has
2289 /// already taken the 32px frame off, as the render pass does). Updates
2290 /// the virtual position and the requested/box screen origin and returns
2291 /// that origin, or `None` when no resize is armed. The anchoring outlives
2292 /// the seat op by one commit — the last configure is usually still in
2293 /// flight at release — so the first commit after the op disarms it.
2294 pub unsafe fn anchor_resize_commit(&mut self, committed_w: i32, committed_h: i32) -> Option<(i32, i32)> {
2295 let edges = self.resize_edges?;
2296 let resize_active = self.resize_op_active();
2297
2298 if edges.left {
2299 self.virtual_x = self.resize_start_vx + (self.resize_start_w as f64 - committed_w as f64);
2300 }
2301 if edges.top {
2302 self.virtual_y = self.resize_start_vy + (self.resize_start_h as f64 - committed_h as f64);
2303 }
2304
2305 let (final_x, final_y) = self.virtual_to_screen(self.virtual_x, self.virtual_y);
2306 self.rendering_requested.x = final_x;
2307 self.rendering_requested.y = final_y;
2308 self.box_geom.x = final_x;
2309 self.box_geom.y = final_y;
2310
2311 if !resize_active {
2312 self.resize_edges = None;
2313 }
2314 Some((final_x, final_y))
2315 }
2316
2317 pub unsafe fn set_decoration_hint(&mut self, hint: ffi::zcce_window_v1_decoration_hint) {
2318 self.wm_scheduled.decoration_hint = hint;
2319 if hint != self.wm_sent.decoration_hint {
2320 (*self.server).wm.dirty_windowing();
2321 self.wm_sent.decoration_hint = hint;
2322 }
2323 }
2324
2325 pub unsafe fn root_surface(&self) -> *mut ffi::wlr_surface {
2326 match self.impl_type {
2327 WindowImpl::Toplevel(toplevel) => {
2328 if toplevel.is_null() {
2329 std::ptr::null_mut()
2330 } else {
2331 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
2332 ffi::river_wlr_xdg_surface_get_surface(base)
2333 }
2334 }
2335 WindowImpl::Xwayland(xwindow) => {
2336 if xwindow.is_null() || (*xwindow).xsurface.is_null() {
2337 std::ptr::null_mut()
2338 } else {
2339 (*(*xwindow).xsurface).surface
2340 }
2341 }
2342 _ => std::ptr::null_mut(),
2343 }
2344 }
2345
2346 pub unsafe fn get_decorations_size(&self) -> (i32, i32) {
2347 if self.wm_requested.ssd {
2348 return (0, 0);
2349 }
2350 self.measure_decorations()
2351 }
2352
2353 /// Raw client-side decoration size (surface minus geometry), regardless
2354 /// of the current SSD setting. Callers that honor SSD gate on it
2355 /// themselves.
2356 pub unsafe fn measure_decorations(&self) -> (i32, i32) {
2357 let surface = self.root_surface();
2358 if surface.is_null() {
2359 return (0, 0);
2360 }
2361 let surf_w = ffi::river_wlr_surface_get_width(surface);
2362 let surf_h = ffi::river_wlr_surface_get_height(surface);
2363
2364 let (geom_w, geom_h) = match self.impl_type {
2365 WindowImpl::Toplevel(toplevel) => {
2366 if toplevel.is_null() {
2367 (surf_w, surf_h)
2368 } else {
2369 ((*toplevel).geometry.width, (*toplevel).geometry.height)
2370 }
2371 }
2372 _ => (surf_w, surf_h),
2373 };
2374
2375 let dec_w = (surf_w - geom_w).max(0);
2376 let dec_h = (surf_h - geom_h).max(0);
2377 (dec_w, dec_h)
2378 }
2379
2380 pub unsafe fn send_frame_done(&self) {
2381 assert_eq!(self.state, WindowState::Mapped);
2382 if !matches!(self.impl_type, WindowImpl::Destroying) {
2383 let mut now = std::mem::zeroed();
2384 clock_gettime(libc::CLOCK_MONOTONIC, &mut now);
2385 let now_ffi = ffi::timespec {
2386 tv_sec: now.tv_sec as _,
2387 tv_nsec: now.tv_nsec as _,
2388 };
2389 ffi::wlr_surface_send_frame_done(self.root_surface(), &now_ffi);
2390 }
2391 }
2392
2393 pub unsafe fn manage_start(&mut self) {
2394 match self.state {
2395 WindowState::Init => {}
2396 WindowState::Closing => {
2397 if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2398 log::debug!("[LinkDbg] manage_start closing->init app={:?} was_linked={}",
2399 self.get_app_id_string(), self.is_linked());
2400 }
2401 self.state = WindowState::Init;
2402 self.wm_sent = WmSentState {
2403 dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
2404 decoration_hint: ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
2405 parent: None,
2406 };
2407 self.wm_requested = WmRequestedState {
2408 dimensions: None,
2409 bounds: Dimensions { width: 0, height: 0 },
2410 ssd: false,
2411 tiled: 0,
2412 capabilities: 1 | 2 | 4 | 8,
2413 resizing: false,
2414 maximized: false,
2415 fullscreen: std::ptr::null_mut(),
2416 inform_fullscreen: false,
2417 close: false,
2418 };
2419 self.rendering_sent = WindowRenderingSent {
2420 width: 0,
2421 height: 0,
2422 presentation_hint: ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC,
2423 };
2424 self.rendering_requested = WindowRenderingRequested {
2425 x: 0,
2426 y: 0,
2427 hidden: false,
2428 border: Border::none(),
2429 clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
2430 content_clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
2431 opacity: 1.0f32,
2432 circular: false,
2433 blur: false,
2434 };
2435
2436 if self.is_linked() {
2437 wl_list_remove_and_reinit(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
2438 }
2439
2440 self.make_inert();
2441 }
2442 WindowState::Ready | WindowState::Initialized | WindowState::Mapped => {
2443 let wm_v1 = (*self.server).wm.object;
2444 if wm_v1.is_null() {
2445 let is_linked = self.is_linked();
2446 if !is_linked {
2447 if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2448 log::debug!("[LinkDbg] manage_start LINK app={:?} state={:?}",
2449 self.get_app_id_string(), self.state);
2450 }
2451 if !self.node.link.prev.is_null() && !self.node.link.next.is_null() {
2452 wl_list_remove_and_reinit(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
2453 }
2454 let rendering_list = &mut (*self.server).wm.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
2455 // The tail is the top of the stack. A shy helper
2456 // window (`is_shy`) links at the head instead — beneath
2457 // the app's own windows, where its app keeps it.
2458 let anchor = if self.is_shy() { rendering_list } else { (*rendering_list).prev };
2459 wl_list_insert(anchor, &mut self.node.link as *mut ffi::wl_list as *mut WlList);
2460
2461 if self.foreign_toplevel_handle.is_null() {
2462 let list = (*self.server).foreign_toplevel_list;
2463 let title = self.get_title();
2464 let app_id = self.get_app_id();
2465 let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
2466 title,
2467 app_id,
2468 };
2469 let handle = ffi::wlr_ext_foreign_toplevel_handle_v1_create(list, &state);
2470 if !handle.is_null() {
2471 self.foreign_toplevel_handle = handle;
2472 (*handle).data = self as *mut Window as *mut _;
2473 }
2474 }
2475
2476 if self.wlr_toplevel_handle.is_null() {
2477 let manager = (*self.server).wlr_foreign_toplevel_manager;
2478 let handle = ffi::wlr_foreign_toplevel_handle_v1_create(manager);
2479 if !handle.is_null() {
2480 self.wlr_toplevel_handle = handle;
2481 let title = self.get_title();
2482 if !title.is_null() {
2483 ffi::wlr_foreign_toplevel_handle_v1_set_title(handle, title);
2484 }
2485 let app_id = self.get_app_id();
2486 if !app_id.is_null() {
2487 ffi::wlr_foreign_toplevel_handle_v1_set_app_id(handle, app_id);
2488 }
2489 }
2490 }
2491 self.rendering_scheduled.resend_dimensions = true;
2492 }
2493 return;
2494 }
2495 let new_resource = self.object.is_null();
2496 let window_v1 = if new_resource {
2497 let client = ffi::wl_resource_get_client(wm_v1);
2498 let res = ffi::wl_resource_create(client, &ffi::zcce_window_v1_interface, ffi::wl_resource_get_version(wm_v1), 0);
2499 if res.is_null() {
2500 log::error!("out of memory");
2501 return;
2502 }
2503 self.object = res;
2504 self.rendering_scheduled.resend_dimensions = true;
2505 ffi::wl_resource_set_implementation(
2506 res,
2507 &WINDOW_INTERFACE as *const _ as *const _,
2508 self as *mut Window as *mut _,
2509 Some(handle_destroy_resource),
2510 );
2511
2512 // Send window to manager
2513 ffi::wl_resource_post_event(wm_v1, ffi::ZCCE_WINDOW_MANAGER_V1_WINDOW, res); // zcce_window_manager_v1.window
2514 res
2515 } else {
2516 self.object
2517 };
2518
2519 let is_linked = self.is_linked();
2520 if !is_linked {
2521 if !self.node.link.prev.is_null() && !self.node.link.next.is_null() {
2522 wl_list_remove_and_reinit(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
2523 }
2524 let rendering_list = &mut (*self.server).wm.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
2525 wl_list_insert((*rendering_list).prev, &mut self.node.link as *mut ffi::wl_list as *mut WlList);
2526
2527 if self.foreign_toplevel_handle.is_null() {
2528 let list = (*self.server).foreign_toplevel_list;
2529 let title = self.get_title();
2530 let app_id = self.get_app_id();
2531 let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
2532 title,
2533 app_id,
2534 };
2535 let handle = ffi::wlr_ext_foreign_toplevel_handle_v1_create(list, &state);
2536 if !handle.is_null() {
2537 self.foreign_toplevel_handle = handle;
2538 (*handle).data = self as *mut Window as *mut _;
2539 }
2540 }
2541
2542 if self.wlr_toplevel_handle.is_null() {
2543 let manager = (*self.server).wlr_foreign_toplevel_manager;
2544 let handle = ffi::wlr_foreign_toplevel_handle_v1_create(manager);
2545 if !handle.is_null() {
2546 self.wlr_toplevel_handle = handle;
2547 let title = self.get_title();
2548 if !title.is_null() {
2549 ffi::wlr_foreign_toplevel_handle_v1_set_title(handle, title);
2550 }
2551 let app_id = self.get_app_id();
2552 if !app_id.is_null() {
2553 ffi::wlr_foreign_toplevel_handle_v1_set_app_id(handle, app_id);
2554 }
2555 }
2556 }
2557 };
2558
2559 if new_resource {
2560 let version = ffi::wl_resource_get_version(window_v1);
2561 if version >= 2 {
2562 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_UNRELIABLE_PID, self.unreliable_pid()); // sendUnreliablePid
2563 }
2564 if version >= 4 {
2565 if !self.foreign_toplevel_handle.is_null() {
2566 let identifier = (*self.foreign_toplevel_handle).identifier;
2567 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_IDENTIFIER, identifier);
2568 }
2569 }
2570 }
2571
2572 if new_resource || self.wm_scheduled.dimensions_hint != self.wm_sent.dimensions_hint {
2573 ffi::wl_resource_post_event(
2574 window_v1,
2575 ffi::ZCCE_WINDOW_V1_DIMENSIONS_HINT, // sendDimensionsHint
2576 self.wm_scheduled.dimensions_hint.min_width as i32,
2577 self.wm_scheduled.dimensions_hint.min_height as i32,
2578 self.wm_scheduled.dimensions_hint.max_width as i32,
2579 self.wm_scheduled.dimensions_hint.max_height as i32,
2580 );
2581 self.wm_sent.dimensions_hint = self.wm_scheduled.dimensions_hint;
2582 }
2583
2584 if new_resource || self.wm_scheduled.decoration_hint != self.wm_sent.decoration_hint {
2585 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_DECORATION_HINT, self.wm_scheduled.decoration_hint); // sendDecorationHint
2586 self.wm_sent.decoration_hint = self.wm_scheduled.decoration_hint;
2587 }
2588
2589 if let Some(ref offset) = self.wm_scheduled.show_window_menu_requested {
2590 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_SHOW_WINDOW_MENU_REQUESTED, offset.x, offset.y); // sendShowWindowMenuRequested
2591 self.wm_scheduled.show_window_menu_requested = None;
2592 }
2593
2594 match self.wm_scheduled.fullscreen_requested {
2595 FullscreenRequest::NoRequest => {}
2596 FullscreenRequest::Fullscreen(output) => {
2597 let mut out_resource = if output.is_null() { std::ptr::null_mut() } else { (*output).object };
2598 if !window_v1.is_null() && !out_resource.is_null() {
2599 let client_win = ffi::wl_resource_get_client(window_v1);
2600 let client_out = ffi::wl_resource_get_client(out_resource);
2601 if client_win != client_out {
2602 log::error!(
2603 "Fullscreen output client mismatch: win_client={:?}, out_client={:?}. Fallback to null_mut",
2604 client_win,
2605 client_out
2606 );
2607 out_resource = std::ptr::null_mut();
2608 }
2609 }
2610 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_FULLSCREEN_REQUESTED, out_resource); // sendFullscreenRequested
2611 }
2612 FullscreenRequest::Exit => {
2613 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_EXIT_FULLSCREEN_REQUESTED); // sendExitFullscreenRequested
2614 }
2615 }
2616 self.wm_scheduled.fullscreen_requested = FullscreenRequest::NoRequest;
2617
2618 match self.wm_scheduled.maximize_requested {
2619 MaximizeRequest::NoRequest => {}
2620 MaximizeRequest::Maximize => {
2621 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_MAXIMIZE_REQUESTED); // sendMaximizeRequested
2622 }
2623 MaximizeRequest::Unmaximize => {
2624 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_UNMAXIMIZE_REQUESTED); // sendUnmaximizeRequested
2625 }
2626 }
2627 self.wm_scheduled.maximize_requested = MaximizeRequest::NoRequest;
2628
2629 if self.wm_scheduled.minimize_requested {
2630 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_MINIMIZE_REQUESTED); // sendMinimizeRequested
2631 }
2632 self.wm_scheduled.minimize_requested = false;
2633
2634 let parent = self.get_parent();
2635 if !parent.is_null() {
2636 let parent_ref = Some((*parent).ref_key);
2637 if self.wm_sent.parent.is_none() || self.wm_sent.parent != parent_ref {
2638 let parent_obj = (*parent).object;
2639 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_PARENT, parent_obj); // sendParent
2640 self.wm_sent.parent = parent_ref;
2641 }
2642 } else if self.wm_sent.parent.is_some() {
2643 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_PARENT, std::ptr::null_mut::<ffi::wl_resource>()); // sendParent
2644 self.wm_sent.parent = None;
2645 }
2646
2647 if new_resource || self.wm_scheduled.dirty_app_id {
2648 let app_id = self.get_app_id();
2649 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_APP_ID, app_id); // sendAppId
2650 self.wm_scheduled.dirty_app_id = false;
2651 }
2652
2653 if new_resource || self.wm_scheduled.dirty_title {
2654 let title = self.get_title();
2655 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_TITLE, title); // sendTitle
2656 self.wm_scheduled.dirty_title = false;
2657 }
2658
2659 if let Some(seat) = self.wm_scheduled.pointer_move_requested.as_mut() {
2660 if !seat.object.is_null() {
2661 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_POINTER_MOVE_REQUESTED, seat.object); // sendPointerMoveRequested
2662 }
2663 }
2664 self.wm_scheduled.pointer_move_requested = std::ptr::null_mut();
2665
2666 if let Some(ref data) = self.wm_scheduled.pointer_resize_requested {
2667 if let Some(seat) = unsafe { data.seat.as_ref() } {
2668 if !seat.object.is_null() {
2669 ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_POINTER_RESIZE_REQUESTED, seat.object, data.edges); // sendPointerResizeRequested
2670 }
2671 }
2672 }
2673 self.wm_scheduled.pointer_resize_requested = None;
2674 }
2675 }
2676 }
2677
2678 pub unsafe fn make_inert(&mut self) {
2679 if !self.object.is_null() {
2680 ffi::wl_resource_post_event(self.object, ffi::ZCCE_WINDOW_V1_CLOSED); // sendClosed // sendClosed
2681 ffi::wl_resource_set_implementation(
2682 self.object,
2683 &INERT_WINDOW_INTERFACE as *const _ as *const _,
2684 std::ptr::null_mut(),
2685 None,
2686 );
2687 self.object = std::ptr::null_mut();
2688 (*self.server).wm.dirty_windowing();
2689 self.node.make_inert();
2690
2691 for decorations in [&mut self.decorations_above as *mut ffi::wl_list, &mut self.decorations_below as *mut ffi::wl_list] {
2692 let list_head = decorations as *mut WlList;
2693 let mut curr = (*list_head).next;
2694 while curr != list_head {
2695 let next = (*curr).next;
2696 let dec = crate::container_of!(curr, Decoration, link);
2697 (*dec).make_inert();
2698 curr = next;
2699 }
2700 }
2701
2702 let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2703 let mut curr = (*seats).next;
2704 while curr != seats {
2705 let next = (*curr).next;
2706 let seat = crate::container_of!(curr, crate::seat::Seat, link);
2707 if let crate::seat::Focus::Window(w) = (*seat).focused {
2708 if w == self as *mut Window {
2709 (*seat).focus(crate::seat::Focus::None);
2710 }
2711 }
2712 curr = next;
2713 }
2714 }
2715 }
2716
2717 pub unsafe fn manage_finish(&mut self) -> bool {
2718 if matches!(self.impl_type, WindowImpl::Destroying) {
2719 assert_eq!(self.state, WindowState::Closing);
2720 return false;
2721 }
2722
2723 match self.state {
2724 WindowState::Init => unreachable!(),
2725 WindowState::Ready => {
2726 if self.wm_requested.dimensions.is_none() && self.wm_requested.fullscreen.is_null() {
2727 return false;
2728 }
2729 if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2730 log::debug!("[LinkDbg] manage_finish ready->initialized app={:?} linked={}",
2731 self.get_app_id_string(), self.is_linked());
2732 }
2733 self.state = WindowState::Initialized;
2734 }
2735 WindowState::Initialized | WindowState::Mapped => {}
2736 WindowState::Closing => return false,
2737 }
2738
2739 if self.wm_requested.close {
2740 self.close();
2741 self.wm_requested.close = false;
2742 }
2743
2744 let mut activated = false;
2745 let seats = &mut (*self.server).wm.sent.seats as *mut ffi::wl_list as *mut WlList;
2746 let mut curr = (*seats).next;
2747 while curr != seats {
2748 let next = (*curr).next;
2749 let seat = crate::container_of!(curr, crate::seat::Seat, link_sent);
2750 if let crate::seat::Focus::Window(w) = (*seat).focused {
2751 if w == self as *mut Window {
2752 activated = true;
2753 break;
2754 }
2755 }
2756 curr = next;
2757 }
2758
2759 if !self.wlr_toplevel_handle.is_null() {
2760 ffi::wlr_foreign_toplevel_handle_v1_set_activated(self.wlr_toplevel_handle, activated);
2761 }
2762
2763 let output = if !self.wm_requested.fullscreen.is_null() {
2764 self.wm_requested.fullscreen
2765 } else if self.is_fullscreen() {
2766 let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
2767 let mut curr = (*outputs_list).next;
2768 let mut found_output = std::ptr::null_mut();
2769 while curr != outputs_list {
2770 let out = crate::container_of!(curr, crate::output::Output, link);
2771 if (*out).sent.state == crate::output::OutputStateValue::Enabled {
2772 found_output = out;
2773 break;
2774 }
2775 curr = (*curr).next;
2776 }
2777 found_output
2778 } else {
2779 std::ptr::null_mut()
2780 };
2781
2782 let new_fullscreen = !output.is_null();
2783 if new_fullscreen && !self.was_fullscreen {
2784 if self.box_geom.width > 0 && self.box_geom.height > 0 {
2785 self.start_fs_anim();
2786 self.saved_width = self.box_geom.width;
2787 self.saved_height = self.box_geom.height;
2788 self.saved_virtual_x = self.virtual_x;
2789 self.saved_virtual_y = self.virtual_y;
2790 self.was_fullscreen = true;
2791 log::info!("[Fullscreen] Saved window {:?} geometry: {}x{} at ({}, {})", self.get_title_string().as_deref().unwrap_or(""), self.saved_width, self.saved_height, self.saved_virtual_x, self.saved_virtual_y);
2792 }
2793 } else if !new_fullscreen && self.was_fullscreen {
2794 if self.saved_width > 0 && self.saved_height > 0 {
2795 // Captures the on-screen fullscreen rect before the restore
2796 // below rewrites box_geom.
2797 self.start_fs_anim();
2798 self.box_geom.width = self.saved_width;
2799 self.box_geom.height = self.saved_height;
2800 self.virtual_x = self.saved_virtual_x;
2801 self.virtual_y = self.saved_virtual_y;
2802 self.was_fullscreen = false;
2803
2804 self.wm_requested.dimensions = Some(crate::window::Dimensions {
2805 width: self.saved_width as u32,
2806 height: self.saved_height as u32,
2807 });
2808 self.wm_requested.bounds = crate::window::Dimensions {
2809 width: self.saved_width as u32,
2810 height: self.saved_height as u32,
2811 };
2812
2813 (*self.server).wm.dirty_windowing();
2814 log::info!("[Fullscreen] Restored window {:?} geometry: {}x{} at ({}, {})", self.get_title_string().as_deref().unwrap_or(""), self.saved_width, self.saved_height, self.saved_virtual_x, self.saved_virtual_y);
2815 }
2816 }
2817
2818 let (width, height) = if !output.is_null() {
2819 let (w, h) = (*output).sent.dimensions();
2820 if self.configure_sent.width != Some(w as u32) || self.configure_sent.height != Some(h as u32) {
2821 self.configure_scheduled.width = Some(w as u32);
2822 self.configure_scheduled.height = Some(h as u32);
2823 self.rendering_scheduled.resend_dimensions = true;
2824 (Some(w as u32), Some(h as u32))
2825 } else {
2826 (None, None)
2827 }
2828 } else if let Some(dimensions) = self.wm_requested.dimensions {
2829 self.rendering_scheduled.resend_dimensions = true;
2830 (Some(dimensions.width), Some(dimensions.height))
2831 } else {
2832 (None, None)
2833 };
2834 self.wm_requested.dimensions = None;
2835
2836 // A tiled window thinks it is maximized: the xdg maximized state
2837 // follows the mode.
2838 let is_maximized_layout = self.tiling_mode == crate::tiling::TilingMode::Tiled;
2839 self.configure_scheduled = Configure {
2840 width,
2841 height,
2842 bounds: self.wm_requested.bounds,
2843 activated,
2844 ssd: self.wm_requested.ssd,
2845 tiled: self.wm_requested.tiled,
2846 capabilities: self.wm_requested.capabilities,
2847 maximized: self.wm_requested.maximized || is_maximized_layout,
2848 inform_fullscreen: self.wm_requested.inform_fullscreen || self.is_fullscreen(),
2849 resizing: self.wm_requested.resizing,
2850 };
2851
2852 let track_configure = match self.impl_type {
2853 WindowImpl::Toplevel(toplevel) => {
2854 if toplevel.is_null() {
2855 false
2856 } else {
2857 (*toplevel).configure()
2858 }
2859 }
2860 WindowImpl::Xwayland(xwindow) => {
2861 if xwindow.is_null() {
2862 false
2863 } else {
2864 (*xwindow).configure()
2865 }
2866 }
2867 WindowImpl::Destroying => unreachable!(),
2868 };
2869
2870 if track_configure && matches!(self.state, WindowState::Mapped) {
2871 self.surfaces.save();
2872 self.send_frame_done();
2873 }
2874
2875 track_configure
2876 }
2877
2878 pub unsafe fn render_start(&mut self) {
2879 match self.impl_type {
2880 WindowImpl::Toplevel(toplevel) => {
2881 if !toplevel.is_null() {
2882 match (*toplevel).configure_state {
2883 ConfigureState::Inflight(serial) => {
2884 (*toplevel).configure_state = ConfigureState::TimedOut(serial);
2885 }
2886 ConfigureState::Acked => {
2887 (*toplevel).configure_state = ConfigureState::TimedOutAcked;
2888 }
2889 ConfigureState::Committed => {
2890 (*toplevel).configure_state = ConfigureState::Idle;
2891 }
2892 _ => {}
2893 }
2894 // The client's committed geometry is the authority on
2895 // this window's size — but only once the client has
2896 // ANSWERED a configure. Before its first ack, `geometry`
2897 // holds the size the client asked for on its own:
2898 // Chromium restores its remembered bounds with
2899 // `set_window_geometry` before it ever acks, and those
2900 // bounds are its window PLUS its CSD shadow insets, so
2901 // they always overhang the cell block the restore just
2902 // gave it. Adopting that wish made it `box_geom` (see
2903 // `render_finish`), the next Tiled arrange covered every
2904 // cell the overhang touched (`snap::tiled_span` floors the
2905 // low edge and CEILS the high one), the grown size was
2906 // saved, and Chrome came back a whole cell wider and
2907 // taller on every login — a one-way ratchet, since each
2908 // session's insets sit on top of the last session's block.
2909 // A window with no restored size still seeds its block
2910 // from the client's first wish, which is where a freshly
2911 // launched app's size comes from.
2912 if !self.restored || (*toplevel).acked_once {
2913 self.rendering_scheduled.width = (*toplevel).geometry.width as u32;
2914 self.rendering_scheduled.height = (*toplevel).geometry.height as u32;
2915 }
2916 }
2917 }
2918 WindowImpl::Xwayland(xwindow) => {
2919 if !xwindow.is_null() {
2920 let s = crate::xwayland_window::x11_scale_for(self.server, (*xwindow).xsurface);
2921 let mut w = crate::xwayland_window::from_x11((*(*xwindow).xsurface).width as i32, s) as u32;
2922 let mut h = crate::xwayland_window::from_x11((*(*xwindow).xsurface).height as i32, s) as u32;
2923 let has_parent = !(*(*xwindow).xsurface).parent.is_null();
2924 if self.is_wine() && !has_parent && !self.is_fullscreen() {
2925 w = w.saturating_sub((crate::xwayland_window::WINE_MARGIN * 2) as u32);
2926 h = h.saturating_sub((crate::xwayland_window::WINE_MARGIN * 2) as u32);
2927 }
2928 self.rendering_scheduled.width = w;
2929 self.rendering_scheduled.height = h;
2930 }
2931 }
2932 WindowImpl::Destroying => {}
2933 }
2934
2935 let presentation_hint = self.presentation_hint();
2936 let sent = &mut self.rendering_sent;
2937 let scheduled = &mut self.rendering_scheduled;
2938
2939 if matches!(self.state, WindowState::Mapped) &&
2940 (scheduled.resend_dimensions ||
2941 scheduled.width != sent.width || scheduled.height != sent.height) {
2942 if !self.object.is_null() {
2943 ffi::wl_resource_post_event(self.object, ffi::ZCCE_WINDOW_V1_DIMENSIONS, scheduled.width as i32, scheduled.height as i32); // sendDimensions
2944 scheduled.resend_dimensions = false;
2945 }
2946 }
2947 sent.width = scheduled.width;
2948 sent.height = scheduled.height;
2949 if sent.presentation_hint != presentation_hint {
2950 if !self.object.is_null() {
2951 let version = ffi::wl_resource_get_version(self.object);
2952 if version >= 4 {
2953 ffi::wl_resource_post_event(self.object, ffi::ZCCE_WINDOW_V1_PRESENTATION_HINT, presentation_hint); // sendPresentationHint
2954 }
2955 }
2956 sent.presentation_hint = presentation_hint;
2957 }
2958 }
2959
2960 pub unsafe fn presentation_hint(&self) -> ffi::zcce_output_v1_presentation_mode {
2961 let root = self.root_surface();
2962 if root.is_null() {
2963 return ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC;
2964 }
2965
2966 // tearing control check stub:
2967 // switch (server.tearing_control_manager.hintFromSurface(root)) {
2968 // .async => .async,
2969 // .vsync => .vsync,
2970 // }
2971 // For now, return VSYNC by default.
2972 ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC
2973 }
2974
2975 pub unsafe fn notify_title(&mut self) {
2976 self.wm_scheduled.dirty_title = true;
2977 self.try_restore();
2978 // A title is arrangement input only through a mode rule that matches
2979 // on it (`title=` in a rule); the built-in policy is what runs — no
2980 // external manager is ever bound to `wm.object` (see the bind
2981 // handler) — so nothing else in the manage sequence reads it. A
2982 // terminal running a busy program retitles several times a second,
2983 // and each retitle used to cost a full manage/arrange/render pass.
2984 // Without a title rule the title's other consumers are the status
2985 // bar's `title` topic and the saved-state file, so feed those directly.
2986 let wm = &mut (*self.server).wm;
2987 if wm.mode_rules.iter().any(|r| r.title_pattern.is_some()) {
2988 wm.dirty_windowing();
2989 } else {
2990 wm.update_status();
2991 wm.schedule_save_state();
2992 }
2993
2994 if !self.foreign_toplevel_handle.is_null() {
2995 let title = self.get_title();
2996 let app_id = self.get_app_id();
2997 let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
2998 title,
2999 app_id,
3000 };
3001 ffi::wlr_ext_foreign_toplevel_handle_v1_update_state(self.foreign_toplevel_handle, &state);
3002 }
3003
3004 if !self.wlr_toplevel_handle.is_null() {
3005 let title = self.get_title();
3006 if !title.is_null() {
3007 ffi::wlr_foreign_toplevel_handle_v1_set_title(self.wlr_toplevel_handle, title);
3008 }
3009 }
3010 }
3011
3012 pub unsafe fn notify_app_id(&mut self) {
3013 self.wm_scheduled.dirty_app_id = true;
3014 let app_id_str = self.get_app_id_string();
3015 if app_id_str.as_deref().map_or(false, |id| id.starts_with("cce-status") || id == "cce-wallpaper") {
3016 self.tiling_mode = crate::tiling::TilingMode::Status;
3017 }
3018 self.try_restore();
3019 (*self.server).wm.dirty_windowing();
3020
3021 if !self.foreign_toplevel_handle.is_null() {
3022 let title = self.get_title();
3023 let app_id = self.get_app_id();
3024 let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
3025 title,
3026 app_id,
3027 };
3028 ffi::wlr_ext_foreign_toplevel_handle_v1_update_state(self.foreign_toplevel_handle, &state);
3029 }
3030
3031 if !self.wlr_toplevel_handle.is_null() {
3032 let app_id = self.get_app_id();
3033 if !app_id.is_null() {
3034 ffi::wlr_foreign_toplevel_handle_v1_set_app_id(self.wlr_toplevel_handle, app_id);
3035 }
3036 }
3037 }
3038
3039 pub unsafe fn render_finish(&mut self) {
3040 let requested = &self.rendering_requested;
3041 let enabled = !requested.hidden && (matches!(self.state, WindowState::Mapped) || matches!(self.state, WindowState::Closing));
3042
3043 let title_ptr = match self.impl_type {
3044 WindowImpl::Xwayland(xwindow) => {
3045 if xwindow.is_null() { std::ptr::null() } else { (*(*xwindow).xsurface).title }
3046 }
3047 _ => std::ptr::null(),
3048 };
3049 let title = if title_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(title_ptr).to_str().unwrap_or("") };
3050 if title.contains("Ubisoft") {
3051 log::info!("render_finish for '{}' (addr={:p}): enabled={} hidden={} state={:?}", title, self as *const Window, enabled, requested.hidden, self.state);
3052 }
3053
3054 ffi::wlr_scene_node_set_enabled(self.tree as *mut ffi::wlr_scene_node, enabled);
3055 ffi::wlr_scene_node_set_enabled(self.popup_tree as *mut ffi::wlr_scene_node, enabled);
3056 if !enabled {
3057 // The segment tree is not a child of `tree`, so disabling the
3058 // window does not hide a revealed border with it.
3059 self.border_reveal = [0.0; 8];
3060 ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, false);
3061 }
3062
3063 if enabled {
3064 let app_id = self.get_app_id_string().unwrap_or_default();
3065 let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
3066 app_id.starts_with("cce-status");
3067 let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
3068 let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status) && !self.droplet_backdrop_on();
3069 let mut ignore_transparent = (*self.server).wm.layout.window_backdrop_blur_ignore_transparent;
3070 if is_status {
3071 ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
3072 }
3073 // Hoisted above the blur setup: the blur node needs this radius, and whether
3074 // the window wants rounded corners at all decides the optimized-blur question
3075 // below.
3076 let radius = if self.is_fullscreen() {
3077 0
3078 } else if requested.circular {
3079 let w = self.rendering_sent.width as i32;
3080 let h = self.rendering_sent.height as i32;
3081 w.min(h) / 2
3082 } else if is_status {
3083 // Status segments draw their own module-box corners. The
3084 // root plate clip is invisible on a bar-thin segment (the
3085 // half-extent cap keeps it inside the transparent band) but
3086 // carves visible sweeps into an EXPANDED segment's in-surface
3087 // menu box once the cap stops binding.
3088 0
3089 } else if self.wm_requested.ssd || is_decorated {
3090 (*self.server).wm.layout.root_plate_corner_radius
3091 } else {
3092 0
3093 };
3094 // Rounded corners do NOT require live blur: the corner shape is applied by the
3095 // standard blur node's sampler (wlr_scene_blur_set_corner_radius) in both modes;
3096 // the optimized node only re-bakes the shared offscreen cache
3097 // (fx_render_pass_add_optimized_blur -> read_to_buffer) and never paints on
3098 // screen. The old `radius > 0` opt-out silently disabled the optimization for
3099 // every (rounded) window, forcing full-backdrop dual-kawase blur per frame per
3100 // translucent window — the DE-wide hover-lag / constant-GPU-load root cause.
3101 let use_optimized = if is_status {
3102 false
3103 } else {
3104 (*self.server).wm.layout.scenefx_optimized_blur
3105 };
3106 let toplevel_w = match self.impl_type {
3107 WindowImpl::Toplevel(toplevel) => {
3108 if toplevel.is_null() { 0 } else { (*toplevel).geometry.width }
3109 }
3110 _ => 0,
3111 };
3112 let toplevel_h = match self.impl_type {
3113 WindowImpl::Toplevel(toplevel) => {
3114 if toplevel.is_null() { 0 } else { (*toplevel).geometry.height }
3115 }
3116 _ => 0,
3117 };
3118 // Status segments are self-sizing: their committed geometry is
3119 // fresher than the render-start snapshot (`rendering_sent`),
3120 // which lags an expand/contract commit by a render pass — same
3121 // rule as the commit-path blur sizing in xdg_toplevel.rs.
3122 let (actual_w, actual_h) = if is_status && toplevel_w > 0 && toplevel_h > 0 {
3123 (toplevel_w as u32, toplevel_h as u32)
3124 } else {
3125 (
3126 if self.rendering_sent.width > 0 { self.rendering_sent.width } else { toplevel_w as u32 },
3127 if self.rendering_sent.height > 0 { self.rendering_sent.height } else { toplevel_h as u32 },
3128 )
3129 };
3130 // Widen squircle corners to the span the clients draw (see
3131 // widen_corner_radius); circles already sit at the half-extent cap.
3132 let radius = if requested.circular { radius } else { widen_corner_radius(radius, actual_w as i32, actual_h as i32) };
3133 // Mid fullscreen-toggle the window draws at the animated rect:
3134 // buffers stretch per-axis toward it (aspect changes in flight,
3135 // so the axes diverge) and the effect extents follow.
3136 let (scale_x, scale_y) = match self.fs_anim {
3137 Some(anim) if actual_w > 0 && actual_h > 0 => {
3138 (anim.w / actual_w as f64, anim.h / actual_h as f64)
3139 }
3140 _ => (self.scale, self.scale),
3141 };
3142 let width = (actual_w as f64 * scale_x).round() as i32;
3143 let height = (actual_h as f64 * scale_y).round() as i32;
3144 ffi::river_scene_node_enable_blur(
3145 self.tree as *mut ffi::wlr_scene_node,
3146 blur_enabled,
3147 use_optimized,
3148 ignore_transparent,
3149 0,
3150 0,
3151 width,
3152 height,
3153 // width/height above are scaled to device pixels, so the radius must be too
3154 // (cf. the window_background rect, which scales it the same way).
3155 (radius as f64 * self.scale) as i32,
3156 );
3157 // The decorated-window predicate feeds two things: the shadow, and
3158 // the bevel's focus glint. Only the shadow honours the tiled switch.
3159 let want_decor = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
3160 let want_shadow = want_decor && self.wants_tiled_shadow();
3161 // The bevel keys on its OWN app list, not on is_decorated:
3162 // every cce-ui app draws its own bevel, so a compositor one
3163 // would sit on top of it.
3164 let want_bevel = !is_status
3165 && !self.is_fullscreen()
3166 && (*self.server).wm.is_beveled_app(&app_id);
3167 self.update_shadow(width, height, radius, want_shadow);
3168 self.update_bevel(width, height, radius, want_bevel, want_decor);
3169 self.update_droplet(width, height);
3170 ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, self.effective_opacity());
3171
3172 // Device px, like the blur radius above: the surface content is
3173 // scaled to its dest size, so an unscaled clip radius would keep
3174 // cutting zoom-1-sized corners into a zoomed-down window (the
3175 // clients' own drawn corners shrink with the buffer).
3176 ffi::river_scene_node_set_corner_radius(
3177 self.surfaces.tree as *mut ffi::wlr_scene_node,
3178 (radius as f64 * self.scale) as i32,
3179 );
3180 ffi::river_scene_rect_set_corner_radius(
3181 self.window_background,
3182 (radius as f64 * self.scale) as i32,
3183 );
3184
3185 struct ScaleData {
3186 scale_x: f64,
3187 scale_y: f64,
3188 ancestor: *mut ffi::wlr_scene_node,
3189 }
3190
3191 unsafe extern "C" fn set_overview_scale_iterator(
3192 buffer: *mut ffi::wlr_scene_buffer,
3193 sx: i32,
3194 sy: i32,
3195 user_data: *mut std::ffi::c_void,
3196 ) {
3197 let data = &*(user_data as *const ScaleData);
3198 let node = buffer as *mut ffi::wlr_scene_node;
3199
3200 let surface = ffi::river_scene_node_get_surface(node);
3201 if !surface.is_null() {
3202 let (w, h, ox, oy) = surface_buffer_extent(buffer, surface);
3203 if data.scale_x == 1.0 && data.scale_y == 1.0 {
3204 ffi::river_scene_buffer_set_dest_size_if_changed(buffer, w, h);
3205 ffi::river_scene_node_set_position_if_changed(node, ox, oy);
3206 } else {
3207 let dest_w = (w as f64 * data.scale_x).round() as i32;
3208 let dest_h = (h as f64 * data.scale_y).round() as i32;
3209 ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
3210
3211 // The parent offset scales like the content; the
3212 // clip origin rides on top of it, scaled the same.
3213 let (px, py) = get_parent_position_relative_to(node, data.ancestor);
3214 let dest_x = (px as f64 * (data.scale_x - 1.0) + ox as f64 * data.scale_x).round() as i32;
3215 let dest_y = (py as f64 * (data.scale_y - 1.0) + oy as f64 * data.scale_y).round() as i32;
3216 ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
3217 }
3218 // Keep the opaque region in step with the dest scale —
3219 // unscaled it covers the shrunken node's translucent CSD
3220 // margins and occlusion culling stops repainting behind
3221 // the client shadow (stale pixels show through it). The
3222 // region must never overclaim, so a briefly non-uniform
3223 // stretch takes the smaller axis.
3224 ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale_x.min(data.scale_y));
3225 }
3226 // Non-surface buffers are frozen SAVED copies (see
3227 // save_surface_tree_iter): their natural buffer size is
3228 // meaningless for geometry — HiDPI clients commit scale-N
3229 // buffers and Chromium pads buffers beyond the surface,
3230 // cropping via viewport src — so rescaling from it ballooned
3231 // ghosts around the window at any zoom change. A frozen copy
3232 // keeps its save-time dest/position; a zoom mid-transaction
3233 // leaves it briefly at the old zoom, which restore corrects.
3234 }
3235
3236 let scale_data_surfaces = ScaleData { scale_x, scale_y, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
3237 ffi::wlr_scene_node_for_each_buffer(
3238 self.surfaces.tree as *mut ffi::wlr_scene_node,
3239 Some(set_overview_scale_iterator),
3240 &scale_data_surfaces as *const ScaleData as *mut std::ffi::c_void,
3241 );
3242
3243 if self.surfaces.saved {
3244 let scale_data_saved = ScaleData { scale_x, scale_y, ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
3245 ffi::wlr_scene_node_for_each_buffer(
3246 self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
3247 Some(set_overview_scale_iterator),
3248 &scale_data_saved as *const ScaleData as *mut std::ffi::c_void,
3249 );
3250 }
3251
3252 let scale_data_popup = ScaleData { scale_x, scale_y, ancestor: self.popup_tree as *mut ffi::wlr_scene_node };
3253 ffi::wlr_scene_node_for_each_buffer(
3254 self.popup_tree as *mut ffi::wlr_scene_node,
3255 Some(set_overview_scale_iterator),
3256 &scale_data_popup as *const ScaleData as *mut std::ffi::c_void,
3257 );
3258 self.last_applied_scale = self.scale;
3259 }
3260
3261 // During an interactive resize, size the box from the client's
3262 // CURRENT committed geometry instead of the render-start snapshot
3263 // (rendering_sent): commits land between render_start and
3264 // render_finish, and the anchored position (rendering_requested.x,
3265 // updated by the commit handler) always tracks the newest commit.
3266 // Pairing it with the older snapshot size clips the surface short
3267 // and makes the anchored edge bounce every cycle.
3268 // self_resized: same reasoning, for a client that resized itself without a
3269 // configure — its newest buffer is already on screen, so rendering_sent is
3270 // behind and would drag the border back to the previous size.
3271 let mut resize_synced = false;
3272 if self.resize_edges.is_some() || self.self_resized {
3273 if let WindowImpl::Toplevel(toplevel) = self.impl_type {
3274 if !toplevel.is_null() {
3275 self.box_geom.width = (*toplevel).geometry.width;
3276 self.box_geom.height = (*toplevel).geometry.height;
3277 resize_synced = true;
3278 }
3279 }
3280 }
3281 if !resize_synced {
3282 if self.rendering_sent.width > 0 {
3283 self.box_geom.width = self.rendering_sent.width as i32;
3284 }
3285 if self.rendering_sent.height > 0 {
3286 self.box_geom.height = self.rendering_sent.height as i32;
3287 }
3288 }
3289 self.self_resized = false;
3290
3291 let mut clip = requested.clip;
3292 let mut content_clip = requested.content_clip;
3293
3294 let output = if !self.wm_requested.fullscreen.is_null() {
3295 self.wm_requested.fullscreen
3296 } else if self.is_fullscreen() {
3297 let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
3298 let mut curr = (*outputs_list).next;
3299 let mut found_output = std::ptr::null_mut();
3300 while curr != outputs_list {
3301 let out = crate::container_of!(curr, crate::output::Output, link);
3302 if (*out).sent.state == crate::output::OutputStateValue::Enabled {
3303 found_output = out;
3304 break;
3305 }
3306 curr = (*curr).next;
3307 }
3308 found_output
3309 } else {
3310 std::ptr::null_mut()
3311 };
3312
3313 if !output.is_null() {
3314 self.box_geom.x = (*output).sent.x;
3315 self.box_geom.y = (*output).sent.y;
3316
3317 let app_id_ptr = self.get_app_id();
3318 let (is_status_bar, is_wallpaper) = if !app_id_ptr.is_null() {
3319 let app_id = std::ffi::CStr::from_ptr(app_id_ptr).to_string_lossy();
3320 (app_id.starts_with("cce-status"), app_id.as_ref() == "cce-wallpaper")
3321 } else {
3322 (false, false)
3323 };
3324
3325 ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, !is_status_bar && !is_wallpaper);
3326 let (width, height) = (*output).sent.dimensions();
3327 ffi::wlr_scene_rect_set_size(self.fullscreen_background, width as i32, height as i32);
3328 clip = ffi::wlr_box { x: 0, y: 0, width: width as i32, height: height as i32 };
3329 content_clip = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
3330
3331 ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
3332 ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
3333 ffi::wlr_scene_node_set_enabled(self.border.top as *mut ffi::wlr_scene_node, false);
3334 ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
3335 ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, false);
3336 // Fullscreen skips draw_borders entirely, and the segment tree
3337 // lives outside this window's tree, so it has to be taken down
3338 // explicitly or a revealed edge would hang over the fullscreen
3339 // surface.
3340 self.border_reveal = [0.0; 8];
3341 ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, false);
3342 } else {
3343 self.box_geom.x = requested.x;
3344 self.box_geom.y = requested.y;
3345 ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, false);
3346 if self.fs_anim.is_none() {
3347 self.draw_borders();
3348 }
3349 }
3350
3351 ffi::river_scene_node_set_position_if_changed(self.tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
3352 ffi::river_scene_node_set_position_if_changed(self.popup_tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
3353
3354 // Mid fullscreen-toggle: draw at the animated rect regardless of which
3355 // branch above ran. The tree overrides its settled position, the black
3356 // backdrop rides the rect (it is what grows/shrinks visually on both
3357 // directions), the clip follows, and the borders stay down until the
3358 // animation settles — the final settling frame re-runs the branch
3359 // above with fs_anim cleared and puts everything back.
3360 if let Some(anim) = self.fs_anim {
3361 let ax = anim.x.round() as i32;
3362 let ay = anim.y.round() as i32;
3363 let aw = (anim.w.round() as i32).max(1);
3364 let ah = (anim.h.round() as i32).max(1);
3365 ffi::river_scene_node_set_position_if_changed(self.tree as *mut ffi::wlr_scene_node, ax, ay);
3366 ffi::river_scene_node_set_position_if_changed(self.popup_tree as *mut ffi::wlr_scene_node, ax, ay);
3367 ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, true);
3368 ffi::wlr_scene_rect_set_size(self.fullscreen_background, aw, ah);
3369 clip = ffi::wlr_box { x: 0, y: 0, width: aw, height: ah };
3370 content_clip = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
3371 ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
3372 ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
3373 ffi::wlr_scene_node_set_enabled(self.border.top as *mut ffi::wlr_scene_node, false);
3374 ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
3375 ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, false);
3376 self.border_reveal = [0.0; 8];
3377 ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, false);
3378 }
3379
3380 // No geometry compensation here: wlr_scene_xdg_surface_create already
3381 // anchors its subtree at the top-left of the xdg window geometry (it
3382 // re-offsets by -geometry on every commit), so subtracting geometry.x/y
3383 // again shifted CSD windows with shadow margins (Electron/Chromium
3384 // floating) up-left by their shadow size, off the desktop grid.
3385 ffi::river_scene_node_set_position_if_changed(self.surfaces.tree as *mut ffi::wlr_scene_node, 0, 0);
3386
3387 self.apply_surface_clip(&clip, &content_clip);
3388
3389 for decorations in [&mut self.decorations_above as *mut ffi::wl_list, &mut self.decorations_below as *mut ffi::wl_list] {
3390 let list_head = decorations as *mut WlList;
3391 let mut curr = (*list_head).next;
3392 while curr != list_head {
3393 let next = (*curr).next;
3394 let dec = crate::container_of!(curr, Decoration, link);
3395 (*dec).render_finish(&clip);
3396 curr = next;
3397 }
3398 }
3399
3400 match self.impl_type {
3401 WindowImpl::Xwayland(xwindow) => {
3402 if !xwindow.is_null() {
3403 if !(*xwindow).surface_tree.is_null() {
3404 let has_parent = !(*(*xwindow).xsurface).parent.is_null();
3405 if self.is_wine() && !has_parent && !self.is_fullscreen() {
3406 ffi::wlr_scene_node_set_position((*xwindow).surface_tree as *mut ffi::wlr_scene_node, -16, -16);
3407 } else {
3408 ffi::wlr_scene_node_set_position((*xwindow).surface_tree as *mut ffi::wlr_scene_node, 0, 0);
3409 }
3410 }
3411 (*xwindow).configure();
3412 }
3413 }
3414 _ => {}
3415 }
3416 }
3417
3418 pub unsafe fn scale_only_render_finish(&mut self) {
3419 // Mid fullscreen-toggle the animation tick owns the buffer dest
3420 // sizes; a uniform-scale pass here would stomp the stretch.
3421 if self.fs_anim.is_some() {
3422 return;
3423 }
3424 // The zoom the overview asks for, times 1/output-scale for an X11
3425 // surface whose buffer is physical pixels (`x11_buffer_scale`).
3426 let eff_scale = self.scale * self.x11_buffer_scale();
3427 if eff_scale == 1.0 {
3428 self.last_applied_scale = 1.0;
3429 return;
3430 }
3431
3432 // No last_applied_scale short-circuit here: wlroots' scene-surface
3433 // commit listener resets a committed buffer's dest size and opaque
3434 // region to the surface's natural extent, so any client repainting
3435 // while scaled (browser animations, caret blink) pops back to full
3436 // size even though the cached scale says nothing changed. This runs
3437 // per rendered frame (output.rs render_and_commit), after commits and
3438 // before build_state, and every setter below is change-checked — an
3439 // already-correct tree produces no damage.
3440 self.last_applied_scale = self.scale;
3441
3442 struct ScaleData {
3443 scale: f64,
3444 ancestor: *mut ffi::wlr_scene_node,
3445 }
3446
3447 unsafe extern "C" fn set_overview_scale_iterator(
3448 buffer: *mut ffi::wlr_scene_buffer,
3449 sx: i32,
3450 sy: i32,
3451 user_data: *mut std::ffi::c_void,
3452 ) {
3453 let data = &*(user_data as *const ScaleData);
3454 let node = buffer as *mut ffi::wlr_scene_node;
3455
3456 let surface = ffi::river_scene_node_get_surface(node);
3457 if !surface.is_null() {
3458 let (w, h, ox, oy) = surface_buffer_extent(buffer, surface);
3459 if data.scale == 1.0 {
3460 ffi::river_scene_buffer_set_dest_size_if_changed(buffer, w, h);
3461 ffi::river_scene_node_set_position_if_changed(node, ox, oy);
3462 } else {
3463 let dest_w = (w as f64 * data.scale).round() as i32;
3464 let dest_h = (h as f64 * data.scale).round() as i32;
3465 ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
3466
3467 let (px, py) = get_parent_position_relative_to(node, data.ancestor);
3468 let dest_x = (px as f64 * (data.scale - 1.0) + ox as f64 * data.scale).round() as i32;
3469 let dest_y = (py as f64 * (data.scale - 1.0) + oy as f64 * data.scale).round() as i32;
3470 ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
3471 }
3472 // Keep the opaque region in step with the dest scale —
3473 // unscaled it covers the shrunken node's translucent CSD
3474 // margins and occlusion culling stops repainting behind
3475 // the client shadow (stale pixels show through it).
3476 ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
3477 }
3478 // Non-surface buffers are frozen SAVED copies (see
3479 // save_surface_tree_iter): their natural buffer size is
3480 // meaningless for geometry — HiDPI clients commit scale-N
3481 // buffers and Chromium pads buffers beyond the surface,
3482 // cropping via viewport src — so rescaling from it ballooned
3483 // ghosts around the window at any zoom change. A frozen copy
3484 // keeps its save-time dest/position; a zoom mid-transaction
3485 // leaves it briefly at the old zoom, which restore corrects.
3486 }
3487
3488 let scale_data_surfaces = ScaleData { scale: eff_scale, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
3489 ffi::wlr_scene_node_for_each_buffer(
3490 self.surfaces.tree as *mut ffi::wlr_scene_node,
3491 Some(set_overview_scale_iterator),
3492 &scale_data_surfaces as *const ScaleData as *mut std::ffi::c_void,
3493 );
3494
3495 if self.surfaces.saved {
3496 let scale_data_saved = ScaleData { scale: eff_scale, ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
3497 ffi::wlr_scene_node_for_each_buffer(
3498 self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
3499 Some(set_overview_scale_iterator),
3500 &scale_data_saved as *const ScaleData as *mut std::ffi::c_void,
3501 );
3502 }
3503
3504 let scale_data_popup = ScaleData { scale: eff_scale, ancestor: self.popup_tree as *mut ffi::wlr_scene_node };
3505 ffi::wlr_scene_node_for_each_buffer(
3506 self.popup_tree as *mut ffi::wlr_scene_node,
3507 Some(set_overview_scale_iterator),
3508 &scale_data_popup as *const ScaleData as *mut std::ffi::c_void,
3509 );
3510
3511 for decorations in [&mut self.decorations_above as *mut ffi::wl_list, &mut self.decorations_below as *mut ffi::wl_list] {
3512 let list_head = decorations as *mut WlList;
3513 let mut curr = (*list_head).next;
3514 while curr != list_head {
3515 let next = (*curr).next;
3516 let dec = crate::container_of!(curr, Decoration, link);
3517 (*dec).scale_only_render_finish();
3518 curr = next;
3519 }
3520 }
3521 }
3522
3523 pub unsafe fn render_viewport_update(&mut self) {
3524 let requested = &self.rendering_requested;
3525 let enabled = !requested.hidden && (matches!(self.state, WindowState::Mapped) || matches!(self.state, WindowState::Closing));
3526
3527 ffi::wlr_scene_node_set_enabled(self.tree as *mut ffi::wlr_scene_node, enabled);
3528 ffi::wlr_scene_node_set_enabled(self.popup_tree as *mut ffi::wlr_scene_node, enabled);
3529 if !enabled {
3530 self.border_reveal = [0.0; 8];
3531 ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, false);
3532 }
3533
3534 if enabled {
3535 self.box_geom.x = requested.x;
3536 self.box_geom.y = requested.y;
3537 ffi::river_scene_node_set_position_if_changed(self.tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
3538 ffi::river_scene_node_set_position_if_changed(self.popup_tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
3539
3540 // Blur stays on through a pan for every window. Non-cce windows
3541 // used to have their blur nodes DESTROYED on the first motion
3542 // frame and rebuilt 120ms after the gesture — a visible pop at
3543 // the end of every pan — on the theory that per-frame blur was
3544 // too expensive to keep during motion. Since the scene freezes
3545 // its optimized-blur caches for the duration of the motion
3546 // (river_scene_set_blur_frozen), a blurred window costs one
3547 // cached-texture sample per frame while moving, so the same
3548 // treatment cce apps always had now applies to all.
3549 // The geometry below is computed for EVERY window regardless: the drop
3550 // shadow has to track the zoom even where live blur does not (see the
3551 // update_shadow call at the end of the block).
3552 let app_id = self.get_app_id_string().unwrap_or_default();
3553 {
3554 let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
3555 app_id.starts_with("cce-status");
3556 let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
3557 let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status) && !self.droplet_backdrop_on();
3558 let mut ignore_transparent = (*self.server).wm.layout.window_backdrop_blur_ignore_transparent;
3559 if is_status {
3560 ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
3561 }
3562 // Same radius/optimized reasoning as set_rendering_state. Before, this path
3563 // set no radius at all, so a blur node recreated during a pan came back
3564 // square and stayed that way.
3565 let radius = if self.is_fullscreen() {
3566 0
3567 } else if requested.circular {
3568 let w = self.rendering_sent.width as i32;
3569 let h = self.rendering_sent.height as i32;
3570 w.min(h) / 2
3571 } else if is_status {
3572 // Same status exemption as set_rendering_state — the two
3573 // paths drive the same nodes and must agree.
3574 0
3575 } else if self.wm_requested.ssd || is_decorated {
3576 (*self.server).wm.layout.root_plate_corner_radius
3577 } else {
3578 0
3579 };
3580 // Rounded corners do NOT require live blur: the corner shape is applied by the
3581 // standard blur node's sampler (wlr_scene_blur_set_corner_radius) in both modes;
3582 // the optimized node only re-bakes the shared offscreen cache
3583 // (fx_render_pass_add_optimized_blur -> read_to_buffer) and never paints on
3584 // screen. The old `radius > 0` opt-out silently disabled the optimization for
3585 // every (rounded) window, forcing full-backdrop dual-kawase blur per frame per
3586 // translucent window — the DE-wide hover-lag / constant-GPU-load root cause.
3587 let use_optimized = if is_status {
3588 false
3589 } else {
3590 (*self.server).wm.layout.scenefx_optimized_blur
3591 };
3592 let toplevel_w = match self.impl_type {
3593 WindowImpl::Toplevel(toplevel) => {
3594 if toplevel.is_null() { 0 } else { (*toplevel).geometry.width }
3595 }
3596 _ => 0,
3597 };
3598 let toplevel_h = match self.impl_type {
3599 WindowImpl::Toplevel(toplevel) => {
3600 if toplevel.is_null() { 0 } else { (*toplevel).geometry.height }
3601 }
3602 _ => 0,
3603 };
3604 // Same self-sizing rule as set_rendering_state above.
3605 let (actual_w, actual_h) = if is_status && toplevel_w > 0 && toplevel_h > 0 {
3606 (toplevel_w as u32, toplevel_h as u32)
3607 } else {
3608 (
3609 if self.rendering_sent.width > 0 { self.rendering_sent.width } else { toplevel_w as u32 },
3610 if self.rendering_sent.height > 0 { self.rendering_sent.height } else { toplevel_h as u32 },
3611 )
3612 };
3613 // Same span widening as set_rendering_state — the two paths
3614 // drive the same blur node and must agree.
3615 let radius = if requested.circular { radius } else { widen_corner_radius(radius, actual_w as i32, actual_h as i32) };
3616 let width = (actual_w as f64 * self.scale) as i32;
3617 let height = (actual_h as f64 * self.scale) as i32;
3618 ffi::river_scene_node_enable_blur(
3619 self.tree as *mut ffi::wlr_scene_node,
3620 blur_enabled,
3621 use_optimized,
3622 ignore_transparent,
3623 0,
3624 0,
3625 width,
3626 height,
3627 (radius as f64 * self.scale) as i32,
3628 );
3629 // Every window, blurred or not: the shadow's size, blur sigma,
3630 // offset and — critically — the clipped region that punches the
3631 // window out of it are all scale-dependent, and nothing else on
3632 // the motion path touches them. Left stale they keep the scale
3633 // from before the gesture, so the punch-out overruns the shrunken
3634 // window and swallows the shadow whole.
3635 // The decorated-window predicate feeds two things: the shadow, and
3636 // the bevel's focus glint. Only the shadow honours the tiled switch.
3637 let want_decor = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
3638 let want_shadow = want_decor && self.wants_tiled_shadow();
3639 // The bevel keys on its OWN app list, not on is_decorated:
3640 // every cce-ui app draws its own bevel, so a compositor one
3641 // would sit on top of it.
3642 let want_bevel = !is_status
3643 && !self.is_fullscreen()
3644 && (*self.server).wm.is_beveled_app(&app_id);
3645 self.update_shadow(width, height, radius, want_shadow);
3646 self.update_bevel(width, height, radius, want_bevel, want_decor);
3647 self.update_droplet(width, height);
3648 }
3649
3650 self.scale_only_render_finish();
3651 self.draw_borders();
3652 }
3653 }
3654
3655 /// The root plate / content-clip corner radius in logical px, before span
3656 /// widening. Single source for every writer of that radius: the two render
3657 /// paths clip the surface with it, and `draw_borders` shapes the root plate
3658 /// rect with it. Those disagreed — draw_borders applied the BORDER ring's
3659 /// radius to the root plate node and, running last, silently overrode the
3660 /// value set_rendering_state had just written, making
3661 /// `root_plate_corner_radius` dead config.
3662 pub unsafe fn root_plate_radius_base(&self) -> i32 {
3663 if self.is_fullscreen() {
3664 return 0;
3665 }
3666 if self.rendering_requested.circular {
3667 let w = self.rendering_sent.width as i32;
3668 let h = self.rendering_sent.height as i32;
3669 return w.min(h) / 2;
3670 }
3671 let app_id = self.get_app_id_string().unwrap_or_default();
3672 let is_status = self.tiling_mode == crate::tiling::TilingMode::Status
3673 || app_id.starts_with("cce-status");
3674 if is_status {
3675 return 0;
3676 }
3677 let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
3678 if self.wm_requested.ssd || is_decorated {
3679 (*self.server).wm.layout.root_plate_corner_radius
3680 } else {
3681 0
3682 }
3683 }
3684
3685 /// Sync the drop shadow with the current geometry. `width`/`height` are the
3686 /// content size in device px, `radius` the corner radius in logical px (as
3687 /// computed for the blur/rounding paths). scenefx's box-shadow shader draws
3688 /// the shadow of a box inset by sigma on all sides of the node box, so the
3689 /// node is padded by sigma and offset so the casting box lands exactly on
3690 /// the window, displaced by the configured offset — which should point away
3691 /// from the light (down-right for the DE's default top-left light). The
3692 /// window's own box is punched out via the clipped region so the shadow
3693 /// darkens only the desktop around the window, never the (translucent)
3694 /// window itself.
3695 pub unsafe fn update_shadow(&self, width: i32, height: i32, radius: i32, want: bool) {
3696 if self.shadow.is_null() {
3697 return;
3698 }
3699 let node = &mut (*self.shadow).node as *mut ffi::wlr_scene_node;
3700 let layout = &(*self.server).wm.layout;
3701 let enabled = want && layout.shadow_enabled && width > 0 && height > 0;
3702 ffi::wlr_scene_node_set_enabled(node, enabled);
3703 if !enabled {
3704 return;
3705 }
3706 let sigma = (layout.shadow_sigma as f64 * self.scale) as f32;
3707 let pad = sigma.ceil() as i32;
3708 let ox = (layout.shadow_offset_x as f64 * self.scale) as i32;
3709 let oy = (layout.shadow_offset_y as f64 * self.scale) as i32;
3710 let radius_dev = (radius as f64 * self.scale) as i32;
3711 ffi::wlr_scene_shadow_set_color(self.shadow, layout.shadow_color.as_ptr());
3712 ffi::wlr_scene_shadow_set_blur_sigma(self.shadow, sigma);
3713 ffi::wlr_scene_shadow_set_corner_radius(self.shadow, radius_dev);
3714 ffi::wlr_scene_shadow_set_size(self.shadow, width + 2 * pad, height + 2 * pad);
3715 ffi::river_scene_node_set_position_if_changed(node, -pad + ox, -pad + oy);
3716 let r = radius_dev.clamp(0, u16::MAX as i32) as u16;
3717 ffi::wlr_scene_shadow_set_clipped_region(self.shadow, ffi::clipped_region {
3718 area: ffi::wlr_box { x: pad - ox, y: pad - oy, width, height },
3719 corners: ffi::fx_corner_radii {
3720 top_left: r, top_right: r, bottom_right: r, bottom_left: r,
3721 },
3722 });
3723 }
3724
3725 /// Sync the edge bevel with the current geometry. `width`/`height` are the
3726 /// content size in device px and `radius` the corner radius in logical px,
3727 /// exactly as `update_shadow` takes them. The rim is drawn INSIDE that box
3728 /// (see the shader), so it overlays the client's outermost pixels and needs
3729 /// no room of its own.
3730 ///
3731 /// The light direction is the DE's convention — the same top-left source
3732 /// the drop shadow is offset away from — so a window reads as a slab lit
3733 /// from the same place as everything else on the desktop.
3734 /// Is this window any seat's keyboard focus? The window's `activated`
3735 /// field is a configure-time snapshot, not live state, so live answers
3736 /// come from the seats.
3737 pub unsafe fn is_seat_focused(&self) -> bool {
3738 let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
3739 let mut curr = (*seats).next;
3740 while curr != seats {
3741 let seat = crate::container_of!(curr, crate::seat::Seat, link);
3742 if let crate::seat::Focus::Window(w) = (*seat).focused {
3743 if w == self as *const Window as *mut Window {
3744 return true;
3745 }
3746 }
3747 curr = (*curr).next;
3748 }
3749 false
3750 }
3751
3752 /// Whether this is the window the adjust-mode handles belong to: the
3753 /// toplevel under some seat's pointer (`Cursor::adjust_hover`), focused
3754 /// or not, in overview and with Super held alike — the handles are
3755 /// shown on what the pointer is over and on nothing else, so a pointer
3756 /// on the background shows none. (Overview's hover-to-focus still moves
3757 /// focus with the pointer, but focus is not what the ring keys on: a
3758 /// pointer resting on the background would otherwise keep the last
3759 /// window's ring up.) The reveal (`step_border_fade`), the drawn handles
3760 /// and catchers (`draw_borders`) and the hit test
3761 /// (`cursor::get_border_zone`) all ask this one predicate, so the ring
3762 /// cannot be drawn on one window and grabbed on another.
3763 pub unsafe fn is_adjust_target(&self) -> bool {
3764 let me = self as *const Window as *mut Window;
3765 let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
3766 let mut curr = (*seats).next;
3767 while curr != seats {
3768 let seat = crate::container_of!(curr, crate::seat::Seat, link);
3769 if (*seat).cursor.adjust_hover == me {
3770 return true;
3771 }
3772 curr = (*curr).next;
3773 }
3774 false
3775 }
3776
3777 pub unsafe fn update_bevel(&self, width: i32, height: i32, radius: i32, want: bool, want_focus: bool) {
3778 if self.bevel.is_null() {
3779 return;
3780 }
3781 let node = &mut (*self.bevel).node as *mut ffi::wlr_scene_node;
3782 let layout = &(*self.server).wm.layout;
3783 // Focused-window treatment: the rim highlight wraps all four sides
3784 // in the accent (the DE focus glint). Focus is read off the seats —
3785 // the window's `activated` field is a configure-time snapshot, not
3786 // live state — and this runs on both render paths, so a focus switch
3787 // restyles on the next frame. A focused window that is NOT in the
3788 // bevel app list still enables the node: the shader's focus branch
3789 // draws ONLY the glint, so it lays cleanly over a cce-ui app's own
3790 // client-side bevel instead of doubling its shading.
3791 let focused = self.is_seat_focused();
3792 let enabled = (want || (focused && want_focus))
3793 && layout.bevel_enabled
3794 && layout.bevel_thickness > 0.0
3795 && width > 0
3796 && height > 0;
3797 ffi::wlr_scene_node_set_enabled(node, enabled);
3798 if !enabled {
3799 return;
3800 }
3801
3802 // Device px, like the blur radius and shadow sigma: the content is
3803 // scaled to its dest size, so an unscaled rim would keep its zoom-1
3804 // width while the window shrinks.
3805 let thickness = (layout.bevel_thickness as f64 * self.scale) as f32;
3806 let radius_dev = (radius as f64 * self.scale) as i32;
3807
3808 // Light from the top-left, matching shadow_offset_x/y pointing away
3809 // from it. Normalized here so the shader can take it as-is.
3810 let (lx, ly) = (layout.bevel_light_x, layout.bevel_light_y);
3811 let len = (lx * lx + ly * ly).sqrt();
3812 let (lx, ly) = if len > 1e-6 { (lx / len, ly / len) } else { (-0.7071, -0.7071) };
3813
3814 ffi::wlr_scene_bevel_set_size(self.bevel, width, height);
3815 ffi::wlr_scene_bevel_set_corner_radius(self.bevel, radius_dev);
3816 ffi::wlr_scene_bevel_set_thickness(self.bevel, thickness.max(1.0));
3817 ffi::wlr_scene_bevel_set_light(
3818 self.bevel,
3819 lx,
3820 ly,
3821 layout.bevel_light_intensity,
3822 layout.bevel_shade_intensity,
3823 );
3824 ffi::wlr_scene_bevel_set_shoulder(self.bevel, layout.bevel_shoulder);
3825 ffi::wlr_scene_bevel_set_color(self.bevel, layout.bevel_color.as_ptr());
3826 ffi::wlr_scene_bevel_set_focus(
3827 self.bevel,
3828 if focused { 1.0 } else { 0.0 },
3829 layout.bevel_focus_sharpness,
3830 layout.bevel_focus_color.as_ptr(),
3831 );
3832 ffi::river_scene_node_set_position_if_changed(node, 0, 0);
3833 }
3834 /// Sync the droplet backdrop-refraction node for a droplet-styled status
3835 /// segment. Called from BOTH render paths, like update_bevel — one-path
3836 /// effects freeze at the pre-gesture zoom (the shadow's old trap).
3837 pub unsafe fn update_droplet(&self, width: i32, height: i32) {
3838 if self.droplet.is_null() {
3839 return;
3840 }
3841 let node = &mut (*self.droplet).node as *mut ffi::wlr_scene_node;
3842 let layout = &(*self.server).wm.layout;
3843 let is_status = self.tiling_mode == crate::tiling::TilingMode::Status;
3844 // Only bar-strip segments: an expanded (menu) segment is taller than
3845 // the bar and draws its own grown drop client-side — refracting the
3846 // collapsed silhouette beneath it would be wrong.
3847 let enabled = is_status
3848 && layout.status_droplet.is_some()
3849 && width > 0
3850 && height > 0
3851 && height <= layout.bar_height as i32;
3852 if !enabled {
3853 ffi::wlr_scene_node_set_enabled(node, false);
3854 return;
3855 }
3856 let spec = cce_ui::scene::paint::DropletSpec::parse(
3857 layout.status_droplet.as_deref().unwrap_or(""),
3858 );
3859 if spec.refr <= 0.0 && spec.ghost <= 0.0 {
3860 ffi::wlr_scene_node_set_enabled(node, false);
3861 return;
3862 }
3863 ffi::wlr_scene_node_set_enabled(node, true);
3864
3865 // Match the client's drop box: inset 1px from the surface bottom.
3866 // Camera-zoom scaling like the bevel; output scale is applied by the
3867 // render pass itself.
3868 let w = width as f32;
3869 let h = (height as f32 - 1.0).max(1.0);
3870 let (sr, ar, bow) = spec.resolve_silhouette(w, h);
3871 let k = (spec.blend.max(0.0) * h).max(1.0);
3872 let band = (spec.band.max(0.05) * h).max(1.0);
3873 let zs = self.scale as f32;
3874 ffi::wlr_scene_droplet_set_size(self.droplet, width, height);
3875 ffi::wlr_scene_droplet_set_silhouette(
3876 self.droplet,
3877 ar * zs,
3878 sr * zs,
3879 bow * zs,
3880 k * zs,
3881 spec.curve.clamp(2.0, 6.0),
3882 );
3883 ffi::wlr_scene_droplet_set_lens(self.droplet, band * zs, spec.refr * zs, spec.ghost.clamp(0.0, 1.0));
3884 ffi::river_scene_node_set_position_if_changed(node, 0, 0);
3885 }
3886 /// True when this status segment's droplet backdrop node is live. The
3887 /// per-window blur must yield to it: the blur pass would composite the
3888 /// UNREFRACTED cached backdrop over the lens output.
3889 pub unsafe fn droplet_backdrop_on(&self) -> bool {
3890 if self.droplet.is_null() || self.tiling_mode != crate::tiling::TilingMode::Status {
3891 return false;
3892 }
3893 match (*self.server).wm.layout.status_droplet.as_deref() {
3894 Some(raw) => {
3895 let spec = cce_ui::scene::paint::DropletSpec::parse(raw);
3896 spec.refr > 0.0 || spec.ghost > 0.0
3897 }
3898 None => false,
3899 }
3900 }
3901
3902
3903
3904 /// The opacity the scene tree gets: the requested one, scaled down by the
3905 /// adjust-mode overlap dim (`adjust_dim`, 0..1) toward
3906 /// `border.overlap_opacity`, and again by the map/close fade
3907 /// (`map_fade`), which rests at 1.0 whenever no fade is in flight.
3908 pub unsafe fn effective_opacity(&self) -> f32 {
3909 let floor = (*self.server).wm.layout.border_overlap_opacity;
3910 self.rendering_requested.opacity
3911 * (1.0 - self.adjust_dim.clamp(0.0, 1.0) * (1.0 - floor))
3912 * self.map_fade.clamp(0.0, 1.0)
3913 }
3914
3915 /// Whether this window takes the map/close fade at all. Surfaces that are
3916 /// part of the desktop itself rather than something the user opened — the
3917 /// status segments, the wallpaper, the grid layer — are left alone: they
3918 /// map once at login and a dissolve there reads as the desktop failing to
3919 /// draw. Same exclusion list `adjust_dim_wanted` uses, for the same
3920 /// reason: these are not windows the user thinks of as opening.
3921 pub unsafe fn wants_map_fade(&self) -> bool {
3922 !self.is_status_bar() && !self.is_wallpaper() && !self.is_grid()
3923 }
3924
3925 /// Begin a fade toward `target` (0.0 out, 1.0 in) over `ms`, and arm the
3926 /// timer that steps it. A `ms` of 0 (or fading disabled) snaps instead,
3927 /// so every caller can treat this as "put the window at `target`".
3928 pub unsafe fn start_map_fade(&mut self, target: f32, ms: u32) {
3929 self.map_fade_target = target.clamp(0.0, 1.0);
3930 if ms == 0 || !self.wants_map_fade() {
3931 self.map_fade = self.map_fade_target;
3932 ffi::river_scene_node_set_opacity(
3933 self.tree as *mut ffi::wlr_scene_node,
3934 self.effective_opacity(),
3935 );
3936 return;
3937 }
3938 // Ticks at 16 ms; at least one step, so a sub-frame duration still
3939 // lands on the target rather than dividing by zero.
3940 let ticks = ((ms as f32) / 16.0).max(1.0);
3941 self.map_fade_step = ((self.map_fade_target - self.map_fade).abs() / ticks).max(1.0e-4);
3942 ffi::river_scene_node_set_opacity(
3943 self.tree as *mut ffi::wlr_scene_node,
3944 self.effective_opacity(),
3945 );
3946 (*self.server).wm.arm_border_fade();
3947 }
3948
3949 /// Advance the map/close fade one tick toward `map_fade_target`, applying
3950 /// the opacity as it goes. Returns true while still in motion, like
3951 /// `step_adjust_dim`.
3952 pub unsafe fn step_map_fade(&mut self) -> bool {
3953 let delta = self.map_fade_target - self.map_fade;
3954 if delta.abs() <= self.map_fade_step {
3955 if self.map_fade == self.map_fade_target {
3956 return false;
3957 }
3958 self.map_fade = self.map_fade_target;
3959 } else {
3960 self.map_fade += self.map_fade_step * delta.signum();
3961 }
3962 ffi::river_scene_node_set_opacity(
3963 self.tree as *mut ffi::wlr_scene_node,
3964 self.effective_opacity(),
3965 );
3966 true
3967 }
3968
3969 /// Whether this window should be dimmed right now: adjust mode is on,
3970 /// this is a Floating window, and it lies ABOVE the adjust target in the
3971 /// render stack while overlapping it on screen — where it would cover
3972 /// the target's handles. Windows under the target are left alone; they
3973 /// hide nothing.
3974 pub unsafe fn adjust_dim_wanted(&self) -> bool {
3975 let wm = &(*self.server).wm;
3976 if !wm.window_adjust_active()
3977 || self.closed
3978 || self.tiling_mode != crate::tiling::TilingMode::Floating
3979 || self.is_status_bar()
3980 || self.is_wallpaper()
3981 || self.is_grid()
3982 {
3983 return false;
3984 }
3985 let me = self as *const Window as *mut Window;
3986 let on_screen = |w: *mut Window| -> (f64, f64, f64, f64) {
3987 let sc = if (*w).scale > 0.0 { (*w).scale } else { 1.0 };
3988 let g = (*w).box_geom;
3989 (g.x as f64, g.y as f64, g.width as f64 * sc, g.height as f64 * sc)
3990 };
3991 let (mx, my, mw, mh) = on_screen(me);
3992 // The render list runs bottom to top (raise_window moves to the
3993 // tail), so a target met before this window sits beneath it.
3994 let list = &wm.rendering_requested.list as *const ffi::wl_list as *mut WlList;
3995 let mut curr = (*list).next;
3996 let mut covered = false;
3997 while curr != list {
3998 let node = crate::container_of!(curr, crate::wm_node::WmNode, link);
3999 if let crate::wm_node::WmNodeType::Window(w) = (*node).get() {
4000 if w == me {
4001 return covered;
4002 }
4003 if !w.is_null()
4004 && !(*w).closed
4005 && window_takes_handles(w)
4006 && (*w).is_adjust_target()
4007 {
4008 let (tx, ty, tw, th) = on_screen(w);
4009 if mx < tx + tw && tx < mx + mw && my < ty + th && ty < my + mh {
4010 covered = true;
4011 }
4012 }
4013 }
4014 curr = (*curr).next;
4015 }
4016 false
4017 }
4018
4019 /// Advance the overlap dim one tick toward where `adjust_dim_wanted`
4020 /// says it should rest, applying the opacity as it goes. Returns true
4021 /// while still in motion, like `step_border_fade`.
4022 pub unsafe fn step_adjust_dim(&mut self) -> bool {
4023 let target = if self.adjust_dim_wanted() { 1.0 } else { 0.0 };
4024 let delta = target - self.adjust_dim;
4025 let moving;
4026 if delta.abs() <= BORDER_FADE_EPSILON {
4027 if self.adjust_dim == target {
4028 return false;
4029 }
4030 self.adjust_dim = target;
4031 moving = false;
4032 } else {
4033 self.adjust_dim += delta * BORDER_FADE_STEP;
4034 moving = true;
4035 }
4036 ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, self.effective_opacity());
4037 moving
4038 }
4039
4040 /// Advance the hover fade one tick. Every zone eases toward 1.0 if it is
4041 /// the one under the pointer and 0.0 otherwise. Returns true while any
4042 /// zone is still in motion, so the caller knows to schedule another tick.
4043 pub unsafe fn step_border_fade(&mut self) -> bool {
4044 let mut moving = false;
4045 let mut changed = false;
4046 // The adjust TARGET — the window under the pointer — shows its whole
4047 // ring for as long as the mode is on; other windows show nothing.
4048 // The ring follows the pointer from window to window, each swap
4049 // easing through this same fade. Hover still reads through on the revealed ring, as
4050 // `color_for` paints the hovered zone in hover_color over the full
4051 // reveal.
4052 let all_on = (*self.server).wm.window_adjust_active()
4053 && window_takes_handles(self as *mut Window)
4054 && self.is_adjust_target();
4055 for elem in BorderElement::ALL {
4056 let i = elem.index();
4057 let target = if all_on || self.hovered_border_element == Some(elem) { 1.0 } else { 0.0 };
4058 let delta = target - self.border_reveal[i];
4059 if delta.abs() <= BORDER_FADE_EPSILON {
4060 if self.border_reveal[i] != target {
4061 self.border_reveal[i] = target;
4062 changed = true;
4063 }
4064 continue;
4065 }
4066 self.border_reveal[i] += delta * BORDER_FADE_STEP;
4067 moving = true;
4068 changed = true;
4069 }
4070 // A hover swap on a fully revealed ring moves nothing above, but the
4071 // shader still has to be told which zone to paint.
4072 if self.hovered_border_element != self.border_hover_drawn {
4073 changed = true;
4074 }
4075 if changed {
4076 self.draw_borders();
4077 }
4078 moving
4079 }
4080
4081 /// The output a fullscreen window fills: the one the WM pinned it to, or
4082 /// the first enabled output (the same fallback manage/render use).
4083 pub unsafe fn fullscreen_output(&self) -> *mut crate::output::Output {
4084 if !self.wm_requested.fullscreen.is_null() {
4085 return self.wm_requested.fullscreen;
4086 }
4087 let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
4088 let mut curr = (*outputs_list).next;
4089 while curr != outputs_list {
4090 let out = crate::container_of!(curr, crate::output::Output, link);
4091 if (*out).sent.state == crate::output::OutputStateValue::Enabled {
4092 return out;
4093 }
4094 curr = (*curr).next;
4095 }
4096 std::ptr::null_mut()
4097 }
4098
4099 /// Arms the fullscreen-toggle animation at the window's current on-screen
4100 /// rect. Called from manage_finish on the enter/exit transition, before
4101 /// the settled geometry is rewritten; a re-toggle mid-flight continues
4102 /// from wherever the previous animation had reached. Sized with
4103 /// last_applied_scale (the scale actually drawn) because self.scale has
4104 /// already been rewritten to the destination state's scale by the arrange
4105 /// pass in this same cycle.
4106 unsafe fn start_fs_anim(&mut self) {
4107 if !matches!(self.impl_type, WindowImpl::Toplevel(_))
4108 || !matches!(self.state, WindowState::Mapped)
4109 || self.box_geom.width <= 0
4110 || self.box_geom.height <= 0
4111 {
4112 return;
4113 }
4114 let (x, y, w, h) = if let Some(a) = self.fs_anim {
4115 (a.x, a.y, a.w, a.h)
4116 } else {
4117 let s = if self.last_applied_scale > 0.0 { self.last_applied_scale } else { 1.0 };
4118 (
4119 self.box_geom.x as f64,
4120 self.box_geom.y as f64,
4121 self.box_geom.width as f64 * s,
4122 self.box_geom.height as f64 * s,
4123 )
4124 };
4125 self.fs_anim = Some(FsAnim { x, y, w, h, moved: false, ticks: 0 });
4126 (*self.server).wm.arm_border_fade();
4127 }
4128
4129 /// One tick of the fullscreen-toggle animation. Returns true while the
4130 /// caller should re-render (including the final settling frame). The
4131 /// target rect is recomputed live every tick — the output box while
4132 /// fullscreen, else the arranged position at the last configured size —
4133 /// so it tracks the client's asynchronous resize instead of freezing a
4134 /// stale goal on the first frame.
4135 pub unsafe fn step_fs_anim(&mut self) -> bool {
4136 let Some(mut anim) = self.fs_anim else {
4137 return false;
4138 };
4139
4140 let (tx, ty, tw, th) = if self.is_fullscreen() {
4141 let output = self.fullscreen_output();
4142 if output.is_null() {
4143 self.fs_anim = None;
4144 return true;
4145 }
4146 let (w, h) = (*output).sent.dimensions();
4147 ((*output).sent.x as f64, (*output).sent.y as f64, w as f64, h as f64)
4148 } else {
4149 let w = self.configure_sent.width.map(|w| w as i32).unwrap_or(self.box_geom.width);
4150 let h = self.configure_sent.height.map(|h| h as i32).unwrap_or(self.box_geom.height);
4151 (
4152 self.rendering_requested.x as f64,
4153 self.rendering_requested.y as f64,
4154 w as f64 * self.scale,
4155 h as f64 * self.scale,
4156 )
4157 };
4158
4159 anim.ticks += 1;
4160 let dx = tx - anim.x;
4161 let dy = ty - anim.y;
4162 let dw = tw - anim.w;
4163 let dh = th - anim.h;
4164 let settled = dx.abs() < FS_ANIM_EPSILON
4165 && dy.abs() < FS_ANIM_EPSILON
4166 && dw.abs() < FS_ANIM_EPSILON
4167 && dh.abs() < FS_ANIM_EPSILON;
4168 if !settled {
4169 anim.moved = true;
4170 }
4171 if (settled && anim.moved) || anim.ticks > FS_ANIM_MAX_TICKS {
4172 self.fs_anim = None;
4173 return true;
4174 }
4175 anim.x += dx * FS_ANIM_STEP;
4176 anim.y += dy * FS_ANIM_STEP;
4177 anim.w += dw * FS_ANIM_STEP;
4178 anim.h += dh * FS_ANIM_STEP;
4179 self.fs_anim = Some(anim);
4180 true
4181 }
4182
4183 /// Outward extent (unscaled px) the interactive border may reach on each
4184 /// side — `[left, right, top, bottom]` — after the foam rule against the
4185 /// other windows: where two windows' bands would overlap across a gap,
4186 /// each band stops at the gap's midline (the ramp key-ring behavior,
4187 /// rectangular — the wall is equidistant from the two content edges).
4188 /// Stacked windows (content rects overlapping) do not clip each other,
4189 /// mirroring the rings' degenerate-distance guard. Per-side, not
4190 /// per-span: one near neighbor claims the whole facing side.
4191 pub unsafe fn border_side_extents(&self, band_unscaled: f64) -> [f64; 4] {
4192 let scale = if self.scale > 0.0 { self.scale } else { 1.0 };
4193 let band = band_unscaled * scale;
4194 let ax0 = self.box_geom.x as f64;
4195 let ay0 = self.box_geom.y as f64;
4196 let ax1 = ax0 + self.box_geom.width as f64 * scale;
4197 let ay1 = ay0 + self.box_geom.height as f64 * scale;
4198 let mut ext = [band; 4]; // left, right, top, bottom (layout px)
4199
4200 let self_ptr = self as *const Window as *mut Window;
4201 for &other in (*self.server).wm.windows.iter() {
4202 if other.is_null() || other == self_ptr {
4203 continue;
4204 }
4205 let o = &*other;
4206 if o.closed
4207 || o.minimized
4208 || o.rendering_requested.hidden
4209 || o.rendering_requested.circular
4210 || matches!(
4211 o.tiling_mode,
4212 crate::tiling::TilingMode::Popup
4213 | crate::tiling::TilingMode::Fullscreen
4214 | crate::tiling::TilingMode::Status
4215 )
4216 || o.is_status_bar()
4217 || o.is_wallpaper()
4218 {
4219 continue;
4220 }
4221 let os = if o.scale > 0.0 { o.scale } else { 1.0 };
4222 let bx0 = o.box_geom.x as f64;
4223 let by0 = o.box_geom.y as f64;
4224 let bx1 = bx0 + o.box_geom.width as f64 * os;
4225 let by1 = by0 + o.box_geom.height as f64 * os;
4226 // Stacked: keep the full band.
4227 if bx0 < ax1 && bx1 > ax0 && by0 < ay1 && by1 > ay0 {
4228 continue;
4229 }
4230 let ob = border_band_width(o.rendering_requested.border.width) * os;
4231 // Spans (including bands) must overlap for a wall to exist.
4232 let v_overlap = by0 - ob < ay1 + band && by1 + ob > ay0 - band;
4233 let h_overlap = bx0 - ob < ax1 + band && bx1 + ob > ax0 - band;
4234 if v_overlap {
4235 if bx0 >= ax1 {
4236 let gap = bx0 - ax1;
4237 if gap < band + ob {
4238 ext[1] = ext[1].min((gap / 2.0).max(0.0));
4239 }
4240 } else if bx1 <= ax0 {
4241 let gap = ax0 - bx1;
4242 if gap < band + ob {
4243 ext[0] = ext[0].min((gap / 2.0).max(0.0));
4244 }
4245 }
4246 }
4247 if h_overlap {
4248 if by0 >= ay1 {
4249 let gap = by0 - ay1;
4250 if gap < band + ob {
4251 ext[3] = ext[3].min((gap / 2.0).max(0.0));
4252 }
4253 } else if by1 <= ay0 {
4254 let gap = ay0 - by1;
4255 if gap < band + ob {
4256 ext[2] = ext[2].min((gap / 2.0).max(0.0));
4257 }
4258 }
4259 }
4260 }
4261 [ext[0] / scale, ext[1] / scale, ext[2] / scale, ext[3] / scale]
4262 }
4263
4264 }
4265
4266 /// Does this window get resize handles at all?
4267 ///
4268 /// The single answer for both halves — `cursor::get_border_zone`'s hit test
4269 /// and `draw_borders`' visuals — so a window can never show a handle it
4270 /// would not honour, or honour one it does not show. Excluded: the internal
4271 /// roles that are not user-geometry (Popup, Fullscreen, Status), Utility
4272 /// (self-sizing by definition — the client owns its size), circular windows
4273 /// (no rectangular ring to hug), and hidden ones.
4274 pub unsafe fn window_takes_handles(window: *mut Window) -> bool {
4275 !matches!(
4276 (*window).tiling_mode,
4277 crate::tiling::TilingMode::Popup
4278 | crate::tiling::TilingMode::Fullscreen
4279 | crate::tiling::TilingMode::Status
4280 | crate::tiling::TilingMode::Utility
4281 ) && !(*window).rendering_requested.circular
4282 && !(*window).rendering_requested.hidden
4283 }
4284
4285 impl Window {
4286 pub unsafe fn draw_borders(&mut self) {
4287 // Taken before `requested` borrows self: `window_takes_handles` is
4288 // the shared predicate with cursor::get_border_zone and must not be
4289 // duplicated here just to satisfy borrowck.
4290 let self_ptr = self as *mut Window;
4291 let requested = &self.rendering_requested;
4292
4293 let border = &requested.border;
4294 let border_color = border.color;
4295 ffi::river_scene_node_set_position_if_changed(self.window_background as *mut ffi::wlr_scene_node, 0, 0);
4296 let bg_width = (self.box_geom.width as f64 * self.scale) as i32;
4297 let bg_height = (self.box_geom.height as f64 * self.scale) as i32;
4298 ffi::river_scene_rect_set_size_if_changed(self.window_background, bg_width, bg_height);
4299 ffi::wlr_scene_rect_set_color(self.window_background, border_color.as_ptr());
4300 // The background plate sits directly under the client's plate, so it
4301 // takes the ROOT_PLATE radius and the same span widening as the
4302 // blur/clip radius — not the border ring's radius, which is a
4303 // separate key describing a different edge.
4304 let bg_radius = widen_corner_radius(
4305 self.root_plate_radius_base(),
4306 self.box_geom.width,
4307 self.box_geom.height,
4308 );
4309 ffi::river_scene_rect_set_corner_radius(self.window_background, (bg_radius as f64 * self.scale) as i32);
4310 ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, !requested.hidden && self.wm_requested.ssd);
4311
4312 // The handles draw as eight discs in one frame node; the hovered
4313 // disc draws in hover_color. Under each disc a transparent square
4314 // rect is a scene hit-test catcher (width 0 keeps the legacy
4315 // invisible 8px virtual resize zones).
4316 //
4317 // They live in `border.tree`, parented to the global border overlay
4318 // layer rather than to this window's tree, so it has to be
4319 // positioned and enabled in step with the window by hand.
4320 let is_virtual_border = border.width == 0;
4321 // Deliberately NOT gated on `wm_requested.ssd`: that flag defaults to
4322 // false and is only set by a client calling use_ssd, and the segments
4323 // have never depended on it — only `window_background` does.
4324 let borders_visible = !requested.hidden
4325 && !requested.circular
4326 && !is_virtual_border
4327 && self.border_reveal.iter().any(|&a| a > 0.0);
4328 ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, borders_visible);
4329 if borders_visible {
4330 ffi::river_scene_node_set_position_if_changed(
4331 self.border.tree as *mut ffi::wlr_scene_node,
4332 self.box_geom.x,
4333 self.box_geom.y,
4334 );
4335 }
4336 if requested.circular {
4337 ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
4338 ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
4339 ffi::wlr_scene_node_set_enabled(self.border.top as *mut ffi::wlr_scene_node, false);
4340 ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
4341 for &seg in self.border.segments.iter() {
4342 ffi::wlr_scene_node_set_enabled(seg as *mut ffi::wlr_scene_node, false);
4343 }
4344 return;
4345 }
4346 let content = ffi::wlr_box {
4347 x: 0,
4348 y: 0,
4349 width: self.box_geom.width,
4350 height: self.box_geom.height,
4351 };
4352
4353 let mut intersect = std::mem::zeroed();
4354 let clip_empty = requested.content_clip.width == 0 && requested.content_clip.height == 0;
4355 if clip_empty || ffi::wlr_box_intersection(&mut intersect, &content, &requested.content_clip) {
4356 let border = &requested.border;
4357 // The interactive band (doubled configured width, floored). The
4358 // per-side foam clipping this used to carry is gone with the
4359 // outside band: it split a gap SHARED with a neighbouring window,
4360 // and the inside ring shares nothing.
4361 let band_f = border_band_width(border.width);
4362 let band = band_f as i32;
4363 let transparent = [0.0f32; 4];
4364
4365 // The rounded-frame path used to leave radius/clip state on the
4366 // top band rect; keep it reset.
4367 ffi::river_scene_rect_set_corner_radius(self.border.top, 0);
4368 ffi::wlr_scene_rect_set_clipped_region(self.border.top, ffi::clipped_region_get_default());
4369
4370 let apply = |rect: *mut ffi::wlr_scene_rect, bx: ffi::wlr_box, color: &[f32; 4], enabled: bool| {
4371 let mut bx = bx;
4372 if enabled && (requested.clip.width != 0 || requested.clip.height != 0) {
4373 let mut clip_intersect = std::mem::zeroed();
4374 ffi::wlr_box_intersection(&mut clip_intersect, &bx, &requested.clip);
4375 bx = clip_intersect;
4376 }
4377 let enabled = enabled && bx.width > 0 && bx.height > 0;
4378 ffi::wlr_scene_node_set_enabled(rect as *mut ffi::wlr_scene_node, enabled);
4379 if !enabled {
4380 return;
4381 }
4382 ffi::river_scene_node_set_position_if_changed(
4383 rect as *mut ffi::wlr_scene_node,
4384 (bx.x as f64 * self.scale) as i32,
4385 (bx.y as f64 * self.scale) as i32,
4386 );
4387 ffi::river_scene_rect_set_size_if_changed(
4388 rect,
4389 (bx.width as f64 * self.scale) as i32,
4390 (bx.height as f64 * self.scale) as i32,
4391 );
4392 ffi::wlr_scene_rect_set_color(rect, color.as_ptr());
4393 };
4394
4395 // Handles live INSIDE the content rect, and only in overview
4396 // mode — see `cursor::get_border_zone`, which hit-tests the same
4397 // ring from the same band width and corner length. In normal
4398 // mode there is nothing to grab, so the catchers and the visible
4399 // segments are both disabled outright. The window's own border
4400 // (`window_background`, above) is untouched in either mode: this
4401 // moved the HANDLES inward, not the border.
4402 // Overview, or Super held: the same adjust mode at any zoom.
4403 let in_overview = (*self.server).wm.window_adjust_active();
4404 let bw = band;
4405 let layout_handle_w = (*self.server).wm.layout.border_handle_width;
4406 let sc = if self.scale > 0.0 { self.scale } else { 1.0 };
4407 let (cw, ch) = (content.width, content.height);
4408 // A window thinner than two bands has no interior left for a
4409 // ring; drawing one would be a solid block over the whole window.
4410 // Live handles: the mode is on and this is the adjust target
4411 // (the window under the pointer). Target-only,
4412 // like the reveal in step_border_fade: without this the
4413 // invisible catcher rects would keep intercepting scene hits on
4414 // windows whose ring is not even drawn.
4415 let handles_live = in_overview && self.is_adjust_target();
4416 // Drawn handles: live, OR still fading out — releasing Super (or
4417 // leaving overview, or losing focus) eases the ring away instead
4418 // of cutting it, so the ring stays drawn while any reveal is
4419 // above zero. The catchers below are gated on `handles_live`
4420 // alone: a fading ring is decoration, never a grab.
4421 let fading_out = !handles_live && self.border_reveal.iter().any(|&a| a > 0.0);
4422 let handles_on = (handles_live || fading_out)
4423 && window_takes_handles(self_ptr)
4424 && !is_virtual_border
4425 && bw > 0
4426 && (cw as f64 * sc) >= 12.0
4427 && (ch as f64 * sc) >= 12.0;
4428 if !handles_on {
4429 // Nothing is drawn, so nothing is stale: without this the
4430 // fade step would see a mismatch and repaint every tick.
4431 self.border_hover_drawn = self.hovered_border_element;
4432 for r in [self.border.left, self.border.right, self.border.top, self.border.bottom] {
4433 ffi::wlr_scene_node_set_enabled(r as *mut ffi::wlr_scene_node, false);
4434 }
4435 for &seg in self.border.segments.iter() {
4436 ffi::wlr_scene_node_set_enabled(seg as *mut ffi::wlr_scene_node, false);
4437 }
4438 ffi::wlr_scene_node_set_enabled(
4439 &mut (*self.border.frame).node as *mut ffi::wlr_scene_node,
4440 false,
4441 );
4442 return;
4443 }
4444
4445 // The band is a SCREEN width, not a world one. Handles exist only
4446 // in overview, which is zoomed OUT, so a band that scaled with the
4447 // window would be at its thinnest exactly where it is the only way
4448 // to resize — 16px becomes 7 at a typical overview zoom, and the
4449 // thin corners 2.5. `apply` scales the boxes it is given, so the
4450 // catchers are sized in unscaled units that come back to
4451 // `band_screen` on screen. cursor::get_border_zone measures the
4452 // same width in layout px; the two must agree.
4453 // Screen thickness, but never more than a fifth of the smaller
4454 // on-screen side: a zoomed-out window would otherwise be mostly
4455 // ring. Shrinking beats the old hard cutoff, which dropped the
4456 // handles altogether below a threshold — a window you cannot
4457 // resize at all is worse than one with a slimmer grip.
4458 let short_side = (cw.min(ch) as f64 * sc).max(1.0);
4459 let band_screen = (layout_handle_w as f64)
4460 .max(crate::window::HOVER_BAND_MIN)
4461 .min(short_side * 0.2);
4462 let px = |v: i32| (v as f64 * sc) as i32;
4463
4464 // The band catchers are retired: between two discs the pointer
4465 // must reach the app, not a catcher.
4466 for r in [self.border.left, self.border.right, self.border.top, self.border.bottom] {
4467 ffi::wlr_scene_node_set_enabled(r as *mut ffi::wlr_scene_node, false);
4468 }
4469
4470 let layout = &(*self.server).wm.layout;
4471 // The discs sit inside the window's own silhouette, so the
4472 // corner discs place against the content radius (the widened
4473 // root plate radius the corner clip uses).
4474 let r_in = bg_radius;
4475
4476 // Hit catchers: one transparent square under each disc, laid out
4477 // by the same function the hit test uses. `apply` takes unscaled
4478 // boxes, so the screen-px layout is divided back out (a px of
4479 // rounding on an invisible catcher is nothing).
4480 let (centres, disc_r) = handle_disc_layout(
4481 cw as f64 * sc,
4482 ch as f64 * sc,
4483 px(r_in) as f64,
4484 band_screen,
4485 );
4486 for (i, &(cx, cy)) in centres.iter().enumerate() {
4487 let b = ffi::wlr_box {
4488 x: ((cx - disc_r) / sc).floor() as i32,
4489 y: ((cy - disc_r) / sc).floor() as i32,
4490 width: (2.0 * disc_r / sc).ceil() as i32,
4491 height: (2.0 * disc_r / sc).ceil() as i32,
4492 };
4493 apply(self.border.segments[i], b, &transparent, handles_live);
4494 }
4495 // corner_len and gap are retired by the wave profile (the
4496 // valleys place the seams now, a quarter along each side) and
4497 // ignored by the shader; still passed so the node API holds.
4498 let cl = border_corner_len(bw as f64, layout.border_corner_length, r_in as f64) as i32;
4499 let g = layout.border_segment_gap;
4500
4501 // Handles rest invisible and fade in with the mode; one alpha for
4502 // the whole ring now that every zone reveals together in overview
4503 // (`step_border_fade`'s all_on branch). The Top slot carries it —
4504 // they are all equal while the ring is up, and taking one keeps
4505 // the fade a single number.
4506 let a = self.border_reveal[BorderElement::Top.index()].clamp(0.0, 1.0);
4507 let premul = |c: &[f32; 4]| [c[0] * a, c[1] * a, c[2] * a, c[3] * a];
4508
4509 ffi::wlr_scene_frame_set_size(self.border.frame, px(cw), px(ch));
4510 ffi::wlr_scene_frame_set_corner_radius(self.border.frame, px(r_in));
4511 // band is the disc diameter; the rest is retired by the discs and
4512 // ignored by the shader, still passed so the node API holds.
4513 ffi::wlr_scene_frame_set_shape(
4514 self.border.frame,
4515 band_screen as f32,
4516 (band_screen as f32 * layout.border_taper.clamp(0.0, 1.0)).max(2.0),
4517 (px(cl) as f64).max(band_screen) as f32,
4518 px(g) as f32,
4519 layout.border_swell_curve,
4520 (layout.border_corner_bulge as f64).min(short_side * 0.3) as f32,
4521 );
4522 // The popover hint arrives in surface-local LOGICAL px; the node
4523 // space is zoom-scaled device px like everything else here, so it
4524 // takes the same px() mapping. Zeroed when clear.
4525 let ex = match self.popover_region {
4526 Some(r) => [
4527 px(r.x) as f32,
4528 px(r.y) as f32,
4529 px(r.width) as f32,
4530 px(r.height) as f32,
4531 ],
4532 None => [0.0; 4],
4533 };
4534 ffi::wlr_scene_frame_set_exclusion(self.border.frame, ex.as_ptr());
4535 // The ring exists only for the SEAT-focused window — `handles_on`
4536 // above says so, and so does step_border_fade's reveal — so it
4537 // paints in the focused color, taken from the layout rather than
4538 // from `requested.border.color`.
4539 //
4540 // That field is a PLAN value, written by the arrange pass from the
4541 // focus it saw when it ran, and a focus change only schedules an
4542 // arrange when the newly focused window happens to be Floating
4543 // (Seat::focus). Every other focus change armed the border fade
4544 // and nothing else, so the ring eased in wearing the UNFOCUSED
4545 // color: hovering a tiled window in overview drew its resize ring
4546 // in the plain border gray instead of the focus color, and it
4547 // stayed gray until some unrelated transaction refreshed the plan.
4548 // Whether the ring is drawn and what color it is are the same
4549 // fact — focused — so both now read it live, the way update_bevel
4550 // already reads focus off the seats for the rim highlight.
4551 //
4552 // `window_background` above keeps the plan color: that one is the
4553 // window's own plate, not this compositor-drawn handle.
4554 ffi::wlr_scene_frame_set_color(
4555 self.border.frame,
4556 premul(&layout.border_color_focused).as_ptr(),
4557 );
4558 let hovered = self
4559 .hovered_border_element
4560 .map(|e| e.index() as f32)
4561 .unwrap_or(-1.0);
4562 self.border_hover_drawn = self.hovered_border_element;
4563 ffi::wlr_scene_frame_set_hover(
4564 self.border.frame,
4565 hovered,
4566 premul(&border.hover_color).as_ptr(),
4567 );
4568 ffi::river_scene_node_set_position_if_changed(
4569 &mut (*self.border.frame).node as *mut ffi::wlr_scene_node,
4570 0,
4571 0,
4572 );
4573 ffi::wlr_scene_node_set_enabled(
4574 &mut (*self.border.frame).node as *mut ffi::wlr_scene_node,
4575 a > 0.0,
4576 );
4577 }
4578 }
4579
4580 #[allow(unused_assignments)]
4581 pub unsafe fn apply_surface_clip(&mut self, a: *const ffi::wlr_box, b: *const ffi::wlr_box) {
4582 let mut surface_clip = std::mem::zeroed::<ffi::wlr_box>();
4583 let a_empty = (*a).width == 0 && (*a).height == 0;
4584 let b_empty = (*b).width == 0 && (*b).height == 0;
4585
4586 let layout_box = ffi::wlr_box {
4587 x: 0,
4588 y: 0,
4589 width: self.box_geom.width,
4590 height: self.box_geom.height,
4591 };
4592
4593 if !a_empty && !b_empty {
4594 let mut temp_clip = std::mem::zeroed::<ffi::wlr_box>();
4595 if !ffi::wlr_box_intersection(&mut temp_clip, a, b) {
4596 self.surfaces.set_enabled(false);
4597 return;
4598 }
4599 if !ffi::wlr_box_intersection(&mut surface_clip, &temp_clip, &layout_box) {
4600 self.surfaces.set_enabled(false);
4601 return;
4602 }
4603 } else if !a_empty {
4604 if !ffi::wlr_box_intersection(&mut surface_clip, a, &layout_box) {
4605 self.surfaces.set_enabled(false);
4606 return;
4607 }
4608 } else if !b_empty {
4609 if !ffi::wlr_box_intersection(&mut surface_clip, b, &layout_box) {
4610 self.surfaces.set_enabled(false);
4611 return;
4612 }
4613 } else {
4614 surface_clip = layout_box;
4615 }
4616
4617 self.surfaces.set_enabled(true);
4618 let margin = 0;
4619 surface_clip.x -= margin;
4620 surface_clip.y -= margin;
4621 surface_clip.width += 2 * margin;
4622 surface_clip.height += 2 * margin;
4623
4624 match self.impl_type {
4625 WindowImpl::Toplevel(toplevel) => {
4626 if !toplevel.is_null() {
4627 let x = if self.wm_requested.ssd { 0 } else { (*toplevel).geometry.x };
4628 let y = if self.wm_requested.ssd { 0 } else { (*toplevel).geometry.y };
4629 surface_clip.x += x;
4630 surface_clip.y += y;
4631 }
4632 }
4633 WindowImpl::Xwayland(xwindow) => {
4634 if !xwindow.is_null() {
4635 let title_ptr = (*(*xwindow).xsurface).title;
4636 let title = if title_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(title_ptr).to_str().unwrap_or("") };
4637 if title.contains("Ubisoft") {
4638 log::info!(
4639 "XWayland window clip check: title='{}' box_geom=({}, {}, {}, {}) xsurface=({}, {}, {}, {})",
4640 title,
4641 self.box_geom.x,
4642 self.box_geom.y,
4643 self.box_geom.width,
4644 self.box_geom.height,
4645 (*(*xwindow).xsurface).x,
4646 (*(*xwindow).xsurface).y,
4647 (*(*xwindow).xsurface).width,
4648 (*(*xwindow).xsurface).height,
4649 );
4650 }
4651 }
4652 }
4653 _ => {}
4654 }
4655
4656 // Crop a CSD toplevel to its xdg geometry. Chromium-family clients
4657 // paint a translucent shadow band outside the geometry whenever they
4658 // are not maximized; the compositor draws its own shadow, and it
4659 // rounds corners per buffer at the buffer's edge, so uncropped the
4660 // rounding fell in that band and the visible window read
4661 // square-cornered (an Electron window un-tiled by a fullscreen round
4662 // trip). A geometry clip was set once and nulled in 34b3ae64: the
4663 // scaling passes rewrote every buffer's dest size from the full
4664 // surface each commit and stretched the crop back out — they go
4665 // through surface_buffer_extent now. Skipped while a cce-ui client
4666 // has a popover overhanging its geometry (set_popover_region): that
4667 // rim is live menu content, not a shadow. And skipped mid
4668 // fullscreen-toggle, where the animation owns the buffers' stretch.
4669 let mut crop = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
4670 if let WindowImpl::Toplevel(toplevel) = self.impl_type {
4671 if !toplevel.is_null()
4672 && !self.wm_requested.ssd
4673 && self.popover_region.is_none()
4674 && self.fs_anim.is_none()
4675 {
4676 crop = (*toplevel).geometry;
4677 }
4678 }
4679 let clip: *const ffi::wlr_box = if crop.width > 0 && crop.height > 0 {
4680 &crop
4681 } else {
4682 std::ptr::null()
4683 };
4684 let children_head = ffi::river_scene_tree_get_children(self.surfaces.tree) as *mut WlList;
4685 if (*children_head).next != children_head {
4686 ffi::wlr_scene_subsurface_tree_set_clip(self.surfaces.tree as *mut ffi::wlr_scene_node, clip);
4687 }
4688 }
4689 }
4690
4691 unsafe fn clock_gettime(clk_id: libc::clockid_t, tp: &mut libc::timespec) -> libc::c_int {
4692 libc::clock_gettime(clk_id, tp)
4693 }
4694
4695 unsafe extern "C" fn window_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4696 ffi::wl_resource_destroy(resource);
4697 }
4698
4699 unsafe extern "C" fn window_close(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4700 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4701 if window.is_null() {
4702 return;
4703 }
4704 let server = (*window).server;
4705 if !(*server).wm.ensure_windowing() {
4706 return;
4707 }
4708 (*window).wm_requested.close = true;
4709 }
4710
4711 unsafe extern "C" fn window_get_node(
4712 client: *mut ffi::wl_client,
4713 resource: *mut ffi::wl_resource,
4714 id: u32,
4715 ) {
4716 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4717 if window.is_null() {
4718 return;
4719 }
4720 if !(*window).node.object.is_null() {
4721 ffi::wl_resource_post_error(
4722 resource,
4723 ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_NODE_EXISTS,
4724 b"window already has a node object\0".as_ptr() as *const _,
4725 );
4726 return;
4727 }
4728 (*window).node.create_object(client, ffi::wl_resource_get_version(resource) as u32, id);
4729 }
4730
4731 unsafe extern "C" fn window_propose_dimensions(
4732 _client: *mut ffi::wl_client,
4733 resource: *mut ffi::wl_resource,
4734 width: i32,
4735 height: i32,
4736 ) {
4737 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4738 if window.is_null() {
4739 return;
4740 }
4741 let server = (*window).server;
4742 if !(*server).wm.ensure_windowing() {
4743 return;
4744 }
4745 if width < 0 || height < 0 {
4746 ffi::wl_resource_post_error(
4747 resource,
4748 ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_DIMENSIONS,
4749 b"dimensions must be greater than or equal to 0\0".as_ptr() as *const _,
4750 );
4751 return;
4752 }
4753 if (*window).get_parent().is_null() {
4754 (*window).wm_requested.dimensions = Some(Dimensions {
4755 width: width as u32,
4756 height: height as u32,
4757 });
4758 }
4759 }
4760
4761 unsafe extern "C" fn window_hide(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4762 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4763 if window.is_null() {
4764 return;
4765 }
4766 let server = (*window).server;
4767 if !(*server).wm.ensure_rendering() {
4768 return;
4769 }
4770 (*window).rendering_requested.hidden = true;
4771 }
4772
4773 unsafe extern "C" fn window_show(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4774 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4775 if window.is_null() {
4776 return;
4777 }
4778 let server = (*window).server;
4779 if !(*server).wm.ensure_rendering() {
4780 return;
4781 }
4782 (*window).rendering_requested.hidden = false;
4783 }
4784
4785 unsafe extern "C" fn window_use_csd(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4786 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4787 if window.is_null() {
4788 return;
4789 }
4790 let server = (*window).server;
4791 if !(*server).wm.ensure_windowing() {
4792 return;
4793 }
4794 (*window).wm_requested.ssd = false;
4795 (*server).wm.dirty_windowing();
4796 }
4797
4798 unsafe extern "C" fn window_use_ssd(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4799 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4800 if window.is_null() {
4801 return;
4802 }
4803 let server = (*window).server;
4804 if !(*server).wm.ensure_windowing() {
4805 return;
4806 }
4807 (*window).wm_requested.ssd = true;
4808 (*server).wm.dirty_windowing();
4809 }
4810
4811 unsafe extern "C" fn window_set_borders(
4812 _client: *mut ffi::wl_client,
4813 resource: *mut ffi::wl_resource,
4814 edges: u32,
4815 width: i32,
4816 r: u32,
4817 g: u32,
4818 b: u32,
4819 a: u32,
4820 ) {
4821 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4822 if window.is_null() {
4823 return;
4824 }
4825 let server = (*window).server;
4826 if !(*server).wm.ensure_rendering() {
4827 return;
4828 }
4829 if width < 0 {
4830 ffi::wl_resource_post_error(
4831 resource,
4832 ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_BORDER,
4833 b"border width must be greater than or equal to 0\0".as_ptr() as *const _,
4834 );
4835 return;
4836 }
4837 let alpha = (a as f64 / u32::MAX as f64) as f32;
4838 // Protocol channels are straight alpha; scene colors are premultiplied.
4839 let color = [
4840 (r as f64 / u32::MAX as f64) as f32 * alpha,
4841 (g as f64 / u32::MAX as f64) as f32 * alpha,
4842 (b as f64 / u32::MAX as f64) as f32 * alpha,
4843 alpha,
4844 ];
4845 (*window).rendering_requested.border = Border {
4846 edges: Edges::from_u32(edges),
4847 width: width as u32,
4848 color,
4849 // Protocol-set borders don't participate in hover highlighting.
4850 hover_color: color,
4851 corner_radius: 0,
4852 };
4853 }
4854
4855 unsafe extern "C" fn window_set_tiled(
4856 _client: *mut ffi::wl_client,
4857 resource: *mut ffi::wl_resource,
4858 edges: u32,
4859 ) {
4860 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4861 if window.is_null() {
4862 return;
4863 }
4864 let server = (*window).server;
4865 if !(*server).wm.ensure_windowing() {
4866 return;
4867 }
4868 (*window).wm_requested.tiled = edges;
4869 }
4870
4871 unsafe extern "C" fn window_get_decoration_above(
4872 client: *mut ffi::wl_client,
4873 resource: *mut ffi::wl_resource,
4874 id: u32,
4875 wl_surface: *mut ffi::wl_resource,
4876 ) {
4877 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4878 if window.is_null() {
4879 return;
4880 }
4881 let wlr_surface = ffi::wlr_surface_from_resource(wl_surface);
4882 let decoration = match Decoration::create(
4883 client,
4884 ffi::wl_resource_get_version(resource) as u32,
4885 id,
4886 wlr_surface,
4887 (*window).decorations_above_tree,
4888 window,
4889 ) {
4890 Ok(d) => d,
4891 Err(e) => {
4892 log::error!("Failed to create decoration: {}", e);
4893 ffi::wl_client_post_no_memory(client);
4894 return;
4895 }
4896 };
4897 let list_head = &mut (*window).decorations_above as *mut ffi::wl_list as *mut WlList;
4898 wl_list_insert((*list_head).prev, &mut (*decoration).link as *mut ffi::wl_list as *mut WlList);
4899 }
4900
4901 unsafe extern "C" fn window_get_decoration_below(
4902 client: *mut ffi::wl_client,
4903 resource: *mut ffi::wl_resource,
4904 id: u32,
4905 wl_surface: *mut ffi::wl_resource,
4906 ) {
4907 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4908 if window.is_null() {
4909 return;
4910 }
4911 let wlr_surface = ffi::wlr_surface_from_resource(wl_surface);
4912 let decoration = match Decoration::create(
4913 client,
4914 ffi::wl_resource_get_version(resource) as u32,
4915 id,
4916 wlr_surface,
4917 (*window).decorations_below_tree,
4918 window,
4919 ) {
4920 Ok(d) => d,
4921 Err(e) => {
4922 log::error!("Failed to create decoration: {}", e);
4923 ffi::wl_client_post_no_memory(client);
4924 return;
4925 }
4926 };
4927 let list_head = &mut (*window).decorations_below as *mut ffi::wl_list as *mut WlList;
4928 wl_list_insert((*list_head).prev, &mut (*decoration).link as *mut ffi::wl_list as *mut WlList);
4929 }
4930
4931 unsafe extern "C" fn window_inform_resize_start(
4932 _client: *mut ffi::wl_client,
4933 resource: *mut ffi::wl_resource,
4934 ) {
4935 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4936 if window.is_null() {
4937 return;
4938 }
4939 let server = (*window).server;
4940 if !(*server).wm.ensure_windowing() {
4941 return;
4942 }
4943 (*window).wm_requested.resizing = true;
4944 }
4945
4946 unsafe extern "C" fn window_inform_resize_end(
4947 _client: *mut ffi::wl_client,
4948 resource: *mut ffi::wl_resource,
4949 ) {
4950 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4951 if window.is_null() {
4952 return;
4953 }
4954 let server = (*window).server;
4955 if !(*server).wm.ensure_windowing() {
4956 return;
4957 }
4958 (*window).wm_requested.resizing = false;
4959 }
4960
4961 unsafe extern "C" fn window_set_capabilities(
4962 _client: *mut ffi::wl_client,
4963 resource: *mut ffi::wl_resource,
4964 caps: u32,
4965 ) {
4966 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4967 if window.is_null() {
4968 return;
4969 }
4970 let server = (*window).server;
4971 if !(*server).wm.ensure_windowing() {
4972 return;
4973 }
4974 (*window).wm_requested.capabilities = caps;
4975 }
4976
4977 unsafe extern "C" fn window_inform_maximized(
4978 _client: *mut ffi::wl_client,
4979 resource: *mut ffi::wl_resource,
4980 ) {
4981 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4982 if window.is_null() {
4983 return;
4984 }
4985 let server = (*window).server;
4986 if !(*server).wm.ensure_windowing() {
4987 return;
4988 }
4989 (*window).wm_requested.maximized = true;
4990 }
4991
4992 unsafe extern "C" fn window_inform_unmaximized(
4993 _client: *mut ffi::wl_client,
4994 resource: *mut ffi::wl_resource,
4995 ) {
4996 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4997 if window.is_null() {
4998 return;
4999 }
5000 let server = (*window).server;
5001 if !(*server).wm.ensure_windowing() {
5002 return;
5003 }
5004 (*window).wm_requested.maximized = false;
5005 }
5006
5007 unsafe extern "C" fn window_inform_fullscreen(
5008 _client: *mut ffi::wl_client,
5009 resource: *mut ffi::wl_resource,
5010 ) {
5011 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5012 if window.is_null() {
5013 return;
5014 }
5015 let server = (*window).server;
5016 if !(*server).wm.ensure_windowing() {
5017 return;
5018 }
5019 (*window).wm_requested.inform_fullscreen = true;
5020 }
5021
5022 unsafe extern "C" fn window_inform_not_fullscreen(
5023 _client: *mut ffi::wl_client,
5024 resource: *mut ffi::wl_resource,
5025 ) {
5026 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5027 if window.is_null() {
5028 return;
5029 }
5030 let server = (*window).server;
5031 if !(*server).wm.ensure_windowing() {
5032 return;
5033 }
5034 (*window).wm_requested.inform_fullscreen = false;
5035 }
5036
5037 unsafe extern "C" fn window_fullscreen(
5038 _client: *mut ffi::wl_client,
5039 resource: *mut ffi::wl_resource,
5040 output: *mut ffi::wl_resource,
5041 ) {
5042 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5043 if window.is_null() {
5044 return;
5045 }
5046 let server = (*window).server;
5047 if !(*server).wm.ensure_windowing() {
5048 return;
5049 }
5050 let out = if output.is_null() {
5051 std::ptr::null_mut()
5052 } else {
5053 let wlr_output = ffi::wlr_output_from_resource(output);
5054 if wlr_output.is_null() {
5055 std::ptr::null_mut()
5056 } else {
5057 ffi::river_wlr_output_get_data(wlr_output) as *mut crate::output::Output
5058 }
5059 };
5060 (*window).wm_requested.fullscreen = out;
5061 }
5062
5063 unsafe extern "C" fn window_exit_fullscreen(
5064 _client: *mut ffi::wl_client,
5065 resource: *mut ffi::wl_resource,
5066 ) {
5067 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5068 if window.is_null() {
5069 return;
5070 }
5071 let server = (*window).server;
5072 if !(*server).wm.ensure_windowing() {
5073 return;
5074 }
5075 (*window).wm_requested.fullscreen = std::ptr::null_mut();
5076 }
5077
5078 unsafe extern "C" fn window_set_clip_box(
5079 _client: *mut ffi::wl_client,
5080 resource: *mut ffi::wl_resource,
5081 x: i32,
5082 y: i32,
5083 width: i32,
5084 height: i32,
5085 ) {
5086 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5087 if window.is_null() {
5088 return;
5089 }
5090 let server = (*window).server;
5091 if !(*server).wm.ensure_rendering() {
5092 return;
5093 }
5094 if width < 0 || height < 0 {
5095 ffi::wl_resource_post_error(
5096 resource,
5097 ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_CLIP_BOX,
5098 b"width/height must be greater than or equal to 0\0".as_ptr() as *const _,
5099 );
5100 return;
5101 }
5102 (*window).rendering_requested.clip = ffi::wlr_box {
5103 x,
5104 y,
5105 width,
5106 height,
5107 };
5108 }
5109
5110 unsafe extern "C" fn window_set_content_clip_box(
5111 _client: *mut ffi::wl_client,
5112 resource: *mut ffi::wl_resource,
5113 x: i32,
5114 y: i32,
5115 width: i32,
5116 height: i32,
5117 ) {
5118 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5119 if window.is_null() {
5120 return;
5121 }
5122 let server = (*window).server;
5123 if !(*server).wm.ensure_rendering() {
5124 return;
5125 }
5126 if width < 0 || height < 0 {
5127 ffi::wl_resource_post_error(
5128 resource,
5129 ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_CLIP_BOX,
5130 b"width/height must be greater than or equal to 0\0".as_ptr() as *const _,
5131 );
5132 return;
5133 }
5134 (*window).rendering_requested.content_clip = ffi::wlr_box {
5135 x,
5136 y,
5137 width,
5138 height,
5139 };
5140 }
5141
5142 unsafe extern "C" fn window_set_dimension_bounds(
5143 _client: *mut ffi::wl_client,
5144 resource: *mut ffi::wl_resource,
5145 max_width: i32,
5146 max_height: i32,
5147 ) {
5148 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5149 if window.is_null() {
5150 return;
5151 }
5152 let server = (*window).server;
5153 if !(*server).wm.ensure_windowing() {
5154 return;
5155 }
5156 if max_width < 0 || max_height < 0 {
5157 ffi::wl_resource_post_error(
5158 resource,
5159 ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_DIMENSIONS,
5160 b"dimensions must be greater than or equal to 0\0".as_ptr() as *const _,
5161 );
5162 return;
5163 }
5164 (*window).wm_requested.bounds = Dimensions {
5165 width: max_width as u32,
5166 height: max_height as u32,
5167 };
5168 }
5169
5170 unsafe extern "C" fn window_set_opacity(
5171 _client: *mut ffi::wl_client,
5172 resource: *mut ffi::wl_resource,
5173 opacity: u32,
5174 ) {
5175 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5176 if window.is_null() {
5177 return;
5178 }
5179 let server = (*window).server;
5180 if !(*server).wm.ensure_rendering() {
5181 return;
5182 }
5183 let opacity_f32 = opacity as f32 / u32::MAX as f32;
5184 (*window).rendering_requested.opacity = opacity_f32;
5185 }
5186
5187 unsafe extern "C" fn window_set_circular(
5188 _client: *mut ffi::wl_client,
5189 resource: *mut ffi::wl_resource,
5190 circular: u32,
5191 ) {
5192 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5193 if window.is_null() {
5194 return;
5195 }
5196 let server = (*window).server;
5197 if !(*server).wm.ensure_rendering() {
5198 return;
5199 }
5200 (*window).rendering_requested.circular = circular != 0;
5201 }
5202
5203 unsafe extern "C" fn window_set_blur(
5204 _client: *mut ffi::wl_client,
5205 resource: *mut ffi::wl_resource,
5206 blur: u32,
5207 ) {
5208 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5209 if window.is_null() {
5210 return;
5211 }
5212 let server = (*window).server;
5213 if !(*server).wm.ensure_rendering() {
5214 return;
5215 }
5216 (*window).rendering_requested.blur = blur != 0;
5217 }
5218
5219 // zcce_window_v1 implementation
5220 static WINDOW_INTERFACE: ffi::zcce_window_v1_interface = ffi::zcce_window_v1_interface {
5221 destroy: Some(window_destroy),
5222 close: Some(window_close),
5223 get_node: Some(window_get_node),
5224 propose_dimensions: Some(window_propose_dimensions),
5225 hide: Some(window_hide),
5226 show: Some(window_show),
5227 use_csd: Some(window_use_csd),
5228 use_ssd: Some(window_use_ssd),
5229 set_borders: Some(window_set_borders),
5230 set_tiled: Some(window_set_tiled),
5231 get_decoration_above: Some(window_get_decoration_above),
5232 get_decoration_below: Some(window_get_decoration_below),
5233 inform_resize_start: Some(window_inform_resize_start),
5234 inform_resize_end: Some(window_inform_resize_end),
5235 set_capabilities: Some(window_set_capabilities),
5236 inform_maximized: Some(window_inform_maximized),
5237 inform_unmaximized: Some(window_inform_unmaximized),
5238 inform_fullscreen: Some(window_inform_fullscreen),
5239 inform_not_fullscreen: Some(window_inform_not_fullscreen),
5240 fullscreen: Some(window_fullscreen),
5241 exit_fullscreen: Some(window_exit_fullscreen),
5242 set_clip_box: Some(window_set_clip_box),
5243 set_content_clip_box: Some(window_set_content_clip_box),
5244 set_dimension_bounds: Some(window_set_dimension_bounds),
5245 set_opacity: Some(window_set_opacity),
5246 set_circular: Some(window_set_circular),
5247 set_blur: Some(window_set_blur),
5248 };
5249
5250 static INERT_WINDOW_INTERFACE: ffi::zcce_window_v1_interface = ffi::zcce_window_v1_interface {
5251 destroy: Some(window_destroy),
5252 close: None,
5253 get_node: None,
5254 propose_dimensions: None,
5255 hide: None,
5256 show: None,
5257 use_csd: None,
5258 use_ssd: None,
5259 set_borders: None,
5260 set_tiled: None,
5261 get_decoration_above: None,
5262 get_decoration_below: None,
5263 inform_resize_start: None,
5264 inform_resize_end: None,
5265 set_capabilities: None,
5266 inform_maximized: None,
5267 inform_unmaximized: None,
5268 inform_fullscreen: None,
5269 inform_not_fullscreen: None,
5270 fullscreen: None,
5271 exit_fullscreen: None,
5272 set_clip_box: None,
5273 set_content_clip_box: None,
5274 set_dimension_bounds: None,
5275 set_opacity: None,
5276 set_circular: None,
5277 set_blur: None,
5278 };
5279
5280 unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
5281 let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5282 if !window.is_null() {
5283 if (*window).object != resource {
5284 return;
5285 }
5286 (*window).object = std::ptr::null_mut();
5287 (*window).node.make_inert();
5288
5289 for decorations in [&mut (*window).decorations_above as *mut ffi::wl_list, &mut (*window).decorations_below as *mut ffi::wl_list] {
5290 let list_head = decorations as *mut WlList;
5291 let mut curr = (*list_head).next;
5292 while curr != list_head {
5293 let next = (*curr).next;
5294 let dec = crate::container_of!(curr, Decoration, link);
5295 (*dec).make_inert();
5296 curr = next;
5297 }
5298 }
5299 }
5300 }
5301
5302 // zcce_decoration_v1 implementation
5303 pub struct DecorationRenderingRequested {
5304 pub offset_x: i32,
5305 pub offset_y: i32,
5306 pub sync_next_commit: bool,
5307 pub blur: bool,
5308 }
5309
5310 pub struct Decoration {
5311 pub object: *mut ffi::wl_resource, // zcce_decoration_v1
5312 pub surface: *mut ffi::wlr_surface,
5313 pub tree: *mut ffi::wlr_scene_tree,
5314 pub surfaces: crate::scene::SaveableSurfaces,
5315 pub link: ffi::wl_list,
5316 pub window: *mut Window,
5317 pub rendering_requested: DecorationRenderingRequested,
5318 }
5319
5320 impl Decoration {
5321 pub unsafe fn create(
5322 client: *mut ffi::wl_client,
5323 version: u32,
5324 id: u32,
5325 surface: *mut ffi::wlr_surface,
5326 parent: *mut ffi::wlr_scene_tree,
5327 window: *mut Window,
5328 ) -> Result<*mut Self, &'static str> {
5329 let decoration_v1 = ffi::wl_resource_create(client, &ffi::zcce_decoration_v1_interface, version as i32, id);
5330 if decoration_v1.is_null() {
5331 ffi::wl_client_post_no_memory(client);
5332 return Err("wl_resource_create failed");
5333 }
5334
5335 if !ffi::wlr_surface_set_role(
5336 surface,
5337 &raw const DECORATION_ROLE,
5338 decoration_v1,
5339 ffi::zcce_window_manager_v1_error_ZCCE_WINDOW_MANAGER_V1_ERROR_ROLE,
5340 ) {
5341 return Err("wlr_surface_set_role failed");
5342 }
5343 ffi::river_wlr_surface_set_role_object(surface, decoration_v1);
5344
5345 let tree = ffi::wlr_scene_tree_create(parent);
5346 if tree.is_null() {
5347 return Err("wlr_scene_tree_create failed");
5348 }
5349
5350 let surfaces = crate::scene::SaveableSurfaces::init(tree)?;
5351 let subsurface_tree = ffi::wlr_scene_subsurface_tree_create(surfaces.tree, surface);
5352 if subsurface_tree.is_null() {
5353 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
5354 return Err("wlr_scene_subsurface_tree_create failed");
5355 }
5356
5357 let dec = Box::new(Decoration {
5358 object: decoration_v1,
5359 surface,
5360 tree,
5361 surfaces,
5362 link: std::mem::zeroed(),
5363 window,
5364 rendering_requested: DecorationRenderingRequested {
5365 offset_x: 0,
5366 offset_y: 0,
5367 sync_next_commit: false,
5368 blur: false,
5369 },
5370 });
5371 let raw = Box::into_raw(dec);
5372
5373 ffi::wl_resource_set_implementation(
5374 decoration_v1,
5375 &DECORATION_INTERFACE as *const _ as *const _,
5376 raw as *mut _,
5377 Some(handle_dec_destroy_resource),
5378 );
5379
5380 Ok(raw)
5381 }
5382
5383 pub unsafe fn destroy(&mut self) {
5384 assert!(self.object.is_null());
5385 ffi::wlr_scene_node_destroy(self.tree as *mut ffi::wlr_scene_node);
5386 wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut WlList);
5387 let _ = Box::from_raw(self);
5388 }
5389
5390 pub unsafe fn make_inert(&mut self) {
5391 if !self.object.is_null() {
5392 ffi::wl_resource_set_implementation(
5393 self.object,
5394 &INERT_DECORATION_INTERFACE as *const _ as *const _,
5395 std::ptr::null_mut(),
5396 None,
5397 );
5398 self.object = std::ptr::null_mut();
5399 }
5400 if !self.surface.is_null() {
5401 ffi::river_wlr_surface_set_role_object(self.surface, std::ptr::null_mut());
5402 }
5403 self.surfaces.save();
5404 }
5405
5406 pub unsafe fn render_finish(&mut self, _window_clip: *const ffi::wlr_box) {
5407 if self.rendering_requested.sync_next_commit {
5408 self.rendering_requested.sync_next_commit = false;
5409
5410 if !self.surfaces.saved {
5411 if !self.object.is_null() {
5412 ffi::wl_resource_post_error(
5413 self.object,
5414 ffi::zcce_decoration_v1_error_ZCCE_DECORATION_V1_ERROR_NO_COMMIT,
5415 b"no wl_surface.commit after sync_next_commit and before update_rendering_finish\0".as_ptr() as *const _,
5416 );
5417 }
5418 }
5419 }
5420
5421 self.surfaces.drop_saved();
5422
5423 let server = (*self.window).server;
5424 let app_id = (*self.window).get_app_id_string().unwrap_or_default();
5425 let mut ignore_transparent = (*server).wm.layout.window_backdrop_blur_ignore_transparent;
5426 let is_status = (*self.window).tiling_mode == crate::tiling::TilingMode::Status ||
5427 app_id.starts_with("cce-status");
5428 if is_status {
5429 ignore_transparent = (*server).wm.layout.status_backdrop_blur_ignore_transparent;
5430 }
5431 let is_decorated = (*server).wm.is_decorated_app(&app_id);
5432 let blur_enabled = self.rendering_requested.blur && ((*self.window).wm_requested.ssd || is_decorated || is_status) && !(*self.window).droplet_backdrop_on();
5433 // Radius 0 preserves existing behaviour on the layer-surface path (see layer_shell.rs)
5434 // — it never had a blur radius applied, and this fix is scoped to toplevels.
5435 ffi::river_scene_node_enable_blur(self.surfaces.tree as *mut ffi::wlr_scene_node, blur_enabled, (*server).wm.layout.scenefx_optimized_blur, ignore_transparent, 0, 0, 0, 0, 0);
5436
5437 let scale = (*self.window).scale;
5438 let scaled_x = (self.rendering_requested.offset_x as f64 * scale) as i32;
5439 let scaled_y = (self.rendering_requested.offset_y as f64 * scale) as i32;
5440 ffi::river_scene_node_set_position_if_changed(self.tree as *mut ffi::wlr_scene_node, scaled_x, scaled_y);
5441
5442 struct ScaleData {
5443 scale: f64,
5444 ancestor: *mut ffi::wlr_scene_node,
5445 }
5446
5447 unsafe extern "C" fn set_overview_scale_iterator(
5448 buffer: *mut ffi::wlr_scene_buffer,
5449 sx: i32,
5450 sy: i32,
5451 user_data: *mut std::ffi::c_void,
5452 ) {
5453 let data = &*(user_data as *const ScaleData);
5454 let node = buffer as *mut ffi::wlr_scene_node;
5455
5456 let surface = ffi::river_scene_node_get_surface(node);
5457 if !surface.is_null() {
5458 let w = ffi::river_wlr_surface_get_width(surface);
5459 let h = ffi::river_wlr_surface_get_height(surface);
5460 if data.scale == 1.0 {
5461 ffi::river_scene_buffer_set_dest_size_if_changed(buffer, w, h);
5462 ffi::river_scene_node_set_position_if_changed(node, 0, 0);
5463 } else {
5464 let dest_w = (w as f64 * data.scale).round() as i32;
5465 let dest_h = (h as f64 * data.scale).round() as i32;
5466 ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
5467
5468 let (px, py) = get_parent_position_relative_to(node, data.ancestor);
5469 let dest_x = (px as f64 * (data.scale - 1.0)) as i32;
5470 let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
5471 ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
5472 }
5473 // Keep the opaque region in step with the dest scale —
5474 // unscaled it covers the shrunken node's translucent CSD
5475 // margins and occlusion culling stops repainting behind
5476 // the client shadow (stale pixels show through it).
5477 ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
5478 }
5479 // Non-surface buffers are frozen SAVED copies (see
5480 // save_surface_tree_iter): their natural buffer size is
5481 // meaningless for geometry — HiDPI clients commit scale-N
5482 // buffers and Chromium pads buffers beyond the surface,
5483 // cropping via viewport src — so rescaling from it ballooned
5484 // ghosts around the window at any zoom change. A frozen copy
5485 // keeps its save-time dest/position; a zoom mid-transaction
5486 // leaves it briefly at the old zoom, which restore corrects.
5487 }
5488
5489 let scale_data = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
5490 ffi::wlr_scene_node_for_each_buffer(
5491 self.surfaces.tree as *mut ffi::wlr_scene_node,
5492 Some(set_overview_scale_iterator),
5493 &scale_data as *const ScaleData as *mut std::ffi::c_void,
5494 );
5495
5496 if self.surfaces.saved {
5497 let scale_data_saved = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
5498 ffi::wlr_scene_node_for_each_buffer(
5499 self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
5500 Some(set_overview_scale_iterator),
5501 &scale_data_saved as *const ScaleData as *mut std::ffi::c_void,
5502 );
5503 }
5504
5505 let children_head = ffi::river_scene_tree_get_children(self.surfaces.tree) as *mut WlList;
5506 if (*children_head).next != children_head {
5507 ffi::wlr_scene_subsurface_tree_set_clip(self.surfaces.tree as *mut ffi::wlr_scene_node, std::ptr::null());
5508 }
5509 }
5510
5511 pub unsafe fn scale_only_render_finish(&mut self) {
5512 let scale = (*self.window).scale;
5513 if scale * (*self.window).x11_buffer_scale() == 1.0 {
5514 return;
5515 }
5516
5517 struct ScaleData {
5518 scale: f64,
5519 ancestor: *mut ffi::wlr_scene_node,
5520 }
5521
5522 unsafe extern "C" fn set_overview_scale_iterator(
5523 buffer: *mut ffi::wlr_scene_buffer,
5524 sx: i32,
5525 sy: i32,
5526 user_data: *mut std::ffi::c_void,
5527 ) {
5528 let data = &*(user_data as *const ScaleData);
5529 let node = buffer as *mut ffi::wlr_scene_node;
5530
5531 let surface = ffi::river_scene_node_get_surface(node);
5532 if !surface.is_null() {
5533 let w = ffi::river_wlr_surface_get_width(surface);
5534 let h = ffi::river_wlr_surface_get_height(surface);
5535 if data.scale == 1.0 {
5536 ffi::river_scene_buffer_set_dest_size_if_changed(buffer, w, h);
5537 ffi::river_scene_node_set_position_if_changed(node, 0, 0);
5538 } else {
5539 let dest_w = (w as f64 * data.scale).round() as i32;
5540 let dest_h = (h as f64 * data.scale).round() as i32;
5541 ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
5542
5543 let (px, py) = get_parent_position_relative_to(node, data.ancestor);
5544 let dest_x = (px as f64 * (data.scale - 1.0)) as i32;
5545 let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
5546 ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
5547 }
5548 // Keep the opaque region in step with the dest scale —
5549 // unscaled it covers the shrunken node's translucent CSD
5550 // margins and occlusion culling stops repainting behind
5551 // the client shadow (stale pixels show through it).
5552 ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
5553 }
5554 // Non-surface buffers are frozen SAVED copies (see
5555 // save_surface_tree_iter): their natural buffer size is
5556 // meaningless for geometry — HiDPI clients commit scale-N
5557 // buffers and Chromium pads buffers beyond the surface,
5558 // cropping via viewport src — so rescaling from it ballooned
5559 // ghosts around the window at any zoom change. A frozen copy
5560 // keeps its save-time dest/position; a zoom mid-transaction
5561 // leaves it briefly at the old zoom, which restore corrects.
5562 }
5563
5564 let scale_data = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
5565 ffi::wlr_scene_node_for_each_buffer(
5566 self.surfaces.tree as *mut ffi::wlr_scene_node,
5567 Some(set_overview_scale_iterator),
5568 &scale_data as *const ScaleData as *mut std::ffi::c_void,
5569 );
5570
5571 if self.surfaces.saved {
5572 let scale_data_saved = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
5573 ffi::wlr_scene_node_for_each_buffer(
5574 self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
5575 Some(set_overview_scale_iterator),
5576 &scale_data_saved as *const ScaleData as *mut std::ffi::c_void,
5577 );
5578 }
5579 }
5580 }
5581
5582 pub unsafe fn decoration_from_wlr_surface(surface: *mut ffi::wlr_surface) -> *mut Decoration {
5583 if surface.is_null() {
5584 return std::ptr::null_mut();
5585 }
5586 let role_ptr = ffi::river_wlr_surface_get_role(surface);
5587 if role_ptr != &raw const DECORATION_ROLE {
5588 return std::ptr::null_mut();
5589 }
5590 let resource = ffi::river_wlr_surface_get_role_resource(surface);
5591 if resource.is_null() {
5592 return std::ptr::null_mut();
5593 }
5594 ffi::wl_resource_get_user_data(resource) as *mut Decoration
5595 }
5596
5597 unsafe extern "C" fn dec_client_commit(surface: *mut ffi::wlr_surface) {
5598 let dec = decoration_from_wlr_surface(surface);
5599 if dec.is_null() {
5600 return;
5601 }
5602 if (*dec).rendering_requested.sync_next_commit {
5603 (*dec).surfaces.save();
5604 }
5605 }
5606
5607 unsafe extern "C" fn dec_commit(surface: *mut ffi::wlr_surface) {
5608 if ffi::wlr_surface_has_buffer(surface) {
5609 ffi::wlr_surface_map(surface);
5610 }
5611 }
5612
5613 unsafe extern "C" fn dec_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
5614 ffi::wl_resource_destroy(resource);
5615 }
5616
5617 unsafe extern "C" fn dec_set_offset(
5618 _client: *mut ffi::wl_client,
5619 resource: *mut ffi::wl_resource,
5620 x: i32,
5621 y: i32,
5622 ) {
5623 let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
5624 if dec.is_null() {
5625 return;
5626 }
5627 let server = (*(*dec).window).server;
5628 if !(*server).wm.ensure_rendering() {
5629 return;
5630 }
5631 (*dec).rendering_requested.offset_x = x;
5632 (*dec).rendering_requested.offset_y = y;
5633 }
5634
5635 unsafe extern "C" fn dec_sync_next_commit(
5636 _client: *mut ffi::wl_client,
5637 resource: *mut ffi::wl_resource,
5638 ) {
5639 let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
5640 if dec.is_null() {
5641 return;
5642 }
5643 let server = (*(*dec).window).server;
5644 if !(*server).wm.ensure_rendering() {
5645 return;
5646 }
5647 (*dec).rendering_requested.sync_next_commit = true;
5648 }
5649
5650 unsafe extern "C" fn dec_set_blur(
5651 _client: *mut ffi::wl_client,
5652 resource: *mut ffi::wl_resource,
5653 blur: u32,
5654 ) {
5655 let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
5656 if dec.is_null() {
5657 return;
5658 }
5659 let server = (*(*dec).window).server;
5660 if !(*server).wm.ensure_rendering() {
5661 return;
5662 }
5663 (*dec).rendering_requested.blur = blur != 0;
5664 }
5665
5666 static DECORATION_INTERFACE: ffi::zcce_decoration_v1_interface = ffi::zcce_decoration_v1_interface {
5667 destroy: Some(dec_destroy),
5668 set_offset: Some(dec_set_offset),
5669 sync_next_commit: Some(dec_sync_next_commit),
5670 set_blur: Some(dec_set_blur),
5671 };
5672
5673 static INERT_DECORATION_INTERFACE: ffi::zcce_decoration_v1_interface = ffi::zcce_decoration_v1_interface {
5674 destroy: Some(dec_destroy),
5675 set_offset: None,
5676 sync_next_commit: None,
5677 set_blur: None,
5678 };
5679
5680 unsafe extern "C" fn handle_dec_destroy_resource(resource: *mut ffi::wl_resource) {
5681 let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
5682 if !dec.is_null() {
5683 ffi::river_wlr_surface_set_role_object((*dec).surface, std::ptr::null_mut());
5684 (*dec).object = std::ptr::null_mut();
5685 (*dec).destroy();
5686 }
5687 }
5688
5689 unsafe extern "C" fn dec_role_destroy(surface: *mut ffi::wlr_surface) {
5690 let dec = decoration_from_wlr_surface(surface);
5691 if dec.is_null() {
5692 return;
5693 }
5694 ffi::river_wlr_surface_set_role_object(surface, std::ptr::null_mut());
5695 if !(*dec).object.is_null() {
5696 ffi::wl_resource_set_user_data((*dec).object, std::ptr::null_mut());
5697 ffi::wl_resource_destroy((*dec).object);
5698 (*dec).object = std::ptr::null_mut();
5699 }
5700 (*dec).destroy();
5701 }
5702
5703 #[no_mangle]
5704 pub static mut DECORATION_ROLE: ffi::wlr_surface_role = ffi::wlr_surface_role {
5705 name: b"zcce_decoration_v1\0".as_ptr() as *const _,
5706 no_object: false,
5707 client_commit: Some(dec_client_commit),
5708 commit: Some(dec_commit),
5709 map: None,
5710 unmap: None,
5711 destroy: Some(dec_role_destroy),
5712 };
5713
5714 /// A surface buffer's visible extent for the scaling passes: `(width,
5715 /// height, x, y)` — the subsurface clip when one is set (the xdg geometry,
5716 /// see `apply_surface_clip`), placed where wlroots puts the cropped content
5717 /// in its parent, else the whole surface at the origin. wlroots re-derives
5718 /// dest size and position from the clip on every commit; a pass that
5719 /// overrides them from the full surface size stretches the crop back out.
5720 unsafe fn surface_buffer_extent(
5721 buffer: *mut ffi::wlr_scene_buffer,
5722 surface: *mut ffi::wlr_surface,
5723 ) -> (i32, i32, i32, i32) {
5724 let mut clip = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
5725 if ffi::river_scene_buffer_get_surface_clip(buffer, &mut clip) {
5726 (clip.width, clip.height, clip.x, clip.y)
5727 } else {
5728 (
5729 ffi::river_wlr_surface_get_width(surface),
5730 ffi::river_wlr_surface_get_height(surface),
5731 0,
5732 0,
5733 )
5734 }
5735 }
5736
5737 unsafe fn get_parent_position_relative_to(
5738 node: *mut ffi::wlr_scene_node,
5739 ancestor: *mut ffi::wlr_scene_node,
5740 ) -> (i32, i32) {
5741 let mut x = 0;
5742 let mut y = 0;
5743 if !node.is_null() {
5744 let mut curr = ffi::river_scene_node_get_parent(node) as *mut ffi::wlr_scene_node;
5745 while !curr.is_null() && curr != ancestor {
5746 x += ffi::river_scene_node_get_x(curr);
5747 y += ffi::river_scene_node_get_y(curr);
5748 curr = ffi::river_scene_node_get_parent(curr) as *mut ffi::wlr_scene_node;
5749 }
5750 }
5751 (x, y)
5752 }
5753
5754 unsafe fn wl_listener_remove_safe(listener: *mut ffi::wl_listener) {
5755 let prev = (*listener).link.prev;
5756 let next = (*listener).link.next;
5757 if !prev.is_null() && !next.is_null() && prev != listener as *mut ffi::wl_list && next != listener as *mut ffi::wl_list {
5758 ffi::wl_list_remove(&mut (*listener).link);
5759 (*listener).link.prev = std::ptr::null_mut();
5760 (*listener).link.next = std::ptr::null_mut();
5761 }
5762 }
5763
5764 unsafe extern "C" fn handle_window_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
5765 let window = crate::container_of!(listener, Window, commit);
5766 (*window).stream_dirty = true;
5767 let was_status = (*window).is_status_bar();
5768 // An X11 client committing under a left/top-edge drag: anchor on the
5769 // size it just committed, ahead of the render_finish below that places
5770 // the tree at `rendering_requested`. (xdg toplevels do the same in their
5771 // own commit handler, where the toplevel geometry is the authority.)
5772 //
5773 // The committed surface is PHYSICAL pixels — under `xwayland_hidpi` twice
5774 // the logical box, as the scale pass below says — while
5775 // `anchor_resize_commit` works in box_geom's logical units. Convert first,
5776 // and take the wine frame off after, the way `render_finish` reads the
5777 // xsurface size back. Feeding it the raw buffer width put the origin a
5778 // whole window-width to the left and made it track the pointer at double
5779 // speed, every X11 left/top drag at scale 2.
5780 if let WindowImpl::Xwayland(xwindow) = (*window).impl_type {
5781 if !xwindow.is_null() && !(*xwindow).xsurface.is_null() && (*window).resize_edges.is_some() {
5782 let surface = (*(*xwindow).xsurface).surface;
5783 if !surface.is_null() {
5784 let s = crate::xwayland_window::x11_scale_for((*window).server, (*xwindow).xsurface);
5785 let mut w = crate::xwayland_window::from_x11(
5786 ffi::river_wlr_surface_get_width(surface), s);
5787 let mut h = crate::xwayland_window::from_x11(
5788 ffi::river_wlr_surface_get_height(surface), s);
5789 let has_parent = !(*(*xwindow).xsurface).parent.is_null();
5790 if (*window).is_wine() && !has_parent && !(*window).is_fullscreen() {
5791 w = (w - crate::xwayland_window::WINE_MARGIN * 2).max(0);
5792 h = (h - crate::xwayland_window::WINE_MARGIN * 2).max(0);
5793 }
5794 (*window).anchor_resize_commit(w, h);
5795 }
5796 }
5797 }
5798 (*window).render_finish();
5799 // The scene's own commit handler (registered before this one, so it has
5800 // already run) resets the committed buffer's dest size to natural. For
5801 // an X11 surface under xwayland_hidpi that is the physical size — twice
5802 // the logical box — and until the per-frame pass restores it every
5803 // pointer event hit-tests through the unscaled buffer and reaches the
5804 // client at HALF its coordinates. Houdini repaints on every hover
5805 // change, so hover flickered: each repaint opened the gap, the next
5806 // motion event landed elsewhere, the widget un-hovered, repeat.
5807 if (*window).x11_buffer_scale() != 1.0 {
5808 (*window).scale_only_render_finish();
5809 }
5810 // A status segment that changed size needs the bar re-arranged around
5811 // it. One that merely repainted (the clock, once a second; the cpu
5812 // meter) does not — and this used to dirty on every commit, which made
5813 // the status bar alone run a full manage/arrange/render transaction for
5814 // each of its ticks, all day. Compare the committed surface size against
5815 // the last commit's; the xdg commit handler tracks box_geom the same way.
5816 if was_status {
5817 let surface = (*window).root_surface();
5818 if !surface.is_null() {
5819 let size = (
5820 ffi::river_wlr_surface_get_width(surface),
5821 ffi::river_wlr_surface_get_height(surface),
5822 );
5823 if size != (*window).status_commit_size {
5824 (*window).status_commit_size = size;
5825 (*(*window).server).wm.dirty_windowing();
5826 }
5827 } else {
5828 (*(*window).server).wm.dirty_windowing();
5829 }
5830 }
5831 }
5832
5833 #[cfg(test)]
5834 mod handle_disc_tests {
5835 use super::*;
5836
5837 #[test]
5838 fn discs_follow_border_element_order_and_stay_inside() {
5839 let (c, r) = handle_disc_layout(400.0, 300.0, 0.0, 32.0);
5840 assert_eq!(r, 16.0);
5841 assert_eq!(c[BorderElement::Top.index()], (200.0, 16.0));
5842 assert_eq!(c[BorderElement::Bottom.index()], (200.0, 284.0));
5843 assert_eq!(c[BorderElement::Left.index()], (16.0, 150.0));
5844 assert_eq!(c[BorderElement::Right.index()], (384.0, 150.0));
5845 assert_eq!(c[BorderElement::TopLeft.index()], (16.0, 16.0));
5846 assert_eq!(c[BorderElement::BottomRight.index()], (384.0, 284.0));
5847 for &(x, y) in &c {
5848 assert!(x - r >= 0.0 && x + r <= 400.0 && y - r >= 0.0 && y + r <= 300.0);
5849 }
5850 }
5851
5852 #[test]
5853 fn corner_disc_is_tangent_to_a_wider_corner_arc() {
5854 let (c, r) = handle_disc_layout(400.0, 300.0, 40.0, 32.0);
5855 let (tx, ty) = c[BorderElement::TopLeft.index()];
5856 assert_eq!(tx, ty);
5857 // Distance from the arc centre (40, 40) plus the disc radius is the
5858 // arc radius: tangent from the inside.
5859 let d = ((tx - 40.0).powi(2) + (ty - 40.0).powi(2)).sqrt();
5860 assert!((d + r - 40.0).abs() < 1e-9);
5861 }
5862 }