Skip to main content

ferroui_controls_inking/
ink_drawing_attributes.rs

1use super::ink_point::InkError;
2use ferroui_base::media::Color;
3use ferroui_base::{Matrix, Size};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6#[repr(i32)]
7pub enum PenTipShape {
8    Circle = 0,
9    Rectangle = 1,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13#[repr(i32)]
14pub enum InkDrawingAttributesKind {
15    Default = 0,
16    Pencil = 1,
17}
18
19/// A value snapshot. Changing presenter defaults never mutates completed ink.
20/// `size` describes the nib at full pressure, in device-independent pixels.
21#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct InkDrawingAttributes {
23    pub color: Color,
24    pub size: Size,
25    pub pen_tip: PenTipShape,
26    pub pen_tip_transform: Matrix,
27    pub draw_as_highlighter: bool,
28    pub ignore_pressure: bool,
29    pub fit_to_curve: bool,
30    pub ignore_tilt: bool,
31    pub kind: InkDrawingAttributesKind,
32    pub pencil_opacity: f64,
33}
34
35impl Default for InkDrawingAttributes {
36    fn default() -> Self {
37        Self {
38            color: Color::from_rgb(0, 0, 0),
39            size: Size::new(2.0, 2.0),
40            pen_tip: PenTipShape::Circle,
41            pen_tip_transform: Matrix::IDENTITY,
42            draw_as_highlighter: false,
43            ignore_pressure: false,
44            fit_to_curve: true,
45            ignore_tilt: false,
46            kind: InkDrawingAttributesKind::Default,
47            pencil_opacity: 1.0,
48        }
49    }
50}
51
52impl InkDrawingAttributes {
53    pub fn new() -> Self {
54        Self::default()
55    }
56    pub fn create_for_pencil() -> Self {
57        Self {
58            kind: InkDrawingAttributesKind::Pencil,
59            pencil_opacity: 0.5,
60            ..Self::default()
61        }
62    }
63
64    pub fn validate(&self) -> Result<(), InkError> {
65        if !self.size.width.is_finite()
66            || !self.size.height.is_finite()
67            || self.size.width <= 0.0
68            || self.size.height <= 0.0
69            || self.size.width > 100_000.0
70            || self.size.height > 100_000.0
71            || !self.pencil_opacity.is_finite()
72            || !(0.0..=1.0).contains(&self.pencil_opacity)
73        {
74            return Err(InkError::InvalidDrawingAttributes);
75        }
76        validate_affine(self.pen_tip_transform)?;
77        let m = self.pen_tip_transform;
78        if m.m31 != 0.0 || m.m32 != 0.0 || (m.m11 * m.m22 - m.m12 * m.m21).abs() < 1.0e-12 {
79            return Err(InkError::InvalidDrawingAttributes);
80        }
81        Ok(())
82    }
83
84    /// The factor of the nib size at `pressure`. The nominal pressure 0.5 (what devices without
85    /// pressure report, and what `InkStrokeBuilder` gives its points) draws the configured
86    /// size, full pressure 1.75 times it and the lightest a quarter of it. A sample without
87    /// pressure (0, no contact) draws nothing: a stroke of such samples is invisible and has no
88    /// bounds.
89    // Deviation (DEVIATIONS.md, WinUI inking): DirectInk's pressure curve is not published; the
90    // port uses the linear curve of WPF's ink, `0.25 + 1.5 * pressure`, so that a mouse or touch
91    // stroke has the width the toolbar shows.
92    pub(crate) fn pressure_scale(&self, pressure: f32) -> f64 {
93        let pressure = f64::from(pressure);
94        if self.ignore_pressure {
95            1.0
96        } else if !(pressure > 0.0) {
97            0.0
98        } else {
99            0.25 + 1.5 * pressure.min(1.0)
100        }
101    }
102}
103
104pub(crate) fn validate_affine(m: Matrix) -> Result<(), InkError> {
105    if [m.m11, m.m12, m.m21, m.m22, m.m31, m.m32]
106        .iter()
107        .any(|v| !v.is_finite() || v.abs() > 1.0e12)
108        || m.m13 != 0.0
109        || m.m23 != 0.0
110        || m.m33 != 1.0
111    {
112        return Err(InkError::InvalidTransform);
113    }
114    Ok(())
115}
116
117ferroui_base::ferro_markup_enum!(PenTipShape { Circle, Rectangle });
118ferroui_base::ferro_markup_enum!(InkDrawingAttributesKind { Default, Pencil });
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    #[test]
124    fn rejects_invalid_size_and_perspective() {
125        let mut a = InkDrawingAttributes::default();
126        a.size.width = 0.0;
127        assert_eq!(a.validate(), Err(InkError::InvalidDrawingAttributes));
128        a = InkDrawingAttributes::default();
129        a.pen_tip_transform.m13 = 1.0;
130        assert_eq!(a.validate(), Err(InkError::InvalidTransform));
131    }
132    #[test]
133    fn pencil_defaults_are_a_value_snapshot() {
134        let a = InkDrawingAttributes::create_for_pencil();
135        let mut b = a;
136        b.pencil_opacity = 0.8;
137        assert_eq!(a.pencil_opacity, 0.5);
138        assert_eq!(a.kind, InkDrawingAttributesKind::Pencil);
139        assert!(a.validate().is_ok());
140    }
141}