| 1 | //! Transparent packed Braille supplied by the host's existing simulation. |
| 2 | //! No clock, activity classification, state transition or color inference. |
| 3 | |
| 4 | use ratatui::{ |
| 5 | buffer::Buffer, |
| 6 | layout::{Alignment, Rect}, |
| 7 | style::Style, |
| 8 | widgets::{Paragraph, Widget, Wrap}, |
| 9 | }; |
| 10 | use unicode_width::UnicodeWidthStr; |
| 11 | |
| 12 | /// Raw-caption companion raster. Tiny viewports retain the complete wrapped |
| 13 | /// non-color cue; zero cells remain transparent. Ink includes host modifiers. |
| 14 | #[derive(Clone, Copy, Debug)] |
| 15 | pub struct BrailleFrame<'a> { |
| 16 | pub cells: &'a [u8], |
| 17 | pub caption: &'a str, |
| 18 | pub style: Style, |
| 19 | } |
| 20 | |
| 21 | /// Shared with Whale's semantic-caption/ombre renderer. Coordinates are the |
| 22 | /// original grid coordinates even when the buffer clips its edges. |
| 23 | pub(crate) fn paint_braille_cells( |
| 24 | area: Rect, |
| 25 | buf: &mut Buffer, |
| 26 | cells: &[u8], |
| 27 | mut ink: impl FnMut(u16) -> Style, |
| 28 | ) { |
| 29 | let visible = area.intersection(buf.area); |
| 30 | for y in visible.y..visible.bottom() { |
| 31 | let row = y - area.y; |
| 32 | let style = ink(row); |
| 33 | for x in visible.x..visible.right() { |
| 34 | let index = usize::from(row) * usize::from(area.width) + usize::from(x - area.x); |
| 35 | let bits = cells.get(index).copied().unwrap_or(0); |
| 36 | if bits != 0 |
| 37 | && let Some(cell) = buf.cell_mut((x, y)) |
| 38 | { |
| 39 | let glyph = char::from_u32(0x2800 + u32::from(bits)).expect("braille"); |
| 40 | let mut encoded = [0; 4]; |
| 41 | cell.set_symbol(glyph.encode_utf8(&mut encoded)) |
| 42 | .set_style(style); |
| 43 | } |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | impl Widget for BrailleFrame<'_> { |
| 49 | fn render(self, area: Rect, buf: &mut Buffer) { |
| 50 | if area.is_empty() { |
| 51 | return; |
| 52 | } |
| 53 | if self.caption.width() > usize::from(area.width) || area.height < 4 { |
| 54 | Paragraph::new(self.caption) |
| 55 | .style(self.style) |
| 56 | .alignment(Alignment::Center) |
| 57 | .wrap(Wrap { trim: false }) |
| 58 | .render(area, buf); |
| 59 | return; |
| 60 | } |
| 61 | paint_braille_cells( |
| 62 | Rect { |
| 63 | height: area.height - 1, |
| 64 | ..area |
| 65 | }, |
| 66 | buf, |
| 67 | self.cells, |
| 68 | |_| self.style, |
| 69 | ); |
| 70 | let x = area |
| 71 | .x |
| 72 | .saturating_add((area.width - self.caption.width() as u16) / 2); |
| 73 | let y = area.bottom().saturating_sub(1); |
| 74 | if buf.area.contains((x, y).into()) { |
| 75 | buf.set_stringn(x, y, self.caption, usize::from(area.width), self.style); |
| 76 | } else if y >= buf.area.y && y < buf.area.bottom() && x < buf.area.right() { |
| 77 | let mut caption = Buffer::empty(Rect::new(0, 0, area.width, 1)); |
| 78 | for offset in 0..area.width { |
| 79 | if let Some(cell) = buf.cell((x.saturating_add(offset), y)) { |
| 80 | caption[(offset, 0)] = cell.clone(); |
| 81 | } |
| 82 | } |
| 83 | let (end, _) = |
| 84 | caption.set_stringn(0, 0, self.caption, usize::from(area.width), self.style); |
| 85 | for offset in 0..end { |
| 86 | if let Some(cell) = buf.cell_mut((x.saturating_add(offset), y)) { |
| 87 | *cell = caption[(offset, 0)].clone(); |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 |