Skip to main content

fui/controls/internal/
radio_indicator_presenter.rs

1use super::pressable_indicator_presenter::{
2    PressableIndicatorMetrics, PressableIndicatorPresenter, PressableIndicatorVisualState,
3};
4use crate::controls::{LabeledControlColors, LabeledControlSizing};
5use crate::ffi::{AlignItems, JustifyContent, Unit};
6use crate::node::{flex_box, FlexBox};
7use crate::theme::Theme;
8use std::rc::Rc;
9
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct RadioIndicatorMetrics {
12    pub indicator_size: f32,
13    pub dot_size: f32,
14    pub border_width: f32,
15}
16
17impl RadioIndicatorMetrics {
18    pub const fn new(indicator_size: f32, dot_size: f32, border_width: f32) -> Self {
19        Self {
20            indicator_size,
21            dot_size,
22            border_width,
23        }
24    }
25}
26
27const DEFAULT_RADIO_METRICS: RadioIndicatorMetrics = RadioIndicatorMetrics::new(20.0, 8.0, 1.0);
28
29fn centered_inset(outer_size: f32, inner_size: f32) -> f32 {
30    if outer_size > inner_size {
31        (outer_size - inner_size) * 0.5
32    } else {
33        0.0
34    }
35}
36
37fn dot_inset(metrics: RadioIndicatorMetrics) -> f32 {
38    let inset = centered_inset(metrics.indicator_size, metrics.dot_size);
39    if inset > metrics.border_width {
40        inset - metrics.border_width
41    } else {
42        0.0
43    }
44}
45
46pub fn resolve_radio_metrics(sizing: Option<LabeledControlSizing>) -> RadioIndicatorMetrics {
47    let Some(sizing) = sizing else {
48        return DEFAULT_RADIO_METRICS;
49    };
50    if !sizing.has_indicator_size() {
51        return DEFAULT_RADIO_METRICS;
52    }
53    let indicator_size = sizing.indicator_size_px();
54    RadioIndicatorMetrics::new(
55        indicator_size,
56        indicator_size * 0.4,
57        if indicator_size >= 24.0 { 2.0 } else { 1.0 },
58    )
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub struct RadioIndicatorVisualState {
63    pub checked: bool,
64    pub hovered: bool,
65    pub pressed: bool,
66    pub focused: bool,
67    pub enabled: bool,
68}
69
70impl RadioIndicatorVisualState {
71    pub fn new(checked: bool, hovered: bool, pressed: bool, focused: bool, enabled: bool) -> Self {
72        Self {
73            checked,
74            hovered,
75            pressed,
76            focused,
77            enabled,
78        }
79    }
80
81    pub fn pressable_state(&self) -> PressableIndicatorVisualState {
82        PressableIndicatorVisualState {
83            hovered: self.hovered,
84            pressed: self.pressed,
85            focused: self.focused,
86            enabled: self.enabled,
87        }
88    }
89}
90
91pub trait RadioIndicatorPresenter: PressableIndicatorPresenter {
92    fn apply(
93        &self,
94        theme: Theme,
95        state: RadioIndicatorVisualState,
96        colors: Option<LabeledControlColors>,
97    );
98}
99
100pub trait RadioIndicatorTemplate {
101    fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn RadioIndicatorPresenter>;
102}
103
104#[derive(Clone)]
105pub struct DefaultRadioIndicatorPresenter {
106    root: FlexBox,
107    metrics: RadioIndicatorMetrics,
108    dot_node: FlexBox,
109}
110
111impl DefaultRadioIndicatorPresenter {
112    pub fn new(metrics: RadioIndicatorMetrics) -> Self {
113        let root = flex_box();
114        root.width(metrics.indicator_size, Unit::Pixel)
115            .height(metrics.indicator_size, Unit::Pixel)
116            .align_items(AlignItems::Center)
117            .justify_content(JustifyContent::Center);
118        let dot_node = flex_box();
119        dot_node
120            .position_type(crate::ffi::PositionType::Absolute)
121            .position(dot_inset(metrics), dot_inset(metrics))
122            .width(metrics.dot_size, Unit::Pixel)
123            .height(metrics.dot_size, Unit::Pixel);
124        root.child(&dot_node);
125        Self {
126            root,
127            metrics,
128            dot_node,
129        }
130    }
131}
132
133impl PressableIndicatorPresenter for DefaultRadioIndicatorPresenter {
134    fn root(&self) -> FlexBox {
135        self.root.clone()
136    }
137
138    fn metrics(&self) -> PressableIndicatorMetrics {
139        PressableIndicatorMetrics::new(self.metrics.indicator_size, self.metrics.indicator_size)
140    }
141}
142
143impl RadioIndicatorPresenter for DefaultRadioIndicatorPresenter {
144    fn apply(
145        &self,
146        theme: Theme,
147        state: RadioIndicatorVisualState,
148        colors: Option<LabeledControlColors>,
149    ) {
150        let metrics = self.metrics;
151        let accent = colors
152            .filter(|colors| colors.has_accent())
153            .map(|colors| colors.accent_color())
154            .unwrap_or_else(|| {
155                if state.pressed {
156                    theme.colors.accent_pressed
157                } else if state.hovered {
158                    theme.colors.accent_hovered
159                } else {
160                    theme.colors.accent
161                }
162            });
163        let outer_color = if state.checked {
164            accent
165        } else {
166            colors
167                .filter(|colors| colors.has_border())
168                .map(|colors| colors.border_color())
169                .unwrap_or(theme.colors.border)
170        };
171        self.root
172            .corner_radius(metrics.indicator_size * 0.5)
173            .border(metrics.border_width, outer_color)
174            .bg_color(
175                colors
176                    .filter(|colors| colors.has_background())
177                    .map(|colors| colors.background_color())
178                    .unwrap_or(theme.colors.surface),
179            );
180        self.dot_node
181            .corner_radius(metrics.dot_size * 0.5)
182            .position(dot_inset(metrics), dot_inset(metrics))
183            .width(metrics.dot_size, Unit::Pixel)
184            .height(metrics.dot_size, Unit::Pixel)
185            .bg_color(outer_color)
186            .opacity(if state.checked { 1.0 } else { 0.0 });
187    }
188}
189
190#[derive(Clone, Copy, Debug, Default)]
191pub struct DefaultRadioIndicatorTemplate;
192
193impl RadioIndicatorTemplate for DefaultRadioIndicatorTemplate {
194    fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn RadioIndicatorPresenter> {
195        create_default_radio_indicator_presenter(sizing)
196    }
197}
198
199pub const DEFAULT_RADIO_INDICATOR_TEMPLATE: DefaultRadioIndicatorTemplate =
200    DefaultRadioIndicatorTemplate;
201
202pub fn create_default_radio_indicator_presenter(
203    sizing: Option<LabeledControlSizing>,
204) -> Rc<dyn RadioIndicatorPresenter> {
205    Rc::new(DefaultRadioIndicatorPresenter::new(resolve_radio_metrics(
206        sizing,
207    )))
208}