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                    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                            // The fresh state starts unturned; the window's turn
334                            // outlives the target it was being presented on.
335                            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                    // The offscreen is square and egui painted into its top-left
352                    // corner, so the wider pitch is all SDL needs to pick an
353                    // unturned frame out of it. A turned one is copied out here
354                    // rather than rotated by the driver: this mode exists for
355                    // drivers that show a texture copy and little else.
356                    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    /// Release the renderer's graphics resources.
380    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/// `SDL_GL_SetAttribute`, reported rather than asserted.
414#[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/// The values `SDL_GL_CONTEXT_PROFILE_MASK` takes.
423#[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    // Not `video.gl_attr()`: its setters panic on rejection, which would kill
443    // the fallthrough on a device without GL.
444    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/// The offscreen surface, its presentation texture, and the size both cover.
504#[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    // Square, on the longer edge: a quarter turn lays the screen out as tall as
518    // the window is wide, and sizing the surface to the turn instead would mean
519    // rebuilding the renderer whenever the turn changed — which takes egui's
520    // textures with it. egui paints into the top-left corner either way.
521    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    // A whole frame replaces rather than blends. SDL gives a format with alpha
529    // `BLEND` by default, which would dim any pixel the offscreen renderer left
530    // short of opaque against whatever the window happened to hold, and there is
531    // nothing under a full frame worth mixing in.
532    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    // No vsync request: not every driver this mode serves advertises it, and
543    // asking excludes those that don't.
544    let canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
545    log::debug!("SDL renderer driver: {} (blit)", canvas.info().name);
546    // The window's renderer decides: the whole frame crosses to it every frame.
547    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    // wgpu's setup is async; this is startup, so blocking on it is the whole
584    // ceremony an app would otherwise write itself.
585    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/// Where a turn is painted before it reaches the window: the window canvas has
600/// no offscreen of its own, and SDL cannot rotate what it draws directly.
601#[cfg(feature = "canvas-backend")]
602pub struct TurnedTarget {
603    /// Square, on the window's longer edge, so a change of turn fits without a
604    /// rebuild. egui paints into its top-left corner.
605    texture: sdl2::render::Texture,
606    /// The window size it was built for.
607    window: (u32, u32),
608}
609
610/// Paint egui into the turned target and copy that onto the window at an angle.
611/// One rotated copy per frame, which every accelerated driver does for free and
612/// SDL's own renderer has an exact path for at multiples of 90°.
613#[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                // A whole frame replaces rather than blends, as in the blit path.
633                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            // Every accelerated driver has render targets, and so does SDL's own
639            // software renderer; a driver without them shows the frame unturned
640            // rather than nothing at all.
641            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    // The frame egui laid out, in the corner of the square target, and where the
665    // turn lands it: SDL rotates a copy about the centre of its destination, so
666    // a turned frame placed centrally comes down over the whole window.
667    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/// Copy the frame out of the square offscreen buffer turned, as a tight
697/// window-sized one. `src` holds the screen egui painted — as wide as the window
698/// is tall on a quarter turn — in the top-left corner of a buffer of `pitch`.
699#[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    // Walked by destination row, so the writes run straight through; on a
713    // quarter turn it is the reads that step down a column instead, which is the
714    // cheaper of the two to scatter.
715    for (y, line) in dst.chunks_exact_mut(row).enumerate() {
716        for (x, pixel) in line.chunks_exact_mut(BYTES_PER_PIXEL).enumerate() {
717            // Where this window pixel sits in the turned screen — the whole-pixel
718            // form of `Rotation::from_window`.
719            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/// egui and GL take linear floats; SDL clears in 8-bit channels.
732#[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    /// A 3x2 window, so a quarter turn is visibly a different shape.
743    const WINDOW: (u32, u32) = (3, 2);
744
745    /// The screen for this turn, painted into the corner of a square buffer, one
746    /// value per pixel repeated across its channels.
747    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    /// One value per window pixel, row by row.
766    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        // The screen is 2 wide and 3 tall:  0  1  ·  10 11  ·  20 21
791        // and comes down on the window as: 20 10 0  ·  21 11 1
792        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}