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}