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