use egui::epaint::{ImageDelta, Primitive};
use egui::{ClippedPrimitive, ImageData, TexturesDelta};
use sdl2::pixels::PixelFormatEnum;
use sdl2::rect::Rect;
use sdl2::render::{BlendMode, Canvas, RenderTarget, Texture, TextureCreator};
use sdl2::surface::{Surface, SurfaceContext};
use sdl2::sys::{SDL_Color, SDL_FPoint, SDL_Vertex};
use sdl2::video::{Window, WindowContext};
use std::collections::HashMap;
use std::os::raw::c_int;
#[cfg(target_endian = "little")]
pub const DEFAULT_FORMAT: PixelFormatEnum = PixelFormatEnum::ABGR8888;
#[cfg(target_endian = "big")]
pub const DEFAULT_FORMAT: PixelFormatEnum = PixelFormatEnum::RGBA8888;
pub(crate) const BYTES_PER_PIXEL: usize = 4;
pub fn preferred_format<T: RenderTarget>(canvas: &Canvas<T>) -> PixelFormatEnum {
let formats = canvas.info().texture_formats;
if formats.contains(&DEFAULT_FORMAT) {
return DEFAULT_FORMAT;
}
formats
.into_iter()
.find(|&format| channel_offsets(format).is_some())
.unwrap_or(DEFAULT_FORMAT)
}
fn channel_offsets(format: PixelFormatEnum) -> Option<[usize; BYTES_PER_PIXEL]> {
let masks = format.into_masks().ok()?;
if masks.bpp as usize != BYTES_PER_PIXEL * 8 || masks.amask == 0 {
return None;
}
let offset = |mask: u32| {
let byte = (mask.trailing_zeros() / 8) as usize;
if cfg!(target_endian = "little") {
byte
} else {
BYTES_PER_PIXEL - 1 - byte
}
};
Some([
offset(masks.rmask),
offset(masks.gmask),
offset(masks.bmask),
offset(masks.amask),
])
}
pub struct Painter<C = WindowContext> {
textures: HashMap<egui::TextureId, Texture>,
texture_creator: TextureCreator<C>,
vertex_scratch: Vec<SDL_Vertex>,
last_clip: Option<Rect>,
index_scratch: Vec<u32>,
pixel_scratch: Vec<u8>,
max_texture_side: Option<usize>,
format: PixelFormatEnum,
channels: [usize; BYTES_PER_PIXEL],
}
impl Painter<WindowContext> {
pub fn new(canvas: &Canvas<Window>) -> Self {
Self::with_format(canvas, preferred_format(canvas))
}
pub fn with_format(canvas: &Canvas<Window>, format: PixelFormatEnum) -> Self {
Self::with_creator(canvas.texture_creator(), max_texture_side(canvas), format)
}
}
impl<'s> Painter<SurfaceContext<'s>> {
pub fn for_surface(canvas: &Canvas<Surface<'s>>) -> Self {
Self::for_surface_with_format(canvas, preferred_format(canvas))
}
pub fn for_surface_with_format(canvas: &Canvas<Surface<'s>>, format: PixelFormatEnum) -> Self {
Self::with_creator(canvas.texture_creator(), max_texture_side(canvas), format)
}
}
impl<C> Painter<C> {
fn with_creator(
texture_creator: TextureCreator<C>,
max_texture_side: Option<usize>,
format: PixelFormatEnum,
) -> Self {
let (format, channels) = match channel_offsets(format) {
Some(channels) => (format, channels),
None => {
log::warn!(
"{format:?} cannot hold an egui texture; painting in {DEFAULT_FORMAT:?}"
);
let channels = channel_offsets(DEFAULT_FORMAT)
.expect("the default format is 32-bit with alpha by construction");
(DEFAULT_FORMAT, channels)
}
};
Self {
textures: HashMap::new(),
texture_creator,
vertex_scratch: Vec::new(),
index_scratch: Vec::new(),
pixel_scratch: Vec::new(),
last_clip: None,
max_texture_side,
format,
channels,
}
}
pub fn format(&self) -> PixelFormatEnum {
self.format
}
pub fn max_texture_side(&self) -> Option<usize> {
self.max_texture_side
}
pub fn destroy(&mut self) {
let textures = std::mem::replace(&mut self.textures, HashMap::with_capacity(0));
for (_id, tex) in textures {
unsafe {
tex.destroy();
}
}
}
pub fn paint_and_update_textures<T: RenderTarget<Context = C>>(
&mut self,
canvas: &mut Canvas<T>,
pixels_per_point: f32,
textures_delta: &mut TexturesDelta,
paint_jobs: Vec<ClippedPrimitive>,
) -> Result<(), String> {
for (id, deltas) in textures_delta.set.drain() {
for delta in deltas {
self.set_texture(id, &delta);
}
}
self.paint_primitives(canvas, pixels_per_point, paint_jobs);
for id in textures_delta.free.drain() {
self.free_texture(&id);
}
Ok(())
}
pub fn paint_primitives<T: RenderTarget<Context = C>>(
&mut self,
canvas: &mut Canvas<T>,
pixels_per_point: f32,
paint_jobs: Vec<ClippedPrimitive>,
) {
self.last_clip = None;
let caller_blend = canvas.blend_mode();
canvas.set_blend_mode(BlendMode::Blend);
for job in paint_jobs.into_iter() {
match job.primitive {
Primitive::Mesh(mesh) => {
self.paint_mesh(canvas, pixels_per_point, job.clip_rect, mesh)
}
Primitive::Callback(_callback) => {
log::warn!("PaintCallbacks are not supported")
}
}
}
if self.last_clip.is_some() {
canvas.set_clip_rect(None);
}
canvas.set_blend_mode(caller_blend);
}
pub fn set_texture(&mut self, id: egui::TextureId, delta: &ImageDelta) {
let ImageData::Color(img) = &delta.image;
self.pixel_scratch.clear();
self.pixel_scratch
.reserve(img.pixels.len() * BYTES_PER_PIXEL);
let [r_at, g_at, b_at, a_at] = self.channels;
if self.channels == [0, 1, 2, 3] {
for pixel in img.pixels.iter() {
self.pixel_scratch
.extend_from_slice(&pixel.to_srgba_unmultiplied());
}
} else {
for pixel in img.pixels.iter() {
let [r, g, b, a] = pixel.to_srgba_unmultiplied();
let mut texel = [0u8; BYTES_PER_PIXEL];
texel[r_at] = r;
texel[g_at] = g;
texel[b_at] = b;
texel[a_at] = a;
self.pixel_scratch.extend_from_slice(&texel);
}
}
let w = img.width() as u32;
let h = img.height() as u32;
let pitch = (w as usize) * BYTES_PER_PIXEL;
if delta.pos.is_none() {
if let Some(tex) = self.textures.get(&id) {
let q = tex.query();
if q.width != w || q.height != h {
self.free_texture(&id);
}
}
}
let format = self.format;
let tex = self
.textures
.entry(id)
.or_insert_with(|| create_texture(&self.texture_creator, w, h, format));
let rect = delta.pos.map(|[x, y]| Rect::new(x as i32, y as i32, w, h));
tex.update(rect, &self.pixel_scratch, pitch).unwrap();
}
#[inline]
pub fn free_texture(&mut self, id: &egui::TextureId) {
if let Some(tex) = self.textures.remove(id) {
unsafe {
tex.destroy();
}
}
}
#[inline]
fn paint_mesh<T: RenderTarget<Context = C>>(
&mut self,
canvas: &mut Canvas<T>,
pixels_per_point: f32,
clip_rect: egui::Rect,
mesh: egui::Mesh,
) {
let (texture_ptr, texture_size) = match self.textures.get(&mesh.texture_id) {
Some(tex) => {
let q = tex.query();
(tex.raw(), Some((q.width as f32, q.height as f32)))
}
None => (std::ptr::null_mut(), None),
};
let min = clip_rect.min * pixels_per_point;
let max = clip_rect.max * pixels_per_point;
let clip_rect = sdl2::rect::Rect::new(
min.x as i32,
min.y as i32,
(max.x - min.x) as u32,
(max.y - min.y) as u32,
);
if self.last_clip != Some(clip_rect) {
canvas.set_clip_rect(clip_rect);
self.last_clip = Some(clip_rect);
}
self.index_scratch.clear();
for corners in mesh.indices.chunks(6) {
match as_axis_aligned_quad(&mesh.vertices, corners, pixels_per_point) {
Some(quad) => {
self.flush_triangles(canvas, texture_ptr, &mesh, pixels_per_point);
quad.blit(canvas, texture_ptr, texture_size);
}
None => self.index_scratch.extend_from_slice(corners),
}
}
self.flush_triangles(canvas, texture_ptr, &mesh, pixels_per_point);
}
fn flush_triangles<T: RenderTarget<Context = C>>(
&mut self,
canvas: &mut Canvas<T>,
texture_ptr: *mut sdl2_sys::SDL_Texture,
mesh: &egui::Mesh,
pixels_per_point: f32,
) {
if self.index_scratch.is_empty() {
return;
}
if !texture_ptr.is_null() {
unsafe {
sdl2_sys::SDL_SetTextureColorMod(texture_ptr, 255, 255, 255);
sdl2_sys::SDL_SetTextureAlphaMod(texture_ptr, 255);
}
}
self.vertex_scratch.clear();
self.vertex_scratch.reserve(mesh.vertices.len());
self.vertex_scratch.extend(
mesh.vertices
.iter()
.map(|v| into_sdl_vertex(v, pixels_per_point)),
);
for triangle in self.index_scratch.chunks_exact(3) {
let hue = triangle
.iter()
.map(|&i| self.vertex_scratch[i as usize].color)
.find(|c| c.a != 0);
let Some(hue) = hue else { continue };
for &i in triangle {
let c = &mut self.vertex_scratch[i as usize].color;
if c.a == 0 {
(c.r, c.g, c.b) = (hue.r, hue.g, hue.b);
}
}
}
let verts_len = self.vertex_scratch.len() as c_int;
let indcs_len = self.index_scratch.len() as c_int;
let result = unsafe {
sdl2_sys::SDL_RenderGeometry(
canvas.raw(),
texture_ptr,
if verts_len == 0 {
std::ptr::null()
} else {
self.vertex_scratch.as_ptr()
},
verts_len,
self.index_scratch.as_ptr() as *const c_int,
indcs_len,
)
};
self.index_scratch.clear();
if result != 0 {
log::error!("SDL_RenderGeometry failed: {}", result);
}
}
}
struct Quad {
dst: sdl2_sys::SDL_FRect,
uv: egui::Rect,
color: egui::Color32,
textured: bool,
}
impl Quad {
fn blit<T: RenderTarget>(
&self,
canvas: &mut Canvas<T>,
texture_ptr: *mut sdl2_sys::SDL_Texture,
texture_size: Option<(f32, f32)>,
) {
let [r, g, b, a] = self.color.to_srgba_unmultiplied();
let result = match (self.textured, texture_size) {
(true, Some((tw, th))) => unsafe {
sdl2_sys::SDL_SetTextureColorMod(texture_ptr, r, g, b);
sdl2_sys::SDL_SetTextureAlphaMod(texture_ptr, a);
let src = sdl2_sys::SDL_Rect {
x: (self.uv.min.x * tw).round() as i32,
y: (self.uv.min.y * th).round() as i32,
w: (self.uv.width() * tw).round() as i32,
h: (self.uv.height() * th).round() as i32,
};
sdl2_sys::SDL_RenderCopyF(canvas.raw(), texture_ptr, &src, &self.dst)
},
_ => unsafe {
sdl2_sys::SDL_SetRenderDrawColor(canvas.raw(), r, g, b, a);
sdl2_sys::SDL_RenderFillRectF(canvas.raw(), &self.dst)
},
};
if result != 0 {
log::error!("blitting a quad failed: {result}");
}
}
}
fn as_axis_aligned_quad(
vertices: &[egui::epaint::Vertex],
corners: &[u32],
pixels_per_point: f32,
) -> Option<Quad> {
if corners.len() != 6 {
return None;
}
let mut uniq: Vec<&egui::epaint::Vertex> = Vec::with_capacity(4);
for &i in corners {
let v = vertices.get(i as usize)?;
if !uniq.iter().any(|u| u.pos == v.pos && u.uv == v.uv) {
uniq.push(v);
}
}
if uniq.len() != 4 {
return None;
}
let color = uniq[0].color;
if uniq.iter().any(|v| v.color != color) {
return None;
}
let rect = egui::Rect::from_points(&uniq.iter().map(|v| v.pos).collect::<Vec<_>>());
let uv = egui::Rect::from_points(&uniq.iter().map(|v| v.uv).collect::<Vec<_>>());
if rect.width() <= 0.0 || rect.height() <= 0.0 {
return None;
}
let textured = uv.width() > 0.0 && uv.height() > 0.0;
for v in &uniq {
let at_min_x = v.pos.x == rect.min.x;
let at_min_y = v.pos.y == rect.min.y;
if !(at_min_x || v.pos.x == rect.max.x) || !(at_min_y || v.pos.y == rect.max.y) {
return None; }
if textured && (at_min_x != (v.uv.x == uv.min.x) || at_min_y != (v.uv.y == uv.min.y)) {
return None;
}
}
Some(Quad {
dst: sdl2_sys::SDL_FRect {
x: rect.min.x * pixels_per_point,
y: rect.min.y * pixels_per_point,
w: rect.width() * pixels_per_point,
h: rect.height() * pixels_per_point,
},
uv,
color,
textured,
})
}
fn max_texture_side<T: RenderTarget>(canvas: &Canvas<T>) -> Option<usize> {
let info = canvas.info();
let side = match (info.max_texture_width, info.max_texture_height) {
(0, 0) => return None,
(0, h) => h,
(w, 0) => w,
(w, h) => w.min(h),
};
Some(side as usize)
}
#[inline]
fn create_texture<C>(
texture_creator: &TextureCreator<C>,
w: u32,
h: u32,
format: PixelFormatEnum,
) -> Texture {
let mut tex = texture_creator
.create_texture_streaming(format, w, h)
.unwrap_or_else(|e| {
panic!("failed to create a {w}x{h} sdl2 texture: {e}")
});
tex.set_blend_mode(BlendMode::Blend);
tex
}
#[inline]
fn into_sdl_vertex(vertex: &egui::epaint::Vertex, pixels_per_point: f32) -> SDL_Vertex {
let [r, g, b, a] = vertex.color.to_srgba_unmultiplied();
SDL_Vertex {
position: SDL_FPoint {
x: vertex.pos.x * pixels_per_point,
y: vertex.pos.y * pixels_per_point,
},
color: SDL_Color { r, g, b, a },
tex_coord: SDL_FPoint {
x: vertex.uv.x,
y: vertex.uv.y,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_format_takes_egui_bytes_as_they_come() {
assert_eq!(channel_offsets(DEFAULT_FORMAT), Some([0, 1, 2, 3]));
}
#[test]
fn a_channel_order_is_the_format_s_own() {
assert_eq!(
channel_offsets(PixelFormatEnum::ARGB8888),
Some([2, 1, 0, 3])
);
assert_eq!(
channel_offsets(PixelFormatEnum::BGRA8888),
Some([1, 2, 3, 0])
);
}
#[test]
fn nothing_short_of_32_bit_with_alpha_holds_a_texture() {
for format in [
PixelFormatEnum::RGB565,
PixelFormatEnum::RGB24,
PixelFormatEnum::RGBX8888,
PixelFormatEnum::YV12,
] {
assert_eq!(channel_offsets(format), None, "{format:?}");
}
}
}