use super::*;
const DEFAULT_GAUGE_WIDTH: u32 = 20;
impl Context {
pub fn gauge(&mut self, ratio: f64) -> Gauge<'_> {
Gauge::new(self, ratio)
}
pub fn line_gauge(&mut self, ratio: f64) -> LineGauge<'_> {
LineGauge::new(self, ratio)
}
}
pub struct Gauge<'a> {
ctx: Option<&'a mut Context>,
ratio: f64,
label: Option<String>,
width: Option<u32>,
color: Option<Color>,
}
impl<'a> Gauge<'a> {
fn new(ctx: &'a mut Context, ratio: f64) -> Self {
Self {
ctx: Some(ctx),
ratio,
label: None,
width: None,
color: None,
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
let label = label.into();
if label.is_empty() {
self.label = None;
} else {
self.label = Some(label);
}
self
}
pub fn width(mut self, w: u32) -> Self {
self.width = Some(w);
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
pub fn show(mut self) -> GaugeResponse {
let ctx = self.ctx.take().expect("Gauge::show called twice");
render_gauge(
ctx,
self.ratio,
self.width.unwrap_or(DEFAULT_GAUGE_WIDTH),
self.label.as_deref().unwrap_or(""),
self.color,
)
}
}
impl Drop for Gauge<'_> {
fn drop(&mut self) {
if let Some(ctx) = self.ctx.take() {
let _ = render_gauge(
ctx,
self.ratio,
self.width.unwrap_or(DEFAULT_GAUGE_WIDTH),
self.label.as_deref().unwrap_or(""),
self.color,
);
}
}
}
pub struct LineGauge<'a> {
ctx: Option<&'a mut Context>,
ratio: f64,
label: Option<String>,
width: Option<u32>,
filled: char,
empty: char,
}
impl<'a> LineGauge<'a> {
fn new(ctx: &'a mut Context, ratio: f64) -> Self {
Self {
ctx: Some(ctx),
ratio,
label: None,
width: None,
filled: '━',
empty: '─',
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
let label = label.into();
if label.is_empty() {
self.label = None;
} else {
self.label = Some(label);
}
self
}
pub fn width(mut self, w: u32) -> Self {
self.width = Some(w);
self
}
pub fn filled(mut self, ch: char) -> Self {
self.filled = ch;
self
}
pub fn empty(mut self, ch: char) -> Self {
self.empty = ch;
self
}
pub fn show(mut self) -> GaugeResponse {
let ctx = self.ctx.take().expect("LineGauge::show called twice");
render_line_gauge(
ctx,
self.ratio,
self.width.unwrap_or(DEFAULT_GAUGE_WIDTH),
self.filled,
self.empty,
self.label.as_deref(),
)
}
}
impl Drop for LineGauge<'_> {
fn drop(&mut self) {
if let Some(ctx) = self.ctx.take() {
let _ = render_line_gauge(
ctx,
self.ratio,
self.width.unwrap_or(DEFAULT_GAUGE_WIDTH),
self.filled,
self.empty,
self.label.as_deref(),
);
}
}
}
fn render_gauge(
ctx: &mut Context,
ratio: f64,
width: u32,
label: &str,
color_override: Option<Color>,
) -> GaugeResponse {
let response = ctx.interaction();
let clamped = ratio.clamp(0.0, 1.0);
let width = width.max(1);
let bar = compose_block_bar(clamped, width, label);
let color = color_override.unwrap_or_else(|| gauge_color_for(ctx, clamped));
ctx.styled(bar, Style::new().fg(color));
GaugeResponse {
response,
ratio: clamped,
}
}
fn render_line_gauge(
ctx: &mut Context,
ratio: f64,
width: u32,
filled: char,
empty: char,
label: Option<&str>,
) -> GaugeResponse {
let response = ctx.interaction();
let clamped = ratio.clamp(0.0, 1.0);
let width = width.max(1);
let bar = compose_line_bar(clamped, width, filled, empty, label);
let color = gauge_color_for(ctx, clamped);
ctx.styled(bar, Style::new().fg(color));
GaugeResponse {
response,
ratio: clamped,
}
}
fn gauge_color_for(ctx: &Context, ratio: f64) -> Color {
if ratio >= 0.80 {
ctx.theme.error
} else if ratio >= 0.50 {
ctx.theme.warning
} else {
ctx.theme.success
}
}
enum LabelMode<'a> {
Centered(&'a str),
Trailing(Option<&'a str>),
}
fn filled_cells(ratio: f64, width: u32) -> u32 {
let count = (ratio * f64::from(width)).round() as u32;
count.min(width)
}
fn compose_bar(
ratio: f64,
width: u32,
fill_ch: char,
empty_ch: char,
mode: LabelMode<'_>,
) -> String {
let width_usize = width as usize;
let filled = filled_cells(ratio, width);
if let LabelMode::Centered(label) = mode {
if !label.is_empty() {
let label_w = UnicodeWidthStr::width(label);
if label_w + 2 <= width_usize {
let mut cells: Vec<char> = Vec::with_capacity(width_usize);
for i in 0..width {
cells.push(if i < filled { fill_ch } else { empty_ch });
}
let label_start = (width_usize.saturating_sub(label_w)) / 2;
let label_end = label_start + label_w;
let mut out = String::with_capacity(width_usize * 4 + label.len());
for ch in cells.iter().take(label_start) {
out.push(*ch);
}
out.push_str(label);
for ch in cells.iter().take(width_usize).skip(label_end) {
out.push(*ch);
}
return out;
}
}
}
let trailing = match mode {
LabelMode::Trailing(Some(lbl)) if !lbl.is_empty() => Some(lbl),
_ => None,
};
let mut out = String::with_capacity(
width_usize * fill_ch.len_utf8().max(empty_ch.len_utf8())
+ trailing.map_or(0, |s| s.len() + 1),
);
for _ in 0..filled {
out.push(fill_ch);
}
for _ in 0..width.saturating_sub(filled) {
out.push(empty_ch);
}
if let Some(lbl) = trailing {
out.push(' ');
out.push_str(lbl);
}
out
}
fn compose_block_bar(ratio: f64, width: u32, label: &str) -> String {
compose_bar(ratio, width, '█', '░', LabelMode::Centered(label))
}
fn compose_line_bar(
ratio: f64,
width: u32,
filled: char,
empty: char,
label: Option<&str>,
) -> String {
compose_bar(ratio, width, filled, empty, LabelMode::Trailing(label))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_bar_no_label() {
let bar = compose_block_bar(0.5, 10, "");
assert_eq!(bar, "█████░░░░░");
}
#[test]
fn block_bar_with_label() {
let bar = compose_block_bar(0.5, 12, "50%");
assert!(bar.contains("50%"), "label visible: {bar}");
assert_eq!(UnicodeWidthStr::width(bar.as_str()), 12);
}
#[test]
fn block_bar_omits_label_when_too_narrow() {
let bar = compose_block_bar(0.5, 6, "12345");
assert!(!bar.contains("12345"));
assert_eq!(UnicodeWidthStr::width(bar.as_str()), 6);
}
#[test]
fn line_bar_default_chars() {
let bar = compose_line_bar(0.5, 10, '━', '─', None);
assert_eq!(bar, "━━━━━─────");
}
#[test]
fn line_bar_appends_label() {
let bar = compose_line_bar(1.0, 4, '#', '.', Some("done"));
assert_eq!(bar, "#### done");
}
#[test]
fn block_bar_f64_precision() {
let bar = compose_block_bar(1.0 / 3.0, 30, "");
let filled = bar.chars().filter(|&c| c == '█').count();
assert_eq!(filled, 10);
}
}