Skip to main content

ferroui_controls/pull_to_refresh/
refresh_container.rs

1use super::{
2    RefreshInfoProvider, RefreshRequestedEventArgs, RefreshVisualizer, ScrollViewerIRefreshInfoProviderAdapter,
3};
4use crate::primitives::{TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt};
5use crate::{ContentControl, ContentControlImpl, ControlImpl, Grid};
6use ferroui_base::data::BindingPriority;
7use ferroui_base::input::{InputElementImpl, PullDirection};
8use ferroui_base::interactivity::{Interactive, InteractiveImpl, RoutedEvent, RoutedEventHandlerToken, RoutingStrategies};
9use ferroui_base::layout::{HorizontalAlignment, LayoutableImpl, VerticalAlignment};
10use ferroui_base::reactive::{IDisposable, ObservableExt};
11use ferroui_base::{
12    ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, DirectProperty, FerroObject,
13    FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Rect, Ref,
14    Size, StyledElementImpl, StyledProperty, Visual, VisualImpl,
15};
16use std::cell::{Cell, RefCell};
17use std::rc::Rc;
18
19/// Represents a container control that provides a [`RefreshVisualizer`] and
20/// pull-to-refresh functionality for scrollable content.
21#[repr(C)]
22pub struct RefreshContainer {
23    base: ContentControl,
24    has_default_refresh_info_provider_adapter: Cell<bool>,
25    refresh_info_provider_adapter: RefCell<Option<Rc<ScrollViewerIRefreshInfoProviderAdapter>>>,
26    refresh_info_provider: RefCell<Option<Ref<RefreshInfoProvider>>>,
27    visualizer_size_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
28    visualizer_presenter: RefCell<Option<Ref<Grid>>>,
29    refresh_visualizer: RefCell<Option<Ref<RefreshVisualizer>>>,
30    /// The subscription to the refresh requested event of the visualizer.
31    visualizer_refresh_requested: Cell<Option<RoutedEventHandlerToken>>,
32    has_default_refresh_visualizer: Cell<bool>,
33}
34
35ferro_class!(RefreshContainer: ContentControl);
36ferroui_base::ferro_class_info!(RefreshContainer { new: RefreshContainer::new });
37ferro_impl_classes!(
38    RefreshContainer: StyledElementImpl,
39    VisualImpl,
40    LayoutableImpl,
41    InteractiveImpl,
42    InputElementImpl,
43    ControlImpl,
44    ContentControlImpl
45);
46
47/// Balances the deferral count of forwarded refresh requested args when
48/// dropped: the `finally` block of the refresh requested handler.
49struct DecrementCountGuard<'a>(&'a RefreshRequestedEventArgs);
50
51impl Drop for DecrementCountGuard<'_> {
52    fn drop(&mut self) {
53        self.0.decrement_count();
54    }
55}
56
57impl FerroObjectImpl for RefreshContainer {
58    fn constructed(this: &Self) {
59        Self::parent_constructed(this);
60
61        this.has_default_refresh_info_provider_adapter.set(true);
62        let adapter = ScrollViewerIRefreshInfoProviderAdapter::new(this.pull_direction(), this.is_mouse_enabled());
63        drop(this.refresh_info_provider_adapter.replace(Some(adapter.clone())));
64        this.raise_refresh_info_provider_adapter_changed(Some(adapter));
65    }
66
67    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
68        Self::parent_on_property_changed(this, change);
69
70        if change.property() == Self::refresh_info_provider_adapter_property().as_property() {
71            let refresh_visualizer = this.refresh_visualizer.borrow().clone();
72            if let Some(refresh_visualizer) = refresh_visualizer {
73                let refresh_info_provider = this.refresh_info_provider.borrow().clone();
74                if let Some(refresh_info_provider) = refresh_info_provider {
75                    refresh_visualizer.set_refresh_info_provider(Some(refresh_info_provider));
76                } else if let Some(adapter) = this.refresh_info_provider_adapter() {
77                    let refresh_info_provider =
78                        adapter.adapt_from_tree(this, Some(refresh_visualizer.bounds().size()));
79                    drop(this.refresh_info_provider.replace(refresh_info_provider.clone()));
80
81                    if let Some(refresh_info_provider) = refresh_info_provider {
82                        refresh_visualizer.set_refresh_info_provider(Some(refresh_info_provider));
83                        if let Some(adapter) = this.refresh_info_provider_adapter() {
84                            adapter.set_animations(&refresh_visualizer);
85                        }
86                    }
87                }
88            }
89        } else if change.property() == Self::visualizer_property().as_property() {
90            let refresh_visualizer = this.refresh_visualizer.borrow().clone();
91
92            let visualizer_presenter = this.visualizer_presenter.borrow().clone();
93            if let Some(visualizer_presenter) = visualizer_presenter {
94                visualizer_presenter.children().clear();
95                if let Some(refresh_visualizer) = &refresh_visualizer {
96                    visualizer_presenter.children().add(refresh_visualizer.clone());
97                }
98            }
99
100            if let Some(refresh_visualizer) = refresh_visualizer {
101                let weak = this.to_ref().downgrade();
102                this.visualizer_refresh_requested.set(Some(refresh_visualizer.refresh_requested(move |_, e| {
103                    if let Some(this) = weak.upgrade() {
104                        this.visualizer_refresh_requested(e);
105                    }
106                })));
107
108                let weak = this.to_ref().downgrade();
109                let object: &FerroObject = &refresh_visualizer;
110                let subscription =
111                    FerroObjectExtensions::get_observable(object, Visual::bounds_property())
112                        .subscribe_fn(move |bounds| {
113                            if let Some(this) = weak.upgrade() {
114                                this.on_visualizer_size_changed(bounds);
115                            }
116                        });
117                drop(this.visualizer_size_subscription.replace(Some(subscription)));
118            }
119        } else if change.property() == Self::pull_direction_property().as_property() {
120            this.on_pull_direction_changed();
121        } else if change.property() == Self::is_mouse_enabled_property().as_property() {
122            this.on_is_mouse_enabled_changed();
123        }
124    }
125}
126
127impl TemplatedControlImpl for RefreshContainer {
128    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
129        Self::parent_on_apply_template(this, e);
130
131        let visualizer_presenter = e.name_scope().find_as::<Grid>("PART_RefreshVisualizerPresenter");
132        drop(this.visualizer_presenter.replace(visualizer_presenter));
133
134        let refresh_visualizer = this.refresh_visualizer.borrow().clone();
135        match refresh_visualizer {
136            None => {
137                this.has_default_refresh_visualizer.set(true);
138                this.set_visualizer(Some(RefreshVisualizer::new()));
139            }
140            Some(refresh_visualizer) => {
141                this.has_default_refresh_visualizer.set(false);
142                this.raise_property_changed(
143                    Self::visualizer_property().as_property(),
144                    Some(&None::<Ref<RefreshVisualizer>>),
145                    &Some(refresh_visualizer),
146                    BindingPriority::LocalValue,
147                    true,
148                );
149            }
150        }
151
152        this.on_pull_direction_changed();
153    }
154}
155
156ferroui_base::ferro_properties! { impl RefreshContainer {
157    ferro_property!(
158        pub(crate) fn refresh_info_provider_adapter_property()
159            -> DirectProperty<RefreshContainer, Option<Rc<ScrollViewerIRefreshInfoProviderAdapter>>> {
160            FerroProperty::register_direct::<RefreshContainer, _>(
161                "RefreshInfoProviderAdapter",
162                |s| s.refresh_info_provider_adapter(),
163                Some(|s, o| s.set_refresh_info_provider_adapter(o)),
164                None,
165            )
166        }
167    );
168
169    ferro_property!(
170        /// Defines the `Visualizer` property.
171        pub fn visualizer_property() -> DirectProperty<RefreshContainer, Option<Ref<RefreshVisualizer>>> {
172            FerroProperty::register_direct::<RefreshContainer, _>(
173                "Visualizer",
174                |s| s.visualizer(),
175                Some(|s, o| s.set_visualizer(o)),
176                None,
177            )
178        }
179    );
180
181    ferro_property!(
182        /// Defines the `PullDirection` property.
183        pub fn pull_direction_property() -> StyledProperty<PullDirection> {
184            FerroProperty::register::<RefreshContainer, _>("PullDirection", PullDirection::TopToBottom)
185        }
186    );
187
188    ferro_property!(
189        /// Defines the `IsMouseEnabled` property.
190        ///
191        /// Allows to enable the pull to refresh gesture for devices using a
192        /// mouse. Disabled by default.
193        pub fn is_mouse_enabled_property() -> StyledProperty<bool> {
194            FerroProperty::register::<RefreshContainer, _>("IsMouseEnabled", false)
195        }
196    );
197} }
198
199impl RefreshContainer {
200    pub(crate) const DEFAULT_PULL_DIMENSION_SIZE: i32 = 100;
201
202    ferro_routed_event!(
203        /// Defines the `RefreshRequested` event.
204        pub fn refresh_requested_event() -> RoutedEvent<RefreshRequestedEventArgs> {
205            RoutedEvent::register::<RefreshContainer, _>("RefreshRequested", RoutingStrategies::BUBBLE)
206        }
207    );
208
209    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
210    pub fn construct() -> Self {
211        Self {
212            base: ContentControl::construct(),
213            has_default_refresh_info_provider_adapter: Cell::new(false),
214            refresh_info_provider_adapter: RefCell::new(None),
215            refresh_info_provider: RefCell::new(None),
216            visualizer_size_subscription: RefCell::new(None),
217            visualizer_presenter: RefCell::new(None),
218            refresh_visualizer: RefCell::new(None),
219            visualizer_refresh_requested: Cell::new(None),
220            has_default_refresh_visualizer: Cell::new(false),
221        }
222    }
223
224    pub fn new() -> Ref<Self> {
225        instantiate(Self::construct())
226    }
227
228    pub(crate) fn refresh_info_provider_adapter(&self) -> Option<Rc<ScrollViewerIRefreshInfoProviderAdapter>> {
229        self.refresh_info_provider_adapter.borrow().clone()
230    }
231
232    pub(crate) fn set_refresh_info_provider_adapter(&self, value: Option<Rc<ScrollViewerIRefreshInfoProviderAdapter>>) {
233        self.has_default_refresh_info_provider_adapter.set(false);
234        self.set_and_raise(Self::refresh_info_provider_adapter_property(), &self.refresh_info_provider_adapter, value);
235    }
236
237    /// Gets or sets a value that indicates whether the pull-to-refresh
238    /// gesture is enabled for desktop devices. Allows to enable the pull to
239    /// refresh gesture for devices using a mouse. Disabled by default.
240    pub fn is_mouse_enabled(&self) -> bool {
241        self.get_value(Self::is_mouse_enabled_property())
242    }
243
244    pub fn set_is_mouse_enabled(&self, value: bool) {
245        self.set_value(Self::is_mouse_enabled_property(), value)
246    }
247
248    /// Gets or sets the [`RefreshVisualizer`] for this container.
249    pub fn visualizer(&self) -> Option<Ref<RefreshVisualizer>> {
250        self.refresh_visualizer.borrow().clone()
251    }
252
253    pub fn set_visualizer(&self, value: Option<Ref<RefreshVisualizer>>) {
254        let refresh_visualizer = self.refresh_visualizer.borrow().clone();
255        if let Some(refresh_visualizer) = refresh_visualizer {
256            let subscription = self.visualizer_size_subscription.borrow().clone();
257            if let Some(subscription) = subscription {
258                subscription.dispose();
259            }
260            if let Some(token) = self.visualizer_refresh_requested.take() {
261                refresh_visualizer.remove_handler(RefreshVisualizer::refresh_requested_event(), token);
262            }
263        }
264
265        self.set_and_raise(Self::visualizer_property(), &self.refresh_visualizer, value);
266    }
267
268    /// Gets or sets a value that specifies the direction to pull to
269    /// initiate a refresh.
270    pub fn pull_direction(&self) -> PullDirection {
271        self.get_value(Self::pull_direction_property())
272    }
273
274    pub fn set_pull_direction(&self, value: PullDirection) {
275        self.set_value(Self::pull_direction_property(), value)
276    }
277
278    /// Occurs when an update of the content has been initiated.
279    pub fn refresh_requested(
280        &self,
281        handler: impl Fn(&Interactive, &RefreshRequestedEventArgs) + 'static,
282    ) -> RoutedEventHandlerToken {
283        self.add_handler(Self::refresh_requested_event(), handler)
284    }
285
286    /// Notifies a change of the adapter property from nothing to `adapter`,
287    /// whether or not the adapter itself changed.
288    fn raise_refresh_info_provider_adapter_changed(&self, adapter: Option<Rc<ScrollViewerIRefreshInfoProviderAdapter>>) {
289        self.raise_property_changed(
290            Self::refresh_info_provider_adapter_property().as_property(),
291            Some(&None::<Rc<ScrollViewerIRefreshInfoProviderAdapter>>),
292            &adapter,
293            BindingPriority::LocalValue,
294            true,
295        );
296    }
297
298    fn on_visualizer_size_changed(&self, obj: Rect) {
299        if self.has_default_refresh_info_provider_adapter.get() {
300            if let Some(adapter) = self.refresh_info_provider_adapter() {
301                adapter.update_visualizer_size(Some(obj.size()));
302                self.raise_refresh_info_provider_adapter_changed(Some(adapter));
303            }
304        }
305    }
306
307    fn visualizer_refresh_requested(&self, e: &RefreshRequestedEventArgs) {
308        // Guarantee the inner deferral is balanced even if a downstream
309        // handler of the refresh requested event panics synchronously from
310        // `raise_event`. Without this, a synchronously failing consumer
311        // leaves the deferral count of the visualizer above zero forever, so
312        // the refresh never completes and the spinner stays stuck in
313        // Refreshing.
314        let ev = RefreshRequestedEventArgs::with_deferral(e.get_deferral(), Some(Self::refresh_requested_event()));
315        let _guard = DecrementCountGuard(&ev);
316        self.raise_event(&ev);
317    }
318
319    fn on_pull_direction_changed(&self) {
320        let visualizer_presenter = self.visualizer_presenter.borrow().clone();
321        let refresh_visualizer = self.refresh_visualizer.borrow().clone();
322        let (Some(visualizer_presenter), Some(refresh_visualizer)) = (visualizer_presenter, refresh_visualizer) else {
323            return;
324        };
325
326        let has_default_refresh_visualizer = self.has_default_refresh_visualizer.get();
327        let default_pull_dimension_size = f64::from(Self::DEFAULT_PULL_DIMENSION_SIZE);
328        let pull_direction = self.pull_direction();
329
330        match pull_direction {
331            PullDirection::TopToBottom => {
332                visualizer_presenter.set_vertical_alignment(VerticalAlignment::Top);
333                visualizer_presenter.set_horizontal_alignment(HorizontalAlignment::Stretch);
334                if has_default_refresh_visualizer {
335                    refresh_visualizer.set_pull_direction(PullDirection::TopToBottom);
336                    refresh_visualizer.set_height(default_pull_dimension_size);
337                    refresh_visualizer.set_width(f64::NAN);
338                }
339            }
340            PullDirection::BottomToTop => {
341                visualizer_presenter.set_vertical_alignment(VerticalAlignment::Bottom);
342                visualizer_presenter.set_horizontal_alignment(HorizontalAlignment::Stretch);
343                if has_default_refresh_visualizer {
344                    refresh_visualizer.set_pull_direction(PullDirection::BottomToTop);
345                    refresh_visualizer.set_height(default_pull_dimension_size);
346                    refresh_visualizer.set_width(f64::NAN);
347                }
348            }
349            PullDirection::LeftToRight => {
350                visualizer_presenter.set_vertical_alignment(VerticalAlignment::Stretch);
351                visualizer_presenter.set_horizontal_alignment(HorizontalAlignment::Left);
352                if has_default_refresh_visualizer {
353                    refresh_visualizer.set_pull_direction(PullDirection::LeftToRight);
354                    refresh_visualizer.set_width(default_pull_dimension_size);
355                    refresh_visualizer.set_height(f64::NAN);
356                }
357            }
358            PullDirection::RightToLeft => {
359                visualizer_presenter.set_vertical_alignment(VerticalAlignment::Stretch);
360                visualizer_presenter.set_horizontal_alignment(HorizontalAlignment::Right);
361                if has_default_refresh_visualizer {
362                    refresh_visualizer.set_pull_direction(PullDirection::RightToLeft);
363                    refresh_visualizer.set_width(default_pull_dimension_size);
364                    refresh_visualizer.set_height(f64::NAN);
365                }
366            }
367        }
368
369        if self.has_default_refresh_info_provider_adapter.get() {
370            let adapter = self.refresh_info_provider_adapter();
371
372            if has_default_refresh_visualizer {
373                let size = Size::new(refresh_visualizer.width(), refresh_visualizer.height());
374                if let Some(adapter) = &adapter {
375                    adapter.update_visualizer_size(Some(size));
376                }
377            }
378
379            if let Some(adapter) = &adapter {
380                adapter.update_pull_direction(pull_direction);
381            }
382            self.raise_refresh_info_provider_adapter_changed(adapter);
383        }
384    }
385
386    fn on_is_mouse_enabled_changed(&self) {
387        if let Some(adapter) = self.refresh_info_provider_adapter() {
388            adapter.update_is_mouse_enabled(self.is_mouse_enabled());
389        }
390    }
391
392    /// Initiates an update of the content.
393    pub fn request_refresh(&self) {
394        let refresh_visualizer = self.refresh_visualizer.borrow().clone();
395        if let Some(refresh_visualizer) = refresh_visualizer {
396            refresh_visualizer.request_refresh();
397        }
398    }
399}