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