1use alloc::{
13 string::{String, ToString},
14 vec::Vec,
15};
16
17use crate::{
18 impl_option, impl_option_inner, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_eq,
19 impl_vec_hash, impl_vec_mut, impl_vec_ord, impl_vec_partialeq, impl_vec_partialord,
20 props::{
21 basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
22 formatter::PrintAsCssValue,
23 macros::PixelValueTaker,
24 },
25};
26
27#[allow(variant_size_differences)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
44#[repr(C, u8)]
45pub enum CalcAstItem {
46 Value(PixelValue),
48 Add,
50 Sub,
52 Mul,
54 Div,
56 BraceOpen,
58 BraceClose,
60}
61
62impl_vec!(
64 CalcAstItem,
65 CalcAstItemVec,
66 CalcAstItemVecDestructor,
67 CalcAstItemVecDestructorType,
68 CalcAstItemVecSlice,
69 OptionCalcAstItem
70);
71impl_vec_clone!(CalcAstItem, CalcAstItemVec, CalcAstItemVecDestructor);
72impl_vec_debug!(CalcAstItem, CalcAstItemVec);
73impl_vec_partialeq!(CalcAstItem, CalcAstItemVec);
74impl_vec_eq!(CalcAstItem, CalcAstItemVec);
75impl_vec_partialord!(CalcAstItem, CalcAstItemVec);
76impl_vec_ord!(CalcAstItem, CalcAstItemVec);
77impl_vec_hash!(CalcAstItem, CalcAstItemVec);
78impl_vec_mut!(CalcAstItem, CalcAstItemVec);
79
80impl_option!(
81 CalcAstItem,
82 OptionCalcAstItem,
83 copy = false,
84 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
85);
86
87#[cfg(feature = "parser")]
101fn parse_calc_expression(input: &str) -> Result<CalcAstItemVec, ()> {
102 use crate::props::basic::pixel::parse_pixel_value;
103
104 let mut items: Vec<CalcAstItem> = Vec::new();
105 let input = input.trim();
106 let bytes = input.as_bytes();
107 let mut i = 0;
108
109 while i < bytes.len() {
110 if bytes[i].is_ascii_whitespace() {
112 i += 1;
113 continue;
114 }
115
116 match bytes[i] {
117 b'+' => { items.push(CalcAstItem::Add); i += 1; }
118 b'*' => { items.push(CalcAstItem::Mul); i += 1; }
119 b'/' => { items.push(CalcAstItem::Div); i += 1; }
120 b'(' => { items.push(CalcAstItem::BraceOpen); i += 1; }
121 b')' => { items.push(CalcAstItem::BraceClose); i += 1; }
122 b'-' => {
123 let is_negative_number = items.is_empty()
128 || matches!(
129 items.last(),
130 Some(CalcAstItem::Add | CalcAstItem::Sub | CalcAstItem::Mul | CalcAstItem::Div
131| CalcAstItem::BraceOpen)
132 );
133
134 if is_negative_number {
135 let rest = &input[i..];
137 let end = find_value_end(rest);
138 if end == 0 { return Err(()); }
139 let val_str = &rest[..end];
140 let pv = parse_pixel_value(val_str).map_err(|_| ())?;
141 items.push(CalcAstItem::Value(pv));
142 i += end;
143 } else {
144 items.push(CalcAstItem::Sub);
145 i += 1;
146 }
147 }
148 _ => {
149 let rest = &input[i..];
151 let end = find_value_end(rest);
152 if end == 0 { return Err(()); }
153 let val_str = &rest[..end];
154 let pv = parse_pixel_value(val_str).map_err(|_| ())?;
155 items.push(CalcAstItem::Value(pv));
156 i += end;
157 }
158 }
159 }
160
161 if items.is_empty() {
162 return Err(());
163 }
164
165 Ok(CalcAstItemVec::from(items))
166}
167
168#[cfg(feature = "parser")]
171fn find_value_end(s: &str) -> usize {
172 let bytes = s.as_bytes();
173 let mut i = 0;
174
175 if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
177 i += 1;
178 }
179
180 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
182 i += 1;
183 }
184
185 while i < bytes.len() && (bytes[i].is_ascii_alphabetic() || bytes[i] == b'%') {
187 i += 1;
188 }
189
190 i
191}
192
193fn calc_ast_to_css_string(items: &CalcAstItemVec) -> String {
195 let inner: Vec<String> = items.iter().map(|i| match i {
196 CalcAstItem::Value(v) => v.to_string(),
197 CalcAstItem::Add => "+".to_string(),
198 CalcAstItem::Sub => "-".to_string(),
199 CalcAstItem::Mul => "*".to_string(),
200 CalcAstItem::Div => "/".to_string(),
201 CalcAstItem::BraceOpen => "(".to_string(),
202 CalcAstItem::BraceClose => ")".to_string(),
203 }).collect();
204 alloc::format!("calc({})", inner.join(" "))
205}
206
207macro_rules! define_dimension_property {
210 ($struct_name:ident, $default_fn:expr) => {
211 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
212 #[repr(C)]
213 pub struct $struct_name {
214 pub inner: PixelValue,
215 }
216
217 impl Default for $struct_name {
218 fn default() -> Self {
219 $default_fn()
220 }
221 }
222
223 impl PixelValueTaker for $struct_name {
224 fn from_pixel_value(inner: PixelValue) -> Self {
225 Self { inner }
226 }
227 }
228
229 impl_pixel_value!($struct_name);
230
231 impl PrintAsCssValue for $struct_name {
232 fn print_as_css_value(&self) -> String {
233 self.inner.to_string()
234 }
235 }
236 };
237}
238
239macro_rules! define_sizing_enum {
240 ($name:ident) => {
241 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
242 #[repr(C, u8)]
243 #[derive(Default)]
244 pub enum $name {
245 #[default]
246 Auto,
247 Px(PixelValue),
248 MinContent,
249 MaxContent,
250 FitContent(PixelValue),
252 Calc(CalcAstItemVec),
254 }
255
256 impl PixelValueTaker for $name {
257 fn from_pixel_value(inner: PixelValue) -> Self {
258 $name::Px(inner)
259 }
260 }
261
262 impl PrintAsCssValue for $name {
263 fn print_as_css_value(&self) -> String {
264 match self {
265 $name::Auto => "auto".to_string(),
266 $name::Px(v) => v.to_string(),
267 $name::MinContent => "min-content".to_string(),
268 $name::MaxContent => "max-content".to_string(),
269 $name::FitContent(v) => alloc::format!("fit-content({})", v),
270 $name::Calc(items) => calc_ast_to_css_string(items),
271 }
272 }
273 }
274
275 impl $name {
276 #[must_use] pub fn px(value: f32) -> Self {
277 $name::Px(PixelValue::px(value))
278 }
279
280 #[must_use] pub const fn const_px(value: isize) -> Self {
281 $name::Px(PixelValue::const_px(value))
282 }
283
284 #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
285 match (self, other) {
286 ($name::Px(a), $name::Px(b)) => $name::Px(a.interpolate(b, t)),
287 ($name::FitContent(a), $name::FitContent(b)) => $name::FitContent(a.interpolate(b, t)),
288 (_, $name::Px(b)) if t >= 0.5 => $name::Px(*b),
289 ($name::Px(a), _) if t < 0.5 => $name::Px(*a),
290 ($name::Auto, $name::Auto) => $name::Auto,
291 (a, _) if t < 0.5 => a.clone(),
292 (_, b) => b.clone(),
293 }
294 }
295 }
296 };
297}
298
299define_sizing_enum!(LayoutWidth);
300define_sizing_enum!(LayoutHeight);
301
302define_dimension_property!(LayoutMinWidth, || Self {
304 inner: PixelValue::zero()
305});
306define_dimension_property!(LayoutMinHeight, || Self {
308 inner: PixelValue::zero()
309});
310define_dimension_property!(LayoutMaxWidth, || Self {
315 inner: PixelValue::px(core::f32::MAX)
316});
317define_dimension_property!(LayoutMaxHeight, || Self {
322 inner: PixelValue::px(core::f32::MAX)
323});
324
325#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
327#[repr(C)]
328#[derive(Default)]
329pub enum LayoutBoxSizing {
330 #[default]
331 ContentBox,
332 BorderBox,
333}
334
335
336impl PrintAsCssValue for LayoutBoxSizing {
337 fn print_as_css_value(&self) -> String {
338 String::from(match self {
339 Self::ContentBox => "content-box",
340 Self::BorderBox => "border-box",
341 })
342 }
343}
344
345#[cfg(feature = "parser")]
348pub mod parser {
349
350 use alloc::string::ToString;
351 use crate::corety::AzString;
352
353 #[allow(clippy::wildcard_imports)] use super::*;
355 use crate::props::basic::pixel::parse_pixel_value;
356
357 macro_rules! define_pixel_dimension_parser {
358 ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident) => {
359 #[derive(Clone, PartialEq, Eq)]
360 pub enum $error_name<'a> {
361 PixelValue(CssPixelValueParseError<'a>),
362 }
363
364 impl_debug_as_display!($error_name<'a>);
365 impl_display! { $error_name<'a>, {
366 PixelValue(e) => format!("{}", e),
367 }}
368
369 impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
370
371 #[derive(Debug, Clone, PartialEq, Eq)]
372 #[repr(C, u8)]
373 pub enum $error_owned_name {
374 PixelValue(CssPixelValueParseErrorOwned),
375 }
376
377 impl $error_name<'_> {
378 #[must_use] pub fn to_contained(&self) -> $error_owned_name {
379 match self {
380 $error_name::PixelValue(e) => {
381 $error_owned_name::PixelValue(e.to_contained())
382 }
383 }
384 }
385 }
386
387 impl $error_owned_name {
388 #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
389 match self {
390 $error_owned_name::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
391 }
392 }
393 }
394
395 pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
399 parse_pixel_value(input)
400 .map(|v| $struct_name { inner: v })
401 .map_err($error_name::PixelValue)
402 }
403 };
404 }
405
406 macro_rules! define_sizing_parser {
407 ($fn_name:ident, $enum_name:ident, $error_name:ident, $error_owned_name:ident, $keyword_label:expr) => {
408 #[derive(Clone, PartialEq, Eq)]
409 pub enum $error_name<'a> {
410 PixelValue(CssPixelValueParseError<'a>),
411 InvalidKeyword(&'a str),
412 }
413
414 impl_debug_as_display!($error_name<'a>);
415 impl_display! { $error_name<'a>, {
416 PixelValue(e) => format!("{}", e),
417 InvalidKeyword(k) => format!("Invalid {} keyword: \"{}\"", $keyword_label, k),
418 }}
419
420 impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
421
422 #[derive(Debug, Clone, PartialEq, Eq)]
423 #[repr(C, u8)]
424 pub enum $error_owned_name {
425 PixelValue(CssPixelValueParseErrorOwned),
426 InvalidKeyword(AzString),
427 }
428
429 impl $error_name<'_> {
430 #[must_use] pub fn to_contained(&self) -> $error_owned_name {
431 match self {
432 $error_name::PixelValue(e) => {
433 $error_owned_name::PixelValue(e.to_contained())
434 }
435 $error_name::InvalidKeyword(k) => {
436 $error_owned_name::InvalidKeyword(k.to_string().into())
437 }
438 }
439 }
440 }
441
442 impl $error_owned_name {
443 #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
444 match self {
445 $error_owned_name::PixelValue(e) => {
446 $error_name::PixelValue(e.to_shared())
447 }
448 $error_owned_name::InvalidKeyword(k) => {
449 $error_name::InvalidKeyword(k)
450 }
451 }
452 }
453 }
454
455 pub fn $fn_name(
459 input: &str,
460 ) -> Result<$enum_name, $error_name<'_>> {
461 let trimmed = input.trim();
462 match trimmed {
463 "auto" => Ok($enum_name::Auto),
464 "min-content" => Ok($enum_name::MinContent),
465 "max-content" => Ok($enum_name::MaxContent),
466 s if s.starts_with("fit-content(") && s.ends_with(')') => {
467 let inner = &s[12..s.len() - 1].trim();
468 parse_pixel_value(inner)
469 .map(|pv| {
470 if pv.number.get() < 0.0 {
471 $enum_name::FitContent(PixelValue::zero())
472 } else {
473 $enum_name::FitContent(pv)
474 }
475 })
476 .map_err($error_name::PixelValue)
477 }
478 s if s.starts_with("calc(") && s.ends_with(')') => {
479 let inner = &s[5..s.len() - 1];
480 parse_calc_expression(inner)
481 .map($enum_name::Calc)
482 .map_err(|_| $error_name::InvalidKeyword(input))
483 }
484 _ => parse_pixel_value(trimmed)
485 .map($enum_name::Px)
486 .map_err($error_name::PixelValue),
487 }
488 }
489 };
490 }
491
492 define_sizing_parser!(parse_layout_width, LayoutWidth, LayoutWidthParseError, LayoutWidthParseErrorOwned, "width");
493 define_sizing_parser!(parse_layout_height, LayoutHeight, LayoutHeightParseError, LayoutHeightParseErrorOwned, "height");
494 define_pixel_dimension_parser!(
495 parse_layout_min_width,
496 LayoutMinWidth,
497 LayoutMinWidthParseError,
498 LayoutMinWidthParseErrorOwned
499 );
500 define_pixel_dimension_parser!(
501 parse_layout_min_height,
502 LayoutMinHeight,
503 LayoutMinHeightParseError,
504 LayoutMinHeightParseErrorOwned
505 );
506 define_pixel_dimension_parser!(
507 parse_layout_max_width,
508 LayoutMaxWidth,
509 LayoutMaxWidthParseError,
510 LayoutMaxWidthParseErrorOwned
511 );
512 define_pixel_dimension_parser!(
513 parse_layout_max_height,
514 LayoutMaxHeight,
515 LayoutMaxHeightParseError,
516 LayoutMaxHeightParseErrorOwned
517 );
518
519 #[derive(Clone, PartialEq, Eq)]
522 pub enum LayoutBoxSizingParseError<'a> {
523 InvalidValue(&'a str),
524 }
525
526 impl_debug_as_display!(LayoutBoxSizingParseError<'a>);
527 impl_display! { LayoutBoxSizingParseError<'a>, {
528 InvalidValue(v) => format!("Invalid box-sizing value: \"{}\"", v),
529 }}
530
531 #[derive(Debug, Clone, PartialEq, Eq)]
532 #[repr(C, u8)]
533 pub enum LayoutBoxSizingParseErrorOwned {
534 InvalidValue(AzString),
535 }
536
537 impl LayoutBoxSizingParseError<'_> {
538 #[must_use] pub fn to_contained(&self) -> LayoutBoxSizingParseErrorOwned {
539 match self {
540 LayoutBoxSizingParseError::InvalidValue(s) => {
541 LayoutBoxSizingParseErrorOwned::InvalidValue((*s).to_string().into())
542 }
543 }
544 }
545 }
546
547 impl LayoutBoxSizingParseErrorOwned {
548 #[must_use] pub fn to_shared(&self) -> LayoutBoxSizingParseError<'_> {
549 match self {
550 Self::InvalidValue(s) => {
551 LayoutBoxSizingParseError::InvalidValue(s)
552 }
553 }
554 }
555 }
556
557 pub fn parse_layout_box_sizing(
561 input: &str,
562 ) -> Result<LayoutBoxSizing, LayoutBoxSizingParseError<'_>> {
563 match input.trim() {
564 "content-box" => Ok(LayoutBoxSizing::ContentBox),
565 "border-box" => Ok(LayoutBoxSizing::BorderBox),
566 other => Err(LayoutBoxSizingParseError::InvalidValue(other)),
567 }
568 }
569}
570
571#[cfg(feature = "parser")]
572pub use self::parser::*;
573
574#[cfg(all(test, feature = "parser"))]
575mod tests {
576 use super::*;
577 use crate::props::basic::pixel::PixelValue;
578
579 #[test]
580 fn test_parse_layout_width() {
581 assert_eq!(
582 parse_layout_width("150px").unwrap(),
583 LayoutWidth::Px(PixelValue::px(150.0))
584 );
585 assert_eq!(
586 parse_layout_width("2.5em").unwrap(),
587 LayoutWidth::Px(PixelValue::em(2.5))
588 );
589 assert_eq!(
590 parse_layout_width("75%").unwrap(),
591 LayoutWidth::Px(PixelValue::percent(75.0))
592 );
593 assert_eq!(
594 parse_layout_width("0").unwrap(),
595 LayoutWidth::Px(PixelValue::px(0.0))
596 );
597 assert_eq!(
598 parse_layout_width(" 100pt ").unwrap(),
599 LayoutWidth::Px(PixelValue::pt(100.0))
600 );
601 assert_eq!(
602 parse_layout_width("min-content").unwrap(),
603 LayoutWidth::MinContent
604 );
605 assert_eq!(
606 parse_layout_width("max-content").unwrap(),
607 LayoutWidth::MaxContent
608 );
609 }
610
611 #[test]
612 fn test_parse_layout_height_invalid() {
613 assert!(parse_layout_height("auto").is_ok());
615 assert!(parse_layout_height("150 px").is_ok());
617 assert!(parse_layout_height("px").is_err());
618 assert!(parse_layout_height("invalid").is_err());
619 }
620
621 #[test]
622 fn test_parse_layout_box_sizing() {
623 assert_eq!(
624 parse_layout_box_sizing("content-box").unwrap(),
625 LayoutBoxSizing::ContentBox
626 );
627 assert_eq!(
628 parse_layout_box_sizing("border-box").unwrap(),
629 LayoutBoxSizing::BorderBox
630 );
631 assert_eq!(
632 parse_layout_box_sizing(" border-box ").unwrap(),
633 LayoutBoxSizing::BorderBox
634 );
635 }
636
637 #[test]
638 fn test_parse_layout_box_sizing_invalid() {
639 assert!(parse_layout_box_sizing("padding-box").is_err());
640 assert!(parse_layout_box_sizing("borderbox").is_err());
641 assert!(parse_layout_box_sizing("").is_err());
642 }
643}