gpuikit/elements/
radio_group.rs1use crate::theme::{ActiveTheme, ControlSize, Themeable};
4use crate::traits::control_sized::ControlSized;
5use crate::traits::disableable::Disableable;
6use crate::traits::orientable::{Orientable, Orientation};
7use gpui::{
8 Context, ElementId, EventEmitter, InteractiveElement, IntoElement, MouseButton, ParentElement,
9 Render, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
10};
11
12pub struct RadioGroupChanged<T: Clone> {
14 pub value: T,
15}
16
17#[derive(Clone)]
19pub struct RadioOption<T: Clone> {
20 pub value: T,
21 pub label: SharedString,
22 pub disabled: bool,
23}
24
25impl<T: Clone> RadioOption<T> {
26 pub fn new(value: T, label: impl Into<SharedString>) -> Self {
27 Self {
28 value,
29 label: label.into(),
30 disabled: false,
31 }
32 }
33
34 pub fn disabled(mut self, disabled: bool) -> Self {
35 self.disabled = disabled;
36 self
37 }
38}
39
40pub struct RadioGroup<T: Clone + PartialEq + 'static> {
42 id: ElementId,
43 options: Vec<RadioOption<T>>,
44 selected: Option<T>,
45 disabled: bool,
46 orientation: Orientation,
47 size: ControlSize,
48}
49
50impl<T: Clone + PartialEq + 'static> EventEmitter<RadioGroupChanged<T>> for RadioGroup<T> {}
51
52impl<T: Clone + PartialEq + 'static> RadioGroup<T> {
53 pub fn new(id: impl Into<ElementId>, options: Vec<RadioOption<T>>) -> Self {
54 Self {
55 id: id.into(),
56 options,
57 selected: None,
58 disabled: false,
59 orientation: Orientation::default(),
60 size: ControlSize::default(),
61 }
62 }
63
64 pub fn selected(mut self, value: T) -> Self {
65 self.selected = Some(value);
66 self
67 }
68
69 pub fn selected_option(mut self, value: Option<T>) -> Self {
70 self.selected = value;
71 self
72 }
73
74 pub fn disabled(mut self, disabled: bool) -> Self {
75 self.disabled = disabled;
76 self
77 }
78
79 pub fn get_selected(&self) -> Option<&T> {
80 self.selected.as_ref()
81 }
82
83 pub fn set_selected(&mut self, value: T, cx: &mut Context<Self>) {
84 if self.selected.as_ref() != Some(&value) {
85 self.selected = Some(value.clone());
86 cx.emit(RadioGroupChanged { value });
87 cx.notify();
88 }
89 }
90
91 fn select_option(&mut self, index: usize, cx: &mut Context<Self>) {
92 if let Some(option) = self.options.get(index) {
93 if !option.disabled && !self.disabled {
94 self.set_selected(option.value.clone(), cx);
95 }
96 }
97 }
98}
99
100impl<T: Clone + PartialEq + 'static> Render for RadioGroup<T> {
101 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
102 let theme = cx.theme();
103 let metrics = theme.control(self.size);
104 let group_disabled = self.disabled;
105 let selected = self.selected.clone();
106 let orientation = self.orientation;
107
108 let radio_size = metrics.ink;
114 let dot_size = metrics.ink / 2.;
115
116 let container = if orientation == Orientation::Vertical {
117 div().flex().flex_col().gap(metrics.gap * 2.0)
118 } else {
119 div()
120 .flex()
121 .flex_row()
122 .gap(metrics.gap * 4.0)
123 .items_center()
124 };
125
126 container.id(self.id.clone()).children(
127 self.options
128 .iter()
129 .enumerate()
130 .map(|(index, option)| {
131 let is_selected = selected.as_ref() == Some(&option.value);
132 let is_disabled = group_disabled || option.disabled;
133 let label = option.label.clone();
134
135 let radio_bg = if is_disabled {
136 theme.surface_tertiary()
137 } else {
138 theme.surface()
139 };
140
141 let radio_border = if is_disabled {
142 theme.border_subtle()
143 } else if is_selected {
144 theme.accent()
145 } else {
146 theme.border()
147 };
148
149 let dot_color = if is_disabled {
150 theme.fg_disabled()
151 } else {
152 theme.accent()
153 };
154
155 let text_color = if is_disabled {
156 theme.fg_disabled()
157 } else {
158 theme.fg()
159 };
160
161 div()
162 .id(ElementId::NamedInteger("radio-option".into(), index as u64))
163 .flex()
164 .flex_row()
165 .h(metrics.height)
169 .gap(metrics.gap * 2.0)
170 .items_center()
171 .when(!is_disabled, |this| {
172 this.cursor_pointer()
173 .on_mouse_down(MouseButton::Left, |_, window, _| {
174 window.prevent_default()
175 })
176 .on_click(cx.listener(move |this, _, _, cx| {
177 this.select_option(index, cx);
178 }))
179 })
180 .when(is_disabled, |this| this.cursor_not_allowed().opacity(0.65))
181 .child(
182 div()
183 .size(radio_size)
184 .flex_none()
185 .flex()
186 .items_center()
187 .justify_center()
188 .bg(radio_bg)
189 .border_1()
190 .border_color(radio_border)
191 .rounded_full()
192 .when(!is_disabled, |this| {
193 this.hover(|style| {
194 style.border_color(if is_selected {
195 theme.accent()
196 } else {
197 theme.border_secondary()
198 })
199 })
200 })
201 .when(is_selected, |this| {
202 this.child(div().size(dot_size).bg(dot_color).rounded_full())
203 }),
204 )
205 .child(
206 div()
207 .text_size(metrics.text_size)
208 .line_height(metrics.line_height)
209 .text_color(text_color)
210 .child(label),
211 )
212 })
213 .collect::<Vec<_>>(),
214 )
215 }
216}
217
218pub fn radio_group<T: Clone + PartialEq + 'static>(
220 id: impl Into<ElementId>,
221 options: Vec<RadioOption<T>>,
222) -> RadioGroup<T> {
223 RadioGroup::new(id, options)
224}
225
226pub fn radio_option<T: Clone>(value: T, label: impl Into<SharedString>) -> RadioOption<T> {
228 RadioOption::new(value, label)
229}
230
231impl<T: Clone + PartialEq + 'static> Disableable for RadioGroup<T> {
232 fn is_disabled(&self) -> bool {
233 self.disabled
234 }
235
236 fn disabled(mut self, disabled: bool) -> Self {
237 self.disabled = disabled;
238 self
239 }
240}
241
242impl<T: Clone> Disableable for RadioOption<T> {
243 fn is_disabled(&self) -> bool {
244 self.disabled
245 }
246
247 fn disabled(mut self, disabled: bool) -> Self {
248 self.disabled = disabled;
249 self
250 }
251}
252
253impl<T: Clone + PartialEq + 'static> Orientable for RadioGroup<T> {
254 fn orientation(mut self, orientation: Orientation) -> Self {
255 self.orientation = orientation;
256 self
257 }
258}
259
260impl<T: Clone + PartialEq + 'static> ControlSized for RadioGroup<T> {
261 fn control_size(mut self, size: ControlSize) -> Self {
262 self.size = size;
263 self
264 }
265}