beamterm_renderer/
terminal.rs

1use std::{cell::RefCell, rc::Rc};
2
3use beamterm_data::FontAtlasData;
4use compact_str::CompactString;
5
6use crate::{
7    gl::{CellQuery, SelectionMode},
8    mouse::{
9        DefaultSelectionHandler, MouseEventCallback, TerminalMouseEvent, TerminalMouseHandler,
10    },
11    CellData, Error, FontAtlas, Renderer, TerminalGrid,
12};
13
14/// High-performance WebGL2 terminal renderer.
15///
16/// `Terminal` encapsulates the complete terminal rendering system, providing a
17/// simplified API over the underlying [`Renderer`] and [`TerminalGrid`] components.
18///
19///  ## Selection and Mouse Input
20///
21/// The renderer supports mouse-driven text selection with automatic clipboard
22/// integration:
23///
24/// ```rust
25/// // Enable default selection handler
26/// use beamterm_renderer::{SelectionMode, Terminal};
27///
28/// let terminal = Terminal::builder("#canvas")
29///     .default_mouse_input_handler(SelectionMode::Linear, true)
30///     .build()?;
31///
32/// // Or implement custom mouse handling
33/// let terminal = Terminal::builder("#canvas")
34///     .mouse_input_handler(|event, grid| {
35///         // Custom handler logic
36///     })
37///     .build()?;
38///```
39///
40/// # Examples
41///
42/// ```rust
43/// use beamterm_renderer::{CellData, Terminal};
44///
45/// // Create and render a simple terminal
46/// let mut terminal = Terminal::builder("#canvas").build()?;
47///
48/// // Update cells with content
49/// let cells: Vec<CellData> = unimplemented!();
50/// terminal.update_cells(cells.into_iter())?;
51///
52/// // Render frame
53/// terminal.render_frame()?;
54///
55/// // Handle window resize
56/// let (new_width, new_height) = (800, 600);
57/// terminal.resize(new_width, new_height)?;
58/// ```
59#[derive(Debug)]
60pub struct Terminal {
61    renderer: Renderer,
62    grid: Rc<RefCell<TerminalGrid>>,
63    mouse_handler: Option<TerminalMouseHandler>, // 🐀
64}
65
66impl Terminal {
67    /// Creates a new terminal builder with the specified canvas source.
68    ///
69    /// # Parameters
70    /// * `canvas` - Canvas identifier (CSS selector) or `HtmlCanvasElement`
71    ///
72    /// # Examples
73    ///
74    /// ```rust
75    /// // Using CSS selector
76    /// use web_sys::HtmlCanvasElement;
77    /// use beamterm_renderer::Terminal;
78    ///
79    /// let terminal = Terminal::builder("my-terminal").build()?;
80    ///
81    /// // Using canvas element
82    /// let canvas: &HtmlCanvasElement = unimplemented!("document.get_element_by_id(...)");
83    /// let terminal = Terminal::builder(canvas).build()?;
84    /// ```
85    #[allow(private_bounds)]
86    pub fn builder(canvas: impl Into<CanvasSource>) -> TerminalBuilder {
87        TerminalBuilder::new(canvas.into())
88    }
89
90    /// Updates terminal cell content efficiently.
91    ///
92    /// This method batches all cell updates and uploads them to the GPU in a single
93    /// operation. For optimal performance, collect all changes and update in one call
94    /// rather than making multiple calls for individual cells.
95    ///
96    /// Delegates to [`TerminalGrid::update_cells`].
97    pub fn update_cells<'a>(
98        &mut self,
99        cells: impl Iterator<Item = CellData<'a>>,
100    ) -> Result<(), Error> {
101        self.grid.borrow_mut().update_cells(self.renderer.gl(), cells)
102    }
103
104    /// Returns the WebGL2 rendering context.
105    pub fn gl(&self) -> &web_sys::WebGl2RenderingContext {
106        self.renderer.gl()
107    }
108
109    /// Resizes the terminal to fit new canvas dimensions.
110    ///
111    /// This method updates both the renderer viewport and terminal grid to match
112    /// the new canvas size. The terminal dimensions (in cells) are automatically
113    /// recalculated based on the cell size from the font atlas.
114    ///
115    /// Combines [`Renderer::resize`] and [`TerminalGrid::resize`] operations.
116    pub fn resize(&mut self, width: i32, height: i32) -> Result<(), Error> {
117        self.renderer.resize(width, height);
118        self.grid.borrow_mut().resize(self.renderer.gl(), (width, height))?;
119
120        if let Some(mouse_input) = &mut self.mouse_handler {
121            let (cols, rows) = self.grid.borrow_mut().terminal_size();
122            mouse_input.update_dimensions(cols, rows);
123        }
124
125        Ok(())
126    }
127
128    /// Returns the terminal dimensions in cells.
129    pub fn terminal_size(&self) -> (u16, u16) {
130        self.grid.borrow().terminal_size()
131    }
132
133    /// Returns the total number of cells in the terminal grid.
134    pub fn cell_count(&self) -> usize {
135        self.grid.borrow().cell_count()
136    }
137
138    /// Returns the size of the canvas in pixels.
139    pub fn canvas_size(&self) -> (i32, i32) {
140        self.renderer.canvas_size()
141    }
142
143    /// Returns the size of each cell in pixels.
144    pub fn cell_size(&self) -> (i32, i32) {
145        self.grid.borrow().cell_size()
146    }
147
148    /// Returns a reference to the HTML canvas element used for rendering.
149    pub fn canvas(&self) -> &web_sys::HtmlCanvasElement {
150        self.renderer.canvas()
151    }
152
153    /// Returns a reference to the underlying renderer.
154    pub fn renderer(&self) -> &Renderer {
155        &self.renderer
156    }
157
158    /// Returns the textual content of the specified cell selection.
159    pub fn get_text(&self, selection: CellQuery) -> CompactString {
160        self.grid.borrow().get_text(selection)
161    }
162
163    /// Renders the current terminal state to the canvas.
164    ///
165    /// This method performs the complete render pipeline: frame setup, grid rendering,
166    /// and frame finalization. Call this after updating terminal content to display
167    /// the changes.
168    ///
169    /// Combines [`Renderer::begin_frame`], [`Renderer::render`], and [`Renderer::end_frame`].
170    pub fn render_frame(&mut self) -> Result<(), Error> {
171        self.renderer.begin_frame();
172        self.renderer.render(&*self.grid.borrow());
173        self.renderer.end_frame();
174        Ok(())
175    }
176}
177
178/// Canvas source for terminal initialization.
179///
180/// Supports both CSS selector strings and direct `HtmlCanvasElement` references
181/// for flexible terminal creation.
182enum CanvasSource {
183    /// CSS selector string for canvas lookup (e.g., "#terminal", "canvas").
184    Id(CompactString),
185    /// Direct reference to an existing canvas element.
186    Element(web_sys::HtmlCanvasElement),
187}
188
189/// Builder for configuring and creating a [`Terminal`].
190///
191/// Provides a fluent API for terminal configuration with sensible defaults.
192/// The terminal will use the default embedded font atlas unless explicitly configured.
193///
194/// # Examples
195///
196/// ```rust
197/// // Simple terminal with default configuration
198/// use beamterm_renderer::{FontAtlas, FontAtlasData, Terminal};
199///
200/// let terminal = Terminal::builder("#canvas").build()?;
201///
202/// // Terminal with custom font atlas
203/// let atlas = FontAtlasData::from_binary(unimplemented!(".atlas data"))?;
204/// let terminal = Terminal::builder("#canvas")
205///     .font_atlas(atlas)
206///     .fallback_glyph("X".into())
207///     .build()?;
208/// ```
209pub struct TerminalBuilder {
210    canvas: CanvasSource,
211    atlas_data: Option<FontAtlasData>,
212    fallback_glyph: Option<CompactString>,
213    input_handler: Option<InputHandler>,
214    canvas_padding_color: u32,
215}
216
217impl TerminalBuilder {
218    /// Creates a new terminal builder with the specified canvas source.
219    fn new(canvas: CanvasSource) -> Self {
220        TerminalBuilder {
221            canvas,
222            atlas_data: None,
223            fallback_glyph: None,
224            input_handler: None,
225            canvas_padding_color: 0x000000,
226        }
227    }
228
229    /// Sets a custom font atlas for the terminal.
230    ///
231    /// By default, the terminal uses an embedded font atlas. Use this method
232    /// to provide a custom atlas with different fonts, sizes, or character sets.
233    pub fn font_atlas(mut self, atlas: FontAtlasData) -> Self {
234        self.atlas_data = Some(atlas);
235        self
236    }
237
238    /// Sets the fallback glyph for missing characters.
239    ///
240    /// When a character is not found in the font atlas, this glyph will be
241    /// displayed instead. Defaults to a space character if not specified.
242    pub fn fallback_glyph(mut self, glyph: &str) -> Self {
243        self.fallback_glyph = Some(glyph.into());
244        self
245    }
246
247    /// Sets the background color for the canvas area outside the terminal grid.
248    ///
249    /// When the canvas dimensions don't align perfectly with the terminal cell grid,
250    /// there may be unused pixels around the edges. This color fills those padding
251    /// areas to maintain a consistent appearance.
252    pub fn canvas_padding_color(mut self, color: u32) -> Self {
253        self.canvas_padding_color = color;
254        self
255    }
256
257    /// Sets a callback for handling terminal mouse input events.
258    pub fn mouse_input_handler<F>(mut self, callback: F) -> Self
259    where
260        F: FnMut(TerminalMouseEvent, &TerminalGrid) + 'static,
261    {
262        self.input_handler = Some(InputHandler::Mouse(Box::new(callback)));
263        self
264    }
265
266    /// Sets a default selection handler for mouse input events. Left
267    /// button selects text, `Ctrl/Cmd + C` copies the selected text to
268    /// the clipboard.
269    pub fn default_mouse_input_handler(
270        mut self,
271        selection_mode: SelectionMode,
272        trim_trailing_whitespace: bool,
273    ) -> Self {
274        self.input_handler =
275            Some(InputHandler::Internal { selection_mode, trim_trailing_whitespace });
276        self
277    }
278
279    /// Builds the terminal with the configured options.
280    pub fn build(self) -> Result<Terminal, Error> {
281        // setup renderer
282        let renderer = match self.canvas {
283            CanvasSource::Id(id) => Renderer::create(&id)?,
284            CanvasSource::Element(element) => Renderer::create_with_canvas(element)?,
285        };
286        let renderer = renderer.canvas_padding_color(self.canvas_padding_color);
287
288        // load font atlas
289        let gl = renderer.gl();
290        let atlas = FontAtlas::load(gl, self.atlas_data.unwrap_or_default())?;
291
292        // create terminal grid
293        let canvas_size = renderer.canvas_size();
294        let mut grid = TerminalGrid::new(gl, atlas, canvas_size)?;
295        if let Some(fallback) = self.fallback_glyph {
296            grid.set_fallback_glyph(&fallback)
297        };
298        let grid = Rc::new(RefCell::new(grid));
299
300        // initialize mouse handler if needed
301        let selection = grid.borrow().selection_tracker();
302        match self.input_handler {
303            None => Ok(Terminal { renderer, grid, mouse_handler: None }),
304            Some(InputHandler::Internal { selection_mode, trim_trailing_whitespace }) => {
305                let handler = DefaultSelectionHandler::new(
306                    grid.clone(),
307                    selection_mode,
308                    trim_trailing_whitespace,
309                );
310
311                let mut mouse_input = TerminalMouseHandler::new(
312                    renderer.canvas(),
313                    grid.clone(),
314                    handler.create_event_handler(selection),
315                )?;
316                mouse_input.default_input_handler = Some(handler);
317
318                Ok(Terminal {
319                    renderer,
320                    grid,
321                    mouse_handler: Some(mouse_input),
322                })
323            },
324            Some(InputHandler::Mouse(callback)) => {
325                let mouse_input =
326                    TerminalMouseHandler::new(renderer.canvas(), grid.clone(), callback)?;
327                Ok(Terminal {
328                    renderer,
329                    grid,
330                    mouse_handler: Some(mouse_input),
331                })
332            },
333        }
334    }
335}
336
337enum InputHandler {
338    Mouse(MouseEventCallback),
339    Internal {
340        selection_mode: SelectionMode,
341        trim_trailing_whitespace: bool,
342    },
343}
344
345impl<'a> From<&'a str> for CanvasSource {
346    fn from(id: &'a str) -> Self {
347        CanvasSource::Id(id.into())
348    }
349}
350
351impl From<web_sys::HtmlCanvasElement> for CanvasSource {
352    fn from(element: web_sys::HtmlCanvasElement) -> Self {
353        CanvasSource::Element(element)
354    }
355}
356
357impl<'a> From<&'a web_sys::HtmlCanvasElement> for CanvasSource {
358    fn from(value: &'a web_sys::HtmlCanvasElement) -> Self {
359        value.clone().into()
360    }
361}