use std::any::TypeId;
use accesskit::{Node, Role};
use masonry_core::core::{NewWidget, Properties};
use tracing::{Span, trace_span};
use vello::Scene;
use vello::kurbo::{Point, Size};
use crate::core::{
AccessCtx, ArcStr, BoxConstraints, ChildrenIds, LayoutCtx, NoAction, PaintCtx, PropertiesMut,
PropertiesRef, RegisterCtx, Update, UpdateCtx, Widget, WidgetId, WidgetMut, WidgetPod,
};
use crate::properties::LineBreaking;
use crate::properties::{Background, BarColor, BorderColor, BorderWidth, CornerRadius};
use crate::util::{fill, include_screenshot, stroke};
use crate::widgets::Label;
#[doc = include_screenshot!("progress_bar_25_percent.png", "25% progress bar.")]
pub struct ProgressBar {
progress: Option<f64>,
label: WidgetPod<Label>,
}
impl ProgressBar {
pub fn new(progress: Option<f64>) -> Self {
let progress = clamp_progress(progress);
let label_props = Properties::one(LineBreaking::Overflow);
let label =
NewWidget::new_with_props(Label::new(Self::value(progress)), label_props).to_pod();
Self { progress, label }
}
fn value_accessibility(&self) -> Box<str> {
if let Some(value) = self.progress {
format!("{:.0}%", value * 100.).into()
} else {
"progress unspecified".into()
}
}
fn value(progress: Option<f64>) -> ArcStr {
if let Some(value) = progress {
format!("{:.0}%", value * 100.).into()
} else {
"".into()
}
}
}
impl ProgressBar {
pub fn set_progress(this: &mut WidgetMut<'_, Self>, progress: Option<f64>) {
let progress = clamp_progress(progress);
let progress_changed = this.widget.progress != progress;
if progress_changed {
this.widget.progress = progress;
let mut label = this.ctx.get_mut(&mut this.widget.label);
Label::set_text(&mut label, Self::value(progress));
}
this.ctx.request_layout();
this.ctx.request_render();
}
}
fn clamp_progress(progress: Option<f64>) -> Option<f64> {
let progress = progress?;
if progress.is_nan() {
None
} else {
Some(progress.clamp(0., 1.))
}
}
impl Widget for ProgressBar {
type Action = NoAction;
fn register_children(&mut self, ctx: &mut RegisterCtx<'_>) {
ctx.register_child(&mut self.label);
}
fn property_changed(&mut self, ctx: &mut UpdateCtx<'_>, property_type: TypeId) {
BorderWidth::prop_changed(ctx, property_type);
CornerRadius::prop_changed(ctx, property_type);
Background::prop_changed(ctx, property_type);
BarColor::prop_changed(ctx, property_type);
BorderColor::prop_changed(ctx, property_type);
}
fn update(
&mut self,
_ctx: &mut UpdateCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &Update,
) {
}
fn layout(
&mut self,
ctx: &mut LayoutCtx<'_>,
_props: &mut PropertiesMut<'_>,
bc: &BoxConstraints,
) -> Size {
const DEFAULT_WIDTH: f64 = 400.;
let label_size = ctx.run_layout(&mut self.label, &bc.loosen());
let desired_size = Size::new(
DEFAULT_WIDTH.max(label_size.width),
crate::theme::BASIC_WIDGET_HEIGHT.max(label_size.height),
);
let final_size = bc.constrain(desired_size);
let text_pos = Point::new(
((final_size.width - label_size.width) * 0.5).max(0.),
((final_size.height - label_size.height) * 0.5).max(0.),
);
ctx.place_child(&mut self.label, text_pos);
final_size
}
fn paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) {
let border_width = props.get::<BorderWidth>();
let border_radius = props.get::<CornerRadius>();
let bg = props.get::<Background>();
let bar_color = props.get::<BarColor>();
let border_color = props.get::<BorderColor>();
let bg_rect = border_width.bg_rect(ctx.size(), border_radius);
let border_rect = border_width.border_rect(ctx.size(), border_radius);
let progress_rect_size = Size::new(
ctx.size().width * self.progress.unwrap_or(1.),
ctx.size().height,
);
let progress_rect = border_width.bg_rect(progress_rect_size, border_radius);
let brush = bg.get_peniko_brush_for_rect(bg_rect.rect());
fill(scene, &bg_rect, &brush);
fill(scene, &progress_rect, bar_color.0);
stroke(scene, &border_rect, border_color.color, border_width.width);
}
fn accessibility_role(&self) -> Role {
Role::ProgressIndicator
}
fn accessibility(
&mut self,
_ctx: &mut AccessCtx<'_>,
_props: &PropertiesRef<'_>,
node: &mut Node,
) {
node.set_min_numeric_value(0.0);
node.set_max_numeric_value(1.0);
if let Some(value) = self.progress {
node.set_numeric_value(value);
}
}
fn children_ids(&self) -> ChildrenIds {
ChildrenIds::from_slice(&[self.label.id()])
}
fn make_trace_span(&self, id: WidgetId) -> Span {
trace_span!("ProgressBar", id = id.trace())
}
fn get_debug_text(&self) -> Option<String> {
Some(self.value_accessibility().into())
}
}
#[cfg(test)]
mod tests {
use masonry_core::core::NewWidget;
use super::*;
use crate::testing::{TestHarness, assert_render_snapshot};
use crate::theme::default_property_set;
use crate::{core::Properties, palette};
#[test]
fn indeterminate_progressbar() {
let widget = NewWidget::new(ProgressBar::new(None));
let window_size = Size::new(150.0, 60.0);
let mut harness =
TestHarness::create_with_size(default_property_set(), widget, window_size);
assert_render_snapshot!(harness, "progress_bar_indeterminate");
}
#[test]
fn _0_percent_progressbar() {
let widget = NewWidget::new(ProgressBar::new(Some(0.)));
let window_size = Size::new(150.0, 60.0);
let mut harness =
TestHarness::create_with_size(default_property_set(), widget, window_size);
assert_render_snapshot!(harness, "progress_bar_0_percent");
}
#[test]
fn _25_percent_progressbar() {
let widget = NewWidget::new(ProgressBar::new(Some(0.25)));
let window_size = Size::new(150.0, 60.0);
let mut harness =
TestHarness::create_with_size(default_property_set(), widget, window_size);
assert_render_snapshot!(harness, "progress_bar_25_percent");
}
#[test]
fn _50_percent_progressbar() {
let widget = NewWidget::new(ProgressBar::new(Some(0.5)));
let window_size = Size::new(150.0, 60.0);
let mut harness =
TestHarness::create_with_size(default_property_set(), widget, window_size);
assert_render_snapshot!(harness, "progress_bar_50_percent");
}
#[test]
fn _75_percent_progressbar() {
let widget = NewWidget::new(ProgressBar::new(Some(0.75)));
let window_size = Size::new(150.0, 60.0);
let mut harness =
TestHarness::create_with_size(default_property_set(), widget, window_size);
assert_render_snapshot!(harness, "progress_bar_75_percent");
}
#[test]
fn _100_percent_progressbar() {
let widget = NewWidget::new(ProgressBar::new(Some(1.)));
let window_size = Size::new(150.0, 60.0);
let mut harness =
TestHarness::create_with_size(default_property_set(), widget, window_size);
assert_render_snapshot!(harness, "progress_bar_100_percent");
}
#[test]
fn edit_progressbar() {
let image_1 = {
let bar = ProgressBar::new(Some(0.5))
.with_props(Properties::new().with(BarColor(palette::css::PURPLE)));
let mut harness =
TestHarness::create_with_size(default_property_set(), bar, Size::new(60.0, 20.0));
harness.render()
};
let image_2 = {
let bar = NewWidget::new(ProgressBar::new(None));
let mut harness =
TestHarness::create_with_size(default_property_set(), bar, Size::new(60.0, 20.0));
harness.edit_root_widget(|mut bar| {
ProgressBar::set_progress(&mut bar, Some(0.5));
bar.insert_prop(BarColor(palette::css::PURPLE));
});
harness.render()
};
assert!(image_1 == image_2);
}
}