dioxus_bootstrap_css/
carousel.rs1use dioxus::prelude::*;
2
3#[derive(Clone, PartialEq)]
5pub struct CarouselSlide {
6 pub src: String,
8 pub alt: String,
10 pub caption_title: Option<String>,
12 pub caption_text: Option<String>,
14}
15
16#[derive(Clone, PartialEq, Props)]
38pub struct CarouselProps {
39 pub active: Signal<usize>,
41 pub slides: Vec<CarouselSlide>,
43 #[props(default = true)]
45 pub indicators: bool,
46 #[props(default = true)]
48 pub controls: bool,
49 #[props(default)]
51 pub fade: bool,
52 #[props(default)]
54 pub dark: bool,
55 #[props(default)]
57 pub ride: bool,
58 #[props(default = 5000)]
60 pub interval: u64,
61 #[props(default)]
63 pub class: String,
64}
65
66#[derive(Clone, Copy, PartialEq)]
68enum SlideDirection {
69 Next,
70 Prev,
71}
72
73#[component]
74pub fn Carousel(props: CarouselProps) -> Element {
75 let current = *props.active.read();
76 let mut active_signal = props.active;
77 let total = props.slides.len();
78
79 if total == 0 {
80 return rsx! {};
81 }
82
83 let mut transitioning = use_signal(|| Option::<(usize, usize, SlideDirection)>::None);
85 let trans = *transitioning.read();
86
87 let mut paused = use_signal(|| false);
89
90 let mut touch_start_x = use_signal(|| 0.0f64);
92
93 let mut go_direction = move |direction: SlideDirection| {
95 if transitioning.read().is_some() {
97 return; }
99 let cur = *active_signal.read();
100 let to = match direction {
101 SlideDirection::Next => {
102 if cur + 1 >= total {
103 0
104 } else {
105 cur + 1
106 }
107 }
108 SlideDirection::Prev => {
109 if cur == 0 {
110 total - 1
111 } else {
112 cur - 1
113 }
114 }
115 };
116 transitioning.set(Some((cur, to, direction)));
117 spawn(async move {
119 gloo_timers::future::TimeoutFuture::new(600).await;
120 active_signal.set(to);
121 transitioning.set(None);
122 });
123 };
124
125 let ride = props.ride;
127 let interval = props.interval;
128 use_future(move || async move {
129 if !ride || total <= 1 {
130 return;
131 }
132 loop {
133 gloo_timers::future::TimeoutFuture::new(interval as u32).await;
134 if !*paused.read() && transitioning.read().is_none() {
135 go_direction(SlideDirection::Next);
136 }
137 }
138 });
139
140 let mut classes = vec!["carousel".to_string(), "slide".to_string()];
141 if props.fade {
142 classes.push("carousel-fade".to_string());
143 }
144 if props.dark {
145 classes.push("carousel-dark".to_string());
146 }
147 if !props.class.is_empty() {
148 classes.push(props.class.clone());
149 }
150 let full_class = classes.join(" ");
151
152 rsx! {
153 div {
154 class: "{full_class}",
155 tabindex: "0",
156 onmouseenter: move |_| paused.set(true),
158 onmouseleave: move |_| paused.set(false),
159 onkeydown: move |evt: KeyboardEvent| {
161 match evt.key() {
162 Key::ArrowLeft => go_direction(SlideDirection::Prev),
163 Key::ArrowRight => go_direction(SlideDirection::Next),
164 _ => {}
165 }
166 },
167 ontouchstart: move |evt: TouchEvent| {
169 if let Some(touch) = evt.touches().first() {
170 let coords: dioxus_elements::geometry::PagePoint = touch.page_coordinates();
171 touch_start_x.set(coords.x);
172 }
173 },
174 ontouchend: move |evt: TouchEvent| {
176 if let Some(touch) = evt.touches_changed().first() {
177 let start = *touch_start_x.read();
178 let coords: dioxus_elements::geometry::PagePoint = touch.page_coordinates();
179 let diff = coords.x - start;
180 if diff < -50.0 {
182 go_direction(SlideDirection::Next);
183 } else if diff > 50.0 {
184 go_direction(SlideDirection::Prev);
185 }
186 }
187 },
188
189 if props.indicators {
191 div { class: "carousel-indicators",
192 for i in 0..total {
193 button {
194 class: if current == i { "active" } else { "" },
195 r#type: "button",
196 "aria-current": if current == i { "true" } else { "false" },
197 "aria-label": "Slide {i}",
198 onclick: move |_| active_signal.set(i),
199 }
200 }
201 }
202 }
203
204 div {
206 class: "carousel-inner",
207 style: "overflow: hidden;",
208 for (i, slide) in props.slides.iter().enumerate() {
209 {
210 let item_class = build_slide_class(i, current, trans, props.fade);
211 let item_style = build_slide_style(i, current, trans, props.fade);
212 rsx! {
213 div {
214 class: "{item_class}",
215 style: "{item_style}",
216 img {
217 class: "d-block w-100",
218 src: "{slide.src}",
219 alt: "{slide.alt}",
220 }
221 if slide.caption_title.is_some() || slide.caption_text.is_some() {
222 div { class: "carousel-caption d-none d-md-block",
223 if let Some(ref title) = slide.caption_title {
224 h5 { "{title}" }
225 }
226 if let Some(ref text) = slide.caption_text {
227 p { "{text}" }
228 }
229 }
230 }
231 }
232 }
233 }
234 }
235 }
236
237 if props.controls && total > 1 {
239 button {
240 class: "carousel-control-prev",
241 r#type: "button",
242 onclick: move |_| go_direction(SlideDirection::Prev),
243 span { class: "carousel-control-prev-icon", "aria-hidden": "true" }
244 span { class: "visually-hidden", "Previous" }
245 }
246 button {
247 class: "carousel-control-next",
248 r#type: "button",
249 onclick: move |_| go_direction(SlideDirection::Next),
250 span { class: "carousel-control-next-icon", "aria-hidden": "true" }
251 span { class: "visually-hidden", "Next" }
252 }
253 }
254 }
255 }
256}
257
258fn build_slide_class(
260 index: usize,
261 current: usize,
262 trans: Option<(usize, usize, SlideDirection)>,
263 fade: bool,
264) -> String {
265 match trans {
266 Some((from, to, direction)) => {
267 if fade {
268 if index == from {
269 "carousel-item active".to_string()
270 } else if index == to {
271 "carousel-item carousel-item-next carousel-item-start active".to_string()
272 } else {
273 "carousel-item".to_string()
274 }
275 } else if index == from {
276 match direction {
277 SlideDirection::Next => "carousel-item active carousel-item-start".to_string(),
278 SlideDirection::Prev => "carousel-item active carousel-item-end".to_string(),
279 }
280 } else if index == to {
281 match direction {
282 SlideDirection::Next => {
283 "carousel-item carousel-item-next carousel-item-start".to_string()
284 }
285 SlideDirection::Prev => {
286 "carousel-item carousel-item-prev carousel-item-end".to_string()
287 }
288 }
289 } else {
290 "carousel-item".to_string()
291 }
292 }
293 None => {
294 if index == current {
295 "carousel-item active".to_string()
296 } else {
297 "carousel-item".to_string()
298 }
299 }
300 }
301}
302
303fn build_slide_style(
305 index: usize,
306 _current: usize,
307 trans: Option<(usize, usize, SlideDirection)>,
308 fade: bool,
309) -> String {
310 if fade {
311 return String::new();
312 }
313 match trans {
314 Some((from, to, _direction)) => {
315 if index == from || index == to {
316 "transition: transform 0.6s ease-in-out;".to_string()
317 } else {
318 String::new()
319 }
320 }
321 None => String::new(),
322 }
323}