use crate::compat::Instant;
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_u32_saturating, 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};
use core::cell::{Ref, RefCell};
fn next_ink_cache_key() -> u64 {
use core::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug, Clone)]
pub struct SignatureStroke {
points: Vec<Point>,
pressures: Vec<f32>,
tilts: Vec<(f32, f32)>,
smoothed: RefCell<Option<Vec<Point>>>,
}
const NEUTRAL_PRESSURE: f32 = 1.0;
impl PartialEq for SignatureStroke {
fn eq(&self, other: &Self) -> bool {
self.points == other.points
&& self.pressures == other.pressures
&& self.tilts == other.tilts
}
}
impl SignatureStroke {
pub fn new() -> Self {
Self {
points: Vec::new(),
pressures: Vec::new(),
tilts: Vec::new(),
smoothed: RefCell::new(None),
}
}
pub fn from_points(points: Vec<Point>) -> Self {
let pressures = vec![NEUTRAL_PRESSURE; points.len()];
let tilts = vec![(0.0, 0.0); points.len()];
Self { points, pressures, tilts, smoothed: RefCell::new(None) }
}
pub fn push(&mut self, point: Point) {
self.push_with_pressure(point, NEUTRAL_PRESSURE);
}
pub fn push_with_pressure(&mut self, point: Point, pressure: f32) {
let clamped =
if pressure.is_finite() { pressure.clamp(0.0, 1.0) } else { NEUTRAL_PRESSURE };
self.points.push(point);
self.pressures.push(clamped);
self.tilts.push((0.0, 0.0));
*self.smoothed.borrow_mut() = None;
}
pub fn push_with_tilt(&mut self, point: Point, pressure: f32, tilt: (f32, f32)) {
self.push_with_pressure(point, pressure);
if let Some(last) = self.tilts.last_mut() {
*last = (
if tilt.0.is_finite() { tilt.0 } else { 0.0 },
if tilt.1.is_finite() { tilt.1 } else { 0.0 },
);
}
}
pub fn points(&self) -> &[Point] {
&self.points
}
pub fn pressures(&self) -> &[f32] {
&self.pressures
}
pub fn tilts(&self) -> &[(f32, f32)] {
&self.tilts
}
pub fn last_pressure(&self) -> f32 {
self.pressures.last().copied().unwrap_or(NEUTRAL_PRESSURE)
}
pub fn len(&self) -> usize {
self.points.len()
}
pub(crate) fn smoothed_points(&self) -> Ref<'_, Vec<Point>> {
if self.smoothed.borrow().is_none() {
*self.smoothed.borrow_mut() = Some(chaikin(&self.points));
}
Ref::map(self.smoothed.borrow(), |cached| {
cached.as_ref().expect("the smoothed polyline was just computed")
})
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn summary(&self) -> String {
if self.pressures.is_empty() {
return format!(
"points=0;pressure_avg={NEUTRAL_PRESSURE:.3};pressure_min={NEUTRAL_PRESSURE:.3};pressure_max={NEUTRAL_PRESSURE:.3}"
);
}
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
let mut total = 0.0f32;
for pressure in &self.pressures {
min = min.min(*pressure);
max = max.max(*pressure);
total += *pressure;
}
let average = total / self.pressures.len() as f32;
format!(
"points={};pressure_avg={average:.3};pressure_min={min:.3};pressure_max={max:.3}",
self.points.len()
)
}
}
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,
min_point_interval_ms: u64,
last_point_at: Option<Instant>,
last_pressure: f32,
last_tilt: (f32, f32),
pressure_affects_width: bool,
tilt_affects_width: bool,
smooth_strokes: bool,
redo_stack: Vec<SignatureStroke>,
ink_cache_key: u64,
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.0,
min_point_interval_ms: 10,
last_point_at: None,
last_pressure: NEUTRAL_PRESSURE,
last_tilt: (0.0, 0.0),
pressure_affects_width: true,
tilt_affects_width: false,
smooth_strokes: false,
redo_stack: Vec::new(),
ink_cache_key: next_ink_cache_key(),
changed: GenericSignal::new(),
stroke_completed: Signal1::new(),
}
}
fn invalidate_ink_cache(&mut self) {
self.ink_cache_key = next_ink_cache_key();
}
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 let Some(stroke) = self.strokes.pop() {
self.redo_stack.push(stroke);
self.invalidate_ink_cache();
self.changed.emit();
self.base.request_redraw();
true
} else {
false
}
}
pub fn redo(&mut self) -> bool {
let Some(stroke) = self.redo_stack.pop() else {
return false;
};
self.strokes.push(stroke);
self.invalidate_ink_cache();
self.changed.emit();
self.base.request_redraw();
true
}
pub fn set_smooth_strokes(&mut self, smooth: bool) {
if self.smooth_strokes == smooth {
return;
}
self.smooth_strokes = smooth;
self.invalidate_ink_cache();
self.base.request_redraw();
}
pub fn smooth_strokes(&self) -> bool {
self.smooth_strokes
}
pub fn set_tilt_affects_width(&mut self, affects: bool) {
if self.tilt_affects_width == affects {
return;
}
self.tilt_affects_width = affects;
self.invalidate_ink_cache();
self.base.request_redraw();
}
pub fn tilt_affects_width(&self) -> bool {
self.tilt_affects_width
}
pub fn export_svg(&self) -> String {
let rect = self.geometry();
let mut svg = format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"{} {} {} {}\">",
rect.width, rect.height, rect.x, rect.y, rect.width, rect.height
);
let color =
format!("rgb({},{},{})", self.stroke_color.r, self.stroke_color.g, self.stroke_color.b);
for stroke in &self.strokes {
let Some(first) = stroke.points().first() else {
continue;
};
let mut path = format!("M {} {}", first.x, first.y);
for point in &stroke.points()[1..] {
path.push_str(&format!(" L {} {}", point.x, point.y));
}
svg.push_str(&format!(
"<path d=\"{path}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{}\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n",
self.stroke_width.max(1)
));
}
svg.push_str("</svg>");
svg
}
pub fn export_rgba(&self, size: (u32, u32)) -> Vec<u8> {
let (width, height) = size;
let mut pixels = vec![0u8; width as usize * height as usize * 4];
if width == 0 || height == 0 {
return pixels;
}
let rect = self.geometry();
let scale_x = width as f32 / rect.width.max(1) as f32;
let scale_y = height as f32 / rect.height.max(1) as f32;
for stroke in &self.strokes {
let points: Vec<(i32, i32)> = stroke
.points()
.iter()
.map(|point| {
(
((point.x - rect.x) as f32 * scale_x).round() as i32,
((point.y - rect.y) as f32 * scale_y).round() as i32,
)
})
.collect();
for pair in points.windows(2) {
raster_line(&mut pixels, width, height, pair[0], pair[1], self.stroke_color);
}
if points.len() == 1 {
raster_line(&mut pixels, width, height, points[0], points[0], self.stroke_color);
}
}
pixels
}
pub fn clear(&mut self) {
if !self.strokes.is_empty() {
self.strokes.clear();
self.current = None;
self.last_point_at = None;
self.invalidate_ink_cache();
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) {
if self.stroke_color == color {
return;
}
self.stroke_color = color;
self.invalidate_ink_cache();
self.base.request_redraw();
}
pub fn stroke_width(&self) -> u32 {
self.stroke_width
}
pub fn set_stroke_width(&mut self, width: u32) {
let width = width.max(1);
if self.stroke_width == width {
return;
}
self.stroke_width = width;
self.invalidate_ink_cache();
self.base.request_redraw();
}
pub fn last_pressure(&self) -> f32 {
self.last_pressure
}
pub fn last_tilt(&self) -> (f32, f32) {
self.last_tilt
}
pub fn pressure_affects_width(&self) -> bool {
self.pressure_affects_width
}
pub fn set_pressure_affects_width(&mut self, enabled: bool) {
if self.pressure_affects_width != enabled {
self.pressure_affects_width = enabled;
self.invalidate_ink_cache();
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 min_point_interval_ms(&self) -> u64 {
self.min_point_interval_ms
}
pub fn set_min_point_interval_ms(&mut self, interval_ms: u64) {
self.min_point_interval_ms = interval_ms;
}
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_with(&mut self, point: Point, pressure: f32, tilt: (f32, f32)) {
self.record_pen_data(pressure, tilt);
let mut stroke = SignatureStroke::new();
stroke.push_with_tilt(point, self.last_pressure, self.last_tilt);
self.current = Some(stroke);
self.last_point_at = Some(Instant::now());
}
fn begin_stroke(&mut self, point: Point) {
self.begin_stroke_with(point, NEUTRAL_PRESSURE, (0.0, 0.0));
}
fn record_pen_data(&mut self, pressure: f32, tilt: (f32, f32)) {
self.last_pressure =
if pressure.is_finite() { pressure.clamp(0.0, 1.0) } else { NEUTRAL_PRESSURE };
self.last_tilt = (
if tilt.0.is_finite() { tilt.0 } else { 0.0 },
if tilt.1.is_finite() { tilt.1 } else { 0.0 },
);
}
fn extend_stroke(&mut self, point: Point) {
self.extend_stroke_with(point, NEUTRAL_PRESSURE, (0.0, 0.0));
}
fn extend_stroke_with(&mut self, point: Point, pressure: f32, tilt: (f32, f32)) {
self.record_pen_data(pressure, tilt);
let now = Instant::now();
let elapsed_ms = self
.last_point_at
.map(|last| now.saturating_duration_since(last).as_millis() as u64)
.unwrap_or(0);
let min_distance = self.min_point_distance;
let min_interval_ms = self.min_point_interval_ms;
let new_pressure = self.last_pressure;
let new_tilt = self.last_tilt;
let Some(current) = self.current.as_mut() else {
return;
};
let Some(last) = current.points().last().copied() else {
current.push_with_tilt(point, new_pressure, new_tilt);
self.last_point_at = Some(now);
self.base.request_redraw();
return;
};
let dx = (point.x - last.x) as f32;
let dy = (point.y - last.y) as f32;
let distance = (dx * dx + dy * dy).sqrt();
let far_enough = min_distance <= 0.0 || distance >= min_distance;
let long_enough = min_interval_ms > 0 && elapsed_ms >= min_interval_ms;
if !far_enough && !long_enough {
return;
}
if min_distance > 0.0 && distance > min_distance {
let from_pressure = current.last_pressure();
for (step, step_pressure) in interpolation_steps_with_pressure(
last,
point,
distance,
min_distance,
from_pressure,
new_pressure,
) {
current.push_with_tilt(step, step_pressure, new_tilt);
}
}
current.push_with_tilt(point, new_pressure, new_tilt);
self.last_point_at = Some(now);
self.base.request_redraw();
}
fn end_stroke(&mut self) {
let Some(stroke) = self.current.take() else {
return;
};
self.strokes.push(stroke.clone());
self.redo_stack.clear();
self.invalidate_ink_cache();
self.stroke_completed.emit(stroke);
self.changed.emit();
self.base.request_redraw();
}
fn cancel_stroke(&mut self) {
if self.current.take().is_some() {
self.last_point_at = None;
self.base.request_redraw();
}
}
}
impl Widget for SignaturePad {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
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!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"changed" => Some(EventSignalRef::unit("changed", &self.changed)),
"stroke_completed" => {
Some(EventSignalRef::mapped("stroke_completed", &self.stroke_completed, |v| {
CapabilityValue::String(v.summary())
}))
}
_ => None,
}
}
}
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)),
"min_point_interval_ms" => Ok(CapabilityValue::UInt(self.min_point_interval_ms())),
"pressure_affects_width" => Ok(CapabilityValue::Bool(self.pressure_affects_width())),
"last_pressure" => Ok(CapabilityValue::Float(self.last_pressure() as f64)),
"last_tilt_x" => Ok(CapabilityValue::Float(self.last_tilt().0 as f64)),
"last_tilt_y" => Ok(CapabilityValue::Float(self.last_tilt().1 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_u32_saturating(value)?);
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(())
}
"min_point_interval_ms" => {
self.set_min_point_interval_ms(expect_usize(value)? as u64);
Ok(())
}
"pressure_affects_width" => match value {
CapabilityValue::Bool(enabled) => {
self.set_pressure_affects_width(enabled);
Ok(())
}
_ => Err(CapabilityAccessError::TypeMismatch),
},
"last_pressure" | "last_tilt_x" | "last_tilt_y" => {
Err(CapabilityAccessError::ReadOnlyProperty)
}
_ => 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",
"min_point_interval_ms",
"pressure_affects_width",
"last_pressure",
"last_tilt_x",
"last_tilt_y",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"undo" => {
let _ = self.undo();
Ok(())
}
"redo" => {
let _ = self.redo();
Ok(())
}
"clear" => {
self.clear();
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);
}
Event::PointerPress { pos, button: 1, pressure, tilt_x, tilt_y }
if rect.contains_point(*pos) =>
{
self.begin_stroke_with(*pos, *pressure, (*tilt_x, *tilt_y));
}
#[cfg(feature = "touch")]
Event::TouchBegin { pos, .. } if rect.contains_point(*pos) => {
self.begin_stroke(*pos);
}
Event::MouseMove { pos } if self.in_progress() => {
self.extend_stroke(*pos);
}
Event::PointerMove { pos, pressure, tilt_x, tilt_y } if self.in_progress() => {
self.extend_stroke_with(*pos, *pressure, (*tilt_x, *tilt_y));
}
#[cfg(feature = "touch")]
Event::TouchMove { pos, .. } if self.in_progress() => {
self.extend_stroke(*pos);
}
Event::MouseRelease { .. } if self.in_progress() => {
self.end_stroke();
}
Event::PointerRelease { pressure, .. } if self.in_progress() => {
let tilt = self.last_tilt;
self.record_pen_data(*pressure, tilt);
self.end_stroke();
}
#[cfg(feature = "touch")]
Event::TouchEnd { .. } if self.in_progress() => {
self.end_stroke();
}
#[cfg(feature = "touch")]
event if crate::event::translator::is_touch_cancel(event) => {
self.cancel_stroke();
}
_ => { }
}
}
}
impl Draw for SignaturePad {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style();
let theme = crate::style::resolved_theme_style_shared("signature_pad");
let window_fill = {
let manager = crate::style::theme_manager();
manager.current_theme().map(|active| active.colors.background).unwrap_or(Color::WHITE)
};
let text_color = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::BLACK);
let background = match style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
{
Some(resolved) if resolved != window_fill => resolved,
_ => window_fill.blend(&text_color, 0.10),
};
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.filter(|resolved| *resolved != background)
.unwrap_or_else(|| background.blend(&text_color, 0.35));
let hint = background.blend(&text_color, 0.4);
context.face_with_gradient(
rect,
background,
self.style().background_gradient.as_ref(),
self.style().surface.unwrap_or_default(),
self.style().border_radius.unwrap_or(0),
Color::BLACK,
);
crate::widget::draw::stroke_surface_border(
context,
rect,
self.style().border_radius.unwrap_or(0),
border,
1,
);
if !self.strokes.is_empty() {
let key = self.ink_cache_key;
context.push_layer_group(rect, None, 1.0, Some(key));
for stroke in &self.strokes {
draw_stroke(
context,
stroke,
self.stroke_color,
self.stroke_width,
self.pressure_affects_width,
self.tilt_affects_width,
self.smooth_strokes,
);
}
context.pop_layer();
}
if let Some(current) = &self.current {
draw_stroke(
context,
current,
self.stroke_color,
self.stroke_width,
self.pressure_affects_width,
self.tilt_affects_width,
self.smooth_strokes,
);
}
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 interpolation_steps_with_pressure(
from: Point,
to: Point,
distance: f32,
min_distance: f32,
from_pressure: f32,
to_pressure: f32,
) -> Vec<(Point, f32)> {
if min_distance <= 0.0 || !distance.is_finite() || !min_distance.is_finite() {
return Vec::new();
}
let steps = (distance / min_distance).ceil() as usize;
if steps <= 1 {
return Vec::new();
}
let mut out = Vec::with_capacity(steps - 1);
for i in 1..steps {
let t = i as f32 / steps as f32;
let point = Point::new(
from.x + ((to.x - from.x) as f32 * t).round() as i32,
from.y + ((to.y - from.y) as f32 * t).round() as i32,
);
let pressure = from_pressure + (to_pressure - from_pressure) * t;
out.push((point, pressure));
}
out
}
fn draw_stroke(
context: &mut RenderContext,
stroke: &SignatureStroke,
color: Color,
width: u32,
pressure_affects_width: bool,
tilt_affects_width: bool,
smooth: bool,
) {
let smoothed;
let points = if smooth {
smoothed = stroke.smoothed_points();
smoothed.as_slice()
} else {
stroke.points()
};
let pressures = stroke.pressures();
let tilts = stroke.tilts();
match points.len() {
0 => {}
1 => {
let pressure = pressures.first().copied().unwrap_or(NEUTRAL_PRESSURE);
let tilt = tilts.first().copied().unwrap_or((0.0, 0.0));
let dot_width = segment_width(width, pressure, pressure, pressure_affects_width)
* tilt_factor(tilt, tilt_affects_width);
context.draw_line_stroke_aa(points[0], points[0], color, dot_width.max(2));
}
_ => {
for (index, pair) in points.windows(2).enumerate() {
let from_pressure = pressures.get(index).copied().unwrap_or(NEUTRAL_PRESSURE);
let to_pressure = pressures.get(index + 1).copied().unwrap_or(NEUTRAL_PRESSURE);
let from_tilt = tilts.get(index).copied().unwrap_or((0.0, 0.0));
let to_tilt = tilts.get(index + 1).copied().unwrap_or((0.0, 0.0));
let segment =
segment_width(width, from_pressure, to_pressure, pressure_affects_width);
let tilt = ((from_tilt.0 + to_tilt.0) * 0.5, (from_tilt.1 + to_tilt.1) * 0.5);
let segment = segment * tilt_factor(tilt, tilt_affects_width);
context.draw_line_stroke_aa(pair[0], pair[1], color, segment.max(1));
}
}
}
}
fn tilt_factor(tilt: (f32, f32), enabled: bool) -> u32 {
if !enabled {
return 1;
}
let magnitude = (tilt.0.abs() + tilt.1.abs()).min(180.0);
let factor = 1.0 + magnitude / 180.0;
factor.round().max(1.0) as u32
}
fn chaikin(points: &[Point]) -> Vec<Point> {
if points.len() < 3 {
return points.to_vec();
}
let mut out = Vec::with_capacity(points.len() * 2);
out.push(points[0]);
for pair in points.windows(2) {
let (from, to) = (pair[0], pair[1]);
out.push(Point::new(
from.x + ((to.x - from.x) as f32 * 0.25).round() as i32,
from.y + ((to.y - from.y) as f32 * 0.25).round() as i32,
));
out.push(Point::new(
from.x + ((to.x - from.x) as f32 * 0.75).round() as i32,
from.y + ((to.y - from.y) as f32 * 0.75).round() as i32,
));
}
out.push(points[points.len() - 1]);
out
}
fn raster_line(
pixels: &mut [u8],
width: u32,
height: u32,
from: (i32, i32),
to: (i32, i32),
color: Color,
) {
let (mut x0, mut y0) = from;
let (x1, y1) = to;
let dx = (x1 - x0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let dy = -(y1 - y0).abs();
let sy = if y0 < y1 { 1 } else { -1 };
let mut error = dx + dy;
loop {
if x0 >= 0 && y0 >= 0 && (x0 as u32) < width && (y0 as u32) < height {
let index = ((y0 as u32 * width + x0 as u32) * 4) as usize;
if index + 3 < pixels.len() {
pixels[index] = color.r;
pixels[index + 1] = color.g;
pixels[index + 2] = color.b;
pixels[index + 3] = 255;
}
}
if x0 == x1 && y0 == y1 {
break;
}
let double = 2 * error;
if double >= dy {
error += dy;
x0 += sx;
}
if double <= dx {
error += dx;
y0 += sy;
}
}
}
fn segment_width(
width: u32,
from_pressure: f32,
to_pressure: f32,
pressure_affects_width: bool,
) -> u32 {
if !pressure_affects_width {
return width.max(1);
}
let mean = ((from_pressure + to_pressure) * 0.5).clamp(0.0, 1.0);
let scaled = (width as f32 * mean).round();
(scaled as u32).max(1)
}
#[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());
let stroke = &p.strokes()[0];
assert_eq!(stroke.points()[0], Point::new(10, 20));
assert_eq!(stroke.points().last().copied(), Some(Point::new(60, 50)));
assert!(stroke.len() >= 3, "the real points survive interpolation");
}
#[test]
fn smoothing_drops_near_duplicate_points() {
let mut p = pad();
p.set_min_point_distance(1.5);
p.set_min_point_interval_ms(0);
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 interpolation_spaces_points_at_the_pads_own_resolution() {
let steps = interpolation_steps_with_pressure(
Point::new(0, 0),
Point::new(10, 0),
10.0,
1.5,
NEUTRAL_PRESSURE,
NEUTRAL_PRESSURE,
);
assert_eq!(steps.len(), 6);
assert_eq!(steps[0].0, Point::new(1, 0));
assert_eq!(steps[5].0, Point::new(9, 0));
assert!(interpolation_steps_with_pressure(
Point::new(0, 0),
Point::new(1, 0),
1.0,
1.5,
NEUTRAL_PRESSURE,
NEUTRAL_PRESSURE,
)
.is_empty());
assert!(interpolation_steps_with_pressure(
Point::new(0, 0),
Point::new(100, 0),
100.0,
0.0,
NEUTRAL_PRESSURE,
NEUTRAL_PRESSURE,
)
.is_empty());
}
#[test]
fn a_stationary_point_is_recorded_once_the_interval_passes() {
let mut p = pad();
p.handle_event(&Event::mouse_press(10, 20, 1));
std::thread::sleep(std::time::Duration::from_millis(25));
p.handle_event(&Event::mouse_move(10, 20));
let current_points = p.current.as_ref().expect("a stroke is in progress").len();
assert_eq!(
current_points, 2,
"a point held in place past the interval must be recorded, so a pause is visible \
in the stroke rather than being silently swallowed"
);
}
#[test]
fn the_interval_is_configurable_and_reported() {
let mut p = pad();
assert_eq!(p.min_point_interval_ms(), 10, "the shipped default");
p.set_min_point_interval_ms(0);
assert_eq!(p.min_point_interval_ms(), 0);
assert_eq!(p.get("min_point_interval_ms").expect("readable"), CapabilityValue::UInt(0));
p.set("min_point_interval_ms", CapabilityValue::UInt(40)).expect("writable");
assert_eq!(p.min_point_interval_ms(), 40);
}
#[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();
let first = p.strokes()[0].len();
let second = p.strokes()[1].len();
assert_eq!(flat.len(), first + 1 + second + 1);
assert_eq!(flat[first], Point::new(-1, -1));
assert_eq!(flat[first + 1 + second], 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]
#[cfg(feature = "touch")]
fn touch_cancel_discards_the_in_progress_stroke() {
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);
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(&crate::event::Event::touch_begin(60, 60, 2));
p.handle_event(&crate::event::Event::touch_move(80, 80, 2));
let cancel = crate::event::translator::touch_cancel(crate::core::Point::new(80, 80), 2);
p.handle_event(&cancel);
assert_eq!(p.stroke_count(), 1, "a cancelled stroke must not be committed");
assert_eq!(changed.load(Ordering::SeqCst), 0, "a cancel must not emit `changed`");
assert_eq!(
completed.load(Ordering::SeqCst),
0,
"a cancel must not emit `stroke_completed`"
);
p.handle_event(&crate::event::Event::touch_begin(90, 90, 3));
p.handle_event(&crate::event::Event::touch_end(90, 90, 3));
assert_eq!(p.stroke_count(), 2, "a completed stroke still commits");
}
#[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 pointer_events_record_a_stroke() {
let mut p = pad();
p.handle_event(&Event::pointer_press(Point::new(10, 20), 1, 0.3, 0.0, 0.0));
assert!(p.current.is_some(), "a stylus press must begin a stroke");
p.handle_event(&Event::pointer_move(Point::new(40, 40), 0.5, 0.0, 0.0));
p.handle_event(&Event::pointer_move(Point::new(80, 50), 0.7, 0.0, 0.0));
p.handle_event(&Event::pointer_release(Point::new(80, 50), 1, 0.0));
assert_eq!(p.stroke_count(), 1, "a stylus gesture commits exactly one stroke");
let stroke = &p.strokes()[0];
assert_eq!(stroke.points()[0], Point::new(10, 20), "the stroke starts at the press");
assert_eq!(stroke.points().last().copied(), Some(Point::new(80, 50)));
}
#[test]
fn pointer_pressure_is_recorded_per_point_and_retained() {
let mut p = pad();
p.handle_event(&Event::pointer_press(Point::new(10, 20), 1, 0.25, 0.0, 0.0));
assert!((p.last_pressure() - 0.25).abs() < 1e-6, "the press pressure is retained");
p.handle_event(&Event::pointer_move(Point::new(40, 40), 0.75, 0.0, 0.0));
assert!((p.last_pressure() - 0.75).abs() < 1e-6, "a move updates the retained pressure");
p.handle_event(&Event::pointer_release(Point::new(40, 40), 1, 0.9));
let stroke = &p.strokes()[0];
assert_eq!(stroke.pressures().len(), stroke.points().len(), "pressure tracks every point");
assert!((stroke.pressures()[0] - 0.25).abs() < 1e-6);
assert!((stroke.last_pressure() - 0.75).abs() < 1e-6);
assert!((p.last_pressure() - 0.9).abs() < 1e-6);
}
#[test]
fn pressure_scales_the_drawn_segment_width() {
assert_eq!(segment_width(10, 1.0, 1.0, true), 10);
assert_eq!(segment_width(10, 0.2, 0.2, true), 2);
assert_eq!(segment_width(10, 0.0, 0.0, true), 1, "a zero-pressure segment is a hairline");
assert_eq!(segment_width(10, 0.2, 0.2, false), 10);
let mut p = pad();
p.set_stroke_width(10);
p.handle_event(&Event::pointer_press(Point::new(0, 0), 1, 0.1, 0.0, 0.0));
p.handle_event(&Event::pointer_move(Point::new(60, 0), 0.1, 0.0, 0.0));
let light = &p.current.as_ref().expect("in progress").pressures()[0];
assert!((*light - 0.1).abs() < 1e-6);
assert_eq!(segment_width(10, *light, *light, true), 1, "a light stroke draws thin");
match p.get("last_pressure").expect("readable") {
CapabilityValue::Float(value) => assert!(
(value - 0.1).abs() < 1e-6,
"last_pressure must read back the retained datum, got {value}"
),
other => panic!("last_pressure must be a Float, got {other:?}"),
}
assert_eq!(p.get("pressure_affects_width").expect("readable"), CapabilityValue::Bool(true));
p.set("pressure_affects_width", CapabilityValue::Bool(false)).expect("writable");
assert!(!p.pressure_affects_width());
}
#[test]
fn pointer_tilt_is_retained() {
let mut p = pad();
p.handle_event(&Event::pointer_press(Point::new(10, 20), 1, 0.5, 0.25, -0.5));
assert_eq!(p.last_tilt(), (0.25, -0.5));
assert_eq!(p.get("last_tilt_x").expect("readable"), CapabilityValue::Float(0.25));
assert_eq!(p.get("last_tilt_y").expect("readable"), CapabilityValue::Float(-0.5));
}
#[test]
fn pointer_press_with_a_non_primary_button_does_not_begin_a_stroke() {
let mut p = pad();
p.handle_event(&Event::pointer_press(Point::new(10, 20), 2, 0.5, 0.0, 0.0));
assert!(p.current.is_none(), "a barrel switch must not draw ink");
}
#[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());
}
#[test]
fn command_routes_redo_and_clear() {
let mut p = pad();
p.strokes.push(SignatureStroke::from_points(vec![Point::new(1, 1)]));
assert!(p.undo());
assert_eq!(p.stroke_count(), 0);
p.command("redo").expect("redo is command-routed");
assert_eq!(p.stroke_count(), 1);
p.command("clear").expect("clear is command-routed");
assert_eq!(p.stroke_count(), 0);
}
#[test]
fn stroke_summary_is_a_documented_key_value_string() {
let mut stroke = SignatureStroke::new();
stroke.push_with_pressure(Point::new(1, 1), 0.25);
stroke.push_with_pressure(Point::new(2, 2), 1.0);
let summary = stroke.summary();
assert_eq!(
summary, "points=2;pressure_avg=0.625;pressure_min=0.250;pressure_max=1.000",
"the payload's grammar must stay stable"
);
assert_eq!(
SignatureStroke::new().summary(),
"points=0;pressure_avg=1.000;pressure_min=1.000;pressure_max=1.000"
);
}
}