1use alloc::{
4 string::{String, ToString},
5 vec::Vec,
6};
7
8use crate::{
9 impl_option, impl_option_inner, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_eq,
10 impl_vec_hash, impl_vec_mut, impl_vec_ord, impl_vec_partialeq, impl_vec_partialord,
11 props::{
12 basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
13 formatter::PrintAsCssValue,
14 macros::PixelValueTaker,
15 },
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35#[repr(C, u8)]
36pub enum CalcAstItem {
37 Value(PixelValue),
39 Add,
41 Sub,
43 Mul,
45 Div,
47 BraceOpen,
49 BraceClose,
51}
52
53impl_vec!(
55 CalcAstItem,
56 CalcAstItemVec,
57 CalcAstItemVecDestructor,
58 CalcAstItemVecDestructorType,
59 CalcAstItemVecSlice,
60 OptionCalcAstItem
61);
62impl_vec_clone!(CalcAstItem, CalcAstItemVec, CalcAstItemVecDestructor);
63impl_vec_debug!(CalcAstItem, CalcAstItemVec);
64impl_vec_partialeq!(CalcAstItem, CalcAstItemVec);
65impl_vec_eq!(CalcAstItem, CalcAstItemVec);
66impl_vec_partialord!(CalcAstItem, CalcAstItemVec);
67impl_vec_ord!(CalcAstItem, CalcAstItemVec);
68impl_vec_hash!(CalcAstItem, CalcAstItemVec);
69impl_vec_mut!(CalcAstItem, CalcAstItemVec);
70
71impl_option!(
72 CalcAstItem,
73 OptionCalcAstItem,
74 copy = false,
75 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
76);
77
78#[cfg(feature = "parser")]
92fn parse_calc_expression(input: &str) -> Result<CalcAstItemVec, ()> {
93 use crate::props::basic::pixel::parse_pixel_value;
94
95 let mut items: Vec<CalcAstItem> = Vec::new();
96 let input = input.trim();
97 let bytes = input.as_bytes();
98 let mut i = 0;
99
100 while i < bytes.len() {
101 if bytes[i].is_ascii_whitespace() {
103 i += 1;
104 continue;
105 }
106
107 match bytes[i] {
108 b'+' => { items.push(CalcAstItem::Add); i += 1; }
109 b'*' => { items.push(CalcAstItem::Mul); i += 1; }
110 b'/' => { items.push(CalcAstItem::Div); i += 1; }
111 b'(' => { items.push(CalcAstItem::BraceOpen); i += 1; }
112 b')' => { items.push(CalcAstItem::BraceClose); i += 1; }
113 b'-' => {
114 let is_negative_number = items.is_empty()
119 || matches!(
120 items.last(),
121 Some(CalcAstItem::Add)
122 | Some(CalcAstItem::Sub)
123 | Some(CalcAstItem::Mul)
124 | Some(CalcAstItem::Div)
125 | Some(CalcAstItem::BraceOpen)
126 );
127
128 if is_negative_number {
129 let rest = &input[i..];
131 let end = find_value_end(rest);
132 if end == 0 { return Err(()); }
133 let val_str = &rest[..end];
134 let pv = parse_pixel_value(val_str).map_err(|_| ())?;
135 items.push(CalcAstItem::Value(pv));
136 i += end;
137 } else {
138 items.push(CalcAstItem::Sub);
139 i += 1;
140 }
141 }
142 _ => {
143 let rest = &input[i..];
145 let end = find_value_end(rest);
146 if end == 0 { return Err(()); }
147 let val_str = &rest[..end];
148 let pv = parse_pixel_value(val_str).map_err(|_| ())?;
149 items.push(CalcAstItem::Value(pv));
150 i += end;
151 }
152 }
153 }
154
155 if items.is_empty() {
156 return Err(());
157 }
158
159 Ok(CalcAstItemVec::from(items))
160}
161
162#[cfg(feature = "parser")]
165fn find_value_end(s: &str) -> usize {
166 let bytes = s.as_bytes();
167 let mut i = 0;
168
169 if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
171 i += 1;
172 }
173
174 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
176 i += 1;
177 }
178
179 while i < bytes.len() && (bytes[i].is_ascii_alphabetic() || bytes[i] == b'%') {
181 i += 1;
182 }
183
184 i
185}
186
187macro_rules! define_dimension_property {
190 ($struct_name:ident, $default_fn:expr) => {
191 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
192 #[repr(C)]
193 pub struct $struct_name {
194 pub inner: PixelValue,
195 }
196
197 impl Default for $struct_name {
198 fn default() -> Self {
199 $default_fn()
200 }
201 }
202
203 impl PixelValueTaker for $struct_name {
204 fn from_pixel_value(inner: PixelValue) -> Self {
205 Self { inner }
206 }
207 }
208
209 impl_pixel_value!($struct_name);
210
211 impl PrintAsCssValue for $struct_name {
212 fn print_as_css_value(&self) -> String {
213 self.inner.to_string()
214 }
215 }
216 };
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
221#[repr(C, u8)]
222pub enum LayoutWidth {
223 Auto,
224 Px(PixelValue),
225 MinContent,
226 MaxContent,
227 Calc(CalcAstItemVec),
229}
230
231impl Default for LayoutWidth {
232 fn default() -> Self {
233 LayoutWidth::Auto
234 }
235}
236
237impl PixelValueTaker for LayoutWidth {
238 fn from_pixel_value(inner: PixelValue) -> Self {
239 LayoutWidth::Px(inner)
240 }
241}
242
243impl PrintAsCssValue for LayoutWidth {
244 fn print_as_css_value(&self) -> String {
245 match self {
246 LayoutWidth::Auto => "auto".to_string(),
247 LayoutWidth::Px(v) => v.to_string(),
248 LayoutWidth::MinContent => "min-content".to_string(),
249 LayoutWidth::MaxContent => "max-content".to_string(),
250 LayoutWidth::Calc(items) => {
251 let inner: Vec<String> = items.iter().map(|i| match i {
252 CalcAstItem::Value(v) => v.to_string(),
253 CalcAstItem::Add => "+".to_string(),
254 CalcAstItem::Sub => "-".to_string(),
255 CalcAstItem::Mul => "*".to_string(),
256 CalcAstItem::Div => "/".to_string(),
257 CalcAstItem::BraceOpen => "(".to_string(),
258 CalcAstItem::BraceClose => ")".to_string(),
259 }).collect();
260 alloc::format!("calc({})", inner.join(" "))
261 }
262 }
263 }
264}
265
266impl LayoutWidth {
267 pub fn px(value: f32) -> Self {
268 LayoutWidth::Px(PixelValue::px(value))
269 }
270
271 pub const fn const_px(value: isize) -> Self {
272 LayoutWidth::Px(PixelValue::const_px(value))
273 }
274
275 pub fn interpolate(&self, other: &Self, t: f32) -> Self {
276 match (self, other) {
277 (LayoutWidth::Px(a), LayoutWidth::Px(b)) => LayoutWidth::Px(a.interpolate(b, t)),
278 (_, LayoutWidth::Px(b)) if t >= 0.5 => LayoutWidth::Px(*b),
279 (LayoutWidth::Px(a), _) if t < 0.5 => LayoutWidth::Px(*a),
280 (LayoutWidth::Auto, LayoutWidth::Auto) => LayoutWidth::Auto,
281 (a, _) if t < 0.5 => a.clone(),
282 (_, b) => b.clone(),
283 }
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
289#[repr(C, u8)]
290pub enum LayoutHeight {
291 Auto,
292 Px(PixelValue),
293 MinContent,
294 MaxContent,
295 Calc(CalcAstItemVec),
297}
298
299impl Default for LayoutHeight {
300 fn default() -> Self {
301 LayoutHeight::Auto
302 }
303}
304
305impl PixelValueTaker for LayoutHeight {
306 fn from_pixel_value(inner: PixelValue) -> Self {
307 LayoutHeight::Px(inner)
308 }
309}
310
311impl PrintAsCssValue for LayoutHeight {
312 fn print_as_css_value(&self) -> String {
313 match self {
314 LayoutHeight::Auto => "auto".to_string(),
315 LayoutHeight::Px(v) => v.to_string(),
316 LayoutHeight::MinContent => "min-content".to_string(),
317 LayoutHeight::MaxContent => "max-content".to_string(),
318 LayoutHeight::Calc(items) => {
319 let inner: Vec<String> = items.iter().map(|i| match i {
320 CalcAstItem::Value(v) => v.to_string(),
321 CalcAstItem::Add => "+".to_string(),
322 CalcAstItem::Sub => "-".to_string(),
323 CalcAstItem::Mul => "*".to_string(),
324 CalcAstItem::Div => "/".to_string(),
325 CalcAstItem::BraceOpen => "(".to_string(),
326 CalcAstItem::BraceClose => ")".to_string(),
327 }).collect();
328 alloc::format!("calc({})", inner.join(" "))
329 }
330 }
331 }
332}
333
334impl LayoutHeight {
335 pub fn px(value: f32) -> Self {
336 LayoutHeight::Px(PixelValue::px(value))
337 }
338
339 pub const fn const_px(value: isize) -> Self {
340 LayoutHeight::Px(PixelValue::const_px(value))
341 }
342
343 pub fn interpolate(&self, other: &Self, t: f32) -> Self {
344 match (self, other) {
345 (LayoutHeight::Px(a), LayoutHeight::Px(b)) => LayoutHeight::Px(a.interpolate(b, t)),
346 (_, LayoutHeight::Px(b)) if t >= 0.5 => LayoutHeight::Px(*b),
347 (LayoutHeight::Px(a), _) if t < 0.5 => LayoutHeight::Px(*a),
348 (LayoutHeight::Auto, LayoutHeight::Auto) => LayoutHeight::Auto,
349 (a, _) if t < 0.5 => a.clone(),
350 (_, b) => b.clone(),
351 }
352 }
353}
354
355define_dimension_property!(LayoutMinWidth, || Self {
356 inner: PixelValue::zero()
357});
358define_dimension_property!(LayoutMinHeight, || Self {
359 inner: PixelValue::zero()
360});
361define_dimension_property!(LayoutMaxWidth, || Self {
362 inner: PixelValue::px(core::f32::MAX)
363});
364define_dimension_property!(LayoutMaxHeight, || Self {
365 inner: PixelValue::px(core::f32::MAX)
366});
367
368#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
370#[repr(C)]
371pub enum LayoutBoxSizing {
372 ContentBox,
373 BorderBox,
374}
375
376impl Default for LayoutBoxSizing {
377 fn default() -> Self {
378 LayoutBoxSizing::ContentBox
379 }
380}
381
382impl PrintAsCssValue for LayoutBoxSizing {
383 fn print_as_css_value(&self) -> String {
384 String::from(match self {
385 LayoutBoxSizing::ContentBox => "content-box",
386 LayoutBoxSizing::BorderBox => "border-box",
387 })
388 }
389}
390
391#[cfg(feature = "parser")]
394mod parser {
395
396 use alloc::string::ToString;
397
398 use super::*;
399 use crate::props::basic::pixel::parse_pixel_value;
400
401 macro_rules! define_pixel_dimension_parser {
402 ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident) => {
403 #[derive(Clone, PartialEq)]
404 pub enum $error_name<'a> {
405 PixelValue(CssPixelValueParseError<'a>),
406 }
407
408 impl_debug_as_display!($error_name<'a>);
409 impl_display! { $error_name<'a>, {
410 PixelValue(e) => format!("{}", e),
411 }}
412
413 impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
414
415 #[derive(Debug, Clone, PartialEq)]
416 pub enum $error_owned_name {
417 PixelValue(CssPixelValueParseErrorOwned),
418 }
419
420 impl<'a> $error_name<'a> {
421 pub fn to_contained(&self) -> $error_owned_name {
422 match self {
423 $error_name::PixelValue(e) => {
424 $error_owned_name::PixelValue(e.to_contained())
425 }
426 }
427 }
428 }
429
430 impl $error_owned_name {
431 pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
432 match self {
433 $error_owned_name::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
434 }
435 }
436 }
437
438 pub fn $fn_name<'a>(input: &'a str) -> Result<$struct_name, $error_name<'a>> {
439 parse_pixel_value(input)
440 .map(|v| $struct_name { inner: v })
441 .map_err($error_name::PixelValue)
442 }
443 };
444 }
445
446 #[derive(Clone, PartialEq)]
449 pub enum LayoutWidthParseError<'a> {
450 PixelValue(CssPixelValueParseError<'a>),
451 InvalidKeyword(&'a str),
452 }
453
454 impl_debug_as_display!(LayoutWidthParseError<'a>);
455 impl_display! { LayoutWidthParseError<'a>, {
456 PixelValue(e) => format!("{}", e),
457 InvalidKeyword(k) => format!("Invalid width keyword: \"{}\"", k),
458 }}
459
460 impl_from! { CssPixelValueParseError<'a>, LayoutWidthParseError::PixelValue }
461
462 #[derive(Debug, Clone, PartialEq)]
463 pub enum LayoutWidthParseErrorOwned {
464 PixelValue(CssPixelValueParseErrorOwned),
465 InvalidKeyword(String),
466 }
467
468 impl<'a> LayoutWidthParseError<'a> {
469 pub fn to_contained(&self) -> LayoutWidthParseErrorOwned {
470 match self {
471 LayoutWidthParseError::PixelValue(e) => {
472 LayoutWidthParseErrorOwned::PixelValue(e.to_contained())
473 }
474 LayoutWidthParseError::InvalidKeyword(k) => {
475 LayoutWidthParseErrorOwned::InvalidKeyword(k.to_string())
476 }
477 }
478 }
479 }
480
481 impl LayoutWidthParseErrorOwned {
482 pub fn to_shared<'a>(&'a self) -> LayoutWidthParseError<'a> {
483 match self {
484 LayoutWidthParseErrorOwned::PixelValue(e) => {
485 LayoutWidthParseError::PixelValue(e.to_shared())
486 }
487 LayoutWidthParseErrorOwned::InvalidKeyword(k) => {
488 LayoutWidthParseError::InvalidKeyword(k)
489 }
490 }
491 }
492 }
493
494 pub fn parse_layout_width<'a>(
495 input: &'a str,
496 ) -> Result<LayoutWidth, LayoutWidthParseError<'a>> {
497 let trimmed = input.trim();
498 match trimmed {
499 "auto" => Ok(LayoutWidth::Auto),
500 "min-content" => Ok(LayoutWidth::MinContent),
501 "max-content" => Ok(LayoutWidth::MaxContent),
502 s if s.starts_with("calc(") && s.ends_with(')') => {
503 let inner = &s[5..s.len() - 1];
504 parse_calc_expression(inner)
505 .map(LayoutWidth::Calc)
506 .map_err(|_| LayoutWidthParseError::InvalidKeyword(input))
507 }
508 _ => parse_pixel_value(trimmed)
509 .map(LayoutWidth::Px)
510 .map_err(LayoutWidthParseError::PixelValue),
511 }
512 }
513
514 #[derive(Clone, PartialEq)]
515 pub enum LayoutHeightParseError<'a> {
516 PixelValue(CssPixelValueParseError<'a>),
517 InvalidKeyword(&'a str),
518 }
519
520 impl_debug_as_display!(LayoutHeightParseError<'a>);
521 impl_display! { LayoutHeightParseError<'a>, {
522 PixelValue(e) => format!("{}", e),
523 InvalidKeyword(k) => format!("Invalid height keyword: \"{}\"", k),
524 }}
525
526 impl_from! { CssPixelValueParseError<'a>, LayoutHeightParseError::PixelValue }
527
528 #[derive(Debug, Clone, PartialEq)]
529 pub enum LayoutHeightParseErrorOwned {
530 PixelValue(CssPixelValueParseErrorOwned),
531 InvalidKeyword(String),
532 }
533
534 impl<'a> LayoutHeightParseError<'a> {
535 pub fn to_contained(&self) -> LayoutHeightParseErrorOwned {
536 match self {
537 LayoutHeightParseError::PixelValue(e) => {
538 LayoutHeightParseErrorOwned::PixelValue(e.to_contained())
539 }
540 LayoutHeightParseError::InvalidKeyword(k) => {
541 LayoutHeightParseErrorOwned::InvalidKeyword(k.to_string())
542 }
543 }
544 }
545 }
546
547 impl LayoutHeightParseErrorOwned {
548 pub fn to_shared<'a>(&'a self) -> LayoutHeightParseError<'a> {
549 match self {
550 LayoutHeightParseErrorOwned::PixelValue(e) => {
551 LayoutHeightParseError::PixelValue(e.to_shared())
552 }
553 LayoutHeightParseErrorOwned::InvalidKeyword(k) => {
554 LayoutHeightParseError::InvalidKeyword(k)
555 }
556 }
557 }
558 }
559
560 pub fn parse_layout_height<'a>(
561 input: &'a str,
562 ) -> Result<LayoutHeight, LayoutHeightParseError<'a>> {
563 let trimmed = input.trim();
564 match trimmed {
565 "auto" => Ok(LayoutHeight::Auto),
566 "min-content" => Ok(LayoutHeight::MinContent),
567 "max-content" => Ok(LayoutHeight::MaxContent),
568 s if s.starts_with("calc(") && s.ends_with(')') => {
569 let inner = &s[5..s.len() - 1];
570 parse_calc_expression(inner)
571 .map(LayoutHeight::Calc)
572 .map_err(|_| LayoutHeightParseError::InvalidKeyword(input))
573 }
574 _ => parse_pixel_value(trimmed)
575 .map(LayoutHeight::Px)
576 .map_err(LayoutHeightParseError::PixelValue),
577 }
578 }
579 define_pixel_dimension_parser!(
580 parse_layout_min_width,
581 LayoutMinWidth,
582 LayoutMinWidthParseError,
583 LayoutMinWidthParseErrorOwned
584 );
585 define_pixel_dimension_parser!(
586 parse_layout_min_height,
587 LayoutMinHeight,
588 LayoutMinHeightParseError,
589 LayoutMinHeightParseErrorOwned
590 );
591 define_pixel_dimension_parser!(
592 parse_layout_max_width,
593 LayoutMaxWidth,
594 LayoutMaxWidthParseError,
595 LayoutMaxWidthParseErrorOwned
596 );
597 define_pixel_dimension_parser!(
598 parse_layout_max_height,
599 LayoutMaxHeight,
600 LayoutMaxHeightParseError,
601 LayoutMaxHeightParseErrorOwned
602 );
603
604 #[derive(Clone, PartialEq)]
607 pub enum LayoutBoxSizingParseError<'a> {
608 InvalidValue(&'a str),
609 }
610
611 impl_debug_as_display!(LayoutBoxSizingParseError<'a>);
612 impl_display! { LayoutBoxSizingParseError<'a>, {
613 InvalidValue(v) => format!("Invalid box-sizing value: \"{}\"", v),
614 }}
615
616 #[derive(Debug, Clone, PartialEq)]
617 pub enum LayoutBoxSizingParseErrorOwned {
618 InvalidValue(String),
619 }
620
621 impl<'a> LayoutBoxSizingParseError<'a> {
622 pub fn to_contained(&self) -> LayoutBoxSizingParseErrorOwned {
623 match self {
624 LayoutBoxSizingParseError::InvalidValue(s) => {
625 LayoutBoxSizingParseErrorOwned::InvalidValue(s.to_string())
626 }
627 }
628 }
629 }
630
631 impl LayoutBoxSizingParseErrorOwned {
632 pub fn to_shared<'a>(&'a self) -> LayoutBoxSizingParseError<'a> {
633 match self {
634 LayoutBoxSizingParseErrorOwned::InvalidValue(s) => {
635 LayoutBoxSizingParseError::InvalidValue(s)
636 }
637 }
638 }
639 }
640
641 pub fn parse_layout_box_sizing<'a>(
642 input: &'a str,
643 ) -> Result<LayoutBoxSizing, LayoutBoxSizingParseError<'a>> {
644 match input.trim() {
645 "content-box" => Ok(LayoutBoxSizing::ContentBox),
646 "border-box" => Ok(LayoutBoxSizing::BorderBox),
647 other => Err(LayoutBoxSizingParseError::InvalidValue(other)),
648 }
649 }
650}
651
652#[cfg(feature = "parser")]
653pub use self::parser::*;
654
655#[cfg(all(test, feature = "parser"))]
656mod tests {
657 use super::*;
658 use crate::props::basic::pixel::PixelValue;
659
660 #[test]
661 fn test_parse_layout_width() {
662 assert_eq!(
663 parse_layout_width("150px").unwrap(),
664 LayoutWidth::Px(PixelValue::px(150.0))
665 );
666 assert_eq!(
667 parse_layout_width("2.5em").unwrap(),
668 LayoutWidth::Px(PixelValue::em(2.5))
669 );
670 assert_eq!(
671 parse_layout_width("75%").unwrap(),
672 LayoutWidth::Px(PixelValue::percent(75.0))
673 );
674 assert_eq!(
675 parse_layout_width("0").unwrap(),
676 LayoutWidth::Px(PixelValue::px(0.0))
677 );
678 assert_eq!(
679 parse_layout_width(" 100pt ").unwrap(),
680 LayoutWidth::Px(PixelValue::pt(100.0))
681 );
682 assert_eq!(
683 parse_layout_width("min-content").unwrap(),
684 LayoutWidth::MinContent
685 );
686 assert_eq!(
687 parse_layout_width("max-content").unwrap(),
688 LayoutWidth::MaxContent
689 );
690 }
691
692 #[test]
693 fn test_parse_layout_height_invalid() {
694 assert!(parse_layout_height("auto").is_ok());
696 assert!(parse_layout_height("150 px").is_ok());
698 assert!(parse_layout_height("px").is_err());
699 assert!(parse_layout_height("invalid").is_err());
700 }
701
702 #[test]
703 fn test_parse_layout_box_sizing() {
704 assert_eq!(
705 parse_layout_box_sizing("content-box").unwrap(),
706 LayoutBoxSizing::ContentBox
707 );
708 assert_eq!(
709 parse_layout_box_sizing("border-box").unwrap(),
710 LayoutBoxSizing::BorderBox
711 );
712 assert_eq!(
713 parse_layout_box_sizing(" border-box ").unwrap(),
714 LayoutBoxSizing::BorderBox
715 );
716 }
717
718 #[test]
719 fn test_parse_layout_box_sizing_invalid() {
720 assert!(parse_layout_box_sizing("padding-box").is_err());
721 assert!(parse_layout_box_sizing("borderbox").is_err());
722 assert!(parse_layout_box_sizing("").is_err());
723 }
724}