Skip to main content

telar_ui_core/
layout_item.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use geometry_core::Rect;
5use layout_core::{LayoutError, LayoutStyle, NodeId};
6use platform_core::Event;
7use reactive_core::RwSignal;
8use ui_tree::{Component, EventResult, RenderNode, Segment};
9
10use crate::context::{new_container, track_layout};
11use crate::layout_leaf::LayoutLeaf;
12
13/// A container child. The boxed widget is shared (`Rc<RefCell<…>>`) between event dispatch (which
14/// borrows it mutably) and its render `segment` (which borrows it immutably to flatten its `view()`)
15/// — they never overlap because dispatch is batched. `rect` is the child's layout signal for hit-testing.
16/// `Clone` is a cheap handle copy (all fields are `Rc`/signal): a reactive list clones a `Child` to move
17/// a reused item to its new position without rebuilding it.
18#[derive(Clone)]
19pub(crate) struct Child {
20    pub(crate) item: Rc<RefCell<Box<dyn LayoutItem>>>,
21    pub(crate) rect: Option<RwSignal<Rect>>,
22    pub(crate) segment: Rc<Segment>,
23}
24
25impl Child {
26    /// The child's layout node, read through the shared widget.
27    pub(crate) fn node(&self) -> layout_core::NodeId {
28        self.item.borrow().layout_node()
29    }
30}
31
32/// Registers an already-built widget as a container child: tracks its layout rect and mounts its render
33/// segment. Used by reactive lists to fold a freshly-built item into the child set (the per-item half of
34/// [`register_container`]).
35pub(crate) fn make_child(widget: Box<dyn LayoutItem>) -> Child {
36    let rect = track_layout(widget.layout_node());
37    let item = Rc::new(RefCell::new(widget));
38    let segment = mount_item_segment(Rc::clone(&item));
39    Child {
40        item,
41        rect,
42        segment,
43    }
44}
45
46pub(crate) type TrackedChildren = Vec<Child>;
47
48/// Mounts a reactive segment that renders a shared boxed item via its `view()`. Uses `try_borrow`
49/// so a re-entrant render while the item is mid event-dispatch (mutably borrowed) keeps the previous
50/// frame instead of panicking; a later flush re-runs it.
51pub(crate) fn mount_item_segment(item: Rc<RefCell<Box<dyn LayoutItem>>>) -> Rc<Segment> {
52    let name = item
53        .try_borrow()
54        .map(|i| i.debug_name())
55        .unwrap_or("Component");
56    Segment::mount_fn_named(name, move || item.try_borrow().ok().map(|i| i.view()))
57}
58
59pub(crate) trait LeafWidget {
60    fn layout_leaf(&self) -> &LayoutLeaf;
61}
62
63pub trait LayoutItem: Component {
64    fn layout_node(&self) -> NodeId;
65}
66
67/// Wraps a child so its rendered output is clipped to the child's own layout rect. When the child
68/// collapses to a zero rect (e.g. a section hidden via `display:none`), the clip is empty, so nothing
69/// inside draws — even a widget left with a stale rect or one that paints at fixed coordinates. Layout
70/// is unchanged: `layout_node` passes through to the wrapped child.
71///
72/// The pointer stops at the same edge. A press or a move landing outside the clip never reaches the subtree,
73/// so a widget cut off by the clip cannot take the click that visually belongs to whatever is drawn over it —
74/// clipped away is *gone*, not merely invisible. Everything else passes through: a release or a `CursorLeft`
75/// is how a widget that was pressed or hovered inside the clip settles again, and swallowing those would leave
76/// it stuck in a state the pointer has already left.
77pub struct ClippedItem {
78    inner: Box<dyn LayoutItem>,
79    rect: RwSignal<Rect>,
80    axis: ClipAxis,
81}
82
83/// Which of a [`ClippedItem`]'s own edges do the cutting.
84#[derive(Clone, Copy, PartialEq, Eq, Debug)]
85pub enum ClipAxis {
86    /// The node's rect, both ways — a viewport.
87    Both,
88    /// Its left and right edges; whatever sits above or below is left alone.
89    Horizontal,
90    /// Its top and bottom edges; whatever sits left or right of it is left alone.
91    Vertical,
92}
93
94/// Half the extent of the free axis of a one-way clip: past any window a platform hands out, and small enough
95/// to stay exact in an `f32`, so the axis bounds nothing without being an infinity the renderer has to
96/// special-case.
97const UNBOUNDED: f32 = 1.0e6;
98
99impl ClippedItem {
100    pub fn new(inner: Box<dyn LayoutItem>) -> Self {
101        Self::along(inner, ClipAxis::Both)
102    }
103
104    /// A clip that cuts along `axis` only, leaving the other free.
105    ///
106    /// What a strip of items wants when it has to stop at its ends but not across its thickness: a tab bar or a
107    /// toolbar cut where the room runs out, whose items still carry a focus ring, a badge or a shadow past the
108    /// strip's own edge. CSS cannot express this — one axis set to `hidden` forces the other out of `visible` —
109    /// so a row that only wanted its ends cut has to clip the overflow it meant to keep.
110    pub fn along(inner: Box<dyn LayoutItem>, axis: ClipAxis) -> Self {
111        let rect = track_layout(inner.layout_node()).expect("clipped item's node not registered");
112        Self { inner, rect, axis }
113    }
114
115    fn clip(&self) -> Rect {
116        let rect = self.rect.get();
117        match self.axis {
118            ClipAxis::Both => rect,
119            ClipAxis::Horizontal => Rect::new(rect.x, -UNBOUNDED, rect.width, UNBOUNDED * 2.0),
120            ClipAxis::Vertical => Rect::new(-UNBOUNDED, rect.y, UNBOUNDED * 2.0, rect.height),
121        }
122    }
123}
124
125impl LayoutItem for ClippedItem {
126    fn layout_node(&self) -> NodeId {
127        self.inner.layout_node()
128    }
129}
130
131impl Component for ClippedItem {
132    fn view(&self) -> RenderNode {
133        RenderNode::Clip {
134            rect: self.clip(),
135            radius: renderer_core::BorderRadius::zero(),
136            children: ui_tree::NodeVec::collect([self.inner.view()]),
137        }
138    }
139
140    fn on_event(&mut self, event: &Event) -> EventResult {
141        let outside = |x: f64, y: f64| !self.clip().contains(x as f32, y as f32);
142        match event {
143            Event::PointerPressed { x, y, .. } | Event::PointerMoved { x, y, .. }
144                if outside(*x, *y) =>
145            {
146                EventResult::Ignored
147            }
148            _ => self.inner.on_event(event),
149        }
150    }
151
152    fn debug_name(&self) -> &'static str {
153        "Clipped"
154    }
155}
156
157impl<T: LeafWidget + Component> LayoutItem for T {
158    fn layout_node(&self) -> NodeId {
159        self.layout_leaf().node
160    }
161}
162
163// Lets an already-boxed child (e.g. the `Box<dyn LayoutItem>` returned by a transpiled `.rsx` component) pass back through `box_item`/`children!` without a second manual wrap, so components compose as `[view]` children.
164impl Component for Box<dyn LayoutItem> {
165    fn view(&self) -> RenderNode {
166        (**self).view()
167    }
168
169    fn on_event(&mut self, event: &Event) -> EventResult {
170        (**self).on_event(event)
171    }
172
173    fn debug_name(&self) -> &'static str {
174        (**self).debug_name()
175    }
176}
177
178impl LayoutItem for Box<dyn LayoutItem> {
179    fn layout_node(&self) -> NodeId {
180        (**self).layout_node()
181    }
182}
183
184// pub so the `children!` macro can call it from any crate without naming the module
185pub fn box_item(item: impl LayoutItem + 'static) -> Box<dyn LayoutItem> {
186    Box::new(item)
187}
188
189pub(crate) fn register_container(
190    layout_style: LayoutStyle,
191    children: Vec<Box<dyn LayoutItem>>,
192) -> Result<(NodeId, RwSignal<Rect>, TrackedChildren), LayoutError> {
193    let child_nodes = children.iter().map(|c| c.layout_node()).collect::<Vec<_>>();
194    let node = new_container(layout_style, &child_nodes)?;
195    let rect = track_layout(node).expect("new_container always registers a signal");
196    let children = children.into_iter().map(make_child).collect();
197    Ok((node, rect, children))
198}
199
200/// Implements `LeafWidget` for a struct that has a `leaf: LayoutLeaf` field.
201#[macro_export]
202macro_rules! impl_leaf_widget {
203    ($struct:ident) => {
204        impl $crate::layout_item::LeafWidget for $struct {
205            fn layout_leaf(&self) -> &$crate::layout_leaf::LayoutLeaf {
206                &self.leaf
207            }
208        }
209    };
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::StyledContainer;
216    use crate::context::{compute_layout, reset_layout_runtime};
217    use layout_core::AvailableSpace;
218    use platform_core::{PointerButton, PointerSource};
219    use renderer_core::RectStyle;
220    use std::cell::Cell;
221    use std::rc::Rc;
222
223    fn press(x: f64, y: f64) -> Event {
224        Event::PointerPressed {
225            x,
226            y,
227            button: PointerButton::Primary,
228            source: PointerSource::Mouse,
229        }
230    }
231
232    fn release(x: f64, y: f64) -> Event {
233        Event::PointerReleased {
234            x,
235            y,
236            button: PointerButton::Primary,
237            source: PointerSource::Mouse,
238        }
239    }
240
241    /// A press outside the clip does not reach the subtree, and one inside it still does.
242    ///
243    /// The case this exists for: a row of items wider than the box it is clipped to. The overflow is not drawn,
244    /// so whatever is painted over that strip looks like the only thing there — and a hidden item that still
245    /// answered a click there would be stealing it from the visible one.
246    #[test]
247    fn a_press_outside_the_clip_never_reaches_what_it_hides() {
248        let pressed = Rc::new(Cell::new(false));
249        let sink = Rc::clone(&pressed);
250        reset_layout_runtime();
251        let inner = StyledContainer::new(
252            LayoutStyle::new().flex_row().width(100.0).height(20.0),
253            |_r| RectStyle::default(),
254            vec![],
255        )
256        .unwrap()
257        .on_press(move || sink.set(true));
258        // The clip is the child's own rect, so it is cut to a 40px window by laying it out in one.
259        let mut clipped = ClippedItem::new(Box::new(
260            StyledContainer::new(
261                LayoutStyle::new().flex_row().width(40.0).height(20.0),
262                |_r| RectStyle::default(),
263                vec![Box::new(inner)],
264            )
265            .unwrap(),
266        ));
267        compute_layout(
268            clipped.layout_node(),
269            AvailableSpace::Definite(40.0),
270            AvailableSpace::Definite(20.0),
271        )
272        .unwrap();
273
274        clipped.on_event(&press(80.0, 10.0));
275        clipped.on_event(&release(80.0, 10.0));
276        assert!(
277            !pressed.get(),
278            "a tap 40px past the clip's edge reached the item hidden behind it"
279        );
280
281        clipped.on_event(&press(10.0, 10.0));
282        clipped.on_event(&release(10.0, 10.0));
283        assert!(
284            pressed.get(),
285            "and a tap on the part that is actually drawn still has to land"
286        );
287    }
288}