zng_wgt_progress/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Progress indicator widget.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use zng_app::widget::border::BorderSide;
15use zng_layout::unit::euclid;
16use zng_var::{
17    VARS,
18    animation::{AnimationHandle, Transition, easing},
19};
20use zng_wgt::{
21    base_color,
22    prelude::{colors::ACCENT_COLOR_VAR, *},
23    visibility,
24};
25use zng_wgt_container::{self as container, Container};
26use zng_wgt_fill::background_color;
27use zng_wgt_size_offset::{height, width, x};
28use zng_wgt_style::{Style, StyleMix, impl_named_style_fn, impl_style_fn};
29
30pub use zng_task::Progress;
31
32/// Progress indicator widget.
33#[widget($crate::ProgressView { ($progress:expr) => { progress = $progress; }; })]
34pub struct ProgressView(StyleMix<WidgetBase>);
35impl ProgressView {
36    fn widget_intrinsic(&mut self) {
37        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
38    }
39}
40impl_style_fn!(ProgressView, DefaultStyle);
41
42context_var! {
43    /// The progress status value in a [`ProgressView`](struct@ProgressView)
44    pub static PROGRESS_VAR: Progress = Progress::indeterminate();
45}
46
47/// The progress status to be displayed.
48///
49/// This property sets the [`PROGRESS_VAR`].
50#[property(CONTEXT, default(PROGRESS_VAR), widget_impl(ProgressView))]
51pub fn progress(child: impl IntoUiNode, progress: impl IntoVar<Progress>) -> UiNode {
52    with_context_var(child, PROGRESS_VAR, progress)
53}
54
55/// Collapse visibility when [`Progress::is_complete`].
56#[property(CONTEXT, default(false), widget_impl(ProgressView, DefaultStyle))]
57pub fn collapse_complete(child: impl IntoUiNode, collapse: impl IntoVar<bool>) -> UiNode {
58    let collapse = collapse.into_var();
59    visibility(
60        child,
61        expr_var! {
62            if #{PROGRESS_VAR}.is_complete() && *#{collapse} {
63                Visibility::Collapsed
64            } else {
65                Visibility::Visible
66            }
67        },
68    )
69}
70
71/// Event raised for each progress update, and once after info init.
72///
73/// This event works in any context that sets [`PROGRESS_VAR`].
74#[property(EVENT, widget_impl(ProgressView))]
75pub fn on_progress(child: impl IntoUiNode, handler: Handler<Progress>) -> UiNode {
76    // copied from `on_info_init`
77    enum State {
78        WaitInfo,
79        InfoInited,
80        Done,
81    }
82    let mut state = State::WaitInfo;
83    let mut handler = handler.into_wgt_runner();
84
85    match_node(child, move |c, op| match op {
86        UiNodeOp::Init => {
87            WIDGET.sub_var(&PROGRESS_VAR);
88            state = State::WaitInfo;
89        }
90        UiNodeOp::Deinit => {
91            handler.deinit();
92        }
93        UiNodeOp::Info { .. } => {
94            if let State::WaitInfo = &state {
95                state = State::InfoInited;
96                WIDGET.update();
97            }
98        }
99        UiNodeOp::Update { updates } => {
100            c.update(updates);
101
102            match state {
103                State::Done => {
104                    if PROGRESS_VAR.is_new() {
105                        PROGRESS_VAR.with(|u| handler.event(u));
106                    } else {
107                        handler.update();
108                    }
109                }
110                State::InfoInited => {
111                    PROGRESS_VAR.with(|u| handler.event(u));
112                    state = State::Done;
113                }
114                State::WaitInfo => {}
115            }
116        }
117        _ => {}
118    })
119}
120
121/// Event raised when progress updates to a complete state or inits completed.
122///
123/// This event works in any context that sets [`PROGRESS_VAR`].
124#[property(EVENT, widget_impl(ProgressView))]
125pub fn on_complete(child: impl IntoUiNode, handler: Handler<Progress>) -> UiNode {
126    let mut is_complete = false;
127    on_progress(
128        child,
129        handler.filtered(move |u| {
130            let complete = u.is_complete();
131            if complete != is_complete {
132                is_complete = complete;
133                return is_complete;
134            }
135            false
136        }),
137    )
138}
139
140/// Getter property that is `true` when progress is indeterminate.
141///
142/// This event works in any context that sets [`PROGRESS_VAR`].
143#[property(EVENT, widget_impl(ProgressView, DefaultStyle))]
144pub fn is_indeterminate(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
145    bind_state(child, PROGRESS_VAR.map(|p| p.is_indeterminate()), state)
146}
147
148/// Progress view default style (progress bar with message text).
149#[widget($crate::DefaultStyle)]
150pub struct DefaultStyle(Style);
151impl DefaultStyle {
152    fn widget_intrinsic(&mut self) {
153        widget_set! {
154            self;
155            base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
156
157            container::child = Container! {
158                height = 5;
159                background_color = colors::BASE_COLOR_VAR.rgba();
160
161                clip_to_bounds = true;
162                child = {
163                    let ind_x = var(Length::from(0));
164                    let ind_width = 10.pct();
165
166                    zng_wgt::Wgt! {
167                        background_color = colors::ACCENT_COLOR_VAR.rgba();
168
169                        #[easing(200.ms())]
170                        width = PROGRESS_VAR.map(|p| Length::from(p.fct()));
171
172                        on_progress = {
173                            let mut handle = VarHandle::dummy();
174                            hn!(ind_x, |p| {
175                                if p.is_indeterminate() {
176                                    // only animates when actually indeterminate
177                                    if handle.is_dummy() {
178                                        handle =
179                                            ind_x.sequence(move |x| x.set_ease(-ind_width, 100.pct(), 1.5.secs(), |t| easing::ease_out(easing::quad, t)));
180                                    }
181                                } else {
182                                    handle = VarHandle::dummy();
183                                }
184                            })
185                        };
186                        when #{PROGRESS_VAR}.is_indeterminate() {
187                            width = ind_width;
188                            x = ind_x;
189                        }
190                    }
191                };
192            };
193
194            container::child_spacing = 6;
195            container::child_out_bottom = zng_wgt_text::Text! {
196                txt = PROGRESS_VAR.map(|p| p.msg());
197                zng_wgt::visibility = PROGRESS_VAR.map(|p| Visibility::from(!p.msg().is_empty()));
198                zng_wgt::align = Align::CENTER;
199            };
200        }
201    }
202}
203
204/// Progress view style that is only the progress bar, no message text.
205#[widget($crate::SimpleBarStyle)]
206pub struct SimpleBarStyle(DefaultStyle);
207impl_named_style_fn!(simple_bar, SimpleBarStyle);
208impl SimpleBarStyle {
209    fn widget_intrinsic(&mut self) {
210        widget_set! {
211            self;
212            named_style_fn = SIMPLE_BAR_STYLE_FN_VAR;
213            container::child_out_bottom = unset!;
214        }
215    }
216}
217
218/// Circular progress indicator style.
219#[widget($crate::CircularStyle)]
220pub struct CircularStyle(Style);
221impl_named_style_fn!(circular, CircularStyle);
222impl CircularStyle {
223    fn widget_intrinsic(&mut self) {
224        widget_set! {
225            self;
226            replace = true;
227            named_style_fn = CIRCULAR_STYLE_FN_VAR;
228            container::child_start = {
229                let start = var(0.rad());
230                let end = var(0.rad());
231                zng_wgt::Wgt! {
232                    zng_wgt_size_offset::size = 1.4.em();
233                    zng_wgt_fill::background = arc_shape(0.2.em(), ACCENT_COLOR_VAR.rgba(), start.clone(), end.clone());
234                    on_progress = {
235                        let mut ind_handle = AnimationHandle::dummy();
236                        hn!(|args| {
237                            if args.is_indeterminate() {
238                                if ind_handle.is_stopped() {
239                                    ind_handle = VARS.animate(clmv!(start, end, |a| {
240                                        if a.count() == 0 {
241                                            let t = a.elapsed_restart(1.secs());
242
243                                            end.set(Transition::new(0.turn(), 1.turn()).sample(t.fct()));
244
245                                            if let Some(t) = t.seg(80.pct()..) {
246                                                start.set(Transition::new(0.turn(), 0.8.turn()).sample(t.fct()));
247                                            }
248                                        } else {
249                                            let t = a.elapsed_restart(500.ms());
250                                            let v = Transition::new(0.turn(), 1.turn()).sample(t.fct());
251                                            start.set(v - 0.2.turn());
252                                            end.set(v);
253                                        }
254                                    }));
255                                }
256                            } else {
257                                if !ind_handle.is_stopped() {
258                                    ind_handle = AnimationHandle::dummy();
259                                    start.ease(0.rad(), 200.ms(), easing::linear).perm();
260                                }
261                                end.ease(args.fct().0.turn(), 200.ms(), |t| easing::ease_out(easing::quad, t))
262                                    .perm();
263                            }
264                        })
265                    };
266                }
267            };
268            container::child_spacing = 6;
269            container::child = zng_wgt_text::Text! {
270                txt = PROGRESS_VAR.map(|p| p.msg());
271                zng_wgt::visibility = PROGRESS_VAR.map(|p| Visibility::from(!p.msg().is_empty()));
272            };
273        }
274    }
275}
276
277/// Circular progress indicator style without message text.
278#[widget($crate::SimpleCircularStyle)]
279pub struct SimpleCircularStyle(Style);
280impl_named_style_fn!(simple_circular, SimpleCircularStyle);
281impl SimpleCircularStyle {
282    fn widget_intrinsic(&mut self) {
283        widget_set! {
284            self;
285            container::child = unset!;
286        }
287    }
288}
289
290/// Render an arc line or circle.
291///
292/// The arc ellipses is defined by the fill area available for the node. If `start` and `end` are equal does not render, if
293/// `end` overlaps one turn renders a full circle. 0ยบ is at the top.
294pub fn arc_shape(
295    thickness: impl IntoVar<Length>,
296    color: impl IntoVar<Rgba>,
297    start: impl IntoVar<AngleRadian>,
298    end: impl IntoVar<AngleRadian>,
299) -> UiNode {
300    // To leverage GPU rendering we render the arc using two halves of a circle drawn
301    // with border+corner-radius and clips
302    let thickness = thickness.into_var();
303    let color = color.into_var();
304    let start = start.into_var();
305    let end = end.into_var();
306
307    let mut render_thickness = Px(0);
308    let mut render_size = PxSize::zero();
309    let rotate_start_key = FrameValueKey::new_unique();
310    let rotate_half0_key = FrameValueKey::new_unique();
311    let rotate_half1_key = FrameValueKey::new_unique();
312
313    // [start, half0, half1]
314    fn rotates(area: PxSize, start: AngleRadian, end: AngleRadian) -> [PxTransform; 3] {
315        let center = area.to_vector().cast::<f32>() * 0.5.fct();
316        let rotate = |rad: f32| {
317            PxTransform::translation(-center.x, -center.y)
318                .then(&Transform::new_rotate(rad.rad()).layout())
319                .then_translate(center)
320        };
321
322        // first half is round border top-right, clipped to left side of area
323        // second is bottom-left, clipped to right side of area
324        let trick = 45.0_f32.to_radians();
325
326        let length = (end.0 - start.0).max(0.0).min(360.0_f32.to_radians());
327        let half_rad = 180.0_f32.to_radians();
328        let rotate_half = |length: f32, stitch: f32| {
329            let t = rotate(trick - half_rad + length);
330
331            // Webrender leaves a faint subpixel line at the edge of clips, translate to hide error
332            if length < 0.001 || length > 180.0_f32.to_radians() - 0.001 {
333                t.then_translate(euclid::vec2(stitch, 0.0))
334            } else {
335                t
336            }
337        };
338
339        let stitch = if start.0.abs() > 0.001 { 1.5 } else { 1.0 };
340        [
341            rotate(start.0),
342            rotate_half(length.min(half_rad), -stitch),
343            rotate_half((length - half_rad).max(0.0), stitch),
344        ]
345    }
346
347    match_node_leaf(move |op| match op {
348        UiNodeOp::Init => {
349            WIDGET
350                .sub_var_layout(&thickness)
351                .sub_var_render(&color)
352                .sub_var_render_update(&start)
353                .sub_var_render_update(&end);
354        }
355        UiNodeOp::Layout { final_size, .. } => {
356            *final_size = LAYOUT.constraints().fill_size();
357
358            // Snap center point, without this can render a faint subpixel line, even with the correction implemented by `rotate_half`.
359            let mut s = *final_size;
360            s.width.0 = ((final_size.width.0 as f32 / 2.0).floor() * 2.0) as _;
361
362            if render_size != s {
363                render_size = s;
364                WIDGET.render();
365            }
366            let t = thickness.layout_x();
367            if render_thickness != t {
368                render_thickness = t;
369                WIDGET.render();
370            }
371        }
372        UiNodeOp::Render { frame } => {
373            let [start_t, half0_t, half1_t] = rotates(render_size, start.get(), end.get());
374            let is_animating = start.is_animating() || end.is_animating();
375
376            frame.push_reference_frame(
377                rotate_start_key.into(),
378                rotate_start_key.bind(start_t, is_animating),
379                false,
380                true,
381                |frame| {
382                    let half = PxPoint::new(render_size.width / Px(2), Px(0));
383                    let color = BorderSide::from(color.get());
384
385                    frame.push_clip_rect(PxRect::new(half, render_size), false, true, |frame| {
386                        frame.push_reference_frame(
387                            rotate_half0_key.into(),
388                            rotate_half0_key.bind(half0_t, is_animating),
389                            false,
390                            true,
391                            |frame| {
392                                let mut b = BorderSides::hidden();
393                                b.top = color;
394                                b.right = b.top;
395                                frame.push_border(
396                                    PxRect::from(render_size),
397                                    PxSideOffsets::new_all_same(render_thickness),
398                                    b,
399                                    PxCornerRadius::new_all(render_size),
400                                );
401                            },
402                        );
403                    });
404
405                    frame.push_clip_rect(PxRect::new(-half, render_size), false, true, |frame| {
406                        frame.push_reference_frame(
407                            rotate_half1_key.into(),
408                            rotate_half1_key.bind(half1_t, is_animating),
409                            false,
410                            true,
411                            |frame| {
412                                let mut b = BorderSides::hidden();
413                                b.bottom = color;
414                                b.left = b.bottom;
415                                frame.push_border(
416                                    PxRect::from(render_size),
417                                    PxSideOffsets::new_all_same(render_thickness),
418                                    b,
419                                    PxCornerRadius::new_all(render_size),
420                                );
421                            },
422                        );
423                    });
424                },
425            );
426        }
427        UiNodeOp::RenderUpdate { update } => {
428            let [start_t, half0_t, half1_t] = rotates(render_size, start.get(), end.get());
429            let is_animating = start.is_animating() || end.is_animating();
430
431            update.update_transform(rotate_start_key.update(start_t, is_animating), true);
432            update.update_transform(rotate_half0_key.update(half0_t, is_animating), true);
433            update.update_transform(rotate_half1_key.update(half1_t, is_animating), true);
434        }
435        _ => {}
436    })
437}