Lines
65.32 %
Functions
15.58 %
Branches
100 %
//! 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()
)
#[derive(Clone, PartialEq)]
pub enum StyleExclusionMarginParseError {
FloatValue(ParseFloatError),
impl_debug_as_display!(StyleExclusionMarginParseError);
impl_display! { StyleExclusionMarginParseError, {
FloatValue(e) => format!("Invalid -azul-exclusion-margin value: {}", e),
}}
impl_from!(ParseFloatError, StyleExclusionMarginParseError::FloatValue);
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum StyleExclusionMarginParseErrorOwned {
FloatValue(AzString),
impl StyleExclusionMarginParseError {
pub fn to_contained(&self) -> StyleExclusionMarginParseErrorOwned {
match self {
Self::FloatValue(e) => {
StyleExclusionMarginParseErrorOwned::FloatValue(format!("{}", e).into())
impl StyleExclusionMarginParseErrorOwned {
pub fn to_shared(&self) -> StyleExclusionMarginParseError {
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())
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"
/// -azul-hyphenation-language: "en-US";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StyleHyphenationLanguage {
pub inner: AzString,
impl Default for StyleHyphenationLanguage {
inner: AzString::from_const_str("en-US"),
impl StyleHyphenationLanguage {
self.inner.as_str() == "en-US"
impl PrintAsCssValue for StyleHyphenationLanguage {
format!("\"{}\"", self.inner.as_str())
impl FormatAsCssValue for StyleHyphenationLanguage {
write!(f, "\"{}\"", self.inner.as_str())
impl FormatAsRustCode for StyleHyphenationLanguage {
"StyleHyphenationLanguage {{ inner: AzString::from_const_str(\"{}\") }}",
self.inner.as_str()
pub enum StyleHyphenationLanguageParseError {
InvalidString(String),
impl_debug_as_display!(StyleHyphenationLanguageParseError);
impl_display! { StyleHyphenationLanguageParseError, {
InvalidString(e) => format!("Invalid -azul-hyphenation-language value: {}", e),
pub enum StyleHyphenationLanguageParseErrorOwned {
InvalidString(AzString),
impl StyleHyphenationLanguageParseError {
pub fn to_contained(&self) -> StyleHyphenationLanguageParseErrorOwned {
Self::InvalidString(e) => {
StyleHyphenationLanguageParseErrorOwned::InvalidString(e.clone().into())
impl StyleHyphenationLanguageParseErrorOwned {
pub fn to_shared(&self) -> StyleHyphenationLanguageParseError {
Self::InvalidString(e) => StyleHyphenationLanguageParseError::InvalidString(e.to_string()),
pub fn parse_style_hyphenation_language(
) -> 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);
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");
fn test_parse_hyphenation_language_invalid() {
assert!(matches!(
parse_style_hyphenation_language(""),
Err(StyleHyphenationLanguageParseError::InvalidString(_))
parse_style_hyphenation_language("-en"),
parse_style_hyphenation_language("en-"),
parse_style_hyphenation_language("en_US"),
parse_style_hyphenation_language("日本語"),
fn test_exclusion_margin_default() {
let margin = StyleExclusionMargin::default();
assert!(margin.is_initial());
fn test_hyphenation_language_default() {
let lang = StyleHyphenationLanguage::default();