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, Rectangle, Shell, Size,
71 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>(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 _clipboard: &mut dyn Clipboard,
327 shell: &mut Shell<'_, Message>,
328 _viewport: &Rectangle,
329 ) {
330 match event {
331 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
332 | Event::Touch(touch::Event::FingerPressed { .. }) => {
333 if cursor.is_over(layout.bounds()) {
334 shell.publish(self.on_click.clone());
335 shell.capture_event();
336 }
337 }
338 _ => {}
339 }
340
341 let current_status = {
342 let is_mouse_over = cursor.is_over(layout.bounds());
343 let is_selected = self.is_selected;
344
345 if is_mouse_over {
346 Status::Hovered { is_selected }
347 } else {
348 Status::Active { is_selected }
349 }
350 };
351
352 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
353 self.last_status = Some(current_status);
354 } else if self
355 .last_status
356 .is_some_and(|last_status| last_status != current_status)
357 {
358 shell.request_redraw();
359 }
360 }
361
362 fn mouse_interaction(
363 &self,
364 _tree: &Tree,
365 layout: Layout<'_>,
366 cursor: mouse::Cursor,
367 _viewport: &Rectangle,
368 _renderer: &Renderer,
369 ) -> mouse::Interaction {
370 if cursor.is_over(layout.bounds()) {
371 mouse::Interaction::Pointer
372 } else {
373 mouse::Interaction::default()
374 }
375 }
376
377 fn draw(
378 &self,
379 tree: &Tree,
380 renderer: &mut Renderer,
381 theme: &Theme,
382 defaults: &renderer::Style,
383 layout: Layout<'_>,
384 _cursor: mouse::Cursor,
385 viewport: &Rectangle,
386 ) {
387 let mut children = layout.children();
388
389 let style = theme.style(
390 &self.class,
391 self.last_status.unwrap_or(Status::Active {
392 is_selected: self.is_selected,
393 }),
394 );
395
396 {
397 let layout = children.next().unwrap();
398 let bounds = layout.bounds();
399
400 let size = bounds.width;
401 let dot_size = size / 2.0;
402
403 renderer.fill_quad(
404 renderer::Quad {
405 bounds,
406 border: Border {
407 radius: (size / 2.0).into(),
408 width: style.border_width,
409 color: style.border_color,
410 },
411 ..renderer::Quad::default()
412 },
413 style.background,
414 );
415
416 if self.is_selected {
417 renderer.fill_quad(
418 renderer::Quad {
419 bounds: Rectangle {
420 x: bounds.x + dot_size / 2.0,
421 y: bounds.y + dot_size / 2.0,
422 width: bounds.width - dot_size,
423 height: bounds.height - dot_size,
424 },
425 border: border::rounded(dot_size / 2.0),
426 ..renderer::Quad::default()
427 },
428 style.dot_color,
429 );
430 }
431 }
432
433 {
434 let label_layout = children.next().unwrap();
435 let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
436
437 crate::text::draw(
438 renderer,
439 defaults,
440 label_layout.bounds(),
441 state.raw(),
442 crate::text::Style {
443 color: style.text_color,
444 },
445 viewport,
446 );
447 }
448 }
449}
450
451impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>>
452 for Element<'a, Message, Theme, Renderer>
453where
454 Message: 'a + Clone,
455 Theme: 'a + Catalog,
456 Renderer: 'a + text::Renderer,
457{
458 fn from(radio: Radio<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> {
459 Element::new(radio)
460 }
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
465pub enum Status {
466 Active {
468 is_selected: bool,
470 },
471 Hovered {
473 is_selected: bool,
475 },
476}
477
478#[derive(Debug, Clone, Copy, PartialEq)]
480pub struct Style {
481 pub background: Background,
483 pub dot_color: Color,
485 pub border_width: f32,
487 pub border_color: Color,
489 pub text_color: Option<Color>,
491}
492
493pub trait Catalog {
495 type Class<'a>;
497
498 fn default<'a>() -> Self::Class<'a>;
500
501 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
503}
504
505pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
507
508impl Catalog for Theme {
509 type Class<'a> = StyleFn<'a, Self>;
510
511 fn default<'a>() -> Self::Class<'a> {
512 Box::new(default)
513 }
514
515 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
516 class(self, status)
517 }
518}
519
520pub fn default(theme: &Theme, status: Status) -> Style {
522 let palette = theme.extended_palette();
523
524 let active = Style {
525 background: Color::TRANSPARENT.into(),
526 dot_color: palette.primary.strong.color,
527 border_width: 1.0,
528 border_color: palette.primary.strong.color,
529 text_color: None,
530 };
531
532 match status {
533 Status::Active { .. } => active,
534 Status::Hovered { .. } => Style {
535 dot_color: palette.primary.strong.color,
536 background: palette.primary.weak.color.into(),
537 ..active
538 },
539 }
540}