use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
use crate::base::{Point, Rgba};
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{Scope, Signal};
use crate::ui::{dyn_view, Element, View};
const H_EIGHTHS: [char; 8] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
const V_EIGHTHS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
#[derive(Copy, Clone, Debug)]
struct Chan {
target: f32,
display: f32,
peak: f32,
peak_since: Option<Instant>,
}
impl Chan {
const ZERO: Chan = Chan {
target: 0.0,
display: 0.0,
peak: 0.0,
peak_since: None,
};
}
struct Ballistics {
channels: Vec<Chan>,
fall_per_s: f32,
peak_hold: Duration,
last: Option<Instant>,
}
impl Ballistics {
fn new(channels: usize, fall_per_s: f32, peak_hold: Duration) -> Ballistics {
Ballistics {
channels: vec![Chan::ZERO; channels.max(1)],
fall_per_s: fall_per_s.max(f32::EPSILON),
peak_hold,
last: None,
}
}
fn set_targets(&mut self, values: &[f32]) -> bool {
if values.len() > self.channels.len() {
self.channels.resize(values.len(), Chan::ZERO);
}
let mut changed = false;
for (chan, v) in self.channels.iter_mut().zip(values.iter()) {
if !v.is_finite() {
continue;
}
let v = v.clamp(0.0, 1.0);
if v != chan.target {
chan.target = v;
changed = true;
}
if v > chan.display {
chan.display = v; changed = true;
}
if chan.display > chan.peak {
chan.peak = chan.display;
chan.peak_since = None; changed = true;
}
}
changed
}
fn advance(&mut self, now: Instant) {
let dt = match self.last.replace(now) {
Some(last) => now.saturating_duration_since(last).as_secs_f32(),
None => 0.0, };
let fall = self.fall_per_s * dt;
for chan in self.channels.iter_mut() {
if chan.display > chan.target {
chan.display = (chan.display - fall).max(chan.target);
}
if chan.peak > chan.display {
let since = *chan.peak_since.get_or_insert(now);
if now.saturating_duration_since(since) >= self.peak_hold {
chan.peak = (chan.peak - fall).max(chan.display);
}
} else {
chan.peak = chan.display;
}
}
}
fn settled(&self) -> bool {
self.channels
.iter()
.all(|c| c.display == c.target && c.peak == c.display)
}
fn reanchor(&mut self) {
self.last = None;
}
}
fn map_level(v: f32, db_floor: Option<f32>) -> f32 {
let Some(floor) = db_floor else {
return v;
};
if !v.is_finite() {
return v;
}
if v <= 0.0 {
return 0.0;
}
let db = 20.0 * v.log10();
((db - floor) / -floor).clamp(0.0, 1.0)
}
enum MeterInput {
Mono(Signal<f32>),
Bands(Signal<Vec<f32>>),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Orientation {
Horizontal,
Vertical,
}
pub struct Meter {
input: MeterInput,
orientation: Orientation,
db_floor: Option<f32>,
decay_db_per_s: f32,
peak_hold: Duration,
warn_at: f32,
danger_at: f32,
bar_w: i32,
gap: i32,
layout: Option<LayoutStyle>,
}
impl Meter {
pub fn new(level: Signal<f32>) -> Meter {
Meter::build(MeterInput::Mono(level))
}
pub fn bands(frames: Signal<Vec<f32>>) -> Meter {
Meter::build(MeterInput::Bands(frames))
}
fn build(input: MeterInput) -> Meter {
Meter {
input,
orientation: Orientation::Horizontal,
db_floor: None,
decay_db_per_s: 20.0,
peak_hold: Duration::from_millis(1500),
warn_at: 0.70,
danger_at: 0.90,
bar_w: 2,
gap: 1,
layout: None,
}
}
pub fn db_floor(mut self, floor: f32) -> Meter {
if floor < 0.0 && floor.is_finite() {
self.db_floor = Some(floor);
}
self
}
pub fn decay(mut self, db_per_s: f32) -> Meter {
if db_per_s > 0.0 && db_per_s.is_finite() {
self.decay_db_per_s = db_per_s;
}
self
}
pub fn peak_hold(mut self, hold: Duration) -> Meter {
self.peak_hold = hold;
self
}
pub fn zones(mut self, warn_at: f32, danger_at: f32) -> Meter {
self.warn_at = warn_at.clamp(0.0, 1.0);
self.danger_at = danger_at.clamp(self.warn_at, 1.0);
self
}
pub fn vertical(mut self) -> Meter {
self.orientation = Orientation::Vertical;
self
}
pub fn bar(mut self, width: i32, gap: i32) -> Meter {
self.bar_w = width.max(1);
self.gap = gap.max(0);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> Meter {
self.layout = Some(layout);
self
}
pub fn view(self, cx: Scope) -> View {
let span_db = self.db_floor.map(|f| -f).unwrap_or(60.0);
let fall_per_s = self.decay_db_per_s / span_db;
let channels = match self.input {
MeterInput::Mono(_) => 1,
MeterInput::Bands(_) => 1, };
let core = Rc::new(RefCell::new(Ballistics::new(
channels,
fall_per_s,
self.peak_hold,
)));
let repaint = cx.signal(0u64);
let task_live = Rc::new(std::cell::Cell::new(false));
let db_floor = self.db_floor;
let input_core = core.clone();
let input_task = task_live.clone();
match self.input {
MeterInput::Mono(level) => {
cx.effect_labeled("meter-input", move || {
let v = map_level(level.get(), db_floor);
fold_input(&input_core, &input_task, repaint, &[v]);
});
}
MeterInput::Bands(frames) => {
cx.effect_labeled("meter-input", move || {
let mapped: Vec<f32> = frames
.with(|frame| frame.iter().map(|v| map_level(*v, db_floor)).collect());
fold_input(&input_core, &input_task, repaint, &mapped);
});
}
}
let orientation = self.orientation;
let (warn_at, danger_at) = (self.warn_at, self.danger_at);
let (bar_w, gap) = (self.bar_w, self.gap);
let layout = self.layout.unwrap_or_else(|| match orientation {
Orientation::Horizontal => LayoutStyle::default().height(Dimension::Cells(1)).grow(1.0),
Orientation::Vertical => LayoutStyle::default().grow(1.0),
});
dyn_view(layout, move || {
let _ = repaint.get(); let t = super::theme_tokens(cx);
let zone_inks = [t.ok, t.warn, t.error];
let track_ink = t.text_faint;
let snapshot: Vec<Chan> = core.borrow().channels.clone();
let el = Element::new().style(LayoutStyle::default().grow(1.0));
let el = match orientation {
Orientation::Horizontal if snapshot.len() == 1 => {
let chan = snapshot[0];
el.draw(move |canvas, rect| {
draw_horizontal(
canvas, rect, chan, warn_at, danger_at, zone_inks, track_ink,
);
})
}
_ => el.draw(move |canvas, rect| {
draw_bars(
canvas, rect, &snapshot, bar_w, gap, warn_at, danger_at, zone_inks,
);
}),
};
el.build()
})
}
}
fn fold_input(
core: &Rc<RefCell<Ballistics>>,
task_live: &Rc<std::cell::Cell<bool>>,
repaint: Signal<u64>,
values: &[f32],
) {
let (changed, settled) = {
let mut b = core.borrow_mut();
let changed = b.set_targets(values);
(changed, b.settled())
};
if changed {
repaint.update(|g| *g = g.wrapping_add(1));
}
if !settled && !task_live.get() {
task_live.set(true);
core.borrow_mut().reanchor();
let core = core.clone();
let task_live = task_live.clone();
crate::reactive::register_frame_task(move |now| {
if !repaint.is_alive() {
task_live.set(false);
return false;
}
let settled = {
let mut b = core.borrow_mut();
b.advance(now);
b.settled()
};
repaint.update(|g| *g = g.wrapping_add(1));
if settled {
task_live.set(false);
false } else {
crate::reactive::request_frame();
true
}
});
crate::reactive::request_frame();
}
}
fn zone_ink(pos: f32, warn_at: f32, danger_at: f32, inks: [Rgba; 3]) -> Rgba {
if pos >= danger_at {
inks[2]
} else if pos >= warn_at {
inks[1]
} else {
inks[0]
}
}
fn draw_horizontal(
canvas: &mut dyn crate::ui::StyledCanvas,
rect: crate::base::Rect,
chan: Chan,
warn_at: f32,
danger_at: f32,
inks: [Rgba; 3],
track_ink: Rgba,
) {
if rect.w <= 0 || rect.h <= 0 {
return;
}
let y = rect.y + rect.h / 2;
let eighths_total = rect.w * 8;
let fill = (chan.display * eighths_total as f32).round() as i32;
let (full, part) = (fill / 8, fill % 8);
for cx in 0..rect.w {
let pos = (cx as f32 + 0.5) / rect.w as f32;
let ink = zone_ink(pos, warn_at, danger_at, inks);
let p = Point::new(rect.x + cx, y);
if cx < full {
canvas.put(p, '█', ink, Rgba::TRANSPARENT);
} else if cx == full && part > 0 {
canvas.put(p, H_EIGHTHS[(part - 1) as usize], ink, Rgba::TRANSPARENT);
} else {
canvas.put(p, '·', track_ink, Rgba::TRANSPARENT);
}
}
let peak_cell = ((chan.peak * eighths_total as f32).round() as i32 / 8).min(rect.w - 1);
if chan.peak > chan.display && peak_cell > full {
let pos = (peak_cell as f32 + 0.5) / rect.w as f32;
let ink = zone_ink(pos, warn_at, danger_at, inks);
canvas.put(
Point::new(rect.x + peak_cell, y),
'│',
ink,
Rgba::TRANSPARENT,
);
}
}
#[allow(clippy::too_many_arguments)]
fn draw_bars(
canvas: &mut dyn crate::ui::StyledCanvas,
rect: crate::base::Rect,
channels: &[Chan],
bar_w: i32,
gap: i32,
warn_at: f32,
danger_at: f32,
inks: [Rgba; 3],
) {
if rect.w <= 0 || rect.h <= 0 || channels.is_empty() {
return;
}
let mut x = rect.x;
for chan in channels {
if x >= rect.right() {
break;
}
let eighths_total = rect.h * 8;
let fill = (chan.display * eighths_total as f32).round() as i32;
let (full, part) = (fill / 8, fill % 8);
let w = bar_w.min(rect.right() - x);
for row in 0..rect.h {
let pos = (row as f32 + 0.5) / rect.h as f32;
let ink = zone_ink(pos, warn_at, danger_at, inks);
let py = rect.bottom() - 1 - row;
for col in 0..w {
let p = Point::new(x + col, py);
if row < full {
canvas.put(p, '█', ink, Rgba::TRANSPARENT);
} else if row == full && part > 0 {
canvas.put(p, V_EIGHTHS[(part - 1) as usize], ink, Rgba::TRANSPARENT);
}
}
}
let peak_row = ((chan.peak * eighths_total as f32).round() as i32 / 8).min(rect.h - 1);
if chan.peak > chan.display && peak_row > full {
let pos = (peak_row as f32 + 0.5) / rect.h as f32;
let ink = zone_ink(pos, warn_at, danger_at, inks);
let py = rect.bottom() - 1 - peak_row;
for col in 0..w {
canvas.put(Point::new(x + col, py), '─', ink, Rgba::TRANSPARENT);
}
}
x += bar_w + gap;
}
}
#[cfg(test)]
#[path = "meter_tests.rs"]
mod tests;