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.
|
||||
- [ ] `[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)
|
||||
|
||||
- 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();
|
||||
(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 => {
|
||||
drop(frames);
|
||||
windowed = true;
|
||||
|
||||
+144
-30
@@ -38,6 +38,9 @@ pub const NAMESPACE: &str = "valw-overlay";
|
||||
pub enum Choice {
|
||||
/// A rectangle in the frame of output `.0`.
|
||||
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.
|
||||
Window,
|
||||
}
|
||||
@@ -46,12 +49,12 @@ pub enum Choice {
|
||||
pub struct Overlay {
|
||||
surfaces: Vec<Surface>,
|
||||
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,
|
||||
/// The surface the pointer is on and its surface-local position.
|
||||
pointer: Option<(usize, (f64, f64))>,
|
||||
/// The pointer in global logical coordinates. A drag's events keep
|
||||
/// 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>>,
|
||||
}
|
||||
|
||||
@@ -68,6 +71,9 @@ struct Surface {
|
||||
configured: bool,
|
||||
frame_pending: 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.
|
||||
@@ -114,7 +120,6 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<
|
||||
wl.state.overlay = Some(Overlay {
|
||||
surfaces,
|
||||
selection: Selection::default(),
|
||||
anchor: None,
|
||||
mods: Mods::default(),
|
||||
pointer: 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())
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
fn new(
|
||||
layer: LayerSurface,
|
||||
@@ -151,6 +171,8 @@ impl Surface {
|
||||
configured: false,
|
||||
frame_pending: false,
|
||||
dirty: true,
|
||||
shown: None,
|
||||
labelled: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -227,9 +249,7 @@ impl Overlay {
|
||||
|
||||
fn apply_mods(&mut self, qh: &QueueHandle<State>) {
|
||||
self.selection.set_mods(self.mods);
|
||||
if let Some(a) = self.anchor {
|
||||
self.redraw(a, qh);
|
||||
}
|
||||
self.redraw_changed(qh);
|
||||
}
|
||||
|
||||
pub fn switch_to_window(&mut self) {
|
||||
@@ -245,26 +265,64 @@ impl Overlay {
|
||||
.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> {
|
||||
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))
|
||||
.flatten()
|
||||
selection::clip(start, end, &s.geom, s.width, s.height)
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
let sel = self.selection_on(i);
|
||||
// The size label goes next to the pointer, on the output it is on.
|
||||
let label = sel.zip(self.pointer.filter(|(on, _)| *on == i).map(|(_, at)| at));
|
||||
let label = self.label_on(i);
|
||||
let s = &mut self.surfaces[i];
|
||||
s.shown = sel;
|
||||
s.labelled = label.is_some();
|
||||
s.dirty = true;
|
||||
if s.configured && !s.frame_pending {
|
||||
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>) {
|
||||
let Some(i) = self.index_of(layer.wl_surface()) else {
|
||||
return;
|
||||
@@ -303,12 +361,12 @@ impl Overlay {
|
||||
let Some(i) = self.index_of(&event.surface) else {
|
||||
continue;
|
||||
};
|
||||
self.pointer = Some((i, event.position));
|
||||
let g = &self.surfaces[i].geom;
|
||||
let p = Point {
|
||||
x: g.x as f64 + event.position.0,
|
||||
y: g.y as f64 + event.position.1,
|
||||
};
|
||||
self.pointer = Some(p);
|
||||
match event.kind {
|
||||
PointerEventKind::Enter { serial } => {
|
||||
if let Some(cursor) = cursor {
|
||||
@@ -320,35 +378,29 @@ impl Overlay {
|
||||
} => {
|
||||
// Modifiers already held apply from the start.
|
||||
self.selection.press_with(p, self.mods);
|
||||
self.anchor = Some(i);
|
||||
self.redraw_changed(qh);
|
||||
}
|
||||
PointerEventKind::Press {
|
||||
button: BTN_RIGHT, ..
|
||||
} => self.cancel(),
|
||||
PointerEventKind::Motion { .. } => {
|
||||
self.selection.motion(p);
|
||||
if let Some(a) = self.anchor {
|
||||
self.redraw(a, qh);
|
||||
}
|
||||
self.redraw_changed(qh);
|
||||
}
|
||||
PointerEventKind::Release {
|
||||
button: BTN_LEFT, ..
|
||||
} => {
|
||||
let (Some((start, end)), Some(a)) = (self.selection.release(p), self.anchor)
|
||||
else {
|
||||
let Some((start, end)) = self.selection.release(p) else {
|
||||
continue;
|
||||
};
|
||||
let s = &self.surfaces[a];
|
||||
match selection::resolve(start, end, &s.geom, s.width, s.height) {
|
||||
Some(rect) => {
|
||||
tracing::info!(
|
||||
"selected {rect:?} on {} (logical {start:?} to {end:?})",
|
||||
s.geom.name
|
||||
);
|
||||
self.outcome = Some(Some(Choice::Region(a, rect)));
|
||||
let choice = choose(start, end, &self.outputs(), p);
|
||||
match choice {
|
||||
Some(choice) => {
|
||||
tracing::info!("selected {choice:?} (logical {start:?} to {end:?})");
|
||||
self.outcome = Some(Some(choice));
|
||||
}
|
||||
// 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.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Part {
|
||||
@@ -412,11 +401,12 @@ mod tests {
|
||||
#[test]
|
||||
fn click_is_not_a_selection() {
|
||||
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);
|
||||
assert_eq!(resolve(p(10.0, 10.0), p(11.0, 30.0), &o, 1920, 1080), None);
|
||||
let outputs = [(&o, (1920, 1080))];
|
||||
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!(
|
||||
resolve(p(10.0, 10.0), p(12.0, 12.0), &o, 1920, 1080),
|
||||
Some(rect(10, 10, 2, 2))
|
||||
span(p(10.0, 10.0), p(12.0, 12.0), &outputs).and_then(|s| s.single()),
|
||||
Some((0, rect(10, 10, 2, 2)))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user