beamterm_renderer/
wasm.rs

1use std::{cell::RefCell, rc::Rc};
2
3use beamterm_data::{FontAtlasData, Glyph};
4use compact_str::CompactString;
5use serde_wasm_bindgen::from_value;
6use unicode_segmentation::UnicodeSegmentation;
7use wasm_bindgen::prelude::*;
8use web_sys::console;
9
10use crate::gl::{CellData, FontAtlas, Renderer, TerminalGrid};
11
12/// JavaScript wrapper for the terminal renderer
13#[wasm_bindgen]
14#[derive(Debug)]
15pub struct BeamtermRenderer {
16    renderer: Renderer,
17    terminal_grid: Rc<RefCell<TerminalGrid>>,
18}
19
20/// JavaScript wrapper for cell data
21#[wasm_bindgen]
22#[derive(Debug, Default, serde::Deserialize)]
23pub struct Cell {
24    symbol: CompactString,
25    style: u16,
26    fg: u32,
27    bg: u32,
28}
29
30#[wasm_bindgen]
31#[derive(Debug, Clone, Copy)]
32pub struct CellStyle {
33    fg: u32,
34    bg: u32,
35    style_bits: u16,
36}
37
38#[wasm_bindgen]
39#[derive(Debug, Clone, Copy)]
40pub struct Size {
41    pub width: u16,
42    pub height: u16,
43}
44
45#[wasm_bindgen]
46#[derive(Debug)]
47pub struct Batch {
48    terminal_grid: Rc<RefCell<TerminalGrid>>,
49    gl: web_sys::WebGl2RenderingContext,
50    dirty: bool,
51}
52
53#[wasm_bindgen]
54pub fn style() -> CellStyle {
55    CellStyle::new()
56}
57
58#[wasm_bindgen]
59pub fn cell(symbol: &str, style: CellStyle) -> Cell {
60    Cell {
61        symbol: symbol.into(),
62        style: style.style_bits,
63        fg: style.fg,
64        bg: style.bg,
65    }
66}
67
68#[wasm_bindgen]
69impl CellStyle {
70    /// Create a new TextStyle with default (normal) style
71    #[wasm_bindgen(constructor)]
72    pub fn new() -> CellStyle {
73        Default::default()
74    }
75
76    /// Sets the foreground color
77    #[wasm_bindgen]
78    pub fn fg(mut self, color: u32) -> CellStyle {
79        self.fg = color;
80        self
81    }
82
83    /// Sets the background color
84    #[wasm_bindgen]
85    pub fn bg(mut self, color: u32) -> CellStyle {
86        self.bg = color;
87        self
88    }
89
90    /// Add bold style
91    #[wasm_bindgen]
92    pub fn bold(mut self) -> CellStyle {
93        self.style_bits |= Glyph::BOLD_FLAG;
94        self
95    }
96
97    /// Add italic style
98    #[wasm_bindgen]
99    pub fn italic(mut self) -> CellStyle {
100        self.style_bits |= Glyph::ITALIC_FLAG;
101        self
102    }
103
104    /// Add underline effect
105    #[wasm_bindgen]
106    pub fn underline(mut self) -> CellStyle {
107        self.style_bits |= Glyph::UNDERLINE_FLAG;
108        self
109    }
110
111    /// Add strikethrough effect
112    #[wasm_bindgen]
113    pub fn strikethrough(mut self) -> CellStyle {
114        self.style_bits |= Glyph::STRIKETHROUGH_FLAG;
115        self
116    }
117
118    /// Get the combined style bits
119    #[wasm_bindgen(getter)]
120    pub fn bits(&self) -> u16 {
121        self.style_bits
122    }
123}
124
125impl Default for CellStyle {
126    fn default() -> Self {
127        CellStyle {
128            fg: 0xFFFFFF,  // Default foreground color (white)
129            bg: 0x000000,  // Default background color (black)
130            style_bits: 0, // No styles applied
131        }
132    }
133}
134
135#[wasm_bindgen]
136impl Batch {
137    /// Updates a single cell at the given position.
138    #[wasm_bindgen(js_name = "cell")]
139    pub fn cell(&mut self, x: u16, y: u16, cell_data: &Cell) {
140        self.dirty = true;
141        self.terminal_grid.borrow_mut().update_cell(x, y, cell_data.as_cell_data());
142    }
143
144    /// Updates a cell by its buffer index.
145    #[wasm_bindgen(js_name = "cellByIndex")]
146    pub fn cell_by_index(&mut self, idx: usize, cell_data: &Cell) {
147        self.dirty = true;
148        self.terminal_grid
149            .borrow_mut()
150            .update_cell_by_index(idx, cell_data.as_cell_data());
151    }
152
153    /// Updates multiple cells from an array.
154    /// Each element should be [x, y, cellData].
155    #[wasm_bindgen(js_name = "cells")]
156    pub fn cells(&mut self, cells_json: JsValue) -> Result<(), JsValue> {
157        self.dirty = true;
158
159        let updates = from_value::<Vec<(u16, u16, Cell)>>(cells_json)
160            .map_err(|e| JsValue::from_str(&e.to_string()));
161
162        match updates {
163            Ok(cells) => {
164                let cell_data = cells.iter().map(|(x, y, data)| (*x, *y, data.as_cell_data()));
165
166                let mut terminal_grid = self.terminal_grid.borrow_mut();
167                terminal_grid
168                    .update_cells_by_position(&self.gl, cell_data)
169                    .map_err(|e| JsValue::from_str(&e.to_string()))
170            },
171            e => e.map(|_| ()),
172        }
173    }
174
175    /// Write text to the terminal
176    #[wasm_bindgen(js_name = "text")]
177    pub fn text(&mut self, x: u16, y: u16, text: &str, style: &CellStyle) -> Result<(), JsValue> {
178        self.dirty = true;
179
180        let mut terminal_grid = self.terminal_grid.borrow_mut();
181        let (cols, rows) = terminal_grid.terminal_size();
182
183        if y >= rows {
184            // todo: feature-toggle?
185            // return Err(JsValue::from_str("Row out of bounds"));
186            return Ok(());
187        }
188
189        for (i, ch) in text.graphemes(true).enumerate() {
190            let current_col = x + i as u16;
191            if current_col >= cols {
192                break;
193            }
194
195            let cell = CellData::new_with_style_bits(ch, style.style_bits, style.fg, style.bg);
196            terminal_grid.update_cell(current_col, y, cell);
197        }
198
199        Ok(())
200    }
201
202    /// Fill a rectangular region
203    #[wasm_bindgen(js_name = "fill")]
204    pub fn fill(
205        &mut self,
206        x: u16,
207        y: u16,
208        width: u16,
209        height: u16,
210        cell_data: &Cell,
211    ) -> Result<(), JsValue> {
212        self.dirty = true;
213
214        let mut terminal_grid = self.terminal_grid.borrow_mut();
215        let (cols, rows) = terminal_grid.terminal_size();
216
217        let width = (x + width).min(cols).saturating_sub(x);
218        let height = (y + height).min(rows).saturating_sub(y);
219
220        let fill_cell = cell_data.as_cell_data();
221        for y in y..y + height {
222            for x in x..x + width {
223                terminal_grid.update_cell(x, y, fill_cell);
224            }
225        }
226
227        Ok(())
228    }
229
230    /// Clear the terminal with specified background color
231    #[wasm_bindgen]
232    pub fn clear(&mut self, bg: u32) -> Result<(), JsValue> {
233        self.dirty = true;
234
235        let mut terminal_grid = self.terminal_grid.borrow_mut();
236        let (cols, rows) = terminal_grid.terminal_size();
237
238        let clear_cell = CellData::new_with_style_bits(" ", 0, 0xFFFFFF, bg);
239        for y in 0..rows {
240            for x in 0..cols {
241                terminal_grid.update_cell(x, y, clear_cell);
242            }
243        }
244
245        Ok(())
246    }
247
248    /// Synchronize all pending updates to the GPU
249    #[wasm_bindgen]
250    pub fn flush(&mut self) -> Result<(), JsValue> {
251        if self.dirty {
252            self.dirty = false;
253            self.terminal_grid
254                .borrow_mut()
255                .flush_cells(&self.gl)
256                .map_err(|e| JsValue::from_str(&e.to_string()))?;
257        }
258
259        Ok(())
260    }
261}
262
263#[wasm_bindgen]
264impl Cell {
265    #[wasm_bindgen(constructor)]
266    pub fn new(symbol: String, style: &CellStyle) -> Cell {
267        Cell {
268            symbol: symbol.into(),
269            style: style.style_bits,
270            fg: style.fg,
271            bg: style.bg,
272        }
273    }
274
275    #[wasm_bindgen(getter)]
276    pub fn symbol(&self) -> String {
277        self.symbol.to_string()
278    }
279
280    #[wasm_bindgen(setter)]
281    pub fn set_symbol(&mut self, symbol: String) {
282        self.symbol = symbol.into();
283    }
284
285    #[wasm_bindgen(getter)]
286    pub fn fg(&self) -> u32 {
287        self.fg
288    }
289
290    #[wasm_bindgen(setter)]
291    pub fn set_fg(&mut self, color: u32) {
292        self.fg = color;
293    }
294
295    #[wasm_bindgen(getter)]
296    pub fn bg(&self) -> u32 {
297        self.bg
298    }
299
300    #[wasm_bindgen(setter)]
301    pub fn set_bg(&mut self, color: u32) {
302        self.bg = color;
303    }
304
305    #[wasm_bindgen(getter)]
306    pub fn style(&self) -> u16 {
307        self.style
308    }
309
310    #[wasm_bindgen(setter)]
311    pub fn set_style(&mut self, style: u16) {
312        self.style = style;
313    }
314}
315
316impl Cell {
317    pub fn as_cell_data(&self) -> CellData {
318        CellData::new_with_style_bits(&self.symbol, self.style, self.fg, self.bg)
319    }
320}
321
322#[wasm_bindgen]
323impl BeamtermRenderer {
324    /// Create a new terminal renderer
325    #[wasm_bindgen(constructor)]
326    pub fn new(canvas_id: &str) -> Result<BeamtermRenderer, JsValue> {
327        console_error_panic_hook::set_once();
328
329        let renderer = Renderer::create(canvas_id)
330            .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {}", e)))?;
331
332        let gl = renderer.gl();
333        let atlas_data = FontAtlasData::default();
334        let atlas = FontAtlas::load(gl, atlas_data)
335            .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {}", e)))?;
336
337        let canvas_size = renderer.canvas_size();
338        let terminal_grid = TerminalGrid::new(gl, atlas, canvas_size)
339            .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {}", e)))?;
340
341        console::log_1(&"BeamtermRenderer initialized successfully".into());
342        let terminal_grid = Rc::new(RefCell::new(terminal_grid));
343        Ok(BeamtermRenderer { renderer, terminal_grid })
344    }
345
346    /// Create a new render batch
347    #[wasm_bindgen(js_name = "batch")]
348    pub fn new_render_batch(&mut self) -> Batch {
349        let gl = self.renderer.gl().clone();
350        let terminal_grid = self.terminal_grid.clone();
351        Batch { terminal_grid, gl, dirty: false }
352    }
353
354    /// Get the terminal dimensions in cells
355    #[wasm_bindgen(js_name = "terminalSize")]
356    pub fn terminal_size(&self) -> Size {
357        let (cols, rows) = self.terminal_grid.borrow().terminal_size();
358        Size { width: cols, height: rows }
359    }
360
361    /// Get the cell size in pixels
362    #[wasm_bindgen(js_name = "cellSize")]
363    pub fn cell_size(&self) -> Size {
364        let (width, height) = self.terminal_grid.borrow().cell_size();
365        Size {
366            width: width as u16,
367            height: height as u16,
368        }
369    }
370
371    /// Render the terminal to the canvas
372    #[wasm_bindgen]
373    pub fn render(&mut self) {
374        self.renderer.begin_frame();
375        let grid: &TerminalGrid = &self.terminal_grid.borrow();
376        self.renderer.render(grid);
377        self.renderer.end_frame();
378    }
379
380    /// Resize the terminal to fit new canvas dimensions
381    #[wasm_bindgen]
382    pub fn resize(&mut self, width: i32, height: i32) -> Result<(), JsValue> {
383        self.renderer.resize(width, height);
384
385        console::log_1(&format!("Resizing terminal to {}x{}", width, height).into());
386
387        let gl = self.renderer.gl();
388        self.terminal_grid
389            .borrow_mut()
390            .resize(gl, (width, height))
391            .map_err(|e| JsValue::from_str(&format!("Failed to resize: {}", e)))?;
392        Ok(())
393    }
394}
395
396/// Initialize the WASM module
397#[wasm_bindgen(start)]
398pub fn main() {
399    console_error_panic_hook::set_once();
400    console::log_1(&"beamterm WASM module loaded".into());
401}