use topcoat_core::fnv1a::Fnv1a;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct ObliqueAngle(f32);
impl ObliqueAngle {
pub const DEFAULT: Self = Self(14.0);
#[must_use]
#[track_caller]
pub const fn new(degrees: f32) -> Self {
assert!(
degrees >= -90.0 && degrees <= 90.0,
"oblique angle out of range -90deg..=90deg"
);
Self(degrees)
}
pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
h.write(&self.0.to_bits().to_le_bytes())
}
}
impl Default for ObliqueAngle {
fn default() -> Self {
Self::DEFAULT
}
}
impl From<ObliqueAngle> for f32 {
fn from(value: ObliqueAngle) -> Self {
value.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObliqueAngleOutOfRangeError;
impl std::fmt::Display for ObliqueAngleOutOfRangeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("oblique angle out of range -90deg..=90deg")
}
}
impl std::error::Error for ObliqueAngleOutOfRangeError {}
impl TryFrom<f32> for ObliqueAngle {
type Error = ObliqueAngleOutOfRangeError;
fn try_from(value: f32) -> Result<Self, Self::Error> {
if !(-90.0..=90.0).contains(&value) {
return Err(ObliqueAngleOutOfRangeError);
}
Ok(Self(value))
}
}
impl std::fmt::Display for ObliqueAngle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}deg", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ObliqueAngleRange {
start: ObliqueAngle,
end: ObliqueAngle,
}
impl ObliqueAngleRange {
#[must_use]
#[track_caller]
pub const fn new(start: ObliqueAngle, end: ObliqueAngle) -> Self {
assert!(end.0 >= start.0, "oblique angle range must not be empty");
Self { start, end }
}
#[must_use]
#[track_caller]
pub const fn from_degrees(start: f32, end: f32) -> Self {
Self::new(ObliqueAngle::new(start), ObliqueAngle::new(end))
}
#[must_use]
pub const fn start(&self) -> ObliqueAngle {
self.start
}
#[must_use]
pub const fn end(&self) -> ObliqueAngle {
self.end
}
pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
self.end.hash(self.start.hash(h))
}
}
impl std::fmt::Display for ObliqueAngleRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.start == self.end {
self.start.fmt(f)
} else {
write!(f, "{} {}", self.start, self.end)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum FontStyle {
#[default]
Normal,
Italic,
Oblique(Option<ObliqueAngleRange>),
}
impl FontStyle {
#[must_use]
pub const fn oblique() -> Self {
Self::Oblique(None)
}
#[must_use]
#[track_caller]
pub const fn oblique_angle(degrees: f32) -> Self {
let angle = ObliqueAngle::new(degrees);
Self::Oblique(Some(ObliqueAngleRange::new(angle, angle)))
}
#[must_use]
#[track_caller]
pub const fn oblique_range(start: f32, end: f32) -> Self {
Self::Oblique(Some(ObliqueAngleRange::from_degrees(start, end)))
}
pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
match self {
Self::Normal => h.write(b"n"),
Self::Italic => h.write(b"i"),
Self::Oblique(None) => h.write(b"o"),
Self::Oblique(Some(range)) => range.hash(h.write(b"oa")),
}
}
}
impl std::fmt::Display for FontStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Normal => f.write_str("normal"),
Self::Italic => f.write_str("italic"),
Self::Oblique(None) => f.write_str("oblique"),
Self::Oblique(Some(angle)) => write!(f, "oblique {angle}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normal_and_italic_display_as_keywords() {
assert_eq!(FontStyle::Normal.to_string(), "normal");
assert_eq!(FontStyle::Italic.to_string(), "italic");
}
#[test]
fn default_is_normal() {
assert_eq!(FontStyle::default(), FontStyle::Normal);
}
#[test]
fn bare_oblique_displays_without_an_angle() {
assert_eq!(FontStyle::oblique().to_string(), "oblique");
}
#[test]
fn oblique_with_an_angle_displays_the_angle() {
assert_eq!(FontStyle::oblique_angle(14.0).to_string(), "oblique 14deg");
}
#[test]
fn oblique_with_a_negative_angle_displays_the_sign() {
assert_eq!(
FontStyle::oblique_angle(-12.5).to_string(),
"oblique -12.5deg",
);
}
#[test]
fn oblique_with_a_range_displays_both_angles() {
assert_eq!(
FontStyle::oblique_range(20.0, 40.0).to_string(),
"oblique 20deg 40deg",
);
}
#[test]
fn oblique_range_collapses_when_start_equals_end() {
assert_eq!(
FontStyle::oblique_range(14.0, 14.0).to_string(),
"oblique 14deg",
);
}
#[test]
fn default_oblique_angle_is_14_degrees() {
assert_eq!(ObliqueAngle::DEFAULT.to_string(), "14deg");
assert_eq!(ObliqueAngle::default(), ObliqueAngle::DEFAULT);
}
#[test]
fn angle_converts_to_f32() {
assert!((f32::from(ObliqueAngle::new(30.0)) - 30.0).abs() < 0.001);
}
#[test]
fn try_from_accepts_the_bounds() {
assert_eq!(ObliqueAngle::try_from(-90.0), Ok(ObliqueAngle::new(-90.0)));
assert_eq!(ObliqueAngle::try_from(90.0), Ok(ObliqueAngle::new(90.0)));
}
#[test]
fn try_from_rejects_out_of_range() {
assert_eq!(
ObliqueAngle::try_from(90.1),
Err(ObliqueAngleOutOfRangeError),
);
assert_eq!(
ObliqueAngle::try_from(-90.1),
Err(ObliqueAngleOutOfRangeError),
);
}
#[test]
#[should_panic = "out of range"]
fn new_panics_above_the_maximum() {
let _ = ObliqueAngle::new(90.1);
}
#[test]
#[should_panic = "out of range"]
fn new_panics_below_the_minimum() {
let _ = ObliqueAngle::new(-90.1);
}
#[test]
#[should_panic = "empty"]
fn range_panics_when_end_precedes_start() {
let _ = ObliqueAngleRange::from_degrees(40.0, 20.0);
}
#[test]
fn range_exposes_its_bounds() {
let range = ObliqueAngleRange::from_degrees(20.0, 40.0);
assert_eq!(range.start(), ObliqueAngle::new(20.0));
assert_eq!(range.end(), ObliqueAngle::new(40.0));
}
}