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 let format = egui.painter.format();
324 match rebuild_blit_targets(canvas, format) {
325 Ok((new_offscreen, new_present, new_size)) => {
326 let rotation = egui.state.rotation();
327 egui.destroy();
328 *egui = crate::EguiCanvas::for_surface_with_format(
329 canvas.window(),
330 &new_offscreen,
331 format,
332 );
333 egui.state.set_rotation(rotation);
336 *offscreen = new_offscreen;
337 *present = new_present;
338 *size = new_size;
339 }
340 Err(e) => log::error!("could not resize the offscreen target: {e}"),
341 }
342 }
343
344 offscreen.set_draw_color(rgb(clear_color));
345 offscreen.clear();
346 egui.paint(offscreen);
347 let rotation = egui.state.rotation();
348 let surface = offscreen.surface();
349 let pitch = surface.pitch() as usize;
350 match surface.without_lock() {
351 Some(pixels) => {
357 let uploaded = if rotation == Rotation::None {
358 present.update(None, pixels, pitch)
359 } else {
360 rotate_frame(rotation, pixels, pitch, *size, frame);
361 present.update(None, frame, size.0 as usize * BYTES_PER_PIXEL)
362 };
363 if let Err(e) = uploaded {
364 log::error!("could not upload the offscreen frame: {e}");
365 }
366 }
367 None => log::error!("offscreen surface has no readable pixels"),
368 }
369 if let Err(e) = canvas.copy(present, None, None) {
370 log::error!("could not blit the offscreen frame: {e}");
371 }
372 canvas.present();
373 }
374 #[cfg(feature = "wgpu-backend")]
375 Backend::Wgpu { egui } => egui.paint(clear_color),
376 }
377 }
378
379 pub fn destroy(&mut self) {
381 match &mut self.backend {
382 #[cfg(feature = "glow-backend")]
383 Backend::Glow { egui, .. } => egui.destroy(),
384 #[cfg(feature = "canvas-backend")]
385 Backend::Canvas { egui, turned, .. } => {
386 egui.destroy();
387 if let Some(target) = turned.take() {
388 unsafe { target.texture.destroy() }
389 }
390 }
391 #[cfg(feature = "canvas-backend")]
392 Backend::CanvasBlit { egui, .. } => egui.destroy(),
393 #[cfg(feature = "wgpu-backend")]
394 Backend::Wgpu { .. } => {}
395 }
396 }
397}
398
399fn build(
400 video: &VideoSubsystem,
401 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
402 renderer: Renderer,
403) -> Result<Backend, String> {
404 match renderer {
405 Renderer::Gles3 => build_glow(video, make_window, sdl2::video::GLProfile::GLES, 3, 0),
406 Renderer::Gl32 => build_glow(video, make_window, sdl2::video::GLProfile::Core, 3, 2),
407 Renderer::Canvas => build_canvas(video, make_window),
408 Renderer::CanvasBlit => build_canvas_blit(video, make_window),
409 Renderer::Wgpu => build_wgpu(video, make_window),
410 }
411}
412
413#[cfg(feature = "glow-backend")]
415fn set_gl_attr(name: &str, attr: sdl2::sys::SDL_GLattr, value: i32) -> Result<(), String> {
416 if unsafe { sdl2::sys::SDL_GL_SetAttribute(attr, value) } == 0 {
417 return Ok(());
418 }
419 Err(format!("{name}={value} rejected: {}", sdl2::get_error()))
420}
421
422#[cfg(feature = "glow-backend")]
424fn gl_profile_value(profile: sdl2::video::GLProfile) -> i32 {
425 use sdl2::video::GLProfile;
426 match profile {
427 GLProfile::Core => 1,
428 GLProfile::Compatibility => 2,
429 GLProfile::GLES => 4,
430 GLProfile::Unknown(i) => i,
431 }
432}
433
434#[cfg(feature = "glow-backend")]
435fn build_glow(
436 video: &VideoSubsystem,
437 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
438 profile: sdl2::video::GLProfile,
439 major: u8,
440 minor: u8,
441) -> Result<Backend, String> {
442 use sdl2::sys::SDL_GLattr::*;
445 set_gl_attr(
446 "profile_mask",
447 SDL_GL_CONTEXT_PROFILE_MASK,
448 gl_profile_value(profile),
449 )?;
450 set_gl_attr("major_version", SDL_GL_CONTEXT_MAJOR_VERSION, major as i32)?;
451 set_gl_attr("minor_version", SDL_GL_CONTEXT_MINOR_VERSION, minor as i32)?;
452 set_gl_attr("doublebuffer", SDL_GL_DOUBLEBUFFER, 1)?;
453
454 let window = make_window(video, true)?;
455 let gl_context = window.gl_create_context()?;
456 window.gl_make_current(&gl_context)?;
457 let _ = video.gl_set_swap_interval(sdl2::video::SwapInterval::VSync);
458
459 let glow_ctx = std::sync::Arc::new(unsafe {
460 glow::Context::from_loader_function(|name| {
461 video.gl_get_proc_address(name) as *const std::os::raw::c_void
462 })
463 });
464 let egui = crate::EguiGlow::new(&window, glow_ctx, None, false);
465 Ok(Backend::Glow {
466 window,
467 _gl_context: gl_context,
468 egui,
469 })
470}
471
472#[cfg(not(feature = "glow-backend"))]
473fn build_glow(
474 _video: &VideoSubsystem,
475 _make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
476 _profile: sdl2::video::GLProfile,
477 _major: u8,
478 _minor: u8,
479) -> Result<Backend, String> {
480 Err("built without the glow-backend feature".to_string())
481}
482
483#[cfg(feature = "canvas-backend")]
484fn build_canvas(
485 video: &VideoSubsystem,
486 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
487) -> Result<Backend, String> {
488 let window = make_window(video, false)?;
489 let canvas = window
490 .into_canvas()
491 .present_vsync()
492 .build()
493 .map_err(|e| e.to_string())?;
494 log::debug!("SDL renderer driver: {}", canvas.info().name);
495 let egui = crate::EguiCanvas::new(&canvas);
496 Ok(Backend::Canvas {
497 canvas,
498 egui,
499 turned: None,
500 })
501}
502
503#[cfg(feature = "canvas-backend")]
505type BlitTargets = (
506 sdl2::render::Canvas<sdl2::surface::Surface<'static>>,
507 sdl2::render::Texture,
508 (u32, u32),
509);
510
511#[cfg(feature = "canvas-backend")]
512fn rebuild_blit_targets(
513 canvas: &sdl2::render::WindowCanvas,
514 format: sdl2::pixels::PixelFormatEnum,
515) -> Result<BlitTargets, String> {
516 let size = canvas.output_size()?;
517 let side = size.0.max(size.1);
522 let surface = sdl2::surface::Surface::new(side, side, format)?;
523 let offscreen = sdl2::render::Canvas::from_surface(surface)?;
524 let mut present = canvas
525 .texture_creator()
526 .create_texture_streaming(format, size.0, size.1)
527 .map_err(|e| e.to_string())?;
528 present.set_blend_mode(sdl2::render::BlendMode::None);
533 Ok((offscreen, present, size))
534}
535
536#[cfg(feature = "canvas-backend")]
537fn build_canvas_blit(
538 video: &VideoSubsystem,
539 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
540) -> Result<Backend, String> {
541 let window = make_window(video, false)?;
542 let canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
545 log::debug!("SDL renderer driver: {} (blit)", canvas.info().name);
546 let format = crate::canvas::painter::preferred_format(&canvas);
548 log::debug!("blit format: {format:?}");
549 let (offscreen, present, size) = rebuild_blit_targets(&canvas, format)?;
550 let egui = crate::EguiCanvas::for_surface_with_format(canvas.window(), &offscreen, format);
551 Ok(Backend::CanvasBlit {
552 canvas,
553 offscreen,
554 present,
555 size,
556 frame: Vec::new(),
557 egui,
558 })
559}
560
561#[cfg(not(feature = "canvas-backend"))]
562fn build_canvas_blit(
563 _video: &VideoSubsystem,
564 _make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
565) -> Result<Backend, String> {
566 Err("built without the canvas-backend feature".to_string())
567}
568
569#[cfg(not(feature = "canvas-backend"))]
570fn build_canvas(
571 _video: &VideoSubsystem,
572 _make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
573) -> Result<Backend, String> {
574 Err("built without the canvas-backend feature".to_string())
575}
576
577#[cfg(feature = "wgpu-backend")]
578fn build_wgpu(
579 video: &VideoSubsystem,
580 make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
581) -> Result<Backend, String> {
582 let window = make_window(video, false)?;
583 let egui = pollster::block_on(crate::EguiWgpu::new(window));
586 Ok(Backend::Wgpu {
587 egui: Box::new(egui),
588 })
589}
590
591#[cfg(not(feature = "wgpu-backend"))]
592fn build_wgpu(
593 _video: &VideoSubsystem,
594 _make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
595) -> Result<Backend, String> {
596 Err("built without the wgpu-backend feature".to_string())
597}
598
599#[cfg(feature = "canvas-backend")]
602pub struct TurnedTarget {
603 texture: sdl2::render::Texture,
606 window: (u32, u32),
608}
609
610#[cfg(feature = "canvas-backend")]
614fn paint_turned(
615 canvas: &mut sdl2::render::WindowCanvas,
616 egui: &mut crate::EguiCanvas,
617 turned: &mut Option<TurnedTarget>,
618 rotation: Rotation,
619 clear_color: [f32; 4],
620) {
621 let window = match canvas.output_size() {
622 Ok(size) => size,
623 Err(e) => return log::error!("could not read the window size: {e}"),
624 };
625 if turned.as_ref().is_none_or(|t| t.window != window) {
626 let side = window.0.max(window.1);
627 match canvas
628 .texture_creator()
629 .create_texture_target(egui.painter.format(), side, side)
630 {
631 Ok(mut texture) => {
632 texture.set_blend_mode(sdl2::render::BlendMode::None);
634 if let Some(old) = turned.replace(TurnedTarget { texture, window }) {
635 unsafe { old.texture.destroy() }
636 }
637 }
638 Err(e) => {
642 log::error!("could not build a {side}x{side} target to turn the frame in: {e}");
643 canvas.set_draw_color(rgb(clear_color));
644 canvas.clear();
645 egui.paint(canvas);
646 canvas.present();
647 return;
648 }
649 }
650 }
651 let Some(target) = turned.as_mut() else {
652 unreachable!("the target was just built, or was already the right size")
653 };
654
655 let painted = canvas.with_texture_canvas(&mut target.texture, |target| {
656 target.set_draw_color(rgb(clear_color));
657 target.clear();
658 egui.paint(target);
659 });
660 if let Err(e) = painted {
661 return log::error!("could not paint into the turned target: {e}");
662 }
663
664 let (w, h) = if rotation.swaps_axes() {
668 (window.1, window.0)
669 } else {
670 window
671 };
672 let src = sdl2::rect::Rect::new(0, 0, w, h);
673 let dst = sdl2::rect::Rect::new(
674 (window.0 as i32 - w as i32) / 2,
675 (window.1 as i32 - h as i32) / 2,
676 w,
677 h,
678 );
679 canvas.set_draw_color(rgb(clear_color));
680 canvas.clear();
681 let copied = canvas.copy_ex(
682 &target.texture,
683 Some(src),
684 Some(dst),
685 rotation.degrees(),
686 None,
687 false,
688 false,
689 );
690 if let Err(e) = copied {
691 log::error!("could not present the turned frame: {e}");
692 }
693 canvas.present();
694}
695
696#[cfg(feature = "canvas-backend")]
700fn rotate_frame(
701 rotation: Rotation,
702 src: &[u8],
703 pitch: usize,
704 window: (u32, u32),
705 dst: &mut Vec<u8>,
706) {
707 let (width, height) = (window.0 as usize, window.1 as usize);
708 let row = width * BYTES_PER_PIXEL;
709 if dst.len() != row * height {
710 dst.resize(row * height, 0);
711 }
712 for (y, line) in dst.chunks_exact_mut(row).enumerate() {
716 for (x, pixel) in line.chunks_exact_mut(BYTES_PER_PIXEL).enumerate() {
717 let (sx, sy) = match rotation {
720 Rotation::None => (x, y),
721 Rotation::Cw90 => (y, width - 1 - x),
722 Rotation::Cw180 => (width - 1 - x, height - 1 - y),
723 Rotation::Cw270 => (height - 1 - y, x),
724 };
725 let at = sy * pitch + sx * BYTES_PER_PIXEL;
726 pixel.copy_from_slice(&src[at..at + BYTES_PER_PIXEL]);
727 }
728 }
729}
730
731#[cfg(feature = "canvas-backend")]
733fn rgb(color: [f32; 4]) -> sdl2::pixels::Color {
734 let byte = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
735 sdl2::pixels::Color::RGB(byte(color[0]), byte(color[1]), byte(color[2]))
736}
737
738#[cfg(all(test, feature = "canvas-backend"))]
739mod tests {
740 use super::*;
741
742 const WINDOW: (u32, u32) = (3, 2);
744
745 fn painted(rotation: Rotation) -> (Vec<u8>, usize) {
748 let (w, h) = if rotation.swaps_axes() {
749 (WINDOW.1, WINDOW.0)
750 } else {
751 WINDOW
752 };
753 let side = WINDOW.0.max(WINDOW.1) as usize;
754 let pitch = side * BYTES_PER_PIXEL;
755 let mut buffer = vec![0u8; pitch * side];
756 for y in 0..h as usize {
757 for x in 0..w as usize {
758 let at = y * pitch + x * BYTES_PER_PIXEL;
759 buffer[at..at + BYTES_PER_PIXEL].fill((y * 10 + x) as u8);
760 }
761 }
762 (buffer, pitch)
763 }
764
765 fn presented(rotation: Rotation) -> Vec<u8> {
767 let (src, pitch) = painted(rotation);
768 let mut frame = Vec::new();
769 rotate_frame(rotation, &src, pitch, WINDOW, &mut frame);
770 assert_eq!(
771 frame.len(),
772 WINDOW.0 as usize * WINDOW.1 as usize * BYTES_PER_PIXEL
773 );
774 frame
775 .chunks_exact(BYTES_PER_PIXEL)
776 .map(|pixel| {
777 assert!(pixel.iter().all(|b| *b == pixel[0]), "a pixel was torn");
778 pixel[0]
779 })
780 .collect()
781 }
782
783 #[test]
784 fn an_unturned_frame_is_copied_across_as_it_is() {
785 assert_eq!(presented(Rotation::None), [0, 1, 2, 10, 11, 12]);
786 }
787
788 #[test]
789 fn a_quarter_turn_clockwise_stands_the_screen_up() {
790 assert_eq!(presented(Rotation::Cw90), [20, 10, 0, 21, 11, 1]);
793 }
794
795 #[test]
796 fn a_half_turn_reverses_both_axes() {
797 assert_eq!(presented(Rotation::Cw180), [12, 11, 10, 2, 1, 0]);
798 }
799
800 #[test]
801 fn a_quarter_turn_counterclockwise_is_the_other_way_round() {
802 assert_eq!(presented(Rotation::Cw270), [1, 11, 21, 0, 10, 20]);
803 }
804}