use denise::Pen;
use denise::{Rect, Role};
use crate::widget::{PaintCtx, Widget};
use crate::widgets::describe::{
Describe, DynDescribe, Group, Mismatch, Property, PropertyKind, ROLES, Value,
};
use crate::widgets::style::interactive_pair;
#[derive(Clone, Copy, Debug)]
pub struct Progress {
value: f32,
role: Role,
}
impl Progress {
pub fn new(value: f32) -> Self {
Self {
value: clamp(value),
role: Role::Primary,
}
}
pub fn with_role(mut self, role: Role) -> Self {
self.role = role;
self
}
#[inline]
pub const fn value(&self) -> f32 {
self.value
}
pub fn set_value(&mut self, value: f32) {
self.value = clamp(value);
}
pub fn update(&mut self, value: f32) -> bool {
let value = clamp(value);
let changed = value != self.value;
self.value = value;
changed
}
pub fn set_role(&mut self, role: Role) {
self.role = role;
}
}
impl Default for Progress {
fn default() -> Self {
Self::new(0.0)
}
}
#[inline]
fn clamp(value: f32) -> f32 {
if value.is_nan() {
0.0
} else {
value.clamp(0.0, 1.0)
}
}
fn fill_width(width: i32, value: f32) -> i32 {
if width <= 0 || value.is_nan() || value <= 0.0 {
return 0;
}
if value >= 1.0 {
return width;
}
let filled = (width as f32 * value) as i32;
filled.clamp(1, width)
}
impl<M: 'static> Widget<M> for Progress {
fn describe(&self) -> Option<&dyn DynDescribe> {
Some(self)
}
fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
Some(self)
}
fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
let bounds = ctx.bounds;
if bounds.is_empty() {
return;
}
let radius = (bounds.height / 2).min(bounds.width / 2);
let (track, _) = interactive_pair(ctx.theme, Role::Base300, ctx.state);
canvas.fill_rounded_rect(bounds, radius, track);
let filled = fill_width(bounds.width, self.value);
if filled == 0 {
return;
}
let (fill, _) = interactive_pair(ctx.theme, self.role, ctx.state);
let bar = Rect::new(bounds.x, bounds.y, filled, bounds.height);
canvas.fill_rounded_rect(bar, radius.min(filled / 2), fill);
}
}
impl Describe for Progress {
const KIND: &'static str = "progress";
const DOC: &'static str = "A bar that fills to show how far along something is.";
const GROUP: Group = Group::Indicator;
const ICON: &'static denise::icon::Icon = &super::icons::PROGRESS;
const PROPERTIES: &'static [Property] = &[
Property::new(
"value",
PropertyKind::Float { min: 0.0, max: 1.0 },
"How much of the bar is filled, from empty at `0.0` to full at `1.0`.",
),
Property::new(
"role",
PropertyKind::Enum(ROLES),
"Colour of the filled portion; `warning` or `error` for a bar that means something is running out rather than something is being achieved.",
),
];
fn get(&self, name: &str) -> Option<Value> {
Some(match name {
"value" => Value::Float(self.value),
"role" => Value::role(self.role),
_ => return None,
})
}
fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
match name {
"value" => self.set_value(value.as_float()?),
"role" => self.role = value.as_role()?,
_ => return Err(Mismatch::Unknown),
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_value_that_is_not_a_number_draws_an_empty_bar() {
let done = core::hint::black_box(0.0f32);
let total = core::hint::black_box(0.0f32);
let zero_over_zero = done / total;
assert!(zero_over_zero.is_nan(), "the premise");
assert_eq!(clamp(zero_over_zero), 0.0);
assert_eq!(fill_width(200, zero_over_zero), 0);
let mut bar = Progress::new(0.5);
bar.set_value(zero_over_zero);
assert_eq!(
bar.value(),
0.0,
"and it does not keep the old value either"
);
}
#[test]
fn infinities_clamp_to_the_end_they_point_at() {
assert_eq!(clamp(f32::INFINITY), 1.0);
assert_eq!(clamp(f32::NEG_INFINITY), 0.0);
assert_eq!(fill_width(200, f32::INFINITY), 200);
assert_eq!(fill_width(200, f32::NEG_INFINITY), 0);
}
#[test]
fn values_outside_the_range_are_clamped_rather_than_refused() {
assert_eq!(clamp(-0.5), 0.0);
assert_eq!(clamp(1.5), 1.0);
assert_eq!(clamp(1e30), 1.0);
assert_eq!(Progress::new(42.0).value(), 1.0);
assert_eq!(Progress::new(-42.0).value(), 0.0);
}
#[test]
fn the_fill_is_exact_at_both_ends_and_in_the_middle() {
assert_eq!(fill_width(200, 0.0), 0);
assert_eq!(fill_width(200, 1.0), 200);
assert_eq!(fill_width(200, 0.5), 100);
assert_eq!(fill_width(200, 0.25), 50);
assert_eq!(
fill_width(101, 1.0),
101,
"an odd width still fills exactly"
);
}
#[test]
fn a_value_just_above_zero_shows_something() {
assert_eq!(fill_width(200, 0.0), 0);
assert!(fill_width(200, 0.0001) >= 1);
assert!(fill_width(2000, 1.0 / 5000.0) >= 1);
}
#[test]
fn the_fill_never_exceeds_the_track_at_any_width() {
for width in [0, 1, 2, 3, 7, 200, 1920] {
for step in 0..=20 {
let value = step as f32 / 20.0;
let filled = fill_width(width, value);
assert!(
(0..=width.max(0)).contains(&filled),
"width {width} at {value} gave {filled}"
);
}
assert_eq!(fill_width(width, 2.0), width.max(0), "width {width} full");
}
assert_eq!(fill_width(-5, 0.5), 0, "a negative width fills nothing");
}
#[test]
fn more_progress_is_never_fewer_pixels() {
for width in [1, 7, 200, 1920] {
let mut previous = 0;
for step in 0..=1000 {
let filled = fill_width(width, step as f32 / 1000.0);
assert!(
filled >= previous,
"width {width} went backwards at step {step}"
);
previous = filled;
}
}
}
#[test]
fn writing_the_same_value_reports_no_change() {
let mut bar = Progress::new(0.0);
assert!(bar.update(0.4));
assert!(!bar.update(0.4));
assert!(bar.update(0.6));
assert!(bar.update(5.0));
assert!(!bar.update(9.0));
assert!(!bar.update(f32::INFINITY));
}
}