ferroui_controls/
carousel.rs1use 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#[repr(C)]
22pub struct Carousel {
23 base: SelectingItemsControl,
24 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 pub fn page_transition_property() -> StyledProperty<Option<Rc<dyn IPageTransition>>> {
151 FerroProperty::register::<Carousel, _>("PageTransition", None)
152 }
153 );
154
155 ferro_property!(
156 pub fn is_swipe_enabled_property() -> StyledProperty<bool> {
158 FerroProperty::register::<Carousel, _>("IsSwipeEnabled", false)
159 }
160 );
161
162 ferro_property!(
163 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 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 fn static_constructor() {
184 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 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 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 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 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 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 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 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 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}