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#[derive(Debug, 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)
131 | Some(CalcAstItem::Sub)
132 | Some(CalcAstItem::Mul)
133 | Some(CalcAstItem::Div)
134 | Some(CalcAstItem::BraceOpen)
135 );
136
137 if is_negative_number {
138 let rest = &input[i..];
140 let end = find_value_end(rest);
141 if end == 0 { return Err(()); }
142 let val_str = &rest[..end];
143 let pv = parse_pixel_value(val_str).map_err(|_| ())?;
144 items.push(CalcAstItem::Value(pv));
145 i += end;
146 } else {
147 items.push(CalcAstItem::Sub);
148 i += 1;
149 }
150 }
151 _ => {
152 let rest = &input[i..];
154 let end = find_value_end(rest);
155 if end == 0 { return Err(()); }
156 let val_str = &rest[..end];
157 let pv = parse_pixel_value(val_str).map_err(|_| ())?;
158 items.push(CalcAstItem::Value(pv));
159 i += end;
160 }
161 }
162 }
163
164 if items.is_empty() {
165 return Err(());
166 }
167
168 Ok(CalcAstItemVec::from(items))
169}
170
171#[cfg(feature = "parser")]
174fn find_value_end(s: &str) -> usize {
175 let bytes = s.as_bytes();
176 let mut i = 0;
177
178 if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
180 i += 1;
181 }
182
183 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
185 i += 1;
186 }
187
188 while i < bytes.len() && (bytes[i].is_ascii_alphabetic() || bytes[i] == b'%') {
190 i += 1;
191 }
192
193 i
194}
195
196fn calc_ast_to_css_string(items: &CalcAstItemVec) -> String {
198 let inner: Vec<String> = items.iter().map(|i| match i {
199 CalcAstItem::Value(v) => v.to_string(),
200 CalcAstItem::Add => "+".to_string(),
201 CalcAstItem::Sub => "-".to_string(),
202 CalcAstItem::Mul => "*".to_string(),
203 CalcAstItem::Div => "/".to_string(),
204 CalcAstItem::BraceOpen => "(".to_string(),
205 CalcAstItem::BraceClose => ")".to_string(),
206 }).collect();
207 alloc::format!("calc({})", inner.join(" "))
208}
209
210macro_rules! define_dimension_property {
213 ($struct_name:ident, $default_fn:expr) => {
214 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
215 #[repr(C)]
216 pub struct $struct_name {
217 pub inner: PixelValue,
218 }
219
220 impl Default for $struct_name {
221 fn default() -> Self {
222 $default_fn()
223 }
224 }
225
226 impl PixelValueTaker for $struct_name {
227 fn from_pixel_value(inner: PixelValue) -> Self {
228 Self { inner }
229 }
230 }
231
232 impl_pixel_value!($struct_name);
233
234 impl PrintAsCssValue for $struct_name {
235 fn print_as_css_value(&self) -> String {
236 self.inner.to_string()
237 }
238 }
239 };
240}
241
242macro_rules! define_sizing_enum {
243 ($name:ident) => {
244 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
245 #[repr(C, u8)]
246 #[derive(Default)]
247 pub enum $name {
248 #[default]
249 Auto,
250 Px(PixelValue),
251 MinContent,
252 MaxContent,
253 FitContent(PixelValue),
255 Calc(CalcAstItemVec),
257 }
258
259 impl PixelValueTaker for $name {
260 fn from_pixel_value(inner: PixelValue) -> Self {
261 $name::Px(inner)
262 }
263 }
264
265 impl PrintAsCssValue for $name {
266 fn print_as_css_value(&self) -> String {
267 match self {
268 $name::Auto => "auto".to_string(),
269 $name::Px(v) => v.to_string(),
270 $name::MinContent => "min-content".to_string(),
271 $name::MaxContent => "max-content".to_string(),
272 $name::FitContent(v) => alloc::format!("fit-content({})", v),
273 $name::Calc(items) => calc_ast_to_css_string(items),
274 }
275 }
276 }
277
278 impl $name {
279 pub fn px(value: f32) -> Self {
280 $name::Px(PixelValue::px(value))
281 }
282
283 pub const fn const_px(value: isize) -> Self {
284 $name::Px(PixelValue::const_px(value))
285 }
286
287 pub fn interpolate(&self, other: &Self, t: f32) -> Self {
288 match (self, other) {
289 ($name::Px(a), $name::Px(b)) => $name::Px(a.interpolate(b, t)),
290 ($name::FitContent(a), $name::FitContent(b)) => $name::FitContent(a.interpolate(b, t)),
291 (_, $name::Px(b)) if t >= 0.5 => $name::Px(*b),
292 ($name::Px(a), _) if t < 0.5 => $name::Px(*a),
293 ($name::Auto, $name::Auto) => $name::Auto,
294 (a, _) if t < 0.5 => a.clone(),
295 (_, b) => b.clone(),
296 }
297 }
298 }
299 };
300}
301
302define_sizing_enum!(LayoutWidth);
303define_sizing_enum!(LayoutHeight);
304
305define_dimension_property!(LayoutMinWidth, || Self {
307 inner: PixelValue::zero()
308});
309define_dimension_property!(LayoutMinHeight, || Self {
311 inner: PixelValue::zero()
312});
313define_dimension_property!(LayoutMaxWidth, || Self {
318 inner: PixelValue::px(core::f32::MAX)
319});
320define_dimension_property!(LayoutMaxHeight, || Self {
325 inner: PixelValue::px(core::f32::MAX)
326});
327
328#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
330#[repr(C)]
331#[derive(Default)]
332pub enum LayoutBoxSizing {
333 #[default]
334 ContentBox,
335 BorderBox,
336}
337
338
339impl PrintAsCssValue for LayoutBoxSizing {
340 fn print_as_css_value(&self) -> String {
341 String::from(match self {
342 LayoutBoxSizing::ContentBox => "content-box",
343 LayoutBoxSizing::BorderBox => "border-box",
344 })
345 }
346}
347
348#[cfg(feature = "parser")]
351pub mod parser {
352
353 use alloc::string::ToString;
354 use crate::corety::AzString;
355
356 use super::*;
357 use crate::props::basic::pixel::parse_pixel_value;
358
359 macro_rules! define_pixel_dimension_parser {
360 ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident) => {
361 #[derive(Clone, PartialEq)]
362 pub enum $error_name<'a> {
363 PixelValue(CssPixelValueParseError<'a>),
364 }
365
366 impl_debug_as_display!($error_name<'a>);
367 impl_display! { $error_name<'a>, {
368 PixelValue(e) => format!("{}", e),
369 }}
370
371 impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
372
373 #[derive(Debug, Clone, PartialEq)]
374 #[repr(C, u8)]
375 pub enum $error_owned_name {
376 PixelValue(CssPixelValueParseErrorOwned),
377 }
378
379 impl<'a> $error_name<'a> {
380 pub fn to_contained(&self) -> $error_owned_name {
381 match self {
382 $error_name::PixelValue(e) => {
383 $error_owned_name::PixelValue(e.to_contained())
384 }
385 }
386 }
387 }
388
389 impl $error_owned_name {
390 pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
391 match self {
392 $error_owned_name::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
393 }
394 }
395 }
396
397 pub fn $fn_name<'a>(input: &'a str) -> Result<$struct_name, $error_name<'a>> {
398 parse_pixel_value(input)
399 .map(|v| $struct_name { inner: v })
400 .map_err($error_name::PixelValue)
401 }
402 };
403 }
404
405 macro_rules! define_sizing_parser {
406 ($fn_name:ident, $enum_name:ident, $error_name:ident, $error_owned_name:ident, $keyword_label:expr) => {
407 #[derive(Clone, PartialEq)]
408 pub enum $error_name<'a> {
409 PixelValue(CssPixelValueParseError<'a>),
410 InvalidKeyword(&'a str),
411 }
412
413 impl_debug_as_display!($error_name<'a>);
414 impl_display! { $error_name<'a>, {
415 PixelValue(e) => format!("{}", e),
416 InvalidKeyword(k) => format!("Invalid {} keyword: \"{}\"", $keyword_label, k),
417 }}
418
419 impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
420
421 #[derive(Debug, Clone, PartialEq)]
422 #[repr(C, u8)]
423 pub enum $error_owned_name {
424 PixelValue(CssPixelValueParseErrorOwned),
425 InvalidKeyword(AzString),
426 }
427
428 impl<'a> $error_name<'a> {
429 pub fn to_contained(&self) -> $error_owned_name {
430 match self {
431 $error_name::PixelValue(e) => {
432 $error_owned_name::PixelValue(e.to_contained())
433 }
434 $error_name::InvalidKeyword(k) => {
435 $error_owned_name::InvalidKeyword(k.to_string().into())
436 }
437 }
438 }
439 }
440
441 impl $error_owned_name {
442 pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
443 match self {
444 $error_owned_name::PixelValue(e) => {
445 $error_name::PixelValue(e.to_shared())
446 }
447 $error_owned_name::InvalidKeyword(k) => {
448 $error_name::InvalidKeyword(k)
449 }
450 }
451 }
452 }
453
454 pub fn $fn_name<'a>(
455 input: &'a str,
456 ) -> Result<$enum_name, $error_name<'a>> {
457 let trimmed = input.trim();
458 match trimmed {
459 "auto" => Ok($enum_name::Auto),
460 "min-content" => Ok($enum_name::MinContent),
461 "max-content" => Ok($enum_name::MaxContent),
462 s if s.starts_with("fit-content(") && s.ends_with(')') => {
463 let inner = &s[12..s.len() - 1].trim();
464 parse_pixel_value(inner)
465 .map(|pv| {
466 if pv.number.get() < 0.0 {
467 $enum_name::FitContent(PixelValue::zero())
468 } else {
469 $enum_name::FitContent(pv)
470 }
471 })
472 .map_err($error_name::PixelValue)
473 }
474 s if s.starts_with("calc(") && s.ends_with(')') => {
475 let inner = &s[5..s.len() - 1];
476 parse_calc_expression(inner)
477 .map($enum_name::Calc)
478 .map_err(|_| $error_name::InvalidKeyword(input))
479 }
480 _ => parse_pixel_value(trimmed)
481 .map($enum_name::Px)
482 .map_err($error_name::PixelValue),
483 }
484 }
485 };
486 }
487
488 define_sizing_parser!(parse_layout_width, LayoutWidth, LayoutWidthParseError, LayoutWidthParseErrorOwned, "width");
489 define_sizing_parser!(parse_layout_height, LayoutHeight, LayoutHeightParseError, LayoutHeightParseErrorOwned, "height");
490 define_pixel_dimension_parser!(
491 parse_layout_min_width,
492 LayoutMinWidth,
493 LayoutMinWidthParseError,
494 LayoutMinWidthParseErrorOwned
495 );
496 define_pixel_dimension_parser!(
497 parse_layout_min_height,
498 LayoutMinHeight,
499 LayoutMinHeightParseError,
500 LayoutMinHeightParseErrorOwned
501 );
502 define_pixel_dimension_parser!(
503 parse_layout_max_width,
504 LayoutMaxWidth,
505 LayoutMaxWidthParseError,
506 LayoutMaxWidthParseErrorOwned
507 );
508 define_pixel_dimension_parser!(
509 parse_layout_max_height,
510 LayoutMaxHeight,
511 LayoutMaxHeightParseError,
512 LayoutMaxHeightParseErrorOwned
513 );
514
515 #[derive(Clone, PartialEq)]
518 pub enum LayoutBoxSizingParseError<'a> {
519 InvalidValue(&'a str),
520 }
521
522 impl_debug_as_display!(LayoutBoxSizingParseError<'a>);
523 impl_display! { LayoutBoxSizingParseError<'a>, {
524 InvalidValue(v) => format!("Invalid box-sizing value: \"{}\"", v),
525 }}
526
527 #[derive(Debug, Clone, PartialEq)]
528 #[repr(C, u8)]
529 pub enum LayoutBoxSizingParseErrorOwned {
530 InvalidValue(AzString),
531 }
532
533 impl<'a> LayoutBoxSizingParseError<'a> {
534 pub fn to_contained(&self) -> LayoutBoxSizingParseErrorOwned {
535 match self {
536 LayoutBoxSizingParseError::InvalidValue(s) => {
537 LayoutBoxSizingParseErrorOwned::InvalidValue(s.to_string().into())
538 }
539 }
540 }
541 }
542
543 impl LayoutBoxSizingParseErrorOwned {
544 pub fn to_shared<'a>(&'a self) -> LayoutBoxSizingParseError<'a> {
545 match self {
546 LayoutBoxSizingParseErrorOwned::InvalidValue(s) => {
547 LayoutBoxSizingParseError::InvalidValue(s)
548 }
549 }
550 }
551 }
552
553 pub fn parse_layout_box_sizing<'a>(
554 input: &'a str,
555 ) -> Result<LayoutBoxSizing, LayoutBoxSizingParseError<'a>> {
556 match input.trim() {
557 "content-box" => Ok(LayoutBoxSizing::ContentBox),
558 "border-box" => Ok(LayoutBoxSizing::BorderBox),
559 other => Err(LayoutBoxSizingParseError::InvalidValue(other)),
560 }
561 }
562}
563
564#[cfg(feature = "parser")]
565pub use self::parser::*;
566
567#[cfg(all(test, feature = "parser"))]
568mod tests {
569 use super::*;
570 use crate::props::basic::pixel::PixelValue;
571
572 #[test]
573 fn test_parse_layout_width() {
574 assert_eq!(
575 parse_layout_width("150px").unwrap(),
576 LayoutWidth::Px(PixelValue::px(150.0))
577 );
578 assert_eq!(
579 parse_layout_width("2.5em").unwrap(),
580 LayoutWidth::Px(PixelValue::em(2.5))
581 );
582 assert_eq!(
583 parse_layout_width("75%").unwrap(),
584 LayoutWidth::Px(PixelValue::percent(75.0))
585 );
586 assert_eq!(
587 parse_layout_width("0").unwrap(),
588 LayoutWidth::Px(PixelValue::px(0.0))
589 );
590 assert_eq!(
591 parse_layout_width(" 100pt ").unwrap(),
592 LayoutWidth::Px(PixelValue::pt(100.0))
593 );
594 assert_eq!(
595 parse_layout_width("min-content").unwrap(),
596 LayoutWidth::MinContent
597 );
598 assert_eq!(
599 parse_layout_width("max-content").unwrap(),
600 LayoutWidth::MaxContent
601 );
602 }
603
604 #[test]
605 fn test_parse_layout_height_invalid() {
606 assert!(parse_layout_height("auto").is_ok());
608 assert!(parse_layout_height("150 px").is_ok());
610 assert!(parse_layout_height("px").is_err());
611 assert!(parse_layout_height("invalid").is_err());
612 }
613
614 #[test]
615 fn test_parse_layout_box_sizing() {
616 assert_eq!(
617 parse_layout_box_sizing("content-box").unwrap(),
618 LayoutBoxSizing::ContentBox
619 );
620 assert_eq!(
621 parse_layout_box_sizing("border-box").unwrap(),
622 LayoutBoxSizing::BorderBox
623 );
624 assert_eq!(
625 parse_layout_box_sizing(" border-box ").unwrap(),
626 LayoutBoxSizing::BorderBox
627 );
628 }
629
630 #[test]
631 fn test_parse_layout_box_sizing_invalid() {
632 assert!(parse_layout_box_sizing("padding-box").is_err());
633 assert!(parse_layout_box_sizing("borderbox").is_err());
634 assert!(parse_layout_box_sizing("").is_err());
635 }
636}