srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/state')
-rw-r--r--crates/wayland/src/state/geometry.rs35
-rw-r--r--crates/wayland/src/state/layers.rs107
-rw-r--r--crates/wayland/src/state/mod.rs114
3 files changed, 250 insertions, 6 deletions
diff --git a/crates/wayland/src/state/geometry.rs b/crates/wayland/src/state/geometry.rs
index c3de7fa..a0d10fa 100644
--- a/crates/wayland/src/state/geometry.rs
+++ b/crates/wayland/src/state/geometry.rs
@@ -28,7 +28,7 @@ impl CompState {
// call for the same window (an ordinary drag/resize frame) goes
// straight back to applying `geometry` immediately, as before.
let anim_from = self.wm.borrow_mut().window_mut(id).and_then(|w| w.anim_from.take());
- let Some((target, decorated)) = self.wm.borrow().window(id).map(|w| (w.geometry, w.decorated)) else { return };
+ let Some((target, decorated, maximized, fullscreen)) = self.wm.borrow().window(id).map(|w| (w.geometry, w.decorated, w.maximized, w.fullscreen)) else { return };
if let Some(from) = anim_from {
let duration_ms = self.wm.borrow().animation_duration_ms;
if from != target && duration_ms > 0 {
@@ -93,6 +93,39 @@ impl CompState {
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
+ // Same "no configure from this compositor ever set
+ // this" gap as the tiled bits above, confirmed the
+ // same way (grepped the whole crate for `State::
+ // Maximized`/`State::Fullscreen` outside foreign-
+ // toplevel-management, which is a *different*
+ // protocol read by external tools like a taskbar,
+ // not the client's own `xdg_toplevel` configure --
+ // zero hits there before this). The window was
+ // resized to the full monitor rect and told it was
+ // tiled on every side, but never actually told via
+ // the real protocol mechanism for it that it was
+ // maximized or fullscreen at all - indistinguishable
+ // from an ordinary tiled-to-the-edges floating
+ // window as far as the client could tell. Reported
+ // live as fullscreen leaving a persistent gap along
+ // one edge (Firefox keeping some of its own chrome
+ // logic that specifically keys off genuinely
+ // *knowing* it's fullscreen, not just being resized
+ // to fullscreen-sized). `unset` the other explicitly
+ // when only one applies - `WindowManager::
+ // toggle_fullscreen`/`toggle_maximize` are mutually
+ // exclusive, but nothing here should assume that
+ // holds forever just because it does today.
+ if maximized {
+ state.states.set(xdg_toplevel::State::Maximized);
+ } else {
+ state.states.unset(xdg_toplevel::State::Maximized);
+ }
+ if fullscreen {
+ state.states.set(xdg_toplevel::State::Fullscreen);
+ } else {
+ state.states.unset(xdg_toplevel::State::Fullscreen);
+ }
});
top.send_configure();
}
diff --git a/crates/wayland/src/state/layers.rs b/crates/wayland/src/state/layers.rs
index 1a02858..34db3f1 100644
--- a/crates/wayland/src/state/layers.rs
+++ b/crates/wayland/src/state/layers.rs
@@ -1,6 +1,113 @@
+use smithay::backend::renderer::utils::{with_renderer_surface_state, RendererSurfaceState};
+
use super::*;
impl CompState {
+ /// Keeps a layer surface's exclusive-zone reservation honest against
+ /// whether it currently has anything to show - called from `commit()`
+ /// before `ensure_layer_initial_configure`, for every surface in the
+ /// desktop (same cheap non-layer-surface early-out reasoning as that
+ /// function's own doc comment).
+ ///
+ /// The protocol's own text (`wlr-layer-shell-unstable-v1.xml`):
+ /// "Attaching a null buffer to a layer surface unmaps it." Nothing in
+ /// smithay's `LayerMap` acts on that by itself - `arrange()` walks
+ /// every layer in `self.layers` unconditionally, using each one's
+ /// last-requested `exclusive_zone` regardless of whether it currently
+ /// has a buffer. `layer_destroyed` (protocols.rs) already handles the
+ /// *destroyed* case with its own zone_before/`unmap_layer`/zone_after
+ /// diff; this is the same fix for a client that hides by committing a
+ /// null buffer while keeping the `zwlr_layer_surface_v1` object alive
+ /// - cheaper than destroying and recreating it, and exactly what
+ /// AGS's dock does to hide itself for a fullscreen window. Without
+ /// this, the dock's last-requested exclusive zone stayed reserved the
+ /// entire time it was hidden - a real, empty, unexplained band at the
+ /// screen edge, reported live by an AGS peer session's own measurement
+ /// across a genuine fullscreen toggle (the dock's zone correctly
+ /// dropped to 0 on a real *maximize*, ruling that path out).
+ ///
+ /// `hidden_layer_surfaces` (see its own doc comment) is what makes the
+ /// reverse direction work: `unmap_layer` removes the surface from
+ /// `LayerMap`'s own list, so there is no way to find it again via
+ /// `layer_for_surface` once that happens - this is the only record
+ /// of "this surface is mine to re-map" for when a real buffer comes
+ /// back.
+ pub(crate) fn sync_layer_visibility(&mut self, surface: &WlSurface) {
+ let Some(initial_configure_sent) = with_states(surface, |states| {
+ states.data_map.get::<LayerSurfaceData>().map(|d| d.lock().unwrap().initial_configure_sent)
+ }) else {
+ return;
+ };
+ // A surface's very first commit legitimately has no buffer yet --
+ // that's the protocol's own required handshake (commit once with
+ // nothing attached so the compositor can send the *first*
+ // `configure`, only after which the client is allowed to attach
+ // real content at all), not a client "hiding" anything. Treating it
+ // as a hide (the bug this early-return fixes) called `unmap_layer`
+ // before `ensure_layer_initial_configure` ever ran, which made that
+ // function's own `layer_for_surface` lookup find nothing and skip
+ // sending the configure entirely - every layer-shell client
+ // (a bar, a dock, a wallpaper daemon) left waiting forever for
+ // permission to draw it was never going to get, each eventually
+ // giving up and destroying/recreating its surface in a loop.
+ // Confirmed live: zero "sent initial configure" log lines across an
+ // entire session, and a repeating ~2-minute create/destroy cycle for
+ // every `gtk4-layer-shell` surface. Only a surface that has already
+ // completed its initial handshake can meaningfully "hide" by
+ // committing a null buffer later - that's the real case this
+ // function still needs to handle, below.
+ if !initial_configure_sent {
+ return;
+ }
+ let has_buffer = with_renderer_surface_state(surface, |state: &mut RendererSurfaceState| state.buffer().is_some()).unwrap_or(false);
+
+ // TEMPORARY diagnostic for the "AGS dock reserves its exclusive
+ // zone but never paints" investigation, relayed from the AGS peer
+ // session - this function's own `has_buffer`-based hide/show logic
+ // (see the doc comment above) is the leading suspect: if a client
+ // ever legitimately commits a null buffer for a reason other than
+ // an intentional hide (an internal `Gtk.Revealer` transition
+ // artifact, a resize-in-progress commit), this treats it as a hide,
+ // `unmap_layer`s it, and requires a *later* has-buffer commit to
+ // ever come back - which would look exactly like this symptom if
+ // the client's own state machine doesn't expect the compositor to
+ // have done that and never re-triggers one. Logs every call for
+ // every layer surface, not just suspected ones, since which
+ // surface is actually affected isn't confirmed yet. Remove once
+ // resolved.
+ log::warn!("LAYER-VIS-DIAG surface={:?} has_buffer={has_buffer} already_hidden={}", surface.id(), self.hidden_layer_surfaces.contains_key(surface));
+
+ if has_buffer {
+ let Some((output, layer)) = self.hidden_layer_surfaces.remove(surface) else { return };
+ let mut map = layer_map_for_output(&output);
+ let zone_before = map.non_exclusive_zone();
+ let _ = map.map_layer(&layer);
+ let zone_after = map.non_exclusive_zone();
+ log::warn!("LAYER-VIS-DIAG re-mapped namespace={:?} zone_before={zone_before:?} zone_after={zone_after:?}", layer.namespace());
+ if zone_after != zone_before {
+ self.pending.borrow_mut().push(CoreEvent::MonitorAdded(srdwm_core::Monitor::new(0, "", srdwm_core::Rect::new(0, 0, 0, 0))));
+ }
+ return;
+ }
+
+ if self.hidden_layer_surfaces.contains_key(surface) {
+ return;
+ }
+ for output in self.outputs().cloned().collect::<Vec<_>>() {
+ let mut map = layer_map_for_output(&output);
+ let Some(layer) = map.layers().find(|l| l.wl_surface() == surface).cloned() else { continue };
+ let zone_before = map.non_exclusive_zone();
+ map.unmap_layer(&layer);
+ let zone_after = map.non_exclusive_zone();
+ log::warn!("LAYER-VIS-DIAG unmapped namespace={:?} zone_before={zone_before:?} zone_after={zone_after:?}", layer.namespace());
+ if zone_after != zone_before {
+ self.pending.borrow_mut().push(CoreEvent::MonitorAdded(srdwm_core::Monitor::new(0, "", srdwm_core::Rect::new(0, 0, 0, 0))));
+ }
+ drop(map);
+ self.hidden_layer_surfaces.insert(surface.clone(), (output, layer));
+ break;
+ }
+ }
/// Layer surfaces need a configure sent in direct response to their
/// first commit (sending it any earlier violates the protocol - see
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index 834cf6a..e5b4d6a 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -40,10 +40,10 @@ use smithay::wayland::dmabuf::DmabufState;
use smithay::wayland::shm::ShmState;
use smithay::wayland::xdg_activation::XdgActivationState;
-use srdwm_core::{Event as CoreEvent, Window as CoreWindow, WindowId, WindowManager, TITLEBAR_HEIGHT};
+use srdwm_core::{Event as CoreEvent, SnapZoneKind, Window as CoreWindow, WindowId, WindowManager, TITLEBAR_HEIGHT};
use crate::lock::SessionLock;
-use crate::{decoration, foreign_toplevel, gamma_control, output_management, output_power, screencopy, udev, workspace, xwayland};
+use crate::{appmenu, decoration, foreign_toplevel, gamma_control, output_management, output_power, screencopy, udev, workspace, xwayland};
#[derive(Default)]
pub(crate) struct ClientState {
@@ -87,6 +87,34 @@ impl OutputEntry {
}
}
+/// Every input `redraw_decoration_buffer` reads to decide what a window's
+/// titlebar/border pixels look like - see `CompState::decoration_
+/// signatures`'s own doc comment for why this exists. `title` is the one
+/// field worth noting the cost of cloning: short in practice (a window
+/// title), and only compared/cloned once per call to this already-more-
+/// expensive-than-a-string-clone rasterization function, not once per
+/// frame.
+#[derive(Clone, PartialEq, Eq)]
+pub(crate) struct DecorationSignature {
+ pub(crate) width: u32,
+ /// The shadow bitmap's own inputs (`height`, `maximized`, `fullscreen`,
+ /// and the global `shadows_enabled` setting) belong here too, even
+ /// though nothing above the titlebar/border needs them - one
+ /// signature covering every input this function reads, not one that
+ /// only happens to match the titlebar/border's inputs and silently
+ /// skips a shadow update a real state change needed.
+ pub(crate) height: u32,
+ pub(crate) decorated: bool,
+ pub(crate) focused: bool,
+ pub(crate) title: String,
+ pub(crate) border_color: (u8, u8, u8),
+ pub(crate) border_width: u32,
+ pub(crate) corner_radius: u32,
+ pub(crate) maximized: bool,
+ pub(crate) fullscreen: bool,
+ pub(crate) shadows_enabled: bool,
+}
+
/// Everything smithay's protocol handlers need `&mut` access to. This is the
/// `D` type parameter of `Display<D>` - every `delegate_*!` macro below
/// requires the corresponding `*Handler` trait to be implemented on it.
@@ -154,6 +182,14 @@ pub(crate) struct CompState {
/// Captures requested via `wlr-screencopy` but not yet serviced; drained
/// inside the render pass (see `screencopy::service_pending`).
pub(crate) screencopy_pending: Vec<screencopy::PendingCapture>,
+ /// `org_kde_kwin_appmenu_manager` - not `Option`-gated, same reasoning
+ /// as `_output_management_state` below: exporting a menu D-Bus address
+ /// straight from a Wayland-native client has nothing GPU/DRM-specific
+ /// about it, so both backends advertise it. See `appmenu.rs`'s module
+ /// doc comment for why this exists alongside `xwayland.rs::read_global_
+ /// menu` rather than instead of it - they cover disjoint sets of
+ /// windows (XWayland-backed vs. Wayland-native), not the same one.
+ pub(crate) _appmenu_state: appmenu::AppmenuManagerState,
pub(crate) _foreign_toplevel_state: foreign_toplevel::ForeignToplevelState,
/// Every bound `zwlr_foreign_toplevel_manager_v1` (one per dock/switcher
/// client), so a newly-created window can be announced to all of them --
@@ -207,6 +243,19 @@ pub(crate) struct CompState {
pub(crate) cursor_buffers: crate::cursor::CursorBuffers,
/// Last titlebar press, for double-click detection.
pub(crate) last_titlebar_click: Option<(WindowId, u32)>,
+ /// Finger count and accumulated horizontal offset of an in-progress
+ /// touchpad swipe (`GestureSwipeBegin`..`GestureSwipeUpdate`*..
+ /// `GestureSwipeEnd`) - `None` between gestures. Finger count is only
+ /// ever reported on the `Begin` event, so it has to be carried forward
+ /// to be checked at `End`. `GestureSwipeUpdateEvent::delta_x` is a
+ /// per-update offset, not a running total (see the smithay struct's own
+ /// doc comment: "relative to the previous event"), so the offset half
+ /// has to sum across every update itself; only the total at `End`
+ /// decides whether the swipe crossed the switch-workspace threshold.
+ /// Never forwarded to a client - see `input::handle_gesture_swipe_end`'s
+ /// doc comment for why 3+-finger swipe is claimed entirely by the
+ /// compositor.
+ pub(crate) gesture_swipe: Option<(u32, f64)>,
/// The right-click titlebar window menu, if one is currently open --
/// see `context_menu.rs`. `None` almost always; a click anywhere while
/// `Some` resolves (selects a row) or dismisses it, never falls
@@ -217,6 +266,16 @@ pub(crate) struct CompState {
/// `decorations`/`border_top_decorations` already use, not rebuilt
/// per frame.
pub(crate) context_menu_buffer: Option<MemoryRenderBuffer>,
+ /// The Snap-Layouts flyout, if one is currently open - see
+ /// `snap_flyout.rs`. Same lifecycle as `context_menu` above (mutually
+ /// exclusive in practice, since both close on any click elsewhere), just
+ /// a separate field rather than an enum of the two: they render
+ /// differently, are triggered by different clicks, and nothing needs to
+ /// treat them uniformly.
+ pub(crate) snap_flyout: Option<crate::snap_flyout::SnapFlyout>,
+ /// Rasterised pixels for the currently-open `snap_flyout`, same
+ /// build-once-on-open pattern as `context_menu_buffer`.
+ pub(crate) snap_flyout_buffer: Option<MemoryRenderBuffer>,
pub(crate) wm: Rc<RefCell<WindowManager>>,
pub(crate) surface_to_id: HashMap<WlSurface, WindowId>,
pub(crate) id_to_window: HashMap<WindowId, DWindow>,
@@ -229,6 +288,21 @@ pub(crate) struct CompState {
/// after the client destroys it too and Rust never reuses the id while
/// any handle (including this one) still exists.
pub(crate) dead_layer_surfaces: HashSet<WlSurface>,
+ /// Layer surfaces this compositor has unmapped itself in response to a
+ /// null-buffer commit - `wlr-layer-shell-unstable-v1.xml`'s own text:
+ /// "Attaching a null buffer to a layer surface unmaps it", but nothing
+ /// in smithay's `LayerMap` does that automatically (`arrange()` walks
+ /// every layer in its list unconditionally, buffer or not; only an
+ /// explicit `unmap_layer` call removes one). `layer_destroyed` already
+ /// did this for the surface-destroyed case; `sync_layer_visibility`
+ /// (state/layers.rs) does it for the hide-without-destroying one --
+ /// AGS's dock, hiding for a fullscreen window, being the live case
+ /// that surfaced this. Stores the output it was unmapped from plus the
+ /// `LayerSurface` handle itself: `unmap_layer` removes it from
+ /// `LayerMap`'s own list, so `layer_for_surface` can never find it
+ /// again on its own - this is the only way `sync_layer_visibility`
+ /// can re-map it once the client commits real content again.
+ pub(crate) hidden_layer_surfaces: HashMap<WlSurface, (smithay::output::Output, smithay::desktop::LayerSurface)>,
pub(crate) decorations: HashMap<WindowId, MemoryRenderBuffer>,
/// The top border strip's rounded-corner bitmap, cached the same way
/// and at the same trigger points as `decorations` (built in
@@ -240,6 +314,24 @@ pub(crate) struct CompState {
/// [`Self::border_top_decorations`]'s mirror for the bottom strip's own
/// two corners - same cache, same trigger points, same reasoning.
pub(crate) border_bottom_decorations: HashMap<WindowId, MemoryRenderBuffer>,
+ /// What `redraw_decoration_buffer` last actually rendered for a window
+ /// - every input its own rasterization reads (width, `decorated`,
+ /// focus, title text, border colour/width) - so a call that would
+ /// rebuild the exact same pixels can skip doing so instead.
+ ///
+ /// Exists because `main.rs`'s `sync()` calls `Platform::redraw_
+ /// decoration` - which always reaches this - for *every visible
+ /// window*, on *every* tick that has anything at all marked dirty, not
+ /// only the window whose state actually changed: a resize drag alone
+ /// fires this for every other open window too, once per pointer-motion
+ /// event, each one re-rendering title text and re-rasterizing border
+ /// strips into a freshly allocated buffer for no visible difference.
+ /// The `decorations`/`border_*_decorations` doc comments already
+ /// establish "only rebuild at real trigger points" as the intended
+ /// contract; this closes the gap between that intent and `sync()`'s
+ /// own blanket call, which never actually checked whether this
+ /// specific window was one of the windows that triggered the tick.
+ pub(crate) decoration_signatures: HashMap<WindowId, DecorationSignature>,
/// A window's drop-shadow bitmap (`decoration::shadow_bitmap`), cached
/// the same way and at the same trigger points as `border_top_decorations`
/// - rebuilt only on creation or a real size change, not per frame, for
@@ -272,13 +364,18 @@ pub(crate) struct CompState {
pub(crate) content_epoch: HashMap<WindowId, u64>,
/// The udev/Pixman-backend rounded-corner masked copy of a window's own
/// content (`rounded_corners_pixman::masked_content_buffer`), paired
- /// with the `content_epoch` value it was built from - see
- /// `elements::rounded_content_buffer`, which owns rebuilding this.
+ /// with the `content_epoch` value and the `corner_radius` (in bit-cast
+ /// `u32` form - `f32` has no `Eq`) it was built from - see
+ /// `elements::rounded_content_buffer`, which owns rebuilding this. The
+ /// radius half exists because `corner_radius` is now live-settable
+ /// (`srd set corner_radius`/a rule) without any client commit - content
+ /// epoch alone wouldn't notice that change, leaving a stale mask built
+ /// from the old radius on screen until the client's next real repaint.
/// Always empty on the winit backend (GLES rounds via a shader instead,
/// `rounded_corners_program`), but costs nothing to declare here
/// unconditionally, the same call `rounded_corners_program` itself
/// already makes.
- pub(crate) rounded_content_buffers: HashMap<WindowId, (u64, MemoryRenderBuffer)>,
+ pub(crate) rounded_content_buffers: HashMap<WindowId, (u64, u32, MemoryRenderBuffer)>,
/// Persistent solid-colour buffers backing a window's other three
/// border strips (bottom, left, right - `decoration::border_strips`'
/// order past index 0), reused by position every frame rather than
@@ -325,6 +422,13 @@ pub(crate) struct CompState {
/// module docs for why this needs its own connection rather than going
/// through `X11Wm`. `None` until XWayland is ready, same as `xwm`.
pub(crate) ewmh: Option<xwayland::EwmhState>,
+ /// `com.canonical.AppMenu.Registrar` - the classic Qt/`appmenu-qt5`
+ /// global-menu source, see `srdwm_platform::appmenu_registrar`'s module
+ /// doc comment for why it lives in the shared platform crate rather
+ /// than here. `None` until XWayland is ready, same as `ewmh`/`xwm` --
+ /// constructed alongside `ewmh` in `xwayland.rs::spawn`'s `XWaylandEvent
+ /// ::Ready` handler, since a raw X11 window id is meaningless without it.
+ pub(crate) appmenu_registrar: Option<srdwm_platform::AppmenuRegistrarState>,
/// `ext_idle_notify_v1` - lets a client (a lock daemon, a bar's idle
/// indicator) ask to be told after N seconds of no real input. Both
/// this and `_idle_inhibit_manager_state` below use smithay's own