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    /// Updates terminal cell content efficiently.
105    ///
106    /// This method batches all cell updates and uploads them to the GPU in a single
107    /// operation. For optimal performance, collect all changes and update in one call
108    /// rather than making multiple calls for individual cells.
109    ///
110    /// Delegates to [`TerminalGrid::update_cells_by_position`].
111    pub fn update_cells_by_position<'a>(
112        &mut self,
113        cells: impl Iterator<Item = (u16, u16, CellData<'a>)>,
114    ) -> Result<(), Error> {
115        self.grid.borrow_mut().update_cells_by_position(self.renderer.gl(), cells)
116    }
117
118    /// Returns the WebGL2 rendering context.
119    pub fn gl(&self) -> &web_sys::WebGl2RenderingContext {
120        self.renderer.gl()
121    }
122
123    /// Resizes the terminal to fit new canvas dimensions.
124    ///
125    /// This method updates both the renderer viewport and terminal grid to match
126    /// the new canvas size. The terminal dimensions (in cells) are automatically
127    /// recalculated based on the cell size from the font atlas.
128    ///
129    /// Combines [`Renderer::resize`] and [`TerminalGrid::resize`] operations.
130    pub fn resize(&mut self, width: i32, height: i32) -> Result<(), Error> {
131        self.renderer.resize(width, height);
132        self.grid.borrow_mut().resize(self.renderer.gl(), (width, height))?;
133
134        if let Some(mouse_input) = &mut self.mouse_handler {
135            let (cols, rows) = self.grid.borrow_mut().terminal_size();
136            mouse_input.update_dimensions(cols, rows);
137        }
138
139        Ok(())
140    }
141
142    /// Returns the terminal dimensions in cells.
143    pub fn terminal_size(&self) -> (u16, u16) {
144        self.grid.borrow().terminal_size()
145    }
146
147    /// Returns the total number of cells in the terminal grid.
148    pub fn cell_count(&self) -> usize {
149        self.grid.borrow().cell_count()
150    }
151
152    /// Returns the size of the canvas in pixels.
153    pub fn canvas_size(&self) -> (i32, i32) {
154        self.renderer.canvas_size()
155    }
156
157    /// Returns the size of each cell in pixels.
158    pub fn cell_size(&self) -> (i32, i32) {
159        self.grid.borrow().cell_size()
160    }
161
162    /// Returns a reference to the HTML canvas element used for rendering.
163    pub fn canvas(&self) -> &web_sys::HtmlCanvasElement {
164        self.renderer.canvas()
165    }
166
167    /// Returns a reference to the underlying renderer.
168    pub fn renderer(&self) -> &Renderer {
169        &self.renderer
170    }
171
172    /// Returns a reference to the terminal grid.
173    pub fn grid(&self) -> Rc<RefCell<TerminalGrid>> {
174        self.grid.clone()
175    }
176
177    /// Returns the textual content of the specified cell selection.
178    pub fn get_text(&self, selection: CellQuery) -> CompactString {
179        self.grid.borrow().get_text(selection)
180    }
181
182    /// Renders the current terminal state to the canvas.
183    ///
184    /// This method performs the complete render pipeline: frame setup, grid rendering,
185    /// and frame finalization. Call this after updating terminal content to display
186    /// the changes.
187    ///
188    /// Combines [`Renderer::begin_frame`], [`Renderer::render`], and [`Renderer::end_frame`].
189    pub fn render_frame(&mut self) -> Result<(), Error> {
190        self.grid.borrow_mut().flush_cells(self.renderer.gl())?;
191
192        self.renderer.begin_frame();
193        self.renderer.render(&*self.grid.borrow());
194        self.renderer.end_frame();
195        Ok(())
196    }
197}
198
199/// Canvas source for terminal initialization.
200///
201/// Supports both CSS selector strings and direct `HtmlCanvasElement` references
202/// for flexible terminal creation.
203enum CanvasSource {
204    /// CSS selector string for canvas lookup (e.g., "#terminal", "canvas").
205    Id(CompactString),
206    /// Direct reference to an existing canvas element.
207    Element(web_sys::HtmlCanvasElement),
208}
209
210/// Builder for configuring and creating a [`Terminal`].
211///
212/// Provides a fluent API for terminal configuration with sensible defaults.
213/// The terminal will use the default embedded font atlas unless explicitly configured.
214///
215/// # Examples
216///
217/// ```rust
218/// // Simple terminal with default configuration
219/// use beamterm_renderer::{FontAtlas, FontAtlasData, Terminal};
220///
221/// let terminal = Terminal::builder("#canvas").build()?;
222///
223/// // Terminal with custom font atlas
224/// let atlas = FontAtlasData::from_binary(unimplemented!(".atlas data"))?;
225/// let terminal = Terminal::builder("#canvas")
226///     .font_atlas(atlas)
227///     .fallback_glyph("X".into())
228///     .build()?;
229/// ```
230pub struct TerminalBuilder {
231    canvas: CanvasSource,
232    atlas_data: Option<FontAtlasData>,
233    fallback_glyph: Option<CompactString>,
234    input_handler: Option<InputHandler>,
235    canvas_padding_color: u32,
236}
237
238impl TerminalBuilder {
239    /// Creates a new terminal builder with the specified canvas source.
240    fn new(canvas: CanvasSource) -> Self {
241        TerminalBuilder {
242            canvas,
243            atlas_data: None,
244            fallback_glyph: None,
245            input_handler: None,
246            canvas_padding_color: 0x000000,
247        }
248    }
249
250    /// Sets a custom font atlas for the terminal.
251    ///
252    /// By default, the terminal uses an embedded font atlas. Use this method
253    /// to provide a custom atlas with different fonts, sizes, or character sets.
254    pub fn font_atlas(mut self, atlas: FontAtlasData) -> Self {
255        self.atlas_data = Some(atlas);
256        self
257    }
258
259    /// Sets the fallback glyph for missing characters.
260    ///
261    /// When a character is not found in the font atlas, this glyph will be
262    /// displayed instead. Defaults to a space character if not specified.
263    pub fn fallback_glyph(mut self, glyph: &str) -> Self {
264        self.fallback_glyph = Some(glyph.into());
265        self
266    }
267
268    /// Sets the background color for the canvas area outside the terminal grid.
269    ///
270    /// When the canvas dimensions don't align perfectly with the terminal cell grid,
271    /// there may be unused pixels around the edges. This color fills those padding
272    /// areas to maintain a consistent appearance.
273    pub fn canvas_padding_color(mut self, color: u32) -> Self {
274        self.canvas_padding_color = color;
275        self
276    }
277
278    /// Sets a callback for handling terminal mouse input events.
279    pub fn mouse_input_handler<F>(mut self, callback: F) -> Self
280    where
281        F: FnMut(TerminalMouseEvent, &TerminalGrid) + 'static,
282    {
283        self.input_handler = Some(InputHandler::Mouse(Box::new(callback)));
284        self
285    }
286
287    /// Sets a default selection handler for mouse input events. Left
288    /// button selects text, `Ctrl/Cmd + C` copies the selected text to
289    /// the clipboard.
290    pub fn default_mouse_input_handler(
291        mut self,
292        selection_mode: SelectionMode,
293        trim_trailing_whitespace: bool,
294    ) -> Self {
295        self.input_handler =
296            Some(InputHandler::Internal { selection_mode, trim_trailing_whitespace });
297        self
298    }
299
300    /// Builds the terminal with the configured options.
301    pub fn build(self) -> Result<Terminal, Error> {
302        // setup renderer
303        let renderer = match self.canvas {
304            CanvasSource::Id(id) => Renderer::create(&id)?,
305            CanvasSource::Element(element) => Renderer::create_with_canvas(element)?,
306        };
307        let renderer = renderer.canvas_padding_color(self.canvas_padding_color);
308
309        // load font atlas
310        let gl = renderer.gl();
311        let atlas = FontAtlas::load(gl, self.atlas_data.unwrap_or_default())?;
312
313        // create terminal grid
314        let canvas_size = renderer.canvas_size();
315        let mut grid = TerminalGrid::new(gl, atlas, canvas_size)?;
316        if let Some(fallback) = self.fallback_glyph {
317            grid.set_fallback_glyph(&fallback)
318        };
319        let grid = Rc::new(RefCell::new(grid));
320
321        // initialize mouse handler if needed
322        let selection = grid.borrow().selection_tracker();
323        match self.input_handler {
324            None => Ok(Terminal { renderer, grid, mouse_handler: None }),
325            Some(InputHandler::Internal { selection_mode, trim_trailing_whitespace }) => {
326                let handler = DefaultSelectionHandler::new(
327                    grid.clone(),
328                    selection_mode,
329                    trim_trailing_whitespace,
330                );
331
332                let mut mouse_input = TerminalMouseHandler::new(
333                    renderer.canvas(),
334                    grid.clone(),
335                    handler.create_event_handler(selection),
336                )?;
337                mouse_input.default_input_handler = Some(handler);
338
339                Ok(Terminal {
340                    renderer,
341                    grid,
342                    mouse_handler: Some(mouse_input),
343                })
344            },
345            Some(InputHandler::Mouse(callback)) => {
346                let mouse_input =
347                    TerminalMouseHandler::new(renderer.canvas(), grid.clone(), callback)?;
348                Ok(Terminal {
349                    renderer,
350                    grid,
351                    mouse_handler: Some(mouse_input),
352                })
353            },
354        }
355    }
356}
357
358enum InputHandler {
359    Mouse(MouseEventCallback),
360    Internal {
361        selection_mode: SelectionMode,
362        trim_trailing_whitespace: bool,
363    },
364}
365
366impl<'a> From<&'a str> for CanvasSource {
367    fn from(id: &'a str) -> Self {
368        CanvasSource::Id(id.into())
369    }
370}
371
372impl From<web_sys::HtmlCanvasElement> for CanvasSource {
373    fn from(element: web_sys::HtmlCanvasElement) -> Self {
374        CanvasSource::Element(element)
375    }
376}
377
378impl<'a> From<&'a web_sys::HtmlCanvasElement> for CanvasSource {
379    fn from(value: &'a web_sys::HtmlCanvasElement) -> Self {
380        value.clone().into()
381    }
382}