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 = {
196 let txt = PROGRESS_VAR.flat_map(|p| p.msg());
197 zng_wgt_text::Text! {
198 zng_wgt::visibility = txt.map(|p| Visibility::from(!p.is_empty()));
199 txt;
200 zng_wgt::align = Align::CENTER;
201 }
202 };
203 }
204 }
205}
206
207#[widget($crate::SimpleBarStyle)]
209pub struct SimpleBarStyle(DefaultStyle);
210impl_named_style_fn!(simple_bar, SimpleBarStyle);
211impl SimpleBarStyle {
212 fn widget_intrinsic(&mut self) {
213 widget_set! {
214 self;
215 named_style_fn = SIMPLE_BAR_STYLE_FN_VAR;
216 container::child_out_bottom = unset!;
217 }
218 }
219}
220
221#[widget($crate::CircularStyle)]
223pub struct CircularStyle(Style);
224impl_named_style_fn!(circular, CircularStyle);
225impl CircularStyle {
226 fn widget_intrinsic(&mut self) {
227 widget_set! {
228 self;
229 replace = true;
230 named_style_fn = CIRCULAR_STYLE_FN_VAR;
231 container::child_start = {
232 let start = var(0.rad());
233 let end = var(0.rad());
234 zng_wgt::Wgt! {
235 zng_wgt_size_offset::size = 1.4.em();
236 zng_wgt_fill::background = arc_shape(0.2.em(), ACCENT_COLOR_VAR.rgba(), start.clone(), end.clone());
237 on_progress = {
238 let mut ind_handle = AnimationHandle::dummy();
239 hn!(|args| {
240 if args.is_indeterminate() {
241 if ind_handle.is_stopped() {
242 ind_handle = VARS.animate(clmv!(start, end, |a| {
243 if a.count() == 0 {
244 let t = a.elapsed_restart(1.secs());
245
246 end.set(Transition::new(0.turn(), 1.turn()).sample(t.fct()));
247
248 if let Some(t) = t.seg(80.pct()..) {
249 start.set(Transition::new(0.turn(), 0.8.turn()).sample(t.fct()));
250 }
251 } else {
252 let t = a.elapsed_restart(500.ms());
253 let v = Transition::new(0.turn(), 1.turn()).sample(t.fct());
254 start.set(v - 0.2.turn());
255 end.set(v);
256 }
257 }));
258 }
259 } else {
260 if !ind_handle.is_stopped() {
261 ind_handle = AnimationHandle::dummy();
262 start.ease(0.rad(), 200.ms(), easing::linear).perm();
263 }
264 end.ease(args.fct().0.turn(), 200.ms(), |t| easing::ease_out(easing::quad, t))
265 .perm();
266 }
267 })
268 };
269 }
270 };
271 container::child_spacing = 6;
272 container::child = {
273 let txt = PROGRESS_VAR.flat_map(|p| p.msg());
274 zng_wgt_text::Text! {
275 zng_wgt::visibility = txt.map(|p| Visibility::from(!p.is_empty()));
276 txt;
277 }
278 };
279 }
280 }
281}
282
283#[widget($crate::SimpleCircularStyle)]
285pub struct SimpleCircularStyle(Style);
286impl_named_style_fn!(simple_circular, SimpleCircularStyle);
287impl SimpleCircularStyle {
288 fn widget_intrinsic(&mut self) {
289 widget_set! {
290 self;
291 container::child = unset!;
292 }
293 }
294}
295
296pub fn arc_shape(
301 thickness: impl IntoVar<Length>,
302 color: impl IntoVar<Rgba>,
303 start: impl IntoVar<AngleRadian>,
304 end: impl IntoVar<AngleRadian>,
305) -> UiNode {
306 let thickness = thickness.into_var();
309 let color = color.into_var();
310 let start = start.into_var();
311 let end = end.into_var();
312
313 let mut render_thickness = Px(0);
314 let mut render_size = PxSize::zero();
315 let rotate_start_key = FrameValueKey::new_unique();
316 let rotate_half0_key = FrameValueKey::new_unique();
317 let rotate_half1_key = FrameValueKey::new_unique();
318
319 fn rotates(area: PxSize, start: AngleRadian, end: AngleRadian) -> [PxTransform; 3] {
321 let center = area.to_vector().cast::<f32>() * 0.5.fct();
322 let rotate = |rad: f32| {
323 PxTransform::translation(-center.x, -center.y)
324 .then(&Transform::new_rotate(rad.rad()).layout())
325 .then_translate(center)
326 };
327
328 let trick = 45.0_f32.to_radians();
331
332 let length = (end.0 - start.0).max(0.0).min(360.0_f32.to_radians());
333 let half_rad = 180.0_f32.to_radians();
334 let rotate_half = |length: f32, stitch: f32| {
335 let t = rotate(trick - half_rad + length);
336
337 if length < 0.001 || length > 180.0_f32.to_radians() - 0.001 {
339 t.then_translate(euclid::vec2(stitch, 0.0))
340 } else {
341 t
342 }
343 };
344
345 let stitch = if start.0.abs() > 0.001 { 1.5 } else { 1.0 };
346 [
347 rotate(start.0),
348 rotate_half(length.min(half_rad), -stitch),
349 rotate_half((length - half_rad).max(0.0), stitch),
350 ]
351 }
352
353 match_node_leaf(move |op| match op {
354 UiNodeOp::Init => {
355 WIDGET
356 .sub_var_layout(&thickness)
357 .sub_var_render(&color)
358 .sub_var_render_update(&start)
359 .sub_var_render_update(&end);
360 }
361 UiNodeOp::Layout { final_size, .. } => {
362 *final_size = LAYOUT.constraints().fill_size();
363
364 let mut s = *final_size;
366 s.width.0 = ((final_size.width.0 as f32 / 2.0).floor() * 2.0) as _;
367
368 if render_size != s {
369 render_size = s;
370 WIDGET.render();
371 }
372 let t = thickness.layout_x();
373 if render_thickness != t {
374 render_thickness = t;
375 WIDGET.render();
376 }
377 }
378 UiNodeOp::Render { frame } => {
379 let [start_t, half0_t, half1_t] = rotates(render_size, start.get(), end.get());
380 let is_animating = start.is_animating() || end.is_animating();
381
382 frame.push_reference_frame(
383 rotate_start_key.into(),
384 rotate_start_key.bind(start_t, is_animating),
385 false,
386 true,
387 |frame| {
388 let half = PxPoint::new(render_size.width / Px(2), Px(0));
389 let color = BorderSide::from(color.get());
390
391 frame.push_clip_rect(PxRect::new(half, render_size), false, true, |frame| {
392 frame.push_reference_frame(
393 rotate_half0_key.into(),
394 rotate_half0_key.bind(half0_t, is_animating),
395 false,
396 true,
397 |frame| {
398 let mut b = BorderSides::hidden();
399 b.top = color;
400 b.right = b.top;
401 frame.push_border(
402 PxRect::from(render_size),
403 PxSideOffsets::new_all_same(render_thickness),
404 b,
405 PxCornerRadius::new_all(render_size),
406 );
407 },
408 );
409 });
410
411 frame.push_clip_rect(PxRect::new(-half, render_size), false, true, |frame| {
412 frame.push_reference_frame(
413 rotate_half1_key.into(),
414 rotate_half1_key.bind(half1_t, is_animating),
415 false,
416 true,
417 |frame| {
418 let mut b = BorderSides::hidden();
419 b.bottom = color;
420 b.left = b.bottom;
421 frame.push_border(
422 PxRect::from(render_size),
423 PxSideOffsets::new_all_same(render_thickness),
424 b,
425 PxCornerRadius::new_all(render_size),
426 );
427 },
428 );
429 });
430 },
431 );
432 }
433 UiNodeOp::RenderUpdate { update } => {
434 let [start_t, half0_t, half1_t] = rotates(render_size, start.get(), end.get());
435 let is_animating = start.is_animating() || end.is_animating();
436
437 update.update_transform(rotate_start_key.update(start_t, is_animating), true);
438 update.update_transform(rotate_half0_key.update(half0_t, is_animating), true);
439 update.update_transform(rotate_half1_key.update(half1_t, is_animating), true);
440 }
441 _ => {}
442 })
443}