use std::time::Duration;
use a7n_proto::v1::Edge;
use serde::{Deserialize, Serialize};
#[cfg(not(target_os = "macos"))]
use crate::input::Bounds;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Style {
#[default]
Glow,
Beam,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Landing {
#[default]
Ring,
Pulse,
Dot,
}
impl Landing {
pub(super) fn frame(self, elapsed: Duration, duration: Duration, animate: bool) -> (f64, f64) {
const PULSE_START: f64 = 0.2;
const DOT_RADIUS: f64 = 0.35;
let progress = (elapsed.as_secs_f64() / duration.as_secs_f64()).clamp(0.0, 1.0);
let motion = if animate { progress } else { 0.0 };
let radius = match self {
Self::Ring => 1.0,
Self::Pulse if !animate => 1.0,
Self::Pulse => PULSE_START + (1.0 - PULSE_START) * motion,
Self::Dot => DOT_RADIUS * (1.0 - motion),
};
let alpha = if progress >= 1.0 {
0.0
} else if animate {
1.0 - progress
} else {
1.0
};
(radius, alpha)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Color {
#[default]
Accent,
Green,
Orange,
Pink,
}
impl Color {
#[must_use]
pub(super) const fn rgb(self, dark: bool) -> [u8; 3] {
match self {
Self::Accent if !dark => [0x2f, 0x5b, 0xea],
Self::Accent => [0x70, 0x88, 0xff],
Self::Green => [0x43, 0xc7, 0x7f],
Self::Orange => [0xe9, 0xa2, 0x3b],
Self::Pink => [0xed, 0x78, 0xb5],
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Size {
Small,
#[default]
Medium,
Large,
}
impl Size {
#[cfg(not(target_os = "macos"))]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(super) fn pixels(self, base: u32) -> u32 {
(f64::from(base) * self.multiplier()).round().max(1.0) as u32
}
#[must_use]
pub(super) const fn multiplier(self) -> f64 {
match self {
Self::Small => 0.5,
Self::Medium => 1.0,
Self::Large => 2.0,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Look {
pub style: Style,
#[serde(rename = "landing_style")]
pub landing: Landing,
pub color: Color,
pub size: Size,
}
pub(super) const BEAM_THICKNESS: f64 = 3.0;
const PROGRESS_FULL: f64 = 1000.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[cfg(not(target_os = "macos"))]
impl Rect {
#[cfg(all(unix, not(target_os = "macos")))]
pub(super) fn from_bounds(bounds: Bounds) -> Self {
Self {
x: f64::from(bounds.x),
y: f64::from(bounds.y),
width: f64::from(bounds.width),
height: f64::from(bounds.height),
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(super) fn bounds(self) -> Bounds {
Bounds {
x: self.x.round() as i32,
y: self.y.round() as i32,
width: self.width.round().max(1.0) as u32,
height: self.height.round().max(1.0) as u32,
}
}
}
pub(super) fn beam_rect(
area: Rect,
edge: Edge,
pointer: (f64, f64),
progress: u16,
thickness: f64,
) -> Option<Rect> {
let horizontal = match edge {
Edge::Top | Edge::Bottom => true,
Edge::Left | Edge::Right => false,
Edge::Unspecified => return None,
};
let (start, span, along) = if horizontal {
(area.x, area.width, pointer.0)
} else {
(area.y, area.height, pointer.1)
};
if span <= 0.0 || thickness <= 0.0 {
return None;
}
let length = (span * f64::from(progress) / PROGRESS_FULL).clamp(1.0_f64.min(span), span);
let from = (along - length / 2.0).clamp(start, start + span - length);
let thickness = thickness.min(if horizontal { area.height } else { area.width });
Some(match edge {
Edge::Top => Rect {
x: from,
y: area.y,
width: length,
height: thickness,
},
Edge::Bottom => Rect {
x: from,
y: area.y + area.height - thickness,
width: length,
height: thickness,
},
Edge::Left => Rect {
x: area.x,
y: from,
width: thickness,
height: length,
},
Edge::Right => Rect {
x: area.x + area.width - thickness,
y: from,
width: thickness,
height: length,
},
Edge::Unspecified => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_elapsed_pulse_grows_and_dot_shrinks() {
let duration = Duration::from_millis(400);
for (style, grows) in [(Landing::Pulse, true), (Landing::Dot, false)] {
let start = style.frame(Duration::ZERO, duration, true);
let later = style.frame(duration / 2, duration, true);
let reduced = style.frame(duration / 2, duration, false);
assert!(if grows {
later.0 > start.0
} else {
later.0 < start.0
});
assert!(later.1 < start.1);
assert_eq!(reduced, style.frame(Duration::ZERO, duration, false));
assert_eq!(style.frame(duration, duration, true).1, 0.0);
}
}
#[test]
fn beam_rect_progress_grows_along_pushed_edge() {
let area = Rect {
x: -100.0,
y: 20.0,
width: 1000.0,
height: 800.0,
};
for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] {
let small = beam_rect(area, edge, (400.0, 420.0), 250, BEAM_THICKNESS).unwrap();
let large = beam_rect(area, edge, (400.0, 420.0), 750, BEAM_THICKNESS).unwrap();
for rect in [small, large] {
assert!(rect.x >= area.x && rect.y >= area.y);
assert!(rect.x + rect.width <= area.x + area.width);
assert!(rect.y + rect.height <= area.y + area.height);
match edge {
Edge::Top => assert_eq!(rect.y, area.y),
Edge::Bottom => assert_eq!(rect.y + rect.height, area.y + area.height),
Edge::Left => assert_eq!(rect.x, area.x),
Edge::Right => assert_eq!(rect.x + rect.width, area.x + area.width),
Edge::Unspecified => unreachable!(),
}
}
if matches!(edge, Edge::Top | Edge::Bottom) {
assert_eq!((small.width, large.width), (250.0, 750.0));
} else {
assert_eq!((small.height, large.height), (200.0, 600.0));
}
}
}
}