use teksilo_canvas::{Point, Rect, Transform2D};
pub const MAGNIFIER_RADIUS: f32 = 48.0;
pub const MAGNIFIER_SCALE: f32 = 1.25;
pub const MAGNIFIER_RISE: f32 = 40.0;
pub const MAGNIFIER_HALF_HEIGHT: f32 = 22.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MagnifierRequest {
pub focus: Point,
pub lens: Rect,
pub scale: f32,
}
impl MagnifierRequest {
pub fn new(
focus: Point,
viewport: Rect,
radius: f32,
half_height: f32,
rise: f32,
scale: f32,
) -> Self {
let width = radius * 2.0;
let height = half_height * 2.0;
let above_top = focus.y - rise - height;
let below_top = focus.y + rise;
let top = if above_top >= viewport.y {
above_top
} else if below_top + height <= viewport.bottom() {
below_top
} else {
above_top
};
let left = focus.x - radius;
let lens = Rect::new(left, top, width, height);
Self {
focus,
lens: clamp_into(lens, viewport),
scale,
}
}
pub fn transform(&self) -> Transform2D {
let centre = self.lens.center();
Transform2D::translate(-self.focus.x, -self.focus.y)
.then(&Transform2D::scale(self.scale, self.scale))
.then(&Transform2D::translate(centre.x, centre.y))
}
}
fn clamp_into(rect: Rect, bounds: Rect) -> Rect {
let x = if rect.width >= bounds.width {
bounds.x
} else {
rect.x.clamp(bounds.x, bounds.right() - rect.width)
};
let y = if rect.height >= bounds.height {
bounds.y
} else {
rect.y.clamp(bounds.y, bounds.bottom() - rect.height)
};
Rect::new(x, y, rect.width, rect.height)
}
#[cfg(test)]
mod tests {
use super::*;
const VIEWPORT: Rect = Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 300.0,
};
fn request(focus: Point) -> MagnifierRequest {
MagnifierRequest::new(
focus,
VIEWPORT,
MAGNIFIER_RADIUS,
MAGNIFIER_HALF_HEIGHT,
MAGNIFIER_RISE,
MAGNIFIER_SCALE,
)
}
#[test]
fn the_contact_point_lands_at_the_lens_centre() {
let req = request(Point::new(200.0, 150.0));
let mapped = req.transform().apply_point(req.focus);
let centre = req.lens.center();
assert!(
(mapped.x - centre.x).abs() < 1e-3 && (mapped.y - centre.y).abs() < 1e-3,
"focus {:?} mapped to {mapped:?}, lens centre {centre:?}",
req.focus
);
}
#[test]
fn content_is_magnified_by_the_scale() {
let req = request(Point::new(200.0, 150.0));
let t = req.transform();
let a = t.apply_point(Point::new(200.0, 150.0));
let b = t.apply_point(Point::new(210.0, 150.0));
assert!(
((b.x - a.x) - 10.0 * MAGNIFIER_SCALE).abs() < 1e-3,
"10 dp apart became {} dp apart",
b.x - a.x
);
}
#[test]
fn the_lens_rises_above_the_contact() {
let req = request(Point::new(200.0, 150.0));
assert!(
req.lens.bottom() <= 150.0 - MAGNIFIER_RISE + 1e-3,
"lens bottom {} should clear the contact by the rise",
req.lens.bottom()
);
}
#[test]
fn a_contact_near_the_top_flips_the_lens_below_it() {
let req = request(Point::new(200.0, 8.0));
assert!(
req.lens.y >= 8.0 + MAGNIFIER_RISE - 1e-3,
"lens should sit below a contact with no room above it, got {:?}",
req.lens
);
}
#[test]
fn a_clamped_lens_keeps_its_size_and_stays_in_the_viewport() {
for focus in [
Point::new(2.0, 150.0),
Point::new(398.0, 150.0),
Point::new(2.0, 4.0),
Point::new(398.0, 296.0),
] {
let req = request(focus);
assert_eq!(
(req.lens.width, req.lens.height),
(MAGNIFIER_RADIUS * 2.0, MAGNIFIER_HALF_HEIGHT * 2.0),
"lens resized at {focus:?}"
);
assert!(
req.lens.x >= VIEWPORT.x - 1e-3
&& req.lens.right() <= VIEWPORT.right() + 1e-3
&& req.lens.y >= VIEWPORT.y - 1e-3
&& req.lens.bottom() <= VIEWPORT.bottom() + 1e-3,
"lens {:?} escaped the viewport at {focus:?}",
req.lens
);
}
}
#[test]
fn a_lens_wider_than_the_viewport_pins_to_the_leading_edge() {
let narrow = Rect::new(10.0, 10.0, 40.0, 20.0);
let req = MagnifierRequest::new(
Point::new(30.0, 20.0),
narrow,
MAGNIFIER_RADIUS,
MAGNIFIER_HALF_HEIGHT,
MAGNIFIER_RISE,
MAGNIFIER_SCALE,
);
assert_eq!((req.lens.x, req.lens.y), (10.0, 10.0));
}
}