use crate::{
AccessibilityAttributes, AccessibilityRole, AccessibilityState, AccessibilityValue, App,
Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, Pixels,
Style, StyleRefinement, Styled, Window, fill, px, relative, rgb,
};
use refineable::Refineable;
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct ProgressRenderState {
pub value: f64,
pub max: f64,
pub percentage: Option<f64>,
pub indeterminate: bool,
}
type ProgressCustomRenderer =
Rc<dyn Fn(ProgressRenderState, Bounds<Pixels>, &mut Window, &mut App)>;
#[track_caller]
pub fn progress(id: impl Into<ElementId>, value: f64) -> Progress {
Progress::new(id.into(), value)
}
pub struct Progress {
element_id: ElementId,
value: f64,
max: f64,
indeterminate: bool,
custom_renderer: Option<ProgressCustomRenderer>,
style: StyleRefinement,
source_location: &'static core::panic::Location<'static>,
}
impl Progress {
#[track_caller]
fn new(element_id: ElementId, value: f64) -> Self {
let mut style = StyleRefinement::default();
style.size.width = Some(relative(1.0).into());
style.size.height = Some(px(12.0).into());
Self {
element_id,
value,
max: 1.0,
indeterminate: false,
custom_renderer: None,
style,
source_location: core::panic::Location::caller(),
}
}
pub fn max(mut self, max: f64) -> Self {
if max.is_finite() && max > 0.0 {
self.max = max;
}
self
}
pub fn indeterminate(mut self) -> Self {
self.indeterminate = true;
self
}
pub fn render_with(
mut self,
renderer: impl Fn(ProgressRenderState, Bounds<Pixels>, &mut Window, &mut App) + 'static,
) -> Self {
self.custom_renderer = Some(Rc::new(renderer));
self
}
fn normalized_value(&self) -> f64 {
self.value.clamp(0.0, self.max)
}
fn render_state(&self) -> ProgressRenderState {
let value = self.normalized_value();
ProgressRenderState {
value,
max: self.max,
percentage: (!self.indeterminate).then_some((value / self.max).clamp(0.0, 1.0)),
indeterminate: self.indeterminate,
}
}
}
impl Element for Progress {
type RequestLayoutState = Style;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.element_id.clone())
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
Some(self.source_location)
}
fn request_layout(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = window.request_layout(style.clone(), [], cx);
(layout_id, style)
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) -> Self::PrepaintState {
}
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
style: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let render_state = self.render_state();
let accessibility_value = (!self.indeterminate).then_some(AccessibilityValue::Range {
current: render_state.value,
min: 0.0,
max: render_state.max,
step: None,
});
style.paint(bounds, window, cx, |window, cx| {
if let Some(renderer) = &self.custom_renderer {
renderer(render_state, bounds, window, cx);
} else {
paint_default_progress(render_state, bounds, window);
}
});
let accessibility = AccessibilityAttributes::new(AccessibilityRole::ProgressBar).states(
if self.indeterminate {
AccessibilityState::BUSY
} else {
AccessibilityState::NONE
},
);
let accessibility = if self.indeterminate {
accessibility
} else {
accessibility.value(accessibility_value.expect("determinate progress has range value"))
};
window.register_accessibility_node(accessibility.to_node(crate::AccessibilityId::new()));
}
}
impl IntoElement for Progress {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for Progress {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
fn paint_default_progress(state: ProgressRenderState, bounds: Bounds<Pixels>, window: &mut Window) {
window.paint_quad(fill(bounds, rgb(0xe2e8f0)).corner_radii(px(999.0)));
let fill_bounds = if state.indeterminate {
let width = (bounds.size.width * 0.35)
.max(px(12.0))
.min(bounds.size.width);
Bounds::new(bounds.origin, crate::size(width, bounds.size.height))
} else {
Bounds::new(
bounds.origin,
crate::size(
bounds.size.width * state.percentage.unwrap_or(0.0) as f32,
bounds.size.height,
),
)
};
if fill_bounds.size.width > Pixels::ZERO {
window.paint_quad(fill(fill_bounds, rgb(0x1d4ed8)).corner_radii(px(999.0)));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AccessibilityRole, AccessibilityState, AccessibilityValue, Context, ParentElement, Render,
TestAppContext, div,
};
use std::cell::Cell;
struct ProgressView;
struct CustomProgressView {
snapshot: Rc<Cell<Option<(f64, Option<f64>, bool)>>>,
}
impl Render for ProgressView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().w(px(240.0)).h(px(12.0)).child(progress("task", 0.25))
}
}
impl Render for CustomProgressView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let snapshot = self.snapshot.clone();
div().w(px(240.0)).h(px(12.0)).child(
progress("task_custom", 0.5)
.indeterminate()
.render_with(move |state, _, _, _| {
snapshot.set(Some((state.value, state.percentage, state.indeterminate)));
}),
)
}
}
#[test]
fn progress_render_state_clamps_to_max() {
let state = progress("task", 2.0).render_state();
assert_eq!(state.value, 1.0);
assert_eq!(state.percentage, Some(1.0));
}
#[crate::test]
fn progress_registers_accessibility_range(cx: &mut TestAppContext) {
let (_view, mut window) = cx.add_window_view(|_, _| ProgressView);
window.update(|window, cx| {
window.draw(cx).clear();
let progress = window
.accessibility_tree
.nodes
.values()
.find(|node| node.role == AccessibilityRole::ProgressBar)
.unwrap();
assert_eq!(
progress.value,
Some(AccessibilityValue::Range {
current: 0.25,
min: 0.0,
max: 1.0,
step: None,
})
);
assert_eq!(progress.states, AccessibilityState::NONE);
});
}
#[crate::test]
fn progress_render_with_receives_indeterminate_state(cx: &mut TestAppContext) {
let snapshot = Rc::new(Cell::new(None));
let snapshot_ref = snapshot.clone();
let (_view, mut window) = cx.add_window_view(|_, _| CustomProgressView {
snapshot: snapshot_ref,
});
window.update(|window, cx| {
window.draw(cx).clear();
});
assert_eq!(snapshot.get(), Some((0.5, None, true)));
window.update(|window, cx| {
window.draw(cx).clear();
let progress = window
.accessibility_tree
.nodes
.values()
.find(|node| node.role == AccessibilityRole::ProgressBar)
.unwrap();
assert!(progress.states.contains(AccessibilityState::BUSY));
assert_eq!(progress.value, None);
});
}
}