Skip to main content

beamterm_renderer/gl/
renderer.rs

1use web_sys::HtmlCanvasElement;
2
3use crate::{
4    error::Error,
5    gl::{GL, context::GlState},
6    js,
7};
8
9/// Rendering context that provides access to WebGL state.
10pub(super) struct RenderContext<'a> {
11    pub gl: &'a web_sys::WebGl2RenderingContext,
12    pub state: &'a mut GlState,
13}
14
15/// High-level WebGL2 renderer for terminal-style applications.
16///
17/// The `Renderer` manages the WebGL2 rendering context, canvas, and provides
18/// a simplified interface for rendering drawable objects. It handles frame
19/// management, viewport setup, and coordinate system transformations.
20#[derive(Debug)]
21pub struct Renderer {
22    gl: web_sys::WebGl2RenderingContext,
23    canvas: web_sys::HtmlCanvasElement,
24    state: GlState,
25    canvas_padding_color: (f32, f32, f32),
26    logical_size_px: (i32, i32),
27    pixel_ratio: f32,
28    auto_resize_canvas_css: bool,
29}
30
31impl Renderer {
32    /// Creates a new renderer by querying for a canvas element with the given ID.
33    ///
34    /// This method searches the DOM for a canvas element with the specified ID,
35    /// initializes a WebGL2 context, and sets up the renderer with orthographic
36    /// projection matching the canvas dimensions.
37    ///
38    /// # Parameters
39    /// * `canvas_id` - CSS selector for the canvas element (e.g., "canvas" or "#my-canvas")
40    ///
41    /// # Returns
42    /// * `Ok(Renderer)` - Successfully created renderer
43    /// * `Err(Error)` - Failed to find canvas, create WebGL context, or initialize renderer
44    ///
45    /// # Errors
46    /// * `Error::UnableToRetrieveCanvas` - Canvas element not found
47    /// * `Error::FailedToRetrieveWebGl2RenderingContext` - WebGL2 not supported or failed to initialize
48    pub fn create(canvas_id: &str, auto_resize_canvas_css: bool) -> Result<Self, Error> {
49        let canvas = js::get_canvas_by_id(canvas_id)?;
50        Self::create_with_canvas(canvas, auto_resize_canvas_css)
51    }
52
53    /// Sets the background color for the canvas area outside the terminal grid.
54    ///
55    /// When the canvas dimensions don't align perfectly with the terminal cell grid,
56    /// there may be unused pixels around the edges. This color fills those padding
57    /// areas to maintain a consistent appearance.
58    pub fn canvas_padding_color(mut self, color: u32) -> Self {
59        let r = ((color >> 16) & 0xFF) as f32 / 255.0;
60        let g = ((color >> 8) & 0xFF) as f32 / 255.0;
61        let b = (color & 0xFF) as f32 / 255.0;
62        self.canvas_padding_color = (r, g, b);
63        self
64    }
65
66    /// Creates a new renderer from an existing HTML canvas element.
67    ///
68    /// This method takes ownership of an existing canvas element and initializes
69    /// the WebGL2 context and renderer state. Useful when you already have a
70    /// reference to the canvas element.
71    ///
72    /// # Parameters
73    /// * `canvas` - HTML canvas element to use for rendering
74    ///
75    /// # Returns
76    /// * `Ok(Renderer)` - Successfully created renderer
77    /// * `Err(Error)` - Failed to create WebGL context or initialize renderer
78    pub fn create_with_canvas(
79        canvas: HtmlCanvasElement,
80        auto_resize_canvas_css: bool,
81    ) -> Result<Self, Error> {
82        let (width, height) = (canvas.width() as i32, canvas.height() as i32);
83
84        // initialize WebGL context
85        let gl = js::get_webgl2_context(&canvas)?;
86        let state = GlState::new(&gl);
87
88        let mut renderer = Self {
89            gl,
90            canvas,
91            state,
92            canvas_padding_color: (0.0, 0.0, 0.0),
93            logical_size_px: (width, height),
94            pixel_ratio: 1.0,
95            auto_resize_canvas_css,
96        };
97        renderer.resize(width as _, height as _);
98        Ok(renderer)
99    }
100
101    /// Resizes the canvas and updates the viewport.
102    ///
103    /// This method changes the canvas resolution and adjusts the WebGL viewport
104    /// to match. The projection matrix is automatically updated to maintain
105    /// proper coordinate mapping.
106    ///
107    /// # Parameters
108    /// * `width` - New canvas width in logical (CSS) pixels
109    /// * `height` - New canvas height in logical (CSS) pixels
110    pub fn resize(&mut self, width: i32, height: i32) {
111        self.logical_size_px = (width, height);
112        let (w, h) = self.physical_size();
113
114        self.canvas.set_width(w as u32);
115        self.canvas.set_height(h as u32);
116
117        if self.auto_resize_canvas_css {
118            let _ = self
119                .canvas
120                .style()
121                .set_property("width", &format!("{width}px"));
122            let _ = self
123                .canvas
124                .style()
125                .set_property("height", &format!("{height}px"));
126        }
127
128        self.state.viewport(&self.gl, 0, 0, w, h);
129    }
130
131    /// Clears the framebuffer with the specified color.
132    ///
133    /// Sets the clear color and clears both the color and depth buffers.
134    /// Color components should be in the range [0.0, 1.0].
135    ///
136    /// # Parameters
137    /// * `r` - Red component (0.0 to 1.0)
138    /// * `g` - Green component (0.0 to 1.0)
139    /// * `b` - Blue component (0.0 to 1.0)
140    pub fn clear(&mut self, r: f32, g: f32, b: f32) {
141        self.state.clear_color(&self.gl, r, g, b, 1.0);
142        self.gl
143            .clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT);
144    }
145
146    /// Begins a new rendering frame.
147    pub fn begin_frame(&mut self) {
148        let (r, g, b) = self.canvas_padding_color;
149        self.clear(r, g, b);
150    }
151
152    /// Renders a drawable object.
153    ///
154    /// This method calls the drawable's prepare, draw, and cleanup methods
155    /// in sequence, providing it with a render context containing.
156    ///
157    /// # Parameters
158    /// * `drawable` - Object implementing the `Drawable` trait
159    #[allow(private_bounds)]
160    pub fn render(&mut self, drawable: &impl Drawable) {
161        let mut context = RenderContext { gl: &self.gl, state: &mut self.state };
162
163        drawable.prepare(&mut context);
164        drawable.draw(&mut context);
165        drawable.cleanup(&mut context);
166    }
167
168    /// Ends the current rendering frame.
169    ///
170    /// This method finalizes the frame rendering. In future versions, this
171    /// may handle buffer swapping or other post-rendering operations.
172    pub fn end_frame(&mut self) {
173        // swap buffers (todo)
174    }
175
176    /// Returns a reference to the WebGL2 rendering context.
177    pub fn gl(&self) -> &GL {
178        &self.gl
179    }
180
181    /// Returns a mutable reference to the WebGL2 rendering context.
182    pub fn canvas(&self) -> &HtmlCanvasElement {
183        &self.canvas
184    }
185
186    /// Returns the current canvas dimensions as a tuple.
187    ///
188    /// # Returns
189    /// Tuple containing (width, height) in pixels
190    pub fn canvas_size(&self) -> (i32, i32) {
191        self.logical_size()
192    }
193
194    /// Returns the logical size of the canvas in pixels. The logical size
195    /// corresponds to the size used for layout and rendering calculations,
196    pub fn logical_size(&self) -> (i32, i32) {
197        self.logical_size_px
198    }
199
200    /// Returns the physical size of the canvas in pixels, taking into account the device
201    /// pixel ratio.
202    pub fn physical_size(&self) -> (i32, i32) {
203        let (w, h) = self.logical_size_px;
204        (
205            (w as f32 * self.pixel_ratio).round() as i32,
206            (h as f32 * self.pixel_ratio).round() as i32,
207        )
208    }
209
210    /// Checks if the WebGL context has been lost.
211    ///
212    /// Returns `true` if the context is lost and needs to be restored.
213    /// Use [`restore_context`] to recover from context loss.
214    pub fn is_context_lost(&self) -> bool {
215        self.gl.is_context_lost()
216    }
217
218    /// Restores the WebGL context after a context loss event.
219    ///
220    /// This method obtains a fresh WebGL2 context from the canvas and
221    /// reinitializes the renderer state. After calling this, you must
222    /// also recreate GPU resources in other components (textures, buffers, etc.).
223    ///
224    /// # Returns
225    /// * `Ok(())` - Context successfully restored
226    /// * `Err(Error)` - Failed to obtain new WebGL context
227    ///
228    /// # Note
229    /// This only restores the renderer's context. You must separately call
230    /// recreation methods on `TerminalGrid` and `FontAtlas` to fully recover.
231    pub fn restore_context(&mut self) -> Result<(), Error> {
232        // Get a fresh WebGL2 context from the same canvas
233        let gl = js::get_webgl2_context(&self.canvas)?;
234        self.state = GlState::new(&gl);
235        self.gl = gl;
236
237        // Restore viewport
238        let (width, height) = self.canvas_size();
239        self.state.viewport(&self.gl, 0, 0, width, height);
240
241        Ok(())
242    }
243
244    /// Sets the pixel ratio. Must separately call [`Self::resize`] and
245    /// [`crate::TerminalGrid::resize`] to update the renderer's viewport and grid dimensions.
246    pub(crate) fn set_pixel_ratio(&mut self, pixel_ratio: f32) {
247        self.pixel_ratio = pixel_ratio;
248    }
249}
250
251/// Trait for objects that can be rendered by the renderer.
252pub(super) trait Drawable {
253    /// Prepares the object for rendering.
254    ///
255    /// This method should set up all necessary OpenGL state, bind shaders,
256    /// textures, and vertex data required for rendering.
257    ///
258    /// # Parameters
259    /// * `context` - Mutable reference to the render context
260    fn prepare(&self, context: &mut RenderContext);
261
262    /// Performs the actual rendering.
263    ///
264    /// This method should issue draw calls to render the object. All necessary
265    /// state should already be set up from the `prepare()` call.
266    ///
267    /// # Parameters
268    /// * `context` - Mutable reference to the render context
269    fn draw(&self, context: &mut RenderContext);
270
271    /// Cleans up after rendering.
272    ///
273    /// This method should restore OpenGL state and unbind any resources
274    /// that were bound during `prepare()`. This ensures proper cleanup
275    /// for subsequent rendering operations.
276    ///
277    /// # Parameters
278    /// * `context` - Mutable reference to the render context
279    fn cleanup(&self, context: &mut RenderContext);
280}