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