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    /// Whether this widget stands in front of whatever its siblings drew underneath it, for a pointer event
67    /// its parent is hit-testing.
68    ///
69    /// True for anything that occupies its box, which is everything that draws: the topmost child under the
70    /// pointer takes the event whether or not it wants it, exactly as a browser hit-tests — otherwise a
71    /// floating panel lets the wheel through to the pane it covers. The one thing that is not there for this
72    /// purpose is an [`Overlay`](crate::Overlay): the registry routes positioned events to it *before* the
73    /// tree walk, so its in-tree node must not shadow the siblings it was portaled away from.
74    fn pointer_opaque(&self) -> bool {
75        true
76    }
77}
78
79/// Wraps a child so its rendered output is clipped to the child's own layout rect. When the child
80/// collapses to a zero rect (e.g. a section hidden via `display:none`), the clip is empty, so nothing
81/// inside draws — even a widget left with a stale rect or one that paints at fixed coordinates. Layout
82/// is unchanged: `layout_node` passes through to the wrapped child.
83///
84/// The pointer stops at the same edge. A press or a move landing outside the clip never reaches the subtree,
85/// so a widget cut off by the clip cannot take the click that visually belongs to whatever is drawn over it —
86/// clipped away is *gone*, not merely invisible. Everything else passes through: a release or a `CursorLeft`
87/// is how a widget that was pressed or hovered inside the clip settles again, and swallowing those would leave
88/// it stuck in a state the pointer has already left.
89pub struct ClippedItem {
90    inner: Box<dyn LayoutItem>,
91    rect: RwSignal<Rect>,
92    axis: ClipAxis,
93}
94
95/// Which of a [`ClippedItem`]'s own edges do the cutting.
96#[derive(Clone, Copy, PartialEq, Eq, Debug)]
97pub enum ClipAxis {
98    /// The node's rect, both ways — a viewport.
99    Both,
100    /// Its left and right edges; whatever sits above or below is left alone.
101    Horizontal,
102    /// Its top and bottom edges; whatever sits left or right of it is left alone.
103    Vertical,
104}
105
106/// Half the extent of the free axis of a one-way clip: past any window a platform hands out, and small enough
107/// to stay exact in an `f32`, so the axis bounds nothing without being an infinity the renderer has to
108/// special-case.
109const UNBOUNDED: f32 = 1.0e6;
110
111impl ClippedItem {
112    pub fn new(inner: Box<dyn LayoutItem>) -> Self {
113        Self::along(inner, ClipAxis::Both)
114    }
115
116    /// A clip that cuts along `axis` only, leaving the other free.
117    ///
118    /// What a strip of items wants when it has to stop at its ends but not across its thickness: a tab bar or a
119    /// toolbar cut where the room runs out, whose items still carry a focus ring, a badge or a shadow past the
120    /// strip's own edge. CSS cannot express this — one axis set to `hidden` forces the other out of `visible` —
121    /// so a row that only wanted its ends cut has to clip the overflow it meant to keep.
122    pub fn along(inner: Box<dyn LayoutItem>, axis: ClipAxis) -> Self {
123        let rect = track_layout(inner.layout_node()).expect("clipped item's node not registered");
124        Self { inner, rect, axis }
125    }
126
127    fn clip(&self) -> Rect {
128        let rect = self.rect.get();
129        match self.axis {
130            ClipAxis::Both => rect,
131            ClipAxis::Horizontal => Rect::new(rect.x, -UNBOUNDED, rect.width, UNBOUNDED * 2.0),
132            ClipAxis::Vertical => Rect::new(-UNBOUNDED, rect.y, UNBOUNDED * 2.0, rect.height),
133        }
134    }
135}
136
137impl LayoutItem for ClippedItem {
138    fn layout_node(&self) -> NodeId {
139        self.inner.layout_node()
140    }
141
142    fn pointer_opaque(&self) -> bool {
143        self.inner.pointer_opaque()
144    }
145}
146
147impl Component for ClippedItem {
148    fn view(&self) -> RenderNode {
149        RenderNode::Clip {
150            rect: self.clip(),
151            radius: renderer_core::BorderRadius::zero(),
152            children: ui_tree::NodeVec::collect([self.inner.view()]),
153        }
154    }
155
156    fn on_event(&mut self, event: &Event) -> EventResult {
157        let outside = |x: f64, y: f64| !self.clip().contains(x as f32, y as f32);
158        match event {
159            Event::PointerPressed { x, y, .. } | Event::PointerMoved { x, y, .. }
160                if outside(*x, *y) =>
161            {
162                EventResult::Ignored
163            }
164            _ => self.inner.on_event(event),
165        }
166    }
167
168    fn debug_name(&self) -> &'static str {
169        "Clipped"
170    }
171}
172
173/// Wraps a widget so that dropping the widget drops a set of [`Effect`](reactive_core::Effect)s with it.
174///
175/// An `Effect` deregisters on drop, so one bound to a `let` inside a function that returns a widget stops
176/// the moment that function returns — the closure runs exactly once and then never again, which reads as a
177/// working binding right up until the value it derives is expected to move. A widget that owns its effects
178/// ([`Container::keeping`](crate::Container::keeping)) solves that for itself, and this solves it for
179/// anything else: a leaf, a boxed component, whatever a `.rsx` happens to have at its root.
180///
181/// Unrelated to [`kept`](crate::kept), which keeps a *value* across rebuilds of a surface. This keeps a
182/// subscription alive for as long as a widget lives.
183///
184/// Invisible in every other respect. Layout, painting, hit-testing and `debug_name` all pass straight
185/// through, so wrapping costs no layout node and the devtools tree still names the widget underneath.
186pub struct Holding {
187    inner: Box<dyn LayoutItem>,
188    _effects: Vec<reactive_core::Effect>,
189}
190
191impl Holding {
192    pub fn new(inner: Box<dyn LayoutItem>, effects: Vec<reactive_core::Effect>) -> Self {
193        Self {
194            inner,
195            _effects: effects,
196        }
197    }
198}
199
200impl LayoutItem for Holding {
201    fn layout_node(&self) -> NodeId {
202        self.inner.layout_node()
203    }
204
205    fn pointer_opaque(&self) -> bool {
206        self.inner.pointer_opaque()
207    }
208}
209
210impl Component for Holding {
211    fn view(&self) -> RenderNode {
212        self.inner.view()
213    }
214
215    fn on_event(&mut self, event: &Event) -> EventResult {
216        self.inner.on_event(event)
217    }
218
219    fn debug_name(&self) -> &'static str {
220        self.inner.debug_name()
221    }
222}
223
224impl<T: LeafWidget + Component> LayoutItem for T {
225    fn layout_node(&self) -> NodeId {
226        self.layout_leaf().node
227    }
228}
229
230// 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.
231impl Component for Box<dyn LayoutItem> {
232    fn view(&self) -> RenderNode {
233        (**self).view()
234    }
235
236    fn on_event(&mut self, event: &Event) -> EventResult {
237        (**self).on_event(event)
238    }
239
240    fn debug_name(&self) -> &'static str {
241        (**self).debug_name()
242    }
243}
244
245impl LayoutItem for Box<dyn LayoutItem> {
246    fn layout_node(&self) -> NodeId {
247        (**self).layout_node()
248    }
249
250    fn pointer_opaque(&self) -> bool {
251        (**self).pointer_opaque()
252    }
253}
254
255// pub so the `children!` macro can call it from any crate without naming the module
256pub fn box_item(item: impl LayoutItem + 'static) -> Box<dyn LayoutItem> {
257    Box::new(item)
258}
259
260pub(crate) fn register_container(
261    layout_style: LayoutStyle,
262    children: Vec<Box<dyn LayoutItem>>,
263) -> Result<(NodeId, RwSignal<Rect>, TrackedChildren), LayoutError> {
264    let child_nodes = children.iter().map(|c| c.layout_node()).collect::<Vec<_>>();
265    let node = new_container(layout_style, &child_nodes)?;
266    let rect = track_layout(node).expect("new_container always registers a signal");
267    let children = children.into_iter().map(make_child).collect();
268    Ok((node, rect, children))
269}
270
271/// Implements `LeafWidget` for a struct that has a `leaf: LayoutLeaf` field.
272#[macro_export]
273macro_rules! impl_leaf_widget {
274    ($struct:ident) => {
275        impl $crate::layout_item::LeafWidget for $struct {
276            fn layout_leaf(&self) -> &$crate::layout_leaf::LayoutLeaf {
277                &self.leaf
278            }
279        }
280    };
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::StyledContainer;
287    use crate::context::{compute_layout, reset_layout_runtime};
288    use layout_core::AvailableSpace;
289    use platform_core::{PointerButton, PointerSource};
290    use renderer_core::RectStyle;
291    use std::cell::Cell;
292    use std::rc::Rc;
293
294    fn press(x: f64, y: f64) -> Event {
295        Event::PointerPressed {
296            x,
297            y,
298            button: PointerButton::Primary,
299            source: PointerSource::Mouse,
300        }
301    }
302
303    fn release(x: f64, y: f64) -> Event {
304        Event::PointerReleased {
305            x,
306            y,
307            button: PointerButton::Primary,
308            source: PointerSource::Mouse,
309        }
310    }
311
312    /// A press outside the clip does not reach the subtree, and one inside it still does.
313    ///
314    /// The case this exists for: a row of items wider than the box it is clipped to. The overflow is not drawn,
315    /// so whatever is painted over that strip looks like the only thing there — and a hidden item that still
316    /// answered a click there would be stealing it from the visible one.
317    #[test]
318    fn a_press_outside_the_clip_never_reaches_what_it_hides() {
319        let pressed = Rc::new(Cell::new(false));
320        let sink = Rc::clone(&pressed);
321        reset_layout_runtime();
322        let inner = StyledContainer::new(
323            LayoutStyle::new().flex_row().width(100.0).height(20.0),
324            |_r| RectStyle::default(),
325            vec![],
326        )
327        .unwrap()
328        .on_press(move || sink.set(true));
329        // The clip is the child's own rect, so it is cut to a 40px window by laying it out in one.
330        let mut clipped = ClippedItem::new(Box::new(
331            StyledContainer::new(
332                LayoutStyle::new().flex_row().width(40.0).height(20.0),
333                |_r| RectStyle::default(),
334                vec![Box::new(inner)],
335            )
336            .unwrap(),
337        ));
338        compute_layout(
339            clipped.layout_node(),
340            AvailableSpace::Definite(40.0),
341            AvailableSpace::Definite(20.0),
342        )
343        .unwrap();
344
345        clipped.on_event(&press(80.0, 10.0));
346        clipped.on_event(&release(80.0, 10.0));
347        assert!(
348            !pressed.get(),
349            "a tap 40px past the clip's edge reached the item hidden behind it"
350        );
351
352        clipped.on_event(&press(10.0, 10.0));
353        clipped.on_event(&release(10.0, 10.0));
354        assert!(
355            pressed.get(),
356            "and a tap on the part that is actually drawn still has to land"
357        );
358    }
359}