1use std::marker::PhantomData;
14
15use fret_runtime::Model;
16use fret_ui::{ElementContext, UiHost};
17
18#[derive(Debug, Clone)]
19pub struct ControllableModel<T> {
20 model: Model<T>,
21 controlled: bool,
22 _phantom: PhantomData<fn() -> T>,
23}
24
25impl<T> ControllableModel<T> {
26 pub fn model(&self) -> Model<T> {
27 self.model.clone()
28 }
29
30 pub fn is_controlled(&self) -> bool {
31 self.controlled
32 }
33}
34
35#[track_caller]
43pub fn use_controllable_model<T: Clone + 'static, H: UiHost>(
44 cx: &mut ElementContext<'_, H>,
45 controlled: Option<Model<T>>,
46 default_value: impl FnOnce() -> T,
47) -> ControllableModel<T> {
48 if let Some(controlled) = controlled {
49 return ControllableModel {
50 model: controlled,
51 controlled: true,
52 _phantom: PhantomData,
53 };
54 }
55
56 struct UncontrolledModelState<T> {
57 model: Option<Model<T>>,
58 }
59 impl<T> Default for UncontrolledModelState<T> {
60 fn default() -> Self {
61 Self { model: None }
62 }
63 }
64
65 let slot = cx.slot_id();
66 let model = cx.state_for(slot, UncontrolledModelState::<T>::default, |st| {
67 st.model.clone()
68 });
69 let model = if let Some(model) = model {
70 model
71 } else {
72 let model = cx.app.models_mut().insert(default_value());
73 cx.state_for(slot, UncontrolledModelState::<T>::default, |st| {
74 st.model = Some(model.clone());
75 });
76 model
77 };
78
79 ControllableModel {
80 model,
81 controlled: false,
82 _phantom: PhantomData,
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 use std::cell::Cell;
91
92 use fret_app::App;
93 use fret_core::{
94 AppWindowId, PathCommand, PathConstraints, PathId, PathMetrics, PathService, PathStyle,
95 Point, Px, Rect, Size, SvgId, SvgService, TextBlobId, TextConstraints, TextInput,
96 TextMetrics, TextService,
97 };
98 use fret_runtime::{FrameId, TickId};
99 use fret_ui::UiTree;
100
101 fn bounds() -> Rect {
102 Rect::new(
103 Point::new(Px(0.0), Px(0.0)),
104 Size::new(Px(200.0), Px(120.0)),
105 )
106 }
107
108 fn bump_frame(app: &mut App) {
109 app.set_tick_id(TickId(app.tick_id().0.saturating_add(1)));
110 app.set_frame_id(FrameId(app.frame_id().0.saturating_add(1)));
111 }
112
113 #[derive(Default)]
114 struct FakeServices;
115
116 impl TextService for FakeServices {
117 fn prepare(
118 &mut self,
119 _input: &TextInput,
120 _constraints: TextConstraints,
121 ) -> (TextBlobId, TextMetrics) {
122 (
123 TextBlobId::default(),
124 TextMetrics {
125 size: Size::new(Px(0.0), Px(0.0)),
126 baseline: Px(0.0),
127 },
128 )
129 }
130
131 fn release(&mut self, _blob: TextBlobId) {}
132 }
133
134 impl PathService for FakeServices {
135 fn prepare(
136 &mut self,
137 _commands: &[PathCommand],
138 _style: PathStyle,
139 _constraints: PathConstraints,
140 ) -> (PathId, PathMetrics) {
141 (PathId::default(), PathMetrics::default())
142 }
143
144 fn release(&mut self, _path: PathId) {}
145 }
146
147 impl SvgService for FakeServices {
148 fn register_svg(&mut self, _bytes: &[u8]) -> SvgId {
149 SvgId::default()
150 }
151
152 fn unregister_svg(&mut self, _svg: SvgId) -> bool {
153 true
154 }
155 }
156
157 impl fret_core::MaterialService for FakeServices {
158 fn register_material(
159 &mut self,
160 _desc: fret_core::MaterialDescriptor,
161 ) -> Result<fret_core::MaterialId, fret_core::MaterialRegistrationError> {
162 Err(fret_core::MaterialRegistrationError::Unsupported)
163 }
164
165 fn unregister_material(&mut self, _id: fret_core::MaterialId) -> bool {
166 true
167 }
168 }
169
170 #[test]
171 fn use_controllable_model_prefers_controlled_and_does_not_call_default() {
172 let window = AppWindowId::default();
173 let mut app = App::new();
174 let mut ui: UiTree<App> = UiTree::new();
175 ui.set_window(window);
176
177 let controlled = app.models_mut().insert(123u32);
178 let called = Cell::new(0);
179
180 fret_ui::elements::with_element_cx(&mut app, window, bounds(), "test", |cx| {
181 let out = use_controllable_model(cx, Some(controlled.clone()), || {
182 called.set(called.get() + 1);
183 7u32
184 });
185 assert!(out.is_controlled());
186 assert_eq!(out.model(), controlled);
187 });
188
189 assert_eq!(called.get(), 0);
190 }
191
192 #[track_caller]
193 fn bump_root_scoped_counter<H: UiHost>(cx: &mut ElementContext<'_, H>) -> u32 {
194 cx.root_state(u32::default, |value| {
195 *value = value.saturating_add(1);
196 *value
197 })
198 }
199
200 #[track_caller]
201 fn bump_callsite_scoped_counter<H: UiHost>(cx: &mut ElementContext<'_, H>) -> u32 {
202 cx.slot_state(u32::default, |value| {
203 *value = value.saturating_add(1);
204 *value
205 })
206 }
207
208 fn two_root_scoped_counters<H: UiHost>(cx: &mut ElementContext<'_, H>) -> (u32, u32) {
209 let a = bump_root_scoped_counter(cx);
210 let b = bump_root_scoped_counter(cx);
211 (a, b)
212 }
213
214 fn two_callsite_scoped_counters<H: UiHost>(cx: &mut ElementContext<'_, H>) -> (u32, u32) {
215 let a = bump_callsite_scoped_counter(cx);
216 let b = bump_callsite_scoped_counter(cx);
217 (a, b)
218 }
219
220 #[test]
221 fn root_state_is_root_scoped_shared_slot_per_type() {
222 let window = AppWindowId::default();
223 let mut app = App::new();
224
225 let first =
226 fret_ui::elements::with_element_cx(&mut app, window, bounds(), "root-state", |cx| {
227 two_root_scoped_counters(cx)
228 });
229 let second =
230 fret_ui::elements::with_element_cx(&mut app, window, bounds(), "root-state", |cx| {
231 two_root_scoped_counters(cx)
232 });
233
234 assert_eq!(first, (1, 2));
235 assert_eq!(second, (3, 4));
236 }
237
238 #[test]
239 fn slot_state_is_independent_per_callsite() {
240 let window = AppWindowId::default();
241 let mut app = App::new();
242
243 let first =
244 fret_ui::elements::with_element_cx(&mut app, window, bounds(), "slot-state", |cx| {
245 two_callsite_scoped_counters(cx)
246 });
247 let second =
248 fret_ui::elements::with_element_cx(&mut app, window, bounds(), "slot-state", |cx| {
249 two_callsite_scoped_counters(cx)
250 });
251
252 assert_eq!(first, (1, 1));
253 assert_eq!(second, (2, 2));
254 }
255
256 #[test]
257 fn use_controllable_model_creates_one_internal_model_and_reuses_it() {
258 let window = AppWindowId::default();
259 let mut app = App::new();
260 let mut ui: UiTree<App> = UiTree::new();
261 ui.set_window(window);
262 let b = bounds();
263
264 let called = Cell::new(0);
265 let model_id_out = Cell::new(None);
266 let mut services = FakeServices;
267
268 let render = |ui: &mut UiTree<App>, app: &mut App, services: &mut FakeServices| {
269 bump_frame(app);
270 let called = &called;
271 let model_id_out = &model_id_out;
272
273 let root = fret_ui::declarative::render_root(
274 ui,
275 app,
276 services,
277 window,
278 b,
279 "controllable-state-test",
280 |cx| {
281 vec![cx.keyed("controllable", |cx| {
282 let out = use_controllable_model(cx, None::<Model<u32>>, || {
283 called.set(called.get() + 1);
284 42u32
285 });
286 model_id_out.set(Some(out.model().id()));
287 cx.spacer(Default::default())
288 })]
289 },
290 );
291 ui.set_root(root);
292 ui.layout_all(app, services, b, 1.0);
293 };
294
295 render(&mut ui, &mut app, &mut services);
296 let first = model_id_out.get().expect("model id after first render");
297 assert_eq!(called.get(), 1);
298
299 render(&mut ui, &mut app, &mut services);
300 let second = model_id_out.get().expect("model id after second render");
301 assert_eq!(called.get(), 1);
302 assert_eq!(first, second);
303 }
304
305 #[test]
306 fn use_controllable_model_uncontrolled_multiple_instances_do_not_collide() {
307 let window = AppWindowId::default();
308 let mut app = App::new();
309 let mut ui: UiTree<App> = UiTree::new();
310 ui.set_window(window);
311 let b = bounds();
312
313 let called = Cell::new(0);
314 let model_a_id_out = Cell::new(None);
315 let model_b_id_out = Cell::new(None);
316 let mut services = FakeServices;
317
318 let render = |ui: &mut UiTree<App>, app: &mut App, services: &mut FakeServices| {
319 bump_frame(app);
320 let called = &called;
321 let model_a_id_out = &model_a_id_out;
322 let model_b_id_out = &model_b_id_out;
323
324 let root = fret_ui::declarative::render_root(
325 ui,
326 app,
327 services,
328 window,
329 b,
330 "controllable-state-multi-test",
331 |cx| {
332 vec![cx.keyed("controllable", |cx| {
333 let a = use_controllable_model(cx, None::<Model<u32>>, || {
334 called.set(called.get() + 1);
335 1u32
336 });
337 let b = use_controllable_model(cx, None::<Model<u32>>, || {
338 called.set(called.get() + 1);
339 2u32
340 });
341 model_a_id_out.set(Some(a.model().id()));
342 model_b_id_out.set(Some(b.model().id()));
343 cx.spacer(Default::default())
344 })]
345 },
346 );
347 ui.set_root(root);
348 ui.layout_all(app, services, b, 1.0);
349 };
350
351 render(&mut ui, &mut app, &mut services);
352 let first_a = model_a_id_out.get().expect("model a id after first render");
353 let first_b = model_b_id_out.get().expect("model b id after first render");
354 assert_eq!(called.get(), 2);
355 assert_ne!(first_a, first_b);
356
357 render(&mut ui, &mut app, &mut services);
358 let second_a = model_a_id_out
359 .get()
360 .expect("model a id after second render");
361 let second_b = model_b_id_out
362 .get()
363 .expect("model b id after second render");
364 assert_eq!(called.get(), 2);
365 assert_eq!(first_a, second_a);
366 assert_eq!(first_b, second_b);
367 }
368}