use std::cell::RefCell;
use std::rc::Rc;
use geometry_core::Rect;
use layout_core::{LayoutError, LayoutStyle, NodeId};
use platform_core::Event;
use reactive_core::RwSignal;
use ui_tree::{
Component, EventResult, OverlaySink, RenderNode, register_overlay, unregister_overlay,
};
use crate::context::{attach_overlay, detach_overlay, remove_node};
use crate::layout_item::{LayoutItem, TrackedChildren, register_container};
use crate::pointer::{dispatch_container_event, offset_pointer};
use crate::scroll_region::visible_rect;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Placement {
Below,
Above,
Start,
End,
}
pub fn anchor_rect(node: NodeId, fallback: &RwSignal<Rect>) -> Rect {
visible_rect(node).unwrap_or_else(|| fallback.peek())
}
#[derive(Clone)]
struct Anchor {
trigger: RwSignal<Rect>,
placement: Placement,
}
fn panel_rect(children: &TrackedChildren, read: impl Fn(&RwSignal<Rect>) -> Rect) -> Rect {
let mut acc: Option<Rect> = None;
for child in children {
if let Some(sig) = &child.rect {
let r = read(sig);
acc = Some(acc.map_or(r, |u| u.union(r)));
}
}
acc.unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0))
}
const EDGE_MARGIN: f32 = 4.0;
const ANCHOR_GAP: f32 = 4.0;
fn anchor_viewport() -> Rect {
crate::context::overlay_viewport().unwrap_or(Rect::new(0.0, 0.0, f32::MAX, f32::MAX))
}
fn anchor_translate(
trigger: Rect,
panel: Rect,
placement: Placement,
viewport: Rect,
) -> (f32, f32) {
let fits_below =
trigger.y + trigger.height + ANCHOR_GAP + panel.height <= viewport.y + viewport.height;
let fits_above = trigger.y - ANCHOR_GAP - panel.height >= viewport.y;
let fits_after =
trigger.x + trigger.width + ANCHOR_GAP + panel.width <= viewport.x + viewport.width;
let fits_before = trigger.x - ANCHOR_GAP - panel.width >= viewport.x;
let placement = match placement {
Placement::Below if !fits_below && fits_above => Placement::Above,
Placement::Above if !fits_above && fits_below => Placement::Below,
Placement::Start if !fits_before && fits_after => Placement::End,
Placement::End if !fits_after && fits_before => Placement::Start,
other => other,
};
let centre_y = trigger.y + (trigger.height - panel.height) / 2.0;
let (target_x, target_y) = match placement {
Placement::Below => (trigger.x, trigger.y + trigger.height + ANCHOR_GAP),
Placement::Above => (trigger.x, trigger.y - panel.height - ANCHOR_GAP),
Placement::Start => (trigger.x - panel.width - ANCHOR_GAP, centre_y),
Placement::End => (trigger.x + trigger.width + ANCHOR_GAP, centre_y),
};
let max_x = viewport.x + viewport.width - panel.width - EDGE_MARGIN;
let target_x = target_x.min(max_x).max(viewport.x + EDGE_MARGIN);
let max_y = viewport.y + viewport.height - panel.height - EDGE_MARGIN;
let target_y = target_y.min(max_y.max(viewport.y)).max(viewport.y);
((target_x - panel.x).round(), (target_y - panel.y).round())
}
fn anchored_placement(
children: &TrackedChildren,
anchor: &Anchor,
read: impl Fn(&RwSignal<Rect>) -> Rect,
) -> (Rect, (f32, f32)) {
let panel = panel_rect(children, &read);
let (dx, dy) = anchor_translate(
read(&anchor.trigger),
panel,
anchor.placement,
anchor_viewport(),
);
(
Rect::new(panel.x + dx, panel.y + dy, panel.width, panel.height),
(dx, dy),
)
}
fn anchored_content_rect(
children: &TrackedChildren,
anchor: &Anchor,
read: impl Fn(&RwSignal<Rect>) -> Rect,
) -> Rect {
anchored_placement(children, anchor, read).0
}
struct OverlaySinkImpl {
content_rect: RwSignal<Rect>,
children: RefCell<TrackedChildren>,
blocking: bool,
anchor: Option<Anchor>,
visible: Rc<dyn Fn() -> bool>,
}
impl OverlaySink for OverlaySinkImpl {
fn content_rect(&self) -> Rect {
if !(self.visible)() {
return Rect::default();
}
match &self.anchor {
None => self.content_rect.peek(),
Some(anchor) => anchored_content_rect(&self.children.borrow(), anchor, |s| s.peek()),
}
}
fn dispatch(&self, event: &Event) -> EventResult {
let offset = self
.anchor
.as_ref()
.map(|anchor| anchored_placement(&self.children.borrow(), anchor, |s| s.peek()).1);
match offset {
Some((dx, dy)) => {
let local = offset_pointer(event, dx as f64, dy as f64);
let event = local.as_ref().unwrap_or(event);
dispatch_container_event(&mut self.children.borrow_mut(), event)
}
None => dispatch_container_event(&mut self.children.borrow_mut(), event),
}
}
fn blocking(&self) -> bool {
self.blocking
}
}
pub struct Overlay {
layout_node: NodeId,
portaled_content: Option<NodeId>,
children: TrackedChildren,
overlay_id: u64,
focus_scope: crate::focus::ScopeId,
anchor: Option<Anchor>,
visible: Rc<dyn Fn() -> bool>,
}
impl Overlay {
pub fn new(
layout_style: LayoutStyle,
children: Vec<Box<dyn LayoutItem>>,
) -> Result<Self, LayoutError> {
Self::build(layout_style, children, true, None, Rc::new(|| true))
}
pub fn toggleable(
layout_style: LayoutStyle,
children: Vec<Box<dyn LayoutItem>>,
visible: impl Fn() -> bool + 'static,
) -> Result<Self, LayoutError> {
Self::build(layout_style, children, true, None, Rc::new(visible))
}
pub fn anchored_click_through(
layout_style: LayoutStyle,
children: Vec<Box<dyn LayoutItem>>,
trigger: RwSignal<Rect>,
placement: Placement,
) -> Result<Self, LayoutError> {
Self::build(
layout_style,
children,
false,
Some(Anchor { trigger, placement }),
Rc::new(|| true),
)
}
fn build(
layout_style: LayoutStyle,
children: Vec<Box<dyn LayoutItem>>,
blocking: bool,
anchor: Option<Anchor>,
visible: Rc<dyn Fn() -> bool>,
) -> Result<Self, LayoutError> {
let (content, content_rect, children) =
register_container(layout_style.absolute_fill(), children)?;
let sink: Rc<dyn OverlaySink> = Rc::new(OverlaySinkImpl {
content_rect,
children: RefCell::new(children.clone()),
blocking,
anchor: anchor.clone(),
visible: visible.clone(),
});
let overlay_id = register_overlay(sink);
let focus_scope = crate::focus::register_scope(
content,
{
let visible = visible.clone();
move || visible()
},
blocking,
);
if attach_overlay(content) {
let (placeholder, _r) =
crate::context::new_leaf(LayoutStyle::new().width(0.0).height(0.0))?;
Ok(Overlay {
layout_node: placeholder,
portaled_content: Some(content),
children,
overlay_id,
focus_scope,
anchor,
visible,
})
} else {
Ok(Overlay {
layout_node: content,
portaled_content: None,
children,
overlay_id,
focus_scope,
anchor,
visible,
})
}
}
}
impl Overlay {
pub fn content_node(&self) -> NodeId {
self.portaled_content.unwrap_or(self.layout_node)
}
}
impl LayoutItem for Overlay {
fn layout_node(&self) -> NodeId {
self.layout_node
}
fn pointer_opaque(&self) -> bool {
false
}
}
impl Component for Overlay {
fn view(&self) -> RenderNode {
if !(self.visible)() {
return RenderNode::Empty;
}
let boundaries = self.children.iter().map(|c| c.segment.boundary());
match &self.anchor {
None => RenderNode::overlay(boundaries),
Some(anchor) => {
let panel = panel_rect(&self.children, |s| s.get());
let (dx, dy) = anchor_translate(
anchor.trigger.get(),
panel,
anchor.placement,
anchor_viewport(),
);
RenderNode::overlay([RenderNode::transform_with(
[1.0, 0.0, 0.0, 1.0, dx, dy],
boundaries,
)])
}
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
if matches!(
event,
Event::PointerPressed { .. }
| Event::PointerMoved { .. }
| Event::PointerReleased { .. }
) {
return EventResult::Ignored;
}
if !(self.visible)() && !matches!(event, Event::CursorLeft | Event::FocusChanged { .. }) {
return EventResult::Ignored;
}
dispatch_container_event(&mut self.children, event)
}
fn debug_name(&self) -> &'static str {
"Overlay"
}
}
impl Drop for Overlay {
fn drop(&mut self) {
unregister_overlay(self.overlay_id);
crate::focus::unregister_scope(self.focus_scope);
if let Some(content) = self.portaled_content {
detach_overlay(content);
remove_node(content);
}
}
}
#[cfg(test)]
impl Overlay {
fn anchored_barrier(&self) -> Rect {
let anchor = self.anchor.as_ref().expect("overlay is not anchored");
anchored_content_rect(&self.children, anchor, |s| s.peek())
}
}
#[cfg(test)]
mod tests {
use crate::context::reset_layout_runtime;
use layout_core::AvailableSpace;
use platform_core::{PointerButton, PointerSource};
use reactive_core::{RwSignal, signal};
use super::*;
use crate::ComponentList;
#[test]
fn a_modal_that_is_up_holds_tab_inside_itself() {
use crate::{StyledContainer, focus};
reset_layout_runtime();
let behind = focus::next_id();
focus::register_as(behind, focus::FocusKind::Widget);
let below = focus::next_id();
let inside = StyledContainer::new(
LayoutStyle::new().width(50.0).height(20.0),
|_r| renderer_core::RectStyle::default(),
vec![],
)
.unwrap()
.on_focus(|_| {});
let _overlay =
Overlay::toggleable(LayoutStyle::new(), vec![Box::new(inside)], || true).unwrap();
let above = focus::next_id();
focus::request(behind);
focus::focus_next();
let landed = focus::current().expect("something took focus");
assert!(
landed > below && landed < above,
"Tab left the modal that is up and landed on the content behind it"
);
}
#[test]
fn a_hidden_overlay_does_not_take_the_keyboard() {
use std::cell::Cell;
use std::rc::Rc;
reset_layout_runtime();
let keys = Rc::new(Cell::new(0u32));
let counted = keys.clone();
let showing = signal(false);
let flag = showing.clone();
let field = crate::StyledContainer::new(
LayoutStyle::new().width(50.0).height(20.0),
|_r| renderer_core::RectStyle::default(),
vec![],
)
.unwrap()
.on_key(move |_| counted.set(counted.get() + 1));
let mut overlay =
Overlay::toggleable(LayoutStyle::new(), vec![Box::new(field)], move || {
flag.get()
})
.unwrap();
let press = Event::KeyPressed {
key: platform_core::Key::Char('a'),
modifiers: platform_core::ModifiersState::default(),
};
overlay.on_event(&press);
assert_eq!(keys.get(), 0, "a shut dialog takes no keys");
showing.set(true);
overlay.on_event(&press);
assert_eq!(keys.get(), 1, "and takes them again once it is up");
showing.set(false);
assert_eq!(
overlay.on_event(&Event::CursorLeft),
crate::EventResult::Ignored,
"CursorLeft reaches the children (they simply had nothing to settle)"
);
}
#[test]
fn tab_does_not_walk_into_an_overlay_that_is_not_showing() {
use crate::{StyledContainer, focus};
reset_layout_runtime();
let base = focus::next_id();
focus::register_as(base, focus::FocusKind::Widget);
let below = focus::next_id();
let field = StyledContainer::new(
LayoutStyle::new().width(50.0).height(20.0),
|_r| renderer_core::RectStyle::default(),
vec![],
)
.unwrap()
.on_focus(|_| {});
let _overlay =
Overlay::toggleable(LayoutStyle::new(), vec![Box::new(field)], || false).unwrap();
let above = focus::next_id();
focus::request(base);
focus::focus_next();
let landed = focus::current().expect("something took focus");
assert!(
!(landed > below && landed < above),
"Tab reached a focusable inside an overlay that is not showing"
);
}
#[test]
fn an_anchored_panel_shifts_and_flips_to_stay_on_screen() {
let viewport = Rect::new(0.0, 0.0, 400.0, 300.0);
let panel = Rect::new(0.0, 0.0, 120.0, 60.0);
let trigger = Rect::new(100.0, 100.0, 40.0, 20.0);
assert_eq!(
anchor_translate(trigger, panel, Placement::Below, viewport),
(100.0, 120.0 + ANCHOR_GAP)
);
let right = Rect::new(380.0, 100.0, 20.0, 20.0);
let (dx, dy) = anchor_translate(right, panel, Placement::Below, viewport);
assert_eq!((dx, dy), (400.0 - 120.0 - EDGE_MARGIN, 120.0 + ANCHOR_GAP));
let low = Rect::new(100.0, 270.0, 40.0, 20.0);
let (_, dy) = anchor_translate(low, panel, Placement::Below, viewport);
assert_eq!(dy, 270.0 - 60.0 - ANCHOR_GAP, "opens upward instead");
let tall = Rect::new(0.0, 0.0, 120.0, 400.0);
let (_, dy) = anchor_translate(low, tall, Placement::Below, viewport);
assert_eq!(dy, 0.0);
}
#[test]
fn a_panel_placed_beside_its_trigger_centres_on_it_and_flips_when_it_has_to() {
let viewport = Rect::new(0.0, 0.0, 400.0, 300.0);
let panel = Rect::new(0.0, 0.0, 120.0, 60.0);
let trigger = Rect::new(200.0, 100.0, 40.0, 20.0);
let (dx, dy) = anchor_translate(trigger, panel, Placement::End, viewport);
assert_eq!(
dx,
240.0 + ANCHOR_GAP,
"starts a gap past where the trigger ends"
);
assert_eq!(dy, 100.0 + (20.0 - 60.0) / 2.0, "centred on the trigger");
let (dx, _) = anchor_translate(trigger, panel, Placement::Start, viewport);
assert_eq!(
dx,
80.0 - ANCHOR_GAP,
"ends a gap before the trigger starts"
);
let rail = Rect::new(4.0, 100.0, 40.0, 20.0);
let (dx, _) = anchor_translate(rail, panel, Placement::Start, viewport);
assert_eq!(dx, 44.0 + ANCHOR_GAP, "flipped to the trailing side");
let odd = Rect::new(200.0, 100.0, 40.0, 21.0);
let (dx, dy) = anchor_translate(odd, panel, Placement::End, viewport);
assert_eq!((dx, dy), (dx.round(), dy.round()), "on the pixel grid");
}
use crate::container::Container;
use crate::context::compute_layout;
fn press(x: f64, y: f64) -> Event {
Event::PointerPressed {
x,
y,
button: PointerButton::Primary,
source: PointerSource::Mouse,
}
}
fn release(x: f64, y: f64) -> Event {
Event::PointerReleased {
x,
y,
button: PointerButton::Primary,
source: PointerSource::Mouse,
}
}
fn route(tree: &mut ComponentList, event: &Event) {
if crate::dispatch_overlays(event) == EventResult::Ignored {
tree.on_event(event);
}
}
fn pressable(flag: RwSignal<bool>) -> Container {
Container::new(LayoutStyle::new().width(400.0).height(400.0), vec![])
.unwrap()
.on_press(move || flag.set(true))
}
#[test]
fn background_alone_receives_tap() {
reset_layout_runtime();
let clicked = signal(false);
let bg = pressable(clicked.clone());
let root = Container::new(
LayoutStyle::new().flex_column().width(400.0).height(400.0),
vec![Box::new(bg)],
)
.unwrap();
let root_node = root.layout_node();
compute_layout(
root_node,
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
let mut tree = ComponentList::new(root);
let _ = tree.commands();
route(&mut tree, &press(200.0, 200.0));
route(&mut tree, &release(200.0, 200.0));
assert!(
clicked.get(),
"background on_press must fire without an overlay"
);
}
#[test]
fn overlay_receives_tap_and_blocks_background() {
reset_layout_runtime();
let bg_clicked = signal(false);
let overlay_clicked = signal(false);
let bg = pressable(bg_clicked.clone());
let scrim = Container::new(LayoutStyle::new().width(400.0).height(400.0), vec![])
.unwrap()
.on_press({
let s = overlay_clicked.clone();
move || s.set(true)
});
let overlay = Overlay::new(LayoutStyle::new(), vec![Box::new(scrim)]).unwrap();
let root = Container::new(
LayoutStyle::new().flex_column().width(400.0).height(400.0),
vec![Box::new(bg), Box::new(overlay)],
)
.unwrap();
let root_node = root.layout_node();
compute_layout(
root_node,
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
let mut tree = ComponentList::new(root);
let _ = tree.commands();
route(&mut tree, &press(200.0, 200.0));
route(&mut tree, &release(200.0, 200.0));
assert!(
overlay_clicked.get(),
"the tap must reach the overlay content"
);
assert!(
!bg_clicked.get(),
"the overlay must block the tap from the content behind it"
);
}
#[test]
fn portaled_overlay_blocks_background() {
use crate::context::relayout_if_dirty;
reset_layout_runtime();
let bg_clicked = signal(false);
let bg = pressable(bg_clicked.clone());
let root = Container::new(
LayoutStyle::new().flex_column().width(400.0).height(400.0),
vec![Box::new(bg)],
)
.unwrap();
let root_node = root.layout_node();
compute_layout(
root_node,
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
let mut tree = ComponentList::new(root);
let _ = tree.commands();
let overlay_clicked = signal(false);
let scrim = Container::new(LayoutStyle::new().width(400.0).height(400.0), vec![])
.unwrap()
.on_press({
let s = overlay_clicked.clone();
move || s.set(true)
});
let _overlay = Overlay::new(LayoutStyle::new(), vec![Box::new(scrim)]).unwrap();
relayout_if_dirty();
route(&mut tree, &press(200.0, 200.0));
route(&mut tree, &release(200.0, 200.0));
assert!(
overlay_clicked.get(),
"the tap must reach the portaled overlay content"
);
assert!(
!bg_clicked.get(),
"the portaled overlay must block the tap from the page behind it"
);
}
#[test]
fn click_through_overlay_lets_background_tap_through() {
reset_layout_runtime();
let bg_clicked = signal(false);
let panel_clicked = signal(false);
let bg = pressable(bg_clicked.clone());
let panel = Container::new(LayoutStyle::new().width(100.0).height(100.0), vec![])
.unwrap()
.on_press({
let s = panel_clicked.clone();
move || s.set(true)
});
let overlay = Overlay::build(
LayoutStyle::new(),
vec![Box::new(panel)],
false,
None,
Rc::new(|| true),
)
.unwrap();
let root = Container::new(
LayoutStyle::new().flex_column().width(400.0).height(400.0),
vec![Box::new(bg), Box::new(overlay)],
)
.unwrap();
let root_node = root.layout_node();
compute_layout(
root_node,
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
let mut tree = ComponentList::new(root);
let _ = tree.commands();
route(&mut tree, &press(200.0, 200.0));
route(&mut tree, &release(200.0, 200.0));
assert!(
bg_clicked.get(),
"a tap on the transparent area must reach the background"
);
assert!(
!panel_clicked.get(),
"the panel must not receive a tap outside it"
);
bg_clicked.set(false);
route(&mut tree, &press(50.0, 50.0));
route(&mut tree, &release(50.0, 50.0));
assert!(panel_clicked.get(), "a tap on the panel must reach it");
assert!(
!bg_clicked.get(),
"the panel must block the tap from the background"
);
}
#[test]
fn anchored_content_tracks_trigger() {
use crate::context::relayout_if_dirty;
reset_layout_runtime();
let root = Container::new(
LayoutStyle::new().flex_column().width(400.0).height(400.0),
vec![],
)
.unwrap();
let root_node = root.layout_node();
compute_layout(
root_node,
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
let tree = ComponentList::new(root);
let _ = tree.commands();
let trigger = signal(Rect::new(50.0, 20.0, 80.0, 30.0));
let panel = Container::new(LayoutStyle::new().width(120.0).height(60.0), vec![]).unwrap();
let overlay = Overlay::build(
LayoutStyle::new(),
vec![Box::new(panel)],
true,
Some(Anchor {
trigger: trigger.clone(),
placement: Placement::Below,
}),
Rc::new(|| true),
)
.unwrap();
relayout_if_dirty();
let rect = overlay.anchored_barrier();
assert_eq!((rect.x, rect.y), (50.0, 50.0 + ANCHOR_GAP));
assert_eq!((rect.width, rect.height), (120.0, 60.0));
trigger.set(Rect::new(200.0, 100.0, 80.0, 30.0));
let rect = overlay.anchored_barrier();
assert_eq!((rect.x, rect.y), (200.0, 130.0 + ANCHOR_GAP));
assert_eq!((rect.width, rect.height), (120.0, 60.0));
}
}