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, RenderNode, Segment};
use crate::context::{new_container, track_layout};
use crate::layout_leaf::LayoutLeaf;
#[derive(Clone)]
pub(crate) struct Child {
pub(crate) item: Rc<RefCell<Box<dyn LayoutItem>>>,
pub(crate) rect: Option<RwSignal<Rect>>,
pub(crate) segment: Rc<Segment>,
node: layout_core::NodeId,
}
impl Child {
pub(crate) fn node(&self) -> layout_core::NodeId {
self.node
}
}
pub(crate) fn make_child(widget: Box<dyn LayoutItem>) -> Child {
let node = widget.layout_node();
let rect = track_layout(node);
let item = Rc::new(RefCell::new(widget));
let segment = mount_item_segment(Rc::clone(&item));
Child {
item,
rect,
segment,
node,
}
}
pub(crate) type TrackedChildren = Vec<Child>;
pub(crate) fn mount_item_segment(item: Rc<RefCell<Box<dyn LayoutItem>>>) -> Rc<Segment> {
let name = item
.try_borrow()
.map(|i| i.debug_name())
.unwrap_or("Component");
Segment::mount_fn_named(name, move || item.try_borrow().ok().map(|i| i.view()))
}
pub(crate) trait LeafWidget {
fn layout_leaf(&self) -> &LayoutLeaf;
}
pub trait LayoutItem: Component {
fn layout_node(&self) -> NodeId;
fn pointer_opaque(&self) -> bool {
true
}
}
pub struct ClippedItem {
inner: Box<dyn LayoutItem>,
rect: RwSignal<Rect>,
axis: ClipAxis,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ClipAxis {
Both,
Horizontal,
Vertical,
}
const UNBOUNDED: f32 = 1.0e6;
impl ClippedItem {
pub fn new(inner: Box<dyn LayoutItem>) -> Self {
Self::along(inner, ClipAxis::Both)
}
pub fn along(inner: Box<dyn LayoutItem>, axis: ClipAxis) -> Self {
let rect = track_layout(inner.layout_node()).expect("clipped item's node not registered");
Self { inner, rect, axis }
}
fn clip(&self) -> Rect {
let rect = self.rect.get();
match self.axis {
ClipAxis::Both => rect,
ClipAxis::Horizontal => Rect::new(rect.x, -UNBOUNDED, rect.width, UNBOUNDED * 2.0),
ClipAxis::Vertical => Rect::new(-UNBOUNDED, rect.y, UNBOUNDED * 2.0, rect.height),
}
}
}
impl LayoutItem for ClippedItem {
fn layout_node(&self) -> NodeId {
self.inner.layout_node()
}
fn pointer_opaque(&self) -> bool {
self.inner.pointer_opaque()
}
}
impl Component for ClippedItem {
fn view(&self) -> RenderNode {
RenderNode::Clip {
rect: self.clip(),
radius: renderer_core::BorderRadius::zero(),
children: ui_tree::NodeVec::collect([self.inner.view()]),
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
let outside = |x: f64, y: f64| !self.clip().contains(x as f32, y as f32);
match event {
Event::PointerPressed { x, y, .. } | Event::PointerMoved { x, y, .. }
if outside(*x, *y) =>
{
EventResult::Ignored
}
_ => self.inner.on_event(event),
}
}
fn debug_name(&self) -> &'static str {
"Clipped"
}
}
pub struct Holding {
inner: Box<dyn LayoutItem>,
_effects: Vec<reactive_core::Effect>,
}
impl Holding {
pub fn new(inner: Box<dyn LayoutItem>, effects: Vec<reactive_core::Effect>) -> Self {
Self {
inner,
_effects: effects,
}
}
}
impl LayoutItem for Holding {
fn layout_node(&self) -> NodeId {
self.inner.layout_node()
}
fn pointer_opaque(&self) -> bool {
self.inner.pointer_opaque()
}
}
impl Component for Holding {
fn view(&self) -> RenderNode {
self.inner.view()
}
fn on_event(&mut self, event: &Event) -> EventResult {
self.inner.on_event(event)
}
fn debug_name(&self) -> &'static str {
self.inner.debug_name()
}
}
impl<T: LeafWidget + Component> LayoutItem for T {
fn layout_node(&self) -> NodeId {
self.layout_leaf().node
}
}
impl Component for Box<dyn LayoutItem> {
fn view(&self) -> RenderNode {
(**self).view()
}
fn on_event(&mut self, event: &Event) -> EventResult {
(**self).on_event(event)
}
fn debug_name(&self) -> &'static str {
(**self).debug_name()
}
}
impl LayoutItem for Box<dyn LayoutItem> {
fn layout_node(&self) -> NodeId {
(**self).layout_node()
}
fn pointer_opaque(&self) -> bool {
(**self).pointer_opaque()
}
}
pub fn box_item(item: impl LayoutItem + 'static) -> Box<dyn LayoutItem> {
Box::new(item)
}
pub(crate) fn register_container(
layout_style: LayoutStyle,
children: Vec<Box<dyn LayoutItem>>,
) -> Result<(NodeId, RwSignal<Rect>, TrackedChildren), LayoutError> {
let child_nodes = children.iter().map(|c| c.layout_node()).collect::<Vec<_>>();
let node = new_container(layout_style, &child_nodes)?;
let rect = track_layout(node).expect("new_container always registers a signal");
let children = children.into_iter().map(make_child).collect();
Ok((node, rect, children))
}
#[macro_export]
macro_rules! impl_leaf_widget {
($struct:ident) => {
impl $crate::layout_item::LeafWidget for $struct {
fn layout_leaf(&self) -> &$crate::layout_leaf::LayoutLeaf {
&self.leaf
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::StyledContainer;
use crate::context::{compute_layout, reset_layout_runtime};
use layout_core::AvailableSpace;
use platform_core::{PointerButton, PointerSource};
use renderer_core::RectStyle;
use std::cell::Cell;
use std::rc::Rc;
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,
}
}
#[test]
fn a_press_outside_the_clip_never_reaches_what_it_hides() {
let pressed = Rc::new(Cell::new(false));
let sink = Rc::clone(&pressed);
reset_layout_runtime();
let inner = StyledContainer::new(
LayoutStyle::new().flex_row().width(100.0).height(20.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_press(move || sink.set(true));
let mut clipped = ClippedItem::new(Box::new(
StyledContainer::new(
LayoutStyle::new().flex_row().width(40.0).height(20.0),
|_r| RectStyle::default(),
vec![Box::new(inner)],
)
.unwrap(),
));
compute_layout(
clipped.layout_node(),
AvailableSpace::Definite(40.0),
AvailableSpace::Definite(20.0),
)
.unwrap();
clipped.on_event(&press(80.0, 10.0));
clipped.on_event(&release(80.0, 10.0));
assert!(
!pressed.get(),
"a tap 40px past the clip's edge reached the item hidden behind it"
);
clipped.on_event(&press(10.0, 10.0));
clipped.on_event(&release(10.0, 10.0));
assert!(
pressed.get(),
"and a tap on the part that is actually drawn still has to land"
);
}
}