epaint/
stroke.rs

1use std::{fmt::Debug, sync::Arc};
2
3use emath::GuiRounding as _;
4
5use super::{Color32, ColorMode, Pos2, Rect, emath};
6
7/// Describes the width and color of a line.
8///
9/// The default stroke is the same as [`Stroke::NONE`].
10#[derive(Clone, Copy, Debug, Default, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
12pub struct Stroke {
13    pub width: f32,
14    pub color: Color32,
15}
16
17impl Stroke {
18    /// Same as [`Stroke::default`].
19    pub const NONE: Self = Self {
20        width: 0.0,
21        color: Color32::TRANSPARENT,
22    };
23
24    #[inline]
25    pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
26        Self {
27            width: width.into(),
28            color: color.into(),
29        }
30    }
31
32    /// True if width is zero or color is transparent
33    #[inline]
34    pub fn is_empty(&self) -> bool {
35        self.width <= 0.0 || self.color == Color32::TRANSPARENT
36    }
37
38    /// For vertical or horizontal lines:
39    /// round the stroke center to produce a sharp, pixel-aligned line.
40    pub fn round_center_to_pixel(&self, pixels_per_point: f32, coord: &mut f32) {
41        // If the stroke is an odd number of pixels wide,
42        // we want to round the center of it to the center of a pixel.
43        //
44        // If however it is an even number of pixels wide,
45        // we want to round the center to be between two pixels.
46        //
47        // We also want to treat strokes that are _almost_ odd as it it was odd,
48        // to make it symmetric. Same for strokes that are _almost_ even.
49        //
50        // For strokes less than a pixel wide we also round to the center,
51        // because it will rendered as a single row of pixels by the tessellator.
52
53        let pixel_size = 1.0 / pixels_per_point;
54
55        if self.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * self.width) {
56            *coord = coord.round_to_pixel_center(pixels_per_point);
57        } else {
58            *coord = coord.round_to_pixels(pixels_per_point);
59        }
60    }
61
62    pub(crate) fn round_rect_to_pixel(&self, pixels_per_point: f32, rect: &mut Rect) {
63        // We put odd-width strokes in the center of pixels.
64        // To understand why, see `fn round_center_to_pixel`.
65
66        let pixel_size = 1.0 / pixels_per_point;
67
68        let width = self.width;
69        if width <= 0.0 {
70            *rect = rect.round_to_pixels(pixels_per_point);
71        } else if width <= pixel_size || is_nearest_integer_odd(pixels_per_point * width) {
72            *rect = rect.round_to_pixel_center(pixels_per_point);
73        } else {
74            *rect = rect.round_to_pixels(pixels_per_point);
75        }
76    }
77}
78
79impl<Color> From<(f32, Color)> for Stroke
80where
81    Color: Into<Color32>,
82{
83    #[inline(always)]
84    fn from((width, color): (f32, Color)) -> Self {
85        Self::new(width, color)
86    }
87}
88
89impl std::hash::Hash for Stroke {
90    #[inline(always)]
91    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
92        let Self { width, color } = *self;
93        emath::OrderedFloat(width).hash(state);
94        color.hash(state);
95    }
96}
97
98/// Describes how the stroke of a shape should be painted.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
101pub enum StrokeKind {
102    /// The stroke should be painted entirely inside of the shape
103    Inside,
104
105    /// The stroke should be painted right on the edge of the shape, half inside and half outside.
106    Middle,
107
108    /// The stroke should be painted entirely outside of the shape
109    Outside,
110}
111
112/// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`]
113///
114/// The default stroke is the same as [`Stroke::NONE`].
115#[derive(Clone, Debug, PartialEq)]
116#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
117pub struct PathStroke {
118    pub width: f32,
119    pub color: ColorMode,
120    pub kind: StrokeKind,
121}
122
123impl Default for PathStroke {
124    #[inline]
125    fn default() -> Self {
126        Self::NONE
127    }
128}
129
130impl PathStroke {
131    /// Same as [`PathStroke::default`].
132    pub const NONE: Self = Self {
133        width: 0.0,
134        color: ColorMode::TRANSPARENT,
135        kind: StrokeKind::Middle,
136    };
137
138    #[inline]
139    pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
140        Self {
141            width: width.into(),
142            color: ColorMode::Solid(color.into()),
143            kind: StrokeKind::Middle,
144        }
145    }
146
147    /// Create a new `PathStroke` with a UV function
148    ///
149    /// The bounding box passed to the callback will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`)
150    #[inline]
151    pub fn new_uv(
152        width: impl Into<f32>,
153        callback: impl Fn(Rect, Pos2) -> Color32 + Send + Sync + 'static,
154    ) -> Self {
155        Self {
156            width: width.into(),
157            color: ColorMode::UV(Arc::new(callback)),
158            kind: StrokeKind::Middle,
159        }
160    }
161
162    #[inline]
163    pub fn with_kind(self, kind: StrokeKind) -> Self {
164        Self { kind, ..self }
165    }
166
167    /// Set the stroke to be painted right on the edge of the shape, half inside and half outside.
168    #[inline]
169    pub fn middle(self) -> Self {
170        Self {
171            kind: StrokeKind::Middle,
172            ..self
173        }
174    }
175
176    /// Set the stroke to be painted entirely outside of the shape
177    #[inline]
178    pub fn outside(self) -> Self {
179        Self {
180            kind: StrokeKind::Outside,
181            ..self
182        }
183    }
184
185    /// Set the stroke to be painted entirely inside of the shape
186    #[inline]
187    pub fn inside(self) -> Self {
188        Self {
189            kind: StrokeKind::Inside,
190            ..self
191        }
192    }
193
194    /// True if width is zero or color is solid and transparent
195    #[inline]
196    pub fn is_empty(&self) -> bool {
197        self.width <= 0.0 || self.color == ColorMode::TRANSPARENT
198    }
199}
200
201impl<Color> From<(f32, Color)> for PathStroke
202where
203    Color: Into<Color32>,
204{
205    #[inline(always)]
206    fn from((width, color): (f32, Color)) -> Self {
207        Self::new(width, color)
208    }
209}
210
211impl From<Stroke> for PathStroke {
212    fn from(value: Stroke) -> Self {
213        if value.is_empty() {
214            // Important, since we use the stroke color when doing feathering of the fill!
215            Self::NONE
216        } else {
217            Self {
218                width: value.width,
219                color: ColorMode::Solid(value.color),
220                kind: StrokeKind::Middle,
221            }
222        }
223    }
224}
225
226/// Returns true if the nearest integer is odd.
227fn is_nearest_integer_odd(x: f32) -> bool {
228    (x * 0.5 + 0.25).fract() > 0.5
229}
230
231#[test]
232fn test_is_nearest_integer_odd() {
233    assert!(is_nearest_integer_odd(0.6));
234    assert!(is_nearest_integer_odd(1.0));
235    assert!(is_nearest_integer_odd(1.4));
236    assert!(!is_nearest_integer_odd(1.6));
237    assert!(!is_nearest_integer_odd(2.0));
238    assert!(!is_nearest_integer_odd(2.4));
239    assert!(is_nearest_integer_odd(2.6));
240    assert!(is_nearest_integer_odd(3.0));
241    assert!(is_nearest_integer_odd(3.4));
242}