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    pub fn rect(&self) -> RwSignal<Rect> {
63        self.rect.clone()
64    }
65
66    fn dispatch_children(&mut self, event: &Event) -> EventResult {
67        match &self.dyn_host {
68            Some(host) => host.dispatch(event),
69            None => dispatch_container_event(&mut self.children, event),
70        }
71    }
72
73    /// Give this container ownership of an [`Effect`](reactive_core::Effect), so it runs for exactly as long as
74    /// the container exists. See [`StyledContainer::keeping`](crate::StyledContainer::keeping) for why that is
75    /// the span an effect belonging to a widget wants, and why neither dropping the handle nor parking it
76    /// somewhere longer-lived is it.
77    pub fn keeping(mut self, subscription: reactive_core::Effect) -> Self {
78        self.kept_effects.push(subscription);
79        self
80    }
81
82    /// Keeps this container's layout style in step with the reactive state it was built from — see
83    /// [`StyledContainer::styled_by`](crate::StyledContainer::styled_by), which is the same thing on a box that
84    /// also paints.
85    pub fn styled_by(self, style: impl Fn() -> LayoutStyle + 'static) -> Self {
86        let node = self.node;
87        self.keeping(crate::styled_container::style_follows(node, style))
88    }
89
90    /// Make the container itself pressable. The callback fires on a tap (release, not press) inside it;
91    /// a child widget that handles the press wins, and a scroll gesture started on it does not fire it.
92    pub fn on_press(mut self, f: impl Fn() + 'static) -> Self {
93        self.press.set(f);
94        self
95    }
96
97    pub fn column(children: Vec<Box<dyn LayoutItem>>) -> Result<Self, LayoutError> {
98        Self::new(LayoutStyle::new().flex_column(), children)
99    }
100}
101
102impl LayoutItem for Container {
103    fn layout_node(&self) -> NodeId {
104        self.node
105    }
106}
107
108impl Component for Container {
109    fn view(&self) -> RenderNode {
110        // 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.
111        match &self.dyn_host {
112            Some(host) => RenderNode::group(host.child_boundaries()),
113            None => RenderNode::group(self.children.iter().map(|c| c.segment.boundary())),
114        }
115    }
116
117    fn on_event(&mut self, event: &Event) -> EventResult {
118        // No tap handler: behave exactly as before (pure child routing).
119        if !self.press.is_set() {
120            return self.dispatch_children(event);
121        }
122        let rect = self.rect.get();
123        match event {
124            Event::PointerMoved { .. } => {
125                self.press.track_move(event);
126                self.dispatch_children(event)
127            }
128            Event::PointerPressed {
129                button: PointerButton::Primary,
130                ..
131            } => {
132                if self.dispatch_children(event) == EventResult::Handled {
133                    self.press.cancel();
134                    return EventResult::Handled;
135                }
136                self.press.arm(event, rect)
137            }
138            Event::PointerReleased {
139                button: PointerButton::Primary,
140                ..
141            } => {
142                if self.dispatch_children(event) == EventResult::Handled {
143                    self.press.cancel();
144                    return EventResult::Handled;
145                }
146                self.press.release(event, rect)
147            }
148            Event::CursorLeft => {
149                self.press.cancel();
150                self.dispatch_children(event)
151            }
152            _ => self.dispatch_children(event),
153        }
154    }
155
156    fn debug_name(&self) -> &'static str {
157        "Container"
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use crate::context::reset_layout_runtime;
164    use layout_core::AvailableSpace;
165    use platform_core::{Event, PointerSource};
166    use renderer_core::{Color, TextStyle};
167
168    use super::*;
169    use crate::context::{compute_layout, new_container};
170    use crate::text::Text;
171
172    fn make_container_with_labels() -> Container {
173        reset_layout_runtime();
174        let text_style = TextStyle::new(14.0, Color::WHITE);
175        let text_a = Text::new(
176            || "A".to_string(),
177            LayoutStyle::new().width(50.0).height(20.0),
178            move || text_style,
179        )
180        .unwrap();
181        let text_b = Text::new(
182            || "B".to_string(),
183            LayoutStyle::new().width(50.0).height(20.0),
184            move || text_style,
185        )
186        .unwrap();
187        let container = Container::new(
188            LayoutStyle::new().flex_row(),
189            vec![Box::new(text_a), Box::new(text_b)],
190        )
191        .unwrap();
192        let root = new_container(
193            LayoutStyle::new().flex_row().width(200.0).height(100.0),
194            &[container.layout_node()],
195        )
196        .unwrap();
197        compute_layout(
198            root,
199            AvailableSpace::Definite(200.0),
200            AvailableSpace::Definite(100.0),
201        )
202        .unwrap();
203        container
204    }
205
206    #[test]
207    fn container_row_creates_ok() {
208        reset_layout_runtime();
209        let result = Container::new(LayoutStyle::new().flex_row(), vec![]);
210        assert!(result.is_ok());
211    }
212
213    #[test]
214    fn container_column_creates_ok() {
215        reset_layout_runtime();
216        let result = Container::column(vec![]);
217        assert!(result.is_ok());
218    }
219
220    #[test]
221    fn container_view_returns_group_with_children() {
222        let container = make_container_with_labels();
223        let view = container.view();
224        if let RenderNode::Group { children, .. } = view {
225            assert_eq!(children.len(), 2);
226        } else {
227            panic!("expected Group");
228        }
229    }
230
231    #[test]
232    fn container_on_event_returns_ignored_with_no_handlers() {
233        let mut container = make_container_with_labels();
234        let result = container.on_event(&Event::PointerMoved {
235            x: 0.0,
236            y: 0.0,
237            source: PointerSource::Mouse,
238        });
239        assert!(matches!(result, EventResult::Ignored));
240    }
241
242    #[test]
243    fn container_layout_node_is_valid() {
244        reset_layout_runtime();
245        let container = Container::new(LayoutStyle::new().flex_row(), vec![]).unwrap();
246        let node = container.layout_node();
247        let _root = new_container(LayoutStyle::new().flex_row(), &[node]).expect("should register");
248    }
249
250    #[test]
251    fn click_with_force_tick_does_not_panic() {
252        use crate::context::track_layout;
253        use crate::styled_container::StyledContainer;
254        use platform_core::PointerButton;
255        use reactive_core::{begin_batch, end_batch, signal};
256
257        reset_layout_runtime();
258        let s = signal(0i32);
259        let s_cb = s.clone();
260        // A pressable primitive stands in for the old high-level Button (now in ui-components).
261        let btn = StyledContainer::new(
262            LayoutStyle::new().width(50.0).height(30.0),
263            |_r| renderer_core::RectStyle::default(),
264            vec![],
265        )
266        .unwrap()
267        .on_press(move || s_cb.update(|n| *n += 1));
268        let btn_node = btn.layout_node();
269        let s_txt = s.clone();
270        let txt = crate::text::Text::new(
271            move || format!("{}", s_txt.get()),
272            LayoutStyle::new().width(50.0).height(20.0),
273            || renderer_core::TextStyle::new(14.0, renderer_core::Color::BLACK),
274        )
275        .unwrap();
276        let root = Container::new(
277            LayoutStyle::new().flex_column().width(200.0).height(100.0),
278            vec![Box::new(btn), Box::new(txt)],
279        )
280        .unwrap();
281        let root_node = root.layout_node();
282        compute_layout(
283            root_node,
284            AvailableSpace::Definite(200.0),
285            AvailableSpace::Definite(100.0),
286        )
287        .unwrap();
288        let br = track_layout(btn_node).unwrap().get();
289
290        let mut tree = crate::ComponentList::new(root);
291        let _ = tree.commands();
292
293        // Mimic the runner's event cycle, including the dev-only force-tick. The button fires on release
294        // (tap), so send press then release.
295        let cx = (br.x + br.width / 2.0) as f64;
296        let cy = (br.y + br.height / 2.0) as f64;
297        for phase in [true, false] {
298            begin_batch();
299            let ev = if phase {
300                Event::PointerPressed {
301                    x: cx,
302                    y: cy,
303                    button: PointerButton::Primary,
304                    source: PointerSource::Mouse,
305                }
306            } else {
307                Event::PointerReleased {
308                    x: cx,
309                    y: cy,
310                    button: PointerButton::Primary,
311                    source: PointerSource::Mouse,
312                }
313            };
314            if tree.on_event(&ev) == EventResult::Handled {
315                tree.bump_force_ticks();
316                end_batch();
317                begin_batch();
318            }
319            let _ = tree.commands();
320            end_batch();
321        }
322
323        assert_eq!(s.get(), 1, "click should have incremented the signal");
324    }
325
326    #[test]
327    fn container_can_be_nested_as_layout_item() {
328        reset_layout_runtime();
329        let inner = Container::column(vec![]).unwrap();
330        let outer = Container::new(LayoutStyle::new().flex_row(), vec![Box::new(inner)]);
331        assert!(outer.is_ok());
332    }
333}