easy_imgui_renderer/
renderer.rs

1use std::mem::size_of;
2
3use crate::glow::{self, HasContext};
4use anyhow::{anyhow, Result};
5use cgmath::{EuclideanSpace, Matrix3, Point2, Transform};
6use easy_imgui as imgui;
7use easy_imgui_opengl as glr;
8use easy_imgui_sys::*;
9use imgui::{Color, TextureId, Vector2};
10
11/// The main `Renderer` type.
12pub struct Renderer {
13    imgui: imgui::Context,
14    gl: glr::GlContext,
15    bg_color: Option<imgui::Color>,
16    matrix: Option<Matrix3<f32>>,
17    objs: GlObjects,
18}
19
20struct GlObjects {
21    atlas: glr::Texture,
22    program: glr::Program,
23    vao: glr::VertexArray,
24    vbuf: glr::Buffer,
25    ibuf: glr::Buffer,
26    a_pos_location: u32,
27    a_uv_location: u32,
28    a_color_location: u32,
29    u_matrix_location: glow::UniformLocation,
30    u_tex_location: glow::UniformLocation,
31}
32
33impl Renderer {
34    /// Creates a new renderer object.
35    ///
36    /// You need to provide the OpenGL context yourself.
37    pub fn new(gl: glr::GlContext) -> Result<Renderer> {
38        let atlas;
39        let program;
40        let vao;
41        let (vbuf, ibuf);
42        let a_pos_location;
43        let a_uv_location;
44        let a_color_location;
45        let u_matrix_location;
46        let u_tex_location;
47
48        let imgui = unsafe { imgui::Context::new() };
49
50        unsafe {
51            if !cfg!(target_arch = "wasm32") {
52                let io = &mut *ImGui_GetIO();
53                io.BackendFlags |= (imgui::BackendFlags::HasMouseCursors
54                    | imgui::BackendFlags::HasSetMousePos
55                    | imgui::BackendFlags::RendererHasVtxOffset)
56                    .bits();
57            }
58
59            atlas = glr::Texture::generate(&gl)?;
60            let glsl_version = if cfg!(not(target_arch = "wasm32")) {
61                "#version 150\n"
62            } else {
63                "#version 300 es\n"
64            };
65            program = gl_program_from_source(&gl, Some(glsl_version), include_str!("shader.glsl"))?;
66            vao = glr::VertexArray::generate(&gl)?;
67            gl.bind_vertex_array(Some(vao.id()));
68
69            let a_pos = program.attrib_by_name("pos").unwrap();
70            a_pos_location = a_pos.location();
71            gl.enable_vertex_attrib_array(a_pos_location);
72
73            let a_uv = program.attrib_by_name("uv").unwrap();
74            a_uv_location = a_uv.location();
75            gl.enable_vertex_attrib_array(a_uv_location);
76
77            let a_color = program.attrib_by_name("color").unwrap();
78            a_color_location = a_color.location();
79            gl.enable_vertex_attrib_array(a_color_location);
80
81            let u_matrix = program.uniform_by_name("matrix").unwrap();
82            u_matrix_location = u_matrix.location();
83
84            let u_tex = program.uniform_by_name("tex").unwrap();
85            u_tex_location = u_tex.location();
86
87            vbuf = glr::Buffer::generate(&gl)?;
88            ibuf = glr::Buffer::generate(&gl)?;
89        }
90        Ok(Renderer {
91            imgui,
92            gl,
93            bg_color: Some(Color::new(0.45, 0.55, 0.60, 1.0)),
94            matrix: None,
95            objs: GlObjects {
96                atlas,
97                program,
98                vao,
99                vbuf,
100                ibuf,
101                a_pos_location,
102                a_uv_location,
103                a_color_location,
104                u_matrix_location,
105                u_tex_location,
106            },
107        })
108    }
109    /// Gets a reference to the OpenGL context.
110    pub fn gl_context(&self) -> &glr::GlContext {
111        &self.gl
112    }
113    /// Sets the default background color.
114    ///
115    /// The set color will be used for `glClear(GL_COLOR_BUFFER_BIT)`.
116    /// Set to `None` to avoid this, and use [`easy_imgui::UiBuilder::pre_render`] to do whatever clearing
117    /// you need, if anything.
118    pub fn set_background_color(&mut self, color: Option<Color>) {
119        self.bg_color = color;
120    }
121    /// Sets the 2D (3x3) matrix transformation for the UI display.
122    ///
123    /// If you set this matrix to `Some` then it is your responsibility to also call the appropriate `gl.viewport()`.
124    pub fn set_matrix(&mut self, matrix: Option<Matrix3<f32>>) {
125        self.matrix = matrix;
126    }
127    /// Gets the background color.
128    pub fn background_color(&self) -> Option<Color> {
129        self.bg_color
130    }
131    /// Gets the stored Dear ImGui context.
132    pub fn imgui(&mut self) -> &mut imgui::Context {
133        &mut self.imgui
134    }
135    /// Sets the UI size, in logical units, and the scale factor.
136    pub fn set_size(&mut self, size: Vector2, scale: f32) {
137        unsafe {
138            self.imgui.set_current().set_size(size, scale);
139        }
140    }
141    /// Gets the UI size, in logical units.
142    pub fn size(&mut self) -> Vector2 {
143        unsafe { self.imgui.set_current().size() }
144    }
145    /// Builds and renders a UI frame, using the `app` [`easy_imgui::UiBuilder`].
146    pub fn do_frame<A: imgui::UiBuilder>(&mut self, app: &mut A) {
147        unsafe {
148            let mut imgui = self.imgui.set_current();
149
150            if imgui.update_atlas(app) {
151                Self::update_atlas(&self.gl, &self.objs.atlas);
152            }
153
154            imgui.do_frame(
155                app,
156                || {
157                    let io = &*ImGui_GetIO();
158                    if self.matrix.is_none() {
159                        self.gl.viewport(
160                            0,
161                            0,
162                            (io.DisplaySize.x * io.DisplayFramebufferScale.x) as i32,
163                            (io.DisplaySize.y * io.DisplayFramebufferScale.y) as i32,
164                        );
165                    }
166                    if let Some(bg) = self.bg_color {
167                        self.gl.clear_color(bg.r, bg.g, bg.b, bg.a);
168                        self.gl.clear(glow::COLOR_BUFFER_BIT);
169                    }
170                },
171                |draw_data| {
172                    Self::render(&self.gl, &self.objs, draw_data, self.matrix.as_ref());
173                },
174            );
175        }
176    }
177    unsafe fn update_atlas(gl: &glr::GlContext, atlas_tex: &glr::Texture) {
178        let io = ImGui_GetIO();
179        let mut data = std::ptr::null_mut();
180        let mut width = 0;
181        let mut height = 0;
182        let mut pixel_size = 0;
183        ImFontAtlas_GetTexDataAsRGBA32(
184            (*io).Fonts,
185            &mut data,
186            &mut width,
187            &mut height,
188            &mut pixel_size,
189        );
190
191        gl.bind_texture(glow::TEXTURE_2D, Some(atlas_tex.id()));
192
193        gl.tex_parameter_i32(
194            glow::TEXTURE_2D,
195            glow::TEXTURE_WRAP_S,
196            glow::CLAMP_TO_EDGE as i32,
197        );
198        gl.tex_parameter_i32(
199            glow::TEXTURE_2D,
200            glow::TEXTURE_WRAP_T,
201            glow::CLAMP_TO_EDGE as i32,
202        );
203        gl.tex_parameter_i32(
204            glow::TEXTURE_2D,
205            glow::TEXTURE_MIN_FILTER,
206            glow::LINEAR as i32,
207        );
208        gl.tex_parameter_i32(
209            glow::TEXTURE_2D,
210            glow::TEXTURE_MAG_FILTER,
211            glow::LINEAR as i32,
212        );
213        gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAX_LEVEL, 0);
214        gl.tex_image_2d(
215            glow::TEXTURE_2D,
216            0,
217            glow::RGBA as i32, //glow::RED as i32,
218            width,
219            height,
220            0,
221            glow::RGBA,
222            glow::UNSIGNED_BYTE,
223            //glow::RED, glow::UNSIGNED_BYTE,
224            glow::PixelUnpackData::Slice(Some(std::slice::from_raw_parts(
225                data,
226                (width * height * pixel_size) as usize,
227            ))),
228        );
229        gl.bind_texture(glow::TEXTURE_2D, None);
230
231        // bindgen: ImFontAtlas_SetTexID is inline
232        (*(*io).Fonts).TexID = Self::map_tex(atlas_tex.id()).id();
233
234        // We keep this, no need for imgui to hold a copy
235        ImFontAtlas_ClearTexData((*io).Fonts);
236    }
237    unsafe fn render(
238        gl: &glow::Context,
239        objs: &GlObjects,
240        draw_data: &ImDrawData,
241        matrix: Option<&Matrix3<f32>>,
242    ) {
243        enum ScissorViewportMatrix {
244            Default,
245            Custom(Matrix3<f32>),
246            None,
247        }
248        let default_matrix;
249        let (matrix, viewport_matrix) = match matrix {
250            None => {
251                let ImVec2 { x: left, y: top } = draw_data.DisplayPos;
252                let ImVec2 {
253                    x: width,
254                    y: height,
255                } = draw_data.DisplaySize;
256                let right = left + width;
257                let bottom = top + height;
258                gl.enable(glow::SCISSOR_TEST);
259                default_matrix = Matrix3::new(
260                    2.0 / width,
261                    0.0,
262                    0.0,
263                    0.0,
264                    -2.0 / height,
265                    0.0,
266                    -(right + left) / width,
267                    (top + bottom) / height,
268                    1.0,
269                );
270                (&default_matrix, ScissorViewportMatrix::Default)
271            }
272            Some(matrix) => {
273                // If there is a custom matrix we have to compute the scissor rectangle in viewport coordinates.
274                // This only works if the transformed scissor rectangle is axis aligned, ie the rotation is 0°, 90°, 180° or 270°.
275                // TODO: for other angles a fragment shader would be needed, maybe with a `discard`.
276                // A rotation of multiple of 90° always has two 0 in the matrix:
277                // * 0° and 180°: at (0,0) and (1,1), the sines.
278                // * 90° and 270°: at (0,1) and (1,0), the cosines.
279                if (matrix[0][0].abs() < f32::EPSILON && matrix[1][1].abs() < f32::EPSILON)
280                    || (matrix[1][0].abs() < f32::EPSILON && matrix[0][1].abs() < f32::EPSILON)
281                {
282                    let mut viewport = [0; 4];
283                    gl.get_parameter_i32_slice(glow::VIEWPORT, &mut viewport);
284                    let viewport_x = viewport[0] as f32;
285                    let viewport_y = viewport[1] as f32;
286                    let viewport_w2 = viewport[2] as f32 / 2.0;
287                    let viewport_h2 = viewport[3] as f32 / 2.0;
288                    let vm = Matrix3::new(
289                        viewport_w2,
290                        0.0,
291                        0.0,
292                        0.0,
293                        viewport_h2,
294                        0.0,
295                        viewport_x + viewport_w2,
296                        viewport_y + viewport_h2,
297                        1.0,
298                    );
299                    gl.enable(glow::SCISSOR_TEST);
300                    (matrix, ScissorViewportMatrix::Custom(vm * matrix))
301                } else {
302                    gl.disable(glow::SCISSOR_TEST);
303                    (matrix, ScissorViewportMatrix::None)
304                }
305            }
306        };
307
308        gl.bind_vertex_array(Some(objs.vao.id()));
309        gl.use_program(Some(objs.program.id()));
310        gl.bind_buffer(glow::ARRAY_BUFFER, Some(objs.vbuf.id()));
311        gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(objs.ibuf.id()));
312        gl.enable(glow::BLEND);
313        gl.blend_func_separate(
314            glow::SRC_ALPHA,
315            glow::ONE_MINUS_SRC_ALPHA,
316            glow::ONE,
317            glow::ONE_MINUS_SRC_ALPHA,
318        );
319        gl.disable(glow::CULL_FACE);
320        gl.disable(glow::DEPTH_TEST);
321
322        gl.active_texture(glow::TEXTURE0);
323        gl.uniform_1_i32(Some(&objs.u_tex_location), 0);
324
325        gl.uniform_matrix_3_f32_slice(
326            Some(&objs.u_matrix_location),
327            false,
328            AsRef::<[f32; 9]>::as_ref(matrix),
329        );
330
331        for cmd_list in &draw_data.CmdLists {
332            let cmd_list = &**cmd_list;
333
334            gl.buffer_data_u8_slice(
335                glow::ARRAY_BUFFER,
336                glr::as_u8_slice(&cmd_list.VtxBuffer),
337                glow::DYNAMIC_DRAW,
338            );
339            gl.buffer_data_u8_slice(
340                glow::ELEMENT_ARRAY_BUFFER,
341                glr::as_u8_slice(&cmd_list.IdxBuffer),
342                glow::DYNAMIC_DRAW,
343            );
344            let stride = size_of::<ImDrawVert>() as i32;
345            gl.vertex_attrib_pointer_f32(
346                objs.a_pos_location,
347                2, /*xy*/
348                glow::FLOAT,
349                false,
350                stride,
351                0,
352            );
353            gl.vertex_attrib_pointer_f32(
354                objs.a_uv_location,
355                2, /*xy*/
356                glow::FLOAT,
357                false,
358                stride,
359                8,
360            );
361            gl.vertex_attrib_pointer_f32(
362                objs.a_color_location,
363                4, /*rgba*/
364                glow::UNSIGNED_BYTE,
365                true,
366                stride,
367                16,
368            );
369
370            for cmd in &cmd_list.CmdBuffer {
371                match viewport_matrix {
372                    ScissorViewportMatrix::Default => {
373                        let clip_x = cmd.ClipRect.x - draw_data.DisplayPos.x;
374                        let clip_y = cmd.ClipRect.y - draw_data.DisplayPos.y;
375                        let clip_w = cmd.ClipRect.z - cmd.ClipRect.x;
376                        let clip_h = cmd.ClipRect.w - cmd.ClipRect.y;
377                        let scale = draw_data.FramebufferScale.x;
378                        gl.scissor(
379                            (clip_x * scale) as i32,
380                            ((draw_data.DisplaySize.y - (clip_y + clip_h)) * scale) as i32,
381                            (clip_w * scale) as i32,
382                            (clip_h * scale) as i32,
383                        );
384                    }
385                    ScissorViewportMatrix::Custom(vm) => {
386                        let pos = Vector2::new(draw_data.DisplayPos.x, draw_data.DisplayPos.y);
387                        let clip_aa = Vector2::new(cmd.ClipRect.x, cmd.ClipRect.y) - pos;
388                        let clip_bb = Vector2::new(cmd.ClipRect.z, cmd.ClipRect.w) - pos;
389                        let clip_aa = vm.transform_point(Point2::from_vec(clip_aa));
390                        let clip_bb = vm.transform_point(Point2::from_vec(clip_bb));
391                        gl.scissor(
392                            clip_aa.x.min(clip_bb.x).round() as i32,
393                            clip_aa.y.min(clip_bb.y).round() as i32,
394                            (clip_bb.x - clip_aa.x).abs().round() as i32,
395                            (clip_bb.y - clip_aa.y).abs().round() as i32,
396                        );
397                    }
398                    ScissorViewportMatrix::None => {}
399                }
400
401                match cmd.UserCallback {
402                    Some(cb) => {
403                        cb(cmd_list, cmd);
404                    }
405                    None => {
406                        gl.bind_texture(
407                            glow::TEXTURE_2D,
408                            Self::unmap_tex(TextureId::from_id(cmd.TextureId)),
409                        );
410
411                        if cfg!(target_arch = "wasm32") {
412                            gl.draw_elements(
413                                glow::TRIANGLES,
414                                cmd.ElemCount as i32,
415                                if size_of::<ImDrawIdx>() == 2 {
416                                    glow::UNSIGNED_SHORT
417                                } else {
418                                    glow::UNSIGNED_INT
419                                },
420                                (size_of::<ImDrawIdx>() * cmd.IdxOffset as usize) as i32,
421                            );
422                        } else {
423                            gl.draw_elements_base_vertex(
424                                glow::TRIANGLES,
425                                cmd.ElemCount as i32,
426                                if size_of::<ImDrawIdx>() == 2 {
427                                    glow::UNSIGNED_SHORT
428                                } else {
429                                    glow::UNSIGNED_INT
430                                },
431                                (size_of::<ImDrawIdx>() * cmd.IdxOffset as usize) as i32,
432                                cmd.VtxOffset as i32,
433                            );
434                        }
435                    }
436                }
437            }
438        }
439        gl.use_program(None);
440        gl.bind_vertex_array(None);
441        gl.disable(glow::SCISSOR_TEST);
442    }
443    /// Maps an OpenGL texture to an ImGui texture.
444    pub fn map_tex(ntex: glow::Texture) -> TextureId {
445        #[cfg(target_arch = "wasm32")]
446        {
447            let mut tex_map = WASM_TEX_MAP.lock().unwrap();
448            let id = tex_map.len();
449            tex_map.push(ntex);
450            unsafe { TextureId::from_id(id as *mut std::ffi::c_void) }
451        }
452        #[cfg(not(target_arch = "wasm32"))]
453        {
454            unsafe { TextureId::from_id(ntex.0.get() as ImTextureID) }
455        }
456    }
457    /// Gets an OpenGL texture from an ImGui texture.
458    pub fn unmap_tex(tex: TextureId) -> Option<glow::Texture> {
459        #[cfg(target_arch = "wasm32")]
460        {
461            let tex_map = WASM_TEX_MAP.lock().unwrap();
462            let id = tex.id() as usize;
463            tex_map.get(id).cloned()
464        }
465        #[cfg(not(target_arch = "wasm32"))]
466        {
467            Some(glow::NativeTexture(std::num::NonZeroU32::new(
468                tex.id() as u32
469            )?))
470        }
471    }
472}
473
474#[cfg(target_arch = "wasm32")]
475static WASM_TEX_MAP: std::sync::Mutex<Vec<glow::Texture>> = std::sync::Mutex::new(Vec::new());
476
477impl Drop for Renderer {
478    fn drop(&mut self) {
479        unsafe {
480            let io = ImGui_GetIO();
481            ImFontAtlas_Clear((*io).Fonts);
482        }
483    }
484}
485
486pub fn gl_program_from_source(
487    gl: &glr::GlContext,
488    prefix: Option<&str>,
489    shaders: &str,
490) -> Result<glr::Program> {
491    let split = shaders
492        .find("###")
493        .ok_or_else(|| anyhow!("shader marker not found"))?;
494    let vertex = &shaders[..split];
495    let frag = &shaders[split..];
496    let split_2 = frag
497        .find('\n')
498        .ok_or_else(|| anyhow!("shader marker not valid"))?;
499
500    let mut frag = &frag[split_2 + 1..];
501
502    let geom = if let Some(split) = frag.find("###") {
503        let geom = &frag[split..];
504        frag = &frag[..split];
505        let split_2 = geom
506            .find('\n')
507            .ok_or_else(|| anyhow!("shader marker not valid"))?;
508        Some(&geom[split_2 + 1..])
509    } else {
510        None
511    };
512
513    use std::borrow::Cow;
514
515    let (vertex, frag, geom) = match prefix {
516        None => (
517            Cow::Borrowed(vertex),
518            Cow::Borrowed(frag),
519            geom.map(Cow::Borrowed),
520        ),
521        Some(prefix) => (
522            Cow::Owned(format!("{0}{1}", prefix, vertex)),
523            Cow::Owned(format!("{0}{1}", prefix, frag)),
524            geom.map(|s| Cow::Owned(format!("{0}{1}", prefix, s))),
525        ),
526    };
527    let prg = glr::Program::from_source(gl, &vertex, &frag, geom.as_deref())?;
528    Ok(prg)
529}