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::{console, WebGl2RenderingContext};
6
7use crate::{
8    error::Error,
9    gl::{
10        buffer_upload_array, selection::SelectionTracker, ubo::UniformBufferObject, CellIterator,
11        Drawable, FontAtlas, RenderContext, ShaderProgram, GL,
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        cells
288            .map(|(x, y, cell)| (w * y as usize + x as usize, cell))
289            .filter(|(idx, _)| *idx < cell_count)
290            .for_each(|(idx, cell)| {
291                if skip_idx.take() == Some(idx) {
292                    // skip this cell, already handled as part of previous double-width emoji
293                    return;
294                }
295
296                let base_glyph_id = atlas
297                    .get_base_glyph_id(cell.symbol)
298                    .unwrap_or(fallback_glyph);
299
300                if base_glyph_id & Glyph::EMOJI_FLAG != 0 {
301                    let glyph_id = base_glyph_id;
302
303                    // render left half in current cell
304                    self.cells[idx] = CellDynamic::new(glyph_id, cell.fg, cell.bg);
305
306                    // render right half in next cell, if within bounds
307                    if let Some(c) = self.cells.get_mut(idx + 1) {
308                        *c = CellDynamic::new(glyph_id + 1, cell.fg, cell.bg);
309                        skip_idx = Some(idx + 1);
310                    }
311                } else {
312                    let glyph_id = base_glyph_id | cell.style_bits;
313                    self.cells[idx] = CellDynamic::new(glyph_id, cell.fg, cell.bg);
314                }
315            });
316
317        self.cells_pending_flush = true;
318
319        Ok(())
320    }
321
322    pub(crate) fn update_cell(&mut self, x: u16, y: u16, cell_data: CellData) {
323        let (cols, _) = self.terminal_size;
324        let idx = y as usize * cols as usize + x as usize;
325        self.update_cell_by_index(idx, cell_data);
326
327        self.cells_pending_flush = true;
328    }
329
330    pub(crate) fn update_cell_by_index(&mut self, idx: usize, cell_data: CellData) {
331        if idx >= self.cells.len() {
332            return;
333        }
334
335        let atlas = &self.atlas;
336        let fallback_glyph = self.fallback_glyph;
337        let base_glyph_id = atlas
338            .get_base_glyph_id(cell_data.symbol)
339            .unwrap_or(fallback_glyph);
340
341        if base_glyph_id & Glyph::EMOJI_FLAG != 0 {
342            // Emoji: don't apply style bits
343            let glyph_id = base_glyph_id;
344            self.cells[idx] = CellDynamic::new(glyph_id, cell_data.fg, cell_data.bg);
345            if let Some(c) = self.cells.get_mut(idx + 1) {
346                *c = CellDynamic::new(glyph_id + 1, cell_data.fg, cell_data.bg);
347            }
348        } else {
349            // Normal glyph: apply style bits
350            let glyph_id = base_glyph_id | cell_data.style_bits;
351            self.cells[idx] = CellDynamic::new(glyph_id, cell_data.fg, cell_data.bg);
352        }
353
354        self.cells_pending_flush = true;
355    }
356
357    /// Flushes pending cell updates to the GPU.
358    pub(crate) fn flush_cells(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
359        if !self.cells_pending_flush {
360            return Ok(()); // no pending updates to flush
361        }
362
363        // If there's an active selection, flip the colors of the selected cells.
364        // This ensures that the selected cells are rendered with inverted colors
365        // during the GPU upload process.
366        self.flip_selected_cell_colors();
367
368        self.buffers.upload_instance_data(gl, &self.cells);
369
370        // Restore the original colors of the selected cells after the upload.
371        // This ensures that the internal state of the cells remains consistent.
372        self.flip_selected_cell_colors();
373
374        self.cells_pending_flush = false;
375        Ok(())
376    }
377
378    fn flip_selected_cell_colors(&mut self) {
379        if let Some(iter) = self.selected_cells_iter() {
380            iter.for_each(|(idx, _)| self.cells[idx].flip_colors());
381        }
382    }
383
384    fn selected_cells_iter(&self) -> Option<CellIterator> {
385        self.selection
386            .get_query()
387            .and_then(|query| query.range())
388            .map(|(start, end)| self.cell_iter(start, end, self.selection.mode()))
389    }
390
391    fn flip_cell_colors(&mut self, x: u16, y: u16) {
392        let (cols, _) = self.terminal_size;
393        let idx = y as usize * cols as usize + x as usize;
394        if idx < self.cells.len() {
395            self.cells[idx].flip_colors();
396        }
397    }
398
399    /// Resizes the terminal grid to fit the new canvas dimensions.
400    ///
401    /// This method recalculates the terminal dimensions based on the canvas size and cell
402    /// dimensions, then recreates the necessary GPU buffers if the grid size changed.
403    /// Existing cell content is preserved where possible during resizing.
404    ///
405    /// # Parameters
406    ///
407    /// * `gl` - WebGL2 rendering context
408    /// * `canvas_size` - New canvas dimensions in pixels as `(width, height)`
409    ///
410    /// # Returns
411    ///
412    /// * `Ok(())` - Successfully resized the terminal
413    /// * `Err(Error)` - Failed to recreate buffers or other WebGL error
414    pub fn resize(
415        &mut self,
416        gl: &WebGl2RenderingContext,
417        canvas_size: (i32, i32),
418    ) -> Result<(), Error> {
419        self.canvas_size_px = canvas_size;
420
421        // update the UBO with new screen size
422        self.upload_ubo_data(gl);
423
424        let cell_size = self.atlas.cell_size();
425        let cols = canvas_size.0 / cell_size.0;
426        let rows = canvas_size.1 / cell_size.1;
427        if self.terminal_size == (cols as u16, rows as u16) {
428            return Ok(()); // no change in terminal size
429        }
430
431        // update buffers; bind VAO to ensure correct state
432        gl.bind_vertex_array(Some(&self.buffers.vao));
433
434        // delete old cell instance buffers
435        gl.delete_buffer(Some(&self.buffers.instance_cell));
436        gl.delete_buffer(Some(&self.buffers.instance_pos));
437
438        // resize cell data vector
439        let current_size = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
440        let cell_data = resize_cell_grid(&self.cells, current_size, (cols, rows));
441        self.cells = cell_data;
442
443        let cell_pos = CellStatic::create_grid(cols, rows);
444
445        // re-create buffers with new data
446        self.buffers.instance_cell = create_dynamic_instance_buffer(gl, &self.cells)?;
447        self.buffers.instance_pos = create_static_instance_buffer(gl, &cell_pos)?;
448
449        // unbind VAO
450        gl.bind_vertex_array(None);
451
452        self.terminal_size = (cols as u16, rows as u16);
453
454        Ok(())
455    }
456
457    /// Returns the base glyph identifier for a given symbol.
458    pub fn base_glyph_id(&self, symbol: &str) -> Option<u16> {
459        self.atlas.get_base_glyph_id(symbol)
460    }
461
462    fn fallback_symbol(&self) -> Cow<'_, str> {
463        self.atlas
464            .get_symbol(self.fallback_glyph)
465            .unwrap_or(Cow::Borrowed(" "))
466    }
467
468    fn fill_glyphs(atlas: &FontAtlas) -> Vec<u16> {
469        [
470            ("🤫", FontStyle::Normal),
471            ("🙌", FontStyle::Normal),
472            ("n", FontStyle::Normal),
473            ("o", FontStyle::Normal),
474            ("r", FontStyle::Normal),
475            ("m", FontStyle::Normal),
476            ("a", FontStyle::Normal),
477            ("l", FontStyle::Normal),
478            ("b", FontStyle::Bold),
479            ("o", FontStyle::Bold),
480            ("l", FontStyle::Bold),
481            ("d", FontStyle::Bold),
482            ("i", FontStyle::Italic),
483            ("t", FontStyle::Italic),
484            ("a", FontStyle::Italic),
485            ("l", FontStyle::Italic),
486            ("i", FontStyle::Italic),
487            ("c", FontStyle::Italic),
488            ("b", FontStyle::BoldItalic),
489            ("-", FontStyle::BoldItalic),
490            ("i", FontStyle::BoldItalic),
491            ("t", FontStyle::BoldItalic),
492            ("a", FontStyle::BoldItalic),
493            ("l", FontStyle::BoldItalic),
494            ("i", FontStyle::BoldItalic),
495            ("c", FontStyle::BoldItalic),
496            ("🤪", FontStyle::Normal),
497            ("🤩", FontStyle::Normal),
498        ]
499        .into_iter()
500        .map(|(symbol, style)| {
501            atlas
502                .get_base_glyph_id(symbol)
503                .map(|g| g | style as u16)
504        })
505        .map(|g| g.unwrap_or(' ' as u16))
506        .collect()
507    }
508}
509
510fn resize_cell_grid(
511    cells: &[CellDynamic],
512    old_size: (i32, i32),
513    new_size: (i32, i32),
514) -> Vec<CellDynamic> {
515    let new_len = new_size.0 * new_size.1;
516
517    let mut new_cells = Vec::with_capacity(new_len as usize);
518    for _ in 0..new_len {
519        new_cells.push(CellDynamic::new(' ' as u16, 0xFFFFFF, 0x000000));
520    }
521
522    for y in 0..min(old_size.1, new_size.1) {
523        for x in 0..min(old_size.0, new_size.0) {
524            let new_idx = (y * new_size.0 + x) as usize;
525            let old_idx = (y * old_size.0 + x) as usize;
526            new_cells[new_idx] = cells[old_idx];
527        }
528    }
529
530    new_cells
531}
532
533fn create_vao(gl: &WebGl2RenderingContext) -> Result<web_sys::WebGlVertexArrayObject, Error> {
534    gl.create_vertex_array()
535        .ok_or(Error::vertex_array_creation_failed())
536}
537
538fn setup_buffers(
539    gl: &WebGl2RenderingContext,
540    vao: web_sys::WebGlVertexArrayObject,
541    cell_pos: &[CellStatic],
542    cell_data: &[CellDynamic],
543    cell_size: (i32, i32),
544) -> Result<TerminalBuffers, Error> {
545    let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
546
547    // let overlap = 0.5;
548    let overlap = 0.0; // no overlap for now, can be adjusted later
549    #[rustfmt::skip]
550    let vertices = [
551        //    x            y       u    v
552        w + overlap,    -overlap, 1.0, 0.0, // top-right
553           -overlap, h + overlap, 0.0, 1.0, // bottom-left
554        w + overlap, h + overlap, 1.0, 1.0, // bottom-right
555           -overlap,    -overlap, 0.0, 0.0  // top-left
556    ];
557    let indices = [0, 1, 2, 0, 3, 1];
558
559    Ok(TerminalBuffers {
560        vao,
561        vertices: create_buffer_f32(gl, GL::ARRAY_BUFFER, &vertices, GL::STATIC_DRAW)?,
562        instance_pos: create_static_instance_buffer(gl, cell_pos)?,
563        instance_cell: create_dynamic_instance_buffer(gl, cell_data)?,
564        indices: create_buffer_u8(gl, GL::ELEMENT_ARRAY_BUFFER, &indices, GL::STATIC_DRAW)?,
565    })
566}
567
568fn create_buffer_u8(
569    gl: &WebGl2RenderingContext,
570    target: u32,
571    data: &[u8],
572    usage: u32,
573) -> Result<web_sys::WebGlBuffer, Error> {
574    let index_buf = gl
575        .create_buffer()
576        .ok_or(Error::buffer_creation_failed("vbo-u8"))?;
577    gl.bind_buffer(target, Some(&index_buf));
578
579    gl.buffer_data_with_u8_array(target, data, usage);
580
581    Ok(index_buf)
582}
583
584fn create_buffer_f32(
585    gl: &WebGl2RenderingContext,
586    target: u32,
587    data: &[f32],
588    usage: u32,
589) -> Result<web_sys::WebGlBuffer, Error> {
590    let buffer = gl
591        .create_buffer()
592        .ok_or(Error::buffer_creation_failed("vbo-f32"))?;
593
594    gl.bind_buffer(target, Some(&buffer));
595
596    unsafe {
597        let view = js_sys::Float32Array::view(data);
598        gl.buffer_data_with_array_buffer_view(target, &view, usage);
599    }
600
601    // vertex attributes \\
602    const STRIDE: i32 = (2 + 2) * 4; // 4 floats per vertex
603    enable_vertex_attrib(gl, attrib::POS, 2, GL::FLOAT, 0, STRIDE);
604    enable_vertex_attrib(gl, attrib::UV, 2, GL::FLOAT, 8, STRIDE);
605
606    Ok(buffer)
607}
608
609fn create_static_instance_buffer(
610    gl: &WebGl2RenderingContext,
611    instance_data: &[CellStatic],
612) -> Result<web_sys::WebGlBuffer, Error> {
613    let instance_buf = gl
614        .create_buffer()
615        .ok_or(Error::buffer_creation_failed("static-instance-buffer"))?;
616
617    gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
618    buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::STATIC_DRAW);
619
620    let stride = size_of::<CellStatic>() as i32;
621    enable_vertex_attrib_array(gl, attrib::GRID_XY, 2, GL::UNSIGNED_SHORT, 0, stride);
622
623    Ok(instance_buf)
624}
625
626fn create_dynamic_instance_buffer(
627    gl: &WebGl2RenderingContext,
628    instance_data: &[CellDynamic],
629) -> Result<web_sys::WebGlBuffer, Error> {
630    let instance_buf = gl
631        .create_buffer()
632        .ok_or(Error::buffer_creation_failed("dynamic-instance-buffer"))?;
633
634    gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
635    buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::DYNAMIC_DRAW);
636
637    let stride = size_of::<CellDynamic>() as i32;
638
639    // setup instance attributes (while VAO is bound)
640    enable_vertex_attrib_array(
641        gl,
642        attrib::PACKED_DEPTH_FG_BG,
643        2,
644        GL::UNSIGNED_INT,
645        0,
646        stride,
647    );
648
649    Ok(instance_buf)
650}
651
652fn enable_vertex_attrib_array(
653    gl: &WebGl2RenderingContext,
654    index: u32,
655    size: i32,
656    type_: u32,
657    offset: i32,
658    stride: i32,
659) {
660    enable_vertex_attrib(gl, index, size, type_, offset, stride);
661    gl.vertex_attrib_divisor(index, 1);
662}
663
664fn enable_vertex_attrib(
665    gl: &WebGl2RenderingContext,
666    index: u32,
667    size: i32,
668    type_: u32,
669    offset: i32,
670    stride: i32,
671) {
672    gl.enable_vertex_attrib_array(index);
673    if type_ == GL::FLOAT {
674        gl.vertex_attrib_pointer_with_i32(index, size, type_, false, stride, offset);
675    } else {
676        gl.vertex_attrib_i_pointer_with_i32(index, size, type_, stride, offset);
677    }
678}
679
680impl Drawable for TerminalGrid {
681    fn prepare(&self, context: &mut RenderContext) {
682        let gl = context.gl;
683
684        self.shader.use_program(gl);
685
686        gl.bind_vertex_array(Some(&self.buffers.vao));
687
688        self.atlas.bind(gl, 0);
689        self.ubo_vertex.bind(context.gl);
690        self.ubo_fragment.bind(context.gl);
691        gl.uniform1i(Some(&self.sampler_loc), 0);
692    }
693
694    fn draw(&self, context: &mut RenderContext) {
695        let gl = context.gl;
696        let cell_count = self.cells.len() as i32;
697        gl.draw_elements_instanced_with_i32(GL::TRIANGLES, 6, GL::UNSIGNED_BYTE, 0, cell_count);
698    }
699
700    fn cleanup(&self, context: &mut RenderContext) {
701        let gl = context.gl;
702        gl.bind_vertex_array(None);
703        gl.bind_texture(GL::TEXTURE_2D_ARRAY, None);
704
705        self.ubo_vertex.unbind(gl);
706        self.ubo_fragment.unbind(gl);
707    }
708}
709
710/// Data for a single terminal cell including character and colors.
711///
712/// `CellData` represents the visual content of one terminal cell, including
713/// the character to display and its foreground and background colors.
714/// Colors are specified as RGB values packed into 32-bit integers.
715///
716/// # Color Format
717/// Colors use the format 0xRRGGBB where:
718/// - RR: Red component
719/// - GG: Green component  
720/// - BB: Blue component
721#[derive(Debug, Copy, Clone)]
722pub struct CellData<'a> {
723    symbol: &'a str,
724    style_bits: u16,
725    fg: u32,
726    bg: u32,
727}
728
729impl<'a> CellData<'a> {
730    /// Creates new cell data with the specified character and colors.
731    ///
732    /// # Parameters
733    /// * `symbol` - Character to display (should be a single character)
734    /// * `style` - Font style for the character (e.g. bold, italic)
735    /// * `effect` - Optional glyph effect (e.g. underline, strikethrough)
736    /// * `fg` - Foreground color as RGB value (0xRRGGBB)
737    /// * `bg` - Background color as RGB value (0xRRGGBB)
738    ///
739    /// # Returns
740    /// New `CellData` instance
741    pub fn new(symbol: &'a str, style: FontStyle, effect: GlyphEffect, fg: u32, bg: u32) -> Self {
742        Self::new_with_style_bits(symbol, style.style_mask() | effect as u16, fg, bg)
743    }
744
745    /// Creates new cell data with pre-encoded style bits.
746    ///
747    /// This is a lower-level constructor that accepts pre-encoded style bits rather than
748    /// separate `FontStyle` and `GlyphEffect` parameters. Use this when you have already
749    /// combined the style flags or when working directly with the bit representation.
750    ///
751    /// # Parameters
752    /// * `symbol` - Character to display
753    /// * `style_bits` - Pre-encoded style flags. Must not overlap with base glyph ID bits (0x01FF).
754    ///   Valid bits include:
755    ///   - `0x0200` - Bold
756    ///   - `0x0400` - Italic
757    ///   - `0x0800` - Emoji (set automatically by the renderer for emoji glyphs)
758    ///   - `0x1000` - Underline
759    ///   - `0x2000` - Strikethrough
760    /// * `fg` - Foreground color as RGB value (0xRRGGBB)
761    /// * `bg` - Background color as RGB value (0xRRGGBB)
762    ///
763    /// # Returns
764    /// New `CellData` instance
765    ///
766    /// # Panics
767    /// Debug builds will panic if `style_bits` contains any invalid bits.
768    pub fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
769        // emoji and glyph base mask should not intersect with style bits
770        debug_assert!(
771            0x81FF & style_bits == 0,
772            "Invalid style bits: {style_bits:#04x}"
773        );
774        Self { symbol, style_bits, fg, bg }
775    }
776}
777
778/// Static instance data for terminal cell positioning.
779///
780/// `CellStatic` represents the unchanging positional data for each terminal cell
781/// in the grid. This data is uploaded once during initialization and remains
782/// constant throughout the lifetime of the terminal grid. Each instance
783/// corresponds to one cell position in the terminal grid.
784///
785/// # Memory Layout
786/// This struct uses `#[repr(C, align(4))]` to ensure:
787/// - C-compatible memory layout for GPU buffer uploads
788/// - 4-byte alignment for efficient GPU access
789/// - Predictable field ordering (grid_xy at offset 0)
790///
791/// # GPU Usage
792/// This data is used as per-instance vertex attributes in the vertex shader,
793/// allowing the same cell geometry to be rendered at different grid positions
794/// using instanced drawing.
795///
796/// # Buffer Upload
797/// Uploaded to GPU using `GL::STATIC_DRAW` since positions don't change.
798#[repr(C, align(4))]
799struct CellStatic {
800    /// Grid position as (x, y) coordinates in cell units.
801    pub grid_xy: [u16; 2],
802}
803
804/// Dynamic instance data for terminal cell appearance.
805///
806/// `CellDynamic` contains the frequently-changing visual data for each terminal
807/// cell, including the character glyph and colors. This data is updated whenever
808/// cell content changes and is efficiently uploaded to the GPU using dynamic
809/// buffer updates.
810///
811/// # Memory Layout
812/// The 8-byte data array is packed as follows:
813/// - Bytes 0-1: Glyph depth/layer index (u16, little-endian)
814/// - Bytes 2-4: Foreground color RGB (3 bytes)
815/// - Bytes 5-7: Background color RGB (3 bytes)
816///
817/// This compact layout minimizes GPU memory usage and allows efficient
818/// instanced rendering of the entire terminal grid.
819///
820/// # Color Format
821/// Colors are stored as RGB bytes (no alpha channel in the instance data).
822/// The alpha channel is handled separately in the shader based on glyph
823/// transparency from the texture atlas.
824///
825/// # GPU Usage
826/// Uploaded as instance attributes and accessed in both vertex and fragment
827/// shaders for character selection and color application.
828///
829/// # Buffer Upload
830/// Uploaded to GPU using `GL::DYNAMIC_DRAW` for efficient updates.
831#[derive(Debug, Clone, Copy)]
832#[repr(C, align(4))]
833pub struct CellDynamic {
834    /// Packed cell data:
835    ///
836    /// # Byte Layout
837    /// - `data[0]`: Lower 8 bits of glyph depth/layer index
838    /// - `data[1]`: Upper 8 bits of glyph depth/layer index  
839    /// - `data[2]`: Foreground red component (0-255)
840    /// - `data[3]`: Foreground green component (0-255)
841    /// - `data[4]`: Foreground blue component (0-255)
842    /// - `data[5]`: Background red component (0-255)
843    /// - `data[6]`: Background green component (0-255)
844    /// - `data[7]`: Background blue component (0-255)
845    data: [u8; 8], // 2b layer, fg:rgb, bg:rgb
846}
847
848impl CellStatic {
849    fn create_grid(cols: i32, rows: i32) -> Vec<Self> {
850        debug_assert!(cols > 0 && cols < u16::MAX as i32, "cols: {cols}");
851        debug_assert!(rows > 0 && rows < u16::MAX as i32, "rows: {rows}");
852
853        (0..rows)
854            .flat_map(|row| (0..cols).map(move |col| (col, row)))
855            .map(|(col, row)| Self { grid_xy: [col as u16, row as u16] })
856            .collect()
857    }
858}
859
860impl CellDynamic {
861    const GLYPH_STYLE_MASK: u16 =
862        Glyph::BOLD_FLAG | Glyph::ITALIC_FLAG | Glyph::UNDERLINE_FLAG | Glyph::STRIKETHROUGH_FLAG;
863
864    #[inline]
865    pub fn new(glyph_id: u16, fg: u32, bg: u32) -> Self {
866        let mut data = [0; 8];
867
868        // pack glyph ID into the first two bytes
869        let glyph_id = glyph_id.to_le_bytes();
870        data[0] = glyph_id[0];
871        data[1] = glyph_id[1];
872
873        let fg = fg.to_le_bytes();
874        data[2] = fg[2]; // R
875        data[3] = fg[1]; // G
876        data[4] = fg[0]; // B
877
878        let bg = bg.to_le_bytes();
879        data[5] = bg[2]; // R
880        data[6] = bg[1]; // G
881        data[7] = bg[0]; // B
882
883        Self { data }
884    }
885
886    /// Overwrites the current cell style bits with the provided style bits.
887    pub fn style(&mut self, style_bits: u16) {
888        let glyph_id = (self.glyph_id() & !Self::GLYPH_STYLE_MASK) | style_bits;
889        self.data[..2].copy_from_slice(&glyph_id.to_le_bytes());
890    }
891
892    /// Sets the foreground color of the cell.
893    pub fn flip_colors(&mut self) {
894        // swap foreground and background colors
895        let fg = [self.data[2], self.data[3], self.data[4]];
896        self.data[2] = self.data[5]; // R
897        self.data[3] = self.data[6]; // G
898        self.data[4] = self.data[7]; // B
899        self.data[5] = fg[0]; // R
900        self.data[6] = fg[1]; // G
901        self.data[7] = fg[2]; // B
902    }
903
904    /// Sets the foreground color of the cell.
905    pub fn fg_color(&mut self, fg: u32) {
906        let fg = fg.to_le_bytes();
907        self.data[2] = fg[2]; // R
908        self.data[3] = fg[1]; // G
909        self.data[4] = fg[0]; // B
910    }
911
912    /// Sets the background color of the cell.
913    pub fn bg_color(&mut self, bg: u32) {
914        let bg = bg.to_le_bytes();
915        self.data[5] = bg[2]; // R
916        self.data[6] = bg[1]; // G
917        self.data[7] = bg[0]; // B
918    }
919
920    /// Returns foreground color as a packed RGB value.
921    pub fn get_fg_color(&self) -> u32 {
922        // unpack foreground color from data
923        ((self.data[2] as u32) << 16) | ((self.data[3] as u32) << 8) | (self.data[4] as u32)
924    }
925
926    /// Returns background color as a packed RGB value.
927    pub fn get_bg_color(&self) -> u32 {
928        // unpack background color from data
929        ((self.data[5] as u32) << 16) | ((self.data[6] as u32) << 8) | (self.data[7] as u32)
930    }
931
932    /// Returns the style bits for this cell, excluding id and emoji bits.
933    pub fn get_style(&self) -> u16 {
934        self.glyph_id() & Self::GLYPH_STYLE_MASK
935    }
936
937    /// Returns true if the glyph is an emoji.
938    pub fn is_emoji(&self) -> bool {
939        self.glyph_id() & Glyph::EMOJI_FLAG != 0
940    }
941
942    #[inline]
943    fn glyph_id(&self) -> u16 {
944        u16::from_le_bytes([self.data[0], self.data[1]])
945    }
946}
947
948#[repr(C, align(16))] // std140 layout requires proper alignment
949struct CellVertexUbo {
950    pub projection: [f32; 16], // mat4
951    pub cell_size: [f32; 2],   // vec2 - screen cell size
952    pub _padding: [f32; 2],
953}
954
955#[repr(C, align(16))] // std140 layout requires proper alignment
956struct CellFragmentUbo {
957    pub padding_frac: [f32; 2],       // padding as a fraction of cell size
958    pub underline_pos: f32,           // underline position (0.0 = top, 1.0 = bottom)
959    pub underline_thickness: f32,     // underline thickness as fraction of cell height
960    pub strikethrough_pos: f32,       // strikethrough position (0.0 = top, 1.0 = bottom)
961    pub strikethrough_thickness: f32, // strikethrough thickness as fraction of cell height
962    pub _padding: [f32; 2],
963}
964
965impl CellVertexUbo {
966    pub const BINDING_POINT: u32 = 0;
967
968    fn new(canvas_size: (i32, i32), cell_size: (i32, i32)) -> Self {
969        let projection =
970            Mat4::orthographic_from_size(canvas_size.0 as f32, canvas_size.1 as f32).data;
971        Self {
972            projection,
973            cell_size: [cell_size.0 as f32, cell_size.1 as f32],
974            _padding: [0.0; 2], // padding to ensure proper alignment
975        }
976    }
977}
978
979impl CellFragmentUbo {
980    pub const BINDING_POINT: u32 = 1;
981
982    fn new(atlas: &FontAtlas) -> Self {
983        let cell_size = atlas.cell_size();
984        let underline = atlas.underline();
985        let strikethrough = atlas.strikethrough();
986        Self {
987            padding_frac: [
988                FontAtlasData::PADDING as f32 / cell_size.0 as f32,
989                FontAtlasData::PADDING as f32 / cell_size.1 as f32,
990            ],
991            underline_pos: underline.position,
992            underline_thickness: underline.thickness,
993            strikethrough_pos: strikethrough.position,
994            strikethrough_thickness: strikethrough.thickness,
995            _padding: [0.0; 2], // padding to ensure proper alignment
996        }
997    }
998}
999
1000fn create_terminal_cell_data(cols: i32, rows: i32, fill_glyph: &[u16]) -> Vec<CellDynamic> {
1001    let glyph_len = fill_glyph.len();
1002    (0..cols * rows)
1003        .map(|i| CellDynamic::new(fill_glyph[i as usize % glyph_len], 0x00ff_ffff, 0x0000_0000))
1004        .collect()
1005}
1006
1007mod attrib {
1008    pub const POS: u32 = 0;
1009    pub const UV: u32 = 1;
1010
1011    pub const GRID_XY: u32 = 2;
1012    pub const PACKED_DEPTH_FG_BG: u32 = 3;
1013}