1use std::sync::Arc;
10
11use fret_core::{Px, SemanticsRole, Size};
12use fret_runtime::Model;
13use fret_ui::element::{AnyElement, PressableA11y, SemanticsProps};
14use fret_ui::elements::GlobalElementId;
15use fret_ui::theme::CubicBezier;
16use fret_ui::{ElementContext, UiHost};
17
18use crate::declarative::ModelWatchExt;
19use crate::primitives::trigger_a11y;
20
21pub fn collapsible_use_open_model<H: UiHost>(
24 cx: &mut ElementContext<'_, H>,
25 controlled_open: Option<Model<bool>>,
26 default_open: impl FnOnce() -> bool,
27) -> crate::primitives::controllable_state::ControllableModel<bool> {
28 crate::primitives::open_state::open_use_model(cx, controlled_open, default_open)
29}
30
31#[derive(Debug, Clone, Default)]
38pub struct CollapsibleRoot {
39 open: Option<Model<bool>>,
40 default_open: bool,
41}
42
43impl CollapsibleRoot {
44 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn open(mut self, open: Option<Model<bool>>) -> Self {
50 self.open = open;
51 self
52 }
53
54 pub fn default_open(mut self, default_open: bool) -> Self {
56 self.default_open = default_open;
57 self
58 }
59
60 pub fn use_open_model<H: UiHost>(
62 &self,
63 cx: &mut ElementContext<'_, H>,
64 ) -> crate::primitives::controllable_state::ControllableModel<bool> {
65 collapsible_use_open_model(cx, self.open.clone(), || self.default_open)
66 }
67
68 pub fn is_open<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> bool {
70 let open_model = self.use_open_model(cx).model();
71 cx.watch_model(&open_model)
72 .layout()
73 .copied()
74 .unwrap_or(false)
75 }
76}
77
78pub fn collapsible_root_semantics(disabled: bool, open: bool) -> SemanticsProps {
80 SemanticsProps {
81 role: SemanticsRole::Generic,
82 disabled,
83 expanded: Some(open),
84 ..Default::default()
85 }
86}
87
88pub fn collapsible_trigger_a11y(label: Option<Arc<str>>, open: bool) -> PressableA11y {
90 PressableA11y {
91 role: Some(SemanticsRole::Button),
92 label,
93 expanded: Some(open),
94 ..Default::default()
95 }
96}
97
98pub fn apply_collapsible_trigger_controls(
105 trigger: AnyElement,
106 content_element: GlobalElementId,
107) -> AnyElement {
108 trigger_a11y::apply_trigger_controls(trigger, Some(content_element))
109}
110
111pub fn apply_collapsible_trigger_controls_expanded(
115 trigger: AnyElement,
116 content_element: GlobalElementId,
117 open: bool,
118) -> AnyElement {
119 trigger_a11y::apply_trigger_controls_expanded(trigger, Some(open), Some(content_element))
120}
121
122pub fn last_measured_height_for<H: UiHost>(
127 cx: &mut ElementContext<'_, H>,
128 state_id: GlobalElementId,
129) -> Px {
130 crate::declarative::collapsible_motion::last_measured_height_for(cx, state_id)
131}
132
133pub fn last_measured_size_for<H: UiHost>(
135 cx: &mut ElementContext<'_, H>,
136 state_id: GlobalElementId,
137) -> Size {
138 crate::declarative::collapsible_motion::last_measured_size_for(cx, state_id)
139}
140
141pub fn update_measured_height_if_open_for<H: UiHost>(
143 cx: &mut ElementContext<'_, H>,
144 state_id: GlobalElementId,
145 wrapper_element_id: GlobalElementId,
146 open: bool,
147 animating: bool,
148) -> Px {
149 crate::declarative::collapsible_motion::update_measured_height_if_open_for(
150 cx,
151 state_id,
152 wrapper_element_id,
153 open,
154 animating,
155 )
156}
157
158pub fn update_measured_size_from_element_if_open_for<H: UiHost>(
160 cx: &mut ElementContext<'_, H>,
161 state_id: GlobalElementId,
162 measure_element_id: GlobalElementId,
163 open: bool,
164) -> Size {
165 crate::declarative::collapsible_motion::update_measured_size_from_element_if_open_for(
166 cx,
167 state_id,
168 measure_element_id,
169 open,
170 )
171}
172
173pub fn collapsible_measurement_wrapper_refinement() -> crate::LayoutRefinement {
175 crate::declarative::collapsible_motion::collapsible_measurement_wrapper_refinement()
176}
177
178#[allow(clippy::too_many_arguments)]
180pub fn collapsible_height_wrapper_refinement(
181 open: bool,
182 force_mount: bool,
183 require_measurement_for_close: bool,
184 transition: crate::headless::transition::TransitionOutput,
185 measured_height: Px,
186) -> (bool, crate::LayoutRefinement) {
187 crate::declarative::collapsible_motion::collapsible_height_wrapper_refinement(
188 open,
189 force_mount,
190 require_measurement_for_close,
191 transition,
192 measured_height,
193 )
194}
195
196pub use crate::declarative::collapsible_motion::MeasuredHeightMotionOutput;
197
198#[allow(clippy::too_many_arguments)]
203pub fn measured_height_motion_for_root<H: UiHost>(
204 cx: &mut ElementContext<'_, H>,
205 open: bool,
206 force_mount: bool,
207 require_measurement_for_close: bool,
208 open_ticks: u64,
209 close_ticks: u64,
210 ease: fn(f32) -> f32,
211) -> MeasuredHeightMotionOutput {
212 crate::declarative::collapsible_motion::measured_height_motion_for_root(
213 cx,
214 open,
215 force_mount,
216 require_measurement_for_close,
217 open_ticks,
218 close_ticks,
219 ease,
220 )
221}
222
223#[allow(clippy::too_many_arguments)]
225pub fn measured_height_motion_for_root_with_cubic_bezier<H: UiHost>(
226 cx: &mut ElementContext<'_, H>,
227 open: bool,
228 force_mount: bool,
229 require_measurement_for_close: bool,
230 open_ticks: u64,
231 close_ticks: u64,
232 bezier: CubicBezier,
233) -> MeasuredHeightMotionOutput {
234 crate::declarative::collapsible_motion::measured_height_motion_for_root_with_cubic_bezier(
235 cx,
236 open,
237 force_mount,
238 require_measurement_for_close,
239 open_ticks,
240 close_ticks,
241 bezier,
242 )
243}
244
245pub fn update_measured_for_motion<H: UiHost>(
247 cx: &mut ElementContext<'_, H>,
248 motion: MeasuredHeightMotionOutput,
249 wrapper_element_id: GlobalElementId,
250) -> Size {
251 crate::declarative::collapsible_motion::update_measured_for_motion(
252 cx,
253 motion,
254 wrapper_element_id,
255 )
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 use std::cell::Cell;
263
264 use fret_app::App;
265 use fret_core::{AppWindowId, Point, Px, Rect, Size};
266 use fret_ui::element::{ElementKind, LayoutStyle, PressableProps};
267
268 fn bounds() -> Rect {
269 Rect::new(
270 Point::new(Px(0.0), Px(0.0)),
271 Size::new(Px(200.0), Px(120.0)),
272 )
273 }
274
275 #[test]
276 fn collapsible_use_open_model_prefers_controlled_and_does_not_call_default() {
277 let window = AppWindowId::default();
278 let mut app = App::new();
279 let b = bounds();
280
281 let controlled = app.models_mut().insert(true);
282 let called = Cell::new(0);
283
284 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
285 let out = collapsible_use_open_model(cx, Some(controlled.clone()), || {
286 called.set(called.get() + 1);
287 false
288 });
289 assert!(out.is_controlled());
290 assert_eq!(out.model(), controlled);
291 });
292
293 assert_eq!(called.get(), 0);
294 }
295
296 #[test]
297 fn apply_collapsible_trigger_controls_sets_controls_on_pressable() {
298 let window = AppWindowId::default();
299 let mut app = App::new();
300 let b = bounds();
301
302 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
303 let trigger = cx.pressable(
304 PressableProps {
305 layout: LayoutStyle::default(),
306 enabled: true,
307 focusable: true,
308 ..Default::default()
309 },
310 |_cx, _st| Vec::new(),
311 );
312 let content = GlobalElementId(0xbeef);
313 let trigger = apply_collapsible_trigger_controls(trigger, content);
314 let ElementKind::Pressable(PressableProps { a11y, .. }) = &trigger.kind else {
315 panic!("expected pressable");
316 };
317 assert_eq!(a11y.controls_element, Some(content.0));
318 });
319 }
320
321 #[test]
322 fn apply_collapsible_trigger_controls_expanded_sets_expanded_and_controls() {
323 let window = AppWindowId::default();
324 let mut app = App::new();
325 let b = bounds();
326
327 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
328 let trigger = cx.pressable(
329 PressableProps {
330 layout: LayoutStyle::default(),
331 enabled: true,
332 focusable: true,
333 ..Default::default()
334 },
335 |_cx, _st| Vec::new(),
336 );
337 let content = GlobalElementId(0xbeef);
338 let trigger = apply_collapsible_trigger_controls_expanded(trigger, content, true);
339 let ElementKind::Pressable(PressableProps { a11y, .. }) = &trigger.kind else {
340 panic!("expected pressable");
341 };
342 assert_eq!(a11y.expanded, Some(true));
343 assert_eq!(a11y.controls_element, Some(content.0));
344 });
345 }
346}