imgui_sdl3_renderer/
lib.rs

1//! An [imgui] rendering backend to integrate with the [sdl3 renderer][sdl3::render]
2
3use imgui::internal::RawWrapper as _;
4use std::{
5    error::Error,
6    ffi::{c_float, c_int, c_void},
7    fmt::Display,
8    mem::offset_of,
9};
10
11type RenderResult = std::result::Result<(), RenderError>;
12
13/// A wrapper around various [sdl3] error types
14#[derive(Debug)]
15pub enum RenderError {
16    UpdateTexture(sdl3::render::UpdateTextureError),
17    TextureValue(sdl3::render::TextureValueError),
18    GenericSDL(sdl3::Error),
19}
20
21impl From<sdl3::render::UpdateTextureError> for RenderError {
22    fn from(value: sdl3::render::UpdateTextureError) -> Self {
23        Self::UpdateTexture(value)
24    }
25}
26
27impl From<sdl3::render::TextureValueError> for RenderError {
28    fn from(value: sdl3::render::TextureValueError) -> Self {
29        Self::TextureValue(value)
30    }
31}
32
33impl From<sdl3::Error> for RenderError {
34    fn from(value: sdl3::Error) -> Self {
35        Self::GenericSDL(value)
36    }
37}
38
39impl Display for RenderError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match &self {
42            Self::UpdateTexture(e) => {
43                write!(f, "{}", e)
44            }
45            Self::TextureValue(e) => {
46                write!(f, "{}", e)
47            }
48            Self::GenericSDL(e) => {
49                write!(f, "{}", e)
50            }
51        }
52    }
53}
54
55impl Error for RenderError {
56    fn source(&self) -> Option<&(dyn Error + 'static)> {
57        match &self {
58            Self::UpdateTexture(e) => Some(e),
59            Self::TextureValue(e) => Some(e),
60            Self::GenericSDL(e) => Some(e),
61        }
62    }
63}
64
65/// Represents the context for the renderer
66pub struct Renderer<'a> {
67    texture_map: imgui::Textures<sdl3::render::Texture<'a>>,
68    color_buffer: Vec<sdl3_sys::pixels::SDL_FColor>,
69}
70
71impl<'a> Renderer<'a> {
72    /// Constructs a new [Renderer]
73    ///
74    /// # Examples
75    /// Make sure to call after setting the imgui font.
76    /// ```
77    /// let sdl_context = sdl3::init().unwrap();
78    /// let video_subsystem = sdl_context.video().unwrap();
79    ///
80    /// let window = video_subsystem
81    ///     .window("rust-sdl3 example", 800, 600)
82    ///     .position_centered()
83    ///     .resizable()
84    ///     .high_pixel_density()
85    ///     .build()
86    ///     .unwrap();
87    /// let mut canvas = window.into_canvas();
88    /// let texture_creator = canvas.texture_creator();
89    ///
90    /// let mut imgui_context = imgui::Context::create();
91    /// imgui_context.set_ini_filename(None);
92    ///
93    /// imgui_context.fonts().add_font(&[imgui::FontSource::DefaultFontData { config: None, }]);
94    ///
95    /// let mut renderer = imgui_sdl3_renderer::Renderer::new(&texture_creator, &mut imgui_context).unwrap();
96    /// ```
97    pub fn new(
98        texture_creator: &'a sdl3::render::TextureCreator<impl std::any::Any>,
99        imgui_context: &mut imgui::Context,
100    ) -> Result<Self, RenderError> {
101        let mut texture_map = imgui::Textures::new();
102        Self::prepare_font_atlas(texture_creator, imgui_context, &mut texture_map)?;
103
104        imgui_context.set_renderer_name(Some(format!(
105            "imgui-rs-sdl3-renderer {}",
106            env!("CARGO_PKG_VERSION")
107        )));
108
109        imgui_context
110            .io_mut()
111            .backend_flags
112            .insert(imgui::BackendFlags::RENDERER_HAS_VTX_OFFSET);
113
114        Ok(Self {
115            texture_map,
116            color_buffer: Vec::new(),
117        })
118    }
119
120    /// Renders the `draw_data` to the `canvas`
121    ///
122    /// <div class="warning">
123    ///
124    /// The `canvas` must be the canvas that owns the [TextureCreator] that was passed to
125    /// [Self::new] and must be the same canvas on each call
126    ///
127    /// </div>
128    ///
129    /// # Examples
130    /// ```ignore
131    /// /* ... */
132    /// let mut canvas = window.into_canvas();
133    /// let texture_creator = canvas.texture_creator();
134    ///
135    /// /* ... */
136    /// let mut renderer = imgui_sdl3_renderer::Renderer::new(&texture_creator, &mut imgui_context).unwrap();
137    ///
138    /// 'main loop {
139    /// canvas.clear();
140    /// /* ... */
141    /// let ui = imgui_context.new_frame();
142    /// ui.show_demo_window(&mut true);
143    /// renderer.render(imgui_context.render(), &mut canvas).unwrap();
144    /// /* ... */
145    /// canvas.present();
146    /// }
147    /// ```
148    pub fn render(
149        &mut self,
150        draw_data: &imgui::DrawData,
151        canvas: &mut sdl3::render::Canvas<impl sdl3::render::RenderTarget>,
152    ) -> RenderResult {
153        struct CanvasBackup {
154            viewport: sdl3::rect::Rect,
155            clip: sdl3::render::ClippingRect,
156        }
157
158        let backup = CanvasBackup {
159            viewport: canvas.viewport(),
160            clip: canvas.clip_rect(),
161        };
162
163        Self::set_up_render_state(canvas);
164
165        // Framebuffer scaling for HiDPI support
166        let fb_width = draw_data.display_size[0] * draw_data.framebuffer_scale[0];
167        let fb_height = draw_data.display_size[1] * draw_data.framebuffer_scale[1];
168
169        // Don't need to render if minimised
170        if fb_width == 0_f32 || fb_height == 0_f32 {
171            return Ok(());
172        }
173
174        for draw_list in draw_data.draw_lists() {
175            for command in draw_list.commands() {
176                match command {
177                    imgui::DrawCmd::Elements { count, cmd_params } => {
178                        Self::render_elements(
179                            &self.texture_map,
180                            &mut self.color_buffer,
181                            canvas,
182                            draw_list.vtx_buffer(),
183                            draw_list.idx_buffer(),
184                            count,
185                            &cmd_params,
186                            &draw_data.display_pos,
187                            &draw_data.framebuffer_scale,
188                            (fb_width, fb_height),
189                        )?;
190                    }
191                    imgui::DrawCmd::ResetRenderState => {
192                        Self::set_up_render_state(canvas);
193                    }
194                    imgui::DrawCmd::RawCallback { callback, raw_cmd } => unsafe {
195                        callback(draw_list.raw(), raw_cmd);
196                    },
197                }
198            }
199        }
200
201        canvas.set_viewport(backup.viewport);
202        canvas.set_clip_rect(backup.clip);
203        Ok(())
204    }
205
206    #[allow(clippy::too_many_arguments)]
207    fn render_elements(
208        texture_map: &imgui::Textures<sdl3::render::Texture<'a>>,
209        color_buffer: &mut Vec<sdl3_sys::pixels::SDL_FColor>,
210        canvas: &mut sdl3::render::Canvas<impl sdl3::render::RenderTarget>,
211        vertex_buffer: &[imgui::DrawVert],
212        index_buffer: &[imgui::DrawIdx],
213        elem_count: usize,
214        elem_params: &imgui::DrawCmdParams,
215        pos: &[f32; 2],
216        scale: &[f32; 2],
217        fb_size: (f32, f32),
218    ) -> RenderResult {
219        let imgui::DrawCmdParams {
220            clip_rect,
221            texture_id,
222            vtx_offset,
223            idx_offset,
224        } = elem_params;
225
226        let clip_min = (
227            (clip_rect[0] - pos[0]) * scale[0],
228            (clip_rect[1] - pos[1]) * scale[1],
229        );
230        let clip_max = (
231            (clip_rect[2] - pos[0]) * scale[0],
232            (clip_rect[3] - pos[1]) * scale[1],
233        );
234        if clip_min.0 >= fb_size.0
235            || clip_min.1 >= fb_size.1
236            || clip_max.0 < 0.0
237            || clip_max.1 < 0.0
238        {
239            return Ok(());
240        }
241
242        let rect = sdl3::rect::Rect::new(
243            clip_min.0 as i32,
244            clip_min.1 as i32,
245            (clip_max.0 - clip_min.0) as u32,
246            (clip_max.1 - clip_min.1) as u32,
247        );
248        canvas.set_clip_rect(rect);
249
250        let texture = texture_map.get(*texture_id);
251        Self::render_raw_geometry(
252            canvas,
253            color_buffer,
254            texture,
255            &vertex_buffer[*vtx_offset..],
256            &index_buffer[*idx_offset..idx_offset + elem_count],
257        )
258    }
259
260    fn render_raw_geometry(
261        canvas: &mut sdl3::render::Canvas<impl sdl3::render::RenderTarget>,
262        color_buffer: &mut Vec<sdl3_sys::pixels::SDL_FColor>,
263        texture: Option<&sdl3::render::Texture>,
264        vertices: &[imgui::DrawVert],
265        indices: &[imgui::DrawIdx],
266    ) -> RenderResult {
267        let vert_stride = size_of::<imgui::DrawVert>() as c_int;
268        color_buffer.clear();
269        // Normalize colours to SDL_Fcolor format 
270        color_buffer.extend(vertices.iter().map(|vert| sdl3_sys::pixels::SDL_FColor {
271            r: vert.col[0] as f32 / 255_f32,
272            g: vert.col[1] as f32 / 255_f32,
273            b: vert.col[2] as f32 / 255_f32,
274            a: vert.col[3] as f32 / 255_f32,
275        }));
276
277        let renderer = canvas.raw();
278        let texture = texture.map_or(std::ptr::null_mut(), |texture| texture.raw());
279
280        let xy = unsafe {
281            vertices.as_ptr().byte_add(offset_of!(imgui::DrawVert, pos)) as *const c_float
282        };
283        let uv = unsafe {
284            vertices.as_ptr().byte_add(offset_of!(imgui::DrawVert, uv)) as *const c_float
285        };
286        let idx = indices.as_ptr() as *const c_void;
287        let colors = color_buffer.as_ptr();
288
289        unsafe {
290            sdl3_sys::render::SDL_RenderGeometryRaw(
291                renderer,
292                texture,
293                xy,
294                vert_stride,
295                colors,
296                size_of::<sdl3_sys::pixels::SDL_FColor>() as c_int,
297                uv,
298                vert_stride,
299                vertices.len() as c_int,
300                idx,
301                indices.len() as c_int,
302                size_of::<imgui::DrawIdx>() as c_int,
303            )
304        }
305        .then_some(())
306        .ok_or_else(|| sdl3::get_error().into())
307    }
308
309    fn set_up_render_state(canvas: &mut sdl3::render::Canvas<impl sdl3::render::RenderTarget>) {
310        canvas.set_clip_rect(None);
311        canvas.set_viewport(None);
312    }
313
314    fn prepare_font_atlas(
315        creator: &'a sdl3::render::TextureCreator<impl std::any::Any>,
316        imgui_context: &mut imgui::Context,
317        texture_map: &mut imgui::Textures<sdl3::render::Texture<'a>>,
318    ) -> RenderResult {
319        let font_atlas = imgui_context.fonts().build_rgba32_texture();
320        let rgba32_format: sdl3::pixels::PixelFormat =
321            sdl3_sys::pixels::SDL_PixelFormat::RGBA32.try_into()?;
322        let mut font_texture =
323            creator.create_texture_static(rgba32_format, font_atlas.width, font_atlas.height)?;
324
325        font_texture.update(
326            None,
327            font_atlas.data,
328            rgba32_format.byte_size_of_pixels(font_atlas.width as usize),
329        )?;
330
331        font_texture.set_blend_mode(sdl3::render::BlendMode::Blend);
332        font_texture.set_scale_mode(sdl3::render::ScaleMode::Linear);
333
334        let id = texture_map.insert(font_texture);
335        imgui_context.fonts().tex_id = id;
336        Ok(())
337    }
338}
339