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