azul-css 0.0.8

Common datatypes used for styling applications using the Azul desktop GUI framework
Documentation
//! Azul-specific CSS properties for advanced layout features
//!
//! Defines `StyleExclusionMargin` (spacing between text and shape exclusions)
//! and `StyleHyphenationLanguage` (BCP 47 language code for automatic hyphenation).

use std::num::ParseFloatError;

#[cfg(feature = "parser")]
use crate::macros::*;
use crate::{
    corety::AzString,
    format_rust_code::FormatAsRustCode,
    props::{
        basic::{length::parse_float_value, FloatValue},
        formatter::{FormatAsCssValue, PrintAsCssValue},
    },
};

/// `-azul-exclusion-margin` property: defines margin around shape exclusions
///
/// This property controls the spacing between text and shapes that text flows around.
/// It's similar to `shape-margin` but specifically for exclusions (text wrapping).
///
/// # Example
/// ```css
/// .element {
///     -azul-exclusion-margin: 10.5;
/// }
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct StyleExclusionMargin {
    pub inner: FloatValue,
}

impl Default for StyleExclusionMargin {
    fn default() -> Self {
        Self {
            inner: FloatValue::const_new(0),
        }
    }
}

impl StyleExclusionMargin {
    pub fn is_initial(&self) -> bool {
        self.inner.number == 0
    }
}

impl PrintAsCssValue for StyleExclusionMargin {
    fn print_as_css_value(&self) -> String {
        format!("{}", self.inner.get())
    }
}

impl FormatAsCssValue for StyleExclusionMargin {
    fn format_as_css_value(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.inner.get())
    }
}

impl FormatAsRustCode for StyleExclusionMargin {
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!(
            "StyleExclusionMargin {{ inner: FloatValue::const_new({}) }}",
            self.inner.get()
        )
    }
}

#[cfg(feature = "parser")]
#[derive(Clone, PartialEq)]
pub enum StyleExclusionMarginParseError {
    FloatValue(ParseFloatError),
}

#[cfg(feature = "parser")]
impl_debug_as_display!(StyleExclusionMarginParseError);

#[cfg(feature = "parser")]
impl_display! { StyleExclusionMarginParseError, {
    FloatValue(e) => format!("Invalid -azul-exclusion-margin value: {}", e),
}}

#[cfg(feature = "parser")]
impl_from!(ParseFloatError, StyleExclusionMarginParseError::FloatValue);

#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum StyleExclusionMarginParseErrorOwned {
    FloatValue(AzString),
}

#[cfg(feature = "parser")]
impl StyleExclusionMarginParseError {
    pub fn to_contained(&self) -> StyleExclusionMarginParseErrorOwned {
        match self {
            Self::FloatValue(e) => {
                StyleExclusionMarginParseErrorOwned::FloatValue(format!("{}", e).into())
            }
        }
    }
}

#[cfg(feature = "parser")]
impl StyleExclusionMarginParseErrorOwned {
    pub fn to_shared(&self) -> StyleExclusionMarginParseError {
        match self {
            Self::FloatValue(_) => {
                // ParseFloatError can't be reconstructed from its display string,
                // so we create one by parsing a known-invalid string
                StyleExclusionMarginParseError::FloatValue("".parse::<f32>().unwrap_err())
            }
        }
    }
}

#[cfg(feature = "parser")]
pub fn parse_style_exclusion_margin(
    input: &str,
) -> Result<StyleExclusionMargin, StyleExclusionMarginParseError> {
    parse_float_value(input)
        .map(|inner| StyleExclusionMargin { inner })
        .map_err(StyleExclusionMarginParseError::FloatValue)
}

/// `-azul-hyphenation-language` property: specifies language for hyphenation
///
/// This property defines the language code (BCP 47 format) used for automatic
/// hyphenation. Examples: "en-US", "de-DE", "fr-FR"
///
/// # Example
/// ```css
/// .element {
///     -azul-hyphenation-language: "en-US";
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct StyleHyphenationLanguage {
    pub inner: AzString,
}

impl Default for StyleHyphenationLanguage {
    fn default() -> Self {
        Self {
            inner: AzString::from_const_str("en-US"),
        }
    }
}

impl StyleHyphenationLanguage {
    pub fn is_initial(&self) -> bool {
        self.inner.as_str() == "en-US"
    }
}

impl PrintAsCssValue for StyleHyphenationLanguage {
    fn print_as_css_value(&self) -> String {
        format!("\"{}\"", self.inner.as_str())
    }
}

impl FormatAsCssValue for StyleHyphenationLanguage {
    fn format_as_css_value(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "\"{}\"", self.inner.as_str())
    }
}

impl FormatAsRustCode for StyleHyphenationLanguage {
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!(
            "StyleHyphenationLanguage {{ inner: AzString::from_const_str(\"{}\") }}",
            self.inner.as_str()
        )
    }
}

#[cfg(feature = "parser")]
#[derive(Clone, PartialEq)]
pub enum StyleHyphenationLanguageParseError {
    InvalidString(String),
}

#[cfg(feature = "parser")]
impl_debug_as_display!(StyleHyphenationLanguageParseError);

#[cfg(feature = "parser")]
impl_display! { StyleHyphenationLanguageParseError, {
    InvalidString(e) => format!("Invalid -azul-hyphenation-language value: {}", e),
}}

#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum StyleHyphenationLanguageParseErrorOwned {
    InvalidString(AzString),
}

#[cfg(feature = "parser")]
impl StyleHyphenationLanguageParseError {
    pub fn to_contained(&self) -> StyleHyphenationLanguageParseErrorOwned {
        match self {
            Self::InvalidString(e) => {
                StyleHyphenationLanguageParseErrorOwned::InvalidString(e.clone().into())
            }
        }
    }
}

#[cfg(feature = "parser")]
impl StyleHyphenationLanguageParseErrorOwned {
    pub fn to_shared(&self) -> StyleHyphenationLanguageParseError {
        match self {
            Self::InvalidString(e) => StyleHyphenationLanguageParseError::InvalidString(e.to_string()),
        }
    }
}

#[cfg(feature = "parser")]
pub fn parse_style_hyphenation_language(
    input: &str,
) -> Result<StyleHyphenationLanguage, StyleHyphenationLanguageParseError> {
    // Remove quotes if present
    let trimmed = input.trim();
    let unquoted = if (trimmed.starts_with('"') && trimmed.ends_with('"'))
        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
    {
        &trimmed[1..trimmed.len() - 1]
    } else {
        trimmed
    };

    // Basic BCP 47 validation: non-empty, ASCII alphanumeric + hyphens, no leading/trailing hyphens
    if unquoted.is_empty()
        || !unquoted.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
        || unquoted.starts_with('-')
        || unquoted.ends_with('-')
    {
        return Err(StyleHyphenationLanguageParseError::InvalidString(
            unquoted.to_string(),
        ));
    }

    Ok(StyleHyphenationLanguage {
        inner: AzString::from_string(unquoted.to_string()),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_exclusion_margin() {
        let margin = parse_style_exclusion_margin("10.5").unwrap();
        assert_eq!(margin.inner.get(), 10.5);

        let margin = parse_style_exclusion_margin("0").unwrap();
        assert_eq!(margin.inner.get(), 0.0);
    }

    #[test]
    fn test_parse_hyphenation_language() {
        let lang = parse_style_hyphenation_language("\"en-US\"").unwrap();
        assert_eq!(lang.inner.as_str(), "en-US");

        let lang = parse_style_hyphenation_language("'de-DE'").unwrap();
        assert_eq!(lang.inner.as_str(), "de-DE");

        let lang = parse_style_hyphenation_language("fr-FR").unwrap();
        assert_eq!(lang.inner.as_str(), "fr-FR");

        let lang = parse_style_hyphenation_language("zh").unwrap();
        assert_eq!(lang.inner.as_str(), "zh");

        let lang = parse_style_hyphenation_language("sr-Latn-RS").unwrap();
        assert_eq!(lang.inner.as_str(), "sr-Latn-RS");

        // Double hyphen is permitted by the current ASCII/format rules.
        let lang = parse_style_hyphenation_language("en--US").unwrap();
        assert_eq!(lang.inner.as_str(), "en--US");
    }

    #[test]
    fn test_parse_hyphenation_language_invalid() {
        assert!(matches!(
            parse_style_hyphenation_language(""),
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
        ));
        assert!(matches!(
            parse_style_hyphenation_language("-en"),
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
        ));
        assert!(matches!(
            parse_style_hyphenation_language("en-"),
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
        ));
        assert!(matches!(
            parse_style_hyphenation_language("en_US"),
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
        ));
        assert!(matches!(
            parse_style_hyphenation_language("日本語"),
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
        ));
    }

    #[test]
    fn test_exclusion_margin_default() {
        let margin = StyleExclusionMargin::default();
        assert_eq!(margin.inner.get(), 0.0);
        assert!(margin.is_initial());
    }

    #[test]
    fn test_hyphenation_language_default() {
        let lang = StyleHyphenationLanguage::default();
        assert_eq!(lang.inner.as_str(), "en-US");
    }
}