Skip to main content

liora_components/
segmented.rs

1use crate::gpui_compat::element_id;
2use crate::motion::pop_in;
3use gpui::{App, Context, IntoElement, Render, SharedString, Window, div, prelude::*, px};
4use liora_core::Config;
5
6pub struct SegmentedOption {
7    pub label: SharedString,
8    pub value: SharedString,
9    pub disabled: bool,
10}
11
12impl SegmentedOption {
13    pub fn new(label: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
14        Self {
15            label: label.into(),
16            value: value.into(),
17            disabled: false,
18        }
19    }
20
21    pub fn disabled(mut self, disabled: bool) -> Self {
22        self.disabled = disabled;
23        self
24    }
25}
26
27pub struct Segmented {
28    id: SharedString,
29    options: Vec<SegmentedOption>,
30    value: Option<SharedString>,
31    block: bool,
32    on_change: Option<Box<dyn Fn(SharedString, &mut Window, &mut App) + 'static>>,
33}
34
35impl Segmented {
36    pub fn new(options: Vec<SegmentedOption>) -> Self {
37        let first_value = options.first().map(|o| o.value.clone());
38        Self {
39            id: liora_core::unique_id("segmented"),
40            options,
41            value: first_value,
42            block: false,
43            on_change: None,
44        }
45    }
46
47    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
48        self.id = id.into();
49        self
50    }
51
52    pub fn value(mut self, val: impl Into<SharedString>) -> Self {
53        self.value = Some(val.into());
54        self
55    }
56
57    pub fn block(mut self, block: bool) -> Self {
58        self.block = block;
59        self
60    }
61
62    pub fn on_change(mut self, f: impl Fn(SharedString, &mut Window, &mut App) + 'static) -> Self {
63        self.on_change = Some(Box::new(f));
64        self
65    }
66
67    pub fn set_on_change(&mut self, f: impl Fn(SharedString, &mut Window, &mut App) + 'static) {
68        self.on_change = Some(Box::new(f));
69    }
70
71    fn select_option(&mut self, value: SharedString, window: &mut Window, cx: &mut Context<Self>) {
72        if Some(&value) != self.value.as_ref() {
73            self.value = Some(value.clone());
74            if let Some(ref on_change) = self.on_change {
75                (on_change)(value, window, cx);
76            }
77            cx.notify();
78        }
79    }
80}
81
82impl Render for Segmented {
83    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
84        let theme = cx.global::<Config>().theme.clone();
85
86        div()
87            .flex()
88            .flex_row()
89            .items_center()
90            .p(px(2.0))
91            .gap(px(2.0))
92            .bg(theme.neutral.hover)
93            .rounded(px(theme.radius.md))
94            .when(self.block, |s| s.w_full())
95            .children(self.options.iter().enumerate().map(|(i, opt)| {
96                let is_active = self.value.as_ref() == Some(&opt.value);
97                let value = opt.value.clone();
98                let disabled = opt.disabled;
99
100                let option = div()
101                    .id(element_id(format!("{}-option-{}", self.id, i)))
102                    .flex()
103                    .items_center()
104                    .justify_center()
105                    .h(px(28.0))
106                    .px_3()
107                    .rounded(px(theme.radius.sm))
108                    .when(self.block, |s| s.flex_1())
109                    .when(is_active, |s| {
110                        s.bg(theme.neutral.card)
111                            .shadow_sm()
112                            .text_color(theme.neutral.text_1)
113                            .font_weight(gpui::FontWeight::BOLD)
114                    })
115                    .when(!is_active && !disabled, |s| {
116                        s.text_color(theme.neutral.text_2).hover(|s| {
117                            s.cursor_pointer()
118                                .bg(theme.neutral.card.opacity(0.6))
119                                .text_color(theme.neutral.text_1)
120                        })
121                    })
122                    .when(disabled, |s| {
123                        s.text_color(theme.neutral.text_3)
124                            .opacity(0.5)
125                            .cursor_not_allowed()
126                    })
127                    .when(!disabled && !is_active, |s| {
128                        s.cursor_pointer().on_click(cx.listener({
129                            let value = value.clone();
130                            move |this, _, window, cx| {
131                                this.select_option(value.clone(), window, cx);
132                            }
133                        }))
134                    })
135                    .child(div().text_sm().child(opt.label.clone()));
136
137                if is_active {
138                    pop_in(
139                        element_id(format!("{}-option-motion-{}", self.id, i)),
140                        option,
141                    )
142                    .into_any_element()
143                } else {
144                    option.into_any_element()
145                }
146            }))
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    #[test]
153    fn segmented_supports_runtime_on_change_binding() {
154        let source = include_str!("segmented.rs");
155        assert!(source.contains("pub fn set_on_change"));
156        assert!(source.contains("on_change: Option"));
157    }
158}