use std::collections::HashMap;
use bevy::prelude::*;
use crate::render::{NoesisRenderState, NoesisSet};
pub use noesis_runtime::typography::{FontStretch, FontStyle, FontWeight};
#[derive(Clone, Default, Debug, PartialEq)]
pub struct FontStyling {
pub font_size: Option<f32>,
pub font_family: Option<String>,
pub font_weight: Option<FontWeight>,
pub font_style: Option<FontStyle>,
pub font_stretch: Option<FontStretch>,
}
impl FontStyling {
pub(crate) fn is_empty(&self) -> bool {
self.font_size.is_none()
&& self.font_family.is_none()
&& self.font_weight.is_none()
&& self.font_style.is_none()
&& self.font_stretch.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TypographyField {
FontSize,
FontFamily,
FontWeight,
FontStyle,
FontStretch,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TypographyValue {
FontSize(f32),
FontFamily(Option<String>),
FontWeight(FontWeight),
FontStyle(FontStyle),
FontStretch(FontStretch),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypographyWatch {
pub name: String,
pub field: TypographyField,
}
#[derive(Message, Debug, Clone)]
pub struct NoesisTypographyChanged {
pub view: Entity,
pub name: String,
pub value: TypographyValue,
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisTypography {
pub set: HashMap<String, FontStyling>,
pub watch: Vec<TypographyWatch>,
}
impl NoesisTypography {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn font_size(mut self, name: impl Into<String>, size: f32) -> Self {
self.entry(name).font_size = Some(size);
self
}
#[must_use]
pub fn font_family(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
self.entry(name).font_family = Some(source.into());
self
}
#[must_use]
pub fn font_weight(mut self, name: impl Into<String>, weight: FontWeight) -> Self {
self.entry(name).font_weight = Some(weight);
self
}
#[must_use]
pub fn font_style(mut self, name: impl Into<String>, style: FontStyle) -> Self {
self.entry(name).font_style = Some(style);
self
}
#[must_use]
pub fn font_stretch(mut self, name: impl Into<String>, stretch: FontStretch) -> Self {
self.entry(name).font_stretch = Some(stretch);
self
}
#[must_use]
pub fn styling(mut self, name: impl Into<String>, styling: FontStyling) -> Self {
self.set.insert(name.into(), styling);
self
}
#[must_use]
pub fn watch(mut self, name: impl Into<String>, field: TypographyField) -> Self {
self.watch.push(TypographyWatch {
name: name.into(),
field,
});
self
}
pub fn set_font_size(&mut self, name: impl Into<String>, size: f32) {
self.entry(name).font_size = Some(size);
}
pub fn set_font_family(&mut self, name: impl Into<String>, source: impl Into<String>) {
self.entry(name).font_family = Some(source.into());
}
pub fn set_font_weight(&mut self, name: impl Into<String>, weight: FontWeight) {
self.entry(name).font_weight = Some(weight);
}
pub fn set_font_style(&mut self, name: impl Into<String>, style: FontStyle) {
self.entry(name).font_style = Some(style);
}
pub fn set_font_stretch(&mut self, name: impl Into<String>, stretch: FontStretch) {
self.entry(name).font_stretch = Some(stretch);
}
pub fn set_styling(&mut self, name: impl Into<String>, styling: FontStyling) {
self.set.insert(name.into(), styling);
}
pub fn observe(&mut self, name: impl Into<String>, field: TypographyField) {
let watch = TypographyWatch {
name: name.into(),
field,
};
if !self.watch.contains(&watch) {
self.watch.push(watch);
}
}
fn entry(&mut self, name: impl Into<String>) -> &mut FontStyling {
self.set.entry(name.into()).or_default()
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_typography_bridge(
views: Query<(Entity, Ref<NoesisTypography>)>,
state: Option<NonSendMut<NoesisRenderState>>,
mut changed: MessageWriter<NoesisTypographyChanged>,
) {
let Some(mut state) = state else {
return;
};
for (entity, typography) in &views {
if typography.is_changed() || state.scene_rebuilt_this_frame(entity) {
state.apply_typography_for(entity, &typography.set);
}
for (name, value) in state.poll_typography_reads_for(entity, &typography.watch) {
changed.write(NoesisTypographyChanged {
view: entity,
name,
value,
});
}
}
}
pub struct NoesisTypographyPlugin;
impl Plugin for NoesisTypographyPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisTypographyChanged>()
.add_systems(PostUpdate, sync_typography_bridge.in_set(NoesisSet::Apply));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_per_name_styling() {
let t = NoesisTypography::new()
.font_size("Title", 28.0)
.font_family("Title", "#PT Root UI")
.font_weight("Title", FontWeight::Bold)
.font_style("Sub", FontStyle::Italic)
.font_stretch("Sub", FontStretch::Condensed);
let title = t.set.get("Title").expect("Title styling present");
assert_eq!(title.font_size, Some(28.0));
assert_eq!(title.font_family.as_deref(), Some("#PT Root UI"));
assert_eq!(title.font_weight, Some(FontWeight::Bold));
assert_eq!(title.font_style, None);
let sub = t.set.get("Sub").expect("Sub styling present");
assert_eq!(sub.font_style, Some(FontStyle::Italic));
assert_eq!(sub.font_stretch, Some(FontStretch::Condensed));
assert_eq!(sub.font_size, None);
}
#[test]
fn last_write_wins_per_field() {
let t = NoesisTypography::new()
.font_size("Title", 12.0)
.font_size("Title", 24.0);
assert_eq!(t.set.get("Title").unwrap().font_size, Some(24.0));
}
}