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