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