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 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 pub fn keeping(mut self, subscription: reactive_core::Effect) -> Self {
78 self.kept_effects.push(subscription);
79 self
80 }
81
82 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 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 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 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 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 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}