use std::hash::{Hash, Hasher};
use std::rc::Rc;
use frust_core::{
BoxConstraints, BuildCtx, ChangeFlags, EventCtx, EventResult, InputEvent, LayoutCtx, PaintCtx,
PaintScene, PointerEvent, PointerPhase, View, Widget,
};
use kurbo::{Affine, Point, Size};
use crate::authoring::presses;
#[derive(Clone, Copy, Debug, PartialEq)]
enum CanvasSize {
Expand,
Explicit(Size),
}
type CanvasPaint = Rc<dyn Fn(&mut dyn PaintScene, Size, &PaintCtx)>;
type HitTest = Rc<dyn Fn(Point, Size) -> bool>;
type TapCallback<State> = Rc<dyn Fn(&mut State)>;
type PointerCallback<State> = Rc<dyn Fn(&mut State, PointerEvent)>;
pub struct CanvasView<State: 'static> {
paint: CanvasPaint,
hit: Option<HitTest>,
on_tap: Option<TapCallback<State>>,
on_pointer: Option<PointerCallback<State>>,
size: CanvasSize,
repaint_key: Option<u64>,
}
pub fn canvas<State: 'static, F>(paint: F) -> CanvasView<State>
where
F: Fn(&mut dyn PaintScene, Size, &PaintCtx) + 'static,
{
CanvasView {
paint: Rc::new(paint),
hit: None,
on_tap: None,
on_pointer: None,
size: CanvasSize::Expand,
repaint_key: None,
}
}
impl<State: 'static> CanvasView<State> {
pub fn size(mut self, size: Size) -> Self {
self.size = CanvasSize::Explicit(size);
self
}
pub fn expand(mut self) -> Self {
self.size = CanvasSize::Expand;
self
}
pub fn on_hit<F: Fn(Point, Size) -> bool + 'static>(mut self, hit: F) -> Self {
self.hit = Some(Rc::new(hit));
self
}
pub fn on_tap<F: Fn(&mut State) + 'static>(mut self, on_tap: F) -> Self {
self.on_tap = Some(Rc::new(on_tap));
self
}
pub fn on_pointer<F: Fn(&mut State, PointerEvent) + 'static>(mut self, on_pointer: F) -> Self {
self.on_pointer = Some(Rc::new(on_pointer));
self
}
pub fn repaint_key(mut self, key: impl Hash) -> Self {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
self.repaint_key = Some(hasher.finish());
self
}
}
fn inside(pos: Point, size: Size) -> bool {
pos.x >= 0.0 && pos.y >= 0.0 && pos.x < size.width && pos.y < size.height
}
pub struct CanvasWidget {
paint: CanvasPaint,
hit: Option<HitTest>,
on_tap: Option<crate::authoring::ErasedCallback>,
on_pointer: Option<crate::authoring::ErasedArgCallback<PointerEvent>>,
size: CanvasSize,
repaint_key: Option<u64>,
captured: bool,
}
impl CanvasWidget {
fn hit_test(&self, point: Point, size: Size) -> bool {
match &self.hit {
Some(hit) => hit(point, size),
None => inside(point, size),
}
}
}
impl<State: 'static> View<State> for CanvasView<State> {
type Element = CanvasWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> CanvasWidget {
CanvasWidget {
paint: self.paint.clone(),
hit: self.hit.clone(),
on_tap: self.on_tap.as_ref().map(crate::authoring::erase_callback),
on_pointer: self
.on_pointer
.as_ref()
.map(crate::authoring::erase_callback_arg),
size: self.size,
repaint_key: self.repaint_key,
captured: false,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut CanvasWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
element.paint = self.paint.clone();
element.hit = self.hit.clone();
element.on_tap = self.on_tap.as_ref().map(crate::authoring::erase_callback);
element.on_pointer = self
.on_pointer
.as_ref()
.map(crate::authoring::erase_callback_arg);
if prev.size != self.size {
element.size = self.size;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
if prev.repaint_key != self.repaint_key {
flags |= ChangeFlags::PAINT;
}
element.repaint_key = self.repaint_key;
flags
}
}
impl Widget for CanvasWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
match self.size {
CanvasSize::Explicit(size) => bc.constrain(size),
CanvasSize::Expand => bc.max(),
}
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let origin = ctx.origin();
let size = ctx.size();
scene.push_transform(Affine::translate(origin.to_vec2()));
scene.push_clip(Point::ZERO, size);
(self.paint)(scene, size, ctx);
scene.pop_clip();
scene.pop_transform();
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
let InputEvent::Pointer(p) = event else {
return EventResult::Ignored;
};
match p.phase {
PointerPhase::Down => {
if !presses(p) || !self.hit_test(p.position, ctx.size()) {
return EventResult::Ignored;
}
self.captured = true;
ctx.capture_pointer();
if let Some(cb) = self.on_pointer.as_mut() {
cb(ctx, *p);
}
ctx.request_redraw();
EventResult::Handled
}
PointerPhase::Move => {
if !self.captured {
return EventResult::Ignored;
}
if let Some(cb) = self.on_pointer.as_mut() {
cb(ctx, *p);
ctx.request_redraw();
}
EventResult::Handled
}
PointerPhase::Up => {
if !self.captured {
return EventResult::Ignored;
}
self.captured = false;
if self.hit_test(p.position, ctx.size()) {
if let Some(cb) = self.on_pointer.as_mut() {
cb(ctx, *p);
}
if let Some(cb) = self.on_tap.as_mut() {
cb(ctx);
}
}
ctx.request_redraw();
EventResult::Handled
}
PointerPhase::Cancel => {
if !self.captured {
return EventResult::Ignored;
}
self.captured = false;
EventResult::Handled
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use frust_core::{BuildCtx, PointerButton};
use peniko::Color;
use std::any::Any;
use std::cell::RefCell;
fn build<State: 'static>(view: &CanvasView<State>) -> CanvasWidget {
let mut counter = 0u64;
View::<State>::build(view, &mut BuildCtx::new(&mut counter))
}
fn rebuild<State: 'static>(
prev: &CanvasView<State>,
next: &CanvasView<State>,
element: &mut CanvasWidget,
) -> ChangeFlags {
let mut counter = 0u64;
View::<State>::rebuild(next, prev, element, &mut BuildCtx::new(&mut counter))
}
#[derive(Default)]
struct Recorder {
ops: Vec<Op>,
}
#[derive(Debug, PartialEq)]
enum Op {
Transform(Affine),
PopTransform,
Clip(Point, Size),
PopClip,
Fill(Point, Size, Color),
}
impl PaintScene for Recorder {
fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
self.ops.push(Op::Fill(origin, size, color));
}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
fn push_transform(&mut self, transform: Affine) {
self.ops.push(Op::Transform(transform));
}
fn pop_transform(&mut self) {
self.ops.push(Op::PopTransform);
}
fn push_clip(&mut self, origin: Point, size: Size) {
self.ops.push(Op::Clip(origin, size));
}
fn pop_clip(&mut self) {
self.ops.push(Op::PopClip);
}
}
fn pointer_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
})
}
#[test]
fn expand_fills_the_incoming_constraints_max() {
let view = canvas::<(), _>(|_scene, _size, _ctx| {});
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
assert_eq!(size, Size::new(300.0, 200.0));
}
#[test]
fn explicit_size_is_clamped_into_constraints() {
let view = canvas::<(), _>(|_scene, _size, _ctx| {}).size(Size::new(1000.0, 1000.0));
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 200.0)));
assert_eq!(size, Size::new(300.0, 200.0));
}
#[test]
fn paint_runs_the_closure_with_the_laid_out_size_translated_and_clipped() {
let seen_size = Rc::new(RefCell::new(None));
let seen_size_cb = seen_size.clone();
let view = canvas::<(), _>(move |scene, size, _ctx| {
*seen_size_cb.borrow_mut() = Some(size);
scene.fill_rect(Point::new(1.0, 2.0), Size::new(3.0, 4.0), Color::BLACK);
});
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
let laid_out = w.layout(&mut lctx, &BoxConstraints::tight(Size::new(80.0, 60.0)));
assert_eq!(laid_out, Size::new(80.0, 60.0));
let mut rec = Recorder::default();
let mut pctx = PaintCtx::new(Point::new(10.0, 20.0), Size::new(80.0, 60.0));
w.paint(&mut pctx, &mut rec);
assert_eq!(
*seen_size.borrow(),
Some(Size::new(80.0, 60.0)),
"the closure sees the laid-out size"
);
assert_eq!(
rec.ops,
vec![
Op::Transform(Affine::translate((10.0, 20.0))),
Op::Clip(Point::ZERO, Size::new(80.0, 60.0)),
Op::Fill(Point::new(1.0, 2.0), Size::new(3.0, 4.0), Color::BLACK),
Op::PopClip,
Op::PopTransform,
],
"the widget pushes its absolute origin as a transform and clips to \
its bounds before the closure paints, then pops both"
);
}
#[test]
fn unset_repaint_key_requests_no_paint_flag_on_an_otherwise_unchanged_rebuild() {
let prev = canvas::<(), _>(|_scene, _size, _ctx| {});
let mut w = build(&prev);
let next = canvas::<(), _>(|_scene, _size, _ctx| {});
let flags = rebuild(&prev, &next, &mut w);
assert_eq!(
flags,
ChangeFlags::NONE,
"no repaint_key set: an otherwise-unchanged rebuild requests nothing"
);
}
#[test]
fn a_changed_repaint_key_requests_paint() {
let prev = canvas::<(), _>(|_scene, _size, _ctx| {}).repaint_key(1u32);
let mut w = build(&prev);
let next = canvas::<(), _>(|_scene, _size, _ctx| {}).repaint_key(2u32);
let flags = rebuild(&prev, &next, &mut w);
assert!(flags.needs_paint(), "a changed repaint_key requests PAINT");
}
#[test]
fn an_unchanged_repaint_key_requests_no_paint() {
let prev = canvas::<(), _>(|_scene, _size, _ctx| {}).repaint_key(7u32);
let mut w = build(&prev);
let next = canvas::<(), _>(|_scene, _size, _ctx| {}).repaint_key(7u32);
let flags = rebuild(&prev, &next, &mut w);
assert_eq!(
flags,
ChangeFlags::NONE,
"an unchanged repaint_key requests nothing"
);
}
#[derive(Default)]
struct TapState {
taps: u32,
pointers: u32,
}
fn dispatch(w: &mut CanvasWidget, state: &mut TapState, event: &InputEvent, size: Size) {
let state_any: &mut dyn Any = state;
let mut ctx = EventCtx::new(state_any, Point::ZERO, size);
w.event(&mut ctx, event);
}
#[test]
fn with_no_on_hit_a_tap_inside_the_rect_fires() {
let view = canvas::<TapState, _>(|_scene, _size, _ctx| {}).on_tap(|s: &mut TapState| {
s.taps += 1;
});
let mut w = build(&view);
let mut state = TapState::default();
let size = Size::new(100.0, 100.0);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Down, 10.0, 10.0),
size,
);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Up, 10.0, 10.0),
size,
);
assert_eq!(state.taps, 1, "the default rectangular hit-test admits it");
}
#[test]
fn on_hit_rejects_a_down_outside_its_shape_never_capturing() {
let view = canvas::<TapState, _>(|_scene, _size, _ctx| {})
.on_hit(|p: Point, size: Size| {
let center = Point::new(size.width / 2.0, size.height / 2.0);
(p - center).hypot() <= 10.0
})
.on_tap(|s: &mut TapState| s.taps += 1)
.on_pointer(|s: &mut TapState, _ev| s.pointers += 1);
let mut w = build(&view);
let mut state = TapState::default();
let size = Size::new(100.0, 100.0);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Down, 1.0, 1.0),
size,
);
assert!(!w.captured, "a hit-failing Down must not open a capture");
assert_eq!(state.pointers, 0);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Up, 1.0, 1.0),
size,
);
assert_eq!(state.taps, 0);
assert_eq!(state.pointers, 0);
}
#[test]
fn on_hit_admits_a_down_inside_its_shape_and_the_tap_fires_on_up() {
let view = canvas::<TapState, _>(|_scene, _size, _ctx| {})
.on_hit(|p: Point, size: Size| {
let center = Point::new(size.width / 2.0, size.height / 2.0);
(p - center).hypot() <= 10.0
})
.on_tap(|s: &mut TapState| s.taps += 1)
.on_pointer(|s: &mut TapState, _ev| s.pointers += 1);
let mut w = build(&view);
let mut state = TapState::default();
let size = Size::new(100.0, 100.0);
let center = (50.0, 50.0);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Down, center.0, center.1),
size,
);
assert!(w.captured, "a hit-passing Down opens a capture");
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Up, center.0, center.1),
size,
);
assert_eq!(state.taps, 1, "the up-inside release fires the tap");
assert_eq!(state.pointers, 2, "on_pointer rides along on Down and Up");
}
#[test]
fn a_captured_press_released_outside_the_hit_shape_does_not_tap() {
let view = canvas::<TapState, _>(|_scene, _size, _ctx| {})
.on_hit(|p: Point, size: Size| {
let center = Point::new(size.width / 2.0, size.height / 2.0);
(p - center).hypot() <= 10.0
})
.on_tap(|s: &mut TapState| s.taps += 1);
let mut w = build(&view);
let mut state = TapState::default();
let size = Size::new(100.0, 100.0);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Down, 50.0, 50.0),
size,
);
assert!(w.captured);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Move, 90.0, 90.0),
size,
);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Up, 90.0, 90.0),
size,
);
assert_eq!(state.taps, 0, "a release outside the hit-shape never taps");
assert!(!w.captured, "capture still clears on Up regardless");
}
#[test]
fn cancel_clears_captured_state_and_fires_nothing() {
let view = canvas::<TapState, _>(|_scene, _size, _ctx| {}).on_tap(|s: &mut TapState| {
s.taps += 1;
});
let mut w = build(&view);
let mut state = TapState::default();
let size = Size::new(100.0, 100.0);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Down, 10.0, 10.0),
size,
);
assert!(w.captured);
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Cancel, 10.0, 10.0),
size,
);
assert!(!w.captured, "Cancel disarms the press");
dispatch(
&mut w,
&mut state,
&pointer_ev(PointerPhase::Up, 10.0, 10.0),
size,
);
assert_eq!(
state.taps, 0,
"an Up after Cancel is a no-op (not captured)"
);
}
}