feat: valw zoom
This commit is contained in:
1 parent
c1b8e5cabe
commit
b9b8c6defc
3 files changed
+385
-12
No files matched your search
+23
-3
@@ -1,6 +1,3 @@
|
|||||||
// Zoom lands in pieces; Task 3 removes this.
|
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
mod capture;
|
mod capture;
|
||||||
mod config;
|
mod config;
|
||||||
mod detach;
|
mod detach;
|
||||||
@@ -66,6 +63,11 @@ enum Command {
|
|||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
common: Common,
|
common: Common,
|
||||||
},
|
},
|
||||||
|
/// Freeze the screen and zoom in: wheel, drag, f, c, 0, Esc.
|
||||||
|
Zoom {
|
||||||
|
#[command(flatten)]
|
||||||
|
common: Common,
|
||||||
|
},
|
||||||
/// Report what the compositor and system support.
|
/// Report what the compositor and system support.
|
||||||
Doctor,
|
Doctor,
|
||||||
/// Internal: the process that shows preview thumbnails.
|
/// Internal: the process that shows preview thumbnails.
|
||||||
@@ -106,6 +108,7 @@ enum Mode {
|
|||||||
Screen { all: bool },
|
Screen { all: bool },
|
||||||
Region,
|
Region,
|
||||||
Window,
|
Window,
|
||||||
|
Zoom,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -139,6 +142,7 @@ fn run(cli: Cli) -> Result<()> {
|
|||||||
Command::Screen { all, common } => capture(Mode::Screen { all }, common),
|
Command::Screen { all, common } => capture(Mode::Screen { all }, common),
|
||||||
Command::Region { common } => capture(Mode::Region, common),
|
Command::Region { common } => capture(Mode::Region, common),
|
||||||
Command::Window { common } => capture(Mode::Window, common),
|
Command::Window { common } => capture(Mode::Window, common),
|
||||||
|
Command::Zoom { common } => capture(Mode::Zoom, common),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +192,20 @@ fn capture(mode: Mode, common: Common) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode::Window => window_shot(&outputs, cursor)?,
|
Mode::Window => window_shot(&outputs, cursor)?,
|
||||||
|
Mode::Zoom => {
|
||||||
|
let focused = focused_output(&outputs);
|
||||||
|
let frame = wl.capture(&outputs[focused..=focused], cursor)?.remove(0);
|
||||||
|
match zoom::run(&mut wl, &outputs[focused], &frame, &config.zoom)? {
|
||||||
|
zoom::Outcome::Capture(r) => {
|
||||||
|
let source = frame.output.name.clone();
|
||||||
|
(vec![(frame.to_rgba(r), None)], 0, source)
|
||||||
|
}
|
||||||
|
zoom::Outcome::Leave => {
|
||||||
|
tracing::info!("left zoom without capturing");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
drop(wl);
|
drop(wl);
|
||||||
|
|
||||||
@@ -313,6 +331,8 @@ mod tests {
|
|||||||
assert!(parses(&["screen", "--all", "--no-preview"]));
|
assert!(parses(&["screen", "--all", "--no-preview"]));
|
||||||
assert!(parses(&["window", "--cursor", "--delay", "2"]));
|
assert!(parses(&["window", "--cursor", "--delay", "2"]));
|
||||||
assert!(!parses(&["window", "--clipboard-only", "-o", "a.png"]));
|
assert!(!parses(&["window", "--clipboard-only", "-o", "a.png"]));
|
||||||
|
assert!(parses(&["zoom", "--cursor", "--no-preview"]));
|
||||||
|
assert!(!parses(&["zoom", "--clipboard-only", "-o", "a.png"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+30
-9
@@ -42,6 +42,7 @@ use crate::error::HintExt;
|
|||||||
use crate::frame::OutputGeom;
|
use crate::frame::OutputGeom;
|
||||||
use crate::host::Host;
|
use crate::host::Host;
|
||||||
use crate::region::Overlay;
|
use crate::region::Overlay;
|
||||||
|
use crate::zoom::Zoom;
|
||||||
|
|
||||||
/// An output as valw sees it.
|
/// An output as valw sees it.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -80,6 +81,8 @@ pub struct State {
|
|||||||
pub captures: Vec<Pending>,
|
pub captures: Vec<Pending>,
|
||||||
/// The region selection overlay, while it is open.
|
/// The region selection overlay, while it is open.
|
||||||
pub overlay: Option<Overlay>,
|
pub overlay: Option<Overlay>,
|
||||||
|
/// Zoom mode, while it is open.
|
||||||
|
pub zoom: Option<Zoom>,
|
||||||
/// The preview thumbnails, in the preview host process.
|
/// The preview thumbnails, in the preview host process.
|
||||||
pub preview: Option<Host>,
|
pub preview: Option<Host>,
|
||||||
}
|
}
|
||||||
@@ -111,6 +114,7 @@ impl Wayland {
|
|||||||
pointer: None,
|
pointer: None,
|
||||||
captures: Vec::new(),
|
captures: Vec::new(),
|
||||||
overlay: None,
|
overlay: None,
|
||||||
|
zoom: None,
|
||||||
preview: None,
|
preview: None,
|
||||||
};
|
};
|
||||||
// Two round trips: one for wl_output, one for the xdg-output details.
|
// Two round trips: one for wl_output, one for the xdg-output details.
|
||||||
@@ -297,6 +301,9 @@ impl CompositorHandler for State {
|
|||||||
if let Some(overlay) = &mut self.overlay {
|
if let Some(overlay) = &mut self.overlay {
|
||||||
overlay.frame_done(surface, qh);
|
overlay.frame_done(surface, qh);
|
||||||
}
|
}
|
||||||
|
if let Some(zoom) = &mut self.zoom {
|
||||||
|
zoom.frame_done(surface, qh);
|
||||||
|
}
|
||||||
if let Some(preview) = &mut self.preview {
|
if let Some(preview) = &mut self.preview {
|
||||||
preview.frame_done(surface, qh);
|
preview.frame_done(surface, qh);
|
||||||
}
|
}
|
||||||
@@ -338,6 +345,9 @@ impl LayerShellHandler for State {
|
|||||||
if let Some(overlay) = &mut self.overlay {
|
if let Some(overlay) = &mut self.overlay {
|
||||||
overlay.cancel();
|
overlay.cancel();
|
||||||
}
|
}
|
||||||
|
if let Some(zoom) = &mut self.zoom {
|
||||||
|
zoom.closed(layer);
|
||||||
|
}
|
||||||
if let Some(preview) = &mut self.preview {
|
if let Some(preview) = &mut self.preview {
|
||||||
preview.closed(layer);
|
preview.closed(layer);
|
||||||
}
|
}
|
||||||
@@ -354,6 +364,9 @@ impl LayerShellHandler for State {
|
|||||||
if let Some(overlay) = &mut self.overlay {
|
if let Some(overlay) = &mut self.overlay {
|
||||||
overlay.configure(layer, configure.new_size, qh);
|
overlay.configure(layer, configure.new_size, qh);
|
||||||
}
|
}
|
||||||
|
if let Some(zoom) = &mut self.zoom {
|
||||||
|
zoom.configure(layer, configure.new_size, qh);
|
||||||
|
}
|
||||||
if let Some(preview) = &mut self.preview {
|
if let Some(preview) = &mut self.preview {
|
||||||
preview.configure(layer, qh);
|
preview.configure(layer, qh);
|
||||||
}
|
}
|
||||||
@@ -444,18 +457,20 @@ impl KeyboardHandler for State {
|
|||||||
fn press_key(
|
fn press_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Connection,
|
_: &Connection,
|
||||||
_: &QueueHandle<Self>,
|
qh: &QueueHandle<Self>,
|
||||||
_: &wl_keyboard::WlKeyboard,
|
_: &wl_keyboard::WlKeyboard,
|
||||||
_: u32,
|
_: u32,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
) {
|
) {
|
||||||
let Some(overlay) = &mut self.overlay else {
|
if let Some(overlay) = &mut self.overlay {
|
||||||
return;
|
match event.keysym {
|
||||||
};
|
Keysym::Escape => overlay.cancel(),
|
||||||
match event.keysym {
|
Keysym::space => overlay.switch_to_window(),
|
||||||
Keysym::Escape => overlay.cancel(),
|
_ => {}
|
||||||
Keysym::space => overlay.switch_to_window(),
|
}
|
||||||
_ => {}
|
}
|
||||||
|
if let Some(zoom) = &mut self.zoom {
|
||||||
|
zoom.key(event.keysym, qh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,10 +500,13 @@ impl KeyboardHandler for State {
|
|||||||
_: &QueueHandle<Self>,
|
_: &QueueHandle<Self>,
|
||||||
_: &wl_keyboard::WlKeyboard,
|
_: &wl_keyboard::WlKeyboard,
|
||||||
_: u32,
|
_: u32,
|
||||||
_: Modifiers,
|
modifiers: Modifiers,
|
||||||
_: RawModifiers,
|
_: RawModifiers,
|
||||||
_: u32,
|
_: u32,
|
||||||
) {
|
) {
|
||||||
|
if let Some(zoom) = &mut self.zoom {
|
||||||
|
zoom.modifiers(modifiers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +521,9 @@ impl PointerHandler for State {
|
|||||||
if let Some(overlay) = &mut self.overlay {
|
if let Some(overlay) = &mut self.overlay {
|
||||||
overlay.pointer(events, self.cursor_device.as_ref(), qh);
|
overlay.pointer(events, self.cursor_device.as_ref(), qh);
|
||||||
}
|
}
|
||||||
|
if let Some(zoom) = &mut self.zoom {
|
||||||
|
zoom.pointer(events, self.cursor_device.as_ref(), qh);
|
||||||
|
}
|
||||||
if self.preview.is_some() {
|
if self.preview.is_some() {
|
||||||
crate::host::pointer(self, events, qh);
|
crate::host::pointer(self, events, qh);
|
||||||
}
|
}
|
||||||
|
|||||||
+332
@@ -2,3 +2,335 @@
|
|||||||
|
|
||||||
pub mod gl;
|
pub mod gl;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
compositor::FrameCallbackData,
|
||||||
|
seat::{
|
||||||
|
keyboard::{Keysym, Modifiers},
|
||||||
|
pointer::{BTN_LEFT, PointerEvent, PointerEventKind},
|
||||||
|
},
|
||||||
|
shell::{
|
||||||
|
WaylandSurface,
|
||||||
|
wlr_layer::{Anchor, KeyboardInteractivity, Layer, LayerSurface},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use wayland_client::{QueueHandle, protocol::wl_surface::WlSurface};
|
||||||
|
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::{
|
||||||
|
Shape, WpCursorShapeDeviceV1,
|
||||||
|
};
|
||||||
|
use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
|
||||||
|
|
||||||
|
use self::view::{Animated, View};
|
||||||
|
use crate::config;
|
||||||
|
use crate::error::HintExt;
|
||||||
|
use crate::frame::{Frame, PixelRect};
|
||||||
|
use crate::wayland::{Output, State, Wayland};
|
||||||
|
|
||||||
|
pub const NAMESPACE: &str = "valw-zoom";
|
||||||
|
|
||||||
|
/// How a zoom session ended.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Outcome {
|
||||||
|
/// `c`: this part of the frame.
|
||||||
|
Capture(PixelRect),
|
||||||
|
/// Esc or `q`: nothing to save.
|
||||||
|
Leave,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Command {
|
||||||
|
Leave,
|
||||||
|
Capture,
|
||||||
|
Flashlight,
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command(key: Keysym) -> Option<Command> {
|
||||||
|
match key {
|
||||||
|
Keysym::Escape | Keysym::q => Some(Command::Leave),
|
||||||
|
Keysym::c => Some(Command::Capture),
|
||||||
|
Keysym::f => Some(Command::Flashlight),
|
||||||
|
Keysym::_0 => Some(Command::Reset),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Zoom {
|
||||||
|
// Dropped first: the EGL surface goes before the wl_surface under it.
|
||||||
|
renderer: gl::Renderer,
|
||||||
|
layer: LayerSurface,
|
||||||
|
viewport: WpViewport,
|
||||||
|
/// Frame size in physical px.
|
||||||
|
size: (u32, u32),
|
||||||
|
/// Buffer px per surface-local (logical) px.
|
||||||
|
ratio: f64,
|
||||||
|
view: Animated,
|
||||||
|
step: f64,
|
||||||
|
/// Pointer position in buffer px.
|
||||||
|
pointer: (f64, f64),
|
||||||
|
/// Last pointer position while the left button pans.
|
||||||
|
panning: Option<(f64, f64)>,
|
||||||
|
flashlight: bool,
|
||||||
|
/// Flashlight radius in logical px.
|
||||||
|
radius: f64,
|
||||||
|
ctrl: bool,
|
||||||
|
configured: bool,
|
||||||
|
frame_pending: bool,
|
||||||
|
dirty: bool,
|
||||||
|
last_draw: Option<Instant>,
|
||||||
|
outcome: Option<Outcome>,
|
||||||
|
error: Option<anyhow::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows `frame` (the frozen `output`) until the user leaves or captures.
|
||||||
|
pub fn run(
|
||||||
|
wl: &mut Wayland,
|
||||||
|
output: &Output,
|
||||||
|
frame: &Frame,
|
||||||
|
config: &config::Zoom,
|
||||||
|
) -> Result<Outcome> {
|
||||||
|
let qh = wl.queue.handle();
|
||||||
|
let s = &wl.state;
|
||||||
|
let layer_shell = s
|
||||||
|
.layer_shell
|
||||||
|
.as_ref()
|
||||||
|
.context("compositor does not support wlr-layer-shell")
|
||||||
|
.hint("run `valw doctor` to see what the compositor supports")?;
|
||||||
|
let viewporter = s
|
||||||
|
.viewporter
|
||||||
|
.as_ref()
|
||||||
|
.context("compositor does not support wp-viewporter")
|
||||||
|
.hint("run `valw doctor` to see what the compositor supports")?;
|
||||||
|
let surface = s.compositor.create_surface(&qh);
|
||||||
|
let layer = layer_shell.create_layer_surface(
|
||||||
|
&qh,
|
||||||
|
surface,
|
||||||
|
Layer::Overlay,
|
||||||
|
Some(NAMESPACE),
|
||||||
|
Some(&output.wl),
|
||||||
|
);
|
||||||
|
layer.set_anchor(Anchor::all());
|
||||||
|
layer.set_exclusive_zone(-1);
|
||||||
|
layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
|
||||||
|
layer.commit();
|
||||||
|
let viewport = viewporter.get_viewport(layer.wl_surface(), &qh, ());
|
||||||
|
let pixels: &Arc<_> = &frame.pixels;
|
||||||
|
let size = pixels.dimensions();
|
||||||
|
let renderer = gl::Renderer::new(&wl.conn, layer.wl_surface(), size, pixels)
|
||||||
|
.context("could not start OpenGL ES for zoom")
|
||||||
|
.hint("run `valw doctor` to check EGL")?;
|
||||||
|
|
||||||
|
wl.state.zoom = Some(Zoom {
|
||||||
|
renderer,
|
||||||
|
layer,
|
||||||
|
viewport,
|
||||||
|
size,
|
||||||
|
ratio: 1.0,
|
||||||
|
view: Animated::new(),
|
||||||
|
step: config.scroll_step,
|
||||||
|
pointer: (0.0, 0.0),
|
||||||
|
panning: None,
|
||||||
|
flashlight: false,
|
||||||
|
radius: config.flashlight_radius,
|
||||||
|
ctrl: false,
|
||||||
|
configured: false,
|
||||||
|
frame_pending: false,
|
||||||
|
dirty: true,
|
||||||
|
last_draw: None,
|
||||||
|
outcome: None,
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
let result = wl.dispatch_blocking(|s| {
|
||||||
|
s.zoom
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|z| z.outcome.is_some() || z.error.is_some())
|
||||||
|
});
|
||||||
|
let ended = wl.state.zoom.take().map(|z| (z.outcome, z.error));
|
||||||
|
// Dropping the zoom destroys its surface; make that visible right away.
|
||||||
|
let _ = wl.conn.flush();
|
||||||
|
result?;
|
||||||
|
match ended {
|
||||||
|
Some((_, Some(e))) => Err(e),
|
||||||
|
Some((Some(outcome), None)) => Ok(outcome),
|
||||||
|
_ => Ok(Outcome::Leave),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Zoom {
|
||||||
|
fn is(&self, surface: &WlSurface) -> bool {
|
||||||
|
self.layer.wl_surface() == surface
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sizef(&self) -> (f64, f64) {
|
||||||
|
(self.size.0 as f64, self.size.1 as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(&mut self, layer: &LayerSurface, (w, h): (u32, u32), qh: &QueueHandle<State>) {
|
||||||
|
if !self.is(layer.wl_surface()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if w > 0 {
|
||||||
|
self.ratio = self.size.0 as f64 / w as f64;
|
||||||
|
}
|
||||||
|
// Show the physical-size buffer 1:1 on the logical-size surface.
|
||||||
|
self.viewport.set_destination(w as i32, h as i32);
|
||||||
|
self.configured = true;
|
||||||
|
self.request_draw(qh);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn closed(&mut self, layer: &LayerSurface) {
|
||||||
|
if self.is(layer.wl_surface()) {
|
||||||
|
self.outcome.get_or_insert(Outcome::Leave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frame_done(&mut self, surface: &WlSurface, qh: &QueueHandle<State>) {
|
||||||
|
if !self.is(surface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.frame_pending = false;
|
||||||
|
if self.dirty {
|
||||||
|
self.draw(qh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_draw(&mut self, qh: &QueueHandle<State>) {
|
||||||
|
self.dirty = true;
|
||||||
|
if self.configured && !self.frame_pending {
|
||||||
|
self.draw(qh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, qh: &QueueHandle<State>) {
|
||||||
|
let now = Instant::now();
|
||||||
|
let dt = self
|
||||||
|
.last_draw
|
||||||
|
.map_or(0.0, |t| now.duration_since(t).as_secs_f64().min(0.1));
|
||||||
|
let moving = self.view.step(dt);
|
||||||
|
self.last_draw = moving.then_some(now);
|
||||||
|
let surface = self.layer.wl_surface();
|
||||||
|
surface.frame(qh, FrameCallbackData(surface.clone()));
|
||||||
|
let flashlight =
|
||||||
|
self.flashlight
|
||||||
|
.then_some((self.pointer.0, self.pointer.1, self.radius * self.ratio));
|
||||||
|
if let Err(e) = self.renderer.draw(&self.view.shown, flashlight) {
|
||||||
|
self.error = Some(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.frame_pending = true;
|
||||||
|
self.dirty = moving;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pointer(
|
||||||
|
&mut self,
|
||||||
|
events: &[PointerEvent],
|
||||||
|
cursor: Option<&WpCursorShapeDeviceV1>,
|
||||||
|
qh: &QueueHandle<State>,
|
||||||
|
) {
|
||||||
|
for event in events {
|
||||||
|
if !self.is(&event.surface) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let p = (event.position.0 * self.ratio, event.position.1 * self.ratio);
|
||||||
|
match event.kind {
|
||||||
|
PointerEventKind::Enter { serial } => {
|
||||||
|
if let Some(cursor) = cursor {
|
||||||
|
cursor.set_shape(serial, Shape::Default);
|
||||||
|
}
|
||||||
|
self.pointer = p;
|
||||||
|
}
|
||||||
|
PointerEventKind::Press {
|
||||||
|
button: BTN_LEFT,
|
||||||
|
serial,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.panning = Some(p);
|
||||||
|
if let Some(cursor) = cursor {
|
||||||
|
cursor.set_shape(serial, Shape::Grabbing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointerEventKind::Release {
|
||||||
|
button: BTN_LEFT,
|
||||||
|
serial,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.panning = None;
|
||||||
|
if let Some(cursor) = cursor {
|
||||||
|
cursor.set_shape(serial, Shape::Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointerEventKind::Motion { .. } => {
|
||||||
|
self.pointer = p;
|
||||||
|
if let Some(last) = self.panning {
|
||||||
|
let size = self.sizef();
|
||||||
|
let moved = self.view.target.pan((p.0 - last.0, p.1 - last.1), size);
|
||||||
|
self.view.set(moved);
|
||||||
|
self.panning = Some(p);
|
||||||
|
self.request_draw(qh);
|
||||||
|
} else if self.flashlight {
|
||||||
|
self.request_draw(qh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointerEventKind::Axis { vertical, .. } => {
|
||||||
|
let steps = view::scroll_steps(vertical.value120, vertical.absolute);
|
||||||
|
if steps == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.ctrl {
|
||||||
|
self.radius = view::radius(self.radius, steps, self.step);
|
||||||
|
} else {
|
||||||
|
let size = self.sizef();
|
||||||
|
self.view.target = self.view.target.zoom_at(p, self.step.powf(steps), size);
|
||||||
|
}
|
||||||
|
self.request_draw(qh);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&mut self, key: Keysym, qh: &QueueHandle<State>) {
|
||||||
|
match command(key) {
|
||||||
|
Some(Command::Leave) => {
|
||||||
|
self.outcome.get_or_insert(Outcome::Leave);
|
||||||
|
}
|
||||||
|
Some(Command::Capture) => {
|
||||||
|
let rect = self.view.target.visible_rect(self.size);
|
||||||
|
tracing::info!("zoom capture {rect:?} at {:.2}x", self.view.target.scale);
|
||||||
|
self.outcome.get_or_insert(Outcome::Capture(rect));
|
||||||
|
}
|
||||||
|
Some(Command::Flashlight) => {
|
||||||
|
self.flashlight = !self.flashlight;
|
||||||
|
self.request_draw(qh);
|
||||||
|
}
|
||||||
|
Some(Command::Reset) => {
|
||||||
|
self.view.target = View::IDENTITY;
|
||||||
|
self.request_draw(qh);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modifiers(&mut self, modifiers: Modifiers) {
|
||||||
|
self.ctrl = modifiers.ctrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keys() {
|
||||||
|
assert_eq!(command(Keysym::Escape), Some(Command::Leave));
|
||||||
|
assert_eq!(command(Keysym::q), Some(Command::Leave));
|
||||||
|
assert_eq!(command(Keysym::c), Some(Command::Capture));
|
||||||
|
assert_eq!(command(Keysym::f), Some(Command::Flashlight));
|
||||||
|
assert_eq!(command(Keysym::_0), Some(Command::Reset));
|
||||||
|
assert_eq!(command(Keysym::a), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user