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()
269 .child(leaf_any(40.0, 40.0))
270 .child(Align(Alignment::CENTER, leaf(20.0, 20.0)));
271 let mut counter = 0u64;
272 let mut w = view.build(&mut BuildCtx::new(&mut counter));
273 let mut lctx = LayoutCtx::new();
274 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(40.0, 40.0)));
275 assert_eq!(size, Size::new(40.0, 40.0));
276
277 let mut scene = RecordingScene::default();
281 let mut pctx = PaintCtx::new(Point::ZERO, size);
282 w.paint(&mut pctx, &mut scene);
283
284 assert!(
285 scene
286 .rects
287 .contains(&(Point::new(10.0, 10.0), Size::new(20.0, 20.0))),
288 "the 20x20 child must center at (10,10) over the 40x40 box, not sit \
289 at the origin; recorded rects: {:?}",
290 scene.rects,
291 );
292 }
293
294 #[derive(Default)]
299 struct Counter {
300 presses: u32,
301 }
302
303 fn pointer_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
304 InputEvent::Pointer(PointerEvent {
305 phase,
306 position: Point::new(x, y),
307 button: PointerButton::Primary,
308 })
309 }
310
311 fn button_align() -> AlignWidget {
314 let view: AlignView<Counter> = Align(
315 Alignment::CENTER,
316 crate::button::<Counter, _>("go", |s: &mut Counter| s.presses += 1),
317 );
318 let mut w = build(&view);
319 let mut text_ctx = frust_text::TextContext::new();
320 let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
321 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(500.0, 500.0)));
322 w
323 }
324
325 fn dispatch<S: 'static>(w: &mut AlignWidget, state: &mut S, event: &InputEvent) -> EventResult {
326 let state_any: &mut dyn Any = state;
327 let mut ctx = EventCtx::new(state_any, Point::ZERO, Size::new(500.0, 500.0));
328 w.event(&mut ctx, event)
329 }
330
331 #[test]
332 fn captured_button_receives_move_and_up_outside_its_bounds() {
333 let mut w = button_align();
334 let mut state = Counter::default();
335 let origin = w.child.origin();
336 assert!(
337 origin.x > 0.0 && origin.y > 0.0,
338 "the centered button should have free space around it"
339 );
340
341 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
342 assert_eq!(
343 dispatch(
344 &mut w,
345 &mut state,
346 &pointer_ev(PointerPhase::Down, inside.x, inside.y)
347 ),
348 EventResult::Handled
349 );
350 assert!(w.child.is_active(), "down captures the pointer");
351
352 let outside = Point::new(1.0, 1.0);
355 assert_eq!(
356 dispatch(
357 &mut w,
358 &mut state,
359 &pointer_ev(PointerPhase::Move, outside.x, outside.y)
360 ),
361 EventResult::Handled,
362 "a captured Move outside the child's bounds must still route to it"
363 );
364
365 assert_eq!(
366 dispatch(
367 &mut w,
368 &mut state,
369 &pointer_ev(PointerPhase::Up, outside.x, outside.y)
370 ),
371 EventResult::Handled,
372 "the capturing Up must still route to the child even though it's outside"
373 );
374 assert_eq!(
375 state.presses, 0,
376 "up outside the button must not fire on_press"
377 );
378 assert!(!w.child.is_active(), "capture releases on Up");
379 }
380
381 #[test]
382 fn captured_button_fires_on_move_back_inside_then_up() {
383 let mut w = button_align();
384 let mut state = Counter::default();
385 let origin = w.child.origin();
386
387 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
388 dispatch(
389 &mut w,
390 &mut state,
391 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
392 );
393
394 let outside = Point::new(1.0, 1.0);
395 dispatch(
396 &mut w,
397 &mut state,
398 &pointer_ev(PointerPhase::Move, outside.x, outside.y),
399 );
400
401 let back_inside = Point::new(origin.x + 4.0, origin.y + 4.0);
402 dispatch(
403 &mut w,
404 &mut state,
405 &pointer_ev(PointerPhase::Move, back_inside.x, back_inside.y),
406 );
407
408 dispatch(
409 &mut w,
410 &mut state,
411 &pointer_ev(PointerPhase::Up, back_inside.x, back_inside.y),
412 );
413 assert_eq!(state.presses, 1, "up back inside must fire on_press");
414 }
415
416 #[test]
417 fn active_clears_on_up_so_a_later_down_elsewhere_is_not_routed() {
418 let mut w = button_align();
419 let mut state = Counter::default();
420 let origin = w.child.origin();
421
422 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
423 dispatch(
424 &mut w,
425 &mut state,
426 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
427 );
428 dispatch(
429 &mut w,
430 &mut state,
431 &pointer_ev(PointerPhase::Up, inside.x, inside.y),
432 );
433 assert!(!w.child.is_active(), "capture releases on Up");
434
435 assert_eq!(
438 dispatch(
439 &mut w,
440 &mut state,
441 &pointer_ev(PointerPhase::Down, 1.0, 1.0)
442 ),
443 EventResult::Ignored
444 );
445 }
446
447 #[test]
452 fn type_swap_at_captured_child_clears_active_and_stops_routing() {
453 let mut counter = 0u64;
454 let prev: AlignView<Counter> = Align(
455 Alignment::CENTER,
456 crate::button::<Counter, _>("go", |s: &mut Counter| s.presses += 1),
457 );
458 let mut w = prev.build(&mut BuildCtx::new(&mut counter));
459 let mut text_ctx = frust_text::TextContext::new();
460 let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
461 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(500.0, 500.0)));
462
463 let mut state = Counter::default();
464 let origin = w.child.origin();
465 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
466 dispatch(
467 &mut w,
468 &mut state,
469 &pointer_ev(PointerPhase::Down, inside.x, inside.y),
470 );
471 assert!(w.child.is_active(), "down captures the pointer");
472
473 let swapped: AlignView<Counter> = Align(
475 Alignment::CENTER,
476 crate::checkbox::<Counter, _>(false, "swapped", |_s: &mut Counter, _v: bool| {}),
477 );
478 swapped.rebuild(&prev, &mut w, &mut BuildCtx::new(&mut counter));
479 assert!(
480 !w.child.is_active(),
481 "the type swap must clear the stale capture"
482 );
483
484 let outside = Point::new(1.0, 1.0);
487 assert_eq!(
488 dispatch(
489 &mut w,
490 &mut state,
491 &pointer_ev(PointerPhase::Down, outside.x, outside.y)
492 ),
493 EventResult::Ignored,
494 "a swap must clear active so an out-of-bounds Down is not delivered"
495 );
496 }
497}