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}