diff --git a/src/region.rs b/src/region.rs index ec5b769..bf2d88a 100644 --- a/src/region.rs +++ b/src/region.rs @@ -3,7 +3,10 @@ use std::sync::Arc; use anyhow::{Context, Result}; use smithay_client_toolkit::{ compositor::FrameCallbackData, - seat::pointer::{BTN_LEFT, BTN_RIGHT, PointerEvent, PointerEventKind}, + seat::{ + keyboard::Modifiers, + pointer::{BTN_LEFT, BTN_RIGHT, PointerEvent, PointerEventKind}, + }, shell::{ WaylandSurface, wlr_layer::{Anchor, KeyboardInteractivity, Layer, LayerSurface}, @@ -25,7 +28,7 @@ use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; use crate::error::{Cancelled, HintExt}; use crate::frame::{Bgrx, Frame, OutputGeom, PixelRect}; use crate::render; -use crate::selection::{self, Point, Selection}; +use crate::selection::{self, Mods, Point, Selection}; use crate::wayland::{Output, State, Wayland}; pub const NAMESPACE: &str = "valw-overlay"; @@ -45,6 +48,8 @@ pub struct Overlay { selection: Selection, /// Output where the current drag started; the capture is clipped to it. anchor: Option, + /// Shift, Alt and Space as the keyboard last reported them. + mods: Mods, outcome: Option>, } @@ -106,6 +111,7 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result< surfaces, selection: Selection::default(), anchor: None, + mods: Mods::default(), outcome: None, }); let result = wl.dispatch_blocking(|s| s.overlay.as_ref().is_some_and(|o| o.outcome.is_some())); @@ -169,6 +175,30 @@ impl Overlay { self.outcome.get_or_insert(None); } + pub fn modifiers(&mut self, m: Modifiers, qh: &QueueHandle) { + self.mods.shift = m.shift; + self.mods.alt = m.alt; + self.apply_mods(qh); + } + + /// Space before a drag switches to window mode; during one it moves + /// the selection while held. + pub fn space(&mut self, down: bool, qh: &QueueHandle) { + if down && self.selection.allows_window_switch() { + self.switch_to_window(); + return; + } + self.mods.space = down; + self.apply_mods(qh); + } + + fn apply_mods(&mut self, qh: &QueueHandle) { + self.selection.set_mods(self.mods); + if let Some(a) = self.anchor { + self.redraw(a, qh); + } + } + pub fn switch_to_window(&mut self) { if self.selection.allows_window_switch() { tracing::info!("switching to window mode"); @@ -184,9 +214,7 @@ impl Overlay { /// The selection as drawn on surface `i`: only the anchor output shows it. fn selection_on(&self, i: usize) -> Option { - let Selection::Dragging { start, end } = self.selection else { - return None; - }; + let (start, end) = self.selection.corners()?; let s = &self.surfaces[i]; (self.anchor == Some(i)) .then(|| selection::clip(start, end, &s.geom, s.width, s.height)) @@ -255,6 +283,8 @@ impl Overlay { button: BTN_LEFT, .. } => { self.selection.press(p); + // Modifiers already held apply from the start. + self.selection.set_mods(self.mods); self.anchor = Some(i); } PointerEventKind::Press { diff --git a/src/selection.rs b/src/selection.rs index 19ca896..f06fc31 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -10,33 +10,138 @@ pub struct Point { pub y: f64, } +/// Modifier keys that shape a selection while dragging. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct Mods { + pub shift: bool, + pub alt: bool, + pub space: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Axis { + X, + Y, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Drag { + /// The press point (the centre with Alt); Space moves it. + start: Point, + /// The far corner before Alt is applied. + end: Point, + /// The last pointer position. + last: Point, + /// Added to the pointer to get `end` (non-zero after a Space move). + offset: (f64, f64), + mods: Mods, + /// Shift: `end` when Shift went down, and the axis that follows. + lock: Option<(Point, Option)>, +} + #[derive(Debug, Clone, Copy, Default, PartialEq)] pub enum Selection { #[default] Idle, - Dragging { - start: Point, - end: Point, - }, + Dragging(Drag), } impl Selection { pub fn press(&mut self, p: Point) { - *self = Selection::Dragging { start: p, end: p }; + let mods = match self { + Selection::Dragging(d) => d.mods, + Selection::Idle => Mods::default(), + }; + *self = Selection::Dragging(Drag { + start: p, + end: p, + last: p, + offset: (0.0, 0.0), + mods, + lock: None, + }); } pub fn motion(&mut self, p: Point) { - if let Selection::Dragging { end, .. } = self { - *end = p; + let Selection::Dragging(d) = self else { return }; + if d.mods.space { + let (dx, dy) = (p.x - d.last.x, p.y - d.last.y); + d.start = Point { + x: d.start.x + dx, + y: d.start.y + dy, + }; + d.end = Point { + x: d.end.x + dx, + y: d.end.y + dy, + }; + } else { + let target = Point { + x: p.x + d.offset.0, + y: p.y + d.offset.1, + }; + d.end = match &mut d.lock { + Some((frozen, axis)) => { + let axis = *axis.get_or_insert_with(|| { + if (target.x - frozen.x).abs() >= (target.y - frozen.y).abs() { + Axis::X + } else { + Axis::Y + } + }); + match axis { + Axis::X => Point { + x: target.x, + y: frozen.y, + }, + Axis::Y => Point { + x: frozen.x, + y: target.y, + }, + } + } + None => target, + }; + } + d.last = p; + } + + /// The keyboard's modifiers changed (also while idle, for the next drag). + pub fn set_mods(&mut self, mods: Mods) { + let Selection::Dragging(d) = self else { return }; + if mods.shift && !d.mods.shift { + d.lock = Some((d.end, None)); + } else if !mods.shift { + d.lock = None; + } + if d.mods.space && !mods.space { + // Resize again from where the corner is, not where the pointer is. + d.offset = (d.end.x - d.last.x, d.end.y - d.last.y); + } + d.mods = mods; + } + + /// The selection's corners in global logical coordinates. + pub fn corners(&self) -> Option<(Point, Point)> { + let Selection::Dragging(d) = self else { + return None; + }; + if d.mods.alt { + let opposite = Point { + x: 2.0 * d.start.x - d.end.x, + y: 2.0 * d.start.y - d.end.y, + }; + Some((opposite, d.end)) + } else { + Some((d.start, d.end)) } } - /// Ends the drag and returns its start and end. Back to `Idle` either way. + /// Ends the drag at `p` and returns its corners. Back to `Idle` either way. pub fn release(&mut self, p: Point) -> Option<(Point, Point)> { - match std::mem::take(self) { - Selection::Dragging { start, .. } => Some((start, p)), - Selection::Idle => None, - } + self.motion(p); + let corners = self.corners(); + *self = Selection::Idle; + corners } /// Space switches to window mode only before a drag starts. @@ -211,17 +316,133 @@ mod tests { s.press(p(1.0, 2.0)); s.motion(p(5.0, 6.0)); - assert_eq!( - s, - Selection::Dragging { - start: p(1.0, 2.0), - end: p(5.0, 6.0) - } - ); + assert_eq!(s.corners(), Some((p(1.0, 2.0), p(5.0, 6.0)))); assert_eq!(s.release(p(7.0, 8.0)), Some((p(1.0, 2.0), p(7.0, 8.0)))); assert_eq!(s, Selection::Idle); s.motion(p(9.0, 9.0)); assert_eq!(s, Selection::Idle); } + + fn pt(x: f64, y: f64) -> Point { + Point { x, y } + } + + const SHIFT: Mods = Mods { + shift: true, + alt: false, + space: false, + }; + const ALT: Mods = Mods { + shift: false, + alt: true, + space: false, + }; + const SPACE: Mods = Mods { + shift: false, + alt: false, + space: true, + }; + + #[test] + fn plain_drag_is_press_to_pointer() { + let mut s = Selection::default(); + s.press(pt(10.0, 10.0)); + s.motion(pt(50.0, 30.0)); + assert_eq!(s.corners(), Some((pt(10.0, 10.0), pt(50.0, 30.0)))); + } + + #[test] + fn shift_locks_the_dimension_that_moves_first() { + let mut s = Selection::default(); + s.press(pt(0.0, 0.0)); + s.motion(pt(40.0, 30.0)); + s.set_mods(SHIFT); + s.motion(pt(60.0, 32.0)); // mostly horizontal: width follows + assert_eq!(s.corners(), Some((pt(0.0, 0.0), pt(60.0, 30.0)))); + s.motion(pt(70.0, 90.0)); // the axis stays decided + assert_eq!(s.corners(), Some((pt(0.0, 0.0), pt(70.0, 30.0)))); + s.set_mods(Mods::default()); + s.motion(pt(70.0, 90.0)); + assert_eq!( + s.corners(), + Some((pt(0.0, 0.0), pt(70.0, 90.0))), + "plain again" + ); + + let mut v = Selection::default(); + v.press(pt(0.0, 0.0)); + v.motion(pt(40.0, 30.0)); + v.set_mods(SHIFT); + v.motion(pt(41.0, 80.0)); // mostly vertical: height follows + assert_eq!(v.corners(), Some((pt(0.0, 0.0), pt(40.0, 80.0)))); + } + + #[test] + fn alt_grows_from_the_centre() { + let mut s = Selection::default(); + s.press(pt(100.0, 100.0)); + s.set_mods(ALT); + s.motion(pt(130.0, 120.0)); + assert_eq!(s.corners(), Some((pt(70.0, 80.0), pt(130.0, 120.0)))); + } + + #[test] + fn shift_and_alt_combine() { + let mut s = Selection::default(); + s.press(pt(100.0, 100.0)); + s.motion(pt(120.0, 110.0)); + s.set_mods(Mods { + shift: true, + alt: true, + space: false, + }); + s.motion(pt(150.0, 112.0)); + assert_eq!(s.corners(), Some((pt(50.0, 90.0), pt(150.0, 110.0)))); + } + + #[test] + fn space_moves_then_resizing_continues_without_a_jump() { + let mut s = Selection::default(); + s.press(pt(0.0, 0.0)); + s.motion(pt(40.0, 30.0)); + s.set_mods(SPACE); + s.motion(pt(50.0, 40.0)); + assert_eq!(s.corners(), Some((pt(10.0, 10.0), pt(50.0, 40.0)))); + s.motion(pt(60.0, 40.0)); + assert_eq!(s.corners(), Some((pt(20.0, 10.0), pt(60.0, 40.0)))); + s.set_mods(Mods::default()); + s.motion(pt(60.0, 40.0)); + assert_eq!( + s.corners(), + Some((pt(20.0, 10.0), pt(60.0, 40.0))), + "no jump on release" + ); + s.motion(pt(70.0, 45.0)); + assert_eq!(s.corners(), Some((pt(20.0, 10.0), pt(70.0, 45.0)))); + } + + #[test] + fn space_with_a_pointer_away_from_the_corner_keeps_the_offset() { + let mut s = Selection::default(); + s.press(pt(0.0, 0.0)); + s.motion(pt(40.0, 30.0)); + s.set_mods(SPACE); + s.motion(pt(45.0, 30.0)); + s.set_mods(Mods::default()); + s.motion(pt(55.0, 40.0)); + assert_eq!(s.corners(), Some((pt(5.0, 0.0), pt(55.0, 40.0)))); + } + + #[test] + fn release_applies_the_held_modifiers() { + let mut s = Selection::default(); + s.press(pt(100.0, 100.0)); + s.set_mods(ALT); + assert_eq!( + s.release(pt(110.0, 105.0)), + Some((pt(90.0, 95.0), pt(110.0, 105.0))) + ); + assert_eq!(s, Selection::Idle); + } } diff --git a/src/wayland.rs b/src/wayland.rs index 19b1fd8..d9f4864 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -483,7 +483,7 @@ impl KeyboardHandler for State { if let Some(overlay) = &mut self.overlay { match event.keysym { Keysym::Escape => overlay.cancel(), - Keysym::space => overlay.switch_to_window(), + Keysym::space => overlay.space(true, qh), _ => {} } } @@ -508,17 +508,22 @@ impl KeyboardHandler for State { fn release_key( &mut self, _: &Connection, - _: &QueueHandle, + qh: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: u32, - _: KeyEvent, + event: KeyEvent, ) { + if event.keysym == Keysym::space + && let Some(overlay) = &mut self.overlay + { + overlay.space(false, qh); + } } fn update_modifiers( &mut self, _: &Connection, - _: &QueueHandle, + qh: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: u32, modifiers: Modifiers, @@ -528,6 +533,9 @@ impl KeyboardHandler for State { if let Some(zoom) = &mut self.zoom { zoom.modifiers(modifiers); } + if let Some(overlay) = &mut self.overlay { + overlay.modifiers(modifiers, qh); + } } }