1use crate::{Rgba, Vec2};
4use serde::{Deserialize, Serialize};
5
6pub const MAX_TEXT_CONTENT_BYTES: usize = 4_096;
8
9pub const MAX_FONT_FAMILY_BYTES: usize = 256;
11
12pub const MAX_TEXT_GENERATOR_FONT_SIZE: f32 = 1.0;
15
16pub const LINE_HEIGHT_RANGE: std::ops::RangeInclusive<f32> = 0.5..=3.0;
18
19pub const MAX_TEXT_OUTLINE_WIDTH: f32 = 0.5;
21
22pub const MAX_TEXT_PADDING: f32 = 2.0;
24
25#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[serde(deny_unknown_fields)]
36pub struct TextStyle {
37 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub font_family: Option<String>,
45 #[serde(default = "default_font_size")]
49 pub font_size: f32,
50 #[serde(default = "white")]
52 pub color: Rgba,
53 #[serde(default = "default_line_height")]
55 pub line_height: f32,
56 #[serde(default)]
58 pub align: TextAlign,
59 #[serde(default = "default_max_width")]
61 pub max_width: f32,
62 #[serde(default = "default_position")]
65 pub position: Vec2,
66 #[serde(default = "default_anchor")]
69 pub anchor: Vec2,
70 #[serde(default)]
72 pub outline: Option<TextOutline>,
73 #[serde(default)]
75 pub background: Option<TextBackground>,
76}
77
78impl Default for TextStyle {
79 fn default() -> Self {
80 Self {
81 font_family: None,
82 font_size: default_font_size(),
83 color: white(),
84 line_height: default_line_height(),
85 align: TextAlign::default(),
86 max_width: default_max_width(),
87 position: default_position(),
88 anchor: default_anchor(),
89 outline: None,
90 background: None,
91 }
92 }
93}
94
95const fn default_font_size() -> f32 {
96 0.05
97}
98
99const fn white() -> Rgba {
100 Rgba::new(1.0, 1.0, 1.0, 1.0)
101}
102
103const fn default_line_height() -> f32 {
104 1.2
105}
106
107const fn default_max_width() -> f32 {
108 0.9
109}
110
111const fn default_position() -> Vec2 {
112 Vec2::new(0.5, 0.92)
113}
114
115const fn default_anchor() -> Vec2 {
116 Vec2::new(0.5, 1.0)
117}
118
119#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
122#[serde(rename_all = "snake_case")]
123pub enum TextAlign {
124 Start,
126 #[default]
128 Center,
129 End,
131}
132
133#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
136#[serde(deny_unknown_fields)]
137pub struct TextOutline {
138 pub color: Rgba,
140 pub width: f32,
142}
143
144#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
147#[serde(deny_unknown_fields)]
148pub struct TextBackground {
149 pub color: Rgba,
151 #[serde(default)]
154 pub padding: f32,
155}
156
157impl TextStyle {
158 pub(crate) fn is_valid(&self, max_font_size: f32) -> bool {
161 let finite = |value: Vec2| value.x.is_finite() && value.y.is_finite();
162 self.font_family
163 .as_ref()
164 .is_none_or(|family| !family.is_empty() && family.len() <= MAX_FONT_FAMILY_BYTES)
165 && self.font_size > 0.0
166 && self.font_size <= max_font_size
167 && self.color.is_valid()
168 && LINE_HEIGHT_RANGE.contains(&self.line_height)
169 && self.max_width > 0.0
170 && self.max_width <= 1.0
171 && finite(self.position)
172 && finite(self.anchor)
173 && self.outline.is_none_or(|outline| {
174 outline.color.is_valid()
175 && outline.width > 0.0
176 && outline.width <= MAX_TEXT_OUTLINE_WIDTH
177 })
178 && self.background.is_none_or(|background| {
179 background.color.is_valid()
180 && (0.0..=MAX_TEXT_PADDING).contains(&background.padding)
181 })
182 }
183}