use rdom_core::NodeId;
use crate::TuiDom;
use crate::render::paint_pass::ChromeText;
use crate::style::{Color, ComputedStyle};
const FILL: &str = "\u{2588}"; const EMPTY: &str = "\u{2591}";
pub fn is_gauge(dom: &TuiDom, id: NodeId) -> bool {
matches!(dom.node(id).tag_name(), Some("progress") | Some("meter"))
}
pub fn gauge_text(dom: &TuiDom, id: NodeId, width: u16) -> Option<(String, Option<Color>)> {
match dom.node(id).tag_name() {
Some("progress") => Some(progress_text(dom, id, width)),
Some("meter") => Some(meter_text(dom, id, width)),
_ => None,
}
}
fn progress_text(dom: &TuiDom, id: NodeId, width: u16) -> (String, Option<Color>) {
let value = parse_attr(dom, id, "value");
let max = parse_attr(dom, id, "max")
.filter(|v| *v > 0.0)
.unwrap_or(1.0);
let bar = match value {
Some(v) => {
let ratio = (v / max).clamp(0.0, 1.0);
fill_bar(width, ratio)
}
None => indeterminate_bar(width),
};
(bar, None)
}
fn meter_text(dom: &TuiDom, id: NodeId, width: u16) -> (String, Option<Color>) {
let min = parse_attr(dom, id, "min").unwrap_or(0.0);
let max = parse_attr(dom, id, "max").unwrap_or(1.0);
let value = parse_attr(dom, id, "value").unwrap_or(0.0).clamp(min, max);
let span = (max - min).max(f64::EPSILON);
let ratio = ((value - min) / span).clamp(0.0, 1.0);
let bar = fill_bar(width, ratio);
let color = meter_zone_color(dom, id, value, min, max);
(bar, color)
}
fn meter_zone_color(dom: &TuiDom, id: NodeId, value: f64, min: f64, max: f64) -> Option<Color> {
let low = parse_attr(dom, id, "low").unwrap_or(min);
let high = parse_attr(dom, id, "high").unwrap_or(max);
let optimum = parse_attr(dom, id, "optimum").unwrap_or((min + max) / 2.0);
let value_zone = zone_of(value, low, high);
let optimum_zone = zone_of(optimum, low, high);
if value_zone == optimum_zone {
None } else if (value_zone as i8 - optimum_zone as i8).abs() == 1 {
Some(Color::Rgb(255, 255, 0)) } else {
Some(Color::Rgb(255, 0, 0)) }
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Zone {
Low = 0,
Mid = 1,
High = 2,
}
fn zone_of(v: f64, low: f64, high: f64) -> Zone {
if v <= low {
Zone::Low
} else if v >= high {
Zone::High
} else {
Zone::Mid
}
}
fn fill_bar(width: u16, ratio: f64) -> String {
let total = width as usize;
let filled = (ratio * total as f64).round() as usize;
let filled = filled.min(total);
let empty = total - filled;
let mut s = String::with_capacity(total * 3);
for _ in 0..filled {
s.push_str(FILL);
}
for _ in 0..empty {
s.push_str(EMPTY);
}
s
}
fn indeterminate_bar(width: u16) -> String {
EMPTY.repeat(width as usize)
}
fn parse_attr(dom: &TuiDom, id: NodeId, name: &str) -> Option<f64> {
dom.node(id)
.get_attribute(name)
.and_then(|s| s.parse().ok())
}
pub fn override_fg(base: ComputedStyle, color: Option<Color>) -> ComputedStyle {
let mut out = base;
if let Some(c) = color {
out.fg = c;
}
out
}
pub(crate) fn inline_chrome(dom: &TuiDom, id: NodeId, width: u16) -> Option<ChromeText> {
let (text, fg) = gauge_text(dom, id, width)?;
Some(ChromeText { text, fg })
}
#[cfg(test)]
mod tests;