Skip to main content

dioxus_tw_components/components/
carousel.rs

1use dioxus::prelude::*;
2use dioxus_core::AttributeValue;
3
4use crate::components::icon::*;
5
6struct CarouselState {
7    is_circular: bool,
8    autoscroll_duration: usize,
9    block_autoscoll: bool,
10    carousel_size: u32,
11    // Use a key there so we can just +1 or -1 instead of having a vec
12    current_item_key: u32,
13    content_width: i32,
14    current_translation: i32,
15}
16
17impl CarouselState {
18    fn new(current_item_key: u32, is_circular: bool, autoscroll_duration: usize) -> Self {
19        Self {
20            current_item_key,
21            autoscroll_duration,
22            block_autoscoll: false,
23            is_circular,
24            carousel_size: 0,
25            content_width: 0,
26            current_translation: 0,
27        }
28    }
29
30    fn increment_carousel_size(&mut self) {
31        self.carousel_size += 1;
32    }
33
34    fn set_content_size(&mut self, scroll_width: i32) {
35        self.content_width = scroll_width;
36    }
37
38    fn go_to_next_item(&mut self) {
39        self.current_item_key += 1;
40    }
41
42    fn go_to_prev_item(&mut self) {
43        self.current_item_key -= 1;
44    }
45
46    fn go_to_item(&mut self, item_key: u32) {
47        self.current_item_key = item_key;
48    }
49
50    fn is_active_to_attr_value(&self, key: u32) -> AttributeValue {
51        match self.current_item_key == key {
52            true => AttributeValue::Text("active".to_string()),
53            false => AttributeValue::Text("inactive".to_string()),
54        }
55    }
56
57    fn translate(&mut self) {
58        self.set_current_translation(self.current_item_key as i32 * self.content_width)
59    }
60
61    fn set_current_translation(&mut self, translation: i32) {
62        self.current_translation = translation;
63    }
64
65    fn get_current_translation(&self) -> i32 {
66        self.current_translation
67    }
68}
69
70#[derive(Clone, PartialEq, Props)]
71pub struct CarouselProps {
72    #[props(extends = div, extends = GlobalAttributes)]
73    attributes: Vec<Attribute>,
74
75    #[props(default = 0)]
76    default_item_key: u32,
77    #[props(default = false)]
78    is_circular: bool,
79    #[props(default = 0)]
80    autoscroll_duration: usize, // 0 means no autoscroll, duration in ms btw
81
82    children: Element,
83}
84
85#[component]
86pub fn Carousel(mut props: CarouselProps) -> Element {
87    use_context_provider(|| {
88        Signal::new(CarouselState::new(
89            props.default_item_key,
90            props.is_circular,
91            props.autoscroll_duration,
92        ))
93    });
94
95    let default_classes = "carousel-container carousel";
96    crate::setup_class_attribute(&mut props.attributes, default_classes);
97
98    rsx! {
99        div { ..props.attributes,{props.children} }
100    }
101}
102
103#[derive(Clone, PartialEq, Props)]
104pub struct CarouselWindowProps {
105    #[props(extends = div, extends = GlobalAttributes)]
106    attributes: Vec<Attribute>,
107
108    children: Element,
109}
110
111#[component]
112pub fn CarouselWindow(mut props: CarouselWindowProps) -> Element {
113    let mut carousel_state = use_context::<Signal<CarouselState>>();
114
115    use_effect(move || {
116        let mut timer = document::eval(&format!(
117            "setInterval(() => {{
118                dioxus.send(true);
119            }}, {});",
120            // Do not read signal due to carousel_state write just bellow which will cause carousel to have panic attack when autoscolling
121            carousel_state.peek().autoscroll_duration
122        ));
123        spawn(async move {
124            while (timer.recv::<bool>().await).is_ok() {
125                // Same as above
126                if carousel_state.peek().autoscroll_duration != 0
127                    && !carousel_state.peek().block_autoscoll
128                {
129                    scroll_carousel(true, carousel_state);
130                    carousel_state.write().translate();
131                }
132            }
133        });
134    });
135
136    let default_classes = "carousel-window";
137    crate::setup_class_attribute(&mut props.attributes, default_classes);
138
139    rsx! {
140        div {
141            onmouseover: move |_| carousel_state.write().block_autoscoll = true,
142            onmouseleave: move |_| carousel_state.write().block_autoscoll = false,
143            ..props.attributes,
144            {props.children}
145            div { class: "carousel-item-indicator",
146                for i in 0..carousel_state.read().carousel_size {
147                    div {
148                        style: format!(
149                            "width: 0.5rem; height: 0.5rem; border-radius: calc(infinity * 1px); {};",
150                            if i == carousel_state.read().current_item_key {
151                                "background-color: var(--foreground)"
152                            } else {
153                                "background-color: color-mix(in oklab, var(--foreground) 50%, transparent)"
154                            },
155                        ),
156                    }
157                }
158            }
159        }
160    }
161}
162
163#[derive(Clone, PartialEq, Props)]
164pub struct CarouselContentProps {
165    #[props(extends = div, extends = GlobalAttributes)]
166    attributes: Vec<Attribute>,
167
168    id: ReadSignal<String>,
169
170    children: Element,
171}
172
173/// You need to pass it an id for it to work
174#[component]
175pub fn CarouselContent(mut props: CarouselContentProps) -> Element {
176    let mut carousel_state = use_context::<Signal<CarouselState>>();
177
178    let style = use_memo(move || {
179        format!(
180            "transform: translateX(-{}px)",
181            carousel_state.read().get_current_translation()
182        )
183    });
184
185    let default_classes = "carousel-content";
186    crate::setup_class_attribute(&mut props.attributes, default_classes);
187
188    rsx! {
189        div {
190            style,
191            id: props.id,
192            onresize: move |element| {
193                carousel_state
194                    .write()
195                    .set_content_size(
196                        match element.data().get_content_box_size() {
197                            Ok(size) => size.width as i32,
198                            Err(_) => 0,
199                        },
200                    );
201            },
202            ..props.attributes,
203            {props.children}
204        }
205    }
206}
207
208#[derive(Clone, PartialEq, Props)]
209pub struct CarouselItemProps {
210    /// Represent position in the carousel
211    item_key: u32,
212
213    #[props(extends = div, extends = GlobalAttributes)]
214    attributes: Vec<Attribute>,
215
216    children: Element,
217}
218
219#[component]
220pub fn CarouselItem(mut props: CarouselItemProps) -> Element {
221    let mut state = use_context::<Signal<CarouselState>>();
222
223    let onmounted = move |_| {
224        state.write().increment_carousel_size();
225    };
226
227    let default_classes = "carousel-item";
228    crate::setup_class_attribute(&mut props.attributes, default_classes);
229
230    rsx! {
231        div {
232            "data-state": state.read().is_active_to_attr_value(props.item_key),
233            onmounted,
234            ..props.attributes,
235            {props.children}
236        }
237    }
238}
239
240#[derive(Default, Clone, PartialEq, Props)]
241pub struct CarouselTriggerProps {
242    #[props(default = false)]
243    next: bool,
244
245    #[props(extends = button, extends = GlobalAttributes)]
246    attributes: Vec<Attribute>,
247}
248
249#[component]
250pub fn CarouselTrigger(mut props: CarouselTriggerProps) -> Element {
251    let mut carousel_state = use_context::<Signal<CarouselState>>();
252
253    let onclick = move |_| async move {
254        scroll_carousel(props.next, carousel_state);
255        carousel_state.write().translate();
256    };
257
258    let icon = get_next_prev_icons(props.next);
259
260    let default_classes = "carousel-trigger";
261    crate::setup_class_attribute(&mut props.attributes, default_classes);
262
263    rsx! {
264        button {
265            onmouseover: move |_| carousel_state.write().block_autoscoll = true,
266            onmouseleave: move |_| carousel_state.write().block_autoscoll = false,
267            onclick,
268            ..props.attributes,
269            {icon}
270        }
271    }
272}
273
274fn scroll_carousel(next: bool, mut carousel_state: Signal<CarouselState>) {
275    let mut carousel_state = carousel_state.write();
276    let current_key = carousel_state.current_item_key;
277    let carousel_size = carousel_state.carousel_size;
278
279    if next {
280        if current_key < carousel_size - 1 {
281            carousel_state.go_to_next_item();
282        } else if carousel_state.is_circular {
283            carousel_state.go_to_item(0);
284        }
285    } else if current_key > 0 {
286        carousel_state.go_to_prev_item();
287    } else if carousel_state.is_circular {
288        carousel_state.go_to_item(carousel_size - 1);
289    }
290}
291
292fn get_next_prev_icons(is_next: bool) -> Element {
293    match is_next {
294        true => rsx! {
295            Icon { icon: Icons::ChevronRight }
296        },
297        false => rsx! {
298            Icon { icon: Icons::ChevronLeft }
299        },
300    }
301}