1#[cfg(feature = "canvas-backend")]
34use crate::canvas::painter::BYTES_PER_PIXEL;
35use crate::Rotation;
36use sdl2::event::Event;
37use sdl2::video::{Window, WindowBuilder};
38use sdl2::VideoSubsystem;
39
40#[non_exhaustive]
42#[derive(Copy, Clone, PartialEq, Eq, Debug)]
43pub enum Renderer {
44 Gles3,
47 Gl32,
49 Canvas,
52 CanvasBlit,
56 Wgpu,
58}
59
60impl Renderer {
61 pub const FALLBACK_CHAIN: [Renderer; 3] = [Renderer::Gles3, Renderer::Gl32, Renderer::Canvas];
63
64 fn name(self) -> &'static str {
65 match self {
66 Renderer::Gles3 => "GLES 3.0",
67 Renderer::Gl32 => "GL 3.2 core",
68 Renderer::Canvas => "SDL renderer",
69 Renderer::CanvasBlit => "SDL renderer (offscreen blit)",
70 Renderer::Wgpu => "wgpu",
71 }
72 }
73}
74
75enum Backend {
76 #[cfg(feature = "glow-backend")]
77 Glow {
78 window: Window,
79 _gl_context: sdl2::video::GLContext,
81 egui: crate::EguiGlow,
82 },
83 #[cfg(feature = "canvas-backend")]
84 Canvas {
85 canvas: sdl2::render::WindowCanvas,
86 egui: crate::EguiCanvas,
87 turned: Option<TurnedTarget>,
90 },
91 #[cfg(feature = "canvas-backend")]
92 CanvasBlit {
93 canvas: sdl2::render::WindowCanvas,
94 offscreen: sdl2::render::Canvas<sdl2::surface::Surface<'static>>,
96 present: sdl2::render::Texture,
98 size: (u32, u32),
100 frame: Vec<u8>,
102 egui: crate::EguiCanvas<sdl2::surface::SurfaceContext<'static>>,
103 },
104 #[cfg(feature = "wgpu-backend")]
106 Wgpu { egui: Box<crate::EguiWgpu> },
107}
108
109pub struct EguiWindow {
112 backend: Backend,
113 renderer: Renderer,
114}
115
116impl EguiWindow {
117 pub fn new(
120 video: &VideoSubsystem,
121 title: &str,
122 size: (u32, u32),
123 configure: impl Fn(&mut WindowBuilder),
124 order: &[Renderer],
125 ) -> Result<Self, String> {
126 let make_window = |video: &VideoSubsystem, gl: bool| {
127 let mut builder = video.window(title, size.0, size.1);
128 if gl {
129 builder.opengl();
130 }
131 configure(&mut builder);
132 builder.build().map_err(|e| e.to_string())
133 };
134 let mut last = "no renderer requested".to_string();
135 for &renderer in order {
136 match build(video, &make_window, renderer) {
137 Ok(backend) => {
138 log::info!("egui renderer: {}", renderer.name());
139 return Ok(Self { backend, renderer });
140 }
141 Err(e) => {
142 log::warn!("{} unavailable: {e}", renderer.name());
143 last = e;
144 }
145 }
146 }
147 Err(last)
148 }
149
150 pub fn renderer(&self) -> Renderer {
152 self.renderer
153 }
154
155 pub fn set_rotation(&mut self, rotation: Rotation) {
165 match &mut self.backend {
166 #[cfg(feature = "glow-backend")]
167 Backend::Glow { egui, .. } => egui.state.set_rotation(rotation),
168 #[cfg(feature = "canvas-backend")]
169 Backend::Canvas { egui, .. } => egui.state.set_rotation(rotation),
170 #[cfg(feature = "canvas-backend")]
171 Backend::CanvasBlit { egui, .. } => egui.state.set_rotation(rotation),
172 #[cfg(feature = "wgpu-backend")]
173 Backend::Wgpu { egui } => egui.state.set_rotation(rotation),
174 }
175 }
176
177 pub fn rotation(&self) -> Rotation {
178 match &self.backend {
179 #[cfg(feature = "glow-backend")]
180 Backend::Glow { egui, .. } => egui.state.rotation(),
181 #[cfg(feature = "canvas-backend")]
182 Backend::Canvas { egui, .. } => egui.state.rotation(),
183 #[cfg(feature = "canvas-backend")]
184 Backend::CanvasBlit { egui, .. } => egui.state.rotation(),
185 #[cfg(feature = "wgpu-backend")]
186 Backend::Wgpu { egui } => egui.state.rotation(),
187 }
188 }
189
190 pub fn ctx(&self) -> &egui::Context {
191 match &self.backend {
192 #[cfg(feature = "glow-backend")]
193 Backend::Glow { egui, .. } => &egui.ctx,
194 #[cfg(feature = "canvas-backend")]
195 Backend::Canvas { egui, .. } => &egui.ctx,
196 #[cfg(feature = "canvas-backend")]
197 Backend::CanvasBlit { egui, .. } => &egui.ctx,
198 #[cfg(feature = "wgpu-backend")]
199 Backend::Wgpu { egui } => &egui.ctx,
200 }
201 }
202
203 pub fn window(&self) -> &Window {
204 match &self.backend {
205 #[cfg(feature = "glow-backend")]
206 Backend::Glow { window, .. } => window,
207 #[cfg(feature = "canvas-backend")]
208 Backend::Canvas { canvas, .. } => canvas.window(),
209 #[cfg(feature = "canvas-backend")]
210 Backend::CanvasBlit { canvas, .. } => canvas.window(),
211 #[cfg(feature = "wgpu-backend")]
212 Backend::Wgpu { egui } => &egui.window,
213 }
214 }
215
216 pub fn window_mut(&mut self) -> &mut Window {
219 match &mut self.backend {
220 #[cfg(feature = "glow-backend")]
221 Backend::Glow { window, .. } => window,
222 #[cfg(feature = "canvas-backend")]
223 Backend::Canvas { canvas, .. } => canvas.window_mut(),
224 #[cfg(feature = "canvas-backend")]
225 Backend::CanvasBlit { canvas, .. } => canvas.window_mut(),
226 #[cfg(feature = "wgpu-backend")]
227 Backend::Wgpu { egui } => &mut egui.window,
228 }
229 }
230
231 pub fn on_event(&mut self, event: &Event) -> crate::EventResponse {
233 match &mut self.backend {
234 #[cfg(feature = "glow-backend")]
235 Backend::Glow { window, egui, .. } => egui.state.on_event(window, event),
236 #[cfg(feature = "canvas-backend")]
237 Backend::Canvas { canvas, egui, .. } => egui.state.on_event(canvas.window(), event),
238 #[cfg(feature = "canvas-backend")]
239 Backend::CanvasBlit { canvas, egui, .. } => egui.state.on_event(canvas.window(), event),
240 #[cfg(feature = "wgpu-backend")]
241 Backend::Wgpu { egui } => egui.on_event(event),
242 }
243 }
244
245 pub fn run(&mut self, run_ui: impl FnMut(&egui::Context)) {
247 match &mut self.backend {
248 #[cfg(feature = "glow-backend")]
249 Backend::Glow { egui, .. } => egui.run(run_ui),
250 #[cfg(feature = "canvas-backend")]
251 Backend::Canvas { egui, .. } => egui.run(run_ui),
252 #[cfg(feature = "canvas-backend")]
253 Backend::CanvasBlit { egui, .. } => egui.run(run_ui),
254 #[cfg(feature = "wgpu-backend")]
255 Backend::Wgpu { egui } => egui.run(run_ui),
256 }
257 }
258
259 pub fn run_ui(&mut self, run_ui: impl FnMut(&mut egui::Ui)) {
261 match &mut self.backend {
262 #[cfg(feature = "glow-backend")]
263 Backend::Glow { egui, .. } => egui.run_ui(run_ui),
264 #[cfg(feature = "canvas-backend")]
265 Backend::Canvas { egui, .. } => egui.run_ui(run_ui),
266 #[cfg(feature = "canvas-backend")]
267 Backend::CanvasBlit { egui, .. } => egui.run_ui(run_ui),
268 #[cfg(feature = "wgpu-backend")]
269 Backend::Wgpu { egui } => egui.run_ui(run_ui),
270 }
271 }
272
273 pub fn repaint_delay(&self) -> std::time::Duration {
275 match &self.backend {
276 #[cfg(feature = "glow-backend")]
277 Backend::Glow { egui, .. } => egui.repaint_delay(),
278 #[cfg(feature = "canvas-backend")]
279 Backend::Canvas { egui, .. } => egui.repaint_delay(),
280 #[cfg(feature = "canvas-backend")]
281 Backend::CanvasBlit { egui, .. } => egui.repaint_delay(),
282 #[cfg(feature = "wgpu-backend")]
283 Backend::Wgpu { egui } => egui.repaint_delay(),
284 }
285 }
286
287 pub fn paint(&mut self, clear_color: [f32; 4]) {
289 match &mut self.backend {
290 #[cfg(feature = "glow-backend")]
291 Backend::Glow { window, egui, .. } => {
292 egui.clear(clear_color);
293 egui.paint();
294 window.gl_swap_window();
295 }
296 #[cfg(feature = "canvas-backend")]
297 Backend::Canvas {
298 canvas,
299 egui,
300 turned,
301 } => {
302 let rotation = egui.state.rotation();
303 if rotation == Rotation::None {
304 canvas.set_draw_color(rgb(clear_color));
305 canvas.clear();
306 egui.paint(canvas);
307 canvas.present();
308 } else {
309 paint_turned(canvas, egui, turned, rotation, clear_color);
310 }
311 }
312 #[cfg(feature = "canvas-backend")]
313 Backend::CanvasBlit {
314 canvas,
315 offscreen,
316 present,
317 size,
318 frame,
319 egui,
320 } => {
321 if canvas.output_size().is_ok_and(|s| s != *size) {
323 match rebuild_blit_targets(canvas) {
324 Ok((new_offscreen, new_present, new_size)) => {
325 let rotation = egui.state.rotation();
326 egui.destroy();
327 *egui = crate::EguiCanvas::for_surface(canvas.window(), &new_offscreen);
328 egui.state.set_rotation(rotation);
331 *offscreen = new_offscreen;
332 *present = new_present;
333 *size = new_size;
334 }
335 Err(e) => log::error!("could not resize the offscreen target: {e}"),
336 }
337 }
338
339 offscreen.set_draw_color(rgb(clear_color));
340 offscreen.clear();
341 egui.paint(offscreen);
342 let rotation = egui.state.rotation();
343 let surface = offscreen.surface();
344 let pitch = surface.pitch() as usize;
345 match surface.without_lock() {
346 Some(pixels) => {
352 let uploaded = if rotation == Rotation::None {
353 present.update(None, pixels, pitch)
354 } else {
355 rotate_frame(rotation, pixels, pitch, *size, frame);
356 present.update(None, frame, size.0 as usize * BYTES_PER_PIXEL)
357 };
358 if let Err(e) = uploaded {
359 log::error!("could not upload the offscreen frame: {e}");
360 }
361 }
362 None => log::error!("offscreen surface has no readable pixels"),
363 }
364 if let Err(e) = canvas.copy(present, None, None) {
365 log::error!("could not blit the offscreen frame: {e}");
366 }
367 canvas.present();
368 }
369 #[cfg(feature = "wgpu-backend")]
370 Backend::Wgpu { egui } => egui.paint(clear_color),
371 }
372 }
373
374 pub fn destroy(&mut self) {
376 match &mut self.backend {
377 #[cfg(feature = "glow-backend")]
378 Backend::Glow { egui, .. } => egui.destroy(),
379 #[cfg(feature = "canvas-backend")]
380 Backend::Canvas { egui, turned, .. } => {
381 egui.destroy();
382 if let Some(target) = turned.take() {
383 unsafe { target.texture.destroy() }
384 }
385 }
386 #[cfg(feature = "canvas-backend")]
387 Backend::CanvasBlit { egui, .. } => egui.destroy(),
388 #[cfg(feature = "wgpu-backend")]
389 Backend::Wgpu { .. } => {}
390 }
391 }
392}
393
394fn build(
395 video: &VideoSubsystem,
396 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
397 renderer: Renderer,
398) -> Result<Backend, String> {
399 match renderer {
400 Renderer::Gles3 => build_glow(video, make_window, sdl2::video::GLProfile::GLES, 3, 0),
401 Renderer::Gl32 => build_glow(video, make_window, sdl2::video::GLProfile::Core, 3, 2),
402 Renderer::Canvas => build_canvas(video, make_window),
403 Renderer::CanvasBlit => build_canvas_blit(video, make_window),
404 Renderer::Wgpu => build_wgpu(video, make_window),
405 }
406}
407
408#[cfg(feature = "glow-backend")]
410fn set_gl_attr(name: &str, attr: sdl2::sys::SDL_GLattr, value: i32) -> Result<(), String> {
411 if unsafe { sdl2::sys::SDL_GL_SetAttribute(attr, value) } == 0 {
412 return Ok(());
413 }
414 Err(format!("{name}={value} rejected: {}", sdl2::get_error()))
415}
416
417#[cfg(feature = "glow-backend")]
419fn gl_profile_value(profile: sdl2::video::GLProfile) -> i32 {
420 use sdl2::video::GLProfile;
421 match profile {
422 GLProfile::Core => 1,
423 GLProfile::Compatibility => 2,
424 GLProfile::GLES => 4,
425 GLProfile::Unknown(i) => i,
426 }
427}
428
429#[cfg(feature = "glow-backend")]
430fn build_glow(
431 video: &VideoSubsystem,
432 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
433 profile: sdl2::video::GLProfile,
434 major: u8,
435 minor: u8,
436) -> Result<Backend, String> {
437 use sdl2::sys::SDL_GLattr::*;
440 set_gl_attr(
441 "profile_mask",
442 SDL_GL_CONTEXT_PROFILE_MASK,
443 gl_profile_value(profile),
444 )?;
445 set_gl_attr("major_version", SDL_GL_CONTEXT_MAJOR_VERSION, major as i32)?;
446 set_gl_attr("minor_version", SDL_GL_CONTEXT_MINOR_VERSION, minor as i32)?;
447 set_gl_attr("doublebuffer", SDL_GL_DOUBLEBUFFER, 1)?;
448
449 let window = make_window(video, true)?;
450 let gl_context = window.gl_create_context()?;
451 window.gl_make_current(&gl_context)?;
452 let _ = video.gl_set_swap_interval(sdl2::video::SwapInterval::VSync);
453
454 let glow_ctx = std::sync::Arc::new(unsafe {
455 glow::Context::from_loader_function(|name| {
456 video.gl_get_proc_address(name) as *const std::os::raw::c_void
457 })
458 });
459 let egui = crate::EguiGlow::new(&window, glow_ctx, None, false);
460 Ok(Backend::Glow {
461 window,
462 _gl_context: gl_context,
463 egui,
464 })
465}
466
467#[cfg(not(feature = "glow-backend"))]
468fn build_glow(
469 _video: &VideoSubsystem,
470 _make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
471 _profile: sdl2::video::GLProfile,
472 _major: u8,
473 _minor: u8,
474) -> Result<Backend, String> {
475 Err("built without the glow-backend feature".to_string())
476}
477
478#[cfg(feature = "canvas-backend")]
479fn build_canvas(
480 video: &VideoSubsystem,
481 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
482) -> Result<Backend, String> {
483 let window = make_window(video, false)?;
484 let canvas = window
485 .into_canvas()
486 .present_vsync()
487 .build()
488 .map_err(|e| e.to_string())?;
489 log::debug!("SDL renderer driver: {}", canvas.info().name);
490 let egui = crate::EguiCanvas::new(&canvas);
491 Ok(Backend::Canvas {
492 canvas,
493 egui,
494 turned: None,
495 })
496}
497
498#[cfg(feature = "canvas-backend")]
500type BlitTargets = (
501 sdl2::render::Canvas<sdl2::surface::Surface<'static>>,
502 sdl2::render::Texture,
503 (u32, u32),
504);
505
506#[cfg(feature = "canvas-backend")]
507fn rebuild_blit_targets(canvas: &sdl2::render::WindowCanvas) -> Result<BlitTargets, String> {
508 let size = canvas.output_size()?;
509 let side = size.0.max(size.1);
514 let surface = sdl2::surface::Surface::new(side, side, crate::canvas::painter::PIXEL_FORMAT)?;
515 let offscreen = sdl2::render::Canvas::from_surface(surface)?;
516 let mut present = canvas
517 .texture_creator()
518 .create_texture_streaming(crate::canvas::painter::PIXEL_FORMAT, size.0, size.1)
519 .map_err(|e| e.to_string())?;
520 present.set_blend_mode(sdl2::render::BlendMode::None);
525 Ok((offscreen, present, size))
526}
527
528#[cfg(feature = "canvas-backend")]
529fn build_canvas_blit(
530 video: &VideoSubsystem,
531 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
532) -> Result<Backend, String> {
533 let window = make_window(video, false)?;
534 let canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
537 log::debug!("SDL renderer driver: {} (blit)", canvas.info().name);
538 let (offscreen, present, size) = rebuild_blit_targets(&canvas)?;
539 let egui = crate::EguiCanvas::for_surface(canvas.window(), &offscreen);
540 Ok(Backend::CanvasBlit {
541 canvas,
542 offscreen,
543 present,
544 size,
545 frame: Vec::new(),
546 egui,
547 })
548}
549
550#[cfg(not(feature = "canvas-backend"))]
551fn build_canvas_blit(
552 _video: &VideoSubsystem,
553 _make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
554) -> Result<Backend, String> {
555 Err("built without the canvas-backend feature".to_string())
556}
557
558#[cfg(not(feature = "canvas-backend"))]
559fn build_canvas(
560 _video: &VideoSubsystem,
561 _make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
562) -> Result<Backend, String> {
563 Err("built without the canvas-backend feature".to_string())
564}
565
566#[cfg(feature = "wgpu-backend")]
567fn build_wgpu(
568 video: &VideoSubsystem,
569 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
570) -> Result<Backend, String> {
571 let window = make_window(video, false)?;
572 let egui = pollster::block_on(crate::EguiWgpu::new(window));
575 Ok(Backend::Wgpu {
576 egui: Box::new(egui),
577 })
578}
579
580#[cfg(not(feature = "wgpu-backend"))]
581fn build_wgpu(
582 _video: &VideoSubsystem,
583 _make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
584) -> Result<Backend, String> {
585 Err("built without the wgpu-backend feature".to_string())
586}
587
588#[cfg(feature = "canvas-backend")]
591pub struct TurnedTarget {
592 texture: sdl2::render::Texture,
595 window: (u32, u32),
597}
598
599#[cfg(feature = "canvas-backend")]
603fn paint_turned(
604 canvas: &mut sdl2::render::WindowCanvas,
605 egui: &mut crate::EguiCanvas,
606 turned: &mut Option<TurnedTarget>,
607 rotation: Rotation,
608 clear_color: [f32; 4],
609) {
610 let window = match canvas.output_size() {
611 Ok(size) => size,
612 Err(e) => return log::error!("could not read the window size: {e}"),
613 };
614 if turned.as_ref().is_none_or(|t| t.window != window) {
615 let side = window.0.max(window.1);
616 match canvas.texture_creator().create_texture_target(
617 crate::canvas::painter::PIXEL_FORMAT,
618 side,
619 side,
620 ) {
621 Ok(mut texture) => {
622 texture.set_blend_mode(sdl2::render::BlendMode::None);
624 if let Some(old) = turned.replace(TurnedTarget { texture, window }) {
625 unsafe { old.texture.destroy() }
626 }
627 }
628 Err(e) => {
632 log::error!("could not build a {side}x{side} target to turn the frame in: {e}");
633 canvas.set_draw_color(rgb(clear_color));
634 canvas.clear();
635 egui.paint(canvas);
636 canvas.present();
637 return;
638 }
639 }
640 }
641 let Some(target) = turned.as_mut() else {
642 unreachable!("the target was just built, or was already the right size")
643 };
644
645 let painted = canvas.with_texture_canvas(&mut target.texture, |target| {
646 target.set_draw_color(rgb(clear_color));
647 target.clear();
648 egui.paint(target);
649 });
650 if let Err(e) = painted {
651 return log::error!("could not paint into the turned target: {e}");
652 }
653
654 let (w, h) = if rotation.swaps_axes() {
658 (window.1, window.0)
659 } else {
660 window
661 };
662 let src = sdl2::rect::Rect::new(0, 0, w, h);
663 let dst = sdl2::rect::Rect::new(
664 (window.0 as i32 - w as i32) / 2,
665 (window.1 as i32 - h as i32) / 2,
666 w,
667 h,
668 );
669 canvas.set_draw_color(rgb(clear_color));
670 canvas.clear();
671 let copied = canvas.copy_ex(
672 &target.texture,
673 Some(src),
674 Some(dst),
675 rotation.degrees(),
676 None,
677 false,
678 false,
679 );
680 if let Err(e) = copied {
681 log::error!("could not present the turned frame: {e}");
682 }
683 canvas.present();
684}
685
686#[cfg(feature = "canvas-backend")]
690fn rotate_frame(
691 rotation: Rotation,
692 src: &[u8],
693 pitch: usize,
694 window: (u32, u32),
695 dst: &mut Vec<u8>,
696) {
697 let (width, height) = (window.0 as usize, window.1 as usize);
698 let row = width * BYTES_PER_PIXEL;
699 if dst.len() != row * height {
700 dst.resize(row * height, 0);
701 }
702 for (y, line) in dst.chunks_exact_mut(row).enumerate() {
706 for (x, pixel) in line.chunks_exact_mut(BYTES_PER_PIXEL).enumerate() {
707 let (sx, sy) = match rotation {
710 Rotation::None => (x, y),
711 Rotation::Cw90 => (y, width - 1 - x),
712 Rotation::Cw180 => (width - 1 - x, height - 1 - y),
713 Rotation::Cw270 => (height - 1 - y, x),
714 };
715 let at = sy * pitch + sx * BYTES_PER_PIXEL;
716 pixel.copy_from_slice(&src[at..at + BYTES_PER_PIXEL]);
717 }
718 }
719}
720
721#[cfg(feature = "canvas-backend")]
723fn rgb(color: [f32; 4]) -> sdl2::pixels::Color {
724 let byte = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
725 sdl2::pixels::Color::RGB(byte(color[0]), byte(color[1]), byte(color[2]))
726}
727
728#[cfg(all(test, feature = "canvas-backend"))]
729mod tests {
730 use super::*;
731
732 const WINDOW: (u32, u32) = (3, 2);
734
735 fn painted(rotation: Rotation) -> (Vec<u8>, usize) {
738 let (w, h) = if rotation.swaps_axes() {
739 (WINDOW.1, WINDOW.0)
740 } else {
741 WINDOW
742 };
743 let side = WINDOW.0.max(WINDOW.1) as usize;
744 let pitch = side * BYTES_PER_PIXEL;
745 let mut buffer = vec![0u8; pitch * side];
746 for y in 0..h as usize {
747 for x in 0..w as usize {
748 let at = y * pitch + x * BYTES_PER_PIXEL;
749 buffer[at..at + BYTES_PER_PIXEL].fill((y * 10 + x) as u8);
750 }
751 }
752 (buffer, pitch)
753 }
754
755 fn presented(rotation: Rotation) -> Vec<u8> {
757 let (src, pitch) = painted(rotation);
758 let mut frame = Vec::new();
759 rotate_frame(rotation, &src, pitch, WINDOW, &mut frame);
760 assert_eq!(
761 frame.len(),
762 WINDOW.0 as usize * WINDOW.1 as usize * BYTES_PER_PIXEL
763 );
764 frame
765 .chunks_exact(BYTES_PER_PIXEL)
766 .map(|pixel| {
767 assert!(pixel.iter().all(|b| *b == pixel[0]), "a pixel was torn");
768 pixel[0]
769 })
770 .collect()
771 }
772
773 #[test]
774 fn an_unturned_frame_is_copied_across_as_it_is() {
775 assert_eq!(presented(Rotation::None), [0, 1, 2, 10, 11, 12]);
776 }
777
778 #[test]
779 fn a_quarter_turn_clockwise_stands_the_screen_up() {
780 assert_eq!(presented(Rotation::Cw90), [20, 10, 0, 21, 11, 1]);
783 }
784
785 #[test]
786 fn a_half_turn_reverses_both_axes() {
787 assert_eq!(presented(Rotation::Cw180), [12, 11, 10, 2, 1, 0]);
788 }
789
790 #[test]
791 fn a_quarter_turn_anticlockwise_is_the_other_way_round() {
792 assert_eq!(presented(Rotation::Cw270), [1, 11, 21, 0, 10, 20]);
793 }
794}