1use frust_core::{
9 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
10 LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
11};
12use kurbo::{Point, Size};
13
14pub struct SizedBoxView<State: 'static> {
17 width: Option<f64>,
18 height: Option<f64>,
19 child: Option<AnyView<State>>,
20}
21
22#[allow(non_snake_case)]
26pub fn SizedBox<State: 'static>(width: Option<f64>, height: Option<f64>) -> SizedBoxView<State> {
27 SizedBoxView {
28 width,
29 height,
30 child: None,
31 }
32}
33
34impl<State: 'static> SizedBoxView<State> {
35 pub fn child<V: View<State>>(mut self, child: V) -> Self {
37 self.child = Some(any(child));
38 self
39 }
40}
41
42pub struct SizedBoxWidget {
44 width: Option<f64>,
45 height: Option<f64>,
46 child: Option<ChildPod>,
47}
48
49impl<State: 'static> View<State> for SizedBoxView<State> {
50 type Element = SizedBoxWidget;
51
52 fn build(&self, ctx: &mut BuildCtx<'_>) -> SizedBoxWidget {
53 SizedBoxWidget {
54 width: self.width,
55 height: self.height,
56 child: self
57 .child
58 .as_ref()
59 .map(|view| crate::authoring::build_child(view, ctx)),
60 }
61 }
62
63 fn rebuild(
64 &self,
65 prev: &Self,
66 element: &mut SizedBoxWidget,
67 ctx: &mut BuildCtx<'_>,
68 ) -> ChangeFlags {
69 let mut flags = ChangeFlags::NONE;
70 if prev.width != self.width || prev.height != self.height {
71 element.width = self.width;
72 element.height = self.height;
73 flags |= ChangeFlags::LAYOUT;
74 }
75 match (&prev.child, &self.child, &mut element.child) {
76 (Some(prev_view), Some(next_view), Some(pod)) => {
77 flags |= crate::authoring::rebuild_child(prev_view, next_view, pod, ctx);
78 }
79 (None, Some(next_view), _) => {
80 element.child = Some(crate::authoring::build_child(next_view, ctx));
81 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
82 }
83 (Some(prev_view), None, Some(pod)) => {
84 crate::authoring::teardown_child(prev_view, pod, ctx);
85 element.child = None;
86 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
87 }
88 _ => {}
89 }
90 flags
91 }
92
93 fn teardown(&self, element: &mut SizedBoxWidget, ctx: &mut BuildCtx<'_>) {
94 if let (Some(view), Some(pod)) = (&self.child, &mut element.child) {
95 crate::authoring::teardown_child(view, pod, ctx);
96 }
97 }
98}
99
100impl Widget for SizedBoxWidget {
101 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
102 let target_w = self.width.map(|w| w.clamp(bc.min().width, bc.max().width));
104 let target_h = self
105 .height
106 .map(|h| h.clamp(bc.min().height, bc.max().height));
107
108 let child_bc = BoxConstraints::new(
110 Size::new(
111 target_w.unwrap_or(bc.min().width),
112 target_h.unwrap_or(bc.min().height),
113 ),
114 Size::new(
115 target_w.unwrap_or(bc.max().width),
116 target_h.unwrap_or(bc.max().height),
117 ),
118 );
119
120 let size = if let Some(pod) = &mut self.child {
121 let child_size = pod.layout_child(ctx, &child_bc);
122 pod.set_origin(Point::ZERO);
123 Size::new(
125 target_w.unwrap_or(child_size.width),
126 target_h.unwrap_or(child_size.height),
127 )
128 } else {
129 Size::new(
131 target_w.unwrap_or(bc.min().width),
132 target_h.unwrap_or(bc.min().height),
133 )
134 };
135 bc.constrain(size)
136 }
137
138 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
139 if let Some(pod) = &mut self.child {
140 pod.paint_child(ctx, scene);
141 }
142 }
143
144 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
145 match &mut self.child {
146 Some(pod) => crate::authoring::route_event_single(pod, ctx, event),
147 None => EventResult::Ignored,
148 }
149 }
150
151 fn semantics(&self, ctx: &mut SemanticsCtx) {
152 if let Some(pod) = &self.child {
155 pod.semantics_child(ctx);
156 }
157 }
158
159 crate::authoring::visit_children!(child);
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::test_support::leaf;
166 use frust_core::{BuildCtx, PointerButton, PointerEvent, PointerPhase};
167 use std::any::Any;
168
169 fn build<S: 'static>(view: &SizedBoxView<S>) -> SizedBoxWidget {
170 let mut counter = 0u64;
171 view.build(&mut BuildCtx::new(&mut counter))
172 }
173
174 #[test]
175 fn tightens_both_axes_over_child_intrinsic() {
176 let view: SizedBoxView<()> = SizedBox(Some(40.0), Some(40.0)).child(leaf(200.0, 10.0));
178 let mut w = build(&view);
179 let mut lctx = LayoutCtx::new();
180 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
181 assert_eq!(size, Size::new(40.0, 40.0));
182 assert_eq!(w.child.as_ref().unwrap().size(), Size::new(40.0, 40.0));
183 }
184
185 #[test]
186 fn passes_through_the_unspecified_axis() {
187 let view: SizedBoxView<()> = SizedBox(Some(40.0), None).child(leaf(200.0, 30.0));
189 let mut w = build(&view);
190 let mut lctx = LayoutCtx::new();
191 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
192 assert_eq!(size, Size::new(40.0, 30.0));
193 }
194
195 #[test]
196 fn childless_box_is_a_fixed_size_spacer() {
197 let view: SizedBoxView<()> = SizedBox(Some(16.0), Some(24.0));
198 let mut w = build(&view);
199 let mut lctx = LayoutCtx::new();
200 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
201 assert_eq!(size, Size::new(16.0, 24.0));
202 }
203
204 #[derive(Default)]
209 struct Counter {
210 presses: u32,
211 }
212
213 fn pointer_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
214 InputEvent::Pointer(PointerEvent {
215 phase,
216 position: Point::new(x, y),
217 button: PointerButton::Primary,
218 })
219 }
220
221 fn button_sized_box() -> SizedBoxWidget {
224 let view: SizedBoxView<Counter> =
225 SizedBox(Some(300.0), None)
226 .child(crate::button::<Counter, _>("go", |s: &mut Counter| {
227 s.presses += 1
228 }));
229 let mut w = build(&view);
230 let mut text_ctx = frust_text::TextContext::new();
231 let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
232 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
233 w
234 }
235
236 fn dispatch<S: 'static>(
237 w: &mut SizedBoxWidget,
238 state: &mut S,
239 event: &InputEvent,
240 ) -> EventResult {
241 let state_any: &mut dyn Any = state;
242 let mut ctx = EventCtx::new(state_any, Point::ZERO, Size::new(500.0, 500.0));
243 w.event(&mut ctx, event)
244 }
245
246 #[test]
247 fn captured_button_receives_move_and_up_outside_its_bounds() {
248 let mut w = button_sized_box();
249 let mut state = Counter::default();
250 let size = w.child.as_ref().unwrap().size();
251 assert!(size.width > 0.0 && size.height > 0.0);
252
253 assert_eq!(
254 dispatch(
255 &mut w,
256 &mut state,
257 &pointer_ev(PointerPhase::Down, 2.0, 2.0)
258 ),
259 EventResult::Handled
260 );
261 assert!(
262 w.child.as_ref().unwrap().is_active(),
263 "down captures the pointer"
264 );
265
266 let outside = Point::new(size.width + 100.0, size.height + 100.0);
269 assert_eq!(
270 dispatch(
271 &mut w,
272 &mut state,
273 &pointer_ev(PointerPhase::Move, outside.x, outside.y)
274 ),
275 EventResult::Handled,
276 "a captured Move outside the child's bounds must still route to it"
277 );
278
279 assert_eq!(
280 dispatch(
281 &mut w,
282 &mut state,
283 &pointer_ev(PointerPhase::Up, outside.x, outside.y)
284 ),
285 EventResult::Handled,
286 "the capturing Up must still route to the child even though it's outside"
287 );
288 assert_eq!(
289 state.presses, 0,
290 "up outside the button must not fire on_press"
291 );
292 assert!(
293 !w.child.as_ref().unwrap().is_active(),
294 "capture releases on Up"
295 );
296 }
297
298 #[test]
299 fn captured_button_fires_on_move_back_inside_then_up() {
300 let mut w = button_sized_box();
301 let mut state = Counter::default();
302 let size = w.child.as_ref().unwrap().size();
303
304 dispatch(
305 &mut w,
306 &mut state,
307 &pointer_ev(PointerPhase::Down, 2.0, 2.0),
308 );
309
310 let outside = Point::new(size.width + 100.0, 2.0);
311 dispatch(
312 &mut w,
313 &mut state,
314 &pointer_ev(PointerPhase::Move, outside.x, outside.y),
315 );
316
317 dispatch(
318 &mut w,
319 &mut state,
320 &pointer_ev(PointerPhase::Move, 4.0, 4.0),
321 );
322 dispatch(&mut w, &mut state, &pointer_ev(PointerPhase::Up, 4.0, 4.0));
323 assert_eq!(state.presses, 1, "up back inside must fire on_press");
324 }
325
326 #[test]
327 fn active_clears_on_up_so_a_later_down_elsewhere_is_not_routed() {
328 let mut w = button_sized_box();
329 let mut state = Counter::default();
330
331 dispatch(
332 &mut w,
333 &mut state,
334 &pointer_ev(PointerPhase::Down, 2.0, 2.0),
335 );
336 dispatch(&mut w, &mut state, &pointer_ev(PointerPhase::Up, 2.0, 2.0));
337 assert!(
338 !w.child.as_ref().unwrap().is_active(),
339 "capture releases on Up"
340 );
341
342 assert_eq!(
345 dispatch(
346 &mut w,
347 &mut state,
348 &pointer_ev(PointerPhase::Down, 400.0, 400.0)
349 ),
350 EventResult::Ignored
351 );
352 }
353
354 #[test]
355 fn childless_box_ignores_events_without_panicking() {
356 let view: SizedBoxView<()> = SizedBox(Some(16.0), Some(24.0));
357 let mut w = build(&view);
358 let mut state = ();
359 assert_eq!(
360 dispatch(
361 &mut w,
362 &mut state,
363 &pointer_ev(PointerPhase::Down, 5.0, 5.0)
364 ),
365 EventResult::Ignored
366 );
367 assert_eq!(
368 dispatch(
369 &mut w,
370 &mut state,
371 &pointer_ev(PointerPhase::Move, 5.0, 5.0)
372 ),
373 EventResult::Ignored
374 );
375 assert_eq!(
376 dispatch(&mut w, &mut state, &pointer_ev(PointerPhase::Up, 5.0, 5.0)),
377 EventResult::Ignored
378 );
379 }
380
381 #[test]
386 fn type_swap_at_captured_child_clears_active_and_stops_routing() {
387 let mut counter = 0u64;
388 let prev: SizedBoxView<Counter> =
389 SizedBox(Some(300.0), None)
390 .child(crate::button::<Counter, _>("go", |s: &mut Counter| {
391 s.presses += 1
392 }));
393 let mut w = prev.build(&mut BuildCtx::new(&mut counter));
394 let mut text_ctx = frust_text::TextContext::new();
395 let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
396 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
397
398 let mut state = Counter::default();
399 dispatch(
400 &mut w,
401 &mut state,
402 &pointer_ev(PointerPhase::Down, 2.0, 2.0),
403 );
404 assert!(
405 w.child.as_ref().unwrap().is_active(),
406 "down captures the pointer"
407 );
408
409 let swapped: SizedBoxView<Counter> =
411 SizedBox(Some(300.0), None).child(crate::checkbox::<Counter, _>(
412 false,
413 "swapped",
414 |_s: &mut Counter, _v: bool| {},
415 ));
416 swapped.rebuild(&prev, &mut w, &mut BuildCtx::new(&mut counter));
417 assert!(
418 !w.child.as_ref().unwrap().is_active(),
419 "the type swap must clear the stale capture"
420 );
421
422 assert_eq!(
425 dispatch(
426 &mut w,
427 &mut state,
428 &pointer_ev(PointerPhase::Down, 400.0, 400.0)
429 ),
430 EventResult::Ignored,
431 "a swap must clear active so an out-of-bounds Down is not delivered"
432 );
433 }
434}