Skip to main content

fret_ui_headless/
checked_state.rs

1/// Tri-state checked value for controls like checkboxes.
2///
3/// Radix models this as `boolean | 'indeterminate'`. In Fret we keep it Rust-native while
4/// preserving the same user-facing outcomes.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6pub enum CheckedState {
7    #[default]
8    Unchecked,
9    Checked,
10    Indeterminate,
11}
12
13impl CheckedState {
14    pub fn is_on(self) -> bool {
15        matches!(self, Self::Checked | Self::Indeterminate)
16    }
17
18    pub fn is_checked(self) -> bool {
19        matches!(self, Self::Checked)
20    }
21
22    pub fn is_indeterminate(self) -> bool {
23        matches!(self, Self::Indeterminate)
24    }
25
26    /// Toggle behavior matching Radix CheckboxTrigger:
27    /// - `indeterminate` -> `checked`
28    /// - otherwise boolean invert
29    pub fn toggle(self) -> Self {
30        match self {
31            Self::Indeterminate => Self::Checked,
32            Self::Checked => Self::Unchecked,
33            Self::Unchecked => Self::Checked,
34        }
35    }
36
37    /// Maps tri-state into Fret's current semantics flag surface.
38    ///
39    /// `None` represents the indeterminate/mixed state.
40    pub fn to_semantics_checked(self) -> Option<bool> {
41        match self {
42            Self::Unchecked => Some(false),
43            Self::Checked => Some(true),
44            Self::Indeterminate => None,
45        }
46    }
47
48    pub fn to_semantics_checked_state(self) -> Option<fret_core::SemanticsCheckedState> {
49        match self {
50            Self::Unchecked => Some(fret_core::SemanticsCheckedState::False),
51            Self::Checked => Some(fret_core::SemanticsCheckedState::True),
52            Self::Indeterminate => Some(fret_core::SemanticsCheckedState::Mixed),
53        }
54    }
55}
56
57impl From<bool> for CheckedState {
58    fn from(value: bool) -> Self {
59        if value {
60            Self::Checked
61        } else {
62            Self::Unchecked
63        }
64    }
65}
66
67impl From<CheckedState> for bool {
68    fn from(value: CheckedState) -> Self {
69        value.is_checked()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn toggle_matches_radix_outcomes() {
79        assert_eq!(CheckedState::Unchecked.toggle(), CheckedState::Checked);
80        assert_eq!(CheckedState::Checked.toggle(), CheckedState::Unchecked);
81        assert_eq!(CheckedState::Indeterminate.toggle(), CheckedState::Checked);
82    }
83
84    #[test]
85    fn semantics_mapping_uses_none_for_indeterminate() {
86        assert_eq!(CheckedState::Unchecked.to_semantics_checked(), Some(false));
87        assert_eq!(CheckedState::Checked.to_semantics_checked(), Some(true));
88        assert_eq!(CheckedState::Indeterminate.to_semantics_checked(), None);
89    }
90
91    #[test]
92    fn semantics_checked_state_maps_indeterminate_to_mixed() {
93        assert_eq!(
94            CheckedState::Unchecked.to_semantics_checked_state(),
95            Some(fret_core::SemanticsCheckedState::False)
96        );
97        assert_eq!(
98            CheckedState::Checked.to_semantics_checked_state(),
99            Some(fret_core::SemanticsCheckedState::True)
100        );
101        assert_eq!(
102            CheckedState::Indeterminate.to_semantics_checked_state(),
103            Some(fret_core::SemanticsCheckedState::Mixed)
104        );
105    }
106}