Skip to main content

liora_components/
flex.rs

1use gpui::{
2    AnyElement, App, Component, ElementId, Hsla, IntoElement, Pixels, RenderOnce, ScrollHandle,
3    Window, div, prelude::*, px,
4};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7enum FlexDirection {
8    Row,
9    Column,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13enum CrossAlign {
14    Start,
15    Center,
16    End,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20enum MainAlign {
21    Start,
22    Center,
23    End,
24    Between,
25}
26
27pub struct Flex {
28    children: Vec<AnyElement>,
29    direction: Option<FlexDirection>,
30    wrap: bool,
31    gap: Option<Pixels>,
32    padding: Option<Pixels>,
33    padding_x: Option<Pixels>,
34    padding_y: Option<Pixels>,
35    margin_y: Option<Pixels>,
36    height: Option<Pixels>,
37    width: Option<Pixels>,
38    width_percent: Option<f32>,
39    h_full: bool,
40    w_full: bool,
41    size_full: bool,
42    flex_1: bool,
43    flex_none: bool,
44    min_h_0: bool,
45    bg: Option<Hsla>,
46    text_color: Option<Hsla>,
47    text_size: Option<Pixels>,
48    bold: bool,
49    border: bool,
50    border_color: Option<Hsla>,
51    rounded: Option<Pixels>,
52    relative: bool,
53    overflow_hidden: bool,
54    overflow_y_scroll: bool,
55    id: Option<ElementId>,
56    scroll_handle: Option<ScrollHandle>,
57    align: Option<CrossAlign>,
58    justify: Option<MainAlign>,
59}
60
61impl Flex {
62    pub fn new() -> Self {
63        Self {
64            children: Vec::new(),
65            direction: None,
66            wrap: false,
67            gap: None,
68            padding: None,
69            padding_x: None,
70            padding_y: None,
71            margin_y: None,
72            height: None,
73            width: None,
74            width_percent: None,
75            h_full: false,
76            w_full: false,
77            size_full: false,
78            flex_1: false,
79            flex_none: false,
80            min_h_0: false,
81            bg: None,
82            text_color: None,
83            text_size: None,
84            bold: false,
85            border: false,
86            border_color: None,
87            rounded: None,
88            relative: false,
89            overflow_hidden: false,
90            overflow_y_scroll: false,
91            id: None,
92            scroll_handle: None,
93            align: None,
94            justify: None,
95        }
96    }
97
98    pub fn row(mut self) -> Self {
99        self.direction = Some(FlexDirection::Row);
100        self
101    }
102
103    pub fn column(mut self) -> Self {
104        self.direction = Some(FlexDirection::Column);
105        self
106    }
107
108    pub fn wrap(mut self) -> Self {
109        self.wrap = true;
110        self
111    }
112
113    pub fn gap_px(mut self, gap: f32) -> Self {
114        self.gap = Some(px(gap));
115        self
116    }
117
118    pub fn gap_sm(self) -> Self {
119        self.gap_px(8.0)
120    }
121
122    pub fn gap_md(self) -> Self {
123        self.gap_px(12.0)
124    }
125
126    pub fn gap_lg(self) -> Self {
127        self.gap_px(16.0)
128    }
129
130    pub fn gap_xl(self) -> Self {
131        self.gap_px(24.0)
132    }
133
134    pub fn padding_px(mut self, padding: f32) -> Self {
135        self.padding = Some(px(padding));
136        self
137    }
138
139    pub fn padding_sm(self) -> Self {
140        self.padding_px(8.0)
141    }
142
143    pub fn padding_md(self) -> Self {
144        self.padding_px(16.0)
145    }
146
147    pub fn padding_lg(self) -> Self {
148        self.padding_px(24.0)
149    }
150
151    pub fn padding_x_px(mut self, padding: f32) -> Self {
152        self.padding_x = Some(px(padding));
153        self
154    }
155
156    pub fn padding_x_units(self, padding: f32) -> Self {
157        self.padding_x_px(padding)
158    }
159
160    pub fn padding_y_px(mut self, padding: f32) -> Self {
161        self.padding_y = Some(px(padding));
162        self
163    }
164
165    pub fn margin_y_px(mut self, margin: f32) -> Self {
166        self.margin_y = Some(px(margin));
167        self
168    }
169
170    pub fn margin_y_units(self, margin: f32) -> Self {
171        self.margin_y_px(margin)
172    }
173
174    pub fn height_px(mut self, height: f32) -> Self {
175        self.height = Some(px(height));
176        self
177    }
178
179    pub fn height_units(self, height: f32) -> Self {
180        self.height_px(height)
181    }
182
183    pub fn width_px(mut self, width: f32) -> Self {
184        self.width = Some(px(width));
185        self
186    }
187
188    pub fn width_percent(mut self, percent: f32) -> Self {
189        self.width_percent = Some((percent / 100.0).clamp(0.0, 1.0));
190        self
191    }
192
193    pub fn h_full(mut self) -> Self {
194        self.h_full = true;
195        self
196    }
197
198    pub fn w_full(mut self) -> Self {
199        self.w_full = true;
200        self
201    }
202
203    pub fn size_full(mut self) -> Self {
204        self.size_full = true;
205        self
206    }
207
208    pub fn flex_1(mut self) -> Self {
209        self.flex_1 = true;
210        self
211    }
212
213    pub fn flex_none(mut self) -> Self {
214        self.flex_none = true;
215        self
216    }
217
218    pub fn min_h_0(mut self) -> Self {
219        self.min_h_0 = true;
220        self
221    }
222
223    pub fn bg(mut self, color: Hsla) -> Self {
224        self.bg = Some(color);
225        self
226    }
227
228    pub fn text_color(mut self, color: Hsla) -> Self {
229        self.text_color = Some(color);
230        self
231    }
232
233    pub fn text_size_px(mut self, size: f32) -> Self {
234        self.text_size = Some(px(size));
235        self
236    }
237
238    pub fn text_xs(self) -> Self {
239        self.text_size_px(12.0)
240    }
241
242    pub fn text_sm(self) -> Self {
243        self.text_size_px(14.0)
244    }
245
246    pub fn bold(mut self) -> Self {
247        self.bold = true;
248        self
249    }
250
251    pub fn border(mut self) -> Self {
252        self.border = true;
253        self
254    }
255
256    pub fn border_color(mut self, color: Hsla) -> Self {
257        self.border_color = Some(color);
258        self
259    }
260
261    pub fn rounded_px(mut self, radius: f32) -> Self {
262        self.rounded = Some(px(radius));
263        self
264    }
265
266    pub fn rounded_units(self, radius: f32) -> Self {
267        self.rounded_px(radius)
268    }
269
270    pub fn rounded_md(mut self) -> Self {
271        self.rounded = Some(px(8.0));
272        self
273    }
274
275    pub fn rounded_pill(mut self) -> Self {
276        self.rounded = Some(px(999.0));
277        self
278    }
279
280    pub fn relative(mut self) -> Self {
281        self.relative = true;
282        self
283    }
284
285    pub fn overflow_hidden(mut self) -> Self {
286        self.overflow_hidden = true;
287        self
288    }
289
290    pub fn overflow_y_scroll(mut self) -> Self {
291        self.overflow_y_scroll = true;
292        self
293    }
294
295    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
296        self.id = Some(id.into());
297        self
298    }
299
300    pub fn track_scroll(mut self, handle: &ScrollHandle) -> Self {
301        self.scroll_handle = Some(handle.clone());
302        self
303    }
304
305    pub fn align_start(mut self) -> Self {
306        self.align = Some(CrossAlign::Start);
307        self
308    }
309
310    pub fn align_center(mut self) -> Self {
311        self.align = Some(CrossAlign::Center);
312        self
313    }
314
315    pub fn align_end(mut self) -> Self {
316        self.align = Some(CrossAlign::End);
317        self
318    }
319
320    pub fn justify_start(mut self) -> Self {
321        self.justify = Some(MainAlign::Start);
322        self
323    }
324
325    pub fn justify_center(mut self) -> Self {
326        self.justify = Some(MainAlign::Center);
327        self
328    }
329
330    pub fn justify_end(mut self) -> Self {
331        self.justify = Some(MainAlign::End);
332        self
333    }
334
335    pub fn justify_between(mut self) -> Self {
336        self.justify = Some(MainAlign::Between);
337        self
338    }
339
340    pub fn center(self) -> Self {
341        self.align_center().justify_center()
342    }
343
344    pub fn child(mut self, child: impl IntoElement) -> Self {
345        self.children.push(child.into_any_element());
346        self
347    }
348
349    pub fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self {
350        self.children
351            .extend(children.into_iter().map(|child| child.into_any_element()));
352        self
353    }
354}
355
356impl RenderOnce for Flex {
357    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
358        let mut el = div();
359
360        if self.direction.is_some()
361            || self.align.is_some()
362            || self.justify.is_some()
363            || self.gap.is_some()
364        {
365            el = el.flex();
366        }
367
368        match self.direction {
369            Some(FlexDirection::Row) => el = el.flex_row(),
370            Some(FlexDirection::Column) => el = el.flex_col(),
371            None => {}
372        }
373
374        if self.wrap {
375            el = el.flex_wrap();
376        }
377        if let Some(gap) = self.gap {
378            el = el.gap(gap);
379        }
380        if let Some(padding) = self.padding {
381            el = el.p(padding);
382        }
383        if let Some(padding_x) = self.padding_x {
384            el = el.px(padding_x);
385        }
386        if let Some(padding_y) = self.padding_y {
387            el = el.py(padding_y);
388        }
389        if let Some(margin_y) = self.margin_y {
390            el = el.my(margin_y);
391        }
392        if let Some(height) = self.height {
393            el = el.h(height);
394        }
395        if let Some(width) = self.width {
396            el = el.w(width);
397        }
398        if let Some(width_percent) = self.width_percent {
399            el = el.w(gpui::relative(width_percent));
400        }
401        if self.h_full {
402            el = el.h_full();
403        }
404        if self.w_full {
405            el = el.w_full();
406        }
407        if self.size_full {
408            el = el.size_full();
409        }
410        if self.flex_1 {
411            el = el.flex_1();
412        }
413        if self.flex_none {
414            el = el.flex_none();
415        }
416        if self.min_h_0 {
417            el = el.min_h_0();
418        }
419        if let Some(bg) = self.bg {
420            el = el.bg(bg);
421        }
422        if let Some(color) = self.text_color {
423            el = el.text_color(color);
424        }
425        if let Some(size) = self.text_size {
426            el = el.text_size(size);
427        }
428        if self.bold {
429            el = el.font_weight(gpui::FontWeight::BOLD);
430        }
431        if self.border {
432            el = el.border_1();
433        }
434        if let Some(color) = self.border_color {
435            el = el.border_color(color);
436        }
437        if let Some(radius) = self.rounded {
438            el = el.rounded(radius);
439        }
440        if self.relative {
441            el = el.relative();
442        }
443        if self.overflow_hidden {
444            el = el.overflow_hidden();
445        }
446
447        match self.align {
448            Some(CrossAlign::Start) => el = el.items_start(),
449            Some(CrossAlign::Center) => el = el.items_center(),
450            Some(CrossAlign::End) => el = el.items_end(),
451            None => {}
452        }
453        match self.justify {
454            Some(MainAlign::Start) => el = el.justify_start(),
455            Some(MainAlign::Center) => el = el.justify_center(),
456            Some(MainAlign::End) => el = el.justify_end(),
457            Some(MainAlign::Between) => el = el.justify_between(),
458            None => {}
459        }
460
461        if let Some(id) = self.id {
462            let mut stateful = el.id(id);
463            if self.overflow_y_scroll {
464                stateful = stateful.overflow_y_scroll();
465            }
466            if let Some(scroll_handle) = self.scroll_handle {
467                stateful = stateful.track_scroll(&scroll_handle);
468            }
469            stateful.children(self.children).into_any_element()
470        } else {
471            el.children(self.children).into_any_element()
472        }
473    }
474}
475
476impl IntoElement for Flex {
477    type Element = Component<Self>;
478    fn into_element(self) -> Self::Element {
479        Component::new(self)
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn flex_tracks_scroll_container_configuration() {
489        let handle = ScrollHandle::new();
490        let flex = Flex::new()
491            .column()
492            .height_px(320.0)
493            .id("test-scroll")
494            .overflow_y_scroll()
495            .track_scroll(&handle);
496
497        assert_eq!(flex.direction, Some(FlexDirection::Column));
498        assert_eq!(flex.height, Some(px(320.0)));
499        assert!(flex.overflow_y_scroll);
500        assert!(flex.scroll_handle.is_some());
501    }
502
503    #[test]
504    fn flex_tracks_visual_box_configuration() {
505        let flex = Flex::new()
506            .row()
507            .wrap()
508            .gap_lg()
509            .padding_md()
510            .width_percent(75.0)
511            .rounded_md()
512            .border();
513
514        assert_eq!(flex.direction, Some(FlexDirection::Row));
515        assert!(flex.wrap);
516        assert_eq!(flex.gap, Some(px(16.0)));
517        assert_eq!(flex.padding, Some(px(16.0)));
518        assert_eq!(flex.width_percent, Some(0.75));
519        assert_eq!(flex.rounded, Some(px(8.0)));
520        assert!(flex.border);
521    }
522}