1use frust_core::{
9 BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent, LayoutCtx,
10 PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
11};
12use kurbo::{Point, Size};
13
14#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct EdgeInsets {
20 pub left: f64,
22 pub top: f64,
24 pub right: f64,
26 pub bottom: f64,
28}
29
30impl EdgeInsets {
31 pub fn all(value: f64) -> Self {
33 Self {
34 left: value,
35 top: value,
36 right: value,
37 bottom: value,
38 }
39 }
40
41 pub fn symmetric(horizontal: f64, vertical: f64) -> Self {
43 Self {
44 left: horizontal,
45 top: vertical,
46 right: horizontal,
47 bottom: vertical,
48 }
49 }
50
51 #[allow(dead_code)]
53 fn horizontal(&self) -> f64 {
54 self.left + self.right
55 }
56
57 #[allow(dead_code)]
59 fn vertical(&self) -> f64 {
60 self.top + self.bottom
61 }
62}
63
64pub struct PaddingView<State: 'static> {
66 insets: EdgeInsets,
67 child: frust_core::AnyView<State>,
68}
69
70#[allow(non_snake_case)]
72pub fn Padding<State: 'static, V: View<State>>(insets: EdgeInsets, child: V) -> PaddingView<State> {
73 PaddingView {
74 insets,
75 child: any(child),
76 }
77}
78
79pub struct PaddingWidget {
81 insets: EdgeInsets,
82 child: ChildPod,
83}
84
85impl<State: 'static> View<State> for PaddingView<State> {
86 type Element = PaddingWidget;
87
88 fn build(&self, ctx: &mut BuildCtx<'_>) -> PaddingWidget {
89 PaddingWidget {
90 insets: self.insets,
91 child: crate::authoring::build_child(&self.child, ctx),
92 }
93 }
94
95 fn rebuild(
96 &self,
97 prev: &Self,
98 element: &mut PaddingWidget,
99 ctx: &mut BuildCtx<'_>,
100 ) -> ChangeFlags {
101 let mut flags = ChangeFlags::NONE;
102 if prev.insets != self.insets {
103 element.insets = self.insets;
104 flags |= ChangeFlags::LAYOUT;
105 }
106 flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
107 flags
108 }
109
110 fn teardown(&self, element: &mut PaddingWidget, ctx: &mut BuildCtx<'_>) {
111 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
112 }
113}
114
115impl Widget for PaddingWidget {
116 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
117 let left = self.insets.left.max(0.0);
120 let top = self.insets.top.max(0.0);
121 let right = self.insets.right.max(0.0);
122 let bottom = self.insets.bottom.max(0.0);
123 let h = left + right;
124 let v = top + bottom;
125 let child_bc = BoxConstraints::new(
128 Size::new(
129 (bc.min().width - h).max(0.0),
130 (bc.min().height - v).max(0.0),
131 ),
132 Size::new(
133 (bc.max().width - h).max(0.0),
134 (bc.max().height - v).max(0.0),
135 ),
136 );
137 let child_size = self.child.layout_child(ctx, &child_bc);
138 self.child.set_origin(Point::new(left, top));
139 bc.constrain(Size::new(child_size.width + h, child_size.height + v))
140 }
141
142 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
143 self.child.paint_child(ctx, scene);
144 }
145
146 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
147 crate::authoring::route_event_single(&mut self.child, ctx, event)
148 }
149
150 fn semantics(&self, ctx: &mut SemanticsCtx) {
151 self.child.semantics_child(ctx);
153 }
154
155 crate::authoring::visit_children!(child);
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::test_support::leaf;
162 use frust_core::{BuildCtx, PointerButton, PointerEvent, PointerPhase};
163 use std::any::Any;
164
165 fn build<S: 'static>(view: &PaddingView<S>) -> PaddingWidget {
166 let mut counter = 0u64;
167 view.build(&mut BuildCtx::new(&mut counter))
168 }
169
170 #[test]
171 fn adds_insets_around_child() {
172 let view: PaddingView<()> = Padding(
174 EdgeInsets {
175 left: 5.0,
176 top: 10.0,
177 right: 15.0,
178 bottom: 20.0,
179 },
180 leaf(40.0, 20.0),
181 );
182 let mut w = build(&view);
183 let mut lctx = LayoutCtx::new();
184 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
185 assert_eq!(size, Size::new(60.0, 50.0));
186 assert_eq!(w.child.origin(), Point::new(5.0, 10.0));
187 assert_eq!(w.child.size(), Size::new(40.0, 20.0));
188 }
189
190 #[test]
191 fn clamps_when_insets_exceed_max() {
192 let view: PaddingView<()> = Padding(EdgeInsets::all(100.0), leaf(30.0, 30.0));
195 let mut w = build(&view);
196 let mut lctx = LayoutCtx::new();
197 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 50.0)));
198 assert_eq!(w.child.size(), Size::ZERO);
199 assert_eq!(size, Size::new(50.0, 50.0));
200 }
201
202 #[test]
203 fn negative_insets_clamp_to_zero() {
204 let view_negative: PaddingView<()> = Padding(
207 EdgeInsets {
208 left: -5.0,
209 top: -10.0,
210 right: -15.0,
211 bottom: -20.0,
212 },
213 leaf(40.0, 20.0),
214 );
215 let view_zero: PaddingView<()> = Padding(EdgeInsets::all(0.0), leaf(40.0, 20.0));
216
217 let mut w_negative = build(&view_negative);
218 let mut w_zero = build(&view_zero);
219 let mut lctx = LayoutCtx::new();
220
221 let size_negative =
222 w_negative.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
223 let size_zero = w_zero.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
224
225 assert_eq!(
226 size_negative, size_zero,
227 "negative and zero insets should produce the same size"
228 );
229 assert_eq!(
230 w_negative.child.origin(),
231 w_zero.child.origin(),
232 "negative and zero insets should place child at the same origin"
233 );
234 assert_eq!(
235 w_negative.child.size(),
236 w_zero.child.size(),
237 "negative and zero insets should give child the same size"
238 );
239 }
240
241 #[derive(Default)]
246 struct Counter {
247 presses: u32,
248 }
249
250 fn pointer_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
251 InputEvent::Pointer(PointerEvent {
252 phase,
253 position: Point::new(x, y),
254 button: PointerButton::Primary,
255 })
256 }
257
258 fn button_padding(insets: EdgeInsets) -> PaddingWidget {
261 let view: PaddingView<Counter> = Padding(
262 insets,
263 crate::button::<Counter, _>("go", |s: &mut Counter| s.presses += 1),
264 );
265 let mut w = build(&view);
266 let mut text_ctx = frust_text::TextContext::new();
267 let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
268 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
269 w
270 }
271
272 fn dispatch<S: 'static>(
273 w: &mut PaddingWidget,
274 state: &mut S,
275 event: &InputEvent,
276 ) -> EventResult {
277 let state_any: &mut dyn Any = state;
278 let mut ctx = EventCtx::new(state_any, Point::ZERO, Size::new(500.0, 500.0));
279 w.event(&mut ctx, event)
280 }
281
282 #[test]
283 fn captured_button_receives_move_and_up_outside_its_bounds() {
284 let mut w = button_padding(EdgeInsets::all(10.0));
285 let mut state = Counter::default();
286 let origin = w.child.origin();
287 let size = w.child.size();
288 assert!(
289 size.width > 0.0 && size.height > 0.0,
290 "button should have real geometry from layout"
291 );
292
293 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
294 assert_eq!(
295 dispatch(
296 &mut w,
297 &mut state,
298 &pointer_ev(PointerPhase::Down, inside.x, inside.y)
299 ),
300 EventResult::Handled
301 );
302 assert!(w.child.is_active(), "down captures the pointer");
303
304 let outside = Point::new(
307 origin.x + size.width + 100.0,
308 origin.y + size.height + 100.0,
309 );
310 assert_eq!(
311 dispatch(
312 &mut w,
313 &mut state,
314 &pointer_ev(PointerPhase::Move, outside.x, outside.y)
315 ),
316 EventResult::Handled,
317 "a captured Move outside the child's bounds must still route to it"
318 );
319
320 assert_eq!(
321 dispatch(
322 &mut w,
323 &mut state,
324 &pointer_ev(PointerPhase::Up, outside.x, outside.y)
325 ),
326 EventResult::Handled,
327 "the capturing Up must still route to the child even though it's outside"
328 );
329 assert_eq!(
330 state.presses, 0,
331 "up outside the button must not fire on_press"
332 );
333 assert!(!w.child.is_active(), "capture releases on Up");
334 }
335
336 #[test]
337 fn captured_button_fires_on_move_back_inside_then_up() {
338 let mut w = button_padding(EdgeInsets::all(10.0));
339 let mut state = Counter::default();
340 let origin = w.child.origin();
341 let size = w.child.size();
342
343 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
344 dispatch(
345 &mut w,
346 &mut state,
347 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
348 );
349
350 let outside = Point::new(origin.x + size.width + 100.0, origin.y);
351 dispatch(
352 &mut w,
353 &mut state,
354 &pointer_ev(PointerPhase::Move, outside.x, outside.y),
355 );
356
357 let back_inside = Point::new(origin.x + 4.0, origin.y + 4.0);
358 dispatch(
359 &mut w,
360 &mut state,
361 &pointer_ev(PointerPhase::Move, back_inside.x, back_inside.y),
362 );
363
364 dispatch(
365 &mut w,
366 &mut state,
367 &pointer_ev(PointerPhase::Up, back_inside.x, back_inside.y),
368 );
369 assert_eq!(state.presses, 1, "up back inside must fire on_press");
370 }
371
372 #[test]
373 fn captured_slider_move_outside_insets_still_reports_value() {
374 #[derive(Default)]
378 struct Val {
379 changes: u32,
380 }
381
382 let view: PaddingView<Val> = Padding(
383 EdgeInsets::all(20.0),
384 crate::slider::<Val, _>(0.0, |s: &mut Val, _v: f64| s.changes += 1),
385 );
386 let mut w = build(&view);
387 let mut lctx = LayoutCtx::new(); w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
389 let origin = w.child.origin();
390
391 let mut state = Val::default();
392 dispatch(
393 &mut w,
394 &mut state,
395 &pointer_ev(PointerPhase::Down, origin.x + 10.0, origin.y + 5.0),
396 );
397 assert_eq!(state.changes, 1);
398 assert!(w.child.is_active());
399
400 let outside = Point::new(5.0, origin.y + 5.0);
403 assert!(
404 !w.child.contains(outside),
405 "sanity: the point is outside the child"
406 );
407 assert_eq!(
408 dispatch(
409 &mut w,
410 &mut state,
411 &pointer_ev(PointerPhase::Move, outside.x, outside.y)
412 ),
413 EventResult::Handled
414 );
415 assert_eq!(
416 state.changes, 2,
417 "captured Move outside the child must still fire on_change"
418 );
419 }
420
421 #[test]
422 fn active_clears_on_up_so_a_later_down_elsewhere_is_not_routed() {
423 let mut w = button_padding(EdgeInsets::all(10.0));
424 let mut state = Counter::default();
425 let origin = w.child.origin();
426
427 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
428 dispatch(
429 &mut w,
430 &mut state,
431 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
432 );
433 dispatch(
434 &mut w,
435 &mut state,
436 &pointer_ev(PointerPhase::Up, inside.x, inside.y),
437 );
438 assert!(!w.child.is_active(), "capture releases on Up");
439
440 assert_eq!(
443 dispatch(
444 &mut w,
445 &mut state,
446 &pointer_ev(PointerPhase::Down, 400.0, 400.0)
447 ),
448 EventResult::Ignored
449 );
450 }
451
452 use std::cell::Cell;
458 use std::rc::Rc;
459
460 const CHILD_W: f64 = 50.0;
461 const CHILD_H: f64 = 20.0;
462
463 struct Captor {
466 id: u32,
467 }
468 struct CaptorWidget {
469 id: u32,
470 armed: bool,
471 }
472
473 impl View<Vec<u32>> for Captor {
474 type Element = CaptorWidget;
475 fn build(&self, _ctx: &mut BuildCtx<'_>) -> CaptorWidget {
476 CaptorWidget {
477 id: self.id,
478 armed: false,
479 }
480 }
481 fn rebuild(
482 &self,
483 _prev: &Self,
484 element: &mut CaptorWidget,
485 _ctx: &mut BuildCtx<'_>,
486 ) -> ChangeFlags {
487 element.id = self.id;
488 ChangeFlags::NONE
489 }
490 }
491
492 impl Widget for CaptorWidget {
493 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
494 bc.constrain(Size::new(CHILD_W, CHILD_H))
495 }
496 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
497 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
498 let InputEvent::Pointer(p) = event else {
499 return EventResult::Ignored;
500 };
501 match p.phase {
502 PointerPhase::Down => {
503 self.armed = true;
504 ctx.capture_pointer();
505 EventResult::Handled
506 }
507 PointerPhase::Move => EventResult::Handled,
508 PointerPhase::Up => {
509 if self.armed {
510 ctx.state_mut::<Vec<u32>>().push(self.id);
511 }
512 self.armed = false;
513 EventResult::Handled
514 }
515 PointerPhase::Cancel => {
516 self.armed = false;
517 EventResult::Handled
518 }
519 }
520 }
521 }
522
523 struct Recorder {
526 seen: Rc<Cell<u32>>,
527 }
528 struct RecorderWidget {
529 seen: Rc<Cell<u32>>,
530 }
531
532 impl View<Vec<u32>> for Recorder {
533 type Element = RecorderWidget;
534 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
535 RecorderWidget {
536 seen: self.seen.clone(),
537 }
538 }
539 fn rebuild(
540 &self,
541 _prev: &Self,
542 _element: &mut RecorderWidget,
543 _ctx: &mut BuildCtx<'_>,
544 ) -> ChangeFlags {
545 ChangeFlags::NONE
546 }
547 }
548
549 impl Widget for RecorderWidget {
550 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
551 bc.constrain(Size::new(CHILD_W, CHILD_H))
552 }
553 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
554 fn event(&mut self, _ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
555 self.seen.set(self.seen.get() + 1);
556 EventResult::Handled
557 }
558 }
559
560 #[test]
561 fn type_swap_at_captured_child_stops_gutter_misrouting() {
562 let insets = EdgeInsets::all(20.0);
570 let prev: PaddingView<Vec<u32>> = Padding(insets, Captor { id: 0 });
571 let mut counter = 0u64;
572 let mut w = prev.build(&mut BuildCtx::new(&mut counter));
573 let mut lctx = LayoutCtx::new();
574 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
575
576 let mut log: Vec<u32> = Vec::new();
577 let origin = w.child.origin();
578 let inside = Point::new(origin.x + 5.0, origin.y + 5.0);
579 dispatch(
580 &mut w,
581 &mut log,
582 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
583 );
584 assert!(
585 w.child.is_active(),
586 "down inside the child arms/captures it"
587 );
588
589 let seen = Rc::new(Cell::new(0u32));
591 let swapped: PaddingView<Vec<u32>> = Padding(insets, Recorder { seen: seen.clone() });
592 swapped.rebuild(&prev, &mut w, &mut BuildCtx::new(&mut counter));
593 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
594 assert!(
595 !w.child.is_active(),
596 "the type swap must clear the stale capture"
597 );
598
599 let gutter = Point::new(5.0, 5.0);
602 assert!(
603 !w.child.contains(gutter),
604 "sanity: the gutter point is outside the child"
605 );
606 let result = dispatch(
607 &mut w,
608 &mut log,
609 &pointer_ev(PointerPhase::Down, gutter.x, gutter.y),
610 );
611 assert_eq!(
612 result,
613 EventResult::Ignored,
614 "a gutter Down must not be routed anywhere"
615 );
616 assert_eq!(
617 seen.get(),
618 0,
619 "the fresh widget must receive nothing from the stale-capture path"
620 );
621 }
622}