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, None)
481 }
482
483 #[wasm_bindgen(js_name = "withStaticAtlas")]
492 pub fn with_static_atlas(
493 canvas_id: &str,
494 atlas_data: Option<js_sys::Uint8Array>,
495 auto_resize_canvas_css: Option<bool>,
496 ) -> Result<BeamtermRenderer, JsValue> {
497 console_error_panic_hook::set_once();
498
499 let auto_resize = auto_resize_canvas_css.unwrap_or(true);
500
501 let mut renderer = Renderer::create(canvas_id, auto_resize)
503 .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {e}")))?;
504 let current_pixel_ratio = crate::js::device_pixel_ratio();
505 renderer.set_pixel_ratio(current_pixel_ratio);
506 let (w, h) = renderer.logical_size();
507 renderer.resize(w, h);
508
509 let gl = renderer.gl();
510 let atlas_config = match atlas_data {
511 Some(data) => {
512 let bytes = data.to_vec();
513 FontAtlasData::from_binary(&bytes)
514 .map_err(|e| JsValue::from_str(&format!("Failed to parse atlas data: {e:?}")))?
515 },
516 None => FontAtlasData::default(),
517 };
518
519 let atlas = StaticFontAtlas::load(gl, atlas_config)
520 .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {e}")))?;
521
522 let canvas_size = renderer.physical_size();
523 let terminal_grid =
524 TerminalGrid::new(gl, atlas.into(), canvas_size, current_pixel_ratio)
525 .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {e}")))?;
526
527 let terminal_grid = Rc::new(RefCell::new(terminal_grid));
528 Ok(BeamtermRenderer {
529 renderer,
530 terminal_grid,
531 mouse_handler: None,
532 current_pixel_ratio,
533 })
534 }
535
536 #[wasm_bindgen(js_name = "withDynamicAtlas")]
558 pub fn with_dynamic_atlas(
559 canvas_id: &str,
560 font_family: js_sys::Array,
561 font_size: f32,
562 auto_resize_canvas_css: Option<bool>,
563 ) -> Result<BeamtermRenderer, JsValue> {
564 console_error_panic_hook::set_once();
565
566 let auto_resize = auto_resize_canvas_css.unwrap_or(true);
567
568 let mut renderer = Renderer::create(canvas_id, auto_resize)
570 .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {e}")))?;
571 let current_pixel_ratio = crate::js::device_pixel_ratio();
572 renderer.set_pixel_ratio(current_pixel_ratio);
573 let (w, h) = renderer.logical_size();
574 renderer.resize(w, h);
575
576 let font_families: Vec<CompactString> = font_family
577 .iter()
578 .filter_map(|v| v.as_string())
579 .map(|s| s.to_compact_string())
580 .collect();
581
582 if font_families.is_empty() {
583 return Err(JsValue::from_str("font_family array cannot be empty"));
584 }
585
586 let gl = renderer.gl();
587 let atlas = DynamicFontAtlas::new(gl, &font_families, font_size, current_pixel_ratio, None)
588 .map_err(|e| JsValue::from_str(&format!("Failed to create dynamic atlas: {e}")))?;
589
590 let canvas_size = renderer.physical_size();
591 let terminal_grid =
592 TerminalGrid::new(gl, atlas.into(), canvas_size, current_pixel_ratio)
593 .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {e}")))?;
594
595 let terminal_grid = Rc::new(RefCell::new(terminal_grid));
596 Ok(BeamtermRenderer {
597 renderer,
598 terminal_grid,
599 mouse_handler: None,
600 current_pixel_ratio,
601 })
602 }
603
604 #[wasm_bindgen(js_name = "enableSelection")]
606 pub fn enable_selection(
607 &mut self,
608 mode: SelectionMode,
609 trim_whitespace: bool,
610 ) -> Result<(), JsValue> {
611 self.enable_selection_internal(mode, trim_whitespace, ModifierKeys::default())
612 }
613
614 #[wasm_bindgen(js_name = "enableSelectionWithOptions")]
641 pub fn enable_selection_with_options(
642 &mut self,
643 mode: SelectionMode,
644 trim_whitespace: bool,
645 require_modifiers: &ModifierKeys,
646 ) -> Result<(), JsValue> {
647 self.enable_selection_internal(mode, trim_whitespace, *require_modifiers)
648 }
649
650 fn enable_selection_internal(
651 &mut self,
652 mode: SelectionMode,
653 trim_whitespace: bool,
654 require_modifiers: ModifierKeys,
655 ) -> Result<(), JsValue> {
656 if let Some(old_handler) = self.mouse_handler.take() {
658 old_handler.cleanup();
659 }
660
661 let selection_tracker = self.terminal_grid.borrow().selection_tracker();
662 let options = MouseSelectOptions::new()
663 .selection_mode(mode.into())
664 .trim_trailing_whitespace(trim_whitespace)
665 .require_modifier_keys(require_modifiers.into());
666 let handler = DefaultSelectionHandler::new(self.terminal_grid.clone(), options);
667
668 let mut mouse_handler = TerminalMouseHandler::new(
669 self.renderer.canvas(),
670 self.terminal_grid.clone(),
671 handler.create_event_handler(selection_tracker),
672 )
673 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {e}")))?;
674
675 self.update_mouse_metrics(&mut mouse_handler);
676
677 self.mouse_handler = Some(mouse_handler);
678 Ok(())
679 }
680
681 #[wasm_bindgen(js_name = "setMouseHandler")]
683 pub fn set_mouse_handler(&mut self, handler: js_sys::Function) -> Result<(), JsValue> {
684 if let Some(old_handler) = self.mouse_handler.take() {
686 old_handler.cleanup();
687 }
688
689 let handler_closure = {
690 let handler = handler.clone();
691 move |event: TerminalMouseEvent, _grid: &TerminalGrid| {
692 let js_event = MouseEvent::from(event);
693 let this = JsValue::null();
694 let args = js_sys::Array::new();
695 args.push(&JsValue::from(js_event));
696
697 if let Err(e) = handler.apply(&this, &args) {
698 console::error_1(&format!("Mouse handler error: {e:?}").into());
699 }
700 }
701 };
702
703 let mut mouse_handler = TerminalMouseHandler::new(
704 self.renderer.canvas(),
705 self.terminal_grid.clone(),
706 handler_closure,
707 )
708 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {e}")))?;
709
710 self.update_mouse_metrics(&mut mouse_handler);
711
712 self.mouse_handler = Some(mouse_handler);
713 Ok(())
714 }
715
716 #[wasm_bindgen(js_name = "getText")]
718 pub fn get_text(&self, query: &CellQuery) -> String {
719 self.terminal_grid
720 .borrow()
721 .get_text(query.inner)
722 .to_string()
723 }
724
725 #[wasm_bindgen(js_name = "copyToClipboard")]
727 pub fn copy_to_clipboard(&self, text: &str) {
728 use wasm_bindgen_futures::spawn_local;
729 let text = text.to_string();
730
731 spawn_local(async move {
732 if let Some(window) = web_sys::window() {
733 let clipboard = window.navigator().clipboard();
734 match wasm_bindgen_futures::JsFuture::from(clipboard.write_text(&text)).await {
735 Ok(_) => {
736 console::log_1(
737 &format!("Copied {} characters to clipboard", text.len()).into(),
738 );
739 },
740 Err(err) => {
741 console::error_1(&format!("Failed to copy to clipboard: {err:?}").into());
742 },
743 }
744 }
745 });
746 }
747
748 #[wasm_bindgen(js_name = "clearSelection")]
750 pub fn clear_selection(&self) {
751 self.terminal_grid
752 .borrow()
753 .selection_tracker()
754 .clear();
755 }
756
757 #[wasm_bindgen(js_name = "hasSelection")]
759 pub fn has_selection(&self) -> bool {
760 self.terminal_grid
761 .borrow()
762 .selection_tracker()
763 .get_query()
764 .is_some()
765 }
766
767 #[wasm_bindgen(js_name = "batch")]
769 pub fn new_render_batch(&mut self) -> Batch {
770 let gl = self.renderer.gl().clone();
771 let terminal_grid = self.terminal_grid.clone();
772 Batch { terminal_grid, gl }
773 }
774
775 #[wasm_bindgen(js_name = "terminalSize")]
777 pub fn terminal_size(&self) -> Size {
778 let (cols, rows) = self.terminal_grid.borrow().terminal_size();
779 Size { width: cols, height: rows }
780 }
781
782 #[wasm_bindgen(js_name = "cellSize")]
784 pub fn cell_size(&self) -> Size {
785 let (width, height) = self.terminal_grid.borrow().cell_size();
786 Size { width: width as u16, height: height as u16 }
787 }
788
789 #[wasm_bindgen]
791 pub fn render(&mut self) {
792 let raw_dpr = device_pixel_ratio();
794 if (raw_dpr - self.current_pixel_ratio).abs() > f32::EPSILON {
795 let _ = self.handle_pixel_ratio_change(raw_dpr);
796 }
797
798 let mut grid = self.terminal_grid.borrow_mut();
799 let _ = grid.flush_cells(self.renderer.gl());
800
801 self.renderer.begin_frame();
802 self.renderer.render(&*grid);
803 self.renderer.end_frame();
804 }
805
806 fn handle_pixel_ratio_change(&mut self, raw_pixel_ratio: f32) -> Result<(), JsValue> {
810 self.current_pixel_ratio = raw_pixel_ratio;
811
812 let gl = self.renderer.gl();
813
814 self.terminal_grid
816 .borrow_mut()
817 .atlas_mut()
818 .update_pixel_ratio(gl, raw_pixel_ratio)
819 .map_err(|e| JsValue::from_str(&format!("Failed to update pixel ratio: {e}")))?;
820
821 self.renderer.set_pixel_ratio(raw_pixel_ratio);
823
824 let (w, h) = self.renderer.logical_size();
826 self.resize(w, h)
827 }
828
829 #[wasm_bindgen]
831 pub fn resize(&mut self, width: i32, height: i32) -> Result<(), JsValue> {
832 self.renderer.resize(width, height);
833
834 let gl = self.renderer.gl();
835 let physical_size = self.renderer.physical_size();
836 self.terminal_grid
837 .borrow_mut()
838 .resize(gl, physical_size, self.current_pixel_ratio)
839 .map_err(|e| JsValue::from_str(&format!("Failed to resize: {e}")))?;
840
841 self.update_mouse_handler_metrics();
842
843 Ok(())
844 }
845
846 fn update_mouse_handler_metrics(&mut self) {
848 if let Some(mouse_handler) = &mut self.mouse_handler {
849 let grid = self.terminal_grid.borrow();
850 let (cols, rows) = grid.terminal_size();
851 let (phys_width, phys_height) = grid.cell_size();
852 let cell_width = phys_width as f32 / self.current_pixel_ratio;
853 let cell_height = phys_height as f32 / self.current_pixel_ratio;
854 mouse_handler.update_metrics(cols, rows, cell_width, cell_height);
855 }
856 }
857
858 #[wasm_bindgen(js_name = "replaceWithStaticAtlas")]
872 pub fn replace_with_static_atlas(
873 &mut self,
874 atlas_data: Option<js_sys::Uint8Array>,
875 ) -> Result<(), JsValue> {
876 let gl = self.renderer.gl();
877
878 let atlas_config = match atlas_data {
879 Some(data) => {
880 let bytes = data.to_vec();
881 FontAtlasData::from_binary(&bytes)
882 .map_err(|e| JsValue::from_str(&format!("Failed to parse atlas data: {e:?}")))?
883 },
884 None => FontAtlasData::default(),
885 };
886
887 let atlas = StaticFontAtlas::load(gl, atlas_config)
888 .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {e}")))?;
889
890 self.terminal_grid
891 .borrow_mut()
892 .replace_atlas(gl, atlas.into());
893
894 self.update_mouse_handler_metrics();
895
896 Ok(())
897 }
898
899 #[wasm_bindgen(js_name = "replaceWithDynamicAtlas")]
914 pub fn replace_with_dynamic_atlas(
915 &mut self,
916 font_family: js_sys::Array,
917 font_size: f32,
918 ) -> Result<(), JsValue> {
919 let font_families: Vec<CompactString> = font_family
920 .iter()
921 .filter_map(|v| v.as_string())
922 .map(|s| s.to_compact_string())
923 .collect();
924
925 if font_families.is_empty() {
926 return Err(JsValue::from_str("font_family array cannot be empty"));
927 }
928
929 let gl = self.renderer.gl();
930 let pixel_ratio = device_pixel_ratio();
931 let atlas = DynamicFontAtlas::new(gl, &font_families, font_size, pixel_ratio, None)
932 .map_err(|e| JsValue::from_str(&format!("Failed to create dynamic atlas: {e}")))?;
933
934 self.terminal_grid
935 .borrow_mut()
936 .replace_atlas(gl, atlas.into());
937
938 self.update_mouse_handler_metrics();
939
940 Ok(())
941 }
942
943 fn update_mouse_metrics(&mut self, mouse_handler: &mut TerminalMouseHandler) {
944 let grid = self.terminal_grid.borrow();
945 let (cols, rows) = grid.terminal_size();
946 let (phys_w, phys_h) = grid.cell_size();
947 let css_w = phys_w as f32 / self.current_pixel_ratio;
948 let css_h = phys_h as f32 / self.current_pixel_ratio;
949 mouse_handler.update_metrics(cols, rows, css_w, css_h);
950 }
951}
952
953impl From<SelectionMode> for RustSelectionMode {
955 fn from(mode: SelectionMode) -> Self {
956 match mode {
957 SelectionMode::Block => RustSelectionMode::Block,
958 SelectionMode::Linear => RustSelectionMode::Linear,
959 }
960 }
961}
962
963impl From<RustSelectionMode> for SelectionMode {
964 fn from(mode: RustSelectionMode) -> Self {
965 match mode {
966 RustSelectionMode::Block => SelectionMode::Block,
967 RustSelectionMode::Linear => SelectionMode::Linear,
968 }
969 }
970}
971
972impl From<TerminalMouseEvent> for MouseEvent {
973 fn from(event: TerminalMouseEvent) -> Self {
974 use crate::mouse::MouseEventType as RustMouseEventType;
975
976 let event_type = match event.event_type {
977 RustMouseEventType::MouseDown => MouseEventType::MouseDown,
978 RustMouseEventType::MouseUp => MouseEventType::MouseUp,
979 RustMouseEventType::MouseMove => MouseEventType::MouseMove,
980 };
981
982 MouseEvent {
983 event_type,
984 col: event.col,
985 row: event.row,
986 button: event.button(),
987 ctrl_key: event.ctrl_key(),
988 shift_key: event.shift_key(),
989 alt_key: event.alt_key(),
990 meta_key: event.meta_key(),
991 }
992 }
993}
994
995impl From<ModifierKeys> for RustModifierKeys {
996 fn from(keys: ModifierKeys) -> Self {
997 RustModifierKeys::from_bits_truncate(keys.0)
998 }
999}
1000
1001#[wasm_bindgen(start)]
1003pub fn main() {
1004 console_error_panic_hook::set_once();
1005 console::log_1(&"beamterm WASM module loaded".into());
1006}