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.update(cx, |carousel, cx| {
133 if carousel.epoch == epoch {
134 carousel.next(cx);
135 }
136 })
137 .ok();
138 })
139 .detach();
140 }
141}
142
143impl Render for Carousel {
144 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
145 let t = theme(cx);
146 let radius = t.radius(t.default_radius);
147 let surface = t.surface().hsla();
148 let surface_hover = t.surface_hover().hsla();
149 let border = t.border().hsla();
150 let dimmed = t.dimmed().hsla();
151 let accent = t.primary().hsla();
152
153 let count = self.slides.len();
154 let content: AnyElement = match self.slides.get(self.current.min(count.saturating_sub(1))) {
155 Some(builder) => builder.clone()(window, cx),
156 None => div().into_any_element(),
157 };
158
159 let mut arrows = Vec::new();
160 for (key, icon, forward) in [
161 ("guise-carousel-prev", IconName::ChevronLeft, false),
162 ("guise-carousel-next", IconName::ChevronRight, true),
163 ] {
164 arrows.push(
165 div()
166 .id(key)
167 .flex()
168 .items_center()
169 .justify_center()
170 .w(px(28.0))
171 .h(px(28.0))
172 .rounded_full()
173 .bg(surface)
174 .border_1()
175 .border_color(border)
176 .text_color(dimmed)
177 .hover(move |s| s.bg(surface_hover))
178 .child(Icon::new(icon).size(Size::Sm))
179 .on_click(cx.listener(move |this, _ev, _window, cx| {
180 if forward {
181 this.next(cx);
182 } else {
183 this.prev(cx);
184 }
185 })),
186 );
187 }
188 let mut arrows = arrows.into_iter();
189
190 let stage = div()
191 .relative()
192 .w_full()
193 .h(px(self.height))
194 .rounded(px(radius))
195 .border_1()
196 .border_color(border)
197 .bg(surface)
198 .overflow_hidden()
199 .child(content)
200 .child(
201 div()
202 .absolute()
203 .inset_0()
204 .flex()
205 .items_center()
206 .justify_between()
207 .px(px(8.0))
208 .child(arrows.next().expect("prev arrow"))
209 .child(arrows.next().expect("next arrow")),
210 );
211
212 let mut dots = div().flex().justify_center().gap(px(6.0)).pt(px(8.0));
213 for i in 0..count {
214 let active = i == self.current;
215 dots = dots.child(
216 div()
217 .id(("guise-carousel-dot", i))
218 .w(px(if active { 18.0 } else { 8.0 }))
219 .h(px(8.0))
220 .rounded_full()
221 .bg(if active { accent } else { border })
222 .on_click(cx.listener(move |this, _ev, _window, cx| this.go_to(i, cx))),
223 );
224 }
225
226 div()
227 .flex()
228 .flex_col()
229 .w_full()
230 .child(stage)
231 .child(dots)
232 .probe("Carousel")
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::step;
239
240 #[test]
241 fn wrapping_steps_cycle() {
242 assert_eq!(step(0, 3, 1, true), 1);
243 assert_eq!(step(2, 3, 1, true), 0);
244 assert_eq!(step(0, 3, -1, true), 2);
245 }
246
247 #[test]
248 fn clamped_steps_stop_at_the_edges() {
249 assert_eq!(step(2, 3, 1, false), 2);
250 assert_eq!(step(0, 3, -1, false), 0);
251 assert_eq!(step(1, 3, 1, false), 2);
252 }
253
254 #[test]
255 fn empty_deck_is_safe() {
256 assert_eq!(step(0, 0, 1, true), 0);
257 assert_eq!(step(5, 0, -1, false), 0);
258 }
259}