1use crate::defaults::FloatType;
9use crate::error::{Error, Result};
10
11#[derive(Clone, Debug, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub enum StyleValue {
19 Whole(i64),
21 Decimal(FloatType),
23 Text(String),
25}
26
27impl StyleValue {
28 pub fn read(text: &str) -> Self {
30 let Ok(value) = text.trim().parse::<FloatType>() else {
31 return Self::Text(text.to_string());
32 };
33 let whole = value.round();
34 if value.is_finite() && (whole - value).abs() <= 1e-6 && whole.abs() < 9.0e15 {
35 Self::Whole(whole as i64)
36 } else {
37 Self::Decimal(value)
38 }
39 }
40
41 pub fn number(&self) -> Option<FloatType> {
43 match self {
44 Self::Whole(whole) => Some(*whole as FloatType),
45 Self::Decimal(value) => Some(*value),
46 Self::Text(_) => None,
47 }
48 }
49}
50
51impl std::fmt::Display for StyleValue {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Self::Whole(whole) => write!(f, "{whole}"),
56 Self::Decimal(value) => write!(f, "{value}"),
57 Self::Text(text) => f.write_str(text),
58 }
59 }
60}
61
62fn format_error(message: impl Into<String>) -> Error {
63 Error::Text(message.into())
64}
65
66#[derive(Clone, Debug, Default, PartialEq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71pub struct TextStyle {
72 pub absolute_x: Option<StyleValue>,
75 pub absolute_y: Option<StyleValue>,
77 pub relative_x: Option<StyleValue>,
79 pub relative_y: Option<StyleValue>,
81 pub font_family: Option<Vec<String>>,
83 font_style: Option<String>,
84 pub font_size: Option<StyleValue>,
86 font_weight: Option<String>,
87 pub color: Option<String>,
89 justify: Option<String>,
90 align_horizontal: Option<String>,
91 align_vertical: Option<String>,
92}
93
94impl TextStyle {
95 pub fn font_style(&self) -> Option<&str> {
97 self.font_style.as_deref()
98 }
99
100 pub fn set_font_style(&mut self, value: Option<&str>) -> Result<()> {
106 self.font_style = checked(
107 value,
108 &["italic", "normal", "bold", "bolditalic"],
109 true,
110 |value| format!("Not a supported fontStyle: {value:?}"),
111 )?;
112 Ok(())
113 }
114
115 pub fn font_weight(&self) -> Option<&str> {
117 self.font_weight.as_deref()
118 }
119
120 pub fn set_font_weight(&mut self, value: Option<&str>) -> Result<()> {
126 self.font_weight = checked(value, &["normal", "bold"], true, |value| {
127 format!("Not a supported fontWeight: {value}")
128 })?;
129 Ok(())
130 }
131
132 pub fn justify(&self) -> Option<&str> {
134 self.justify.as_deref()
135 }
136
137 pub fn set_justify(&mut self, value: Option<&str>) -> Result<()> {
143 self.justify = checked(value, &["left", "center", "right", "full"], true, |value| {
144 format!("Not a supported justification: {value:?}")
145 })?;
146 Ok(())
147 }
148
149 pub fn align_horizontal(&self) -> Option<&str> {
152 self.align_horizontal.as_deref()
153 }
154
155 pub fn set_align_horizontal(&mut self, value: Option<&str>) -> Result<()> {
161 self.align_horizontal = checked(value, &["left", "right", "center"], false, |value| {
162 format!("Invalid horizontal align: {value:?}")
163 })?;
164 Ok(())
165 }
166
167 pub fn align_vertical(&self) -> Option<&str> {
170 self.align_vertical.as_deref()
171 }
172
173 pub fn set_align_vertical(&mut self, value: Option<&str>) -> Result<()> {
179 self.align_vertical = checked(
180 value,
181 &["top", "middle", "bottom", "baseline"],
182 false,
183 |value| format!("Invalid vertical align: {value:?}"),
184 )?;
185 Ok(())
186 }
187}
188
189fn checked(
192 value: Option<&str>,
193 allowed: &[&str],
194 lowercase: bool,
195 message: impl Fn(&str) -> String,
196) -> Result<Option<String>> {
197 let Some(value) = value else {
198 return Ok(None);
199 };
200 let read = if lowercase {
201 value.to_lowercase()
202 } else {
203 value.to_string()
204 };
205 if allowed.contains(&read.as_str()) {
206 Ok(Some(read))
207 } else {
208 Err(format_error(message(value)))
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn a_value_is_read_as_music21_reads_it() {
218 assert_eq!(StyleValue::read("12"), StyleValue::Whole(12));
219 assert_eq!(StyleValue::read("12.0000001"), StyleValue::Whole(12));
220 assert_eq!(StyleValue::read("12.5"), StyleValue::Decimal(12.5));
221 assert_eq!(
222 StyleValue::read("medium"),
223 StyleValue::Text("medium".to_string())
224 );
225 assert_eq!(StyleValue::Decimal(12.5).to_string(), "12.5");
226 }
227
228 #[test]
229 fn styles_music21_does_not_name_are_refused() {
230 let mut style = TextStyle::default();
231 style.set_justify(Some("Center")).unwrap();
232 assert_eq!(style.justify(), Some("center"));
233 assert!(style.set_justify(Some("middle")).is_err());
234 assert!(style.set_align_horizontal(Some("Center")).is_err());
235 style.set_align_vertical(Some("baseline")).unwrap();
236 assert!(style.set_font_style(Some("oblique")).is_err());
237 style.set_font_weight(Some("BOLD")).unwrap();
238 assert_eq!(style.font_weight(), Some("bold"));
239 }
240}