1use glyphon::Metrics;
2use glyphon::cosmic_text::Align;
3use taffy::prelude::*;
4use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent};
5use winit::keyboard::{Key, ModifiersState, NamedKey};
6
7use crate::framework::{DrawContext, EventContext, Widget};
8use crate::signal::{Signal, SetSignal};
9use crate::WidgetNode;
10
11pub struct RadioButton {
12 label: String,
13 index: usize,
14 selected: Signal<usize>,
15 set_selected: SetSignal<usize>,
16 metrics: Metrics,
17 circle_size: f32,
18 gap: f32,
19 circle_bg: [f32; 4],
21 circle_border: [f32; 4],
22 dot_color: [f32; 4],
23 text_color: [u8; 3],
24 hover: bool,
26 focus: bool,
27}
28
29impl RadioButton {
30 pub fn new(
31 label: impl Into<String>,
32 index: usize,
33 selected: Signal<usize>,
34 set_selected: SetSignal<usize>,
35 metrics: Metrics,
36 ) -> Self {
37 Self {
38 label: label.into(),
39 index,
40 selected,
41 set_selected,
42 metrics,
43 circle_size: 20.0,
44 gap: 8.0,
45 circle_bg: [0.16, 0.28, 0.38, 1.0],
46 circle_border: [0.4, 0.55, 0.7, 1.0],
47 dot_color: [0.20, 0.65, 0.85, 1.0],
48 text_color: [230, 230, 230],
49 hover: false,
50 focus: false,
51 }
52 }
53
54 pub fn with_circle_size(mut self, size: f32) -> Self {
55 self.circle_size = size;
56 self
57 }
58
59 pub fn with_gap(mut self, gap: f32) -> Self {
60 self.gap = gap;
61 self
62 }
63
64 pub fn with_colors(
65 mut self,
66 circle_bg: [f32; 4],
67 circle_border: [f32; 4],
68 dot_color: [f32; 4],
69 ) -> Self {
70 self.circle_bg = circle_bg;
71 self.circle_border = circle_border;
72 self.dot_color = dot_color;
73 self
74 }
75
76 pub fn with_text_color(mut self, color: [u8; 3]) -> Self {
77 self.text_color = color;
78 self
79 }
80
81 fn select(&self) {
82 self.set_selected.set(self.index);
83 }
84
85 fn is_selected(&self) -> bool {
86 self.selected.get() == self.index
87 }
88
89 fn hit_test(&self, layout: &Layout, x: f32, y: f32) -> bool {
90 x >= layout.location.x
91 && x <= layout.location.x + layout.size.width
92 && y >= layout.location.y
93 && y <= layout.location.y + layout.size.height
94 }
95}
96
97impl Widget for RadioButton {
98 fn style(&self) -> Style {
99 let height = self.circle_size.max(self.metrics.line_height) + 8.0;
100 Style {
101 size: Size {
102 width: Dimension::Auto,
103 height: Dimension::Length(height),
104 },
105 flex_shrink: 0.0,
106 ..Default::default()
107 }
108 }
109
110 fn draw(&self, ctx: &mut DrawContext) {
111 let layout = ctx.layout;
112 let selected = self.is_selected();
113
114 let cx = layout.location.x + 4.0;
116 let cy = layout.location.y + (layout.size.height - self.circle_size) / 2.0;
117 let radius = self.circle_size / 2.0;
118 let border_w = if self.focus { 2.0 } else { 1.0 };
119 let border_c = if self.focus {
120 [0.3, 0.6, 0.9, 1.0]
121 } else {
122 self.circle_border
123 };
124 ctx.renderer.fill_rect_styled(
125 (cx, cy, self.circle_size, self.circle_size),
126 self.circle_bg,
127 radius,
128 border_w,
129 border_c,
130 );
131
132 if selected {
134 let dot_size = self.circle_size * 0.5;
135 let dot_x = cx + (self.circle_size - dot_size) / 2.0;
136 let dot_y = cy + (self.circle_size - dot_size) / 2.0;
137 ctx.renderer.fill_rect_rounded(
138 (dot_x, dot_y, dot_size, dot_size),
139 self.dot_color,
140 dot_size / 2.0,
141 );
142 }
143
144 let text_x = cx + self.circle_size + self.gap;
146 let text_y = layout.location.y + (layout.size.height - self.metrics.line_height) / 2.0;
147 let text_w = (layout.size.width - (text_x - layout.location.x)).max(0.0);
148 ctx.renderer.draw_text(
149 &self.label,
150 (text_x, text_y),
151 self.text_color,
152 (text_w, self.metrics.line_height),
153 self.metrics,
154 Align::Left,
155 );
156 }
157
158 fn handle_event(&mut self, ctx: &mut EventContext) -> bool {
159 let layout = ctx.layout;
160 let mut changed = false;
161 match ctx.event {
162 WindowEvent::CursorMoved { position, .. } => {
163 let over = self.hit_test(layout, position.x as f32, position.y as f32);
164 if over != self.hover {
165 self.hover = over;
166 changed = true;
167 }
168 }
169 WindowEvent::MouseInput {
170 state: ElementState::Pressed,
171 button: MouseButton::Left,
172 ..
173 } => {
174 if self.hover && !self.is_selected() {
175 self.select();
176 changed = true;
177 }
178 }
179 _ => {}
180 }
181 changed
182 }
183
184 fn handle_key_event(&mut self, event: &KeyEvent, _modifiers: ModifiersState) -> bool {
185 if event.state != ElementState::Pressed {
186 return false;
187 }
188 match &event.logical_key {
189 Key::Named(NamedKey::Space) | Key::Named(NamedKey::Enter) => {
190 if !self.is_selected() {
191 self.select();
192 }
193 true
194 }
195 _ => false,
196 }
197 }
198
199 fn is_focusable(&self) -> bool {
200 true
201 }
202
203 fn set_focus(&mut self, focused: bool) {
204 self.focus = focused;
205 }
206
207 fn activate(&mut self) {
208 self.select();
209 }
210}
211
212pub fn radio_group(
214 options: &[&str],
215 selected: Signal<usize>,
216 set_selected: SetSignal<usize>,
217 metrics: Metrics,
218) -> Vec<WidgetNode> {
219 options
220 .iter()
221 .enumerate()
222 .map(|(i, label)| {
223 WidgetNode::new(
224 RadioButton::new(*label, i, selected.clone(), set_selected.clone(), metrics),
225 vec![],
226 )
227 })
228 .collect()
229}