1use std::{cell::RefCell, rc::Rc};
2
3use beamterm_data::{FontAtlasData, Glyph};
4use compact_str::{CompactString, ToCompactString};
5use serde_wasm_bindgen::from_value;
6use unicode_segmentation::UnicodeSegmentation;
7use unicode_width::UnicodeWidthStr;
8use wasm_bindgen::prelude::*;
9use web_sys::console;
10
11use crate::{
12 gl::{
13 CellData, CellQuery as RustCellQuery, DynamicFontAtlas, Renderer,
14 SelectionMode as RustSelectionMode, StaticFontAtlas, TerminalGrid, select,
15 },
16 js::device_pixel_ratio,
17 mouse::{
18 DefaultSelectionHandler, ModifierKeys as RustModifierKeys, MouseSelectOptions,
19 TerminalMouseEvent, TerminalMouseHandler,
20 },
21};
22
23#[wasm_bindgen]
25#[derive(Debug)]
26pub struct BeamtermRenderer {
27 renderer: Renderer,
28 terminal_grid: Rc<RefCell<TerminalGrid>>,
29 mouse_handler: Option<TerminalMouseHandler>,
30 current_pixel_ratio: f32,
32}
33
34#[wasm_bindgen]
36#[derive(Debug, Default, serde::Deserialize)]
37pub struct Cell {
38 symbol: CompactString,
39 style: u16,
40 fg: u32,
41 bg: u32,
42}
43
44#[wasm_bindgen]
45#[derive(Debug, Clone, Copy)]
46pub struct CellStyle {
47 fg: u32,
48 bg: u32,
49 style_bits: u16,
50}
51
52#[wasm_bindgen]
53#[derive(Debug, Clone, Copy)]
54pub struct Size {
55 pub width: u16,
56 pub height: u16,
57}
58
59#[wasm_bindgen]
60#[derive(Debug)]
61pub struct Batch {
62 terminal_grid: Rc<RefCell<TerminalGrid>>,
63 gl: web_sys::WebGl2RenderingContext,
64}
65
66#[wasm_bindgen]
68#[derive(Debug, Clone, Copy)]
69pub enum SelectionMode {
70 Block,
72 Linear,
74}
75
76#[wasm_bindgen]
78#[derive(Debug, Clone, Copy)]
79pub enum MouseEventType {
80 MouseDown,
82 MouseUp,
84 MouseMove,
86}
87
88#[wasm_bindgen]
90#[derive(Debug, Clone, Copy)]
91pub struct MouseEvent {
92 pub event_type: MouseEventType,
94 pub col: u16,
96 pub row: u16,
98 pub button: i16,
100 pub ctrl_key: bool,
102 pub shift_key: bool,
104 pub alt_key: bool,
106 pub meta_key: bool,
108}
109
110#[wasm_bindgen]
118#[derive(Debug, Clone, Copy, Default)]
119pub struct ModifierKeys(u8);
120
121#[wasm_bindgen]
122#[allow(non_snake_case)]
123impl ModifierKeys {
124 #[wasm_bindgen(getter)]
126 pub fn NONE() -> ModifierKeys {
127 ModifierKeys(0)
128 }
129
130 #[wasm_bindgen(getter)]
132 pub fn CONTROL() -> ModifierKeys {
133 ModifierKeys(RustModifierKeys::CONTROL.bits())
134 }
135
136 #[wasm_bindgen(getter)]
138 pub fn SHIFT() -> ModifierKeys {
139 ModifierKeys(RustModifierKeys::SHIFT.bits())
140 }
141
142 #[wasm_bindgen(getter)]
144 pub fn ALT() -> ModifierKeys {
145 ModifierKeys(RustModifierKeys::ALT.bits())
146 }
147
148 #[wasm_bindgen(getter)]
150 pub fn META() -> ModifierKeys {
151 ModifierKeys(RustModifierKeys::META.bits())
152 }
153
154 #[wasm_bindgen(js_name = "or")]
156 pub fn or(&self, other: &ModifierKeys) -> ModifierKeys {
157 ModifierKeys(self.0 | other.0)
158 }
159}
160
161#[wasm_bindgen]
163#[derive(Debug, Clone)]
164pub struct CellQuery {
165 inner: RustCellQuery,
166}
167
168#[wasm_bindgen]
169impl CellQuery {
170 #[wasm_bindgen(constructor)]
172 pub fn new(mode: SelectionMode) -> CellQuery {
173 CellQuery { inner: select(mode.into()) }
174 }
175
176 pub fn start(mut self, col: u16, row: u16) -> CellQuery {
178 self.inner = self.inner.start((col, row));
179 self
180 }
181
182 pub fn end(mut self, col: u16, row: u16) -> CellQuery {
184 self.inner = self.inner.end((col, row));
185 self
186 }
187
188 #[wasm_bindgen(js_name = "trimTrailingWhitespace")]
190 pub fn trim_trailing_whitespace(mut self, enabled: bool) -> CellQuery {
191 self.inner = self.inner.trim_trailing_whitespace(enabled);
192 self
193 }
194
195 #[wasm_bindgen(js_name = "isEmpty")]
197 pub fn is_empty(&self) -> bool {
198 self.inner.is_empty()
199 }
200}
201
202#[wasm_bindgen]
203pub fn style() -> CellStyle {
204 CellStyle::new()
205}
206
207#[wasm_bindgen]
208pub fn cell(symbol: &str, style: CellStyle) -> Cell {
209 Cell {
210 symbol: symbol.into(),
211 style: style.style_bits,
212 fg: style.fg,
213 bg: style.bg,
214 }
215}
216
217#[wasm_bindgen]
218impl CellStyle {
219 #[wasm_bindgen(constructor)]
221 pub fn new() -> CellStyle {
222 Default::default()
223 }
224
225 #[wasm_bindgen]
227 pub fn fg(mut self, color: u32) -> CellStyle {
228 self.fg = color;
229 self
230 }
231
232 #[wasm_bindgen]
234 pub fn bg(mut self, color: u32) -> CellStyle {
235 self.bg = color;
236 self
237 }
238
239 #[wasm_bindgen]
241 pub fn bold(mut self) -> CellStyle {
242 self.style_bits |= Glyph::BOLD_FLAG;
243 self
244 }
245
246 #[wasm_bindgen]
248 pub fn italic(mut self) -> CellStyle {
249 self.style_bits |= Glyph::ITALIC_FLAG;
250 self
251 }
252
253 #[wasm_bindgen]
255 pub fn underline(mut self) -> CellStyle {
256 self.style_bits |= Glyph::UNDERLINE_FLAG;
257 self
258 }
259
260 #[wasm_bindgen]
262 pub fn strikethrough(mut self) -> CellStyle {
263 self.style_bits |= Glyph::STRIKETHROUGH_FLAG;
264 self
265 }
266
267 #[wasm_bindgen(getter)]
269 pub fn bits(&self) -> u16 {
270 self.style_bits
271 }
272}
273
274impl Default for CellStyle {
275 fn default() -> Self {
276 CellStyle {
277 fg: 0xFFFFFF, bg: 0x000000, style_bits: 0, }
281 }
282}
283
284#[wasm_bindgen]
285impl Batch {
286 #[wasm_bindgen(js_name = "cell")]
288 pub fn cell(&mut self, x: u16, y: u16, cell_data: &Cell) {
289 let _ = self
290 .terminal_grid
291 .borrow_mut()
292 .update_cell(x, y, cell_data.as_cell_data());
293 }
294
295 #[wasm_bindgen(js_name = "cellByIndex")]
297 pub fn cell_by_index(&mut self, idx: usize, cell_data: &Cell) {
298 let _ = self
299 .terminal_grid
300 .borrow_mut()
301 .update_cell_by_index(idx, cell_data.as_cell_data());
302 }
303
304 #[wasm_bindgen(js_name = "cells")]
307 pub fn cells(&mut self, cells_json: JsValue) -> Result<(), JsValue> {
308 let updates = from_value::<Vec<(u16, u16, Cell)>>(cells_json)
309 .map_err(|e| JsValue::from_str(&e.to_string()));
310
311 match updates {
312 Ok(cells) => {
313 let cell_data = cells
314 .iter()
315 .map(|(x, y, data)| (*x, *y, data.as_cell_data()));
316
317 let mut terminal_grid = self.terminal_grid.borrow_mut();
318 terminal_grid
319 .update_cells_by_position(cell_data)
320 .map_err(|e| JsValue::from_str(&e.to_string()))
321 },
322 e => e.map(|_| ()),
323 }
324 }
325
326 #[wasm_bindgen(js_name = "text")]
328 pub fn text(&mut self, x: u16, y: u16, text: &str, style: &CellStyle) -> Result<(), JsValue> {
329 let mut terminal_grid = self.terminal_grid.borrow_mut();
330 let (cols, rows) = terminal_grid.terminal_size();
331
332 if y >= rows {
333 return Ok(()); }
335
336 let mut col_offset: u16 = 0;
337 for ch in text.graphemes(true) {
338 let char_width = if ch.len() == 1 { 1 } else { ch.width() };
339
340 if char_width == 0 {
342 continue;
343 }
344
345 let current_col = x + col_offset;
346 if current_col >= cols {
347 break;
348 }
349
350 let cell = CellData::new_with_style_bits(ch, style.style_bits, style.fg, style.bg);
351 terminal_grid
352 .update_cell(current_col, y, cell)
353 .map_err(|e| JsValue::from_str(&e.to_string()))?;
354
355 col_offset += char_width as u16;
356 }
357
358 Ok(())
359 }
360
361 #[wasm_bindgen(js_name = "fill")]
363 pub fn fill(
364 &mut self,
365 x: u16,
366 y: u16,
367 width: u16,
368 height: u16,
369 cell_data: &Cell,
370 ) -> Result<(), JsValue> {
371 let mut terminal_grid = self.terminal_grid.borrow_mut();
372 let (cols, rows) = terminal_grid.terminal_size();
373
374 let width = (x + width).min(cols).saturating_sub(x);
375 let height = (y + height).min(rows).saturating_sub(y);
376
377 let fill_cell = cell_data.as_cell_data();
378 for y in y..y + height {
379 for x in x..x + width {
380 terminal_grid
381 .update_cell(x, y, fill_cell)
382 .map_err(|e| JsValue::from_str(&e.to_string()))?;
383 }
384 }
385
386 Ok(())
387 }
388
389 #[wasm_bindgen]
391 pub fn clear(&mut self, bg: u32) -> Result<(), JsValue> {
392 let mut terminal_grid = self.terminal_grid.borrow_mut();
393 let (cols, rows) = terminal_grid.terminal_size();
394
395 let clear_cell = CellData::new_with_style_bits(" ", 0, 0xFFFFFF, bg);
396 for y in 0..rows {
397 for x in 0..cols {
398 terminal_grid
399 .update_cell(x, y, clear_cell)
400 .map_err(|e| JsValue::from_str(&e.to_string()))?;
401 }
402 }
403
404 Ok(())
405 }
406
407 #[wasm_bindgen]
409 #[deprecated(since = "0.4.0", note = "no-op, flush is now automatic")]
410 #[allow(deprecated)]
411 pub fn flush(&mut self) -> Result<(), JsValue> {
412 Ok(())
413 }
414}
415
416#[wasm_bindgen]
417impl Cell {
418 #[wasm_bindgen(constructor)]
419 pub fn new(symbol: String, style: &CellStyle) -> Cell {
420 Cell {
421 symbol: symbol.into(),
422 style: style.style_bits,
423 fg: style.fg,
424 bg: style.bg,
425 }
426 }
427
428 #[wasm_bindgen(getter)]
429 pub fn symbol(&self) -> String {
430 self.symbol.to_string()
431 }
432
433 #[wasm_bindgen(setter)]
434 pub fn set_symbol(&mut self, symbol: String) {
435 self.symbol = symbol.into();
436 }
437
438 #[wasm_bindgen(getter)]
439 pub fn fg(&self) -> u32 {
440 self.fg
441 }
442
443 #[wasm_bindgen(setter)]
444 pub fn set_fg(&mut self, color: u32) {
445 self.fg = color;
446 }
447
448 #[wasm_bindgen(getter)]
449 pub fn bg(&self) -> u32 {
450 self.bg
451 }
452
453 #[wasm_bindgen(setter)]
454 pub fn set_bg(&mut self, color: u32) {
455 self.bg = color;
456 }
457
458 #[wasm_bindgen(getter)]
459 pub fn style(&self) -> u16 {
460 self.style
461 }
462
463 #[wasm_bindgen(setter)]
464 pub fn set_style(&mut self, style: u16) {
465 self.style = style;
466 }
467}
468
469impl Cell {
470 pub fn as_cell_data(&self) -> CellData<'_> {
471 CellData::new_with_style_bits(&self.symbol, self.style, self.fg, self.bg)
472 }
473}
474
475#[wasm_bindgen]
476impl BeamtermRenderer {
477 #[wasm_bindgen(constructor)]
479 pub fn new(canvas_id: &str) -> Result<BeamtermRenderer, JsValue> {
480 Self::with_static_atlas(canvas_id, None)
481 }
482
483 #[wasm_bindgen(js_name = "withStaticAtlas")]
489 pub fn with_static_atlas(
490 canvas_id: &str,
491 atlas_data: Option<js_sys::Uint8Array>,
492 ) -> Result<BeamtermRenderer, JsValue> {
493 console_error_panic_hook::set_once();
494
495 let mut renderer = Renderer::create(canvas_id)
497 .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {e}")))?;
498 let current_pixel_ratio = crate::js::device_pixel_ratio();
499 renderer.set_pixel_ratio(current_pixel_ratio);
500 let (w, h) = renderer.logical_size();
501 renderer.resize(w, h);
502
503 let gl = renderer.gl();
504 let atlas_config = match atlas_data {
505 Some(data) => {
506 let bytes = data.to_vec();
507 FontAtlasData::from_binary(&bytes)
508 .map_err(|e| JsValue::from_str(&format!("Failed to parse atlas data: {e:?}")))?
509 },
510 None => FontAtlasData::default(),
511 };
512
513 let atlas = StaticFontAtlas::load(gl, atlas_config)
514 .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {e}")))?;
515
516 let canvas_size = renderer.physical_size();
517 let terminal_grid =
518 TerminalGrid::new(gl, atlas.into(), canvas_size, current_pixel_ratio)
519 .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {e}")))?;
520
521 let terminal_grid = Rc::new(RefCell::new(terminal_grid));
522 Ok(BeamtermRenderer {
523 renderer,
524 terminal_grid,
525 mouse_handler: None,
526 current_pixel_ratio,
527 })
528 }
529
530 #[wasm_bindgen(js_name = "withDynamicAtlas")]
549 pub fn with_dynamic_atlas(
550 canvas_id: &str,
551 font_family: js_sys::Array,
552 font_size: f32,
553 ) -> Result<BeamtermRenderer, JsValue> {
554 console_error_panic_hook::set_once();
555
556 let mut renderer = Renderer::create(canvas_id)
558 .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {e}")))?;
559 let current_pixel_ratio = crate::js::device_pixel_ratio();
560 renderer.set_pixel_ratio(current_pixel_ratio);
561 let (w, h) = renderer.logical_size();
562 renderer.resize(w, h);
563
564 let font_families: Vec<CompactString> = font_family
565 .iter()
566 .filter_map(|v| v.as_string())
567 .map(|s| s.to_compact_string())
568 .collect();
569
570 if font_families.is_empty() {
571 return Err(JsValue::from_str("font_family array cannot be empty"));
572 }
573
574 let gl = renderer.gl();
575 let atlas = DynamicFontAtlas::new(gl, &font_families, font_size, current_pixel_ratio, None)
576 .map_err(|e| JsValue::from_str(&format!("Failed to create dynamic atlas: {e}")))?;
577
578 let canvas_size = renderer.physical_size();
579 let terminal_grid =
580 TerminalGrid::new(gl, atlas.into(), canvas_size, current_pixel_ratio)
581 .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {e}")))?;
582
583 let terminal_grid = Rc::new(RefCell::new(terminal_grid));
584 Ok(BeamtermRenderer {
585 renderer,
586 terminal_grid,
587 mouse_handler: None,
588 current_pixel_ratio,
589 })
590 }
591
592 #[wasm_bindgen(js_name = "enableSelection")]
594 pub fn enable_selection(
595 &mut self,
596 mode: SelectionMode,
597 trim_whitespace: bool,
598 ) -> Result<(), JsValue> {
599 self.enable_selection_internal(mode, trim_whitespace, ModifierKeys::default())
600 }
601
602 #[wasm_bindgen(js_name = "enableSelectionWithOptions")]
629 pub fn enable_selection_with_options(
630 &mut self,
631 mode: SelectionMode,
632 trim_whitespace: bool,
633 require_modifiers: &ModifierKeys,
634 ) -> Result<(), JsValue> {
635 self.enable_selection_internal(mode, trim_whitespace, *require_modifiers)
636 }
637
638 fn enable_selection_internal(
639 &mut self,
640 mode: SelectionMode,
641 trim_whitespace: bool,
642 require_modifiers: ModifierKeys,
643 ) -> Result<(), JsValue> {
644 if let Some(old_handler) = self.mouse_handler.take() {
646 old_handler.cleanup();
647 }
648
649 let selection_tracker = self.terminal_grid.borrow().selection_tracker();
650 let options = MouseSelectOptions::new()
651 .selection_mode(mode.into())
652 .trim_trailing_whitespace(trim_whitespace)
653 .require_modifier_keys(require_modifiers.into());
654 let handler = DefaultSelectionHandler::new(self.terminal_grid.clone(), options);
655
656 let mut mouse_handler = TerminalMouseHandler::new(
657 self.renderer.canvas(),
658 self.terminal_grid.clone(),
659 handler.create_event_handler(selection_tracker),
660 )
661 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {e}")))?;
662
663 self.update_mouse_metrics(&mut mouse_handler);
664
665 self.mouse_handler = Some(mouse_handler);
666 Ok(())
667 }
668
669 #[wasm_bindgen(js_name = "setMouseHandler")]
671 pub fn set_mouse_handler(&mut self, handler: js_sys::Function) -> Result<(), JsValue> {
672 if let Some(old_handler) = self.mouse_handler.take() {
674 old_handler.cleanup();
675 }
676
677 let handler_closure = {
678 let handler = handler.clone();
679 move |event: TerminalMouseEvent, _grid: &TerminalGrid| {
680 let js_event = MouseEvent::from(event);
681 let this = JsValue::null();
682 let args = js_sys::Array::new();
683 args.push(&JsValue::from(js_event));
684
685 if let Err(e) = handler.apply(&this, &args) {
686 console::error_1(&format!("Mouse handler error: {e:?}").into());
687 }
688 }
689 };
690
691 let mut mouse_handler = TerminalMouseHandler::new(
692 self.renderer.canvas(),
693 self.terminal_grid.clone(),
694 handler_closure,
695 )
696 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {e}")))?;
697
698 self.update_mouse_metrics(&mut mouse_handler);
699
700 self.mouse_handler = Some(mouse_handler);
701 Ok(())
702 }
703
704 #[wasm_bindgen(js_name = "getText")]
706 pub fn get_text(&self, query: &CellQuery) -> String {
707 self.terminal_grid
708 .borrow()
709 .get_text(query.inner)
710 .to_string()
711 }
712
713 #[wasm_bindgen(js_name = "copyToClipboard")]
715 pub fn copy_to_clipboard(&self, text: &str) {
716 use wasm_bindgen_futures::spawn_local;
717 let text = text.to_string();
718
719 spawn_local(async move {
720 if let Some(window) = web_sys::window() {
721 let clipboard = window.navigator().clipboard();
722 match wasm_bindgen_futures::JsFuture::from(clipboard.write_text(&text)).await {
723 Ok(_) => {
724 console::log_1(
725 &format!("Copied {} characters to clipboard", text.len()).into(),
726 );
727 },
728 Err(err) => {
729 console::error_1(&format!("Failed to copy to clipboard: {err:?}").into());
730 },
731 }
732 }
733 });
734 }
735
736 #[wasm_bindgen(js_name = "clearSelection")]
738 pub fn clear_selection(&self) {
739 self.terminal_grid
740 .borrow()
741 .selection_tracker()
742 .clear();
743 }
744
745 #[wasm_bindgen(js_name = "hasSelection")]
747 pub fn has_selection(&self) -> bool {
748 self.terminal_grid
749 .borrow()
750 .selection_tracker()
751 .get_query()
752 .is_some()
753 }
754
755 #[wasm_bindgen(js_name = "batch")]
757 pub fn new_render_batch(&mut self) -> Batch {
758 let gl = self.renderer.gl().clone();
759 let terminal_grid = self.terminal_grid.clone();
760 Batch { terminal_grid, gl }
761 }
762
763 #[wasm_bindgen(js_name = "terminalSize")]
765 pub fn terminal_size(&self) -> Size {
766 let (cols, rows) = self.terminal_grid.borrow().terminal_size();
767 Size { width: cols, height: rows }
768 }
769
770 #[wasm_bindgen(js_name = "cellSize")]
772 pub fn cell_size(&self) -> Size {
773 let (width, height) = self.terminal_grid.borrow().cell_size();
774 Size { width: width as u16, height: height as u16 }
775 }
776
777 #[wasm_bindgen]
779 pub fn render(&mut self) {
780 let raw_dpr = device_pixel_ratio();
782 if (raw_dpr - self.current_pixel_ratio).abs() > f32::EPSILON {
783 let _ = self.handle_pixel_ratio_change(raw_dpr);
784 }
785
786 let mut grid = self.terminal_grid.borrow_mut();
787 let _ = grid.flush_cells(self.renderer.gl());
788
789 self.renderer.begin_frame();
790 self.renderer.render(&*grid);
791 self.renderer.end_frame();
792 }
793
794 fn handle_pixel_ratio_change(&mut self, raw_pixel_ratio: f32) -> Result<(), JsValue> {
798 self.current_pixel_ratio = raw_pixel_ratio;
799
800 let gl = self.renderer.gl();
801
802 self.terminal_grid
804 .borrow_mut()
805 .atlas_mut()
806 .update_pixel_ratio(gl, raw_pixel_ratio)
807 .map_err(|e| JsValue::from_str(&format!("Failed to update pixel ratio: {e}")))?;
808
809 self.renderer.set_pixel_ratio(raw_pixel_ratio);
811
812 let (w, h) = self.renderer.logical_size();
814 self.resize(w, h)
815 }
816
817 #[wasm_bindgen]
819 pub fn resize(&mut self, width: i32, height: i32) -> Result<(), JsValue> {
820 self.renderer.resize(width, height);
821
822 let gl = self.renderer.gl();
823 let physical_size = self.renderer.physical_size();
824 self.terminal_grid
825 .borrow_mut()
826 .resize(gl, physical_size, self.current_pixel_ratio)
827 .map_err(|e| JsValue::from_str(&format!("Failed to resize: {e}")))?;
828
829 self.update_mouse_handler_metrics();
830
831 Ok(())
832 }
833
834 fn update_mouse_handler_metrics(&mut self) {
836 if let Some(mouse_handler) = &mut self.mouse_handler {
837 let grid = self.terminal_grid.borrow();
838 let (cols, rows) = grid.terminal_size();
839 let (phys_width, phys_height) = grid.cell_size();
840 let cell_width = phys_width as f32 / self.current_pixel_ratio;
841 let cell_height = phys_height as f32 / self.current_pixel_ratio;
842 mouse_handler.update_metrics(cols, rows, cell_width, cell_height);
843 }
844 }
845
846 #[wasm_bindgen(js_name = "replaceWithStaticAtlas")]
860 pub fn replace_with_static_atlas(
861 &mut self,
862 atlas_data: Option<js_sys::Uint8Array>,
863 ) -> Result<(), JsValue> {
864 let gl = self.renderer.gl();
865
866 let atlas_config = match atlas_data {
867 Some(data) => {
868 let bytes = data.to_vec();
869 FontAtlasData::from_binary(&bytes)
870 .map_err(|e| JsValue::from_str(&format!("Failed to parse atlas data: {e:?}")))?
871 },
872 None => FontAtlasData::default(),
873 };
874
875 let atlas = StaticFontAtlas::load(gl, atlas_config)
876 .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {e}")))?;
877
878 self.terminal_grid
879 .borrow_mut()
880 .replace_atlas(gl, atlas.into());
881
882 self.update_mouse_handler_metrics();
883
884 Ok(())
885 }
886
887 #[wasm_bindgen(js_name = "replaceWithDynamicAtlas")]
902 pub fn replace_with_dynamic_atlas(
903 &mut self,
904 font_family: js_sys::Array,
905 font_size: f32,
906 ) -> Result<(), JsValue> {
907 let font_families: Vec<CompactString> = font_family
908 .iter()
909 .filter_map(|v| v.as_string())
910 .map(|s| s.to_compact_string())
911 .collect();
912
913 if font_families.is_empty() {
914 return Err(JsValue::from_str("font_family array cannot be empty"));
915 }
916
917 let gl = self.renderer.gl();
918 let pixel_ratio = device_pixel_ratio();
919 let atlas = DynamicFontAtlas::new(gl, &font_families, font_size, pixel_ratio, None)
920 .map_err(|e| JsValue::from_str(&format!("Failed to create dynamic atlas: {e}")))?;
921
922 self.terminal_grid
923 .borrow_mut()
924 .replace_atlas(gl, atlas.into());
925
926 self.update_mouse_handler_metrics();
927
928 Ok(())
929 }
930
931 fn update_mouse_metrics(&mut self, mouse_handler: &mut TerminalMouseHandler) {
932 let grid = self.terminal_grid.borrow();
933 let (cols, rows) = grid.terminal_size();
934 let (phys_w, phys_h) = grid.cell_size();
935 let css_w = phys_w as f32 / self.current_pixel_ratio;
936 let css_h = phys_h as f32 / self.current_pixel_ratio;
937 mouse_handler.update_metrics(cols, rows, css_w, css_h);
938 }
939}
940
941impl From<SelectionMode> for RustSelectionMode {
943 fn from(mode: SelectionMode) -> Self {
944 match mode {
945 SelectionMode::Block => RustSelectionMode::Block,
946 SelectionMode::Linear => RustSelectionMode::Linear,
947 }
948 }
949}
950
951impl From<RustSelectionMode> for SelectionMode {
952 fn from(mode: RustSelectionMode) -> Self {
953 match mode {
954 RustSelectionMode::Block => SelectionMode::Block,
955 RustSelectionMode::Linear => SelectionMode::Linear,
956 }
957 }
958}
959
960impl From<TerminalMouseEvent> for MouseEvent {
961 fn from(event: TerminalMouseEvent) -> Self {
962 use crate::mouse::MouseEventType as RustMouseEventType;
963
964 let event_type = match event.event_type {
965 RustMouseEventType::MouseDown => MouseEventType::MouseDown,
966 RustMouseEventType::MouseUp => MouseEventType::MouseUp,
967 RustMouseEventType::MouseMove => MouseEventType::MouseMove,
968 };
969
970 MouseEvent {
971 event_type,
972 col: event.col,
973 row: event.row,
974 button: event.button(),
975 ctrl_key: event.ctrl_key(),
976 shift_key: event.shift_key(),
977 alt_key: event.alt_key(),
978 meta_key: event.meta_key(),
979 }
980 }
981}
982
983impl From<ModifierKeys> for RustModifierKeys {
984 fn from(keys: ModifierKeys) -> Self {
985 RustModifierKeys::from_bits_truncate(keys.0)
986 }
987}
988
989#[wasm_bindgen(start)]
991pub fn main() {
992 console_error_panic_hook::set_once();
993 console::log_1(&"beamterm WASM module loaded".into());
994}