use std::sync::mpsc;
#[derive(Debug, thiserror::Error)]
pub enum PlatformError {
#[error("not supported on this platform/session: {what}")]
Unsupported { what: &'static str },
#[error("portal request denied or unavailable: {reason}")]
Portal { reason: String },
#[error("monitor not found: {0:?}")]
MonitorNotFound(MonitorId),
#[error("backend i/o error")]
Io(#[from] std::io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, PlatformError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct MonitorId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub w: u32,
pub h: u32,
}
impl Rect {
pub const fn new(x: i32, y: i32, w: u32, h: u32) -> Self {
Self { x, y, w, h }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Point {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
}
#[derive(Debug, Clone)]
pub struct MonitorInfo {
pub id: MonitorId,
pub name: String,
pub bounds: Rect,
pub scale_factor: f64,
pub is_primary: bool,
}
#[derive(Debug, Clone)]
pub struct Frame {
pub width: u32,
pub height: u32,
pub scale_factor: f64,
pub bounds: Rect,
pub pixels: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PixelFormat {
Bgra8,
Bgrx8,
Rgba8,
Rgbx8,
Xrgb8,
Xbgr8,
}
#[derive(Debug, Clone)]
pub struct NativeFrame {
pub width: u32,
pub height: u32,
pub stride: u32,
pub format: PixelFormat,
pub bounds: Rect,
pub scale_factor: f64,
pub pixels: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct Hud {
pub kind: HudKind,
pub background: Color,
pub foreground: Color,
pub toast: Option<HudToast>,
pub guides: Vec<Guide>,
pub stuck_measurements: Vec<StuckMeasurement>,
pub held_rects: Vec<HeldRect>,
pub cursor_in_rect: bool,
pub move_cursor_at: Option<(f64, f64)>,
pub cursor_kind: CursorKind,
pub align_mode: bool,
pub context_menu: Option<HudContextMenu>,
pub guide_color: Color,
pub alternative_guide_color: Color,
pub primary_fg: Color,
pub alternate_fg: Color,
pub measurement_format: HudMeasurementFormat,
pub show_cursor: bool,
pub corner_indicator: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HudMeasurementFormat {
pub unit_suffix: String,
pub rounding: HudRounding,
pub scale_factor: f64,
pub wh_indicators: bool,
pub aspect_in_area: bool,
pub aspect_in_distance: bool,
pub aspect_mode: vernier_core::AspectMode,
pub dimension_divisor: f64,
}
impl Default for HudMeasurementFormat {
fn default() -> Self {
Self {
unit_suffix: "px".to_string(),
rounding: HudRounding::PointsRounded,
scale_factor: 1.0,
wh_indicators: false,
aspect_in_area: true,
aspect_in_distance: false,
aspect_mode: vernier_core::AspectMode::Automatic,
dimension_divisor: 1.0,
}
}
}
impl HudMeasurementFormat {
pub fn format_number(&self, value_logical: f64) -> String {
let divisor = if self.dimension_divisor > 0.0 {
self.dimension_divisor
} else {
1.0
};
let value = value_logical / divisor;
match self.rounding {
HudRounding::Points => {
let r = (value * 10.0).round() / 10.0;
if (r - r.round()).abs() < f64::EPSILON {
format!("{}", r as i64)
} else {
format!("{r:.1}")
}
}
HudRounding::PointsRounded => format!("{}", value.round() as i64),
HudRounding::ScreenPixels => {
format!("{}", (value * self.scale_factor).round() as i64)
}
}
}
pub fn format_value(&self, value_logical: f64) -> String {
format!("{}{}", self.format_number(value_logical), self.unit_suffix)
}
pub fn format_wh(&self, w_logical: f64, h_logical: f64) -> String {
if self.wh_indicators {
format!(
"W: {}{} \u{00D7} H: {}{}",
self.format_number(w_logical),
self.unit_suffix,
self.format_number(h_logical),
self.unit_suffix,
)
} else {
format!(
"{}{} \u{00D7} {}{}",
self.format_number(w_logical),
self.unit_suffix,
self.format_number(h_logical),
self.unit_suffix,
)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HudRounding {
Points,
PointsRounded,
ScreenPixels,
}
#[derive(Debug, Clone)]
pub struct HudContextMenu {
pub origin: (f64, f64),
pub width: f64,
pub items: Vec<HudContextMenuItem>,
pub hovered: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct HudContextMenuItem {
pub label: String,
pub shortcut: Option<String>,
pub icon: HudContextMenuIcon,
pub divider_after: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HudContextMenuIcon {
GuideH,
GuideV,
StuckH,
StuckV,
Camera,
Background,
Restore,
Clear,
Close,
Settings,
}
#[derive(Debug, Clone, Copy)]
pub struct HeldRect {
pub rect_start: (f64, f64),
pub rect_end: (f64, f64),
pub camera_armed: bool,
pub color_alternate: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct StuckMeasurement {
pub axis: GuideAxis,
pub at: f64,
pub start: f64,
pub end: f64,
pub pill_offset: (f64, f64),
pub color_alternate: bool,
pub hovered: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct Guide {
pub axis: GuideAxis,
pub position: i32,
pub color_alternate: bool,
pub hovered: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GuideAxis {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CursorKind {
Move,
ResizeNS,
ResizeEW,
ResizeNWSE,
ResizeNESW,
}
#[derive(Debug, Clone)]
pub struct HudToast {
pub text: String,
}
impl Hud {
pub fn hover(cursor: (f64, f64)) -> Self {
Self {
kind: HudKind::Hover {
cursor,
edges: [None; 4],
},
background: Color::TRANSPARENT,
foreground: Color::rgba(0xFF, 0x5C, 0x5C, 0xF5),
toast: None,
guides: Vec::new(),
stuck_measurements: Vec::new(),
held_rects: Vec::new(),
cursor_in_rect: false,
move_cursor_at: None,
cursor_kind: CursorKind::Move,
align_mode: false,
context_menu: None,
guide_color: Color::rgba(0x42, 0x9C, 0xFF, 0xF5),
alternative_guide_color: Color::rgba(0xFF, 0xA9, 0x4A, 0xF0),
primary_fg: Color::rgba(0xFF, 0x5C, 0x5C, 0xF5),
alternate_fg: Color::rgba(0x10, 0x10, 0x10, 0xF5),
measurement_format: HudMeasurementFormat::default(),
show_cursor: true,
corner_indicator: None,
}
}
}
#[derive(Debug, Clone)]
pub enum HudKind {
Hover {
cursor: (f64, f64),
edges: [Option<HudEdge>; 4],
},
Drawing { start: (f64, f64), cursor: (f64, f64) },
Held {
rect_start: (f64, f64),
rect_end: (f64, f64),
cursor: (f64, f64),
edges: [Option<HudEdge>; 4],
camera_armed: bool,
cursor_in_rect: bool,
},
None,
}
#[derive(Debug, Clone, Copy)]
pub struct HudEdge {
pub axis: HudAxis,
pub position: (f64, f64),
pub distance_px: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HudAxis {
Left,
Right,
Up,
Down,
}
#[derive(Debug, Clone)]
pub struct AppIdentity {
pub id: String,
pub display_name: String,
pub executable: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct HotkeyId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Accelerator {
pub modifiers: Modifiers,
pub key: Key,
}
impl Default for Accelerator {
fn default() -> Self {
Self {
modifiers: Modifiers::CTRL | Modifiers::SHIFT,
key: Key::Char('f'),
}
}
}
impl Accelerator {
pub fn parse(s: &str) -> Option<Self> {
let mut modifiers = Modifiers::NONE;
let mut key: Option<Key> = None;
for tok_raw in s.split('+') {
let tok = tok_raw.trim();
if tok.is_empty() {
continue;
}
let lower = tok.to_ascii_lowercase();
match lower.as_str() {
"shift" => modifiers |= Modifiers::SHIFT,
"ctrl" | "control" => modifiers |= Modifiers::CTRL,
"alt" | "opt" | "option" => modifiers |= Modifiers::ALT,
"super" | "meta" | "cmd" | "command" | "win" => modifiers |= Modifiers::META,
"esc" | "escape" => key = Some(Key::Escape),
"enter" | "return" => key = Some(Key::Enter),
"space" => key = Some(Key::Space),
"tab" => key = Some(Key::Tab),
"backspace" => key = Some(Key::Backspace),
"delete" | "del" => key = Some(Key::Delete),
"up" => key = Some(Key::Up),
"down" => key = Some(Key::Down),
"left" => key = Some(Key::Left),
"right" => key = Some(Key::Right),
"plus" | "kp_add" => key = Some(Key::Char('+')),
"minus" | "kp_subtract" => key = Some(Key::Char('-')),
"equal" | "equals" => key = Some(Key::Char('=')),
"underscore" => key = Some(Key::Char('_')),
other => {
if let Some(rest) = other.strip_prefix('f') {
if let Ok(n) = rest.parse::<u8>() {
if (1..=24).contains(&n) {
key = Some(Key::F(n));
continue;
}
}
}
if other.chars().count() == 1 {
key = other.chars().next().map(|c| Key::Char(c.to_ascii_lowercase()));
continue;
}
return None;
}
}
}
Some(Self { modifiers, key: key? })
}
pub fn to_string_key(&self) -> String {
let mut parts = Vec::new();
if self.modifiers.contains(Modifiers::SHIFT) {
parts.push("SHIFT".to_string());
}
if self.modifiers.contains(Modifiers::CTRL) {
parts.push("CTRL".to_string());
}
if self.modifiers.contains(Modifiers::ALT) {
parts.push("ALT".to_string());
}
if self.modifiers.contains(Modifiers::META) {
parts.push("SUPER".to_string());
}
let key_str = match self.key {
Key::Char('+') => "PLUS".to_string(),
Key::Char('-') => "MINUS".to_string(),
Key::Char('=') => "EQUAL".to_string(),
Key::Char('_') => "UNDERSCORE".to_string(),
Key::Char(c) => c.to_ascii_uppercase().to_string(),
Key::F(n) => format!("F{n}"),
Key::Escape => "ESC".to_string(),
Key::Enter => "ENTER".to_string(),
Key::Space => "SPACE".to_string(),
Key::Tab => "TAB".to_string(),
Key::Backspace => "BACKSPACE".to_string(),
Key::Delete => "DELETE".to_string(),
Key::Up => "UP".to_string(),
Key::Down => "DOWN".to_string(),
Key::Left => "LEFT".to_string(),
Key::Right => "RIGHT".to_string(),
};
parts.push(key_str);
parts.join("+")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Modifiers(pub u8);
impl Modifiers {
pub const NONE: Self = Self(0);
pub const SHIFT: Self = Self(1 << 0);
pub const CTRL: Self = Self(1 << 1);
pub const ALT: Self = Self(1 << 2);
pub const META: Self = Self(1 << 3);
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
}
impl std::ops::BitOr for Modifiers {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for Modifiers {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Key {
Char(char),
F(u8),
Escape,
Enter,
Space,
Tab,
Backspace,
Delete,
Up,
Down,
Left,
Right,
}
impl Default for Key {
fn default() -> Self {
Key::Char('\0')
}
}
#[derive(Debug, Clone)]
pub struct TrayMenu {
pub tooltip: String,
pub items: Vec<TrayMenuItem>,
}
impl TrayMenu {
pub fn minimal(tooltip: impl Into<String>) -> Self {
Self {
tooltip: tooltip.into(),
items: vec![
TrayMenuItem::Action {
id: "toggle_overlay".into(),
label: "Toggle overlay".into(),
enabled: true,
accelerator: None,
},
TrayMenuItem::Action {
id: "open_prefs".into(),
label: "Preferences…".into(),
enabled: true,
accelerator: None,
},
TrayMenuItem::Separator,
TrayMenuItem::Action {
id: "quit".into(),
label: "Quit Vernier".into(),
enabled: true,
accelerator: None,
},
],
}
}
}
#[derive(Debug, Clone)]
pub enum TrayMenuItem {
Action {
id: String,
label: String,
enabled: bool,
accelerator: Option<Accelerator>,
},
Toggle {
id: String,
label: String,
enabled: bool,
checked: bool,
},
Separator,
Submenu {
id: String,
label: String,
items: Vec<TrayMenuItem>,
},
}
#[derive(Debug, Clone)]
pub enum PlatformEvent {
HotkeyPressed(HotkeyId),
TrayMenuActivated { id: String },
TrayIconLeftClicked { x: i32, y: i32 },
OverlayClosed(MonitorId),
MonitorsChanged,
PointerEnter { monitor: MonitorId, x: f64, y: f64 },
PointerLeave { monitor: MonitorId },
PointerMove { monitor: MonitorId, x: f64, y: f64 },
PointerButton {
monitor: MonitorId,
button: u32,
pressed: bool,
x: f64,
y: f64,
},
KeyboardKey {
monitor: MonitorId,
keysym: u32,
pressed: bool,
is_repeat: bool,
},
Quit,
}
pub type EventReceiver = mpsc::Receiver<PlatformEvent>;
#[allow(dead_code)]
pub(crate) type EventSender = mpsc::Sender<PlatformEvent>;