ui/components/
carousel.rs1use gpui::{Context, Empty, Hsla, Render};
6
7use crate::prelude::*;
8
9#[derive(Debug)]
11struct CarouselDrag {
12 start_x: Pixels,
13}
14
15pub struct Carousel {
19 slides: Vec<(SharedString, Hsla)>,
20 active_index: usize,
21 drag_offset: Pixels,
22}
23
24impl Carousel {
25 pub fn new(slides: impl IntoIterator<Item = (impl Into<SharedString>, Hsla)>) -> Self {
26 Self {
27 slides: slides
28 .into_iter()
29 .map(|(label, color)| (label.into(), color))
30 .collect(),
31 active_index: 0,
32 drag_offset: px(0.),
33 }
34 }
35
36 pub fn active_index(&self) -> usize {
37 self.active_index
38 }
39
40 fn snap_to_nearest(&mut self, cx: &mut Context<Self>) {
41 if self.slides.is_empty() {
42 return;
43 }
44 let threshold = px(48.);
45 if self.drag_offset < -threshold && self.active_index + 1 < self.slides.len() {
46 self.active_index += 1;
47 } else if self.drag_offset > threshold && self.active_index > 0 {
48 self.active_index -= 1;
49 }
50 self.drag_offset = px(0.);
51 cx.notify();
52 }
53}
54
55impl Render for Carousel {
56 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
57 let count = self.slides.len();
58 let active = self.active_index.min(count.saturating_sub(1));
59 let drag_offset = self.drag_offset;
60 let (label, color) = self
61 .slides
62 .get(active)
63 .cloned()
64 .unwrap_or_else(|| ("No items".into(), palette::neutral(100)));
65
66 let track = div()
67 .id("carousel-track")
68 .w_full()
69 .h(px(200.))
70 .overflow_hidden()
71 .cursor_pointer()
72 .on_drag(
73 CarouselDrag {
74 start_x: window.mouse_position().x,
75 },
76 |_, _, _, cx| cx.new(|_| Empty),
77 )
78 .on_drag_move::<CarouselDrag>(cx.listener(
79 |this, event: &gpui::DragMoveEvent<CarouselDrag>, _, cx| {
80 let drag = event.drag(cx);
81 this.drag_offset = event.event.position.x - drag.start_x;
82 cx.notify();
83 },
84 ))
85 .on_drop::<CarouselDrag>(cx.listener(|this, _, _, cx| {
86 this.snap_to_nearest(cx);
87 }))
88 .child(
89 div()
90 .w_full()
91 .h_full()
92 .ml(drag_offset)
93 .flex()
94 .items_center()
95 .justify_center()
96 .bg(color)
97 .child(Headline::new(label).size(HeadlineSize::Small)),
98 );
99
100 let prev_disabled = active == 0;
101 let next_disabled = active + 1 >= count;
102
103 v_flex()
104 .id("carousel")
105 .w_full()
106 .max_w(px(480.))
107 .gap_3()
108 .child(track)
109 .child(
110 h_flex()
111 .items_center()
112 .justify_between()
113 .child(
114 div().debug_selector(|| "CAROUSEL-PREV".into()).child(
129 IconButton::new("carousel-prev", IconName::ChevronLeft)
130 .disabled(prev_disabled)
131 .when(!prev_disabled, |this| {
132 this.on_click(cx.listener(|this, _, _, cx| {
133 if this.active_index > 0 {
134 this.active_index -= 1;
135 this.drag_offset = px(0.);
136 cx.notify();
137 }
138 }))
139 }),
140 ),
141 )
142 .child(
143 Label::new(format!("{} / {}", active + 1, count.max(1)))
144 .size(LabelSize::Small)
145 .color(Color::Muted),
146 )
147 .child(
148 div().debug_selector(|| "CAROUSEL-NEXT".into()).child(
151 IconButton::new("carousel-next", IconName::ChevronRight)
152 .disabled(next_disabled)
153 .when(!next_disabled, |this| {
154 this.on_click(cx.listener(|this, _, _, cx| {
155 if this.active_index + 1 < this.slides.len() {
156 this.active_index += 1;
157 this.drag_offset = px(0.);
158 cx.notify();
159 }
160 }))
161 }),
162 ),
163 ),
164 )
165 }
166}
167
168#[derive(IntoElement, RegisterComponent)]
170pub struct CarouselPreview;
171
172impl RenderOnce for CarouselPreview {
173 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
174 cx.new(|_| {
175 Carousel::new([
176 ("Slide 1", palette::primary(100)),
177 ("Slide 2", palette::success(100)),
178 ("Slide 3", palette::warning(100)),
179 ])
180 })
181 }
182}
183
184impl Component for CarouselPreview {
185 fn scope() -> ComponentScope {
186 ComponentScope::DataDisplay
187 }
188
189 fn description() -> Option<&'static str> {
190 Some("Horizontal carousel with next/prev navigation and instant drag-snap.")
191 }
192
193 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
194 CarouselPreview.render(window, cx).into_any_element().into()
195 }
196}