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(crate) const PIXEL_FORMAT: PixelFormatEnum = PixelFormatEnum::ABGR8888;
#[cfg(target_endian = "big")]
pub(crate) const PIXEL_FORMAT: PixelFormatEnum = PixelFormatEnum::RGBA8888;
const BYTES_PER_PIXEL: usize = 4;
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>,
}
impl Painter<WindowContext> {
pub fn new(canvas: &Canvas<Window>) -> Self {
Self::with_creator(canvas.texture_creator(), max_texture_side(canvas))
}
}
impl<'s> Painter<SurfaceContext<'s>> {
pub fn for_surface(canvas: &Canvas<Surface<'s>>) -> Self {
Self::with_creator(canvas.texture_creator(), max_texture_side(canvas))
}
}
impl<C> Painter<C> {
fn with_creator(texture_creator: TextureCreator<C>, max_texture_side: Option<usize>) -> Self {
Self {
textures: HashMap::new(),
texture_creator,
vertex_scratch: Vec::new(),
index_scratch: Vec::new(),
pixel_scratch: Vec::new(),
last_clip: None,
max_texture_side,
}
}
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);
for pixel in img.pixels.iter() {
self.pixel_scratch
.extend_from_slice(&pixel.to_srgba_unmultiplied());
}
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 tex = self
.textures
.entry(id)
.or_insert_with(|| create_texture(&self.texture_creator, w, h));
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) -> Texture {
let mut tex = texture_creator
.create_texture_streaming(PIXEL_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,
},
}
}