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}