1use crate::engine::bridge::text::{self, FallbackTextMeasurer, MeasuredFragment, TextMeasurer};
4use crate::engine::css::{
5 matcher::{ElementChain, ElementInfo},
6 values::{CssValue, Unit},
7};
8use crate::engine::html::HtmlNodeType;
9use crate::engine::tree::TreeNode;
10
11use std::cell::RefCell;
12use std::collections::HashMap;
13use std::rc::Rc;
14
15use ui_layout::{
16 AlignItems, BoxSizing, Display, FlexDirection, Fragment, InnerDisplay, ItemFragment,
17 JustifyContent, LayoutChild, LayoutNode, Length, LengthOrAuto, OuterDisplay, Style,
18};
19
20use super::css_resolver::ResolvedStyles;
21use super::types::{
22 Background, BorderStyle, Color, ColorStop, ContainerRole, ContainerStyle, FontStyle,
23 FontWeight, Gradient, GradientKind, InfoNode, LineHeight, NodeKind, RadialShape,
24 RadialSizeKind, TextAlign, TextDecoration, TextStyle, TextTransform,
25};
26
27const DEFAULT_LINE_FACTOR: f32 = 1.2;
28
29#[derive(Clone, Copy)]
34pub struct InheritedCss {
35 pub text_style: TextStyle,
36}
37
38fn length_to_px(len: &Length, font_size: f32) -> f32 {
40 match len {
41 Length::Px(v) => *v,
42 Length::Percent(v) => v * font_size / 100.0,
43 Length::Add(a, b) => length_to_px(a, font_size) + length_to_px(b, font_size),
44 Length::Sub(a, b) => length_to_px(a, font_size) - length_to_px(b, font_size),
45 Length::Mul(a, f) => length_to_px(a, font_size) * f,
46 Length::Div(a, f) => length_to_px(a, font_size) / f,
47 _ => font_size * DEFAULT_LINE_FACTOR,
48 }
49}
50
51pub fn build_layout_and_info(
92 dom: &Rc<RefCell<TreeNode<HtmlNodeType>>>,
93 resolved_styles: &ResolvedStyles,
94 measurer: &dyn text::TextMeasurer<TextStyle>,
95 parent: InheritedCss,
96 mut chain: ElementChain,
97) -> (LayoutNode, InfoNode) {
98 let html_node = dom.borrow().value.clone();
99
100 let mut text_style = parent.text_style;
101 let mut container_style = ContainerStyle::default();
102 let mut style = Style::default();
103
104 if let HtmlNodeType::Element {
108 tag_name,
109 attributes,
110 ..
111 } = &html_node
112 {
113 let id = attributes
114 .iter()
115 .find(|a| a.name == "id")
116 .map(|a| a.value.clone());
117
118 let class_list: Vec<String> = attributes
119 .iter()
120 .find(|attr| attr.name == "class")
121 .map(|attr| {
122 attr.value
123 .split_whitespace()
124 .map(|s| s.to_string())
125 .collect()
126 })
127 .unwrap_or_default();
128
129 chain.insert(
130 0,
131 ElementInfo {
132 tag_name: tag_name.clone(),
133 id,
134 classes: class_list,
135 },
136 );
137 }
138
139 let candidates: Option<HashMap<String, (CssValue, (u32, u32, u32), usize)>> =
143 if let HtmlNodeType::Element { .. } = &html_node {
144 Some(collect_candidates(resolved_styles, &chain))
145 } else {
146 None
147 };
148
149 if let Some(candidates) = &candidates {
153 for (name, (value, _, _)) in candidates {
154 if name.starts_with("--") {
155 continue;
156 }
157 apply_declaration(
158 name,
159 value,
160 &mut style,
161 &mut container_style,
162 &mut text_style,
163 );
164 }
165 }
166
167 style.line_height = match text_style.line_height {
174 LineHeight::Number(factor) => Length::Px(text_style.font_size * factor),
175 LineHeight::Normal => Length::Px(text_style.font_size * DEFAULT_LINE_FACTOR),
176 LineHeight::Px(px) => Length::Px(px),
177 };
178
179 let child = InheritedCss { text_style };
180
181 let (mut kind, inline_fragments_opt) = if let HtmlNodeType::Text(t) = &html_node {
182 let t = normalize_whitespace(t);
183 let t = match text_style.text_transform {
184 TextTransform::None => t,
185 TextTransform::Uppercase => t.to_ascii_uppercase(),
186 TextTransform::Lowercase => t.to_ascii_lowercase(),
187 };
188
189 let _t = std::time::Instant::now();
190 let measured = measurer
191 .measure(&text::TextMeasureRequest {
192 text: t.clone(),
193 style: text_style,
194 })
195 .expect("text measure failed");
196 let preview = if t.len() > 40 {
197 let cut = t.floor_char_boundary(40);
198 format!("{}...", &t[..cut])
199 } else {
200 t.clone()
201 };
202 log::info!(
203 target: "Layouter",
204 "measure inline: text={:?} len={} took={:?}",
205 preview,
206 t.len(),
207 _t.elapsed(),
208 );
209
210 let kind = NodeKind::Text {
211 texts: measured.iter().map(|f| f.text.clone()).collect(),
212 style: text_style,
213 };
214
215 let inline_fragments: Vec<ItemFragment> = measured
216 .into_iter()
217 .map(|f| {
218 ItemFragment::Fragment(Fragment {
219 width: f.width,
220 height: f.height,
221 })
222 })
223 .collect();
224
225 (kind, Some(inline_fragments))
226 } else if let Some(name) = html_node.tag_name()
227 && name == "a"
228 && let Some(href) = html_node.get_attr("href")
229 {
230 (
231 NodeKind::Container {
232 scroll_x: false,
233 scroll_y: false,
234 scroll_offset_x: 0.0,
235 scroll_offset_y: 0.0,
236 style: container_style,
237 role: ContainerRole::Link {
238 href: href.to_string(),
239 },
240 },
241 None,
242 )
243 } else {
244 (
245 NodeKind::Container {
246 scroll_x: false,
247 scroll_y: false,
248 scroll_offset_x: 0.0,
249 scroll_offset_y: 0.0,
250 style: container_style,
251 role: ContainerRole::Normal,
252 },
253 None,
254 )
255 };
256
257 let (layout, info) = if let Some(inline_fragments) = inline_fragments_opt {
259 let style = Style {
264 display: Display {
265 outer: OuterDisplay::Inline,
266 inner: InnerDisplay::Flow,
267 },
268 ..style
269 };
270
271 let layout = LayoutNode::with_children(style, inline_fragments);
272
273 let info = InfoNode {
274 kind,
275 children: vec![],
276 };
277
278 (layout, info)
279 } else {
280 let mut layout_children: Vec<LayoutChild> = Vec::new();
289 let mut info_children = Vec::new();
290
291 if style.display.outer != OuterDisplay::None {
292 match &html_node {
294 HtmlNodeType::Element { tag_name, .. }
295 if tag_name == "table"
296 || tag_name == "tbody"
297 || tag_name == "thead"
298 || tag_name == "tfoot" =>
299 {
300 style.display = Display {
301 outer: OuterDisplay::Block,
302 inner: InnerDisplay::Flex,
303 };
304 style.flex_direction = FlexDirection::Column;
305 }
306 HtmlNodeType::Element { tag_name, .. } if tag_name == "tr" => {
307 style.display = Display {
308 outer: OuterDisplay::Block,
309 inner: InnerDisplay::Flex,
310 };
311 style.flex_direction = FlexDirection::Row;
312 }
313 _ => {}
314 }
315
316 for child_dom in dom.borrow().children() {
317 let child_node = child_dom.borrow().value.clone();
318
319 if let HtmlNodeType::Text(t) = &child_node {
320 let t = normalize_whitespace(t);
321 let t = match text_style.text_transform {
322 TextTransform::None => t,
323 TextTransform::Uppercase => t.to_ascii_uppercase(),
324 TextTransform::Lowercase => t.to_ascii_lowercase(),
325 };
326
327 let _t = std::time::Instant::now();
328 let request = &text::TextMeasureRequest {
329 text: t.clone(),
330 style: text_style,
331 };
332 let measured = measurer.measure(request).unwrap_or_else(|_|
333 FallbackTextMeasurer.measure(request).unwrap());
335 let preview = if t.len() > 40 {
336 let cut = t.floor_char_boundary(40);
337 format!("{}...", &t[..cut])
338 } else {
339 t.clone()
340 };
341 log::info!(
342 target: "Layouter",
343 "measure child: text={:?} len={} took={:?}",
344 preview,
345 t.len(),
346 _t.elapsed(),
347 );
348
349 let text_kind = NodeKind::Text {
350 texts: measured.iter().map(|f| f.text.clone()).collect(),
351 style: text_style,
352 };
353
354 for fragment in &measured {
355 layout_children.push(generate_fragment_node(fragment).into());
356 }
357
358 info_children.push(InfoNode {
359 kind: text_kind,
360 children: vec![],
361 });
362 } else {
363 if child_dom.borrow().value.tag_name() == Some("br") {
364 layout_children.push(ItemFragment::LineBreak.into());
365 info_children.push(InfoNode {
366 kind: NodeKind::LineBreak,
367 children: vec![],
368 });
369 continue;
370 }
371
372 let (child_layout, child_info) = build_layout_and_info(
373 child_dom,
374 resolved_styles,
375 measurer,
376 child,
377 chain.clone(),
378 );
379
380 if dom.borrow().value.tag_name() == Some("html")
381 && child_dom.borrow().value.tag_name() == Some("body")
382 && let NodeKind::Container { style, .. } = &mut kind
383 && style.background == Background::Color(Color(0, 0, 0, 0))
384 {
385 let background = {
386 let NodeKind::Container { style, .. } = &child_info.kind else {
387 continue;
388 };
389 style.background.clone()
390 };
391 style.background = background;
394 }
395
396 layout_children.push(child_layout.into());
397 info_children.push(child_info);
398 }
399 }
400 }
401
402 let layout = LayoutNode::with_children(style, layout_children);
403
404 let info = InfoNode {
405 kind,
406 children: info_children,
407 };
408
409 (layout, info)
410 };
411
412 (layout, info)
413}
414
415fn normalize_whitespace(text: &str) -> String {
416 let mut result = String::new();
417 let mut prev_was_space = false;
418
419 for c in text.chars() {
420 if c.is_whitespace() {
421 if !prev_was_space {
422 result.push(' ');
423 prev_was_space = true;
424 }
425 } else {
426 result.push(c);
427 prev_was_space = false;
428 }
429 }
430
431 result
432}
433
434fn generate_fragment_node(fragment: &MeasuredFragment) -> ItemFragment {
435 if fragment.text == "\n" {
436 ItemFragment::LineBreak
437 } else {
438 ItemFragment::Fragment(Fragment {
439 width: fragment.width,
440 height: fragment.height,
441 })
442 }
443}
444
445fn collect_candidates(
446 resolved_styles: &ResolvedStyles,
447 chain: &ElementChain,
448) -> HashMap<String, (CssValue, (u32, u32, u32), usize)> {
449 let mut candidates: HashMap<String, (CssValue, (u32, u32, u32), usize)> = HashMap::new();
450
451 for decl in resolved_styles {
452 if decl.selector.matches(chain) {
453 let entry = candidates.get(&decl.name);
454
455 let should_replace = match entry {
456 None => true,
457 Some((_, spec, order)) => {
458 decl.specificity > *spec || (decl.specificity == *spec && decl.order > *order)
459 }
460 };
461
462 if should_replace {
463 candidates.insert(
464 decl.name.clone(),
465 (decl.value.clone(), decl.specificity, decl.order),
466 );
467 }
468 }
469 }
470
471 candidates
472}
473
474fn apply_declaration(
475 name: &str,
476 value: &CssValue,
477 style: &mut Style,
478 container_style: &mut ContainerStyle,
479 text_style: &mut TextStyle,
480) -> Option<()> {
481 fn expand_box<T: Clone, F>(
482 name: &str,
483 value: &CssValue,
484 text_style: &TextStyle,
485 resolve: &impl Fn(&str, &CssValue, &TextStyle) -> Option<T>,
486 mut set: F,
487 ) -> Option<()>
488 where
489 F: FnMut(T, T, T, T),
490 {
491 let resolve = |v: &CssValue| -> Option<T> { resolve(name, v, text_style) };
492
493 match value {
494 CssValue::List(values) => {
495 let vals: Vec<T> = values.iter().map(resolve).collect::<Option<_>>()?;
496
497 match vals.as_slice() {
498 [a] => set(a.clone(), a.clone(), a.clone(), a.clone()),
499 [v, h] => set(v.clone(), h.clone(), v.clone(), h.clone()),
500 [t, h, b] => set(t.clone(), h.clone(), b.clone(), h.clone()),
501 [t, r, b, l] => set(t.clone(), r.clone(), b.clone(), l.clone()),
502 _ => return None,
503 }
504 }
505
506 _ => {
507 let v = resolve(value)?;
508 set(v.clone(), v.clone(), v.clone(), v);
509 }
510 }
511
512 Some(())
513 }
514
515 fn parse_border_shorthand(
516 name: &str,
517 value: &CssValue,
518 text_style: &TextStyle,
519 ) -> Option<(Option<Length>, Option<BorderStyle>, Option<Color>)> {
520 let mut width: Option<Length> = None;
521 let mut style_v: Option<BorderStyle> = None;
522 let mut color_v: Option<Color> = None;
523
524 let items: Vec<&CssValue> = match value {
525 CssValue::List(values) => values.iter().collect(),
526 _ => vec![value],
527 };
528
529 for v in items {
530 let token = v;
531
532 if width.is_none()
534 && let Some(l) = resolve_css_len(name, token, text_style)
535 {
536 width = Some(l);
537 continue;
538 }
539
540 if width.is_none()
542 && let CssValue::Keyword(s) = token
543 {
544 match s.as_str().to_ascii_lowercase().as_str() {
545 "thin" => {
546 width = Some(Length::Px(1.0));
547 continue;
548 }
549 "medium" => {
550 width = Some(Length::Px(3.0));
551 continue;
552 }
553 "midium" => {
554 width = Some(Length::Px(3.0));
555 continue;
556 } "thick" => {
558 width = Some(Length::Px(5.0));
559 continue;
560 }
561 _ => {}
562 }
563 }
564
565 if style_v.is_none()
567 && let CssValue::Keyword(s) = token
568 {
569 let s_lower = s.as_str();
570 let parsed = match s_lower {
571 "none" => Some(BorderStyle::None),
572 "solid" => Some(BorderStyle::Solid),
573 "dashed" => Some(BorderStyle::Dashed),
574 "dotted" => Some(BorderStyle::Dotted),
575 "inset" | "outset" | "groove" | "ridge" | "double" | "hidden" => {
576 style_v = Some(BorderStyle::Solid);
578 continue;
579 }
580 _ => None,
581 };
582
583 if let Some(p) = parsed {
584 style_v = Some(p);
585 continue;
586 }
587 }
588
589 if color_v.is_none()
591 && let Some(c) = resolve_css_color(name, token)
592 {
593 color_v = Some(c);
594 continue;
595 }
596
597 }
599
600 Some((width, style_v, color_v))
601 }
602
603 match (name, value) {
604 ("display", CssValue::Keyword(v)) => {
608 if let Some(parsed_display) = Display::from_css_name(v.as_str()) {
609 style.display = parsed_display;
610 }
611 }
612
613 ("background-color", _) => {
617 container_style.background = match value {
618 CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => {
619 Background::Color(text_style.color)
620 }
621 CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("currentColor") => {
622 Background::Color(text_style.color)
623 }
624 CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("initial") => {
625 Background::Color(Color(0, 0, 0, 0))
626 }
627 _ => Background::Color(resolve_css_color(name, value)?),
628 };
629 }
630
631 ("background", _) => {
632 container_style.background = parse_background_shorthand(name, value, text_style)?;
633 }
634
635 ("color", _) => {
636 text_style.color = match value {
637 CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => {
638 text_style.color
640 }
641 CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("currentColor") => {
642 text_style.color
643 }
644 _ => resolve_css_color(name, value)?,
645 }
646 }
647
648 ("font-size", CssValue::Length(_, _)) => {
649 let len = resolve_css_len(name, value, text_style)?;
651 let px = match &len {
652 Length::Px(v) => *v,
653 Length::Percent(v) => *v * text_style.font_size / 100.0,
654 _ => {
655 log::error!(target: "Layouter", "Unknown size type for `{}`: {:?}", name, len);
656 return None;
657 }
658 };
659 text_style.font_size = px;
660 }
661
662 ("line-height", CssValue::Number(factor)) => {
663 text_style.line_height = LineHeight::Number(*factor);
664 style.line_height = Length::Px(text_style.font_size * factor);
665 }
666 ("line-height", CssValue::Keyword(v)) if v == "normal" => {
667 text_style.line_height = LineHeight::Normal;
668 style.line_height = Length::Px(text_style.font_size * DEFAULT_LINE_FACTOR);
669 }
670 ("line-height", _) => {
671 let len = resolve_css_len(name, value, text_style)?;
672 text_style.line_height = LineHeight::Px(length_to_px(&len, text_style.font_size));
673 style.line_height = len;
674 }
675
676 ("font-weight", CssValue::Keyword(v)) => {
677 text_style.font_weight = match v.as_str() {
678 "normal" => FontWeight::NORMAL,
679 "bold" => FontWeight::BOLD,
680 _ => text_style.font_weight,
681 };
682 }
683 ("font-weight", CssValue::Number(v)) => {
684 text_style.font_weight = FontWeight(*v as u16);
685 }
686
687 ("font-style", CssValue::Keyword(v)) => {
688 text_style.font_style = match v.as_str() {
689 "normal" => FontStyle::Normal,
690 "italic" => FontStyle::Italic,
691 "oblique" => FontStyle::Oblique,
692 _ => text_style.font_style,
693 };
694 }
695
696 ("text-decoration", CssValue::Keyword(v)) => {
697 text_style.text_decoration = match v.as_str() {
698 "none" => TextDecoration::None,
699 "underline" => TextDecoration::Underline,
700 "line-through" => TextDecoration::LineThrough,
701 "overline" => TextDecoration::Overline,
702 _ => TextDecoration::None,
703 };
704 }
705
706 ("text-transform", CssValue::Keyword(v)) => {
707 text_style.text_transform = match v.as_str() {
708 "none" => TextTransform::None,
709 "uppercase" => TextTransform::Uppercase,
710 "lowercase" => TextTransform::Lowercase,
711 _ => TextTransform::None,
712 };
713 }
714
715 ("text-align", CssValue::Keyword(v)) if v == "left" => {
716 text_style.text_align = TextAlign::Left;
717 }
718 ("text-align", CssValue::Keyword(v)) if v == "center" => {
719 text_style.text_align = TextAlign::Center;
720 }
721 ("text-align", CssValue::Keyword(v)) if v == "right" => {
722 text_style.text_align = TextAlign::Right;
723 }
724
725 ("box-sizing", CssValue::Keyword(v)) => {
729 style.box_sizing = match v.as_str() {
730 "content-box" => BoxSizing::ContentBox,
731 "border-box" => BoxSizing::BorderBox,
732 _ => BoxSizing::ContentBox,
733 };
734 }
735
736 ("border-style", CssValue::Keyword(v)) => {
737 let s = match v.as_str() {
738 "none" => BorderStyle::None,
739 "solid" => BorderStyle::Solid,
740 "dashed" => BorderStyle::Dashed,
741 "dotted" => BorderStyle::Dotted,
742 _ => BorderStyle::None,
743 };
744
745 container_style.border_style.top = s;
746 container_style.border_style.right = s;
747 container_style.border_style.bottom = s;
748 container_style.border_style.left = s;
749 }
750
751 ("margin", v) => {
752 expand_box(
753 name,
754 v,
755 text_style,
756 &|_, cv, ts| match cv {
757 CssValue::Keyword(s) if s == "auto" => Some(ui_layout::LengthOrAuto::Auto),
758 _ => resolve_css_len(name, cv, ts).map(ui_layout::LengthOrAuto::Length),
759 },
760 |t, r, b, l| {
761 style.spacing.margin_top = t;
762 style.spacing.margin_right = r;
763 style.spacing.margin_bottom = b;
764 style.spacing.margin_left = l;
765 },
766 )?;
767 }
768 ("margin-top", _) => {
769 style.spacing.margin_top = resolve_css_len_auto(name, value, text_style)?;
770 }
771 ("margin-right", _) => {
772 style.spacing.margin_right = resolve_css_len_auto(name, value, text_style)?;
773 }
774 ("margin-bottom", _) => {
775 style.spacing.margin_bottom = resolve_css_len_auto(name, value, text_style)?;
776 }
777 ("margin-left", _) => {
778 style.spacing.margin_left = resolve_css_len_auto(name, value, text_style)?;
779 }
780
781 ("border", v) => {
782 let (maybe_width, maybe_style, maybe_color) = if let CssValue::Keyword(k) = v
783 && (k.eq_ignore_ascii_case("inset") || k.eq_ignore_ascii_case("initial"))
784 {
785 (Some(Length::Px(0.0)), None, None)
786 } else {
787 parse_border_shorthand(name, v, text_style)?
788 };
789
790 if let Some(w) = maybe_width {
791 style.spacing.border_top = w.clone();
792 style.spacing.border_right = w.clone();
793 style.spacing.border_bottom = w.clone();
794 style.spacing.border_left = w;
795 }
796
797 if let Some(s) = maybe_style {
798 container_style.border_style.top = s;
799 container_style.border_style.right = s;
800 container_style.border_style.bottom = s;
801 container_style.border_style.left = s;
802 }
803
804 if let Some(c) = maybe_color {
805 container_style.border_color.top = c;
806 container_style.border_color.right = c;
807 container_style.border_color.bottom = c;
808 container_style.border_color.left = c;
809 }
810 }
811 ("border-top", _) => {
812 let (maybe_width, maybe_style, maybe_color) =
813 parse_border_shorthand(name, value, text_style)?;
814 if let Some(w) = maybe_width {
815 style.spacing.border_top = w;
816 }
817 if let Some(s) = maybe_style {
818 container_style.border_style.top = s;
819 }
820 if let Some(c) = maybe_color {
821 container_style.border_color.top = c;
822 }
823 }
824 ("border-right", _) => {
825 let (maybe_width, maybe_style, maybe_color) =
826 parse_border_shorthand(name, value, text_style)?;
827 if let Some(w) = maybe_width {
828 style.spacing.border_right = w;
829 }
830 if let Some(s) = maybe_style {
831 container_style.border_style.right = s;
832 }
833 if let Some(c) = maybe_color {
834 container_style.border_color.right = c;
835 }
836 }
837 ("border-bottom", _) => {
838 let (maybe_width, maybe_style, maybe_color) =
839 parse_border_shorthand(name, value, text_style)?;
840 if let Some(w) = maybe_width {
841 style.spacing.border_bottom = w;
842 }
843 if let Some(s) = maybe_style {
844 container_style.border_style.bottom = s;
845 }
846 if let Some(c) = maybe_color {
847 container_style.border_color.bottom = c;
848 }
849 }
850 ("border-left", _) => {
851 let (maybe_width, maybe_style, maybe_color) =
852 parse_border_shorthand(name, value, text_style)?;
853 if let Some(w) = maybe_width {
854 style.spacing.border_left = w;
855 }
856 if let Some(s) = maybe_style {
857 container_style.border_style.left = s;
858 }
859 if let Some(c) = maybe_color {
860 container_style.border_color.left = c;
861 }
862 }
863
864 ("padding", v) => {
865 expand_box(
866 name,
867 v,
868 text_style,
869 &|_, v, ts| resolve_css_len(name, v, ts),
870 |t, r, b, l| {
871 style.spacing.padding_top = t;
872 style.spacing.padding_right = r;
873 style.spacing.padding_bottom = b;
874 style.spacing.padding_left = l;
875 },
876 )?;
877 }
878 ("padding-top", _) => {
879 style.spacing.padding_top = resolve_css_len(name, value, text_style)?;
880 }
881 ("padding-right", _) => {
882 style.spacing.padding_right = resolve_css_len(name, value, text_style)?;
883 }
884 ("padding-bottom", _) => {
885 style.spacing.padding_bottom = resolve_css_len(name, value, text_style)?;
886 }
887 ("padding-left", _) => {
888 style.spacing.padding_left = resolve_css_len(name, value, text_style)?;
889 }
890
891 ("width", _) => {
895 style.size.width = resolve_css_len_auto(name, value, text_style)?;
896 }
897 ("height", _) => {
898 style.size.height = resolve_css_len_auto(name, value, text_style)?;
899 }
900 ("min-width", _) => {
901 style.size.min_width = resolve_css_len_auto(name, value, text_style)?;
902 }
903 ("min-height", _) => {
904 style.size.min_height = resolve_css_len_auto(name, value, text_style)?;
905 }
906 ("max-width", _) => {
907 style.size.max_width = resolve_css_len_auto(name, value, text_style)?;
908 }
909 ("max-height", _) => {
910 style.size.max_height = resolve_css_len_auto(name, value, text_style)?;
911 }
912
913 ("flex-direction", CssValue::Keyword(v)) => {
917 style.flex_direction = match v.as_str() {
918 "row" => FlexDirection::Row,
919 "column" => FlexDirection::Column,
920 "row-reverse" => FlexDirection::RowReverse,
921 "column-reverse" => FlexDirection::ColumnReverse,
922 _ => return None,
923 };
924 }
925
926 ("justify-content", CssValue::Keyword(v)) => {
927 style.justify_content = match v.as_str() {
928 "flex-start" | "start" => JustifyContent::Start,
929 "center" => JustifyContent::Center,
930 "flex-end" | "end" => JustifyContent::End,
931 "space-between" => JustifyContent::SpaceBetween,
932 "space-around" => JustifyContent::SpaceAround,
933 "space-evenly" => JustifyContent::SpaceEvenly,
934 _ => return None,
935 };
936 }
937
938 ("align-items", CssValue::Keyword(v)) => {
939 style.align_items = match v.as_str() {
940 "stretch" => AlignItems::Stretch,
941 "flex-start" | "start" => AlignItems::Start,
942 "center" => AlignItems::Center,
943 "flex-end" | "end" => AlignItems::End,
944 _ => return None,
945 };
946 }
947
948 ("gap", _) => match value {
949 CssValue::List(l) if l.len() == 2 => {
950 let mut l = l.iter();
951 let gap = resolve_css_len_auto(name, l.next()?, text_style)?;
952 style.row_gap = gap;
953 let gap = resolve_css_len_auto(name, l.next()?, text_style)?;
954 style.column_gap = gap;
955 }
956 CssValue::Length(_, _) => {
957 let gap = resolve_css_len_auto(name, value, text_style)?;
958 style.row_gap = gap.clone();
959 style.column_gap = gap;
960 }
961 _ => {}
962 },
963
964 ("flex-grow", CssValue::Number(v)) => {
965 style.item_style.flex_grow = *v;
966 }
967
968 ("flex-basis", _) => {
969 style.item_style.flex_basis = resolve_css_len_auto(name, value, text_style)?;
970 }
971
972 _ => {
973 }
975 }
976 Some(())
977}
978
979fn parse_background_shorthand(
984 name: &str,
985 value: &CssValue,
986 text_style: &TextStyle,
987) -> Option<Background> {
988 let items: Vec<&CssValue> = match value {
989 CssValue::List(values) => values.iter().collect(),
990 _ => vec![value],
991 };
992
993 let mut maybe_color: Option<Color> = None;
994 let mut maybe_gradient: Option<Gradient> = None;
995
996 for v in items {
997 if let CssValue::Keyword(kw) = v {
999 if kw.eq_ignore_ascii_case("inherit") {
1000 maybe_color = Some(text_style.color);
1001 continue;
1002 }
1003 if kw.eq_ignore_ascii_case("currentColor") {
1004 maybe_color = Some(text_style.color);
1005 continue;
1006 }
1007 }
1008
1009 if let CssValue::Number(0.0) = v {
1010 maybe_color = Some(Color(0, 0, 0, 0));
1011 continue;
1012 }
1013
1014 if let CssValue::Function(fn_name, args) = v {
1016 if fn_name == "linear-gradient" || fn_name == "radial-gradient" {
1017 maybe_gradient = Some(parse_gradient(fn_name, args, text_style)?);
1018 continue;
1019 }
1020 }
1021
1022 if let Some(c) = resolve_css_color(name, v) {
1024 maybe_color = Some(c);
1025 continue;
1026 }
1027 }
1028
1029 if let Some(g) = maybe_gradient {
1030 return Some(Background::Gradient(g));
1031 }
1032 if let Some(c) = maybe_color {
1033 return Some(Background::Color(c));
1034 }
1035
1036 None
1037}
1038
1039fn parse_gradient(fn_name: &str, args: &[CssValue], text_style: &TextStyle) -> Option<Gradient> {
1044 match fn_name {
1045 "linear-gradient" => parse_linear_gradient(args, text_style),
1046 "radial-gradient" => parse_radial_gradient(args, text_style),
1047 _ => None,
1048 }
1049}
1050
1051fn parse_linear_gradient(args: &[CssValue], _text_style: &TextStyle) -> Option<Gradient> {
1052 if args.is_empty() {
1053 return None;
1054 }
1055
1056 let (skip, angle) = parse_linear_direction(args);
1057 let angle = angle.unwrap_or(180.0);
1058 let stops = parse_color_stops(&args[skip..])?;
1059
1060 Some(Gradient {
1061 kind: GradientKind::Linear { angle },
1062 stops,
1063 })
1064}
1065
1066fn parse_linear_direction(args: &[CssValue]) -> (usize, Option<f32>) {
1068 if args.is_empty() {
1069 return (0, None);
1070 }
1071
1072 if let CssValue::Length(v, Unit::Deg) = &args[0] {
1074 return (1, Some(*v));
1075 }
1076
1077 if let CssValue::Keyword(k) = &args[0] {
1079 if k.as_str() == "to" && args.len() > 1 {
1080 let mut idx = 1;
1081 let mut sides: Vec<&str> = Vec::new();
1082 while idx < args.len() {
1083 if let CssValue::Keyword(k) = &args[idx] {
1084 match k.as_str() {
1085 "top" | "bottom" | "left" | "right" => {
1086 sides.push(k.as_str());
1087 idx += 1;
1088 }
1089 _ => break,
1090 }
1091 } else {
1092 break;
1093 }
1094 }
1095 if !sides.is_empty() {
1096 let angle = match sides.as_slice() {
1097 ["top"] => Some(0.0),
1098 ["top", "left"] => Some(315.0),
1099 ["top", "right"] => Some(45.0),
1100 ["bottom"] => Some(180.0),
1101 ["bottom", "left"] => Some(225.0),
1102 ["bottom", "right"] => Some(135.0),
1103 ["left"] => Some(270.0),
1104 ["right"] => Some(90.0),
1105 _ => None,
1106 };
1107 return (idx, angle);
1108 }
1109 }
1110 }
1111
1112 (0, None)
1113}
1114
1115fn parse_radial_gradient(args: &[CssValue], _text_style: &TextStyle) -> Option<Gradient> {
1116 let mut shape = RadialShape::Ellipse;
1117 let mut size = RadialSizeKind::default();
1118 let mut position = (0.5f32, 0.5f32);
1119
1120 let mut idx = 0;
1121
1122 while idx < args.len() {
1124 if let CssValue::Keyword(k) = &args[idx] {
1125 match k.as_str() {
1126 "circle" => {
1127 shape = RadialShape::Circle;
1128 idx += 1;
1129 continue;
1130 }
1131 "ellipse" => {
1132 shape = RadialShape::Ellipse;
1133 idx += 1;
1134 continue;
1135 }
1136 "closest-side" => {
1137 size = RadialSizeKind::ClosestSide;
1138 idx += 1;
1139 continue;
1140 }
1141 "farthest-side" => {
1142 size = RadialSizeKind::FarthestSide;
1143 idx += 1;
1144 continue;
1145 }
1146 "closest-corner" => {
1147 size = RadialSizeKind::ClosestCorner;
1148 idx += 1;
1149 continue;
1150 }
1151 "farthest-corner" => {
1152 size = RadialSizeKind::FarthestCorner;
1153 idx += 1;
1154 continue;
1155 }
1156 _ => break,
1157 }
1158 } else {
1159 break;
1160 }
1161 }
1162
1163 if idx < args.len() && args[idx] == CssValue::Keyword("at".into()) {
1165 idx += 1; if idx < args.len() {
1167 if let CssValue::Keyword(k) = &args[idx] {
1168 match k.as_str() {
1170 "center" => position = (0.5, 0.5),
1171 "top" => position = (0.5, 0.0),
1172 "bottom" => position = (0.5, 1.0),
1173 "left" => position = (0.0, 0.5),
1174 "right" => position = (1.0, 0.5),
1175 _ => {} }
1177 idx += 1;
1178 if idx < args.len() {
1180 if let CssValue::Keyword(k2) = &args[idx] {
1181 match (k.as_str(), k2.as_str()) {
1182 ("top", "left") | ("left", "top") => position = (0.0, 0.0),
1183 ("top", "right") | ("right", "top") => position = (1.0, 0.0),
1184 ("bottom", "left") | ("left", "bottom") => position = (0.0, 1.0),
1185 ("bottom", "right") | ("right", "bottom") => position = (1.0, 1.0),
1186 _ => {}
1187 }
1188 idx += 1;
1189 }
1190 }
1191 }
1192 }
1193 }
1194
1195 let stops = parse_color_stops(&args[idx..])?;
1196 if stops.is_empty() {
1197 return None;
1198 }
1199 Some(Gradient {
1200 kind: GradientKind::Radial {
1201 shape,
1202 size,
1203 position,
1204 },
1205 stops,
1206 })
1207}
1208
1209fn parse_color_stops(args: &[CssValue]) -> Option<Vec<ColorStop>> {
1210 let mut stops = Vec::new();
1211 let mut i = 0;
1212
1213 while i < args.len() {
1214 let color = resolve_css_color("gradient", &args[i])?;
1215 i += 1;
1216
1217 let position = if i < args.len() {
1218 match &args[i] {
1219 CssValue::Length(v, Unit::Percent) => {
1220 i += 1;
1221 Some((*v / 100.0).clamp(0.0, 1.0))
1222 }
1223 CssValue::Length(_v, Unit::Px) => {
1224 i += 1;
1225 None
1226 }
1227 _ => None,
1228 }
1229 } else {
1230 None
1231 };
1232
1233 stops.push(ColorStop { color, position });
1234 }
1235
1236 Some(stops)
1237}
1238
1239fn resolve_css_len_auto(
1241 name: &str,
1242 css_len: &CssValue,
1243 text_style: &TextStyle,
1244) -> Option<LengthOrAuto> {
1245 match &css_len {
1246 CssValue::Keyword(s) if s == "auto" => Some(LengthOrAuto::Auto),
1247 _ => resolve_css_len(name, css_len, text_style).map(|l| l.into()),
1248 }
1249}
1250
1251fn resolve_css_len(name: &str, css_len: &CssValue, text_style: &TextStyle) -> Option<Length> {
1253 match &css_len {
1254 CssValue::Length(v, Unit::Em) => Some(Length::Px(text_style.font_size * v)),
1255 CssValue::Length(v, Unit::Rem) => Some(Length::Px(16.0 * v)), CssValue::Length(v, u) => match u {
1257 Unit::Percent => Some(Length::Percent(*v)),
1258 Unit::Px => Some(Length::Px(*v)),
1259 Unit::Vw => Some(Length::Vw(*v)),
1260 Unit::Vh => Some(Length::Vh(*v)),
1261 Unit::Em | Unit::Rem => unreachable!(),
1262 Unit::Deg => {
1263 log::error!(target: "Layouter", "Unexpected deg unit for `{}` (expected length)", name);
1264 return None;
1265 }
1266 },
1267 CssValue::Number(0.0) => Some(Length::Px(0.0)),
1268 CssValue::Keyword(_) => None,
1269 CssValue::Function(fn_name, args) if fn_name == "calc" && !args.is_empty() => {
1270 let mut iter = args.iter();
1271 let mut result = resolve_css_len(name, iter.next().unwrap(), text_style)?;
1272
1273 while let (Some(op), Some(val)) = (iter.next(), iter.next()) {
1274 match op {
1275 CssValue::Keyword(o) if o == "+" => {
1276 let val_resolved = resolve_css_len(name, val, text_style)?;
1277 result = Length::Add(Box::new(result), Box::new(val_resolved));
1278 }
1279 CssValue::Keyword(o) if o == "-" => {
1280 let val_resolved = resolve_css_len(name, val, text_style)?;
1281 result = Length::Sub(Box::new(result), Box::new(val_resolved));
1282 }
1283 CssValue::Keyword(o) if o == "*" => {
1284 if let CssValue::Number(factor) = val {
1285 result = Length::Mul(Box::new(result), *factor);
1286 } else {
1287 log::error!(target: "Layouter", "Invalid operand for multiplication in calc() for `{}`: {:?}", name, val);
1288 return None;
1289 }
1290 }
1291 CssValue::Keyword(o) if o == "/" => {
1292 if let CssValue::Number(factor) = val {
1293 if *factor == 0.0 {
1294 log::error!(target: "Layouter", "Division by zero in calc() for `{}`", name);
1295 return None;
1296 }
1297 result = Length::Div(Box::new(result), *factor);
1298 } else {
1299 log::error!(target: "Layouter", "Invalid operand for division in calc() for `{}`: {:?}", name, val);
1300 return None;
1301 }
1302 }
1303 _ => {
1304 log::error!(target: "Layouter", "Unknown operator in calc() for `{}`: {:?}", name, op);
1305 return None;
1306 }
1307 }
1308 }
1309
1310 Some(result)
1311 }
1312 CssValue::Color(_) => None,
1313 _ => {
1314 log::error!(target: "Layouter", "Unknown CSS Length type for `{}`: {:?}", name, css_len);
1315 None
1316 }
1317 }
1318}
1319
1320fn resolve_css_color(name: &str, css_color: &CssValue) -> Option<Color> {
1328 fn keyword_color_to_color(name: &str, keyword: &str) -> Option<Color> {
1329 match keyword.to_ascii_lowercase().as_str() {
1333 "black" => Some(Color(0, 0, 0, 255)),
1335 "silver" => Some(Color(192, 192, 192, 255)),
1336 "gray" | "grey" => Some(Color(128, 128, 128, 255)),
1337 "white" => Some(Color(255, 255, 255, 255)),
1338
1339 "maroon" => Some(Color(128, 0, 0, 255)),
1341 "red" => Some(Color(255, 0, 0, 255)),
1342 "firebrick" => Some(Color(178, 34, 34, 255)),
1343 "crimson" => Some(Color(220, 20, 60, 255)),
1344 "indianred" => Some(Color(205, 92, 92, 255)),
1345 "lightcoral" => Some(Color(240, 128, 128, 255)),
1346 "salmon" => Some(Color(250, 128, 114, 255)),
1347 "darksalmon" => Some(Color(233, 150, 122, 255)),
1348 "lightsalmon" => Some(Color(255, 160, 122, 255)),
1349
1350 "pink" => Some(Color(255, 192, 203, 255)),
1352 "lightpink" => Some(Color(255, 182, 193, 255)),
1353 "hotpink" => Some(Color(255, 105, 180, 255)),
1354 "deeppink" => Some(Color(255, 20, 147, 255)),
1355 "palevioletred" => Some(Color(219, 112, 147, 255)),
1356 "magenta" | "fuchsia" => Some(Color(255, 0, 255, 255)),
1357
1358 "coral" => Some(Color(255, 127, 80, 255)),
1360 "tomato" => Some(Color(255, 99, 71, 255)),
1361 "orangered" => Some(Color(255, 69, 0, 255)),
1362 "orange" => Some(Color(255, 165, 0, 255)),
1363
1364 "gold" => Some(Color(255, 215, 0, 255)),
1366 "yellow" => Some(Color(255, 255, 0, 255)),
1367 "lightyellow" => Some(Color(255, 255, 224, 255)),
1368 "lemonchiffon" => Some(Color(255, 250, 205, 255)),
1369 "lightgoldenrodyellow" => Some(Color(250, 250, 210, 255)),
1370 "papayawhip" => Some(Color(255, 239, 213, 255)),
1371 "moccasin" => Some(Color(255, 228, 181, 255)),
1372
1373 "green" => Some(Color(0, 128, 0, 255)),
1375 "darkgreen" => Some(Color(0, 100, 0, 255)),
1376 "forestgreen" => Some(Color(34, 139, 34, 255)),
1377 "lime" => Some(Color(0, 255, 0, 255)),
1378 "limegreen" => Some(Color(50, 205, 50, 255)),
1379 "lightgreen" => Some(Color(144, 238, 144, 255)),
1380 "palegreen" => Some(Color(152, 251, 152, 255)),
1381 "springgreen" => Some(Color(0, 255, 127, 255)),
1382 "seagreen" => Some(Color(46, 139, 87, 255)),
1383 "mediumseagreen" => Some(Color(60, 179, 113, 255)),
1384 "yellowgreen" => Some(Color(154, 205, 50, 255)),
1385
1386 "aqua" | "cyan" => Some(Color(0, 255, 255, 255)),
1388 "lightcyan" => Some(Color(224, 255, 255, 255)),
1389 "paleturquoise" => Some(Color(175, 238, 238, 255)),
1390 "turquoise" => Some(Color(64, 224, 208, 255)),
1391 "mediumturquoise" => Some(Color(72, 209, 204, 255)),
1392
1393 "blue" => Some(Color(0, 0, 255, 255)),
1395 "mediumblue" => Some(Color(0, 0, 205, 255)),
1396 "darkblue" => Some(Color(0, 0, 139, 255)),
1397 "navy" => Some(Color(0, 0, 128, 255)),
1398 "royalblue" => Some(Color(65, 105, 225, 255)),
1399 "cornflowerblue" => Some(Color(100, 149, 237, 255)),
1400 "skyblue" => Some(Color(135, 206, 235, 255)),
1401 "lightblue" => Some(Color(173, 216, 230, 255)),
1402 "deepskyblue" => Some(Color(0, 191, 255, 255)),
1403
1404 "purple" => Some(Color(128, 0, 128, 255)),
1406 "indigo" => Some(Color(75, 0, 130, 255)),
1407 "violet" => Some(Color(238, 130, 238, 255)),
1408 "plum" => Some(Color(221, 160, 221, 255)),
1409 "orchid" => Some(Color(218, 112, 214, 255)),
1410 "mediumpurple" => Some(Color(147, 112, 219, 255)),
1411 "rebeccapurple" => Some(Color(102, 51, 153, 255)),
1412
1413 "brown" => Some(Color(165, 42, 42, 255)),
1415 "saddlebrown" => Some(Color(139, 69, 19, 255)),
1416 "sienna" => Some(Color(160, 82, 45, 255)),
1417 "chocolate" => Some(Color(210, 105, 30, 255)),
1418 "peru" => Some(Color(205, 133, 63, 255)),
1419 "burlywood" => Some(Color(222, 184, 135, 255)),
1420
1421 "snow" => Some(Color(255, 250, 250, 255)),
1423 "honeydew" => Some(Color(240, 255, 240, 255)),
1424 "mintcream" => Some(Color(245, 255, 250, 255)),
1425 "azure" => Some(Color(240, 255, 255, 255)),
1426 "aliceblue" => Some(Color(240, 248, 255, 255)),
1427 "ghostwhite" => Some(Color(248, 248, 255, 255)),
1428
1429 "gainsboro" => Some(Color(220, 220, 220, 255)),
1431 "lightgray" | "lightgrey" => Some(Color(211, 211, 211, 255)),
1432 "darkgray" | "darkgrey" => Some(Color(169, 169, 169, 255)),
1433 "dimgray" | "dimgrey" => Some(Color(105, 105, 105, 255)),
1434 "lightslategray" | "lightslategrey" => Some(Color(119, 136, 153, 255)),
1435 "slategray" | "slategrey" => Some(Color(112, 128, 144, 255)),
1436
1437 "buttonface" => Some(Color(240, 240, 240, 255)),
1439 "buttontext" => Some(Color(0, 0, 0, 255)),
1440
1441 "linktext" => Some(Color(0, 0, 238, 255)),
1442 "visitedtext" => Some(Color(85, 26, 139, 255)),
1443 "activetext" => Some(Color(255, 0, 0, 255)),
1444
1445 "canvas" => Some(Color(255, 255, 255, 255)),
1446 "canvastext" => Some(Color(0, 0, 0, 255)),
1447
1448 "field" => Some(Color(255, 255, 255, 255)),
1449 "fieldtext" => Some(Color(0, 0, 0, 255)),
1450
1451 "highlight" => Some(Color(0, 120, 215, 255)),
1452 "highlighttext" => Some(Color(255, 255, 255, 255)),
1453
1454 "graytext" => Some(Color(128, 128, 128, 255)),
1455
1456 "transparent" => Some(Color(0, 0, 0, 0)),
1458 "none" => Some(Color(0, 0, 0, 0)),
1459
1460 _ => {
1461 log::error!(target: "Layouter", "Unknown CSS color keyword `{}` for `{}`", keyword, name);
1462 None
1463 }
1464 }
1465 }
1466
1467 fn hsla_to_rgba(h: f32, s: f32, l: f32, a: f32) -> (u8, u8, u8, u8) {
1469 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
1471 let h_prime = h / 60.0;
1472 let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
1473
1474 let (r1, g1, b1) = match h_prime as u32 {
1476 0 => (c, x, 0.0),
1477 1 => (x, c, 0.0),
1478 2 => (0.0, c, x),
1479 3 => (0.0, x, c),
1480 4 => (x, 0.0, c),
1481 5 | 6 => (c, 0.0, x),
1482 _ => (0.0, 0.0, 0.0),
1483 };
1484
1485 let m = l - c / 2.0;
1487 let r = ((r1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1488 let g = ((g1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1489 let b = ((b1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1490 let a = (a * 255.0).round().clamp(0.0, 255.0) as u8;
1491
1492 (r, g, b, a)
1493 }
1494
1495 match css_color {
1496 CssValue::Color(_) => {
1498 let (r, g, b, a) = css_color.to_rgba_tuple()?;
1499 Some(Color(r, g, b, a))
1500 }
1501
1502 CssValue::Keyword(value) => keyword_color_to_color(name, value),
1504
1505 CssValue::Function(func, args) if func == "rgb" || func == "rgba" => {
1507 let mut numbers = Vec::new();
1509 let mut alpha: Option<f32> = None;
1510 let mut after_slash = false;
1511
1512 for arg in args {
1513 match arg {
1514 CssValue::Keyword(k) if k == "/" => {
1515 after_slash = true;
1516 }
1517 CssValue::Number(n) => {
1518 if after_slash {
1519 alpha = Some(*n);
1520 } else {
1521 numbers.push(*n);
1522 }
1523 }
1524 _ => return None,
1525 }
1526 }
1527
1528 if numbers.len() != 3 {
1529 return None;
1530 }
1531
1532 let a = alpha.unwrap_or(1.0);
1533
1534 Some(Color(
1535 (numbers[0] * 255.0).round() as u8,
1536 (numbers[1] * 255.0).round() as u8,
1537 (numbers[2] * 255.0).round() as u8,
1538 (a * 255.0).round() as u8,
1539 ))
1540 }
1541
1542 CssValue::Function(func, args) if func == "hsl" || func == "hsla" => {
1544 let mut numbers = Vec::new();
1546 let mut alpha: Option<f32> = None;
1547 let mut after_slash = false;
1548
1549 for arg in args {
1550 match arg {
1551 CssValue::Keyword(k) if k == "/" => {
1552 after_slash = true;
1553 }
1554 CssValue::Number(n) => {
1555 if after_slash {
1556 alpha = Some(*n);
1557 } else {
1558 numbers.push(*n);
1559 }
1560 }
1561 _ => return None,
1562 }
1563 }
1564
1565 if numbers.len() != 3 {
1566 return None;
1567 }
1568
1569 let a = alpha.unwrap_or(1.0);
1570 let (r, g, b, a) = hsla_to_rgba(numbers[0], numbers[1], numbers[2], a);
1571
1572 Some(Color(r, g, b, a))
1573 }
1574
1575 _ => {
1577 log::error!(
1578 target: "Layouter",
1579 "Unexpected CSS color value for `{}` at layout stage: {:?}",
1580 name,
1581 css_color
1582 );
1583 None
1584 }
1585 }
1586}