Skip to main content

ferroui_controls/
carousel.rs

1use crate::primitives::{
2    SelectingItemsControl, SelectingItemsControlImpl, TemplateAppliedEventArgs, TemplatedControlImpl,
3    TemplatedControlImplExt,
4};
5use crate::templates::{FuncTemplate, ITemplateOf};
6use crate::{ControlImpl, ItemsControl, ItemsControlImpl, Panel, SelectionMode, VirtualizingCarouselPanel};
7use ferroui_base::animation::{IPageTransition, PageSlide, SlideAxis};
8use ferroui_base::input::{InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs};
9use ferroui_base::interactivity::InteractiveImpl;
10use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt};
11use ferroui_base::{
12    ferro_class, ferro_impl_classes, ferro_property, instantiate, DirectProperty, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
13    FerroPropertyChangedEventArgs, Ref, Size, StyledElementImpl, StyledProperty, StyledPropertyOptions, Vector,
14    VisualImpl,
15};
16use std::cell::{Cell, RefCell};
17use std::rc::Rc;
18
19/// An items control that displays its items as pages and can reveal adjacent
20/// pages using `viewport_fraction`.
21#[repr(C)]
22pub struct Carousel {
23    base: SelectingItemsControl,
24    /// The template part named `PART_ScrollViewer`: an input element that
25    /// is a scrollable.
26    scroller: RefCell<Option<Ref<InputElement>>>,
27    is_swiping: Cell<bool>,
28}
29
30ferro_class!(Carousel: SelectingItemsControl);
31ferroui_base::ferro_class_info!(Carousel { new: Carousel::new });
32ferro_impl_classes!(
33    Carousel: StyledElementImpl,
34    VisualImpl,
35    InteractiveImpl,
36    ControlImpl,
37    ItemsControlImpl,
38    SelectingItemsControlImpl
39);
40
41impl FerroObjectImpl for Carousel {
42    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
43        Self::parent_on_property_changed(this, change);
44
45        if change.property() == SelectingItemsControl::selected_index_property().as_property() {
46            this.sync_scroll_offset();
47        }
48
49        if change.property() == Self::is_swipe_enabled_property().as_property()
50            || change.property() == Self::page_transition_property().as_property()
51            || change.property() == Self::viewport_fraction_property().as_property()
52            || change.property() == SelectingItemsControl::wrap_selection_property().as_property()
53        {
54            if let Some(panel) = this.carousel_panel() {
55                if change.property() == Self::viewport_fraction_property().as_property()
56                    && !panel.is_managing_interaction_offset()
57                {
58                    panel.sync_selection_offset(this.selected_index());
59                }
60
61                panel.refresh_gesture_recognizer();
62                panel.invalidate_measure();
63            }
64
65            this.sync_scroll_offset();
66        }
67    }
68}
69
70impl InputElementImpl for Carousel {
71    fn on_key_down(this: &Self, e: &KeyEventArgs) {
72        Self::parent_on_key_down(this, e);
73
74        if e.handled() || this.item_count() == 0 {
75            return;
76        }
77
78        let axis = if this.viewport_fraction() != 1.0 {
79            Some(this.get_layout_axis())
80        } else {
81            this.get_transition_axis()
82        };
83        let is_vertical = axis == Some(SlideAxis::Vertical);
84        let is_horizontal = axis == Some(SlideAxis::Horizontal);
85
86        match e.key {
87            Key::Left if !is_vertical => {
88                this.previous();
89                e.set_handled(true);
90            }
91            Key::Up if !is_horizontal => {
92                this.previous();
93                e.set_handled(true);
94            }
95            Key::Right if !is_vertical => {
96                this.next();
97                e.set_handled(true);
98            }
99            Key::Down if !is_horizontal => {
100                this.next();
101                e.set_handled(true);
102            }
103            Key::Home => {
104                this.set_selected_index(0);
105                e.set_handled(true);
106            }
107            Key::End => {
108                this.set_selected_index(this.item_count() - 1);
109                e.set_handled(true);
110            }
111            _ => {}
112        }
113    }
114}
115
116impl LayoutableImpl for Carousel {
117    fn arrange_override(this: &Self, final_size: Size) -> Size {
118        let result = Self::parent_arrange_override(this, final_size);
119
120        this.sync_scroll_offset();
121
122        result
123    }
124}
125
126impl TemplatedControlImpl for Carousel {
127    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
128        Self::parent_on_apply_template(this, e);
129
130        let scroller = e.name_scope().find("PART_ScrollViewer").map(|found| {
131            match found.cast::<InputElement>().filter(|element| element.as_scrollable().is_some()) {
132                Some(scroller) => scroller,
133                None => panic!(
134                    "Expected control 'PART_ScrollViewer' to be 'IScrollable' but it was '{}'.",
135                    found.get_type().name()
136                ),
137            }
138        });
139        *this.scroller.borrow_mut() = scroller;
140
141        if let Some(panel) = this.carousel_panel() {
142            panel.refresh_gesture_recognizer();
143        }
144    }
145}
146
147ferroui_base::ferro_properties! { impl Carousel {
148    ferro_property!(
149        /// Defines the `PageTransition` property.
150        pub fn page_transition_property() -> StyledProperty<Option<Rc<dyn IPageTransition>>> {
151            FerroProperty::register::<Carousel, _>("PageTransition", None)
152        }
153    );
154
155    ferro_property!(
156        /// Defines the `IsSwipeEnabled` property.
157        pub fn is_swipe_enabled_property() -> StyledProperty<bool> {
158            FerroProperty::register::<Carousel, _>("IsSwipeEnabled", false)
159        }
160    );
161
162    ferro_property!(
163        /// Defines the `ViewportFraction` property.
164        pub fn viewport_fraction_property() -> StyledProperty<f64> {
165            FerroProperty::register_with::<Carousel, _>(
166                "ViewportFraction",
167                StyledPropertyOptions::new(1.0)
168                    .coerce(|_, value: f64| if value.is_finite() && value > 0.0 { value } else { 1.0 }),
169            )
170        }
171    );
172
173    ferro_property!(
174        /// Defines the `IsSwiping` property.
175        pub fn is_swiping_property() -> DirectProperty<Carousel, bool> {
176            FerroProperty::register_direct::<Carousel, _>("IsSwiping", |o| o.is_swiping(), None, false)
177        }
178    );
179} }
180
181impl Carousel {
182    /// Initializes static members of the [`Carousel`] class.
183    fn static_constructor() {
184        // The default value of the `ItemsPanel` property for a carousel.
185        let default_panel: Rc<dyn ITemplateOf<Option<Ref<Panel>>>> =
186            FuncTemplate::new(|| Some(VirtualizingCarouselPanel::new().upcast::<Panel>()));
187
188        SelectingItemsControl::selection_mode_property()
189            .override_default_value::<Carousel>(SelectionMode::ALWAYS_SELECTED);
190        ItemsControl::items_panel_property().override_default_value::<Carousel>(default_panel);
191    }
192
193    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
194    pub fn construct() -> Self {
195        Self { base: SelectingItemsControl::construct(), scroller: RefCell::new(None), is_swiping: Cell::new(false) }
196    }
197
198    pub fn new() -> Ref<Self> {
199        instantiate(Self::construct())
200    }
201
202    /// Gets or sets the transition to use when moving between pages.
203    pub fn page_transition(&self) -> Option<Rc<dyn IPageTransition>> {
204        self.get_value(Self::page_transition_property())
205    }
206
207    pub fn set_page_transition(&self, value: Option<Rc<dyn IPageTransition>>) {
208        self.set_value(Self::page_transition_property(), value)
209    }
210
211    /// Gets or sets whether swipe gestures are enabled for navigating
212    /// between pages. When enabled, mouse pointer events are also accepted
213    /// in addition to touch and pen.
214    pub fn is_swipe_enabled(&self) -> bool {
215        self.get_value(Self::is_swipe_enabled_property())
216    }
217
218    pub fn set_is_swipe_enabled(&self, value: bool) {
219        self.set_value(Self::is_swipe_enabled_property(), value)
220    }
221
222    /// Gets or sets the fraction of the viewport occupied by each page. A
223    /// value of 1 shows a single full page; values below 1 reveal adjacent
224    /// pages.
225    pub fn viewport_fraction(&self) -> f64 {
226        self.get_value(Self::viewport_fraction_property())
227    }
228
229    pub fn set_viewport_fraction(&self, value: f64) {
230        self.set_value(Self::viewport_fraction_property(), value)
231    }
232
233    /// Gets a value indicating whether a swipe gesture is currently in
234    /// progress.
235    pub fn is_swiping(&self) -> bool {
236        self.is_swiping.get()
237    }
238
239    pub(crate) fn set_is_swiping(&self, value: bool) {
240        self.set_and_raise_cell(Self::is_swiping_property(), &self.is_swiping, value);
241    }
242
243    /// Moves to the next item in the carousel.
244    pub fn next(&self) {
245        if self.item_count() == 0 {
246            return;
247        }
248
249        if self.selected_index() < self.item_count() - 1 {
250            self.set_selected_index(self.selected_index() + 1);
251        } else if self.wrap_selection() {
252            self.set_selected_index(0);
253        }
254    }
255
256    /// Moves to the previous item in the carousel.
257    pub fn previous(&self) {
258        if self.item_count() == 0 {
259            return;
260        }
261
262        if self.selected_index() > 0 {
263            self.set_selected_index(self.selected_index() - 1);
264        } else if self.wrap_selection() {
265            self.set_selected_index(self.item_count() - 1);
266        }
267    }
268
269    pub(crate) fn get_transition_axis(&self) -> Option<SlideAxis> {
270        let transition = self.page_transition()?;
271
272        if let Some(composite) = transition.as_composite_page_transition() {
273            for t in composite.page_transitions() {
274                if let Some(slide) = t.as_page_slide() {
275                    return Some(slide.orientation());
276                }
277            }
278
279            return None;
280        }
281
282        transition.as_page_slide().map(PageSlide::orientation)
283    }
284
285    pub(crate) fn get_layout_axis(&self) -> SlideAxis {
286        self.get_transition_axis().unwrap_or(SlideAxis::Horizontal)
287    }
288
289    /// The items panel, when it is a carousel panel.
290    fn carousel_panel(&self) -> Option<Ref<VirtualizingCarouselPanel>> {
291        self.items_panel_root().and_then(|panel| panel.cast::<VirtualizingCarouselPanel>())
292    }
293
294    fn sync_scroll_offset(&self) {
295        if let Some(panel) = self.carousel_panel() {
296            if panel.is_managing_interaction_offset() {
297                return;
298            }
299
300            panel.sync_selection_offset(self.selected_index());
301
302            if self.viewport_fraction() != 1.0 {
303                return;
304            }
305        }
306
307        let scroller = self.scroller.borrow().clone();
308        let Some(scroller) = scroller else {
309            return;
310        };
311
312        if let Some(scrollable) = scroller.as_scrollable() {
313            scrollable.set_offset(self.create_scroll_offset(self.selected_index()));
314        }
315    }
316
317    fn create_scroll_offset(&self, index: i32) -> Vector {
318        if self.viewport_fraction() != 1.0 && self.get_layout_axis() == SlideAxis::Vertical {
319            return Vector::new(0.0, index as f64);
320        }
321
322        Vector::new(index as f64, 0.0)
323    }
324}