Skip to main content

fret_ui_kit/primitives/
field_state.rs

1//! Field state primitives (shadcn/Base UI aligned outcomes).
2//!
3//! Upstream reference:
4//! - `repo-ref/ui/apps/v4/registry/bases/base/ui/field.tsx` (`data-invalid`, `data-disabled`)
5//!
6//! In shadcn/ui v4, `Field` is a styling/grouping wrapper that carries state via data attributes
7//! (e.g. `data-invalid`, `data-disabled`). Downstream parts like `FieldLabel` and `FieldTitle`
8//! respond to those attributes via CSS selectors.
9//!
10//! Fret does not have DOM/CSS inheritance, so we model the same outcome via an element-scope
11//! provider that components can query during `into_element` construction.
12
13use crate::primitives::control_registry::ControlId;
14use fret_ui::{ElementContext, UiHost};
15
16#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
17pub struct FieldState {
18    pub invalid: bool,
19    pub disabled: bool,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct FieldControlAssociation {
24    pub control_id: ControlId,
25}
26
27pub fn inherited_field_state<H: UiHost>(cx: &ElementContext<'_, H>) -> Option<FieldState> {
28    cx.provided::<FieldState>().copied()
29}
30
31pub fn use_field_state_in_scope<H: UiHost>(
32    cx: &ElementContext<'_, H>,
33    local: Option<FieldState>,
34) -> FieldState {
35    local.or(inherited_field_state(cx)).unwrap_or_default()
36}
37
38pub fn inherited_field_control_id<H: UiHost>(cx: &ElementContext<'_, H>) -> Option<ControlId> {
39    cx.provided::<FieldControlAssociation>()
40        .map(|assoc| assoc.control_id.clone())
41}
42
43pub fn use_field_control_id_in_scope<H: UiHost>(
44    cx: &ElementContext<'_, H>,
45    local: Option<ControlId>,
46) -> Option<ControlId> {
47    local.or_else(|| inherited_field_control_id(cx))
48}
49
50#[track_caller]
51pub fn with_field_state_provider<H: UiHost, R>(
52    cx: &mut ElementContext<'_, H>,
53    state: FieldState,
54    f: impl FnOnce(&mut ElementContext<'_, H>) -> R,
55) -> R {
56    cx.provide(state, f)
57}
58
59#[track_caller]
60pub fn with_field_control_association_provider<H: UiHost, R>(
61    cx: &mut ElementContext<'_, H>,
62    control_id: Option<ControlId>,
63    f: impl FnOnce(&mut ElementContext<'_, H>) -> R,
64) -> R {
65    if let Some(control_id) = control_id {
66        cx.provide(FieldControlAssociation { control_id }, f)
67    } else {
68        f(cx)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    use fret_app::App;
77    use fret_core::{AppWindowId, Point, Px, Rect, Size};
78
79    fn bounds() -> Rect {
80        Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(10.0), Px(10.0)))
81    }
82
83    #[test]
84    fn field_state_provider_inherits_and_restores() {
85        let window = AppWindowId::default();
86        let mut app = App::new();
87
88        fret_ui::elements::with_element_cx(&mut app, window, bounds(), "test", |cx| {
89            assert_eq!(inherited_field_state(cx), None);
90            assert_eq!(use_field_state_in_scope(cx, None), FieldState::default());
91
92            with_field_state_provider(
93                cx,
94                FieldState {
95                    invalid: true,
96                    disabled: false,
97                },
98                |cx| {
99                    assert_eq!(
100                        use_field_state_in_scope(cx, None),
101                        FieldState {
102                            invalid: true,
103                            disabled: false,
104                        }
105                    );
106                    cx.scope(|cx| {
107                        assert_eq!(
108                            use_field_state_in_scope(cx, None),
109                            FieldState {
110                                invalid: true,
111                                disabled: false,
112                            }
113                        );
114                    });
115                },
116            );
117
118            assert_eq!(use_field_state_in_scope(cx, None), FieldState::default());
119        });
120    }
121
122    #[test]
123    fn field_control_association_provider_inherits_and_restores() {
124        let window = AppWindowId::default();
125        let mut app = App::new();
126        let control_id = ControlId::from("field.control");
127
128        fret_ui::elements::with_element_cx(&mut app, window, bounds(), "test", |cx| {
129            assert_eq!(inherited_field_control_id(cx), None);
130            assert_eq!(use_field_control_id_in_scope(cx, None), None);
131
132            with_field_control_association_provider(cx, Some(control_id.clone()), |cx| {
133                assert_eq!(inherited_field_control_id(cx), Some(control_id.clone()));
134                assert_eq!(
135                    use_field_control_id_in_scope(cx, None),
136                    Some(control_id.clone())
137                );
138                cx.scope(|cx| {
139                    assert_eq!(
140                        use_field_control_id_in_scope(cx, None),
141                        Some(control_id.clone())
142                    );
143                });
144            });
145
146            assert_eq!(use_field_control_id_in_scope(cx, None), None);
147        });
148    }
149}