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, ContextLossHandler, 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 position::CursorPosition,
22 url::find_url_at_cursor,
23};
24
25#[wasm_bindgen]
27#[derive(Debug)]
28pub struct BeamtermRenderer {
29 renderer: Renderer,
30 terminal_grid: Rc<RefCell<TerminalGrid>>,
31 mouse_handler: Option<TerminalMouseHandler>,
32 context_loss_handler: Option<ContextLossHandler>,
34 current_pixel_ratio: f32,
36}
37
38#[wasm_bindgen]
40#[derive(Debug, Default, serde::Deserialize)]
41pub struct Cell {
42 symbol: CompactString,
43 style: u16,
44 fg: u32,
45 bg: u32,
46}
47
48#[wasm_bindgen]
49#[derive(Debug, Clone, Copy)]
50pub struct CellStyle {
51 fg: u32,
52 bg: u32,
53 style_bits: u16,
54}
55
56#[wasm_bindgen]
57#[derive(Debug, Clone, Copy)]
58pub struct Size {
59 pub width: u16,
60 pub height: u16,
61}
62
63#[wasm_bindgen]
64#[derive(Debug)]
65pub struct Batch {
66 terminal_grid: Rc<RefCell<TerminalGrid>>,
67 gl: web_sys::WebGl2RenderingContext,
68}
69
70#[wasm_bindgen]
72#[derive(Debug, Clone, Copy)]
73pub enum SelectionMode {
74 Block,
76 Linear,
78}
79
80#[wasm_bindgen]
82#[derive(Debug, Clone, Copy)]
83pub enum MouseEventType {
84 MouseDown,
86 MouseUp,
88 MouseMove,
90 Click,
92 MouseEnter,
94 MouseLeave,
96}
97
98#[wasm_bindgen]
100#[derive(Debug, Clone, Copy)]
101pub struct MouseEvent {
102 pub event_type: MouseEventType,
104 pub col: u16,
106 pub row: u16,
108 pub button: i16,
110 pub ctrl_key: bool,
112 pub shift_key: bool,
114 pub alt_key: bool,
116 pub meta_key: bool,
118}
119
120#[wasm_bindgen]
128#[derive(Debug, Clone, Copy, Default)]
129pub struct ModifierKeys(u8);
130
131#[wasm_bindgen]
132#[allow(non_snake_case)]
133impl ModifierKeys {
134 #[wasm_bindgen(getter)]
136 pub fn NONE() -> ModifierKeys {
137 ModifierKeys(0)
138 }
139
140 #[wasm_bindgen(getter)]
142 pub fn CONTROL() -> ModifierKeys {
143 ModifierKeys(RustModifierKeys::CONTROL.bits())
144 }
145
146 #[wasm_bindgen(getter)]
148 pub fn SHIFT() -> ModifierKeys {
149 ModifierKeys(RustModifierKeys::SHIFT.bits())
150 }
151
152 #[wasm_bindgen(getter)]
154 pub fn ALT() -> ModifierKeys {
155 ModifierKeys(RustModifierKeys::ALT.bits())
156 }
157
158 #[wasm_bindgen(getter)]
160 pub fn META() -> ModifierKeys {
161 ModifierKeys(RustModifierKeys::META.bits())
162 }
163
164 #[wasm_bindgen(js_name = "or")]
166 pub fn or(&self, other: &ModifierKeys) -> ModifierKeys {
167 ModifierKeys(self.0 | other.0)
168 }
169}
170
171#[wasm_bindgen]
173#[derive(Debug, Clone)]
174pub struct CellQuery {
175 inner: RustCellQuery,
176}
177
178#[wasm_bindgen]
183#[derive(Debug)]
184pub struct UrlMatch {
185 url: String,
187 query: CellQuery,
189}
190
191#[wasm_bindgen]
192impl UrlMatch {
193 #[wasm_bindgen(getter)]
195 pub fn url(&self) -> String {
196 self.url.clone()
197 }
198
199 #[wasm_bindgen(getter)]
203 pub fn query(&self) -> CellQuery {
204 self.query.clone()
205 }
206}
207
208#[wasm_bindgen]
209impl CellQuery {
210 #[wasm_bindgen(constructor)]
212 pub fn new(mode: SelectionMode) -> CellQuery {
213 CellQuery { inner: select(mode.into()) }
214 }
215
216 pub fn start(mut self, col: u16, row: u16) -> CellQuery {
218 self.inner = self.inner.start((col, row));
219 self
220 }
221
222 pub fn end(mut self, col: u16, row: u16) -> CellQuery {
224 self.inner = self.inner.end((col, row));
225 self
226 }
227
228 #[wasm_bindgen(js_name = "trimTrailingWhitespace")]
230 pub fn trim_trailing_whitespace(mut self, enabled: bool) -> CellQuery {
231 self.inner = self.inner.trim_trailing_whitespace(enabled);
232 self
233 }
234
235 #[wasm_bindgen(js_name = "isEmpty")]
237 pub fn is_empty(&self) -> bool {
238 self.inner.is_empty()
239 }
240}
241
242#[wasm_bindgen]
243pub fn style() -> CellStyle {
244 CellStyle::new()
245}
246
247#[wasm_bindgen]
248pub fn cell(symbol: &str, style: CellStyle) -> Cell {
249 Cell {
250 symbol: symbol.into(),
251 style: style.style_bits,
252 fg: style.fg,
253 bg: style.bg,
254 }
255}
256
257#[wasm_bindgen]
258impl CellStyle {
259 #[wasm_bindgen(constructor)]
261 pub fn new() -> CellStyle {
262 Default::default()
263 }
264
265 #[wasm_bindgen]
267 pub fn fg(mut self, color: u32) -> CellStyle {
268 self.fg = color;
269 self
270 }
271
272 #[wasm_bindgen]
274 pub fn bg(mut self, color: u32) -> CellStyle {
275 self.bg = color;
276 self
277 }
278
279 #[wasm_bindgen]
281 pub fn bold(mut self) -> CellStyle {
282 self.style_bits |= Glyph::BOLD_FLAG;
283 self
284 }
285
286 #[wasm_bindgen]
288 pub fn italic(mut self) -> CellStyle {
289 self.style_bits |= Glyph::ITALIC_FLAG;
290 self
291 }
292
293 #[wasm_bindgen]
295 pub fn underline(mut self) -> CellStyle {
296 self.style_bits |= Glyph::UNDERLINE_FLAG;
297 self
298 }
299
300 #[wasm_bindgen]
302 pub fn strikethrough(mut self) -> CellStyle {
303 self.style_bits |= Glyph::STRIKETHROUGH_FLAG;
304 self
305 }
306
307 #[wasm_bindgen(getter)]
309 pub fn bits(&self) -> u16 {
310 self.style_bits
311 }
312}
313
314impl Default for CellStyle {
315 fn default() -> Self {
316 CellStyle {
317 fg: 0xFFFFFF, bg: 0x000000, style_bits: 0, }
321 }
322}
323
324#[wasm_bindgen]
325impl Batch {
326 #[wasm_bindgen(js_name = "cell")]
328 pub fn cell(&mut self, x: u16, y: u16, cell_data: &Cell) {
329 let _ = self
330 .terminal_grid
331 .borrow_mut()
332 .update_cell(x, y, cell_data.as_cell_data());
333 }
334
335 #[wasm_bindgen(js_name = "cellByIndex")]
337 pub fn cell_by_index(&mut self, idx: usize, cell_data: &Cell) {
338 let _ = self
339 .terminal_grid
340 .borrow_mut()
341 .update_cell_by_index(idx, cell_data.as_cell_data());
342 }
343
344 #[wasm_bindgen(js_name = "cells")]
347 pub fn cells(&mut self, cells_json: JsValue) -> Result<(), JsValue> {
348 let updates = from_value::<Vec<(u16, u16, Cell)>>(cells_json)
349 .map_err(|e| JsValue::from_str(&e.to_string()));
350
351 match updates {
352 Ok(cells) => {
353 let cell_data = cells
354 .iter()
355 .map(|(x, y, data)| (*x, *y, data.as_cell_data()));
356
357 let mut terminal_grid = self.terminal_grid.borrow_mut();
358 terminal_grid
359 .update_cells_by_position(cell_data)
360 .map_err(|e| JsValue::from_str(&e.to_string()))
361 },
362 e => e.map(|_| ()),
363 }
364 }
365
366 #[wasm_bindgen(js_name = "text")]
368 pub fn text(&mut self, x: u16, y: u16, text: &str, style: &CellStyle) -> Result<(), JsValue> {
369 let mut terminal_grid = self.terminal_grid.borrow_mut();
370 let (cols, rows) = terminal_grid.terminal_size();
371
372 if y >= rows {
373 return Ok(()); }
375
376 let mut col_offset: u16 = 0;
377 for ch in text.graphemes(true) {
378 let char_width = if ch.len() == 1 { 1 } else { ch.width() };
379
380 if char_width == 0 {
382 continue;
383 }
384
385 let current_col = x + col_offset;
386 if current_col >= cols {
387 break;
388 }
389
390 let cell = CellData::new_with_style_bits(ch, style.style_bits, style.fg, style.bg);
391 terminal_grid
392 .update_cell(current_col, y, cell)
393 .map_err(|e| JsValue::from_str(&e.to_string()))?;
394
395 col_offset += char_width as u16;
396 }
397
398 Ok(())
399 }
400
401 #[wasm_bindgen(js_name = "fill")]
403 pub fn fill(
404 &mut self,
405 x: u16,
406 y: u16,
407 width: u16,
408 height: u16,
409 cell_data: &Cell,
410 ) -> Result<(), JsValue> {
411 let mut terminal_grid = self.terminal_grid.borrow_mut();
412 let (cols, rows) = terminal_grid.terminal_size();
413
414 let width = (x + width).min(cols).saturating_sub(x);
415 let height = (y + height).min(rows).saturating_sub(y);
416
417 let fill_cell = cell_data.as_cell_data();
418 for y in y..y + height {
419 for x in x..x + width {
420 terminal_grid
421 .update_cell(x, y, fill_cell)
422 .map_err(|e| JsValue::from_str(&e.to_string()))?;
423 }
424 }
425
426 Ok(())
427 }
428
429 #[wasm_bindgen]
431 pub fn clear(&mut self, bg: u32) -> Result<(), JsValue> {
432 let mut terminal_grid = self.terminal_grid.borrow_mut();
433 let (cols, rows) = terminal_grid.terminal_size();
434
435 let clear_cell = CellData::new_with_style_bits(" ", 0, 0xFFFFFF, bg);
436 for y in 0..rows {
437 for x in 0..cols {
438 terminal_grid
439 .update_cell(x, y, clear_cell)
440 .map_err(|e| JsValue::from_str(&e.to_string()))?;
441 }
442 }
443
444 Ok(())
445 }
446
447 #[wasm_bindgen]
449 #[deprecated(since = "0.4.0", note = "no-op, flush is now automatic")]
450 #[allow(deprecated)]
451 pub fn flush(&mut self) -> Result<(), JsValue> {
452 Ok(())
453 }
454}
455
456#[wasm_bindgen]
457impl Cell {
458 #[wasm_bindgen(constructor)]
459 pub fn new(symbol: String, style: &CellStyle) -> Cell {
460 Cell {
461 symbol: symbol.into(),
462 style: style.style_bits,
463 fg: style.fg,
464 bg: style.bg,
465 }
466 }
467
468 #[wasm_bindgen(getter)]
469 pub fn symbol(&self) -> String {
470 self.symbol.to_string()
471 }
472
473 #[wasm_bindgen(setter)]
474 pub fn set_symbol(&mut self, symbol: String) {
475 self.symbol = symbol.into();
476 }
477
478 #[wasm_bindgen(getter)]
479 pub fn fg(&self) -> u32 {
480 self.fg
481 }
482
483 #[wasm_bindgen(setter)]
484 pub fn set_fg(&mut self, color: u32) {
485 self.fg = color;
486 }
487
488 #[wasm_bindgen(getter)]
489 pub fn bg(&self) -> u32 {
490 self.bg
491 }
492
493 #[wasm_bindgen(setter)]
494 pub fn set_bg(&mut self, color: u32) {
495 self.bg = color;
496 }
497
498 #[wasm_bindgen(getter)]
499 pub fn style(&self) -> u16 {
500 self.style
501 }
502
503 #[wasm_bindgen(setter)]
504 pub fn set_style(&mut self, style: u16) {
505 self.style = style;
506 }
507}
508
509impl Cell {
510 pub fn as_cell_data(&self) -> CellData<'_> {
511 CellData::new_with_style_bits(&self.symbol, self.style, self.fg, self.bg)
512 }
513}
514
515#[wasm_bindgen]
516impl BeamtermRenderer {
517 #[wasm_bindgen(constructor)]
519 pub fn new(canvas_id: &str) -> Result<BeamtermRenderer, JsValue> {
520 Self::with_static_atlas(canvas_id, None, None)
521 }
522
523 #[wasm_bindgen(js_name = "withStaticAtlas")]
532 pub fn with_static_atlas(
533 canvas_id: &str,
534 atlas_data: Option<js_sys::Uint8Array>,
535 auto_resize_canvas_css: Option<bool>,
536 ) -> Result<BeamtermRenderer, JsValue> {
537 console_error_panic_hook::set_once();
538
539 let auto_resize = auto_resize_canvas_css.unwrap_or(true);
540
541 let mut renderer = Renderer::create(canvas_id, auto_resize)
543 .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {e}")))?;
544 let current_pixel_ratio = crate::js::device_pixel_ratio();
545 renderer.set_pixel_ratio(current_pixel_ratio);
546 let (w, h) = renderer.logical_size();
547 renderer.resize(w, h);
548
549 let gl = renderer.gl();
550 let atlas_config = match atlas_data {
551 Some(data) => {
552 let bytes = data.to_vec();
553 FontAtlasData::from_binary(&bytes)
554 .map_err(|e| JsValue::from_str(&format!("Failed to parse atlas data: {e:?}")))?
555 },
556 None => FontAtlasData::default(),
557 };
558
559 let atlas = StaticFontAtlas::load(gl, atlas_config)
560 .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {e}")))?;
561
562 let canvas_size = renderer.physical_size();
563 let terminal_grid =
564 TerminalGrid::new(gl, atlas.into(), canvas_size, current_pixel_ratio)
565 .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {e}")))?;
566
567 let terminal_grid = Rc::new(RefCell::new(terminal_grid));
568
569 let context_loss_handler = ContextLossHandler::new(renderer.canvas()).map_err(|e| {
570 JsValue::from_str(&format!("Failed to create context loss handler: {e}"))
571 })?;
572
573 Ok(BeamtermRenderer {
574 renderer,
575 terminal_grid,
576 mouse_handler: None,
577 context_loss_handler: Some(context_loss_handler),
578 current_pixel_ratio,
579 })
580 }
581
582 #[wasm_bindgen(js_name = "withDynamicAtlas")]
604 pub fn with_dynamic_atlas(
605 canvas_id: &str,
606 font_family: js_sys::Array,
607 font_size: f32,
608 auto_resize_canvas_css: Option<bool>,
609 ) -> Result<BeamtermRenderer, JsValue> {
610 console_error_panic_hook::set_once();
611
612 let auto_resize = auto_resize_canvas_css.unwrap_or(true);
613
614 let mut renderer = Renderer::create(canvas_id, auto_resize)
616 .map_err(|e| JsValue::from_str(&format!("Failed to create renderer: {e}")))?;
617 let current_pixel_ratio = crate::js::device_pixel_ratio();
618 renderer.set_pixel_ratio(current_pixel_ratio);
619 let (w, h) = renderer.logical_size();
620 renderer.resize(w, h);
621
622 let font_families: Vec<CompactString> = font_family
623 .iter()
624 .filter_map(|v| v.as_string())
625 .map(|s| s.to_compact_string())
626 .collect();
627
628 if font_families.is_empty() {
629 return Err(JsValue::from_str("font_family array cannot be empty"));
630 }
631
632 let gl = renderer.gl();
633 let atlas = DynamicFontAtlas::new(gl, &font_families, font_size, current_pixel_ratio, None)
634 .map_err(|e| JsValue::from_str(&format!("Failed to create dynamic atlas: {e}")))?;
635
636 let canvas_size = renderer.physical_size();
637 let terminal_grid =
638 TerminalGrid::new(gl, atlas.into(), canvas_size, current_pixel_ratio)
639 .map_err(|e| JsValue::from_str(&format!("Failed to create terminal grid: {e}")))?;
640
641 let terminal_grid = Rc::new(RefCell::new(terminal_grid));
642
643 let context_loss_handler = ContextLossHandler::new(renderer.canvas()).map_err(|e| {
644 JsValue::from_str(&format!("Failed to create context loss handler: {e}"))
645 })?;
646
647 Ok(BeamtermRenderer {
648 renderer,
649 terminal_grid,
650 mouse_handler: None,
651 context_loss_handler: Some(context_loss_handler),
652 current_pixel_ratio,
653 })
654 }
655
656 #[wasm_bindgen(js_name = "enableSelection")]
658 pub fn enable_selection(
659 &mut self,
660 mode: SelectionMode,
661 trim_whitespace: bool,
662 ) -> Result<(), JsValue> {
663 self.enable_selection_internal(mode, trim_whitespace, ModifierKeys::default())
664 }
665
666 #[wasm_bindgen(js_name = "enableSelectionWithOptions")]
693 pub fn enable_selection_with_options(
694 &mut self,
695 mode: SelectionMode,
696 trim_whitespace: bool,
697 require_modifiers: &ModifierKeys,
698 ) -> Result<(), JsValue> {
699 self.enable_selection_internal(mode, trim_whitespace, *require_modifiers)
700 }
701
702 fn enable_selection_internal(
703 &mut self,
704 mode: SelectionMode,
705 trim_whitespace: bool,
706 require_modifiers: ModifierKeys,
707 ) -> Result<(), JsValue> {
708 if let Some(old_handler) = self.mouse_handler.take() {
710 old_handler.cleanup();
711 }
712
713 let selection_tracker = self.terminal_grid.borrow().selection_tracker();
714 let options = MouseSelectOptions::new()
715 .selection_mode(mode.into())
716 .trim_trailing_whitespace(trim_whitespace)
717 .require_modifier_keys(require_modifiers.into());
718 let handler = DefaultSelectionHandler::new(self.terminal_grid.clone(), options);
719
720 let mut mouse_handler = TerminalMouseHandler::new(
721 self.renderer.canvas(),
722 self.terminal_grid.clone(),
723 handler.create_event_handler(selection_tracker),
724 )
725 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {e}")))?;
726
727 self.update_mouse_metrics(&mut mouse_handler);
728
729 self.mouse_handler = Some(mouse_handler);
730 Ok(())
731 }
732
733 #[wasm_bindgen(js_name = "setMouseHandler")]
735 pub fn set_mouse_handler(&mut self, handler: js_sys::Function) -> Result<(), JsValue> {
736 if let Some(old_handler) = self.mouse_handler.take() {
738 old_handler.cleanup();
739 }
740
741 let handler_closure = {
742 let handler = handler.clone();
743 move |event: TerminalMouseEvent, _grid: &TerminalGrid| {
744 let js_event = MouseEvent::from(event);
745 let this = JsValue::null();
746 let args = js_sys::Array::new();
747 args.push(&JsValue::from(js_event));
748
749 if let Err(e) = handler.apply(&this, &args) {
750 console::error_1(&format!("Mouse handler error: {e:?}").into());
751 }
752 }
753 };
754
755 let mut mouse_handler = TerminalMouseHandler::new(
756 self.renderer.canvas(),
757 self.terminal_grid.clone(),
758 handler_closure,
759 )
760 .map_err(|e| JsValue::from_str(&format!("Failed to create mouse handler: {e}")))?;
761
762 self.update_mouse_metrics(&mut mouse_handler);
763
764 self.mouse_handler = Some(mouse_handler);
765 Ok(())
766 }
767
768 #[wasm_bindgen(js_name = "getText")]
770 pub fn get_text(&self, query: &CellQuery) -> String {
771 self.terminal_grid
772 .borrow()
773 .get_text(query.inner)
774 .to_string()
775 }
776
777 #[wasm_bindgen(js_name = "findUrlAt")]
800 pub fn find_url_at(&self, col: u16, row: u16) -> Option<UrlMatch> {
801 let cursor = CursorPosition::new(col, row);
802 let grid = self.terminal_grid.borrow();
803
804 find_url_at_cursor(cursor, &grid).map(|m| UrlMatch {
805 url: m.url.to_string(),
806 query: CellQuery { inner: m.query },
807 })
808 }
809
810 #[wasm_bindgen(js_name = "copyToClipboard")]
812 pub fn copy_to_clipboard(&self, text: &str) {
813 use wasm_bindgen_futures::spawn_local;
814 let text = text.to_string();
815
816 spawn_local(async move {
817 if let Some(window) = web_sys::window() {
818 let clipboard = window.navigator().clipboard();
819 match wasm_bindgen_futures::JsFuture::from(clipboard.write_text(&text)).await {
820 Ok(_) => {
821 console::log_1(
822 &format!("Copied {} characters to clipboard", text.len()).into(),
823 );
824 },
825 Err(err) => {
826 console::error_1(&format!("Failed to copy to clipboard: {err:?}").into());
827 },
828 }
829 }
830 });
831 }
832
833 #[wasm_bindgen(js_name = "clearSelection")]
835 pub fn clear_selection(&self) {
836 self.terminal_grid
837 .borrow()
838 .selection_tracker()
839 .clear();
840 }
841
842 #[wasm_bindgen(js_name = "hasSelection")]
844 pub fn has_selection(&self) -> bool {
845 self.terminal_grid
846 .borrow()
847 .selection_tracker()
848 .get_query()
849 .is_some()
850 }
851
852 #[wasm_bindgen(js_name = "batch")]
854 pub fn new_render_batch(&mut self) -> Batch {
855 let gl = self.renderer.gl().clone();
856 let terminal_grid = self.terminal_grid.clone();
857 Batch { terminal_grid, gl }
858 }
859
860 #[wasm_bindgen(js_name = "terminalSize")]
862 pub fn terminal_size(&self) -> Size {
863 let (cols, rows) = self.terminal_grid.borrow().terminal_size();
864 Size { width: cols, height: rows }
865 }
866
867 #[wasm_bindgen(js_name = "cellSize")]
869 pub fn cell_size(&self) -> Size {
870 let (width, height) = self.terminal_grid.borrow().cell_size();
871 Size { width: width as u16, height: height as u16 }
872 }
873
874 #[wasm_bindgen]
876 pub fn render(&mut self) {
877 if self.needs_gl_reinit()
879 && let Err(e) = self.restore_context()
880 {
881 console::error_1(&format!("Failed to restore WebGL context: {e:?}").into());
882 return;
883 }
884
885 if self.is_context_lost() {
887 return;
888 }
889
890 let raw_dpr = device_pixel_ratio();
892 if (raw_dpr - self.current_pixel_ratio).abs() > f32::EPSILON {
893 let _ = self.handle_pixel_ratio_change(raw_dpr);
894 }
895
896 let mut grid = self.terminal_grid.borrow_mut();
897 let _ = grid.flush_cells(self.renderer.gl());
898
899 self.renderer.begin_frame();
900 self.renderer.render(&*grid);
901 self.renderer.end_frame();
902 }
903
904 fn is_context_lost(&self) -> bool {
906 if let Some(handler) = &self.context_loss_handler {
907 handler.is_context_lost()
908 } else {
909 self.renderer.is_context_lost()
910 }
911 }
912
913 fn needs_gl_reinit(&self) -> bool {
915 self.context_loss_handler
916 .as_ref()
917 .is_some_and(ContextLossHandler::context_pending_rebuild)
918 }
919
920 fn restore_context(&mut self) -> Result<(), JsValue> {
922 self.renderer
923 .restore_context()
924 .map_err(|e| JsValue::from_str(&format!("Failed to restore renderer context: {e}")))?;
925
926 let gl = self.renderer.gl();
927
928 self.terminal_grid
929 .borrow_mut()
930 .recreate_atlas_texture(gl)
931 .map_err(|e| JsValue::from_str(&format!("Failed to recreate atlas texture: {e}")))?;
932
933 self.terminal_grid
934 .borrow_mut()
935 .recreate_resources(gl)
936 .map_err(|e| JsValue::from_str(&format!("Failed to recreate grid resources: {e}")))?;
937
938 self.terminal_grid
939 .borrow_mut()
940 .flush_cells(gl)
941 .map_err(|e| JsValue::from_str(&format!("Failed to flush cells: {e}")))?;
942
943 if let Some(handler) = &self.context_loss_handler {
944 handler.clear_context_rebuild_needed();
945 }
946
947 let dpr = device_pixel_ratio();
950 if (dpr - self.current_pixel_ratio).abs() > f32::EPSILON {
951 self.handle_pixel_ratio_change(dpr)?;
952 } else {
953 self.renderer.set_pixel_ratio(dpr);
955 let (w, h) = self.renderer.logical_size();
956 self.renderer.resize(w, h);
957 }
958
959 console::log_1(&"WebGL context restored successfully".into());
960 Ok(())
961 }
962
963 fn handle_pixel_ratio_change(&mut self, raw_pixel_ratio: f32) -> Result<(), JsValue> {
967 self.current_pixel_ratio = raw_pixel_ratio;
968
969 let gl = self.renderer.gl();
970
971 self.terminal_grid
973 .borrow_mut()
974 .atlas_mut()
975 .update_pixel_ratio(gl, raw_pixel_ratio)
976 .map_err(|e| JsValue::from_str(&format!("Failed to update pixel ratio: {e}")))?;
977
978 self.renderer.set_pixel_ratio(raw_pixel_ratio);
980
981 let (w, h) = self.renderer.logical_size();
983 self.resize(w, h)
984 }
985
986 #[wasm_bindgen]
988 pub fn resize(&mut self, width: i32, height: i32) -> Result<(), JsValue> {
989 self.renderer.resize(width, height);
990
991 let gl = self.renderer.gl();
992 let physical_size = self.renderer.physical_size();
993 self.terminal_grid
994 .borrow_mut()
995 .resize(gl, physical_size, self.current_pixel_ratio)
996 .map_err(|e| JsValue::from_str(&format!("Failed to resize: {e}")))?;
997
998 self.update_mouse_handler_metrics();
999
1000 Ok(())
1001 }
1002
1003 fn update_mouse_handler_metrics(&mut self) {
1005 if let Some(mouse_handler) = &mut self.mouse_handler {
1006 let grid = self.terminal_grid.borrow();
1007 let (cols, rows) = grid.terminal_size();
1008 let (phys_width, phys_height) = grid.cell_size();
1009 let cell_width = phys_width as f32 / self.current_pixel_ratio;
1010 let cell_height = phys_height as f32 / self.current_pixel_ratio;
1011 mouse_handler.update_metrics(cols, rows, cell_width, cell_height);
1012 }
1013 }
1014
1015 #[wasm_bindgen(js_name = "replaceWithStaticAtlas")]
1029 pub fn replace_with_static_atlas(
1030 &mut self,
1031 atlas_data: Option<js_sys::Uint8Array>,
1032 ) -> Result<(), JsValue> {
1033 let gl = self.renderer.gl();
1034
1035 let atlas_config = match atlas_data {
1036 Some(data) => {
1037 let bytes = data.to_vec();
1038 FontAtlasData::from_binary(&bytes)
1039 .map_err(|e| JsValue::from_str(&format!("Failed to parse atlas data: {e:?}")))?
1040 },
1041 None => FontAtlasData::default(),
1042 };
1043
1044 let atlas = StaticFontAtlas::load(gl, atlas_config)
1045 .map_err(|e| JsValue::from_str(&format!("Failed to load font atlas: {e}")))?;
1046
1047 self.terminal_grid
1048 .borrow_mut()
1049 .replace_atlas(gl, atlas.into());
1050
1051 self.update_mouse_handler_metrics();
1052
1053 Ok(())
1054 }
1055
1056 #[wasm_bindgen(js_name = "replaceWithDynamicAtlas")]
1071 pub fn replace_with_dynamic_atlas(
1072 &mut self,
1073 font_family: js_sys::Array,
1074 font_size: f32,
1075 ) -> Result<(), JsValue> {
1076 let font_families: Vec<CompactString> = font_family
1077 .iter()
1078 .filter_map(|v| v.as_string())
1079 .map(|s| s.to_compact_string())
1080 .collect();
1081
1082 if font_families.is_empty() {
1083 return Err(JsValue::from_str("font_family array cannot be empty"));
1084 }
1085
1086 let gl = self.renderer.gl();
1087 let pixel_ratio = device_pixel_ratio();
1088 let atlas = DynamicFontAtlas::new(gl, &font_families, font_size, pixel_ratio, None)
1089 .map_err(|e| JsValue::from_str(&format!("Failed to create dynamic atlas: {e}")))?;
1090
1091 self.terminal_grid
1092 .borrow_mut()
1093 .replace_atlas(gl, atlas.into());
1094
1095 self.update_mouse_handler_metrics();
1096
1097 Ok(())
1098 }
1099
1100 fn update_mouse_metrics(&mut self, mouse_handler: &mut TerminalMouseHandler) {
1101 let grid = self.terminal_grid.borrow();
1102 let (cols, rows) = grid.terminal_size();
1103 let (phys_w, phys_h) = grid.cell_size();
1104 let css_w = phys_w as f32 / self.current_pixel_ratio;
1105 let css_h = phys_h as f32 / self.current_pixel_ratio;
1106 mouse_handler.update_metrics(cols, rows, css_w, css_h);
1107 }
1108}
1109
1110impl From<SelectionMode> for RustSelectionMode {
1112 fn from(mode: SelectionMode) -> Self {
1113 match mode {
1114 SelectionMode::Block => RustSelectionMode::Block,
1115 SelectionMode::Linear => RustSelectionMode::Linear,
1116 }
1117 }
1118}
1119
1120impl From<RustSelectionMode> for SelectionMode {
1121 fn from(mode: RustSelectionMode) -> Self {
1122 match mode {
1123 RustSelectionMode::Block => SelectionMode::Block,
1124 RustSelectionMode::Linear => SelectionMode::Linear,
1125 }
1126 }
1127}
1128
1129impl From<TerminalMouseEvent> for MouseEvent {
1130 fn from(event: TerminalMouseEvent) -> Self {
1131 use crate::mouse::MouseEventType as RustMouseEventType;
1132
1133 let event_type = match event.event_type {
1134 RustMouseEventType::MouseDown => MouseEventType::MouseDown,
1135 RustMouseEventType::MouseUp => MouseEventType::MouseUp,
1136 RustMouseEventType::MouseMove => MouseEventType::MouseMove,
1137 RustMouseEventType::Click => MouseEventType::Click,
1138 RustMouseEventType::MouseEnter => MouseEventType::MouseEnter,
1139 RustMouseEventType::MouseLeave => MouseEventType::MouseLeave,
1140 };
1141
1142 MouseEvent {
1143 event_type,
1144 col: event.col,
1145 row: event.row,
1146 button: event.button(),
1147 ctrl_key: event.ctrl_key(),
1148 shift_key: event.shift_key(),
1149 alt_key: event.alt_key(),
1150 meta_key: event.meta_key(),
1151 }
1152 }
1153}
1154
1155impl From<ModifierKeys> for RustModifierKeys {
1156 fn from(keys: ModifierKeys) -> Self {
1157 RustModifierKeys::from_bits_truncate(keys.0)
1158 }
1159}
1160
1161#[wasm_bindgen(start)]
1163pub fn main() {
1164 console_error_panic_hook::set_once();
1165 console::log_1(&"beamterm WASM module loaded".into());
1166}