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::{
11 gl::{
12 select, CellData, CellQuery as RustCellQuery, FontAtlas, Renderer,
13 SelectionMode as RustSelectionMode, TerminalGrid,
14 },
15 mouse::{DefaultSelectionHandler, TerminalMouseEvent, TerminalMouseHandler},
16};
17
18#[wasm_bindgen]
20#[derive(Debug)]
21pub struct BeamtermRenderer {
22 renderer: Renderer,
23 terminal_grid: Rc<RefCell<TerminalGrid>>,
24 mouse_handler: Option<TerminalMouseHandler>,
25}
26
27#[wasm_bindgen]
29#[derive(Debug, Default, serde::Deserialize)]
30pub struct Cell {
31 symbol: CompactString,
32 style: u16,
33 fg: u32,
34 bg: u32,
35}
36
37#[wasm_bindgen]
38#[derive(Debug, Clone, Copy)]
39pub struct CellStyle {
40 fg: u32,
41 bg: u32,
42 style_bits: u16,
43}
44
45#[wasm_bindgen]
46#[derive(Debug, Clone, Copy)]
47pub struct Size {
48 pub width: u16,
49 pub height: u16,
50}
51
52#[wasm_bindgen]
53#[derive(Debug)]
54pub struct Batch {
55 terminal_grid: Rc<RefCell<TerminalGrid>>,
56 gl: web_sys::WebGl2RenderingContext,
57 dirty: bool,
58}
59
60#[wasm_bindgen]
62#[derive(Debug, Clone, Copy)]
63pub enum SelectionMode {
64 Block,
66 Linear,
68}
69
70#[wasm_bindgen]
72#[derive(Debug, Clone, Copy)]
73pub enum MouseEventType {
74 MouseDown,
76 MouseUp,
78 MouseMove,
80}
81
82#[wasm_bindgen]
84#[derive(Debug, Clone, Copy)]
85pub struct MouseEvent {
86 pub event_type: MouseEventType,
88 pub col: u16,
90 pub row: u16,
92 pub button: i16,
94 pub ctrl_key: bool,
96 pub shift_key: bool,
98 pub alt_key: bool,
100}
101
102#[wasm_bindgen]
104#[derive(Debug, Clone)]
105pub struct CellQuery {
106 inner: RustCellQuery,
107}
108
109#[wasm_bindgen]
110impl CellQuery {
111 #[wasm_bindgen(constructor)]
113 pub fn new(mode: SelectionMode) -> CellQuery {
114 CellQuery { inner: select(mode.into()) }
115 }
116
117 pub fn start(mut self, col: u16, row: u16) -> CellQuery {
119 self.inner = self.inner.start((col, row));
120 self
121 }
122
123 pub fn end(mut self, col: u16, row: u16) -> CellQuery {
125 self.inner = self.inner.end((col, row));
126 self
127 }
128
129 #[wasm_bindgen(js_name = "trimTrailingWhitespace")]
131 pub fn trim_trailing_whitespace(mut self, enabled: bool) -> CellQuery {
132 self.inner = self.inner.trim_trailing_whitespace(enabled);
133 self
134 }
135
136 #[wasm_bindgen(js_name = "isEmpty")]
138 pub fn is_empty(&self) -> bool {
139 self.inner.is_empty()
140 }
141}
142
143#[wasm_bindgen]
144pub fn style() -> CellStyle {
145 CellStyle::new()
146}
147
148#[wasm_bindgen]
149pub fn cell(symbol: &str, style: CellStyle) -> Cell {
150 Cell {
151 symbol: symbol.into(),
152 style: style.style_bits,
153 fg: style.fg,
154 bg: style.bg,
155 }
156}
157
158#[wasm_bindgen]
159impl CellStyle {
160 #[wasm_bindgen(constructor)]
162 pub fn new() -> CellStyle {
163 Default::default()
164 }
165
166 #[wasm_bindgen]
168 pub fn fg(mut self, color: u32) -> CellStyle {
169 self.fg = color;
170 self
171 }
172
173 #[wasm_bindgen]
175 pub fn bg(mut self, color: u32) -> CellStyle {
176 self.bg = color;
177 self
178 }
179
180 #[wasm_bindgen]
182 pub fn bold(mut self) -> CellStyle {
183 self.style_bits |= Glyph::BOLD_FLAG;
184 self
185 }
186
187 #[wasm_bindgen]
189 pub fn italic(mut self) -> CellStyle {
190 self.style_bits |= Glyph::ITALIC_FLAG;
191 self
192 }
193
194 #[wasm_bindgen]
196 pub fn underline(mut self) -> CellStyle {
197 self.style_bits |= Glyph::UNDERLINE_FLAG;
198 self
199 }
200
201 #[wasm_bindgen]
203 pub fn strikethrough(mut self) -> CellStyle {
204 self.style_bits |= Glyph::STRIKETHROUGH_FLAG;
205 self
206 }
207
208 #[wasm_bindgen(getter)]
210 pub fn bits(&self) -> u16 {
211 self.style_bits
212 }
213}
214
215impl Default for CellStyle {
216 fn default() -> Self {
217 CellStyle {
218 fg: 0xFFFFFF, bg: 0x000000, style_bits: 0, }
222 }
223}
224
225#[wasm_bindgen]
226impl Batch {
227 #[wasm_bindgen(js_name = "cell")]
229 pub fn cell(&mut self, x: u16, y: u16, cell_data: &Cell) {
230 self.dirty = true;
231 self.terminal_grid.borrow_mut().update_cell(x, y, cell_data.as_cell_data());
232 }
233
234 #[wasm_bindgen(js_name = "cellByIndex")]
236 pub fn cell_by_index(&mut self, idx: usize, cell_data: &Cell) {
237 self.dirty = true;
238 self.terminal_grid
239 .borrow_mut()
240 .update_cell_by_index(idx, cell_data.as_cell_data());
241 }
242
243 #[wasm_bindgen(js_name = "cells")]
246 pub fn cells(&mut self, cells_json: JsValue) -> Result<(), JsValue> {
247 self.dirty = true;
248
249 let updates = from_value::<Vec<(u16, u16, Cell)>>(cells_json)
250 .map_err(|e| JsValue::from_str(&e.to_string()));
251
252 match updates {
253 Ok(cells) => {
254 let cell_data = cells.iter().map(|(x, y, data)| (*x, *y, data.as_cell_data()));
255
256 let mut terminal_grid = self.terminal_grid.borrow_mut();
257 terminal_grid
258 .update_cells_by_position(&self.gl, cell_data)
259 .map_err(|e| JsValue::from_str(&e.to_string()))
260 },
261 e => e.map(|_| ()),
262 }
263 }
264
265 #[wasm_bindgen(js_name = "text")]
267 pub fn text(&mut self, x: u16, y: u16, text: &str, style: &CellStyle) -> Result<(), JsValue> {
268 self.dirty = true;
269
270 let mut terminal_grid = self.terminal_grid.borrow_mut();
271 let (cols, rows) = terminal_grid.terminal_size();
272
273 if y >= rows {
274 return Ok(()); }
276
277 for (i, ch) in text.graphemes(true).enumerate() {
278 let current_col = x + i as u16;
279 if current_col >= cols {
280 break;
281 }
282
283 let cell = CellData::new_with_style_bits(ch, style.style_bits, style.fg, style.bg);
284 terminal_grid.update_cell(current_col, y, cell);
285 }
286
287 Ok(())
288 }
289
290 #[wasm_bindgen(js_name = "fill")]
292 pub fn fill(
293 &mut self,
294 x: u16,
295 y: u16,
296 width: u16,
297 height: u16,
298 cell_data: &Cell,
299 ) -> Result<(), JsValue> {
300 self.dirty = true;
301
302 let mut terminal_grid = self.terminal_grid.borrow_mut();
303 let (cols, rows) = terminal_grid.terminal_size();
304
305 let width = (x + width).min(cols).saturating_sub(x);
306 let height = (y + height).min(rows).saturating_sub(y);
307
308 let fill_cell = cell_data.as_cell_data();
309 for y in y..y + height {
310 for x in x..x + width {
311 terminal_grid.update_cell(x, y, fill_cell);
312 }
313 }
314
315 Ok(())
316 }
317
318 #[wasm_bindgen]
320 pub fn clear(&mut self, bg: u32) -> Result<(), JsValue> {
321 self.dirty = true;
322
323 let mut terminal_grid = self.terminal_grid.borrow_mut();
324 let (cols, rows) = terminal_grid.terminal_size();
325
326 let clear_cell = CellData::new_with_style_bits(" ", 0, 0xFFFFFF, bg);
327 for y in 0..rows {
328 for x in 0..cols {
329 terminal_grid.update_cell(x, y, clear_cell);
330 }
331 }
332
333 Ok(())
334 }
335
336 #[wasm_bindgen]
338 pub fn flush(&mut self) -> Result<(), JsValue> {
339 if self.dirty {
340 self.dirty = false;
341 self.terminal_grid
342 .borrow_mut()
343 .flush_cells(&self.gl)
344 .map_err(|e| JsValue::from_str(&e.to_string()))?;
345 }
346
347 Ok(())
348 }
349}
350
351#[wasm_bindgen]
352impl Cell {
353 #[wasm_bindgen(constructor)]
354 pub fn new(symbol: String, style: &CellStyle) -> Cell {
355 Cell {
356 symbol: symbol.into(),
357 style: style.style_bits,
358 fg: style.fg,
359 bg: style.bg,
360 }
361 }
362
363 #[wasm_bindgen(getter)]
364 pub fn symbol(&self) -> String {
365 self.symbol.to_string()
366 }
367
368 #[wasm_bindgen(setter)]
369 pub fn set_symbol(&mut self, symbol: String) {
370 self.symbol = symbol.into();
371 }
372
373 #[wasm_bindgen(getter)]
374 pub fn fg(&self) -> u32 {
375 self.fg
376 }
377
378 #[wasm_bindgen(setter)]
379 pub fn set_fg(&mut self, color: u32) {
380 self.fg = color;
381 }
382
383 #[wasm_bindgen(getter)]
384 pub fn bg(&self) -> u32 {
385 self.bg
386 }
387
388 #[wasm_bindgen(setter)]
389 pub fn set_bg(&mut self, color: u32) {
390 self.bg = color;
391 }
392
393 #[wasm_bindgen(getter)]
394 pub fn style(&self) -> u16 {
395 self.style
396 }
397
398 #[wasm_bindgen(setter)]
399 pub fn set_style(&mut self, style: u16) {
400 self.style = style;
401 }
402}
403
404impl Cell {
405 pub fn as_cell_data(&self) -> CellData {
406 CellData::new_with_style_bits(&self.symbol, self.style, self.fg, self.bg)
407 }
408}
409
410#[wasm_bindgen]
411impl BeamtermRenderer {
412 #[wasm_bindgen(constructor)]
414 pub fn new(canvas_id: &str) -> Result<BeamtermRenderer, JsValue> {
415 console_error_panic_hook::set_once();
416
417 let renderer = Renderer::create(canvas_id)
418 .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {}", e)))?;
419
420 let gl = renderer.gl();
421 let atlas_data = FontAtlasData::default();
422 let atlas = FontAtlas::load(gl, atlas_data)
423 .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {}", e)))?;
424
425 let canvas_size = renderer.canvas_size();
426 let terminal_grid = TerminalGrid::new(gl, atlas, canvas_size)
427 .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {}", e)))?;
428
429 console::log_1(&"BeamtermRenderer initialized successfully".into());
430 let terminal_grid = Rc::new(RefCell::new(terminal_grid));
431 Ok(BeamtermRenderer {
432 renderer,
433 terminal_grid,
434 mouse_handler: None,
435 })
436 }
437
438 #[wasm_bindgen(js_name = "enableSelection")]
440 pub fn enable_selection(
441 &mut self,
442 mode: SelectionMode,
443 trim_whitespace: bool,
444 ) -> Result<(), JsValue> {
445 if let Some(old_handler) = self.mouse_handler.take() {
447 old_handler.cleanup();
448 }
449
450 let selection_tracker = self.terminal_grid.borrow().selection_tracker();
451 let handler =
452 DefaultSelectionHandler::new(self.terminal_grid.clone(), mode.into(), trim_whitespace);
453
454 let mouse_handler = TerminalMouseHandler::new(
455 self.renderer.canvas(),
456 self.terminal_grid.clone(),
457 handler.create_event_handler(selection_tracker),
458 )
459 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {}", e)))?;
460
461 self.mouse_handler = Some(mouse_handler);
462 Ok(())
463 }
464
465 #[wasm_bindgen(js_name = "setMouseHandler")]
467 pub fn set_mouse_handler(&mut self, handler: js_sys::Function) -> Result<(), JsValue> {
468 if let Some(old_handler) = self.mouse_handler.take() {
470 old_handler.cleanup();
471 }
472
473 let handler_closure = {
474 let handler = handler.clone();
475 move |event: TerminalMouseEvent, _grid: &TerminalGrid| {
476 let js_event = MouseEvent::from(event);
477 let this = JsValue::null();
478 let args = js_sys::Array::new();
479 args.push(&JsValue::from(js_event));
480
481 if let Err(e) = handler.apply(&this, &args) {
482 console::error_1(&format!("Mouse handler error: {:?}", e).into());
483 }
484 }
485 };
486
487 let mouse_handler = TerminalMouseHandler::new(
488 self.renderer.canvas(),
489 self.terminal_grid.clone(),
490 handler_closure,
491 )
492 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {}", e)))?;
493
494 self.mouse_handler = Some(mouse_handler);
495 Ok(())
496 }
497
498 #[wasm_bindgen(js_name = "getText")]
500 pub fn get_text(&self, query: &CellQuery) -> String {
501 self.terminal_grid.borrow().get_text(query.inner).to_string()
502 }
503
504 #[wasm_bindgen(js_name = "copyToClipboard")]
506 pub fn copy_to_clipboard(&self, text: &str) {
507 use wasm_bindgen_futures::spawn_local;
508 let text = text.to_string();
509
510 spawn_local(async move {
511 if let Some(window) = web_sys::window() {
512 let clipboard = window.navigator().clipboard();
513 match wasm_bindgen_futures::JsFuture::from(clipboard.write_text(&text)).await {
514 Ok(_) => {
515 console::log_1(
516 &format!("Copied {} characters to clipboard", text.len()).into(),
517 );
518 },
519 Err(err) => {
520 console::error_1(&format!("Failed to copy to clipboard: {:?}", err).into());
521 },
522 }
523 }
524 });
525 }
526
527 #[wasm_bindgen(js_name = "clearSelection")]
529 pub fn clear_selection(&self) {
530 self.terminal_grid.borrow().selection_tracker().clear();
531 }
532
533 #[wasm_bindgen(js_name = "hasSelection")]
535 pub fn has_selection(&self) -> bool {
536 self.terminal_grid.borrow().selection_tracker().get_query().is_some()
537 }
538
539 #[wasm_bindgen(js_name = "batch")]
541 pub fn new_render_batch(&mut self) -> Batch {
542 let gl = self.renderer.gl().clone();
543 let terminal_grid = self.terminal_grid.clone();
544 Batch { terminal_grid, gl, dirty: false }
545 }
546
547 #[wasm_bindgen(js_name = "terminalSize")]
549 pub fn terminal_size(&self) -> Size {
550 let (cols, rows) = self.terminal_grid.borrow().terminal_size();
551 Size { width: cols, height: rows }
552 }
553
554 #[wasm_bindgen(js_name = "cellSize")]
556 pub fn cell_size(&self) -> Size {
557 let (width, height) = self.terminal_grid.borrow().cell_size();
558 Size {
559 width: width as u16,
560 height: height as u16,
561 }
562 }
563
564 #[wasm_bindgen]
566 pub fn render(&mut self) {
567 self.renderer.begin_frame();
568 let grid: &TerminalGrid = &self.terminal_grid.borrow();
569 self.renderer.render(grid);
570 self.renderer.end_frame();
571 }
572
573 #[wasm_bindgen]
575 pub fn resize(&mut self, width: i32, height: i32) -> Result<(), JsValue> {
576 self.renderer.resize(width, height);
577
578 console::log_1(&format!("Resizing terminal to {}x{}", width, height).into());
579
580 let gl = self.renderer.gl();
581 self.terminal_grid
582 .borrow_mut()
583 .resize(gl, (width, height))
584 .map_err(|e| JsValue::from_str(&format!("Failed to resize: {}", e)))?;
585
586 if let Some(mouse_handler) = &self.mouse_handler {
588 let (cols, rows) = self.terminal_grid.borrow().terminal_size();
589 mouse_handler.update_dimensions(cols, rows);
590 }
591
592 Ok(())
593 }
594}
595
596impl From<SelectionMode> for RustSelectionMode {
598 fn from(mode: SelectionMode) -> Self {
599 match mode {
600 SelectionMode::Block => RustSelectionMode::Block,
601 SelectionMode::Linear => RustSelectionMode::Linear,
602 }
603 }
604}
605
606impl From<RustSelectionMode> for SelectionMode {
607 fn from(mode: RustSelectionMode) -> Self {
608 match mode {
609 RustSelectionMode::Block => SelectionMode::Block,
610 RustSelectionMode::Linear => SelectionMode::Linear,
611 }
612 }
613}
614
615impl From<TerminalMouseEvent> for MouseEvent {
616 fn from(event: TerminalMouseEvent) -> Self {
617 use crate::mouse::MouseEventType as RustMouseEventType;
618
619 let event_type = match event.event_type {
620 RustMouseEventType::MouseDown => MouseEventType::MouseDown,
621 RustMouseEventType::MouseUp => MouseEventType::MouseUp,
622 RustMouseEventType::MouseMove => MouseEventType::MouseMove,
623 };
624
625 MouseEvent {
626 event_type,
627 col: event.col,
628 row: event.row,
629 button: event.button,
630 ctrl_key: event.ctrl_key,
631 shift_key: event.shift_key,
632 alt_key: event.alt_key,
633 }
634 }
635}
636
637#[wasm_bindgen(start)]
639pub fn main() {
640 console_error_panic_hook::set_once();
641 console::log_1(&"beamterm WASM module loaded".into());
642}