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