Skip to main content

egui_sdl2/
window.rs

1//! A window plus the best renderer it can get: [`EguiWindow`] walks a list of
2//! [`Renderer`]s and keeps the first that comes up, so an app on a device with
3//! missing or broken GL drivers still shows a UI instead of exiting.
4//!
5//! The window is owned here because the attempts need different ones — GL sets
6//! flags on the [`WindowBuilder`], wgpu takes the window by value — and a
7//! builder cannot be reused. Pass the title and size; `configure` runs on a
8//! fresh builder per attempt, for whatever else the window needs.
9//!
10//! ```no_run
11//! let sdl = sdl2::init().unwrap();
12//! let video = sdl.video().unwrap();
13//! let mut egui = egui_sdl2::EguiWindow::new(
14//!     &video,
15//!     "Egui SDL2",
16//!     (800, 600),
17//!     |builder| {
18//!         builder.resizable();
19//!     },
20//!     &egui_sdl2::Renderer::FALLBACK_CHAIN,
21//! )
22//! .unwrap();
23//! let mut event_pump = sdl.event_pump().unwrap();
24//! loop {
25//!     for event in event_pump.poll_iter() {
26//!         egui.on_event(&event);
27//!     }
28//!     egui.run(|ctx: &egui::Context| {});
29//!     egui.paint([0.1, 0.1, 0.1, 1.0]);
30//! }
31//! ```
32
33#[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/// A way to put egui on screen. Apps list the ones they accept, best first.
41#[non_exhaustive]
42#[derive(Copy, Clone, PartialEq, Eq, Debug)]
43pub enum Renderer {
44    /// OpenGL ES 3.0 through [`glow`](https://crates.io/crates/glow) — what
45    /// mobile and handheld GPU blobs expose.
46    Gles3,
47    /// Desktop OpenGL 3.2 core through glow.
48    Gl32,
49    /// SDL's own 2D renderer: an accelerated driver when SDL finds one, its
50    /// software rasterizer otherwise. The one that needs no GL at all.
51    Canvas,
52    /// [`Self::Canvas`] rasterized offscreen, presented as one texture copy per
53    /// frame — for drivers that show nothing else, like the Miyoo Mini's
54    /// `mmiyoo`. Costs an upload per frame.
55    CanvasBlit,
56    /// wgpu (`wgpu-backend` feature).
57    Wgpu,
58}
59
60impl Renderer {
61    /// GL first, SDL's renderer as the safety net.
62    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        // Kept alive for the lifetime of the window; dropping it destroys the context.
80        _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        /// Built the first time a turn is presented, and only then: an unturned
88        /// frame goes straight to the window as it always did.
89        turned: Option<TurnedTarget>,
90    },
91    #[cfg(feature = "canvas-backend")]
92    CanvasBlit {
93        canvas: sdl2::render::WindowCanvas,
94        /// egui is drawn here, by SDL's software renderer.
95        offscreen: sdl2::render::Canvas<sdl2::surface::Surface<'static>>,
96        /// The offscreen pixels, uploaded and copied once per frame.
97        present: sdl2::render::Texture,
98        /// Window size `offscreen` and `present` were built for.
99        size: (u32, u32),
100        /// The turned copy of the offscreen frame, reused across frames.
101        frame: Vec<u8>,
102        egui: crate::EguiCanvas<sdl2::surface::SurfaceContext<'static>>,
103    },
104    // Boxed: an EguiWgpu is twice the size of the other variants.
105    #[cfg(feature = "wgpu-backend")]
106    Wgpu { egui: Box<crate::EguiWgpu> },
107}
108
109/// egui and the window it draws into, over whichever renderer was available.
110/// Vsync is on; every backend clears, paints and presents in [`Self::paint`].
111pub struct EguiWindow {
112    backend: Backend,
113    renderer: Renderer,
114}
115
116impl EguiWindow {
117    /// Try `order` in sequence, returning the first renderer that comes up.
118    /// The error is the last attempt's, since that is the one that decided it.
119    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    /// Which one won, for the app's own logging and about screens.
151    pub fn renderer(&self) -> Renderer {
152        self.renderer
153    }
154
155    /// Present the UI at a quarter turn to the window, for a panel that is not
156    /// mounted the way it is read.
157    ///
158    /// egui lays out for the turned screen — a quarter turn trades the window's
159    /// width and height — and this window puts the frame back on the panel:
160    /// [`Renderer::Gles3`] and [`Renderer::Gl32`] turn the geometry as they draw
161    /// it, the SDL renderers paint offscreen and present that turned. Pointer
162    /// and touch positions travel back the same way, so a tap lands where it
163    /// looks. May be called at any time; nothing is rebuilt on a change of turn.
164    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    /// For the things SDL only does through the window itself, like
217    /// [`Window::set_icon`].
218    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    /// Feed an SDL event to egui; wgpu also resizes its surface here.
232    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    /// Run the UI; [`Self::paint`] puts the result on screen.
246    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    /// Like [`Self::run`], but hands the closure egui's root [`egui::Ui`].
260    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    /// How long until egui wants another frame, from the last [`Self::run`].
274    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    /// Clear to `clear_color`, paint the last [`Self::run`], present.
288    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                // Rebuild on resize so surface, texture and window stay 1:1.
322                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                            // The fresh state starts unturned; the window's turn
329                            // outlives the target it was being presented on.
330                            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                    // The offscreen is square and egui painted into its top-left
347                    // corner, so the wider pitch is all SDL needs to pick an
348                    // unturned frame out of it. A turned one is copied out here
349                    // rather than rotated by the driver: this mode exists for
350                    // drivers that show a texture copy and little else.
351                    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    /// Release the renderer's graphics resources.
375    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/// `SDL_GL_SetAttribute`, reported rather than asserted.
409#[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/// The values `SDL_GL_CONTEXT_PROFILE_MASK` takes.
418#[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    // Not `video.gl_attr()`: its setters panic on rejection, which would kill
438    // the fallthrough on a device without GL.
439    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/// The offscreen surface, its presentation texture, and the size both cover.
499#[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    // Square, on the longer edge: a quarter turn lays the screen out as tall as
510    // the window is wide, and sizing the surface to the turn instead would mean
511    // rebuilding the renderer whenever the turn changed — which takes egui's
512    // textures with it. egui paints into the top-left corner either way.
513    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    // A whole frame replaces rather than blends. SDL gives a format with alpha
521    // `BLEND` by default, which would dim any pixel the offscreen renderer left
522    // short of opaque against whatever the window happened to hold, and there is
523    // nothing under a full frame worth mixing in.
524    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    // No vsync request: not every driver this mode serves advertises it, and
535    // asking excludes those that don't.
536    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    // wgpu's setup is async; this is startup, so blocking on it is the whole
573    // ceremony an app would otherwise write itself.
574    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/// Where a turn is painted before it reaches the window: the window canvas has
589/// no offscreen of its own, and SDL cannot rotate what it draws directly.
590#[cfg(feature = "canvas-backend")]
591pub struct TurnedTarget {
592    /// Square, on the window's longer edge, so a change of turn fits without a
593    /// rebuild. egui paints into its top-left corner.
594    texture: sdl2::render::Texture,
595    /// The window size it was built for.
596    window: (u32, u32),
597}
598
599/// Paint egui into the turned target and copy that onto the window at an angle.
600/// One rotated copy per frame, which every accelerated driver does for free and
601/// SDL's own renderer has an exact path for at multiples of 90°.
602#[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                // A whole frame replaces rather than blends, as in the blit path.
623                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            // Every accelerated driver has render targets, and so does SDL's own
629            // software renderer; a driver without them shows the frame unturned
630            // rather than nothing at all.
631            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    // The frame egui laid out, in the corner of the square target, and where the
655    // turn lands it: SDL rotates a copy about the centre of its destination, so
656    // a turned frame placed centrally comes down over the whole window.
657    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/// Copy the frame out of the square offscreen buffer turned, as a tight
687/// window-sized one. `src` holds the screen egui painted — as wide as the window
688/// is tall on a quarter turn — in the top-left corner of a buffer of `pitch`.
689#[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    // Walked by destination row, so the writes run straight through; on a
703    // quarter turn it is the reads that step down a column instead, which is the
704    // cheaper of the two to scatter.
705    for (y, line) in dst.chunks_exact_mut(row).enumerate() {
706        for (x, pixel) in line.chunks_exact_mut(BYTES_PER_PIXEL).enumerate() {
707            // Where this window pixel sits in the turned screen — the whole-pixel
708            // form of `Rotation::from_window`.
709            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/// egui and GL take linear floats; SDL clears in 8-bit channels.
722#[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    /// A 3x2 window, so a quarter turn is visibly a different shape.
733    const WINDOW: (u32, u32) = (3, 2);
734
735    /// The screen for this turn, painted into the corner of a square buffer, one
736    /// value per pixel repeated across its channels.
737    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    /// One value per window pixel, row by row.
756    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        // The screen is 2 wide and 3 tall:  0  1  ·  10 11  ·  20 21
781        // and comes down on the window as: 20 10 0  ·  21 11 1
782        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}