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 CursorPosition,
9 error::Error,
10 gl::{
11 CellIterator, CellQuery, Drawable, GL, RenderContext, ShaderProgram, StaticFontAtlas,
12 atlas::{FontAtlas, GlyphSlot},
13 buffer_upload_array,
14 selection::SelectionTracker,
15 ubo::UniformBufferObject,
16 },
17 mat4::Mat4,
18};
19
20#[derive(Debug)]
27pub struct TerminalGrid {
28 gpu: GpuResources,
30 cells: Vec<CellDynamic>,
32 terminal_size: (u16, u16),
34 canvas_size_px: (i32, i32),
36 pixel_ratio: f32,
38 atlas: FontAtlas,
40 fallback_glyph: u16,
42 selection: SelectionTracker,
44 cells_pending_flush: bool,
46}
47
48#[derive(Debug)]
55struct GpuResources {
56 shader: ShaderProgram,
58 buffers: TerminalBuffers,
60 ubo_vertex: UniformBufferObject,
62 ubo_fragment: UniformBufferObject,
64 sampler_loc: web_sys::WebGlUniformLocation,
66}
67
68impl GpuResources {
69 const FRAGMENT_GLSL: &'static str = include_str!("../shaders/cell.frag");
70 const VERTEX_GLSL: &'static str = include_str!("../shaders/cell.vert");
71
72 fn new(
81 gl: &WebGl2RenderingContext,
82 cell_pos: &[CellStatic],
83 cell_data: &[CellDynamic],
84 cell_size: (i32, i32),
85 ) -> Result<Self, Error> {
86 let vao = create_vao(gl)?;
88 gl.bind_vertex_array(Some(&vao));
89
90 let buffers = setup_buffers(gl, vao, cell_pos, cell_data, cell_size)?;
92
93 gl.bind_vertex_array(None);
95
96 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 Ok(Self {
110 shader,
111 buffers,
112 ubo_vertex,
113 ubo_fragment,
114 sampler_loc,
115 })
116 }
117}
118
119#[derive(Debug)]
120struct TerminalBuffers {
121 vao: web_sys::WebGlVertexArrayObject,
122 vertices: web_sys::WebGlBuffer,
123 instance_pos: web_sys::WebGlBuffer,
124 instance_cell: web_sys::WebGlBuffer,
125 indices: web_sys::WebGlBuffer,
126}
127
128impl TerminalBuffers {
129 fn upload_instance_data<T>(&self, gl: &WebGl2RenderingContext, cell_data: &[T]) {
130 gl.bind_vertex_array(Some(&self.vao));
131 gl.bind_buffer(GL::ARRAY_BUFFER, Some(&self.instance_cell));
132
133 buffer_upload_array(gl, GL::ARRAY_BUFFER, cell_data, GL::DYNAMIC_DRAW);
134
135 gl.bind_vertex_array(None);
136 }
137
138 fn update_vertex_buffer(&self, gl: &WebGl2RenderingContext, cell_size: (i32, i32)) {
140 let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
141
142 #[rustfmt::skip]
143 let vertices: [f32; 16] = [
144 w, 0.0, 1.0, 0.0, 0.0, h, 0.0, 1.0, w, h, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0 ];
150
151 gl.bind_vertex_array(Some(&self.vao));
152 gl.bind_buffer(GL::ARRAY_BUFFER, Some(&self.vertices));
153
154 unsafe {
155 let view = js_sys::Float32Array::view(&vertices);
156 gl.buffer_sub_data_with_i32_and_array_buffer_view(GL::ARRAY_BUFFER, 0, &view);
157 }
158
159 gl.bind_vertex_array(None);
160 }
161}
162
163impl TerminalGrid {
164 pub(crate) fn new(
165 gl: &WebGl2RenderingContext,
166 atlas: FontAtlas,
167 screen_size: (i32, i32),
168 pixel_ratio: f32,
169 ) -> Result<Self, Error> {
170 let cell_scale = atlas.cell_scale_for_dpr(pixel_ratio);
171 let base_cell_size = atlas.cell_size();
172 let cell_size = (
173 (base_cell_size.0 as f32 * cell_scale).round() as i32,
174 (base_cell_size.1 as f32 * cell_scale).round() as i32,
175 );
176 let (cols, rows) = (screen_size.0 / cell_size.0, screen_size.1 / cell_size.1);
177
178 let space_glyph = atlas.space_glyph_id();
179 let cell_data = create_terminal_cell_data(cols, rows, space_glyph);
180 let cell_pos = CellStatic::create_grid(cols, rows);
181
182 let grid = Self {
183 gpu: GpuResources::new(gl, &cell_pos, &cell_data, cell_size)?,
184 terminal_size: (cols as u16, rows as u16),
185 canvas_size_px: screen_size,
186 pixel_ratio,
187 cells: cell_data,
188 atlas,
189 fallback_glyph: space_glyph,
190 selection: SelectionTracker::new(),
191 cells_pending_flush: false,
192 };
193
194 grid.upload_ubo_data(gl);
195
196 Ok(grid)
197 }
198
199 fn effective_cell_size(&self) -> (i32, i32) {
201 let cell_scale = self.atlas.cell_scale_for_dpr(self.pixel_ratio);
202 let base = self.atlas.cell_size();
203 (
204 (base.0 as f32 * cell_scale).round() as i32,
205 (base.1 as f32 * cell_scale).round() as i32,
206 )
207 }
208
209 pub fn set_fallback_glyph(&mut self, fallback: &str) {
211 self.fallback_glyph = self
212 .atlas
213 .get_glyph_id(fallback, FontStyle::Normal as u16)
214 .unwrap_or(' ' as u16);
215 }
216
217 pub(crate) fn replace_atlas(&mut self, gl: &WebGl2RenderingContext, atlas: FontAtlas) {
230 let glyph_mask = self.atlas.base_lookup_mask() as u16;
231 let style_mask = !glyph_mask;
232
233 self.fallback_glyph = self
235 .atlas
236 .get_symbol(self.fallback_glyph & glyph_mask)
237 .and_then(|symbol| {
238 let style_bits = self.fallback_glyph & style_mask;
239 atlas.resolve_glyph_slot(symbol.as_str(), style_bits)
240 })
241 .map(|slot| slot.slot_id())
242 .unwrap_or(atlas.space_glyph_id());
243
244 let mut skip_next = false;
246 for idx in 0..self.cells.len() {
247 if skip_next {
248 skip_next = false;
249 continue;
250 }
251
252 let old_glyph_id = self.cells[idx].glyph_id();
253 let style_bits = old_glyph_id & style_mask;
254
255 let slot = self
256 .atlas
257 .get_symbol(old_glyph_id & glyph_mask)
258 .and_then(|symbol| atlas.resolve_glyph_slot(symbol.as_str(), style_bits));
259
260 match slot {
261 Some(GlyphSlot::Normal(id)) => {
262 self.cells[idx].set_glyph_id(id);
263 },
264 Some(GlyphSlot::Wide(id)) | Some(GlyphSlot::Emoji(id)) => {
265 self.cells[idx].set_glyph_id(id);
266 if let Some(next_cell) = self.cells.get_mut(idx + 1) {
268 next_cell.set_glyph_id(id + 1);
269 skip_next = true;
270 }
271 },
272 None => {
273 self.cells[idx].set_glyph_id(self.fallback_glyph);
274 },
275 }
276 }
277
278 self.selection.clear();
280
281 let old_atlas = std::mem::replace(&mut self.atlas, atlas);
283 old_atlas.delete(gl);
284 self.cells_pending_flush = true;
285
286 self.gpu
288 .buffers
289 .update_vertex_buffer(gl, self.effective_cell_size());
290
291 let _ = self.resize(gl, self.canvas_size_px, self.pixel_ratio);
292 }
293
294 pub(crate) fn atlas(&self) -> &FontAtlas {
296 &self.atlas
297 }
298
299 pub(crate) fn atlas_mut(&mut self) -> &mut FontAtlas {
301 &mut self.atlas
302 }
303
304 pub(crate) fn canvas_size(&self) -> (i32, i32) {
306 self.canvas_size_px
307 }
308
309 pub fn cell_size(&self) -> (i32, i32) {
311 self.effective_cell_size()
312 }
313
314 pub fn terminal_size(&self) -> (u16, u16) {
316 self.terminal_size
317 }
318
319 pub fn cell_data_mut(&mut self, x: u16, y: u16) -> Option<&mut CellDynamic> {
321 let (cols, _) = self.terminal_size;
322 let idx = y as usize * cols as usize + x as usize;
323 self.cells.get_mut(idx)
324 }
325
326 pub(crate) fn selection_tracker(&self) -> SelectionTracker {
328 self.selection.clone()
329 }
330
331 pub(super) fn get_symbols(&self, selection: CellIterator) -> CompactString {
333 let (cols, rows) = self.terminal_size;
334 let mut text = CompactString::new("");
335
336 for (idx, require_newline_after) in selection {
337 let cell_symbol = self.get_cell_symbol(idx);
338 if cell_symbol.is_some() {
339 text.push_str(&cell_symbol.unwrap_or_default());
340 }
341
342 if require_newline_after {
343 text.push('\n'); }
345 }
346
347 text
348 }
349
350 pub(crate) fn get_ascii_char_at(&self, cursor: CursorPosition) -> Option<char> {
355 let idx = cursor.row as usize * self.terminal_size.0 as usize + cursor.col as usize;
356 if idx < self.cells.len() {
357 let glyph_id = self.cells[idx].glyph_id();
358 self.atlas.get_ascii_char(glyph_id)
359 } else {
360 None
361 }
362 }
363
364 pub(crate) fn hash_cells(&self, selection: CellQuery) -> u64 {
365 use std::hash::{Hash, Hasher};
366
367 use rustc_hash::FxHasher;
368
369 let mut hasher = FxHasher::default();
370 for (idx, _) in self.cell_iter(selection) {
371 self.cells[idx].hash(&mut hasher);
372 }
373
374 hasher.finish()
375 }
376
377 fn get_cell_symbol(&self, idx: usize) -> Option<CompactString> {
378 if idx < self.cells.len() {
379 let glyph_id = self.cells[idx].glyph_id();
380 let cell_symbol = self.atlas.get_symbol(glyph_id);
381 if cell_symbol.is_some() {
382 return cell_symbol;
383 }
384 }
385
386 self.fallback_symbol()
387 }
388
389 fn upload_ubo_data(&self, gl: &WebGl2RenderingContext) {
398 let vertex_ubo = CellVertexUbo::new(self.canvas_size_px, self.effective_cell_size());
399 self.gpu.ubo_vertex.upload_data(gl, &vertex_ubo);
400
401 let fragment_ubo = CellFragmentUbo::new(&self.atlas);
402 self.gpu
403 .ubo_fragment
404 .upload_data(gl, &fragment_ubo);
405 }
406
407 pub fn cell_count(&self) -> usize {
409 self.cells.len()
410 }
411
412 pub fn update_cells<'a>(
426 &mut self,
427 gl: &WebGl2RenderingContext,
428 cells: impl Iterator<Item = CellData<'a>>,
429 ) -> Result<(), Error> {
430 let atlas = &self.atlas;
432
433 let fallback_glyph = GlyphSlot::Normal(self.fallback_glyph);
434
435 let mut pending_cell: Option<CellDynamic> = None;
437 self.cells
438 .iter_mut()
439 .zip(cells)
440 .for_each(|(cell, data)| {
441 let glyph = atlas
442 .resolve_glyph_slot(data.symbol, data.style_bits)
443 .unwrap_or(fallback_glyph);
444
445 *cell = if let Some(second_cell) = pending_cell.take() {
446 second_cell
447 } else {
448 match glyph {
449 GlyphSlot::Normal(id) => CellDynamic::new(id, data.fg, data.bg),
450
451 GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => {
452 pending_cell = Some(CellDynamic::new(id + 1, data.fg, data.bg));
454 CellDynamic::new(id, data.fg, data.bg)
455 },
456 }
457 }
458 });
459
460 self.cells_pending_flush = true;
461 Ok(())
462 }
463
464 pub(crate) fn update_cells_by_position<'a>(
465 &mut self,
466 cells: impl Iterator<Item = (u16, u16, CellData<'a>)>,
467 ) -> Result<(), Error> {
468 let cols = self.terminal_size.0 as usize;
469 let cells_by_index = cells.map(|(x, y, data)| (y as usize * cols + x as usize, data));
470
471 self.update_cells_by_index(cells_by_index)
472 }
473
474 pub(crate) fn update_cells_by_index<'a>(
475 &mut self,
476 cells: impl Iterator<Item = (usize, CellData<'a>)>,
477 ) -> Result<(), Error> {
478 let atlas = &self.atlas;
480
481 let cell_count = self.cells.len();
482 let fallback_glyph = GlyphSlot::Normal(self.fallback_glyph);
483
484 let mut skip_idx = None;
489
490 cells
491 .filter(|(idx, _)| *idx < cell_count)
492 .for_each(|(idx, cell)| {
493 if skip_idx.take() == Some(idx) {
494 return;
496 }
497
498 let glyph = atlas
499 .resolve_glyph_slot(cell.symbol, cell.style_bits)
500 .unwrap_or(fallback_glyph);
501
502 match glyph {
503 GlyphSlot::Normal(id) => {
504 self.cells[idx] = CellDynamic::new(id, cell.fg, cell.bg);
505 },
506
507 GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => {
508 self.cells[idx] = CellDynamic::new(id, cell.fg, cell.bg);
510
511 if let Some(c) = self.cells.get_mut(idx + 1) {
513 *c = CellDynamic::new(id + 1, cell.fg, cell.bg);
514 skip_idx = Some(idx + 1);
515 }
516 },
517 }
518 });
519
520 self.cells_pending_flush = true;
521
522 Ok(())
523 }
524
525 pub(crate) fn update_cell(&mut self, x: u16, y: u16, cell_data: CellData) -> Result<(), Error> {
526 let (cols, _) = self.terminal_size;
527 let idx = y as usize * cols as usize + x as usize;
528 self.update_cell_by_index(idx, cell_data)
529 }
530
531 pub(crate) fn update_cell_by_index(
532 &mut self,
533 idx: usize,
534 cell_data: CellData,
535 ) -> Result<(), Error> {
536 self.update_cells_by_index(std::iter::once((idx, cell_data)))
537 }
538
539 pub(crate) fn flush_cells(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
541 if !self.cells_pending_flush {
542 return Ok(()); }
544
545 self.clear_stale_selection();
548
549 self.flip_selected_cell_colors();
553
554 self.gpu
555 .buffers
556 .upload_instance_data(gl, &self.cells);
557
558 self.flip_selected_cell_colors();
561
562 self.cells_pending_flush = false;
563 Ok(())
564 }
565
566 fn flip_selected_cell_colors(&mut self) {
567 if let Some(iter) = self.selected_cells_iter() {
568 iter.for_each(|(idx, _)| self.cells[idx].flip_colors());
569 }
570 }
571
572 fn selected_cells_iter(&self) -> Option<CellIterator> {
573 self.selection
574 .get_query()
575 .map(|query| self.cell_iter(query))
576 }
577
578 fn flip_cell_colors(&mut self, x: u16, y: u16) {
579 let (cols, _) = self.terminal_size;
580 let idx = y as usize * cols as usize + x as usize;
581 if idx < self.cells.len() {
582 self.cells[idx].flip_colors();
583 }
584 }
585
586 pub fn resize(
602 &mut self,
603 gl: &WebGl2RenderingContext,
604 canvas_size: (i32, i32),
605 pixel_ratio: f32,
606 ) -> Result<(), Error> {
607 self.canvas_size_px = canvas_size;
608 self.pixel_ratio = pixel_ratio;
609
610 let cell_size = self.effective_cell_size();
611
612 self.gpu
614 .buffers
615 .update_vertex_buffer(gl, cell_size);
616
617 self.upload_ubo_data(gl);
619
620 let cols = canvas_size.0 / cell_size.0;
621 let rows = canvas_size.1 / cell_size.1;
622 if self.terminal_size == (cols as u16, rows as u16) {
623 return Ok(()); }
625
626 gl.bind_vertex_array(Some(&self.gpu.buffers.vao));
628
629 gl.delete_buffer(Some(&self.gpu.buffers.instance_cell));
631 gl.delete_buffer(Some(&self.gpu.buffers.instance_pos));
632
633 let current_size = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
635 let cell_data = self.resize_cell_grid(current_size, (cols, rows));
636 self.cells = cell_data;
637
638 let cell_pos = CellStatic::create_grid(cols, rows);
639
640 self.gpu.buffers.instance_cell = create_dynamic_instance_buffer(gl, &self.cells)?;
642 self.gpu.buffers.instance_pos = create_static_instance_buffer(gl, &cell_pos)?;
643
644 gl.bind_vertex_array(None);
646
647 self.terminal_size = (cols as u16, rows as u16);
648
649 Ok(())
650 }
651
652 pub fn recreate_resources(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
669 let cell_size = self.effective_cell_size();
670 let (cols, rows) = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
671 let cell_pos = CellStatic::create_grid(cols, rows);
672
673 self.gpu = GpuResources::new(gl, &cell_pos, &self.cells, cell_size)?;
675
676 self.upload_ubo_data(gl);
678
679 self.cells_pending_flush = true;
681
682 Ok(())
683 }
684
685 pub fn recreate_atlas_texture(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
690 self.atlas.recreate_texture(gl)
691 }
692
693 pub fn base_glyph_id(&self, symbol: &str) -> Option<u16> {
695 self.atlas.get_base_glyph_id(symbol)
696 }
697
698 fn fallback_symbol(&self) -> Option<CompactString> {
699 self.atlas.get_symbol(self.fallback_glyph)
700 }
701
702 fn clear_stale_selection(&self) {
703 if let Some(query) = self.selection_tracker().get_query()
704 && let Some(hash) = query.content_hash
705 && hash != self.hash_cells(query)
706 {
707 self.selection.clear();
708 }
709 }
710
711 fn resize_cell_grid(&self, old_size: (i32, i32), new_size: (i32, i32)) -> Vec<CellDynamic> {
712 let empty_cell = CellDynamic::new(self.atlas.space_glyph_id(), 0xFFFFFF, 0x000000);
713
714 let new_len = new_size.0 * new_size.1;
715 let mut new_cells = Vec::with_capacity(new_len as usize);
716 for _ in 0..new_len {
717 new_cells.push(empty_cell);
718 }
719
720 let cells = &self.cells;
721 for y in 0..min(old_size.1, new_size.1) {
722 for x in 0..min(old_size.0, new_size.0) {
723 let new_idx = (y * new_size.0 + x) as usize;
724 let old_idx = (y * old_size.0 + x) as usize;
725 new_cells[new_idx] = cells[old_idx];
726 }
727 }
728
729 new_cells
730 }
731}
732
733fn create_vao(gl: &WebGl2RenderingContext) -> Result<web_sys::WebGlVertexArrayObject, Error> {
734 gl.create_vertex_array()
735 .ok_or(Error::vertex_array_creation_failed())
736}
737
738fn setup_buffers(
739 gl: &WebGl2RenderingContext,
740 vao: web_sys::WebGlVertexArrayObject,
741 cell_pos: &[CellStatic],
742 cell_data: &[CellDynamic],
743 cell_size: (i32, i32),
744) -> Result<TerminalBuffers, Error> {
745 let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
746
747 #[rustfmt::skip]
748 let vertices = [
749 w, 0.0, 1.0, 0.0, 0.0, h, 0.0, 1.0, w, h, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0 ];
755 let indices = [0, 1, 2, 0, 3, 1];
756
757 Ok(TerminalBuffers {
758 vao,
759 vertices: create_buffer_f32(gl, GL::ARRAY_BUFFER, &vertices, GL::STATIC_DRAW)?,
760 instance_pos: create_static_instance_buffer(gl, cell_pos)?,
761 instance_cell: create_dynamic_instance_buffer(gl, cell_data)?,
762 indices: create_buffer_u8(gl, GL::ELEMENT_ARRAY_BUFFER, &indices, GL::STATIC_DRAW)?,
763 })
764}
765
766fn create_buffer_u8(
767 gl: &WebGl2RenderingContext,
768 target: u32,
769 data: &[u8],
770 usage: u32,
771) -> Result<web_sys::WebGlBuffer, Error> {
772 let index_buf = gl
773 .create_buffer()
774 .ok_or(Error::buffer_creation_failed("vbo-u8"))?;
775 gl.bind_buffer(target, Some(&index_buf));
776
777 gl.buffer_data_with_u8_array(target, data, usage);
778
779 Ok(index_buf)
780}
781
782fn create_buffer_f32(
783 gl: &WebGl2RenderingContext,
784 target: u32,
785 data: &[f32],
786 usage: u32,
787) -> Result<web_sys::WebGlBuffer, Error> {
788 let buffer = gl
789 .create_buffer()
790 .ok_or(Error::buffer_creation_failed("vbo-f32"))?;
791
792 gl.bind_buffer(target, Some(&buffer));
793
794 unsafe {
795 let view = js_sys::Float32Array::view(data);
796 gl.buffer_data_with_array_buffer_view(target, &view, usage);
797 }
798
799 const STRIDE: i32 = (2 + 2) * 4; enable_vertex_attrib(gl, attrib::POS, 2, GL::FLOAT, 0, STRIDE);
802 enable_vertex_attrib(gl, attrib::UV, 2, GL::FLOAT, 8, STRIDE);
803
804 Ok(buffer)
805}
806
807fn create_static_instance_buffer(
808 gl: &WebGl2RenderingContext,
809 instance_data: &[CellStatic],
810) -> Result<web_sys::WebGlBuffer, Error> {
811 let instance_buf = gl
812 .create_buffer()
813 .ok_or(Error::buffer_creation_failed("static-instance-buffer"))?;
814
815 gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
816 buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::STATIC_DRAW);
817
818 let stride = size_of::<CellStatic>() as i32;
819 enable_vertex_attrib_array(gl, attrib::GRID_XY, 2, GL::UNSIGNED_SHORT, 0, stride);
820
821 Ok(instance_buf)
822}
823
824fn create_dynamic_instance_buffer(
825 gl: &WebGl2RenderingContext,
826 instance_data: &[CellDynamic],
827) -> Result<web_sys::WebGlBuffer, Error> {
828 let instance_buf = gl
829 .create_buffer()
830 .ok_or(Error::buffer_creation_failed("dynamic-instance-buffer"))?;
831
832 gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
833 buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::DYNAMIC_DRAW);
834
835 let stride = size_of::<CellDynamic>() as i32;
836
837 enable_vertex_attrib_array(
839 gl,
840 attrib::PACKED_DEPTH_FG_BG,
841 2,
842 GL::UNSIGNED_INT,
843 0,
844 stride,
845 );
846
847 Ok(instance_buf)
848}
849
850fn enable_vertex_attrib_array(
851 gl: &WebGl2RenderingContext,
852 index: u32,
853 size: i32,
854 type_: u32,
855 offset: i32,
856 stride: i32,
857) {
858 enable_vertex_attrib(gl, index, size, type_, offset, stride);
859 gl.vertex_attrib_divisor(index, 1);
860}
861
862fn enable_vertex_attrib(
863 gl: &WebGl2RenderingContext,
864 index: u32,
865 size: i32,
866 type_: u32,
867 offset: i32,
868 stride: i32,
869) {
870 gl.enable_vertex_attrib_array(index);
871 if type_ == GL::FLOAT {
872 gl.vertex_attrib_pointer_with_i32(index, size, type_, false, stride, offset);
873 } else {
874 gl.vertex_attrib_i_pointer_with_i32(index, size, type_, stride, offset);
875 }
876}
877
878impl Drawable for TerminalGrid {
879 fn prepare(&self, context: &mut RenderContext) {
880 let gl = context.gl;
881
882 self.gpu.shader.use_program(gl);
883
884 gl.bind_vertex_array(Some(&self.gpu.buffers.vao));
885
886 self.atlas.bind(gl, 0);
887 self.atlas.flush(gl).unwrap(); self.gpu.ubo_vertex.bind(context.gl);
889 self.gpu.ubo_fragment.bind(context.gl);
890 gl.uniform1i(Some(&self.gpu.sampler_loc), 0);
891 }
892
893 fn draw(&self, context: &mut RenderContext) {
894 let gl = context.gl;
895 let cell_count = self.cells.len() as i32;
896
897 gl.draw_elements_instanced_with_i32(GL::TRIANGLES, 6, GL::UNSIGNED_BYTE, 0, cell_count);
898 }
899
900 fn cleanup(&self, context: &mut RenderContext) {
901 let gl = context.gl;
902 gl.bind_vertex_array(None);
903 gl.bind_texture(GL::TEXTURE_2D_ARRAY, None);
904
905 self.gpu.ubo_vertex.unbind(gl);
906 self.gpu.ubo_fragment.unbind(gl);
907 }
908}
909
910#[derive(Debug, Copy, Clone)]
922pub struct CellData<'a> {
923 symbol: &'a str,
924 style_bits: u16,
925 fg: u32,
926 bg: u32,
927}
928
929impl<'a> CellData<'a> {
930 pub fn new(symbol: &'a str, style: FontStyle, effect: GlyphEffect, fg: u32, bg: u32) -> Self {
942 Self::new_with_style_bits(symbol, style.style_mask() | effect as u16, fg, bg)
943 }
944
945 pub fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
969 debug_assert!(
971 0x81FF & style_bits == 0,
972 "Invalid style bits: {style_bits:#04x}"
973 );
974 Self { symbol, style_bits, fg, bg }
975 }
976}
977
978#[repr(C, align(4))]
999struct CellStatic {
1000 pub grid_xy: [u16; 2],
1002}
1003
1004#[derive(Debug, Clone, Copy, Hash)]
1032#[repr(C, align(4))]
1033pub struct CellDynamic {
1034 data: [u8; 8], }
1047
1048impl CellStatic {
1049 fn create_grid(cols: i32, rows: i32) -> Vec<Self> {
1050 debug_assert!(cols > 0 && cols < u16::MAX as i32, "cols: {cols}");
1051 debug_assert!(rows > 0 && rows < u16::MAX as i32, "rows: {rows}");
1052
1053 (0..rows)
1054 .flat_map(|row| (0..cols).map(move |col| (col, row)))
1055 .map(|(col, row)| Self { grid_xy: [col as u16, row as u16] })
1056 .collect()
1057 }
1058}
1059
1060impl CellDynamic {
1061 const GLYPH_STYLE_MASK: u16 =
1062 Glyph::BOLD_FLAG | Glyph::ITALIC_FLAG | Glyph::UNDERLINE_FLAG | Glyph::STRIKETHROUGH_FLAG;
1063
1064 #[inline]
1065 pub fn new(glyph_id: u16, fg: u32, bg: u32) -> Self {
1066 let mut data = [0; 8];
1067
1068 let glyph_id = glyph_id.to_le_bytes();
1070 data[0] = glyph_id[0];
1071 data[1] = glyph_id[1];
1072
1073 let fg = fg.to_le_bytes();
1074 data[2] = fg[2]; data[3] = fg[1]; data[4] = fg[0]; let bg = bg.to_le_bytes();
1079 data[5] = bg[2]; data[6] = bg[1]; data[7] = bg[0]; Self { data }
1084 }
1085
1086 pub fn style(&mut self, style_bits: u16) {
1088 let glyph_id = (self.glyph_id() & !Self::GLYPH_STYLE_MASK) | style_bits;
1089 self.data[..2].copy_from_slice(&glyph_id.to_le_bytes());
1090 }
1091
1092 pub fn flip_colors(&mut self) {
1094 let fg = [self.data[2], self.data[3], self.data[4]];
1096 self.data[2] = self.data[5]; self.data[3] = self.data[6]; self.data[4] = self.data[7]; self.data[5] = fg[0]; self.data[6] = fg[1]; self.data[7] = fg[2]; }
1103
1104 pub fn fg_color(&mut self, fg: u32) {
1106 let fg = fg.to_le_bytes();
1107 self.data[2] = fg[2]; self.data[3] = fg[1]; self.data[4] = fg[0]; }
1111
1112 pub fn bg_color(&mut self, bg: u32) {
1114 let bg = bg.to_le_bytes();
1115 self.data[5] = bg[2]; self.data[6] = bg[1]; self.data[7] = bg[0]; }
1119
1120 pub fn get_fg_color(&self) -> u32 {
1122 ((self.data[2] as u32) << 16) | ((self.data[3] as u32) << 8) | (self.data[4] as u32)
1124 }
1125
1126 pub fn get_bg_color(&self) -> u32 {
1128 ((self.data[5] as u32) << 16) | ((self.data[6] as u32) << 8) | (self.data[7] as u32)
1130 }
1131
1132 pub fn get_style(&self) -> u16 {
1134 self.glyph_id() & Self::GLYPH_STYLE_MASK
1135 }
1136
1137 pub fn is_emoji(&self) -> bool {
1139 self.glyph_id() & Glyph::EMOJI_FLAG != 0
1140 }
1141
1142 #[inline]
1143 fn glyph_id(&self) -> u16 {
1144 u16::from_le_bytes([self.data[0], self.data[1]])
1145 }
1146
1147 fn set_glyph_id(&mut self, glyph_id: u16) {
1148 let bytes = glyph_id.to_le_bytes();
1149 self.data[0] = bytes[0];
1150 self.data[1] = bytes[1];
1151 }
1152}
1153
1154#[repr(C, align(16))] struct CellVertexUbo {
1156 pub projection: [f32; 16], pub cell_size: [f32; 2], pub _padding: [f32; 2],
1159}
1160
1161#[repr(C, align(16))] struct CellFragmentUbo {
1163 pub padding_frac: [f32; 2], pub underline_pos: f32, pub underline_thickness: f32, pub strikethrough_pos: f32, pub strikethrough_thickness: f32, pub texture_lookup_mask: u32, pub _padding: f32,
1170}
1171
1172impl CellVertexUbo {
1173 pub const BINDING_POINT: u32 = 0;
1174
1175 fn new(canvas_size: (i32, i32), cell_size: (i32, i32)) -> Self {
1176 let projection =
1177 Mat4::orthographic_from_size(canvas_size.0 as f32, canvas_size.1 as f32).data;
1178 Self {
1179 projection,
1180 cell_size: [cell_size.0 as f32, cell_size.1 as f32],
1181 _padding: [0.0; 2], }
1183 }
1184}
1185
1186impl CellFragmentUbo {
1187 pub const BINDING_POINT: u32 = 1;
1188
1189 fn new(atlas: &FontAtlas) -> Self {
1190 let texture_cell_size = atlas.texture_cell_size();
1192 let underline = atlas.underline();
1193 let strikethrough = atlas.strikethrough();
1194
1195 Self {
1196 padding_frac: [
1197 FontAtlasData::PADDING as f32 / texture_cell_size.0 as f32,
1198 FontAtlasData::PADDING as f32 / texture_cell_size.1 as f32,
1199 ],
1200 underline_pos: underline.position,
1201 underline_thickness: underline.thickness,
1202 strikethrough_pos: strikethrough.position,
1203 strikethrough_thickness: strikethrough.thickness,
1204 texture_lookup_mask: atlas.base_lookup_mask(),
1205 _padding: 0.0, }
1207 }
1208}
1209
1210fn create_terminal_cell_data(cols: i32, rows: i32, fill_glyph: u16) -> Vec<CellDynamic> {
1211 (0..cols * rows)
1212 .map(|i| CellDynamic::new(fill_glyph, 0x00ff_ffff, 0x0000_0000))
1213 .collect()
1214}
1215
1216mod attrib {
1217 pub const POS: u32 = 0;
1218 pub const UV: u32 = 1;
1219
1220 pub const GRID_XY: u32 = 2;
1221 pub const PACKED_DEPTH_FG_BG: u32 = 3;
1222}