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