region: a selection across monitors, stitched into one image
This commit is contained in:
1 parent
3495b42afa
commit
857c1f0b65
4 files changed
+155
-45
No files matched your search
@@ -147,6 +147,8 @@ image, a cancel brought the thumbnail back, and a swipe still dismissed it.
|
|||||||
- [ ] Each shot is a two-hit "ka-chunk" (macOS-like) with a metallic clatter that grows from shot 2; shots in quick succession play sounds 1 → 5 getting faster and brighter, then 6, 7, 6, 7 (a climbing bell arpeggio over a glide) rising endlessly; every shot about as loud; after a 5 s pause it starts at 1 again. `valw __combo-demo` plays the whole sequence for tuning.
|
- [ ] Each shot is a two-hit "ka-chunk" (macOS-like) with a metallic clatter that grows from shot 2; shots in quick succession play sounds 1 → 5 getting faster and brighter, then 6, 7, 6, 7 (a climbing bell arpeggio over a glide) rising endlessly; every shot about as loud; after a 5 s pause it starts at 1 again. `valw __combo-demo` plays the whole sequence for tuning.
|
||||||
- [ ] `[sound] enabled = false`, `volume`, `combo_reset_secs` take effect; the toolbar's Play sound option overrides the config; no `pw-play` → no sound, capture still works (warning in the log).
|
- [ ] `[sound] enabled = false`, `volume`, `combo_reset_secs` take effect; the toolbar's Play sound option overrides the config; no `pw-play` → no sound, capture still works (warning in the log).
|
||||||
|
|
||||||
|
- [ ] Region across monitors: drag from the laptop up onto the monitor and back; each screen shows its part, the label shows the whole size on the screen under the pointer; Shift, Alt and Space work across the edge; the PNG is the whole rectangle, seams exact, parts over no screen transparent; the preview appears on the screen where you let go; a box on one screen is exactly as before.
|
||||||
|
|
||||||
### Known minors (deferred from the polish review)
|
### Known minors (deferred from the polish review)
|
||||||
|
|
||||||
- Releasing Shift mid-drag only updates the rectangle on the next pointer motion.
|
- Releasing Shift mid-drag only updates the rectangle on the next pointer motion.
|
||||||
|
|||||||
@@ -310,6 +310,10 @@ fn capture(mode: Mode, common: Common) -> Result<()> {
|
|||||||
let source = frames[i].output.name.clone();
|
let source = frames[i].output.name.clone();
|
||||||
(vec![(frames[i].to_rgba(r), None)], 0, source)
|
(vec![(frames[i].to_rgba(r), None)], 0, source)
|
||||||
}
|
}
|
||||||
|
region::Choice::Span(span, on) => {
|
||||||
|
let source = frames[on].output.name.clone();
|
||||||
|
(vec![(stitch::stitch(&frames, &span), None)], 0, source)
|
||||||
|
}
|
||||||
region::Choice::Window => {
|
region::Choice::Window => {
|
||||||
drop(frames);
|
drop(frames);
|
||||||
windowed = true;
|
windowed = true;
|
||||||
|
|||||||
+144
-30
@@ -38,6 +38,9 @@ pub const NAMESPACE: &str = "valw-overlay";
|
|||||||
pub enum Choice {
|
pub enum Choice {
|
||||||
/// A rectangle in the frame of output `.0`.
|
/// A rectangle in the frame of output `.0`.
|
||||||
Region(usize, PixelRect),
|
Region(usize, PixelRect),
|
||||||
|
/// A rectangle over several outputs (or past one's edge), with the
|
||||||
|
/// output the pointer was over at release (for the preview).
|
||||||
|
Span(selection::Span, usize),
|
||||||
/// Space before dragging: capture a window instead.
|
/// Space before dragging: capture a window instead.
|
||||||
Window,
|
Window,
|
||||||
}
|
}
|
||||||
@@ -46,12 +49,12 @@ pub enum Choice {
|
|||||||
pub struct Overlay {
|
pub struct Overlay {
|
||||||
surfaces: Vec<Surface>,
|
surfaces: Vec<Surface>,
|
||||||
selection: Selection,
|
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.
|
/// Shift, Alt and Space as the keyboard last reported them.
|
||||||
mods: Mods,
|
mods: Mods,
|
||||||
/// The surface the pointer is on and its surface-local position.
|
/// The pointer in global logical coordinates. A drag's events keep
|
||||||
pointer: Option<(usize, (f64, f64))>,
|
/// arriving on the surface it started on, with positions past its
|
||||||
|
/// edges, so this stays right across outputs.
|
||||||
|
pointer: Option<Point>,
|
||||||
outcome: Option<Option<Choice>>,
|
outcome: Option<Option<Choice>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +71,9 @@ struct Surface {
|
|||||||
configured: bool,
|
configured: bool,
|
||||||
frame_pending: bool,
|
frame_pending: bool,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
|
/// The selection part and whether the label were last drawn here.
|
||||||
|
shown: Option<PixelRect>,
|
||||||
|
labelled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the overlay on every output and waits for a selection.
|
/// Shows the overlay on every output and waits for a selection.
|
||||||
@@ -114,7 +120,6 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<
|
|||||||
wl.state.overlay = Some(Overlay {
|
wl.state.overlay = Some(Overlay {
|
||||||
surfaces,
|
surfaces,
|
||||||
selection: Selection::default(),
|
selection: Selection::default(),
|
||||||
anchor: None,
|
|
||||||
mods: Mods::default(),
|
mods: Mods::default(),
|
||||||
pointer: None,
|
pointer: None,
|
||||||
outcome: None,
|
outcome: None,
|
||||||
@@ -127,6 +132,21 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<
|
|||||||
outcome.flatten().ok_or_else(|| Cancelled.into())
|
outcome.flatten().ok_or_else(|| Cancelled.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a finished drag from `a` to `b` captures: one output's crop as
|
||||||
|
/// before, or the whole rectangle to stitch; `None` for a click.
|
||||||
|
fn choose(
|
||||||
|
a: Point,
|
||||||
|
b: Point,
|
||||||
|
outputs: &[(&OutputGeom, (u32, u32))],
|
||||||
|
pointer: Point,
|
||||||
|
) -> Option<Choice> {
|
||||||
|
let span = selection::span(a, b, outputs)?;
|
||||||
|
Some(match span.single() {
|
||||||
|
Some((i, rect)) => Choice::Region(i, rect),
|
||||||
|
None => Choice::Span(span, selection::locate(pointer, outputs)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl Surface {
|
impl Surface {
|
||||||
fn new(
|
fn new(
|
||||||
layer: LayerSurface,
|
layer: LayerSurface,
|
||||||
@@ -151,6 +171,8 @@ impl Surface {
|
|||||||
configured: false,
|
configured: false,
|
||||||
frame_pending: false,
|
frame_pending: false,
|
||||||
dirty: true,
|
dirty: true,
|
||||||
|
shown: None,
|
||||||
|
labelled: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,9 +249,7 @@ impl Overlay {
|
|||||||
|
|
||||||
fn apply_mods(&mut self, qh: &QueueHandle<State>) {
|
fn apply_mods(&mut self, qh: &QueueHandle<State>) {
|
||||||
self.selection.set_mods(self.mods);
|
self.selection.set_mods(self.mods);
|
||||||
if let Some(a) = self.anchor {
|
self.redraw_changed(qh);
|
||||||
self.redraw(a, qh);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn switch_to_window(&mut self) {
|
pub fn switch_to_window(&mut self) {
|
||||||
@@ -245,26 +265,64 @@ impl Overlay {
|
|||||||
.position(|s| s.layer.wl_surface() == surface)
|
.position(|s| s.layer.wl_surface() == surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The selection as drawn on surface `i`: only the anchor output shows it.
|
fn outputs(&self) -> Vec<(&OutputGeom, (u32, u32))> {
|
||||||
|
self.surfaces
|
||||||
|
.iter()
|
||||||
|
.map(|s| (&s.geom, (s.width, s.height)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selection's part on surface `i`.
|
||||||
fn selection_on(&self, i: usize) -> Option<PixelRect> {
|
fn selection_on(&self, i: usize) -> Option<PixelRect> {
|
||||||
let (start, end) = self.selection.corners()?;
|
let (start, end) = self.selection.corners()?;
|
||||||
let s = &self.surfaces[i];
|
let s = &self.surfaces[i];
|
||||||
(self.anchor == Some(i))
|
selection::clip(start, end, &s.geom, s.width, s.height)
|
||||||
.then(|| selection::clip(start, end, &s.geom, s.width, s.height))
|
}
|
||||||
.flatten()
|
|
||||||
|
/// The size label on surface `i`: the whole shot's size next to the
|
||||||
|
/// pointer, on the output the pointer is over.
|
||||||
|
fn label_on(&self, i: usize) -> Option<(PixelRect, (f64, f64))> {
|
||||||
|
let (start, end) = self.selection.corners()?;
|
||||||
|
let p = self.pointer?;
|
||||||
|
let outputs = self.outputs();
|
||||||
|
if selection::locate(p, &outputs) != i {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (width, height) = selection::span(start, end, &outputs)?.size;
|
||||||
|
let g = &self.surfaces[i].geom;
|
||||||
|
Some((
|
||||||
|
PixelRect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
(p.x - g.x as f64, p.y - g.y as f64),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw(&mut self, i: usize, qh: &QueueHandle<State>) {
|
fn redraw(&mut self, i: usize, qh: &QueueHandle<State>) {
|
||||||
let sel = self.selection_on(i);
|
let sel = self.selection_on(i);
|
||||||
// The size label goes next to the pointer, on the output it is on.
|
let label = self.label_on(i);
|
||||||
let label = sel.zip(self.pointer.filter(|(on, _)| *on == i).map(|(_, at)| at));
|
|
||||||
let s = &mut self.surfaces[i];
|
let s = &mut self.surfaces[i];
|
||||||
|
s.shown = sel;
|
||||||
|
s.labelled = label.is_some();
|
||||||
s.dirty = true;
|
s.dirty = true;
|
||||||
if s.configured && !s.frame_pending {
|
if s.configured && !s.frame_pending {
|
||||||
s.draw(sel, label, qh);
|
s.draw(sel, label, qh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redraws the surfaces whose part of the selection or label changed.
|
||||||
|
fn redraw_changed(&mut self, qh: &QueueHandle<State>) {
|
||||||
|
for i in 0..self.surfaces.len() {
|
||||||
|
let s = &self.surfaces[i];
|
||||||
|
if self.selection_on(i) != s.shown || s.labelled || self.label_on(i).is_some() {
|
||||||
|
self.redraw(i, qh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn configure(&mut self, layer: &LayerSurface, (w, h): (u32, u32), qh: &QueueHandle<State>) {
|
pub fn configure(&mut self, layer: &LayerSurface, (w, h): (u32, u32), qh: &QueueHandle<State>) {
|
||||||
let Some(i) = self.index_of(layer.wl_surface()) else {
|
let Some(i) = self.index_of(layer.wl_surface()) else {
|
||||||
return;
|
return;
|
||||||
@@ -303,12 +361,12 @@ impl Overlay {
|
|||||||
let Some(i) = self.index_of(&event.surface) else {
|
let Some(i) = self.index_of(&event.surface) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
self.pointer = Some((i, event.position));
|
|
||||||
let g = &self.surfaces[i].geom;
|
let g = &self.surfaces[i].geom;
|
||||||
let p = Point {
|
let p = Point {
|
||||||
x: g.x as f64 + event.position.0,
|
x: g.x as f64 + event.position.0,
|
||||||
y: g.y as f64 + event.position.1,
|
y: g.y as f64 + event.position.1,
|
||||||
};
|
};
|
||||||
|
self.pointer = Some(p);
|
||||||
match event.kind {
|
match event.kind {
|
||||||
PointerEventKind::Enter { serial } => {
|
PointerEventKind::Enter { serial } => {
|
||||||
if let Some(cursor) = cursor {
|
if let Some(cursor) = cursor {
|
||||||
@@ -320,35 +378,29 @@ impl Overlay {
|
|||||||
} => {
|
} => {
|
||||||
// Modifiers already held apply from the start.
|
// Modifiers already held apply from the start.
|
||||||
self.selection.press_with(p, self.mods);
|
self.selection.press_with(p, self.mods);
|
||||||
self.anchor = Some(i);
|
self.redraw_changed(qh);
|
||||||
}
|
}
|
||||||
PointerEventKind::Press {
|
PointerEventKind::Press {
|
||||||
button: BTN_RIGHT, ..
|
button: BTN_RIGHT, ..
|
||||||
} => self.cancel(),
|
} => self.cancel(),
|
||||||
PointerEventKind::Motion { .. } => {
|
PointerEventKind::Motion { .. } => {
|
||||||
self.selection.motion(p);
|
self.selection.motion(p);
|
||||||
if let Some(a) = self.anchor {
|
self.redraw_changed(qh);
|
||||||
self.redraw(a, qh);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
PointerEventKind::Release {
|
PointerEventKind::Release {
|
||||||
button: BTN_LEFT, ..
|
button: BTN_LEFT, ..
|
||||||
} => {
|
} => {
|
||||||
let (Some((start, end)), Some(a)) = (self.selection.release(p), self.anchor)
|
let Some((start, end)) = self.selection.release(p) else {
|
||||||
else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let s = &self.surfaces[a];
|
let choice = choose(start, end, &self.outputs(), p);
|
||||||
match selection::resolve(start, end, &s.geom, s.width, s.height) {
|
match choice {
|
||||||
Some(rect) => {
|
Some(choice) => {
|
||||||
tracing::info!(
|
tracing::info!("selected {choice:?} (logical {start:?} to {end:?})");
|
||||||
"selected {rect:?} on {} (logical {start:?} to {end:?})",
|
self.outcome = Some(Some(choice));
|
||||||
s.geom.name
|
|
||||||
);
|
|
||||||
self.outcome = Some(Some(Choice::Region(a, rect)));
|
|
||||||
}
|
}
|
||||||
// A click, not a drag: clear it and keep waiting.
|
// A click, not a drag: clear it and keep waiting.
|
||||||
None => self.redraw(a, qh),
|
None => self.redraw_changed(qh),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -356,3 +408,65 @@ impl Overlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn out(name: &str, x: i32, y: i32, width: i32, height: i32) -> OutputGeom {
|
||||||
|
OutputGeom {
|
||||||
|
name: name.into(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn p(x: f64, y: f64) -> Point {
|
||||||
|
Point { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_output_is_todays_region_and_several_a_span() {
|
||||||
|
let laptop = out("LVDS-1", 0, 0, 1366, 768);
|
||||||
|
let monitor = out("HDMI-A-1", -277, -1080, 1920, 1080);
|
||||||
|
let outputs = [(&laptop, (1366, 768)), (&monitor, (1920, 1080))];
|
||||||
|
assert_eq!(
|
||||||
|
choose(p(100.0, 100.0), p(300.0, 250.0), &outputs, p(300.0, 250.0)),
|
||||||
|
Some(Choice::Region(
|
||||||
|
0,
|
||||||
|
PixelRect {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 200,
|
||||||
|
height: 150
|
||||||
|
}
|
||||||
|
))
|
||||||
|
);
|
||||||
|
let across = choose(
|
||||||
|
p(100.0, 500.0),
|
||||||
|
p(600.0, -300.0),
|
||||||
|
&outputs,
|
||||||
|
p(600.0, -300.0),
|
||||||
|
);
|
||||||
|
let Some(Choice::Span(span, on)) = across else {
|
||||||
|
panic!("expected a span, got {across:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
(span.size, span.parts.len(), on),
|
||||||
|
((500, 800), 2, 1),
|
||||||
|
"preview on the monitor"
|
||||||
|
);
|
||||||
|
// A box touching only the laptop but reaching past its left edge.
|
||||||
|
assert!(matches!(
|
||||||
|
choose(p(-50.0, 100.0), p(200.0, 300.0), &outputs, p(200.0, 300.0)),
|
||||||
|
Some(Choice::Span(_, 0))
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
choose(p(10.0, 10.0), p(11.0, 30.0), &outputs, p(11.0, 30.0)),
|
||||||
|
None,
|
||||||
|
"a click"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-15
@@ -183,17 +183,6 @@ pub fn clip(a: Point, b: Point, out: &OutputGeom, frame_w: u32, frame_h: u32) ->
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`clip`], but treats anything under [`MIN_PIXELS`] as a click.
|
|
||||||
pub fn resolve(
|
|
||||||
a: Point,
|
|
||||||
b: Point,
|
|
||||||
out: &OutputGeom,
|
|
||||||
frame_w: u32,
|
|
||||||
frame_h: u32,
|
|
||||||
) -> Option<PixelRect> {
|
|
||||||
clip(a, b, out, frame_w, frame_h).filter(|r| r.width >= MIN_PIXELS && r.height >= MIN_PIXELS)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One output's share of a selection and where it goes in the whole image.
|
/// One output's share of a selection and where it goes in the whole image.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Part {
|
pub struct Part {
|
||||||
@@ -412,11 +401,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn click_is_not_a_selection() {
|
fn click_is_not_a_selection() {
|
||||||
let o = out("A", 0, 0, 1920, 1080);
|
let o = out("A", 0, 0, 1920, 1080);
|
||||||
assert_eq!(resolve(p(10.0, 10.0), p(10.0, 10.0), &o, 1920, 1080), None);
|
let outputs = [(&o, (1920, 1080))];
|
||||||
assert_eq!(resolve(p(10.0, 10.0), p(11.0, 30.0), &o, 1920, 1080), None);
|
assert_eq!(span(p(10.0, 10.0), p(10.0, 10.0), &outputs), None);
|
||||||
|
assert_eq!(span(p(10.0, 10.0), p(11.0, 30.0), &outputs), None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(p(10.0, 10.0), p(12.0, 12.0), &o, 1920, 1080),
|
span(p(10.0, 10.0), p(12.0, 12.0), &outputs).and_then(|s| s.single()),
|
||||||
Some(rect(10, 10, 2, 2))
|
Some((0, rect(10, 10, 2, 2)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user