use crate::{prelude::FluentBuilder as _, *};
use std::sync::Arc;
use std::time::Duration;
pub struct CarouselState {
index: usize,
autoplay: Option<Duration>,
looping: bool,
press_x: Option<Pixels>,
pending_advance: bool,
autoplay_running: bool,
on_change: Option<Arc<dyn Fn(usize, usize, &mut Window, &mut App) + Send + Sync>>,
}
impl CarouselState {
pub fn new() -> Self {
Self {
index: 0,
autoplay: None,
looping: true,
press_x: None,
pending_advance: false,
autoplay_running: false,
on_change: None,
}
}
pub fn autoplay(mut self, interval: Duration) -> Self {
self.autoplay = Some(interval);
self
}
pub fn looping(mut self, looping: bool) -> Self {
self.looping = looping;
self
}
pub fn on_change<F>(mut self, handler: F) -> Self
where
F: Fn(usize, usize, &mut Window, &mut App) + Send + Sync + 'static,
{
self.on_change = Some(Arc::new(handler));
self
}
pub fn index(&self) -> usize {
self.index
}
pub fn next(&mut self, count: usize, window: &mut Window, cx: &mut App) {
if count == 0 {
return;
}
let next = if self.index + 1 >= count {
if self.looping { 0 } else { count - 1 }
} else {
self.index + 1
};
self.go_to(next, window, cx);
}
pub fn prev(&mut self, count: usize, window: &mut Window, cx: &mut App) {
if count == 0 {
return;
}
let prev = if self.index == 0 {
if self.looping { count - 1 } else { 0 }
} else {
self.index - 1
};
self.go_to(prev, window, cx);
}
pub fn go_to(&mut self, index: usize, window: &mut Window, cx: &mut App) {
let old = self.index;
self.index = index;
if old != index {
if let Some(ref cb) = self.on_change.clone() {
cb(old, index, window, cx);
}
}
}
fn press(&mut self, x: Pixels) {
self.press_x = Some(x);
}
fn release(&mut self, x: Pixels, count: usize, window: &mut Window, cx: &mut App) {
if let Some(start) = self.press_x.take() {
let dx: f32 = (x - start).into();
if dx <= -24.0 {
self.next(count, window, cx);
} else if dx >= 24.0 {
self.prev(count, window, cx);
}
}
}
fn ensure_autoplay(&mut self, count: usize, cx: &mut Context<Self>) {
let Some(interval) = self.autoplay else {
return;
};
if self.autoplay_running || count == 0 {
return;
}
self.autoplay_running = true;
cx.spawn(async move |this, cx| {
loop {
cx.background_executor().timer(interval).await;
let alive = this
.update(cx, |state, cx| {
state.pending_advance = true;
cx.notify();
})
.is_ok();
if !alive {
break;
}
}
})
.detach();
}
}
impl Default for CarouselState {
fn default() -> Self {
Self::new()
}
}
#[derive(IntoElement)]
pub struct Carousel {
state: Entity<CarouselState>,
children: Vec<AnyElement>,
show_arrows: bool,
show_dots: bool,
style: StyleRefinement,
}
impl Carousel {
pub fn new(state: Entity<CarouselState>) -> Self {
Self {
state,
children: Vec::new(),
show_arrows: true,
show_dots: true,
style: StyleRefinement::default(),
}
}
pub fn arrows(mut self, show: bool) -> Self {
self.show_arrows = show;
self
}
pub fn dots(mut self, show: bool) -> Self {
self.show_dots = show;
self
}
}
impl ParentElement for Carousel {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl Styled for Carousel {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Carousel {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let count = self.children.len();
let (index, show_arrows, show_dots) = self.state.read_with(cx, |state, _| {
(state.index, self.show_arrows, self.show_dots)
});
let index = if count == 0 { 0 } else { index.min(count - 1) };
let state = self.state.clone();
let user_style = self.style;
self.state.update(cx, |state, cx| {
state.ensure_autoplay(count, cx);
});
if self.state.read(cx).pending_advance {
self.state.update(cx, |state, cx| {
state.pending_advance = false;
state.next(count, window, cx);
cx.notify();
});
}
let theme = cx.theme();
let muted_foreground = theme.tokens.muted_foreground;
let accent = theme.tokens.accent.color;
let mut root = div()
.flex()
.flex_col()
.w_full()
.overflow_hidden()
.on_mouse_down(MouseButton::Left, {
let state = state.clone();
move |event: &MouseDownEvent, _, cx| {
state.update(cx, |state, _| state.press(event.position.x));
}
})
.on_mouse_up(MouseButton::Left, {
let state = state.clone();
move |event: &MouseUpEvent, window, cx| {
state.update(cx, |state, cx| {
state.release(event.position.x, count, window, cx);
cx.notify();
});
}
});
if let Some(child) = self.children.into_iter().nth(index) {
root = root.child(div().flex_1().child(child));
}
if show_arrows && count > 1 {
let prev_state = state.clone();
let next_state = state.clone();
root = root.child(
div()
.flex()
.items_center()
.justify_between()
.py(px(4.0))
.child(
Button::new("carousel-prev")
.ghost()
.small()
.icon(IconName::ChevronLeft)
.on_click(move |_, window, cx| {
prev_state.update(cx, |state, cx| {
state.prev(count, window, cx);
cx.notify();
});
}),
)
.child(
Button::new("carousel-next")
.ghost()
.small()
.icon(IconName::ChevronRight)
.on_click(move |_, window, cx| {
next_state.update(cx, |state, cx| {
state.next(count, window, cx);
cx.notify();
});
}),
),
);
}
if show_dots && count > 1 {
let mut dots = div().flex().items_center().justify_center().gap(px(6.0));
for dot_ix in 0..count {
let dot_state = state.clone();
dots = dots.child(
div()
.id(ElementId::named_usize("carousel-dot", dot_ix))
.w(px(if dot_ix == index { 20.0 } else { 8.0 }))
.h(px(8.0))
.rounded_full()
.bg(if dot_ix == index {
accent
} else {
muted_foreground.color.opacity(0.3)
})
.cursor_pointer()
.on_click(move |_, window, cx| {
dot_state.update(cx, |state, cx| {
state.go_to(dot_ix, window, cx);
cx.notify();
});
}),
);
}
root = root.child(dots);
}
root.map(|mut this| {
this.style().refine(&user_style);
this
})
}
}