Skip to main content

azul_css/props/style/
azul_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    format_rust_code::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    pub 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)]
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)]
90#[repr(C, u8)]
91pub enum StyleExclusionMarginParseErrorOwned {
92    FloatValue(AzString),
93}
94
95#[cfg(feature = "parser")]
96impl StyleExclusionMarginParseError {
97    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    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")]
120pub fn parse_style_exclusion_margin(
121    input: &str,
122) -> Result<StyleExclusionMargin, StyleExclusionMarginParseError> {
123    parse_float_value(input)
124        .map(|inner| StyleExclusionMargin { inner })
125        .map_err(StyleExclusionMarginParseError::FloatValue)
126}
127
128/// `-azul-hyphenation-language` property: specifies language for hyphenation
129///
130/// This property defines the language code (BCP 47 format) used for automatic
131/// hyphenation. Examples: "en-US", "de-DE", "fr-FR"
132///
133/// # Example
134/// ```css
135/// .element {
136///     -azul-hyphenation-language: "en-US";
137/// }
138/// ```
139#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
140#[repr(C)]
141pub struct StyleHyphenationLanguage {
142    pub inner: AzString,
143}
144
145impl Default for StyleHyphenationLanguage {
146    fn default() -> Self {
147        Self {
148            inner: AzString::from_const_str("en-US"),
149        }
150    }
151}
152
153impl StyleHyphenationLanguage {
154    pub fn is_initial(&self) -> bool {
155        self.inner.as_str() == "en-US"
156    }
157}
158
159impl PrintAsCssValue for StyleHyphenationLanguage {
160    fn print_as_css_value(&self) -> String {
161        format!("\"{}\"", self.inner.as_str())
162    }
163}
164
165impl FormatAsCssValue for StyleHyphenationLanguage {
166    fn format_as_css_value(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
167        write!(f, "\"{}\"", self.inner.as_str())
168    }
169}
170
171impl FormatAsRustCode for StyleHyphenationLanguage {
172    fn format_as_rust_code(&self, _tabs: usize) -> String {
173        format!(
174            "StyleHyphenationLanguage {{ inner: AzString::from_const_str(\"{}\") }}",
175            self.inner.as_str()
176        )
177    }
178}
179
180#[cfg(feature = "parser")]
181#[derive(Clone, PartialEq)]
182pub enum StyleHyphenationLanguageParseError {
183    InvalidString(String),
184}
185
186#[cfg(feature = "parser")]
187impl_debug_as_display!(StyleHyphenationLanguageParseError);
188
189#[cfg(feature = "parser")]
190impl_display! { StyleHyphenationLanguageParseError, {
191    InvalidString(e) => format!("Invalid -azul-hyphenation-language value: {}", e),
192}}
193
194#[cfg(feature = "parser")]
195#[derive(Debug, Clone, PartialEq)]
196#[repr(C, u8)]
197pub enum StyleHyphenationLanguageParseErrorOwned {
198    InvalidString(AzString),
199}
200
201#[cfg(feature = "parser")]
202impl StyleHyphenationLanguageParseError {
203    pub fn to_contained(&self) -> StyleHyphenationLanguageParseErrorOwned {
204        match self {
205            Self::InvalidString(e) => {
206                StyleHyphenationLanguageParseErrorOwned::InvalidString(e.clone().into())
207            }
208        }
209    }
210}
211
212#[cfg(feature = "parser")]
213impl StyleHyphenationLanguageParseErrorOwned {
214    pub fn to_shared(&self) -> StyleHyphenationLanguageParseError {
215        match self {
216            Self::InvalidString(e) => StyleHyphenationLanguageParseError::InvalidString(e.to_string()),
217        }
218    }
219}
220
221#[cfg(feature = "parser")]
222pub fn parse_style_hyphenation_language(
223    input: &str,
224) -> Result<StyleHyphenationLanguage, StyleHyphenationLanguageParseError> {
225    // Remove quotes if present
226    let trimmed = input.trim();
227    let unquoted = if (trimmed.starts_with('"') && trimmed.ends_with('"'))
228        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
229    {
230        &trimmed[1..trimmed.len() - 1]
231    } else {
232        trimmed
233    };
234
235    // Basic BCP 47 validation: non-empty, ASCII alphanumeric + hyphens, no leading/trailing hyphens
236    if unquoted.is_empty()
237        || !unquoted.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
238        || unquoted.starts_with('-')
239        || unquoted.ends_with('-')
240    {
241        return Err(StyleHyphenationLanguageParseError::InvalidString(
242            unquoted.to_string(),
243        ));
244    }
245
246    Ok(StyleHyphenationLanguage {
247        inner: AzString::from_string(unquoted.to_string()),
248    })
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_parse_exclusion_margin() {
257        let margin = parse_style_exclusion_margin("10.5").unwrap();
258        assert_eq!(margin.inner.get(), 10.5);
259
260        let margin = parse_style_exclusion_margin("0").unwrap();
261        assert_eq!(margin.inner.get(), 0.0);
262    }
263
264    #[test]
265    fn test_parse_hyphenation_language() {
266        let lang = parse_style_hyphenation_language("\"en-US\"").unwrap();
267        assert_eq!(lang.inner.as_str(), "en-US");
268
269        let lang = parse_style_hyphenation_language("'de-DE'").unwrap();
270        assert_eq!(lang.inner.as_str(), "de-DE");
271
272        let lang = parse_style_hyphenation_language("fr-FR").unwrap();
273        assert_eq!(lang.inner.as_str(), "fr-FR");
274
275        let lang = parse_style_hyphenation_language("zh").unwrap();
276        assert_eq!(lang.inner.as_str(), "zh");
277
278        let lang = parse_style_hyphenation_language("sr-Latn-RS").unwrap();
279        assert_eq!(lang.inner.as_str(), "sr-Latn-RS");
280
281        // Double hyphen is permitted by the current ASCII/format rules.
282        let lang = parse_style_hyphenation_language("en--US").unwrap();
283        assert_eq!(lang.inner.as_str(), "en--US");
284    }
285
286    #[test]
287    fn test_parse_hyphenation_language_invalid() {
288        assert!(matches!(
289            parse_style_hyphenation_language(""),
290            Err(StyleHyphenationLanguageParseError::InvalidString(_))
291        ));
292        assert!(matches!(
293            parse_style_hyphenation_language("-en"),
294            Err(StyleHyphenationLanguageParseError::InvalidString(_))
295        ));
296        assert!(matches!(
297            parse_style_hyphenation_language("en-"),
298            Err(StyleHyphenationLanguageParseError::InvalidString(_))
299        ));
300        assert!(matches!(
301            parse_style_hyphenation_language("en_US"),
302            Err(StyleHyphenationLanguageParseError::InvalidString(_))
303        ));
304        assert!(matches!(
305            parse_style_hyphenation_language("日本語"),
306            Err(StyleHyphenationLanguageParseError::InvalidString(_))
307        ));
308    }
309
310    #[test]
311    fn test_exclusion_margin_default() {
312        let margin = StyleExclusionMargin::default();
313        assert_eq!(margin.inner.get(), 0.0);
314        assert!(margin.is_initial());
315    }
316
317    #[test]
318    fn test_hyphenation_language_default() {
319        let lang = StyleHyphenationLanguage::default();
320        assert_eq!(lang.inner.as_str(), "en-US");
321    }
322}