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 children: TrackedChildren,
19 dyn_host: Option<DynHost>,
20 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 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 pub fn keeping(mut self, subscription: reactive_core::Effect) -> Self {
74 self.kept_effects.push(subscription);
75 self
76 }
77
78 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 pub fn on_press(self, f: impl Fn() + 'static) -> Self {
89 self.maybe_on_press(Some(f))
90 }
91
92 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 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 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 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 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 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 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 #[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}