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