1use crate::{Theme, theme};
3use iced::border;
4use iced_core::event::{self, Event};
5use iced_core::layout;
6use iced_core::mouse;
7use iced_core::overlay;
8use iced_core::renderer;
9use iced_core::touch;
10use iced_core::widget::tree::Tree;
11use iced_core::{
12 Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget,
13};
14
15use iced_widget::radio as iced_radio;
16pub use iced_widget::radio::Catalog;
17
18pub fn radio<'a, Message: Clone, V, F>(
19 label: impl Into<Element<'a, Message, Theme, crate::Renderer>>,
20 value: V,
21 selected: Option<V>,
22 f: F,
23) -> Radio<'a, Message, crate::Renderer>
24where
25 V: Eq + Copy,
26 F: FnOnce(V) -> Message,
27{
28 Radio::new(label, value, selected, f)
29}
30
31#[allow(missing_debug_implementations)]
89pub struct Radio<'a, Message, Renderer = crate::Renderer>
90where
91 Renderer: iced_core::Renderer,
92{
93 is_selected: bool,
94 on_click: Message,
95 label: Option<Element<'a, Message, Theme, Renderer>>,
96 width: Length,
97 size: f32,
98 spacing: f32,
99}
100
101impl<'a, Message, Renderer> Radio<'a, Message, Renderer>
102where
103 Message: Clone,
104 Renderer: iced_core::Renderer,
105{
106 pub const DEFAULT_SIZE: f32 = 16.0;
108
109 pub fn new<T, F, V>(label: T, value: V, selected: Option<V>, f: F) -> Self
118 where
119 V: Eq + Copy,
120 F: FnOnce(V) -> Message,
121 T: Into<Element<'a, Message, Theme, Renderer>>,
122 {
123 Radio {
124 is_selected: Some(value) == selected,
125 on_click: f(value),
126 label: Some(label.into()),
127 width: Length::Shrink,
128 size: Self::DEFAULT_SIZE,
129 spacing: theme::spacing().space_xs as f32,
130 }
131 }
132
133 pub(crate) fn new_no_label<V, F>(value: V, selected: Option<V>, f: F) -> Self
138 where
139 V: Eq + Copy,
140 F: FnOnce(V) -> Message,
141 {
142 Radio {
143 is_selected: Some(value) == selected,
144 on_click: f(value),
145 label: None,
146 width: Length::Shrink,
147 size: Self::DEFAULT_SIZE,
148 spacing: theme::spacing().space_xs as f32,
149 }
150 }
151
152 #[must_use]
153 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
155 self.size = size.into().0;
156 self
157 }
158
159 #[must_use]
160 pub fn width(mut self, width: impl Into<Length>) -> Self {
162 self.width = width.into();
163 self
164 }
165
166 #[must_use]
167 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
169 self.spacing = spacing.into().0;
170 self
171 }
172}
173
174impl<Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'_, Message, Renderer>
175where
176 Message: Clone,
177 Renderer: iced_core::Renderer,
178{
179 fn children(&self) -> Vec<Tree> {
180 if let Some(label) = &self.label {
181 vec![Tree::new(label)]
182 } else {
183 vec![]
184 }
185 }
186
187 fn diff(&mut self, tree: &mut Tree) {
188 if let Some(label) = &mut self.label {
189 tree.diff_children(std::slice::from_mut(label));
190 }
191 }
192 fn size(&self) -> Size<Length> {
193 Size {
194 width: self.width,
195 height: Length::Shrink,
196 }
197 }
198
199 fn layout(
200 &mut self,
201 tree: &mut Tree,
202 renderer: &Renderer,
203 limits: &layout::Limits,
204 ) -> layout::Node {
205 if let Some(label) = &mut self.label {
206 layout::next_to_each_other(
207 &limits.width(self.width),
208 self.spacing,
209 |_| layout::Node::new(Size::new(self.size, self.size)),
210 |limits| {
211 label
212 .as_widget_mut()
213 .layout(&mut tree.children[0], renderer, limits)
214 },
215 )
216 } else {
217 layout::Node::new(Size::new(self.size, self.size))
218 }
219 }
220
221 fn operate(
222 &mut self,
223 tree: &mut Tree,
224 layout: Layout<'_>,
225 renderer: &Renderer,
226 operation: &mut dyn iced_core::widget::Operation<()>,
227 ) {
228 if let Some(label) = &mut self.label {
229 label.as_widget_mut().operate(
230 &mut tree.children[0],
231 layout.children().nth(1).unwrap(),
232 renderer,
233 operation,
234 );
235 }
236 }
237
238 fn update(
239 &mut self,
240 tree: &mut Tree,
241 event: &Event,
242 layout: Layout<'_>,
243 cursor: mouse::Cursor,
244 renderer: &Renderer,
245 clipboard: &mut dyn Clipboard,
246 shell: &mut Shell<'_, Message>,
247 viewport: &Rectangle,
248 ) {
249 if let Some(label) = &mut self.label {
250 label.as_widget_mut().update(
251 &mut tree.children[0],
252 event,
253 layout.children().nth(1).unwrap(),
254 cursor,
255 renderer,
256 clipboard,
257 shell,
258 viewport,
259 );
260 }
261
262 if !shell.is_event_captured() {
263 match event {
264 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
265 | Event::Touch(touch::Event::FingerLifted { .. }) => {
266 if cursor.is_over(layout.bounds()) {
267 shell.publish(self.on_click.clone());
268 shell.capture_event();
269 return;
270 }
271 }
272 _ => {}
273 }
274 }
275 }
276
277 fn mouse_interaction(
278 &self,
279 tree: &Tree,
280 layout: Layout<'_>,
281 cursor: mouse::Cursor,
282 viewport: &Rectangle,
283 renderer: &Renderer,
284 ) -> mouse::Interaction {
285 let interaction = if let Some(label) = &self.label {
286 label.as_widget().mouse_interaction(
287 &tree.children[0],
288 layout.children().nth(1).unwrap(),
289 cursor,
290 viewport,
291 renderer,
292 )
293 } else {
294 mouse::Interaction::default()
295 };
296
297 if interaction == mouse::Interaction::default() {
298 if cursor.is_over(layout.bounds()) {
299 mouse::Interaction::Pointer
300 } else {
301 mouse::Interaction::default()
302 }
303 } else {
304 interaction
305 }
306 }
307
308 fn draw(
309 &self,
310 tree: &Tree,
311 renderer: &mut Renderer,
312 theme: &Theme,
313 style: &renderer::Style,
314 layout: Layout<'_>,
315 cursor: mouse::Cursor,
316 viewport: &Rectangle,
317 ) {
318 let is_mouse_over = cursor.is_over(layout.bounds());
319
320 let custom_style = if is_mouse_over {
321 theme.style(
322 &(),
323 iced_radio::Status::Hovered {
324 is_selected: self.is_selected,
325 },
326 )
327 } else {
328 theme.style(
329 &(),
330 iced_radio::Status::Active {
331 is_selected: self.is_selected,
332 },
333 )
334 };
335
336 let (dot_bounds, label_layout) = if self.label.is_some() {
337 let mut children = layout.children();
338 let dot_bounds = children.next().unwrap().bounds();
339 (dot_bounds, children.next())
340 } else {
341 (layout.bounds(), None)
342 };
343
344 {
345 let size = dot_bounds.width;
346 let dot_size = 6.0;
347
348 renderer.fill_quad(
349 renderer::Quad {
350 bounds: dot_bounds,
351 border: Border {
352 radius: (size / 2.0).into(),
353 width: custom_style.border_width,
354 color: custom_style.border_color,
355 },
356 ..renderer::Quad::default()
357 },
358 custom_style.background,
359 );
360
361 if self.is_selected {
362 renderer.fill_quad(
363 renderer::Quad {
364 bounds: Rectangle {
365 x: dot_bounds.x + (size - dot_size) / 2.0,
366 y: dot_bounds.y + (size - dot_size) / 2.0,
367 width: dot_size,
368 height: dot_size,
369 },
370 border: border::rounded(dot_size / 2.0),
371 ..renderer::Quad::default()
372 },
373 custom_style.dot_color,
374 );
375 }
376 }
377
378 if let (Some(label), Some(label_layout)) = (&self.label, label_layout) {
379 label.as_widget().draw(
380 &tree.children[0],
381 renderer,
382 theme,
383 style,
384 label_layout,
385 cursor,
386 viewport,
387 );
388 }
389 }
390
391 fn overlay<'b>(
392 &'b mut self,
393 tree: &'b mut Tree,
394 layout: Layout<'b>,
395 renderer: &Renderer,
396 viewport: &Rectangle,
397 translation: Vector,
398 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
399 self.label.as_mut()?.as_widget_mut().overlay(
400 &mut tree.children[0],
401 layout.children().nth(1).unwrap(),
402 renderer,
403 viewport,
404 translation,
405 )
406 }
407
408 fn drag_destinations(
409 &self,
410 state: &Tree,
411 layout: Layout<'_>,
412 renderer: &Renderer,
413 dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
414 ) {
415 if let Some(label) = &self.label {
416 label.as_widget().drag_destinations(
417 &state.children[0],
418 layout.children().nth(1).unwrap(),
419 renderer,
420 dnd_rectangles,
421 );
422 }
423 }
424}
425
426impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>>
427 for Element<'a, Message, Theme, Renderer>
428where
429 Message: 'a + Clone,
430 Renderer: 'a + iced_core::Renderer,
431{
432 fn from(radio: Radio<'a, Message, Renderer>) -> Element<'a, Message, Theme, Renderer> {
433 Element::new(radio)
434 }
435}