#![warn(missing_docs)]
use crate::event::{KeyModifiers, PointerButton};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PanTrigger {
pub button: PointerButton,
pub modifiers: KeyModifiers,
}
impl Default for PanTrigger {
fn default() -> Self {
Self {
button: PointerButton::Primary,
modifiers: KeyModifiers::default(),
}
}
}
impl PanTrigger {
pub fn matches(self, button: PointerButton, modifiers: KeyModifiers) -> bool {
self.button == button && self.modifiers == modifiers
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum PanBounds {
#[default]
Contain,
Center,
Free,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ViewportConfig {
pub min_zoom: f32,
pub max_zoom: f32,
pub pan_trigger: PanTrigger,
pub pan_bounds: PanBounds,
}
impl Default for ViewportConfig {
fn default() -> Self {
Self {
min_zoom: 0.2,
max_zoom: 5.0,
pan_trigger: PanTrigger::default(),
pan_bounds: PanBounds::default(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ViewportView {
pub pan: (f32, f32),
pub zoom: f32,
}
impl Default for ViewportView {
fn default() -> Self {
Self {
pan: (0.0, 0.0),
zoom: 1.0,
}
}
}
impl ViewportView {
pub fn project(self, p: (f32, f32), origin: (f32, f32)) -> (f32, f32) {
(
origin.0 + self.pan.0 + self.zoom * (p.0 - origin.0),
origin.1 + self.pan.1 + self.zoom * (p.1 - origin.1),
)
}
pub fn unproject(self, s: (f32, f32), origin: (f32, f32)) -> (f32, f32) {
(
origin.0 + (s.0 - origin.0 - self.pan.0) / self.zoom,
origin.1 + (s.1 - origin.1 - self.pan.1) / self.zoom,
)
}
pub fn zoom_about(self, new_zoom: f32, anchor: (f32, f32), origin: (f32, f32)) -> Self {
let r = new_zoom / self.zoom;
let dx = anchor.0 - origin.0;
let dy = anchor.1 - origin.1;
Self {
pan: (
dx * (1.0 - r) + self.pan.0 * r,
dy * (1.0 - r) + self.pan.1 * r,
),
zoom: new_zoom,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ViewportRequest {
FitContent {
key: String,
padding: f32,
},
ResetView {
key: String,
},
CenterOn {
key: String,
point: (f32, f32),
},
}
impl ViewportRequest {
pub fn key(&self) -> &str {
match self {
ViewportRequest::FitContent { key, .. }
| ViewportRequest::ResetView { key }
| ViewportRequest::CenterOn { key, .. } => key,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: (f32, f32), b: (f32, f32)) {
assert!(
(a.0 - b.0).abs() < 1e-3 && (a.1 - b.1).abs() < 1e-3,
"{a:?} != {b:?}"
);
}
#[test]
fn project_unproject_roundtrip() {
let origin = (100.0, 50.0);
let v = ViewportView {
pan: (30.0, -20.0),
zoom: 2.5,
};
let p = (140.0, 75.0);
approx(v.unproject(v.project(p, origin), origin), p);
}
#[test]
fn identity_is_a_noop() {
let origin = (10.0, 10.0);
let v = ViewportView::default();
approx(v.project((200.0, 300.0), origin), (200.0, 300.0));
}
#[test]
fn zoom_about_keeps_cursor_point_fixed() {
let origin = (0.0, 0.0);
let v = ViewportView {
pan: (15.0, 40.0),
zoom: 1.0,
};
let cursor = (250.0, 120.0);
let before = v.unproject(cursor, origin);
let zoomed = v.zoom_about(3.0, cursor, origin);
let after = zoomed.unproject(cursor, origin);
approx(before, after);
approx(zoomed.project(before, origin), cursor);
}
#[test]
fn pan_trigger_matches_exactly() {
let t = PanTrigger::default();
assert!(t.matches(PointerButton::Primary, KeyModifiers::default()));
assert!(!t.matches(PointerButton::Middle, KeyModifiers::default()));
let shift = KeyModifiers {
shift: true,
..Default::default()
};
assert!(!t.matches(PointerButton::Primary, shift));
}
}