egui/widgets/
radio_button.rs

1use crate::{
2    epaint, pos2, vec2, NumExt, Response, Sense, TextStyle, Ui, Vec2, Widget, WidgetInfo,
3    WidgetText, WidgetType,
4};
5
6/// One out of several alternatives, either selected or not.
7///
8/// Usually you'd use [`Ui::radio_value`] or [`Ui::radio`] instead.
9///
10/// ```
11/// # egui::__run_test_ui(|ui| {
12/// #[derive(PartialEq)]
13/// enum Enum { First, Second, Third }
14/// let mut my_enum = Enum::First;
15///
16/// ui.radio_value(&mut my_enum, Enum::First, "First");
17///
18/// // is equivalent to:
19///
20/// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() {
21///     my_enum = Enum::First
22/// }
23/// # });
24/// ```
25#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
26pub struct RadioButton {
27    checked: bool,
28    text: WidgetText,
29}
30
31impl RadioButton {
32    pub fn new(checked: bool, text: impl Into<WidgetText>) -> Self {
33        Self {
34            checked,
35            text: text.into(),
36        }
37    }
38}
39
40impl Widget for RadioButton {
41    fn ui(self, ui: &mut Ui) -> Response {
42        let Self { checked, text } = self;
43
44        let spacing = &ui.spacing();
45        let icon_width = spacing.icon_width;
46        let icon_spacing = spacing.icon_spacing;
47
48        let (galley, mut desired_size) = if text.is_empty() {
49            (None, vec2(icon_width, 0.0))
50        } else {
51            let total_extra = vec2(icon_width + icon_spacing, 0.0);
52
53            let wrap_width = ui.available_width() - total_extra.x;
54            let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
55
56            let mut desired_size = total_extra + text.size();
57            desired_size = desired_size.at_least(spacing.interact_size);
58
59            (Some(text), desired_size)
60        };
61
62        desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
63        desired_size.y = desired_size.y.max(icon_width);
64        let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
65
66        response.widget_info(|| {
67            WidgetInfo::selected(
68                WidgetType::RadioButton,
69                ui.is_enabled(),
70                checked,
71                galley.as_ref().map_or("", |x| x.text()),
72            )
73        });
74
75        if ui.is_rect_visible(rect) {
76            // let visuals = ui.style().interact_selectable(&response, checked); // too colorful
77            let visuals = ui.style().interact(&response);
78
79            let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
80
81            let painter = ui.painter();
82
83            painter.add(epaint::CircleShape {
84                center: big_icon_rect.center(),
85                radius: big_icon_rect.width() / 2.0 + visuals.expansion,
86                fill: visuals.bg_fill,
87                stroke: visuals.bg_stroke,
88            });
89
90            if checked {
91                painter.add(epaint::CircleShape {
92                    center: small_icon_rect.center(),
93                    radius: small_icon_rect.width() / 3.0,
94                    fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill
95                    // fill: ui.visuals().selection.stroke.color, // too much color
96                    stroke: Default::default(),
97                });
98            }
99
100            if let Some(galley) = galley {
101                let text_pos = pos2(
102                    rect.min.x + icon_width + icon_spacing,
103                    rect.center().y - 0.5 * galley.size().y,
104                );
105                ui.painter().galley(text_pos, galley, visuals.text_color());
106            }
107        }
108
109        response
110    }
111}