#![cfg_attr(
not(windows),
allow(dead_code, reason = "off Windows only the tests compile this module")
)]
#[cfg(test)]
use std::time::Duration;
use a7n_proto::v1::Edge;
use crate::input::Bounds;
const PROGRESS_FULL: i32 = 1000;
const GLOW_MIN_PERMILLE: i32 = 250;
#[cfg(test)]
pub(super) const LANDING_DURATION: Duration = Duration::from_millis(400);
#[cfg(test)]
const LANDING_ALPHA: u8 = 220;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Rect {
pub left: i32,
pub top: i32,
pub right: i32,
pub bottom: i32,
}
impl Rect {
pub(super) fn from_bounds(bounds: Bounds) -> Self {
let extent = |size: u32| i32::try_from(size).unwrap_or(i32::MAX);
Self {
left: bounds.x,
top: bounds.y,
right: bounds.x.saturating_add(extent(bounds.width)),
bottom: bounds.y.saturating_add(extent(bounds.height)),
}
}
#[cfg(windows)]
pub(super) fn as_rect(self) -> crate::feedback::look::Rect {
crate::feedback::look::Rect {
x: f64::from(self.left),
y: f64::from(self.top),
width: f64::from(self.width()),
height: f64::from(self.height()),
}
}
pub(super) fn width(self) -> i32 {
self.right - self.left
}
pub(super) fn height(self) -> i32 {
self.bottom - self.top
}
pub(super) fn intersect(self, other: Self) -> Option<Self> {
let overlap = Self {
left: self.left.max(other.left),
top: self.top.max(other.top),
right: self.right.min(other.right),
bottom: self.bottom.min(other.bottom),
};
(overlap.width() > 0 && overlap.height() > 0).then_some(overlap)
}
}
pub(super) fn glow_rect(
area: Rect,
edge: Edge,
progress: u16,
thickness: i32,
pointer: (i32, i32),
) -> Option<Rect> {
let (span_start, span, along) = match edge {
Edge::Left | Edge::Right => (area.top, area.height(), pointer.1),
Edge::Top | Edge::Bottom => (area.left, area.width(), pointer.0),
Edge::Unspecified => return None,
};
let progress = i32::from(progress).min(PROGRESS_FULL);
let permille =
GLOW_MIN_PERMILLE + progress * (PROGRESS_FULL - GLOW_MIN_PERMILLE) / PROGRESS_FULL;
let length = scale(span, permille);
if length <= 0 || thickness <= 0 {
return None;
}
let start = (along - length / 2).clamp(span_start, span_start + span - length);
let end = start + length;
Some(match edge {
Edge::Left | Edge::Unspecified => Rect {
left: area.left,
top: start,
right: area.left + thickness,
bottom: end,
},
Edge::Right => Rect {
left: area.right - thickness,
top: start,
right: area.right,
bottom: end,
},
Edge::Top => Rect {
left: start,
top: area.top,
right: end,
bottom: area.top + thickness,
},
Edge::Bottom => Rect {
left: start,
top: area.bottom - thickness,
right: end,
bottom: area.bottom,
},
})
}
#[cfg(test)]
pub(super) fn landing_alpha(elapsed: Duration, animate: bool) -> u8 {
let (_, alpha) = crate::feedback::Landing::Ring.frame(elapsed, LANDING_DURATION, animate);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let alpha = (f64::from(LANDING_ALPHA) * alpha) as u8;
alpha
}
fn scale(value: i32, permille: i32) -> i32 {
i32::try_from(i64::from(value) * i64::from(permille) / i64::from(PROGRESS_FULL))
.unwrap_or(value)
}
#[cfg(test)]
mod tests {
use super::*;
const AREA: Rect = Rect {
left: -1920,
top: 0,
right: 0,
bottom: 1000,
};
#[test]
fn glow_rect_edge_and_progress_hugs_edge_and_grows_inside_area() {
let rect = |left, top, right, bottom| Rect {
left,
top,
right,
bottom,
};
let cases = [
(Edge::Left, 0, (0, 500), rect(-1920, 375, -1914, 625)),
(Edge::Right, 1000, (0, 500), rect(-6, 0, 0, 1000)),
(Edge::Top, 0, (-100, 0), rect(-480, 0, 0, 6)),
(
Edge::Bottom,
500,
(-1900, 999),
rect(-1920, 994, -720, 1000),
),
];
for (edge, progress, pointer, expected) in cases {
let result = glow_rect(AREA, edge, progress, 6, pointer);
assert_eq!(result, Some(expected), "{edge:?} at {progress}");
}
}
#[test]
fn landing_alpha_elapsed_fades_or_holds_then_ends() {
let cases = [
(0, true, 220),
(200, true, 110),
(0, false, 220),
(399, false, 220),
(400, true, 0),
(400, false, 0),
];
for (millis, animate, expected) in cases {
let alpha = landing_alpha(Duration::from_millis(millis), animate);
assert_eq!(alpha, expected, "{millis} ms, animate {animate}");
}
}
}