1use frus_core::{Rect, Scene};
35use frus_layout::{Dimension, Style};
36
37use crate::interaction::Status;
38use crate::theme::Theme;
39use crate::widget::Widget;
40
41#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
46pub struct ModalBarrier {
47 pub pointer: bool,
51 pub absorb: bool,
55 pub paint: bool,
57 pub semantics: bool,
59}
60
61impl ModalBarrier {
62 pub const NONE: Self = Self {
64 pointer: false,
65 absorb: false,
66 paint: false,
67 semantics: false,
68 };
69
70 pub fn is_none(&self) -> bool {
72 *self == Self::NONE
73 }
74}
75
76fn collapsed() -> Style {
78 Style {
79 width: Dimension::Length(0.0),
80 height: Dimension::Length(0.0),
81 ..Default::default()
82 }
83}
84
85pub struct IgnorePointer<Msg = crate::callback::Callback> {
98 ignoring: bool,
99 children: Vec<Box<dyn Widget<Msg>>>,
100}
101
102impl<Msg> IgnorePointer<Msg> {
103 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
105 Self {
106 ignoring: true,
107 children: vec![Box::new(child)],
108 }
109 }
110
111 pub fn ignoring(mut self, ignoring: bool) -> Self {
114 self.ignoring = ignoring;
115 self
116 }
117}
118
119impl<Msg: Clone> Widget<Msg> for IgnorePointer<Msg> {
120 fn style(&self) -> Style {
121 Style::default()
122 }
123
124 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
125 &self.children
126 }
127
128 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
129
130 fn on_click(&self) -> Option<Msg> {
131 None
132 }
133
134 fn barrier(&self) -> Option<ModalBarrier> {
135 self.ignoring.then_some(ModalBarrier {
136 pointer: true,
137 ..ModalBarrier::NONE
138 })
139 }
140}
141
142pub struct AbsorbPointer<Msg = crate::callback::Callback> {
152 absorbing: bool,
153 children: Vec<Box<dyn Widget<Msg>>>,
154}
155
156impl<Msg> AbsorbPointer<Msg> {
157 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
159 Self {
160 absorbing: true,
161 children: vec![Box::new(child)],
162 }
163 }
164
165 pub fn absorbing(mut self, absorbing: bool) -> Self {
167 self.absorbing = absorbing;
168 self
169 }
170}
171
172impl<Msg: Clone> Widget<Msg> for AbsorbPointer<Msg> {
173 fn style(&self) -> Style {
174 Style::default()
175 }
176
177 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
178 &self.children
179 }
180
181 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
182
183 fn on_click(&self) -> Option<Msg> {
184 None
185 }
186
187 fn barrier(&self) -> Option<ModalBarrier> {
188 self.absorbing.then_some(ModalBarrier {
189 pointer: true,
190 absorb: true,
191 ..ModalBarrier::NONE
192 })
193 }
194}
195
196pub struct ExcludeSemantics<Msg = crate::callback::Callback> {
204 excluding: bool,
205 children: Vec<Box<dyn Widget<Msg>>>,
206}
207
208impl<Msg> ExcludeSemantics<Msg> {
209 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
211 Self {
212 excluding: true,
213 children: vec![Box::new(child)],
214 }
215 }
216
217 pub fn excluding(mut self, excluding: bool) -> Self {
219 self.excluding = excluding;
220 self
221 }
222}
223
224impl<Msg: Clone> Widget<Msg> for ExcludeSemantics<Msg> {
225 fn style(&self) -> Style {
226 Style::default()
227 }
228
229 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
230 &self.children
231 }
232
233 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
234
235 fn on_click(&self) -> Option<Msg> {
236 None
237 }
238
239 fn barrier(&self) -> Option<ModalBarrier> {
240 self.excluding.then_some(ModalBarrier {
241 semantics: true,
242 ..ModalBarrier::NONE
243 })
244 }
245}
246
247pub struct Offstage<Msg = crate::callback::Callback> {
260 offstage: bool,
261 children: Vec<Box<dyn Widget<Msg>>>,
262}
263
264impl<Msg> Offstage<Msg> {
265 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
267 Self {
268 offstage: true,
269 children: vec![Box::new(child)],
270 }
271 }
272
273 pub fn offstage(mut self, offstage: bool) -> Self {
275 self.offstage = offstage;
276 self
277 }
278}
279
280impl<Msg: Clone> Widget<Msg> for Offstage<Msg> {
281 fn style(&self) -> Style {
282 if self.offstage {
283 collapsed()
284 } else {
285 Style::default()
286 }
287 }
288
289 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
290 if self.offstage {
291 &[]
292 } else {
293 &self.children
294 }
295 }
296
297 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
298
299 fn on_click(&self) -> Option<Msg> {
300 None
301 }
302}
303
304pub struct Visibility<Msg = crate::callback::Callback> {
324 visible: bool,
325 maintain_size: bool,
326 maintain_interactivity: bool,
327 maintain_semantics: bool,
328 child: Vec<Box<dyn Widget<Msg>>>,
331 replacement: Vec<Box<dyn Widget<Msg>>>,
332}
333
334impl<Msg> Visibility<Msg> {
335 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
337 Self {
338 visible: true,
339 maintain_size: false,
340 maintain_interactivity: false,
341 maintain_semantics: false,
342 child: vec![Box::new(child)],
343 replacement: Vec::new(),
344 }
345 }
346
347 pub fn visible(mut self, visible: bool) -> Self {
349 self.visible = visible;
350 self
351 }
352
353 pub fn maintain_size(mut self) -> Self {
356 self.maintain_size = true;
357 self
358 }
359
360 pub fn maintain_interactivity(mut self) -> Self {
364 self.maintain_interactivity = true;
365 self
366 }
367
368 pub fn maintain_semantics(mut self) -> Self {
371 self.maintain_semantics = true;
372 self
373 }
374
375 pub fn replacement(mut self, replacement: impl Widget<Msg> + 'static) -> Self {
378 self.replacement = vec![Box::new(replacement)];
379 self
380 }
381
382 fn keeps_child(&self) -> bool {
385 self.visible || self.maintain_size
386 }
387}
388
389impl<Msg: Clone> Widget<Msg> for Visibility<Msg> {
390 fn style(&self) -> Style {
391 if self.keeps_child() || !self.replacement.is_empty() {
392 Style::default()
393 } else {
394 collapsed()
395 }
396 }
397
398 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
399 if self.keeps_child() {
400 &self.child
401 } else {
402 &self.replacement
403 }
404 }
405
406 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
407
408 fn on_click(&self) -> Option<Msg> {
409 None
410 }
411
412 fn barrier(&self) -> Option<ModalBarrier> {
413 if self.visible || !self.maintain_size {
416 return None;
417 }
418 Some(ModalBarrier {
419 pointer: !self.maintain_interactivity,
420 absorb: false,
421 paint: true,
422 semantics: !self.maintain_semantics,
423 })
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use crate::{build_ui, Button, Container, Runtime, SingleChildScrollView, Text};
431 use frus_core::{Color, Point, Primitive, Size};
432
433 fn ui_of(root: &dyn Widget<i32>) -> crate::Ui<i32> {
434 build_ui(
435 root,
436 Size::new(100.0, 100.0),
437 &Runtime::default(),
438 &crate::Theme::dark(),
439 )
440 }
441
442 fn button(msg: i32) -> Container<i32> {
444 Container::new()
445 .width(100.0)
446 .height(100.0)
447 .color(Color::rgb(1.0, 0.0, 0.0))
448 .on_click(msg)
449 }
450
451 fn centre() -> Point {
453 Point::new(50.0, 50.0)
454 }
455
456 #[test]
457 fn ignoring_takes_the_subtree_out_of_the_input_path() {
458 let plain =
459 crate::Flex::<i32>::column().child(IgnorePointer::new(button(1)).ignoring(false));
460 let ui = ui_of(&plain);
461 assert_eq!(
462 ui.hit(centre()).and_then(|id| ui.msg_for(id)),
463 Some(1),
464 "ignoring(false) leaves the child fully interactive"
465 );
466
467 let ignored = crate::Flex::<i32>::column().child(IgnorePointer::new(button(1)));
468 let ui = ui_of(&ignored);
469 assert!(ui.hit(centre()).is_none());
470 }
471
472 #[test]
473 fn ignoring_lets_input_reach_what_is_behind() {
474 let root = crate::Stack::<i32>::new()
476 .width(100.0)
477 .height(100.0)
478 .layer(button(1))
479 .layer(IgnorePointer::new(button(2)));
480 let ui = ui_of(&root);
481 assert_eq!(
482 ui.hit(centre()).and_then(|id| ui.msg_for(id)),
483 Some(1),
484 "the click falls through the ignored layer to the one underneath"
485 );
486 }
487
488 #[test]
489 fn absorbing_stops_input_at_the_barrier() {
490 let root = crate::Stack::<i32>::new()
491 .width(100.0)
492 .height(100.0)
493 .layer(button(1))
494 .layer(AbsorbPointer::new(button(2)));
495 let ui = ui_of(&root);
496 let id = ui.hit(centre()).expect("the barrier is itself a target");
497 assert_eq!(
498 ui.msg_for(id),
499 None,
500 "it swallows the click rather than emitting anything"
501 );
502 }
503
504 #[test]
505 fn absorbing_can_be_turned_off_from_a_condition() {
506 let root = crate::Stack::<i32>::new()
507 .width(100.0)
508 .height(100.0)
509 .layer(button(1))
510 .layer(AbsorbPointer::new(button(2)).absorbing(false));
511 let ui = ui_of(&root);
512 assert_eq!(ui.hit(centre()).and_then(|id| ui.msg_for(id)), Some(2));
513 }
514
515 #[test]
516 fn a_barrier_still_paints_its_child() {
517 let root = crate::Flex::<i32>::column().child(IgnorePointer::new(button(1)));
518 let ui = ui_of(&root);
519 assert!(
520 ui.scene()
521 .primitives()
522 .iter()
523 .any(|p| matches!(p, Primitive::Rect { color, .. } if color.r > 0.5)),
524 "an ignored subtree is invisible to input, not to the eye"
525 );
526 }
527
528 #[test]
531 fn a_target_deep_inside_is_caught() {
532 let deep = crate::Flex::<i32>::column().child(
533 crate::Flex::<i32>::column().child(crate::Flex::<i32>::column().child(button(1))),
534 );
535 let root = crate::Flex::<i32>::column().child(IgnorePointer::new(deep));
536 let ui = ui_of(&root);
537 assert!(ui.hit(centre()).is_none());
538 }
539
540 #[test]
541 fn a_scrollable_inside_a_barrier_stops_scrolling() {
542 let long = SingleChildScrollView::<i32>::new()
543 .width(100.0)
544 .height(100.0)
545 .child(Container::new().width(100.0).height(1000.0));
546 let root = crate::Flex::<i32>::column().child(IgnorePointer::new(long));
547 let ui = ui_of(&root);
548 assert!(
549 ui.scroll_hit(centre()).is_none(),
550 "the wheel has nothing to scroll through a barrier"
551 );
552 }
553
554 fn announced() -> Button<i32> {
557 Button::new("Send").on_press(1)
558 }
559
560 #[test]
561 fn excluding_semantics_leaves_input_and_paint_alone() {
562 let visible = crate::Flex::<i32>::column().child(announced());
563 let plain = ui_of(&visible);
564 assert!(
565 !plain.semantics().is_empty(),
566 "the fixture has something to exclude"
567 );
568 let on_the_button = Point::new(20.0, 12.0);
570 assert!(plain.hit(on_the_button).is_some(), "and it is clickable");
571
572 let root = crate::Flex::<i32>::column().child(ExcludeSemantics::new(announced()));
573 let ui = ui_of(&root);
574 assert!(
575 ui.semantics().is_empty(),
576 "gone from the accessibility tree"
577 );
578 assert!(ui.hit(on_the_button).is_some(), "and still clickable");
579 }
580
581 #[test]
582 fn an_ignored_subtree_keeps_its_semantics() {
583 let root = crate::Flex::<i32>::column().child(IgnorePointer::new(announced()));
584 let ui = ui_of(&root);
585 assert!(
586 !ui.semantics().is_empty(),
587 "a momentarily inert form is still worth reading out"
588 );
589 assert!(
590 ui.hit(Point::new(20.0, 12.0)).is_none(),
591 "but it takes no input"
592 );
593 }
594
595 #[test]
596 fn offstage_takes_no_room() {
597 let shown = crate::Flex::<i32>::column()
598 .child(Offstage::new(button(1)).offstage(false))
599 .child(Text::new("after"));
600 let ui = ui_of(&shown);
601 assert!(ui.hit(centre()).is_some());
602
603 let hidden = crate::Flex::<i32>::column()
604 .child(Offstage::new(button(1)))
605 .child(Text::new("after"));
606 let ui = ui_of(&hidden);
607 assert!(ui.hit(centre()).is_none());
608 assert!(
609 ui.scene()
610 .primitives()
611 .iter()
612 .all(|p| !matches!(p, Primitive::Rect { color, .. } if color.r > 0.5)),
613 "an offstage child is not painted either"
614 );
615 }
616
617 #[test]
618 fn a_hidden_visibility_collapses_by_default() {
619 let root = crate::Flex::<i32>::column().child(Visibility::new(button(1)).visible(false));
620 let ui = ui_of(&root);
621 assert!(ui.hit(centre()).is_none());
622 assert_eq!(
623 Widget::<i32>::children(&Visibility::new(button(1)).visible(false)).len(),
624 0,
625 "no child in the tree at all"
626 );
627 }
628
629 #[test]
630 fn maintaining_the_size_keeps_the_box_but_not_the_paint() {
631 let hidden = || Visibility::new(button(1)).visible(false).maintain_size();
632 let root = crate::Flex::<i32>::column().child(hidden());
633 let ui = ui_of(&root);
634 assert!(
635 ui.scene()
636 .primitives()
637 .iter()
638 .all(|p| !matches!(p, Primitive::Rect { color, .. } if color.r > 0.5)),
639 "not drawn"
640 );
641 assert!(ui.hit(centre()).is_none(), "and not clickable");
642 assert_eq!(
643 Widget::<i32>::children(&hidden()).len(),
644 1,
645 "but still laid out, so the row does not twitch"
646 );
647 }
648
649 #[test]
650 fn maintained_interactivity_survives_the_barrier() {
651 let root = crate::Flex::<i32>::column().child(
652 Visibility::new(button(1))
653 .visible(false)
654 .maintain_size()
655 .maintain_interactivity(),
656 );
657 let ui = ui_of(&root);
658 assert_eq!(ui.hit(centre()).and_then(|id| ui.msg_for(id)), Some(1));
659 }
660
661 #[test]
662 fn a_replacement_takes_the_hidden_childs_place() {
663 let root = crate::Flex::<i32>::column().child(
664 Visibility::new(button(1))
665 .visible(false)
666 .replacement(button(2)),
667 );
668 let ui = ui_of(&root);
669 assert_eq!(
670 ui.hit(centre()).and_then(|id| ui.msg_for(id)),
671 Some(2),
672 "the replacement is an ordinary child, barrier-free"
673 );
674 }
675
676 #[test]
677 fn a_widget_that_withholds_nothing_declares_no_barrier() {
678 assert_eq!(Widget::<i32>::barrier(&Visibility::new(button(1))), None);
679 assert_eq!(
680 Widget::<i32>::barrier(&IgnorePointer::new(button(1)).ignoring(false)),
681 None
682 );
683 }
684}