| 1 | //! The shared braille renderer, promoted from the portable pet study's TUI. |
| 2 | //! It owns no clock, telemetry, or motion. The habitat owns placement and ink. |
| 3 | |
| 4 | use super::pet_sim::{PetSim, PetState, braille}; |
| 5 | use ratatui::{ |
| 6 | buffer::Buffer, |
| 7 | layout::{Alignment, Rect}, |
| 8 | style::{Color, Style}, |
| 9 | widgets::{Paragraph, Widget, Wrap}, |
| 10 | }; |
| 11 | use unicode_width::UnicodeWidthStr; |
| 12 | |
| 13 | pub struct PetWidget<'a> { |
| 14 | pub sim: &'a PetSim, |
| 15 | pub state: &'a PetState, |
| 16 | } |
| 17 | |
| 18 | impl Widget for PetWidget<'_> { |
| 19 | fn render(self, area: Rect, buf: &mut Buffer) { |
| 20 | let frame = self.sim.frame; |
| 21 | let label = format!( |
| 22 | "{} · {}{}", |
| 23 | frame.channel, |
| 24 | frame.arch, |
| 25 | if frame.hollow { " · unobserved" } else { "" } |
| 26 | ); |
| 27 | // The creature and its non-colour cue are one unit. A narrow surface |
| 28 | // withholds both instead of silently dropping uncertainty or the gait. |
| 29 | if area.height < 4 || usize::from(area.width) < label.chars().count() { |
| 30 | return; |
| 31 | } |
| 32 | let tank = Rect { |
| 33 | height: area.height - 1, |
| 34 | ..area |
| 35 | }; |
| 36 | let grid = braille( |
| 37 | self.sim, |
| 38 | tank.width as usize, |
| 39 | tank.height as usize, |
| 40 | self.state, |
| 41 | ); |
| 42 | let fg = Color::Rgb( |
| 43 | (frame.r * frame.alpha).clamp(0.0, 255.0) as u8, |
| 44 | (frame.g * frame.alpha).clamp(0.0, 255.0) as u8, |
| 45 | (frame.b * frame.alpha).clamp(0.0, 255.0) as u8, |
| 46 | ); |
| 47 | render_grid(area, buf, &grid, &label, Style::default().fg(fg)); |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | /// Shared paint path for the Rust cameo and the embedded world's live raster. |
| 52 | /// A tiny viewport keeps the complete text cue before spending cells on dots. |
| 53 | pub(crate) fn render_grid(area: Rect, buf: &mut Buffer, grid: &[u8], label: &str, style: Style) { |
| 54 | if area.width == 0 || area.height == 0 { |
| 55 | return; |
| 56 | } |
| 57 | if label.width() > usize::from(area.width) || area.height < 4 { |
| 58 | Paragraph::new(label) |
| 59 | .style(style) |
| 60 | .alignment(Alignment::Center) |
| 61 | .wrap(Wrap { trim: false }) |
| 62 | .render(area, buf); |
| 63 | return; |
| 64 | } |
| 65 | for y in 0..area.height - 1 { |
| 66 | for x in 0..area.width { |
| 67 | let bits = grid |
| 68 | .get(usize::from(y) * usize::from(area.width) + usize::from(x)) |
| 69 | .copied() |
| 70 | .unwrap_or(0); |
| 71 | if bits != 0 |
| 72 | && let Some(cell) = buf.cell_mut((area.x + x, area.y + y)) |
| 73 | { |
| 74 | let glyph = char::from_u32(0x2800 + u32::from(bits)).expect("braille"); |
| 75 | cell.set_symbol(&glyph.to_string()).set_style(style); |
| 76 | } |
| 77 | } |
| 78 | } |
| 79 | let x = area.x + (area.width - label.width() as u16) / 2; |
| 80 | buf.set_stringn(x, area.bottom() - 1, label, usize::from(area.width), style); |
| 81 | } |
| 82 |