1use std::rc::Rc;
8use std::time::Duration;
9
10use gpui::prelude::*;
11use gpui::{div, px, AnyElement, App, Context, EventEmitter, IntoElement, Window};
12
13use crate::devtools::Probed;
14use crate::icon::{Icon, IconName};
15use crate::theme::{theme, Size};
16
17#[derive(Debug, Clone, Copy)]
19pub struct CarouselEvent(pub usize);
20
21type SlideBuilder = Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>;
22
23fn step(current: usize, len: usize, delta: isize, wrap: bool) -> usize {
26 if len == 0 {
27 return 0;
28 }
29 let last = len as isize - 1;
30 let target = current as isize + delta;
31 if wrap {
32 target.rem_euclid(len as isize) as usize
33 } else {
34 target.clamp(0, last) as usize
35 }
36}
37
38pub struct Carousel {
40 slides: Vec<SlideBuilder>,
41 current: usize,
42 wrap: bool,
43 height: f32,
44 autoplay: Option<Duration>,
45 epoch: usize,
47}
48
49impl EventEmitter<CarouselEvent> for Carousel {}
50
51impl Carousel {
52 pub fn new(_cx: &mut Context<Self>) -> Self {
53 Carousel {
54 slides: Vec::new(),
55 current: 0,
56 wrap: true,
57 height: 220.0,
58 autoplay: None,
59 epoch: 0,
60 }
61 }
62
63 pub fn slide<E>(mut self, builder: impl Fn(&mut Window, &mut App) -> E + 'static) -> Self
65 where
66 E: IntoElement,
67 {
68 self.slides.push(Rc::new(move |window, cx| {
69 builder(window, cx).into_any_element()
70 }));
71 self
72 }
73
74 pub fn no_wrap(mut self) -> Self {
76 self.wrap = false;
77 self
78 }
79
80 pub fn height(mut self, height: f32) -> Self {
81 self.height = height.max(40.0);
82 self
83 }
84
85 pub fn autoplay(mut self, every: Duration, cx: &mut Context<Self>) -> Self {
88 self.autoplay = Some(every);
89 self.schedule(cx);
90 self
91 }
92
93 pub fn current(&self) -> usize {
94 self.current
95 }
96
97 pub fn go_to(&mut self, index: usize, cx: &mut Context<Self>) {
98 if index < self.slides.len() && index != self.current {
99 self.current = index;
100 self.changed(cx);
101 }
102 }
103
104 pub fn next(&mut self, cx: &mut Context<Self>) {
105 let target = step(self.current, self.slides.len(), 1, self.wrap);
106 if target != self.current {
107 self.current = target;
108 self.changed(cx);
109 }
110 }
111
112 pub fn prev(&mut self, cx: &mut Context<Self>) {
113 let target = step(self.current, self.slides.len(), -1, self.wrap);
114 if target != self.current {
115 self.current = target;
116 self.changed(cx);
117 }
118 }
119
120 fn changed(&mut self, cx: &mut Context<Self>) {
121 cx.emit(CarouselEvent(self.current));
122 self.schedule(cx);
123 cx.notify();
124 }
125
126 fn schedule(&mut self, cx: &mut Context<Self>) {
127 let Some(every) = self.autoplay else { return };
128 self.epoch += 1;
129 let epoch = self.epoch;
130 cx.spawn(async move |this, cx| {
131 cx.background_executor().timer(every).await;
132 this
133 .update(cx, |carousel, cx| {
134 if carousel.epoch == epoch {
135 carousel.next(cx);
136 }
137 })
138 .ok();
139 })
140 .detach();
141 }
142}
143
144impl Render for Carousel {
145 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
146 let t = theme(cx);
147 let radius = t.radius(t.default_radius);
148 let surface = t.surface().hsla();
149 let surface_hover = t.surface_hover().hsla();
150 let border = t.border().hsla();
151 let dimmed = t.dimmed().hsla();
152 let accent = t.primary().hsla();
153
154 let count = self.slides.len();
155 let content: AnyElement = match self.slides.get(self.current.min(count.saturating_sub(1))) {
156 Some(builder) => builder.clone()(window, cx),
157 None => div().into_any_element(),
158 };
159
160 let mut arrows = Vec::new();
161 for (key, icon, forward) in [
162 ("guise-carousel-prev", IconName::ChevronLeft, false),
163 ("guise-carousel-next", IconName::ChevronRight, true),
164 ] {
165 arrows.push(
166 div()
167 .id(key)
168 .flex()
169 .items_center()
170 .justify_center()
171 .w(px(28.0))
172 .h(px(28.0))
173 .rounded_full()
174 .bg(surface)
175 .border_1()
176 .border_color(border)
177 .text_color(dimmed)
178 .hover(move |s| s.bg(surface_hover))
179 .child(Icon::new(icon).size(Size::Sm))
180 .on_click(cx.listener(move |this, _ev, _window, cx| {
181 if forward {
182 this.next(cx);
183 } else {
184 this.prev(cx);
185 }
186 })),
187 );
188 }
189 let mut arrows = arrows.into_iter();
190
191 let stage = div()
192 .relative()
193 .w_full()
194 .h(px(self.height))
195 .rounded(px(radius))
196 .border_1()
197 .border_color(border)
198 .bg(surface)
199 .overflow_hidden()
200 .child(content)
201 .child(
202 div()
203 .absolute()
204 .inset_0()
205 .flex()
206 .items_center()
207 .justify_between()
208 .px(px(8.0))
209 .child(arrows.next().expect("prev arrow"))
210 .child(arrows.next().expect("next arrow")),
211 );
212
213 let mut dots = div().flex().justify_center().gap(px(6.0)).pt(px(8.0));
214 for i in 0..count {
215 let active = i == self.current;
216 dots = dots.child(
217 div()
218 .id(("guise-carousel-dot", i))
219 .w(px(if active { 18.0 } else { 8.0 }))
220 .h(px(8.0))
221 .rounded_full()
222 .bg(if active { accent } else { border })
223 .on_click(cx.listener(move |this, _ev, _window, cx| this.go_to(i, cx))),
224 );
225 }
226
227 div()
228 .flex()
229 .flex_col()
230 .w_full()
231 .child(stage)
232 .child(dots)
233 .probe("Carousel")
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::step;
240
241 #[test]
242 fn wrapping_steps_cycle() {
243 assert_eq!(step(0, 3, 1, true), 1);
244 assert_eq!(step(2, 3, 1, true), 0);
245 assert_eq!(step(0, 3, -1, true), 2);
246 }
247
248 #[test]
249 fn clamped_steps_stop_at_the_edges() {
250 assert_eq!(step(2, 3, 1, false), 2);
251 assert_eq!(step(0, 3, -1, false), 0);
252 assert_eq!(step(1, 3, 1, false), 2);
253 }
254
255 #[test]
256 fn empty_deck_is_safe() {
257 assert_eq!(step(0, 0, 1, true), 0);
258 assert_eq!(step(5, 0, -1, false), 0);
259 }
260}