Skip to main content

fui/controls/internal/
checkbox_indicator_presenter.rs

1use super::pressable_indicator_presenter::{
2    PressableIndicatorMetrics, PressableIndicatorPresenter, PressableIndicatorVisualState,
3};
4use crate::controls::{LabeledControlColors, LabeledControlSizing};
5use crate::ffi::{AlignItems, JustifyContent, SemanticCheckedState, Unit};
6use crate::node::{flex_box, svg, BoxStyleSurface, FlexBox, SvgNode};
7use crate::theme::Theme;
8use std::rc::Rc;
9
10const CHECKBOX_CHECK_SVG_URL: &str = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 14 14'><path d='M2.25 7.15 5.35 10.25 11.75 3.85' fill='none' stroke='%23000000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/></svg>";
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct CheckboxIndicatorMetrics {
14    pub indicator_size: f32,
15    pub corner_radius: f32,
16    pub check_mark_size: f32,
17}
18
19impl CheckboxIndicatorMetrics {
20    pub const fn new(indicator_size: f32, corner_radius: f32, check_mark_size: f32) -> Self {
21        Self {
22            indicator_size,
23            corner_radius,
24            check_mark_size,
25        }
26    }
27}
28
29const DEFAULT_CHECKBOX_METRICS: CheckboxIndicatorMetrics =
30    CheckboxIndicatorMetrics::new(20.0, 4.0, 16.0);
31
32pub fn resolve_checkbox_metrics(sizing: Option<LabeledControlSizing>) -> CheckboxIndicatorMetrics {
33    let Some(sizing) = sizing else {
34        return DEFAULT_CHECKBOX_METRICS;
35    };
36    if !sizing.has_indicator_size() {
37        return DEFAULT_CHECKBOX_METRICS;
38    }
39    let indicator_size = sizing.indicator_size_px();
40    CheckboxIndicatorMetrics::new(indicator_size, indicator_size * 0.2, indicator_size * 0.8)
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct CheckboxIndicatorVisualState {
45    pub checked_state: SemanticCheckedState,
46    pub hovered: bool,
47    pub pressed: bool,
48    pub focused: bool,
49    pub enabled: bool,
50}
51
52impl CheckboxIndicatorVisualState {
53    pub fn new(
54        checked_state: SemanticCheckedState,
55        hovered: bool,
56        pressed: bool,
57        focused: bool,
58        enabled: bool,
59    ) -> Self {
60        Self {
61            checked_state,
62            hovered,
63            pressed,
64            focused,
65            enabled,
66        }
67    }
68
69    pub fn pressable_state(&self) -> PressableIndicatorVisualState {
70        PressableIndicatorVisualState {
71            hovered: self.hovered,
72            pressed: self.pressed,
73            focused: self.focused,
74            enabled: self.enabled,
75        }
76    }
77}
78
79pub trait CheckboxIndicatorPresenter: PressableIndicatorPresenter {
80    fn apply(
81        &self,
82        theme: Theme,
83        state: CheckboxIndicatorVisualState,
84        colors: Option<LabeledControlColors>,
85    );
86}
87
88pub trait CheckboxIndicatorTemplate {
89    fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn CheckboxIndicatorPresenter>;
90}
91
92#[derive(Clone)]
93pub struct DefaultCheckboxIndicatorPresenter {
94    root: FlexBox,
95    metrics: CheckboxIndicatorMetrics,
96    mark_node: SvgNode,
97}
98
99impl DefaultCheckboxIndicatorPresenter {
100    pub fn new(metrics: CheckboxIndicatorMetrics) -> Self {
101        let root = flex_box();
102        root.width(metrics.indicator_size, Unit::Pixel)
103            .height(metrics.indicator_size, Unit::Pixel)
104            .align_items(AlignItems::Center)
105            .justify_content(JustifyContent::Center);
106        let mark_host = flex_box();
107        mark_host
108            .fill_size()
109            .align_items(AlignItems::Center)
110            .justify_content(JustifyContent::Center);
111        let mark_node = svg(0);
112        mark_node
113            .width(metrics.check_mark_size, Unit::Pixel)
114            .height(metrics.check_mark_size, Unit::Pixel);
115        mark_host.child(&mark_node);
116        root.child(&mark_host);
117        Self {
118            root,
119            metrics,
120            mark_node,
121        }
122    }
123}
124
125impl PressableIndicatorPresenter for DefaultCheckboxIndicatorPresenter {
126    fn root(&self) -> FlexBox {
127        self.root.clone()
128    }
129
130    fn metrics(&self) -> PressableIndicatorMetrics {
131        PressableIndicatorMetrics::new(self.metrics.indicator_size, self.metrics.indicator_size)
132    }
133}
134
135impl CheckboxIndicatorPresenter for DefaultCheckboxIndicatorPresenter {
136    fn apply(
137        &self,
138        theme: Theme,
139        state: CheckboxIndicatorVisualState,
140        colors: Option<LabeledControlColors>,
141    ) {
142        let metrics = self.metrics;
143        let accent = colors
144            .filter(|colors| colors.has_accent())
145            .map(|colors| colors.accent_color())
146            .unwrap_or_else(|| {
147                if state.pressed {
148                    theme.colors.accent_pressed
149                } else if state.hovered {
150                    theme.colors.accent_hovered
151                } else {
152                    theme.colors.accent
153                }
154            });
155        let mut background = colors
156            .filter(|colors| colors.has_background())
157            .map(|colors| colors.background_color())
158            .unwrap_or(theme.colors.surface);
159        let mut border_color = colors
160            .filter(|colors| colors.has_border())
161            .map(|colors| colors.border_color())
162            .unwrap_or(theme.colors.border);
163        let mut mark_visible = false;
164        let mut mark_color = theme.colors.text_primary;
165        if state.checked_state == SemanticCheckedState::True
166            || state.checked_state == SemanticCheckedState::Mixed
167        {
168            background = accent;
169            border_color = background;
170            mark_visible = state.checked_state == SemanticCheckedState::True;
171            mark_color = theme.colors.text_on_accent;
172        } else if state.hovered && colors.is_none_or(|colors| !colors.has_background()) {
173            background = theme.colors.background;
174        }
175        self.root
176            .corner_radius(metrics.corner_radius)
177            .border(1.0, border_color)
178            .bg_color(background);
179        if mark_visible {
180            self.mark_node.source(CHECKBOX_CHECK_SVG_URL);
181        } else {
182            self.mark_node.clear_source();
183        }
184        self.mark_node
185            .width(metrics.check_mark_size, Unit::Pixel)
186            .height(metrics.check_mark_size, Unit::Pixel)
187            .opacity(if mark_visible { 1.0 } else { 0.0 })
188            .tint(mark_color);
189    }
190}
191
192#[derive(Clone, Copy, Debug, Default)]
193pub struct DefaultCheckboxIndicatorTemplate;
194
195impl CheckboxIndicatorTemplate for DefaultCheckboxIndicatorTemplate {
196    fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn CheckboxIndicatorPresenter> {
197        create_default_checkbox_indicator_presenter(sizing)
198    }
199}
200
201pub const DEFAULT_CHECKBOX_INDICATOR_TEMPLATE: DefaultCheckboxIndicatorTemplate =
202    DefaultCheckboxIndicatorTemplate;
203
204pub fn create_default_checkbox_indicator_presenter(
205    sizing: Option<LabeledControlSizing>,
206) -> Rc<dyn CheckboxIndicatorPresenter> {
207    Rc::new(DefaultCheckboxIndicatorPresenter::new(
208        resolve_checkbox_metrics(sizing),
209    ))
210}