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