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}