use std::panic::Location;
use crate::layout::LayoutCtx;
use crate::metrics::MetricsRole;
use crate::shader::{ShaderBinding, StockShader, UniformValue};
use crate::tokens;
use crate::tree::*;
pub const DEFAULT_HEIGHT: f32 = 8.0;
#[track_caller]
pub fn progress(value: f32, fill_color: Color) -> El {
let value = value.clamp(0.0, 1.0);
let layout = move |ctx: LayoutCtx| {
let r = ctx.container;
vec![
Rect::new(r.x, r.y, r.w, r.h),
Rect::new(r.x, r.y, r.w * value, r.h),
]
};
stack([
El::new(Kind::Custom("progress-track"))
.fill(tokens::MUTED)
.radius(tokens::RADIUS_PILL),
El::new(Kind::Custom("progress-fill"))
.fill(fill_color)
.radius(tokens::RADIUS_PILL),
])
.at_loc(Location::caller())
.metrics_role(MetricsRole::Progress)
.layout(layout)
.width(Size::Fill(1.0))
.default_height(Size::Fixed(DEFAULT_HEIGHT))
}
#[track_caller]
pub fn progress_indeterminate(bar_color: Color) -> El {
let binding = ShaderBinding::stock(StockShader::ProgressIndeterminate)
.with("vec_a", UniformValue::Color(bar_color))
.with("vec_b", UniformValue::Color(tokens::MUTED))
.with(
"vec_c",
UniformValue::Vec4([tokens::RADIUS_PILL, 0.0, 0.0, 0.0]),
);
El::new(Kind::Custom("progress-indeterminate"))
.at_loc(Location::caller())
.shader(binding)
.metrics_role(MetricsRole::Progress)
.width(Size::Fill(1.0))
.default_height(Size::Fixed(DEFAULT_HEIGHT))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn track_and_fill_use_expected_tokens() {
let p = progress(0.5, tokens::PRIMARY);
assert_eq!(p.children.len(), 2);
assert_eq!(p.children[0].fill, Some(tokens::MUTED), "track is muted");
assert_eq!(
p.children[1].fill,
Some(tokens::PRIMARY),
"fill uses caller's color"
);
assert_eq!(p.children[0].radius, tokens::RADIUS_PILL);
assert_eq!(p.children[1].radius, tokens::RADIUS_PILL);
}
#[test]
fn layout_clamps_value_below_zero() {
use crate::layout::layout;
use crate::state::UiState;
let mut tree = progress(-0.5, tokens::PRIMARY);
let mut state = UiState::new();
let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
layout(&mut tree, &mut state, viewport);
let fill_rect = state.rect(&tree.children[1].computed_id);
assert_eq!(fill_rect.w, 0.0, "negative values clamp to empty fill");
}
#[test]
fn layout_clamps_value_above_one() {
use crate::layout::layout;
use crate::state::UiState;
let mut tree = progress(1.5, tokens::PRIMARY);
let mut state = UiState::new();
let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
layout(&mut tree, &mut state, viewport);
let fill_rect = state.rect(&tree.children[1].computed_id);
assert_eq!(fill_rect.w, 200.0, "values above 1.0 clamp to full track");
}
#[test]
fn indeterminate_binds_stock_shader() {
use crate::shader::ShaderHandle;
let p = progress_indeterminate(tokens::PRIMARY);
let binding = p.shader_override.as_ref().expect("shader binding");
assert_eq!(
binding.handle,
ShaderHandle::Stock(StockShader::ProgressIndeterminate),
"progress_indeterminate must paint through stock::progress_indeterminate",
);
match binding.uniforms.get("vec_a") {
Some(UniformValue::Color(c)) => assert_eq!(*c, tokens::PRIMARY),
other => panic!("expected vec_a=PRIMARY, got {other:?}"),
}
}
#[test]
fn indeterminate_inherits_progress_dimensions() {
let p = progress_indeterminate(tokens::PRIMARY);
assert_eq!(p.width, Size::Fill(1.0));
assert_eq!(p.height, Size::Fixed(DEFAULT_HEIGHT));
}
#[test]
fn layout_fills_proportionally_to_value() {
use crate::layout::layout;
use crate::state::UiState;
let mut tree = progress(0.25, tokens::PRIMARY);
let mut state = UiState::new();
let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
layout(&mut tree, &mut state, viewport);
let fill_rect = state.rect(&tree.children[1].computed_id);
assert!(
(fill_rect.w - 50.0).abs() < 1e-3,
"0.25 * 200 = 50; got {}",
fill_rect.w
);
}
}