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