Skip to main content

beamterm_renderer/gl/
renderer.rs

1use beamterm_core::gl::{Drawable, GlState, RenderContext};
2use glow::HasContext;
3use web_sys::HtmlCanvasElement;
4
5use crate::{error::Error, js};
6
7/// High-level WebGL2 renderer for terminal-style applications.
8///
9/// The `Renderer` manages the WebGL2 rendering context, canvas, and provides
10/// a simplified interface for rendering drawable objects. It handles frame
11/// management, viewport setup, and coordinate system transformations.
12pub struct Renderer {
13    gl: glow::Context,
14    raw_gl: web_sys::WebGl2RenderingContext, // for is_context_lost() only
15    canvas: web_sys::HtmlCanvasElement,
16    state: GlState,
17    canvas_padding_color: (f32, f32, f32),
18    logical_size_px: (i32, i32),
19    pixel_ratio: f32,
20    auto_resize_canvas_css: bool,
21}
22
23impl std::fmt::Debug for Renderer {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("Renderer")
26            .field("canvas_padding_color", &self.canvas_padding_color)
27            .field("logical_size_px", &self.logical_size_px)
28            .field("pixel_ratio", &self.pixel_ratio)
29            .field("auto_resize_canvas_css", &self.auto_resize_canvas_css)
30            .finish_non_exhaustive()
31    }
32}
33
34impl Renderer {
35    /// Creates a new renderer by querying for a canvas element with the given ID.
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if the canvas element cannot be found or the WebGL2 context
40    /// cannot be created.
41    pub fn create(canvas_id: &str, auto_resize_canvas_css: bool) -> Result<Self, Error> {
42        let canvas = js::get_canvas_by_id(canvas_id)?;
43        Self::create_with_canvas(canvas, auto_resize_canvas_css)
44    }
45
46    /// Sets the background color for the canvas area outside the terminal grid.
47    #[must_use]
48    pub fn canvas_padding_color(mut self, color: u32) -> Self {
49        let r = ((color >> 16) & 0xFF) as f32 / 255.0;
50        let g = ((color >> 8) & 0xFF) as f32 / 255.0;
51        let b = (color & 0xFF) as f32 / 255.0;
52        self.canvas_padding_color = (r, g, b);
53        self
54    }
55
56    /// Creates a new renderer from an existing HTML canvas element.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the WebGL2 context cannot be created from the canvas.
61    pub fn create_with_canvas(
62        canvas: HtmlCanvasElement,
63        auto_resize_canvas_css: bool,
64    ) -> Result<Self, Error> {
65        let (width, height) = (canvas.width() as i32, canvas.height() as i32);
66
67        // initialize WebGL context
68        let (gl, raw_gl) = js::create_glow_context(&canvas)?;
69        let state = GlState::new(&gl);
70
71        let mut renderer = Self {
72            gl,
73            raw_gl,
74            canvas,
75            state,
76            canvas_padding_color: (0.0, 0.0, 0.0),
77            logical_size_px: (width, height),
78            pixel_ratio: 1.0,
79            auto_resize_canvas_css,
80        };
81        renderer.resize(width as _, height as _);
82        Ok(renderer)
83    }
84
85    /// Resizes the canvas and updates the viewport.
86    pub fn resize(&mut self, width: i32, height: i32) {
87        self.logical_size_px = (width, height);
88        let (w, h) = self.physical_size();
89
90        self.canvas.set_width(w as u32);
91        self.canvas.set_height(h as u32);
92
93        if self.auto_resize_canvas_css {
94            let _ = self
95                .canvas
96                .style()
97                .set_property("width", &format!("{width}px"));
98            let _ = self
99                .canvas
100                .style()
101                .set_property("height", &format!("{height}px"));
102        }
103
104        self.state.viewport(&self.gl, 0, 0, w, h);
105    }
106
107    /// Clears the framebuffer with the specified color.
108    pub fn clear(&mut self, r: f32, g: f32, b: f32) {
109        self.state.clear_color(&self.gl, r, g, b, 1.0);
110        unsafe {
111            self.gl
112                .clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);
113        };
114    }
115
116    /// Begins a new rendering frame.
117    pub fn begin_frame(&mut self) {
118        let (r, g, b) = self.canvas_padding_color;
119        self.clear(r, g, b);
120    }
121
122    /// Renders a drawable object.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the drawable's `prepare` step fails (e.g., GPU buffer
127    /// upload or shader compilation errors).
128    pub fn render(&mut self, drawable: &impl Drawable) -> Result<(), crate::Error> {
129        let mut context = RenderContext { gl: &self.gl, state: &mut self.state };
130
131        drawable.prepare(&mut context)?;
132        drawable.draw(&mut context);
133        drawable.cleanup(&mut context);
134        Ok(())
135    }
136
137    /// Ends the current rendering frame.
138    pub fn end_frame(&mut self) {
139        // swap buffers (todo)
140    }
141
142    /// Returns a reference to the glow rendering context.
143    pub fn gl(&self) -> &glow::Context {
144        &self.gl
145    }
146
147    /// Returns a reference to the HTML canvas element.
148    pub fn canvas(&self) -> &HtmlCanvasElement {
149        &self.canvas
150    }
151
152    /// Returns the current canvas dimensions as a tuple.
153    pub fn canvas_size(&self) -> (i32, i32) {
154        self.logical_size()
155    }
156
157    /// Returns the logical size of the canvas in pixels.
158    pub fn logical_size(&self) -> (i32, i32) {
159        self.logical_size_px
160    }
161
162    /// Returns the physical size of the canvas in pixels, taking into account the device
163    /// pixel ratio.
164    pub fn physical_size(&self) -> (i32, i32) {
165        let (w, h) = self.logical_size_px;
166        (
167            (w as f32 * self.pixel_ratio).round() as i32,
168            (h as f32 * self.pixel_ratio).round() as i32,
169        )
170    }
171
172    /// Checks if the WebGL context has been lost.
173    pub fn is_context_lost(&self) -> bool {
174        self.raw_gl.is_context_lost()
175    }
176
177    /// Restores the WebGL context after a context loss event.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error if the new WebGL2 context cannot be created.
182    pub fn restore_context(&mut self) -> Result<(), Error> {
183        let (gl, raw_gl) = js::create_glow_context(&self.canvas)?;
184        self.state = GlState::new(&gl);
185        self.gl = gl;
186        self.raw_gl = raw_gl;
187
188        // Restore viewport using physical (device) pixels
189        let (width, height) = self.physical_size();
190        self.state.viewport(&self.gl, 0, 0, width, height);
191
192        Ok(())
193    }
194
195    /// Sets the pixel ratio.
196    pub(crate) fn set_pixel_ratio(&mut self, pixel_ratio: f32) {
197        self.pixel_ratio = pixel_ratio;
198    }
199}