Skip to main content

ferroui_controls/pull_to_refresh/
refresh_visualizer.rs

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/// Shows the progress of a pull-to-refresh interaction and of the refresh
28/// it starts.
29#[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    /// The subscription to the loaded event of the content.
42    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        /// Defines the `PullDirection` property.
153        pub(crate) fn pull_direction_property() -> StyledProperty<PullDirection> {
154            FerroProperty::register::<RefreshVisualizer, _>("PullDirection", PullDirection::TopToBottom)
155        }
156    );
157
158    ferro_property!(
159        /// Defines the `RefreshVisualizerState` property.
160        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        /// Defines the `Orientation` property.
172        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        /// Defines the `RefreshInfoProvider` property.
184        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        /// Defines the `RefreshRequested` event.
198        pub fn refresh_requested_event() -> RoutedEvent<RefreshRequestedEventArgs> {
199            RoutedEvent::register::<RefreshVisualizer, _>("RefreshRequested", RoutingStrategies::BUBBLE)
200        }
201    );
202
203    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
204    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    /// Gets a value that indicates the refresh state of the visualizer.
240    ///
241    /// Protected upstream; the property itself is public.
242    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    /// Gets or sets a value that indicates the orientation of the
252    /// visualizer.
253    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    /// Occurs when an update of the content has been initiated.
282    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                // Upstream computes a translation ratio here that it never
408                // uses.
409
410                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            // As long as the indicator container isn't visible, the initial
424            // offset is the current offset.
425            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    /// Initiates an update of the content.
448    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                    // We don't want to interrupt a currently executing
605                    // refresh.
606                }
607                _ => self.set_refresh_visualizer_state(RefreshVisualizerState::Idle),
608            }
609        }
610    }
611}