1use egui::{ClippedPrimitive, Pos2, Rect, Vec2};
6
7#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Hash)]
13pub enum Rotation {
14 #[default]
15 None,
16 Cw90,
19 Cw180,
21 Cw270,
24}
25
26impl Rotation {
27 pub const ALL: [Rotation; 4] = [
29 Rotation::None,
30 Rotation::Cw90,
31 Rotation::Cw180,
32 Rotation::Cw270,
33 ];
34
35 #[inline]
37 pub fn quarter_turns(self) -> u8 {
38 match self {
39 Rotation::None => 0,
40 Rotation::Cw90 => 1,
41 Rotation::Cw180 => 2,
42 Rotation::Cw270 => 3,
43 }
44 }
45
46 #[inline]
49 pub fn from_quarter_turns(turns: i32) -> Self {
50 Self::ALL[turns.rem_euclid(4) as usize]
51 }
52
53 #[inline]
55 pub fn degrees(self) -> f64 {
56 self.quarter_turns() as f64 * 90.0
57 }
58
59 #[inline]
61 pub fn swaps_axes(self) -> bool {
62 matches!(self, Rotation::Cw90 | Rotation::Cw270)
63 }
64
65 #[inline]
67 pub fn screen_size(self, window: Vec2) -> Vec2 {
68 if self.swaps_axes() {
69 Vec2::new(window.y, window.x)
70 } else {
71 window
72 }
73 }
74
75 #[inline]
79 pub fn to_window(self, p: Pos2, window: Vec2) -> Pos2 {
80 match self {
81 Rotation::None => p,
82 Rotation::Cw90 => Pos2::new(window.x - p.y, p.x),
83 Rotation::Cw180 => Pos2::new(window.x - p.x, window.y - p.y),
84 Rotation::Cw270 => Pos2::new(p.y, window.y - p.x),
85 }
86 }
87
88 #[inline]
91 pub fn from_window(self, p: Pos2, window: Vec2) -> Pos2 {
92 match self {
93 Rotation::None => p,
94 Rotation::Cw90 => Pos2::new(p.y, window.x - p.x),
95 Rotation::Cw180 => Pos2::new(window.x - p.x, window.y - p.y),
96 Rotation::Cw270 => Pos2::new(window.y - p.y, p.x),
97 }
98 }
99
100 #[inline]
103 pub fn rect_to_window(self, rect: Rect, window: Vec2) -> Rect {
104 if self == Rotation::None {
105 return rect;
106 }
107 Rect::from_two_pos(
108 self.to_window(rect.min, window),
109 self.to_window(rect.max, window),
110 )
111 }
112
113 pub fn turn_primitives(self, jobs: &mut [ClippedPrimitive], window: Vec2) {
119 if self == Rotation::None {
120 return;
121 }
122 for job in jobs.iter_mut() {
123 job.clip_rect = self.rect_to_window(job.clip_rect, window);
124 match &mut job.primitive {
125 egui::epaint::Primitive::Mesh(mesh) => {
126 for vertex in &mut mesh.vertices {
127 vertex.pos = self.to_window(vertex.pos, window);
128 }
129 }
130 egui::epaint::Primitive::Callback(callback) => {
131 log::warn!("a paint callback is not turned with the screen");
132 callback.rect = self.rect_to_window(callback.rect, window);
133 }
134 }
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 const WINDOW: Vec2 = Vec2::new(640.0, 480.0);
144
145 fn corners(rotation: Rotation) -> Vec<Pos2> {
147 let screen = rotation.screen_size(WINDOW);
148 [
149 Pos2::new(0.0, 0.0),
150 Pos2::new(screen.x, 0.0),
151 Pos2::new(screen.x, screen.y),
152 Pos2::new(0.0, screen.y),
153 ]
154 .iter()
155 .map(|p| rotation.to_window(*p, WINDOW))
156 .collect()
157 }
158
159 #[test]
160 fn a_quarter_turn_trades_width_for_height() {
161 assert_eq!(Rotation::None.screen_size(WINDOW), WINDOW);
162 assert_eq!(Rotation::Cw180.screen_size(WINDOW), WINDOW);
163 let turned = Vec2::new(WINDOW.y, WINDOW.x);
164 assert_eq!(Rotation::Cw90.screen_size(WINDOW), turned);
165 assert_eq!(Rotation::Cw270.screen_size(WINDOW), turned);
166 }
167
168 #[test]
169 fn the_screen_lands_on_the_window_and_nowhere_else() {
170 for rotation in Rotation::ALL {
173 let corners = corners(rotation);
174 let rect = Rect::from_points(&corners);
175 assert_eq!(rect.min, Pos2::ZERO, "{rotation:?} leaves the window");
176 assert_eq!(rect.max, WINDOW.to_pos2(), "{rotation:?} leaves the window");
177 }
178 }
179
180 #[test]
181 fn the_screens_top_left_goes_where_the_turn_says() {
182 let top_left = |rotation: Rotation| corners(rotation)[0];
183 assert_eq!(top_left(Rotation::None), Pos2::new(0.0, 0.0));
184 assert_eq!(top_left(Rotation::Cw90), Pos2::new(WINDOW.x, 0.0));
185 assert_eq!(top_left(Rotation::Cw180), WINDOW.to_pos2());
186 assert_eq!(top_left(Rotation::Cw270), Pos2::new(0.0, WINDOW.y));
187 }
188
189 #[test]
190 fn a_pointer_comes_back_to_where_it_was_pressed() {
191 for rotation in Rotation::ALL {
194 let screen = rotation.screen_size(WINDOW);
195 for p in [
196 Pos2::ZERO,
197 Pos2::new(screen.x, screen.y),
198 Pos2::new(screen.x / 3.0, screen.y / 7.0),
199 ] {
200 let there_and_back = rotation.from_window(rotation.to_window(p, WINDOW), WINDOW);
201 assert!(
202 there_and_back.distance(p) < 1e-3,
203 "{rotation:?}: {p:?} came back as {there_and_back:?}"
204 );
205 }
206 }
207 }
208
209 #[test]
210 fn turns_wrap_in_both_directions() {
211 assert_eq!(Rotation::from_quarter_turns(4), Rotation::None);
212 assert_eq!(Rotation::from_quarter_turns(-1), Rotation::Cw270);
213 assert_eq!(Rotation::from_quarter_turns(5), Rotation::Cw90);
214 for rotation in Rotation::ALL {
215 assert_eq!(
216 Rotation::from_quarter_turns(rotation.quarter_turns() as i32),
217 rotation
218 );
219 }
220 }
221
222 #[test]
223 fn a_clip_rect_stays_a_rect_the_right_way_up() {
224 let rect = Rect::from_min_max(Pos2::new(10.0, 20.0), Pos2::new(110.0, 60.0));
225 for rotation in Rotation::ALL {
226 let turned = rotation.rect_to_window(rect, WINDOW);
227 assert!(turned.min.x <= turned.max.x && turned.min.y <= turned.max.y);
228 let (w, h) = (rect.width(), rect.height());
229 let (tw, th) = (turned.width(), turned.height());
230 if rotation.swaps_axes() {
231 assert!(
232 (tw - h).abs() < 1e-3 && (th - w).abs() < 1e-3,
233 "{rotation:?}"
234 );
235 } else {
236 assert!(
237 (tw - w).abs() < 1e-3 && (th - h).abs() < 1e-3,
238 "{rotation:?}"
239 );
240 }
241 }
242 }
243
244 #[test]
245 fn a_turned_mesh_holds_its_shape() {
246 let mut mesh = egui::Mesh::default();
247 let vertex = |x: f32, y: f32| egui::epaint::Vertex {
248 pos: Pos2::new(x, y),
249 uv: Pos2::ZERO,
250 color: egui::Color32::WHITE,
251 };
252 mesh.vertices = vec![vertex(0.0, 0.0), vertex(40.0, 0.0), vertex(40.0, 10.0)];
253 let mut jobs = vec![ClippedPrimitive {
254 clip_rect: Rect::from_min_max(Pos2::ZERO, Pos2::new(480.0, 640.0)),
255 primitive: egui::epaint::Primitive::Mesh(mesh),
256 }];
257 Rotation::Cw90.turn_primitives(&mut jobs, WINDOW);
258 let egui::epaint::Primitive::Mesh(mesh) = &jobs[0].primitive else {
259 unreachable!("the job was built as a mesh");
260 };
261 assert_eq!(mesh.vertices[0].pos, Pos2::new(640.0, 0.0));
264 assert_eq!(mesh.vertices[1].pos, Pos2::new(640.0, 40.0));
265 assert_eq!(mesh.vertices[2].pos, Pos2::new(630.0, 40.0));
266 assert_eq!(
267 jobs[0].clip_rect,
268 Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(640.0, 480.0))
269 );
270 }
271}