use std::cmp::Ordering;
use std::ops::Add;
use bevy::prelude::*;
use smol_str::SmolStr;
use crate::prelude::FontMap;
fn register_bevy_default_font(mut fonts: ResMut<FontMap>)
{
fonts.manual_insert(Handle::default(), FontRequest::new_static("Bevy"));
}
pub trait UpdateFontRequest
{
fn update(self, req: FontRequest) -> FontRequest;
}
#[derive(Reflect, Default, Deref, Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct FontFamily(pub SmolStr);
impl FontFamily
{
pub fn new(s: impl Into<SmolStr>) -> Self
{
Self(s.into())
}
pub const fn new_static(s: &'static str) -> Self
{
Self(SmolStr::new_static(s))
}
pub fn request(self) -> FontRequest
{
FontRequest::from(self)
}
}
impl<S: Into<SmolStr>> From<S> for FontFamily
{
fn from(s: S) -> Self
{
Self::new(s)
}
}
impl UpdateFontRequest for FontFamily
{
fn update(self, mut req: FontRequest) -> FontRequest
{
req.family = self;
req
}
}
pub trait FontFamilyExt
{
fn family(self, family: impl Into<FontFamily>) -> FontRequest;
}
impl<T: Into<FontRequest>> FontFamilyExt for T
{
fn family(self, family: impl Into<FontFamily>) -> FontRequest
{
self.into().set(family.into())
}
}
#[derive(Reflect, Default, Debug, Copy, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum FontWidth
{
UltraCondensed,
ExtraCondensed,
Condensed,
#[default]
Normal,
SemiExpanded,
Expanded,
ExtraExpanded,
UltraExpanded,
Width(f32),
}
impl FontWidth
{
pub fn width(&self) -> f32
{
match self {
Self::UltraCondensed => 0.5,
Self::ExtraCondensed => 0.625,
Self::Condensed => 0.75,
Self::Normal => 1.,
Self::SemiExpanded => 1.125,
Self::Expanded => 1.25,
Self::ExtraExpanded => 1.5,
Self::UltraExpanded => 2.,
Self::Width(width) => width.clamp(0.5, 2.),
}
}
pub fn negotiate<I, F>(request: FontWidth, test_vals_fn: F) -> Option<FontWidth>
where
I: Iterator<Item = FontWidth>,
F: Fn() -> I,
{
let width = request.width();
let mut nearest_min: Option<f32> = None; let mut nearest_max: Option<f32> = None; for test_width in (test_vals_fn)().map(|w| w.width()) {
if test_width == width {
return Some(request);
} else if test_width < width {
let diff = width - test_width;
if diff <= (width - nearest_min.unwrap_or(0.5)) {
nearest_min = Some(test_width);
}
} else {
let diff = test_width - width;
if diff <= (nearest_max.unwrap_or(2.) - width) {
nearest_max = Some(test_width);
}
}
}
if width <= 1. {
if let Some(nearest_min) = nearest_min {
return Some(FontWidth::Width(nearest_min));
}
if let Some(nearest_max) = nearest_max {
return Some(FontWidth::Width(nearest_max));
}
}
if width > 1. {
if let Some(nearest_max) = nearest_max {
return Some(FontWidth::Width(nearest_max));
}
if let Some(nearest_min) = nearest_min {
return Some(FontWidth::Width(nearest_min));
}
}
None
}
}
impl PartialEq for FontWidth
{
fn eq(&self, other: &Self) -> bool
{
self.width() == other.width()
}
}
impl PartialOrd for FontWidth
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering>
{
self.width().partial_cmp(&other.width())
}
}
impl UpdateFontRequest for FontWidth
{
fn update(self, mut req: FontRequest) -> FontRequest
{
req.width = self;
req
}
}
pub trait FontWidthExt
{
fn ultra_condensed(self) -> FontRequest;
fn extra_condensed(self) -> FontRequest;
fn condensed(self) -> FontRequest;
fn normal_width(self) -> FontRequest;
fn semi_expanded(self) -> FontRequest;
fn expanded(self) -> FontRequest;
fn extra_expanded(self) -> FontRequest;
fn ultra_expanded(self) -> FontRequest;
fn width(self, width: f32) -> FontRequest;
}
impl<T: Into<FontRequest>> FontWidthExt for T
{
fn ultra_condensed(self) -> FontRequest
{
self.into() + FontWidth::UltraCondensed
}
fn extra_condensed(self) -> FontRequest
{
self.into() + FontWidth::ExtraCondensed
}
fn condensed(self) -> FontRequest
{
self.into() + FontWidth::Condensed
}
fn normal_width(self) -> FontRequest
{
self.into() + FontWidth::Normal
}
fn semi_expanded(self) -> FontRequest
{
self.into() + FontWidth::SemiExpanded
}
fn expanded(self) -> FontRequest
{
self.into() + FontWidth::Expanded
}
fn extra_expanded(self) -> FontRequest
{
self.into() + FontWidth::ExtraExpanded
}
fn ultra_expanded(self) -> FontRequest
{
self.into() + FontWidth::UltraExpanded
}
fn width(self, width: f32) -> FontRequest
{
self.into() + FontWidth::Width(FontWidth::Width(width).width())
}
}
#[derive(Reflect, Default, Debug, Copy, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum FontStyle
{
#[default]
Normal,
Italic,
Oblique(Option<i16>),
}
impl FontStyle
{
pub fn oblique_angle(&self) -> Option<i16>
{
match *self {
Self::Normal => Some(0),
Self::Italic => None,
Self::Oblique(angle) => Some(Self::normalize_oblique(angle)),
}
}
pub fn negotiate<I, F>(request: FontStyle, test_vals_fn: F) -> Option<FontStyle>
where
I: Iterator<Item = FontStyle>,
F: Fn() -> I,
{
match request {
Self::Normal => Self::negotiate_inner(Self::Oblique(Some(0)), &test_vals_fn)
.or_else(|| Self::negotiate_inner(Self::Italic, &test_vals_fn))
.or_else(|| Self::negotiate_inner(Self::Oblique(Some(-1)), &test_vals_fn)),
Self::Italic => Self::negotiate_inner(Self::Italic, &test_vals_fn)
.or_else(|| Self::negotiate_inner(Self::Oblique(Some(11)), &test_vals_fn))
.or_else(|| Self::negotiate_inner(Self::Oblique(Some(-1)), &test_vals_fn)),
Self::Oblique(angle) => {
let angle = Self::normalize_oblique(angle);
let mut angle_res = Self::negotiate_inner(Self::Oblique(Some(angle)), &test_vals_fn);
if angle >= 0 {
angle_res = angle_res.or_else(|| Self::negotiate_inner(Self::Italic, &test_vals_fn));
}
let opposite = if angle >= 0 { -1 } else { 0 };
angle_res =
angle_res.or_else(|| Self::negotiate_inner(Self::Oblique(Some(opposite)), &test_vals_fn));
if angle < 0 {
angle_res = angle_res.or_else(|| Self::negotiate_inner(Self::Italic, &test_vals_fn));
}
angle_res
}
}
}
fn negotiate_inner<I, F>(request: FontStyle, test_vals_fn: &F) -> Option<FontStyle>
where
I: Iterator<Item = FontStyle>,
F: Fn() -> I,
{
match request {
Self::Normal | Self::Italic => (*test_vals_fn)().find(|t| *t == request),
Self::Oblique(angle) => {
let angle = Self::normalize_oblique(angle);
let mut nearest_zero: Option<i16> = None; let mut nearest_max: Option<i16> = None; for test_angle in (test_vals_fn)().filter_map(|s| s.oblique_angle()) {
if !((angle >= 0 && test_angle >= 0) || (angle < 0 && test_angle < 0)) {
return None;
}
if test_angle.abs() == angle.abs() {
return Some(request);
} else if test_angle.abs() < angle.abs() {
let diff = angle.abs() - test_angle.abs();
if diff <= (angle.abs() - nearest_zero.unwrap_or(0).abs()) {
nearest_zero = Some(test_angle);
}
} else {
let diff = test_angle.abs() - angle.abs();
if diff <= (nearest_max.unwrap_or(90).abs() - angle.abs()) {
nearest_max = Some(test_angle);
}
}
}
if angle.abs() < 11 {
if nearest_zero.is_some() {
return Some(FontStyle::Oblique(nearest_zero));
}
if nearest_max.is_some() {
return Some(FontStyle::Oblique(nearest_max));
}
}
if angle.abs() >= 11 {
if nearest_max.is_some() {
return Some(FontStyle::Oblique(nearest_max));
}
if nearest_zero.is_some() {
return Some(FontStyle::Oblique(nearest_zero));
}
}
None
}
}
}
fn normalize_oblique(angle: Option<i16>) -> i16
{
angle.unwrap_or(14).clamp(-90, 90)
}
}
impl PartialEq for FontStyle
{
fn eq(&self, other: &Self) -> bool
{
match *self {
Self::Normal => matches!(*other, Self::Normal) || matches!(*other, Self::Oblique(Some(0))),
Self::Italic => matches!(*other, Self::Italic),
Self::Oblique(angle) => match *other {
Self::Normal => angle == Some(0),
Self::Oblique(other_angle) => {
Self::normalize_oblique(angle) == Self::normalize_oblique(other_angle)
}
Self::Italic => false,
},
}
}
}
impl UpdateFontRequest for FontStyle
{
fn update(self, mut req: FontRequest) -> FontRequest
{
req.style = self;
req
}
}
pub trait FontStyleExt
{
fn normal_style(self) -> FontRequest;
fn italic(self) -> FontRequest;
fn oblique(self) -> FontRequest;
fn oblique_angle(self, angle: i16) -> FontRequest;
}
impl<T: Into<FontRequest>> FontStyleExt for T
{
fn normal_style(self) -> FontRequest
{
self.into() + FontStyle::Normal
}
fn italic(self) -> FontRequest
{
self.into() + FontStyle::Italic
}
fn oblique(self) -> FontRequest
{
self.into() + FontStyle::Oblique(None)
}
fn oblique_angle(self, angle: i16) -> FontRequest
{
self.into() + FontStyle::Oblique(Some(FontStyle::normalize_oblique(Some(angle))))
}
}
#[derive(Reflect, Default, Debug, Copy, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum FontWeight
{
Thin,
Hairline,
ExtraLight,
UltraLight,
Light,
#[default]
Normal,
Regular,
Medium,
SemiBold,
DemiBold,
Bold,
ExtraBold,
UltraBold,
Black,
Heavy,
ExtraBlack,
UltraBlack,
Weight(u16),
}
impl FontWeight
{
pub fn weight(&self) -> u16
{
match *self {
Self::Thin => 100,
Self::Hairline => 100,
Self::ExtraLight => 200,
Self::UltraLight => 200,
Self::Light => 300,
Self::Normal => 400,
Self::Regular => 400,
Self::Medium => 500,
Self::SemiBold => 600,
Self::DemiBold => 600,
Self::Bold => 700,
Self::ExtraBold => 800,
Self::UltraBold => 800,
Self::Black => 900,
Self::Heavy => 900,
Self::ExtraBlack => 950,
Self::UltraBlack => 950,
Self::Weight(weight) => weight.clamp(1, 1000),
}
}
pub fn negotiate<I, F>(request: FontWeight, test_vals_fn: F) -> Option<FontWeight>
where
I: Iterator<Item = FontWeight>,
F: Fn() -> I,
{
let weight = request.weight();
let mut nearest_min: Option<u16> = None; let mut nearest_max: Option<u16> = None; for test_weight in (test_vals_fn)().map(|w| w.weight()) {
if test_weight == weight {
return Some(request);
} else if test_weight < weight {
let diff = weight - test_weight;
if diff <= (weight - nearest_min.unwrap_or(1)) {
nearest_min = Some(test_weight);
}
} else {
let diff = test_weight - weight;
if diff <= (nearest_max.unwrap_or(1000) - weight) {
nearest_max = Some(test_weight);
}
}
}
if weight >= 400 && weight <= 500 {
if let Some(nearest_max) = nearest_max {
if nearest_max <= 500 {
return Some(FontWeight::Weight(nearest_max));
}
}
if let Some(nearest_min) = nearest_min {
return Some(FontWeight::Weight(nearest_min));
}
if let Some(nearest_max) = nearest_max {
return Some(FontWeight::Weight(nearest_max));
}
}
if weight < 400 {
if let Some(nearest_min) = nearest_min {
return Some(FontWeight::Weight(nearest_min));
}
if let Some(nearest_max) = nearest_max {
return Some(FontWeight::Weight(nearest_max));
}
}
if weight > 500 {
if let Some(nearest_max) = nearest_max {
return Some(FontWeight::Weight(nearest_max));
}
if let Some(nearest_min) = nearest_min {
return Some(FontWeight::Weight(nearest_min));
}
}
None
}
}
impl PartialEq for FontWeight
{
fn eq(&self, other: &Self) -> bool
{
self.weight() == other.weight()
}
}
impl PartialOrd for FontWeight
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering>
{
self.weight().partial_cmp(&other.weight())
}
}
impl UpdateFontRequest for FontWeight
{
fn update(self, mut req: FontRequest) -> FontRequest
{
req.weight = self;
req
}
}
pub trait FontWeightExt
{
fn thin(self) -> FontRequest;
fn hairline(self) -> FontRequest;
fn extra_light(self) -> FontRequest;
fn ultra_light(self) -> FontRequest;
fn light(self) -> FontRequest;
fn normal_weight(self) -> FontRequest;
fn regular(self) -> FontRequest;
fn medium(self) -> FontRequest;
fn semi_bold(self) -> FontRequest;
fn demi_bold(self) -> FontRequest;
fn bold(self) -> FontRequest;
fn extra_bold(self) -> FontRequest;
fn ultra_bold(self) -> FontRequest;
fn black(self) -> FontRequest;
fn heavy(self) -> FontRequest;
fn extra_black(self) -> FontRequest;
fn ultra_black(self) -> FontRequest;
fn weight(self, weight: u16) -> FontRequest;
}
impl<T: Into<FontRequest>> FontWeightExt for T
{
fn thin(self) -> FontRequest
{
self.into() + FontWeight::Thin
}
fn hairline(self) -> FontRequest
{
self.into() + FontWeight::Hairline
}
fn extra_light(self) -> FontRequest
{
self.into() + FontWeight::ExtraLight
}
fn ultra_light(self) -> FontRequest
{
self.into() + FontWeight::UltraLight
}
fn light(self) -> FontRequest
{
self.into() + FontWeight::Light
}
fn normal_weight(self) -> FontRequest
{
self.into() + FontWeight::Normal
}
fn regular(self) -> FontRequest
{
self.into() + FontWeight::Regular
}
fn medium(self) -> FontRequest
{
self.into() + FontWeight::Medium
}
fn semi_bold(self) -> FontRequest
{
self.into() + FontWeight::SemiBold
}
fn demi_bold(self) -> FontRequest
{
self.into() + FontWeight::DemiBold
}
fn bold(self) -> FontRequest
{
self.into() + FontWeight::Bold
}
fn extra_bold(self) -> FontRequest
{
self.into() + FontWeight::ExtraBold
}
fn ultra_bold(self) -> FontRequest
{
self.into() + FontWeight::UltraBold
}
fn black(self) -> FontRequest
{
self.into() + FontWeight::Black
}
fn heavy(self) -> FontRequest
{
self.into() + FontWeight::Heavy
}
fn extra_black(self) -> FontRequest
{
self.into() + FontWeight::ExtraBlack
}
fn ultra_black(self) -> FontRequest
{
self.into() + FontWeight::UltraBlack
}
fn weight(self, weight: u16) -> FontRequest
{
self.into() + FontWeight::Weight(FontWeight::Weight(weight).weight())
}
}
#[derive(Reflect, Default, Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FontAttributes
{
#[reflect(default)]
pub width: FontWidth,
#[reflect(default)]
pub style: FontStyle,
#[reflect(default)]
pub weight: FontWeight,
}
impl FontAttributes
{
pub fn negotiate_eligible_fonts<I>(self, attrs_fn: impl Fn() -> I) -> Option<Self>
where
I: Iterator<Item = FontAttributes>,
{
let width = FontWidth::negotiate(self.width, || (attrs_fn)().map(|a| a.width))?;
let style = FontStyle::negotiate(self.style, || {
(attrs_fn)().filter(|a| a.width == width).map(|a| a.style)
})?;
let weight = FontWeight::negotiate(self.weight, || {
(attrs_fn)()
.filter(|a| (a.width == width) && (a.style == style))
.map(|a| a.weight)
})?;
Some(Self { width, style, weight })
}
}
impl UpdateFontRequest for FontAttributes
{
fn update(self, req: FontRequest) -> FontRequest
{
req + self.width + self.style + self.weight
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FontRequest
{
pub family: FontFamily,
#[reflect(default)]
pub width: FontWidth,
#[reflect(default)]
pub style: FontStyle,
#[reflect(default)]
pub weight: FontWeight,
}
impl FontRequest
{
pub fn new(family: impl Into<FontFamily>) -> Self
{
Self {
family: family.into(),
width: FontWidth::Normal,
style: FontStyle::Normal,
weight: FontWeight::Normal,
}
}
pub const fn new_static(family: &'static str) -> Self
{
Self {
family: FontFamily::new_static(family),
width: FontWidth::Normal,
style: FontStyle::Normal,
weight: FontWeight::Normal,
}
}
pub fn with(
family: impl Into<FontFamily>,
width: impl Into<FontWidth>,
style: impl Into<FontStyle>,
weight: impl Into<FontWeight>,
) -> Self
{
Self {
family: family.into(),
width: width.into(),
style: style.into(),
weight: weight.into(),
}
}
pub fn attributes(&self) -> FontAttributes
{
FontAttributes { width: self.width, style: self.style, weight: self.weight }
}
pub fn set(self, val: impl UpdateFontRequest) -> Self
{
val.update(self)
}
}
impl From<FontFamily> for FontRequest
{
fn from(family: FontFamily) -> Self
{
Self::new(family)
}
}
impl<U: UpdateFontRequest> Add<U> for FontRequest
{
type Output = FontRequest;
fn add(self, val: U) -> Self::Output
{
self.set(val)
}
}
impl<U: UpdateFontRequest> Add<U> for FontFamily
{
type Output = FontRequest;
fn add(self, val: U) -> Self::Output
{
FontRequest::from(self).set(val)
}
}
#[derive(Resource, Debug, Deref, DerefMut)]
pub struct DefaultFont(pub FontRequest);
impl Default for DefaultFont
{
fn default() -> Self
{
if cfg!(feature = "firasans_default") {
Self(FontRequest::new_static("Fira Sans").medium())
} else {
Self(FontRequest::new_static("Bevy"))
}
}
}
pub(crate) struct FontExtPlugin;
impl Plugin for FontExtPlugin
{
fn build(&self, app: &mut App)
{
app.register_type::<FontFamily>()
.register_type::<FontWidth>()
.register_type::<FontStyle>()
.register_type::<FontWeight>()
.register_type::<FontRequest>()
.init_resource::<DefaultFont>()
.add_systems(PreStartup, register_bevy_default_font);
}
}