emath 0.33.3

Minimal 2D math library for GUI work
Documentation
/// We (sometimes) round sizes and coordinates to an even multiple of this value.
///
/// This is only used for rounding _logical UI points_, used for widget coordinates and sizes.
/// When rendering, you may want to round to an integer multiple of the physical _pixels_ instead,
/// using [`GuiRounding::round_to_pixels`].
///
/// See [`GuiRounding::round_ui`] for more information.
///
/// This constant has to be a (negative) power of two so that it can be represented exactly
/// by a floating point number.
///
/// If we pick too large a value (e.g. 1 or 1/2), then we get judder during scrolling and animations.
/// If we pick too small a value (e.g. 1/4096), we run the risk of rounding errors again.
///
/// `f32` has 23 bits of mantissa, so if we use e.g. 1/8 as the rounding factor,
/// we can represent all numbers up to 2^20 exactly, which is plenty
/// (to my knowledge there are no displays that are a million pixels wide).
pub const GUI_ROUNDING: f32 = 1.0 / 32.0;

/// Trait for rounding coordinates and sizes to align with either .
///
/// See [`GuiRounding::round_ui`] for more information.
pub trait GuiRounding {
    /// Rounds floating point numbers to an even multiple of the GUI rounding factor, [`crate::GUI_ROUNDING`].
    ///
    /// Use this for widget coordinates and sizes.
    ///
    /// Rounding sizes and positions prevent rounding errors when doing sizing calculations.
    /// We don't round to integers, because that would be too coarse (causing visible juddering when scrolling, for instance).
    /// Instead we round to an even multiple of [`GUI_ROUNDING`].
    fn round_ui(self) -> Self;

    /// Like [`Self::round_ui`], but always rounds towards negative infinity.
    fn floor_ui(self) -> Self;

    /// Round a size or position to an even multiple of the physical pixel size.
    ///
    /// This can be useful for crisp rendering.
    ///
    /// The `self` should be in coordinates of _logical UI points_.
    /// The argument `pixels_per_point` is the number of _physical pixels_ per logical UI point.
    /// For instance, on a high-DPI screen, `pixels_per_point` could be `2.0`.
    fn round_to_pixels(self, pixels_per_point: f32) -> Self;

    /// Will round the position to be in the center of a pixel.
    ///
    /// The pixel size is `1.0 / pixels_per_point`.
    ///
    /// So if `pixels_per_point = 2` (i.e. `pixel size = 0.5`),
    /// then the position will be rounded to the closest of `…, 0.25, 0.75, 1.25, …`.
    ///
    /// This is useful, for instance, when picking the center of a line that is one pixel wide.
    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self;
}

impl GuiRounding for f32 {
    #[inline]
    fn round_ui(self) -> Self {
        (self / GUI_ROUNDING).round() * GUI_ROUNDING
    }

    #[inline]
    fn floor_ui(self) -> Self {
        (self / GUI_ROUNDING).floor() * GUI_ROUNDING
    }

    #[inline]
    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
        (self * pixels_per_point).round() / pixels_per_point
    }

    #[inline]
    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
        ((self * pixels_per_point - 0.5).round() + 0.5) / pixels_per_point
    }
}

impl GuiRounding for f64 {
    #[inline]
    fn round_ui(self) -> Self {
        (self / GUI_ROUNDING as Self).round() * GUI_ROUNDING as Self
    }

    #[inline]
    fn floor_ui(self) -> Self {
        (self / GUI_ROUNDING as Self).floor() * GUI_ROUNDING as Self
    }

    #[inline]
    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
        (self * pixels_per_point as Self).round() / pixels_per_point as Self
    }

    #[inline]
    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
        ((self * pixels_per_point as Self - 0.5).round() + 0.5) / pixels_per_point as Self
    }
}

impl GuiRounding for crate::Vec2 {
    #[inline]
    fn round_ui(self) -> Self {
        Self::new(self.x.round_ui(), self.y.round_ui())
    }

    #[inline]
    fn floor_ui(self) -> Self {
        Self::new(self.x.floor_ui(), self.y.floor_ui())
    }

    #[inline]
    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
        Self::new(
            self.x.round_to_pixels(pixels_per_point),
            self.y.round_to_pixels(pixels_per_point),
        )
    }

    // This doesn't really make sense for a Vec2, but 🤷‍♂️
    #[inline]
    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
        Self::new(
            self.x.round_to_pixel_center(pixels_per_point),
            self.y.round_to_pixel_center(pixels_per_point),
        )
    }
}

impl GuiRounding for crate::Pos2 {
    #[inline]
    fn round_ui(self) -> Self {
        Self::new(self.x.round_ui(), self.y.round_ui())
    }

    #[inline]
    fn floor_ui(self) -> Self {
        Self::new(self.x.floor_ui(), self.y.floor_ui())
    }

    #[inline]
    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
        Self::new(
            self.x.round_to_pixels(pixels_per_point),
            self.y.round_to_pixels(pixels_per_point),
        )
    }

    #[inline]
    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
        Self::new(
            self.x.round_to_pixel_center(pixels_per_point),
            self.y.round_to_pixel_center(pixels_per_point),
        )
    }
}

impl GuiRounding for crate::Rect {
    /// Rounded so that two adjacent rects that tile perfectly
    /// will continue to tile perfectly.
    #[inline]
    fn round_ui(self) -> Self {
        Self::from_min_max(self.min.round_ui(), self.max.round_ui())
    }

    /// Rounded so that two adjacent rects that tile perfectly
    /// will continue to tile perfectly.
    #[inline]
    fn floor_ui(self) -> Self {
        Self::from_min_max(self.min.floor_ui(), self.max.floor_ui())
    }

    /// Rounded so that two adjacent rects that tile perfectly
    /// will continue to tile perfectly.
    #[inline]
    fn round_to_pixels(self, pixels_per_point: f32) -> Self {
        Self::from_min_max(
            self.min.round_to_pixels(pixels_per_point),
            self.max.round_to_pixels(pixels_per_point),
        )
    }

    /// Rounded so that two adjacent rects that tile perfectly
    /// will continue to tile perfectly.
    #[inline]
    fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
        Self::from_min_max(
            self.min.round_to_pixel_center(pixels_per_point),
            self.max.round_to_pixel_center(pixels_per_point),
        )
    }
}

#[test]
fn test_gui_rounding() {
    assert_eq!(0.0_f32.round_ui(), 0.0);
    assert_eq!((GUI_ROUNDING * 1.11).round_ui(), GUI_ROUNDING);
    assert_eq!((-GUI_ROUNDING * 1.11).round_ui(), -GUI_ROUNDING);
    assert_eq!(f32::NEG_INFINITY.round_ui(), f32::NEG_INFINITY);
    assert_eq!(f32::INFINITY.round_ui(), f32::INFINITY);

    assert_eq!(0.17_f32.round_to_pixel_center(2.0), 0.25);
}