feat: Shift, Alt and Space while selecting a region

This commit is contained in:
elwrcl committed 2026-10-03 02:31:23 +03:00
1 parent 7f9782dc97
commit e0b5d0e9f5
3 files changed
+287 -28

No files matched your search

+35 -5
View File
@@ -3,7 +3,10 @@ use std::sync::Arc;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use smithay_client_toolkit::{ use smithay_client_toolkit::{
compositor::FrameCallbackData, compositor::FrameCallbackData,
seat::pointer::{BTN_LEFT, BTN_RIGHT, PointerEvent, PointerEventKind}, seat::{
keyboard::Modifiers,
pointer::{BTN_LEFT, BTN_RIGHT, PointerEvent, PointerEventKind},
},
shell::{ shell::{
WaylandSurface, WaylandSurface,
wlr_layer::{Anchor, KeyboardInteractivity, Layer, LayerSurface}, 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::error::{Cancelled, HintExt};
use crate::frame::{Bgrx, Frame, OutputGeom, PixelRect}; use crate::frame::{Bgrx, Frame, OutputGeom, PixelRect};
use crate::render; use crate::render;
use crate::selection::{self, Point, Selection}; use crate::selection::{self, Mods, Point, Selection};
use crate::wayland::{Output, State, Wayland}; use crate::wayland::{Output, State, Wayland};
pub const NAMESPACE: &str = "valw-overlay"; pub const NAMESPACE: &str = "valw-overlay";
@@ -45,6 +48,8 @@ pub struct Overlay {
selection: Selection, selection: Selection,
/// Output where the current drag started; the capture is clipped to it. /// Output where the current drag started; the capture is clipped to it.
anchor: Option<usize>, anchor: Option<usize>,
/// Shift, Alt and Space as the keyboard last reported them.
mods: Mods,
outcome: Option<Option<Choice>>, outcome: Option<Option<Choice>>,
} }
@@ -106,6 +111,7 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<
surfaces, surfaces,
selection: Selection::default(), selection: Selection::default(),
anchor: None, anchor: None,
mods: Mods::default(),
outcome: None, outcome: None,
}); });
let result = wl.dispatch_blocking(|s| s.overlay.as_ref().is_some_and(|o| o.outcome.is_some())); 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); self.outcome.get_or_insert(None);
} }
pub fn modifiers(&mut self, m: Modifiers, qh: &QueueHandle<State>) {
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<State>) {
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<State>) {
self.selection.set_mods(self.mods);
if let Some(a) = self.anchor {
self.redraw(a, qh);
}
}
pub fn switch_to_window(&mut self) { pub fn switch_to_window(&mut self) {
if self.selection.allows_window_switch() { if self.selection.allows_window_switch() {
tracing::info!("switching to window mode"); 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. /// The selection as drawn on surface `i`: only the anchor output shows it.
fn selection_on(&self, i: usize) -> Option<PixelRect> { fn selection_on(&self, i: usize) -> Option<PixelRect> {
let Selection::Dragging { start, end } = self.selection else { let (start, end) = self.selection.corners()?;
return None;
};
let s = &self.surfaces[i]; let s = &self.surfaces[i];
(self.anchor == Some(i)) (self.anchor == Some(i))
.then(|| selection::clip(start, end, &s.geom, s.width, s.height)) .then(|| selection::clip(start, end, &s.geom, s.width, s.height))
@@ -255,6 +283,8 @@ impl Overlay {
button: BTN_LEFT, .. button: BTN_LEFT, ..
} => { } => {
self.selection.press(p); self.selection.press(p);
// Modifiers already held apply from the start.
self.selection.set_mods(self.mods);
self.anchor = Some(i); self.anchor = Some(i);
} }
PointerEventKind::Press { PointerEventKind::Press {
+240 -19
View File
@@ -10,33 +10,138 @@ pub struct Point {
pub y: f64, 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<Axis>)>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq)] #[derive(Debug, Clone, Copy, Default, PartialEq)]
pub enum Selection { pub enum Selection {
#[default] #[default]
Idle, Idle,
Dragging { Dragging(Drag),
start: Point,
end: Point,
},
} }
impl Selection { impl Selection {
pub fn press(&mut self, p: Point) { 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) { pub fn motion(&mut self, p: Point) {
if let Selection::Dragging { end, .. } = self { let Selection::Dragging(d) = self else { return };
*end = p; 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)> { pub fn release(&mut self, p: Point) -> Option<(Point, Point)> {
match std::mem::take(self) { self.motion(p);
Selection::Dragging { start, .. } => Some((start, p)), let corners = self.corners();
Selection::Idle => None, *self = Selection::Idle;
} corners
} }
/// Space switches to window mode only before a drag starts. /// Space switches to window mode only before a drag starts.
@@ -211,17 +316,133 @@ mod tests {
s.press(p(1.0, 2.0)); s.press(p(1.0, 2.0));
s.motion(p(5.0, 6.0)); s.motion(p(5.0, 6.0));
assert_eq!( assert_eq!(s.corners(), Some((p(1.0, 2.0), p(5.0, 6.0))));
s,
Selection::Dragging {
start: p(1.0, 2.0),
end: 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.release(p(7.0, 8.0)), Some((p(1.0, 2.0), p(7.0, 8.0))));
assert_eq!(s, Selection::Idle); assert_eq!(s, Selection::Idle);
s.motion(p(9.0, 9.0)); s.motion(p(9.0, 9.0));
assert_eq!(s, Selection::Idle); 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);
}
} }
+12 -4
View File
@@ -483,7 +483,7 @@ impl KeyboardHandler for State {
if let Some(overlay) = &mut self.overlay { if let Some(overlay) = &mut self.overlay {
match event.keysym { match event.keysym {
Keysym::Escape => overlay.cancel(), 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( fn release_key(
&mut self, &mut self,
_: &Connection, _: &Connection,
_: &QueueHandle<Self>, qh: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard, _: &wl_keyboard::WlKeyboard,
_: u32, _: u32,
_: KeyEvent, event: KeyEvent,
) { ) {
if event.keysym == Keysym::space
&& let Some(overlay) = &mut self.overlay
{
overlay.space(false, qh);
}
} }
fn update_modifiers( fn update_modifiers(
&mut self, &mut self,
_: &Connection, _: &Connection,
_: &QueueHandle<Self>, qh: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard, _: &wl_keyboard::WlKeyboard,
_: u32, _: u32,
modifiers: Modifiers, modifiers: Modifiers,
@@ -528,6 +533,9 @@ impl KeyboardHandler for State {
if let Some(zoom) = &mut self.zoom { if let Some(zoom) = &mut self.zoom {
zoom.modifiers(modifiers); zoom.modifiers(modifiers);
} }
if let Some(overlay) = &mut self.overlay {
overlay.modifiers(modifiers, qh);
}
} }
} }