srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/state/geometry.rs
blob: 21cac8db4643b547e39987a09ea8e51879279af9 (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
use super::*;

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);
            }
        }
    }

    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)) = self.wm.borrow().window(id).map(|w| (w.geometry, w.decorated, w.maximized, w.fullscreen)) else { return };
        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).
        let size = (geom.width as i32, geom.height as i32 - band);
        let size_changed = self.last_synced_size.insert(id, size) != 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. `render_udev_frame`/
            // `winit/render.rs` both subtract this same offset from where
            // they draw the window's content, specifically so the client's
            // visible content lands at `geom.x, geom.y` instead of a
            // shadow-margin's width/height short of it - `space` has to
            // agree with that adjustment, not just rendering, or every
            // click computed via `win_relative = pos - space_loc` would
            // land `content_offset` short of whatever the user actually
            // clicked on: rendering moves the content, hit-testing keeps
            // routing against where the client's raw, unshifted buffer
            // origin used to be.
            let content_offset = w.geometry().loc;
            self.space.map_element(w.clone(), (geom.x - content_offset.x, geom.y + band - content_offset.y), 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.
                if size_changed {
                    top.with_pending_state(|state| {
                        state.size = 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()));
            }
        }
        if size_changed && self.decorations.contains_key(&id) {
            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();
        }
    }
}