srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/state/geometry.rs
blob: b8629bcfe2514837cc20e6d4860458474e11f7c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
use super::*;

/// How long `sync_geometry` waits for a client to catch up to a previous
/// size-changing configure before giving up on the throttle and sending a
/// new one anyway - see `pending_size_configure`'s own doc comment for
/// the throttle itself. Generous relative to any real client's own
/// resize-and-recommit latency (a terminal reflowing text, a browser
/// re-laying-out a page), so this essentially never fires in practice;
/// it exists purely as the same kind of bounded self-heal this session's
/// DRM flip-pending watchdog already uses, not a tuning knob expected to
/// matter day to day.
const CONFIGURE_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100);

impl CompState {

    /// Re-raises always-on-top windows in the `Space`.
    ///
    /// `WindowManager` keeps pinned windows last in its own stacking order,
    /// but the `Space` has an order of its own that decides what actually
    /// draws on top - so pinning is only real once it is pushed here.
    /// Called after anything that raises a window.
    pub(crate) fn raise_pinned(&mut self) {
        let pinned: Vec<WindowId> = self.wm.borrow().stacking_order().filter(|w| w.always_on_top).map(|w| w.id).collect();
        for id in pinned {
            if let Some(w) = self.id_to_window.get(&id).cloned() {
                self.space.raise_element(&w, false);
            }
        }
    }

    /// `Self::effective_frame`, but as a free function taking only the two
    /// fields it actually needs (`wm`, `id_to_window`) instead of `&self` --
    /// a render loop holding `self.udev`/`self.backend` mutably borrowed
    /// can't also pass `&self` to a method, since Rust can't see through a
    /// method call to know it only touches two unrelated fields. Called
    /// through the inherent method below wherever a plain `&self` is
    /// available (input handling, `redraw_decoration_buffer`); this
    /// version exists for the render loops specifically.
    pub(crate) fn effective_frame_of(
        wm: &Rc<RefCell<WindowManager>>,
        id_to_window: &HashMap<WindowId, DWindow>,
        pending_size_configure: &HashMap<WindowId, ((i32, i32), Instant)>,
        id: WindowId,
        geom: srdwm_core::Rect,
    ) -> srdwm_core::Rect {
        // A version of this function briefly (this same session) skipped
        // the committed-size correction below entirely during an active
        // resize, on the reasoning that trusting the client's stale last
        // commit over this compositor's own live drag target was what made
        // the border visibly lag behind content while dragging. Reverted:
        // that fix was real for *position*-independent reasoning but wrong
        // in a more important way - every caller of this function that
        // reads a *bitmap*-backed element (the titlebar, the top/bottom
        // border strip's own rounded-corner bitmap, both built by `redraw_
        // decoration_buffer`) uses this rect's width/height to size the
        // `src` crop rectangle it samples that bitmap with. Making this
        // function return the *live* drag target while the underlying
        // bitmap was still sized for whatever the *last commit* actually
        // was meant that crop could end up larger than the real bitmap's
        // own stored dimensions - `MemoryRenderBufferRenderElement::from_
        // buffer` does not validate `src` against the texture's real size,
        // so an oversized crop reads as an out-of-bounds texture sample
        // (stretched/repeated/garbage pixels, not a clean error).
        //
        // Now reinstated, safely: this returns the *live* drag target
        // (`geom`, unmodified) while a resize of this specific window is
        // active, same as the reverted attempt did - but two things are
        // different this time, together closing the gap that made it
        // unsafe rather than just re-taking the risk:
        // 1. `input::pointer::handle_pointer_position` now calls
        //    `redraw_decoration_buffer` on every resize motion tick (see
        //    its own doc comment), not just on a real client commit, so
        //    the bitmap itself keeps catching up to this same live value
        //    almost every frame instead of staying pinned to the last
        //    commit for the resize's whole duration.
        // 2. Independent of how well-synced that keeps the two, every
        //    render-loop call site that turns this rect's width/height
        //    into a `src` crop now clamps it against `decoration_
        //    signatures`' own recorded `(width, height)` - the bitmap's
        //    own *actual* last-built size, tracked there already for
        //    unrelated caching reasons - before handing it to `from_
        //    buffer`. That clamp is what actually prevents the out-of-
        //    bounds read now, structurally, regardless of any remaining
        //    timing gap between this function and the next rebuild; this
        //    branch existing is what keeps that gap small in practice
        //    rather than a full commit-cycle wide.
        if wm.borrow().resizing_window() == Some(id) {
            return geom;
        }
        // Same reasoning as the active-resize branch just above, for a gap
        // that isn't a drag at all: a plain cross-monitor *move* between two
        // differently-scaled outputs still forces a size-changing configure
        // (see `sync_geometry`'s own doc comment - a window's physical
        // footprint stays constant across the move, so its *logical* size,
        // what the client is actually told, necessarily changes with the
        // new monitor's scale). Until the client catches up, `bbox()` below
        // still reflects its *last real commit* - content sized for the
        // *old* monitor's scale - but `w.monitor` has already flipped to
        // the *new* one (updated live, every drag tick, independently of
        // any commit). Multiplying the stale logical content by the new
        // scale produces neither the old physical size nor the new one, a
        // real mismatch between where the border/shadow get drawn and where
        // the client's actual pixels are. Reported live, confirmed with
        // before/after screenshots: a window's left border missing and its
        // content clipped for one frame right after crossing from a
        // scale-0.843 output onto a scale-1.0 one, self-correcting once the
        // client's own commit landed - see `docs/TODO.md`. `pending_size_
        // configure` (see its own doc comment) already tracks exactly this
        // gap for the throttle above; reusing it here instead of adding new
        // state closes it: while a configure is outstanding, trust this
        // compositor's own live target (`geom`) the same way an active
        // resize already does, rather than reconstructing physical size
        // from a logical value that was never committed under this scale.
        if pending_size_configure.get(&id).is_some_and(|(pending_size, sent_at)| {
            let caught_up = id_to_window.get(&id).is_some_and(|w| w.geometry().size.w == pending_size.0 && w.geometry().size.h == pending_size.1);
            !caught_up && sent_at.elapsed() < CONFIGURE_THROTTLE_TIMEOUT
        }) {
            return geom;
        }
        let Some(w) = wm.borrow().window(id).cloned() else { return geom };
        let Some(dwindow) = id_to_window.get(&id) else { return geom };
        // `dwindow.geometry()` - `xdg_surface::set_window_geometry` - is,
        // per smithay's own implementation, that cached hint *intersected*
        // with `bbox()`, falling back to `bbox()` only if the client never
        // set one. Nothing in the protocol obliges a client to resend the
        // hint on every resize (only when the visible-content-vs-buffer
        // relationship itself changes), and `intersection()` can never
        // return something larger than its smaller operand - so once a
        // client's cached hint is smaller than its current real buffer,
        // `geometry()` stays clamped there permanently, no matter how much
        // larger the buffer grows afterward. Confirmed live: after a
        // *passive* tiling reflow (this window resized only as a side
        // effect of a sibling moving, no direct action on this window
        // itself), Firefox's real content filled the correct, much larger
        // area immediately, but its border/decoration - both driven by
        // this function, see `redraw_decoration_buffer`'s and the render
        // loops' own call sites - stayed rendered at a small fraction of
        // that, unchanged for several seconds (long past both animation
        // settling and any reasonable commit-throttle window), until an
        // unrelated maximize/restore cycle on the same window happened to
        // prompt Firefox into resending a fresh hint and self-correcting.
        //
        // A first fix switched outright to `bbox()` - the real bounding
        // box of the window's current surface tree, which updates on every
        // commit unconditionally - on the reasoning that `sync_geometry`
        // already unconditionally tells every window it is tiled on all
        // four sides specifically so a compliant client reserves no
        // invisible shadow margin, so nothing would be lost by no longer
        // excluding one. Wrong: confirmed live via temporary diagnostic
        // logging, Chrome reserves a real, correctly-current 10px margin on
        // all four sides regardless of the tiled hint (Firefox, the window
        // that exposed the original bug, does not - the two disagree on
        // this, not just on how quickly they resend the hint). Raw `bbox()`
        // gave the border/content mask Chrome's *entire* buffer, margin
        // included - 20px wider and taller than its real visible chrome on
        // each axis, with no compensating position shift - so the rounded
        // border curve traced a rectangle Chrome's real content never
        // reached, and its true, still-square corner poked straight through
        // the curve instead of being hidden by it.
        //
        // The fix keeps both properties at once: a margin, `dwindow_
        // geometry.loc`, assumed symmetric (left == right, top == bottom --
        // true of every real CSD shadow margin observed here: a fixed
        // design constant, not something that scales with window size) is
        // far more durable than the rest of that same hint. A resize
        // changes a client's real content size; it does not change how
        // large that client's own shadow is, so the margin has no
        // equivalent staleness window even while the hint's absolute size
        // does. Subtracting it from the always-fresh `bbox()` - instead of
        // trusting the hint's own absolute size (stale-prone) or using
        // bbox() raw (margin-blind) - gets a content rect that is both
        // current and correctly excludes the invisible margin, for a
        // client that reserves one (Chrome) and one that doesn't (Firefox,
        // where the hint's `.loc` is always `(0, 0)` and this reduces to
        // plain `bbox()`) alike.
        let dwindow_geometry = dwindow.geometry();
        let bbox = dwindow.bbox();
        let margin = (dwindow_geometry.loc.x.max(0), dwindow_geometry.loc.y.max(0));
        let content = Rectangle::new(bbox.loc, (bbox.size.w - 2 * margin.0, bbox.size.h - 2 * margin.1).into());
        if content.size.w <= 0 || content.size.h <= 0 {
            // No real committed content yet - racing the first commit
            // right after creation, most likely. Nothing to correct
            // against, so fall back to the requested rect rather than
            // collapsing every dimension down to (near) zero.
            return geom;
        }
        // `content` (`bbox()`) is in the same *logical* points as
        // `xdg_surface::set_window_geometry` would have been, same as
        // `sync_geometry`'s own `size` going the other direction (see that
        // function's matching doc comment). Every caller of this method
        // (border, shadow, occlusion,
        // resize-margin hit-test) works in this compositor's own physical
        // convention, same as `geom` - so `content.size` needs converting
        // back to physical here, the same `* scale` `sync_geometry` divides
        // by on the way out, or a window on a scaled monitor gets a
        // border/shadow drawn at the *logical* size while its real content
        // renders at a different *physical* one. On a monitor with
        // `scale == 1.0` logical and physical are numerically identical, so
        // this was invisible until this session's own auto-scale feature
        // gave a monitor a non-1.0 value - reported live as a purple
        // border sitting visibly detached, to the east and south, from an
        // undecorated (CSD) window's real content once that happened.
        let scale = wm.borrow().monitors().iter().find(|m| m.id == w.monitor).map(|m| m.scale).unwrap_or(1.0);
        let content_physical = ((content.size.w as f64 * scale).round() as i32, (content.size.h as f64 * scale).round() as i32);
        let band = if w.decorated { TITLEBAR_HEIGHT as i32 } else { 0 };
        srdwm_core::Rect { x: geom.x, y: geom.y, width: content_physical.0.max(0) as u32, height: (band + content_physical.1.max(0)) as u32 }
    }

    /// The rect a window's border, shadow, occlusion test, and resize-
    /// margin hit-test should actually use - `geom` (the requested target,
    /// or mid-animation the interpolated rect) with its width/height
    /// replaced by what the client's own surface really committed, when
    /// that's known and non-degenerate. `x`/`y` are left untouched: the
    /// top-left corner is already correctly anchored by `content_offset`
    /// elsewhere (`sync_geometry`/the render loops), only the far edge can
    /// end up wrong.
    ///
    /// `Window.geometry` (what `geom`'s width/height ultimately come from)
    /// is this compositor's own *request* - what `sync_geometry` asked the
    /// client to become via `xdg_toplevel::configure`'s `size`. Nothing
    /// before this ever read back whether the client actually complied.
    /// Most do, to the pixel - but a client with its own internal size
    /// quantization (a terminal emulator, snapping its real content to a
    /// whole number of character cells) can settle on a slightly different
    /// real size than what was requested, without that being any kind of
    /// protocol violation. Every caller of this method used to read `geom`
    /// directly regardless, so the border (and the shadow, and the resize-
    /// margin hit-test) kept drawing/testing at the *asked-for* edge while
    /// the client's real content stopped a few pixels short of it --
    /// reported live as a transparent gap between a terminal's content and
    /// srdwm's own border, letting the desktop show through underneath.
    ///
    /// Niri's own `LayoutElement::size` (`src/window/mapped.rs` in its
    /// source) is the model this follows: its entire layout - tile size,
    /// border, focus ring - is driven by `self.window.geometry().size`,
    /// the client's real, committed value, never by whatever niri itself
    /// originally requested. This mirrors that for the specific things
    /// srdwm draws that have to visually hug the real edge. Deliberately
    /// narrow, not a wholesale switch: `Space` positioning, the
    /// `xdg_toplevel::configure` math itself, and tiling layout all keep
    /// reading `Window.geometry` unchanged - those are about this
    /// compositor's own bookkeeping staying self-consistent, not about
    /// matching a client's real pixels.
    pub(crate) fn effective_frame(&self, id: WindowId, geom: srdwm_core::Rect) -> srdwm_core::Rect {
        Self::effective_frame_of(&self.wm, &self.id_to_window, &self.pending_size_configure, id, geom)
    }

    pub(crate) fn sync_geometry(&mut self, id: WindowId) {
        // A pending `anim_from` (set by `toggle_maximize`/`toggle_fullscreen`,
        // or by `new_managed_window` for the open-slide) means the target
        // geometry below is where this window is *headed*, not where it
        // should appear right now - register (or replace) a tween and use
        // `WindowAnim::current_rect` in its place for this call and every
        // `tick_animations` call afterward, until it completes. `take()`
        // both reads and clears it, so a later, non-animated `sync_geometry`
        // call for the same window (an ordinary drag/resize frame) goes
        // straight back to applying `geometry` immediately, as before.
        let anim_from = self.wm.borrow_mut().window_mut(id).and_then(|w| w.anim_from.take());
        let Some((target, decorated, maximized, fullscreen, monitor)) =
            self.wm.borrow().window(id).map(|w| (w.geometry, w.decorated, w.maximized, w.fullscreen, w.monitor))
        else {
            return;
        };
        // This compositor's own placement/geometry tracking is physical
        // pixels throughout (see `Platform::monitors()`'s own doc comment
        // on that choice); `xdg_toplevel::configure`'s `size` is specified
        // to carry *logical* points, always, independent of which output a
        // window is on. Every output was `1.0` before this session's own
        // auto-scale feature existed, so physical and logical were
        // numerically identical and this conversion's absence was
        // invisible. Falls back to `1.0` (no conversion) if this window's
        // own monitor can't be resolved - the same "assume unscaled
        // rather than guess" default `MonitorInfo::scale`'s own doc
        // comment already uses for a disabled output.
        let scale = self.wm.borrow().monitors().iter().find(|m| m.id == monitor).map(|m| m.scale).unwrap_or(1.0);
        if let Some(from) = anim_from {
            let duration_ms = self.wm.borrow().animation_duration_ms;
            if from != target && duration_ms > 0 {
                self.window_anims
                    .insert(id, WindowAnim { from, to: target, start: Instant::now(), duration: Duration::from_millis(duration_ms as u64) });
            }
        }
        let geom = self.window_anims.get(&id).map(WindowAnim::current_rect).unwrap_or(target);
        // The titlebar band is only actually reserved when there is one --
        // an undecorated window (client-side decoration, see
        // `set_decorated_from_mode`) gets the whole of `geom` as content,
        // not `geom` minus a band that's no longer being drawn. Without
        // this, a window that negotiated client-side decoration kept the
        // same 30px gap at its top anyway: our titlebar wasn't drawn there
        // (correctly), but the content was still offset down and told it
        // was 30px shorter than the window actually is, leaving a blank
        // strip and the frame sitting visibly wrong relative to what's
        // inside it.
        let band = if decorated { TITLEBAR_HEIGHT as i32 } else { 0 };
        // Position always moves with the pointer; only a size change needs a
        // client configure or a titlebar re-render (see `last_synced_size`'s
        // doc comment).
        //
        // Converted to logical points here, before anything below reads
        // `size` - `xdg_toplevel::configure` is specified to carry
        // logical points, and `w.geometry()` (what the throttle check
        // below compares a client's real commit against) is a client's own
        // `xdg_surface::set_window_geometry`, logical by the same
        // specification - so keeping the rest of this function in that
        // one space, not switching back to physical partway through, is
        // what actually keeps every comparison here meaningful.
        //
        // This has a real, desirable second effect beyond fixing the unit
        // mismatch itself: a window that crosses onto a monitor with a
        // different scale, at the *same* physical size (an ordinary drag
        // never changes `geom.width`/`geom.height`), now computes a
        // *different* logical size purely from `scale` changing --
        // correctly triggering a fresh configure asking the client to
        // resize to match, the same way real desktop environments keep a
        // window's true on-screen footprint consistent across a DPI
        // change. Before this, a plain cross-monitor drag sent no configure
        // at all (physical size hadn't changed), so the client kept
        // rendering its old logical size at the new monitor's different
        // scale while this compositor's own border kept drawing at the
        // physical rect it always had - reported live as a window's
        // border ending up visibly detached from its own content after
        // being dragged to the other monitor.
        let size_physical = (geom.width as i32, geom.height as i32 - band);
        let size = ((size_physical.0 as f64 / scale).round() as i32, (size_physical.1 as f64 / scale).round() as i32);
        // Peeked, not inserted yet - only actually updated once a
        // configure for `size` is decided below, so a size that keeps
        // changing tick to tick while throttled (an active drag didn't
        // stop just because the client hasn't caught up yet) is still
        // correctly seen as "different from what's actually been sent"
        // on every later tick, not just the first.
        let size_changed = self.last_synced_size.get(&id).copied() != Some(size);
        let mut moved = false;
        if let Some(w) = self.id_to_window.get(&id) {
            // `w.geometry().loc` is the client's own `xdg_surface::
            // set_window_geometry` offset - a CSD client (GTK4/Firefox
            // concretely) declares its real visible content as a sub-rect
            // inset within a larger buffer that also reserves an invisible
            // shadow margin, even once the tiled-state hint below has told
            // it to skip drawing that shadow.
            //
            // This used to be subtracted from `location` right here, on the
            // reasoning that `space` needed to be told about it explicitly,
            // the same way `render_udev_frame`/`winit/render.rs` do for
            // drawing. That reasoning was wrong about `Space` specifically:
            // smithay's own `SpaceElement for Window` reports `geometry()`
            // as `self.geometry()` (this exact `content_offset`, non-zero
            // `.loc` included), and `Space`'s internal `render_location()`
            // (what every hit-test - `element_under`, and so `refresh_
            // pointer_focus`'s `win_relative = pos - loc` - actually reads)
            // already computes `location - element.geometry().loc` on its
            // own, unconditionally, for every mapped element. Subtracting
            // `content_offset` again here meant `Space`'s own tracked
            // position ended up short by *two* `content_offset`s, not one --
            // confirmed live via temporary diagnostic logging on both sides:
            // this call computing a correct, single-subtraction position,
            // and `Space::element_under` reporting a position exactly one
            // more `content_offset` short of it for the same window on the
            // very same commit. The render loops' own manual subtraction is
            // unaffected and stays - they position elements by hand,
            // entirely bypassing `Space`'s automatic handling, so they still
            // have to do this themselves; `xwayland.rs`'s own `map_element`
            // calls already never did this (X11 windows have no equivalent
            // shadow-margin geometry), which in hindsight was the correct
            // pattern being followed there all along.
            self.space.map_element(w.clone(), (geom.x, geom.y + band), false);
            moved = true;
            if let Some(top) = w.toplevel() {
                // xdg-shell position is a purely compositor-side concept --
                // the client is never told it - so only a size change
                // needs a configure here.
                //
                // Throttled to at most one size-changing configure "in
                // flight" per window, the same way niri does (`window/
                // mapped.rs`'s `ConfigureIntent::Throttled`) - see
                // `pending_size_configure`'s own doc comment for why: this
                // used to send a fresh configure on every single pointer-
                // motion tick of an active resize regardless of whether the
                // client had caught up to the *previous* one yet, which a
                // fast pointer (a real high-poll-rate mouse, niri's own
                // stated motivation for the same throttle) could easily
                // outrun into a real backlog. `w.geometry().size` is the
                // client's actual last-committed content size - once it
                // matches whatever was last sent, that configure is
                // considered caught up and the throttle clears on its own,
                // no separate ack-tracking needed. Bounded by
                // `CONFIGURE_THROTTLE_TIMEOUT` regardless, so a client that
                // never catches up for any reason (slow, buggy, wedged)
                // can't jam resizing shut forever - the same self-healing
                // shape as this session's own DRM flip-pending watchdog.
                let throttled = self.pending_size_configure.get(&id).is_some_and(|(pending_size, sent_at)| {
                    let caught_up = w.geometry().size.w == pending_size.0 && w.geometry().size.h == pending_size.1;
                    !caught_up && sent_at.elapsed() < CONFIGURE_THROTTLE_TIMEOUT
                });
                if size_changed && !throttled {
                    // See `Window::size_is_provisional`'s own doc comment:
                    // the very first configure for a window whose size was
                    // never a real decision - just `Window::new`'s/a
                    // backend's own hardcoded guess - tells the client to
                    // pick its own size (`state.size = None`) instead of
                    // forcing this one on it. `last_synced_size` having no
                    // entry yet is exactly "this is that first configure";
                    // checked before the `insert` just below overwrites it.
                    let let_client_choose = self.provisional_size.contains(&id) && !self.last_synced_size.contains_key(&id);
                    self.last_synced_size.insert(id, size);
                    self.pending_size_configure.insert(id, (size, Instant::now()));
                    top.with_pending_state(|state| {
                        state.size = if let_client_choose { None } else { Some(size.into()) };
                        // No configure from this compositor, ever, set any
                        // `xdg_toplevel` state bit at all before this --
                        // confirmed by grepping the whole crate for
                        // `xdg_toplevel::State`, zero hits. GTK4 (Firefox
                        // concretely) reads the tiled bits to decide whether
                        // to reserve its own invisible client-side shadow
                        // margin around its actual content, independent of
                        // whether decoration is server- or client-side --
                        // with none ever sent, it always assumed "floating,
                        // might need a shadow" and kept reserving one. That
                        // margin sits inside the committed buffer but is
                        // functionally invisible, so this compositor's own
                        // border - drawn at the *full* geometry, margin
                        // included, since nothing here knew the margin
                        // existed - ended up visibly offset from where the
                        // client's real chrome began. Reported live as
                        // Firefox's border "not with the window," and more
                        // generally never feeling like part of it. Setting
                        // all four unconditionally (the same technique
                        // river/dwl use) tells every window it's flush
                        // against something and should skip its own shadow,
                        // regardless of whether it's actually in a tiled
                        // layout - which is the outcome actually wanted:
                        // this compositor draws the frame, so nothing else
                        // should also be reserving room for one.
                        state.states.set(xdg_toplevel::State::TiledLeft);
                        state.states.set(xdg_toplevel::State::TiledRight);
                        state.states.set(xdg_toplevel::State::TiledTop);
                        state.states.set(xdg_toplevel::State::TiledBottom);
                        // Same "no configure from this compositor ever set
                        // this" gap as the tiled bits above, confirmed the
                        // same way (grepped the whole crate for `State::
                        // Maximized`/`State::Fullscreen` outside foreign-
                        // toplevel-management, which is a *different*
                        // protocol read by external tools like a taskbar,
                        // not the client's own `xdg_toplevel` configure --
                        // zero hits there before this). The window was
                        // resized to the full monitor rect and told it was
                        // tiled on every side, but never actually told via
                        // the real protocol mechanism for it that it was
                        // maximized or fullscreen at all - indistinguishable
                        // from an ordinary tiled-to-the-edges floating
                        // window as far as the client could tell. Reported
                        // live as fullscreen leaving a persistent gap along
                        // one edge (Firefox keeping some of its own chrome
                        // logic that specifically keys off genuinely
                        // *knowing* it's fullscreen, not just being resized
                        // to fullscreen-sized). `unset` the other explicitly
                        // when only one applies - `WindowManager::
                        // toggle_fullscreen`/`toggle_maximize` are mutually
                        // exclusive, but nothing here should assume that
                        // holds forever just because it does today.
                        if maximized {
                            state.states.set(xdg_toplevel::State::Maximized);
                        } else {
                            state.states.unset(xdg_toplevel::State::Maximized);
                        }
                        if fullscreen {
                            state.states.set(xdg_toplevel::State::Fullscreen);
                        } else {
                            state.states.unset(xdg_toplevel::State::Fullscreen);
                        }
                    });
                    top.send_configure();
                }
            } else if let Some(x11) = w.x11_surface() {
                // Unlike xdg-shell, an X11 client's real on-screen position
                // is part of its own window state - it has to be told on
                // every move, not just every resize, the same way a real
                // X11 window manager sends continuous `ConfigureNotify`
                // during an interactive drag. Without this branch at all,
                // `sync_geometry` never reconfigured an XWayland window a
                // second time past its initial map: `space.map_element`
                // above still moved smithay's own tracked position (see
                // `resync_stacking_order`'s doc comment for the real
                // z-order side effect that has, since fixed below) and the
                // border/titlebar still redrew at the new `Window.geometry`
                // (both read it fresh every frame), but the real X11
                // client window was never told to move or resize - any
                // drag, resize, maximize, edge-snap, or tiling re-layout of
                // an XWayland-backed app left its actual content frozen at
                // its original position/size forever while srdwm's own
                // decoration moved freely around it.
                let _ = x11.configure(Rectangle::new((geom.x, geom.y + band).into(), size.into()));
            }
        }
        // Not gated on `self.decorations.contains_key(&id)` - that map only
        // ever holds an entry for a *decorated* window (see
        // `redraw_decoration_buffer`, which only inserts into it when
        // `w.decorated`), so that gate was permanently false for every
        // undecorated/CSD window, even one with `border_width > 0`. Its
        // border bitmaps were rendered once at creation and never rebuilt on
        // any later resize - reported live as the border "not truly around"
        // the window after resizing. `redraw_decoration_buffer` already
        // self-guards via `decoration_signatures` (see its own doc comment),
        // so calling it unconditionally here costs nothing once the size
        // genuinely hasn't changed the rasterized output.
        if size_changed {
            self.redraw_decoration_buffer(id);
        }
        // See `resync_stacking_order`'s doc comment: `map_element` above
        // always re-stacks its target to the top of `Space`'s own order as
        // a side effect of updating position, `activate` or not - and
        // `sync_geometry` runs for reasons with nothing to do with raising
        // a window (a title changing, an ordinary resize frame), so left
        // uncorrected this silently, non-deterministically desynced
        // `Space`'s notion of "on top" from `WindowManager`'s.
        if moved {
            self.resync_stacking_order();
        }
    }

    /// Called from `CompositorHandler::commit`, right after `w.on_commit()`
    /// - the first time a window still in `provisional_size` commits a
    /// real, non-empty buffer, adopts the client's own chosen content size
    /// into `Window::geometry` instead of leaving `add_window`'s guessed
    /// placeholder in place. A no-op once `provisional_size` no longer
    /// names this window (the ordinary case, checked first, so every other
    /// commit pays only one `HashSet` lookup).
    ///
    /// Position is left exactly where `SmartPlacement` put it - only
    /// clamped so a client that picked a bigger size than the guess can't
    /// end up hanging off its monitor's right/bottom edge - since the
    /// guessed size was only ever wrong about *size*; the cascade/grid
    /// position it computed is still a perfectly good place for a window
    /// of any size to open.
    pub(crate) fn adopt_provisional_size(&mut self, id: WindowId) {
        if !self.provisional_size.contains(&id) {
            return;
        }
        let Some(dwindow) = self.id_to_window.get(&id) else { return };
        // Logical points, same as `xdg_toplevel::configure`'s own `size`
        // (see `sync_geometry`'s doc comment on that) - converted to this
        // compositor's physical-pixel `Rect` space below via the window's
        // own monitor scale, the same conversion `sync_geometry` does in
        // reverse.
        let content = dwindow.geometry();
        if content.size.w <= 0 || content.size.h <= 0 {
            // Compositor/role-only commit, no real buffer attached yet --
            // wait for the commit that actually has one.
            return;
        }
        self.provisional_size.remove(&id);
        let mut wm = self.wm.borrow_mut();
        let Some(w) = wm.window(id) else { return };
        let scale = wm.monitors().iter().find(|m| m.id == w.monitor).map(|m| m.scale).unwrap_or(1.0);
        let monitor_geometry = wm.monitors().iter().find(|m| m.id == w.monitor).map(|m| m.geometry);
        let band = if w.decorated { TITLEBAR_HEIGHT } else { 0 };
        let width = ((content.size.w as f64 * scale).round() as u32).max(srdwm_core::placement::MIN_WINDOW_WIDTH);
        let height = ((content.size.h as f64 * scale).round() as u32).max(srdwm_core::placement::MIN_WINDOW_HEIGHT) + band;
        let Some(w) = wm.window_mut(id) else { return };
        w.geometry.width = width;
        w.geometry.height = height;
        if let Some(monitor) = monitor_geometry {
            w.geometry.x = w.geometry.x.min(monitor.right() - width as i32).max(monitor.x);
            w.geometry.y = w.geometry.y.min(monitor.bottom() - height as i32).max(monitor.y);
        }
    }
}