1use dioxus::prelude::*;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub enum CarouselEffect {
11 #[default]
13 ScrollX,
14 Fade,
16}
17
18impl CarouselEffect {
19 fn as_class(&self) -> &'static str {
20 match self {
21 CarouselEffect::ScrollX => "adui-carousel-scroll",
22 CarouselEffect::Fade => "adui-carousel-fade",
23 }
24 }
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum DotPlacement {
30 Top,
32 #[default]
34 Bottom,
35 Left,
37 Right,
39}
40
41impl DotPlacement {
42 fn as_class(&self) -> &'static str {
43 match self {
44 DotPlacement::Top => "adui-carousel-dots-top",
45 DotPlacement::Bottom => "adui-carousel-dots-bottom",
46 DotPlacement::Left => "adui-carousel-dots-left",
47 DotPlacement::Right => "adui-carousel-dots-right",
48 }
49 }
50
51 fn is_vertical(&self) -> bool {
52 matches!(self, DotPlacement::Left | DotPlacement::Right)
53 }
54}
55
56#[derive(Clone, Debug, PartialEq)]
58pub struct CarouselItem {
59 pub content: String,
61 pub background: Option<String>,
63}
64
65impl CarouselItem {
66 pub fn new(content: impl Into<String>) -> Self {
68 Self {
69 content: content.into(),
70 background: None,
71 }
72 }
73
74 pub fn with_background(mut self, bg: impl Into<String>) -> Self {
76 self.background = Some(bg.into());
77 self
78 }
79}
80
81#[derive(Props, Clone, PartialEq)]
83pub struct CarouselProps {
84 #[props(default)]
86 pub items: Vec<CarouselItem>,
87 #[props(optional)]
89 pub slide_count: Option<usize>,
90 #[props(default)]
92 pub effect: CarouselEffect,
93 #[props(default)]
95 pub autoplay: bool,
96 #[props(default = 3000)]
98 pub autoplay_speed: u64,
99 #[props(default = true)]
101 pub dots: bool,
102 #[props(default)]
104 pub dot_placement: DotPlacement,
105 #[props(default)]
107 pub arrows: bool,
108 #[props(default = 500)]
110 pub speed: u32,
111 #[props(default)]
113 pub initial_slide: usize,
114 #[props(default = true)]
116 pub infinite: bool,
117 #[props(default = true)]
119 pub pause_on_hover: bool,
120 #[props(optional)]
122 pub on_change: Option<EventHandler<usize>>,
123 #[props(optional)]
125 pub before_change: Option<EventHandler<(usize, usize)>>,
126 #[props(optional)]
128 pub class: Option<String>,
129 #[props(optional)]
131 pub style: Option<String>,
132 pub children: Element,
134}
135
136#[component]
138pub fn Carousel(props: CarouselProps) -> Element {
139 let CarouselProps {
140 items,
141 slide_count,
142 effect,
143 autoplay: _autoplay,
144 autoplay_speed: _autoplay_speed,
145 dots,
146 dot_placement,
147 arrows,
148 speed,
149 initial_slide,
150 infinite,
151 pause_on_hover: _pause_on_hover,
152 on_change,
153 before_change,
154 class,
155 style,
156 children,
157 } = props;
158
159 let mut current: Signal<usize> = use_signal(|| initial_slide);
161 let is_hovered: Signal<bool> = use_signal(|| false);
163
164 let count = if !items.is_empty() {
166 items.len()
167 } else {
168 slide_count.unwrap_or(0)
169 };
170
171 let go_to = {
173 let on_change = on_change.clone();
174 let before_change = before_change.clone();
175 move |index: usize| {
176 if count == 0 || index >= count {
177 return;
178 }
179 let curr = *current.read();
180 if curr == index {
181 return;
182 }
183 if let Some(handler) = &before_change {
184 handler.call((curr, index));
185 }
186 current.set(index);
187 if let Some(handler) = &on_change {
188 handler.call(index);
189 }
190 }
191 };
192
193 let go_prev = {
194 let on_change = on_change.clone();
195 let before_change = before_change.clone();
196 move |_: MouseEvent| {
197 if count == 0 {
198 return;
199 }
200 let curr = *current.read();
201 let prev = if curr == 0 {
202 if infinite {
203 count - 1
204 } else {
205 return;
206 }
207 } else {
208 curr - 1
209 };
210 if let Some(handler) = &before_change {
211 handler.call((curr, prev));
212 }
213 current.set(prev);
214 if let Some(handler) = &on_change {
215 handler.call(prev);
216 }
217 }
218 };
219
220 let go_next = {
221 let on_change = on_change.clone();
222 let before_change = before_change.clone();
223 move |_: MouseEvent| {
224 if count == 0 {
225 return;
226 }
227 let curr = *current.read();
228 let next = if curr + 1 >= count {
229 if infinite {
230 0
231 } else {
232 return;
233 }
234 } else {
235 curr + 1
236 };
237 if let Some(handler) = &before_change {
238 handler.call((curr, next));
239 }
240 current.set(next);
241 if let Some(handler) = &on_change {
242 handler.call(next);
243 }
244 }
245 };
246
247 let mut class_list = vec!["adui-carousel".to_string()];
249 class_list.push(effect.as_class().to_string());
250 class_list.push(dot_placement.as_class().to_string());
251 if dot_placement.is_vertical() {
252 class_list.push("adui-carousel-vertical".into());
253 }
254 if let Some(extra) = class {
255 class_list.push(extra);
256 }
257 let class_attr = class_list.join(" ");
258
259 let transition_style = format!("--adui-carousel-speed: {}ms;", speed);
260 let style_attr = format!("{}{}", transition_style, style.unwrap_or_default());
261
262 let current_index = *current.read();
263
264 let on_mouse_enter = {
266 let mut hovered = is_hovered;
267 move |_| hovered.set(true)
268 };
269 let on_mouse_leave = {
270 let mut hovered = is_hovered;
271 move |_| hovered.set(false)
272 };
273
274 let track_style = match effect {
276 CarouselEffect::ScrollX => format!("transform: translateX(-{}%);", current_index * 100),
277 CarouselEffect::Fade => String::new(),
278 };
279
280 rsx! {
281 div {
282 class: "{class_attr}",
283 style: "{style_attr}",
284 onmouseenter: on_mouse_enter,
285 onmouseleave: on_mouse_leave,
286
287 div { class: "adui-carousel-inner",
289 div {
290 class: "adui-carousel-track",
291 style: "{track_style}",
292 for (i, item) in items.iter().enumerate() {
294 {
295 let is_active = i == current_index;
296 let mut slide_class = vec!["adui-carousel-slide".to_string()];
297 if is_active {
298 slide_class.push("adui-carousel-slide-active".into());
299 }
300 let slide_style = item.background.as_ref()
301 .map(|bg| format!("background: {};", bg))
302 .unwrap_or_default();
303 rsx! {
304 div {
305 key: "{i}",
306 class: "{slide_class.join(\" \")}",
307 style: "{slide_style}",
308 "{item.content}"
309 }
310 }
311 }
312 }
313 if items.is_empty() {
315 {children}
316 }
317 }
318 }
319
320 if arrows && count > 0 {
322 button {
323 class: "adui-carousel-arrow adui-carousel-arrow-prev",
324 r#type: "button",
325 onclick: go_prev,
326 "‹"
327 }
328 button {
329 class: "adui-carousel-arrow adui-carousel-arrow-next",
330 r#type: "button",
331 onclick: go_next,
332 "›"
333 }
334 }
335
336 if dots && count > 0 {
338 div { class: "adui-carousel-dots",
339 for i in 0..count {
340 button {
341 key: "{i}",
342 class: if i == current_index { "adui-carousel-dot adui-carousel-dot-active" } else { "adui-carousel-dot" },
343 r#type: "button",
344 onclick: {
345 let mut go_to = go_to.clone();
346 move |_| go_to(i)
347 },
348 }
349 }
350 }
351 }
352 }
353 }
354}
355
356#[derive(Props, Clone, PartialEq)]
358pub struct CarouselSlideProps {
359 #[props(default)]
361 pub active: bool,
362 #[props(optional)]
364 pub class: Option<String>,
365 #[props(optional)]
367 pub style: Option<String>,
368 pub children: Element,
370}
371
372#[component]
374pub fn CarouselSlide(props: CarouselSlideProps) -> Element {
375 let CarouselSlideProps {
376 active,
377 class,
378 style,
379 children,
380 } = props;
381
382 let mut class_list = vec!["adui-carousel-slide".to_string()];
383 if active {
384 class_list.push("adui-carousel-slide-active".into());
385 }
386 if let Some(extra) = class {
387 class_list.push(extra);
388 }
389 let class_attr = class_list.join(" ");
390 let style_attr = style.unwrap_or_default();
391
392 rsx! {
393 div { class: "{class_attr}", style: "{style_attr}",
394 {children}
395 }
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn carousel_effect_class_names() {
405 assert_eq!(CarouselEffect::ScrollX.as_class(), "adui-carousel-scroll");
406 assert_eq!(CarouselEffect::Fade.as_class(), "adui-carousel-fade");
407 }
408
409 #[test]
410 fn dot_placement_class_names() {
411 assert_eq!(DotPlacement::Top.as_class(), "adui-carousel-dots-top");
412 assert_eq!(DotPlacement::Bottom.as_class(), "adui-carousel-dots-bottom");
413 assert_eq!(DotPlacement::Left.as_class(), "adui-carousel-dots-left");
414 assert_eq!(DotPlacement::Right.as_class(), "adui-carousel-dots-right");
415 }
416
417 #[test]
418 fn dot_placement_vertical_detection() {
419 assert!(!DotPlacement::Top.is_vertical());
420 assert!(!DotPlacement::Bottom.is_vertical());
421 assert!(DotPlacement::Left.is_vertical());
422 assert!(DotPlacement::Right.is_vertical());
423 }
424
425 #[test]
426 fn carousel_item_builder() {
427 let item = CarouselItem::new("Hello").with_background("#ff0000");
428 assert_eq!(item.content, "Hello");
429 assert_eq!(item.background, Some("#ff0000".to_string()));
430 }
431}