Skip to main content

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                        width = PROGRESS_VAR.map(|p| Length::from(p.fct())).easing(200.ms(), easing::linear);
170
171                        on_progress = {
172                            let mut handle = VarHandle::dummy();
173                            hn!(ind_x, |p| {
174                                if p.is_indeterminate() {
175                                    // only animates when actually indeterminate
176                                    if handle.is_dummy() {
177                                        handle =
178                                            ind_x.sequence(move |x| x.set_ease(-ind_width, 100.pct(), 1.5.secs(), |t| easing::ease_out(easing::quad, t)));
179                                    }
180                                } else {
181                                    handle = VarHandle::dummy();
182                                }
183                            })
184                        };
185                        when #{PROGRESS_VAR}.is_indeterminate() {
186                            width = ind_width;
187                            x = ind_x;
188                        }
189                    }
190                };
191            };
192
193            container::child_spacing = 6;
194            container::child_out_bottom = {
195                let txt = PROGRESS_VAR.flat_map(|p| p.msg());
196                zng_wgt_text::Text! {
197                    zng_wgt::visibility = txt.map(|p| Visibility::from(!p.is_empty()));
198                    txt;
199                    zng_wgt::align = Align::CENTER;
200                }
201            };
202        }
203    }
204}
205
206/// Progress view style that is only the progress bar, no message text.
207#[widget($crate::SimpleBarStyle)]
208pub struct SimpleBarStyle(DefaultStyle);
209impl_named_style_fn!(simple_bar, SimpleBarStyle);
210impl SimpleBarStyle {
211    fn widget_intrinsic(&mut self) {
212        widget_set! {
213            self;
214            named_style_fn = SIMPLE_BAR_STYLE_FN_VAR;
215            container::child_out_bottom = unset!;
216        }
217    }
218}
219
220/// Circular progress indicator style.
221#[widget($crate::CircularStyle)]
222pub struct CircularStyle(Style);
223impl_named_style_fn!(circular, CircularStyle);
224impl CircularStyle {
225    fn widget_intrinsic(&mut self) {
226        widget_set! {
227            self;
228            replace = true;
229            named_style_fn = CIRCULAR_STYLE_FN_VAR;
230            container::child_start = {
231                let start = var(0.rad());
232                let end = var(0.rad());
233                zng_wgt::Wgt! {
234                    zng_wgt_size_offset::size = 1.4.em();
235                    zng_wgt_fill::background = arc_shape(0.2.em(), ACCENT_COLOR_VAR.rgba(), start.clone(), end.clone());
236                    on_progress = {
237                        let mut ind_handle = AnimationHandle::dummy();
238                        hn!(|args| {
239                            if args.is_indeterminate() {
240                                if ind_handle.is_stopped() {
241                                    ind_handle = VARS.animate(clmv!(start, end, |a| {
242                                        if a.count() == 0 {
243                                            let t = a.elapsed_restart(1.secs());
244
245                                            end.set(Transition::new(0.turn(), 1.turn()).sample(t.fct()));
246
247                                            if let Some(t) = t.seg(80.pct()..) {
248                                                start.set(Transition::new(0.turn(), 0.8.turn()).sample(t.fct()));
249                                            }
250                                        } else {
251                                            let t = a.elapsed_restart(500.ms());
252                                            let v = Transition::new(0.turn(), 1.turn()).sample(t.fct());
253                                            start.set(v - 0.2.turn());
254                                            end.set(v);
255                                        }
256                                    }));
257                                }
258                            } else {
259                                if !ind_handle.is_stopped() {
260                                    ind_handle = AnimationHandle::dummy();
261                                    start.ease(0.rad(), 200.ms(), easing::linear).perm();
262                                }
263                                end.ease(args.fct().0.turn(), 200.ms(), |t| easing::ease_out(easing::quad, t))
264                                    .perm();
265                            }
266                        })
267                    };
268                }
269            };
270            container::child_spacing = 6;
271            container::child = {
272                let txt = PROGRESS_VAR.flat_map(|p| p.msg());
273                zng_wgt_text::Text! {
274                    zng_wgt::visibility = txt.map(|p| Visibility::from(!p.is_empty()));
275                    txt;
276                }
277            };
278        }
279    }
280}
281
282/// Circular progress indicator style without message text.
283#[widget($crate::SimpleCircularStyle)]
284pub struct SimpleCircularStyle(Style);
285impl_named_style_fn!(simple_circular, SimpleCircularStyle);
286impl SimpleCircularStyle {
287    fn widget_intrinsic(&mut self) {
288        widget_set! {
289            self;
290            container::child = unset!;
291        }
292    }
293}
294
295/// Render an arc line or circle.
296///
297/// The arc ellipses is defined by the fill area available for the node. If `start` and `end` are equal does not render, if
298/// `end` overlaps one turn renders a full circle. 0ยบ is at the top.
299pub fn arc_shape(
300    thickness: impl IntoVar<Length>,
301    color: impl IntoVar<Rgba>,
302    start: impl IntoVar<AngleRadian>,
303    end: impl IntoVar<AngleRadian>,
304) -> UiNode {
305    // To leverage GPU rendering we render the arc using two halves of a circle drawn
306    // with border+corner-radius and clips
307    let thickness = thickness.into_var();
308    let color = color.into_var();
309    let start = start.into_var();
310    let end = end.into_var();
311
312    let mut render_thickness = Px(0);
313    let mut render_size = PxSize::zero();
314    let rotate_start_key = FrameValueKey::new_unique();
315    let rotate_half0_key = FrameValueKey::new_unique();
316    let rotate_half1_key = FrameValueKey::new_unique();
317
318    // [start, half0, half1]
319    fn rotates(area: PxSize, start: AngleRadian, end: AngleRadian) -> [PxTransform; 3] {
320        let center = area.to_vector().cast::<f32>() * 0.5.fct();
321        let rotate = |rad: f32| {
322            PxTransform::translation(-center.x, -center.y)
323                .then(&Transform::new_rotate(rad.rad()).layout())
324                .then_translate(center)
325        };
326
327        // first half is round border top-right, clipped to left side of area
328        // second is bottom-left, clipped to right side of area
329        let trick = 45.0_f32.to_radians();
330
331        let length = (end.0 - start.0).max(0.0).min(360.0_f32.to_radians());
332        let half_rad = 180.0_f32.to_radians();
333        let rotate_half = |length: f32, stitch: f32| {
334            let t = rotate(trick - half_rad + length);
335
336            // Webrender leaves a faint subpixel line at the edge of clips, translate to hide error
337            if length < 0.001 || length > 180.0_f32.to_radians() - 0.001 {
338                t.then_translate(euclid::vec2(stitch, 0.0))
339            } else {
340                t
341            }
342        };
343
344        let stitch = if start.0.abs() > 0.001 { 1.5 } else { 1.0 };
345        [
346            rotate(start.0),
347            rotate_half(length.min(half_rad), -stitch),
348            rotate_half((length - half_rad).max(0.0), stitch),
349        ]
350    }
351
352    match_node_leaf(move |op| match op {
353        UiNodeOp::Init => {
354            WIDGET
355                .sub_var_layout(&thickness)
356                .sub_var_render(&color)
357                .sub_var_render_update(&start)
358                .sub_var_render_update(&end);
359        }
360        UiNodeOp::Layout { final_size, .. } => {
361            *final_size = LAYOUT.constraints().fill_size();
362
363            // Snap center point, without this can render a faint subpixel line, even with the correction implemented by `rotate_half`.
364            let mut s = *final_size;
365            s.width.0 = ((final_size.width.0 as f32 / 2.0).floor() * 2.0) as _;
366
367            if render_size != s {
368                render_size = s;
369                WIDGET.render();
370            }
371            let t = thickness.layout_x();
372            if render_thickness != t {
373                render_thickness = t;
374                WIDGET.render();
375            }
376        }
377        UiNodeOp::Render { frame } => {
378            let [start_t, half0_t, half1_t] = rotates(render_size, start.get(), end.get());
379            let is_animating = start.is_animating() || end.is_animating();
380
381            frame.push_reference_frame(
382                rotate_start_key.into(),
383                rotate_start_key.bind(start_t, is_animating),
384                false,
385                true,
386                |frame| {
387                    let half = PxPoint::new(render_size.width / Px(2), Px(0));
388                    let color = BorderSide::from(color.get());
389
390                    frame.push_clip_rect(PxRect::new(half, render_size), false, true, |frame| {
391                        frame.push_reference_frame(
392                            rotate_half0_key.into(),
393                            rotate_half0_key.bind(half0_t, is_animating),
394                            false,
395                            true,
396                            |frame| {
397                                let mut b = BorderSides::hidden();
398                                b.top = color;
399                                b.right = b.top;
400                                frame.push_border(
401                                    PxRect::from(render_size),
402                                    PxSideOffsets::new_all_same(render_thickness),
403                                    b,
404                                    PxCornerRadius::new_all(render_size),
405                                );
406                            },
407                        );
408                    });
409
410                    frame.push_clip_rect(PxRect::new(-half, render_size), false, true, |frame| {
411                        frame.push_reference_frame(
412                            rotate_half1_key.into(),
413                            rotate_half1_key.bind(half1_t, is_animating),
414                            false,
415                            true,
416                            |frame| {
417                                let mut b = BorderSides::hidden();
418                                b.bottom = color;
419                                b.left = b.bottom;
420                                frame.push_border(
421                                    PxRect::from(render_size),
422                                    PxSideOffsets::new_all_same(render_thickness),
423                                    b,
424                                    PxCornerRadius::new_all(render_size),
425                                );
426                            },
427                        );
428                    });
429                },
430            );
431        }
432        UiNodeOp::RenderUpdate { update } => {
433            let [start_t, half0_t, half1_t] = rotates(render_size, start.get(), end.get());
434            let is_animating = start.is_animating() || end.is_animating();
435
436            update.update_transform(rotate_start_key.update(start_t, is_animating), true);
437            update.update_transform(rotate_half0_key.update(half0_t, is_animating), true);
438            update.update_transform(rotate_half1_key.update(half1_t, is_animating), true);
439        }
440        _ => {}
441    })
442}