use std::rc::Rc; use gpui::{ App, Bounds, Corners, DragMoveEvent, ElementId, EmptyView, EntityId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Path, PathBuilder, Pixels, Point, RenderOnce, StatefulInteractiveElement, Styled, Window, canvas, div, fill, point, prelude::*, size, }; use crate::{ buttons::{Button, ButtonVariant}, layout::seeded::use_seeded, theme::{ActiveTheme, ControlSize, Radius, TextSize}, }; /// A pen stroke: points from the pad's top left corner. pub type Stroke = Vec>; type OnStrokes = Rc; type Lift = Rc, &mut Window, &mut App)>; struct Pen { owner: EntityId, } /// Where the pad sits, and the stroke under the pen. #[derive(Default)] struct Drawing { bounds: Bounds, stroke: Stroke, } impl Drawing { /// Adds `at` to the stroke under the pen, unless the pen has not moved. fn reach(&mut self, at: Point) { let at = local(self.bounds, at); if self.stroke.last() != Some(&at) { self.stroke.push(at); } } } /// `at` from the pad's corner, kept inside the pad. fn local(bounds: Bounds, at: Point) -> Point { point( (at.x - bounds.left()) .max(Pixels::ZERO) .min(bounds.size.width), (at.y - bounds.top()) .max(Pixels::ZERO) .min(bounds.size.height), ) } /// A line through `points`, bent through their midpoints so it reads smooth. fn ink(points: &[Point], origin: Point, width: Pixels) -> Option> { let [first, .., last] = points else { return None; }; let at = |p: Point| point(origin.x + p.x, origin.y + p.y); let mut path = PathBuilder::stroke(width); path.move_to(at(*first)); for pair in points.windows(2) { let middle = point((pair[0].x + pair[1].x) / 2.0, (pair[0].y + pair[1].y) / 2.0); path.curve_to(at(middle), at(pair[0])); } path.line_to(at(*last)); Some(path.build().expect("a stroke is a simple open path")) } fn paint( strokes: &[Stroke], bounds: Bounds, width: Pixels, color: Hsla, window: &mut Window, ) { let dot = |at: Point| { let center = point(bounds.left() + at.x, bounds.top() + at.y); fill(Bounds::centered_at(center, size(width, width)), color) .corner_radii(Corners::all(width / 2.0)) }; for stroke in strokes { if let Some(path) = ink(stroke, bounds.origin, width) { window.paint_path(path, color); } for end in [stroke.first(), stroke.last()].into_iter().flatten() { window.paint_quad(dot(*end)); } } } /// A pad to sign with the pointer. A stroke lands when the pen lifts; Clear wipes them all. #[derive(IntoElement)] pub struct SignaturePad { id: ElementId, strokes: Vec, on_change: Option, } impl SignaturePad { pub fn new(id: impl Into, strokes: impl IntoIterator) -> Self { Self { id: id.into(), strokes: strokes.into_iter().collect(), on_change: None, } } pub fn on_change( mut self, handler: impl Fn(&[Stroke], &mut Window, &mut App) + 'static, ) -> Self { self.on_change = Some(Rc::new(handler)); self } } impl RenderOnce for SignaturePad { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let strokes = use_seeded((self.id.clone(), "strokes"), self.strokes, window, cx); let drawing = window.use_keyed_state((self.id.clone(), "drawing"), cx, |_, _| Drawing::default()); let owner = drawing.entity_id(); let set: OnStrokes = { let (id, strokes, on_change) = (self.id.clone(), strokes.clone(), self.on_change); Rc::new(move |next, window, cx| { log::info!("signature pad {id:?}: {} strokes", next.len()); strokes.update(cx, |strokes, cx| { strokes.value = next.to_vec(); cx.notify(); }); if let Some(on_change) = &on_change { on_change(next, window, cx); } }) }; let lift: Lift = { let (strokes, drawing, set) = (strokes.clone(), drawing.clone(), set.clone()); Rc::new(move |at, window, cx| { let stroke = drawing.update(cx, |drawing, _| { if !drawing.stroke.is_empty() { drawing.reach(at); } std::mem::take(&mut drawing.stroke) }); if stroke.is_empty() { return; } let mut next = strokes.read(cx).value.clone(); next.push(stroke); set(&next, window, cx); }) }; let theme = cx.theme(); let colors = &theme.colors; let (height, width) = theme.signature(); let width = width.to_pixels(window.rem_size()); let empty = strokes.read(cx).value.is_empty(); let mut shown = strokes.read(cx).value.clone(); shown.push(drawing.read(cx).stroke.clone()); let (measure, press, start, pen) = (drawing.clone(), drawing.clone(), drawing.clone(), drawing); let (up, out) = (lift.clone(), lift); let color = colors.fg; let pad = div() .id(self.id.clone()) .relative() .w_full() .h(height) .rounded(theme.radius(Radius::Lg)) .border_1() .border_color(colors.border_strong) .bg(colors.surface) .cursor_crosshair() .child( div() .absolute() .left_6() .right_6() .bottom_8() .flex() .items_end() .gap_2() .pb_1() .border_b_1() .border_dashed() .border_color(colors.border_strong) .text_size(theme.text_size(TextSize::Sm)) .text_color(colors.fg_subtle) .child("×") .when(empty, |line| line.child("Sign here")), ) .child( canvas( move |bounds, _, cx| { if measure.read(cx).bounds != bounds { measure.update(cx, |drawing, _| drawing.bounds = bounds); } }, move |bounds, _, window, _| paint(&shown, bounds, width, color, window), ) .absolute() .top_0() .left_0() .size_full(), ) .on_mouse_down(MouseButton::Left, move |event, _, cx| { press.update(cx, |drawing, cx| { drawing.stroke = vec![local(drawing.bounds, event.position)]; cx.notify(); }) }) .on_drag(Pen { owner }, move |_, _, window, cx| { let at = window.mouse_position(); start.update(cx, |drawing, cx| { drawing.reach(at); cx.notify(); }); cx.new(|_| EmptyView) }) .on_drag_move(move |event: &DragMoveEvent, _, cx| { if event.drag(cx).owner != owner { return; } pen.update(cx, |drawing, cx| { drawing.reach(event.event.position); cx.notify(); }) }) .on_mouse_up(MouseButton::Left, move |event, window, cx| { up(event.position, window, cx) }) .on_mouse_up_out(MouseButton::Left, move |event, window, cx| { out(event.position, window, cx) }); div() .flex() .flex_col() .items_end() .gap_1() .w_full() .child(pad) .child( Button::new((self.id, "clear"), "Clear") .size(ControlSize::Sm) .variant(ButtonVariant::Ghost) .disabled(empty) .on_click(move |_, window, cx| set(&[], window, cx)), ) } } #[cfg(test)] mod tests { use gpui::{Bounds, point, px, size}; use super::{ink, local}; #[test] fn points_stay_inside_the_pad() { let bounds = Bounds::new(point(px(10.0), px(20.0)), size(px(100.0), px(50.0))); assert_eq!( local(bounds, point(px(30.0), px(40.0))), point(px(20.0), px(20.0)) ); assert_eq!( local(bounds, point(px(-5.0), px(90.0))), point(px(0.0), px(50.0)) ); } #[test] fn a_stroke_needs_two_points_for_a_line() { let origin = point(px(0.0), px(0.0)); assert!(ink(&[point(px(1.0), px(1.0))], origin, px(2.0)).is_none()); let line = [ point(px(1.0), px(1.0)), point(px(9.0), px(4.0)), point(px(12.0), px(9.0)), ]; assert!(ink(&line, origin, px(2.0)).is_some()); } }