Skip to main content

azul_css/props/style/
exclusion.rs

1//! Azul-specific CSS properties for advanced layout features
2//!
3//! Defines `StyleExclusionMargin` (spacing between text and shape exclusions)
4//! and `StyleHyphenationLanguage` (BCP 47 language code for automatic hyphenation).
5
6use std::num::ParseFloatError;
7
8#[cfg(feature = "parser")]
9use crate::macros::*;
10use crate::{
11    corety::AzString,
12    codegen::format::FormatAsRustCode,
13    props::{
14        basic::{length::parse_float_value, FloatValue},
15        formatter::{FormatAsCssValue, PrintAsCssValue},
16    },
17};
18
19/// `-azul-exclusion-margin` property: defines margin around shape exclusions
20///
21/// This property controls the spacing between text and shapes that text flows around.
22/// It's similar to `shape-margin` but specifically for exclusions (text wrapping).
23///
24/// # Example
25/// ```css
26/// .element {
27///     -azul-exclusion-margin: 10.5;
28/// }
29/// ```
30#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31#[repr(C)]
32pub struct StyleExclusionMargin {
33    pub inner: FloatValue,
34}
35
36impl Default for StyleExclusionMargin {
37    fn default() -> Self {
38        Self {
39            inner: FloatValue::const_new(0),
40        }
41    }
42}
43
44impl StyleExclusionMargin {
45    #[must_use] pub const fn is_initial(&self) -> bool {
46        self.inner.number == 0
47    }
48}
49
50impl PrintAsCssValue for StyleExclusionMargin {
51    fn print_as_css_value(&self) -> String {
52        format!("{}", self.inner.get())
53    }
54}
55
56impl FormatAsCssValue for StyleExclusionMargin {
57    fn format_as_css_value(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.inner.get())
59    }
60}
61
62impl FormatAsRustCode for StyleExclusionMargin {
63    fn format_as_rust_code(&self, _tabs: usize) -> String {
64        format!(
65            "StyleExclusionMargin {{ inner: FloatValue::const_new({}) }}",
66            self.inner.get()
67        )
68    }
69}
70
71#[cfg(feature = "parser")]
72#[derive(Clone, PartialEq, Eq)]
73pub enum StyleExclusionMarginParseError {
74    FloatValue(ParseFloatError),
75}
76
77#[cfg(feature = "parser")]
78impl_debug_as_display!(StyleExclusionMarginParseError);
79
80#[cfg(feature = "parser")]
81impl_display! { StyleExclusionMarginParseError, {
82    FloatValue(e) => format!("Invalid -azul-exclusion-margin value: {}", e),
83}}
84
85#[cfg(feature = "parser")]
86impl_from!(ParseFloatError, StyleExclusionMarginParseError::FloatValue);
87
88#[cfg(feature = "parser")]
89#[derive(Debug, Clone, PartialEq, Eq)]
90#[repr(C, u8)]
91pub enum StyleExclusionMarginParseErrorOwned {
92    FloatValue(AzString),
93}
94
95#[cfg(feature = "parser")]
96impl StyleExclusionMarginParseError {
97    #[must_use] pub fn to_contained(&self) -> StyleExclusionMarginParseErrorOwned {
98        match self {
99            Self::FloatValue(e) => {
100                StyleExclusionMarginParseErrorOwned::FloatValue(format!("{e}").into())
101            }
102        }
103    }
104}
105
106#[cfg(feature = "parser")]
107impl StyleExclusionMarginParseErrorOwned {
108    #[must_use] pub fn to_shared(&self) -> StyleExclusionMarginParseError {
109        match self {
110            Self::FloatValue(_) => {
111                // ParseFloatError can't be reconstructed from its display string,
112                // so we create one by parsing a known-invalid string
113                StyleExclusionMarginParseError::FloatValue("".parse::<f32>().unwrap_err())
114            }
115        }
116    }
117}
118
119#[cfg(feature = "parser")]
120/// # Errors
121///
122/// Returns an error if `input` is not a valid CSS `exclusion-margin` value.
123pub fn parse_style_exclusion_margin(
124    input: &str,
125) -> Result<StyleExclusionMargin, StyleExclusionMarginParseError> {
126    parse_float_value(input)
127        .map(|inner| StyleExclusionMargin { inner })
128        .map_err(StyleExclusionMarginParseError::FloatValue)
129}
130
131/// `-azul-hyphenation-language` property: specifies language for hyphenation
132///
133/// This property defines the language code (BCP 47 format) used for automatic
134/// hyphenation. Examples: "en-US", "de-DE", "fr-FR"
135///
136/// # Example
137/// ```css
138/// .element {
139///     -azul-hyphenation-language: "en-US";
140/// }
141/// ```
142#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
143#[repr(C)]
144pub struct StyleHyphenationLanguage {
145    pub inner: AzString,
146}
147
148impl Default for StyleHyphenationLanguage {
149    fn default() -> Self {
150        Self {
151            inner: AzString::from_const_str("en-US"),
152        }
153    }
154}
155
156impl StyleHyphenationLanguage {
157    #[must_use] pub fn is_initial(&self) -> bool {
158        self.inner.as_str() == "en-US"
159    }
160}
161
162impl PrintAsCssValue for StyleHyphenationLanguage {
163    fn print_as_css_value(&self) -> String {
164        format!("\"{}\"", self.inner.as_str())
165    }
166}
167
168impl FormatAsCssValue for StyleHyphenationLanguage {
169    fn format_as_css_value(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        write!(f, "\"{}\"", self.inner.as_str())
171    }
172}
173
174impl FormatAsRustCode for StyleHyphenationLanguage {
175    fn format_as_rust_code(&self, _tabs: usize) -> String {
176        format!(
177            "StyleHyphenationLanguage {{ inner: AzString::from_const_str(\"{}\") }}",
178            self.inner.as_str()
179        )
180    }
181}
182
183#[cfg(feature = "parser")]
184#[derive(Clone, PartialEq, Eq)]
185pub enum StyleHyphenationLanguageParseError {
186    InvalidString(String),
187}
188
189#[cfg(feature = "parser")]
190impl_debug_as_display!(StyleHyphenationLanguageParseError);
191
192#[cfg(feature = "parser")]
193impl_display! { StyleHyphenationLanguageParseError, {
194    InvalidString(e) => format!("Invalid -azul-hyphenation-language value: {}", e),
195}}
196
197#[cfg(feature = "parser")]
198#[derive(Debug, Clone, PartialEq, Eq)]
199#[repr(C, u8)]
200pub enum StyleHyphenationLanguageParseErrorOwned {
201    InvalidString(AzString),
202}
203
204#[cfg(feature = "parser")]
205impl StyleHyphenationLanguageParseError {
206    #[must_use] pub fn to_contained(&self) -> StyleHyphenationLanguageParseErrorOwned {
207        match self {
208            Self::InvalidString(e) => {
209                StyleHyphenationLanguageParseErrorOwned::InvalidString(e.clone().into())
210            }
211        }
212    }
213}
214
215#[cfg(feature = "parser")]
216impl StyleHyphenationLanguageParseErrorOwned {
217    #[must_use] pub fn to_shared(&self) -> StyleHyphenationLanguageParseError {
218        match self {
219            Self::InvalidString(e) => StyleHyphenationLanguageParseError::InvalidString(e.to_string()),
220        }
221    }
222}
223
224#[cfg(feature = "parser")]
225/// # Errors
226///
227/// Returns an error if `input` is not a valid CSS `hyphenation-language` value.
228pub fn parse_style_hyphenation_language(
229    input: &str,
230) -> Result<StyleHyphenationLanguage, StyleHyphenationLanguageParseError> {
231    // Remove quotes if present
232    let trimmed = input.trim();
233    let unquoted = if (trimmed.starts_with('"') && trimmed.ends_with('"'))
234        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
235    {
236        &trimmed[1..trimmed.len() - 1]
237    } else {
238        trimmed
239    };
240
241    // Basic BCP 47 validation: non-empty, ASCII alphanumeric + hyphens, no leading/trailing hyphens
242    if unquoted.is_empty()
243        || !unquoted.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
244        || unquoted.starts_with('-')
245        || unquoted.ends_with('-')
246    {
247        return Err(StyleHyphenationLanguageParseError::InvalidString(
248            unquoted.to_string(),
249        ));
250    }
251
252    Ok(StyleHyphenationLanguage {
253        inner: AzString::from_string(unquoted.to_string()),
254    })
255}
256
257#[cfg(test)]
258mod tests {
259    // Tests assert that parsed values equal the exact source literals.
260    #![allow(clippy::float_cmp)]
261    use super::*;
262
263    #[test]
264    fn test_parse_exclusion_margin() {
265        let margin = parse_style_exclusion_margin("10.5").unwrap();
266        assert_eq!(margin.inner.get(), 10.5);
267
268        let margin = parse_style_exclusion_margin("0").unwrap();
269        assert_eq!(margin.inner.get(), 0.0);
270    }
271
272    #[test]
273    fn test_parse_hyphenation_language() {
274        let lang = parse_style_hyphenation_language("\"en-US\"").unwrap();
275        assert_eq!(lang.inner.as_str(), "en-US");
276
277        let lang = parse_style_hyphenation_language("'de-DE'").unwrap();
278        assert_eq!(lang.inner.as_str(), "de-DE");
279
280        let lang = parse_style_hyphenation_language("fr-FR").unwrap();
281        assert_eq!(lang.inner.as_str(), "fr-FR");
282
283        let lang = parse_style_hyphenation_language("zh").unwrap();
284        assert_eq!(lang.inner.as_str(), "zh");
285
286        let lang = parse_style_hyphenation_language("sr-Latn-RS").unwrap();
287        assert_eq!(lang.inner.as_str(), "sr-Latn-RS");
288
289        // Double hyphen is permitted by the current ASCII/format rules.
290        let lang = parse_style_hyphenation_language("en--US").unwrap();
291        assert_eq!(lang.inner.as_str(), "en--US");
292    }
293
294    #[test]
295    fn test_parse_hyphenation_language_invalid() {
296        assert!(matches!(
297            parse_style_hyphenation_language(""),
298            Err(StyleHyphenationLanguageParseError::InvalidString(_))
299        ));
300        assert!(matches!(
301            parse_style_hyphenation_language("-en"),
302            Err(StyleHyphenationLanguageParseError::InvalidString(_))
303        ));
304        assert!(matches!(
305            parse_style_hyphenation_language("en-"),
306            Err(StyleHyphenationLanguageParseError::InvalidString(_))
307        ));
308        assert!(matches!(
309            parse_style_hyphenation_language("en_US"),
310            Err(StyleHyphenationLanguageParseError::InvalidString(_))
311        ));
312        assert!(matches!(
313            parse_style_hyphenation_language("日本語"),
314            Err(StyleHyphenationLanguageParseError::InvalidString(_))
315        ));
316    }
317
318    #[test]
319    fn test_exclusion_margin_default() {
320        let margin = StyleExclusionMargin::default();
321        assert_eq!(margin.inner.get(), 0.0);
322        assert!(margin.is_initial());
323    }
324
325    #[test]
326    fn test_hyphenation_language_default() {
327        let lang = StyleHyphenationLanguage::default();
328        assert_eq!(lang.inner.as_str(), "en-US");
329    }
330}