1#![allow(clippy::derived_hash_with_manual_eq)] use std::{fmt::Debug, sync::Arc};
4
5use emath::GuiRounding as _;
6
7use super::{Color32, ColorMode, Pos2, Rect, emath};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
14pub struct Stroke {
15 pub width: f32,
16 pub color: Color32,
17}
18
19impl Stroke {
20 pub const NONE: Self = Self {
22 width: 0.0,
23 color: Color32::TRANSPARENT,
24 };
25
26 #[inline]
27 pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
28 Self {
29 width: width.into(),
30 color: color.into(),
31 }
32 }
33
34 #[inline]
36 pub fn is_empty(&self) -> bool {
37 self.width <= 0.0 || self.color == Color32::TRANSPARENT
38 }
39
40 pub fn round_center_to_pixel(&self, pixels_per_point: f32, coord: &mut f32) {
43 let pixel_size = 1.0 / pixels_per_point;
56
57 if self.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * self.width) {
58 *coord = coord.round_to_pixel_center(pixels_per_point);
59 } else {
60 *coord = coord.round_to_pixels(pixels_per_point);
61 }
62 }
63
64 pub(crate) fn round_rect_to_pixel(&self, pixels_per_point: f32, rect: &mut Rect) {
65 let pixel_size = 1.0 / pixels_per_point;
69
70 let width = self.width;
71 if width <= 0.0 {
72 *rect = rect.round_to_pixels(pixels_per_point);
73 } else if width <= pixel_size || is_nearest_integer_odd(pixels_per_point * width) {
74 *rect = rect.round_to_pixel_center(pixels_per_point);
75 } else {
76 *rect = rect.round_to_pixels(pixels_per_point);
77 }
78 }
79}
80
81impl<Color> From<(f32, Color)> for Stroke
82where
83 Color: Into<Color32>,
84{
85 #[inline(always)]
86 fn from((width, color): (f32, Color)) -> Self {
87 Self::new(width, color)
88 }
89}
90
91impl std::hash::Hash for Stroke {
92 #[inline(always)]
93 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
94 let Self { width, color } = *self;
95 emath::OrderedFloat(width).hash(state);
96 color.hash(state);
97 }
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
103pub enum StrokeKind {
104 Inside,
106
107 Middle,
109
110 Outside,
112}
113
114#[derive(Clone, Debug, PartialEq)]
118#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
119pub struct PathStroke {
120 pub width: f32,
121 pub color: ColorMode,
122 pub kind: StrokeKind,
123}
124
125impl Default for PathStroke {
126 #[inline]
127 fn default() -> Self {
128 Self::NONE
129 }
130}
131
132impl PathStroke {
133 pub const NONE: Self = Self {
135 width: 0.0,
136 color: ColorMode::TRANSPARENT,
137 kind: StrokeKind::Middle,
138 };
139
140 #[inline]
141 pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
142 Self {
143 width: width.into(),
144 color: ColorMode::Solid(color.into()),
145 kind: StrokeKind::Middle,
146 }
147 }
148
149 #[inline]
153 pub fn new_uv(
154 width: impl Into<f32>,
155 callback: impl Fn(Rect, Pos2) -> Color32 + Send + Sync + 'static,
156 ) -> Self {
157 Self {
158 width: width.into(),
159 color: ColorMode::UV(Arc::new(callback)),
160 kind: StrokeKind::Middle,
161 }
162 }
163
164 #[inline]
165 pub fn with_kind(self, kind: StrokeKind) -> Self {
166 Self { kind, ..self }
167 }
168
169 #[inline]
171 pub fn middle(self) -> Self {
172 Self {
173 kind: StrokeKind::Middle,
174 ..self
175 }
176 }
177
178 #[inline]
180 pub fn outside(self) -> Self {
181 Self {
182 kind: StrokeKind::Outside,
183 ..self
184 }
185 }
186
187 #[inline]
189 pub fn inside(self) -> Self {
190 Self {
191 kind: StrokeKind::Inside,
192 ..self
193 }
194 }
195
196 #[inline]
198 pub fn is_empty(&self) -> bool {
199 self.width <= 0.0 || self.color == ColorMode::TRANSPARENT
200 }
201}
202
203impl<Color> From<(f32, Color)> for PathStroke
204where
205 Color: Into<Color32>,
206{
207 #[inline(always)]
208 fn from((width, color): (f32, Color)) -> Self {
209 Self::new(width, color)
210 }
211}
212
213impl From<Stroke> for PathStroke {
214 fn from(value: Stroke) -> Self {
215 if value.is_empty() {
216 Self::NONE
218 } else {
219 Self {
220 width: value.width,
221 color: ColorMode::Solid(value.color),
222 kind: StrokeKind::Middle,
223 }
224 }
225 }
226}
227
228fn is_nearest_integer_odd(x: f32) -> bool {
230 (x * 0.5 + 0.25).fract() > 0.5
231}
232
233#[test]
234fn test_is_nearest_integer_odd() {
235 assert!(is_nearest_integer_odd(0.6));
236 assert!(is_nearest_integer_odd(1.0));
237 assert!(is_nearest_integer_odd(1.4));
238 assert!(!is_nearest_integer_odd(1.6));
239 assert!(!is_nearest_integer_odd(2.0));
240 assert!(!is_nearest_integer_odd(2.4));
241 assert!(is_nearest_integer_odd(2.6));
242 assert!(is_nearest_integer_odd(3.0));
243 assert!(is_nearest_integer_odd(3.4));
244}