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