fui/controls/internal/
radio_indicator_presenter.rs1use 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}