1use frust_core::{
31 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
32 LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
33};
34use kurbo::{Point, Size};
35
36#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct Alignment {
40 pub x: f64,
42 pub y: f64,
44}
45
46impl Alignment {
47 pub const CENTER: Self = Self { x: 0.0, y: 0.0 };
49 pub const TOP_LEFT: Self = Self { x: -1.0, y: -1.0 };
51 pub const TOP_RIGHT: Self = Self { x: 1.0, y: -1.0 };
53 pub const BOTTOM_LEFT: Self = Self { x: -1.0, y: 1.0 };
55 pub const BOTTOM_RIGHT: Self = Self { x: 1.0, y: 1.0 };
57
58 pub fn new(x: f64, y: f64) -> Self {
60 Self { x, y }
61 }
62
63 fn fraction(component: f64) -> f64 {
66 (component + 1.0) / 2.0
67 }
68}
69
70pub struct AlignView<State: 'static> {
72 alignment: Alignment,
73 child: AnyView<State>,
74}
75
76#[allow(non_snake_case)]
78pub fn Align<State: 'static, V: View<State>>(alignment: Alignment, child: V) -> AlignView<State> {
79 AlignView {
80 alignment,
81 child: any(child),
82 }
83}
84
85pub struct AlignWidget {
87 alignment: Alignment,
88 child: ChildPod,
89}
90
91impl<State: 'static> View<State> for AlignView<State> {
92 type Element = AlignWidget;
93
94 fn build(&self, ctx: &mut BuildCtx<'_>) -> AlignWidget {
95 AlignWidget {
96 alignment: self.alignment,
97 child: crate::authoring::build_child(&self.child, ctx),
98 }
99 }
100
101 fn rebuild(
102 &self,
103 prev: &Self,
104 element: &mut AlignWidget,
105 ctx: &mut BuildCtx<'_>,
106 ) -> ChangeFlags {
107 let mut flags = ChangeFlags::NONE;
108 if prev.alignment != self.alignment {
109 element.alignment = self.alignment;
110 flags |= ChangeFlags::LAYOUT;
111 }
112 flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
113 flags
114 }
115
116 fn teardown(&self, element: &mut AlignWidget, ctx: &mut BuildCtx<'_>) {
117 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
118 }
119}
120
121impl Widget for AlignWidget {
122 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
123 let child_size = self.child.layout_child(ctx, &bc.loosen());
125 let width = if bc.max().width.is_finite() {
129 bc.max().width
130 } else {
131 child_size.width
132 };
133 let height = if bc.max().height.is_finite() {
134 bc.max().height
135 } else {
136 child_size.height
137 };
138 let size = bc.constrain(Size::new(width, height));
139 let x = (size.width - child_size.width) * Alignment::fraction(self.alignment.x);
141 let y = (size.height - child_size.height) * Alignment::fraction(self.alignment.y);
142 self.child.set_origin(Point::new(x, y));
143 size
144 }
145
146 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
147 self.child.paint_child(ctx, scene);
148 }
149
150 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
151 crate::authoring::route_event_single(&mut self.child, ctx, event)
152 }
153
154 fn semantics(&self, ctx: &mut SemanticsCtx) {
155 self.child.semantics_child(ctx);
157 }
158
159 crate::authoring::visit_children!(child);
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::test_support::{RecordingScene, leaf, leaf_any};
166 use frust_core::{BuildCtx, PointerButton, PointerEvent, PointerPhase};
167 use std::any::Any;
168
169 fn build<S: 'static>(view: &AlignView<S>) -> AlignWidget {
170 let mut counter = 0u64;
171 view.build(&mut BuildCtx::new(&mut counter))
172 }
173
174 fn placed(alignment: Alignment) -> (Size, Point) {
175 let view: AlignView<()> = Align(alignment, leaf(20.0, 20.0));
177 let mut w = build(&view);
178 let mut lctx = LayoutCtx::new();
179 let size = w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
180 (size, w.child.origin())
181 }
182
183 #[test]
184 fn fills_box_and_centers_child() {
185 let (size, origin) = placed(Alignment::CENTER);
186 assert_eq!(size, Size::new(100.0, 100.0));
187 assert_eq!(origin, Point::new(40.0, 40.0));
189 }
190
191 #[test]
192 fn positions_child_at_corners() {
193 assert_eq!(placed(Alignment::TOP_LEFT).1, Point::new(0.0, 0.0));
194 assert_eq!(placed(Alignment::TOP_RIGHT).1, Point::new(80.0, 0.0));
195 assert_eq!(placed(Alignment::BOTTOM_LEFT).1, Point::new(0.0, 80.0));
196 assert_eq!(placed(Alignment::BOTTOM_RIGHT).1, Point::new(80.0, 80.0));
197 }
198
199 fn placed_bc(alignment: Alignment, bc: &BoxConstraints) -> (Size, Point) {
205 let view: AlignView<()> = Align(alignment, leaf(20.0, 20.0));
206 let mut w = build(&view);
207 let mut lctx = LayoutCtx::new();
208 let size = w.layout(&mut lctx, bc);
209 (size, w.child.origin())
210 }
211
212 #[test]
213 fn bounded_loose_fills_and_centers_child() {
214 let (size, origin) = placed_bc(
217 Alignment::CENTER,
218 &BoxConstraints::loose(Size::new(100.0, 100.0)),
219 );
220 assert_eq!(size, Size::new(100.0, 100.0), "bounded axes fill to max");
221 assert_eq!(origin, Point::new(40.0, 40.0), "(100-20)*0.5 on each axis");
222 }
223
224 #[test]
225 fn unbounded_shrink_wraps_to_child() {
226 let bc = BoxConstraints::loose(Size::new(f64::INFINITY, f64::INFINITY));
229 let (size, origin) = placed_bc(Alignment::CENTER, &bc);
230 assert_eq!(
231 size,
232 Size::new(20.0, 20.0),
233 "unbounded axes shrink to child"
234 );
235 assert_eq!(origin, Point::ZERO, "no free space to align within");
236 }
237
238 #[test]
239 fn mixed_axis_fills_bounded_shrinks_unbounded() {
240 let bc = BoxConstraints::new(Size::ZERO, Size::new(100.0, f64::INFINITY));
243 let (size, origin) = placed_bc(Alignment::CENTER, &bc);
244 assert_eq!(size, Size::new(100.0, 20.0));
245 assert_eq!(origin, Point::new(40.0, 0.0));
247 }
248
249 #[test]
250 fn tight_passes_through() {
251 let (size, origin) = placed_bc(
253 Alignment::CENTER,
254 &BoxConstraints::tight(Size::new(60.0, 60.0)),
255 );
256 assert_eq!(size, Size::new(60.0, 60.0));
257 assert_eq!(origin, Point::new(20.0, 20.0), "(60-20)*0.5");
258 }
259
260 #[test]
261 fn stack_align_center_positions_small_child_over_larger_sibling() {
262 let view: crate::StackView<()> = crate::Stack(vec![
269 leaf_any(40.0, 40.0),
270 any(Align(Alignment::CENTER, leaf(20.0, 20.0))),
271 ]);
272 let mut counter = 0u64;
273 let mut w = view.build(&mut BuildCtx::new(&mut counter));
274 let mut lctx = LayoutCtx::new();
275 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(40.0, 40.0)));
276 assert_eq!(size, Size::new(40.0, 40.0));
277
278 let mut scene = RecordingScene::default();
282 let mut pctx = PaintCtx::new(Point::ZERO, size);
283 w.paint(&mut pctx, &mut scene);
284
285 assert!(
286 scene
287 .rects
288 .contains(&(Point::new(10.0, 10.0), Size::new(20.0, 20.0))),
289 "the 20x20 child must center at (10,10) over the 40x40 box, not sit \
290 at the origin; recorded rects: {:?}",
291 scene.rects,
292 );
293 }
294
295 #[derive(Default)]
300 struct Counter {
301 presses: u32,
302 }
303
304 fn pointer_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
305 InputEvent::Pointer(PointerEvent {
306 phase,
307 position: Point::new(x, y),
308 button: PointerButton::Primary,
309 })
310 }
311
312 fn button_align() -> AlignWidget {
315 let view: AlignView<Counter> = Align(
316 Alignment::CENTER,
317 crate::button::<Counter, _>("go", |s: &mut Counter| s.presses += 1),
318 );
319 let mut w = build(&view);
320 let mut text_ctx = frust_text::TextContext::new();
321 let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
322 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(500.0, 500.0)));
323 w
324 }
325
326 fn dispatch<S: 'static>(w: &mut AlignWidget, state: &mut S, event: &InputEvent) -> EventResult {
327 let state_any: &mut dyn Any = state;
328 let mut ctx = EventCtx::new(state_any, Point::ZERO, Size::new(500.0, 500.0));
329 w.event(&mut ctx, event)
330 }
331
332 #[test]
333 fn captured_button_receives_move_and_up_outside_its_bounds() {
334 let mut w = button_align();
335 let mut state = Counter::default();
336 let origin = w.child.origin();
337 assert!(
338 origin.x > 0.0 && origin.y > 0.0,
339 "the centered button should have free space around it"
340 );
341
342 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
343 assert_eq!(
344 dispatch(
345 &mut w,
346 &mut state,
347 &pointer_ev(PointerPhase::Down, inside.x, inside.y)
348 ),
349 EventResult::Handled
350 );
351 assert!(w.child.is_active(), "down captures the pointer");
352
353 let outside = Point::new(1.0, 1.0);
356 assert_eq!(
357 dispatch(
358 &mut w,
359 &mut state,
360 &pointer_ev(PointerPhase::Move, outside.x, outside.y)
361 ),
362 EventResult::Handled,
363 "a captured Move outside the child's bounds must still route to it"
364 );
365
366 assert_eq!(
367 dispatch(
368 &mut w,
369 &mut state,
370 &pointer_ev(PointerPhase::Up, outside.x, outside.y)
371 ),
372 EventResult::Handled,
373 "the capturing Up must still route to the child even though it's outside"
374 );
375 assert_eq!(
376 state.presses, 0,
377 "up outside the button must not fire on_press"
378 );
379 assert!(!w.child.is_active(), "capture releases on Up");
380 }
381
382 #[test]
383 fn captured_button_fires_on_move_back_inside_then_up() {
384 let mut w = button_align();
385 let mut state = Counter::default();
386 let origin = w.child.origin();
387
388 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
389 dispatch(
390 &mut w,
391 &mut state,
392 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
393 );
394
395 let outside = Point::new(1.0, 1.0);
396 dispatch(
397 &mut w,
398 &mut state,
399 &pointer_ev(PointerPhase::Move, outside.x, outside.y),
400 );
401
402 let back_inside = Point::new(origin.x + 4.0, origin.y + 4.0);
403 dispatch(
404 &mut w,
405 &mut state,
406 &pointer_ev(PointerPhase::Move, back_inside.x, back_inside.y),
407 );
408
409 dispatch(
410 &mut w,
411 &mut state,
412 &pointer_ev(PointerPhase::Up, back_inside.x, back_inside.y),
413 );
414 assert_eq!(state.presses, 1, "up back inside must fire on_press");
415 }
416
417 #[test]
418 fn active_clears_on_up_so_a_later_down_elsewhere_is_not_routed() {
419 let mut w = button_align();
420 let mut state = Counter::default();
421 let origin = w.child.origin();
422
423 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
424 dispatch(
425 &mut w,
426 &mut state,
427 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
428 );
429 dispatch(
430 &mut w,
431 &mut state,
432 &pointer_ev(PointerPhase::Up, inside.x, inside.y),
433 );
434 assert!(!w.child.is_active(), "capture releases on Up");
435
436 assert_eq!(
439 dispatch(
440 &mut w,
441 &mut state,
442 &pointer_ev(PointerPhase::Down, 1.0, 1.0)
443 ),
444 EventResult::Ignored
445 );
446 }
447
448 #[test]
453 fn type_swap_at_captured_child_clears_active_and_stops_routing() {
454 let mut counter = 0u64;
455 let prev: AlignView<Counter> = Align(
456 Alignment::CENTER,
457 crate::button::<Counter, _>("go", |s: &mut Counter| s.presses += 1),
458 );
459 let mut w = prev.build(&mut BuildCtx::new(&mut counter));
460 let mut text_ctx = frust_text::TextContext::new();
461 let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
462 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(500.0, 500.0)));
463
464 let mut state = Counter::default();
465 let origin = w.child.origin();
466 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
467 dispatch(
468 &mut w,
469 &mut state,
470 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
471 );
472 assert!(w.child.is_active(), "down captures the pointer");
473
474 let swapped: AlignView<Counter> = Align(
476 Alignment::CENTER,
477 crate::checkbox::<Counter, _>(false, "swapped", |_s: &mut Counter, _v: bool| {}),
478 );
479 swapped.rebuild(&prev, &mut w, &mut BuildCtx::new(&mut counter));
480 assert!(
481 !w.child.is_active(),
482 "the type swap must clear the stale capture"
483 );
484
485 let outside = Point::new(1.0, 1.0);
488 assert_eq!(
489 dispatch(
490 &mut w,
491 &mut state,
492 &pointer_ev(PointerPhase::Down, outside.x, outside.y)
493 ),
494 EventResult::Ignored,
495 "a swap must clear active so an out-of-bounds Down is not delivered"
496 );
497 }
498}