1use std::rc::Rc;
2
3use crate::{
4 checkbox::checkbox_check_icon, h_flex, text::Text, v_flex, ActiveTheme, AxisExt,
5 FocusableExt as _, Sizable, Size, StyledExt,
6};
7use gpui::{
8 div, prelude::FluentBuilder, px, relative, rems, AnyElement, App, Axis, Div, ElementId,
9 InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
10 StatefulInteractiveElement, StyleRefinement, Styled, Window,
11};
12
13#[derive(IntoElement)]
17pub struct Radio {
18 base: Div,
19 style: StyleRefinement,
20 id: ElementId,
21 label: Option<Text>,
22 children: Vec<AnyElement>,
23 checked: bool,
24 disabled: bool,
25 tab_stop: bool,
26 tab_index: isize,
27 size: Size,
28 on_click: Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
29}
30
31impl Radio {
32 pub fn new(id: impl Into<ElementId>) -> Self {
33 Self {
34 id: id.into(),
35 base: div(),
36 style: StyleRefinement::default(),
37 label: None,
38 children: Vec::new(),
39 checked: false,
40 disabled: false,
41 tab_index: 0,
42 tab_stop: true,
43 size: Size::default(),
44 on_click: None,
45 }
46 }
47
48 pub fn label(mut self, label: impl Into<Text>) -> Self {
49 self.label = Some(label.into());
50 self
51 }
52
53 pub fn checked(mut self, checked: bool) -> Self {
54 self.checked = checked;
55 self
56 }
57
58 pub fn disabled(mut self, disabled: bool) -> Self {
59 self.disabled = disabled;
60 self
61 }
62
63 pub fn tab_index(mut self, tab_index: isize) -> Self {
65 self.tab_index = tab_index;
66 self
67 }
68
69 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
71 self.tab_stop = tab_stop;
72 self
73 }
74
75 pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
76 self.on_click = Some(Rc::new(handler));
77 self
78 }
79
80 fn handle_click(
81 on_click: &Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
82 checked: bool,
83 window: &mut Window,
84 cx: &mut App,
85 ) {
86 let new_checked = !checked;
87 if let Some(f) = on_click {
88 (f)(&new_checked, window, cx);
89 }
90 }
91}
92
93impl Sizable for Radio {
94 fn with_size(mut self, size: impl Into<Size>) -> Self {
95 self.size = size.into();
96 self
97 }
98}
99
100impl Styled for Radio {
101 fn style(&mut self) -> &mut gpui::StyleRefinement {
102 &mut self.style
103 }
104}
105impl InteractiveElement for Radio {
106 fn interactivity(&mut self) -> &mut gpui::Interactivity {
107 self.base.interactivity()
108 }
109}
110impl StatefulInteractiveElement for Radio {}
111
112impl ParentElement for Radio {
113 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
114 self.children.extend(elements);
115 }
116}
117
118impl RenderOnce for Radio {
119 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
120 let checked = self.checked;
121 let focus_handle = window
122 .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
123 .read(cx)
124 .clone();
125 let is_focused = focus_handle.is_focused(window);
126 let disabled = self.disabled;
127
128 let (border_color, bg) = if checked {
129 (cx.theme().primary, cx.theme().primary)
130 } else {
131 (cx.theme().input, cx.theme().input.opacity(0.3))
132 };
133 let (border_color, bg) = if disabled {
134 (border_color.opacity(0.5), bg.opacity(0.5))
135 } else {
136 (border_color, bg)
137 };
138
139 div().child(
141 self.base
142 .id(self.id.clone())
143 .when(!self.disabled, |this| {
144 this.track_focus(
145 &focus_handle
146 .tab_stop(self.tab_stop)
147 .tab_index(self.tab_index),
148 )
149 })
150 .h_flex()
151 .gap_x_2()
152 .text_color(cx.theme().foreground)
153 .items_start()
154 .line_height(relative(1.))
155 .rounded(cx.theme().radius * 0.5)
156 .focus_ring(is_focused, px(2.), window, cx)
157 .map(|this| match self.size {
158 Size::XSmall => this.text_xs(),
159 Size::Small => this.text_sm(),
160 Size::Medium => this.text_base(),
161 Size::Large => this.text_lg(),
162 _ => this,
163 })
164 .refine_style(&self.style)
165 .child(
166 div()
167 .relative()
168 .map(|this| match self.size {
169 Size::XSmall => this.size_3(),
170 Size::Small => this.size_3p5(),
171 Size::Medium => this.size_4(),
172 Size::Large => this.size(rems(1.125)),
173 _ => this.size_4(),
174 })
175 .flex_shrink_0()
176 .rounded_full()
177 .border_1()
178 .border_color(border_color)
179 .when(cx.theme().shadow && !disabled, |this| this.shadow_xs())
180 .map(|this| match self.checked {
181 false => this.bg(cx.theme().background),
182 _ => this.bg(bg),
183 })
184 .child(checkbox_check_icon(
185 self.id, self.size, checked, disabled, window, cx,
186 )),
187 )
188 .when(!self.children.is_empty() || self.label.is_some(), |this| {
189 this.child(
190 v_flex()
191 .w_full()
192 .line_height(relative(1.2))
193 .gap_1()
194 .when_some(self.label, |this, label| {
195 this.child(
196 div()
197 .size_full()
198 .overflow_hidden()
199 .line_height(relative(1.))
200 .when(self.disabled, |this| {
201 this.text_color(cx.theme().muted_foreground)
202 })
203 .child(label),
204 )
205 })
206 .children(self.children),
207 )
208 })
209 .on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
210 window.prevent_default();
212 })
213 .when(!self.disabled, |this| {
214 this.on_click({
215 let on_click = self.on_click.clone();
216 move |_, window, cx| {
217 window.prevent_default();
218 Self::handle_click(&on_click, checked, window, cx);
219 }
220 })
221 }),
222 )
223 }
224}
225
226#[derive(IntoElement)]
228pub struct RadioGroup {
229 id: ElementId,
230 style: StyleRefinement,
231 radios: Vec<Radio>,
232 layout: Axis,
233 selected_index: Option<usize>,
234 disabled: bool,
235 on_change: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
236}
237
238impl RadioGroup {
239 fn new(id: impl Into<ElementId>) -> Self {
240 Self {
241 id: id.into(),
242 style: StyleRefinement::default().flex_1(),
243 on_change: None,
244 layout: Axis::Vertical,
245 selected_index: None,
246 disabled: false,
247 radios: vec![],
248 }
249 }
250
251 pub fn vertical(id: impl Into<ElementId>) -> Self {
253 Self::new(id)
254 }
255
256 pub fn horizontal(id: impl Into<ElementId>) -> Self {
258 Self::new(id).layout(Axis::Horizontal)
259 }
260
261 pub fn layout(mut self, layout: Axis) -> Self {
263 self.layout = layout;
264 self
265 }
266
267 pub fn on_change(mut self, handler: impl Fn(&usize, &mut Window, &mut App) + 'static) -> Self {
269 self.on_change = Some(Rc::new(handler));
270 self
271 }
272
273 pub fn selected_index(mut self, index: Option<usize>) -> Self {
275 self.selected_index = index;
276 self
277 }
278
279 pub fn disabled(mut self, disabled: bool) -> Self {
281 self.disabled = disabled;
282 self
283 }
284
285 pub fn child(mut self, child: impl Into<Radio>) -> Self {
287 self.radios.push(child.into());
288 self
289 }
290
291 pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Radio>>) -> Self {
293 self.radios.extend(children.into_iter().map(Into::into));
294 self
295 }
296}
297
298impl Styled for RadioGroup {
299 fn style(&mut self) -> &mut StyleRefinement {
300 &mut self.style
301 }
302}
303
304impl From<&'static str> for Radio {
305 fn from(label: &'static str) -> Self {
306 Self::new(label).label(label)
307 }
308}
309
310impl From<SharedString> for Radio {
311 fn from(label: SharedString) -> Self {
312 Self::new(label.clone()).label(label)
313 }
314}
315
316impl From<String> for Radio {
317 fn from(label: String) -> Self {
318 Self::new(SharedString::from(label.clone())).label(SharedString::from(label))
319 }
320}
321
322impl RenderOnce for RadioGroup {
323 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
324 let on_change = self.on_change;
325 let disabled = self.disabled;
326 let selected_ix = self.selected_index;
327
328 let base = if self.layout.is_vertical() {
329 v_flex()
330 } else {
331 h_flex().w_full().flex_wrap()
332 };
333
334 let mut container = div().id(self.id);
335 *container.style() = self.style;
336
337 container.child(
338 base.gap_3()
339 .children(self.radios.into_iter().enumerate().map(|(ix, mut radio)| {
340 let checked = selected_ix == Some(ix);
341
342 radio.id = ix.into();
343 radio.disabled(disabled).checked(checked).when_some(
344 on_change.clone(),
345 |this, on_change| {
346 this.on_click(move |_, window, cx| {
347 on_change(&ix, window, cx);
348 })
349 },
350 )
351 })),
352 )
353 }
354}