diff options
| author | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
| commit | 0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch) | |
| tree | 7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/protocols | |
| parent | 413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff) | |
| download | srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.tar.gz srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.zip | |
Checkpoint: preserve all uncommitted rust-rewrite worktree work
Safety commit before reconciling this worktree with main, which has
diverged with its own separate fixes today. Nothing here is reviewed
or curated yet - this exists purely so none of this work can be lost
to a git operation, disk issue, or worktree cleanup while that
reconciliation happens.
Diffstat (limited to 'crates/wayland/src/protocols')
| -rw-r--r-- | crates/wayland/src/protocols/buffer.rs | 68 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/compositor.rs | 182 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/idle.rs | 35 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/input_method.rs | 86 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/layer_shell.rs | 117 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/misc.rs | 30 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/seat.rs | 31 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/selection.rs | 52 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/xdg_activation.rs | 36 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/xdg_decoration.rs | 63 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/xdg_shell.rs | 258 |
11 files changed, 958 insertions, 0 deletions
diff --git a/crates/wayland/src/protocols/buffer.rs b/crates/wayland/src/protocols/buffer.rs new file mode 100644 index 0000000..7730de6 --- /dev/null +++ b/crates/wayland/src/protocols/buffer.rs @@ -0,0 +1,68 @@ +//! `wl_shm`/`wl_buffer`/`zwp_linux_dmabuf_v1`: the three buffer-transport +//! protocols, grouped together since none has more than a handful of lines +//! on its own. + +use smithay::backend::allocator::dmabuf::Dmabuf; +use smithay::backend::renderer::ImportDma; +use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer; +use smithay::wayland::buffer::BufferHandler; +use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}; +use smithay::wayland::shm::{ShmHandler, ShmState}; + +use crate::state::CompState; + +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>(); + } + } + } +} diff --git a/crates/wayland/src/protocols/compositor.rs b/crates/wayland/src/protocols/compositor.rs new file mode 100644 index 0000000..f7e7b74 --- /dev/null +++ b/crates/wayland/src/protocols/compositor.rs @@ -0,0 +1,182 @@ +//! `wl_compositor`/`wl_surface`: surface creation and the per-commit +//! bookkeeping every other protocol handler in this module tree depends on +//! (window mapping, layer-surface visibility, popup lifecycle). + +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::reexports::wayland_server::Client; +use smithay::wayland::compositor::{add_pre_commit_hook, with_states, CompositorClientState, CompositorHandler, CompositorState}; +use smithay::wayland::shell::wlr_layer::LayerSurfaceCachedState; + +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); + // `redraw_decoration_buffer` reads `dwindow.geometry()` (via + // `effective_frame`) to size the border/titlebar/shadow against + // what the client's surface *really* committed - but nothing + // updates that value except this very commit + // (`on_commit()` above). Without a call here, a client whose + // first real commit settles at a different size than what was + // requested (a terminal snapping to a whole character-cell + // grid) wouldn't get corrected decoration until some unrelated + // trigger (a resize, a focus change) happened to call this + // again - cheap regardless, since the signature check inside + // makes every commit that didn't actually change the *visible* + // size an early return, not a real rebuild. + self.redraw_decoration_buffer(id); + // `sync_geometry` is what actually maps this window into + // `self.space` at `geom.x - content_offset.x, ...` - the same + // `content_offset` (`dwindow.geometry().loc`) the render loop + // (`udev/render.rs`'s per-frame `pos` computation) reads fresh + // on every single frame, straight off the live surface, not + // from any cache. Before this call existed here, `self.space` + // only got a fresh position from whichever *other* trigger last + // called `sync_geometry` (a resize, `maximize_request`, a + // decoration-mode change) - so a client that recommits a + // *different* `xdg_surface::set_window_geometry` on its own, + // with no accompanying resize (a GTK4/Firefox CSD window + // shrinking its declared shadow margin once real content + // replaces its first, provisional paint, concretely), left + // `self.space`'s cached position silently stale while the + // render loop kept self-correcting every frame - confirmed + // live via temporary diagnostic logging: a window's real render + // position and `self.space`'s own `element_under`-reported + // position for it disagreed by exactly one `content_offset`, + // 10 physical pixels on both axes for the Firefox window that + // exposed it. `refresh_pointer_focus`'s content-click path + // (`input.rs`) computes `win_relative` from *that* stale + // position, not the render loop's fresh one - every click on + // such a window was silently off by the same 10px the whole + // time it stayed unmapped-and-remapped-by-nothing-else, which + // reads as "clicks land near, but not on, whatever's visibly + // there" - worst for a window's own small CSD buttons, + // exactly what was reported live. Same idempotent-when-nothing- + // moved shape as `redraw_decoration_buffer` above: `map_element` + // itself is unconditional and cheap (a hashmap insert), and the + // one potentially-expensive part - sending a fresh + // `xdg_toplevel::configure` - stays gated on `size_changed` + // and the existing throttle, both untouched, so a commit that + // didn't change size never sends one just because this call is + // now here too. + self.sync_geometry(id); + } else { + // `surface` itself isn't a tracked window's root, but may be a + // descendant (subsurface) of one - a real commit still + // happened, just not on the surface `surface_to_id` keys off. + // `masked_content_buffer`'s own resolver + // (`rounded_corners_pixman::resolve_content_surface`) reads a + // *child* subsurface's buffer directly for the common GTK4/ + // WebRender pattern (confirmed live: Firefox), so a repaint + // that only ever commits that child - which is the normal + // case, that's where the real content lives - must still + // bump this window's own `content_epoch`, or the masked- + // corner cache never sees a reason to invalidate and freezes + // on whatever the first frame happened to show. Bounded to a + // handful of hops purely as a safety net against a malformed + // subsurface tree looping back on itself - a real one is + // never more than one or two levels deep. + let mut ancestor = smithay::wayland::compositor::get_parent(surface); + for _ in 0..8 { + let Some(parent) = ancestor else { break }; + if let Some(&id) = self.surface_to_id.get(&parent) { + *self.content_epoch.entry(id).or_insert(0) += 1; + break; + } + ancestor = smithay::wayland::compositor::get_parent(&parent); + } + } + // 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(); + } +} diff --git a/crates/wayland/src/protocols/idle.rs b/crates/wayland/src/protocols/idle.rs new file mode 100644 index 0000000..a554453 --- /dev/null +++ b/crates/wayland/src/protocols/idle.rs @@ -0,0 +1,35 @@ +//! `ext_idle_notify_v1` + `zwp_idle_inhibit_manager_v1`. + +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; + +use crate::state::CompState; + +/// `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()); + } +} diff --git a/crates/wayland/src/protocols/input_method.rs b/crates/wayland/src/protocols/input_method.rs new file mode 100644 index 0000000..525383f --- /dev/null +++ b/crates/wayland/src/protocols/input_method.rs @@ -0,0 +1,86 @@ +//! `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. + +use smithay::desktop::PopupKind; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::wayland::input_method::PopupSurface as ImePopupSurface; + +use crate::state::CompState; + +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 (geometry, decorated) = { + let wm = self.wm.borrow(); + let Some(w) = wm.window(id) else { + return smithay::utils::Rectangle::default(); + }; + (w.geometry, w.decorated) + }; + let band = if decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 }; + // `content_offset`/`effective_frame`: same corrections every other + // real position/size computation in this codebase applies (see + // `state/geometry.rs::effective_frame`'s doc comment) - missed + // here originally, so an IME popup anchored against a CSD window's + // raw, unshifted geometry instead of its real visible content, + // same class of drift as the border/screenshot gaps fixed + // elsewhere. + let content_offset = self.id_to_window.get(&id).map(|w| w.geometry().loc).unwrap_or_default(); + // `frame.height` includes the titlebar band (see `effective_frame`'s + // own doc comment) - subtracted back out here since this rect is + // meant to cover the content area only, matching the original + // (pre-fix) code's own intent for `w.geometry.height`. + let frame = self.effective_frame(id, geometry); + let content_height = (frame.height as i32 - band).max(0); + smithay::utils::Rectangle::new((frame.x - content_offset.x, frame.y + band - content_offset.y).into(), (frame.width as i32, content_height).into()) + } +} diff --git a/crates/wayland/src/protocols/layer_shell.rs b/crates/wayland/src/protocols/layer_shell.rs new file mode 100644 index 0000000..931ddb4 --- /dev/null +++ b/crates/wayland/src/protocols/layer_shell.rs @@ -0,0 +1,117 @@ +//! `zwlr_layer_shell_v1`: panels, bars, launchers, and other output-anchored +//! shell surfaces (AGS's own bar and popups, notably). + +use smithay::desktop::{layer_map_for_output, LayerSurface as DesktopLayerSurface}; +use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; +use smithay::reexports::wayland_server::Resource; +use smithay::wayland::shell::wlr_layer::{Layer, LayerSurface as WlrLayerSurface, WlrLayerShellHandler, WlrLayerShellState}; + +use crate::state::CompState; + +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); + } + } +} diff --git a/crates/wayland/src/protocols/misc.rs b/crates/wayland/src/protocols/misc.rs new file mode 100644 index 0000000..7999ec3 --- /dev/null +++ b/crates/wayland/src/protocols/misc.rs @@ -0,0 +1,30 @@ +//! Small protocol handlers whose entire implementation is smithay's own +//! no-op default - the global still has to exist for clients that treat it +//! as mandatory, but there's nothing for this compositor to do in response. + +use smithay::wayland::tablet_manager::TabletSeatHandler; + +use crate::state::CompState; + +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` +/// (see `seat.rs`), 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 {} diff --git a/crates/wayland/src/protocols/seat.rs b/crates/wayland/src/protocols/seat.rs new file mode 100644 index 0000000..bd7ae4c --- /dev/null +++ b/crates/wayland/src/protocols/seat.rs @@ -0,0 +1,31 @@ +//! `wl_seat`: keyboard/pointer/touch focus types and the client-set cursor +//! image. + +use smithay::input::pointer::CursorImageStatus; +use smithay::input::{Seat, SeatHandler, SeatState}; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; + +use crate::state::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; + // The client has now explicitly claimed the cursor - see + // `decoration_cursor_active`'s own doc comment and `input.rs:: + // update_cursor_shape` for why this has to be tracked separately + // from just overwriting `cursor_status`. + self.decoration_cursor_active = false; + } +} diff --git a/crates/wayland/src/protocols/selection.rs b/crates/wayland/src/protocols/selection.rs new file mode 100644 index 0000000..fadb0ca --- /dev/null +++ b/crates/wayland/src/protocols/selection.rs @@ -0,0 +1,52 @@ +//! 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. + +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::selection::SelectionHandler; + +use crate::state::CompState; + +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 + } +} diff --git a/crates/wayland/src/protocols/xdg_activation.rs b/crates/wayland/src/protocols/xdg_activation.rs new file mode 100644 index 0000000..846a213 --- /dev/null +++ b/crates/wayland/src/protocols/xdg_activation.rs @@ -0,0 +1,36 @@ +//! `xdg_activation_v1`: a launcher hands a spawned app a token, and the +//! app's own first window presents it back to ask to be raised and focused. + +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::wayland::xdg_activation::{XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData}; + +use crate::state::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); + } + } +} diff --git a/crates/wayland/src/protocols/xdg_decoration.rs b/crates/wayland/src/protocols/xdg_decoration.rs new file mode 100644 index 0000000..25973c0 --- /dev/null +++ b/crates/wayland/src/protocols/xdg_decoration.rs @@ -0,0 +1,63 @@ +//! `zxdg_decoration_manager_v1`: negotiates whether a toplevel draws its own +//! (client-side) chrome or lets us draw it (server-side). + +use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode; +use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler; +use smithay::wayland::shell::xdg::ToplevelSurface; + +use crate::state::CompState; + +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); + } +} diff --git a/crates/wayland/src/protocols/xdg_shell.rs b/crates/wayland/src/protocols/xdg_shell.rs new file mode 100644 index 0000000..4fe0f6f --- /dev/null +++ b/crates/wayland/src/protocols/xdg_shell.rs @@ -0,0 +1,258 @@ +//! `xdg_shell`: toplevel and popup lifecycle, and the client-initiated +//! move/resize/maximize/fullscreen/minimize requests a CSD client sends +//! instead of (or alongside) the pointer-driven titlebar handlers in +//! `input.rs`. + +use smithay::desktop::{find_popup_root_surface, PopupKeyboardGrab, PopupKind, PopupPointerGrab}; +use smithay::input::pointer::Focus; +use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; +use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; +use smithay::reexports::wayland_server::protocol::wl_seat; +use smithay::reexports::wayland_server::Resource; +use smithay::utils::Serial; +use smithay::wayland::shell::xdg::{PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState}; + +use crate::state::CompState; + +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) { + // Temporary: live report is that Nemo's right-click context menu + // never appears at all (not mispositioned - entirely invisible), + // while the exact same xdg_popup mechanism works for Firefox. Logs + // the unconstrained geometry this popup gets so a live repro tells + // us whether it's landing off-screen/degenerate (the known gap this + // function's own doc comment already flags) or something else + // entirely. Remove once resolved. + let geom = positioner.get_geometry(); + let parent = surface.get_parent_surface(); + log::warn!("POPUP-GEOM-DIAG geometry={geom:?} parent={:?}", parent.as_ref().map(|s| s.id())); + surface.with_pending_state(|state| { + state.geometry = geom; + state.positioner = positioner; + }); + if surface.send_configure().is_err() { + log::warn!("POPUP-GEOM-DIAG send_configure failed"); + 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()); + } +} |