tui_lipan/widgets/ascii_canvas/mod.rs
1//! ASCII canvas widget.
2//!
3//! `AsciiCanvas` is a versatile widget for displaying ASCII art, cell grids,
4//! and multi-frame sprite sheets. It supports:
5//!
6//! - **Static text lines** - simple rows of text
7//! - **Cell grids** - per-cell styled character buffers
8//! - **Frame sequences** - multi-frame displays with tag-based lookup
9//! (sprite sheets, interactive animations, directional sprites)
10
11pub mod animation;
12
13mod layout;
14mod node;
15mod reconcile;
16
17pub(crate) use layout::measure_ascii_canvas;
18pub use node::AsciiCanvasNode;
19pub(crate) use reconcile::reconcile_ascii_canvas;
20
21use std::sync::Arc;
22
23use crate::core::element::{Element, ElementKind};
24use crate::style::{Length, Style};
25use crate::utils::gradient::{ColorGradient, GradientDirection};
26
27use self::animation::FrameSequence;
28
29/// A single cell in an ASCII canvas.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub struct AsciiCell {
32 /// Cell character.
33 pub ch: char,
34 /// Cell style.
35 pub style: Style,
36}
37
38impl Default for AsciiCell {
39 fn default() -> Self {
40 Self {
41 ch: ' ',
42 style: Style::default(),
43 }
44 }
45}
46
47impl AsciiCell {
48 /// Create a new cell with the given character.
49 pub fn new(ch: char) -> Self {
50 Self {
51 ch,
52 style: Style::default(),
53 }
54 }
55
56 /// Set the cell style.
57 pub fn style(mut self, style: Style) -> Self {
58 self.style = style;
59 self
60 }
61}
62
63impl From<char> for AsciiCell {
64 fn from(ch: char) -> Self {
65 Self::new(ch)
66 }
67}
68
69/// A mutable buffer for building an [`AsciiCanvas`].
70#[derive(Clone, Debug)]
71pub struct AsciiCanvasBuffer {
72 width: u16,
73 height: u16,
74 cells: Vec<AsciiCell>,
75}
76
77impl Default for AsciiCanvasBuffer {
78 fn default() -> Self {
79 Self::new(0, 0)
80 }
81}
82
83impl AsciiCanvasBuffer {
84 /// Create a new buffer filled with spaces.
85 pub fn new(width: u16, height: u16) -> Self {
86 let len = width as usize * height as usize;
87 Self {
88 width,
89 height,
90 cells: vec![AsciiCell::default(); len],
91 }
92 }
93
94 /// Buffer width in cells.
95 pub fn width(&self) -> u16 {
96 self.width
97 }
98
99 /// Buffer height in cells.
100 pub fn height(&self) -> u16 {
101 self.height
102 }
103
104 /// Fill the buffer with a single cell value.
105 pub fn fill(&mut self, cell: AsciiCell) {
106 self.cells.fill(cell);
107 }
108
109 /// Fill the buffer with a single character.
110 pub fn fill_char(&mut self, ch: char) {
111 self.fill(AsciiCell::new(ch));
112 }
113
114 /// Set a cell at a position.
115 pub fn set(&mut self, x: u16, y: u16, cell: AsciiCell) {
116 if x >= self.width || y >= self.height {
117 return;
118 }
119 let idx = (y as usize).saturating_mul(self.width as usize) + x as usize;
120 if let Some(slot) = self.cells.get_mut(idx) {
121 *slot = cell;
122 }
123 }
124
125 /// Set a character at a position.
126 pub fn set_char(&mut self, x: u16, y: u16, ch: char) {
127 self.set(x, y, AsciiCell::new(ch));
128 }
129
130 /// Borrow the cell slice.
131 pub fn cells(&self) -> &[AsciiCell] {
132 &self.cells
133 }
134
135 /// Collect all unique colors (both foreground and background) used in this
136 /// buffer, in order of first appearance.
137 ///
138 /// **Note:** if the same color value is used in both fg and bg, it appears
139 /// only once in the returned list. When you need to map the same color
140 /// differently per channel, use [`collect_fg_colors`](Self::collect_fg_colors)
141 /// and [`collect_bg_colors`](Self::collect_bg_colors) instead.
142 pub fn collect_colors(&self) -> Vec<crate::style::Color> {
143 let mut seen = std::collections::HashSet::new();
144 let mut out = Vec::new();
145 for cell in &self.cells {
146 for color in [cell.style.fg, cell.style.bg]
147 .into_iter()
148 .flatten()
149 .map(crate::style::Paint::color)
150 {
151 if seen.insert(color) {
152 out.push(color);
153 }
154 }
155 }
156 out
157 }
158
159 /// Collect all unique **foreground** colors used in this buffer, in order
160 /// of first appearance.
161 pub fn collect_fg_colors(&self) -> Vec<crate::style::Color> {
162 let mut seen = std::collections::HashSet::new();
163 let mut out = Vec::new();
164 for cell in &self.cells {
165 if let Some(color) = cell.style.fg.map(crate::style::Paint::color)
166 && seen.insert(color)
167 {
168 out.push(color);
169 }
170 }
171 out
172 }
173
174 /// Collect all unique **background** colors used in this buffer, in order
175 /// of first appearance.
176 pub fn collect_bg_colors(&self) -> Vec<crate::style::Color> {
177 let mut seen = std::collections::HashSet::new();
178 let mut out = Vec::new();
179 for cell in &self.cells {
180 if let Some(color) = cell.style.bg.map(crate::style::Paint::color)
181 && seen.insert(color)
182 {
183 out.push(color);
184 }
185 }
186 out
187 }
188}
189
190/// A 2D ASCII canvas element.
191///
192/// Supports three display modes:
193///
194/// 1. **Text lines** - created with [`AsciiCanvas::new`]
195/// 2. **Cell grid** - created with [`AsciiCanvas::from_cells`] or [`From<AsciiCanvasBuffer>`]
196/// 3. **Frame sequence** - created with [`AsciiCanvas::from_sequence`], displays one frame
197/// at a time from a [`FrameSequence`] (sprite sheet / animation)
198///
199/// # Examples
200///
201/// ```rust,ignore
202/// // Static text
203/// AsciiCanvas::new([" ██ ", " ████ ", "██████"])
204///
205/// // Cell grid
206/// AsciiCanvas::from(buffer)
207///
208/// // Multi-frame sprite sheet
209/// AsciiCanvas::from_sequence(sequence)
210/// .frame_by_tag("direction", "left")
211/// .style(Style::new().fg(Color::White))
212/// ```
213#[derive(Clone)]
214pub struct AsciiCanvas {
215 /// Rows of text for the canvas.
216 pub lines: Vec<Arc<str>>,
217 /// Optional styled cells (row-major, len == width * height).
218 pub cells: Option<Arc<[AsciiCell]>>,
219 /// Explicit grid size for cell layouts.
220 pub grid_size: Option<(u16, u16)>,
221 /// Optional frame sequence for multi-frame display.
222 pub sequence: Option<Arc<FrameSequence>>,
223 /// Current frame index (only used when `sequence` is set).
224 pub current_frame: usize,
225 /// Base style applied to all cells.
226 pub style: Style,
227 /// Background fill style (only bg is respected).
228 pub background: Option<Style>,
229 /// Requested width.
230 /// Default: `Length::Auto`.
231 pub width: Length,
232 /// Requested height.
233 /// Default: `Length::Auto`.
234 pub height: Length,
235 /// Optional color gradient applied at render time.
236 pub gradient: Option<(ColorGradient, GradientDirection)>,
237 /// Optional color remapping applied at render time to **both** fg and bg
238 /// channels.
239 ///
240 /// Each entry maps a source color to a replacement color. The mapping is
241 /// applied to both foreground and background channels - if a cell's fg or
242 /// bg matches a source entry, it is replaced with the corresponding target
243 /// color. Colors not present in the map are rendered unchanged.
244 ///
245 /// When the same source color appears in both fg and bg but needs
246 /// different replacements, use [`fg_color_map`](Self::fg_color_map) and/or
247 /// [`bg_color_map`](Self::bg_color_map) instead (they take precedence over
248 /// this field for their respective channel).
249 ///
250 /// See [`FrameSequence::collect_colors`] to discover which colors an
251 /// asset uses.
252 pub color_map: Option<Arc<[(crate::style::Color, crate::style::Color)]>>,
253 /// Optional color remapping applied **only** to the foreground channel.
254 ///
255 /// Takes precedence over [`color_map`](Self::color_map) for fg lookups.
256 /// See [`FrameSequence::collect_fg_colors`] to discover fg-specific colors.
257 pub fg_color_map: Option<Arc<[(crate::style::Color, crate::style::Color)]>>,
258 /// Optional color remapping applied **only** to the background channel.
259 ///
260 /// Takes precedence over [`color_map`](Self::color_map) for bg lookups.
261 /// See [`FrameSequence::collect_bg_colors`] to discover bg-specific colors.
262 pub bg_color_map: Option<Arc<[(crate::style::Color, crate::style::Color)]>>,
263}
264
265impl Default for AsciiCanvas {
266 fn default() -> Self {
267 Self {
268 lines: Vec::new(),
269 cells: None,
270 grid_size: None,
271 sequence: None,
272 current_frame: 0,
273 style: Style::default(),
274 background: None,
275 width: Length::Auto,
276 height: Length::Auto,
277 gradient: None,
278 color_map: None,
279 fg_color_map: None,
280 bg_color_map: None,
281 }
282 }
283}
284
285impl AsciiCanvas {
286 /// Create a new canvas from text lines.
287 pub fn new(lines: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Self {
288 Self {
289 lines: lines.into_iter().map(Into::into).collect(),
290 ..Self::default()
291 }
292 }
293
294 /// Create a blank canvas with an empty cell grid.
295 pub fn blank(width: u16, height: u16) -> Self {
296 let len = width as usize * height as usize;
297 Self::from_cells(width, height, vec![AsciiCell::default(); len])
298 }
299
300 /// Create a canvas by generating each cell from a callback.
301 pub fn with_cell_fn(width: u16, height: u16, mut f: impl FnMut(u16, u16) -> AsciiCell) -> Self {
302 let mut cells = Vec::with_capacity(width as usize * height as usize);
303 for y in 0..height {
304 for x in 0..width {
305 cells.push(f(x, y));
306 }
307 }
308 Self::from_cells(width, height, cells)
309 }
310
311 /// Create a canvas from a flat row-major grid of cells.
312 pub fn from_cells(width: u16, height: u16, cells: impl Into<Arc<[AsciiCell]>>) -> Self {
313 Self {
314 lines: Vec::new(),
315 cells: Some(cells.into()),
316 grid_size: Some((width, height)),
317 ..Self::default()
318 }
319 }
320
321 /// Create a canvas from a frame sequence (sprite sheet / animation).
322 ///
323 /// Displays the first frame by default. Use [`frame`](Self::frame) or
324 /// [`frame_by_tag`](Self::frame_by_tag) to select a specific frame.
325 pub fn from_sequence(sequence: Arc<FrameSequence>) -> Self {
326 Self {
327 sequence: Some(sequence),
328 ..Self::default()
329 }
330 }
331
332 /// Provide a flat row-major grid of cells.
333 pub fn cells(mut self, cells: impl Into<Arc<[AsciiCell]>>) -> Self {
334 self.cells = Some(cells.into());
335 self
336 }
337
338 /// Set the explicit grid size for cell rendering.
339 pub fn grid_size(mut self, width: u16, height: u16) -> Self {
340 self.grid_size = Some((width, height));
341 self
342 }
343
344 /// Set the current frame index (for sequence mode).
345 pub fn frame(mut self, idx: usize) -> Self {
346 if let Some(ref seq) = self.sequence {
347 self.current_frame = idx.min(seq.len().saturating_sub(1));
348 }
349 self
350 }
351
352 /// Set the current frame by tag lookup (for sequence mode).
353 ///
354 /// If no frame with the given tag is found, the current frame is unchanged.
355 pub fn frame_by_tag(mut self, key: &str, value: &str) -> Self {
356 if let Some(ref seq) = self.sequence
357 && let Some(idx) = seq.find_by_tag(key, value)
358 {
359 self.current_frame = idx;
360 }
361 self
362 }
363
364 /// Set the base style.
365 pub fn style(mut self, style: Style) -> Self {
366 self.style = style;
367 self
368 }
369
370 /// Set background style (only bg is used).
371 pub fn background(mut self, style: Style) -> Self {
372 self.background = Some(style);
373 self
374 }
375
376 /// Set requested width.
377 pub fn width(mut self, width: Length) -> Self {
378 self.width = width;
379 self
380 }
381
382 /// Set requested height.
383 pub fn height(mut self, height: Length) -> Self {
384 self.height = height;
385 self
386 }
387
388 /// Apply a color remapping over the rendered output to **both** fg and bg
389 /// channels.
390 ///
391 /// Each entry maps a source color to a replacement color. The mapping is
392 /// applied to both foreground and background channels - if a cell's fg or
393 /// bg matches a source entry, it is replaced with the corresponding target
394 /// color. Colors not present in the map are rendered unchanged.
395 ///
396 /// When the same source color appears in both fg and bg but needs
397 /// different replacements, use [`fg_color_map`](Self::fg_color_map) and/or
398 /// [`bg_color_map`](Self::bg_color_map) instead.
399 ///
400 /// Use [`FrameSequence::collect_colors`] or [`AsciiCanvasBuffer::collect_colors`]
401 /// to discover the colors an asset contains, then build a mapping from them
402 /// to your theme colors:
403 ///
404 /// ```rust,ignore
405 /// let colors = sequence.collect_colors(); // [Color::Red, Color::Blue]
406 /// let canvas = AsciiCanvas::from_sequence(arc_seq)
407 /// .color_map(vec![
408 /// (colors[0], theme.selection.fg.unwrap_or(Color::White)),
409 /// (colors[1], theme.primary.fg.unwrap_or(Color::Gray)),
410 /// ]);
411 /// ```
412 pub fn color_map(
413 mut self,
414 map: impl Into<Arc<[(crate::style::Color, crate::style::Color)]>>,
415 ) -> Self {
416 self.color_map = Some(map.into());
417 self
418 }
419
420 /// Apply a color remapping **only** to the foreground channel.
421 ///
422 /// Takes precedence over [`color_map`](Self::color_map) for fg lookups.
423 /// Use [`FrameSequence::collect_fg_colors`] to discover fg-specific colors.
424 ///
425 /// ```rust,ignore
426 /// let fg_colors = sequence.collect_fg_colors();
427 /// let bg_colors = sequence.collect_bg_colors();
428 /// let canvas = AsciiCanvas::from_sequence(arc_seq)
429 /// .fg_color_map(vec![
430 /// (fg_colors[0], Color::White),
431 /// (fg_colors[1], Color::Gray),
432 /// ])
433 /// .bg_color_map(vec![
434 /// (bg_colors[0], Color::Black),
435 /// ]);
436 /// ```
437 pub fn fg_color_map(
438 mut self,
439 map: impl Into<Arc<[(crate::style::Color, crate::style::Color)]>>,
440 ) -> Self {
441 self.fg_color_map = Some(map.into());
442 self
443 }
444
445 /// Apply a color remapping **only** to the background channel.
446 ///
447 /// Takes precedence over [`color_map`](Self::color_map) for bg lookups.
448 /// See [`FrameSequence::collect_bg_colors`] to discover bg-specific colors.
449 pub fn bg_color_map(
450 mut self,
451 map: impl Into<Arc<[(crate::style::Color, crate::style::Color)]>>,
452 ) -> Self {
453 self.bg_color_map = Some(map.into());
454 self
455 }
456
457 /// Apply a color gradient over the rendered output.
458 ///
459 /// For cell-grid and sequence mode the gradient overrides each cell's
460 /// foreground base before per-cell style is patched on top, so cells with
461 /// an explicit `fg` color will still take priority.
462 ///
463 /// For text-lines mode the gradient is the sole foreground color.
464 pub fn gradient(mut self, gradient: ColorGradient, direction: GradientDirection) -> Self {
465 self.gradient = Some((gradient, direction));
466 self
467 }
468
469 /// Get the resolved cells and grid size for the current display mode.
470 ///
471 /// Returns `(cells, grid_width, grid_height)` if in cell/sequence mode,
472 /// or `None` if in text-lines mode.
473 pub fn resolved_cells(&self) -> Option<(&[AsciiCell], u16, u16)> {
474 if let Some(ref seq) = self.sequence {
475 let frame = seq.get(self.current_frame)?;
476 let buf = &frame.buffer;
477 Some((buf.cells(), buf.width(), buf.height()))
478 } else {
479 let cells = self.cells.as_ref()?;
480 let (w, h) = self.grid_size.unwrap_or((0, 0));
481 Some((cells, w, h))
482 }
483 }
484
485 /// Get the effective width of the content.
486 pub fn content_width(&self) -> u16 {
487 if let Some(ref seq) = self.sequence {
488 return seq.width();
489 }
490 if let Some((w, _)) = self.grid_size {
491 return w;
492 }
493 self.lines
494 .iter()
495 .map(|l| l.chars().count() as u16)
496 .max()
497 .unwrap_or(0)
498 }
499
500 /// Get the effective height of the content.
501 pub fn content_height(&self) -> u16 {
502 if let Some(ref seq) = self.sequence {
503 return seq.height();
504 }
505 if let Some((_, h)) = self.grid_size {
506 return h;
507 }
508 self.lines.len() as u16
509 }
510}
511
512impl From<AsciiCanvasBuffer> for AsciiCanvas {
513 fn from(buffer: AsciiCanvasBuffer) -> Self {
514 AsciiCanvas::from_cells(buffer.width, buffer.height, buffer.cells)
515 }
516}
517
518impl From<AsciiCanvas> for Element {
519 fn from(value: AsciiCanvas) -> Self {
520 Element::new(ElementKind::AsciiCanvas(value))
521 }
522}
523
524impl crate::layout::hash::LayoutHash for AsciiCanvas {
525 fn layout_hash(
526 &self,
527 hasher: &mut impl std::hash::Hasher,
528 _recurse: &dyn Fn(&crate::core::element::Element) -> Option<u64>,
529 ) -> Option<()> {
530 use std::hash::Hash;
531 self.width.hash(hasher);
532 self.height.hash(hasher);
533 self.grid_size.hash(hasher);
534
535 // Hash sequence identity + current frame for multi-frame mode
536 if let Some(ref seq) = self.sequence {
537 std::sync::Arc::as_ptr(seq).hash(hasher);
538 self.current_frame.hash(hasher);
539 }
540
541 let needs_content =
542 matches!(self.width, Length::Auto) || matches!(self.height, Length::Auto);
543 if needs_content {
544 self.lines.hash(hasher);
545 self.cells.hash(hasher);
546 }
547 Some(())
548 }
549}