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,
}
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))
}
fn anchor_translate(trigger: Rect, panel: Rect, placement: Placement) -> (f32, f32) {
let (target_x, target_y) = match placement {
Placement::Below => (trigger.x, trigger.y + trigger.height),
Placement::Above => (trigger.x, trigger.y - panel.height),
};
(target_x - panel.x, target_y - panel.y)
}
fn anchored_content_rect(
children: &TrackedChildren,
anchor: &Anchor,
read: impl Fn(&RwSignal<Rect>) -> Rect,
) -> Rect {
let panel = panel_rect(children, &read);
let (dx, dy) = anchor_translate(read(&anchor.trigger), panel, anchor.placement);
Rect::new(panel.x + dx, panel.y + dy, panel.width, panel.height)
}
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| {
let panel = panel_rect(&self.children.borrow(), |s| s.peek());
anchor_translate(anchor.trigger.peek(), panel, anchor.placement)
});
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,
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 new_click_through(
layout_style: LayoutStyle,
children: Vec<Box<dyn LayoutItem>>,
) -> Result<Self, LayoutError> {
Self::build(layout_style, children, false, None, Rc::new(|| true))
}
pub fn anchored(
layout_style: LayoutStyle,
children: Vec<Box<dyn LayoutItem>>,
trigger: RwSignal<Rect>,
placement: Placement,
) -> Result<Self, LayoutError> {
Self::build(
layout_style,
children,
true,
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);
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,
anchor,
visible,
})
} else {
Ok(Overlay {
layout_node: content,
portaled_content: None,
children,
overlay_id,
anchor,
visible,
})
}
}
}
impl LayoutItem for Overlay {
fn layout_node(&self) -> NodeId {
self.layout_node
}
}
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);
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;
}
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);
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;
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::new_click_through(LayoutStyle::new(), vec![Box::new(panel)]).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::anchored(
LayoutStyle::new(),
vec![Box::new(panel)],
trigger.clone(),
Placement::Below,
)
.unwrap();
relayout_if_dirty();
let rect = overlay.anchored_barrier();
assert_eq!((rect.x, rect.y), (50.0, 50.0));
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));
assert_eq!((rect.width, rect.height), (120.0, 60.0));
}
}