beamterm_renderer/
terminal.rs

1use std::{cell::RefCell, rc::Rc};
2
3use beamterm_data::{DebugSpacePattern, FontAtlasData};
4use compact_str::{CompactString, ToCompactString};
5use unicode_width::UnicodeWidthStr;
6use wasm_bindgen::prelude::*;
7
8use crate::{
9    CellData, DynamicFontAtlas, Error, FontAtlas, Renderer, StaticFontAtlas, TerminalGrid,
10    gl::{CellQuery, ContextLossHandler, SelectionMode},
11    mouse::{
12        DefaultSelectionHandler, MouseEventCallback, TerminalMouseEvent, TerminalMouseHandler,
13    },
14};
15
16/// High-performance WebGL2 terminal renderer.
17///
18/// `Terminal` encapsulates the complete terminal rendering system, providing a
19/// simplified API over the underlying [`Renderer`] and [`TerminalGrid`] components.
20///
21///  ## Selection and Mouse Input
22///
23/// The renderer supports mouse-driven text selection with automatic clipboard
24/// integration:
25///
26/// ```rust,no_run
27/// // Enable default selection handler
28/// use beamterm_renderer::{SelectionMode, Terminal};
29///
30/// let terminal = Terminal::builder("#canvas")
31///     .default_mouse_input_handler(SelectionMode::Linear, true)
32///     .build().unwrap();
33///
34/// // Or implement custom mouse handling
35/// let terminal = Terminal::builder("#canvas")
36///     .mouse_input_handler(|event, grid| {
37///         // Custom handler logic
38///     })
39///     .build().unwrap();
40///```
41///
42/// # Examples
43///
44/// ```rust,no_run
45/// use beamterm_renderer::{CellData, Terminal};
46///
47/// // Create and render a simple terminal
48/// let mut terminal = Terminal::builder("#canvas").build().unwrap();
49///
50/// // Update cells with content
51/// let cells: Vec<CellData> = unimplemented!();
52/// terminal.update_cells(cells.into_iter()).unwrap();
53///
54/// // Render frame
55/// terminal.render_frame().unwrap();
56///
57/// // Handle window resize
58/// let (new_width, new_height) = (800, 600);
59/// terminal.resize(new_width, new_height).unwrap();
60/// ```
61#[derive(Debug)]
62pub struct Terminal {
63    renderer: Renderer,
64    grid: Rc<RefCell<TerminalGrid>>,
65    mouse_handler: Option<TerminalMouseHandler>,
66    context_loss_handler: Option<ContextLossHandler>,
67}
68
69impl Terminal {
70    /// Creates a new terminal builder with the specified canvas source.
71    ///
72    /// # Parameters
73    /// * `canvas` - Canvas identifier (CSS selector) or `HtmlCanvasElement`
74    ///
75    /// # Examples
76    ///
77    /// ```rust,no_run
78    /// // Using CSS selector
79    /// use web_sys::HtmlCanvasElement;
80    /// use beamterm_renderer::Terminal;
81    ///
82    /// let terminal = Terminal::builder("my-terminal").build().unwrap();
83    ///
84    /// // Using canvas element
85    /// let canvas: &HtmlCanvasElement = unimplemented!("document.get_element_by_id(...)");
86    /// let terminal = Terminal::builder(canvas).build().unwrap();
87    /// ```
88    #[allow(private_bounds)]
89    pub fn builder(canvas: impl Into<CanvasSource>) -> TerminalBuilder {
90        TerminalBuilder::new(canvas.into())
91    }
92
93    /// Updates terminal cell content efficiently.
94    ///
95    /// This method batches all cell updates and uploads them to the GPU in a single
96    /// operation. For optimal performance, collect all changes and update in one call
97    /// rather than making multiple calls for individual cells.
98    ///
99    /// Delegates to [`TerminalGrid::update_cells`].
100    pub fn update_cells<'a>(
101        &mut self,
102        cells: impl Iterator<Item = CellData<'a>>,
103    ) -> Result<(), Error> {
104        self.grid
105            .borrow_mut()
106            .update_cells(self.renderer.gl(), cells)
107    }
108
109    /// Updates terminal cell content efficiently.
110    ///
111    /// This method batches all cell updates and uploads them to the GPU in a single
112    /// operation. For optimal performance, collect all changes and update in one call
113    /// rather than making multiple calls for individual cells.
114    ///
115    /// Delegates to [`TerminalGrid::update_cells_by_position`].
116    pub fn update_cells_by_position<'a>(
117        &mut self,
118        cells: impl Iterator<Item = (u16, u16, CellData<'a>)>,
119    ) -> Result<(), Error> {
120        self.grid
121            .borrow_mut()
122            .update_cells_by_position(cells)
123    }
124
125    pub fn update_cells_by_index<'a>(
126        &mut self,
127        cells: impl Iterator<Item = (usize, CellData<'a>)>,
128    ) -> Result<(), Error> {
129        self.grid
130            .borrow_mut()
131            .update_cells_by_index(cells)
132    }
133
134    /// Returns the WebGL2 rendering context.
135    pub fn gl(&self) -> &web_sys::WebGl2RenderingContext {
136        self.renderer.gl()
137    }
138
139    /// Resizes the terminal to fit new canvas dimensions.
140    ///
141    /// This method updates both the renderer viewport and terminal grid to match
142    /// the new canvas size. The terminal dimensions (in cells) are automatically
143    /// recalculated based on the cell size from the font atlas.
144    ///
145    /// Combines [`Renderer::resize`] and [`TerminalGrid::resize`] operations.
146    pub fn resize(&mut self, width: i32, height: i32) -> Result<(), Error> {
147        self.renderer.resize(width, height);
148        self.grid
149            .borrow_mut()
150            .resize(self.renderer.gl(), (width, height))?;
151
152        if let Some(mouse_input) = &mut self.mouse_handler {
153            let (cols, rows) = self.grid.borrow_mut().terminal_size();
154            mouse_input.update_dimensions(cols, rows);
155        }
156
157        Ok(())
158    }
159
160    /// Returns the terminal dimensions in cells.
161    pub fn terminal_size(&self) -> (u16, u16) {
162        self.grid.borrow().terminal_size()
163    }
164
165    /// Returns the total number of cells in the terminal grid.
166    pub fn cell_count(&self) -> usize {
167        self.grid.borrow().cell_count()
168    }
169
170    /// Returns the size of the canvas in pixels.
171    pub fn canvas_size(&self) -> (i32, i32) {
172        self.renderer.canvas_size()
173    }
174
175    /// Returns the size of each cell in pixels.
176    pub fn cell_size(&self) -> (i32, i32) {
177        self.grid.borrow().cell_size()
178    }
179
180    /// Returns a reference to the HTML canvas element used for rendering.
181    pub fn canvas(&self) -> &web_sys::HtmlCanvasElement {
182        self.renderer.canvas()
183    }
184
185    /// Returns a reference to the underlying renderer.
186    pub fn renderer(&self) -> &Renderer {
187        &self.renderer
188    }
189
190    /// Returns a reference to the terminal grid.
191    pub fn grid(&self) -> Rc<RefCell<TerminalGrid>> {
192        self.grid.clone()
193    }
194
195    /// Returns the textual content of the specified cell selection.
196    pub fn get_text(&self, selection: CellQuery) -> CompactString {
197        self.grid.borrow().get_text(selection)
198    }
199
200    /// Renders the current terminal state to the canvas.
201    ///
202    /// This method performs the complete render pipeline: frame setup, grid rendering,
203    /// and frame finalization. Call this after updating terminal content to display
204    /// the changes.
205    ///
206    /// If a WebGL context loss occurred and the context has been restored by the browser,
207    /// this method will automatically recreate all GPU resources before rendering.
208    /// The terminal's cell content is preserved during this process.
209    ///
210    /// Combines [`Renderer::begin_frame`], [`Renderer::render`], and [`Renderer::end_frame`].
211    pub fn render_frame(&mut self) -> Result<(), Error> {
212        if self.needs_gl_reinit() {
213            self.restore_context()?;
214        }
215
216        // skip rendering if context is currently lost (waiting for restoration)
217        if self.is_context_lost() {
218            return Ok(());
219        }
220
221        self.grid
222            .borrow_mut()
223            .flush_cells(self.renderer.gl())?;
224
225        self.renderer.begin_frame();
226        self.renderer.render(&*self.grid.borrow());
227        self.renderer.end_frame();
228        Ok(())
229    }
230
231    /// Returns a sorted list of all glyphs that were requested but not found in the font atlas.
232    pub fn missing_glyphs(&self) -> Vec<CompactString> {
233        let mut glyphs: Vec<_> = self
234            .grid
235            .borrow()
236            .atlas()
237            .glyph_tracker()
238            .missing_glyphs()
239            .into_iter()
240            .collect();
241        glyphs.sort();
242        glyphs
243    }
244
245    /// Checks if the WebGL context has been lost.
246    ///
247    /// Returns `true` if the context is lost and waiting for restoration.
248    fn is_context_lost(&self) -> bool {
249        if let Some(handler) = &self.context_loss_handler {
250            handler.is_context_lost()
251        } else {
252            self.renderer.is_context_lost()
253        }
254    }
255
256    /// Restores all GPU resources after a WebGL context loss.
257    ///
258    /// # Returns
259    /// * `Ok(())` - All resources successfully restored
260    /// * `Err(Error)` - Failed to restore context or recreate resources
261    fn restore_context(&mut self) -> Result<(), Error> {
262        self.renderer.restore_context()?;
263
264        let gl = self.renderer.gl();
265
266        self.grid
267            .borrow_mut()
268            .recreate_atlas_texture(gl)?;
269        self.grid.borrow_mut().recreate_resources(gl)?;
270        self.grid.borrow_mut().flush_cells(gl)?;
271
272        if let Some(handler) = &self.context_loss_handler {
273            handler.clear_context_rebuild_needed();
274        }
275
276        Ok(())
277    }
278
279    /// Checks if the terminal needs to restore GPU resources after a context loss.
280    fn needs_gl_reinit(&mut self) -> bool {
281        self.context_loss_handler
282            .as_ref()
283            .map(ContextLossHandler::context_pending_rebuild)
284            .unwrap_or(false)
285    }
286
287    /// Exposes this terminal instance to the browser console for debugging.
288    ///
289    /// After calling this method, you can access the terminal from the console:
290    /// ```javascript
291    /// // In browser console:
292    /// window.__beamterm_debug.getMissingGlyphs();
293    /// ```
294    ///
295    /// Note: This creates a live reference that will show current missing glyphs
296    /// each time you call it.
297    fn expose_to_console(&self) {
298        let debug_api = TerminalDebugApi { grid: self.grid.clone() };
299
300        let window = web_sys::window().expect("no window");
301        js_sys::Reflect::set(
302            &window,
303            &"__beamterm_debug".into(),
304            &JsValue::from(debug_api),
305        )
306        .unwrap();
307
308        web_sys::console::log_1(
309            &"Terminal debugging API exposed at window.__beamterm_debug".into(),
310        );
311    }
312}
313
314/// Canvas source for terminal initialization.
315///
316/// Supports both CSS selector strings and direct `HtmlCanvasElement` references
317/// for flexible terminal creation.
318#[derive(Debug)]
319enum CanvasSource {
320    /// CSS selector string for canvas lookup (e.g., "#terminal", "canvas").
321    Id(CompactString),
322    /// Direct reference to an existing canvas element.
323    Element(web_sys::HtmlCanvasElement),
324}
325
326/// Builder for configuring and creating a [`Terminal`].
327///
328/// Provides a fluent API for terminal configuration with sensible defaults.
329/// The terminal will use the default embedded font atlas unless explicitly configured.
330///
331/// # Examples
332///
333/// ```rust,no_run
334/// // Simple terminal with default configuration
335/// use beamterm_renderer::{FontAtlasData, Terminal};
336///
337/// let terminal = Terminal::builder("#canvas").build().unwrap();
338///
339/// // Terminal with custom font atlas
340/// let atlas = FontAtlasData::from_binary(unimplemented!(".atlas data")).unwrap();
341/// let terminal = Terminal::builder("#canvas")
342///     .font_atlas(atlas)
343///     .fallback_glyph("X".into())
344///     .build().unwrap();
345/// ```
346pub struct TerminalBuilder {
347    canvas: CanvasSource,
348    atlas_kind: AtlasKind,
349    fallback_glyph: Option<CompactString>,
350    input_handler: Option<InputHandler>,
351    canvas_padding_color: u32,
352    enable_debug_api: bool,
353}
354
355#[derive(Debug)]
356enum AtlasKind {
357    Static(Option<FontAtlasData>),
358    Dynamic {
359        font_size: f32,
360        font_family: Vec<CompactString>,
361    },
362    DebugDynamic {
363        font_size: f32,
364        font_family: Vec<CompactString>,
365        debug_space_pattern: DebugSpacePattern,
366    },
367}
368
369impl TerminalBuilder {
370    /// Creates a new terminal builder with the specified canvas source.
371    fn new(canvas: CanvasSource) -> Self {
372        TerminalBuilder {
373            canvas,
374            atlas_kind: AtlasKind::Static(None),
375            fallback_glyph: None,
376            input_handler: None,
377            canvas_padding_color: 0x000000,
378            enable_debug_api: false,
379        }
380    }
381
382    /// Sets a custom static font atlas for the terminal.
383    ///
384    /// By default, the terminal uses an embedded font atlas. Use this method
385    /// to provide a custom atlas with different fonts, sizes, or character sets.
386    ///
387    /// Static atlases are pre-generated using the `beamterm-atlas` CLI tool and
388    /// loaded from binary `.atlas` files. They provide consistent rendering but
389    /// require the character set to be known at build time.
390    ///
391    /// For dynamic glyph rasterization at runtime, see [`dynamic_font_atlas`](Self::dynamic_font_atlas).
392    pub fn font_atlas(mut self, atlas: FontAtlasData) -> Self {
393        self.atlas_kind = AtlasKind::Static(Some(atlas));
394        self
395    }
396
397    /// Configures the terminal to use a dynamic font atlas.
398    ///
399    /// Unlike static atlases, the dynamic atlas rasterizes glyphs on-demand using
400    /// the browser's Canvas API. This enables:
401    /// - Runtime font selection without pre-generation
402    /// - Support for any system font available in the browser
403    /// - Automatic handling of unpredictable Unicode content
404    ///
405    /// # Parameters
406    /// * `font_family` - Font family names in priority order (e.g., `&["JetBrains Mono", "Fira Code"]`)
407    /// * `font_size` - Font size in pixels
408    ///
409    /// For pre-generated atlases with fixed character sets, see [`font_atlas`](Self::font_atlas).
410    pub fn dynamic_font_atlas(mut self, font_family: &[&str], font_size: f32) -> Self {
411        self.atlas_kind = AtlasKind::Dynamic {
412            font_family: font_family.iter().map(|&s| s.into()).collect(),
413            font_size,
414        };
415        self
416    }
417
418    /// Configures the terminal to use a dynamic font atlas with debug space pattern.
419    ///
420    /// This is the same as [`dynamic_font_atlas`](Self::dynamic_font_atlas), but replaces
421    /// the space glyph with a checkered pattern for validating pixel-perfect rendering.
422    ///
423    /// # Parameters
424    /// * `font_family` - Font family names in priority order
425    /// * `font_size` - Font size in pixels
426    /// * `pattern` - The checkered pattern to use (1px or 2x2 pixels)
427    pub fn debug_dynamic_font_atlas(
428        mut self,
429        font_family: &[&str],
430        font_size: f32,
431        pattern: DebugSpacePattern,
432    ) -> Self {
433        self.atlas_kind = AtlasKind::DebugDynamic {
434            font_family: font_family.iter().map(|&s| s.into()).collect(),
435            font_size,
436            debug_space_pattern: pattern,
437        };
438        self
439    }
440
441    /// Sets the fallback glyph for missing characters.
442    ///
443    /// When a character is not found in the font atlas, this glyph will be
444    /// displayed instead. Defaults to a space character if not specified.
445    pub fn fallback_glyph(mut self, glyph: &str) -> Self {
446        self.fallback_glyph = Some(glyph.into());
447        self
448    }
449
450    /// Sets the background color for the canvas area outside the terminal grid.
451    ///
452    /// When the canvas dimensions don't align perfectly with the terminal cell grid,
453    /// there may be unused pixels around the edges. This color fills those padding
454    /// areas to maintain a consistent appearance.
455    pub fn canvas_padding_color(mut self, color: u32) -> Self {
456        self.canvas_padding_color = color;
457        self
458    }
459
460    /// Enables the debug API that will be exposed to the browser console.
461    ///
462    /// When enabled, a debug API will be available at `window.__beamterm_debug`
463    /// with methods like `getMissingGlyphs()` for inspecting the terminal state.
464    pub fn enable_debug_api(mut self) -> Self {
465        self.enable_debug_api = true;
466        self
467    }
468
469    /// Sets a callback for handling terminal mouse input events.
470    pub fn mouse_input_handler<F>(mut self, callback: F) -> Self
471    where
472        F: FnMut(TerminalMouseEvent, &TerminalGrid) + 'static,
473    {
474        self.input_handler = Some(InputHandler::Mouse(Box::new(callback)));
475        self
476    }
477
478    /// Sets a default selection handler for mouse input events. Left
479    /// button selects text, `Ctrl/Cmd + C` copies the selected text to
480    /// the clipboard.
481    pub fn default_mouse_input_handler(
482        mut self,
483        selection_mode: SelectionMode,
484        trim_trailing_whitespace: bool,
485    ) -> Self {
486        self.input_handler =
487            Some(InputHandler::Internal { selection_mode, trim_trailing_whitespace });
488        self
489    }
490
491    /// Builds the terminal with the configured options.
492    pub fn build(self) -> Result<Terminal, Error> {
493        // setup renderer
494        let renderer = match self.canvas {
495            CanvasSource::Id(id) => Renderer::create(&id)?,
496            CanvasSource::Element(element) => Renderer::create_with_canvas(element)?,
497        };
498        let renderer = renderer.canvas_padding_color(self.canvas_padding_color);
499
500        // load font atlas
501        let gl = renderer.gl();
502        let atlas: FontAtlas = match self.atlas_kind {
503            AtlasKind::Static(atlas_data) => {
504                StaticFontAtlas::load(gl, atlas_data.unwrap_or_default())?.into()
505            },
506            AtlasKind::Dynamic { font_family, font_size } => {
507                DynamicFontAtlas::new(gl, &font_family, font_size, None)?.into()
508            },
509            AtlasKind::DebugDynamic { font_family, font_size, debug_space_pattern } => {
510                DynamicFontAtlas::new(gl, &font_family, font_size, Some(debug_space_pattern))?
511                    .into()
512            },
513        };
514
515        // create terminal grid
516        let canvas_size = renderer.canvas_size();
517        let mut grid = TerminalGrid::new(gl, atlas, canvas_size)?;
518        if let Some(fallback) = self.fallback_glyph {
519            grid.set_fallback_glyph(&fallback)
520        };
521        let grid = Rc::new(RefCell::new(grid));
522
523        // Set up context loss handler for automatic recovery
524        let context_loss_handler = ContextLossHandler::new(renderer.canvas()).ok();
525
526        // initialize mouse handler if needed
527        let selection = grid.borrow().selection_tracker();
528        match self.input_handler {
529            None => Ok(Terminal {
530                renderer,
531                grid,
532                mouse_handler: None,
533                context_loss_handler,
534            }),
535            Some(InputHandler::Internal { selection_mode, trim_trailing_whitespace }) => {
536                let handler = DefaultSelectionHandler::new(
537                    grid.clone(),
538                    selection_mode,
539                    trim_trailing_whitespace,
540                );
541
542                let mut mouse_input = TerminalMouseHandler::new(
543                    renderer.canvas(),
544                    grid.clone(),
545                    handler.create_event_handler(selection),
546                )?;
547                mouse_input.default_input_handler = Some(handler);
548
549                Ok(Terminal {
550                    renderer,
551                    grid,
552                    mouse_handler: Some(mouse_input),
553                    context_loss_handler,
554                })
555            },
556            Some(InputHandler::Mouse(callback)) => {
557                let mouse_input =
558                    TerminalMouseHandler::new(renderer.canvas(), grid.clone(), callback)?;
559                Ok(Terminal {
560                    renderer,
561                    grid,
562                    mouse_handler: Some(mouse_input),
563                    context_loss_handler,
564                })
565            },
566        }
567        .inspect(|terminal| {
568            if self.enable_debug_api {
569                terminal.expose_to_console();
570            }
571        })
572    }
573}
574
575enum InputHandler {
576    Mouse(MouseEventCallback),
577    Internal {
578        selection_mode: SelectionMode,
579        trim_trailing_whitespace: bool,
580    },
581}
582
583/// Checks if a grapheme is double-width (emoji or fullwidth character).
584pub(crate) fn is_double_width(grapheme: &str) -> bool {
585    grapheme.len() > 1 && (emojis::get(grapheme).is_some() || grapheme.width() == 2)
586}
587
588/// Debug API exposed to browser console for terminal inspection.
589#[wasm_bindgen]
590pub struct TerminalDebugApi {
591    grid: Rc<RefCell<TerminalGrid>>,
592}
593
594#[wasm_bindgen]
595impl TerminalDebugApi {
596    /// Returns an array of glyphs that were requested but not found in the font atlas.
597    #[wasm_bindgen(js_name = "getMissingGlyphs")]
598    pub fn get_missing_glyphs(&self) -> js_sys::Array {
599        let missing_set = self
600            .grid
601            .borrow()
602            .atlas()
603            .glyph_tracker()
604            .missing_glyphs();
605        let mut missing: Vec<_> = missing_set.into_iter().collect();
606        missing.sort();
607
608        let js_array = js_sys::Array::new();
609        for glyph in missing {
610            js_array.push(&JsValue::from_str(&glyph));
611        }
612        js_array
613    }
614
615    /// Returns the terminal size in cells as an object with `cols` and `rows` fields.
616    #[wasm_bindgen(js_name = "getTerminalSize")]
617    pub fn get_terminal_size(&self) -> JsValue {
618        let (cols, rows) = self.grid.borrow().terminal_size();
619        let obj = js_sys::Object::new();
620
621        js_sys::Reflect::set(&obj, &"cols".into(), &JsValue::from(cols)).unwrap();
622        js_sys::Reflect::set(&obj, &"rows".into(), &JsValue::from(rows)).unwrap();
623
624        obj.into()
625    }
626
627    /// Returns the canvas size in pixels as an object with `width` and `height` fields.
628    #[wasm_bindgen(js_name = "getCanvasSize")]
629    pub fn get_canvas_size(&self) -> JsValue {
630        let (width, height) = self.grid.borrow().canvas_size();
631        let obj = js_sys::Object::new();
632
633        js_sys::Reflect::set(&obj, &"width".into(), &JsValue::from(width)).unwrap();
634        js_sys::Reflect::set(&obj, &"height".into(), &JsValue::from(height)).unwrap();
635
636        obj.into()
637    }
638
639    /// Returns the number of glyphs available in the font atlas.
640    #[wasm_bindgen(js_name = "getGlyphCount")]
641    pub fn get_glyph_count(&self) -> u32 {
642        self.grid.borrow().atlas().glyph_count()
643    }
644
645    /// Returns the base glyph ID for a given symbol, or null if not found.
646    #[wasm_bindgen(js_name = "getBaseGlyphId")]
647    pub fn get_base_glyph_id(&self, symbol: &str) -> Option<u16> {
648        self.grid
649            .borrow()
650            .atlas()
651            .get_base_glyph_id(symbol)
652    }
653
654    /// Returns the symbol for a given glyph ID, or null if not found.
655    #[wasm_bindgen(js_name = "getSymbol")]
656    pub fn get_symbol(&self, glyph_id: u16) -> Option<String> {
657        self.grid
658            .borrow()
659            .atlas()
660            .get_symbol(glyph_id)
661            .map(|s| s.to_string())
662    }
663
664    /// Returns the cell size in pixels as an object with `width` and `height` fields.
665    #[wasm_bindgen(js_name = "getCellSize")]
666    pub fn get_cell_size(&self) -> JsValue {
667        let (width, height) = self.grid.borrow().atlas().cell_size();
668        let obj = js_sys::Object::new();
669
670        js_sys::Reflect::set(&obj, &"width".into(), &JsValue::from(width)).unwrap();
671        js_sys::Reflect::set(&obj, &"height".into(), &JsValue::from(height)).unwrap();
672
673        obj.into()
674    }
675
676    #[wasm_bindgen(js_name = "getAtlasLookup")]
677    pub fn get_symbol_lookup(&self) -> js_sys::Array {
678        let grid = self.grid.borrow();
679        let atlas = grid.atlas();
680
681        let mut glyphs: Vec<(u16, CompactString)> = Vec::new();
682        atlas.for_each_symbol(&mut |glyph_id, symbol| {
683            glyphs.push((glyph_id, symbol.to_compact_string()));
684        });
685
686        glyphs.sort();
687
688        let js_array = js_sys::Array::new();
689        for (glyph_id, symbol) in glyphs.iter() {
690            let obj = js_sys::Object::new();
691            js_sys::Reflect::set(&obj, &"glyph_id".into(), &JsValue::from(*glyph_id)).unwrap();
692            js_sys::Reflect::set(&obj, &"symbol".into(), &JsValue::from(symbol.as_str())).unwrap();
693
694            js_array.push(&obj.into());
695        }
696        js_array
697    }
698}
699
700impl<'a> From<&'a str> for CanvasSource {
701    fn from(id: &'a str) -> Self {
702        CanvasSource::Id(id.into())
703    }
704}
705
706impl From<web_sys::HtmlCanvasElement> for CanvasSource {
707    fn from(element: web_sys::HtmlCanvasElement) -> Self {
708        CanvasSource::Element(element)
709    }
710}
711
712impl<'a> From<&'a web_sys::HtmlCanvasElement> for CanvasSource {
713    fn from(value: &'a web_sys::HtmlCanvasElement) -> Self {
714        value.clone().into()
715    }
716}