emath/
gui_rounding.rs

1/// We (sometimes) round sizes and coordinates to an even multiple of this value.
2///
3/// This is only used for rounding _logical UI points_, used for widget coordinates and sizes.
4/// When rendering, you may want to round to an integer multiple of the physical _pixels_ instead,
5/// using [`GuiRounding::round_to_pixels`].
6///
7/// See [`GuiRounding::round_ui`] for more information.
8///
9/// This constant has to be a (negative) power of two so that it can be represented exactly
10/// by a floating point number.
11///
12/// If we pick too large a value (e.g. 1 or 1/2), then we get judder during scrolling and animations.
13/// If we pick too small a value (e.g. 1/4096), we run the risk of rounding errors again.
14///
15/// `f32` has 23 bits of mantissa, so if we use e.g. 1/8 as the rounding factor,
16/// we can represent all numbers up to 2^20 exactly, which is plenty
17/// (to my knowledge there are no displays that are a million pixels wide).
18pub const GUI_ROUNDING: f32 = 1.0 / 32.0;
19
20/// Trait for rounding coordinates and sizes to align with either .
21///
22/// See [`GuiRounding::round_ui`] for more information.
23pub trait GuiRounding {
24    /// Rounds floating point numbers to an even multiple of the GUI rounding factor, [`crate::GUI_ROUNDING`].
25    ///
26    /// Use this for widget coordinates and sizes.
27    ///
28    /// Rounding sizes and positions prevent rounding errors when doing sizing calculations.
29    /// We don't round to integers, because that would be too coarse (causing visible juddering when scrolling, for instance).
30    /// Instead we round to an even multiple of [`GUI_ROUNDING`].
31    fn round_ui(self) -> Self;
32
33    /// Like [`Self::round_ui`], but always rounds towards negative infinity.
34    fn floor_ui(self) -> Self;
35
36    /// Round a size or position to an even multiple of the physical pixel size.
37    ///
38    /// This can be useful for crisp rendering.
39    ///
40    /// The `self` should be in coordinates of _logical UI points_.
41    /// The argument `pixels_per_point` is the number of _physical pixels_ per logical UI point.
42    /// For instance, on a high-DPI screen, `pixels_per_point` could be `2.0`.
43    fn round_to_pixels(self, pixels_per_point: f32) -> Self;
44
45    /// Will round the position to be in the center of a pixel.
46    ///
47    /// The pixel size is `1.0 / pixels_per_point`.
48    ///
49    /// So if `pixels_per_point = 2` (i.e. `pixel size = 0.5`),
50    /// then the position will be rounded to the closest of `…, 0.25, 0.75, 1.25, …`.
51    ///
52    /// This is useful, for instance, when picking the center of a line that is one pixel wide.
53    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self;
54}
55
56impl GuiRounding for f32 {
57    #[inline]
58    fn round_ui(self) -> Self {
59        (self / GUI_ROUNDING).round() * GUI_ROUNDING
60    }
61
62    #[inline]
63    fn floor_ui(self) -> Self {
64        (self / GUI_ROUNDING).floor() * GUI_ROUNDING
65    }
66
67    #[inline]
68    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
69        (self * pixels_per_point).round() / pixels_per_point
70    }
71
72    #[inline]
73    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
74        ((self * pixels_per_point - 0.5).round() + 0.5) / pixels_per_point
75    }
76}
77
78impl GuiRounding for f64 {
79    #[inline]
80    fn round_ui(self) -> Self {
81        (self / GUI_ROUNDING as Self).round() * GUI_ROUNDING as Self
82    }
83
84    #[inline]
85    fn floor_ui(self) -> Self {
86        (self / GUI_ROUNDING as Self).floor() * GUI_ROUNDING as Self
87    }
88
89    #[inline]
90    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
91        (self * pixels_per_point as Self).round() / pixels_per_point as Self
92    }
93
94    #[inline]
95    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
96        ((self * pixels_per_point as Self - 0.5).round() + 0.5) / pixels_per_point as Self
97    }
98}
99
100impl GuiRounding for crate::Vec2 {
101    #[inline]
102    fn round_ui(self) -> Self {
103        Self::new(self.x.round_ui(), self.y.round_ui())
104    }
105
106    #[inline]
107    fn floor_ui(self) -> Self {
108        Self::new(self.x.floor_ui(), self.y.floor_ui())
109    }
110
111    #[inline]
112    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
113        Self::new(
114            self.x.round_to_pixels(pixels_per_point),
115            self.y.round_to_pixels(pixels_per_point),
116        )
117    }
118
119    // This doesn't really make sense for a Vec2, but 🤷‍♂️
120    #[inline]
121    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
122        Self::new(
123            self.x.round_to_pixel_center(pixels_per_point),
124            self.y.round_to_pixel_center(pixels_per_point),
125        )
126    }
127}
128
129impl GuiRounding for crate::Pos2 {
130    #[inline]
131    fn round_ui(self) -> Self {
132        Self::new(self.x.round_ui(), self.y.round_ui())
133    }
134
135    #[inline]
136    fn floor_ui(self) -> Self {
137        Self::new(self.x.floor_ui(), self.y.floor_ui())
138    }
139
140    #[inline]
141    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
142        Self::new(
143            self.x.round_to_pixels(pixels_per_point),
144            self.y.round_to_pixels(pixels_per_point),
145        )
146    }
147
148    #[inline]
149    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
150        Self::new(
151            self.x.round_to_pixel_center(pixels_per_point),
152            self.y.round_to_pixel_center(pixels_per_point),
153        )
154    }
155}
156
157impl GuiRounding for crate::Rect {
158    /// Rounded so that two adjacent rects that tile perfectly
159    /// will continue to tile perfectly.
160    #[inline]
161    fn round_ui(self) -> Self {
162        Self::from_min_max(self.min.round_ui(), self.max.round_ui())
163    }
164
165    /// Rounded so that two adjacent rects that tile perfectly
166    /// will continue to tile perfectly.
167    #[inline]
168    fn floor_ui(self) -> Self {
169        Self::from_min_max(self.min.floor_ui(), self.max.floor_ui())
170    }
171
172    /// Rounded so that two adjacent rects that tile perfectly
173    /// will continue to tile perfectly.
174    #[inline]
175    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
176        Self::from_min_max(
177            self.min.round_to_pixels(pixels_per_point),
178            self.max.round_to_pixels(pixels_per_point),
179        )
180    }
181
182    /// Rounded so that two adjacent rects that tile perfectly
183    /// will continue to tile perfectly.
184    #[inline]
185    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
186        Self::from_min_max(
187            self.min.round_to_pixel_center(pixels_per_point),
188            self.max.round_to_pixel_center(pixels_per_point),
189        )
190    }
191}
192
193#[test]
194fn test_gui_rounding() {
195    assert_eq!(0.0_f32.round_ui(), 0.0);
196    assert_eq!((GUI_ROUNDING * 1.11).round_ui(), GUI_ROUNDING);
197    assert_eq!((-GUI_ROUNDING * 1.11).round_ui(), -GUI_ROUNDING);
198    assert_eq!(f32::NEG_INFINITY.round_ui(), f32::NEG_INFINITY);
199    assert_eq!(f32::INFINITY.round_ui(), f32::INFINITY);
200
201    assert_eq!(0.17_f32.round_to_pixel_center(2.0), 0.25);
202}