feat: Shift, Alt and Space while selecting a region
This commit is contained in:
1 parent
7f9782dc97
commit
e0b5d0e9f5
3 files changed
+287
-28
No files matched your search
+35
-5
@@ -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<usize>,
|
||||
/// Shift, Alt and Space as the keyboard last reported them.
|
||||
mods: Mods,
|
||||
outcome: Option<Option<Choice>>,
|
||||
}
|
||||
|
||||
@@ -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<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) {
|
||||
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<PixelRect> {
|
||||
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 {
|
||||
|
||||
+240
-19
@@ -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<Axis>)>,
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -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<Self>,
|
||||
qh: &QueueHandle<Self>,
|
||||
_: &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<Self>,
|
||||
qh: &QueueHandle<Self>,
|
||||
_: &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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user