Skip to main content

beamterm_renderer/gl/
terminal_grid.rs

1use std::{borrow::Cow, cmp::min, fmt::Debug, ops::Index};
2
3use beamterm_data::{FontAtlasData, FontStyle, Glyph, GlyphEffect};
4use compact_str::{CompactString, CompactStringExt};
5use web_sys::{WebGl2RenderingContext, console};
6
7use crate::{
8    CursorPosition,
9    error::Error,
10    gl::{
11        CellIterator, CellQuery, Drawable, GL, RenderContext, ShaderProgram, StaticFontAtlas,
12        atlas::{FontAtlas, GlyphSlot},
13        buffer_upload_array,
14        selection::SelectionTracker,
15        ubo::UniformBufferObject,
16    },
17    mat4::Mat4,
18};
19
20/// A high-performance terminal grid renderer using instanced rendering.
21///
22/// `TerminalGrid` renders a grid of terminal cells using WebGL2 instanced drawing.
23/// Each cell can display a character from a font atlas with customizable foreground
24/// and background colors. The renderer uses a 2D texture array to efficiently
25/// store glyph data and supports real-time updates of cell content.
26#[derive(Debug)]
27pub struct TerminalGrid {
28    /// GPU resources (shader, buffers, UBOs) - recreated on context loss
29    gpu: GpuResources,
30    /// Terminal cell instance data
31    cells: Vec<CellDynamic>,
32    /// Terminal size in cells
33    terminal_size: (u16, u16),
34    /// Size of the canvas in pixels (physical)
35    canvas_size_px: (i32, i32),
36    /// Current device pixel ratio
37    pixel_ratio: f32,
38    /// Font atlas for rendering text.
39    atlas: FontAtlas,
40    /// Fallback glyph for missing symbols.
41    fallback_glyph: u16,
42    /// Selection tracker for managing cell selections.
43    selection: SelectionTracker,
44    /// Indicates whether there are cells pending flush to the GPU.
45    cells_pending_flush: bool,
46}
47
48/// GPU resources that need to be recreated after a WebGL context loss.
49///
50/// This struct encapsulates all WebGL-dependent resources: shader program,
51/// vertex buffers, uniform buffer objects, and uniform locations. These
52/// resources become invalid after a context loss and must be recreated
53/// with a fresh WebGL context.
54#[derive(Debug)]
55struct GpuResources {
56    /// Shader program for rendering the terminal cells.
57    shader: ShaderProgram,
58    /// Buffers for the terminal grid (VAO, VBO, instance buffers)
59    buffers: TerminalBuffers,
60    /// Shared state for the vertex shader
61    ubo_vertex: UniformBufferObject,
62    /// Shared state for the fragment shader
63    ubo_fragment: UniformBufferObject,
64    /// Uniform location for the texture sampler.
65    sampler_loc: web_sys::WebGlUniformLocation,
66}
67
68impl GpuResources {
69    const FRAGMENT_GLSL: &'static str = include_str!("../shaders/cell.frag");
70    const VERTEX_GLSL: &'static str = include_str!("../shaders/cell.vert");
71
72    /// Creates all GPU resources for the terminal grid.
73    ///
74    /// This method creates and initializes:
75    /// - Vertex Array Object (VAO)
76    /// - Vertex and index buffers
77    /// - Instance buffers for cell positions and data
78    /// - Shader program
79    /// - Uniform Buffer Objects (UBOs)
80    fn new(
81        gl: &WebGl2RenderingContext,
82        cell_pos: &[CellStatic],
83        cell_data: &[CellDynamic],
84        cell_size: (i32, i32),
85    ) -> Result<Self, Error> {
86        // Create and setup the Vertex Array Object
87        let vao = create_vao(gl)?;
88        gl.bind_vertex_array(Some(&vao));
89
90        // Create all buffers
91        let buffers = setup_buffers(gl, vao, cell_pos, cell_data, cell_size)?;
92
93        // Unbind VAO to prevent accidental modification
94        gl.bind_vertex_array(None);
95
96        // Setup shader and uniform data
97        let shader = ShaderProgram::create(gl, Self::VERTEX_GLSL, Self::FRAGMENT_GLSL)?;
98        shader.use_program(gl);
99
100        let ubo_vertex = UniformBufferObject::new(gl, CellVertexUbo::BINDING_POINT)?;
101        ubo_vertex.bind_to_shader(gl, &shader, "VertUbo")?;
102        let ubo_fragment = UniformBufferObject::new(gl, CellFragmentUbo::BINDING_POINT)?;
103        ubo_fragment.bind_to_shader(gl, &shader, "FragUbo")?;
104
105        let sampler_loc = gl
106            .get_uniform_location(&shader.program, "u_sampler")
107            .ok_or(Error::uniform_location_failed("u_sampler"))?;
108
109        Ok(Self {
110            shader,
111            buffers,
112            ubo_vertex,
113            ubo_fragment,
114            sampler_loc,
115        })
116    }
117}
118
119#[derive(Debug)]
120struct TerminalBuffers {
121    vao: web_sys::WebGlVertexArrayObject,
122    vertices: web_sys::WebGlBuffer,
123    instance_pos: web_sys::WebGlBuffer,
124    instance_cell: web_sys::WebGlBuffer,
125    indices: web_sys::WebGlBuffer,
126}
127
128impl TerminalBuffers {
129    fn upload_instance_data<T>(&self, gl: &WebGl2RenderingContext, cell_data: &[T]) {
130        gl.bind_vertex_array(Some(&self.vao));
131        gl.bind_buffer(GL::ARRAY_BUFFER, Some(&self.instance_cell));
132
133        buffer_upload_array(gl, GL::ARRAY_BUFFER, cell_data, GL::DYNAMIC_DRAW);
134
135        gl.bind_vertex_array(None);
136    }
137
138    /// Updates the vertex buffer with new cell dimensions.
139    fn update_vertex_buffer(&self, gl: &WebGl2RenderingContext, cell_size: (i32, i32)) {
140        let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
141
142        #[rustfmt::skip]
143        let vertices: [f32; 16] = [
144            //x    y    u    v
145              w, 0.0, 1.0, 0.0, // top-right
146            0.0,   h, 0.0, 1.0, // bottom-left
147              w,   h, 1.0, 1.0, // bottom-right
148            0.0, 0.0, 0.0, 0.0  // top-left
149        ];
150
151        gl.bind_vertex_array(Some(&self.vao));
152        gl.bind_buffer(GL::ARRAY_BUFFER, Some(&self.vertices));
153
154        unsafe {
155            let view = js_sys::Float32Array::view(&vertices);
156            gl.buffer_sub_data_with_i32_and_array_buffer_view(GL::ARRAY_BUFFER, 0, &view);
157        }
158
159        gl.bind_vertex_array(None);
160    }
161}
162
163impl TerminalGrid {
164    pub(crate) fn new(
165        gl: &WebGl2RenderingContext,
166        atlas: FontAtlas,
167        screen_size: (i32, i32),
168        pixel_ratio: f32,
169    ) -> Result<Self, Error> {
170        let cell_scale = atlas.cell_scale_for_dpr(pixel_ratio);
171        let base_cell_size = atlas.cell_size();
172        let cell_size = (
173            (base_cell_size.0 as f32 * cell_scale).round() as i32,
174            (base_cell_size.1 as f32 * cell_scale).round() as i32,
175        );
176        let (cols, rows) = (screen_size.0 / cell_size.0, screen_size.1 / cell_size.1);
177
178        let space_glyph = atlas.space_glyph_id();
179        let cell_data = create_terminal_cell_data(cols, rows, space_glyph);
180        let cell_pos = CellStatic::create_grid(cols, rows);
181
182        let grid = Self {
183            gpu: GpuResources::new(gl, &cell_pos, &cell_data, cell_size)?,
184            terminal_size: (cols as u16, rows as u16),
185            canvas_size_px: screen_size,
186            pixel_ratio,
187            cells: cell_data,
188            atlas,
189            fallback_glyph: space_glyph,
190            selection: SelectionTracker::new(),
191            cells_pending_flush: false,
192        };
193
194        grid.upload_ubo_data(gl);
195
196        Ok(grid)
197    }
198
199    /// Returns the effective cell size for layout (base cell size * cell scale).
200    fn effective_cell_size(&self) -> (i32, i32) {
201        let cell_scale = self.atlas.cell_scale_for_dpr(self.pixel_ratio);
202        let base = self.atlas.cell_size();
203        (
204            (base.0 as f32 * cell_scale).round() as i32,
205            (base.1 as f32 * cell_scale).round() as i32,
206        )
207    }
208
209    /// Sets the fallback glyph for missing characters.
210    pub fn set_fallback_glyph(&mut self, fallback: &str) {
211        self.fallback_glyph = self
212            .atlas
213            .get_glyph_id(fallback, FontStyle::Normal as u16)
214            .unwrap_or(' ' as u16);
215    }
216
217    /// Replaces the current font atlas with a new one, translating all existing
218    /// glyph IDs to the new atlas.
219    ///
220    /// This method handles the transition between atlases by:
221    /// 1. Looking up the symbol for each existing glyph ID in the old atlas
222    /// 2. Resolving the corresponding glyph slot in the new atlas
223    /// 3. Updating double-width glyphs (emoji, wide chars) across both cells
224    /// 4. Resizing the grid if cell dimensions changed
225    ///
226    /// # Parameters
227    /// * `gl` - WebGL2 rendering context
228    /// * `atlas` - The new font atlas to use
229    pub(crate) fn replace_atlas(&mut self, gl: &WebGl2RenderingContext, atlas: FontAtlas) {
230        let glyph_mask = self.atlas.base_lookup_mask() as u16;
231        let style_mask = !glyph_mask;
232
233        // update fallback glyph to new atlas, before translating existing cells
234        self.fallback_glyph = self
235            .atlas
236            .get_symbol(self.fallback_glyph & glyph_mask)
237            .and_then(|symbol| {
238                let style_bits = self.fallback_glyph & style_mask;
239                atlas.resolve_glyph_slot(symbol.as_str(), style_bits)
240            })
241            .map(|slot| slot.slot_id())
242            .unwrap_or(atlas.space_glyph_id());
243
244        // translate existing glyph ids to new atlas
245        let mut skip_next = false;
246        for idx in 0..self.cells.len() {
247            if skip_next {
248                skip_next = false;
249                continue;
250            }
251
252            let old_glyph_id = self.cells[idx].glyph_id();
253            let style_bits = old_glyph_id & style_mask;
254
255            let slot = self
256                .atlas
257                .get_symbol(old_glyph_id & glyph_mask)
258                .and_then(|symbol| atlas.resolve_glyph_slot(symbol.as_str(), style_bits));
259
260            match slot {
261                Some(GlyphSlot::Normal(id)) => {
262                    self.cells[idx].set_glyph_id(id);
263                },
264                Some(GlyphSlot::Wide(id)) | Some(GlyphSlot::Emoji(id)) => {
265                    self.cells[idx].set_glyph_id(id);
266                    // update right-half in next cell if within bounds
267                    if let Some(next_cell) = self.cells.get_mut(idx + 1) {
268                        next_cell.set_glyph_id(id + 1);
269                        skip_next = true;
270                    }
271                },
272                None => {
273                    self.cells[idx].set_glyph_id(self.fallback_glyph);
274                },
275            }
276        }
277
278        // clear any active selection, just to keep it simple
279        self.selection.clear();
280
281        // replace atlas and resize grid accordingly
282        let old_atlas = std::mem::replace(&mut self.atlas, atlas);
283        old_atlas.delete(gl);
284        self.cells_pending_flush = true;
285
286        // update vertex buffer with new cell dimensions
287        self.gpu
288            .buffers
289            .update_vertex_buffer(gl, self.effective_cell_size());
290
291        let _ = self.resize(gl, self.canvas_size_px, self.pixel_ratio);
292    }
293
294    /// Returns the [`FontAtlas`] used by this terminal grid.
295    pub(crate) fn atlas(&self) -> &FontAtlas {
296        &self.atlas
297    }
298
299    /// Returns a mutable reference to the font atlas.
300    pub(crate) fn atlas_mut(&mut self) -> &mut FontAtlas {
301        &mut self.atlas
302    }
303
304    /// Returns the canvas size in pixels.
305    pub(crate) fn canvas_size(&self) -> (i32, i32) {
306        self.canvas_size_px
307    }
308
309    /// Returns the effective cell dimensions in pixels (base size * cell scale).
310    pub fn cell_size(&self) -> (i32, i32) {
311        self.effective_cell_size()
312    }
313
314    /// Returns the size of the terminal grid in cells.
315    pub fn terminal_size(&self) -> (u16, u16) {
316        self.terminal_size
317    }
318
319    /// Returns a mutable reference to the cell data at the specified cell coordinates.
320    pub fn cell_data_mut(&mut self, x: u16, y: u16) -> Option<&mut CellDynamic> {
321        let (cols, _) = self.terminal_size;
322        let idx = y as usize * cols as usize + x as usize;
323        self.cells.get_mut(idx)
324    }
325
326    /// Returns the active selection state of the terminal grid.
327    pub(crate) fn selection_tracker(&self) -> SelectionTracker {
328        self.selection.clone()
329    }
330
331    /// Returns the symbols in the specified block range as a `CompactString`.
332    pub(super) fn get_symbols(&self, selection: CellIterator) -> CompactString {
333        let (cols, rows) = self.terminal_size;
334        let mut text = CompactString::new("");
335
336        for (idx, require_newline_after) in selection {
337            let cell_symbol = self.get_cell_symbol(idx);
338            if cell_symbol.is_some() {
339                text.push_str(&cell_symbol.unwrap_or_default());
340            }
341
342            if require_newline_after {
343                text.push('\n'); // add newline after each row
344            }
345        }
346
347        text
348    }
349
350    /// Returns the ASCII character at the given position, if it's an ASCII char.
351    ///
352    /// Returns `None` for non-ASCII characters or out-of-bounds positions.
353    /// This is an optimized path for URL detection that avoids string allocation.
354    pub(crate) fn get_ascii_char_at(&self, cursor: CursorPosition) -> Option<char> {
355        let idx = cursor.row as usize * self.terminal_size.0 as usize + cursor.col as usize;
356        if idx < self.cells.len() {
357            let glyph_id = self.cells[idx].glyph_id();
358            self.atlas.get_ascii_char(glyph_id)
359        } else {
360            None
361        }
362    }
363
364    pub(crate) fn hash_cells(&self, selection: CellQuery) -> u64 {
365        use std::hash::{Hash, Hasher};
366
367        use rustc_hash::FxHasher;
368
369        let mut hasher = FxHasher::default();
370        for (idx, _) in self.cell_iter(selection) {
371            self.cells[idx].hash(&mut hasher);
372        }
373
374        hasher.finish()
375    }
376
377    fn get_cell_symbol(&self, idx: usize) -> Option<CompactString> {
378        if idx < self.cells.len() {
379            let glyph_id = self.cells[idx].glyph_id();
380            let cell_symbol = self.atlas.get_symbol(glyph_id);
381            if cell_symbol.is_some() {
382                return cell_symbol;
383            }
384        }
385
386        self.fallback_symbol()
387    }
388
389    /// Uploads uniform buffer data for screen and cell dimensions.
390    ///
391    /// This method updates the shader uniform buffers with the current screen
392    /// size and cell dimensions. Must be called when the screen size changes
393    /// or when initializing the grid.
394    ///
395    /// # Parameters
396    /// * `gl` - WebGL2 rendering context
397    fn upload_ubo_data(&self, gl: &WebGl2RenderingContext) {
398        let vertex_ubo = CellVertexUbo::new(self.canvas_size_px, self.effective_cell_size());
399        self.gpu.ubo_vertex.upload_data(gl, &vertex_ubo);
400
401        let fragment_ubo = CellFragmentUbo::new(&self.atlas);
402        self.gpu
403            .ubo_fragment
404            .upload_data(gl, &fragment_ubo);
405    }
406
407    /// Returns the total number of cells in the terminal grid.
408    pub fn cell_count(&self) -> usize {
409        self.cells.len()
410    }
411
412    /// Updates the content of terminal cells with new data.
413    ///
414    /// This method efficiently updates the dynamic instance buffer with new
415    /// cell data. The iterator must provide exactly the same number of cells
416    /// as the grid contains, in row-major order.
417    ///
418    /// # Parameters
419    /// * `gl` - WebGL2 rendering context
420    /// * `cells` - Iterator providing `CellData` for each cell in the grid
421    ///
422    /// # Returns
423    /// * `Ok(())` - Successfully updated cell data
424    /// * `Err(Error)` - Failed to update buffer or other WebGL error
425    pub fn update_cells<'a>(
426        &mut self,
427        gl: &WebGl2RenderingContext,
428        cells: impl Iterator<Item = CellData<'a>>,
429    ) -> Result<(), Error> {
430        // update instance buffer with new cell data
431        let atlas = &self.atlas;
432
433        let fallback_glyph = GlyphSlot::Normal(self.fallback_glyph);
434
435        // handle double-width emoji that span two cells
436        let mut pending_cell: Option<CellDynamic> = None;
437        self.cells
438            .iter_mut()
439            .zip(cells)
440            .for_each(|(cell, data)| {
441                let glyph = atlas
442                    .resolve_glyph_slot(data.symbol, data.style_bits)
443                    .unwrap_or(fallback_glyph);
444
445                *cell = if let Some(second_cell) = pending_cell.take() {
446                    second_cell
447                } else {
448                    match glyph {
449                        GlyphSlot::Normal(id) => CellDynamic::new(id, data.fg, data.bg),
450
451                        GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => {
452                            // storing a double-width glyph, reserve next cell with right-half id
453                            pending_cell = Some(CellDynamic::new(id + 1, data.fg, data.bg));
454                            CellDynamic::new(id, data.fg, data.bg)
455                        },
456                    }
457                }
458            });
459
460        self.cells_pending_flush = true;
461        Ok(())
462    }
463
464    pub(crate) fn update_cells_by_position<'a>(
465        &mut self,
466        cells: impl Iterator<Item = (u16, u16, CellData<'a>)>,
467    ) -> Result<(), Error> {
468        let cols = self.terminal_size.0 as usize;
469        let cells_by_index = cells.map(|(x, y, data)| (y as usize * cols + x as usize, data));
470
471        self.update_cells_by_index(cells_by_index)
472    }
473
474    pub(crate) fn update_cells_by_index<'a>(
475        &mut self,
476        cells: impl Iterator<Item = (usize, CellData<'a>)>,
477    ) -> Result<(), Error> {
478        // update instance buffer with new cell data by position
479        let atlas = &self.atlas;
480
481        let cell_count = self.cells.len();
482        let fallback_glyph = GlyphSlot::Normal(self.fallback_glyph);
483
484        // ratatui and beamterm can disagree on which emoji
485        // are double-width (beamterm assumes double-width for all emoji),
486        // so for ratatui and similar clients we need to skip the next cell
487        // if we just wrote a double-width emoji in the current cell.
488        let mut skip_idx = None;
489
490        cells
491            .filter(|(idx, _)| *idx < cell_count)
492            .for_each(|(idx, cell)| {
493                if skip_idx.take() == Some(idx) {
494                    // skip this cell, already handled as part of previous double-width emoji
495                    return;
496                }
497
498                let glyph = atlas
499                    .resolve_glyph_slot(cell.symbol, cell.style_bits)
500                    .unwrap_or(fallback_glyph);
501
502                match glyph {
503                    GlyphSlot::Normal(id) => {
504                        self.cells[idx] = CellDynamic::new(id, cell.fg, cell.bg);
505                    },
506
507                    GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => {
508                        // render left half in current cell
509                        self.cells[idx] = CellDynamic::new(id, cell.fg, cell.bg);
510
511                        // render right half in next cell, if within bounds
512                        if let Some(c) = self.cells.get_mut(idx + 1) {
513                            *c = CellDynamic::new(id + 1, cell.fg, cell.bg);
514                            skip_idx = Some(idx + 1);
515                        }
516                    },
517                }
518            });
519
520        self.cells_pending_flush = true;
521
522        Ok(())
523    }
524
525    pub(crate) fn update_cell(&mut self, x: u16, y: u16, cell_data: CellData) -> Result<(), Error> {
526        let (cols, _) = self.terminal_size;
527        let idx = y as usize * cols as usize + x as usize;
528        self.update_cell_by_index(idx, cell_data)
529    }
530
531    pub(crate) fn update_cell_by_index(
532        &mut self,
533        idx: usize,
534        cell_data: CellData,
535    ) -> Result<(), Error> {
536        self.update_cells_by_index(std::iter::once((idx, cell_data)))
537    }
538
539    /// Flushes pending cell updates to the GPU.
540    pub(crate) fn flush_cells(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
541        if !self.cells_pending_flush {
542            return Ok(()); // no pending updates to flush
543        }
544
545        // if there is an active selected region with a content hash,
546        // check if the underlying content has changed; if so, clear the selection
547        self.clear_stale_selection();
548
549        // If there's an active selection, flip the colors of the selected cells.
550        // This ensures that the selected cells are rendered with inverted colors
551        // during the GPU upload process.
552        self.flip_selected_cell_colors();
553
554        self.gpu
555            .buffers
556            .upload_instance_data(gl, &self.cells);
557
558        // Restore the original colors of the selected cells after the upload.
559        // This ensures that the internal state of the cells remains consistent.
560        self.flip_selected_cell_colors();
561
562        self.cells_pending_flush = false;
563        Ok(())
564    }
565
566    fn flip_selected_cell_colors(&mut self) {
567        if let Some(iter) = self.selected_cells_iter() {
568            iter.for_each(|(idx, _)| self.cells[idx].flip_colors());
569        }
570    }
571
572    fn selected_cells_iter(&self) -> Option<CellIterator> {
573        self.selection
574            .get_query()
575            .map(|query| self.cell_iter(query))
576    }
577
578    fn flip_cell_colors(&mut self, x: u16, y: u16) {
579        let (cols, _) = self.terminal_size;
580        let idx = y as usize * cols as usize + x as usize;
581        if idx < self.cells.len() {
582            self.cells[idx].flip_colors();
583        }
584    }
585
586    /// Resizes the terminal grid to fit the new canvas dimensions.
587    ///
588    /// This method recalculates the terminal dimensions based on the canvas size and cell
589    /// dimensions, then recreates the necessary GPU buffers if the grid size changed.
590    /// Existing cell content is preserved where possible during resizing.
591    ///
592    /// # Parameters
593    ///
594    /// * `gl` - WebGL2 rendering context
595    /// * `canvas_size` - New canvas dimensions in pixels as `(width, height)`
596    ///
597    /// # Returns
598    ///
599    /// * `Ok(())` - Successfully resized the terminal
600    /// * `Err(Error)` - Failed to recreate buffers or other WebGL error
601    pub fn resize(
602        &mut self,
603        gl: &WebGl2RenderingContext,
604        canvas_size: (i32, i32),
605        pixel_ratio: f32,
606    ) -> Result<(), Error> {
607        self.canvas_size_px = canvas_size;
608        self.pixel_ratio = pixel_ratio;
609
610        let cell_size = self.effective_cell_size();
611
612        // Update vertex buffer with new cell dimensions
613        self.gpu
614            .buffers
615            .update_vertex_buffer(gl, cell_size);
616
617        // Update the UBO with new screen size
618        self.upload_ubo_data(gl);
619
620        let cols = canvas_size.0 / cell_size.0;
621        let rows = canvas_size.1 / cell_size.1;
622        if self.terminal_size == (cols as u16, rows as u16) {
623            return Ok(()); // no change in terminal size
624        }
625
626        // update buffers; bind VAO to ensure correct state
627        gl.bind_vertex_array(Some(&self.gpu.buffers.vao));
628
629        // delete old cell instance buffers
630        gl.delete_buffer(Some(&self.gpu.buffers.instance_cell));
631        gl.delete_buffer(Some(&self.gpu.buffers.instance_pos));
632
633        // resize cell data vector
634        let current_size = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
635        let cell_data = self.resize_cell_grid(current_size, (cols, rows));
636        self.cells = cell_data;
637
638        let cell_pos = CellStatic::create_grid(cols, rows);
639
640        // re-create buffers with new data
641        self.gpu.buffers.instance_cell = create_dynamic_instance_buffer(gl, &self.cells)?;
642        self.gpu.buffers.instance_pos = create_static_instance_buffer(gl, &cell_pos)?;
643
644        // unbind VAO
645        gl.bind_vertex_array(None);
646
647        self.terminal_size = (cols as u16, rows as u16);
648
649        Ok(())
650    }
651
652    /// Recreates all GPU resources after a WebGL context loss.
653    ///
654    /// This method rebuilds all GPU-side resources (VAO, buffers, shaders, UBOs)
655    /// while preserving the current cell data and terminal state. Call this after
656    /// obtaining a new WebGL context following a context loss event.
657    ///
658    /// # Parameters
659    /// * `gl` - The new WebGL2 rendering context
660    ///
661    /// # Returns
662    /// * `Ok(())` - All resources successfully recreated
663    /// * `Err(Error)` - Failed to recreate one or more resources
664    ///
665    /// # Note
666    /// The font atlas texture must be recreated separately via
667    /// [`StaticFontAtlas::recreate_texture`] before calling this method.
668    pub fn recreate_resources(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
669        let cell_size = self.effective_cell_size();
670        let (cols, rows) = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
671        let cell_pos = CellStatic::create_grid(cols, rows);
672
673        // Recreate all GPU resources
674        self.gpu = GpuResources::new(gl, &cell_pos, &self.cells, cell_size)?;
675
676        // Upload UBO data
677        self.upload_ubo_data(gl);
678
679        // Mark cells as needing flush to upload to new buffers
680        self.cells_pending_flush = true;
681
682        Ok(())
683    }
684
685    /// Recreates the font atlas texture after a WebGL context loss.
686    ///
687    /// This is a convenience method that delegates to [`StaticFontAtlas::recreate_texture`].
688    /// Call this before [`recreate_resources`] when recovering from context loss.
689    pub fn recreate_atlas_texture(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
690        self.atlas.recreate_texture(gl)
691    }
692
693    /// Returns the base glyph identifier for a given symbol.
694    pub fn base_glyph_id(&self, symbol: &str) -> Option<u16> {
695        self.atlas.get_base_glyph_id(symbol)
696    }
697
698    fn fallback_symbol(&self) -> Option<CompactString> {
699        self.atlas.get_symbol(self.fallback_glyph)
700    }
701
702    fn clear_stale_selection(&self) {
703        if let Some(query) = self.selection_tracker().get_query()
704            && let Some(hash) = query.content_hash
705            && hash != self.hash_cells(query)
706        {
707            self.selection.clear();
708        }
709    }
710
711    fn resize_cell_grid(&self, old_size: (i32, i32), new_size: (i32, i32)) -> Vec<CellDynamic> {
712        let empty_cell = CellDynamic::new(self.atlas.space_glyph_id(), 0xFFFFFF, 0x000000);
713
714        let new_len = new_size.0 * new_size.1;
715        let mut new_cells = Vec::with_capacity(new_len as usize);
716        for _ in 0..new_len {
717            new_cells.push(empty_cell);
718        }
719
720        let cells = &self.cells;
721        for y in 0..min(old_size.1, new_size.1) {
722            for x in 0..min(old_size.0, new_size.0) {
723                let new_idx = (y * new_size.0 + x) as usize;
724                let old_idx = (y * old_size.0 + x) as usize;
725                new_cells[new_idx] = cells[old_idx];
726            }
727        }
728
729        new_cells
730    }
731}
732
733fn create_vao(gl: &WebGl2RenderingContext) -> Result<web_sys::WebGlVertexArrayObject, Error> {
734    gl.create_vertex_array()
735        .ok_or(Error::vertex_array_creation_failed())
736}
737
738fn setup_buffers(
739    gl: &WebGl2RenderingContext,
740    vao: web_sys::WebGlVertexArrayObject,
741    cell_pos: &[CellStatic],
742    cell_data: &[CellDynamic],
743    cell_size: (i32, i32),
744) -> Result<TerminalBuffers, Error> {
745    let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
746
747    #[rustfmt::skip]
748    let vertices = [
749        //x    y    u    v
750          w, 0.0, 1.0, 0.0, // top-right
751        0.0,   h, 0.0, 1.0, // bottom-left
752          w,   h, 1.0, 1.0, // bottom-right
753        0.0, 0.0, 0.0, 0.0  // top-left
754    ];
755    let indices = [0, 1, 2, 0, 3, 1];
756
757    Ok(TerminalBuffers {
758        vao,
759        vertices: create_buffer_f32(gl, GL::ARRAY_BUFFER, &vertices, GL::STATIC_DRAW)?,
760        instance_pos: create_static_instance_buffer(gl, cell_pos)?,
761        instance_cell: create_dynamic_instance_buffer(gl, cell_data)?,
762        indices: create_buffer_u8(gl, GL::ELEMENT_ARRAY_BUFFER, &indices, GL::STATIC_DRAW)?,
763    })
764}
765
766fn create_buffer_u8(
767    gl: &WebGl2RenderingContext,
768    target: u32,
769    data: &[u8],
770    usage: u32,
771) -> Result<web_sys::WebGlBuffer, Error> {
772    let index_buf = gl
773        .create_buffer()
774        .ok_or(Error::buffer_creation_failed("vbo-u8"))?;
775    gl.bind_buffer(target, Some(&index_buf));
776
777    gl.buffer_data_with_u8_array(target, data, usage);
778
779    Ok(index_buf)
780}
781
782fn create_buffer_f32(
783    gl: &WebGl2RenderingContext,
784    target: u32,
785    data: &[f32],
786    usage: u32,
787) -> Result<web_sys::WebGlBuffer, Error> {
788    let buffer = gl
789        .create_buffer()
790        .ok_or(Error::buffer_creation_failed("vbo-f32"))?;
791
792    gl.bind_buffer(target, Some(&buffer));
793
794    unsafe {
795        let view = js_sys::Float32Array::view(data);
796        gl.buffer_data_with_array_buffer_view(target, &view, usage);
797    }
798
799    // vertex attributes \\
800    const STRIDE: i32 = (2 + 2) * 4; // 4 floats per vertex
801    enable_vertex_attrib(gl, attrib::POS, 2, GL::FLOAT, 0, STRIDE);
802    enable_vertex_attrib(gl, attrib::UV, 2, GL::FLOAT, 8, STRIDE);
803
804    Ok(buffer)
805}
806
807fn create_static_instance_buffer(
808    gl: &WebGl2RenderingContext,
809    instance_data: &[CellStatic],
810) -> Result<web_sys::WebGlBuffer, Error> {
811    let instance_buf = gl
812        .create_buffer()
813        .ok_or(Error::buffer_creation_failed("static-instance-buffer"))?;
814
815    gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
816    buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::STATIC_DRAW);
817
818    let stride = size_of::<CellStatic>() as i32;
819    enable_vertex_attrib_array(gl, attrib::GRID_XY, 2, GL::UNSIGNED_SHORT, 0, stride);
820
821    Ok(instance_buf)
822}
823
824fn create_dynamic_instance_buffer(
825    gl: &WebGl2RenderingContext,
826    instance_data: &[CellDynamic],
827) -> Result<web_sys::WebGlBuffer, Error> {
828    let instance_buf = gl
829        .create_buffer()
830        .ok_or(Error::buffer_creation_failed("dynamic-instance-buffer"))?;
831
832    gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
833    buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::DYNAMIC_DRAW);
834
835    let stride = size_of::<CellDynamic>() as i32;
836
837    // setup instance attributes (while VAO is bound)
838    enable_vertex_attrib_array(
839        gl,
840        attrib::PACKED_DEPTH_FG_BG,
841        2,
842        GL::UNSIGNED_INT,
843        0,
844        stride,
845    );
846
847    Ok(instance_buf)
848}
849
850fn enable_vertex_attrib_array(
851    gl: &WebGl2RenderingContext,
852    index: u32,
853    size: i32,
854    type_: u32,
855    offset: i32,
856    stride: i32,
857) {
858    enable_vertex_attrib(gl, index, size, type_, offset, stride);
859    gl.vertex_attrib_divisor(index, 1);
860}
861
862fn enable_vertex_attrib(
863    gl: &WebGl2RenderingContext,
864    index: u32,
865    size: i32,
866    type_: u32,
867    offset: i32,
868    stride: i32,
869) {
870    gl.enable_vertex_attrib_array(index);
871    if type_ == GL::FLOAT {
872        gl.vertex_attrib_pointer_with_i32(index, size, type_, false, stride, offset);
873    } else {
874        gl.vertex_attrib_i_pointer_with_i32(index, size, type_, stride, offset);
875    }
876}
877
878impl Drawable for TerminalGrid {
879    fn prepare(&self, context: &mut RenderContext) {
880        let gl = context.gl;
881
882        self.gpu.shader.use_program(gl);
883
884        gl.bind_vertex_array(Some(&self.gpu.buffers.vao));
885
886        self.atlas.bind(gl, 0);
887        self.atlas.flush(gl).unwrap(); // fixme: handle error
888        self.gpu.ubo_vertex.bind(context.gl);
889        self.gpu.ubo_fragment.bind(context.gl);
890        gl.uniform1i(Some(&self.gpu.sampler_loc), 0);
891    }
892
893    fn draw(&self, context: &mut RenderContext) {
894        let gl = context.gl;
895        let cell_count = self.cells.len() as i32;
896
897        gl.draw_elements_instanced_with_i32(GL::TRIANGLES, 6, GL::UNSIGNED_BYTE, 0, cell_count);
898    }
899
900    fn cleanup(&self, context: &mut RenderContext) {
901        let gl = context.gl;
902        gl.bind_vertex_array(None);
903        gl.bind_texture(GL::TEXTURE_2D_ARRAY, None);
904
905        self.gpu.ubo_vertex.unbind(gl);
906        self.gpu.ubo_fragment.unbind(gl);
907    }
908}
909
910/// Data for a single terminal cell including character and colors.
911///
912/// `CellData` represents the visual content of one terminal cell, including
913/// the character to display and its foreground and background colors.
914/// Colors are specified as RGB values packed into 32-bit integers.
915///
916/// # Color Format
917/// Colors use the format 0xRRGGBB where:
918/// - RR: Red component
919/// - GG: Green component  
920/// - BB: Blue component
921#[derive(Debug, Copy, Clone)]
922pub struct CellData<'a> {
923    symbol: &'a str,
924    style_bits: u16,
925    fg: u32,
926    bg: u32,
927}
928
929impl<'a> CellData<'a> {
930    /// Creates new cell data with the specified character and colors.
931    ///
932    /// # Parameters
933    /// * `symbol` - Character to display (should be a single character)
934    /// * `style` - Font style for the character (e.g. bold, italic)
935    /// * `effect` - Optional glyph effect (e.g. underline, strikethrough)
936    /// * `fg` - Foreground color as RGB value (0xRRGGBB)
937    /// * `bg` - Background color as RGB value (0xRRGGBB)
938    ///
939    /// # Returns
940    /// New `CellData` instance
941    pub fn new(symbol: &'a str, style: FontStyle, effect: GlyphEffect, fg: u32, bg: u32) -> Self {
942        Self::new_with_style_bits(symbol, style.style_mask() | effect as u16, fg, bg)
943    }
944
945    /// Creates new cell data with pre-encoded style bits.
946    ///
947    /// This is a lower-level constructor that accepts pre-encoded style bits rather than
948    /// separate `FontStyle` and `GlyphEffect` parameters. Use this when you have already
949    /// combined the style flags or when working directly with the bit representation.
950    ///
951    /// # Parameters
952    /// * `symbol` - Character to display
953    /// * `style_bits` - Pre-encoded style flags. Must not overlap with base glyph ID bits (0x01FF).
954    ///   Valid bits include:
955    ///   - `0x0200` - Bold
956    ///   - `0x0400` - Italic
957    ///   - `0x0800` - Emoji (set automatically by the renderer for emoji glyphs)
958    ///   - `0x1000` - Underline
959    ///   - `0x2000` - Strikethrough
960    /// * `fg` - Foreground color as RGB value (0xRRGGBB)
961    /// * `bg` - Background color as RGB value (0xRRGGBB)
962    ///
963    /// # Returns
964    /// New `CellData` instance
965    ///
966    /// # Panics
967    /// Debug builds will panic if `style_bits` contains any invalid bits.
968    pub fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
969        // emoji and glyph base mask should not intersect with style bits
970        debug_assert!(
971            0x81FF & style_bits == 0,
972            "Invalid style bits: {style_bits:#04x}"
973        );
974        Self { symbol, style_bits, fg, bg }
975    }
976}
977
978/// Static instance data for terminal cell positioning.
979///
980/// `CellStatic` represents the unchanging positional data for each terminal cell
981/// in the grid. This data is uploaded once during initialization and remains
982/// constant throughout the lifetime of the terminal grid. Each instance
983/// corresponds to one cell position in the terminal grid.
984///
985/// # Memory Layout
986/// This struct uses `#[repr(C, align(4))]` to ensure:
987/// - C-compatible memory layout for GPU buffer uploads
988/// - 4-byte alignment for efficient GPU access
989/// - Predictable field ordering (grid_xy at offset 0)
990///
991/// # GPU Usage
992/// This data is used as per-instance vertex attributes in the vertex shader,
993/// allowing the same cell geometry to be rendered at different grid positions
994/// using instanced drawing.
995///
996/// # Buffer Upload
997/// Uploaded to GPU using `GL::STATIC_DRAW` since positions don't change.
998#[repr(C, align(4))]
999struct CellStatic {
1000    /// Grid position as (x, y) coordinates in cell units.
1001    pub grid_xy: [u16; 2],
1002}
1003
1004/// Dynamic instance data for terminal cell appearance.
1005///
1006/// `CellDynamic` contains the frequently-changing visual data for each terminal
1007/// cell, including the character glyph and colors. This data is updated whenever
1008/// cell content changes and is efficiently uploaded to the GPU using dynamic
1009/// buffer updates.
1010///
1011/// # Memory Layout
1012/// The 8-byte data array is packed as follows:
1013/// - Bytes 0-1: Glyph depth/layer index (u16, little-endian)
1014/// - Bytes 2-4: Foreground color RGB (3 bytes)
1015/// - Bytes 5-7: Background color RGB (3 bytes)
1016///
1017/// This compact layout minimizes GPU memory usage and allows efficient
1018/// instanced rendering of the entire terminal grid.
1019///
1020/// # Color Format
1021/// Colors are stored as RGB bytes (no alpha channel in the instance data).
1022/// The alpha channel is handled separately in the shader based on glyph
1023/// transparency from the texture atlas.
1024///
1025/// # GPU Usage
1026/// Uploaded as instance attributes and accessed in both vertex and fragment
1027/// shaders for character selection and color application.
1028///
1029/// # Buffer Upload
1030/// Uploaded to GPU using `GL::DYNAMIC_DRAW` for efficient updates.
1031#[derive(Debug, Clone, Copy, Hash)]
1032#[repr(C, align(4))]
1033pub struct CellDynamic {
1034    /// Packed cell data:
1035    ///
1036    /// # Byte Layout
1037    /// - `data[0]`: Lower 8 bits of glyph depth/layer index
1038    /// - `data[1]`: Upper 8 bits of glyph depth/layer index  
1039    /// - `data[2]`: Foreground red component (0-255)
1040    /// - `data[3]`: Foreground green component (0-255)
1041    /// - `data[4]`: Foreground blue component (0-255)
1042    /// - `data[5]`: Background red component (0-255)
1043    /// - `data[6]`: Background green component (0-255)
1044    /// - `data[7]`: Background blue component (0-255)
1045    data: [u8; 8], // 2b layer, fg:rgb, bg:rgb
1046}
1047
1048impl CellStatic {
1049    fn create_grid(cols: i32, rows: i32) -> Vec<Self> {
1050        debug_assert!(cols > 0 && cols < u16::MAX as i32, "cols: {cols}");
1051        debug_assert!(rows > 0 && rows < u16::MAX as i32, "rows: {rows}");
1052
1053        (0..rows)
1054            .flat_map(|row| (0..cols).map(move |col| (col, row)))
1055            .map(|(col, row)| Self { grid_xy: [col as u16, row as u16] })
1056            .collect()
1057    }
1058}
1059
1060impl CellDynamic {
1061    const GLYPH_STYLE_MASK: u16 =
1062        Glyph::BOLD_FLAG | Glyph::ITALIC_FLAG | Glyph::UNDERLINE_FLAG | Glyph::STRIKETHROUGH_FLAG;
1063
1064    #[inline]
1065    pub fn new(glyph_id: u16, fg: u32, bg: u32) -> Self {
1066        let mut data = [0; 8];
1067
1068        // pack glyph ID into the first two bytes
1069        let glyph_id = glyph_id.to_le_bytes();
1070        data[0] = glyph_id[0];
1071        data[1] = glyph_id[1];
1072
1073        let fg = fg.to_le_bytes();
1074        data[2] = fg[2]; // R
1075        data[3] = fg[1]; // G
1076        data[4] = fg[0]; // B
1077
1078        let bg = bg.to_le_bytes();
1079        data[5] = bg[2]; // R
1080        data[6] = bg[1]; // G
1081        data[7] = bg[0]; // B
1082
1083        Self { data }
1084    }
1085
1086    /// Overwrites the current cell style bits with the provided style bits.
1087    pub fn style(&mut self, style_bits: u16) {
1088        let glyph_id = (self.glyph_id() & !Self::GLYPH_STYLE_MASK) | style_bits;
1089        self.data[..2].copy_from_slice(&glyph_id.to_le_bytes());
1090    }
1091
1092    /// Sets the foreground color of the cell.
1093    pub fn flip_colors(&mut self) {
1094        // swap foreground and background colors
1095        let fg = [self.data[2], self.data[3], self.data[4]];
1096        self.data[2] = self.data[5]; // R
1097        self.data[3] = self.data[6]; // G
1098        self.data[4] = self.data[7]; // B
1099        self.data[5] = fg[0]; // R
1100        self.data[6] = fg[1]; // G
1101        self.data[7] = fg[2]; // B
1102    }
1103
1104    /// Sets the foreground color of the cell.
1105    pub fn fg_color(&mut self, fg: u32) {
1106        let fg = fg.to_le_bytes();
1107        self.data[2] = fg[2]; // R
1108        self.data[3] = fg[1]; // G
1109        self.data[4] = fg[0]; // B
1110    }
1111
1112    /// Sets the background color of the cell.
1113    pub fn bg_color(&mut self, bg: u32) {
1114        let bg = bg.to_le_bytes();
1115        self.data[5] = bg[2]; // R
1116        self.data[6] = bg[1]; // G
1117        self.data[7] = bg[0]; // B
1118    }
1119
1120    /// Returns foreground color as a packed RGB value.
1121    pub fn get_fg_color(&self) -> u32 {
1122        // unpack foreground color from data
1123        ((self.data[2] as u32) << 16) | ((self.data[3] as u32) << 8) | (self.data[4] as u32)
1124    }
1125
1126    /// Returns background color as a packed RGB value.
1127    pub fn get_bg_color(&self) -> u32 {
1128        // unpack background color from data
1129        ((self.data[5] as u32) << 16) | ((self.data[6] as u32) << 8) | (self.data[7] as u32)
1130    }
1131
1132    /// Returns the style bits for this cell, excluding id and emoji bits.
1133    pub fn get_style(&self) -> u16 {
1134        self.glyph_id() & Self::GLYPH_STYLE_MASK
1135    }
1136
1137    /// Returns true if the glyph is an emoji.
1138    pub fn is_emoji(&self) -> bool {
1139        self.glyph_id() & Glyph::EMOJI_FLAG != 0
1140    }
1141
1142    #[inline]
1143    fn glyph_id(&self) -> u16 {
1144        u16::from_le_bytes([self.data[0], self.data[1]])
1145    }
1146
1147    fn set_glyph_id(&mut self, glyph_id: u16) {
1148        let bytes = glyph_id.to_le_bytes();
1149        self.data[0] = bytes[0];
1150        self.data[1] = bytes[1];
1151    }
1152}
1153
1154#[repr(C, align(16))] // std140 layout requires proper alignment
1155struct CellVertexUbo {
1156    pub projection: [f32; 16], // mat4
1157    pub cell_size: [f32; 2],   // vec2 - screen cell size
1158    pub _padding: [f32; 2],
1159}
1160
1161#[repr(C, align(16))] // std140 layout requires proper alignment
1162struct CellFragmentUbo {
1163    pub padding_frac: [f32; 2],       // padding as a fraction of cell size
1164    pub underline_pos: f32,           // underline position (0.0 = top, 1.0 = bottom)
1165    pub underline_thickness: f32,     // underline thickness as fraction of cell height
1166    pub strikethrough_pos: f32,       // strikethrough position (0.0 = top, 1.0 = bottom)
1167    pub strikethrough_thickness: f32, // strikethrough thickness as fraction of cell height
1168    pub texture_lookup_mask: u32,     // static atlas: 0x1FFF, dynamic atlas: 0x0FFF
1169    pub _padding: f32,
1170}
1171
1172impl CellVertexUbo {
1173    pub const BINDING_POINT: u32 = 0;
1174
1175    fn new(canvas_size: (i32, i32), cell_size: (i32, i32)) -> Self {
1176        let projection =
1177            Mat4::orthographic_from_size(canvas_size.0 as f32, canvas_size.1 as f32).data;
1178        Self {
1179            projection,
1180            cell_size: [cell_size.0 as f32, cell_size.1 as f32],
1181            _padding: [0.0; 2], // padding to ensure proper alignment
1182        }
1183    }
1184}
1185
1186impl CellFragmentUbo {
1187    pub const BINDING_POINT: u32 = 1;
1188
1189    fn new(atlas: &FontAtlas) -> Self {
1190        // Use texture cell size for padding calculation (physical pixels in texture)
1191        let texture_cell_size = atlas.texture_cell_size();
1192        let underline = atlas.underline();
1193        let strikethrough = atlas.strikethrough();
1194
1195        Self {
1196            padding_frac: [
1197                FontAtlasData::PADDING as f32 / texture_cell_size.0 as f32,
1198                FontAtlasData::PADDING as f32 / texture_cell_size.1 as f32,
1199            ],
1200            underline_pos: underline.position,
1201            underline_thickness: underline.thickness,
1202            strikethrough_pos: strikethrough.position,
1203            strikethrough_thickness: strikethrough.thickness,
1204            texture_lookup_mask: atlas.base_lookup_mask(),
1205            _padding: 0.0, // padding to ensure proper alignment
1206        }
1207    }
1208}
1209
1210fn create_terminal_cell_data(cols: i32, rows: i32, fill_glyph: u16) -> Vec<CellDynamic> {
1211    (0..cols * rows)
1212        .map(|i| CellDynamic::new(fill_glyph, 0x00ff_ffff, 0x0000_0000))
1213        .collect()
1214}
1215
1216mod attrib {
1217    pub const POS: u32 = 0;
1218    pub const UV: u32 = 1;
1219
1220    pub const GRID_XY: u32 = 2;
1221    pub const PACKED_DEPTH_FG_BG: u32 = 3;
1222}