use crate::core::{Color, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::widget::capability::coercion::{expect_f64, expect_usize};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
#[derive(Debug, Clone, PartialEq)]
pub struct SignatureStroke {
points: Vec<Point>,
}
impl SignatureStroke {
pub fn new() -> Self {
Self { points: Vec::new() }
}
pub fn from_points(points: Vec<Point>) -> Self {
Self { points }
}
pub fn push(&mut self, point: Point) {
self.points.push(point);
}
pub fn points(&self) -> &[Point] {
&self.points
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
impl Default for SignatureStroke {
fn default() -> Self {
Self::new()
}
}
pub struct SignaturePad {
base: BaseWidget,
strokes: Vec<SignatureStroke>,
current: Option<SignatureStroke>,
stroke_color: Color,
stroke_width: u32,
min_point_distance: f32,
pub changed: GenericSignal,
pub stroke_completed: Signal1<SignatureStroke>,
}
impl SignaturePad {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::SignaturePad, geometry, "SignaturePad"),
strokes: Vec::new(),
current: None,
stroke_color: Color::rgb(20, 20, 20),
stroke_width: 2,
min_point_distance: 1.5,
changed: GenericSignal::new(),
stroke_completed: Signal1::new(),
}
}
pub fn strokes(&self) -> &[SignatureStroke] {
&self.strokes
}
pub fn stroke_count(&self) -> usize {
self.strokes.len()
}
pub fn is_empty(&self) -> bool {
self.strokes.is_empty()
}
pub fn undo(&mut self) -> bool {
if self.strokes.pop().is_some() {
self.changed.emit();
self.base.request_redraw();
true
} else {
false
}
}
pub fn clear(&mut self) {
if !self.strokes.is_empty() {
self.strokes.clear();
self.current = None;
self.changed.emit();
self.base.request_redraw();
}
}
pub fn stroke_color(&self) -> Color {
self.stroke_color
}
pub fn set_stroke_color(&mut self, color: Color) {
self.stroke_color = color;
self.base.request_redraw();
}
pub fn stroke_width(&self) -> u32 {
self.stroke_width
}
pub fn set_stroke_width(&mut self, width: u32) {
self.stroke_width = width.max(1);
self.base.request_redraw();
}
pub fn min_point_distance(&self) -> f32 {
self.min_point_distance
}
pub fn set_min_point_distance(&mut self, distance: f32) {
self.min_point_distance = distance.max(0.0);
}
pub fn export_polylines(&self) -> Vec<Point> {
let mut out = Vec::new();
for stroke in &self.strokes {
out.extend_from_slice(stroke.points());
out.push(Point::new(-1, -1));
}
out
}
fn in_progress(&self) -> bool {
self.current.is_some()
}
fn begin_stroke(&mut self, point: Point) {
let mut stroke = SignatureStroke::new();
stroke.push(point);
self.current = Some(stroke);
}
fn extend_stroke(&mut self, point: Point) {
let Some(current) = self.current.as_mut() else {
return;
};
if let Some(last) = current.points().last() {
let dx = (point.x - last.x) as f32;
let dy = (point.y - last.y) as f32;
if (dx * dx + dy * dy).sqrt() < self.min_point_distance {
return;
}
}
current.push(point);
self.base.request_redraw();
}
fn end_stroke(&mut self) {
let Some(stroke) = self.current.take() else {
return;
};
self.strokes.push(stroke.clone());
self.stroke_completed.emit(stroke);
self.changed.emit();
self.base.request_redraw();
}
}
impl Widget for SignaturePad {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(320, 160)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for SignaturePad {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"stroke_count" => Ok(CapabilityValue::UInt(self.stroke_count() as u64)),
"stroke_width" => Ok(CapabilityValue::UInt(self.stroke_width() as u64)),
"stroke_color" => Ok(CapabilityValue::Color(self.stroke_color())),
"min_point_distance" => Ok(CapabilityValue::Float(self.min_point_distance() as f64)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"stroke_count" => Err(CapabilityAccessError::ReadOnlyProperty),
"stroke_width" => {
self.set_stroke_width(expect_usize(value)? as u32);
Ok(())
}
"stroke_color" => match value {
CapabilityValue::Color(color) => {
self.set_stroke_color(color);
Ok(())
}
_ => Err(CapabilityAccessError::TypeMismatch),
},
"min_point_distance" => {
self.set_min_point_distance(expect_f64(value)? as f32);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"stroke_count",
"stroke_width",
"stroke_color",
"min_point_distance",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"undo" => {
let _ = self.undo();
Ok(())
}
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl EventHandler for SignaturePad {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
let rect = self.geometry();
match event {
Event::MousePress { pos, button: 1 } if rect.contains_point(*pos) => {
self.begin_stroke(*pos);
}
#[cfg(feature = "touch")]
Event::TouchBegin { pos, .. } if rect.contains_point(*pos) => {
self.begin_stroke(*pos);
}
Event::MouseMove { pos } | Event::PointerMove { pos, .. } if self.in_progress() => {
self.extend_stroke(*pos);
}
#[cfg(feature = "touch")]
Event::TouchMove { pos, .. } if self.in_progress() => {
self.extend_stroke(*pos);
}
Event::MouseRelease { .. } if self.in_progress() => {
self.end_stroke();
}
#[cfg(feature = "touch")]
Event::TouchEnd { .. } if self.in_progress() => {
self.end_stroke();
}
_ => { }
}
}
}
impl Draw for SignaturePad {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style().clone();
let theme = crate::style::resolved_theme_style("signature_pad");
let background = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::WHITE);
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.unwrap_or_else(|| background.blend(&Color::BLACK, 0.18));
let text_color = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::BLACK);
let hint = background.blend(&text_color, 0.4);
context.fill_rect(rect, background);
context.draw_rect(rect, border);
for stroke in &self.strokes {
draw_stroke(context, stroke, self.stroke_color, self.stroke_width);
}
if let Some(current) = &self.current {
draw_stroke(context, current, self.stroke_color, self.stroke_width);
}
if self.strokes.is_empty() && self.current.is_none() {
let mid_y = rect.y + rect.height as i32 / 2;
let inset = rect.width as i32 / 6;
context.draw_line_stroke(
Point::new(rect.x + inset, mid_y),
Point::new(rect.x + rect.width as i32 - inset, mid_y),
hint,
1,
);
}
}
}
fn draw_stroke(context: &mut RenderContext, stroke: &SignatureStroke, color: Color, width: u32) {
let points = stroke.points();
match points.len() {
0 => {}
1 => {
context.draw_line_stroke_aa(points[0], points[0], color, width.max(2));
}
_ => {
for pair in points.windows(2) {
context.draw_line_stroke_aa(pair[0], pair[1], color, width);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
fn pad() -> SignaturePad {
SignaturePad::new(Rect::new(0, 0, 320, 160))
}
#[test]
fn new_pad_is_empty() {
let p = pad();
assert!(p.is_empty());
assert_eq!(p.stroke_count(), 0);
assert!(p.strokes().is_empty());
}
#[test]
fn a_press_move_release_records_one_stroke() {
let mut p = pad();
p.handle_event(&Event::mouse_press(10, 20, 1));
p.handle_event(&Event::mouse_move(30, 40));
p.handle_event(&Event::mouse_move(60, 50));
p.handle_event(&Event::mouse_release(60, 50, 1));
assert_eq!(p.stroke_count(), 1);
assert!(!p.is_empty());
assert_eq!(p.strokes()[0].len(), 3);
assert_eq!(p.strokes()[0].points()[0], Point::new(10, 20));
}
#[test]
fn smoothing_drops_near_duplicate_points() {
let mut p = pad();
p.handle_event(&Event::mouse_press(10, 20, 1));
p.handle_event(&Event::mouse_move(10, 20));
p.handle_event(&Event::mouse_move(10, 21));
p.handle_event(&Event::mouse_release(10, 21, 1));
let stroke = &p.strokes()[0];
assert_eq!(stroke.len(), 1);
assert_eq!(stroke.points()[0], Point::new(10, 20));
}
#[test]
fn undo_and_clear_work() {
let mut p = pad();
p.handle_event(&Event::mouse_press(10, 20, 1));
p.handle_event(&Event::mouse_release(10, 20, 1));
assert_eq!(p.stroke_count(), 1);
assert!(p.undo());
assert_eq!(p.stroke_count(), 0);
assert!(!p.undo());
p.handle_event(&Event::mouse_press(10, 20, 1));
p.handle_event(&Event::mouse_release(10, 20, 1));
assert_eq!(p.stroke_count(), 1);
p.clear();
assert!(p.is_empty());
}
#[test]
fn changed_and_stroke_completed_signals_fire() {
let mut p = pad();
let changed = Arc::new(AtomicUsize::new(0));
let c = changed.clone();
p.changed.connect(move || {
c.fetch_add(1, Ordering::SeqCst);
});
let completed = Arc::new(AtomicUsize::new(0));
let cc = completed.clone();
p.stroke_completed.connect(move |_| {
cc.fetch_add(1, Ordering::SeqCst);
});
p.handle_event(&Event::mouse_press(10, 20, 1));
p.handle_event(&Event::mouse_release(10, 20, 1));
assert_eq!(changed.load(Ordering::SeqCst), 1);
assert_eq!(completed.load(Ordering::SeqCst), 1);
}
#[test]
fn export_polylines_emits_sentinels() {
let mut p = pad();
p.handle_event(&Event::mouse_press(10, 20, 1));
p.handle_event(&Event::mouse_move(30, 40));
p.handle_event(&Event::mouse_release(30, 40, 1));
p.handle_event(&Event::mouse_press(50, 60, 1));
p.handle_event(&Event::mouse_release(50, 60, 1));
let flat = p.export_polylines();
assert_eq!(flat.len(), 2 + 1 + 1 + 1);
assert_eq!(flat[2], Point::new(-1, -1));
assert_eq!(flat[4], Point::new(-1, -1));
}
#[test]
#[cfg(feature = "touch")]
fn touch_events_record_strokes_too() {
let mut p = pad();
p.handle_event(&crate::event::Event::touch_begin(5, 5, 1));
p.handle_event(&crate::event::Event::touch_move(40, 40, 1));
p.handle_event(&crate::event::Event::touch_end(40, 40, 1));
assert_eq!(p.stroke_count(), 1);
}
#[test]
fn stroke_width_is_floored_at_one() {
let mut p = pad();
p.set_stroke_width(0);
assert_eq!(p.stroke_width(), 1);
}
#[test]
fn disabled_pad_ignores_input() {
let mut p = pad();
p.set_enabled(false);
p.handle_event(&Event::mouse_press(10, 20, 1));
p.handle_event(&Event::mouse_release(10, 20, 1));
assert!(p.is_empty());
}
}