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#![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#[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 pub static PROGRESS_VAR: Progress = Progress::indeterminate();
45}
46
47#[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#[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#[property(EVENT, widget_impl(ProgressView))]
75pub fn on_progress(child: impl IntoUiNode, handler: Handler<Progress>) -> UiNode {
76 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#[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#[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#[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 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#[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#[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#[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
290pub 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 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 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 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 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 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}