dioxus_tw_components/components/
carousel.rs1use 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 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, 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 carousel_state.peek().autoscroll_duration
122 ));
123 spawn(async move {
124 while (timer.recv::<bool>().await).is_ok() {
125 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#[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 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}