use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::{Block, Widget},
};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum WaveformMode {
#[default]
HighResBraille,
UltraThinBlock,
}
pub struct WaveformWidget<'a> {
block: Option<Block<'a>>,
mode: WaveformMode,
top_data: &'a [f64],
top_style: Style,
bottom_data: &'a [f64],
bottom_style: Style,
fade_effect: bool,
gradient_effect: bool,
top_max: f64,
bottom_max: f64,
}
impl<'a> WaveformWidget<'a> {
pub fn new(top_data: &'a [f64], bottom_data: &'a [f64]) -> Self {
Self {
top_data,
bottom_data,
block: None,
mode: WaveformMode::HighResBraille,
fade_effect: false,
gradient_effect: false,
top_style: Style::default(),
bottom_style: Style::default(),
top_max: 1.0,
bottom_max: 1.0,
}
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn mode(mut self, mode: WaveformMode) -> Self {
self.mode = mode;
self
}
pub fn top_style(mut self, style: Style) -> Self {
self.top_style = style;
self
}
pub fn bottom_style(mut self, style: Style) -> Self {
self.bottom_style = style;
self
}
pub fn fade_effect(mut self, enable: bool) -> Self {
self.fade_effect = enable;
self
}
pub fn gradient_effect(mut self, enable: bool) -> Self {
self.gradient_effect = enable;
self
}
pub fn top_max(mut self, max: f64) -> Self {
self.top_max = max;
self
}
pub fn bottom_max(mut self, max: f64) -> Self {
self.bottom_max = max;
self
}
}
impl<'a> Widget for WaveformWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let inner_area = match &self.block {
Some(b) => {
let inner = b.inner(area);
b.render(area, buf);
inner
}
None => area,
};
if inner_area.height < 1 || inner_area.width < 1 {
return;
}
let center_y = inner_area.top() + (inner_area.height / 2);
let max_char_height = inner_area.height / 2;
let data_len = self.top_data.len().min(self.bottom_data.len());
let width = inner_area.width as usize;
let start_x_offset = width.saturating_sub(data_len) as u16;
for x in inner_area.left()..inner_area.right() {
let relative_x = x - inner_area.left();
if relative_x < start_x_offset {
continue;
}
let data_index = (relative_x - start_x_offset) as usize;
if data_index >= self.top_data.len() || data_index >= self.bottom_data.len() {
continue;
}
let top_val = (self.top_data[data_index] / self.top_max).clamp(0.0, 1.0);
let bottom_val = (self.bottom_data[data_index] / self.bottom_max).clamp(0.0, 1.0);
let fade_factor = if self.fade_effect {
let relative_x_f = (x - inner_area.left()) as f64;
let width_f = inner_area.width as f64;
let linear = relative_x_f / width_f;
if linear > 0.5 {
1.0
} else {
linear * 2.0
}
} else {
1.0
};
let top_base_style = self.top_style;
let bottom_base_style = self.bottom_style;
match self.mode {
WaveformMode::HighResBraille => {
self.render_braille_column(buf, x, center_y, max_char_height, top_val, true, top_base_style, self.gradient_effect, fade_factor);
self.render_braille_column(buf, x, center_y, max_char_height, bottom_val, false, bottom_base_style, self.gradient_effect, fade_factor);
}
WaveformMode::UltraThinBlock => {
self.render_block_column(buf, x, center_y, max_char_height, top_val, true, inner_area, top_base_style, self.gradient_effect, fade_factor);
self.render_block_column(buf, x, center_y, max_char_height, bottom_val, false, inner_area, bottom_base_style, self.gradient_effect, fade_factor);
}
}
}
}
}
fn apply_fade(style: Style, factor: f64) -> Style {
let (r, g, b) = match style.fg {
Some(c) => color_to_rgb(c),
None => return style,
};
let new_r = (r as f64 * factor) as u8;
let new_g = (g as f64 * factor) as u8;
let new_b = (b as f64 * factor) as u8;
style.fg(Color::Rgb(new_r, new_g, new_b))
}
fn color_to_rgb(color: Color) -> (u8, u8, u8) {
match color {
Color::Rgb(r, g, b) => (r, g, b),
Color::Indexed(i) => {
match i {
0 => (0, 0, 0), 1 => (170, 0, 0), 2 => (0, 170, 0), 3 => (170, 85, 0), 4 => (0, 0, 170), 5 => (170, 0, 170), 6 => (0, 170, 170), 7 => (170, 170, 170), 8 => (85, 85, 85), 9 => (255, 85, 85), 10 => (85, 255, 85), 11 => (255, 255, 85), 12 => (85, 85, 255), 13 => (255, 85, 255), 14 => (85, 255, 255), 15 => (255, 255, 255), _ => (255, 255, 255), }
},
Color::Black => (0, 0, 0),
Color::Red => (170, 0, 0),
Color::Green => (0, 170, 0),
Color::Yellow => (170, 85, 0),
Color::Blue => (0, 0, 170),
Color::Magenta => (170, 0, 170),
Color::Cyan => (0, 170, 170),
Color::Gray => (170, 170, 170),
Color::DarkGray => (85, 85, 85),
Color::LightRed => (255, 85, 85),
Color::LightGreen => (85, 255, 85),
Color::LightYellow => (255, 255, 85),
Color::LightBlue => (85, 85, 255),
Color::LightMagenta => (255, 85, 255),
Color::LightCyan => (85, 255, 255),
Color::White => (255, 255, 255),
_ => (255, 255, 255),
}
}
impl<'a> WaveformWidget<'a> {
fn render_braille_column(
&self,
buf: &mut Buffer,
x: u16,
center_y: u16,
max_char_height: u16,
val: f64,
is_top: bool,
base_style: Style,
use_gradient: bool,
fade_factor: f64,
) {
let total_dots = max_char_height as f64 * 4.0;
let needed_dots = (val * total_dots).round() as u16;
let mut dots_remaining = needed_dots;
let mut y = if is_top { center_y.saturating_sub(1) } else { center_y };
for i in 0..max_char_height {
if dots_remaining == 0 {
break;
}
let char_to_draw = if dots_remaining >= 4 {
dots_remaining -= 4;
'\u{2847}' } else {
let c = if is_top {
get_thin_braille_fill(dots_remaining as u8)
} else {
get_thin_braille_fill_bottom(dots_remaining as u8)
};
dots_remaining = 0;
c
};
let style = if use_gradient {
let height_ratio = i as f64 / max_char_height as f64;
apply_gradient(base_style, height_ratio)
} else {
base_style
};
let final_style = apply_fade(style, fade_factor);
buf[(x, y)].set_char(char_to_draw).set_style(final_style);
if is_top {
if y == 0 { break; } y -= 1;
} else {
y += 1;
}
}
}
fn render_block_column(
&self,
buf: &mut Buffer,
x: u16,
center_y: u16,
max_char_height: u16,
val: f64,
is_top: bool,
inner_area: Rect,
base_style: Style,
use_gradient: bool,
fade_factor: f64,
) {
let needed_rows = (val * max_char_height as f64).round() as u16;
for i in 0..needed_rows {
let y = if is_top {
(center_y.saturating_sub(1)).saturating_sub(i)
} else {
center_y + i
};
if is_top {
if y < inner_area.top() { continue; }
} else {
if y >= inner_area.bottom() { continue; }
}
let style = if use_gradient {
let height_ratio = i as f64 / max_char_height as f64;
apply_gradient(base_style, height_ratio)
} else {
base_style
};
let final_style = apply_fade(style, fade_factor);
buf[(x, y)].set_char('▌').set_style(final_style);
}
}
}
fn apply_gradient(style: Style, ratio: f64) -> Style {
if let Some(color) = style.fg {
let (r, g, b) = color_to_rgb(color);
let brightness = 1.0 - (ratio * 0.7);
let new_r = (r as f64 * brightness) as u8;
let new_g = (g as f64 * brightness) as u8;
let new_b = (b as f64 * brightness) as u8;
style.fg(Color::Rgb(new_r, new_g, new_b))
} else {
style
}
}
fn get_thin_braille_fill(height_in_dots: u8) -> char {
match height_in_dots {
1 => '\u{2840}', 2 => '\u{2844}', 3 => '\u{2846}', 4 => '\u{2847}', _ => ' ',
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_thin_braille_fill() {
assert_eq!(get_thin_braille_fill(1), '\u{2840}');
assert_eq!(get_thin_braille_fill(2), '\u{2844}');
assert_eq!(get_thin_braille_fill(3), '\u{2846}');
assert_eq!(get_thin_braille_fill(4), '\u{2847}');
assert_eq!(get_thin_braille_fill(0), ' ');
assert_eq!(get_thin_braille_fill(5), ' ');
}
#[test]
fn test_get_thin_braille_fill_bottom() {
assert_eq!(get_thin_braille_fill_bottom(1), '\u{2801}');
assert_eq!(get_thin_braille_fill_bottom(2), '\u{2803}');
assert_eq!(get_thin_braille_fill_bottom(3), '\u{2807}');
assert_eq!(get_thin_braille_fill_bottom(4), '\u{2847}');
assert_eq!(get_thin_braille_fill_bottom(0), ' ');
assert_eq!(get_thin_braille_fill_bottom(5), ' ');
}
#[test]
fn test_apply_fade() {
let style = Style::default().fg(Color::Rgb(100, 200, 50));
let faded_100 = apply_fade(style, 1.0);
assert_eq!(faded_100.fg, Some(Color::Rgb(100, 200, 50)));
let faded_50 = apply_fade(style, 0.5);
assert_eq!(faded_50.fg, Some(Color::Rgb(50, 100, 25)));
let faded_0 = apply_fade(style, 0.0);
assert_eq!(faded_0.fg, Some(Color::Rgb(0, 0, 0)));
}
#[test]
fn test_apply_gradient() {
let style = Style::default().fg(Color::Rgb(0, 0, 255));
let grad_0 = apply_gradient(style, 0.0);
assert_eq!(grad_0.fg, Some(Color::Rgb(0, 0, 255)));
let grad_100 = apply_gradient(style, 1.0);
assert_eq!(grad_100.fg, Some(Color::Rgb(0, 0, 76)));
}
}
fn get_thin_braille_fill_bottom(height_in_dots: u8) -> char {
match height_in_dots {
1 => '\u{2801}', 2 => '\u{2803}', 3 => '\u{2807}', 4 => '\u{2847}', _ => ' ',
}
}