srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/state
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-09-10 11:24:00 +0200
committersrdusr <[email protected]>2025-09-10 11:24:00 +0200
commit5ee627928d0cb017e59d796c71793c35d5b65d33 (patch)
tree3285a7df82a04b178fb41fae220d60fe1dd38bd7 /crates/wayland/src/state
parent4a55182ce17fd4474f8be1e8819a41b7ccd11045 (diff)
downloadsrdwm-5ee627928d0cb017e59d796c71793c35d5b65d33.tar.gz
srdwm-5ee627928d0cb017e59d796c71793c35d5b65d33.zip
Wayland backend: desktop icons v2, menu rebuild, layer-shell/scale fixes,
XWayland stability, GPU rendering, and multi-cursor Phase 2 The bulk of a multi-session shift's real work landed in crates/wayland. Full root-cause/verification narrative for every item below lives in docs/TODO.md (each has its own dated entry); this is the summary: Desktop shell: - Real desktop icons v2 (state/desktop_icons.rs, desktop_icons.rs): fixed origin-baked-before-the-bar-connects, fixed-icon sort order, and a proper Rename/Delete-to-Trash menu (window_memory.rs backs the rename-persistence side). Rubber-band marquee multi-select. - icon_theme.rs: real freedesktop icon-theme lookup (inherits chain, hicolor fallback) rendering actual theme SVGs via resvg/tiny-skia, replacing the hand-drawn placeholder glyphs. - Context/desktop menus (decoration.rs, desktop_menu.rs, state/menu.rs) rebuilt to match the project's own AGS panel styling: rounded floating panel, tinted-fill row highlight, real separators, a much fuller titlebar window-menu action set. Layer-shell / multi-monitor: - Layer-shell hit-testing and render positioning (input/pointer.rs, udev/render.rs's element placement) now correctly convert LayerMap's logical geometry into physical pixels on a fractionally-scaled output - root cause of a bottom-anchored dock being unclickable and unpainted while a top-anchored bar on the same output worked. udev/outputs.rs's relayout_outputs gained the same physical/logical split for cross-output positioning, now backed by a real unit test (next_logical_x) built from the original measured incident numbers. - state/geometry.rs: a window's border/decoration no longer briefly clips when moved between differently-scaled monitors mid-drag. XWayland / stability: - xwayland.rs, udev/session.rs, udev/platform.rs: fixed a 100%- reproducible cold-start XKEYBOARD crash-loop (XWayland's own stdin inherited a real, already-owned VT; env passthrough and idle-callback spawn timing were both real, independent gaps) that had silently taken down all X11-app support and the global-menu registrar every session. - state/toplevel.rs, state/lifecycle.rs: XWayland dialog detection via WM_TRANSIENT_FOR, not just a native xdg_toplevel parent. Rendering: - udev/render.rs, decoration.rs: real GPU (GBM+EGL+DrmCompositor) window-content and cursor rendering on the udev backend, falling back to the untouched Pixman path automatically on any init failure. - decoration/tests.rs, state/mod.rs: rounded-corner/border fixes for interactive resize lag and cross-monitor moves. Multi-cursor Phase 2 (virtual_pointer.rs, new; state/mod.rs, udev/ platform.rs, winit/nested_platform.rs): pins a zwlr_virtual_pointer_ unstable_v1 object to a specific window, bypassing the shared seat/ focus/pointer_pos path entirely via hand-rolled wl_pointer.enter/motion/ button/frame/leave against every WlPointer the target client has bound (PointerHandle::client_pointers). Lets an agent operate one window while a human uses another, genuinely simultaneously, with zero client cooperation and no second wl_seat (confirmed a dead end: real clients only ever bind the first seat advertised). Full workspace build/test/clippy clean.
Diffstat (limited to 'crates/wayland/src/state')
-rw-r--r--crates/wayland/src/state/desktop_icons.rs188
-rw-r--r--crates/wayland/src/state/geometry.rs40
-rw-r--r--crates/wayland/src/state/lifecycle.rs5
-rw-r--r--crates/wayland/src/state/menu.rs18
-rw-r--r--crates/wayland/src/state/mod.rs27
-rw-r--r--crates/wayland/src/state/toplevel.rs4
6 files changed, 262 insertions, 20 deletions
diff --git a/crates/wayland/src/state/desktop_icons.rs b/crates/wayland/src/state/desktop_icons.rs
index bdaff43..f111336 100644
--- a/crates/wayland/src/state/desktop_icons.rs
+++ b/crates/wayland/src/state/desktop_icons.rs
@@ -52,19 +52,44 @@ impl CompState {
if !self.wm.borrow().desktop_icons_enabled {
return;
}
- let Some(monitor) = self.wm.borrow().monitors().iter().find(|m| m.primary).cloned() else { return };
- let origin = (monitor.geometry.x + GRID_MARGIN, monitor.geometry.y + GRID_MARGIN);
+ let origins = self.desktop_icon_origins();
+ if origins.is_empty() {
+ return;
+ }
+ // Rows (for the default-cell-assignment grid) are always derived
+ // from the *primary* monitor's own height, even when mirroring onto
+ // every monitor - one shared cell layout for the shared icon list,
+ // not a different grid shape per monitor.
+ let rows_source = self.wm.borrow().monitors().iter().find(|m| m.primary).map(|m| m.geometry.height as i32).unwrap_or(600);
if let Some(icons) = &mut self.desktop_icons {
- icons.origin = origin;
+ icons.origins = origins;
return;
}
- let rows = ((monitor.geometry.height as i32 - 2 * GRID_MARGIN) / CELL_HEIGHT).max(1);
+ let rows = ((rows_source - 2 * GRID_MARGIN) / CELL_HEIGHT).max(1);
let saved = crate::desktop_icons_state::load();
let icons = crate::desktop_icons::rescan(&saved, rows);
- self.desktop_icons = Some(DesktopIcons { origin, icons });
+ self.desktop_icons = Some(DesktopIcons { origins, icons });
self.desktop_icon_buffers.clear();
}
+ /// One grid origin per monitor icons should mirror onto: every enabled
+ /// monitor when `general.desktop_icons_all_monitors` is on (the
+ /// default - see that field's own doc comment), otherwise just the
+ /// primary monitor's, matching the original single-monitor behaviour.
+ /// Sorted by monitor id so the list (and therefore which origin
+ /// `icon_at` matches first) is stable call to call, not at the mercy of
+ /// `WindowManager::monitors()`'s own iteration order.
+ fn desktop_icon_origins(&self) -> Vec<(i32, i32)> {
+ let wm = self.wm.borrow();
+ let mut monitors = wm.monitors().to_vec();
+ monitors.sort_by_key(|m| m.id);
+ if wm.desktop_icons_all_monitors {
+ monitors.iter().map(|m| (m.geometry.x + GRID_MARGIN, m.geometry.y + GRID_MARGIN)).collect()
+ } else {
+ monitors.iter().find(|m| m.primary).map(|m| vec![(m.geometry.x + GRID_MARGIN, m.geometry.y + GRID_MARGIN)]).unwrap_or_default()
+ }
+ }
+
/// Re-derives the icon list from the real filesystem (a new/removed
/// `~/Desktop` entry) without disturbing any already-persisted cell --
/// `rescan` itself already only assigns a fresh default cell to an
@@ -73,9 +98,9 @@ impl CompState {
let Some(icons) = &self.desktop_icons else { return };
let rows = ((self.primary_monitor_height()) / CELL_HEIGHT).max(1);
let saved = crate::desktop_icons_state::load();
- let origin = icons.origin;
+ let origins = icons.origins.clone();
let icons = crate::desktop_icons::rescan(&saved, rows);
- self.desktop_icons = Some(DesktopIcons { origin, icons });
+ self.desktop_icons = Some(DesktopIcons { origins, icons });
self.desktop_icon_buffers.clear();
}
@@ -100,6 +125,19 @@ impl CompState {
Some((rid, buf)) if rid == id => format!("{buf}_").into(),
_ => icon.label.as_str().into(),
};
+ // Real icon-theme artwork (WhiteSur, or whatever the user has
+ // configured) when it resolves; `None` - no installed theme ships
+ // this name, or the file it found didn't parse/render - falls
+ // back to `render_desktop_icon`'s own hand-drawn glyph rather than
+ // a blank box. Re-looked-up on every rebuild (icon selection
+ // toggling, a rename) rather than cached separately: rebuilds are
+ // already infrequent (see this function's own doc comment), and a
+ // theme change while running should just work on the next one
+ // without a separate cache-invalidation path to get wrong.
+ let glyph_box = decoration::desktop_icon_glyph_box(CELL_WIDTH as u32, CELL_HEIGHT as u32);
+ let (glyph_w, glyph_h) = ((glyph_box.2 - glyph_box.0).max(1) as u32, (glyph_box.3 - glyph_box.1).max(1) as u32);
+ let real_icon = crate::icon_theme::find_icon(crate::icon_theme::icon_name(icon.kind))
+ .and_then(|path| crate::icon_theme::rasterize_svg(&path, glyph_w, glyph_h));
let data = decoration::render_desktop_icon(
CELL_WIDTH as u32,
CELL_HEIGHT as u32,
@@ -109,6 +147,7 @@ impl CompState {
ICON_COLOR,
label_color,
theme.default_border_color,
+ real_icon.as_deref(),
);
let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (CELL_WIDTH, CELL_HEIGHT), 1, Transform::Normal, None);
self.desktop_icon_buffers.insert(id.to_string(), buffer);
@@ -130,9 +169,9 @@ impl CompState {
pub(crate) fn desktop_icon_render_list(&mut self) -> Vec<((i32, i32), MemoryRenderBuffer)> {
let Some(icons) = &self.desktop_icons else { return Vec::new() };
let ids: Vec<String> = icons.icons.iter().map(|i| i.id.clone()).collect();
- let origin = icons.origin;
+ let origins = icons.origins.clone();
let dragging = self.desktop_icon_drag.clone();
- let mut out = Vec::with_capacity(ids.len());
+ let mut out = Vec::with_capacity(ids.len() * origins.len());
for id in ids {
let buffer = match self.icon_buffer(&id) {
Some(b) => b.clone(),
@@ -140,11 +179,19 @@ impl CompState {
};
let icons = self.desktop_icons.as_ref().unwrap();
let icon = icons.icons.iter().find(|i| i.id == id).unwrap();
- let pos = match &dragging {
- Some((drag_id, _, live_pos)) if *drag_id == id => *live_pos,
- _ => icon.top_left(origin),
- };
- out.push((pos, buffer));
+ // The dragged copy follows the live pointer on whichever
+ // monitor it's actually being dragged over; every other
+ // mirror (if any) stays put at its own origin's cell position
+ // - dragging on one monitor doesn't yank the icon's other
+ // mirrors around mid-gesture, only the one actually grabbed.
+ match &dragging {
+ Some((drag_id, _, live_pos)) if *drag_id == id => out.push((*live_pos, buffer)),
+ _ => {
+ for &origin in &origins {
+ out.push((icon.top_left(origin), buffer.clone()));
+ }
+ }
+ }
}
out
}
@@ -167,6 +214,66 @@ impl CompState {
}
}
+ /// Starts a rubber-band selection at `pos` (global space) - clears
+ /// whatever was selected before, matching real desktop convention
+ /// (Windows/GNOME/macOS all start a fresh marquee selection, not an
+ /// additive one, unless a modifier like Ctrl/Shift is held - not
+ /// implemented here, same as this menu's own already-documented "no
+ /// multi-select via click" gap, just now closed for the drag case).
+ pub(crate) fn start_desktop_marquee(&mut self, pos: (i32, i32)) {
+ self.select_desktop_icon(None);
+ self.desktop_marquee = Some((pos, pos));
+ }
+
+ /// Updates the live end corner of an in-progress marquee and re-
+ /// selects whatever icon cells the resulting rectangle now overlaps --
+ /// called from every pointer-motion event while a marquee is active,
+ /// same shape as `update_desktop_icon_drag`.
+ pub(crate) fn update_desktop_marquee(&mut self, pos: (i32, i32)) {
+ let Some((start, _)) = self.desktop_marquee else { return };
+ self.desktop_marquee = Some((start, pos));
+ let (x0, y0) = (start.0.min(pos.0), start.1.min(pos.1));
+ let (x1, y1) = (start.0.max(pos.0), start.1.max(pos.1));
+ // Only the mirror on whichever monitor the marquee itself *started*
+ // on - a drag on one monitor selecting another monitor's mirrored
+ // copies (or double-selecting both) would be actively confusing,
+ // not a feature. Falls back to every origin if no monitor claims
+ // the start point at all (shouldn't happen in practice; `Compositor
+ // ::pointer_monitor`-style clamping already keeps the pointer
+ // inside some monitor's bounds).
+ let origins: Vec<(i32, i32)> = {
+ let wm = self.wm.borrow();
+ match wm.monitors().iter().find(|m| m.full_geometry.contains_point(start.0, start.1)) {
+ Some(m) => vec![(m.geometry.x + GRID_MARGIN, m.geometry.y + GRID_MARGIN)],
+ None => self.desktop_icons.as_ref().map(|i| i.origins.clone()).unwrap_or_default(),
+ }
+ };
+ let Some(icons) = &mut self.desktop_icons else { return };
+ let mut changed = Vec::new();
+ for icon in &mut icons.icons {
+ let overlaps = origins.iter().any(|&origin| {
+ let (left, top) = icon.top_left(origin);
+ let (right, bottom) = (left + CELL_WIDTH, top + CELL_HEIGHT);
+ left < x1 && right > x0 && top < y1 && bottom > y0
+ });
+ if icon.selected != overlaps {
+ icon.selected = overlaps;
+ changed.push(icon.id.clone());
+ }
+ }
+ for id in changed {
+ self.rebuild_icon_buffer(&id);
+ }
+ }
+
+ /// Ends an in-progress marquee, if any - the final `update_desktop_
+ /// marquee` call already left the right icons selected, so this only
+ /// needs to clear the drag state itself (and the rendered outline with
+ /// it).
+ pub(crate) fn end_desktop_marquee(&mut self) {
+ self.desktop_marquee = None;
+ }
+
/// True when this press is the second of a double-click on the same
/// icon - same 400ms threshold and reset-after-a-double shape as
/// `is_double_click`, keyed by `DesktopIcon::id` since an icon has no
@@ -181,10 +288,16 @@ impl CompState {
doubled
}
- pub(crate) fn start_desktop_icon_drag(&mut self, id: &str, pointer: (i32, i32)) {
+ /// `origin` is whichever mirror was actually clicked (`icon_at`'s own
+ /// return value, threaded through by the caller) - with the icon
+ /// mirrored onto several monitors, grabbing it relative to the copy
+ /// under the pointer, not always the primary monitor's, is what makes
+ /// the drag track the cursor instead of jumping to a different
+ /// monitor's copy the instant the drag starts.
+ pub(crate) fn start_desktop_icon_drag(&mut self, id: &str, origin: (i32, i32), pointer: (i32, i32)) {
let Some(icons) = &self.desktop_icons else { return };
let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
- let top_left = icon.top_left(icons.origin);
+ let top_left = icon.top_left(origin);
let grab_offset = (pointer.0 - top_left.0, pointer.1 - top_left.1);
self.desktop_icon_drag = Some((id.to_string(), grab_offset, top_left));
}
@@ -204,8 +317,22 @@ impl CompState {
/// and persists it.
pub(crate) fn end_desktop_icon_drag(&mut self) {
let Some((id, _, live_pos)) = self.desktop_icon_drag.take() else { return };
+ // The cell math below needs the origin of whichever monitor the
+ // icon was actually dropped on, not always the first mirror --
+ // recomputed fresh (same formula `desktop_icon_origins` uses)
+ // rather than searched for in `icons.origins`, since a drop
+ // just past every monitor's own strict icon-grid rect (but still
+ // on-screen) should still resolve to that monitor's grid, not fall
+ // through to a stale/wrong one.
+ let origin = self
+ .wm
+ .borrow()
+ .monitors()
+ .iter()
+ .find(|m| m.full_geometry.contains_point(live_pos.0, live_pos.1))
+ .map(|m| (m.geometry.x + GRID_MARGIN, m.geometry.y + GRID_MARGIN))
+ .unwrap_or(self.desktop_icons.as_ref().map(|i| i.origins.first().copied().unwrap_or((0, 0))).unwrap_or((0, 0)));
let Some(icons) = &mut self.desktop_icons else { return };
- let origin = icons.origin;
let raw = (live_pos.0 - origin.0, live_pos.1 - origin.1);
let raw_cell = ((raw.0 as f64 / CELL_WIDTH as f64).round() as i32, (raw.1 as f64 / CELL_HEIGHT as f64).round() as i32).max_zero();
let occupied: std::collections::HashSet<(i32, i32)> = icons.icons.iter().filter(|i| i.id != id).map(|i| i.cell).collect();
@@ -364,6 +491,27 @@ impl CompState {
self.refresh_desktop_icons();
}
+ /// "New > Text Document" (Windows) / a blank file (macOS's own desktop
+ /// menu has no direct equivalent, but every mainstream file manager
+ /// does) - the concrete gap behind "not even new file" reported live
+ /// against this menu next to Windows'/macOS' own. Same collision-
+ /// avoidance and refresh as `new_desktop_folder` just above.
+ pub(crate) fn new_desktop_text_file(&mut self) {
+ let Ok(home) = std::env::var("HOME") else { return };
+ let desktop = std::path::PathBuf::from(home).join("Desktop");
+ let mut name = "New Text Document.txt".to_string();
+ let mut n = 2;
+ while desktop.join(&name).exists() {
+ name = format!("New Text Document ({n}).txt");
+ n += 1;
+ }
+ if let Err(e) = std::fs::write(desktop.join(&name), "") {
+ log::warn!("desktop_icons: couldn't create {name:?}: {e}");
+ return;
+ }
+ self.refresh_desktop_icons();
+ }
+
pub(crate) fn open_desktop_icon_menu(&mut self, icon_id: &str, pos: (i32, i32)) {
let Some(icons) = &self.desktop_icons else { return };
let Some(icon) = icons.icons.iter().find(|i| i.id == icon_id) else { return };
@@ -397,9 +545,15 @@ impl CompState {
DesktopMenuAction::Delete(id) => self.delete_desktop_icon(&id),
DesktopMenuAction::EmptyTrash => self.empty_trash(),
DesktopMenuAction::NewFolder => self.new_desktop_folder(),
+ DesktopMenuAction::NewTextFile => self.new_desktop_text_file(),
DesktopMenuAction::OpenTerminalHere => self.open_terminal_here(),
DesktopMenuAction::OpenInFileManager => self.open_desktop_in_file_manager(),
DesktopMenuAction::Refresh => self.refresh_desktop_icons(),
+ // Never actually reached - the click-dispatch site intercepts
+ // `Separator` first, same as `context_menu::MenuAction::
+ // Separator`'s own dispatch. Handled here too so this match
+ // stays exhaustive.
+ DesktopMenuAction::Separator => {}
}
}
}
diff --git a/crates/wayland/src/state/geometry.rs b/crates/wayland/src/state/geometry.rs
index 26c23fe..3d201dd 100644
--- a/crates/wayland/src/state/geometry.rs
+++ b/crates/wayland/src/state/geometry.rs
@@ -36,7 +36,13 @@ impl CompState {
/// through the inherent method below wherever a plain `&self` is
/// available (input handling, `redraw_decoration_buffer`); this
/// version exists for the render loops specifically.
- pub(crate) fn effective_frame_of(wm: &Rc<RefCell<WindowManager>>, id_to_window: &HashMap<WindowId, DWindow>, id: WindowId, geom: srdwm_core::Rect) -> srdwm_core::Rect {
+ pub(crate) fn effective_frame_of(
+ wm: &Rc<RefCell<WindowManager>>,
+ id_to_window: &HashMap<WindowId, DWindow>,
+ pending_size_configure: &HashMap<WindowId, ((i32, i32), Instant)>,
+ id: WindowId,
+ geom: srdwm_core::Rect,
+ ) -> srdwm_core::Rect {
// A version of this function briefly (this same session) skipped
// the committed-size correction below entirely during an active
// resize, on the reasoning that trusting the client's stale last
@@ -81,6 +87,36 @@ impl CompState {
if wm.borrow().resizing_window() == Some(id) {
return geom;
}
+ // Same reasoning as the active-resize branch just above, for a gap
+ // that isn't a drag at all: a plain cross-monitor *move* between two
+ // differently-scaled outputs still forces a size-changing configure
+ // (see `sync_geometry`'s own doc comment - a window's physical
+ // footprint stays constant across the move, so its *logical* size,
+ // what the client is actually told, necessarily changes with the
+ // new monitor's scale). Until the client catches up, `bbox()` below
+ // still reflects its *last real commit* - content sized for the
+ // *old* monitor's scale - but `w.monitor` has already flipped to
+ // the *new* one (updated live, every drag tick, independently of
+ // any commit). Multiplying the stale logical content by the new
+ // scale produces neither the old physical size nor the new one, a
+ // real mismatch between where the border/shadow get drawn and where
+ // the client's actual pixels are. Reported live, confirmed with
+ // before/after screenshots: a window's left border missing and its
+ // content clipped for one frame right after crossing from a
+ // scale-0.843 output onto a scale-1.0 one, self-correcting once the
+ // client's own commit landed - see `docs/TODO.md`. `pending_size_
+ // configure` (see its own doc comment) already tracks exactly this
+ // gap for the throttle above; reusing it here instead of adding new
+ // state closes it: while a configure is outstanding, trust this
+ // compositor's own live target (`geom`) the same way an active
+ // resize already does, rather than reconstructing physical size
+ // from a logical value that was never committed under this scale.
+ if pending_size_configure.get(&id).is_some_and(|(pending_size, sent_at)| {
+ let caught_up = id_to_window.get(&id).is_some_and(|w| w.geometry().size.w == pending_size.0 && w.geometry().size.h == pending_size.1);
+ !caught_up && sent_at.elapsed() < CONFIGURE_THROTTLE_TIMEOUT
+ }) {
+ return geom;
+ }
let Some(w) = wm.borrow().window(id).cloned() else { return geom };
let Some(dwindow) = id_to_window.get(&id) else { return geom };
// `dwindow.geometry()` - `xdg_surface::set_window_geometry` - is,
@@ -206,7 +242,7 @@ impl CompState {
/// compositor's own bookkeeping staying self-consistent, not about
/// matching a client's real pixels.
pub(crate) fn effective_frame(&self, id: WindowId, geom: srdwm_core::Rect) -> srdwm_core::Rect {
- Self::effective_frame_of(&self.wm, &self.id_to_window, id, geom)
+ Self::effective_frame_of(&self.wm, &self.id_to_window, &self.pending_size_configure, id, geom)
}
pub(crate) fn sync_geometry(&mut self, id: WindowId) {
diff --git a/crates/wayland/src/state/lifecycle.rs b/crates/wayland/src/state/lifecycle.rs
index 53a5e86..838e308 100644
--- a/crates/wayland/src/state/lifecycle.rs
+++ b/crates/wayland/src/state/lifecycle.rs
@@ -156,6 +156,11 @@ impl CompState {
traffic_light_buttons: theme.traffic_light_buttons,
is_dialog,
};
+ log::warn!(
+ "DECO-DIAG redraw_decoration_buffer id={id} w.decorated={} early_return={}",
+ w.decorated,
+ self.decoration_signatures.get(&id) == Some(&signature)
+ );
if self.decoration_signatures.get(&id) == Some(&signature) {
return;
}
diff --git a/crates/wayland/src/state/menu.rs b/crates/wayland/src/state/menu.rs
index 9f645a8..b8cf094 100644
--- a/crates/wayland/src/state/menu.rs
+++ b/crates/wayland/src/state/menu.rs
@@ -45,14 +45,32 @@ impl CompState {
self.sync_geometry(window);
foreign_toplevel::send_state(self, window);
}
+ MenuAction::ToggleFullscreen => {
+ self.wm.borrow_mut().toggle_fullscreen(window);
+ self.sync_geometry(window);
+ foreign_toplevel::send_state(self, window);
+ }
+ MenuAction::ToggleFloating => {
+ self.wm.borrow_mut().toggle_floating(window);
+ self.sync_geometry(window);
+ }
MenuAction::ToggleAlwaysOnTop => {
self.wm.borrow_mut().toggle_always_on_top(window);
}
+ MenuAction::MoveToWorkspace(workspace) => {
+ self.wm.borrow_mut().move_window_to_workspace(window, workspace);
+ }
MenuAction::Close => {
if let Some(w) = self.id_to_window.get(&window) {
crate::input::close_dwindow(w);
}
}
+ // Never actually reached: the click-dispatch site
+ // (`input/pointer.rs`) intercepts `Separator` before calling
+ // this function at all. Handled here too so this match stays
+ // exhaustive without a catch-all that would silently swallow a
+ // real future variant added without updating this function.
+ MenuAction::Separator => {}
}
}
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index 0fb303e..3519297 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -39,6 +39,7 @@ use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
use smithay::wayland::dmabuf::DmabufState;
use smithay::wayland::shm::ShmState;
use smithay::wayland::xdg_activation::XdgActivationState;
+use wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1;
use srdwm_core::{Event as CoreEvent, SnapZoneKind, Window as CoreWindow, WindowId, WindowManager, TITLEBAR_HEIGHT};
@@ -217,6 +218,7 @@ pub(crate) struct CompState {
pub(crate) _fractional_scale_state: smithay::wayland::fractional_scale::FractionalScaleManagerState,
pub(crate) _cursor_shape_state: smithay::wayland::cursor_shape::CursorShapeManagerState,
pub(crate) _screencopy_state: screencopy::ScreencopyState,
+ pub(crate) _virtual_pointer_state: crate::virtual_pointer::VirtualPointerState,
/// 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>,
@@ -362,6 +364,20 @@ pub(crate) struct CompState {
/// drag is active.
#[allow(clippy::type_complexity)]
pub(crate) desktop_icon_drag: Option<(String, (i32, i32), (i32, i32))>,
+ /// An active rubber-band/marquee selection drag on bare desktop --
+ /// `(start, current)`, both global-space pointer positions. The one
+ /// "click and drag" desktop interaction this compositor never had at
+ /// all (only single-icon click-select existed) - reported live next
+ /// to "missing click and drag stuff like from windows". `None`
+ /// whenever no marquee is active. See `start_desktop_marquee`/
+ /// `update_desktop_marquee`/`end_desktop_marquee`.
+ pub(crate) desktop_marquee: Option<((i32, i32), (i32, i32))>,
+ /// Four thin solid-color strips forming the marquee's own rectangle
+ /// outline (top/bottom/left/right) - same "keep a persistent `Solid
+ /// ColorBuffer` per strip, update it in place every frame" pattern
+ /// `border_side_buffers` already uses for window borders, reused here
+ /// rather than allocating a fresh buffer on every motion tick.
+ pub(crate) marquee_buffers: [SolidColorBuffer; 4],
/// The right-click desktop-icon/bare-desktop menu, if one is currently
/// open - see `desktop_menu.rs`. Same lifecycle/mutual-exclusion
/// story as `context_menu`/`snap_flyout` above.
@@ -383,6 +399,17 @@ pub(crate) struct CompState {
pub(crate) wm: Rc<RefCell<WindowManager>>,
pub(crate) surface_to_id: HashMap<WlSurface, WindowId>,
pub(crate) id_to_window: HashMap<WindowId, DWindow>,
+ /// Every live `zwlr_virtual_pointer_v1` object, so `set_virtual_pointer_
+ /// pin` (`virtual_pointer.rs`) can find every pointer a given client
+ /// (identified by pid, via `Client::get_credentials`) owns without a
+ /// second, redundant per-client map - see that module's own doc
+ /// comment for why pid, not an opaque per-object id nothing outside
+ /// this compositor could otherwise learn, is the pinning handle. Pruned
+ /// lazily (a destroyed resource's own methods become no-ops, and dead
+ /// entries are filtered out the next time this is walked) rather than
+ /// on every single destroy - this list is only ever touched by an
+ /// infrequent pin/unpin request, never a hot path.
+ pub(crate) virtual_pointers: Vec<ZwlrVirtualPointerV1>,
/// Surfaces whose `zwlr_layer_surface_v1` role has been destroyed --
/// consulted by the pre-commit hook `CompositorHandler::new_surface`
/// registers (see its doc comment) to work around a real smithay bug
diff --git a/crates/wayland/src/state/toplevel.rs b/crates/wayland/src/state/toplevel.rs
index 2504ad2..90cf688 100644
--- a/crates/wayland/src/state/toplevel.rs
+++ b/crates/wayland/src/state/toplevel.rs
@@ -73,7 +73,9 @@ pub(crate) fn sync_toplevel_metadata(state: &mut CompState, id: WindowId, surfac
// window back to the front any time its title happened to
// update - reported live as an older window jumping in front of
// a newer, focused one with no user action to explain it.
- if state.wm.borrow_mut().reapply_rules_if_pending(id) {
+ let reapplied = state.wm.borrow_mut().reapply_rules_if_pending(id);
+ log::warn!("DECO-DIAG sync_toplevel_metadata id={id} changed=true reapplied={reapplied}");
+ if reapplied {
state.redraw_decoration_buffer(id);
state.sync_geometry(id);
}