1use crate::field::{Rect, Vec2};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct Viewport {
5 pub center: Vec2,
7
8 pub size: Vec2,
10
11 pub home: Vec2,
13}
14
15impl Viewport {
16 pub fn new(center: Vec2, size: Vec2) -> Self {
17 Self {
18 center,
19 size,
20 home: center,
21 }
22 }
23
24 pub fn rect(&self) -> Rect {
26 let half = Vec2 {
27 x: self.size.x * 0.5,
28 y: self.size.y * 0.5,
29 };
30
31 Rect {
32 min: Vec2 {
33 x: self.center.x - half.x,
34 y: self.center.y - half.y,
35 },
36 max: Vec2 {
37 x: self.center.x + half.x,
38 y: self.center.y + half.y,
39 },
40 }
41 }
42
43 pub fn move_to(&mut self, center: Vec2) {
45 self.center = center;
46 }
47
48 pub fn pan(&mut self, delta: Vec2) {
50 self.center.x += delta.x;
51 self.center.y += delta.y;
52 }
53
54 pub fn set_home(&mut self) {
56 self.home = self.center;
57 }
58
59 pub fn return_home(&mut self) {
61 self.center = self.home;
62 }
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum FocusZone {
68 Inside,
69 Outside,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq)]
79pub struct FocusRing {
80 pub radius_x: f32,
81 pub radius_y: f32,
82 pub offset_x: f32,
83 pub offset_y: f32,
84}
85
86impl FocusRing {
87 pub fn new(radius_x: f32, radius_y: f32, offset_x: f32, offset_y: f32) -> Self {
88 Self {
89 radius_x,
90 radius_y,
91 offset_x,
92 offset_y,
93 }
94 }
95
96 pub fn contains(&self, center: Vec2, p: Vec2) -> bool {
97 self.normalized_distance2(center, p) <= 1.0
98 }
99
100 pub fn zone(&self, vp_center: Vec2, p: Vec2) -> FocusZone {
101 if self.contains(vp_center, p) {
102 FocusZone::Inside
103 } else {
104 FocusZone::Outside
105 }
106 }
107
108 pub fn normalized_distance2(&self, center: Vec2, p: Vec2) -> f32 {
113 let ring_center = Vec2 {
114 x: center.x + self.offset_x,
115 y: center.y + self.offset_y,
116 };
117
118 let dx = p.x - ring_center.x;
119 let dy = p.y - ring_center.y;
120
121 let rx = self.radius_x.max(0.0001);
122 let ry = self.radius_y.max(0.0001);
123
124 let nx = dx / rx;
125 let ny = dy / ry;
126
127 nx * nx + ny * ny
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn rect_is_correct() {
137 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
138
139 let r = vp.rect();
140
141 assert_eq!(r.min, Vec2 { x: -50.0, y: -25.0 });
142 assert_eq!(r.max, Vec2 { x: 50.0, y: 25.0 });
143 }
144
145 #[test]
146 fn return_home_works() {
147 let mut vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
148
149 vp.pan(Vec2 { x: 10.0, y: 5.0 });
150 assert_eq!(vp.center, Vec2 { x: 10.0, y: 5.0 });
151
152 vp.return_home();
153 assert_eq!(vp.center, Vec2 { x: 0.0, y: 0.0 });
154 }
155
156 #[test]
157 fn focus_ring_contains_axis_aligned() {
158 let ring = FocusRing::new(10.0, 5.0, 0.0, 0.0);
159 let c = Vec2 { x: 0.0, y: 0.0 };
160
161 assert!(ring.contains(c, Vec2 { x: 0.0, y: 0.0 }));
162 assert!(ring.contains(c, Vec2 { x: 10.0, y: 0.0 }));
163 assert!(ring.contains(c, Vec2 { x: 0.0, y: 5.0 }));
164
165 assert!(!ring.contains(c, Vec2 { x: 10.01, y: 0.0 }));
166 assert!(!ring.contains(c, Vec2 { x: 0.0, y: 5.01 }));
167 }
168
169 #[test]
170 fn focus_ring_respects_offset() {
171 let ring = FocusRing::new(10.0, 5.0, 4.0, -2.0);
172 let c = Vec2 { x: 0.0, y: 0.0 };
173
174 assert!(ring.contains(c, Vec2 { x: 4.0, y: -2.0 }));
175 assert!(ring.contains(c, Vec2 { x: 14.0, y: -2.0 }));
176 assert!(ring.contains(c, Vec2 { x: 4.0, y: 3.0 }));
177
178 assert!(!ring.contains(c, Vec2 { x: 14.01, y: -2.0 }));
179 assert!(!ring.contains(c, Vec2 { x: 4.0, y: 3.01 }));
180 }
181
182 #[test]
183 fn focus_zone_classifies() {
184 let ring = FocusRing::new(10.0, 10.0, 0.0, 0.0);
185 let c = Vec2 { x: 0.0, y: 0.0 };
186
187 assert_eq!(ring.zone(c, Vec2 { x: 0.0, y: 0.0 }), FocusZone::Inside);
188 assert_eq!(ring.zone(c, Vec2 { x: 20.0, y: 0.0 }), FocusZone::Outside);
189 }
190}