1use std::collections::HashSet;
12use std::sync::Arc;
13
14use fret_core::{AppWindowId, NodeId, Rect, UiServices};
15use fret_ui::elements::GlobalElementId;
16use fret_ui::{ElementContext, UiHost, UiTree};
17
18use crate::IntoUiElement;
19
20pub use fret_ui::action::{
21 ActionCx, DismissReason, DismissRequestCx, OnDismissRequest, UiActionHost,
22};
23pub use fret_ui::action::{OnDismissiblePointerMove, PointerMoveCx};
24
25#[allow(clippy::too_many_arguments)]
29pub fn render_dismissable_root_with_hooks<H: UiHost + 'static, I, T>(
30 ui: &mut UiTree<H>,
31 app: &mut H,
32 services: &mut dyn UiServices,
33 window: AppWindowId,
34 bounds: Rect,
35 root_name: &str,
36 render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
37) -> fret_core::NodeId
38where
39 I: IntoIterator<Item = T>,
40 T: IntoUiElement<H>,
41{
42 crate::declarative::dismissible::render_dismissible_root_with_hooks(
43 ui, app, services, window, bounds, root_name, render,
44 )
45}
46
47pub fn on_dismiss_request<H: UiHost>(cx: &mut ElementContext<'_, H>, handler: OnDismissRequest) {
51 cx.dismissible_on_dismiss_request(handler);
52}
53
54pub fn on_pointer_move<H: UiHost>(
59 cx: &mut ElementContext<'_, H>,
60 handler: OnDismissiblePointerMove,
61) {
62 cx.dismissible_on_pointer_move(handler);
63}
64
65pub fn handler(
67 f: impl Fn(&mut dyn UiActionHost, ActionCx, &mut DismissRequestCx) + 'static,
68) -> OnDismissRequest {
69 Arc::new(f)
70}
71
72pub fn pointer_move_handler(
74 f: impl Fn(&mut dyn UiActionHost, ActionCx, PointerMoveCx) -> bool + 'static,
75) -> OnDismissiblePointerMove {
76 Arc::new(f)
77}
78
79pub fn resolve_branch_nodes_for_trigger_and_elements<H: UiHost>(
86 app: &mut H,
87 window: AppWindowId,
88 trigger: GlobalElementId,
89 branches: &[GlobalElementId],
90) -> Vec<NodeId> {
91 let mut out: Vec<NodeId> = Vec::with_capacity(1 + branches.len());
92 if let Some(node) = fret_ui::elements::live_node_for_element(app, window, trigger) {
93 out.push(node);
94 }
95 out.extend(
96 branches
97 .iter()
98 .filter_map(|branch| fret_ui::elements::live_node_for_element(app, window, *branch)),
99 );
100 let mut seen: HashSet<NodeId> = HashSet::with_capacity(out.len());
101 out.retain(|id| seen.insert(*id));
102 out
103}
104
105pub fn resolve_branch_nodes_for_elements<H: UiHost>(
112 app: &mut H,
113 window: AppWindowId,
114 branches: &[GlobalElementId],
115) -> Vec<NodeId> {
116 let mut out: Vec<NodeId> = branches
117 .iter()
118 .filter_map(|branch| fret_ui::elements::live_node_for_element(app, window, *branch))
119 .collect();
120 let mut seen: HashSet<NodeId> = HashSet::with_capacity(out.len());
121 out.retain(|id| seen.insert(*id));
122 out
123}
124
125pub fn resolve_branch_nodes_for_popover_request<H: UiHost>(
135 app: &mut H,
136 window: AppWindowId,
137 trigger: GlobalElementId,
138 branches: &[GlobalElementId],
139 disable_outside_pointer_events: bool,
140) -> Vec<NodeId> {
141 if disable_outside_pointer_events {
142 resolve_branch_nodes_for_elements(app, window, branches)
143 } else {
144 resolve_branch_nodes_for_trigger_and_elements(app, window, trigger, branches)
145 }
146}
147
148pub fn focus_is_inside_layer_or_branches<H: UiHost>(
150 ui: &UiTree<H>,
151 layer_root: NodeId,
152 focus: NodeId,
153 branch_roots: &[NodeId],
154) -> bool {
155 ui.is_descendant(layer_root, focus)
156 || branch_roots
157 .iter()
158 .copied()
159 .any(|branch| ui.is_descendant(branch, focus))
160}
161
162pub fn should_dismiss_on_focus_outside<H: UiHost>(
166 ui: &UiTree<H>,
167 layer_root: NodeId,
168 focus_now: Option<NodeId>,
169 last_focus: Option<NodeId>,
170 branch_roots: &[NodeId],
171) -> bool {
172 let Some(focus) = focus_now else {
173 return false;
174 };
175 let Some(last_focus) = last_focus else {
178 return false;
179 };
180 if ui.node_layer(focus).is_none() {
184 return false;
185 }
186 if last_focus == focus {
187 return false;
188 }
189 !focus_is_inside_layer_or_branches(ui, layer_root, focus, branch_roots)
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use fret_app::App;
196 use fret_core::{
197 AppWindowId, PathCommand, PathConstraints, PathId, PathMetrics, PathService, PathStyle,
198 Point, Px, Rect, Size, SvgId, SvgService, TextBlobId, TextConstraints, TextInput,
199 TextMetrics, TextService,
200 };
201 use fret_ui::element::{LayoutStyle, Length, PressableProps, SemanticsProps};
202 use std::cell::Cell;
203 use std::rc::Rc;
204
205 #[derive(Default)]
206 struct FakeServices;
207
208 impl TextService for FakeServices {
209 fn prepare(
210 &mut self,
211 _input: &TextInput,
212 _constraints: TextConstraints,
213 ) -> (TextBlobId, TextMetrics) {
214 (
215 TextBlobId::default(),
216 TextMetrics {
217 size: Size::new(Px(0.0), Px(0.0)),
218 baseline: Px(0.0),
219 },
220 )
221 }
222
223 fn release(&mut self, _blob: TextBlobId) {}
224 }
225
226 impl PathService for FakeServices {
227 fn prepare(
228 &mut self,
229 _commands: &[PathCommand],
230 _style: PathStyle,
231 _constraints: PathConstraints,
232 ) -> (PathId, PathMetrics) {
233 (PathId::default(), PathMetrics::default())
234 }
235
236 fn release(&mut self, _path: PathId) {}
237 }
238
239 impl SvgService for FakeServices {
240 fn register_svg(&mut self, _bytes: &[u8]) -> SvgId {
241 SvgId::default()
242 }
243
244 fn unregister_svg(&mut self, _svg: SvgId) -> bool {
245 true
246 }
247 }
248
249 impl fret_core::MaterialService for FakeServices {
250 fn register_material(
251 &mut self,
252 _desc: fret_core::MaterialDescriptor,
253 ) -> Result<fret_core::MaterialId, fret_core::MaterialRegistrationError> {
254 Err(fret_core::MaterialRegistrationError::Unsupported)
255 }
256
257 fn unregister_material(&mut self, _id: fret_core::MaterialId) -> bool {
258 true
259 }
260 }
261
262 fn bounds() -> Rect {
263 Rect::new(
264 Point::new(Px(0.0), Px(0.0)),
265 Size::new(Px(200.0), Px(120.0)),
266 )
267 }
268
269 #[test]
270 fn resolve_branch_nodes_dedupes_and_preserves_order() {
271 let window = AppWindowId::default();
272 let mut app = App::new();
273 let mut ui: UiTree<App> = UiTree::new();
274 ui.set_window(window);
275
276 let mut services = FakeServices;
277 let b = bounds();
278
279 let mut trigger: Option<GlobalElementId> = None;
280 let mut branch_a: Option<GlobalElementId> = None;
281 let mut branch_b: Option<GlobalElementId> = None;
282
283 let root = fret_ui::declarative::render_root(
284 &mut ui,
285 &mut app,
286 &mut services,
287 window,
288 b,
289 "test",
290 |cx| {
291 let props = PressableProps {
292 layout: {
293 let mut layout = LayoutStyle::default();
294 layout.size.width = Length::Px(Px(10.0));
295 layout.size.height = Length::Px(Px(10.0));
296 layout
297 },
298 focusable: true,
299 ..Default::default()
300 };
301
302 vec![
303 cx.pressable_with_id(props.clone(), |_cx, _st, id| {
304 trigger = Some(id);
305 Vec::new()
306 }),
307 cx.pressable_with_id(props.clone(), |_cx, _st, id| {
308 branch_a = Some(id);
309 Vec::new()
310 }),
311 cx.pressable_with_id(props, |_cx, _st, id| {
312 branch_b = Some(id);
313 Vec::new()
314 }),
315 ]
316 },
317 );
318 ui.set_root(root);
319 ui.layout_all(&mut app, &mut services, b, 1.0);
320
321 let trigger = trigger.expect("trigger id");
322 let branch_a = branch_a.expect("branch a id");
323 let branch_b = branch_b.expect("branch b id");
324
325 let trigger_node =
326 fret_ui::elements::node_for_element(&mut app, window, trigger).expect("trigger node");
327 let branch_a_node =
328 fret_ui::elements::node_for_element(&mut app, window, branch_a).expect("branch a node");
329 let branch_b_node =
330 fret_ui::elements::node_for_element(&mut app, window, branch_b).expect("branch b node");
331
332 let out = resolve_branch_nodes_for_trigger_and_elements(
333 &mut app,
334 window,
335 trigger,
336 &[branch_a, trigger, branch_b, branch_a],
337 );
338
339 assert_eq!(out, vec![trigger_node, branch_a_node, branch_b_node]);
340 }
341
342 #[test]
343 fn focus_inside_layer_or_branch_is_treated_as_inside() {
344 let window = AppWindowId::default();
345 let mut app = App::new();
346 let mut ui: UiTree<App> = UiTree::new();
347 ui.set_window(window);
348
349 let mut services = FakeServices;
350 let b = bounds();
351
352 let mut layer_root: Option<GlobalElementId> = None;
353 let mut branch_root: Option<GlobalElementId> = None;
354 let mut in_layer: Option<GlobalElementId> = None;
355 let mut in_branch: Option<GlobalElementId> = None;
356 let mut outside: Option<GlobalElementId> = None;
357
358 let focusable = PressableProps {
359 layout: LayoutStyle::default(),
360 focusable: true,
361 ..Default::default()
362 };
363
364 let root = fret_ui::declarative::render_root(
365 &mut ui,
366 &mut app,
367 &mut services,
368 window,
369 b,
370 "test",
371 |cx| {
372 vec![
373 cx.semantics_with_id(SemanticsProps::default(), |cx, id| {
374 layer_root = Some(id);
375 vec![cx.pressable_with_id(focusable.clone(), |_cx, _st, id| {
376 in_layer = Some(id);
377 Vec::new()
378 })]
379 }),
380 cx.semantics_with_id(SemanticsProps::default(), |cx, id| {
381 branch_root = Some(id);
382 vec![cx.pressable_with_id(focusable.clone(), |_cx, _st, id| {
383 in_branch = Some(id);
384 Vec::new()
385 })]
386 }),
387 cx.pressable_with_id(focusable, |_cx, _st, id| {
388 outside = Some(id);
389 Vec::new()
390 }),
391 ]
392 },
393 );
394 ui.set_root(root);
395 ui.layout_all(&mut app, &mut services, b, 1.0);
396
397 let layer_root = layer_root.expect("layer root");
398 let branch_root = branch_root.expect("branch root");
399 let in_layer = in_layer.expect("in layer");
400 let in_branch = in_branch.expect("in branch");
401 let outside = outside.expect("outside");
402
403 let layer_root_node =
404 fret_ui::elements::node_for_element(&mut app, window, layer_root).expect("layer node");
405 let branch_root_node = fret_ui::elements::node_for_element(&mut app, window, branch_root)
406 .expect("branch node");
407 let in_layer_node =
408 fret_ui::elements::node_for_element(&mut app, window, in_layer).expect("in layer node");
409 let in_branch_node = fret_ui::elements::node_for_element(&mut app, window, in_branch)
410 .expect("in branch node");
411 let outside_node =
412 fret_ui::elements::node_for_element(&mut app, window, outside).expect("outside node");
413
414 assert!(focus_is_inside_layer_or_branches(
415 &ui,
416 layer_root_node,
417 in_layer_node,
418 &[branch_root_node]
419 ));
420 assert!(focus_is_inside_layer_or_branches(
421 &ui,
422 layer_root_node,
423 in_branch_node,
424 &[branch_root_node]
425 ));
426 assert!(!focus_is_inside_layer_or_branches(
427 &ui,
428 layer_root_node,
429 outside_node,
430 &[branch_root_node]
431 ));
432 }
433
434 #[test]
435 fn should_not_dismiss_on_focus_outside_without_last_focus_sample() {
436 let window = AppWindowId::default();
437 let mut app = App::new();
438 let mut ui: UiTree<App> = UiTree::new();
439 ui.set_window(window);
440
441 let mut services = FakeServices;
442 let b = bounds();
443
444 let mut layer_root: Option<GlobalElementId> = None;
445 let mut outside: Option<GlobalElementId> = None;
446
447 let focusable = PressableProps {
448 layout: LayoutStyle::default(),
449 focusable: true,
450 ..Default::default()
451 };
452
453 let root = fret_ui::declarative::render_root(
454 &mut ui,
455 &mut app,
456 &mut services,
457 window,
458 b,
459 "test",
460 |cx| {
461 vec![
462 cx.semantics_with_id(SemanticsProps::default(), |_cx, id| {
463 layer_root = Some(id);
464 Vec::new()
465 }),
466 cx.pressable_with_id(focusable, |_cx, _st, id| {
467 outside = Some(id);
468 Vec::new()
469 }),
470 ]
471 },
472 );
473 ui.set_root(root);
474 ui.layout_all(&mut app, &mut services, b, 1.0);
475
476 let layer_root = layer_root.expect("layer root");
477 let outside = outside.expect("outside");
478
479 let layer_root_node =
480 fret_ui::elements::node_for_element(&mut app, window, layer_root).expect("layer node");
481 let outside_node =
482 fret_ui::elements::node_for_element(&mut app, window, outside).expect("outside node");
483
484 assert!(!should_dismiss_on_focus_outside(
485 &ui,
486 layer_root_node,
487 Some(outside_node),
488 None,
489 &[]
490 ));
491 }
492
493 #[test]
494 fn should_dismiss_on_focus_outside_when_focus_changes_to_outside() {
495 let window = AppWindowId::default();
496 let mut app = App::new();
497 let mut ui: UiTree<App> = UiTree::new();
498 ui.set_window(window);
499
500 let mut services = FakeServices;
501 let b = bounds();
502
503 let mut layer_root: Option<GlobalElementId> = None;
504 let mut in_layer: Option<GlobalElementId> = None;
505 let mut outside: Option<GlobalElementId> = None;
506
507 let focusable = PressableProps {
508 layout: LayoutStyle::default(),
509 focusable: true,
510 ..Default::default()
511 };
512
513 let root = fret_ui::declarative::render_root(
514 &mut ui,
515 &mut app,
516 &mut services,
517 window,
518 b,
519 "test",
520 |cx| {
521 vec![
522 cx.semantics_with_id(SemanticsProps::default(), |cx, id| {
523 layer_root = Some(id);
524 vec![cx.pressable_with_id(focusable.clone(), |_cx, _st, id| {
525 in_layer = Some(id);
526 Vec::new()
527 })]
528 }),
529 cx.pressable_with_id(focusable, |_cx, _st, id| {
530 outside = Some(id);
531 Vec::new()
532 }),
533 ]
534 },
535 );
536 ui.set_root(root);
537 ui.layout_all(&mut app, &mut services, b, 1.0);
538
539 let layer_root = layer_root.expect("layer root");
540 let in_layer = in_layer.expect("in layer");
541 let outside = outside.expect("outside");
542
543 let layer_root_node =
544 fret_ui::elements::node_for_element(&mut app, window, layer_root).expect("layer node");
545 let in_layer_node =
546 fret_ui::elements::node_for_element(&mut app, window, in_layer).expect("in layer node");
547 let outside_node =
548 fret_ui::elements::node_for_element(&mut app, window, outside).expect("outside node");
549
550 assert!(should_dismiss_on_focus_outside(
551 &ui,
552 layer_root_node,
553 Some(outside_node),
554 Some(in_layer_node),
555 &[]
556 ));
557 }
558
559 #[test]
560 fn resolve_branch_nodes_ignores_removed_trigger_with_only_last_known_mapping() {
561 let window = AppWindowId::default();
562 let mut app = App::new();
563 let mut ui: UiTree<App> = UiTree::new();
564 ui.set_window(window);
565
566 let mut services = FakeServices;
567 let b = bounds();
568
569 let trigger: Rc<Cell<Option<GlobalElementId>>> = Rc::new(Cell::new(None));
570 let branch: Rc<Cell<Option<GlobalElementId>>> = Rc::new(Cell::new(None));
571 let show_trigger = Cell::new(true);
572
573 let props = PressableProps {
574 layout: LayoutStyle::default(),
575 focusable: true,
576 ..Default::default()
577 };
578
579 let render_frame =
580 |ui: &mut UiTree<App>, app: &mut App, services: &mut dyn fret_core::UiServices| {
581 let trigger = trigger.clone();
582 let branch = branch.clone();
583 fret_ui::declarative::render_root(ui, app, services, window, b, "test", |cx| {
584 let mut out = Vec::new();
585 if show_trigger.get() {
586 out.push(cx.keyed("trigger", |cx| {
587 cx.pressable_with_id(props.clone(), |_cx, _st, id| {
588 trigger.set(Some(id));
589 Vec::new()
590 })
591 }));
592 }
593 out.push(cx.keyed("branch", |cx| {
594 cx.pressable_with_id(props.clone(), |_cx, _st, id| {
595 branch.set(Some(id));
596 Vec::new()
597 })
598 }));
599 out
600 })
601 };
602
603 let root = render_frame(&mut ui, &mut app, &mut services);
604 ui.set_root(root);
605 ui.layout_all(&mut app, &mut services, b, 1.0);
606
607 let trigger = trigger.get().expect("trigger id");
608
609 show_trigger.set(false);
610 app.set_frame_id(fret_runtime::FrameId(app.frame_id().0.saturating_add(1)));
611 let root = render_frame(&mut ui, &mut app, &mut services);
612 ui.set_root(root);
613 ui.layout_all(&mut app, &mut services, b, 1.0);
614
615 let branch = branch.get().expect("branch id");
616 let branch_node =
617 fret_ui::elements::node_for_element(&mut app, window, branch).expect("branch node");
618
619 let out =
620 resolve_branch_nodes_for_trigger_and_elements(&mut app, window, trigger, &[branch]);
621
622 assert_eq!(
623 out,
624 vec![branch_node],
625 "expected branch resolution to ignore a removed trigger that only still has a last-known node mapping"
626 );
627 }
628}