use sdl2::event::Event;
use sdl2::video::{Window, WindowBuilder};
use sdl2::VideoSubsystem;
#[non_exhaustive]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Renderer {
Gles3,
Gl32,
Canvas,
CanvasBlit,
Wgpu,
}
impl Renderer {
pub const FALLBACK_CHAIN: [Renderer; 3] = [Renderer::Gles3, Renderer::Gl32, Renderer::Canvas];
fn name(self) -> &'static str {
match self {
Renderer::Gles3 => "GLES 3.0",
Renderer::Gl32 => "GL 3.2 core",
Renderer::Canvas => "SDL renderer",
Renderer::CanvasBlit => "SDL renderer (offscreen blit)",
Renderer::Wgpu => "wgpu",
}
}
}
enum Backend {
#[cfg(feature = "glow-backend")]
Glow {
window: Window,
_gl_context: sdl2::video::GLContext,
egui: crate::EguiGlow,
},
#[cfg(feature = "canvas-backend")]
Canvas {
canvas: sdl2::render::WindowCanvas,
egui: crate::EguiCanvas,
},
#[cfg(feature = "canvas-backend")]
CanvasBlit {
canvas: sdl2::render::WindowCanvas,
offscreen: sdl2::render::Canvas<sdl2::surface::Surface<'static>>,
present: sdl2::render::Texture,
size: (u32, u32),
egui: crate::EguiCanvas<sdl2::surface::SurfaceContext<'static>>,
},
#[cfg(feature = "wgpu-backend")]
Wgpu { egui: Box<crate::EguiWgpu> },
}
pub struct EguiWindow {
backend: Backend,
renderer: Renderer,
}
impl EguiWindow {
pub fn new(
video: &VideoSubsystem,
title: &str,
size: (u32, u32),
configure: impl Fn(&mut WindowBuilder),
order: &[Renderer],
) -> Result<Self, String> {
let make_window = |video: &VideoSubsystem, gl: bool| {
let mut builder = video.window(title, size.0, size.1);
if gl {
builder.opengl();
}
configure(&mut builder);
builder.build().map_err(|e| e.to_string())
};
let mut last = "no renderer requested".to_string();
for &renderer in order {
match build(video, &make_window, renderer) {
Ok(backend) => {
log::info!("egui renderer: {}", renderer.name());
return Ok(Self { backend, renderer });
}
Err(e) => {
log::warn!("{} unavailable: {e}", renderer.name());
last = e;
}
}
}
Err(last)
}
pub fn renderer(&self) -> Renderer {
self.renderer
}
pub fn ctx(&self) -> &egui::Context {
match &self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { egui, .. } => &egui.ctx,
#[cfg(feature = "canvas-backend")]
Backend::Canvas { egui, .. } => &egui.ctx,
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit { egui, .. } => &egui.ctx,
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { egui } => &egui.ctx,
}
}
pub fn window(&self) -> &Window {
match &self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { window, .. } => window,
#[cfg(feature = "canvas-backend")]
Backend::Canvas { canvas, .. } => canvas.window(),
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit { canvas, .. } => canvas.window(),
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { egui } => &egui.window,
}
}
pub fn window_mut(&mut self) -> &mut Window {
match &mut self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { window, .. } => window,
#[cfg(feature = "canvas-backend")]
Backend::Canvas { canvas, .. } => canvas.window_mut(),
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit { canvas, .. } => canvas.window_mut(),
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { egui } => &mut egui.window,
}
}
pub fn on_event(&mut self, event: &Event) -> crate::EventResponse {
match &mut self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { window, egui, .. } => egui.state.on_event(window, event),
#[cfg(feature = "canvas-backend")]
Backend::Canvas { canvas, egui } => egui.state.on_event(canvas.window(), event),
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit { canvas, egui, .. } => egui.state.on_event(canvas.window(), event),
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { egui } => egui.on_event(event),
}
}
pub fn run(&mut self, run_ui: impl FnMut(&egui::Context)) {
match &mut self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { egui, .. } => egui.run(run_ui),
#[cfg(feature = "canvas-backend")]
Backend::Canvas { egui, .. } => egui.run(run_ui),
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit { egui, .. } => egui.run(run_ui),
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { egui } => egui.run(run_ui),
}
}
pub fn run_ui(&mut self, run_ui: impl FnMut(&mut egui::Ui)) {
match &mut self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { egui, .. } => egui.run_ui(run_ui),
#[cfg(feature = "canvas-backend")]
Backend::Canvas { egui, .. } => egui.run_ui(run_ui),
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit { egui, .. } => egui.run_ui(run_ui),
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { egui } => egui.run_ui(run_ui),
}
}
pub fn repaint_delay(&self) -> std::time::Duration {
match &self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { egui, .. } => egui.repaint_delay(),
#[cfg(feature = "canvas-backend")]
Backend::Canvas { egui, .. } => egui.repaint_delay(),
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit { egui, .. } => egui.repaint_delay(),
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { egui } => egui.repaint_delay(),
}
}
pub fn paint(&mut self, clear_color: [f32; 4]) {
match &mut self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { window, egui, .. } => {
egui.clear(clear_color);
egui.paint();
window.gl_swap_window();
}
#[cfg(feature = "canvas-backend")]
Backend::Canvas { canvas, egui } => {
canvas.set_draw_color(rgb(clear_color));
canvas.clear();
egui.paint(canvas);
canvas.present();
}
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit {
canvas,
offscreen,
present,
size,
egui,
} => {
if canvas.output_size().is_ok_and(|s| s != *size) {
match rebuild_blit_targets(canvas) {
Ok((new_offscreen, new_present, new_size)) => {
egui.destroy();
*egui = crate::EguiCanvas::for_surface(canvas.window(), &new_offscreen);
*offscreen = new_offscreen;
*present = new_present;
*size = new_size;
}
Err(e) => log::error!("could not resize the offscreen target: {e}"),
}
}
offscreen.set_draw_color(rgb(clear_color));
offscreen.clear();
egui.paint(offscreen);
let surface = offscreen.surface();
let pitch = surface.pitch() as usize;
match surface.without_lock() {
Some(pixels) => {
if let Err(e) = present.update(None, pixels, pitch) {
log::error!("could not upload the offscreen frame: {e}");
}
}
None => log::error!("offscreen surface has no readable pixels"),
}
if let Err(e) = canvas.copy(present, None, None) {
log::error!("could not blit the offscreen frame: {e}");
}
canvas.present();
}
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { egui } => egui.paint(clear_color),
}
}
pub fn destroy(&mut self) {
match &mut self.backend {
#[cfg(feature = "glow-backend")]
Backend::Glow { egui, .. } => egui.destroy(),
#[cfg(feature = "canvas-backend")]
Backend::Canvas { egui, .. } => egui.destroy(),
#[cfg(feature = "canvas-backend")]
Backend::CanvasBlit { egui, .. } => egui.destroy(),
#[cfg(feature = "wgpu-backend")]
Backend::Wgpu { .. } => {}
}
}
}
fn build(
video: &VideoSubsystem,
make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
renderer: Renderer,
) -> Result<Backend, String> {
match renderer {
Renderer::Gles3 => build_glow(video, make_window, sdl2::video::GLProfile::GLES, 3, 0),
Renderer::Gl32 => build_glow(video, make_window, sdl2::video::GLProfile::Core, 3, 2),
Renderer::Canvas => build_canvas(video, make_window),
Renderer::CanvasBlit => build_canvas_blit(video, make_window),
Renderer::Wgpu => build_wgpu(video, make_window),
}
}
#[cfg(feature = "glow-backend")]
fn set_gl_attr(name: &str, attr: sdl2::sys::SDL_GLattr, value: i32) -> Result<(), String> {
if unsafe { sdl2::sys::SDL_GL_SetAttribute(attr, value) } == 0 {
return Ok(());
}
Err(format!("{name}={value} rejected: {}", sdl2::get_error()))
}
#[cfg(feature = "glow-backend")]
fn gl_profile_value(profile: sdl2::video::GLProfile) -> i32 {
use sdl2::video::GLProfile;
match profile {
GLProfile::Core => 1,
GLProfile::Compatibility => 2,
GLProfile::GLES => 4,
GLProfile::Unknown(i) => i,
}
}
#[cfg(feature = "glow-backend")]
fn build_glow(
video: &VideoSubsystem,
make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
profile: sdl2::video::GLProfile,
major: u8,
minor: u8,
) -> Result<Backend, String> {
use sdl2::sys::SDL_GLattr::*;
set_gl_attr(
"profile_mask",
SDL_GL_CONTEXT_PROFILE_MASK,
gl_profile_value(profile),
)?;
set_gl_attr("major_version", SDL_GL_CONTEXT_MAJOR_VERSION, major as i32)?;
set_gl_attr("minor_version", SDL_GL_CONTEXT_MINOR_VERSION, minor as i32)?;
set_gl_attr("doublebuffer", SDL_GL_DOUBLEBUFFER, 1)?;
let window = make_window(video, true)?;
let gl_context = window.gl_create_context()?;
window.gl_make_current(&gl_context)?;
let _ = video.gl_set_swap_interval(sdl2::video::SwapInterval::VSync);
let glow_ctx = std::sync::Arc::new(unsafe {
glow::Context::from_loader_function(|name| {
video.gl_get_proc_address(name) as *const std::os::raw::c_void
})
});
let egui = crate::EguiGlow::new(&window, glow_ctx, None, false);
Ok(Backend::Glow {
window,
_gl_context: gl_context,
egui,
})
}
#[cfg(not(feature = "glow-backend"))]
fn build_glow(
_video: &VideoSubsystem,
_make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
_profile: sdl2::video::GLProfile,
_major: u8,
_minor: u8,
) -> Result<Backend, String> {
Err("built without the glow-backend feature".to_string())
}
#[cfg(feature = "canvas-backend")]
fn build_canvas(
video: &VideoSubsystem,
make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
) -> Result<Backend, String> {
let window = make_window(video, false)?;
let canvas = window
.into_canvas()
.present_vsync()
.build()
.map_err(|e| e.to_string())?;
log::debug!("SDL renderer driver: {}", canvas.info().name);
let egui = crate::EguiCanvas::new(&canvas);
Ok(Backend::Canvas { canvas, egui })
}
#[cfg(feature = "canvas-backend")]
type BlitTargets = (
sdl2::render::Canvas<sdl2::surface::Surface<'static>>,
sdl2::render::Texture,
(u32, u32),
);
#[cfg(feature = "canvas-backend")]
fn rebuild_blit_targets(canvas: &sdl2::render::WindowCanvas) -> Result<BlitTargets, String> {
let size = canvas.output_size()?;
let surface =
sdl2::surface::Surface::new(size.0, size.1, crate::canvas::painter::PIXEL_FORMAT)?;
let offscreen = sdl2::render::Canvas::from_surface(surface)?;
let present = canvas
.texture_creator()
.create_texture_streaming(crate::canvas::painter::PIXEL_FORMAT, size.0, size.1)
.map_err(|e| e.to_string())?;
Ok((offscreen, present, size))
}
#[cfg(feature = "canvas-backend")]
fn build_canvas_blit(
video: &VideoSubsystem,
make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
) -> Result<Backend, String> {
let window = make_window(video, false)?;
let canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
log::debug!("SDL renderer driver: {} (blit)", canvas.info().name);
let (offscreen, present, size) = rebuild_blit_targets(&canvas)?;
let egui = crate::EguiCanvas::for_surface(canvas.window(), &offscreen);
Ok(Backend::CanvasBlit {
canvas,
offscreen,
present,
size,
egui,
})
}
#[cfg(not(feature = "canvas-backend"))]
fn build_canvas_blit(
_video: &VideoSubsystem,
_make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
) -> Result<Backend, String> {
Err("built without the canvas-backend feature".to_string())
}
#[cfg(not(feature = "canvas-backend"))]
fn build_canvas(
_video: &VideoSubsystem,
_make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
) -> Result<Backend, String> {
Err("built without the canvas-backend feature".to_string())
}
#[cfg(feature = "wgpu-backend")]
fn build_wgpu(
video: &VideoSubsystem,
make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
) -> Result<Backend, String> {
let window = make_window(video, false)?;
let egui = pollster::block_on(crate::EguiWgpu::new(window));
Ok(Backend::Wgpu {
egui: Box::new(egui),
})
}
#[cfg(not(feature = "wgpu-backend"))]
fn build_wgpu(
_video: &VideoSubsystem,
_make_window: &impl Fn(&VideoSubsystem, bool) -> Result<Window, String>,
) -> Result<Backend, String> {
Err("built without the wgpu-backend feature".to_string())
}
#[cfg(feature = "canvas-backend")]
fn rgb(color: [f32; 4]) -> sdl2::pixels::Color {
let byte = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
sdl2::pixels::Color::RGB(byte(color[0]), byte(color[1]), byte(color[2]))
}