Skip to main content

fret_ui_kit/declarative/
model_watch.rs

1use std::any::Any;
2
3#[cfg(feature = "state-query")]
4use fret_query::{QueryHandle, QueryState};
5use fret_runtime::{Model, ModelUpdateError};
6use fret_ui::{ElementContext, Invalidation, UiHost};
7
8/// Ergonomic helpers for observing-and-reading models during declarative rendering.
9///
10/// This is intentionally a component-layer API (ADR 0066): it provides sugar on top of
11/// `fret-ui`'s explicit `observe_model(..., Invalidation)` contract (ADR 0051).
12pub trait ModelWatchExt {
13    type WatchedModel<'cx, 'm, T: Any>
14    where
15        Self: 'cx;
16
17    fn watch_model<'cx, 'm, T: Any>(
18        &'cx mut self,
19        model: &'m Model<T>,
20    ) -> Self::WatchedModel<'cx, 'm, T>;
21}
22
23impl<'a, H: UiHost> ModelWatchExt for ElementContext<'a, H> {
24    type WatchedModel<'cx, 'm, T: Any>
25        = WatchedModel<'cx, 'm, 'a, H, T>
26    where
27        Self: 'cx;
28
29    fn watch_model<'cx, 'm, T: Any>(
30        &'cx mut self,
31        model: &'m Model<T>,
32    ) -> Self::WatchedModel<'cx, 'm, T> {
33        WatchedModel {
34            cx: self,
35            model,
36            invalidation: Invalidation::Paint,
37        }
38    }
39}
40
41/// Handle-first tracked-read helpers for helper-heavy `ElementContext` surfaces.
42///
43/// This stays in the component/declarative layer for the same reason as `ModelWatchExt`: it is
44/// sugar over the explicit `observe_model(..., Invalidation)` contract, not a new runtime
45/// mechanism.
46pub trait TrackedModelExt<T: Any> {
47    fn watch_in<'cx, 'a, H: UiHost>(
48        &self,
49        cx: &'cx mut ElementContext<'a, H>,
50    ) -> WatchedModel<'cx, '_, 'a, H, T>;
51
52    fn paint_in<'cx, 'a, H: UiHost>(
53        &self,
54        cx: &'cx mut ElementContext<'a, H>,
55    ) -> WatchedModel<'cx, '_, 'a, H, T> {
56        self.watch_in(cx).paint()
57    }
58
59    fn layout_in<'cx, 'a, H: UiHost>(
60        &self,
61        cx: &'cx mut ElementContext<'a, H>,
62    ) -> WatchedModel<'cx, '_, 'a, H, T> {
63        self.watch_in(cx).layout()
64    }
65
66    fn hit_test_in<'cx, 'a, H: UiHost>(
67        &self,
68        cx: &'cx mut ElementContext<'a, H>,
69    ) -> WatchedModel<'cx, '_, 'a, H, T> {
70        self.watch_in(cx).hit_test()
71    }
72}
73
74impl<T: Any> TrackedModelExt<T> for Model<T> {
75    fn watch_in<'cx, 'a, H: UiHost>(
76        &self,
77        cx: &'cx mut ElementContext<'a, H>,
78    ) -> WatchedModel<'cx, '_, 'a, H, T> {
79        WatchedModel {
80            cx,
81            model: self,
82            invalidation: Invalidation::Paint,
83        }
84    }
85}
86
87#[must_use]
88pub struct WatchedModel<'cx, 'm, 'a, H: UiHost, T: Any> {
89    cx: &'cx mut ElementContext<'a, H>,
90    model: &'m Model<T>,
91    invalidation: Invalidation,
92}
93
94impl<'cx, 'm, 'a, H: UiHost, T: Any> WatchedModel<'cx, 'm, 'a, H, T> {
95    pub fn invalidation(mut self, invalidation: Invalidation) -> Self {
96        self.invalidation = invalidation;
97        self
98    }
99
100    pub fn paint(self) -> Self {
101        self.invalidation(Invalidation::Paint)
102    }
103
104    pub fn layout(self) -> Self {
105        self.invalidation(Invalidation::Layout)
106    }
107
108    pub fn hit_test(self) -> Self {
109        self.invalidation(Invalidation::HitTest)
110    }
111
112    pub fn observe(self) {
113        self.cx.observe_model(self.model, self.invalidation);
114    }
115
116    pub fn revision(self) -> Option<u64> {
117        self.cx.observe_model(self.model, self.invalidation);
118        self.cx.app.models().revision(self.model)
119    }
120
121    pub fn copied(self) -> Option<T>
122    where
123        T: Copy,
124    {
125        self.cx.get_model_copied(self.model, self.invalidation)
126    }
127
128    pub fn copied_or(self, default: T) -> T
129    where
130        T: Copy,
131    {
132        self.copied().unwrap_or(default)
133    }
134
135    pub fn copied_or_default(self) -> T
136    where
137        T: Copy + Default,
138    {
139        self.copied().unwrap_or_default()
140    }
141
142    pub fn cloned(self) -> Option<T>
143    where
144        T: Clone,
145    {
146        self.cx.get_model_cloned(self.model, self.invalidation)
147    }
148
149    pub fn cloned_or(self, default: T) -> T
150    where
151        T: Clone,
152    {
153        self.cloned().unwrap_or(default)
154    }
155
156    pub fn cloned_or_else(self, f: impl FnOnce() -> T) -> T
157    where
158        T: Clone,
159    {
160        self.cloned().unwrap_or_else(f)
161    }
162
163    pub fn cloned_or_default(self) -> T
164    where
165        T: Clone + Default,
166    {
167        self.cloned().unwrap_or_default()
168    }
169
170    /// Default post-v1 read path: clone/copy the tracked value without choosing between
171    /// `copied_*` and `cloned_*` at every call site.
172    pub fn value(self) -> Option<T>
173    where
174        T: Clone,
175    {
176        self.cloned()
177    }
178
179    pub fn value_or(self, default: T) -> T
180    where
181        T: Clone,
182    {
183        self.value().unwrap_or(default)
184    }
185
186    pub fn value_or_else(self, f: impl FnOnce() -> T) -> T
187    where
188        T: Clone,
189    {
190        self.value().unwrap_or_else(f)
191    }
192
193    pub fn value_or_default(self) -> T
194    where
195        T: Clone + Default,
196    {
197        self.value().unwrap_or_default()
198    }
199
200    pub fn read_ref<R>(self, f: impl FnOnce(&T) -> R) -> Result<R, ModelUpdateError> {
201        self.cx.read_model_ref(self.model, self.invalidation, f)
202    }
203
204    pub fn read<R>(self, f: impl FnOnce(&mut H, &T) -> R) -> Result<R, ModelUpdateError> {
205        self.cx.read_model(self.model, self.invalidation, f)
206    }
207}
208
209#[cfg(feature = "state-query")]
210pub trait QueryHandleWatchExt<T: 'static> {
211    fn watch_query<'cx, 'a, H: UiHost>(
212        &self,
213        cx: &'cx mut ElementContext<'a, H>,
214    ) -> WatchedModel<'cx, '_, 'a, H, QueryState<T>>;
215
216    fn paint_query<'cx, 'a, H: UiHost>(
217        &self,
218        cx: &'cx mut ElementContext<'a, H>,
219    ) -> WatchedModel<'cx, '_, 'a, H, QueryState<T>> {
220        self.watch_query(cx).paint()
221    }
222
223    fn layout_query<'cx, 'a, H: UiHost>(
224        &self,
225        cx: &'cx mut ElementContext<'a, H>,
226    ) -> WatchedModel<'cx, '_, 'a, H, QueryState<T>> {
227        self.watch_query(cx).layout()
228    }
229
230    fn hit_test_query<'cx, 'a, H: UiHost>(
231        &self,
232        cx: &'cx mut ElementContext<'a, H>,
233    ) -> WatchedModel<'cx, '_, 'a, H, QueryState<T>> {
234        self.watch_query(cx).hit_test()
235    }
236}
237
238#[cfg(feature = "state-query")]
239impl<T: 'static> QueryHandleWatchExt<T> for QueryHandle<T> {
240    fn watch_query<'cx, 'a, H: UiHost>(
241        &self,
242        cx: &'cx mut ElementContext<'a, H>,
243    ) -> WatchedModel<'cx, '_, 'a, H, QueryState<T>> {
244        WatchedModel {
245            cx,
246            model: self.model(),
247            invalidation: Invalidation::Paint,
248        }
249    }
250}