beamterm_renderer/gl/
terminal_grid.rs

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