1use super::{RefreshInfoProvider, RefreshRequestedEventArgs, RefreshVisualizerOrientation, RefreshVisualizerState};
2use crate::primitives::{TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt};
3use crate::{ContentControl, ContentControlImpl, Control, ControlImpl, Grid};
4use ferroui_base::input::{InputElementImpl, PullDirection};
5use ferroui_base::interactivity::{Interactive, InteractiveImpl, RoutedEvent, RoutedEventHandlerToken, RoutingStrategies};
6use ferroui_base::layout::{HorizontalAlignment, LayoutableImpl, VerticalAlignment};
7use ferroui_base::media::TranslateTransform;
8use ferroui_base::reactive::{IDisposable, ObservableExt};
9use ferroui_base::animation::easings::{Easing, LinearEasing};
10use ferroui_base::numerics::Vector3;
11use ferroui_base::rendering::composition::animations::{AnimationIterationBehavior, AnimationStopBehavior};
12use ferroui_base::rendering::composition::{
13 CompositionVisual, ElementComposition, ICompositionObjectAnimations, ScalarKeyFrameAnimation,
14};
15use ferroui_base::{
16 ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, BoxedValue, DirectProperty,
17 FerroObject, FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref,
18 StyledElementImpl, StyledProperty, Vector3D, Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
19};
20use std::cell::{Cell, RefCell};
21use std::f64::consts::PI;
22use std::rc::Rc;
23use std::time::Duration;
24
25const MINIMUM_INDICATOR_OPACITY: f32 = 0.4;
26
27#[repr(C)]
30pub struct RefreshVisualizer {
31 base: ContentControl,
32 executing_ratio: Cell<f64>,
33 initial_visual_offset: Cell<Vector3D>,
34 refresh_visualizer_state: Cell<RefreshVisualizerState>,
35 refresh_info_provider: RefCell<Option<Ref<RefreshInfoProvider>>>,
36 is_interacting_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
37 interaction_ratio_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
38 is_interacting_for_refresh: Cell<bool>,
39 root: RefCell<Option<Ref<Grid>>>,
40 content: RefCell<Option<Ref<Control>>>,
41 content_loaded: Cell<Option<RoutedEventHandlerToken>>,
43 orientation: Cell<RefreshVisualizerOrientation>,
44 starting_rotation_angle: Cell<f32>,
45 interaction_ratio: Cell<f64>,
46 played: Cell<bool>,
47 rotate_animation: RefCell<Option<Rc<ScalarKeyFrameAnimation>>>,
48}
49
50ferro_class!(RefreshVisualizer: ContentControl);
51ferroui_base::ferro_class_info!(RefreshVisualizer { new: RefreshVisualizer::new });
52ferro_impl_classes!(
53 RefreshVisualizer: StyledElementImpl,
54 LayoutableImpl,
55 InteractiveImpl,
56 InputElementImpl,
57 ControlImpl,
58 ContentControlImpl
59);
60
61impl TemplatedControlImpl for RefreshVisualizer {
62 fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
63 Self::parent_on_apply_template(this, e);
64
65 this.set_clip_to_bounds(false);
66
67 let root = e.name_scope().find_as::<Grid>("PART_Root");
68 drop(this.root.replace(root.clone()));
69
70 if let Some(root) = root {
71 this.on_orientation_changed();
72
73 let content = this.content.borrow().clone();
74 if let Some(content) = content {
75 root.children().insert(0, content.clone());
76 content.set_vertical_alignment(VerticalAlignment::Center);
77 content.set_horizontal_alignment(HorizontalAlignment::Center);
78
79 this.update_content();
80 }
81 }
82 }
83}
84
85impl VisualImpl for RefreshVisualizer {
86 fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
87 Self::parent_on_attached_to_visual_tree(this, e);
88
89 this.update_content();
90 }
91}
92
93impl FerroObjectImpl for RefreshVisualizer {
94 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
95 Self::parent_on_property_changed(this, change);
96
97 if change.property() == Self::refresh_info_provider_property().as_property() {
98 this.on_refresh_info_provider_changed();
99 } else if change.property() == ContentControl::content_property().as_property() {
100 let (old_value, new_value) = change.get_old_and_new_value::<Option<BoxedValue>>();
101
102 if let Some(c) = old_value.as_ref().and_then(Control::from_boxed) {
103 if let Some(token) = this.content_loaded.take() {
104 c.remove_handler(Control::loaded_event(), token);
105 }
106 let root = this.root.borrow().clone();
107 if let Some(root) = root {
108 root.children().remove(c);
109 }
110 }
111
112 let content = new_value.as_ref().and_then(Control::from_boxed);
113 drop(this.content.replace(content.clone()));
114
115 if let Some(content) = &content {
116 let weak = this.to_ref().downgrade();
117 this.content_loaded.set(Some(content.loaded(move |_, _| {
118 if let Some(this) = weak.upgrade() {
119 this.on_content_loaded();
120 }
121 })));
122 }
123
124 let root = this.root.borrow().clone();
125 if let (Some(root), Some(content)) = (root, content) {
126 root.children().insert(0, content.clone());
127 content.set_vertical_alignment(VerticalAlignment::Center);
128 content.set_horizontal_alignment(HorizontalAlignment::Center);
129
130 this.update_content();
131 }
132 } else if change.property() == Self::orientation_property().as_property() {
133 this.on_orientation_changed();
134
135 this.update_content();
136 } else if change.property() == Visual::bounds_property().as_property() {
137 this.on_bounds_changed();
138
139 this.update_content();
140 } else if change.property() == Self::pull_direction_property().as_property() {
141 this.on_orientation_changed();
142
143 this.on_bounds_changed();
144
145 this.update_content();
146 }
147 }
148}
149
150ferroui_base::ferro_properties! { impl RefreshVisualizer {
151 ferro_property!(
152 pub(crate) fn pull_direction_property() -> StyledProperty<PullDirection> {
154 FerroProperty::register::<RefreshVisualizer, _>("PullDirection", PullDirection::TopToBottom)
155 }
156 );
157
158 ferro_property!(
159 pub fn refresh_visualizer_state_property() -> DirectProperty<RefreshVisualizer, RefreshVisualizerState> {
161 FerroProperty::register_direct::<RefreshVisualizer, _>(
162 "RefreshVisualizerState",
163 |s| s.refresh_visualizer_state(),
164 None,
165 RefreshVisualizerState::Idle,
166 )
167 }
168 );
169
170 ferro_property!(
171 pub fn orientation_property() -> DirectProperty<RefreshVisualizer, RefreshVisualizerOrientation> {
173 FerroProperty::register_direct::<RefreshVisualizer, _>(
174 "Orientation",
175 |s| s.orientation(),
176 Some(|s, o| s.set_orientation(o)),
177 RefreshVisualizerOrientation::Auto,
178 )
179 }
180 );
181
182 ferro_property!(
183 pub(crate) fn refresh_info_provider_property() -> DirectProperty<RefreshVisualizer, Option<Ref<RefreshInfoProvider>>> {
185 FerroProperty::register_direct::<RefreshVisualizer, _>(
186 "RefreshInfoProvider",
187 |s| s.refresh_info_provider(),
188 Some(|s, o| s.set_refresh_info_provider(o)),
189 None,
190 )
191 }
192 );
193} }
194
195impl RefreshVisualizer {
196 ferro_routed_event!(
197 pub fn refresh_requested_event() -> RoutedEvent<RefreshRequestedEventArgs> {
199 RoutedEvent::register::<RefreshVisualizer, _>("RefreshRequested", RoutingStrategies::BUBBLE)
200 }
201 );
202
203 pub fn construct() -> Self {
205 Self {
206 base: ContentControl::construct(),
207 executing_ratio: Cell::new(0.8),
208 initial_visual_offset: Cell::new(Vector3D::default()),
209 refresh_visualizer_state: Cell::new(RefreshVisualizerState::Idle),
210 refresh_info_provider: RefCell::new(None),
211 is_interacting_subscription: RefCell::new(None),
212 interaction_ratio_subscription: RefCell::new(None),
213 is_interacting_for_refresh: Cell::new(false),
214 root: RefCell::new(None),
215 content: RefCell::new(None),
216 content_loaded: Cell::new(None),
217 orientation: Cell::new(RefreshVisualizerOrientation::Auto),
218 starting_rotation_angle: Cell::new(0.0),
219 interaction_ratio: Cell::new(0.0),
220 played: Cell::new(false),
221 rotate_animation: RefCell::new(None),
222 }
223 }
224
225 pub fn new() -> Ref<Self> {
226 instantiate(Self::construct())
227 }
228
229 fn is_pull_direction_vertical(&self) -> bool {
230 let pull_direction = self.pull_direction();
231 pull_direction == PullDirection::TopToBottom || pull_direction == PullDirection::BottomToTop
232 }
233
234 fn is_pull_direction_far(&self) -> bool {
235 let pull_direction = self.pull_direction();
236 pull_direction == PullDirection::BottomToTop || pull_direction == PullDirection::RightToLeft
237 }
238
239 pub fn refresh_visualizer_state(&self) -> RefreshVisualizerState {
243 self.refresh_visualizer_state.get()
244 }
245
246 fn set_refresh_visualizer_state(&self, value: RefreshVisualizerState) {
247 self.set_and_raise_cell(Self::refresh_visualizer_state_property(), &self.refresh_visualizer_state, value);
248 self.update_content();
249 }
250
251 pub fn orientation(&self) -> RefreshVisualizerOrientation {
254 self.orientation.get()
255 }
256
257 pub fn set_orientation(&self, value: RefreshVisualizerOrientation) {
258 self.set_and_raise_cell(Self::orientation_property(), &self.orientation, value);
259 }
260
261 pub(crate) fn pull_direction(&self) -> PullDirection {
262 self.get_value(Self::pull_direction_property())
263 }
264
265 pub(crate) fn set_pull_direction(&self, value: PullDirection) {
266 self.set_value(Self::pull_direction_property(), value)
267 }
268
269 pub(crate) fn refresh_info_provider(&self) -> Option<Ref<RefreshInfoProvider>> {
270 self.refresh_info_provider.borrow().clone()
271 }
272
273 pub(crate) fn set_refresh_info_provider(&self, value: Option<Ref<RefreshInfoProvider>>) {
274 let current = self.refresh_info_provider.borrow().clone();
275 if let Some(current) = current {
276 current.set_render_transform(None);
277 }
278 self.set_and_raise(Self::refresh_info_provider_property(), &self.refresh_info_provider, value);
279 }
280
281 pub fn refresh_requested(
283 &self,
284 handler: impl Fn(&Interactive, &RefreshRequestedEventArgs) + 'static,
285 ) -> RoutedEventHandlerToken {
286 self.add_handler(Self::refresh_requested_event(), handler)
287 }
288
289 fn on_content_loaded(&self) {
290 let content = self.content.borrow().clone();
291 let Some(content) = content else { return };
292
293 let Some(composition) = ElementComposition::get_element_visual(&content) else { return };
294
295 let compositor = composition.compositor().clone();
296 composition.set_opacity(0.0);
297
298 let smooth_rotation_animation = compositor.create_scalar_key_frame_animation();
299 smooth_rotation_animation.set_target(Some("RotationAngle".to_owned()));
300 smooth_rotation_animation.insert_expression_key_frame(1.0, "this.FinalValue", Some(Easing::new(LinearEasing::new())));
301 smooth_rotation_animation.set_duration(Duration::from_millis(100));
302
303 let opacity_animation = compositor.create_scalar_key_frame_animation();
304 opacity_animation.set_target(Some("Opacity".to_owned()));
305 opacity_animation.insert_expression_key_frame(1.0, "this.FinalValue", Some(Easing::new(LinearEasing::new())));
306 opacity_animation.set_duration(Duration::from_millis(100));
307
308 let offset_animation = compositor.create_vector3_key_frame_animation();
309 offset_animation.set_target(Some("Offset".to_owned()));
310 offset_animation.insert_expression_key_frame(1.0, "this.FinalValue", Some(Easing::new(LinearEasing::new())));
311 offset_animation.set_duration(Duration::from_millis(150));
312
313 let scale_animation = compositor.create_vector3_key_frame_animation();
314 scale_animation.set_target(Some("Scale".to_owned()));
315 scale_animation.insert_expression_key_frame(1.0, "this.FinalValue", Some(Easing::new(LinearEasing::new())));
316 scale_animation.set_duration(Duration::from_millis(100));
317
318 let animation = compositor.create_implicit_animation_collection();
319 animation.set("RotationAngle", smooth_rotation_animation);
320 animation.set("Offset", offset_animation);
321 animation.set("Scale", scale_animation);
322 animation.set("Opacity", opacity_animation);
323
324 composition.set_implicit_animations(Some(animation));
325
326 self.update_content();
327 }
328
329 fn update_content(&self) {
330 let content = self.content.borrow().clone();
331 let root = self.root.borrow().clone();
332 let (Some(content), Some(root)) = (content, root) else { return };
333
334 let visual = self.refresh_info_provider().and_then(|provider| provider.visual());
335 let content_visual = ElementComposition::get_element_visual(&content);
336 let visualizer_visual = ElementComposition::get_element_visual(self);
337 let (Some(visual), Some(content_visual), Some(visualizer_visual)) = (visual, content_visual, visualizer_visual)
338 else {
339 return;
340 };
341
342 let starting_rotation_angle = self.starting_rotation_angle.get();
343
344 content_visual.set_center_point(Vector3D::new(
345 content.bounds().width / 2.0,
346 content.bounds().height / 2.0,
347 0.0,
348 ));
349 match self.refresh_visualizer_state() {
350 RefreshVisualizerState::Idle => {
351 self.played.set(false);
352 if let Some(rotate_animation) = self.rotate_animation.take() {
353 rotate_animation.set_iteration_behavior(AnimationIterationBehavior::Count);
354 }
355
356 content_visual.set_opacity(MINIMUM_INDICATOR_OPACITY);
357 content_visual.set_rotation_angle(starting_rotation_angle);
358
359 if visualizer_visual.offset().x != 0.0 || visualizer_visual.offset().y != 0.0 {
360 visual.set_offset(self.initial_visual_offset.get());
361 visualizer_visual.set_offset(Vector3D::new(0.0, 0.0, 0.0));
362 }
363
364 content.invalidate_measure();
365 }
366 RefreshVisualizerState::Interacting => {
367 self.played.set(false);
368 content_visual.set_opacity(MINIMUM_INDICATOR_OPACITY);
369 content_visual
370 .set_rotation_angle((starting_rotation_angle as f64 + self.interaction_ratio.get() * 2.0 * PI) as f32);
371
372 self.calculate_and_set_offsets(&root, &visual, &visualizer_visual);
373 }
374 RefreshVisualizerState::Pending => {
375 content_visual.set_opacity(1.0);
376 content_visual.set_rotation_angle(starting_rotation_angle + (2.0 * PI) as f32);
377
378 self.calculate_and_set_offsets(&root, &visual, &visualizer_visual);
379
380 if !self.played.get() {
381 self.played.set(true);
382 let scale_animation = content_visual.compositor().create_vector3_key_frame_animation();
383 scale_animation.set_target(Some("Scale".to_owned()));
384 scale_animation.insert_key_frame(0.5, Vector3::new(1.5, 1.5, 1.0));
385 scale_animation.insert_key_frame(1.0, Vector3::new(1.0, 1.0, 1.0));
386 scale_animation.set_duration(Duration::from_secs_f64(0.3));
387
388 content_visual.start_animation("Scale", &*scale_animation);
389 }
390 }
391 RefreshVisualizerState::Refreshing => {
392 let rotate_animation = content_visual.compositor().create_scalar_key_frame_animation();
393 rotate_animation.set_target(Some("RotationAngle".to_owned()));
394 rotate_animation.insert_key_frame_with_easing(0.0, starting_rotation_angle, Rc::new(LinearEasing::new()));
395 rotate_animation.insert_key_frame_with_easing(
396 1.0,
397 starting_rotation_angle + (2.0 * PI) as f32,
398 Rc::new(LinearEasing::new()),
399 );
400 rotate_animation.set_iteration_behavior(AnimationIterationBehavior::Forever);
401 rotate_animation.set_stop_behavior(AnimationStopBehavior::LeaveCurrentValue);
402 rotate_animation.set_duration(Duration::from_secs_f64(0.5));
403
404 content_visual.start_animation("RotationAngle", &*rotate_animation);
405 *self.rotate_animation.borrow_mut() = Some(rotate_animation);
406 content_visual.set_opacity(1.0);
407 self.calculate_and_set_offsets(&root, &visual, &visualizer_visual);
411 }
412 RefreshVisualizerState::Peeking => {
413 content_visual.set_opacity(1.0);
414 content_visual.set_rotation_angle(starting_rotation_angle);
415 }
416 }
417 }
418
419 fn calculate_and_set_offsets(&self, root: &Grid, visual: &CompositionVisual, indicator_visualizer: &CompositionVisual) {
420 let direction = if self.is_pull_direction_far() { -1.0 } else { 1.0 };
421
422 if self.is_pull_direction_vertical() {
423 if indicator_visualizer.offset().y == 0.0 {
426 self.initial_visual_offset.set(visual.offset());
427 }
428
429 let offset = self.interaction_ratio.get() * direction * root.bounds().height;
430 indicator_visualizer.set_offset(Vector3D { y: offset, ..indicator_visualizer.offset() });
431
432 let initial_visual_offset = self.initial_visual_offset.get();
433 visual.set_offset(Vector3D { y: initial_visual_offset.y + offset, ..initial_visual_offset });
434 } else {
435 if indicator_visualizer.offset().x == 0.0 {
436 self.initial_visual_offset.set(visual.offset());
437 }
438
439 let offset = self.interaction_ratio.get() * direction * root.bounds().width;
440 indicator_visualizer.set_offset(Vector3D { x: offset, ..indicator_visualizer.offset() });
441
442 let initial_visual_offset = self.initial_visual_offset.get();
443 visual.set_offset(Vector3D { x: initial_visual_offset.x + offset, ..initial_visual_offset });
444 }
445 }
446
447 pub fn request_refresh(&self) {
449 if let Some(provider) = self.refresh_info_provider() {
450 provider.set_interaction_ratio(1.0);
451 }
452 self.set_refresh_visualizer_state(RefreshVisualizerState::Refreshing);
453 if let Some(provider) = self.refresh_info_provider() {
454 provider.on_refresh_started();
455 }
456
457 self.raise_refresh_requested();
458 }
459
460 fn refresh_completed(&self) {
461 if let Some(provider) = self.refresh_info_provider() {
462 provider.set_interaction_ratio(0.0);
463 }
464 self.set_refresh_visualizer_state(RefreshVisualizerState::Idle);
465 if let Some(provider) = self.refresh_info_provider() {
466 provider.on_refresh_completed();
467 }
468 }
469
470 fn raise_refresh_requested(&self) {
471 let this = self.to_ref();
472 let refresh_args =
473 RefreshRequestedEventArgs::new(move || this.refresh_completed(), Some(Self::refresh_requested_event()));
474
475 refresh_args.increment_count();
476
477 self.raise_event(&refresh_args);
478
479 refresh_args.decrement_count();
480 }
481
482 fn on_bounds_changed(&self) {
483 let bounds = self.bounds();
484 let transform = match self.pull_direction() {
485 PullDirection::TopToBottom => TranslateTransform::with_offset(0.0, -bounds.height),
486 PullDirection::BottomToTop => TranslateTransform::with_offset(0.0, bounds.height),
487 PullDirection::LeftToRight => TranslateTransform::with_offset(-bounds.width, 0.0),
488 PullDirection::RightToLeft => TranslateTransform::with_offset(bounds.width, 0.0),
489 };
490 self.set_render_transform(Some(transform.into()));
491 }
492
493 fn on_orientation_changed(&self) {
494 let starting_rotation_angle = match self.orientation.get() {
495 RefreshVisualizerOrientation::Auto => match self.pull_direction() {
496 PullDirection::TopToBottom | PullDirection::BottomToTop => 0.0,
497 PullDirection::LeftToRight => (-PI / 2.0) as f32,
498 PullDirection::RightToLeft => (PI / 2.0) as f32,
499 },
500 RefreshVisualizerOrientation::Normal => 0.0,
501 RefreshVisualizerOrientation::Rotate90DegreesCounterclockwise => (PI / 2.0) as f32,
502 RefreshVisualizerOrientation::Rotate270DegreesCounterclockwise => (-PI / 2.0) as f32,
503 };
504 self.starting_rotation_angle.set(starting_rotation_angle);
505 }
506
507 fn on_refresh_info_provider_changed(&self) {
508 let subscription = self.is_interacting_subscription.replace(None);
509 if let Some(subscription) = subscription {
510 subscription.dispose();
511 }
512 let subscription = self.interaction_ratio_subscription.replace(None);
513 if let Some(subscription) = subscription {
514 subscription.dispose();
515 }
516
517 if let Some(provider) = self.refresh_info_provider() {
518 let object: &FerroObject = &provider;
519
520 let weak = self.to_ref().downgrade();
521 let subscription = FerroObjectExtensions::get_observable(
522 object,
523 RefreshInfoProvider::is_interacting_for_refresh_property(),
524 )
525 .subscribe_fn(move |value| {
526 if let Some(this) = weak.upgrade() {
527 this.interacting_for_refresh_observer(value);
528 }
529 });
530 drop(self.is_interacting_subscription.replace(Some(subscription)));
531
532 let weak = self.to_ref().downgrade();
533 let subscription =
534 FerroObjectExtensions::get_observable(object, RefreshInfoProvider::interaction_ratio_property())
535 .subscribe_fn(move |value| {
536 if let Some(this) = weak.upgrade() {
537 this.interaction_ratio_observer(value);
538 }
539 });
540 drop(self.interaction_ratio_subscription.replace(Some(subscription)));
541
542 self.executing_ratio.set(provider.execution_ratio());
543 } else {
544 self.executing_ratio.set(1.0);
545 }
546 }
547
548 fn interaction_ratio_observer(&self, obj: f64) {
549 let was_at_zero = self.interaction_ratio.get() == 0.0;
550 self.interaction_ratio.set(obj);
551
552 let interaction_ratio = obj;
553 let executing_ratio = self.executing_ratio.get();
554
555 if self.is_interacting_for_refresh.get() {
556 match self.refresh_visualizer_state() {
557 RefreshVisualizerState::Idle => {
558 if was_at_zero {
559 if interaction_ratio > executing_ratio {
560 self.set_refresh_visualizer_state(RefreshVisualizerState::Pending);
561 } else if interaction_ratio > 0.0 {
562 self.set_refresh_visualizer_state(RefreshVisualizerState::Interacting);
563 }
564 } else if interaction_ratio > 0.0 {
565 self.set_refresh_visualizer_state(RefreshVisualizerState::Peeking);
566 }
567 }
568 RefreshVisualizerState::Interacting => {
569 if interaction_ratio <= 0.0 {
570 self.set_refresh_visualizer_state(RefreshVisualizerState::Idle);
571 } else if interaction_ratio > executing_ratio {
572 self.set_refresh_visualizer_state(RefreshVisualizerState::Pending);
573 } else {
574 self.update_content();
575 }
576 }
577 RefreshVisualizerState::Pending => {
578 if interaction_ratio <= executing_ratio {
579 self.set_refresh_visualizer_state(RefreshVisualizerState::Interacting);
580 } else if interaction_ratio <= 0.0 {
581 self.set_refresh_visualizer_state(RefreshVisualizerState::Idle);
582 } else {
583 self.update_content();
584 }
585 }
586 RefreshVisualizerState::Peeking | RefreshVisualizerState::Refreshing => {}
587 }
588 } else if self.refresh_visualizer_state() != RefreshVisualizerState::Refreshing {
589 if interaction_ratio > 0.0 {
590 self.set_refresh_visualizer_state(RefreshVisualizerState::Peeking);
591 } else {
592 self.set_refresh_visualizer_state(RefreshVisualizerState::Idle);
593 }
594 }
595 }
596
597 fn interacting_for_refresh_observer(&self, obj: bool) {
598 self.is_interacting_for_refresh.set(obj);
599
600 if !obj {
601 match self.refresh_visualizer_state.get() {
602 RefreshVisualizerState::Pending => self.request_refresh(),
603 RefreshVisualizerState::Refreshing => {
604 }
607 _ => self.set_refresh_visualizer_state(RefreshVisualizerState::Idle),
608 }
609 }
610 }
611}