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