use super::{
BACKGROUND_COLOR, CONTENT_DESCRIPTION, INDETERMINATE, MAX, MIN, PROGRESS_TINT, Plan, Setter,
VALUE, color, owned_text, plan_color, slot_of,
};
use crate::NativeWidgetError;
use crate::registry::SlotId;
use crate::runtime::Params;
pub(crate) const KIND: &str = "progress";
pub(crate) struct Progress;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ProgressProps {
pub(crate) slot: SlotId,
pub(crate) value: i32,
pub(crate) min: i32,
pub(crate) max: i32,
pub(crate) indeterminate: bool,
pub(crate) background_color: Option<i32>,
pub(crate) progress_tint: Option<i32>,
pub(crate) content_description: Option<String>,
}
impl ProgressProps {
pub(crate) fn platform_default(slot: SlotId) -> Self {
Self {
slot,
value: 0,
min: 0,
max: super::slider::PLATFORM_DEFAULT_MAX,
indeterminate: false,
background_color: None,
progress_tint: None,
content_description: None,
}
}
pub(crate) fn decode(params: &Params<'_>) -> Result<Self, NativeWidgetError> {
Ok(Self {
slot: slot_of(params)?,
value: int_or(params, VALUE, 0),
min: int_or(params, MIN, 0),
max: int_or(params, MAX, super::slider::PLATFORM_DEFAULT_MAX),
indeterminate: params.flag(INDETERMINATE).unwrap_or(false),
background_color: color(params, BACKGROUND_COLOR),
progress_tint: color(params, PROGRESS_TINT),
content_description: owned_text(params, CONTENT_DESCRIPTION),
})
}
pub(crate) fn span(&self) -> i32 {
self.max.saturating_sub(self.min).max(0)
}
pub(crate) fn progress(&self) -> i32 {
self.value
.clamp(self.min, self.max.max(self.min))
.saturating_sub(self.min)
}
pub(crate) fn plan<'a>(old: &Self, new: &'a Self) -> Plan<'a> {
let mut plan = Plan::new();
if old.span() != new.span() {
plan.push(Setter::Max(new.span()));
}
if old.progress() != new.progress() {
plan.push(Setter::Progress(new.progress()));
}
if old.indeterminate != new.indeterminate {
plan.push(Setter::Indeterminate(new.indeterminate));
}
plan_color(
&mut plan,
old.background_color,
new.background_color,
Setter::BackgroundColor,
);
if old.progress_tint != new.progress_tint {
plan.push(Setter::ProgressTint(new.progress_tint));
}
if old.content_description != new.content_description {
plan.push(Setter::ContentDescription(
new.content_description.as_deref(),
));
}
plan
}
}
fn int_or(params: &Params<'_>, key: &str, fallback: i32) -> i32 {
params
.int(key)
.and_then(|raw| i32::try_from(raw).ok())
.unwrap_or(fallback)
}
pub(crate) fn progress_fraction(progress: i32, span: i32) -> f32 {
if span <= 0 {
return 0.0;
}
(progress as f32 / span as f32).clamp(0.0, 1.0)
}
#[cfg(target_os = "android")]
pub(crate) mod platform {
use jni::objects::{JObject, JValue};
use jni::refs::Global;
use jni::{jni_sig, jni_str};
use super::{Progress, ProgressProps};
use crate::NativeWidgetError;
use crate::android::{NativeCtx, NativeView};
use crate::controls::platform::{FRAME_CAPACITY, apply_all};
use crate::runtime::{NativeWidget, Params};
const CLASS: &str = "android.widget.ProgressBar";
const R_ATTR_CLASS: &str = "android.R$attr";
fn new_horizontal_bar<'local>(
ctx: &mut NativeCtx<'local, '_>,
) -> Result<JObject<'local>, NativeWidgetError> {
let attrs = ctx.class(R_ATTR_CLASS)?;
let style = ctx.run_jni("android.R.attr.progressBarStyleHorizontal", |env| {
env.get_static_field(
&attrs,
jni_str!("progressBarStyleHorizontal"),
jni_sig!("I"),
)?
.i()
})?;
let class = ctx.class(CLASS)?;
let context = ctx.context()?;
ctx.run_jni("new ProgressBar(Context, AttributeSet, int)", |env| {
env.new_object(
&class,
jni_sig!("(Landroid/content/Context;Landroid/util/AttributeSet;I)V"),
&[
JValue::Object(context),
JValue::Object(&JObject::null()),
JValue::Int(style),
],
)
})
}
pub(crate) struct ProgressState {
view: Global<JObject<'static>>,
}
impl NativeWidget for Progress {
type Props = ProgressProps;
type State = ProgressState;
fn decode_props(params: &Params<'_>) -> Result<Self::Props, NativeWidgetError> {
ProgressProps::decode(params)
}
fn create(
ctx: &mut NativeCtx<'_, '_>,
props: &Self::Props,
) -> Result<(NativeView, Self::State), NativeWidgetError> {
let view = new_horizontal_bar(ctx)?;
let plan = ProgressProps::plan(&ProgressProps::platform_default(props.slot), props);
ctx.with_frame(FRAME_CAPACITY, |ctx| apply_all(ctx, &view, &plan))?;
let handle = ctx.retain(&view)?;
let retained = ctx.retain(&view)?;
Ok((NativeView::new(handle), ProgressState { view: retained }))
}
fn update(
ctx: &mut NativeCtx<'_, '_>,
state: &mut Self::State,
old: &Self::Props,
new: &Self::Props,
) -> Result<(), NativeWidgetError> {
let plan = ProgressProps::plan(old, new);
ctx.with_frame(FRAME_CAPACITY, |ctx| apply_all(ctx, &state.view, &plan))
}
fn dispose(
_ctx: &mut NativeCtx<'_, '_>,
_state: Self::State,
) -> Result<(), NativeWidgetError> {
Ok(())
}
}
}
#[cfg(target_os = "ios")]
pub(crate) mod platform {
use objc2::MainThreadMarker;
use objc2::rc::Retained;
use objc2_ui_kit::UIProgressView;
use super::{KIND, Progress, ProgressProps, progress_fraction};
use crate::NativeWidgetError;
use crate::apple::{NativeCtx, NativeView};
use crate::controls::platform;
use crate::controls::{Plan, Setter};
use crate::runtime::{NativeWidget, Params};
pub(crate) struct ProgressState {
view: Retained<UIProgressView>,
span: i32,
progress: i32,
}
impl ProgressState {
fn refresh(&self) {
self.view
.setProgress(progress_fraction(self.progress, self.span));
}
}
impl NativeWidget for Progress {
type Props = ProgressProps;
type State = ProgressState;
fn decode_props(params: &Params<'_>) -> Result<Self::Props, NativeWidgetError> {
ProgressProps::decode(params)
}
fn create(
ctx: &mut NativeCtx<'_, '_>,
props: &Self::Props,
) -> Result<(NativeView, Self::State), NativeWidgetError> {
let mtm = ctx.mtm();
let default = ProgressProps::platform_default(props.slot);
let mut state = ProgressState {
view: UIProgressView::new(mtm),
span: default.span(),
progress: default.progress(),
};
let plan = ProgressProps::plan(&default, props);
apply_all(mtm, &mut state, &plan);
let handle = NativeView::new(Retained::clone(&state.view).into_super(), mtm);
Ok((handle, state))
}
fn update(
ctx: &mut NativeCtx<'_, '_>,
state: &mut Self::State,
old: &Self::Props,
new: &Self::Props,
) -> Result<(), NativeWidgetError> {
let plan = ProgressProps::plan(old, new);
apply_all(ctx.mtm(), state, &plan);
Ok(())
}
fn dispose(
_ctx: &mut NativeCtx<'_, '_>,
_state: Self::State,
) -> Result<(), NativeWidgetError> {
Ok(())
}
}
fn apply_all(mtm: MainThreadMarker, state: &mut ProgressState, plan: &Plan<'_>) {
for setter in plan {
apply(mtm, state, setter);
}
}
fn apply(mtm: MainThreadMarker, state: &mut ProgressState, setter: &Setter<'_>) {
match *setter {
Setter::Max(span) => {
state.span = span;
state.refresh();
}
Setter::Progress(progress) => {
state.progress = progress;
state.refresh();
}
Setter::Indeterminate(indeterminate) => {
platform::warn_indeterminate_unsupported(indeterminate);
}
Setter::BackgroundColor(argb) => platform::set_background_color(&state.view, argb),
Setter::ProgressTint(argb) => {
state
.view
.setProgressTintColor(platform::optional_ui_color(argb).as_deref());
}
Setter::ContentDescription(label) => {
platform::set_accessibility_label(&state.view, label, mtm);
}
ref other => platform::warn_unexpected_setter(KIND, other),
}
}
}
#[cfg(target_os = "macos")]
pub(crate) mod platform {
use objc2::rc::Retained;
use objc2_app_kit::{NSProgressIndicator, NSProgressIndicatorStyle};
use super::{KIND, Progress, ProgressProps};
use crate::NativeWidgetError;
use crate::appkit::{NativeCtx, NativeView};
use crate::controls::platform;
use crate::controls::{Plan, Setter};
use crate::runtime::{NativeWidget, Params};
pub(crate) struct ProgressState {
view: Retained<NSProgressIndicator>,
}
impl NativeWidget for Progress {
type Props = ProgressProps;
type State = ProgressState;
fn decode_props(params: &Params<'_>) -> Result<Self::Props, NativeWidgetError> {
ProgressProps::decode(params)
}
fn create(
ctx: &mut NativeCtx<'_, '_>,
props: &Self::Props,
) -> Result<(NativeView, Self::State), NativeWidgetError> {
let mtm = ctx.mtm();
let view = NSProgressIndicator::new(mtm);
let default = ProgressProps::platform_default(props.slot);
view.setStyle(NSProgressIndicatorStyle::Bar);
view.setIndeterminate(default.indeterminate);
view.setMinValue(0.0);
view.setMaxValue(f64::from(default.span()));
view.setDoubleValue(f64::from(default.progress()));
let plan = ProgressProps::plan(&default, props);
apply_all(&view, &plan);
let handle = NativeView::new(Retained::clone(&view).into_super(), mtm);
Ok((handle, ProgressState { view }))
}
fn update(
_ctx: &mut NativeCtx<'_, '_>,
state: &mut Self::State,
old: &Self::Props,
new: &Self::Props,
) -> Result<(), NativeWidgetError> {
apply_all(&state.view, &ProgressProps::plan(old, new));
Ok(())
}
fn dispose(
_ctx: &mut NativeCtx<'_, '_>,
state: Self::State,
) -> Result<(), NativeWidgetError> {
if state.view.isIndeterminate() {
set_indeterminate(&state.view, false);
}
Ok(())
}
}
fn apply_all(view: &NSProgressIndicator, plan: &Plan<'_>) {
for setter in plan {
apply(view, setter);
}
}
fn apply(view: &NSProgressIndicator, setter: &Setter<'_>) {
match *setter {
Setter::Max(span) => view.setMaxValue(f64::from(span)),
Setter::Progress(progress) => view.setDoubleValue(f64::from(progress)),
Setter::Indeterminate(indeterminate) => set_indeterminate(view, indeterminate),
Setter::BackgroundColor(argb) => platform::set_background_color(view, argb),
Setter::ProgressTint(argb) => platform::set_tint(view, "ProgressTint", argb),
Setter::ContentDescription(label) => platform::set_accessibility_label(view, label),
ref other => platform::warn_unexpected_setter(KIND, other),
}
}
fn set_indeterminate(view: &NSProgressIndicator, indeterminate: bool) {
if indeterminate {
view.setIndeterminate(true);
unsafe { view.startAnimation(None) };
} else {
unsafe { view.stopAnimation(None) };
view.setIndeterminate(false);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::with_identity;
fn decode(body: &str) -> ProgressProps {
let raw = with_identity(KIND, 2, body);
ProgressProps::decode(&Params::new(&raw)).expect("decodes")
}
#[test]
fn a_bare_payload_matches_the_constructed_bar_and_plans_nothing() {
let props = decode("");
assert_eq!(props, ProgressProps::platform_default(2));
assert!(ProgressProps::plan(&ProgressProps::platform_default(2), &props).is_empty());
}
#[test]
fn asking_for_a_spinner_plans_exactly_one_setter() {
let props = decode("\"indeterminate\":true");
assert_eq!(
ProgressProps::plan(&ProgressProps::platform_default(2), &props),
vec![Setter::Indeterminate(true)]
);
}
#[test]
fn the_create_plan_sets_exactly_the_non_default_fields_in_order() {
let props = decode(
"\"value\":30,\"min\":10,\"max\":110,\"progressTint\":9,\
\"contentDescription\":\"upload\"",
);
assert_eq!(
ProgressProps::plan(&ProgressProps::platform_default(2), &props),
vec![
Setter::Progress(20),
Setter::ProgressTint(Some(9)),
Setter::ContentDescription(Some("upload")),
],
"span is already 100, so only the offset progress is planned"
);
}
#[test]
fn a_streaming_value_change_is_one_cheap_setter_per_update() {
let old = decode("\"value\":1,\"max\":1000");
let new = decode("\"value\":2,\"max\":1000");
let plan = ProgressProps::plan(&old, &new);
assert_eq!(plan, vec![Setter::Progress(2)]);
assert!(
plan.iter()
.all(|setter| setter.tier() == super::super::Tier::Cheap),
"the whole ProgressBar surface is frame-rate safe"
);
}
#[test]
fn the_value_is_kept_current_behind_a_spinner() {
let bar = decode("\"value\":5,\"max\":10");
let spinner = decode("\"value\":6,\"max\":10,\"indeterminate\":true");
assert_eq!(
ProgressProps::plan(&bar, &spinner),
vec![Setter::Progress(6), Setter::Indeterminate(true)]
);
}
#[test]
fn a_background_color_alone_plans_exactly_one_setter() {
let old = decode("\"backgroundColor\":1");
let new = decode("\"backgroundColor\":2");
assert_eq!(
ProgressProps::plan(&old, &new),
vec![Setter::BackgroundColor(2)]
);
assert_eq!(
ProgressProps::plan(&new, &new),
vec![],
"an unchanged background plans nothing — the zero-FFI property"
);
}
#[test]
fn clearing_a_background_plans_nothing_unlike_a_tint() {
let with_bg = decode("\"backgroundColor\":5");
let without = decode("");
assert_eq!(ProgressProps::plan(&with_bg, &without), vec![]);
assert_eq!(
ProgressProps::plan(&without, &with_bg),
vec![Setter::BackgroundColor(5)]
);
}
#[test]
fn the_planned_span_and_value_become_a_unit_fraction() {
let props = decode("\"value\":30,\"min\":10,\"max\":110");
assert_eq!((props.progress(), props.span()), (20, 100));
assert_eq!(progress_fraction(props.progress(), props.span()), 0.2);
}
#[test]
fn the_fraction_spans_the_whole_range_including_both_ends() {
assert_eq!(progress_fraction(0, 100), 0.0);
assert_eq!(progress_fraction(50, 100), 0.5);
assert_eq!(progress_fraction(100, 100), 1.0);
}
#[test]
fn an_empty_range_reads_as_an_empty_bar_rather_than_nan() {
let inverted = decode("\"value\":3,\"min\":10,\"max\":2");
assert_eq!(inverted.span(), 0);
let fraction = progress_fraction(inverted.progress(), inverted.span());
assert_eq!(fraction, 0.0);
assert!(!fraction.is_nan());
assert_eq!(progress_fraction(7, 0), 0.0);
assert_eq!(progress_fraction(7, -5), 0.0);
}
#[test]
fn a_value_beyond_the_span_is_clamped_into_the_unit_interval() {
assert_eq!(progress_fraction(150, 100), 1.0);
assert_eq!(progress_fraction(-10, 100), 0.0);
}
}