#[cfg(debug_assertions)]
use std::{thread, time::Duration};
use anyhow::{Context, Result, bail, ensure};
use x11rb::{
COPY_DEPTH_FROM_PARENT, CURRENT_TIME, NONE, connect,
connection::Connection,
protocol::{
Event,
shape::{ConnectionExt as ShapeConnectionExt, SK, SO},
xproto::{
AtomEnum, ButtonPressEvent, ButtonReleaseEvent, ChangeWindowAttributesAux,
ClipOrdering, Colormap, ConfigureWindowAux, ConnectionExt, CreateGCAux,
CreateWindowAux, Cursor, EventMask, ExposeEvent, Gcontext, GrabMode, GrabStatus,
KeyButMask, KeyPressEvent, LineStyle, MotionNotifyEvent, Point, PropMode, Rectangle,
StackMode, Window, WindowClass,
},
},
rust_connection::RustConnection,
wrapper::ConnectionExt as _,
};
use crate::pipeline::{CaptureArea, VideoEffect};
const OVERLAY_OPACITY: u32 = 0x8a00_0000;
const DEFAULT_SELECTION_MIN_SIZE: i32 = 80;
const DEFAULT_SELECTION_MAX_SIZE: i32 = 480;
const HANDLE_SIZE: i32 = 18;
const BORDER_LINE_WIDTH: u32 = 2;
const BORDER_SHAPE_PADDING: i32 = 8;
const LABEL_CHAR_WIDTH: i32 = 8;
const LABEL_TEXT_HEIGHT: i32 = 14;
#[cfg(test)]
const LABEL_PADDING: i32 = 12;
const LABEL_BOX_PADDING_X: i32 = 10;
const LABEL_BOX_PADDING_Y: i32 = 6;
const HELP_TITLE: &str = "Select recording area";
const HELP_TEXT: &str = "Drag an area. Drag inside it to move.";
#[cfg(test)]
const SHORTCUT_TEXT: &str = "Drag inside to move Handles resize Enter confirms";
const CONFIRM_TEXT: &str = "USE THIS CROP";
const CONFIRM_TEXT_COMPACT: &str = "USE CROP";
const CONFIRM_TEXT_TINY: &str = "USE";
const CANCEL_TEXT: &str = "CANCEL";
const CANCEL_TEXT_TINY: &str = "NO";
const ACTION_BUTTON_HEIGHT: i32 = 42;
const ACTION_BUTTON_PADDING_X: i32 = 18;
const ACTION_BUTTON_GAP: i32 = 10;
const ACTION_BUTTON_SHADOW_OFFSET: i32 = 5;
const ACTION_BUTTON_PANEL_PADDING_X: i32 = 10;
const ACTION_BUTTON_PANEL_PADDING_Y: i32 = 8;
const ACTION_BUTTON_PANEL_HEADER_HEIGHT: i32 = 0;
const ACTION_BUTTON_PANEL_SHADOW_OFFSET: i32 = 5;
const ACTION_BUTTON_HIGHLIGHT_HEIGHT: i32 = 6;
const ACTION_BUTTON_ACCENT_WIDTH: i32 = 4;
const ACTION_RAIL_GAP: i32 = 6;
const CONFIRM_BUTTON_MIN_WIDTH: i32 = 164;
const CANCEL_BUTTON_MIN_WIDTH: i32 = 96;
const SELECTION_GRID_THICKNESS: i32 = 1;
const MOVE_AFFORDANCE_LENGTH: i32 = 22;
const MOVE_AFFORDANCE_GAP: i32 = 5;
const MOVE_AFFORDANCE_THICKNESS: i32 = 2;
const SELECTION_EDGE_METER_LENGTH: i32 = 54;
const SELECTION_EDGE_METER_THICKNESS: i32 = 3;
const SELECTION_CORNER_LENGTH: i32 = 42;
const SELECTION_CORNER_THICKNESS: i32 = 4;
const XC_BOTTOM_LEFT_CORNER: u16 = 12;
const XC_BOTTOM_RIGHT_CORNER: u16 = 14;
const XC_BOTTOM_SIDE: u16 = 16;
const XC_CROSSHAIR: u16 = 34;
const XC_FLEUR: u16 = 52;
const XC_HAND2: u16 = 60;
const XC_LEFT_PTR: u16 = 68;
const XC_LEFT_SIDE: u16 = 70;
const XC_RIGHT_SIDE: u16 = 96;
const XC_TOP_LEFT_CORNER: u16 = 134;
const XC_TOP_RIGHT_CORNER: u16 = 136;
const XC_TOP_SIDE: u16 = 138;
const XK_ESCAPE: u32 = 0xff1b;
const XK_RETURN: u32 = 0xff0d;
const XK_KP_ENTER: u32 = 0xff8d;
const XK_SPACE: u32 = 0x20;
const XK_BACK_SPACE: u32 = 0xff08;
const XK_DELETE: u32 = 0xffff;
const XK_LEFT: u32 = 0xff51;
const XK_UP: u32 = 0xff52;
const XK_RIGHT: u32 = 0xff53;
const XK_DOWN: u32 = 0xff54;
#[cfg(debug_assertions)]
const AUTODRAG_ENV: &str = "OCULA_SELECT_AUTODRAG";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Selection {
pub area: CaptureArea,
pub screen_width: i32,
pub screen_height: i32,
}
pub fn select_area() -> Result<Selection> {
select_area_with_effect(VideoEffect::None)
}
pub fn select_area_once() -> Result<Selection> {
select_area_once_with_effect(VideoEffect::None)
}
pub fn adjust_area(initial: CaptureArea) -> Result<Selection> {
adjust_area_with_effect(initial, VideoEffect::None)
}
pub fn select_area_with_effect(effect: VideoEffect) -> Result<Selection> {
select_area_with_mode(SelectorMode::Editable, None, effect)
}
pub fn select_area_once_with_effect(effect: VideoEffect) -> Result<Selection> {
select_area_with_mode(SelectorMode::ConfirmOnRelease, None, effect)
}
pub fn adjust_area_with_effect(initial: CaptureArea, effect: VideoEffect) -> Result<Selection> {
select_area_with_mode(SelectorMode::Editable, Some(initial), effect)
}
fn select_area_with_mode(
mode: SelectorMode,
initial: Option<CaptureArea>,
effect: VideoEffect,
) -> Result<Selection> {
ensure!(
std::env::var_os("DISPLAY").is_some(),
"--select requires an X11/XWayland DISPLAY; use --area X,Y,WIDTH,HEIGHT instead"
);
eprintln!("{}", mode.instructions());
let (conn, screen_num) = connect(None).context("failed to connect to X11 display")?;
let screen = &conn.setup().roots[screen_num];
let root = screen.root;
let width = screen.width_in_pixels;
let height = screen.height_in_pixels;
let screen_width = i32::from(width);
let screen_height = i32::from(height);
let window = conn.generate_id()?;
conn.create_window(
COPY_DEPTH_FROM_PARENT,
window,
root,
0,
0,
width,
height,
0,
WindowClass::INPUT_OUTPUT,
0,
&CreateWindowAux::new()
.background_pixel(screen.black_pixel)
.override_redirect(1)
.event_mask(EventMask::EXPOSURE),
)?;
let input_window = conn.generate_id()?;
conn.create_window(
COPY_DEPTH_FROM_PARENT,
input_window,
root,
0,
0,
width,
height,
0,
WindowClass::INPUT_ONLY,
0,
&CreateWindowAux::new()
.override_redirect(1)
.event_mask(pointer_event_mask() | EventMask::KEY_PRESS),
)?;
let mut guard = SelectorWindow::new(&conn, window, input_window);
let opacity_atom = conn
.intern_atom(false, b"_NET_WM_WINDOW_OPACITY")?
.reply()?
.atom;
conn.change_property32(
PropMode::REPLACE,
window,
opacity_atom,
AtomEnum::CARDINAL,
&[OVERLAY_OPACITY],
)?;
conn.change_property8(
PropMode::REPLACE,
window,
AtomEnum::WM_NAME,
AtomEnum::STRING,
b"Ocula Area Selector",
)?;
let initial = initial.map(|area| move_area(area, 0, 0, screen_width, screen_height));
update_overlay_shape(
&conn,
window,
screen_width,
screen_height,
initial,
SelectorDrawMode::Full,
effect,
)?;
let graphics = SelectorGraphics::new(
&conn,
window,
screen.white_pixel,
screen.black_pixel,
screen.default_colormap,
effect,
)?;
let cursors = SelectorCursors::new(&conn)?;
conn.change_window_attributes(
input_window,
&ChangeWindowAttributesAux::new().cursor(cursors.id(CursorKind::Crosshair)),
)?;
conn.map_window(window)?;
conn.configure_window(
window,
&ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
)?;
conn.map_window(input_window)?;
conn.configure_window(
input_window,
&ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
)?;
conn.flush()?;
let pointer = conn
.grab_pointer(
false,
input_window,
pointer_event_mask(),
GrabMode::ASYNC,
GrabMode::ASYNC,
NONE,
cursors.id(CursorKind::Crosshair),
CURRENT_TIME,
)?
.reply()?;
ensure!(
pointer.status == GrabStatus::SUCCESS,
"failed to grab pointer for area selection"
);
guard.pointer_grabbed = true;
let keyboard = conn
.grab_keyboard(
false,
input_window,
CURRENT_TIME,
GrabMode::ASYNC,
GrabMode::ASYNC,
)?
.reply()?;
ensure!(
keyboard.status == GrabStatus::SUCCESS,
"failed to grab keyboard for area selection"
);
guard.keyboard_grabbed = true;
#[cfg(debug_assertions)]
spawn_autodrag_if_requested(root, input_window);
let mut selection = initial;
let mut drag = None;
let mut active_cursor = CursorKind::Crosshair;
loop {
let event = conn.wait_for_event()?;
match event {
Event::Expose(event) => redraw_selection(
&conn,
window,
&graphics,
event,
screen_width,
screen_height,
selection,
effect,
)?,
Event::ButtonPress(event) if is_cancel_button_press(&event) => {
bail!("area selection cancelled");
}
Event::ButtonPress(event) if is_primary_button_press(&event) => {
let point = (event.event_x, event.event_y);
if let Some(area) = selection {
match action_button_hit(point, area, screen_width, screen_height) {
Some(ActionButton::Confirm) => {
return Ok(Selection {
area,
screen_width,
screen_height,
});
}
Some(ActionButton::Cancel) => bail!("area selection cancelled"),
None => {}
}
}
let Some(active_drag) =
drag_for_primary_press(point, selection, screen_width, screen_height)
else {
continue;
};
drag = Some(active_drag);
update_active_cursor(
&conn,
&cursors,
&mut active_cursor,
cursor_kind_for_drag(active_drag),
)?;
if selection.is_none() {
selection = default_area_around(point, screen_width, screen_height).ok();
}
redraw(
&conn,
window,
&graphics,
screen_width,
screen_height,
selection,
SelectorDrawMode::Dragging,
effect,
)?;
}
Event::MotionNotify(event) => {
let point = motion_point(&conn, input_window, &event);
update_active_cursor(
&conn,
&cursors,
&mut active_cursor,
cursor_kind_for_state(point, selection, drag, screen_width, screen_height),
)?;
let Some(active_drag) = drag else {
continue;
};
let next_selection = update_drag(active_drag, point, screen_width, screen_height)?;
if next_selection == selection {
continue;
}
selection = next_selection;
redraw(
&conn,
window,
&graphics,
screen_width,
screen_height,
selection,
SelectorDrawMode::Dragging,
effect,
)?;
#[cfg(debug_assertions)]
if autodrag_requested()
&& let Some(area) = selection
{
return Ok(Selection {
area,
screen_width,
screen_height,
});
}
}
Event::ButtonRelease(event) if is_primary_button_release(&event) => {
if let Some(active_drag) = drag.take() {
let point = (event.event_x, event.event_y);
selection = update_drag(active_drag, point, screen_width, screen_height)?
.or_else(|| default_area_around(point, screen_width, screen_height).ok());
update_active_cursor(
&conn,
&cursors,
&mut active_cursor,
cursor_kind_for_state(point, selection, drag, screen_width, screen_height),
)?;
if let Some(area) = selection_on_release(mode, selection) {
return Ok(Selection {
area,
screen_width,
screen_height,
});
}
}
redraw(
&conn,
window,
&graphics,
screen_width,
screen_height,
selection,
SelectorDrawMode::Full,
effect,
)?;
}
Event::KeyPress(event) => match key_action(&conn, &event)? {
KeyAction::Confirm if let Some(area) = selection => {
return Ok(Selection {
area,
screen_width,
screen_height,
});
}
KeyAction::Cancel => bail!("area selection cancelled"),
KeyAction::Reset => {
selection = None;
drag = None;
update_active_cursor(
&conn,
&cursors,
&mut active_cursor,
CursorKind::Crosshair,
)?;
redraw(
&conn,
window,
&graphics,
screen_width,
screen_height,
selection,
SelectorDrawMode::Full,
effect,
)?;
}
KeyAction::Move { dx, dy } if let Some(area) = selection => {
selection = Some(move_area(area, dx, dy, screen_width, screen_height));
redraw(
&conn,
window,
&graphics,
screen_width,
screen_height,
selection,
SelectorDrawMode::Full,
effect,
)?;
}
KeyAction::Resize { dw, dh } if let Some(area) = selection => {
selection = Some(resize_area_by_delta(
area,
dw,
dh,
screen_width,
screen_height,
)?);
redraw(
&conn,
window,
&graphics,
screen_width,
screen_height,
selection,
SelectorDrawMode::Full,
effect,
)?;
}
_ => {}
},
_ => {}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelectorMode {
Editable,
ConfirmOnRelease,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelectorDrawMode {
Full,
Dragging,
}
impl SelectorMode {
fn instructions(self) -> &'static str {
match self {
Self::Editable => {
"drag to select; click Use This Crop or press Enter to record; edge/corner handles resize; arrows move; Shift+arrows resize; Ctrl adjusts faster; Delete/Backspace clears; Escape/right-click cancels..."
}
Self::ConfirmOnRelease => {
"drag an area and release to select; Escape/right-click cancels..."
}
}
}
}
fn selection_on_release(mode: SelectorMode, selection: Option<CaptureArea>) -> Option<CaptureArea> {
match mode {
SelectorMode::ConfirmOnRelease => selection,
SelectorMode::Editable => None,
}
}
struct SelectorGraphics {
border_gc: Gcontext,
edge_gc: Gcontext,
glow_gc: Gcontext,
grid_gc: Gcontext,
meter_gc: Gcontext,
hot_gc: Gcontext,
handle_gc: Gcontext,
handle_border_gc: Gcontext,
panel_gc: Gcontext,
panel_shadow_gc: Gcontext,
panel_border_gc: Gcontext,
text_gc: Gcontext,
dim_text_gc: Gcontext,
confirm_gc: Gcontext,
confirm_text_gc: Gcontext,
cancel_gc: Gcontext,
cancel_text_gc: Gcontext,
button_shadow_gc: Gcontext,
button_border_gc: Gcontext,
shader_primary_gc: Gcontext,
shader_secondary_gc: Gcontext,
shader_tertiary_gc: Gcontext,
}
impl SelectorGraphics {
fn new(
conn: &RustConnection,
window: Window,
foreground: u32,
background: u32,
colormap: Colormap,
effect: VideoEffect,
) -> Result<Self> {
let font = conn.generate_id()?;
conn.open_font(font, b"fixed")?.check()?;
let style = selector_effect_style(effect);
let accent_color = color_pixel(conn, colormap, style.primary, foreground);
let glow_color = color_pixel(conn, colormap, style.secondary, foreground);
let edge_color = color_pixel(conn, colormap, b"#f4fdff", foreground);
let grid_color = color_pixel(conn, colormap, style.tertiary, foreground);
let meter_color = color_pixel(conn, colormap, style.confirm, foreground);
let hot_color = color_pixel(conn, colormap, style.hot, foreground);
let handle_color = color_pixel(conn, colormap, b"#f8ffff", foreground);
let panel_color = color_pixel(conn, colormap, b"#101822", background);
let panel_shadow_color = color_pixel(conn, colormap, b"#02060a", background);
let dim_text_color = color_pixel(conn, colormap, b"#b8d4e0", foreground);
let confirm_color = color_pixel(conn, colormap, b"#00f0b2", foreground);
let confirm_text_color = color_pixel(conn, colormap, b"#031612", background);
let cancel_color = color_pixel(conn, colormap, b"#1a2635", foreground);
let cancel_text_color = color_pixel(conn, colormap, b"#f4fbff", foreground);
let border_gc = conn.generate_id()?;
conn.create_gc(
border_gc,
window,
&CreateGCAux::new()
.foreground(accent_color)
.line_width(BORDER_LINE_WIDTH)
.line_style(LineStyle::SOLID),
)?;
let edge_gc = conn.generate_id()?;
conn.create_gc(
edge_gc,
window,
&CreateGCAux::new()
.foreground(edge_color)
.line_width(1)
.line_style(LineStyle::SOLID),
)?;
let glow_gc = conn.generate_id()?;
conn.create_gc(
glow_gc,
window,
&CreateGCAux::new()
.foreground(glow_color)
.line_width(8)
.line_style(LineStyle::SOLID),
)?;
let grid_gc = conn.generate_id()?;
conn.create_gc(
grid_gc,
window,
&CreateGCAux::new()
.foreground(grid_color)
.line_width(u32::try_from(SELECTION_GRID_THICKNESS).unwrap_or(1))
.line_style(LineStyle::SOLID),
)?;
let meter_gc = conn.generate_id()?;
conn.create_gc(
meter_gc,
window,
&CreateGCAux::new()
.foreground(meter_color)
.background(meter_color),
)?;
let hot_gc = conn.generate_id()?;
conn.create_gc(
hot_gc,
window,
&CreateGCAux::new()
.foreground(hot_color)
.line_width(3)
.line_style(LineStyle::SOLID),
)?;
let handle_gc = conn.generate_id()?;
conn.create_gc(
handle_gc,
window,
&CreateGCAux::new()
.foreground(handle_color)
.background(handle_color),
)?;
let handle_border_gc = conn.generate_id()?;
conn.create_gc(
handle_border_gc,
window,
&CreateGCAux::new()
.foreground(accent_color)
.line_width(1)
.line_style(LineStyle::SOLID),
)?;
let panel_gc = conn.generate_id()?;
conn.create_gc(
panel_gc,
window,
&CreateGCAux::new()
.foreground(panel_color)
.background(panel_color),
)?;
let panel_shadow_gc = conn.generate_id()?;
conn.create_gc(
panel_shadow_gc,
window,
&CreateGCAux::new()
.foreground(panel_shadow_color)
.background(panel_shadow_color),
)?;
let panel_border_gc = conn.generate_id()?;
conn.create_gc(
panel_border_gc,
window,
&CreateGCAux::new()
.foreground(accent_color)
.line_width(1)
.line_style(LineStyle::SOLID),
)?;
let text_gc = conn.generate_id()?;
conn.create_gc(
text_gc,
window,
&CreateGCAux::new()
.foreground(edge_color)
.background(panel_color)
.font(font),
)?;
let dim_text_gc = conn.generate_id()?;
conn.create_gc(
dim_text_gc,
window,
&CreateGCAux::new()
.foreground(dim_text_color)
.background(panel_color)
.font(font),
)?;
let confirm_gc = conn.generate_id()?;
conn.create_gc(
confirm_gc,
window,
&CreateGCAux::new()
.foreground(confirm_color)
.background(confirm_color),
)?;
let confirm_text_gc = conn.generate_id()?;
conn.create_gc(
confirm_text_gc,
window,
&CreateGCAux::new()
.foreground(confirm_text_color)
.background(confirm_color)
.font(font),
)?;
let cancel_gc = conn.generate_id()?;
conn.create_gc(
cancel_gc,
window,
&CreateGCAux::new()
.foreground(cancel_color)
.background(cancel_color),
)?;
let cancel_text_gc = conn.generate_id()?;
conn.create_gc(
cancel_text_gc,
window,
&CreateGCAux::new()
.foreground(cancel_text_color)
.background(cancel_color)
.font(font),
)?;
let button_shadow_gc = conn.generate_id()?;
conn.create_gc(
button_shadow_gc,
window,
&CreateGCAux::new()
.foreground(panel_shadow_color)
.background(panel_shadow_color),
)?;
let button_border_gc = conn.generate_id()?;
conn.create_gc(
button_border_gc,
window,
&CreateGCAux::new()
.foreground(edge_color)
.line_width(1)
.line_style(LineStyle::SOLID),
)?;
let shader_primary_gc = conn.generate_id()?;
conn.create_gc(
shader_primary_gc,
window,
&CreateGCAux::new()
.foreground(accent_color)
.line_width(2)
.line_style(LineStyle::SOLID),
)?;
let shader_secondary_gc = conn.generate_id()?;
conn.create_gc(
shader_secondary_gc,
window,
&CreateGCAux::new()
.foreground(glow_color)
.line_width(1)
.line_style(LineStyle::SOLID),
)?;
let shader_tertiary_gc = conn.generate_id()?;
conn.create_gc(
shader_tertiary_gc,
window,
&CreateGCAux::new()
.foreground(grid_color)
.background(grid_color)
.line_width(1)
.line_style(LineStyle::SOLID),
)?;
conn.close_font(font)?;
Ok(Self {
border_gc,
edge_gc,
glow_gc,
grid_gc,
meter_gc,
hot_gc,
handle_gc,
handle_border_gc,
panel_gc,
panel_shadow_gc,
panel_border_gc,
text_gc,
dim_text_gc,
confirm_gc,
confirm_text_gc,
cancel_gc,
cancel_text_gc,
button_shadow_gc,
button_border_gc,
shader_primary_gc,
shader_secondary_gc,
shader_tertiary_gc,
})
}
}
#[derive(Debug, Clone, Copy)]
struct SelectorEffectStyle {
primary: &'static [u8],
secondary: &'static [u8],
tertiary: &'static [u8],
hot: &'static [u8],
confirm: &'static [u8],
}
fn selector_effect_style(effect: VideoEffect) -> SelectorEffectStyle {
match effect {
VideoEffect::Neon => SelectorEffectStyle {
primary: b"#00f0ff",
secondary: b"#ff28d8",
tertiary: b"#00ffa6",
hot: b"#ff4fd8",
confirm: b"#00e0a4",
},
VideoEffect::Heatmap => SelectorEffectStyle {
primary: b"#ffc400",
secondary: b"#ff3b00",
tertiary: b"#2e7fff",
hot: b"#ffffff",
confirm: b"#ffb000",
},
VideoEffect::Glitch => SelectorEffectStyle {
primary: b"#00ffc8",
secondary: b"#ff2464",
tertiary: b"#bdfffb",
hot: b"#ff2464",
confirm: b"#00ffc8",
},
VideoEffect::Glass | VideoEffect::Fracture => SelectorEffectStyle {
primary: b"#d8fbff",
secondary: b"#7aa7ff",
tertiary: b"#ffffff",
hot: b"#ff4f8f",
confirm: b"#84f7ff",
},
VideoEffect::Prism | VideoEffect::Kaleido => SelectorEffectStyle {
primary: b"#ff3048",
secondary: b"#00d7ff",
tertiary: b"#ffe100",
hot: b"#ff28d8",
confirm: b"#00d7ff",
},
VideoEffect::Aurora => SelectorEffectStyle {
primary: b"#11f08a",
secondary: b"#24aaff",
tertiary: b"#b535ff",
hot: b"#b535ff",
confirm: b"#11f08a",
},
VideoEffect::Ripple => SelectorEffectStyle {
primary: b"#32b8ff",
secondary: b"#20ffd4",
tertiary: b"#b8f3ff",
hot: b"#20ffd4",
confirm: b"#20ffd4",
},
VideoEffect::Crt => SelectorEffectStyle {
primary: b"#38ff62",
secondary: b"#008a3a",
tertiary: b"#c2ffd0",
hot: b"#38ff62",
confirm: b"#38ff62",
},
VideoEffect::Holo => SelectorEffectStyle {
primary: b"#40efff",
secondary: b"#ff40ee",
tertiary: b"#e2ffff",
hot: b"#ff40ee",
confirm: b"#40efff",
},
VideoEffect::Vapor => SelectorEffectStyle {
primary: b"#44dfff",
secondary: b"#d82bff",
tertiary: b"#ff9b2c",
hot: b"#d82bff",
confirm: b"#44dfff",
},
VideoEffect::None => SelectorEffectStyle {
primary: b"#7df9ff",
secondary: b"#00d5ff",
tertiary: b"#b7fbff",
hot: b"#ff4fd8",
confirm: b"#00e0a4",
},
}
}
fn color_pixel(conn: &RustConnection, colormap: Colormap, name: &[u8], fallback: u32) -> u32 {
conn.alloc_named_color(colormap, name)
.ok()
.and_then(|cookie| cookie.reply().ok())
.map_or(fallback, |reply| reply.pixel)
}
struct SelectorCursors {
default_: Cursor,
crosshair: Cursor,
hand: Cursor,
move_: Cursor,
top_left: Cursor,
top_right: Cursor,
bottom_right: Cursor,
bottom_left: Cursor,
top: Cursor,
right: Cursor,
bottom: Cursor,
left: Cursor,
}
impl SelectorCursors {
fn new(conn: &RustConnection) -> Result<Self> {
let font = conn.generate_id()?;
conn.open_font(font, b"cursor")?.check()?;
let cursors = Self {
default_: create_cursor(conn, font, XC_LEFT_PTR)?,
crosshair: create_cursor(conn, font, XC_CROSSHAIR)?,
hand: create_cursor(conn, font, XC_HAND2)?,
move_: create_cursor(conn, font, XC_FLEUR)?,
top_left: create_cursor(conn, font, XC_TOP_LEFT_CORNER)?,
top_right: create_cursor(conn, font, XC_TOP_RIGHT_CORNER)?,
bottom_right: create_cursor(conn, font, XC_BOTTOM_RIGHT_CORNER)?,
bottom_left: create_cursor(conn, font, XC_BOTTOM_LEFT_CORNER)?,
top: create_cursor(conn, font, XC_TOP_SIDE)?,
right: create_cursor(conn, font, XC_RIGHT_SIDE)?,
bottom: create_cursor(conn, font, XC_BOTTOM_SIDE)?,
left: create_cursor(conn, font, XC_LEFT_SIDE)?,
};
conn.close_font(font)?.check()?;
Ok(cursors)
}
fn id(&self, kind: CursorKind) -> Cursor {
match kind {
CursorKind::Default => self.default_,
CursorKind::Crosshair => self.crosshair,
CursorKind::Pointer => self.hand,
CursorKind::Move => self.move_,
CursorKind::Resize(Handle::TopLeft) => self.top_left,
CursorKind::Resize(Handle::TopRight) => self.top_right,
CursorKind::Resize(Handle::BottomRight) => self.bottom_right,
CursorKind::Resize(Handle::BottomLeft) => self.bottom_left,
CursorKind::Resize(Handle::Top) => self.top,
CursorKind::Resize(Handle::Right) => self.right,
CursorKind::Resize(Handle::Bottom) => self.bottom,
CursorKind::Resize(Handle::Left) => self.left,
}
}
}
fn create_cursor(conn: &RustConnection, font: u32, shape: u16) -> Result<Cursor> {
let cursor = conn.generate_id()?;
conn.create_glyph_cursor(
cursor,
font,
font,
shape,
shape + 1,
u16::MAX,
u16::MAX,
u16::MAX,
0,
0,
0,
)?;
Ok(cursor)
}
struct SelectorWindow<'a> {
conn: &'a RustConnection,
window: Window,
input_window: Window,
pointer_grabbed: bool,
keyboard_grabbed: bool,
}
impl<'a> SelectorWindow<'a> {
fn new(conn: &'a RustConnection, window: Window, input_window: Window) -> Self {
Self {
conn,
window,
input_window,
pointer_grabbed: false,
keyboard_grabbed: false,
}
}
}
impl Drop for SelectorWindow<'_> {
fn drop(&mut self) {
if self.pointer_grabbed {
let _ = self.conn.ungrab_pointer(CURRENT_TIME);
}
if self.keyboard_grabbed {
let _ = self.conn.ungrab_keyboard(CURRENT_TIME);
}
let _ = self.conn.destroy_window(self.input_window);
let _ = self.conn.destroy_window(self.window);
let _ = self.conn.flush();
}
}
fn is_primary_button_press(event: &ButtonPressEvent) -> bool {
is_primary_button_detail(event.detail)
}
fn is_cancel_button_press(event: &ButtonPressEvent) -> bool {
is_cancel_button_detail(event.detail)
}
fn is_primary_button_detail(detail: u8) -> bool {
detail == 1
}
fn is_cancel_button_detail(detail: u8) -> bool {
detail == 3
}
fn is_primary_button_release(event: &ButtonReleaseEvent) -> bool {
event.detail == 1
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Drag {
New {
start: (i16, i16),
},
Move {
start: (i16, i16),
original: CaptureArea,
},
Resize {
handle: Handle,
original: CaptureArea,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Handle {
TopLeft,
TopRight,
BottomRight,
BottomLeft,
Top,
Right,
Bottom,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Hit {
Handle(Handle),
Inside,
Outside,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ActionButton {
Confirm,
Cancel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CursorKind {
Default,
Crosshair,
Pointer,
Move,
Resize(Handle),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum KeyAction {
Confirm,
Cancel,
Reset,
Move { dx: i32, dy: i32 },
Resize { dw: i32, dh: i32 },
Other,
}
fn start_drag(point: (i16, i16), selection: Option<CaptureArea>) -> Drag {
let Some(area) = selection else {
return Drag::New { start: point };
};
match hit_test(point, area) {
Hit::Handle(handle) => Drag::Resize {
handle,
original: area,
},
Hit::Inside => Drag::Move {
start: point,
original: area,
},
Hit::Outside => Drag::New { start: point },
}
}
fn drag_for_primary_press(
point: (i16, i16),
selection: Option<CaptureArea>,
screen_width: i32,
screen_height: i32,
) -> Option<Drag> {
if let Some(area) = selection
&& action_control_surface_hit(point, area, screen_width, screen_height)
{
return None;
}
Some(start_drag(point, selection))
}
fn cursor_kind_for_state(
point: (i16, i16),
selection: Option<CaptureArea>,
drag: Option<Drag>,
screen_width: i32,
screen_height: i32,
) -> CursorKind {
if let Some(drag) = drag {
return cursor_kind_for_drag(drag);
}
cursor_kind_for_point(point, selection, screen_width, screen_height)
}
fn cursor_kind_for_drag(drag: Drag) -> CursorKind {
match drag {
Drag::New { .. } => CursorKind::Crosshair,
Drag::Move { .. } => CursorKind::Move,
Drag::Resize { handle, .. } => CursorKind::Resize(handle),
}
}
fn cursor_kind_for_point(
point: (i16, i16),
selection: Option<CaptureArea>,
screen_width: i32,
screen_height: i32,
) -> CursorKind {
let Some(area) = selection else {
return CursorKind::Crosshair;
};
if action_button_hit(point, area, screen_width, screen_height).is_some() {
return CursorKind::Pointer;
}
if action_control_surface_hit(point, area, screen_width, screen_height) {
return CursorKind::Default;
}
match hit_test(point, area) {
Hit::Handle(handle) => CursorKind::Resize(handle),
Hit::Inside => CursorKind::Move,
Hit::Outside => CursorKind::Crosshair,
}
}
fn update_active_cursor(
conn: &RustConnection,
cursors: &SelectorCursors,
active: &mut CursorKind,
next: CursorKind,
) -> Result<()> {
if *active == next {
return Ok(());
}
conn.change_active_pointer_grab(cursors.id(next), CURRENT_TIME, pointer_event_mask())?;
*active = next;
Ok(())
}
fn pointer_event_mask() -> EventMask {
EventMask::BUTTON_PRESS
| EventMask::BUTTON_RELEASE
| EventMask::POINTER_MOTION
| EventMask::POINTER_MOTION_HINT
}
fn motion_point(conn: &RustConnection, window: Window, event: &MotionNotifyEvent) -> (i16, i16) {
#[cfg(debug_assertions)]
if autodrag_requested() {
return (event.event_x, event.event_y);
}
conn.query_pointer(window)
.ok()
.and_then(|cookie| cookie.reply().ok())
.filter(|reply| reply.same_screen)
.map_or((event.event_x, event.event_y), |reply| {
(reply.win_x, reply.win_y)
})
}
fn update_drag(
drag: Drag,
point: (i16, i16),
screen_width: i32,
screen_height: i32,
) -> Result<Option<CaptureArea>> {
match drag {
Drag::New { start } => area_from_screen_points(start, point, screen_width, screen_height),
Drag::Move { start, original } => Ok(Some(move_area(
original,
i32::from(point.0) - i32::from(start.0),
i32::from(point.1) - i32::from(start.1),
screen_width,
screen_height,
))),
Drag::Resize { handle, original } => {
resize_area(original, handle, point, screen_width, screen_height).map(Some)
}
}
}
fn hit_test(point: (i16, i16), area: CaptureArea) -> Hit {
for (handle, rectangle) in handle_rectangles_with_handles(area) {
if rectangle_contains(rectangle, point) {
return Hit::Handle(handle);
}
}
if point_in_area(point, area) {
Hit::Inside
} else {
Hit::Outside
}
}
fn point_in_area(point: (i16, i16), area: CaptureArea) -> bool {
let x = i32::from(point.0);
let y = i32::from(point.1);
x >= area.x && x < area.right() && y >= area.y && y < area.bottom()
}
fn rectangle_contains(rectangle: Rectangle, point: (i16, i16)) -> bool {
let x = i32::from(point.0);
let y = i32::from(point.1);
let left = i32::from(rectangle.x);
let top = i32::from(rectangle.y);
let right = left + i32::from(rectangle.width);
let bottom = top + i32::from(rectangle.height);
x >= left && x < right && y >= top && y < bottom
}
fn move_area(
area: CaptureArea,
dx: i32,
dy: i32,
screen_width: i32,
screen_height: i32,
) -> CaptureArea {
let max_x = (screen_width - area.width).max(0);
let max_y = (screen_height - area.height).max(0);
CaptureArea {
x: area.x.saturating_add(dx).clamp(0, max_x),
y: area.y.saturating_add(dy).clamp(0, max_y),
width: area.width.min(screen_width.max(1)),
height: area.height.min(screen_height.max(1)),
}
}
fn resize_area_by_delta(
area: CaptureArea,
dw: i32,
dh: i32,
screen_width: i32,
screen_height: i32,
) -> Result<CaptureArea> {
let max_width = (screen_width - area.x).max(1);
let max_height = (screen_height - area.y).max(1);
let width = area.width.saturating_add(dw).clamp(1, max_width);
let height = area.height.saturating_add(dh).clamp(1, max_height);
CaptureArea::new(area.x, area.y, width, height)
}
fn resize_area(
area: CaptureArea,
handle: Handle,
point: (i16, i16),
screen_width: i32,
screen_height: i32,
) -> Result<CaptureArea> {
let x = i32::from(point.0).clamp(0, screen_width.max(0));
let y = i32::from(point.1).clamp(0, screen_height.max(0));
let (start, end) = match handle {
Handle::TopLeft => ((area.right(), area.bottom()), (x, y)),
Handle::TopRight => ((area.x, area.bottom()), (x, y)),
Handle::BottomRight => ((area.x, area.y), (x, y)),
Handle::BottomLeft => ((area.right(), area.y), (x, y)),
Handle::Top => ((area.x, area.bottom()), (area.right(), y)),
Handle::Right => ((area.x, area.y), (x, area.bottom())),
Handle::Bottom => ((area.x, area.y), (area.right(), y)),
Handle::Left => ((area.right(), area.y), (x, area.bottom())),
};
area_from_resize_points(start, end, handle, screen_width, screen_height)
}
fn area_from_resize_points(
fixed: (i32, i32),
moving: (i32, i32),
handle: Handle,
screen_width: i32,
screen_height: i32,
) -> Result<CaptureArea> {
let (x, width) = resize_interval(
fixed.0,
moving.0,
screen_width,
matches!(handle, Handle::TopLeft | Handle::BottomLeft | Handle::Left),
);
let (y, height) = resize_interval(
fixed.1,
moving.1,
screen_height,
matches!(handle, Handle::TopLeft | Handle::TopRight | Handle::Top),
);
CaptureArea::new(x, y, width, height)
}
fn resize_interval(
fixed: i32,
moving: i32,
screen_limit: i32,
fixed_is_max_when_equal: bool,
) -> (i32, i32) {
if moving < fixed {
return (moving, fixed.saturating_sub(moving));
}
if moving > fixed {
return (fixed, moving.saturating_sub(fixed));
}
let limit = screen_limit.max(1);
let (start, end) = if fixed_is_max_when_equal {
(fixed.saturating_sub(1).max(0), fixed)
} else {
(fixed, fixed.saturating_add(1).min(limit))
};
if end > start {
(start, end.saturating_sub(start))
} else if fixed > 0 {
(fixed.saturating_sub(1), 1)
} else {
(0, 1)
}
}
fn key_action(conn: &RustConnection, event: &KeyPressEvent) -> Result<KeyAction> {
let keymap = conn.get_keyboard_mapping(event.detail, 1)?.reply()?;
Ok(key_action_from_keysyms(&keymap.keysyms, event.state))
}
fn key_action_from_keysyms(keysyms: &[u32], state: KeyButMask) -> KeyAction {
if keysyms.contains(&XK_ESCAPE) {
return KeyAction::Cancel;
}
if keysyms.contains(&XK_BACK_SPACE) || keysyms.contains(&XK_DELETE) {
return KeyAction::Reset;
}
if keysyms
.iter()
.any(|keysym| matches!(*keysym, XK_RETURN | XK_KP_ENTER | XK_SPACE))
{
return KeyAction::Confirm;
}
let step = if has_modifier(state, KeyButMask::CONTROL) {
10
} else {
1
};
for keysym in keysyms {
let (dx, dy) = match *keysym {
XK_LEFT => (-step, 0),
XK_RIGHT => (step, 0),
XK_UP => (0, -step),
XK_DOWN => (0, step),
_ => continue,
};
return if has_modifier(state, KeyButMask::SHIFT) {
KeyAction::Resize { dw: dx, dh: dy }
} else {
KeyAction::Move { dx, dy }
};
}
KeyAction::Other
}
fn has_modifier(state: KeyButMask, mask: KeyButMask) -> bool {
u16::from(state) & u16::from(mask) != 0
}
#[allow(clippy::too_many_arguments)]
fn redraw_selection(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
_event: ExposeEvent,
screen_width: i32,
screen_height: i32,
selection: Option<CaptureArea>,
effect: VideoEffect,
) -> Result<()> {
redraw(
conn,
window,
graphics,
screen_width,
screen_height,
selection,
SelectorDrawMode::Full,
effect,
)
}
#[allow(clippy::too_many_arguments)]
fn redraw(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
screen_width: i32,
screen_height: i32,
selection: Option<CaptureArea>,
draw_mode: SelectorDrawMode,
effect: VideoEffect,
) -> Result<()> {
update_overlay_shape(
conn,
window,
screen_width,
screen_height,
selection,
draw_mode,
effect,
)?;
conn.clear_area(false, window, 0, 0, 0, 0)?;
if let Some(area) = selection {
draw_selection_frame(conn, window, graphics, area)?;
if draw_mode == SelectorDrawMode::Full {
draw_shader_effect_overlay(conn, window, graphics, area, effect)?;
draw_selection_grid(conn, window, graphics, area)?;
draw_move_affordance(conn, window, graphics, area)?;
draw_handles(conn, window, graphics, area)?;
draw_action_buttons(conn, window, graphics, area, screen_width, screen_height)?;
}
} else {
draw_help_label(conn, window, graphics, screen_width, screen_height)?;
}
conn.flush()?;
Ok(())
}
fn draw_selection_frame(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
) -> Result<()> {
if let Some(rectangle) = rectangle_from_area(area) {
conn.poly_rectangle(window, graphics.glow_gc, &[rectangle])?;
conn.poly_rectangle(window, graphics.hot_gc, &[rectangle])?;
conn.poly_rectangle(window, graphics.border_gc, &[rectangle])?;
conn.poly_rectangle(window, graphics.edge_gc, &[rectangle])?;
}
let corners = selection_corner_rectangles(area);
conn.poly_fill_rectangle(window, graphics.border_gc, &corners)?;
let meters = selection_edge_meter_rectangles(area);
if !meters.is_empty() {
conn.poly_fill_rectangle(window, graphics.meter_gc, &meters)?;
}
Ok(())
}
fn draw_selection_grid(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
) -> Result<()> {
let grid = selection_grid_rectangles(area);
if !grid.is_empty() {
conn.poly_fill_rectangle(window, graphics.grid_gc, &grid)?;
}
Ok(())
}
fn draw_move_affordance(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
) -> Result<()> {
let affordance = move_affordance_rectangles(area);
if !affordance.is_empty() {
conn.poly_fill_rectangle(window, graphics.shader_primary_gc, &affordance)?;
}
Ok(())
}
fn draw_shader_effect_overlay(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
effect: VideoEffect,
) -> Result<()> {
match effect {
VideoEffect::Neon | VideoEffect::Glitch | VideoEffect::Holo => {
draw_shader_circuit_lines(conn, window, graphics, area)?;
}
VideoEffect::Ripple | VideoEffect::Aurora | VideoEffect::Vapor => {
draw_shader_flow_lines(conn, window, graphics, area)?;
}
VideoEffect::Glass | VideoEffect::Fracture | VideoEffect::Prism | VideoEffect::Kaleido => {
draw_shader_facets(conn, window, graphics, area)?;
}
VideoEffect::Heatmap | VideoEffect::Crt | VideoEffect::None => {
draw_shader_scan_lines(conn, window, graphics, area)?;
}
}
Ok(())
}
fn draw_shader_circuit_lines(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
) -> Result<()> {
let line_count = (area.height / 36).clamp(3, 8);
for i in 0..line_count {
let y = area.y + ((i + 1) * area.height) / (line_count + 1);
let jog = ((i * 17 + area.x + area.y) % area.width.max(1)).min(area.width);
let points = [
point(area.x + 10, y),
point(area.x + jog / 2, y),
point(area.x + jog / 2 + 12, y - 8),
point(area.right() - 10, y - 8),
];
conn.poly_line(
x11rb::protocol::xproto::CoordMode::ORIGIN,
window,
if i % 2 == 0 {
graphics.shader_primary_gc
} else {
graphics.shader_secondary_gc
},
&points,
)?;
}
Ok(())
}
fn draw_shader_flow_lines(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
) -> Result<()> {
let line_count = (area.height / 34).clamp(3, 7);
for i in 0..line_count {
let y = area.y + ((i + 1) * area.height) / (line_count + 1);
let mut points = Vec::new();
let steps = 8;
for step in 0..=steps {
let x = area.x + (step * area.width) / steps;
let wave = ((step * 11 + i * 9 + area.width) % 17) - 8;
points.push(point(x, y + wave));
}
conn.poly_line(
x11rb::protocol::xproto::CoordMode::ORIGIN,
window,
graphics.shader_secondary_gc,
&points,
)?;
}
Ok(())
}
fn draw_shader_facets(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
) -> Result<()> {
let columns = (area.width / 72).clamp(3, 9);
for i in 0..columns {
let x = area.x + ((i + 1) * area.width) / (columns + 1);
let tilt = ((i * 19 + area.height) % 29) - 14;
let points = [
point(x, area.y + 8),
point(x + tilt, area.y + area.height / 2),
point(x - tilt / 2, area.bottom() - 8),
];
conn.poly_line(
x11rb::protocol::xproto::CoordMode::ORIGIN,
window,
if i % 2 == 0 {
graphics.shader_primary_gc
} else {
graphics.shader_tertiary_gc
},
&points,
)?;
}
Ok(())
}
fn draw_shader_scan_lines(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
) -> Result<()> {
let lines = shader_scan_rectangles(area);
if !lines.is_empty() {
conn.poly_fill_rectangle(window, graphics.shader_tertiary_gc, &lines)?;
}
Ok(())
}
fn draw_handles(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
) -> Result<()> {
for (_, rectangle) in handle_rectangles_with_handles(area) {
let shadow = offset_rectangle(rectangle, 2, 2).unwrap_or(rectangle);
conn.poly_fill_rectangle(window, graphics.panel_shadow_gc, &[shadow])?;
conn.poly_fill_rectangle(window, graphics.handle_gc, &[rectangle])?;
conn.poly_rectangle(window, graphics.handle_border_gc, &[rectangle])?;
}
Ok(())
}
fn draw_action_buttons(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
area: CaptureArea,
screen_width: i32,
screen_height: i32,
) -> Result<()> {
if let Some(panel) = action_button_panel_rectangle(area, screen_width, screen_height) {
if let Some(shadow) = offset_rectangle(panel, 0, ACTION_BUTTON_PANEL_SHADOW_OFFSET) {
conn.poly_fill_rectangle(window, graphics.button_shadow_gc, &[shadow])?;
}
conn.poly_fill_rectangle(window, graphics.panel_gc, &[panel])?;
conn.poly_rectangle(window, graphics.button_border_gc, &[panel])?;
if let Some(highlight) = rect_i32(
i32::from(panel.x).saturating_add(2),
i32::from(panel.y).saturating_add(2),
i32::from(panel.width).saturating_sub(4),
2,
) {
conn.poly_fill_rectangle(window, graphics.edge_gc, &[highlight])?;
}
}
for (button, rectangle) in action_button_rectangles(area, screen_width, screen_height) {
let (fill_gc, text_gc) = match button {
ActionButton::Confirm => (graphics.confirm_gc, graphics.confirm_text_gc),
ActionButton::Cancel => (graphics.cancel_gc, graphics.cancel_text_gc),
};
let label = action_button_label(button, i32::from(rectangle.width));
if let Some(shadow) = offset_rectangle(
rectangle,
0,
ACTION_BUTTON_SHADOW_OFFSET.min(i32::from(u16::MAX)),
) {
conn.poly_fill_rectangle(window, graphics.button_shadow_gc, &[shadow])?;
}
conn.poly_fill_rectangle(window, fill_gc, &[rectangle])?;
draw_action_button_glass(conn, window, graphics, button, rectangle)?;
conn.poly_rectangle(window, graphics.button_border_gc, &[rectangle])?;
let text_width = label_width(label.len());
let x = i32::from(rectangle.x)
.saturating_add((i32::from(rectangle.width).saturating_sub(text_width) / 2).max(0));
let y = i32::from(rectangle.y)
.saturating_add((ACTION_BUTTON_HEIGHT + LABEL_TEXT_HEIGHT) / 2)
.saturating_sub(3);
conn.image_text8(
window,
text_gc,
clamp_i16(x),
clamp_i16(y),
label.as_bytes(),
)?;
}
Ok(())
}
fn draw_action_button_glass(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
button: ActionButton,
rectangle: Rectangle,
) -> Result<()> {
if let Some(highlight) = rect_i32(
i32::from(rectangle.x).saturating_add(3),
i32::from(rectangle.y).saturating_add(3),
i32::from(rectangle.width).saturating_sub(6),
ACTION_BUTTON_HIGHLIGHT_HEIGHT,
) {
conn.poly_fill_rectangle(window, graphics.edge_gc, &[highlight])?;
}
if let Some(depth) = rect_i32(
i32::from(rectangle.x).saturating_add(3),
i32::from(rectangle.y)
.saturating_add(i32::from(rectangle.height))
.saturating_sub(5),
i32::from(rectangle.width).saturating_sub(6),
2,
) {
conn.poly_fill_rectangle(window, graphics.button_shadow_gc, &[depth])?;
}
let accent_gc = match button {
ActionButton::Confirm => graphics.edge_gc,
ActionButton::Cancel => graphics.grid_gc,
};
let y = i32::from(rectangle.y).saturating_add(7);
let height = i32::from(rectangle.height).saturating_sub(14);
let left = rect_i32(
i32::from(rectangle.x).saturating_add(6),
y,
ACTION_BUTTON_ACCENT_WIDTH,
height,
);
let right = rect_i32(
i32::from(rectangle.x)
.saturating_add(i32::from(rectangle.width))
.saturating_sub(6 + ACTION_BUTTON_ACCENT_WIDTH),
y,
ACTION_BUTTON_ACCENT_WIDTH,
height,
);
let accents: Vec<_> = [left, right].into_iter().flatten().collect();
if !accents.is_empty() {
conn.poly_fill_rectangle(window, accent_gc, &accents)?;
}
Ok(())
}
fn draw_help_label(
conn: &RustConnection,
window: Window,
graphics: &SelectorGraphics,
screen_width: i32,
screen_height: i32,
) -> Result<()> {
let panel = help_panel_rectangle(screen_width, screen_height);
conn.poly_fill_rectangle(window, graphics.panel_shadow_gc, &[panel.shadow])?;
conn.poly_fill_rectangle(window, graphics.panel_gc, &[panel.body])?;
conn.poly_rectangle(window, graphics.panel_border_gc, &[panel.body])?;
conn.image_text8(
window,
graphics.text_gc,
panel.title_x,
panel.title_y,
HELP_TITLE.as_bytes(),
)?;
conn.image_text8(
window,
graphics.dim_text_gc,
panel.body_x,
panel.body_y,
HELP_TEXT.as_bytes(),
)?;
Ok(())
}
struct HelpPanel {
body: Rectangle,
shadow: Rectangle,
title_x: i16,
title_y: i16,
body_x: i16,
body_y: i16,
}
fn help_panel_rectangle(screen_width: i32, screen_height: i32) -> HelpPanel {
let title_width = label_width(HELP_TITLE.len());
let body_width = label_width(HELP_TEXT.len());
let content_width = title_width.max(body_width);
let wanted_width = content_width.saturating_add(LABEL_BOX_PADDING_X.saturating_mul(2));
let wanted_height = LABEL_TEXT_HEIGHT
.saturating_mul(2)
.saturating_add(LABEL_BOX_PADDING_Y.saturating_mul(3));
let panel_width = wanted_width.min(screen_width.max(1));
let panel_height = wanted_height.min(screen_height.max(1));
let x = screen_width.saturating_sub(panel_width).max(0) / 2;
let y = screen_height.saturating_sub(panel_height).max(0) / 2;
let body = rect_i32(x, y, panel_width, panel_height).unwrap_or(Rectangle {
x: 0,
y: 0,
width: 1,
height: 1,
});
let shadow = offset_rectangle(body, 0, 4).unwrap_or(body);
let content_left = x.saturating_add(LABEL_BOX_PADDING_X);
let title_x = content_left.saturating_add((content_width.saturating_sub(title_width)) / 2);
let body_x = content_left.saturating_add((content_width.saturating_sub(body_width)) / 2);
let title_y = y
.saturating_add(LABEL_BOX_PADDING_Y)
.saturating_add(LABEL_TEXT_HEIGHT);
let body_y = title_y
.saturating_add(LABEL_TEXT_HEIGHT)
.saturating_add(LABEL_BOX_PADDING_Y);
HelpPanel {
body,
shadow,
title_x: clamp_i16(title_x),
title_y: clamp_i16(title_y),
body_x: clamp_i16(body_x),
body_y: clamp_i16(body_y),
}
}
fn selection_grid_rectangles(area: CaptureArea) -> Vec<Rectangle> {
if area.width < 24 || area.height < 24 {
return Vec::new();
}
let thickness = SELECTION_GRID_THICKNESS.max(1);
[
rect_i32(area.x + area.width / 3, area.y, thickness, area.height),
rect_i32(
area.x + area.width.saturating_mul(2) / 3,
area.y,
thickness,
area.height,
),
rect_i32(area.x, area.y + area.height / 3, area.width, thickness),
rect_i32(
area.x,
area.y + area.height.saturating_mul(2) / 3,
area.width,
thickness,
),
]
.into_iter()
.flatten()
.collect()
}
fn move_affordance_rectangles(area: CaptureArea) -> Vec<Rectangle> {
if area.width < 96 || area.height < 72 {
return Vec::new();
}
let length = MOVE_AFFORDANCE_LENGTH
.min(area.width / 4)
.min(area.height / 4)
.max(MOVE_AFFORDANCE_THICKNESS);
let gap = MOVE_AFFORDANCE_GAP.max(MOVE_AFFORDANCE_THICKNESS);
let thickness = MOVE_AFFORDANCE_THICKNESS.max(1);
let cx = area.x + area.width / 2;
let cy = area.y + area.height / 2;
let half = thickness / 2;
[
rect_i32(cx - gap - length, cy - half, length, thickness),
rect_i32(cx + gap, cy - half, length, thickness),
rect_i32(cx - half, cy - gap - length, thickness, length),
rect_i32(cx - half, cy + gap, thickness, length),
]
.into_iter()
.flatten()
.collect()
}
fn shader_effect_rectangles(area: CaptureArea, effect: VideoEffect) -> Vec<Rectangle> {
match effect {
VideoEffect::Neon | VideoEffect::Glitch | VideoEffect::Holo => {
shader_circuit_rectangles(area)
}
VideoEffect::Ripple | VideoEffect::Aurora | VideoEffect::Vapor => {
shader_flow_rectangles(area)
}
VideoEffect::Glass | VideoEffect::Fracture | VideoEffect::Prism | VideoEffect::Kaleido => {
shader_facet_rectangles(area)
}
VideoEffect::Heatmap | VideoEffect::Crt | VideoEffect::None => shader_scan_rectangles(area),
}
}
fn shader_circuit_rectangles(area: CaptureArea) -> Vec<Rectangle> {
let line_count = (area.height / 36).clamp(3, 8);
let mut rectangles = Vec::new();
for i in 0..line_count {
let y = area.y + ((i + 1) * area.height) / (line_count + 1);
let jog = ((i * 17 + area.x + area.y) % area.width.max(1)).min(area.width);
rectangles.extend([
rect_i32(area.x + 10, y - 1, jog / 2, 3),
rect_i32(area.x + jog / 2 + 10, y - 9, 3, 10),
rect_i32(area.x + jog / 2 + 10, y - 9, area.width - jog / 2 - 20, 3),
]);
}
rectangles.into_iter().flatten().collect()
}
fn shader_flow_rectangles(area: CaptureArea) -> Vec<Rectangle> {
let line_count = (area.height / 34).clamp(3, 7);
let mut rectangles = Vec::new();
for i in 0..line_count {
let y = area.y + ((i + 1) * area.height) / (line_count + 1);
let segment_width = (area.width / 8).max(1);
for step in 0..8 {
let x = area.x + step * segment_width;
let wave = ((step * 11 + i * 9 + area.width) % 17) - 8;
rectangles.push(rect_i32(x, y + wave, segment_width, 3));
}
}
rectangles.into_iter().flatten().collect()
}
fn shader_facet_rectangles(area: CaptureArea) -> Vec<Rectangle> {
let columns = (area.width / 72).clamp(3, 9);
let mut rectangles = Vec::new();
for i in 0..columns {
let x = area.x + ((i + 1) * area.width) / (columns + 1);
let tilt = ((i * 19 + area.height) % 29) - 14;
let mid_y = area.y + area.height / 2;
rectangles.extend([
rect_i32(
x.min(x + tilt),
area.y + 8,
(tilt.abs() + 2).max(2),
mid_y - area.y,
),
rect_i32(
(x + tilt).min(x - tilt / 2),
mid_y,
(tilt + tilt / 2).abs().max(2),
area.bottom() - mid_y - 8,
),
]);
}
rectangles.into_iter().flatten().collect()
}
fn shader_scan_rectangles(area: CaptureArea) -> Vec<Rectangle> {
let mut rectangles = Vec::new();
let mut y = area.y + 8;
while y < area.bottom() - 8 {
rectangles.push(rect_i32(area.x + 8, y, area.width.saturating_sub(16), 1));
y += 12;
}
rectangles.into_iter().flatten().collect()
}
fn point(x: i32, y: i32) -> Point {
Point {
x: clamp_i16(x),
y: clamp_i16(y),
}
}
fn selection_corner_rectangles(area: CaptureArea) -> Vec<Rectangle> {
let thickness = SELECTION_CORNER_THICKNESS.max(1);
let half = thickness / 2;
let length = SELECTION_CORNER_LENGTH
.min(area.width.max(1))
.min(area.height.max(1))
.max(thickness);
let left = area.x.saturating_sub(half);
let top = area.y.saturating_sub(half);
let right = area.right().saturating_sub(length).saturating_add(half);
let bottom = area.bottom().saturating_sub(thickness).saturating_add(half);
let edge_right = area.right().saturating_sub(thickness).saturating_add(half);
let edge_bottom = area.bottom().saturating_sub(length).saturating_add(half);
[
rect_i32(left, top, length, thickness),
rect_i32(left, top, thickness, length),
rect_i32(right, top, length, thickness),
rect_i32(edge_right, top, thickness, length),
rect_i32(left, bottom, length, thickness),
rect_i32(left, edge_bottom, thickness, length),
rect_i32(right, bottom, length, thickness),
rect_i32(edge_right, edge_bottom, thickness, length),
]
.into_iter()
.flatten()
.collect()
}
fn selection_edge_meter_rectangles(area: CaptureArea) -> Vec<Rectangle> {
let length = SELECTION_EDGE_METER_LENGTH
.min(area.width.max(1))
.min(area.height.max(1));
let thickness = SELECTION_EDGE_METER_THICKNESS.max(1);
let gap = BORDER_SHAPE_PADDING.saturating_sub(thickness).max(1);
let center_x = area
.x
.saturating_add(area.width / 2)
.saturating_sub(length / 2);
let center_y = area
.y
.saturating_add(area.height / 2)
.saturating_sub(length / 2);
[
rect_i32(center_x, area.y.saturating_sub(gap), length, thickness),
rect_i32(
center_x,
area.bottom().saturating_add(gap),
length,
thickness,
),
rect_i32(area.x.saturating_sub(gap), center_y, thickness, length),
rect_i32(
area.right().saturating_add(gap),
center_y,
thickness,
length,
),
]
.into_iter()
.flatten()
.collect()
}
#[cfg(test)]
fn help_label_position(screen_width: i32, screen_height: i32, label_len: usize) -> (i16, i16) {
let width = label_width(label_len);
let available_width = screen_width.saturating_sub(width);
let max_x = available_width.max(0);
let x = (available_width / 2).clamp(0, max_x);
let max_y = screen_height
.saturating_sub(LABEL_PADDING)
.max(LABEL_TEXT_HEIGHT);
let y = (screen_height / 2).clamp(LABEL_TEXT_HEIGHT, max_y);
(clamp_i16(x), clamp_i16(y))
}
#[cfg(test)]
fn shortcut_label_position(
area: CaptureArea,
screen_width: i32,
screen_height: i32,
label_len: usize,
) -> (i16, i16) {
let width = label_width(label_len);
let available_width = screen_width.saturating_sub(width);
let max_x = available_width.max(0);
let x = (available_width / 2).clamp(0, max_x);
let bottom_y = screen_height
.saturating_sub(LABEL_PADDING)
.max(LABEL_TEXT_HEIGHT);
let top_y = LABEL_TEXT_HEIGHT
.saturating_add(LABEL_PADDING)
.min(bottom_y);
let dimension_label = format!("{}x{} px", area.width, area.height);
let (_, dimension_y) = label_position(area, screen_width, screen_height, dimension_label.len());
let bottom_overlaps_dimension = label_baselines_overlap(i32::from(dimension_y), bottom_y);
let bottom_overlaps_selection = label_overlaps_area(x, bottom_y, width, area);
let top_overlaps_selection = label_overlaps_area(x, top_y, width, area);
let should_use_top = (bottom_overlaps_selection && !top_overlaps_selection)
|| (bottom_overlaps_dimension && (bottom_overlaps_selection || !top_overlaps_selection));
let y = if should_use_top { top_y } else { bottom_y };
(clamp_i16(x), clamp_i16(y))
}
#[cfg(test)]
fn label_baselines_overlap(first: i32, second: i32) -> bool {
let first_top = first.saturating_sub(LABEL_TEXT_HEIGHT);
let first_bottom = first.saturating_add(4);
let second_top = second.saturating_sub(LABEL_TEXT_HEIGHT);
let second_bottom = second.saturating_add(4);
first_top <= second_bottom && second_top <= first_bottom
}
#[cfg(test)]
fn label_overlaps_area(x: i32, y: i32, width: i32, area: CaptureArea) -> bool {
let left = x.saturating_sub(2);
let right = x.saturating_add(width).saturating_add(2);
let top = y.saturating_sub(LABEL_TEXT_HEIGHT);
let bottom = y.saturating_add(4);
left < area.right() && right > area.x && top < area.bottom() && bottom > area.y
}
#[cfg(test)]
fn label_position(
area: CaptureArea,
screen_width: i32,
screen_height: i32,
label_len: usize,
) -> (i16, i16) {
let label_width = label_width(label_len);
let max_x = screen_width
.saturating_sub(label_width)
.saturating_sub(LABEL_PADDING)
.max(0);
let x = area.x.saturating_add(LABEL_PADDING).clamp(0, max_x);
let above = area.y.saturating_sub(LABEL_PADDING);
let below = area
.bottom()
.saturating_add(LABEL_TEXT_HEIGHT)
.saturating_add(LABEL_PADDING);
let max_y = screen_height
.saturating_sub(LABEL_PADDING)
.max(LABEL_TEXT_HEIGHT);
let y = if above >= LABEL_TEXT_HEIGHT {
above
} else {
below.clamp(LABEL_TEXT_HEIGHT, max_y)
};
(clamp_i16(x), clamp_i16(y))
}
fn label_width(label_len: usize) -> i32 {
i32::try_from(label_len)
.unwrap_or(i32::MAX)
.saturating_mul(LABEL_CHAR_WIDTH)
}
fn clamp_i16(value: i32) -> i16 {
value.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16
}
fn update_overlay_shape(
conn: &RustConnection,
window: Window,
screen_width: i32,
screen_height: i32,
area: Option<CaptureArea>,
draw_mode: SelectorDrawMode,
effect: VideoEffect,
) -> Result<()> {
let rectangles =
overlay_shape_rectangles_for_mode(screen_width, screen_height, area, draw_mode, effect);
conn.shape_rectangles(
SO::SET,
SK::BOUNDING,
ClipOrdering::UNSORTED,
window,
0,
0,
&rectangles,
)?;
let input_rectangles: [Rectangle; 0] = [];
conn.shape_rectangles(
SO::SET,
SK::INPUT,
ClipOrdering::UNSORTED,
window,
0,
0,
&input_rectangles,
)?;
Ok(())
}
#[cfg(test)]
fn overlay_shape_rectangles(
screen_width: i32,
screen_height: i32,
area: Option<CaptureArea>,
) -> Vec<Rectangle> {
overlay_shape_rectangles_for_mode(
screen_width,
screen_height,
area,
SelectorDrawMode::Full,
VideoEffect::None,
)
}
fn overlay_shape_rectangles_for_mode(
screen_width: i32,
screen_height: i32,
area: Option<CaptureArea>,
draw_mode: SelectorDrawMode,
effect: VideoEffect,
) -> Vec<Rectangle> {
let mut rectangles = shade_rectangles(screen_width, screen_height, area);
let Some(area) = area else {
return rectangles;
};
rectangles.extend(border_shape_rectangles(area, screen_width, screen_height));
rectangles.extend(selection_corner_rectangles(area));
if draw_mode == SelectorDrawMode::Full {
rectangles.extend(shader_effect_rectangles(area, effect));
rectangles.extend(selection_grid_rectangles(area));
rectangles.extend(move_affordance_rectangles(area));
rectangles.extend(handle_rectangles(area));
rectangles.extend(handle_shadow_rectangles(area));
rectangles.extend(action_button_shape_rectangles(
area,
screen_width,
screen_height,
));
}
rectangles
}
#[cfg(test)]
fn overlay_input_rectangles(screen_width: i32, screen_height: i32) -> Vec<Rectangle> {
rect_i32(0, 0, screen_width, screen_height)
.into_iter()
.collect()
}
fn shade_rectangles(
screen_width: i32,
screen_height: i32,
area: Option<CaptureArea>,
) -> Vec<Rectangle> {
let Some(area) = area else {
return rect_i32(0, 0, screen_width, screen_height)
.into_iter()
.collect();
};
[
rect_i32(0, 0, screen_width, area.y),
rect_i32(
0,
area.bottom(),
screen_width,
screen_height - area.bottom(),
),
rect_i32(0, area.y, area.x, area.height),
rect_i32(
area.right(),
area.y,
screen_width - area.right(),
area.height,
),
]
.into_iter()
.flatten()
.collect()
}
fn border_shape_rectangles(
area: CaptureArea,
screen_width: i32,
screen_height: i32,
) -> Vec<Rectangle> {
let pad = BORDER_SHAPE_PADDING;
let padding = pad.saturating_mul(2);
let thickness = padding.saturating_add(1);
let padded_width = area.width.saturating_add(padding);
let padded_height = area.height.saturating_add(padding);
let left = area.x.saturating_sub(pad);
let top = area.y.saturating_sub(pad);
let right = area.right().saturating_sub(pad);
let bottom = area.bottom().saturating_sub(pad);
[
clipped_rect_i32(
left,
top,
padded_width,
thickness,
screen_width,
screen_height,
),
clipped_rect_i32(
left,
bottom,
padded_width,
thickness,
screen_width,
screen_height,
),
clipped_rect_i32(
left,
top,
thickness,
padded_height,
screen_width,
screen_height,
),
clipped_rect_i32(
right,
top,
thickness,
padded_height,
screen_width,
screen_height,
),
]
.into_iter()
.flatten()
.collect()
}
fn action_button_hit(
point: (i16, i16),
area: CaptureArea,
screen_width: i32,
screen_height: i32,
) -> Option<ActionButton> {
action_button_rectangles(area, screen_width, screen_height)
.find(|(_, rectangle)| rectangle_contains(*rectangle, point))
.map(|(button, _)| button)
}
fn action_control_surface_hit(
point: (i16, i16),
area: CaptureArea,
screen_width: i32,
screen_height: i32,
) -> bool {
action_button_shape_rectangles(area, screen_width, screen_height)
.into_iter()
.any(|rectangle| rectangle_contains(rectangle, point))
}
fn action_button_rectangles(
area: CaptureArea,
screen_width: i32,
screen_height: i32,
) -> impl Iterator<Item = (ActionButton, Rectangle)> {
let (confirm_width, cancel_width) = action_button_widths(screen_width);
let total_width = confirm_width
.saturating_add(ACTION_BUTTON_GAP)
.saturating_add(cancel_width);
let center_x = area.x.saturating_add(area.width / 2);
let max_x = screen_width.saturating_sub(total_width).max(0);
let x = center_x.saturating_sub(total_width / 2).clamp(0, max_x);
let y = action_button_y(area, screen_height);
[
rect_i32(x, y, confirm_width, ACTION_BUTTON_HEIGHT)
.map(|rect| (ActionButton::Confirm, rect)),
rect_i32(
x.saturating_add(confirm_width)
.saturating_add(ACTION_BUTTON_GAP),
y,
cancel_width,
ACTION_BUTTON_HEIGHT,
)
.map(|rect| (ActionButton::Cancel, rect)),
]
.into_iter()
.flatten()
}
fn action_button_widths(screen_width: i32) -> (i32, i32) {
let confirm_width = action_button_intrinsic_width(ActionButton::Confirm);
let cancel_width = action_button_intrinsic_width(ActionButton::Cancel);
let total_width = confirm_width
.saturating_add(ACTION_BUTTON_GAP)
.saturating_add(cancel_width);
if total_width <= screen_width {
return (confirm_width, cancel_width);
}
let available = screen_width.saturating_sub(ACTION_BUTTON_GAP).max(2);
let confirm_width = (available.saturating_mul(2) / 3).max(1);
let cancel_width = available.saturating_sub(confirm_width).max(1);
(confirm_width, cancel_width)
}
fn action_button_shape_rectangles(
area: CaptureArea,
screen_width: i32,
screen_height: i32,
) -> Vec<Rectangle> {
let mut rectangles = Vec::new();
if let Some(panel) = action_button_panel_rectangle(area, screen_width, screen_height) {
if let Some(shadow) = offset_rectangle(panel, 0, ACTION_BUTTON_PANEL_SHADOW_OFFSET) {
rectangles.push(shadow);
}
rectangles.push(panel);
}
for (_, rect) in action_button_rectangles(area, screen_width, screen_height) {
if let Some(shadow) = offset_rectangle(rect, 0, ACTION_BUTTON_SHADOW_OFFSET) {
rectangles.push(shadow);
}
rectangles.push(rect);
}
rectangles
}
fn action_button_panel_rectangle(
area: CaptureArea,
screen_width: i32,
screen_height: i32,
) -> Option<Rectangle> {
let buttons: Vec<_> = action_button_rectangles(area, screen_width, screen_height)
.map(|(_, rectangle)| rectangle)
.collect();
let first = *buttons.first()?;
let (left, top, right, bottom) = buttons.iter().fold(
(
i32::from(first.x),
i32::from(first.y),
i32::from(first.x).saturating_add(i32::from(first.width)),
i32::from(first.y).saturating_add(i32::from(first.height)),
),
|(left, top, right, bottom), rectangle| {
let rect_left = i32::from(rectangle.x);
let rect_top = i32::from(rectangle.y);
let rect_right = rect_left.saturating_add(i32::from(rectangle.width));
let rect_bottom = rect_top.saturating_add(i32::from(rectangle.height));
(
left.min(rect_left),
top.min(rect_top),
right.max(rect_right),
bottom.max(rect_bottom),
)
},
);
clipped_rect_i32(
left.saturating_sub(ACTION_BUTTON_PANEL_PADDING_X),
top.saturating_sub(
ACTION_BUTTON_PANEL_PADDING_Y.saturating_add(ACTION_BUTTON_PANEL_HEADER_HEIGHT),
),
right
.saturating_sub(left)
.saturating_add(ACTION_BUTTON_PANEL_PADDING_X.saturating_mul(2)),
bottom
.saturating_sub(top)
.saturating_add(ACTION_BUTTON_PANEL_PADDING_Y.saturating_mul(2))
.saturating_add(ACTION_BUTTON_PANEL_HEADER_HEIGHT),
screen_width,
screen_height,
)
}
fn action_button_y(area: CaptureArea, screen_height: i32) -> i32 {
let clearance = (HANDLE_SIZE / 2).saturating_add(ACTION_RAIL_GAP);
let max_y = screen_height.saturating_sub(ACTION_BUTTON_HEIGHT).max(0);
let min_below = area.bottom().saturating_add(clearance);
let below = area
.bottom()
.saturating_add(clearance)
.saturating_add(ACTION_BUTTON_PANEL_PADDING_Y);
if min_below <= max_y {
return below.min(max_y);
}
area.y
.saturating_sub(clearance)
.saturating_sub(ACTION_BUTTON_PANEL_PADDING_Y)
.saturating_sub(ACTION_BUTTON_HEIGHT)
.clamp(0, max_y)
}
fn action_button_label(button: ActionButton, button_width: i32) -> &'static str {
let available = button_width.saturating_sub(ACTION_BUTTON_PADDING_X);
match button {
ActionButton::Confirm if label_width(CONFIRM_TEXT.len()) <= available => CONFIRM_TEXT,
ActionButton::Confirm if label_width(CONFIRM_TEXT_COMPACT.len()) <= available => {
CONFIRM_TEXT_COMPACT
}
ActionButton::Confirm => CONFIRM_TEXT_TINY,
ActionButton::Cancel if label_width(CANCEL_TEXT.len()) <= available => CANCEL_TEXT,
ActionButton::Cancel => CANCEL_TEXT_TINY,
}
}
fn action_button_intrinsic_width(button: ActionButton) -> i32 {
let (label_len, min_width) = match button {
ActionButton::Confirm => (CONFIRM_TEXT.len(), CONFIRM_BUTTON_MIN_WIDTH),
ActionButton::Cancel => (CANCEL_TEXT.len(), CANCEL_BUTTON_MIN_WIDTH),
};
label_width(label_len)
.saturating_add(ACTION_BUTTON_PADDING_X.saturating_mul(2))
.max(min_width)
}
#[cfg(test)]
fn label_box_width(label_len: usize) -> i32 {
label_width(label_len).saturating_add(LABEL_BOX_PADDING_X.saturating_mul(2))
}
#[cfg(test)]
fn label_box_height() -> i32 {
LABEL_TEXT_HEIGHT.saturating_add(LABEL_BOX_PADDING_Y.saturating_mul(2))
}
fn rect_i32(x: i32, y: i32, width: i32, height: i32) -> Option<Rectangle> {
if width <= 0 || height <= 0 {
return None;
}
Some(Rectangle {
x: i16::try_from(x).ok()?,
y: i16::try_from(y).ok()?,
width: u16::try_from(width).ok()?,
height: u16::try_from(height).ok()?,
})
}
fn clipped_rect_i32(
x: i32,
y: i32,
width: i32,
height: i32,
screen_width: i32,
screen_height: i32,
) -> Option<Rectangle> {
if width <= 0 || height <= 0 {
return None;
}
let left = x.clamp(0, screen_width.max(0));
let top = y.clamp(0, screen_height.max(0));
let right = x.saturating_add(width).clamp(0, screen_width.max(0));
let bottom = y.saturating_add(height).clamp(0, screen_height.max(0));
rect_i32(left, top, right - left, bottom - top)
}
fn offset_rectangle(rectangle: Rectangle, dx: i32, dy: i32) -> Option<Rectangle> {
rect_i32(
i32::from(rectangle.x).saturating_add(dx),
i32::from(rectangle.y).saturating_add(dy),
i32::from(rectangle.width),
i32::from(rectangle.height),
)
}
#[cfg(test)]
fn area_from_points(start: (i16, i16), end: (i16, i16)) -> Result<Option<CaptureArea>> {
area_from_i32_points(
(i32::from(start.0), i32::from(start.1)),
(i32::from(end.0), i32::from(end.1)),
)
}
fn area_from_screen_points(
start: (i16, i16),
end: (i16, i16),
screen_width: i32,
screen_height: i32,
) -> Result<Option<CaptureArea>> {
area_from_i32_points(
clamp_point_to_screen(start, screen_width, screen_height),
clamp_point_to_screen(end, screen_width, screen_height),
)
}
fn clamp_point_to_screen(point: (i16, i16), screen_width: i32, screen_height: i32) -> (i32, i32) {
(
i32::from(point.0).clamp(0, screen_width.max(0)),
i32::from(point.1).clamp(0, screen_height.max(0)),
)
}
fn area_from_i32_points(start: (i32, i32), end: (i32, i32)) -> Result<Option<CaptureArea>> {
let x1 = start.0.min(end.0);
let y1 = start.1.min(end.1);
let x2 = start.0.max(end.0);
let y2 = start.1.max(end.1);
let width = x2 - x1;
let height = y2 - y1;
if width == 0 || height == 0 {
return Ok(None);
}
CaptureArea::new(x1, y1, width, height).map(Some)
}
fn rectangle_from_area(area: CaptureArea) -> Option<Rectangle> {
Some(Rectangle {
x: i16::try_from(area.x).ok()?,
y: i16::try_from(area.y).ok()?,
width: u16::try_from(area.width).ok()?,
height: u16::try_from(area.height).ok()?,
})
}
fn default_area_around(
point: (i16, i16),
screen_width: i32,
screen_height: i32,
) -> Result<CaptureArea> {
let size = default_selection_size(screen_width, screen_height);
let half = size / 2;
let max_x = (screen_width - size).max(0);
let max_y = (screen_height - size).max(0);
let x = (i32::from(point.0) - half).clamp(0, max_x);
let y = (i32::from(point.1) - half).clamp(0, max_y);
CaptureArea::new(x, y, size, size)
}
fn default_selection_size(screen_width: i32, screen_height: i32) -> i32 {
let short_side = screen_width.min(screen_height).max(1);
(short_side / 4)
.clamp(DEFAULT_SELECTION_MIN_SIZE, DEFAULT_SELECTION_MAX_SIZE)
.min(short_side)
}
fn handle_rectangles(area: CaptureArea) -> Vec<Rectangle> {
handle_rectangles_with_handles(area)
.into_iter()
.map(|(_, rectangle)| rectangle)
.collect()
}
fn handle_shadow_rectangles(area: CaptureArea) -> Vec<Rectangle> {
handle_rectangles_with_handles(area)
.into_iter()
.filter_map(|(_, rectangle)| offset_rectangle(rectangle, 2, 2))
.collect()
}
fn handle_rectangles_with_handles(area: CaptureArea) -> [(Handle, Rectangle); 8] {
let half = HANDLE_SIZE / 2;
let center_x = area.x + area.width / 2;
let center_y = area.y + area.height / 2;
[
(Handle::TopLeft, handle_rectangle(area.x, area.y, half)),
(
Handle::TopRight,
handle_rectangle(area.right(), area.y, half),
),
(
Handle::BottomRight,
handle_rectangle(area.right(), area.bottom(), half),
),
(
Handle::BottomLeft,
handle_rectangle(area.x, area.bottom(), half),
),
(Handle::Top, handle_rectangle(center_x, area.y, half)),
(
Handle::Right,
handle_rectangle(area.right(), center_y, half),
),
(
Handle::Bottom,
handle_rectangle(center_x, area.bottom(), half),
),
(Handle::Left, handle_rectangle(area.x, center_y, half)),
]
}
fn handle_rectangle(x: i32, y: i32, half: i32) -> Rectangle {
Rectangle {
x: clamp_i16(x - half),
y: clamp_i16(y - half),
width: u16::try_from(HANDLE_SIZE).unwrap_or(u16::MAX),
height: u16::try_from(HANDLE_SIZE).unwrap_or(u16::MAX),
}
}
#[cfg(debug_assertions)]
fn autodrag_requested() -> bool {
std::env::var_os(AUTODRAG_ENV).is_some()
}
#[cfg(debug_assertions)]
fn spawn_autodrag_if_requested(root: Window, window: Window) {
let Some(raw) = std::env::var_os(AUTODRAG_ENV) else {
return;
};
let raw = raw.to_string_lossy().into_owned();
let area = match raw.parse::<CaptureArea>() {
Ok(area) => area,
Err(err) => {
eprintln!("warning: ignoring invalid {AUTODRAG_ENV}={raw:?}: {err:#}");
return;
}
};
thread::spawn(move || {
thread::sleep(Duration::from_millis(250));
if let Err(err) = autodrag(root, window, area) {
eprintln!("warning: automated selector drag failed: {err:#}");
}
});
}
#[cfg(debug_assertions)]
fn autodrag(root: Window, window: Window, area: CaptureArea) -> Result<()> {
use x11rb::protocol::xproto::{
BUTTON_PRESS_EVENT, BUTTON_RELEASE_EVENT, KeyButMask, MOTION_NOTIFY_EVENT, Motion,
MotionNotifyEvent,
};
let (conn, _) = connect(None).context("failed to connect to X11 display for autodrag")?;
let start_x = i16::try_from(area.x).context("autodrag x is outside X11 coordinate range")?;
let start_y = i16::try_from(area.y).context("autodrag y is outside X11 coordinate range")?;
let end_x = i16::try_from(area.x + area.width)
.context("autodrag right edge is outside X11 coordinate range")?;
let end_y = i16::try_from(area.y + area.height)
.context("autodrag bottom edge is outside X11 coordinate range")?;
conn.send_event(
false,
window,
EventMask::BUTTON_PRESS,
ButtonPressEvent {
response_type: BUTTON_PRESS_EVENT,
detail: 1,
sequence: 0,
time: CURRENT_TIME,
root,
event: window,
child: NONE,
root_x: start_x,
root_y: start_y,
event_x: start_x,
event_y: start_y,
state: KeyButMask::default(),
same_screen: true,
},
)?;
conn.flush()?;
thread::sleep(Duration::from_millis(100));
conn.send_event(
false,
window,
EventMask::POINTER_MOTION,
MotionNotifyEvent {
response_type: MOTION_NOTIFY_EVENT,
detail: Motion::NORMAL,
sequence: 0,
time: CURRENT_TIME,
root,
event: window,
child: NONE,
root_x: end_x,
root_y: end_y,
event_x: end_x,
event_y: end_y,
state: KeyButMask::BUTTON1,
same_screen: true,
},
)?;
conn.flush()?;
thread::sleep(Duration::from_millis(100));
conn.send_event(
false,
window,
EventMask::BUTTON_RELEASE,
ButtonPressEvent {
response_type: BUTTON_RELEASE_EVENT,
detail: 1,
sequence: 0,
time: CURRENT_TIME,
root,
event: window,
child: NONE,
root_x: end_x,
root_y: end_y,
event_x: end_x,
event_y: end_y,
state: KeyButMask::BUTTON1,
same_screen: true,
},
)?;
conn.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn area_from_drag_points_normalizes_direction() {
assert_eq!(
area_from_points((300, 200), (100, 50)).unwrap(),
Some(CaptureArea {
x: 100,
y: 50,
width: 200,
height: 150,
})
);
}
#[test]
fn area_from_drag_points_rejects_click_without_drag() {
assert_eq!(area_from_points((100, 50), (100, 50)).unwrap(), None);
}
#[test]
fn button_mapping_distinguishes_primary_and_cancel() {
assert!(is_primary_button_detail(1));
assert!(!is_primary_button_detail(3));
assert!(is_cancel_button_detail(3));
assert!(!is_cancel_button_detail(1));
}
#[test]
fn area_from_screen_points_clamps_to_screen_bounds() {
assert_eq!(
area_from_screen_points((-10, -20), (150, 90), 100, 80).unwrap(),
Some(CaptureArea {
x: 0,
y: 0,
width: 100,
height: 80,
})
);
}
#[test]
fn update_drag_clamps_new_selection_to_screen_bounds() {
assert_eq!(
update_drag(Drag::New { start: (50, 40) }, (150, 90), 100, 80).unwrap(),
Some(CaptureArea {
x: 50,
y: 40,
width: 50,
height: 40,
})
);
}
#[test]
fn default_area_around_click_is_centered() {
assert_eq!(
default_area_around((100, 100), 300, 300).unwrap(),
CaptureArea {
x: 60,
y: 60,
width: 80,
height: 80,
}
);
}
#[test]
fn default_area_around_click_stays_inside_screen() {
assert_eq!(
default_area_around((5, 5), 100, 100).unwrap(),
CaptureArea {
x: 0,
y: 0,
width: 80,
height: 80,
}
);
}
#[test]
fn default_area_shrinks_to_tiny_screen() {
assert_eq!(
default_area_around((5, 5), 30, 20).unwrap(),
CaptureArea {
x: 0,
y: 0,
width: 20,
height: 20,
}
);
}
#[test]
fn default_selection_size_scales_with_screen_size() {
assert_eq!(default_selection_size(100, 100), 80);
assert_eq!(default_selection_size(1000, 500), 125);
assert_eq!(default_selection_size(3840, 2160), 480);
}
#[test]
fn default_area_around_click_scales_on_large_screen() {
assert_eq!(
default_area_around((500, 250), 1000, 500).unwrap(),
CaptureArea {
x: 438,
y: 188,
width: 125,
height: 125,
}
);
}
#[test]
fn handle_rectangles_are_centered_on_corners_and_edges() {
let handles = handle_rectangles(CaptureArea::new(20, 10, 30, 40).unwrap());
assert_eq!(handles.len(), 8);
assert_eq!(rect_tuple(handles[0]), (11, 1, 18, 18));
assert_eq!(rect_tuple(handles[1]), (41, 1, 18, 18));
assert_eq!(rect_tuple(handles[2]), (41, 41, 18, 18));
assert_eq!(rect_tuple(handles[3]), (11, 41, 18, 18));
assert_eq!(rect_tuple(handles[4]), (26, 1, 18, 18));
assert_eq!(rect_tuple(handles[5]), (41, 21, 18, 18));
assert_eq!(rect_tuple(handles[6]), (26, 41, 18, 18));
assert_eq!(rect_tuple(handles[7]), (11, 21, 18, 18));
}
#[test]
fn hit_test_detects_handles_inside_and_outside() {
let area = CaptureArea::new(20, 10, 30, 40).unwrap();
assert_eq!(hit_test((20, 10), area), Hit::Handle(Handle::TopLeft));
assert_eq!(hit_test((35, 25), area), Hit::Inside);
assert_eq!(hit_test((80, 70), area), Hit::Outside);
}
#[test]
fn hit_test_keeps_area_edges_resizable() {
let area = CaptureArea::new(20, 10, 30, 40).unwrap();
assert_eq!(hit_test((50, 18), area), Hit::Handle(Handle::TopRight));
assert_eq!(hit_test((35, 50), area), Hit::Handle(Handle::Bottom));
}
#[test]
fn rectangle_contains_treats_right_and_bottom_as_exclusive() {
let rectangle = Rectangle {
x: 10,
y: 20,
width: 30,
height: 40,
};
assert!(rectangle_contains(rectangle, (39, 59)));
assert!(!rectangle_contains(rectangle, (40, 59)));
assert!(!rectangle_contains(rectangle, (39, 60)));
}
#[test]
fn clipped_rect_saturates_edges_before_clamping() {
assert_eq!(
rect_tuple(clipped_rect_i32(90, 90, i32::MAX, i32::MAX, 100, 100).unwrap()),
(90, 90, 10, 10)
);
}
#[test]
fn hit_test_detects_edge_handles() {
let area = CaptureArea::new(20, 10, 30, 40).unwrap();
assert_eq!(hit_test((35, 10), area), Hit::Handle(Handle::Top));
assert_eq!(hit_test((50, 30), area), Hit::Handle(Handle::Right));
assert_eq!(hit_test((35, 50), area), Hit::Handle(Handle::Bottom));
assert_eq!(hit_test((20, 30), area), Hit::Handle(Handle::Left));
}
#[test]
fn cursor_kind_tracks_selection_hit_target() {
let area = CaptureArea::new(20, 10, 30, 40).unwrap();
assert_eq!(
cursor_kind_for_point((5, 5), None, 800, 600),
CursorKind::Crosshair
);
assert_eq!(
cursor_kind_for_point((300, 70), Some(area), 800, 600),
CursorKind::Crosshair
);
assert_eq!(
cursor_kind_for_point((35, 25), Some(area), 800, 600),
CursorKind::Move
);
assert_eq!(
cursor_kind_for_point((50, 50), Some(area), 800, 600),
CursorKind::Resize(Handle::BottomRight)
);
assert_eq!(
cursor_kind_for_point((35, 10), Some(area), 800, 600),
CursorKind::Resize(Handle::Top)
);
}
#[test]
fn cursor_kind_tracks_action_controls() {
let area = CaptureArea::new(100, 100, 300, 200).unwrap();
let confirm = action_button_rectangles(area, 800, 600)
.find(|(button, _)| *button == ActionButton::Confirm)
.map(|(_, rectangle)| rectangle)
.unwrap();
let panel = action_button_panel_rectangle(area, 800, 600).unwrap();
let button_point = (confirm.x + 1, confirm.y + 1);
let panel_point = (panel.x + 1, panel.y + 1);
assert_eq!(
cursor_kind_for_point(button_point, Some(area), 800, 600),
CursorKind::Pointer
);
assert_eq!(action_button_hit(panel_point, area, 800, 600), None);
assert_eq!(
cursor_kind_for_point(panel_point, Some(area), 800, 600),
CursorKind::Default
);
}
#[test]
fn cursor_kind_stays_locked_to_active_drag() {
let area = CaptureArea::new(20, 10, 30, 40).unwrap();
assert_eq!(
cursor_kind_for_state(
(90, 70),
Some(area),
Some(Drag::Resize {
handle: Handle::TopLeft,
original: area,
}),
800,
600,
),
CursorKind::Resize(Handle::TopLeft)
);
assert_eq!(
cursor_kind_for_state(
(90, 70),
Some(area),
Some(Drag::Move {
start: (35, 25),
original: area,
}),
800,
600,
),
CursorKind::Move
);
}
#[test]
fn move_area_clamps_to_screen() {
assert_eq!(
move_area(CaptureArea::new(20, 10, 30, 40).unwrap(), 100, 100, 100, 80),
CaptureArea {
x: 70,
y: 40,
width: 30,
height: 40,
}
);
}
#[test]
fn move_area_saturates_extreme_delta_before_clamping() {
assert_eq!(
move_area(
CaptureArea::new(i32::MAX - 20, 10, 10, 10).unwrap(),
i32::MAX,
i32::MIN,
i32::MAX,
100,
),
CaptureArea {
x: i32::MAX - 10,
y: 0,
width: 10,
height: 10,
}
);
}
#[test]
fn resize_area_by_delta_clamps_to_screen() {
assert_eq!(
resize_area_by_delta(CaptureArea::new(80, 60, 30, 40).unwrap(), 100, 100, 100, 80)
.unwrap(),
CaptureArea {
x: 80,
y: 60,
width: 20,
height: 20,
}
);
}
#[test]
fn resize_area_by_delta_saturates_extreme_delta_before_clamping() {
assert_eq!(
resize_area_by_delta(
CaptureArea::new(0, 0, i32::MAX - 10, 10).unwrap(),
i32::MAX,
i32::MIN,
i32::MAX,
100,
)
.unwrap(),
CaptureArea {
x: 0,
y: 0,
width: i32::MAX,
height: 1,
}
);
}
#[test]
fn resize_interval_saturates_extreme_endpoints() {
assert_eq!(
resize_interval(i32::MAX, i32::MIN, i32::MAX, true),
(i32::MIN, i32::MAX)
);
assert_eq!(
resize_interval(i32::MIN, i32::MAX, i32::MAX, false),
(i32::MIN, i32::MAX)
);
assert_eq!(
resize_interval(i32::MAX, i32::MAX, i32::MAX, false),
(i32::MAX - 1, 1)
);
}
#[test]
fn resize_area_from_corner_normalizes_crossed_edges() {
assert_eq!(
resize_area(
CaptureArea::new(20, 10, 30, 40).unwrap(),
Handle::TopLeft,
(80, 70),
100,
80,
)
.unwrap(),
CaptureArea {
x: 50,
y: 50,
width: 30,
height: 20,
}
);
}
#[test]
fn resize_area_from_edge_keeps_opposite_edge_fixed() {
assert_eq!(
resize_area(
CaptureArea::new(20, 10, 30, 40).unwrap(),
Handle::Left,
(10, 30),
100,
80,
)
.unwrap(),
CaptureArea {
x: 10,
y: 10,
width: 40,
height: 40,
}
);
assert_eq!(
resize_area(
CaptureArea::new(20, 10, 30, 40).unwrap(),
Handle::Bottom,
(35, 70),
100,
80,
)
.unwrap(),
CaptureArea {
x: 20,
y: 10,
width: 30,
height: 60,
}
);
}
#[test]
fn resize_area_collapses_corner_to_one_pixel_instead_of_erroring() {
assert_eq!(
resize_area(
CaptureArea::new(20, 10, 30, 40).unwrap(),
Handle::TopLeft,
(50, 50),
100,
80,
)
.unwrap(),
CaptureArea {
x: 49,
y: 49,
width: 1,
height: 1,
}
);
assert_eq!(
resize_area(
CaptureArea::new(20, 10, 30, 40).unwrap(),
Handle::BottomRight,
(20, 10),
100,
80,
)
.unwrap(),
CaptureArea {
x: 20,
y: 10,
width: 1,
height: 1,
}
);
}
#[test]
fn resize_area_collapses_edge_to_one_pixel_instead_of_erroring() {
assert_eq!(
resize_area(
CaptureArea::new(20, 10, 30, 40).unwrap(),
Handle::Left,
(50, 30),
100,
80,
)
.unwrap(),
CaptureArea {
x: 49,
y: 10,
width: 1,
height: 40,
}
);
assert_eq!(
resize_area(
CaptureArea::new(20, 10, 30, 40).unwrap(),
Handle::Top,
(35, 50),
100,
80,
)
.unwrap(),
CaptureArea {
x: 20,
y: 49,
width: 30,
height: 1,
}
);
}
#[test]
fn update_drag_creates_new_selection() {
assert_eq!(
update_drag(Drag::New { start: (10, 20) }, (50, 70), 100, 100).unwrap(),
Some(CaptureArea {
x: 10,
y: 20,
width: 40,
height: 50,
})
);
}
#[test]
fn key_action_maps_arrows_to_move() {
assert_eq!(
key_action_from_keysyms(&[XK_RIGHT], KeyButMask::default()),
KeyAction::Move { dx: 1, dy: 0 }
);
assert_eq!(
key_action_from_keysyms(&[XK_UP], KeyButMask::CONTROL),
KeyAction::Move { dx: 0, dy: -10 }
);
}
#[test]
fn key_action_maps_shift_arrows_to_resize() {
assert_eq!(
key_action_from_keysyms(&[XK_LEFT], KeyButMask::SHIFT),
KeyAction::Resize { dw: -1, dh: 0 }
);
assert_eq!(
key_action_from_keysyms(&[XK_DOWN], KeyButMask::SHIFT | KeyButMask::CONTROL),
KeyAction::Resize { dw: 0, dh: 10 }
);
}
#[test]
fn key_action_maps_confirm_cancel_and_reset() {
assert_eq!(
key_action_from_keysyms(&[XK_RETURN], KeyButMask::default()),
KeyAction::Confirm
);
assert_eq!(
key_action_from_keysyms(&[XK_ESCAPE], KeyButMask::default()),
KeyAction::Cancel
);
assert_eq!(
key_action_from_keysyms(&[XK_DELETE], KeyButMask::default()),
KeyAction::Reset
);
}
#[test]
fn action_buttons_are_clickable_after_selection() {
let area = CaptureArea::new(100, 100, 300, 200).unwrap();
let buttons: Vec<_> = action_button_rectangles(area, 800, 600).collect();
assert_eq!(buttons.len(), 2);
for (button, rectangle) in buttons {
let point = (rectangle.x + 1, rectangle.y + 1);
assert_eq!(action_button_hit(point, area, 800, 600), Some(button));
}
}
#[test]
fn action_buttons_have_comfortable_targets() {
let area = CaptureArea::new(100, 100, 300, 200).unwrap();
let buttons: Vec<_> = action_button_rectangles(area, 800, 600).collect();
let confirm = buttons
.iter()
.find(|(button, _)| *button == ActionButton::Confirm)
.map(|(_, rectangle)| *rectangle)
.unwrap();
let cancel = buttons
.iter()
.find(|(button, _)| *button == ActionButton::Cancel)
.map(|(_, rectangle)| *rectangle)
.unwrap();
assert!(confirm.width >= u16::try_from(CONFIRM_BUTTON_MIN_WIDTH).unwrap());
assert!(cancel.width >= u16::try_from(CANCEL_BUTTON_MIN_WIDTH).unwrap());
assert!(confirm.height >= 40);
assert!(cancel.height >= 40);
}
#[test]
fn action_buttons_fit_narrow_screens() {
let area = CaptureArea::new(20, 10, 60, 50).unwrap();
let buttons: Vec<_> = action_button_rectangles(area, 140, 120).collect();
assert_eq!(buttons.len(), 2);
for (button, rectangle) in buttons {
let right = i32::from(rectangle.x).saturating_add(i32::from(rectangle.width));
assert!(i32::from(rectangle.x) >= 0);
assert!(right <= 140);
assert_eq!(
action_button_hit((rectangle.x + 1, rectangle.y + 1), area, 140, 120),
Some(button)
);
}
}
#[test]
fn action_button_labels_compact_for_narrow_targets() {
assert_eq!(
action_button_label(ActionButton::Confirm, 164),
CONFIRM_TEXT
);
assert_eq!(
action_button_label(ActionButton::Confirm, 86),
CONFIRM_TEXT_COMPACT
);
assert_eq!(
action_button_label(ActionButton::Confirm, 42),
CONFIRM_TEXT_TINY
);
assert_eq!(action_button_label(ActionButton::Cancel, 96), CANCEL_TEXT);
assert_eq!(
action_button_label(ActionButton::Cancel, 42),
CANCEL_TEXT_TINY
);
}
#[test]
fn action_buttons_move_above_selection_when_bottom_is_full() {
let area = CaptureArea::new(100, 550, 300, 40).unwrap();
let buttons: Vec<_> = action_button_rectangles(area, 800, 600).collect();
assert!(
buttons
.iter()
.all(|(_, rectangle)| i32::from(rectangle.y) < area.y)
);
}
#[test]
fn action_panel_stays_clear_of_resize_handles() {
let area = CaptureArea::new(100, 100, 300, 200).unwrap();
let panel = action_button_panel_rectangle(area, 800, 600).unwrap();
let clearance = (HANDLE_SIZE / 2).saturating_add(ACTION_RAIL_GAP);
assert!(i32::from(panel.y) >= area.bottom().saturating_add(clearance));
}
#[test]
fn primary_press_ignores_action_panel_padding() {
let area = CaptureArea::new(100, 100, 300, 200).unwrap();
let panel = action_button_panel_rectangle(area, 800, 600).unwrap();
let panel_point = (panel.x + 1, panel.y + 1);
let outside_point = (10, 10);
assert_eq!(action_button_hit(panel_point, area, 800, 600), None);
assert!(action_control_surface_hit(panel_point, area, 800, 600));
assert_eq!(
drag_for_primary_press(panel_point, Some(area), 800, 600),
None
);
assert_eq!(
drag_for_primary_press(outside_point, Some(area), 800, 600),
Some(Drag::New {
start: outside_point
})
);
}
#[test]
fn confirm_on_release_mode_accepts_selection_immediately() {
let area = CaptureArea::new(10, 20, 160, 120).unwrap();
assert_eq!(
selection_on_release(SelectorMode::ConfirmOnRelease, Some(area)),
Some(area)
);
assert_eq!(
selection_on_release(SelectorMode::Editable, Some(area)),
None
);
assert_eq!(
selection_on_release(SelectorMode::ConfirmOnRelease, None),
None
);
}
#[test]
fn existing_selection_is_clamped_to_current_screen() {
let area = CaptureArea::new(700, 500, 400, 300).unwrap();
assert_eq!(
move_area(area, 0, 0, 800, 600),
CaptureArea {
x: 400,
y: 300,
width: 400,
height: 300,
}
);
}
#[test]
fn label_position_prefers_above_selection() {
let (x, y) = label_position(
CaptureArea::new(100, 100, 300, 200).unwrap(),
1920,
1080,
10,
);
assert_eq!(x, 112);
assert_eq!(y, 88);
}
#[test]
fn label_position_stays_visible_near_top_left() {
let (x, y) = label_position(CaptureArea::new(0, 0, 300, 200).unwrap(), 320, 240, 10);
assert_eq!(x, 12);
assert!(y >= 14);
}
#[test]
fn label_position_stays_visible_near_right_edge() {
let (x, _) = label_position(CaptureArea::new(280, 100, 30, 30).unwrap(), 320, 240, 10);
assert!(x <= 232);
}
#[test]
fn help_label_position_is_centered() {
let (x, y) = help_label_position(1000, 500, HELP_TEXT.len());
assert_eq!(x, 352);
assert_eq!(y, 250);
}
#[test]
fn help_label_position_stays_visible_on_tiny_screen() {
let (x, y) = help_label_position(40, 20, HELP_TEXT.len());
assert_eq!(x, 0);
assert_eq!(y, 14);
}
#[test]
fn label_width_saturates_large_lengths() {
assert_eq!(
label_width((i32::MAX as usize / LABEL_CHAR_WIDTH as usize) + 1),
i32::MAX
);
assert_eq!(label_width(usize::MAX), i32::MAX);
}
#[test]
fn label_box_dimensions_saturate() {
assert_eq!(label_box_width(usize::MAX), i32::MAX);
assert_eq!(
label_box_height(),
LABEL_TEXT_HEIGHT + LABEL_BOX_PADDING_Y * 2
);
}
#[test]
fn label_positions_tolerate_saturated_label_widths() {
let tiny_area = CaptureArea::new(0, 0, 40, 20).unwrap();
assert_eq!(help_label_position(40, 20, usize::MAX), (0, 14));
assert_eq!(
shortcut_label_position(tiny_area, 40, 20, usize::MAX),
(0, 14)
);
assert_eq!(
label_position(
CaptureArea::new(i32::MAX - 1, i32::MAX - 1, 1, 1).unwrap(),
i32::MAX,
i32::MAX,
usize::MAX,
),
(0, i16::MAX)
);
}
#[test]
fn label_overlap_saturates_extreme_geometry() {
assert!(label_baselines_overlap(i32::MAX, i32::MAX));
assert!(label_overlaps_area(
0,
LABEL_TEXT_HEIGHT,
i32::MAX,
CaptureArea::new(100, 0, 1, 1).unwrap(),
));
}
#[test]
fn shortcut_label_position_uses_bottom_when_dimension_is_above() {
let (x, y) = shortcut_label_position(
CaptureArea::new(100, 100, 300, 200).unwrap(),
1000,
500,
SHORTCUT_TEXT.len(),
);
assert_eq!(x, 296);
assert_eq!(y, 488);
}
#[test]
fn shortcut_label_position_uses_top_when_dimension_is_at_bottom() {
let (x, y) = shortcut_label_position(
CaptureArea::new(0, 0, 1000, 500).unwrap(),
1000,
500,
SHORTCUT_TEXT.len(),
);
assert_eq!(x, 296);
assert_eq!(y, 26);
}
#[test]
fn shortcut_label_position_uses_bottom_when_top_would_cover_selection() {
let (x, y) = shortcut_label_position(
CaptureArea::new(20, 10, 30, 40).unwrap(),
100,
80,
SHORTCUT_TEXT.len(),
);
assert_eq!(x, 0);
assert_eq!(y, 68);
}
#[test]
fn shortcut_label_position_uses_top_when_bottom_would_cover_selection() {
let (x, y) = shortcut_label_position(
CaptureArea::new(20, 50, 30, 25).unwrap(),
100,
80,
SHORTCUT_TEXT.len(),
);
assert_eq!(x, 0);
assert_eq!(y, 26);
}
#[test]
fn shortcut_label_position_stays_visible_on_tiny_screen() {
let (x, y) = shortcut_label_position(
CaptureArea::new(0, 0, 40, 20).unwrap(),
40,
20,
SHORTCUT_TEXT.len(),
);
assert_eq!(x, 0);
assert_eq!(y, 14);
}
#[test]
fn shade_rectangles_cut_out_selection() {
let rectangles = shade_rectangles(
100,
80,
Some(CaptureArea {
x: 20,
y: 10,
width: 30,
height: 40,
}),
);
assert_eq!(rectangles.len(), 4);
assert_eq!(rect_tuple(rectangles[0]), (0, 0, 100, 10));
assert_eq!(rect_tuple(rectangles[1]), (0, 50, 100, 30));
assert_eq!(rect_tuple(rectangles[2]), (0, 10, 20, 40));
assert_eq!(rect_tuple(rectangles[3]), (50, 10, 50, 40));
}
#[test]
fn shade_rectangles_skip_zero_sized_edges() {
let rectangles = shade_rectangles(
100,
80,
Some(CaptureArea {
x: 0,
y: 0,
width: 100,
height: 40,
}),
);
assert_eq!(rectangles.len(), 1);
assert_eq!(rect_tuple(rectangles[0]), (0, 40, 100, 40));
}
#[test]
fn overlay_shape_keeps_selection_clear_and_decorations_visible() {
let area = CaptureArea {
x: 20,
y: 10,
width: 60,
height: 50,
};
let rectangles = overlay_shape_rectangles(140, 120, Some(area));
assert!(rectangles_contain_point(&rectangles, (0, 0)));
assert!(rectangles_contain_point(&rectangles, (20, 10)));
assert!(rectangles_contain_point(&rectangles, (40, 35)));
assert!(rectangles_contain_point(&rectangles, (52, 72)));
assert!(!rectangles_contain_point(&rectangles, (55, 36)));
}
#[test]
fn overlay_input_covers_clear_selection_for_drag_move() {
let rectangles = overlay_input_rectangles(140, 120);
assert!(rectangles_contain_point(&rectangles, (55, 36)));
assert!(rectangles_contain_point(&rectangles, (0, 0)));
}
#[test]
fn dragging_shape_skips_heavy_controls() {
let area = CaptureArea::new(20, 10, 60, 50).unwrap();
let full = overlay_shape_rectangles_for_mode(
140,
120,
Some(area),
SelectorDrawMode::Full,
VideoEffect::None,
);
let dragging = overlay_shape_rectangles_for_mode(
140,
120,
Some(area),
SelectorDrawMode::Dragging,
VideoEffect::None,
);
assert!(full.len() > dragging.len());
assert!(rectangles_contain_point(&full, (40, 35)));
assert!(!rectangles_contain_point(&dragging, (40, 35)));
}
#[test]
fn border_shape_saturates_extreme_padded_dimensions() {
let rectangles =
border_shape_rectangles(CaptureArea::new(0, 0, i32::MAX, 1).unwrap(), 100, 10);
assert_eq!(rectangles.len(), 3);
assert!(rectangles.iter().all(|rectangle| rectangle.width > 0));
assert!(rectangles.iter().all(|rectangle| rectangle.height > 0));
assert!(rectangles_contain_point(&rectangles, (0, 0)));
}
#[test]
fn overlay_shape_keeps_full_screen_label_visible() {
let rectangles = overlay_shape_rectangles(
100,
80,
Some(CaptureArea {
x: 0,
y: 0,
width: 100,
height: 80,
}),
);
assert!(rectangles_contain_point(&rectangles, (8, 72)));
assert!(rectangles_contain_point(&rectangles, (90, 22)));
assert!(!rectangles_contain_point(&rectangles, (90, 60)));
}
#[test]
fn overlay_shape_exposes_full_screen_action_panel() {
let rectangles = overlay_shape_rectangles(
300,
200,
Some(CaptureArea {
x: 0,
y: 0,
width: 300,
height: 200,
}),
);
assert!(rectangles_contain_point(&rectangles, (34, 30)));
}
#[test]
fn selection_grid_adds_subtle_interior_guides() {
let rectangles = selection_grid_rectangles(CaptureArea::new(20, 10, 60, 50).unwrap());
assert_eq!(rectangles.len(), 4);
assert!(rectangles_contain_point(&rectangles, (40, 35)));
assert!(rectangles_contain_point(&rectangles, (55, 26)));
assert!(!rectangles_contain_point(&rectangles, (55, 36)));
}
#[test]
fn move_affordance_is_small_and_skips_tiny_crops() {
let rectangles = move_affordance_rectangles(CaptureArea::new(20, 10, 160, 120).unwrap());
assert_eq!(rectangles.len(), 4);
assert!(rectangles.iter().all(|rectangle| {
i32::from(rectangle.width) <= MOVE_AFFORDANCE_LENGTH
|| i32::from(rectangle.height) <= MOVE_AFFORDANCE_LENGTH
}));
assert!(move_affordance_rectangles(CaptureArea::new(20, 10, 80, 60).unwrap()).is_empty());
}
fn rect_tuple(rect: Rectangle) -> (i16, i16, u16, u16) {
(rect.x, rect.y, rect.width, rect.height)
}
fn rectangles_contain_point(rectangles: &[Rectangle], point: (i16, i16)) -> bool {
rectangles
.iter()
.any(|rectangle| rectangle_contains(*rectangle, point))
}
}