use dioxus::prelude::*;
#[derive(Clone, PartialEq)]
pub struct CarouselSlide {
pub src: String,
pub alt: String,
pub caption_title: Option<String>,
pub caption_text: Option<String>,
}
#[derive(Clone, PartialEq, Props)]
pub struct CarouselProps {
pub active: Signal<usize>,
pub slides: Vec<CarouselSlide>,
#[props(default = true)]
pub indicators: bool,
#[props(default = true)]
pub controls: bool,
#[props(default)]
pub fade: bool,
#[props(default)]
pub dark: bool,
#[props(default)]
pub ride: bool,
#[props(default = 5000)]
pub interval: u64,
#[props(default)]
pub class: String,
}
#[derive(Clone, Copy, PartialEq)]
enum SlideDirection {
Next,
Prev,
}
#[component]
pub fn Carousel(props: CarouselProps) -> Element {
let current = *props.active.read();
let mut active_signal = props.active;
let total = props.slides.len();
if total == 0 {
return rsx! {};
}
let mut transitioning = use_signal(|| Option::<(usize, usize, SlideDirection)>::None);
let trans = *transitioning.read();
let mut paused = use_signal(|| false);
let mut touch_start_x = use_signal(|| 0.0f64);
let mut go_direction = move |direction: SlideDirection| {
if transitioning.read().is_some() {
return; }
let cur = *active_signal.read();
let to = match direction {
SlideDirection::Next => {
if cur + 1 >= total {
0
} else {
cur + 1
}
}
SlideDirection::Prev => {
if cur == 0 {
total - 1
} else {
cur - 1
}
}
};
transitioning.set(Some((cur, to, direction)));
spawn(async move {
gloo_timers::future::TimeoutFuture::new(600).await;
active_signal.set(to);
transitioning.set(None);
});
};
let ride = props.ride;
let interval = props.interval;
use_future(move || async move {
if !ride || total <= 1 {
return;
}
loop {
gloo_timers::future::TimeoutFuture::new(interval as u32).await;
if !*paused.read() && transitioning.read().is_none() {
go_direction(SlideDirection::Next);
}
}
});
let mut classes = vec!["carousel".to_string(), "slide".to_string()];
if props.fade {
classes.push("carousel-fade".to_string());
}
if props.dark {
classes.push("carousel-dark".to_string());
}
if !props.class.is_empty() {
classes.push(props.class.clone());
}
let full_class = classes.join(" ");
rsx! {
div {
class: "{full_class}",
tabindex: "0",
onmouseenter: move |_| paused.set(true),
onmouseleave: move |_| paused.set(false),
onkeydown: move |evt: KeyboardEvent| {
match evt.key() {
Key::ArrowLeft => go_direction(SlideDirection::Prev),
Key::ArrowRight => go_direction(SlideDirection::Next),
_ => {}
}
},
ontouchstart: move |evt: TouchEvent| {
if let Some(touch) = evt.touches().first() {
let coords: dioxus_elements::geometry::PagePoint = touch.page_coordinates();
touch_start_x.set(coords.x);
}
},
ontouchend: move |evt: TouchEvent| {
if let Some(touch) = evt.touches_changed().first() {
let start = *touch_start_x.read();
let coords: dioxus_elements::geometry::PagePoint = touch.page_coordinates();
let diff = coords.x - start;
if diff < -50.0 {
go_direction(SlideDirection::Next);
} else if diff > 50.0 {
go_direction(SlideDirection::Prev);
}
}
},
if props.indicators {
div { class: "carousel-indicators",
for i in 0..total {
button {
class: if current == i { "active" } else { "" },
r#type: "button",
"aria-current": if current == i { "true" } else { "false" },
"aria-label": "Slide {i}",
onclick: move |_| active_signal.set(i),
}
}
}
}
div {
class: "carousel-inner",
style: "overflow: hidden;",
for (i, slide) in props.slides.iter().enumerate() {
{
let item_class = build_slide_class(i, current, trans, props.fade);
let item_style = build_slide_style(i, current, trans, props.fade);
rsx! {
div {
class: "{item_class}",
style: "{item_style}",
img {
class: "d-block w-100",
src: "{slide.src}",
alt: "{slide.alt}",
}
if slide.caption_title.is_some() || slide.caption_text.is_some() {
div { class: "carousel-caption d-none d-md-block",
if let Some(ref title) = slide.caption_title {
h5 { "{title}" }
}
if let Some(ref text) = slide.caption_text {
p { "{text}" }
}
}
}
}
}
}
}
}
if props.controls && total > 1 {
button {
class: "carousel-control-prev",
r#type: "button",
onclick: move |_| go_direction(SlideDirection::Prev),
span { class: "carousel-control-prev-icon", "aria-hidden": "true" }
span { class: "visually-hidden", "Previous" }
}
button {
class: "carousel-control-next",
r#type: "button",
onclick: move |_| go_direction(SlideDirection::Next),
span { class: "carousel-control-next-icon", "aria-hidden": "true" }
span { class: "visually-hidden", "Next" }
}
}
}
}
}
fn build_slide_class(
index: usize,
current: usize,
trans: Option<(usize, usize, SlideDirection)>,
fade: bool,
) -> String {
match trans {
Some((from, to, direction)) => {
if fade {
if index == from {
"carousel-item active".to_string()
} else if index == to {
"carousel-item carousel-item-next carousel-item-start active".to_string()
} else {
"carousel-item".to_string()
}
} else if index == from {
match direction {
SlideDirection::Next => "carousel-item active carousel-item-start".to_string(),
SlideDirection::Prev => "carousel-item active carousel-item-end".to_string(),
}
} else if index == to {
match direction {
SlideDirection::Next => {
"carousel-item carousel-item-next carousel-item-start".to_string()
}
SlideDirection::Prev => {
"carousel-item carousel-item-prev carousel-item-end".to_string()
}
}
} else {
"carousel-item".to_string()
}
}
None => {
if index == current {
"carousel-item active".to_string()
} else {
"carousel-item".to_string()
}
}
}
}
fn build_slide_style(
index: usize,
_current: usize,
trans: Option<(usize, usize, SlideDirection)>,
fade: bool,
) -> String {
if fade {
return String::new();
}
match trans {
Some((from, to, _direction)) => {
if index == from || index == to {
"transition: transform 0.6s ease-in-out;".to_string()
} else {
String::new()
}
}
None => String::new(),
}
}