srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/protocols.rs
blob: 391f66540186162b1d6c382e64536a8438db9af3 (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
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
//! smithay protocol-handler implementations for [`CompState`], plus the
//! `delegate_*!` macros that route each protocol's dispatch to them.
//!
//! Deliberately thin: these methods translate a protocol event into a call
//! on [`crate::state`] (window bookkeeping) or [`crate::input`] (focus), and
//! hold no logic of their own beyond what the protocol itself dictates. The
//! session-lock handler is the one exception, living in [`crate::lock`]
//! alongside the rest of that feature.

use smithay::desktop::{find_popup_root_surface, layer_map_for_output, LayerSurface as DesktopLayerSurface, PopupKeyboardGrab, PopupKind, PopupPointerGrab};
use smithay::input::pointer::{CursorImageStatus, Focus};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_seat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::Client;
use smithay::reexports::wayland_server::Resource;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::ImportDma;
use smithay::utils::Serial;
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::xdg_activation::{XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData};
use smithay::wayland::input_method::PopupSurface as ImePopupSurface;
use smithay::wayland::selection::data_device::{
    ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
};
use smithay::wayland::selection::primary_selection::{PrimarySelectionHandler, PrimarySelectionState};
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
use smithay::wayland::compositor::{add_pre_commit_hook, with_states};
use smithay::wayland::selection::SelectionHandler;
use smithay::wayland::shell::wlr_layer::{
    Layer, LayerSurface as WlrLayerSurface, LayerSurfaceCachedState, WlrLayerShellHandler, WlrLayerShellState,
};
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState};
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::wayland::tablet_manager::TabletSeatHandler;
use smithay::{
    delegate_compositor, delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
    delegate_layer_shell, delegate_output, delegate_primary_selection, delegate_seat, delegate_session_lock,
    delegate_shm, delegate_xdg_activation, delegate_xdg_decoration, delegate_xdg_shell,
    delegate_input_method_manager, delegate_text_input_manager,
};

use crate::state::{ClientState, CompState};

impl CompositorHandler for CompState {
    fn compositor_state(&mut self) -> &mut CompositorState {
        &mut self.compositor_state
    }

    fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
        // Two possible client kinds now: our own `ClientState` for regular
        // Wayland clients, or smithay's `XWaylandClientData` for the single
        // XWayland client (see `xwayland.rs`) - both carry a
        // `CompositorClientState`, just under different wrapper types.
        if let Some(state) = client.get_data::<ClientState>() {
            return &state.compositor_state;
        }
        &client.get_data::<smithay::xwayland::XWaylandClientData>().expect("client is neither ours nor XWayland's").compositor_state
    }

    /// Workaround for a real smithay bug (see docs/PANEL_SUPPORT_TODO.md and
    /// `layer_destroyed` below): destroying a `zwlr_layer_surface_v1` role
    /// resets the surface's `LayerSurfaceCachedState` to
    /// `Default::default()` (size 0x0, no anchor) rather than removing it,
    /// but the pre-commit hook smithay itself registers at
    /// `get_layer_surface` time keeps validating that state against every
    /// future commit regardless of whether the role still exists --
    /// tripping its own `width/height 0 requested without ... anchors`
    /// check and posting `invalid_size`, which kills the client's whole
    /// connection over what is protocol-legal (committing a now-roleless
    /// surface).
    ///
    /// Fixed by registering our own pre-commit hook here, in `new_surface`
    /// - called at `wl_compositor.create_surface`, strictly before any
    /// later `get_layer_surface` on the same surface could register
    /// smithay's own hook. Hooks run in registration order (`tree.rs`:
    /// `pre_commit_hooks` is a plain `Vec`, pushed and iterated in order),
    /// so ours always runs first and can neutralize the stale reset state
    /// before smithay's hook ever inspects it. This depends on that
    /// ordering guarantee holding in future smithay versions - it isn't
    /// documented as an API contract, just an implementation detail
    /// confirmed against 0.7.0's source - so re-check this file against
    /// whatever smithay version replaces it.
    ///
    /// Cost: one closure registered per `wl_surface` (not just layer
    /// surfaces, since we don't know in advance which ones will become
    /// one), each a no-op unless that exact surface is in
    /// `dead_layer_surfaces`.
    fn new_surface(&mut self, surface: &WlSurface) {
        add_pre_commit_hook::<CompState, _>(surface, |state, _dh, surface| {
            if !state.dead_layer_surfaces.contains(surface) {
                return;
            }
            with_states(surface, |states| {
                let mut cached = states.cached_state.get::<LayerSurfaceCachedState>();
                let pending = cached.pending();
                if pending.size.w == 0 && !pending.anchor.anchored_horizontally() {
                    pending.size.w = 1;
                }
                if pending.size.h == 0 && !pending.anchor.anchored_vertically() {
                    pending.size.h = 1;
                }
            });
        });
    }

    fn commit(&mut self, surface: &WlSurface) {
        smithay::backend::renderer::utils::on_commit_buffer_handler::<CompState>(surface);
        // XWayland's association of an X11 window with this wl_surface can
        // arrive at any point relative to the map request (see
        // `xwayland.rs`'s module docs); `surface_associated` handles the
        // common ordering, this retries the surfaces still waiting on a
        // commit to actually make that association queryable.
        self.retry_pending_x11_windows();
        if let Some(&id) = self.surface_to_id.get(surface) {
            if let Some(w) = self.id_to_window.get(&id) {
                w.on_commit();
            }
            // See `content_epoch`'s doc comment: this is the only per-commit
            // signal the udev backend's rounded-corner mask cache has to
            // invalidate itself, since content can change every frame,
            // independent of the geometry-driven points `redraw_decoration_
            // buffer` already runs at.
            *self.content_epoch.entry(id).or_insert(0) += 1;
            crate::state::sync_toplevel_metadata(self, id, surface);
        }
        // Before `ensure_layer_initial_configure`: if this commit just
        // hid or re-showed a layer surface, `sync_layer_visibility` needs
        // to unmap/re-map it first, so the lookup that function does via
        // `layer_for_surface` sees the corrected state rather than acting
        // on stale membership in `LayerMap`'s own list.
        self.sync_layer_visibility(surface);
        self.ensure_layer_initial_configure(surface);
        // Advances a just-created popup from unmapped to mapped (needed for
        // `PopupManager::popups_for_surface`, which `popup_render_elements`
        // reads at render time) and prunes dead ones. Cheap and only does
        // real work on a popup-role surface, so doing it on every commit
        // rather than throttling is not worth the extra bookkeeping.
        self.popups.commit(surface);
        self.popups.cleanup();
    }
}

impl XdgShellHandler for CompState {
    fn xdg_shell_state(&mut self) -> &mut XdgShellState {
        &mut self.xdg_shell_state
    }

    fn new_toplevel(&mut self, surface: ToplevelSurface) {
        self.new_managed_window(surface);
    }

    /// `move_request`/`resize_request` were also still smithay's default
    /// no-op implementations - a much larger gap than the five below:
    /// this is *how a client-side-decorated window gets dragged or resized
    /// by its own titlebar/edges at all*. A window we draw our own
    /// decoration for never needed this (`TitlebarHit::Drag`/`Resize` in
    /// `input.rs` detect the click directly, since we own those pixels),
    /// but a window that negotiated client-side decoration and draws its
    /// own titlebar - Firefox, and most GTK4 apps by default - handles
    /// the click itself and then asks the compositor to actually perform
    /// the move/resize via exactly these two requests. Left unimplemented,
    /// dragging or resizing any such window by its own chrome did
    /// nothing at all - the only way to reposition it was the
    /// modifier+drag-anywhere gesture (`bindm`), which most users have no
    /// reason to know exists and doesn't work for resize-from-a-specific-
    /// edge at all. Reuses the exact same `WindowManager::start_drag`/
    /// `start_resize` the pointer-driven titlebar handlers call --
    /// `handle_pointer_position`/`handle_pointer_button` already drive any
    /// in-progress drag/resize to completion on subsequent motion/release
    /// regardless of what started it, so no smithay pointer grab is
    /// needed here at all, just the same start call from a different
    /// trigger.
    fn move_request(&mut self, surface: ToplevelSurface, _seat: wl_seat::WlSeat, _serial: Serial) {
        // Temporary: added to trace a live report that dragging a CSD
        // window (Firefox) by its own tab strip/header bar does nothing --
        // this is the only way to tell "the client never sent xdg_toplevel
        // ::move at all" apart from "it sent it and something downstream
        // of here didn't follow through." Remove once that's settled.
        match self.surface_to_id.get(surface.wl_surface()) {
            Some(&id) => {
                let pos = crate::input::last_pointer_pos(self);
                log::info!("move_request: window {id:?} at pointer {pos:?}");
                self.wm.borrow_mut().start_drag(id, pos.x as i32, pos.y as i32);
            }
            None => log::warn!("move_request: surface has no tracked window id"),
        }
    }

    fn resize_request(&mut self, surface: ToplevelSurface, _seat: wl_seat::WlSeat, _serial: Serial, edges: xdg_toplevel::ResizeEdge) {
        let Some(edge) = (match edges {
            xdg_toplevel::ResizeEdge::Top => Some(srdwm_core::ResizeEdge::Top),
            xdg_toplevel::ResizeEdge::Bottom => Some(srdwm_core::ResizeEdge::Bottom),
            xdg_toplevel::ResizeEdge::Left => Some(srdwm_core::ResizeEdge::Left),
            xdg_toplevel::ResizeEdge::Right => Some(srdwm_core::ResizeEdge::Right),
            xdg_toplevel::ResizeEdge::TopLeft => Some(srdwm_core::ResizeEdge::TopLeft),
            xdg_toplevel::ResizeEdge::TopRight => Some(srdwm_core::ResizeEdge::TopRight),
            xdg_toplevel::ResizeEdge::BottomLeft => Some(srdwm_core::ResizeEdge::BottomLeft),
            xdg_toplevel::ResizeEdge::BottomRight => Some(srdwm_core::ResizeEdge::BottomRight),
            // `None` is a valid protocol value (the client leaves the edge
            // unspecified) but `WindowManager::start_resize` needs one --
            // there's nothing sensible to default it to that wouldn't be a
            // guess, so this is a no-op rather than picking one.
            _ => None,
        }) else {
            return;
        };
        if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) {
            let pos = crate::input::last_pointer_pos(self);
            self.wm.borrow_mut().start_resize(id, edge, pos.x as i32, pos.y as i32);
        }
    }

    /// `maximize_request`/`unmaximize_request`/`fullscreen_request`/
    /// `unfullscreen_request`/`minimize_request` were all still smithay's
    /// default no-op (or configure-only) implementations - found
    /// investigating the `toggle_fullscreen` decoration bug above, by
    /// checking what else routes through the same `WindowManager` calls
    /// the titlebar-button click handlers in `input.rs` already use.
    /// These five are the *client-initiated* equivalent of those clicks: a
    /// client's own window-menu "Maximize", pressing F11, an HTML5 video
    /// going fullscreen, or (for a client that negotiated client-side
    /// decoration and draws its own titlebar, like Firefox) that titlebar's
    /// own maximize button - all ask the compositor to actually perform
    /// the state change via these requests rather than the compositor
    /// noticing on its own. Left unimplemented, every one of them was a
    /// silent no-op: the client's button did nothing, with no error and
    /// nothing to suggest why, from any app that relies on this instead of
    /// (or in addition to) a compositor-side keybinding.
    fn maximize_request(&mut self, surface: ToplevelSurface) {
        if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) {
            if !self.wm.borrow().window(id).is_some_and(|w| w.maximized) {
                self.wm.borrow_mut().toggle_maximize(id);
                self.sync_geometry(id);
                crate::foreign_toplevel::send_state(self, id);
            }
        }
        surface.send_configure();
    }

    fn unmaximize_request(&mut self, surface: ToplevelSurface) {
        if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) {
            if self.wm.borrow().window(id).is_some_and(|w| w.maximized) {
                self.wm.borrow_mut().toggle_maximize(id);
                self.sync_geometry(id);
                crate::foreign_toplevel::send_state(self, id);
            }
        }
        surface.send_configure();
    }

    /// `_output` (the client's requested target output) is ignored --
    /// single-seat, and every other fullscreen entry point (the titlebar
    /// button, `srd.window.fullscreen()`) already fullscreens on whatever
    /// monitor the window is already on, so this matches that instead of
    /// introducing an output-aware fullscreen path only this one request
    /// would use.
    fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<WlOutput>) {
        if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) {
            if !self.wm.borrow().is_fullscreen(id) {
                // `redraw_decoration_buffer` first, same reason
                // `set_decorated_from_mode` calls it before `sync_geometry`:
                // fullscreen also flips `Window.decorated`, and dropping
                // the decoration needs the buffer actually removed, not
                // just left stale for `sync_geometry`'s own resize-only
                // redraw check to skip.
                self.wm.borrow_mut().toggle_fullscreen(id);
                self.redraw_decoration_buffer(id);
                self.sync_geometry(id);
                crate::foreign_toplevel::send_state(self, id);
            }
        }
        surface.send_configure();
    }

    fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
        if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) {
            if self.wm.borrow().is_fullscreen(id) {
                self.wm.borrow_mut().toggle_fullscreen(id);
                self.redraw_decoration_buffer(id);
                self.sync_geometry(id);
                crate::foreign_toplevel::send_state(self, id);
            }
        }
        surface.send_configure();
    }

    /// No `send_configure` here, matching the pointer-driven
    /// `TitlebarHit::Minimize` handler in `input.rs`: minimizing doesn't
    /// change the window's own size, only whether it's currently shown, so
    /// there's nothing new to tell the client about its own geometry.
    fn minimize_request(&mut self, surface: ToplevelSurface) {
        if let Some(&id) = self.surface_to_id.get(surface.wl_surface()) {
            self.wm.borrow_mut().minimize_window(id);
            crate::foreign_toplevel::send_state(self, id);
        }
    }

    /// Was a bare no-op - no `send_configure` at all. Per xdg-shell,
    /// `xdg_surface.configure` is required before a popup's first commit;
    /// real toolkits (confirmed live: GTK4's Wayland backend) block that
    /// commit in a synchronous roundtrip waiting for it, so every popup
    /// hung its client forever. GTK4 implements tooltips *and*
    /// `Gtk.Popover` as `xdg_popup`, so this fired on hovering almost any
    /// widget with a tooltip - confirmed by a peer session's gdb backtrace
    /// (blocked in `wl_display_dispatch_queue` under `gtk_widget_show`)
    /// after AGS wedged.
    ///
    /// Geometry is `positioner.get_geometry()` un-constrained - no
    /// on-screen clamping yet (`PositionerState::get_unconstrained_geometry`
    /// needs a target rect in the parent's surface-local space, which is a
    /// real follow-up, not this fix); an occasional popup placed near a
    /// screen edge may render partly off it, which is cosmetic, not a hang.
    fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {
        surface.with_pending_state(|state| {
            state.geometry = positioner.get_geometry();
            state.positioner = positioner;
        });
        if surface.send_configure().is_err() {
            return;
        }
        let _ = self.popups.track_popup(smithay::desktop::PopupKind::Xdg(surface));
    }

    /// Implicit grab + dismiss-on-outside-click. Previously believed
    /// blocked on `CompState`'s `SeatHandler` associated types not
    /// satisfying `PopupManager::grab_popup`'s `WaylandFocus +
    /// From<PopupKind>` bound - rechecked while implementing
    /// `move_request`/`resize_request` (same trait, adjacent methods) and
    /// it turns out they already do: `KeyboardFocus`/`PointerFocus` are
    /// both plain `WlSurface`, smithay provides `impl From<PopupKind> for
    /// WlSurface` itself, and `WlSurface: From<WlSurface>` trivially. No
    /// blocker ever existed by the time of this pass; the bound just
    /// hadn't been rechecked since being noted as unmet.
    ///
    /// `self.seat.clone()` rather than resolving `_seat` (the client's
    /// `wl_seat` resource) via `Seat::from_resource` - this compositor
    /// only ever has the one seat, matching how `move_request`/
    /// `resize_request` already ignore the same parameter.
    fn grab(&mut self, surface: PopupSurface, _seat: wl_seat::WlSeat, serial: Serial) {
        let popup = PopupKind::Xdg(surface);
        let Ok(root) = find_popup_root_surface(&popup) else {
            log::warn!("POPUP-GRAB-DIAG find_popup_root_surface failed");
            return;
        };
        let seat = self.seat.clone();
        let grab = match self.popups.grab_popup(root, popup, &seat, serial) {
            Ok(g) => g,
            Err(e) => {
                log::warn!("POPUP-GRAB-DIAG grab_popup failed: {e:?}");
                return;
            }
        };
        log::warn!("POPUP-GRAB-DIAG grab established, has_pointer={} has_keyboard={}", seat.get_pointer().is_some(), seat.get_keyboard().is_some());
        if let Some(keyboard) = seat.get_keyboard() {
            keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
        }
        if let Some(pointer) = seat.get_pointer() {
            pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
        }
    }

    fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) {
        surface.with_pending_state(|state| {
            state.geometry = positioner.get_geometry();
            state.positioner = positioner;
        });
        surface.send_repositioned(token);
    }

    fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
        self.remove_window(surface.wl_surface());
    }
}

impl XdgDecorationHandler for CompState {
    /// Offers whichever mode `theme.decorations.default_mode`/`srd set
    /// decoration_mode` currently prefers - a client with a real opinion
    /// of its own still overrides this via `request_mode` below regardless
    /// of what's offered here; this only decides what a client with *no*
    /// preference ends up with. See `srdwm_core::ThemeConfig::
    /// default_decorated`'s own doc comment for why this is configurable
    /// rather than hardcoded to one mode.
    fn new_decoration(&mut self, toplevel: ToplevelSurface) {
        let offer = if self.wm.borrow().theme.default_decorated { DecorationMode::ServerSide } else { DecorationMode::ClientSide };
        toplevel.with_pending_state(|state| {
            state.decoration_mode = Some(offer);
        });
    }

    /// Honors whichever mode the client actually asked for, rather than
    /// always forcing server-side - and mirrors the result into our own
    /// `Window.decorated`, so a client drawing its own titlebar doesn't
    /// *also* get one drawn on top of it by us.
    ///
    /// Always forcing `ServerSide` (what this used to do) is why some
    /// clients ended up with two sets of window buttons: Firefox requests
    /// client-side decoration when its own "use system titlebar" setting
    /// is off, and draws its own close/minimize/maximize row regardless of
    /// what the compositor grants - so forcing server-side just added
    /// srdwm's row on top of the one Firefox was drawing anyway, instead
    /// of preventing it. Respecting the request means srdwm steps out of
    /// the way for exactly those clients, while everything that accepts
    /// (or has no preference and gets offered) server-side still gets our
    /// titlebar as before.
    fn request_mode(&mut self, toplevel: ToplevelSurface, mode: DecorationMode) {
        toplevel.with_pending_state(|state| {
            state.decoration_mode = Some(mode);
        });
        toplevel.send_configure();
        self.set_decorated_from_mode(toplevel.wl_surface(), mode == DecorationMode::ServerSide);
    }

    /// The client dropped its decoration-mode preference. `new_decoration`
    /// already offers the configured default as the mode the next
    /// configure will carry, so mirror that same default here rather than
    /// leaving whatever mode was negotiated before this - otherwise a
    /// client that requests one mode, then later unsets it expecting the
    /// default back, would stay stuck in that mode forever.
    fn unset_mode(&mut self, toplevel: ToplevelSurface) {
        let default_decorated = self.wm.borrow().theme.default_decorated;
        let mode = if default_decorated { DecorationMode::ServerSide } else { DecorationMode::ClientSide };
        toplevel.with_pending_state(|state| {
            state.decoration_mode = Some(mode);
        });
        toplevel.send_configure();
        self.set_decorated_from_mode(toplevel.wl_surface(), default_decorated);
    }
}

impl ShmHandler for CompState {
    fn shm_state(&self) -> &ShmState {
        &self.shm_state
    }
}

impl BufferHandler for CompState {
    fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {}
}

impl DmabufHandler for CompState {
    fn dmabuf_state(&mut self) -> &mut DmabufState {
        &mut self.dmabuf_state
    }

    /// Validates a client's dmabuf by actually importing it wherever a
    /// renderer is reachable from here, so a genuinely bad buffer (wrong
    /// modifier, format the renderer doesn't support) gets the protocol
    /// error instead of silently rendering garbage later.
    ///
    /// That's only the udev backend: its `PixmanRenderer` lives inside
    /// `self.udev` (`UdevState`), a field of this same struct.
    /// `PixmanRenderer` supports dmabuf import despite being a pure
    /// software renderer - `dmabuf_formats()` only advertises the Linear
    /// modifier, which it imports by mmap'ing the buffer and reading it
    /// directly as pixels, no GPU involved. This is what actually answers
    /// `docs/PANEL_SUPPORT_TODO.md`'s P0.3: GTK4 allocates via its own
    /// EGL/gbm path against the real DRM render node (untouched by this
    /// compositor either way) and hands the result here as a Linear-
    /// modifier dmabuf, which pixman can read straight off.
    ///
    /// The winit (nested/dev) backend's `GlesRenderer` lives on
    /// `WaylandPlatform`, a sibling of `CompState`, not reachable from a
    /// method on `CompState` itself. Accepted there without eager
    /// validation - the buffer still gets imported the same way every
    /// other buffer type already is, lazily, the first time it is actually
    /// rendered via `render_elements_from_surface_tree`. Real hardware,
    /// where P0.3 actually bites, always goes through the udev path.
    fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {
        match self.udev.as_mut() {
            Some(udev) => match udev.renderer.import_dmabuf(&dmabuf, None) {
                Ok(_) => {
                    let _ = notifier.successful::<CompState>();
                }
                Err(e) => {
                    log::warn!("udev: rejecting dmabuf import: {e}");
                    notifier.failed();
                }
            },
            None => {
                let _ = notifier.successful::<CompState>();
            }
        }
    }
}

impl XdgActivationHandler for CompState {
    fn activation_state(&mut self) -> &mut XdgActivationState {
        &mut self.xdg_activation_state
    }

    /// A launcher spawns an app after first getting a token
    /// (`get_activation_token`) and handing it to the new process (usually
    /// via `XDG_ACTIVATION_TOKEN`); the app's own first window then
    /// presents that same token back here via `activate`, asking to be
    /// raised. Without this, that request was silently ignored - the new
    /// window opened and just sat there unfocused behind everything,
    /// exactly the gap `docs/PANEL_SUPPORT_TODO.md`'s P1 flagged.
    ///
    /// No token bookkeeping of our own: `token_created`'s default already
    /// accepts every token (fine for a single-user session with no
    /// cross-client trust boundary to enforce), so all that's left is
    /// mapping the activating `surface` to a `WindowId` and reusing the
    /// exact same `focus_window` path a dock's "activate" request already
    /// goes through (`foreign_toplevel.rs`). If the surface isn't tracked
    /// yet - the activation raced ahead of this window's own mapping --
    /// there is nothing to focus yet, so this is a no-op rather than an
    /// error; the protocol doesn't require honoring every activation.
    fn request_activation(&mut self, _token: XdgActivationToken, _token_data: XdgActivationTokenData, surface: WlSurface) {
        if let Some(&id) = self.surface_to_id.get(&surface) {
            crate::input::focus_window(self, id);
        }
    }
}

/// `zwp_text_input_manager_v3` + `zwp_input_method_manager_v2`: lets a real
/// input method (fcitx5, ibus, any CJK/dead-key/emoji-picker IME) attach to
/// whichever surface has keyboard focus and draw its own candidate/
/// composition popup. Without these two globals a client that only speaks
/// text-input (most modern toolkits do, GTK4/Qt6 included) has no way to
/// tell the compositor "I have an editable text field, here is its cursor
/// rectangle" - every desktop app's search box, address bar, and chat
/// input silently loses IME support, not just an edge case.
///
/// Focus tracking needs *no* wiring here at all: `CompState::KeyboardFocus`
/// is a plain `WlSurface`, and smithay's own blanket `impl KeyboardTarget
/// for WlSurface` already calls `seat.text_input().set_focus/.enter()/
/// .leave()` and `seat.input_method().activate_input_method()/
/// deactivate_input_method()` from inside `enter`/`leave` - which
/// `set_keyboard_focus`'s existing `keyboard.set_focus(...)` call already
/// triggers on every real focus change. The only things actually missing
/// were the two manager globals and this handler for the popup surface
/// lifecycle.
impl smithay::wayland::input_method::InputMethodHandler for CompState {
    /// A candidate/composition window (an emoji picker, a CJK candidate
    /// list) just opened. Tracked as a regular [`PopupKind::InputMethod`]
    /// in the same [`PopupManager`](smithay::desktop::PopupManager) that
    /// already owns every `xdg_popup` - `elements::popup_render_elements`
    /// renders both kinds identically, so no separate render path is
    /// needed for this to actually become visible.
    fn new_popup(&mut self, surface: ImePopupSurface) {
        if let Err(e) = self.popups.track_popup(PopupKind::from(surface)) {
            log::warn!("input-method: failed to track popup: {e}");
        }
    }

    fn dismiss_popup(&mut self, surface: ImePopupSurface) {
        if let Some(parent) = surface.get_parent().map(|p| p.surface.clone()) {
            let _ = smithay::desktop::PopupManager::dismiss_popup(&parent, &PopupKind::from(surface));
        }
    }

    /// The IME moved its own popup (e.g. following the text cursor as the
    /// user types) - `PopupSurface::location()` already reflects the new
    /// position; nothing else needs updating on this side, matching every
    /// other smithay-based compositor's own no-op here.
    fn popup_repositioned(&mut self, _surface: ImePopupSurface) {}

    /// Where the IME should anchor its popup, in the parent surface's own
    /// output-independent (logical, window-relative-origin) space - same
    /// geometry `elements::popup_targets` already computes for xdg popups,
    /// reused here rather than duplicated. A window not yet tracked (the
    /// activation raced ahead of its own mapping) gets a default/zero rect,
    /// same "no-op rather than an error" stance as `request_activation`
    /// above.
    fn parent_geometry(&self, parent: &WlSurface) -> smithay::utils::Rectangle<i32, smithay::utils::Logical> {
        let Some(&id) = self.surface_to_id.get(parent) else {
            return smithay::utils::Rectangle::default();
        };
        let wm = self.wm.borrow();
        let Some(w) = wm.window(id) else {
            return smithay::utils::Rectangle::default();
        };
        let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
        smithay::utils::Rectangle::new((w.geometry.x, w.geometry.y + band).into(), (w.geometry.width as i32, w.geometry.height as i32).into())
    }
}

impl smithay::wayland::output::OutputHandler for CompState {}

/// `wp_cursor_shape_v1`: lets a client ask for a *named* cursor (text,
/// grab, resize edges, ...) instead of rendering and attaching its own
/// surface. Its requests route straight into `SeatHandler::cursor_image`
/// below, same as a client-drawn cursor surface does - no extra state on
/// our side. Without this global at all, a client that only speaks this
/// (increasingly the norm - recent GTK4/Firefox use it for most cursor
/// changes) has no way to tell us the pointer should look like anything
/// but whatever it last was, which reads as the cursor going stale, wrong,
/// or simply disappearing depending on what was showing when the client
/// gave up trying.
///
/// `TabletSeatHandler` is a supertrait bound of this protocol's `Dispatch`
/// impl (cursor-shape covers tablet tools too); srdwm has no tablet
/// support to speak of, so every method is left at its no-op default.
impl TabletSeatHandler for CompState {}

/// Fractional scaling. srdwm runs every output at scale 1, so there is
/// nothing to compute - but the global has to exist, because clients that
/// use it (notably wallpaper daemons) treat it as mandatory.
impl smithay::wayland::fractional_scale::FractionalScaleHandler for CompState {}

impl SeatHandler for CompState {
    type KeyboardFocus = WlSurface;
    type PointerFocus = WlSurface;
    type TouchFocus = WlSurface;

    fn seat_state(&mut self) -> &mut SeatState<Self> {
        &mut self.seat_state
    }

    fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
    /// Clients set their own cursor (an I-beam over text, a hand over a
    /// link). Recorded here and drawn by the render paths - on a bare TTY
    /// nothing else would draw it. See `cursor.rs`.
    fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
        self.cursor_status = image;
    }
}

impl WlrLayerShellHandler for CompState {
    fn shell_state(&mut self) -> &mut WlrLayerShellState {
        &mut self.layer_shell_state
    }

    fn new_layer_surface(&mut self, surface: WlrLayerSurface, wl_output: Option<WlOutput>, _layer: Layer, namespace: String) {
        // Logged before anything else can early-return or panic: the
        // question this answers (see docs/PANEL_SUPPORT_TODO.md) is
        // whether this handler is reached AT ALL for a later
        // `get_layer_surface` request in a create -> commit -> destroy ->
        // commit-again -> create sequence, or whether the client's
        // dispatch is already dead by then and this never runs.
        log::debug!("layer-shell: new_layer_surface entered, surface={:?} namespace={namespace:?} output_named={}", surface.wl_surface().id(), wl_output.is_some());
        // A client may name the output it wants (a bar on a specific
        // monitor); if it doesn't, or names one we don't drive, it lands on
        // the primary output.
        let output = wl_output
            .as_ref()
            .and_then(|wl| self.output_for_wl(wl))
            .map(|e| e.output.clone())
            .or_else(|| self.primary_output().cloned());
        let Some(output) = output else {
            log::warn!("wayland: layer surface requested but no output exists yet");
            return;
        };
        // Paired with the debug log in `ensure_layer_initial_configure`'s
        // early return - see docs/PANEL_SUPPORT_TODO.md's P0. This is the
        // other half of "did map_layer actually succeed, and on which
        // output": logged unconditionally (not just on the error paths
        // that already existed) so a real reproduction shows both sides of
        // the handoff instead of just the failure.
        let surface_id = surface.wl_surface().id();
        let layer_surface = DesktopLayerSurface::new(surface, namespace);
        let result = layer_map_for_output(&output).map_layer(&layer_surface);
        match &result {
            Ok(()) => log::debug!("layer-shell: mapped surface {surface_id:?} onto output {}", output.name()),
            Err(e) => log::warn!("wayland: failed to map layer surface {surface_id:?}: {e}"),
        }
    }

    fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
        // See the matching top-of-function log in `new_layer_surface`.
        log::debug!("layer-shell: layer_destroyed entered, surface={:?}", surface.wl_surface().id());
        // Marks this surface for the pre-commit-hook workaround in
        // `new_surface` - see that function's doc comment for the bug
        // this exists to route around.
        self.dead_layer_surfaces.insert(surface.wl_surface().clone());
        // GTK (confirmed live via an AGS peer session's WAYLAND_DEBUG trace)
        // reuses the same `wl_surface` for the next `get_layer_surface` role
        // rather than creating a fresh one - so without this, a "shown at
        // least once" flag from *this* role would leak onto the next one
        // and make `sync_layer_visibility` treat that new role's own
        // ack-configure commit as eligible to hide again, the same bug
        // `layer_surfaces_shown_once` exists to prevent, just reintroduced
        // for exactly the reused-surface case that matters here.
        self.layer_surfaces_shown_once.remove(surface.wl_surface());
        // The surface belongs to exactly one output's map, but which one is
        // the client's choice, so unmap from whichever holds it.
        for output in self.outputs().cloned().collect::<Vec<_>>() {
            let mut map = layer_map_for_output(&output);
            let found = map.layers().find(|l| l.layer_surface() == &surface).cloned();
            if let Some(layer) = found {
                // Same zone-change recompute `ensure_layer_initial_configure`
                // already does on every commit that changes a layer's
                // exclusive zone (state/layers.rs) - but this is the *only* place
                // that ever runs for a surface that goes away without one
                // last commit. `unmap_layer` alone doesn't trigger it:
                // reported live (by the AGS peer session) as a bar unmapping
                // for fullscreen yet `srd monitors` still reporting the
                // bar's old reserved_top for as long as fullscreen lasted --
                // harmless there only because fullscreen targets
                // `full_geometry`, which ignores the reservation anyway, but
                // wrong for anything that reads `usable`/`geometry` while a
                // bar is unmapped without exiting cleanly (a crash, not just
                // AGS's cooperative fullscreen hide).
                let zone_before = map.non_exclusive_zone();
                map.unmap_layer(&layer);
                let zone_after = map.non_exclusive_zone();
                if zone_after != zone_before {
                    self.pending.borrow_mut().push(srdwm_core::Event::MonitorAdded(srdwm_core::Monitor::new(0, "", srdwm_core::Rect::new(0, 0, 0, 0))));
                }
                break;
            }
        }
        // A lock/launcher surface holding exclusive keyboard focus just
        // vanished (crash, or a normal close) - don't leave focus dangling
        // on a dead surface.
        //
        // `sync_keyboard_focus`, not a bare `set_keyboard_focus(None)`: an
        // `OnDemand` layer surface (a launcher/quicksettings/datemenu
        // popup, per `wlr-layer-shell`) claiming focus on click
        // (`input.rs`'s `on_demand` branch) goes straight through
        // `set_keyboard_focus` without ever touching `WindowManager::
        // focused` - core has no concept of a layer surface to focus, so
        // it still correctly points at whatever real toplevel was focused
        // before the popup opened. Hardcoding `None` here threw that away
        // regardless, leaving nothing focused until the user happened to
        // click a window again - reported live (an AGS peer session's
        // user) as "focus never returns after using the bar". `sync_
        // keyboard_focus` reads that still-correct core state and restores
        // real Wayland focus to it, falling through to `None` only if core
        // genuinely has nothing focused either.
        if self.seat.get_keyboard().and_then(|k| k.current_focus()).as_ref() == Some(surface.wl_surface()) {
            crate::input::sync_keyboard_focus(self);
        }
    }
}

/// Clipboard/primary-selection/drag-and-drop.
///
/// All three selection protocols below (`wl_data_device_manager`,
/// `zwp_primary_selection_v1`, `zwlr_data_control_manager_v1`) share
/// smithay's single `SelectionHandler`. Every transfer here is
/// *client-to-client*: one client owns the selection and writes the bytes
/// itself, and smithay wires the two ends together without the data passing
/// through us. `send_selection` is only ever called for a
/// **compositor-provided** selection (one this WM set itself via
/// `set_data_device_selection`), which srdwm never does - so it is
/// deliberately left unimplemented rather than faked.
impl SelectionHandler for CompState {
    type SelectionUserData = ();
}

impl DataDeviceHandler for CompState {
    fn data_device_state(&self) -> &DataDeviceState {
        &self.data_device_state
    }
}

// Drag-and-drop: the default trait methods already do the right thing for a
// compositor that doesn't draw its own drag icon or offer server-side drag
// sources - smithay runs the pointer grab and the offer/accept negotiation
// internally. Both are implemented empty (rather than skipped) because
// `DataDeviceHandler` requires them as supertraits.
impl ClientDndGrabHandler for CompState {}
impl ServerDndGrabHandler for CompState {}

impl PrimarySelectionHandler for CompState {
    fn primary_selection_state(&self) -> &PrimarySelectionState {
        &self.primary_selection_state
    }
}

/// `zwlr_data_control_manager_v1`: lets a client read/watch the selection
/// without ever holding keyboard focus. This is what `wl-paste --watch`
/// (and thus `cliphist store`, which the user's session autostarts) needs
/// - a focus-following clipboard manager is impossible without it.
impl DataControlHandler for CompState {
    fn data_control_state(&self) -> &DataControlState {
        &self.data_control_state
    }
}

/// `ext_idle_notify_v1`. All the real logic (per-notification timers,
/// resetting them on activity, honouring inhibition) already lives in
/// smithay's own `IdleNotifierState` - this is just the getter it needs.
/// See `input.rs`'s `notify_idle_activity` for the other half: nothing
/// calls `notify_activity` on its own, that has to happen from every real
/// input path.
impl smithay::wayland::idle_notify::IdleNotifierHandler for CompState {
    fn idle_notifier_state(&mut self) -> &mut smithay::wayland::idle_notify::IdleNotifierState<Self> {
        &mut self.idle_notifier_state
    }
}

/// `zwp_idle_inhibit_manager_v1`. A video player (or anything else that
/// wants the screen to stay on/unlocked while it runs) creates one of
/// these tied to its own surface; as long as at least one is alive,
/// `IdleNotifierState::set_is_inhibited` stops idle timers from firing at
/// all - see `idle_inhibiting_surfaces`'s doc comment on `CompState` for
/// the one simplification (not workspace-visibility-aware) this takes.
impl smithay::wayland::idle_inhibit::IdleInhibitHandler for CompState {
    fn inhibit(&mut self, surface: WlSurface) {
        self.idle_inhibiting_surfaces.push(surface);
        self.idle_notifier_state.set_is_inhibited(true);
    }

    fn uninhibit(&mut self, surface: WlSurface) {
        self.idle_inhibiting_surfaces.retain(|s| s != &surface);
        self.idle_notifier_state.set_is_inhibited(!self.idle_inhibiting_surfaces.is_empty());
    }
}

delegate_compositor!(CompState);
delegate_xdg_shell!(CompState);
delegate_xdg_decoration!(CompState);
delegate_shm!(CompState);
delegate_dmabuf!(CompState);
delegate_xdg_activation!(CompState);
delegate_text_input_manager!(CompState);
delegate_input_method_manager!(CompState);
delegate_seat!(CompState);
delegate_output!(CompState);
delegate_layer_shell!(CompState);
delegate_data_device!(CompState);
delegate_primary_selection!(CompState);
delegate_data_control!(CompState);
delegate_session_lock!(CompState);
delegate_cursor_shape!(CompState);
smithay::delegate_viewporter!(CompState);
smithay::delegate_fractional_scale!(CompState);
smithay::delegate_idle_notify!(CompState);
smithay::delegate_idle_inhibit!(CompState);