1use std::hash::{Hash, Hasher};
68use std::rc::Rc;
69
70use frust_core::{
71 BoxConstraints, BuildCtx, ChangeFlags, EventCtx, EventResult, InputEvent, LayoutCtx, PaintCtx,
72 PaintScene, PointerEvent, PointerPhase, View, Widget,
73};
74use kurbo::{Affine, Point, Size};
75
76use crate::authoring::presses;
77
78#[derive(Clone, Copy, Debug, PartialEq)]
83enum CanvasSize {
84 Expand,
86 Explicit(Size),
88}
89
90type CanvasPaint = Rc<dyn Fn(&mut dyn PaintScene, Size, &PaintCtx)>;
95
96type HitTest = Rc<dyn Fn(Point, Size) -> bool>;
99
100type TapCallback<State> = Rc<dyn Fn(&mut State)>;
102
103type PointerCallback<State> = Rc<dyn Fn(&mut State, PointerEvent)>;
106
107pub struct CanvasView<State: 'static> {
109 paint: CanvasPaint,
110 hit: Option<HitTest>,
111 on_tap: Option<TapCallback<State>>,
112 on_pointer: Option<PointerCallback<State>>,
113 size: CanvasSize,
114 repaint_key: Option<u64>,
117}
118
119pub fn canvas<State: 'static, F>(paint: F) -> CanvasView<State>
124where
125 F: Fn(&mut dyn PaintScene, Size, &PaintCtx) + 'static,
126{
127 CanvasView {
128 paint: Rc::new(paint),
129 hit: None,
130 on_tap: None,
131 on_pointer: None,
132 size: CanvasSize::Expand,
133 repaint_key: None,
134 }
135}
136
137impl<State: 'static> CanvasView<State> {
138 pub fn size(mut self, size: Size) -> Self {
143 self.size = CanvasSize::Explicit(size);
144 self
145 }
146
147 pub fn expand(mut self) -> Self {
151 self.size = CanvasSize::Expand;
152 self
153 }
154
155 pub fn on_hit<F: Fn(Point, Size) -> bool + 'static>(mut self, hit: F) -> Self {
161 self.hit = Some(Rc::new(hit));
162 self
163 }
164
165 pub fn on_tap<F: Fn(&mut State) + 'static>(mut self, on_tap: F) -> Self {
168 self.on_tap = Some(Rc::new(on_tap));
169 self
170 }
171
172 pub fn on_pointer<F: Fn(&mut State, PointerEvent) + 'static>(mut self, on_pointer: F) -> Self {
177 self.on_pointer = Some(Rc::new(on_pointer));
178 self
179 }
180
181 pub fn repaint_key(mut self, key: impl Hash) -> Self {
187 let mut hasher = std::collections::hash_map::DefaultHasher::new();
188 key.hash(&mut hasher);
189 self.repaint_key = Some(hasher.finish());
190 self
191 }
192}
193
194fn inside(pos: Point, size: Size) -> bool {
198 pos.x >= 0.0 && pos.y >= 0.0 && pos.x < size.width && pos.y < size.height
199}
200
201pub struct CanvasWidget {
203 paint: CanvasPaint,
204 hit: Option<HitTest>,
205 on_tap: Option<crate::authoring::ErasedCallback>,
206 on_pointer: Option<crate::authoring::ErasedArgCallback<PointerEvent>>,
207 size: CanvasSize,
208 repaint_key: Option<u64>,
209 captured: bool,
213}
214
215impl CanvasWidget {
216 fn hit_test(&self, point: Point, size: Size) -> bool {
218 match &self.hit {
219 Some(hit) => hit(point, size),
220 None => inside(point, size),
221 }
222 }
223}
224
225impl<State: 'static> View<State> for CanvasView<State> {
226 type Element = CanvasWidget;
227
228 fn build(&self, _ctx: &mut BuildCtx<'_>) -> CanvasWidget {
229 CanvasWidget {
230 paint: self.paint.clone(),
231 hit: self.hit.clone(),
232 on_tap: self.on_tap.as_ref().map(crate::authoring::erase_callback),
233 on_pointer: self
234 .on_pointer
235 .as_ref()
236 .map(crate::authoring::erase_callback_arg),
237 size: self.size,
238 repaint_key: self.repaint_key,
239 captured: false,
240 }
241 }
242
243 fn rebuild(
244 &self,
245 prev: &Self,
246 element: &mut CanvasWidget,
247 _ctx: &mut BuildCtx<'_>,
248 ) -> ChangeFlags {
249 let mut flags = ChangeFlags::NONE;
250
251 element.paint = self.paint.clone();
256 element.hit = self.hit.clone();
257 element.on_tap = self.on_tap.as_ref().map(crate::authoring::erase_callback);
258 element.on_pointer = self
259 .on_pointer
260 .as_ref()
261 .map(crate::authoring::erase_callback_arg);
262
263 if prev.size != self.size {
264 element.size = self.size;
265 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
266 }
267
268 if prev.repaint_key != self.repaint_key {
272 flags |= ChangeFlags::PAINT;
273 }
274 element.repaint_key = self.repaint_key;
275
276 flags
277 }
278}
279
280impl Widget for CanvasWidget {
281 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
282 match self.size {
283 CanvasSize::Explicit(size) => bc.constrain(size),
284 CanvasSize::Expand => bc.max(),
285 }
286 }
287
288 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
289 let origin = ctx.origin();
290 let size = ctx.size();
291 scene.push_transform(Affine::translate(origin.to_vec2()));
296 scene.push_clip(Point::ZERO, size);
297 (self.paint)(scene, size, ctx);
298 scene.pop_clip();
299 scene.pop_transform();
300 }
301
302 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
303 let InputEvent::Pointer(p) = event else {
304 return EventResult::Ignored;
305 };
306 match p.phase {
307 PointerPhase::Down => {
308 if !presses(p) || !self.hit_test(p.position, ctx.size()) {
312 return EventResult::Ignored;
313 }
314 self.captured = true;
315 ctx.capture_pointer();
316 if let Some(cb) = self.on_pointer.as_mut() {
317 cb(ctx, *p);
318 }
319 ctx.request_redraw();
320 EventResult::Handled
321 }
322 PointerPhase::Move => {
323 if !self.captured {
324 return EventResult::Ignored;
325 }
326 if let Some(cb) = self.on_pointer.as_mut() {
327 cb(ctx, *p);
328 ctx.request_redraw();
329 }
330 EventResult::Handled
331 }
332 PointerPhase::Up => {
333 if !self.captured {
334 return EventResult::Ignored;
335 }
336 self.captured = false;
337 if self.hit_test(p.position, ctx.size()) {
341 if let Some(cb) = self.on_pointer.as_mut() {
342 cb(ctx, *p);
343 }
344 if let Some(cb) = self.on_tap.as_mut() {
345 cb(ctx);
346 }
347 }
348 ctx.request_redraw();
349 EventResult::Handled
350 }
351 PointerPhase::Cancel => {
352 if !self.captured {
353 return EventResult::Ignored;
354 }
355 self.captured = false;
359 EventResult::Handled
360 }
361 }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use frust_core::{BuildCtx, PointerButton};
369 use peniko::Color;
370 use std::any::Any;
371 use std::cell::RefCell;
372
373 fn build<State: 'static>(view: &CanvasView<State>) -> CanvasWidget {
374 let mut counter = 0u64;
375 View::<State>::build(view, &mut BuildCtx::new(&mut counter))
376 }
377
378 fn rebuild<State: 'static>(
379 prev: &CanvasView<State>,
380 next: &CanvasView<State>,
381 element: &mut CanvasWidget,
382 ) -> ChangeFlags {
383 let mut counter = 0u64;
384 View::<State>::rebuild(next, prev, element, &mut BuildCtx::new(&mut counter))
385 }
386
387 #[derive(Default)]
389 struct Recorder {
390 ops: Vec<Op>,
391 }
392
393 #[derive(Debug, PartialEq)]
394 enum Op {
395 Transform(Affine),
396 PopTransform,
397 Clip(Point, Size),
398 PopClip,
399 Fill(Point, Size, Color),
400 }
401
402 impl PaintScene for Recorder {
403 fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
404 self.ops.push(Op::Fill(origin, size, color));
405 }
406 fn draw_text(&mut self, _origin: Point, _text: &str) {}
407 fn push_transform(&mut self, transform: Affine) {
408 self.ops.push(Op::Transform(transform));
409 }
410 fn pop_transform(&mut self) {
411 self.ops.push(Op::PopTransform);
412 }
413 fn push_clip(&mut self, origin: Point, size: Size) {
414 self.ops.push(Op::Clip(origin, size));
415 }
416 fn pop_clip(&mut self) {
417 self.ops.push(Op::PopClip);
418 }
419 }
420
421 fn pointer_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
422 InputEvent::Pointer(PointerEvent {
423 phase,
424 position: Point::new(x, y),
425 button: PointerButton::Primary,
426 })
427 }
428
429 #[test]
432 fn expand_fills_the_incoming_constraints_max() {
433 let view = canvas::<(), _>(|_scene, _size, _ctx| {});
434 let mut w = build(&view);
435 let mut lctx = LayoutCtx::new();
436 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
437 assert_eq!(size, Size::new(300.0, 200.0));
438 }
439
440 #[test]
441 fn explicit_size_is_clamped_into_constraints() {
442 let view = canvas::<(), _>(|_scene, _size, _ctx| {}).size(Size::new(1000.0, 1000.0));
443 let mut w = build(&view);
444 let mut lctx = LayoutCtx::new();
445 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
446 assert_eq!(size, Size::new(300.0, 200.0));
447 }
448
449 #[test]
452 fn paint_runs_the_closure_with_the_laid_out_size_translated_and_clipped() {
453 let seen_size = Rc::new(RefCell::new(None));
454 let seen_size_cb = seen_size.clone();
455 let view = canvas::<(), _>(move |scene, size, _ctx| {
456 *seen_size_cb.borrow_mut() = Some(size);
457 scene.fill_rect(Point::new(1.0, 2.0), Size::new(3.0, 4.0), Color::BLACK);
458 });
459 let mut w = build(&view);
460 let mut lctx = LayoutCtx::new();
461 let laid_out = w.layout(&mut lctx, &BoxConstraints::tight(Size::new(80.0, 60.0)));
462 assert_eq!(laid_out, Size::new(80.0, 60.0));
463
464 let mut rec = Recorder::default();
465 let mut pctx = PaintCtx::new(Point::new(10.0, 20.0), Size::new(80.0, 60.0));
466 w.paint(&mut pctx, &mut rec);
467
468 assert_eq!(
469 *seen_size.borrow(),
470 Some(Size::new(80.0, 60.0)),
471 "the closure sees the laid-out size"
472 );
473 assert_eq!(
474 rec.ops,
475 vec![
476 Op::Transform(Affine::translate((10.0, 20.0))),
477 Op::Clip(Point::ZERO, Size::new(80.0, 60.0)),
478 Op::Fill(Point::new(1.0, 2.0), Size::new(3.0, 4.0), Color::BLACK),
479 Op::PopClip,
480 Op::PopTransform,
481 ],
482 "the widget pushes its absolute origin as a transform and clips to \
483 its bounds before the closure paints, then pops both"
484 );
485 }
486
487 #[test]
490 fn unset_repaint_key_requests_no_paint_flag_on_an_otherwise_unchanged_rebuild() {
491 let prev = canvas::<(), _>(|_scene, _size, _ctx| {});
492 let mut w = build(&prev);
493 let next = canvas::<(), _>(|_scene, _size, _ctx| {});
494 let flags = rebuild(&prev, &next, &mut w);
495 assert_eq!(
496 flags,
497 ChangeFlags::NONE,
498 "no repaint_key set: an otherwise-unchanged rebuild requests nothing"
499 );
500 }
501
502 #[test]
503 fn a_changed_repaint_key_requests_paint() {
504 let prev = canvas::<(), _>(|_scene, _size, _ctx| {}).repaint_key(1u32);
505 let mut w = build(&prev);
506 let next = canvas::<(), _>(|_scene, _size, _ctx| {}).repaint_key(2u32);
507 let flags = rebuild(&prev, &next, &mut w);
508 assert!(flags.needs_paint(), "a changed repaint_key requests PAINT");
509 }
510
511 #[test]
512 fn an_unchanged_repaint_key_requests_no_paint() {
513 let prev = canvas::<(), _>(|_scene, _size, _ctx| {}).repaint_key(7u32);
514 let mut w = build(&prev);
515 let next = canvas::<(), _>(|_scene, _size, _ctx| {}).repaint_key(7u32);
516 let flags = rebuild(&prev, &next, &mut w);
517 assert_eq!(
518 flags,
519 ChangeFlags::NONE,
520 "an unchanged repaint_key requests nothing"
521 );
522 }
523
524 #[derive(Default)]
527 struct TapState {
528 taps: u32,
529 pointers: u32,
530 }
531
532 fn dispatch(w: &mut CanvasWidget, state: &mut TapState, event: &InputEvent, size: Size) {
533 let state_any: &mut dyn Any = state;
534 let mut ctx = EventCtx::new(state_any, Point::ZERO, size);
535 w.event(&mut ctx, event);
536 }
537
538 #[test]
539 fn with_no_on_hit_a_tap_inside_the_rect_fires() {
540 let view = canvas::<TapState, _>(|_scene, _size, _ctx| {}).on_tap(|s: &mut TapState| {
541 s.taps += 1;
542 });
543 let mut w = build(&view);
544 let mut state = TapState::default();
545 let size = Size::new(100.0, 100.0);
546 dispatch(
547 &mut w,
548 &mut state,
549 &pointer_ev(PointerPhase::Down, 10.0, 10.0),
550 size,
551 );
552 dispatch(
553 &mut w,
554 &mut state,
555 &pointer_ev(PointerPhase::Up, 10.0, 10.0),
556 size,
557 );
558 assert_eq!(state.taps, 1, "the default rectangular hit-test admits it");
559 }
560
561 #[test]
562 fn on_hit_rejects_a_down_outside_its_shape_never_capturing() {
563 let view = canvas::<TapState, _>(|_scene, _size, _ctx| {})
568 .on_hit(|p: Point, size: Size| {
569 let center = Point::new(size.width / 2.0, size.height / 2.0);
570 (p - center).hypot() <= 10.0
571 })
572 .on_tap(|s: &mut TapState| s.taps += 1)
573 .on_pointer(|s: &mut TapState, _ev| s.pointers += 1);
574 let mut w = build(&view);
575 let mut state = TapState::default();
576 let size = Size::new(100.0, 100.0);
577
578 dispatch(
579 &mut w,
580 &mut state,
581 &pointer_ev(PointerPhase::Down, 1.0, 1.0),
582 size,
583 );
584 assert!(!w.captured, "a hit-failing Down must not open a capture");
585 assert_eq!(state.pointers, 0);
586
587 dispatch(
590 &mut w,
591 &mut state,
592 &pointer_ev(PointerPhase::Up, 1.0, 1.0),
593 size,
594 );
595 assert_eq!(state.taps, 0);
596 assert_eq!(state.pointers, 0);
597 }
598
599 #[test]
600 fn on_hit_admits_a_down_inside_its_shape_and_the_tap_fires_on_up() {
601 let view = canvas::<TapState, _>(|_scene, _size, _ctx| {})
602 .on_hit(|p: Point, size: Size| {
603 let center = Point::new(size.width / 2.0, size.height / 2.0);
604 (p - center).hypot() <= 10.0
605 })
606 .on_tap(|s: &mut TapState| s.taps += 1)
607 .on_pointer(|s: &mut TapState, _ev| s.pointers += 1);
608 let mut w = build(&view);
609 let mut state = TapState::default();
610 let size = Size::new(100.0, 100.0);
611 let center = (50.0, 50.0);
612
613 dispatch(
614 &mut w,
615 &mut state,
616 &pointer_ev(PointerPhase::Down, center.0, center.1),
617 size,
618 );
619 assert!(w.captured, "a hit-passing Down opens a capture");
620 dispatch(
621 &mut w,
622 &mut state,
623 &pointer_ev(PointerPhase::Up, center.0, center.1),
624 size,
625 );
626 assert_eq!(state.taps, 1, "the up-inside release fires the tap");
627 assert_eq!(state.pointers, 2, "on_pointer rides along on Down and Up");
628 }
629
630 #[test]
631 fn a_captured_press_released_outside_the_hit_shape_does_not_tap() {
632 let view = canvas::<TapState, _>(|_scene, _size, _ctx| {})
636 .on_hit(|p: Point, size: Size| {
637 let center = Point::new(size.width / 2.0, size.height / 2.0);
638 (p - center).hypot() <= 10.0
639 })
640 .on_tap(|s: &mut TapState| s.taps += 1);
641 let mut w = build(&view);
642 let mut state = TapState::default();
643 let size = Size::new(100.0, 100.0);
644
645 dispatch(
646 &mut w,
647 &mut state,
648 &pointer_ev(PointerPhase::Down, 50.0, 50.0),
649 size,
650 );
651 assert!(w.captured);
652 dispatch(
653 &mut w,
654 &mut state,
655 &pointer_ev(PointerPhase::Move, 90.0, 90.0),
656 size,
657 );
658 dispatch(
659 &mut w,
660 &mut state,
661 &pointer_ev(PointerPhase::Up, 90.0, 90.0),
662 size,
663 );
664 assert_eq!(state.taps, 0, "a release outside the hit-shape never taps");
665 assert!(!w.captured, "capture still clears on Up regardless");
666 }
667
668 #[test]
669 fn cancel_clears_captured_state_and_fires_nothing() {
670 let view = canvas::<TapState, _>(|_scene, _size, _ctx| {}).on_tap(|s: &mut TapState| {
671 s.taps += 1;
672 });
673 let mut w = build(&view);
674 let mut state = TapState::default();
675 let size = Size::new(100.0, 100.0);
676 dispatch(
677 &mut w,
678 &mut state,
679 &pointer_ev(PointerPhase::Down, 10.0, 10.0),
680 size,
681 );
682 assert!(w.captured);
683 dispatch(
684 &mut w,
685 &mut state,
686 &pointer_ev(PointerPhase::Cancel, 10.0, 10.0),
687 size,
688 );
689 assert!(!w.captured, "Cancel disarms the press");
690 dispatch(
691 &mut w,
692 &mut state,
693 &pointer_ev(PointerPhase::Up, 10.0, 10.0),
694 size,
695 );
696 assert_eq!(
697 state.taps, 0,
698 "an Up after Cancel is a no-op (not captured)"
699 );
700 }
701}