1use egui::{pos2, vec2, FontId, Response, Sense, Stroke, Ui, Widget};
17use egui_components_theme::{mix, Theme};
18
19pub struct Radio {
20 selected: bool,
21 label: Option<String>,
22 disabled: bool,
23}
24
25impl Radio {
26 pub fn new(selected: bool, label: impl Into<String>) -> Self {
27 Self {
28 selected,
29 label: Some(label.into()),
30 disabled: false,
31 }
32 }
33 pub fn bare(selected: bool) -> Self {
35 Self {
36 selected,
37 label: None,
38 disabled: false,
39 }
40 }
41 pub fn disabled(mut self, d: bool) -> Self {
42 self.disabled = d;
43 self
44 }
45
46 pub fn selectable<'a, T: PartialEq>(
50 current: &'a mut T,
51 value: T,
52 label: impl Into<String>,
53 ) -> SelectableRadio<'a, T> {
54 SelectableRadio {
55 radio: Radio::new(*current == value, label),
56 current,
57 value,
58 }
59 }
60}
61
62impl Widget for Radio {
63 fn ui(self, ui: &mut Ui) -> Response {
64 let theme = Theme::get(ui.ctx());
65 let c = theme.colors;
66 let m = theme.metrics;
67 let size = m.checkbox_size;
68 let gap = 8.0;
69 let font = FontId::proportional(m.font_size_md);
70
71 let label_galley = self.label.as_ref().map(|t| {
72 ui.ctx()
73 .fonts_mut(|f| f.layout_no_wrap(t.clone(), font.clone(), c.foreground))
74 });
75 let label_w = label_galley.as_ref().map(|g| g.size().x + gap).unwrap_or(0.0);
76 let label_h = label_galley.as_ref().map(|g| g.size().y).unwrap_or(0.0);
77
78 let desired = vec2(size + label_w, size.max(label_h));
79 let sense = if self.disabled {
80 Sense::hover()
81 } else {
82 Sense::click()
83 };
84 let (rect, response) = ui.allocate_exact_size(desired, sense);
85
86 if ui.is_rect_visible(rect) {
87 let center = pos2(rect.left() + size * 0.5, rect.center().y);
88 let radius = size * 0.5;
89 let painter = ui.painter();
90
91 let (ring, dot) = if self.disabled {
92 (mix(c.input_border, c.background, 0.4), mix(c.primary_background, c.background, 0.4))
93 } else if self.selected {
94 (c.primary_background, c.primary_background)
95 } else {
96 (c.input_border, c.primary_background)
97 };
98
99 painter.circle(
100 center,
101 radius,
102 c.background,
103 Stroke::new(m.border_width + if self.selected { 0.5 } else { 0.0 }, ring),
104 );
105 if self.selected {
106 painter.circle_filled(center, radius * 0.5, dot);
107 }
108
109 if response.has_focus() {
110 painter.circle_stroke(center, radius + 2.5, theme.focus_ring());
111 }
112
113 if let Some(g) = label_galley {
114 let pos = pos2(rect.left() + size + gap, rect.center().y - g.size().y * 0.5);
115 let color = if self.disabled {
116 c.muted_foreground
117 } else {
118 c.foreground
119 };
120 painter.galley_with_override_text_color(pos, g, color);
121 }
122
123 if !self.disabled && response.hovered() {
124 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
125 }
126 }
127
128 response
129 }
130}
131
132pub struct SelectableRadio<'a, T: PartialEq> {
134 radio: Radio,
135 current: &'a mut T,
136 value: T,
137}
138
139impl<T: PartialEq> Widget for SelectableRadio<'_, T> {
140 fn ui(self, ui: &mut Ui) -> Response {
141 let mut response = self.radio.ui(ui);
142 if response.clicked() && *self.current != self.value {
143 *self.current = self.value;
144 response.mark_changed();
145 }
146 response
147 }
148}