liora_components/
segmented.rs1use 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}