gpui_ui_kit/
stack.rs

1//! Stack layout components
2//!
3//! Vertical and horizontal stack layouts with spacing.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Spacing values
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum StackSpacing {
11    /// No spacing
12    None,
13    /// Extra small (2px)
14    Xs,
15    /// Small (4px)
16    Sm,
17    /// Medium (8px, default)
18    #[default]
19    Md,
20    /// Large (16px)
21    Lg,
22    /// Extra large (24px)
23    Xl,
24    /// 2X large (32px)
25    Xxl,
26}
27
28impl StackSpacing {
29    fn to_pixels(&self) -> Pixels {
30        match self {
31            StackSpacing::None => px(0.0),
32            StackSpacing::Xs => px(2.0),
33            StackSpacing::Sm => px(4.0),
34            StackSpacing::Md => px(8.0),
35            StackSpacing::Lg => px(16.0),
36            StackSpacing::Xl => px(24.0),
37            StackSpacing::Xxl => px(32.0),
38        }
39    }
40}
41
42/// Alignment options
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum StackAlign {
45    /// Align to start
46    Start,
47    /// Center alignment (default)
48    #[default]
49    Center,
50    /// Align to end
51    End,
52    /// Stretch to fill
53    Stretch,
54}
55
56/// Justify options
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum StackJustify {
59    /// Justify to start (default)
60    #[default]
61    Start,
62    /// Center justify
63    Center,
64    /// Justify to end
65    End,
66    /// Space between items
67    SpaceBetween,
68    /// Space around items
69    SpaceAround,
70    /// Space evenly
71    SpaceEvenly,
72}
73
74/// A vertical stack (column) layout
75pub struct VStack {
76    children: Vec<AnyElement>,
77    spacing: StackSpacing,
78    align: StackAlign,
79    justify: StackJustify,
80}
81
82impl VStack {
83    /// Create a new vertical stack
84    pub fn new() -> Self {
85        Self {
86            children: Vec::new(),
87            spacing: StackSpacing::default(),
88            align: StackAlign::Stretch,
89            justify: StackJustify::default(),
90        }
91    }
92
93    /// Add a child element
94    pub fn child(mut self, child: impl IntoElement) -> Self {
95        self.children.push(child.into_any_element());
96        self
97    }
98
99    /// Add multiple children
100    pub fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self {
101        self.children
102            .extend(children.into_iter().map(|c| c.into_any_element()));
103        self
104    }
105
106    /// Set spacing
107    pub fn spacing(mut self, spacing: StackSpacing) -> Self {
108        self.spacing = spacing;
109        self
110    }
111
112    /// Set alignment
113    pub fn align(mut self, align: StackAlign) -> Self {
114        self.align = align;
115        self
116    }
117
118    /// Set justify
119    pub fn justify(mut self, justify: StackJustify) -> Self {
120        self.justify = justify;
121        self
122    }
123
124    /// Build into element
125    pub fn build(self) -> Div {
126        let mut stack = div().flex().flex_col().gap(self.spacing.to_pixels());
127
128        // Apply alignment
129        stack = match self.align {
130            StackAlign::Start => stack.items_start(),
131            StackAlign::Center => stack.items_center(),
132            StackAlign::End => stack.items_end(),
133            StackAlign::Stretch => stack,
134        };
135
136        // Apply justify
137        stack = match self.justify {
138            StackJustify::Start => stack.justify_start(),
139            StackJustify::Center => stack.justify_center(),
140            StackJustify::End => stack.justify_end(),
141            StackJustify::SpaceBetween => stack.justify_between(),
142            StackJustify::SpaceAround => stack.justify_around(),
143            StackJustify::SpaceEvenly => stack,
144        };
145
146        for child in self.children {
147            stack = stack.child(child);
148        }
149
150        stack
151    }
152}
153
154impl Default for VStack {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160impl IntoElement for VStack {
161    type Element = Div;
162
163    fn into_element(self) -> Self::Element {
164        self.build()
165    }
166}
167
168/// A horizontal stack (row) layout
169pub struct HStack {
170    children: Vec<AnyElement>,
171    spacing: StackSpacing,
172    align: StackAlign,
173    justify: StackJustify,
174    wrap: bool,
175}
176
177impl HStack {
178    /// Create a new horizontal stack
179    pub fn new() -> Self {
180        Self {
181            children: Vec::new(),
182            spacing: StackSpacing::default(),
183            align: StackAlign::Center,
184            justify: StackJustify::default(),
185            wrap: false,
186        }
187    }
188
189    /// Add a child element
190    pub fn child(mut self, child: impl IntoElement) -> Self {
191        self.children.push(child.into_any_element());
192        self
193    }
194
195    /// Add multiple children
196    pub fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self {
197        self.children
198            .extend(children.into_iter().map(|c| c.into_any_element()));
199        self
200    }
201
202    /// Set spacing
203    pub fn spacing(mut self, spacing: StackSpacing) -> Self {
204        self.spacing = spacing;
205        self
206    }
207
208    /// Set alignment
209    pub fn align(mut self, align: StackAlign) -> Self {
210        self.align = align;
211        self
212    }
213
214    /// Set justify
215    pub fn justify(mut self, justify: StackJustify) -> Self {
216        self.justify = justify;
217        self
218    }
219
220    /// Enable flex wrap
221    pub fn wrap(mut self, wrap: bool) -> Self {
222        self.wrap = wrap;
223        self
224    }
225
226    /// Build into element
227    pub fn build(self) -> Div {
228        let mut stack = div().flex().gap(self.spacing.to_pixels());
229
230        if self.wrap {
231            stack = stack.flex_wrap();
232        }
233
234        // Apply alignment
235        stack = match self.align {
236            StackAlign::Start => stack.items_start(),
237            StackAlign::Center => stack.items_center(),
238            StackAlign::End => stack.items_end(),
239            StackAlign::Stretch => stack,
240        };
241
242        // Apply justify
243        stack = match self.justify {
244            StackJustify::Start => stack.justify_start(),
245            StackJustify::Center => stack.justify_center(),
246            StackJustify::End => stack.justify_end(),
247            StackJustify::SpaceBetween => stack.justify_between(),
248            StackJustify::SpaceAround => stack.justify_around(),
249            StackJustify::SpaceEvenly => stack,
250        };
251
252        for child in self.children {
253            stack = stack.child(child);
254        }
255
256        stack
257    }
258}
259
260impl Default for HStack {
261    fn default() -> Self {
262        Self::new()
263    }
264}
265
266impl IntoElement for HStack {
267    type Element = Div;
268
269    fn into_element(self) -> Self::Element {
270        self.build()
271    }
272}
273
274/// A spacer element that fills available space
275pub struct Spacer;
276
277impl Spacer {
278    /// Create a new spacer
279    pub fn new() -> Self {
280        Self
281    }
282
283    /// Build into element
284    pub fn build(self) -> Div {
285        div().flex_1()
286    }
287}
288
289impl Default for Spacer {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295impl IntoElement for Spacer {
296    type Element = Div;
297
298    fn into_element(self) -> Self::Element {
299        self.build()
300    }
301}
302
303/// A divider line
304pub struct Divider {
305    id: Option<SharedString>,
306    vertical: bool,
307    color: Option<Rgba>,
308    hover_color: Option<Rgba>,
309    thickness: Option<Pixels>,
310    interactive: bool,
311}
312
313impl Divider {
314    /// Create a new horizontal divider
315    pub fn new() -> Self {
316        Self {
317            id: None,
318            vertical: false,
319            color: None,
320            hover_color: None,
321            thickness: None,
322            interactive: false,
323        }
324    }
325
326    /// Create a vertical divider
327    pub fn vertical() -> Self {
328        Self {
329            id: None,
330            vertical: true,
331            color: None,
332            hover_color: None,
333            thickness: None,
334            interactive: false,
335        }
336    }
337
338    /// Set an ID for the divider (required for interactive dividers)
339    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
340        self.id = Some(id.into());
341        self
342    }
343
344    /// Set custom color
345    pub fn color(mut self, color: Rgba) -> Self {
346        self.color = Some(color);
347        self
348    }
349
350    /// Set hover color (for interactive dividers)
351    pub fn hover_color(mut self, color: Rgba) -> Self {
352        self.hover_color = Some(color);
353        self
354    }
355
356    /// Set custom thickness
357    pub fn thickness(mut self, thickness: Pixels) -> Self {
358        self.thickness = Some(thickness);
359        self
360    }
361
362    /// Make this an interactive resize divider
363    pub fn interactive(mut self) -> Self {
364        self.interactive = true;
365        self
366    }
367
368    /// Build into a stateful element that can have handlers attached
369    /// Use this when you need to add event handlers (e.g., for resize dividers)
370    pub fn build(self) -> Stateful<Div> {
371        let color = self.color.unwrap_or(rgb(0x3a3a3a));
372        let id = self.id.unwrap_or_else(|| SharedString::from("divider"));
373
374        let base = if self.vertical {
375            let thickness = self.thickness.unwrap_or(px(1.0));
376            div().id(id).w(thickness).h_full().bg(color)
377        } else {
378            let thickness = self.thickness.unwrap_or(px(1.0));
379            div().id(id).h(thickness).w_full().bg(color)
380        };
381
382        if self.interactive {
383            let hover_color = self.hover_color.unwrap_or(rgb(0x007acc));
384            let cursor = if self.vertical {
385                gpui::CursorStyle::ResizeLeftRight
386            } else {
387                gpui::CursorStyle::ResizeUpDown
388            };
389            base.cursor(cursor)
390                .hover(move |style| style.bg(hover_color))
391        } else {
392            base
393        }
394    }
395
396    /// Build into a non-stateful element (for simple visual dividers)
397    pub fn build_simple(self) -> Div {
398        let color = self.color.unwrap_or(rgb(0x3a3a3a));
399
400        if self.vertical {
401            let thickness = self.thickness.unwrap_or(px(1.0));
402            div().w(thickness).h_full().bg(color)
403        } else {
404            let thickness = self.thickness.unwrap_or(px(1.0));
405            div().h(thickness).w_full().bg(color)
406        }
407    }
408}
409
410impl Default for Divider {
411    fn default() -> Self {
412        Self::new()
413    }
414}
415
416impl IntoElement for Divider {
417    type Element = Stateful<Div>;
418
419    fn into_element(self) -> Self::Element {
420        self.build()
421    }
422}