Skip to content

Commit

Permalink
feat: add progress bar widget (#513)
Browse files Browse the repository at this point in the history
@PoignardAzur I wanted to have a play with masonry, so I had a go at
building a progress bar. I've made a PR in case you want it, but I won't
be offended if you close the PR. I'm happy to make changes if you see
anything you'd like to change.

---------

Co-authored-by: Olivier FAURE <[email protected]>
Co-authored-by: jaredoconnell <[email protected]>
  • Loading branch information
3 people authored Aug 30, 2024
1 parent 6c49516 commit a1c7d74
Show file tree
Hide file tree
Showing 18 changed files with 506 additions and 0 deletions.
2 changes: 2 additions & 0 deletions masonry/src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod flex;
mod image;
mod label;
mod portal;
mod progress_bar;
mod prose;
mod root_widget;
mod scroll_bar;
Expand All @@ -37,6 +38,7 @@ pub use checkbox::Checkbox;
pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment};
pub use label::{Label, LineBreaking};
pub use portal::Portal;
pub use progress_bar::ProgressBar;
pub use prose::Prose;
pub use root_widget::RootWidget;
pub use scroll_bar::ScrollBar;
Expand Down
281 changes: 281 additions & 0 deletions masonry/src/widget/progress_bar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
// Copyright 2019 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0

//! A progress bar widget.

use crate::Point;
use accesskit::Role;
use smallvec::{smallvec, SmallVec};
use tracing::{trace, trace_span, Span};
use vello::Scene;

use crate::kurbo::Size;
use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint};
use crate::text::TextLayout;
use crate::widget::WidgetMut;
use crate::{
theme, AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle,
LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget, WidgetId,
};

/// A progress bar
pub struct ProgressBar {
/// A value in the range `[0, 1]` inclusive, where 0 is 0% and 1 is 100% complete.
///
/// `None` variant can be used to show a progress bar without a percentage.
/// It is also used if an invalid float (outside of [0, 1]) is passed.
progress: Option<f64>,
label: TextLayout<ArcStr>,
}

impl ProgressBar {
/// Create a new `ProgressBar`.
///
/// `progress` is a number between 0 and 1 inclusive. If it is `NaN`, then an
/// indefinite progress bar will be shown.
/// Otherwise, the input will be clamped to [0, 1].
pub fn new(progress: Option<f64>) -> Self {
let mut out = Self::new_indefinite();
out.set_progress(progress);
out
}

fn new_indefinite() -> Self {
Self {
progress: None,
label: TextLayout::new("".into(), crate::theme::TEXT_SIZE_NORMAL as f32),
}
}

fn set_progress(&mut self, mut progress: Option<f64>) {
clamp_progress(&mut progress);
// check to see if we can avoid doing work
if self.progress != progress {
self.progress = progress;
self.update_text();
}
}

/// Updates the text layout with the current part-complete value
fn update_text(&mut self) {
self.label.set_text(self.value());
}

fn value(&self) -> ArcStr {
if let Some(value) = self.progress {
format!("{:.0}%", value * 100.).into()
} else {
"".into()
}
}

fn value_accessibility(&self) -> Box<str> {
if let Some(value) = self.progress {
format!("{:.0}%", value * 100.).into()
} else {
"progress unspecified".into()
}
}
}

// --- MARK: WIDGETMUT ---
impl WidgetMut<'_, ProgressBar> {
pub fn set_progress(&mut self, progress: Option<f64>) {
self.widget.set_progress(progress);
self.ctx.request_layout();
self.ctx.request_accessibility_update();
}
}

/// Helper to ensure progress is either a number between [0, 1] inclusive, or `None`.
///
/// NaNs are converted to `None`.
fn clamp_progress(progress: &mut Option<f64>) {
if let Some(value) = progress {
if value.is_nan() {
*progress = None;
} else {
*progress = Some(value.clamp(0., 1.));
}
}
}

// --- MARK: IMPL WIDGET ---
impl Widget for ProgressBar {
// pointer events unhandled for now
fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {}

fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}

// access events unhandled for now
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}

fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, _event: &StatusChange) {
ctx.request_paint();
}

fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {}

fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
const DEFAULT_WIDTH: f64 = 400.;

if self.label.needs_rebuild() {
let (font_ctx, layout_ctx) = ctx.text_contexts();
self.label.rebuild(font_ctx, layout_ctx);
}
let label_size = self.label.size();

let desired_size = Size::new(
DEFAULT_WIDTH.max(label_size.width),
crate::theme::BASIC_WIDGET_HEIGHT.max(label_size.height),
);
let our_size = bc.constrain(desired_size);
trace!("Computed layout: size={}", our_size);
our_size
}

fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
let border_width = 1.;

if self.label.needs_rebuild() {
debug_panic!("Called ProgressBar paint before layout");
}

let rect = ctx
.size()
.to_rect()
.inset(-border_width / 2.)
.to_rounded_rect(2.);

fill_lin_gradient(
scene,
&rect,
[theme::BACKGROUND_LIGHT, theme::BACKGROUND_DARK],
UnitPoint::TOP,
UnitPoint::BOTTOM,
);

stroke(scene, &rect, theme::BORDER_DARK, border_width);

let progress_rect_size = Size::new(
ctx.size().width * self.progress.unwrap_or(1.),
ctx.size().height,
);
let progress_rect = progress_rect_size
.to_rect()
.inset(-border_width / 2.)
.to_rounded_rect(2.);

fill_lin_gradient(
scene,
&progress_rect,
[theme::PRIMARY_LIGHT, theme::PRIMARY_DARK],
UnitPoint::TOP,
UnitPoint::BOTTOM,
);
stroke(scene, &progress_rect, theme::BORDER_DARK, border_width);

// center text
let widget_size = ctx.size();
let label_size = self.label.size();
let text_pos = Point::new(
((widget_size.width - label_size.width) * 0.5).max(0.),
((widget_size.height - label_size.height) * 0.5).max(0.),
);
self.label.draw(scene, text_pos);
}

fn accessibility_role(&self) -> Role {
Role::ProgressIndicator
}

fn accessibility(&mut self, ctx: &mut AccessCtx) {
ctx.current_node().set_value(self.value_accessibility());
if let Some(value) = self.progress {
ctx.current_node().set_numeric_value(value * 100.0);
}
}

fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
smallvec![]
}

fn make_trace_span(&self) -> Span {
trace_span!("ProgressBar")
}

fn get_debug_text(&self) -> Option<String> {
Some(self.value_accessibility().into())
}
}

// --- MARK: TESTS ---
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;

use super::*;
use crate::assert_render_snapshot;
use crate::testing::{widget_ids, TestHarness, TestWidgetExt};

#[test]
fn indeterminate_progressbar() {
let [progressbar_id] = widget_ids();
let widget = ProgressBar::new(None).with_id(progressbar_id);

let mut harness = TestHarness::create(widget);

assert_debug_snapshot!(harness.root_widget());
assert_render_snapshot!(harness, "indeterminate_progressbar");
}

#[test]
fn _0_percent_progressbar() {
let [_0percent] = widget_ids();

let widget = ProgressBar::new(Some(0.)).with_id(_0percent);
let mut harness = TestHarness::create(widget);
assert_debug_snapshot!(harness.root_widget());
assert_render_snapshot!(harness, "0_percent_progressbar");
}

#[test]
fn _25_percent_progressbar() {
let [_25percent] = widget_ids();

let widget = ProgressBar::new(Some(0.25)).with_id(_25percent);
let mut harness = TestHarness::create(widget);
assert_debug_snapshot!(harness.root_widget());
assert_render_snapshot!(harness, "25_percent_progressbar");
}

#[test]
fn _50_percent_progressbar() {
let [_50percent] = widget_ids();

let widget = ProgressBar::new(Some(0.5)).with_id(_50percent);
let mut harness = TestHarness::create(widget);
assert_debug_snapshot!(harness.root_widget());
assert_render_snapshot!(harness, "50_percent_progressbar");
}

#[test]
fn _75_percent_progressbar() {
let [_75percent] = widget_ids();

let widget = ProgressBar::new(Some(0.75)).with_id(_75percent);
let mut harness = TestHarness::create(widget);
assert_debug_snapshot!(harness.root_widget());
assert_render_snapshot!(harness, "75_percent_progressbar");
}

#[test]
fn _100_percent_progressbar() {
let [_100percent] = widget_ids();

let widget = ProgressBar::new(Some(1.)).with_id(_100percent);
let mut harness = TestHarness::create(widget);
assert_debug_snapshot!(harness.root_widget());
assert_render_snapshot!(harness, "100_percent_progressbar");
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/progress_bar.rs
expression: harness.root_widget()
---
SizedBox(
ProgressBar<0%>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/progress_bar.rs
expression: harness.root_widget()
---
SizedBox(
ProgressBar<100%>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/progress_bar.rs
expression: harness.root_widget()
---
SizedBox(
ProgressBar<25%>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/progress_bar.rs
expression: harness.root_widget()
---
SizedBox(
ProgressBar<50%>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/progress_bar.rs
expression: harness.root_widget()
---
SizedBox(
ProgressBar<75%>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/progress_bar.rs
expression: harness.root_widget()
---
SizedBox(
ProgressBar<progress unspecified>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/progress_bar.rs
expression: harness.root_widget()
---
SizedBox(
ProgressBar<0%>,
)
Loading

0 comments on commit a1c7d74

Please sign in to comment.