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}
58
59#[wasm_bindgen]
61#[derive(Debug, Clone, Copy)]
62pub enum SelectionMode {
63 Block,
65 Linear,
67}
68
69#[wasm_bindgen]
71#[derive(Debug, Clone, Copy)]
72pub enum MouseEventType {
73 MouseDown,
75 MouseUp,
77 MouseMove,
79}
80
81#[wasm_bindgen]
83#[derive(Debug, Clone, Copy)]
84pub struct MouseEvent {
85 pub event_type: MouseEventType,
87 pub col: u16,
89 pub row: u16,
91 pub button: i16,
93 pub ctrl_key: bool,
95 pub shift_key: bool,
97 pub alt_key: bool,
99}
100
101#[wasm_bindgen]
103#[derive(Debug, Clone)]
104pub struct CellQuery {
105 inner: RustCellQuery,
106}
107
108#[wasm_bindgen]
109impl CellQuery {
110 #[wasm_bindgen(constructor)]
112 pub fn new(mode: SelectionMode) -> CellQuery {
113 CellQuery { inner: select(mode.into()) }
114 }
115
116 pub fn start(mut self, col: u16, row: u16) -> CellQuery {
118 self.inner = self.inner.start((col, row));
119 self
120 }
121
122 pub fn end(mut self, col: u16, row: u16) -> CellQuery {
124 self.inner = self.inner.end((col, row));
125 self
126 }
127
128 #[wasm_bindgen(js_name = "trimTrailingWhitespace")]
130 pub fn trim_trailing_whitespace(mut self, enabled: bool) -> CellQuery {
131 self.inner = self.inner.trim_trailing_whitespace(enabled);
132 self
133 }
134
135 #[wasm_bindgen(js_name = "isEmpty")]
137 pub fn is_empty(&self) -> bool {
138 self.inner.is_empty()
139 }
140}
141
142#[wasm_bindgen]
143pub fn style() -> CellStyle {
144 CellStyle::new()
145}
146
147#[wasm_bindgen]
148pub fn cell(symbol: &str, style: CellStyle) -> Cell {
149 Cell {
150 symbol: symbol.into(),
151 style: style.style_bits,
152 fg: style.fg,
153 bg: style.bg,
154 }
155}
156
157#[wasm_bindgen]
158impl CellStyle {
159 #[wasm_bindgen(constructor)]
161 pub fn new() -> CellStyle {
162 Default::default()
163 }
164
165 #[wasm_bindgen]
167 pub fn fg(mut self, color: u32) -> CellStyle {
168 self.fg = color;
169 self
170 }
171
172 #[wasm_bindgen]
174 pub fn bg(mut self, color: u32) -> CellStyle {
175 self.bg = color;
176 self
177 }
178
179 #[wasm_bindgen]
181 pub fn bold(mut self) -> CellStyle {
182 self.style_bits |= Glyph::BOLD_FLAG;
183 self
184 }
185
186 #[wasm_bindgen]
188 pub fn italic(mut self) -> CellStyle {
189 self.style_bits |= Glyph::ITALIC_FLAG;
190 self
191 }
192
193 #[wasm_bindgen]
195 pub fn underline(mut self) -> CellStyle {
196 self.style_bits |= Glyph::UNDERLINE_FLAG;
197 self
198 }
199
200 #[wasm_bindgen]
202 pub fn strikethrough(mut self) -> CellStyle {
203 self.style_bits |= Glyph::STRIKETHROUGH_FLAG;
204 self
205 }
206
207 #[wasm_bindgen(getter)]
209 pub fn bits(&self) -> u16 {
210 self.style_bits
211 }
212}
213
214impl Default for CellStyle {
215 fn default() -> Self {
216 CellStyle {
217 fg: 0xFFFFFF, bg: 0x000000, style_bits: 0, }
221 }
222}
223
224#[wasm_bindgen]
225impl Batch {
226 #[wasm_bindgen(js_name = "cell")]
228 pub fn cell(&mut self, x: u16, y: u16, cell_data: &Cell) {
229 self.terminal_grid
230 .borrow_mut()
231 .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.terminal_grid
238 .borrow_mut()
239 .update_cell_by_index(idx, cell_data.as_cell_data());
240 }
241
242 #[wasm_bindgen(js_name = "cells")]
245 pub fn cells(&mut self, cells_json: JsValue) -> Result<(), JsValue> {
246 let updates = from_value::<Vec<(u16, u16, Cell)>>(cells_json)
247 .map_err(|e| JsValue::from_str(&e.to_string()));
248
249 match updates {
250 Ok(cells) => {
251 let cell_data = cells
252 .iter()
253 .map(|(x, y, data)| (*x, *y, data.as_cell_data()));
254
255 let mut terminal_grid = self.terminal_grid.borrow_mut();
256 terminal_grid
257 .update_cells_by_position(&self.gl, cell_data)
258 .map_err(|e| JsValue::from_str(&e.to_string()))
259 },
260 e => e.map(|_| ()),
261 }
262 }
263
264 #[wasm_bindgen(js_name = "text")]
266 pub fn text(&mut self, x: u16, y: u16, text: &str, style: &CellStyle) -> Result<(), JsValue> {
267 let mut terminal_grid = self.terminal_grid.borrow_mut();
268 let (cols, rows) = terminal_grid.terminal_size();
269
270 if y >= rows {
271 return Ok(()); }
273
274 let mut width_offset: u16 = 0;
275 for (i, ch) in text.graphemes(true).enumerate() {
276 let current_col = x + width_offset + i as u16;
277 if current_col >= cols {
278 break;
279 }
280
281 let cell = CellData::new_with_style_bits(ch, style.style_bits, style.fg, style.bg);
282 terminal_grid.update_cell(current_col, y, cell);
283
284 if emojis::get(ch).is_some() {
285 width_offset += 1;
286 }
287 }
288
289 Ok(())
290 }
291
292 #[wasm_bindgen(js_name = "fill")]
294 pub fn fill(
295 &mut self,
296 x: u16,
297 y: u16,
298 width: u16,
299 height: u16,
300 cell_data: &Cell,
301 ) -> Result<(), JsValue> {
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 let mut terminal_grid = self.terminal_grid.borrow_mut();
322 let (cols, rows) = terminal_grid.terminal_size();
323
324 let clear_cell = CellData::new_with_style_bits(" ", 0, 0xFFFFFF, bg);
325 for y in 0..rows {
326 for x in 0..cols {
327 terminal_grid.update_cell(x, y, clear_cell);
328 }
329 }
330
331 Ok(())
332 }
333
334 #[wasm_bindgen]
336 #[deprecated(since = "0.4.0", note = "no-op, flush is now automatic")]
337 #[allow(deprecated)]
338 pub fn flush(&mut self) -> Result<(), JsValue> {
339 Ok(())
340 }
341}
342
343#[wasm_bindgen]
344impl Cell {
345 #[wasm_bindgen(constructor)]
346 pub fn new(symbol: String, style: &CellStyle) -> Cell {
347 Cell {
348 symbol: symbol.into(),
349 style: style.style_bits,
350 fg: style.fg,
351 bg: style.bg,
352 }
353 }
354
355 #[wasm_bindgen(getter)]
356 pub fn symbol(&self) -> String {
357 self.symbol.to_string()
358 }
359
360 #[wasm_bindgen(setter)]
361 pub fn set_symbol(&mut self, symbol: String) {
362 self.symbol = symbol.into();
363 }
364
365 #[wasm_bindgen(getter)]
366 pub fn fg(&self) -> u32 {
367 self.fg
368 }
369
370 #[wasm_bindgen(setter)]
371 pub fn set_fg(&mut self, color: u32) {
372 self.fg = color;
373 }
374
375 #[wasm_bindgen(getter)]
376 pub fn bg(&self) -> u32 {
377 self.bg
378 }
379
380 #[wasm_bindgen(setter)]
381 pub fn set_bg(&mut self, color: u32) {
382 self.bg = color;
383 }
384
385 #[wasm_bindgen(getter)]
386 pub fn style(&self) -> u16 {
387 self.style
388 }
389
390 #[wasm_bindgen(setter)]
391 pub fn set_style(&mut self, style: u16) {
392 self.style = style;
393 }
394}
395
396impl Cell {
397 pub fn as_cell_data(&self) -> CellData<'_> {
398 CellData::new_with_style_bits(&self.symbol, self.style, self.fg, self.bg)
399 }
400}
401
402#[wasm_bindgen]
403impl BeamtermRenderer {
404 #[wasm_bindgen(constructor)]
406 pub fn new(canvas_id: &str) -> Result<BeamtermRenderer, JsValue> {
407 console_error_panic_hook::set_once();
408
409 let renderer = Renderer::create(canvas_id)
410 .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {e}")))?;
411
412 let gl = renderer.gl();
413 let atlas_data = FontAtlasData::default();
414 let atlas = FontAtlas::load(gl, atlas_data)
415 .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {e}")))?;
416
417 let canvas_size = renderer.canvas_size();
418 let terminal_grid = TerminalGrid::new(gl, atlas, canvas_size)
419 .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {e}")))?;
420
421 console::log_1(&"BeamtermRenderer initialized successfully".into());
422 let terminal_grid = Rc::new(RefCell::new(terminal_grid));
423 Ok(BeamtermRenderer { renderer, terminal_grid, mouse_handler: None })
424 }
425
426 #[wasm_bindgen(js_name = "enableSelection")]
428 pub fn enable_selection(
429 &mut self,
430 mode: SelectionMode,
431 trim_whitespace: bool,
432 ) -> Result<(), JsValue> {
433 if let Some(old_handler) = self.mouse_handler.take() {
435 old_handler.cleanup();
436 }
437
438 let selection_tracker = self.terminal_grid.borrow().selection_tracker();
439 let handler =
440 DefaultSelectionHandler::new(self.terminal_grid.clone(), mode.into(), trim_whitespace);
441
442 let mouse_handler = TerminalMouseHandler::new(
443 self.renderer.canvas(),
444 self.terminal_grid.clone(),
445 handler.create_event_handler(selection_tracker),
446 )
447 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {e}")))?;
448
449 self.mouse_handler = Some(mouse_handler);
450 Ok(())
451 }
452
453 #[wasm_bindgen(js_name = "setMouseHandler")]
455 pub fn set_mouse_handler(&mut self, handler: js_sys::Function) -> Result<(), JsValue> {
456 if let Some(old_handler) = self.mouse_handler.take() {
458 old_handler.cleanup();
459 }
460
461 let handler_closure = {
462 let handler = handler.clone();
463 move |event: TerminalMouseEvent, _grid: &TerminalGrid| {
464 let js_event = MouseEvent::from(event);
465 let this = JsValue::null();
466 let args = js_sys::Array::new();
467 args.push(&JsValue::from(js_event));
468
469 if let Err(e) = handler.apply(&this, &args) {
470 console::error_1(&format!("Mouse handler error: {e:?}").into());
471 }
472 }
473 };
474
475 let mouse_handler = TerminalMouseHandler::new(
476 self.renderer.canvas(),
477 self.terminal_grid.clone(),
478 handler_closure,
479 )
480 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {e}")))?;
481
482 self.mouse_handler = Some(mouse_handler);
483 Ok(())
484 }
485
486 #[wasm_bindgen(js_name = "getText")]
488 pub fn get_text(&self, query: &CellQuery) -> String {
489 self.terminal_grid
490 .borrow()
491 .get_text(query.inner)
492 .to_string()
493 }
494
495 #[wasm_bindgen(js_name = "copyToClipboard")]
497 pub fn copy_to_clipboard(&self, text: &str) {
498 use wasm_bindgen_futures::spawn_local;
499 let text = text.to_string();
500
501 spawn_local(async move {
502 if let Some(window) = web_sys::window() {
503 let clipboard = window.navigator().clipboard();
504 match wasm_bindgen_futures::JsFuture::from(clipboard.write_text(&text)).await {
505 Ok(_) => {
506 console::log_1(
507 &format!("Copied {} characters to clipboard", text.len()).into(),
508 );
509 },
510 Err(err) => {
511 console::error_1(&format!("Failed to copy to clipboard: {err:?}").into());
512 },
513 }
514 }
515 });
516 }
517
518 #[wasm_bindgen(js_name = "clearSelection")]
520 pub fn clear_selection(&self) {
521 self.terminal_grid
522 .borrow()
523 .selection_tracker()
524 .clear();
525 }
526
527 #[wasm_bindgen(js_name = "hasSelection")]
529 pub fn has_selection(&self) -> bool {
530 self.terminal_grid
531 .borrow()
532 .selection_tracker()
533 .get_query()
534 .is_some()
535 }
536
537 #[wasm_bindgen(js_name = "batch")]
539 pub fn new_render_batch(&mut self) -> Batch {
540 let gl = self.renderer.gl().clone();
541 let terminal_grid = self.terminal_grid.clone();
542 Batch { terminal_grid, gl }
543 }
544
545 #[wasm_bindgen(js_name = "terminalSize")]
547 pub fn terminal_size(&self) -> Size {
548 let (cols, rows) = self.terminal_grid.borrow().terminal_size();
549 Size { width: cols, height: rows }
550 }
551
552 #[wasm_bindgen(js_name = "cellSize")]
554 pub fn cell_size(&self) -> Size {
555 let (width, height) = self.terminal_grid.borrow().cell_size();
556 Size { width: width as u16, height: height as u16 }
557 }
558
559 #[wasm_bindgen]
561 pub fn render(&mut self) {
562 let mut grid = self.terminal_grid.borrow_mut();
563 let _ = grid.flush_cells(self.renderer.gl());
564
565 self.renderer.begin_frame();
566 self.renderer.render(&*grid);
567 self.renderer.end_frame();
568 }
569
570 #[wasm_bindgen]
572 pub fn resize(&mut self, width: i32, height: i32) -> Result<(), JsValue> {
573 self.renderer.resize(width, height);
574
575 let gl = self.renderer.gl();
576 self.terminal_grid
577 .borrow_mut()
578 .resize(gl, (width, height))
579 .map_err(|e| JsValue::from_str(&format!("Failed to resize: {e}")))?;
580
581 if let Some(mouse_handler) = &mut self.mouse_handler {
583 let (cols, rows) = self.terminal_grid.borrow().terminal_size();
584 mouse_handler.update_dimensions(cols, rows);
585 }
586
587 Ok(())
588 }
589}
590
591impl From<SelectionMode> for RustSelectionMode {
593 fn from(mode: SelectionMode) -> Self {
594 match mode {
595 SelectionMode::Block => RustSelectionMode::Block,
596 SelectionMode::Linear => RustSelectionMode::Linear,
597 }
598 }
599}
600
601impl From<RustSelectionMode> for SelectionMode {
602 fn from(mode: RustSelectionMode) -> Self {
603 match mode {
604 RustSelectionMode::Block => SelectionMode::Block,
605 RustSelectionMode::Linear => SelectionMode::Linear,
606 }
607 }
608}
609
610impl From<TerminalMouseEvent> for MouseEvent {
611 fn from(event: TerminalMouseEvent) -> Self {
612 use crate::mouse::MouseEventType as RustMouseEventType;
613
614 let event_type = match event.event_type {
615 RustMouseEventType::MouseDown => MouseEventType::MouseDown,
616 RustMouseEventType::MouseUp => MouseEventType::MouseUp,
617 RustMouseEventType::MouseMove => MouseEventType::MouseMove,
618 };
619
620 MouseEvent {
621 event_type,
622 col: event.col,
623 row: event.row,
624 button: event.button(),
625 ctrl_key: event.ctrl_key(),
626 shift_key: event.shift_key(),
627 alt_key: event.alt_key(),
628 }
629 }
630}
631
632#[wasm_bindgen(start)]
634pub fn main() {
635 console_error_panic_hook::set_once();
636 console::log_1(&"beamterm WASM module loaded".into());
637}