srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/input
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-04-27 00:18:00 +0200
committersrdusr <[email protected]>2026-04-27 00:18:00 +0200
commitc4dc99cc3211c4dc1a461f228402be1593b883c9 (patch)
tree10a85d60acb863ca98b7e70118dec5cda357efa6 /crates/wayland/src/input
parent436d42da6ef61a5ea20d5102c4baed7bf0993606 (diff)
downloadsrdwm-c4dc99cc3211c4dc1a461f228402be1593b883c9.tar.gz
srdwm-c4dc99cc3211c4dc1a461f228402be1593b883c9.zip
Build the eight asks recovered from the previous session's transcript
The punch list was not the whole record. These are the owner's own typed requests, read back out of the previous session's transcript rather than guessed at, then each checked against the code before being treated as open. Three they suspected were already done really were: dialogs already had a Close-only titlebar, inactive dimming already existed, and the corner resize hitbox had already been tuned. Snap layouts on drag, asked for twice. Edge snapping worked but committed silently on release with nothing shown first, so there was no way to know it would happen or where. A translucent drop-target preview now follows the drag, and throwing the pointer at a monitor's top edge drops down the existing six-cell grid to aim at. The preview calls the same snap_zone that end_drag does, so the two cannot disagree. Two defects found by screenshot before landing: moving down onto the flyout closed it, and its labels overflowed at a fixed cell width - the same "text goes out of view" fault already fixed once for the context menu. New File now offers real types, chosen by extension, with the de-duplication counter placed before the extension so the file stays what it says it is. Refresh re-reads init.lua and fires a new srd.on("refresh") handler instead of only re-scanning the icon grid. What refresh means beyond srdwm's own config stays the config's decision. general.config_reload_on_write (default on) applies an edited config on save, via an mtime sweep rather than an inotify watch: no new dependency, same behaviour on every target, and unaffected by editors that write through a temp file. A real bug behind "what happens when our config fails": you lost every keybinding. do_reload cleared the binding, handler and repeat tables before re-executing and never restored them, so a syntax error left neither the old config nor the new one, and the only key still working was the reload combo nobody thinks to press. The tables are now restored on any failure and config errors reach notify-send, not just the log. srd.lock() and a default Mod4+Ctrl+l binding: the built-in lock screen could not be reached from Lua at all. Native rather than shelling out, because a lock key that shells out fails silently when the binary is not on PATH. Default bindings added for srd.window.move and a dynamic/tiling toggle, both of which existed with no way to reach them, plus srd.layout.get() so the toggle reads the live workspace rather than the configured default. Dialogs open centred, and are excluded from remembered geometry in both directions - that table is keyed by app_id, which a dialog shares with the window that spawned it, so dialogs inherited an unrelated position and size and then overwrote it with their own. theme.decorations.title_bar.button_mode (dynamic by default, or fixed) drops the Maximize button on a window whose client pinned min == max size, where pressing it can do nothing. Maximize is removed from the slot list rather than skipped in place on both the render and hit-test sides, so the remaining buttons close the gap identically; three tests pin that agreement, which is what fails silently when it drifts. Also: the nested backend's screencopy pass now draws both menus, the flyout and the drag preview. Four investigations in one day started from a screenshot missing a tier, so that pass carries an explicit list of what it still omits and the on-screen loop points at it. 512 tests pass, clippy clean.
Diffstat (limited to 'crates/wayland/src/input')
-rw-r--r--crates/wayland/src/input/pointer.rs82
1 files changed, 82 insertions, 0 deletions
diff --git a/crates/wayland/src/input/pointer.rs b/crates/wayland/src/input/pointer.rs
index c6ed5ba..e82b25f 100644
--- a/crates/wayland/src/input/pointer.rs
+++ b/crates/wayland/src/input/pointer.rs
@@ -23,6 +23,26 @@ use super::{notify_idle_activity, DRAG_MODIFIER};
/// mouse or touchpad can produce.
pub(crate) const RESIZE_REDRAW_INTERVAL: std::time::Duration = std::time::Duration::from_millis(1000 / 60);
+/// How far below a monitor's top edge the drag-triggered Snap-Layouts
+/// flyout hangs. Enough to clear a typical bar, so the grid is not opening
+/// underneath one.
+const SNAP_FLYOUT_DROP: i32 = 36;
+
+/// What one pointer-motion tick decided about the drag-triggered
+/// Snap-Layouts flyout, decided while `WindowManager` is borrowed and acted
+/// on once that borrow is released (opening the flyout rasterises a buffer,
+/// which needs `&mut CompState` as a whole).
+///
+/// `NotDragging` is distinct from `Close` on purpose: a flyout opened the
+/// other way - by right-clicking a maximize button - must survive an
+/// unrelated pointer move, so a tick with no drag in progress has to leave
+/// it alone rather than close it.
+enum DragFlyout {
+ Open(WindowId, (i32, i32)),
+ Close,
+ NotDragging,
+}
+
/// `WindowManager::hit_test`, but substituting each window's currently
/// *animated* rect (if it has one active in `state.window_anims`) for its
/// final `geometry` - see `hit_test_with`'s own doc comment in
@@ -311,6 +331,9 @@ pub(crate) fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Log
update_cursor_shape(state, hit, over_layer_surface, over_content);
+ let mut drag_flyout = DragFlyout::NotDragging;
+ // Resolved before `wm` is borrowed below, since it reads `state`.
+ let over_open_flyout = state.snap_flyout.as_ref().is_some_and(|f| f.zone_at(pos.x as i32, pos.y as i32).is_some());
let mut wm = state.wm.borrow_mut();
let dragging_or_resizing = wm.is_dragging() || wm.is_resizing();
// Captured now, while `wm` is already borrowed, and acted on further
@@ -320,6 +343,26 @@ pub(crate) fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Log
let resizing_id = wm.resizing_window();
if wm.is_dragging() {
wm.update_drag(pos.x as i32, pos.y as i32);
+ // Windows-11-style: throw the window at the top of the screen and
+ // the Snap-Layouts grid drops down to be aimed at. Captured here
+ // while `wm` is borrowed and acted on after `drop(wm)` below --
+ // opening the flyout rasterises a buffer, which needs `&mut state`
+ // as a whole.
+ drag_flyout = match (wm.drag_top_edge_monitor(), wm.dragged_window()) {
+ (Some(m), Some(id)) => {
+ let g = m.full_geometry;
+ DragFlyout::Open(id, (g.x + g.width as i32 / 2, g.y + SNAP_FLYOUT_DROP))
+ }
+ // Leaving the trigger band does not dismiss an already-open
+ // flyout while the pointer is still on it - aiming at a cell
+ // means moving down, away from the band, which would otherwise
+ // close the grid on the way to using it. Confirmed by
+ // screenshot before this check existed: the flyout appeared at
+ // the top edge and had vanished by the time the pointer
+ // reached its first cell.
+ _ if over_open_flyout => DragFlyout::NotDragging,
+ _ => DragFlyout::Close,
+ };
} else if wm.is_resizing() {
wm.update_resize(pos.x as i32, pos.y as i32);
}
@@ -348,6 +391,26 @@ pub(crate) fn handle_pointer_position(state: &mut CompState, pos: Point<f64, Log
}
}
drop(wm);
+ // Open/close the drag-triggered Snap-Layouts flyout, now that `wm`'s
+ // borrow is released - see the capture site above.
+ match drag_flyout {
+ DragFlyout::Open(id, anchor) => {
+ let already_there = state.snap_flyout.as_ref().is_some_and(|f| f.window == id);
+ if !already_there {
+ state.open_snap_flyout(id, anchor);
+ // Centre it on the anchor, which is the monitor's own
+ // horizontal midpoint - `open_snap_flyout` places its
+ // top-left corner at whatever it is given (the maximize
+ // button's position, for the click-driven caller), and
+ // only this caller wants it centred.
+ if let Some(flyout) = state.snap_flyout.as_mut() {
+ flyout.pos.0 -= flyout.width() as i32 / 2;
+ }
+ }
+ }
+ DragFlyout::Close => state.close_snap_flyout(),
+ DragFlyout::NotDragging => {}
+ }
if let Some(id) = focus_follow_target {
focus_window(state, id);
}
@@ -745,6 +808,15 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic
// below, just for a desktop icon instead of a window.
state.end_desktop_icon_drag();
state.end_desktop_marquee();
+ // Releasing a drag over a cell of the drag-triggered Snap-Layouts
+ // flyout picks that cell's zone, in place of whatever edge snap
+ // `end_drag` would otherwise have computed from the drop point.
+ // Resolved before `end_drag` runs, because that consumes the drag.
+ let flyout_pick = state
+ .snap_flyout
+ .as_ref()
+ .filter(|f| state.wm.borrow().dragged_window() == Some(f.window))
+ .and_then(|f| f.zone_at(pos.x as i32, pos.y as i32).map(|zone| (f.window, zone)));
let mut wm = state.wm.borrow_mut();
let was_dragging = wm.is_dragging();
let was_resizing = wm.is_resizing();
@@ -757,6 +829,12 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic
let id = wm.focused_id();
if was_dragging {
wm.end_drag();
+ // After `end_drag`, so the flyout's own choice is the last
+ // word rather than being overwritten by the edge snap that
+ // dropping at the top of the screen would otherwise apply.
+ if let Some((window, zone)) = flyout_pick {
+ wm.apply_snap_zone(window, zone);
+ }
} else if was_resizing {
wm.end_resize();
}
@@ -769,6 +847,10 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic
crate::window_memory::save_all(wm.all_remembered_geometry());
}
drop(wm);
+ if was_dragging {
+ // Whether or not it was used, a drag ending closes it.
+ state.close_snap_flyout();
+ }
// `end_drag` can snap the geometry one more time (edge/top-of-
// screen snapping, `SmartPlacement::snap_zone`) *after* the last
// `update_drag` already moved the window - without this, that