Skip to main content

telar_ui_core/
container.rs

1use geometry_core::Rect;
2use layout_core::{LayoutError, LayoutStyle, NodeId};
3use platform_core::{Event, PointerButton};
4use reactive_core::RwSignal;
5use ui_tree::{Component, EventResult, RenderNode};
6
7use crate::child_host::{ChildSlot, DynHost};
8use crate::context::{new_container, track_layout};
9use crate::layout_item::{LayoutItem, TrackedChildren, register_container};
10use crate::pointer::dispatch_container_event;
11use crate::press::PressGesture;
12
13pub struct Container {
14    node: NodeId,
15    rect: RwSignal<Rect>,
16    // Static children; empty when `dyn_host` is set (a container holding a reactive fragment routes all
17    // children — static and dynamic — through the host so they interleave in the layout node).
18    children: TrackedChildren,
19    dyn_host: Option<DynHost>,
20    // Optional tap gesture so a plain row/col can be pressable; children still hit-test first.
21    // See `StyledContainer::keeping` for why a widget owns its effects.
22    press: PressGesture,
23    kept_effects: Vec<reactive_core::Effect>,
24}
25
26impl Container {
27    pub fn new(
28        layout_style: LayoutStyle,
29        children: Vec<Box<dyn LayoutItem>>,
30    ) -> Result<Self, LayoutError> {
31        let (node, rect, children) = register_container(layout_style, children)?;
32        Ok(Container {
33            node,
34            rect,
35            children,
36            dyn_host: None,
37            press: PressGesture::default(),
38            kept_effects: Vec::new(),
39        })
40    }
41
42    /// A container whose children are a mix of static widgets and reactive fragments (`ChildSlot`s). The
43    /// fragments reconcile into this container's own node, so their items are real siblings of the static
44    /// children and inherit this container's flex direction/gap — the transparent `for`/`if` path.
45    pub fn from_slots(
46        layout_style: LayoutStyle,
47        slots: Vec<ChildSlot>,
48    ) -> Result<Self, LayoutError> {
49        let node = new_container(layout_style, &[])?;
50        let rect = track_layout(node).expect("new_container always registers a signal");
51        let dyn_host = DynHost::build(node, slots)?;
52        Ok(Container {
53            node,
54            rect,
55            children: Vec::new(),
56            dyn_host: Some(dyn_host),
57            press: PressGesture::default(),
58            kept_effects: Vec::new(),
59        })
60    }
61
62    fn dispatch_children(&mut self, event: &Event) -> EventResult {
63        match &self.dyn_host {
64            Some(host) => host.dispatch(event),
65            None => dispatch_container_event(&mut self.children, event),
66        }
67    }
68
69    /// Give this container ownership of an [`Effect`](reactive_core::Effect), so it runs for exactly as long as
70    /// the container exists. See [`StyledContainer::keeping`](crate::StyledContainer::keeping) for why that is
71    /// the span an effect belonging to a widget wants, and why neither dropping the handle nor parking it
72    /// somewhere longer-lived is it.
73    pub fn keeping(mut self, subscription: reactive_core::Effect) -> Self {
74        self.kept_effects.push(subscription);
75        self
76    }
77
78    /// Keeps this container's layout style in step with the reactive state it was built from — see
79    /// [`StyledContainer::styled_by`](crate::StyledContainer::styled_by), which is the same thing on a box that
80    /// also paints.
81    pub fn styled_by(self, style: impl Fn() -> LayoutStyle + 'static) -> Self {
82        let node = self.node;
83        self.keeping(crate::styled_container::style_follows(node, style))
84    }
85
86    /// Make the container itself pressable. The callback fires on a tap (release, not press) inside it;
87    /// a child widget that handles the press wins, and a scroll gesture started on it does not fire it.
88    pub fn on_press(self, f: impl Fn() + 'static) -> Self {
89        self.maybe_on_press(Some(f))
90    }
91
92    /// [`on_press`](Self::on_press) for a handler the caller may not have supplied.
93    ///
94    /// The emitter picks this form for any `on_press:` whose value is not a closure literal, which is how a
95    /// wrapper component forwards an `Option` — and a `Container` reached that emitter with no such method,
96    /// so a plain container forwarding one did not compile. `None` leaves the container untouched: a no-op
97    /// handler would still report the tap `Handled`, turning a display-only row into one that swallows it.
98    pub fn maybe_on_press(mut self, f: Option<impl Fn() + 'static>) -> Self {
99        let Some(f) = f else { return self };
100        self.press.set(f);
101        self.mark_interactive();
102        self
103    }
104
105    /// Registers this node in the interactive registry a click-through surface reads to carve its input region — see `StyledContainer::mark_interactive`.
106    fn mark_interactive(&self) {
107        crate::input_region::register_interactive(self.node, self.rect.read_only());
108    }
109
110    pub fn column(children: Vec<Box<dyn LayoutItem>>) -> Result<Self, LayoutError> {
111        Self::new(LayoutStyle::new().flex_column(), children)
112    }
113}
114
115impl LayoutItem for Container {
116    fn layout_node(&self) -> NodeId {
117        self.node
118    }
119}
120
121impl Component for Container {
122    fn view(&self) -> RenderNode {
123        // Each child is its own segment: referencing it is a cheap Rc clone, so this view() does not re-run children and is not subscribed to their signals.
124        match &self.dyn_host {
125            Some(host) => RenderNode::group(host.child_boundaries()),
126            None => RenderNode::group(self.children.iter().map(|c| c.segment.boundary())),
127        }
128    }
129
130    fn on_event(&mut self, event: &Event) -> EventResult {
131        // No tap handler: behave exactly as before (pure child routing).
132        if !self.press.is_set() {
133            return self.dispatch_children(event);
134        }
135        let rect = self.rect.get();
136        match event {
137            Event::PointerMoved { .. } => {
138                self.press.track_move(event);
139                self.dispatch_children(event)
140            }
141            Event::PointerPressed {
142                button: PointerButton::Primary,
143                ..
144            } => {
145                if self.dispatch_children(event) == EventResult::Handled {
146                    self.press.cancel();
147                    return EventResult::Handled;
148                }
149                self.press.arm(event, rect)
150            }
151            Event::PointerReleased {
152                button: PointerButton::Primary,
153                ..
154            } => {
155                if self.dispatch_children(event) == EventResult::Handled {
156                    self.press.cancel();
157                    return EventResult::Handled;
158                }
159                self.press.release(event, rect)
160            }
161            // Neither will ever deliver the release a tap needs, and a press left armed pairs with whatever release arrives next and fires a click the user never made.
162            Event::CursorLeft | Event::FocusChanged { is_focused: false } => {
163                self.press.cancel();
164                self.dispatch_children(event)
165            }
166            _ => self.dispatch_children(event),
167        }
168    }
169
170    fn debug_name(&self) -> &'static str {
171        "Container"
172    }
173}
174
175impl Drop for Container {
176    fn drop(&mut self) {
177        crate::input_region::unregister_interactive(self.node);
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use crate::context::reset_layout_runtime;
184    use layout_core::AvailableSpace;
185    use platform_core::{Event, PointerSource};
186    use renderer_core::{Color, TextStyle};
187
188    use super::*;
189    use crate::context::{compute_layout, new_container};
190    use crate::text::Text;
191
192    fn make_container_with_labels() -> Container {
193        reset_layout_runtime();
194        let text_style = TextStyle::new(14.0, Color::WHITE);
195        let text_a = Text::new(
196            || "A".to_string(),
197            LayoutStyle::new().width(50.0).height(20.0),
198            move || text_style,
199        )
200        .unwrap();
201        let text_b = Text::new(
202            || "B".to_string(),
203            LayoutStyle::new().width(50.0).height(20.0),
204            move || text_style,
205        )
206        .unwrap();
207        let container = Container::new(
208            LayoutStyle::new().flex_row(),
209            vec![Box::new(text_a), Box::new(text_b)],
210        )
211        .unwrap();
212        let root = new_container(
213            LayoutStyle::new().flex_row().width(200.0).height(100.0),
214            &[container.layout_node()],
215        )
216        .unwrap();
217        compute_layout(
218            root,
219            AvailableSpace::Definite(200.0),
220            AvailableSpace::Definite(100.0),
221        )
222        .unwrap();
223        container
224    }
225
226    #[test]
227    fn container_row_creates_ok() {
228        reset_layout_runtime();
229        let result = Container::new(LayoutStyle::new().flex_row(), vec![]);
230        assert!(result.is_ok());
231    }
232
233    #[test]
234    fn container_column_creates_ok() {
235        reset_layout_runtime();
236        let result = Container::column(vec![]);
237        assert!(result.is_ok());
238    }
239
240    #[test]
241    fn container_view_returns_group_with_children() {
242        let container = make_container_with_labels();
243        let view = container.view();
244        if let RenderNode::Group { children, .. } = view {
245            assert_eq!(children.len(), 2);
246        } else {
247            panic!("expected Group");
248        }
249    }
250
251    #[test]
252    fn container_on_event_returns_ignored_with_no_handlers() {
253        let mut container = make_container_with_labels();
254        let result = container.on_event(&Event::PointerMoved {
255            x: 0.0,
256            y: 0.0,
257            source: PointerSource::Mouse,
258        });
259        assert!(matches!(result, EventResult::Ignored));
260    }
261
262    #[test]
263    fn container_layout_node_is_valid() {
264        reset_layout_runtime();
265        let container = Container::new(LayoutStyle::new().flex_row(), vec![]).unwrap();
266        let node = container.layout_node();
267        let _root = new_container(LayoutStyle::new().flex_row(), &[node]).expect("should register");
268    }
269
270    #[test]
271    fn click_with_force_tick_does_not_panic() {
272        use crate::context::track_layout;
273        use crate::styled_container::StyledContainer;
274        use platform_core::PointerButton;
275        use reactive_core::{begin_batch, end_batch, signal};
276
277        reset_layout_runtime();
278        let s = signal(0i32);
279        let s_cb = s.clone();
280        // A pressable primitive stands in for the old high-level Button (now in ui-components).
281        let btn = StyledContainer::new(
282            LayoutStyle::new().width(50.0).height(30.0),
283            |_r| renderer_core::RectStyle::default(),
284            vec![],
285        )
286        .unwrap()
287        .on_press(move || s_cb.update(|n| *n += 1));
288        let btn_node = btn.layout_node();
289        let s_txt = s.clone();
290        let txt = crate::text::Text::new(
291            move || format!("{}", s_txt.get()),
292            LayoutStyle::new().width(50.0).height(20.0),
293            || renderer_core::TextStyle::new(14.0, renderer_core::Color::BLACK),
294        )
295        .unwrap();
296        let root = Container::new(
297            LayoutStyle::new().flex_column().width(200.0).height(100.0),
298            vec![Box::new(btn), Box::new(txt)],
299        )
300        .unwrap();
301        let root_node = root.layout_node();
302        compute_layout(
303            root_node,
304            AvailableSpace::Definite(200.0),
305            AvailableSpace::Definite(100.0),
306        )
307        .unwrap();
308        let br = track_layout(btn_node).unwrap().get();
309
310        let mut tree = crate::ComponentList::new(root);
311        let _ = tree.commands();
312
313        // Mimic the runner's event cycle, including the dev-only force-tick. The button fires on release
314        // (tap), so send press then release.
315        let cx = (br.x + br.width / 2.0) as f64;
316        let cy = (br.y + br.height / 2.0) as f64;
317        for phase in [true, false] {
318            begin_batch();
319            let ev = if phase {
320                Event::PointerPressed {
321                    x: cx,
322                    y: cy,
323                    button: PointerButton::Primary,
324                    source: PointerSource::Mouse,
325                }
326            } else {
327                Event::PointerReleased {
328                    x: cx,
329                    y: cy,
330                    button: PointerButton::Primary,
331                    source: PointerSource::Mouse,
332                }
333            };
334            if tree.on_event(&ev) == EventResult::Handled {
335                tree.bump_force_ticks();
336                end_batch();
337                begin_batch();
338            }
339            let _ = tree.commands();
340            end_batch();
341        }
342
343        assert_eq!(s.get(), 1, "click should have incremented the signal");
344    }
345
346    // A pressable plain Container must publish its rect to the same interactive registry StyledContainer uses, or a click-through surface never carves an input region for it.
347    #[test]
348    fn pressable_container_publishes_rect_to_interactive_registry_and_withdraws_on_drop() {
349        use crate::interactive_rects;
350
351        reset_layout_runtime();
352        let baseline = interactive_rects().len();
353        let container = Container::new(LayoutStyle::new().width(120.0).height(40.0), vec![])
354            .unwrap()
355            .on_press(|| {});
356        let node = container.layout_node();
357        assert_eq!(
358            interactive_rects().len(),
359            baseline,
360            "an unlaid-out pressable contributes no rect"
361        );
362        compute_layout(
363            node,
364            AvailableSpace::Definite(120.0),
365            AvailableSpace::Definite(40.0),
366        )
367        .unwrap();
368        let rects = interactive_rects();
369        assert_eq!(rects.len(), baseline + 1);
370        assert!(
371            rects.iter().any(|r| r.width == 120.0 && r.height == 40.0),
372            "a laid-out pressable reports its rect"
373        );
374        drop(container);
375        assert_eq!(
376            interactive_rects().len(),
377            baseline,
378            "dropping the pressable withdraws its rect"
379        );
380    }
381
382    #[test]
383    fn container_can_be_nested_as_layout_item() {
384        reset_layout_runtime();
385        let inner = Container::column(vec![]).unwrap();
386        let outer = Container::new(LayoutStyle::new().flex_row(), vec![Box::new(inner)]);
387        assert!(outer.is_ok());
388    }
389}