1use crate::core::alignment;
60use crate::core::border::{self, Border};
61use crate::core::layout;
62use crate::core::mouse;
63use crate::core::renderer;
64use crate::core::text;
65use crate::core::touch;
66use crate::core::widget;
67use crate::core::widget::tree::{self, Tree};
68use crate::core::window;
69use crate::core::{
70 Background, Color, Event, Font, Layout, Length, Pixels, Rectangle, Shell, Size, Theme, Widget,
71};
72
73pub struct Radio<'a, Message, Theme = crate::Theme>
132where
133 Theme: Catalog,
134{
135 is_selected: bool,
136 on_click: Message,
137 label: String,
138 width: Length,
139 size: f32,
140 spacing: f32,
141 text_size: Option<Pixels>,
142 line_height: Option<text::LineHeight>,
143 shaping: text::Shaping,
144 wrapping: text::Wrapping,
145 font: Option<Font>,
146 class: Theme::Class<'a>,
147 last_status: Option<Status>,
148}
149
150impl<'a, Message, Theme> Radio<'a, Message, Theme>
151where
152 Message: Clone,
153 Theme: Catalog,
154{
155 pub const DEFAULT_SIZE: f32 = 16.0;
157
158 pub const DEFAULT_SPACING: f32 = 8.0;
160
161 pub fn new<F, V>(label: impl Into<String>, value: V, selected: Option<V>, f: F) -> Self
170 where
171 V: Eq + Copy,
172 F: FnOnce(V) -> Message,
173 {
174 Radio {
175 is_selected: Some(value) == selected,
176 on_click: f(value),
177 label: label.into(),
178 width: Length::Shrink,
179 size: Self::DEFAULT_SIZE,
180 spacing: Self::DEFAULT_SPACING,
181 text_size: None,
182 line_height: None,
183 shaping: text::Shaping::default(),
184 wrapping: text::Wrapping::default(),
185 font: None,
186 class: Theme::default(),
187 last_status: None,
188 }
189 }
190
191 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
193 self.size = size.into().0;
194 self
195 }
196
197 pub fn width(mut self, width: impl Into<Length>) -> Self {
199 self.width = width.into();
200 self
201 }
202
203 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
205 self.spacing = spacing.into().0;
206 self
207 }
208
209 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
211 self.text_size = Some(text_size.into());
212 self
213 }
214
215 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
217 self.line_height = Some(line_height.into());
218 self
219 }
220
221 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
223 self.shaping = shaping;
224 self
225 }
226
227 pub fn wrapping(mut self, wrapping: text::Wrapping) -> Self {
229 self.wrapping = wrapping;
230 self
231 }
232
233 pub fn font(mut self, font: impl Into<Font>) -> Self {
235 self.font = Some(font.into());
236 self
237 }
238
239 #[must_use]
241 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
242 where
243 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
244 {
245 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
246 self
247 }
248
249 #[cfg(feature = "advanced")]
251 #[must_use]
252 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
253 self.class = class.into();
254 self
255 }
256}
257
258impl<Message, Theme> widget::Meta for Radio<'_, Message, Theme> where Theme: Catalog {}
259
260impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Radio<'_, Message, Theme>
261where
262 Message: Clone,
263 Theme: Catalog,
264 Renderer: text::Renderer,
265{
266 fn tag(&self) -> tree::Tag {
267 tree::Tag::of::<text::paragraph::Plain<Renderer::Paragraph>>()
268 }
269
270 fn state(&self) -> tree::State {
271 tree::State::new(text::paragraph::Plain::<Renderer::Paragraph>::default())
272 }
273
274 fn size(&self) -> Size<Length> {
275 Size {
276 width: self.width,
277 height: Length::Shrink,
278 }
279 }
280
281 fn diff(&mut self, tree: &mut Tree) {
282 tree.children.resize_with(2, Tree::empty);
285 }
286
287 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) {
288 let limits = limits.width(self.width);
289
290 let radio = Size::new(self.size, self.size);
291
292 let state = tree
293 .state
294 .downcast_mut::<text::paragraph::Plain<Renderer::Paragraph>>();
295
296 let label = widget::text::layout(
297 state,
298 renderer,
299 &limits.shrink(Size::new(radio.width + self.spacing, 0.0)),
300 &self.label,
301 widget::text::Format {
302 width: self.width,
303 height: Length::Shrink,
304 line_height: self.line_height,
305 size: self.text_size,
306 font: self.font,
307 align_x: text::Alignment::Default,
308 align_y: alignment::Vertical::Top,
309 shaping: self.shaping,
310 wrapping: self.wrapping,
311 ellipsis: text::Ellipsis::default(),
312 },
313 );
314
315 layout::next_to_each_other(tree, radio, label, self.spacing);
316 }
317
318 fn update(
319 &mut self,
320 _tree: &mut Tree,
321 event: &Event,
322 layout: Layout,
323 cursor: mouse::Cursor,
324 _renderer: &Renderer,
325 shell: &mut Shell<'_, Message>,
326 _viewport: &Rectangle,
327 ) {
328 match event {
329 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
330 | Event::Touch(touch::Event::FingerPressed { .. })
331 if cursor.is_over(layout.bounds()) =>
332 {
333 shell.publish(self.on_click.clone());
334 shell.capture_event();
335 }
336 _ => {}
337 }
338
339 let current_status = {
340 let is_mouse_over = cursor.is_over(layout.bounds());
341 let is_selected = self.is_selected;
342
343 if is_mouse_over {
344 Status::Hovered { is_selected }
345 } else {
346 Status::Active { is_selected }
347 }
348 };
349
350 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
351 self.last_status = Some(current_status);
352 } else if self
353 .last_status
354 .is_some_and(|last_status| last_status != current_status)
355 {
356 shell.request_redraw();
357 }
358 }
359
360 fn mouse_interaction(
361 &self,
362 _tree: &Tree,
363 layout: Layout,
364 cursor: mouse::Cursor,
365 _viewport: &Rectangle,
366 _renderer: &Renderer,
367 ) -> mouse::Interaction {
368 if cursor.is_over(layout.bounds()) {
369 mouse::Interaction::Pointer
370 } else {
371 mouse::Interaction::default()
372 }
373 }
374
375 fn draw(
376 &self,
377 tree: &Tree,
378 renderer: &mut Renderer,
379 theme: &Theme,
380 defaults: &renderer::Style,
381 layout: Layout,
382 _cursor: mouse::Cursor,
383 viewport: &Rectangle,
384 ) {
385 let mut children = layout.iter(&tree.children);
386
387 let style = theme.style(
388 &self.class,
389 self.last_status.unwrap_or(Status::Active {
390 is_selected: self.is_selected,
391 }),
392 );
393
394 let state = tree
395 .state
396 .downcast_ref::<text::paragraph::Plain<Renderer::Paragraph>>();
397
398 let (radio_layout, _) = children.next().unwrap();
399 let bounds = radio_layout.bounds();
400
401 let size = bounds.width;
402 let dot_size = size / 2.0;
403
404 renderer.fill_quad(
405 renderer::Quad {
406 bounds,
407 border: Border {
408 radius: (size / 2.0).into(),
409 width: style.border_width,
410 color: style.border_color,
411 },
412 ..renderer::Quad::default()
413 },
414 style.background,
415 );
416
417 if self.is_selected {
418 renderer.fill_quad(
419 renderer::Quad {
420 bounds: Rectangle {
421 x: bounds.x + dot_size / 2.0,
422 y: bounds.y + dot_size / 2.0,
423 width: bounds.width - dot_size,
424 height: bounds.height - dot_size,
425 },
426 border: border::rounded(dot_size / 2.0),
427 ..renderer::Quad::default()
428 },
429 style.dot_color,
430 );
431 }
432
433 let (label_layout, _) = children.next().unwrap();
434
435 crate::text::draw(
436 renderer,
437 defaults,
438 label_layout.bounds(),
439 state.raw(),
440 crate::text::Style {
441 color: style.text_color,
442 selection: None,
443 },
444 theme.selection(),
445 viewport,
446 );
447 }
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452pub enum Status {
453 Active {
455 is_selected: bool,
457 },
458 Hovered {
460 is_selected: bool,
462 },
463}
464
465#[derive(Debug, Clone, Copy, PartialEq)]
467pub struct Style {
468 pub background: Background,
470 pub dot_color: Color,
472 pub border_width: f32,
474 pub border_color: Color,
476 pub text_color: Option<Color>,
478}
479
480pub trait Catalog {
482 type Class<'a>;
484
485 fn default<'a>() -> Self::Class<'a>;
487
488 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
490
491 fn selection(&self) -> Color;
493}
494
495pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
497
498impl Catalog for Theme {
499 type Class<'a> = StyleFn<'a, Self>;
500
501 fn default<'a>() -> Self::Class<'a> {
502 Box::new(default)
503 }
504
505 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
506 class(self, status)
507 }
508
509 fn selection(&self) -> Color {
510 self.palette().background.strongest.color
511 }
512}
513
514pub fn default(theme: &Theme, status: Status) -> Style {
516 let palette = theme.palette();
517
518 let active = Style {
519 background: Color::TRANSPARENT.into(),
520 dot_color: palette.primary.strong.color,
521 border_width: 1.0,
522 border_color: palette.primary.strong.color,
523 text_color: None,
524 };
525
526 match status {
527 Status::Active { .. } => active,
528 Status::Hovered { .. } => Style {
529 dot_color: palette.primary.strong.color,
530 background: palette.primary.weak.color.into(),
531 ..active
532 },
533 }
534}