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 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 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#[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#[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#[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
295pub 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 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 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 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 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 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}