use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
use crate::ui::palette as p;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphStyle {
Bars,
Dots,
}
impl GraphStyle {
pub fn label(self) -> &'static str {
match self {
GraphStyle::Bars => "bars",
GraphStyle::Dots => "dots",
}
}
pub fn next(self) -> GraphStyle {
match self {
GraphStyle::Bars => GraphStyle::Dots,
GraphStyle::Dots => GraphStyle::Bars,
}
}
}
const BLOCK_GLYPHS: [char; 8] = [
'\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}',
];
const BRAILLE_BIT: [[u8; 4]; 2] = [
[0, 1, 2, 6], [3, 4, 5, 7], ];
const BRAILLE_BASE: u32 = 0x2800;
pub fn render(f: &mut Frame, area: Rect, samples: &[f32], style: GraphStyle, color: Color) {
if area.width == 0 || area.height == 0 {
return;
}
match style {
GraphStyle::Bars => render_bars(f, area, samples, color),
GraphStyle::Dots => render_dots(f.buffer_mut(), area, samples, color),
}
}
fn render_bars(f: &mut Frame, area: Rect, samples: &[f32], color: Color) {
let take = area.width as usize;
let slice: &[f32] = if samples.len() > take {
&samples[samples.len() - take..]
} else {
samples
};
let s: String = slice
.iter()
.map(|v| {
let v = v.clamp(0.0, 1.0);
let idx = ((v * 7.0).round() as usize).min(7);
BLOCK_GLYPHS[idx]
})
.collect();
let lines: Vec<Line> = (0..area.height)
.map(|_| Line::from(Span::styled(s.clone(), Style::default().fg(color))))
.collect();
f.render_widget(
Paragraph::new(lines).style(Style::default().bg(p::bg())),
area,
);
}
fn render_dots(buf: &mut Buffer, area: Rect, samples: &[f32], color: Color) {
let cell_w = area.width as usize;
let cell_h = area.height as usize;
if cell_w == 0 || cell_h == 0 || samples.is_empty() {
return;
}
let pix_h = cell_h * 4;
let take = cell_w;
let slice: &[f32] = if samples.len() > take {
&samples[samples.len() - take..]
} else {
samples
};
for y in 0..cell_h {
for x in 0..cell_w {
if let Some(cell) = buf.cell_mut((area.x + x as u16, area.y + y as u16)) {
cell.set_char(' ');
cell.set_style(Style::default().bg(p::bg()));
}
}
}
let mut masks = vec![vec![0u8; cell_w]; cell_h];
for (i, &v) in slice.iter().enumerate() {
let v = v.clamp(0.0, 1.0);
if v <= 0.0 {
continue;
}
let top_pixel_from_bottom = ((v * (pix_h as f32 - 1.0)).round() as usize).min(pix_h - 1);
for fill in 0..=top_pixel_from_bottom {
let pix_y_from_top = (pix_h - 1) - fill;
let cell_y = pix_y_from_top / 4;
let row_in_cell = pix_y_from_top % 4;
masks[cell_y][i] |= 1 << BRAILLE_BIT[0][row_in_cell];
masks[cell_y][i] |= 1 << BRAILLE_BIT[1][row_in_cell];
}
}
for (y, row) in masks.iter().enumerate() {
for (x, &mask) in row.iter().enumerate() {
if mask == 0 {
continue;
}
let ch = char::from_u32(BRAILLE_BASE | mask as u32).unwrap_or(' ');
if let Some(cell) = buf.cell_mut((area.x + x as u16, area.y + y as u16)) {
cell.set_char(ch);
cell.set_style(Style::default().fg(color).bg(p::bg()));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn next_cycles_styles() {
assert_eq!(GraphStyle::Bars.next(), GraphStyle::Dots);
assert_eq!(GraphStyle::Dots.next(), GraphStyle::Bars);
}
#[test]
fn label_is_stable() {
assert_eq!(GraphStyle::Bars.label(), "bars");
assert_eq!(GraphStyle::Dots.label(), "dots");
}
#[test]
fn dots_writes_braille_chars_for_nonzero_samples() {
let area = Rect::new(0, 0, 4, 2);
let mut buf = Buffer::empty(area);
render_dots(&mut buf, area, &[1.0, 0.5, 0.25, 0.0], Color::White);
let top_left = buf
.cell((0u16, 0u16))
.unwrap()
.symbol()
.chars()
.next()
.unwrap();
assert!(
(top_left as u32) >= BRAILLE_BASE && (top_left as u32) < BRAILLE_BASE + 256,
"expected braille at top-left, got {:?}",
top_left
);
let zero_top = buf
.cell((3u16, 0u16))
.unwrap()
.symbol()
.chars()
.next()
.unwrap();
assert_eq!(zero_top, ' ');
}
#[test]
fn dots_handles_zero_area() {
let area = Rect::new(0, 0, 0, 0);
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
render_dots(&mut buf, area, &[1.0], Color::White);
}
}