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#[wasm_bindgen]
14#[derive(Debug)]
15pub struct BeamtermRenderer {
16 renderer: Renderer,
17 terminal_grid: Rc<RefCell<TerminalGrid>>,
18}
19
20#[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 #[wasm_bindgen(constructor)]
72 pub fn new() -> CellStyle {
73 Default::default()
74 }
75
76 #[wasm_bindgen]
78 pub fn fg(mut self, color: u32) -> CellStyle {
79 self.fg = color;
80 self
81 }
82
83 #[wasm_bindgen]
85 pub fn bg(mut self, color: u32) -> CellStyle {
86 self.bg = color;
87 self
88 }
89
90 #[wasm_bindgen]
92 pub fn bold(mut self) -> CellStyle {
93 self.style_bits |= Glyph::BOLD_FLAG;
94 self
95 }
96
97 #[wasm_bindgen]
99 pub fn italic(mut self) -> CellStyle {
100 self.style_bits |= Glyph::ITALIC_FLAG;
101 self
102 }
103
104 #[wasm_bindgen]
106 pub fn underline(mut self) -> CellStyle {
107 self.style_bits |= Glyph::UNDERLINE_FLAG;
108 self
109 }
110
111 #[wasm_bindgen]
113 pub fn strikethrough(mut self) -> CellStyle {
114 self.style_bits |= Glyph::STRIKETHROUGH_FLAG;
115 self
116 }
117
118 #[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, bg: 0x000000, style_bits: 0, }
132 }
133}
134
135#[wasm_bindgen]
136impl Batch {
137 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[wasm_bindgen(start)]
398pub fn main() {
399 console_error_panic_hook::set_once();
400 console::log_1(&"beamterm WASM module loaded".into());
401}