1use ratatui::{
13 layout::{Alignment, Constraint, Rect},
14 style::Style as RStyle,
15 widgets::Block,
16};
17
18use crate::box_model::{BorderStyle, BorderStyleValue, BoxEdges, BoxEdgesValue, Length};
19use crate::cache::{node_signature, ComputeCache};
20use crate::color::Color;
21use crate::media::MediaContext;
22use crate::node::{Classes, StyledNode};
23use crate::selector::NodeIdentity;
24use crate::style::CssStyle;
25use crate::stylesheet::Stylesheet;
26use crate::token::{self, ThemeTokens};
27
28#[derive(Debug, Clone, Default, PartialEq)]
32pub struct ComputedStyle {
33 pub style: CssStyle,
34}
35
36impl ComputedStyle {
37 pub fn new(style: CssStyle) -> Self {
38 Self { style }
39 }
40 pub fn to_style(&self) -> RStyle {
41 self.style.to_style()
42 }
43 pub fn to_block(&self) -> Block<'_> {
44 self.style.to_block()
45 }
46 pub fn apply_margin(&self, area: Rect) -> Rect {
47 self.style.apply_margin(area)
48 }
49 pub fn constraints(&self) -> Option<(Constraint, Constraint)> {
50 self.style.constraints()
51 }
52 pub fn alignment(&self) -> Option<Alignment> {
53 self.style.alignment()
54 }
55
56 pub fn apply_inline(&mut self, inline: &CssStyle) {
68 self.style.overlay(inline);
69 }
70
71 pub fn with_inline(mut self, inline: &CssStyle) -> Self {
74 self.apply_inline(inline);
75 self
76 }
77
78 pub fn layout(&self, area: Rect) -> (Block<'_>, RStyle, Rect) {
104 let shrunk = self.apply_margin(area);
105 self.layout_with_shrunk(shrunk)
106 }
107
108 fn layout_with_shrunk(&self, shrunk: Rect) -> (Block<'_>, RStyle, Rect) {
117 let block = self.to_block();
118 let inner = block.inner(shrunk);
119 let style = self.to_style();
120 (block, style, inner)
121 }
122}
123
124pub fn render_computed<W, F>(
145 frame: &mut ratatui::Frame<'_>,
146 computed: &ComputedStyle,
147 area: Rect,
148 make: F,
149) where
150 F: FnOnce(Rect, RStyle) -> W,
151 W: ratatui::widgets::Widget,
152{
153 let shrunk = computed.apply_margin(area);
154 let (block, style, inner) = computed.layout_with_shrunk(shrunk);
155 frame.render_widget(block, shrunk);
156 frame.render_widget(make(inner, style), inner);
157}
158
159pub struct CascadeContext<'s> {
211 sheet: &'s Stylesheet,
212 scratch: ComputeScratch,
213 stack: Vec<ComputedStyle>,
214 identity_stack: Vec<NodeIdentity>,
220 siblings: Vec<Vec<NodeIdentity>>,
228 media: MediaContext,
234 cache: Option<ComputeCache>,
239 sig_stack: Vec<u64>,
243}
244
245impl<'s> CascadeContext<'s> {
246 pub fn new(sheet: &'s Stylesheet) -> Self {
249 Self {
250 sheet,
251 scratch: ComputeScratch::new(),
252 stack: Vec::new(),
253 identity_stack: Vec::new(),
254 siblings: Vec::new(),
255 media: MediaContext::default(),
256 cache: None,
257 sig_stack: Vec::new(),
258 }
259 }
260
261 pub fn set_media(&mut self, media: MediaContext) -> &mut Self {
265 self.media = media;
266 self
267 }
268
269 pub fn with_media(mut self, media: MediaContext) -> Self {
271 self.media = media;
272 self
273 }
274
275 pub fn with_cache(mut self, capacity: usize) -> Self {
292 self.cache = Some(ComputeCache::new(capacity));
293 self
294 }
295
296 pub fn cache(&self) -> Option<&ComputeCache> {
299 self.cache.as_ref()
300 }
301
302 pub fn media(&self) -> &MediaContext {
304 &self.media
305 }
306
307 pub fn enter(&mut self, node: &dyn StyledNode) -> ComputedStyle {
320 let parent = self.stack.last();
321 let has_comb = self.sheet.has_combinators();
322 let depth = self.stack.len();
324
325 let (computed, sig) = if let Some(cache) = self.cache.as_mut() {
326 let parent_sig = self.sig_stack.last().copied();
329 if has_comb {
330 let prev_sibs: &[NodeIdentity] = self
331 .siblings
332 .get(depth)
333 .map(Vec::as_slice)
334 .unwrap_or(&[]);
335 self.sheet.compute_cached_ancestors(
336 node,
337 parent,
338 parent_sig,
339 &self.identity_stack,
340 prev_sibs,
341 &self.media,
342 &mut self.scratch,
343 cache,
344 )
345 } else {
346 self.sheet.compute_cached(
347 node,
348 parent,
349 parent_sig,
350 &self.media,
351 &mut self.scratch,
352 cache,
353 )
354 }
355 } else {
356 let c = if has_comb {
358 let prev_sibs: &[NodeIdentity] = self
359 .siblings
360 .get(depth)
361 .map(Vec::as_slice)
362 .unwrap_or(&[]);
363 self.sheet.compute_with_ancestors_media(
364 node,
365 parent,
366 &mut self.scratch,
367 &self.identity_stack,
368 prev_sibs,
369 &self.media,
370 )
371 } else {
372 self.sheet
373 .compute_with_media(node, parent, &mut self.scratch, &self.media)
374 };
375 (c, 0)
376 };
377
378 self.stack.push(computed.clone());
379 if self.cache.is_some() {
380 self.sig_stack.push(sig);
381 }
382 if has_comb {
383 self.identity_stack.push(NodeIdentity::from_node(node));
384 let child_depth = depth + 1;
387 if self.siblings.len() <= child_depth {
388 self.siblings.resize_with(child_depth + 1, Vec::new);
389 }
390 self.siblings[child_depth].clear();
391 }
392 computed
393 }
394
395 pub fn leave(&mut self) -> Option<ComputedStyle> {
397 if self.sheet.has_combinators() && !self.identity_stack.is_empty() {
401 let popped = self.identity_stack.pop().expect("identity stack non-empty");
402 let depth = self.stack.len() - 1;
407 if self.siblings.len() <= depth {
408 self.siblings.resize_with(depth + 1, Vec::new);
409 }
410 self.siblings[depth].push(popped);
411 }
412 if self.cache.is_some() && !self.sig_stack.is_empty() {
413 self.sig_stack.pop();
414 }
415 self.stack.pop()
416 }
417
418 pub fn current(&self) -> Option<&ComputedStyle> {
421 self.stack.last()
422 }
423
424 pub fn depth(&self) -> usize {
426 self.stack.len()
427 }
428
429 pub fn sheet(&self) -> &Stylesheet {
431 self.sheet
432 }
433}
434
435pub struct ComputeScratch {
451 matching: Vec<usize>,
452}
453
454impl ComputeScratch {
455 pub fn new() -> Self {
456 Self {
457 matching: Vec::new(),
458 }
459 }
460}
461
462impl Default for ComputeScratch {
463 fn default() -> Self {
464 Self::new()
465 }
466}
467
468impl Stylesheet {
469 pub fn compute(&self, node: &dyn StyledNode, parent: Option<&ComputedStyle>) -> ComputedStyle {
489 let mut scratch = ComputeScratch::new();
490 self.compute_with(node, parent, &mut scratch)
491 }
492
493 pub fn compute_with(
516 &self,
517 node: &dyn StyledNode,
518 parent: Option<&ComputedStyle>,
519 scratch: &mut ComputeScratch,
520 ) -> ComputedStyle {
521 self.compute_with_media(node, parent, scratch, &MediaContext::default())
524 }
525
526 pub fn compute_with_media(
543 &self,
544 node: &dyn StyledNode,
545 parent: Option<&ComputedStyle>,
546 scratch: &mut ComputeScratch,
547 media: &MediaContext,
548 ) -> ComputedStyle {
549 self.compute_inner(node, parent, scratch, None, None, media)
552 }
553
554 pub(crate) fn compute_with_ancestors_media(
561 &self,
562 node: &dyn StyledNode,
563 parent: Option<&ComputedStyle>,
564 scratch: &mut ComputeScratch,
565 ancestors: &[NodeIdentity],
566 siblings: &[NodeIdentity],
567 media: &MediaContext,
568 ) -> ComputedStyle {
569 self.compute_inner(node, parent, scratch, Some(ancestors), Some(siblings), media)
570 }
571
572 pub fn compute_cached(
584 &self,
585 node: &dyn StyledNode,
586 parent: Option<&ComputedStyle>,
587 parent_sig: Option<u64>,
588 media: &MediaContext,
589 scratch: &mut ComputeScratch,
590 cache: &mut ComputeCache,
591 ) -> (ComputedStyle, u64) {
592 let node_id = NodeIdentity::from_node(node);
593 let sig = node_signature(&node_id, parent_sig, &[], media);
594 if let Some(hit) = cache.get(sig, self.generation()) {
595 return (hit, sig);
596 }
597 let computed = self.compute_with_media(node, parent, scratch, media);
598 cache.insert(sig, computed.clone(), self.generation());
599 (computed, sig)
600 }
601
602 #[allow(clippy::too_many_arguments)] pub(crate) fn compute_cached_ancestors(
615 &self,
616 node: &dyn StyledNode,
617 parent: Option<&ComputedStyle>,
618 parent_sig: Option<u64>,
619 ancestors: &[NodeIdentity],
620 siblings: &[NodeIdentity],
621 media: &MediaContext,
622 scratch: &mut ComputeScratch,
623 cache: &mut ComputeCache,
624 ) -> (ComputedStyle, u64) {
625 let node_id = NodeIdentity::from_node(node);
626 let sig = node_signature(&node_id, parent_sig, siblings, media);
627 if let Some(hit) = cache.get(sig, self.generation()) {
628 return (hit, sig);
629 }
630 let computed =
631 self.compute_with_ancestors_media(node, parent, scratch, ancestors, siblings, media);
632 cache.insert(sig, computed.clone(), self.generation());
633 (computed, sig)
634 }
635
636 fn compute_inner(
650 &self,
651 node: &dyn StyledNode,
652 parent: Option<&ComputedStyle>,
653 scratch: &mut ComputeScratch,
654 ancestors: Option<&[NodeIdentity]>,
655 siblings: Option<&[NodeIdentity]>,
656 media: &MediaContext,
657 ) -> ComputedStyle {
658 let rules = self.rules();
659
660 scratch.matching.clear();
665 match ancestors {
666 None => {
667 let type_name = node.type_name();
669 let id = node.id();
670 let classes: Classes<'_> = node.classes();
671 let state = node.state();
672 let position = node.position();
673 for (i, r) in rules.iter().enumerate() {
674 if r.selector.matches_values(type_name, id, &classes, state, &position)
675 && rule_media_matches(&r.media, media)
676 && rule_supports_matches(&r.supports, media)
677 {
678 scratch.matching.push(i);
679 }
680 }
681 }
682 Some(stack) => {
683 let node_id = NodeIdentity::from_node(node);
686 let sibs: &[NodeIdentity] = siblings.unwrap_or(&[]);
687 for (i, r) in rules.iter().enumerate() {
688 if r.selector.matches_chain(&node_id, stack, sibs)
689 && rule_media_matches(&r.media, media)
690 && rule_supports_matches(&r.supports, media)
691 {
692 scratch.matching.push(i);
693 }
694 }
695 }
696 }
697
698 let mut own = CssStyle::new();
703 for &i in &scratch.matching {
704 own.overlay(&rules[i].style);
705 }
706
707 if let Some(parent) = parent {
709 resolve_explicit_inherit(&mut own, &parent.style);
710 own.inherit_from(&parent.style);
711 }
712
713 resolve_vars_in_place(&mut own, self.tokens(), media);
717
718 ComputedStyle::new(own)
719 }
720}
721
722#[inline]
727fn rule_media_matches(query: &Option<crate::media::MediaQuery>, ctx: &MediaContext) -> bool {
728 match query {
729 None => true,
730 Some(q) => q.matches(ctx),
731 }
732}
733
734#[inline]
740fn rule_supports_matches(query: &Option<crate::supports::SupportsQuery>, ctx: &MediaContext) -> bool {
741 match query {
742 None => true,
743 Some(q) => q.matches(ctx),
744 }
745}
746
747fn resolve_explicit_inherit(own: &mut CssStyle, parent: &CssStyle) {
751 if matches!(own.color, Some(Color::Inherit)) {
752 own.color = parent.color.clone();
753 }
754 if matches!(own.background, Some(Color::Inherit)) {
755 own.background = parent.background.clone();
756 }
757 if matches!(own.underline_color, Some(Color::Inherit)) {
758 own.underline_color = parent.underline_color.clone();
759 }
760}
761
762fn resolve_vars_in_place(style: &mut CssStyle, tokens: &ThemeTokens, media: &MediaContext) {
775 resolve_color_field(&mut style.color, tokens, media);
776 resolve_color_field(&mut style.background, tokens, media);
777 resolve_color_field(&mut style.underline_color, tokens, media);
778 if let Some(border) = style.border.as_mut() {
783 resolve_color_field(&mut border.color, tokens, media);
784 resolve_border_style_field(&mut border.style, tokens, media);
785 }
786 resolve_length_field(&mut style.width, tokens, media);
787 resolve_length_field(&mut style.height, tokens, media);
788 resolve_box_edges_field(&mut style.padding, tokens, media);
789 resolve_box_edges_field(&mut style.margin, tokens, media);
790}
791
792fn resolve_color_field(field: &mut Option<Color>, tokens: &ThemeTokens, media: &MediaContext) {
793 if let Some(inner) = field {
794 match inner {
795 Color::Literal(_) | Color::Reset => {} Color::Var { .. } | Color::Inherit => {
797 *field = Some(Color::Literal(token::resolve_with_media(inner, tokens, media)));
798 }
799 }
800 }
801}
802
803fn resolve_length_field(field: &mut Option<Length>, tokens: &ThemeTokens, media: &MediaContext) {
809 if let Some(inner) = field {
810 if let Length::Var { .. } = inner {
811 *field = Some(token::resolve_length_with_media(inner, tokens, media));
812 }
813 }
814}
815
816fn resolve_box_edges_field(
823 field: &mut Option<BoxEdgesValue>,
824 tokens: &ThemeTokens,
825 media: &MediaContext,
826) {
827 if let Some(inner) = field.take() {
828 *field = Some(resolve_box_edges_value(inner, tokens, media, 0));
829 }
830}
831
832fn resolve_box_edges_value(
836 value: BoxEdgesValue,
837 tokens: &ThemeTokens,
838 media: &MediaContext,
839 depth: u8,
840) -> BoxEdgesValue {
841 if depth > 32 {
842 return BoxEdgesValue::Edges(BoxEdges::zero());
843 }
844 match value {
845 BoxEdgesValue::Edges(_) => value,
846 BoxEdgesValue::Var { name, fallback } => {
847 match tokens.get_box_edges_with(&name, media) {
848 Some(edges) => BoxEdgesValue::Edges(edges),
849 None => match fallback {
850 Some(fb) => resolve_box_edges_value(*fb, tokens, media, depth + 1),
851 None => BoxEdgesValue::Edges(BoxEdges::zero()),
852 },
853 }
854 }
855 }
856}
857
858fn resolve_border_style_field(
864 field: &mut BorderStyleValue,
865 tokens: &ThemeTokens,
866 media: &MediaContext,
867) {
868 let owned = std::mem::take(field);
869 *field = resolve_border_style_value(owned, tokens, media, 0);
870}
871
872fn resolve_border_style_value(
876 value: BorderStyleValue,
877 tokens: &ThemeTokens,
878 media: &MediaContext,
879 depth: u8,
880) -> BorderStyleValue {
881 if depth > 32 {
882 return BorderStyleValue::Fixed(BorderStyle::None);
883 }
884 match value {
885 BorderStyleValue::Fixed(_) => value,
886 BorderStyleValue::Var { name, fallback } => {
887 match tokens.get_border_style_with(&name, media) {
888 Some(style) => BorderStyleValue::Fixed(style),
889 None => match fallback {
890 Some(fb) => resolve_border_style_value(*fb, tokens, media, depth + 1),
891 None => BorderStyleValue::Fixed(BorderStyle::None),
892 },
893 }
894 }
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901 use crate::node::{NodeRef, OwnedNode, State};
902 use crate::stylesheet::Origin;
903 use ratatui::style::Color as RC;
904
905 fn sheet() -> Stylesheet {
906 let mut s = Stylesheet::with_tokens(
907 crate::token::ThemeTokens::new().set("accent", Color::literal(RC::Cyan)),
908 );
909 s.add("Button", CssStyle::new().color(RC::Gray), Origin::User)
911 .unwrap();
912 s.add(
914 "Button.primary",
915 CssStyle::new().background(RC::Blue),
916 Origin::User,
917 )
918 .unwrap();
919 s.add("#save", CssStyle::new().color(RC::Yellow), Origin::User)
921 .unwrap();
922 s.add(
924 "Button:focus",
925 CssStyle::new().background(RC::Green),
926 Origin::User,
927 )
928 .unwrap();
929 s.add(
931 ".accented",
932 CssStyle::new().color(Color::var("accent")),
933 Origin::User,
934 )
935 .unwrap();
936 s
938 }
939
940 #[test]
941 fn specificity_wins() {
942 let s = sheet();
943 let n = OwnedNode::new("Button")
944 .with_id("save")
945 .with_classes(["primary"]);
946 let c = s.compute(&n, None);
947 assert_eq!(c.style.color, Some(Color::literal(RC::Yellow)));
949 assert_eq!(c.style.background, Some(Color::literal(RC::Blue)));
951 }
952
953 #[test]
954 fn pseudo_state_matches() {
955 let s = sheet();
956 let n = OwnedNode::new("Button").with_state(State::focus());
957 let c = s.compute(&n, None);
958 assert_eq!(c.style.background, Some(Color::literal(RC::Green)));
959 }
960
961 #[test]
962 fn nth_child_cascade_end_to_end() {
963 let mut s = Stylesheet::new();
965 s.add(
966 "Item:nth-child(odd)",
967 CssStyle::new().color(RC::Red),
968 Origin::User,
969 )
970 .unwrap();
971
972 let first = OwnedNode::new("Item").with_position(crate::node::Position::new(0, 3));
974 let second = OwnedNode::new("Item").with_position(crate::node::Position::new(1, 3));
976 let third = OwnedNode::new("Item").with_position(crate::node::Position::new(2, 3));
978
979 assert_eq!(
980 s.compute(&first, None).style.color,
981 Some(Color::literal(RC::Red))
982 );
983 assert_eq!(s.compute(&second, None).style.color, None);
984 assert_eq!(
985 s.compute(&third, None).style.color,
986 Some(Color::literal(RC::Red))
987 );
988 }
989
990 #[test]
991 fn nth_child_default_position_does_not_match() {
992 let mut s = Stylesheet::new();
995 s.add(
996 "Item:nth-child(odd)",
997 CssStyle::new().color(RC::Red),
998 Origin::User,
999 )
1000 .unwrap();
1001
1002 let n = OwnedNode::new("Item"); assert_eq!(n.position().sibling_count, 0);
1004 let c = s.compute(&n, None);
1005 assert_eq!(c.style.color, None);
1006 }
1007
1008 #[test]
1009 fn var_resolves_from_tokens() {
1010 let s = sheet();
1011 let n = OwnedNode::new("Text").with_classes(["accented"]);
1012 let c = s.compute(&n, None);
1013 assert_eq!(c.style.color, Some(Color::literal(RC::Cyan)));
1014 }
1015
1016 #[test]
1017 fn border_color_var_resolves_from_tokens() {
1018 let sheet = Stylesheet::parse(
1026 ":root{--border-dim:#003237} .panel { border: rounded var(--border-dim); }",
1027 )
1028 .unwrap();
1029 let n = OwnedNode::new("Div").with_classes(["panel"]);
1030 let c = sheet.compute(&n, None);
1031 let border = c.style.border.expect("border present");
1032 assert_eq!(
1033 border.style,
1034 crate::box_model::BorderStyleValue::Fixed(crate::box_model::BorderStyle::Rounded)
1035 );
1036 assert_eq!(
1037 border.color,
1038 Some(Color::literal(RC::Rgb(0x00, 0x32, 0x37)))
1039 );
1040 }
1041
1042 #[test]
1043 fn border_color_var_via_subdeclaration_resolves() {
1044 let sheet = Stylesheet::parse(
1047 ":root{--rim:#ff0000} .b { border-style: single; border-color: var(--rim); }",
1048 )
1049 .unwrap();
1050 let n = OwnedNode::new("Div").with_classes(["b"]);
1051 let c = sheet.compute(&n, None);
1052 let border = c.style.border.expect("border present");
1053 assert_eq!(
1054 border.color,
1055 Some(Color::literal(RC::Rgb(0xff, 0x00, 0x00)))
1056 );
1057 }
1058
1059 #[test]
1060 fn border_color_var_fallback_resolves() {
1061 let sheet = Stylesheet::parse(".b { border: rounded var(--nope, #00ff00); }").unwrap();
1064 let n = OwnedNode::new("Div").with_classes(["b"]);
1065 let c = sheet.compute(&n, None);
1066 let border = c.style.border.expect("border present");
1067 assert_eq!(
1068 border.color,
1069 Some(Color::literal(RC::Rgb(0x00, 0xff, 0x00)))
1070 );
1071 }
1072
1073 #[test]
1074 fn inheritance_from_parent() {
1075 let s = sheet();
1076 let parent_node = OwnedNode::new("Button").with_classes(["primary"]);
1077 let parent = s.compute(&parent_node, None);
1078 let child = OwnedNode::new("Text");
1080 let computed = s.compute(&child, Some(&parent));
1081 assert_eq!(computed.style.color, Some(Color::literal(RC::Gray)));
1082 }
1083
1084 #[test]
1085 fn origin_overrides_specificity() {
1086 let mut s = Stylesheet::new();
1087 s.add("Button", CssStyle::new().color(RC::Red), Origin::User)
1088 .unwrap();
1089 s.add("Button", CssStyle::new().color(RC::Blue), Origin::Inline)
1091 .unwrap();
1092 let n = OwnedNode::new("Button");
1093 let c = s.compute(&n, None);
1094 assert_eq!(c.style.color, Some(Color::literal(RC::Blue)));
1095 }
1096
1097 #[test]
1098 fn rules_stored_in_cascade_sorted_order() {
1099 let mut s = Stylesheet::new();
1103 s.add("Button", CssStyle::new(), Origin::User).unwrap();
1105 s.add("#save", CssStyle::new(), Origin::User).unwrap();
1107 s.add(".primary", CssStyle::new(), Origin::User).unwrap();
1109 s.add("Button", CssStyle::new(), Origin::Inline).unwrap();
1111 s.add(".primary", CssStyle::new(), Origin::UserAgent)
1113 .unwrap();
1114
1115 let rules = s.rules();
1116 for w in rules.windows(2) {
1117 let a = &w[0];
1118 let b = &w[1];
1119 let ka = (a.origin, a.selector.specificity(), a.order);
1120 let kb = (b.origin, b.selector.specificity(), b.order);
1121 assert!(ka <= kb, "rules not sorted: {ka:?} > {kb:?}");
1122 }
1123
1124 assert_eq!(rules.first().unwrap().origin, Origin::UserAgent);
1127 assert_eq!(rules.last().unwrap().origin, Origin::Inline);
1128 }
1129
1130 #[test]
1131 fn compute_unchanged_after_sort_removal_scrambled_insertion() {
1132 let mut s = Stylesheet::new();
1137 s.add("#save", CssStyle::new().color(RC::Yellow), Origin::User)
1139 .unwrap();
1140 s.add(
1141 "Button.primary",
1142 CssStyle::new().background(RC::Blue),
1143 Origin::User,
1144 )
1145 .unwrap();
1146 s.add("Button", CssStyle::new().color(RC::Gray), Origin::User)
1147 .unwrap();
1148
1149 let n = OwnedNode::new("Button")
1150 .with_id("save")
1151 .with_classes(["primary"]);
1152 let c = s.compute(&n, None);
1153 assert_eq!(c.style.color, Some(Color::literal(RC::Yellow)));
1155 assert_eq!(c.style.background, Some(Color::literal(RC::Blue)));
1157 }
1158
1159 #[test]
1160 fn inline_origin_wins_in_scrambled_insertion_order() {
1161 let mut s = Stylesheet::new();
1164 s.add("Button", CssStyle::new().color(RC::Blue), Origin::Inline)
1165 .unwrap();
1166 s.add("Button", CssStyle::new().color(RC::Red), Origin::User)
1167 .unwrap();
1168 let n = OwnedNode::new("Button");
1169 let c = s.compute(&n, None);
1170 assert_eq!(c.style.color, Some(Color::literal(RC::Blue)));
1171 }
1172
1173 #[test]
1174 fn render_computed_applies_margin_once() {
1175 let computed = ComputedStyle::new(
1183 CssStyle::new()
1184 .margin("2")
1185 .padding("1")
1186 .border("rounded #00d4ff"),
1187 );
1188 let area = Rect::new(0, 0, 44, 8);
1189
1190 let shrunk = computed.apply_margin(area);
1192 let (_block, _style, inner) = computed.layout_with_shrunk(shrunk);
1193
1194 assert_eq!(shrunk, Rect::new(2, 2, 40, 4));
1196 assert_eq!(inner, Rect::new(4, 4, 36, 0));
1198 }
1199
1200 #[test]
1201 fn with_inline_overrides_specificity() {
1202 let mut s = Stylesheet::new();
1206 s.add("#save", CssStyle::new().color(RC::Yellow), Origin::User)
1207 .unwrap();
1208 let n = OwnedNode::new("Button").with_id("save");
1209 let c = s
1210 .compute(&n, None)
1211 .with_inline(&CssStyle::new().color("red"));
1212 assert_eq!(c.style.color, Some(Color::literal(RC::Red)));
1214 }
1215
1216 #[test]
1217 fn apply_inline_in_place_overrides() {
1218 let mut s = Stylesheet::new();
1220 s.add(
1221 "Button.primary",
1222 CssStyle::new().color(RC::Blue),
1223 Origin::User,
1224 )
1225 .unwrap();
1226 let n = OwnedNode::new("Button").with_classes(["primary"]);
1227 let mut c = s.compute(&n, None);
1228 c.apply_inline(&CssStyle::new().color("red"));
1229 assert_eq!(c.style.color, Some(Color::literal(RC::Red)));
1230 }
1231
1232 #[test]
1233 fn layout_inner_matches_handwritten_sequence() {
1234 let computed = ComputedStyle::new(
1236 CssStyle::new()
1237 .margin("2")
1238 .padding("1")
1239 .border("rounded #00d4ff"),
1240 );
1241 let area = Rect::new(0, 0, 44, 8);
1242
1243 let (_block, _style, inner_from_layout) = computed.layout(area);
1244
1245 let shrunk = computed.apply_margin(area);
1247 let block = computed.to_block();
1248 let inner_from_hand = block.inner(shrunk);
1249
1250 assert_eq!(inner_from_layout, inner_from_hand);
1251 assert_eq!(inner_from_layout, Rect::new(4, 4, 36, 0));
1254 }
1255
1256 #[test]
1257 fn layout_inner_equals_area_with_no_box_model() {
1258 let computed = ComputedStyle::new(CssStyle::new());
1259 let area = Rect::new(0, 0, 30, 10);
1260 let (_block, _style, inner) = computed.layout(area);
1261 assert_eq!(inner, area);
1262 }
1263
1264 #[test]
1265 fn layout_content_style_matches_to_style() {
1266 let computed = ComputedStyle::new(CssStyle::new().color(RC::Cyan).bold().padding("1"));
1267 let area = Rect::new(0, 0, 20, 5);
1268 let (_block, style, _inner) = computed.layout(area);
1269 assert_eq!(style, computed.to_style());
1270 }
1271
1272 fn parity_sheet() -> Stylesheet {
1277 let mut s = Stylesheet::new();
1278 s.add("Button", CssStyle::new().color(RC::Gray), Origin::User)
1279 .unwrap();
1280 s.add(
1281 "Button.primary",
1282 CssStyle::new().background(RC::Blue),
1283 Origin::User,
1284 )
1285 .unwrap();
1286 s.add("#save", CssStyle::new().color(RC::Yellow), Origin::User)
1287 .unwrap();
1288 s.add(
1289 "Button:focus",
1290 CssStyle::new().background(RC::Green),
1291 Origin::User,
1292 )
1293 .unwrap();
1294 s
1295 }
1296
1297 #[test]
1298 fn noderef_behavioral_parity() {
1299 let sheet = parity_sheet();
1302
1303 let owned = OwnedNode::new("Button")
1304 .with_id("save")
1305 .with_classes(["primary"])
1306 .with_state(State::focus());
1307 let borrowed = NodeRef::new("Button")
1308 .id("save")
1309 .classes(&["primary"])
1310 .state(State::focus());
1311
1312 let c_owned = sheet.compute(&owned, None);
1313 let c_borrowed = sheet.compute(&borrowed, None);
1314 assert_eq!(c_owned, c_borrowed);
1315 }
1316
1317 #[test]
1318 fn noderef_zero_string_construction() {
1319 let sheet = parity_sheet();
1321 let node = NodeRef::new("Button")
1322 .classes(&["primary"])
1323 .state(State::focus());
1324 let c = sheet.compute(&node, None);
1325 assert_eq!(c.style.background, Some(Color::literal(RC::Green)));
1329 assert_eq!(c.style.color, Some(Color::literal(RC::Gray)));
1330 }
1331
1332 #[test]
1333 fn compute_with_matches_compute() {
1334 let sheet = parity_sheet();
1335 let mut scratch = ComputeScratch::new();
1336
1337 let cases: [(&str, OwnedNode); 5] = [
1338 ("plain", OwnedNode::new("Button")),
1339 (
1340 "primary",
1341 OwnedNode::new("Button").with_classes(["primary"]),
1342 ),
1343 ("id", OwnedNode::new("Button").with_id("save")),
1344 ("focus", OwnedNode::new("Button").with_state(State::focus())),
1345 (
1346 "combo",
1347 OwnedNode::new("Button")
1348 .with_id("save")
1349 .with_classes(["primary"])
1350 .with_state(State::focus()),
1351 ),
1352 ];
1353
1354 for (name, node) in cases {
1355 let via_compute = sheet.compute(&node, None);
1356 let via_compute_with = sheet.compute_with(&node, None, &mut scratch);
1357 assert_eq!(via_compute, via_compute_with, "mismatch for case `{name}`");
1358 }
1359 }
1360
1361 #[test]
1362 fn scratch_reuse_no_panic() {
1363 let sheet = parity_sheet();
1366 let mut scratch = ComputeScratch::new();
1367
1368 let big = NodeRef::new("Button")
1371 .id("save")
1372 .classes(&["primary"])
1373 .state(State::focus());
1374 let none = NodeRef::new("NoSuchType");
1375
1376 let c1 = sheet.compute_with(&big, None, &mut scratch);
1377 let c_none = sheet.compute_with(&none, None, &mut scratch);
1378 let c2 = sheet.compute_with(&big, None, &mut scratch);
1379
1380 assert_eq!(c_none.style.color, None);
1382 assert_eq!(c1, c2);
1384 assert_eq!(c1.style.color, Some(Color::literal(RC::Yellow)));
1385 }
1386
1387 fn context_sheet() -> Stylesheet {
1392 let mut s = Stylesheet::new();
1394 s.add("Panel", CssStyle::new().color("#cdd6f4"), Origin::User)
1395 .unwrap();
1396 s
1397 }
1398
1399 #[test]
1400 fn context_inherits_without_manual_threading() {
1401 let sheet = context_sheet();
1404 let mut ctx = CascadeContext::new(&sheet);
1405
1406 let _panel = ctx.enter(&OwnedNode::new("Panel"));
1407 let text = ctx.enter(&OwnedNode::new("Text"));
1408
1409 assert_eq!(
1410 text.style.color,
1411 Some(Color::literal(RC::Rgb(0xcd, 0xd6, 0xf4)))
1412 );
1413 }
1414
1415 #[test]
1416 fn context_parity_with_manual_compute() {
1417 let mut sheet = Stylesheet::new();
1421 sheet
1422 .add("Root", CssStyle::new().color(RC::Red), Origin::User)
1423 .unwrap();
1424 sheet
1425 .add("Panel", CssStyle::new().padding("1"), Origin::User)
1426 .unwrap();
1427 sheet.add("Text", CssStyle::new(), Origin::User).unwrap();
1429
1430 let mut ctx = CascadeContext::new(&sheet);
1432 let ctx_root = ctx.enter(&OwnedNode::new("Root"));
1433 let ctx_panel = ctx.enter(&OwnedNode::new("Panel"));
1434 let ctx_text = ctx.enter(&OwnedNode::new("Text"));
1435
1436 let man_root = sheet.compute(&OwnedNode::new("Root"), None);
1438 let man_panel = sheet.compute(&OwnedNode::new("Panel"), Some(&man_root));
1439 let man_text = sheet.compute(&OwnedNode::new("Text"), Some(&man_panel));
1440
1441 assert_eq!(ctx_root, man_root);
1442 assert_eq!(ctx_panel, man_panel);
1443 assert_eq!(ctx_text, man_text);
1444 }
1445
1446 #[test]
1447 fn context_leave_restores_parent() {
1448 let mut sheet = Stylesheet::new();
1451 sheet
1452 .add("A", CssStyle::new().color(RC::Red), Origin::User)
1453 .unwrap();
1454 sheet
1455 .add("B", CssStyle::new().color(RC::Blue), Origin::User)
1456 .unwrap();
1457 sheet.add("C", CssStyle::new(), Origin::User).unwrap();
1459
1460 let mut ctx = CascadeContext::new(&sheet);
1461 let _a = ctx.enter(&OwnedNode::new("A"));
1462 let _b = ctx.enter(&OwnedNode::new("B"));
1463 ctx.leave(); let c = ctx.enter(&OwnedNode::new("C"));
1465
1466 assert_eq!(c.style.color, Some(Color::literal(RC::Red)));
1468 }
1469
1470 #[test]
1471 fn context_depth() {
1472 let sheet = context_sheet();
1473 let mut ctx = CascadeContext::new(&sheet);
1474
1475 assert_eq!(ctx.depth(), 0);
1476 ctx.enter(&OwnedNode::new("Panel"));
1477 assert_eq!(ctx.depth(), 1);
1478 ctx.enter(&OwnedNode::new("Text"));
1479 assert_eq!(ctx.depth(), 2);
1480 ctx.leave();
1481 assert_eq!(ctx.depth(), 1);
1482 ctx.leave();
1483 assert_eq!(ctx.depth(), 0);
1484 assert!(ctx.leave().is_none());
1485 }
1486
1487 #[test]
1488 fn context_scratch_reused() {
1489 let mut sheet = Stylesheet::new();
1492 sheet
1493 .add("A", CssStyle::new().color(RC::Red), Origin::User)
1494 .unwrap();
1495 sheet
1496 .add("A.child", CssStyle::new().bold(), Origin::User)
1497 .unwrap();
1498 sheet
1499 .add("NoMatch", CssStyle::new().color(RC::Green), Origin::User)
1500 .unwrap();
1501
1502 let mut ctx = CascadeContext::new(&sheet);
1503
1504 let child = ctx.enter(&OwnedNode::new("A").with_classes(["child"]));
1506 assert_eq!(child.style.color, Some(Color::literal(RC::Red)));
1507
1508 let none = ctx.enter(&OwnedNode::new("TotallyUnknown"));
1509 assert_eq!(none.style.color, Some(Color::literal(RC::Red)));
1512
1513 ctx.leave();
1515 let child2 = ctx.enter(&OwnedNode::new("A").with_classes(["child"]));
1516 assert_eq!(child2.style.color, Some(Color::literal(RC::Red)));
1517 }
1518
1519 #[test]
1524 fn width_var_resolves() {
1525 let sheet = Stylesheet::parse(":root{--w:50%} .col { width: var(--w);}").unwrap();
1526 let node = OwnedNode::new("Div").with_classes(["col"]);
1527 let c = sheet.compute(&node, None);
1528 assert_eq!(c.style.width, Some(crate::box_model::Length::Percent(50)));
1529 }
1530
1531 #[test]
1532 fn width_var_chain() {
1533 let sheet =
1534 Stylesheet::parse(":root{--w: var(--w2); --w2: 10;} .x { width: var(--w); }").unwrap();
1535 let node = OwnedNode::new("Div").with_classes(["x"]);
1536 let c = sheet.compute(&node, None);
1537 assert_eq!(c.style.width, Some(crate::box_model::Length::Cells(10)));
1538 }
1539
1540 #[test]
1541 fn width_var_undefined_degrades_to_auto() {
1542 let sheet = Stylesheet::parse(".x { width: var(--nope); }").unwrap();
1544 let node = OwnedNode::new("Div").with_classes(["x"]);
1545 let c = sheet.compute(&node, None);
1546 assert_eq!(c.style.width, Some(crate::box_model::Length::Auto));
1547 }
1548
1549 #[test]
1550 fn width_var_mistype_degrades_to_auto() {
1551 let sheet = Stylesheet::parse(":root{--c:#fff} .x { width: var(--c); }").unwrap();
1553 let node = OwnedNode::new("Div").with_classes(["x"]);
1554 let c = sheet.compute(&node, None);
1555 assert_eq!(c.style.width, Some(crate::box_model::Length::Auto));
1556 }
1557
1558 #[test]
1559 fn height_var_resolves() {
1560 let sheet = Stylesheet::parse(":root{--h:max(8)} .row { height: var(--h); }").unwrap();
1561 let node = OwnedNode::new("Div").with_classes(["row"]);
1562 let c = sheet.compute(&node, None);
1563 assert_eq!(c.style.height, Some(crate::box_model::Length::Max(8)));
1564 }
1565
1566 #[test]
1567 fn width_var_undefined_uses_fallback() {
1568 let sheet = Stylesheet::parse(".x { width: var(--nope, 7); }").unwrap();
1571 let node = OwnedNode::new("Div").with_classes(["x"]);
1572 let c = sheet.compute(&node, None);
1573 assert_eq!(c.style.width, Some(crate::box_model::Length::Cells(7)));
1574 }
1575
1576 #[test]
1581 fn padding_var_resolves_from_token() {
1582 let sheet = Stylesheet::parse(
1583 ":root{--pad: 1 2} .x { padding: var(--pad); }",
1584 )
1585 .unwrap();
1586 let node = OwnedNode::new("Div").with_classes(["x"]);
1587 let c = sheet.compute(&node, None);
1588 assert_eq!(
1590 c.style.padding,
1591 Some(crate::box_model::BoxEdgesValue::Edges(BoxEdges {
1592 top: 1,
1593 right: 2,
1594 bottom: 1,
1595 left: 2,
1596 }))
1597 );
1598 }
1599
1600 #[test]
1601 fn padding_var_resolved_into_block() {
1602 let sheet = Stylesheet::parse(
1605 ":root{--pad: 1 2} .x { padding: var(--pad); }",
1606 )
1607 .unwrap();
1608 let node = OwnedNode::new("Div").with_classes(["x"]);
1609 let c = sheet.compute(&node, None);
1610 let block = c.to_block();
1611 let area = Rect::new(0, 0, 10, 10);
1614 let inner = block.inner(area);
1615 assert_eq!(inner, Rect::new(2, 1, 6, 8));
1616 }
1617
1618 #[test]
1619 fn margin_var_resolves_from_token() {
1620 let sheet = Stylesheet::parse(
1621 ":root{--m: 3} .x { margin: var(--m); }",
1622 )
1623 .unwrap();
1624 let node = OwnedNode::new("Div").with_classes(["x"]);
1625 let c = sheet.compute(&node, None);
1626 assert_eq!(
1627 c.style.margin,
1628 Some(crate::box_model::BoxEdgesValue::Edges(BoxEdges::uniform(3)))
1629 );
1630 let area = Rect::new(0, 0, 10, 10);
1632 let shrunk = c.apply_margin(area);
1633 assert_eq!((shrunk.x, shrunk.y, shrunk.width, shrunk.height), (3, 3, 4, 4));
1634 }
1635
1636 #[test]
1637 fn border_style_var_resolves() {
1638 let sheet = Stylesheet::parse(
1639 ":root{--bs: rounded} .x { border-style: var(--bs); }",
1640 )
1641 .unwrap();
1642 let node = OwnedNode::new("Div").with_classes(["x"]);
1643 let c = sheet.compute(&node, None);
1644 let border = c.style.border.expect("border present");
1645 assert_eq!(
1646 border.style,
1647 crate::box_model::BorderStyleValue::Fixed(crate::box_model::BorderStyle::Rounded)
1648 );
1649 }
1650
1651 #[test]
1652 fn border_style_var_via_shorthand_resolves() {
1653 let sheet = Stylesheet::parse(
1655 ":root{--bs: double} .x { border: var(--bs); }",
1656 )
1657 .unwrap();
1658 let node = OwnedNode::new("Div").with_classes(["x"]);
1659 let c = sheet.compute(&node, None);
1660 let border = c.style.border.expect("border present");
1661 assert_eq!(
1662 border.style,
1663 crate::box_model::BorderStyleValue::Fixed(crate::box_model::BorderStyle::Double)
1664 );
1665 }
1666
1667 #[test]
1668 fn padding_var_undefined_degrades_to_zero() {
1669 let sheet = Stylesheet::parse(".x { padding: var(--nope); }").unwrap();
1671 let node = OwnedNode::new("Div").with_classes(["x"]);
1672 let c = sheet.compute(&node, None);
1673 assert_eq!(
1674 c.style.padding,
1675 Some(crate::box_model::BoxEdgesValue::Edges(BoxEdges::zero()))
1676 );
1677 let block = c.to_block();
1679 let area = Rect::new(0, 0, 10, 10);
1680 assert_eq!(block.inner(area), area);
1681 }
1682
1683 #[test]
1684 fn padding_var_undefined_uses_fallback() {
1685 let sheet = Stylesheet::parse(".x { padding: var(--nope, 3); }").unwrap();
1687 let node = OwnedNode::new("Div").with_classes(["x"]);
1688 let c = sheet.compute(&node, None);
1689 assert_eq!(
1690 c.style.padding,
1691 Some(crate::box_model::BoxEdgesValue::Edges(BoxEdges::uniform(3)))
1692 );
1693 }
1694
1695 #[test]
1696 fn border_style_var_undefined_degrades_to_none() {
1697 let sheet = Stylesheet::parse(".x { border-style: var(--nope); }").unwrap();
1699 let node = OwnedNode::new("Div").with_classes(["x"]);
1700 let c = sheet.compute(&node, None);
1701 let border = c.style.border.expect("border present");
1702 assert_eq!(
1703 border.style,
1704 crate::box_model::BorderStyleValue::Fixed(crate::box_model::BorderStyle::None)
1705 );
1706 }
1707
1708 #[test]
1709 fn box_edges_var_mistype_degrades() {
1710 let sheet = Stylesheet::parse(
1713 ":root{--c:#fff} .x { padding: var(--c); }",
1714 )
1715 .unwrap();
1716 let node = OwnedNode::new("Div").with_classes(["x"]);
1717 let c = sheet.compute(&node, None);
1718 assert_eq!(
1719 c.style.padding,
1720 Some(crate::box_model::BoxEdgesValue::Edges(BoxEdges::zero()))
1721 );
1722 }
1723
1724 #[test]
1725 fn box_edges_media_gated_token_resolves() {
1726 let sheet = Stylesheet::parse(
1729 ":root{--pad:1} @media (min-width:80){:root{--pad:2}} .x{padding:var(--pad)}",
1730 )
1731 .unwrap();
1732 let node = OwnedNode::new("Div").with_classes(["x"]);
1733 let mut scratch = ComputeScratch::new();
1734 let large = MediaContext { cols: 100, ..Default::default() };
1736 let c = sheet.compute_with_media(&node, None, &mut scratch, &large);
1737 assert_eq!(
1738 c.style.padding,
1739 Some(crate::box_model::BoxEdgesValue::Edges(BoxEdges::uniform(2)))
1740 );
1741 let small = MediaContext { cols: 40, ..Default::default() };
1743 let c = sheet.compute_with_media(&node, None, &mut scratch, &small);
1744 assert_eq!(
1745 c.style.padding,
1746 Some(crate::box_model::BoxEdgesValue::Edges(BoxEdges::uniform(1)))
1747 );
1748 }
1749
1750 #[test]
1755 fn has_combinators_flag() {
1756 let mut plain = Stylesheet::new();
1758 plain.add("Button", CssStyle::new(), Origin::User).unwrap();
1759 assert!(!plain.has_combinators());
1760
1761 let mut with_comb = Stylesheet::new();
1763 with_comb
1764 .add("Panel Button", CssStyle::new(), Origin::User)
1765 .unwrap();
1766 assert!(with_comb.has_combinators());
1767
1768 let mut merged = Stylesheet::new();
1770 merged.add("Text", CssStyle::new(), Origin::User).unwrap();
1771 assert!(!merged.has_combinators());
1772 merged.extend(&with_comb);
1773 assert!(merged.has_combinators());
1774 }
1775
1776 #[test]
1777 fn descendant_combinator_matches_in_context() {
1778 let mut sheet = Stylesheet::new();
1780 sheet
1781 .add("Panel Text", CssStyle::new().color(RC::Red), Origin::User)
1782 .unwrap();
1783
1784 let mut ctx = CascadeContext::new(&sheet);
1785 let _root = ctx.enter(&OwnedNode::new("Root"));
1786 let _panel = ctx.enter(&OwnedNode::new("Panel"));
1787 let text = ctx.enter(&OwnedNode::new("Text"));
1788
1789 assert_eq!(text.style.color, Some(Color::literal(RC::Red)));
1790 }
1791
1792 #[test]
1793 fn child_combinator_direct_child_matches() {
1794 let mut sheet = Stylesheet::new();
1796 sheet
1797 .add("Panel > Text", CssStyle::new().color(RC::Blue), Origin::User)
1798 .unwrap();
1799
1800 let mut ctx = CascadeContext::new(&sheet);
1801 let _root = ctx.enter(&OwnedNode::new("Root"));
1802 let _panel = ctx.enter(&OwnedNode::new("Panel"));
1803 let text = ctx.enter(&OwnedNode::new("Text"));
1804
1805 assert_eq!(text.style.color, Some(Color::literal(RC::Blue)));
1806 }
1807
1808 #[test]
1809 fn child_combinator_indirect_child_does_not_match() {
1810 let mut sheet = Stylesheet::new();
1813 sheet
1814 .add("Panel > Text", CssStyle::new().color(RC::Blue), Origin::User)
1815 .unwrap();
1816
1817 let mut ctx = CascadeContext::new(&sheet);
1818 let _root = ctx.enter(&OwnedNode::new("Root"));
1819 let _panel = ctx.enter(&OwnedNode::new("Panel"));
1820 let _other = ctx.enter(&OwnedNode::new("Other"));
1821 let text = ctx.enter(&OwnedNode::new("Text"));
1822
1823 assert_eq!(text.style.color, None);
1825 }
1826
1827 #[test]
1828 fn descendant_vs_child_distinction() {
1829 let mut child_sheet = Stylesheet::new();
1833 child_sheet
1834 .add("Root > Text", CssStyle::new().color(RC::Red), Origin::User)
1835 .unwrap();
1836 let mut desc_sheet = Stylesheet::new();
1837 desc_sheet
1838 .add("Root Text", CssStyle::new().color(RC::Green), Origin::User)
1839 .unwrap();
1840
1841 let mut ctx_c = CascadeContext::new(&child_sheet);
1843 let _r = ctx_c.enter(&OwnedNode::new("Root"));
1844 let _p = ctx_c.enter(&OwnedNode::new("Panel"));
1845 let t_c = ctx_c.enter(&OwnedNode::new("Text"));
1846 assert_eq!(t_c.style.color, None, "Root > Text must not match a grandchild");
1847
1848 let mut ctx_d = CascadeContext::new(&desc_sheet);
1850 let _r = ctx_d.enter(&OwnedNode::new("Root"));
1851 let _p = ctx_d.enter(&OwnedNode::new("Panel"));
1852 let t_d = ctx_d.enter(&OwnedNode::new("Text"));
1853 assert_eq!(t_d.style.color, Some(Color::literal(RC::Green)));
1854 }
1855
1856 #[test]
1857 fn non_combinator_rules_match_in_context() {
1858 let mut sheet = Stylesheet::new();
1862 sheet
1863 .add("Button", CssStyle::new().color(RC::Yellow), Origin::User)
1864 .unwrap();
1865 sheet
1866 .add("Panel Button", CssStyle::new().bold(), Origin::User)
1867 .unwrap();
1868 assert!(sheet.has_combinators());
1869
1870 let mut ctx = CascadeContext::new(&sheet);
1871 let _panel = ctx.enter(&OwnedNode::new("Panel"));
1872 let btn = ctx.enter(&OwnedNode::new("Button"));
1873
1874 assert_eq!(btn.style.color, Some(Color::literal(RC::Yellow)));
1877 assert!(btn.style.weight.is_some());
1878 }
1879
1880 #[test]
1881 fn combinator_rule_does_not_match_one_shot() {
1882 let mut sheet = Stylesheet::new();
1885 sheet
1886 .add("Panel Text", CssStyle::new().color(RC::Red), Origin::User)
1887 .unwrap();
1888
1889 let node = OwnedNode::new("Text");
1890 let c = sheet.compute(&node, None);
1891 assert_eq!(c.style.color, None);
1893 }
1894
1895 #[test]
1896 fn context_leave_keeps_stacks_in_sync() {
1897 let mut sheet = Stylesheet::new();
1901 sheet
1903 .add("Panel > Text", CssStyle::new().color(RC::Red), Origin::User)
1904 .unwrap();
1905
1906 let mut ctx = CascadeContext::new(&sheet);
1907 let _root = ctx.enter(&OwnedNode::new("Root"));
1908 let _panel = ctx.enter(&OwnedNode::new("Panel"));
1909 let text1 = ctx.enter(&OwnedNode::new("Text"));
1910 assert_eq!(text1.style.color, Some(Color::literal(RC::Red)));
1911 ctx.leave(); let text2 = ctx.enter(&OwnedNode::new("Text"));
1915 assert_eq!(text2.style.color, Some(Color::literal(RC::Red)));
1916 ctx.leave(); ctx.leave(); let text3 = ctx.enter(&OwnedNode::new("Text"));
1921 assert_eq!(text3.style.color, None);
1922 }
1923
1924 #[test]
1929 fn adjacent_combinator_matches_preceding_sibling() {
1930 let mut sheet = Stylesheet::new();
1933 sheet
1934 .add("Item + Item", CssStyle::new().color(RC::Red), Origin::User)
1935 .unwrap();
1936 assert!(sheet.has_combinators());
1937
1938 let mut ctx = CascadeContext::new(&sheet);
1939 let _root = ctx.enter(&OwnedNode::new("Root"));
1940
1941 let first = ctx.enter(&OwnedNode::new("Item"));
1942 assert_eq!(first.style.color, None, "first Item has no preceding sibling");
1943 ctx.leave();
1944
1945 let second = ctx.enter(&OwnedNode::new("Item"));
1946 assert_eq!(
1947 second.style.color,
1948 Some(Color::literal(RC::Red)),
1949 "second Item follows a sibling Item"
1950 );
1951 ctx.leave();
1952
1953 let third = ctx.enter(&OwnedNode::new("Item"));
1954 assert_eq!(
1955 third.style.color,
1956 Some(Color::literal(RC::Red)),
1957 "third Item follows a sibling Item"
1958 );
1959 }
1960
1961 #[test]
1962 fn general_sibling_combinator_matches_any_preceding() {
1963 let mut sheet = Stylesheet::new();
1966 sheet
1967 .add("Item ~ Item", CssStyle::new().color(RC::Blue), Origin::User)
1968 .unwrap();
1969
1970 let mut ctx = CascadeContext::new(&sheet);
1971 let _root = ctx.enter(&OwnedNode::new("Root"));
1972
1973 let first = ctx.enter(&OwnedNode::new("Item"));
1974 assert_eq!(first.style.color, None);
1975 ctx.leave();
1976
1977 let second = ctx.enter(&OwnedNode::new("Item"));
1978 assert_eq!(second.style.color, Some(Color::literal(RC::Blue)));
1979 ctx.leave();
1980
1981 let third = ctx.enter(&OwnedNode::new("Item"));
1982 assert_eq!(third.style.color, Some(Color::literal(RC::Blue)));
1983 }
1984
1985 #[test]
1986 fn adjacent_combinator_requires_immediate_predecessor_type() {
1987 let mut sheet = Stylesheet::new();
1990 sheet
1991 .add("Header + Content", CssStyle::new().color(RC::Green), Origin::User)
1992 .unwrap();
1993
1994 let mut ctx = CascadeContext::new(&sheet);
1995 let _root = ctx.enter(&OwnedNode::new("Root"));
1996
1997 let _sidebar = ctx.enter(&OwnedNode::new("Sidebar"));
1999 ctx.leave();
2000 let content = ctx.enter(&OwnedNode::new("Content"));
2001 assert_eq!(content.style.color, None);
2002
2003 ctx.leave();
2004 let _header = ctx.enter(&OwnedNode::new("Header"));
2006 ctx.leave();
2007 let content2 = ctx.enter(&OwnedNode::new("Content"));
2008 assert_eq!(content2.style.color, Some(Color::literal(RC::Green)));
2009 }
2010
2011 #[test]
2012 fn sibling_plus_descendant_combinator() {
2013 let mut sheet = Stylesheet::new();
2017 sheet
2018 .add("Panel Item + Item", CssStyle::new().color(RC::Red), Origin::User)
2019 .unwrap();
2020
2021 let mut ctx = CascadeContext::new(&sheet);
2022 let _root = ctx.enter(&OwnedNode::new("Root"));
2023 let _panel = ctx.enter(&OwnedNode::new("Panel"));
2024
2025 let first = ctx.enter(&OwnedNode::new("Item"));
2026 assert_eq!(first.style.color, None);
2027 ctx.leave();
2028
2029 let second = ctx.enter(&OwnedNode::new("Item"));
2030 assert_eq!(second.style.color, Some(Color::literal(RC::Red)));
2031 }
2032
2033 #[test]
2034 fn sibling_lists_reset_on_new_parent() {
2035 let mut sheet = Stylesheet::new();
2039 sheet
2040 .add("Item + Item", CssStyle::new().color(RC::Red), Origin::User)
2041 .unwrap();
2042
2043 let mut ctx = CascadeContext::new(&sheet);
2044 let _root = ctx.enter(&OwnedNode::new("Root"));
2045
2046 let _pa = ctx.enter(&OwnedNode::new("ParentA"));
2048 let _pa_first = ctx.enter(&OwnedNode::new("Item"));
2049 ctx.leave();
2050 let _pa_second = ctx.enter(&OwnedNode::new("Item"));
2051 assert_eq!(_pa_second.style.color, Some(Color::literal(RC::Red)));
2052 ctx.leave();
2053 ctx.leave(); let _pb = ctx.enter(&OwnedNode::new("ParentB"));
2057 let pb_first = ctx.enter(&OwnedNode::new("Item"));
2058 assert_eq!(pb_first.style.color, None, "ParentB's first item has no prior sibling");
2059 }
2060
2061 #[test]
2062 fn sibling_combinator_does_not_match_one_shot() {
2063 let mut sheet = Stylesheet::new();
2066 sheet
2067 .add("Item + Item", CssStyle::new().color(RC::Red), Origin::User)
2068 .unwrap();
2069
2070 let node = OwnedNode::new("Item");
2071 let c = sheet.compute(&node, None);
2072 assert_eq!(c.style.color, None);
2073 }
2074
2075 #[test]
2076 fn descendant_combinator_still_matches_via_context() {
2077 let mut sheet = Stylesheet::new();
2080 sheet
2081 .add("Panel Button", CssStyle::new().color(RC::Yellow), Origin::User)
2082 .unwrap();
2083 sheet
2084 .add("Panel > Button", CssStyle::new().bold(), Origin::User)
2085 .unwrap();
2086
2087 let mut ctx = CascadeContext::new(&sheet);
2088 let _panel = ctx.enter(&OwnedNode::new("Panel"));
2089 let btn = ctx.enter(&OwnedNode::new("Button"));
2090 assert_eq!(btn.style.color, Some(Color::literal(RC::Yellow)));
2091 assert!(btn.style.weight.is_some());
2092 }
2093
2094 fn media_sheet() -> Stylesheet {
2099 Stylesheet::parse("@media (min-width: 80) { Button { color: red; } }").unwrap()
2101 }
2102
2103 #[test]
2104 fn media_rule_applies_when_context_matches() {
2105 let sheet = media_sheet();
2106 let mut scratch = ComputeScratch::new();
2107 let media = MediaContext { cols: 100, rows: 24, ..Default::default() };
2108 let c = sheet.compute_with_media(&OwnedNode::new("Button"), None, &mut scratch, &media);
2109 assert_eq!(c.style.color, Some(Color::literal(RC::Red)));
2110 }
2111
2112 #[test]
2113 fn media_rule_skipped_when_context_does_not_match() {
2114 let sheet = media_sheet();
2115 let mut scratch = ComputeScratch::new();
2116 let media = MediaContext { cols: 60, rows: 24, ..Default::default() };
2117 let c = sheet.compute_with_media(&OwnedNode::new("Button"), None, &mut scratch, &media);
2118 assert_eq!(c.style.color, None, "media-gated rule must not apply when cols < 80");
2119 }
2120
2121 #[test]
2122 fn media_rule_skipped_by_default_context() {
2123 let sheet = media_sheet();
2125 let c = sheet.compute(&OwnedNode::new("Button"), None);
2126 assert_eq!(c.style.color, None, "default-context compute does not apply media-gated rules");
2127 }
2128
2129 #[test]
2130 fn plain_and_media_rules_coexist() {
2131 let sheet = Stylesheet::parse(
2133 "Button { color: blue; } @media (min-width: 80) { Button { color: red; } }",
2134 )
2135 .unwrap();
2136 let mut scratch = ComputeScratch::new();
2137
2138 let small = MediaContext { cols: 40, ..Default::default() };
2140 let c_small = sheet.compute_with_media(&OwnedNode::new("Button"), None, &mut scratch, &small);
2141 assert_eq!(c_small.style.color, Some(Color::literal(RC::Blue)));
2142
2143 let large = MediaContext { cols: 120, ..Default::default() };
2145 let c_large = sheet.compute_with_media(&OwnedNode::new("Button"), None, &mut scratch, &large);
2146 assert_eq!(c_large.style.color, Some(Color::literal(RC::Red)));
2147 }
2148
2149 #[test]
2150 fn cascade_context_with_media_applies_gated_rule() {
2151 let sheet = media_sheet();
2152 let mut ctx = CascadeContext::new(&sheet).with_media(MediaContext {
2153 cols: 100,
2154 rows: 24,
2155 ..Default::default()
2156 });
2157 let btn = ctx.enter(&OwnedNode::new("Button"));
2158 assert_eq!(btn.style.color, Some(Color::literal(RC::Red)));
2159
2160 ctx.set_media(MediaContext { cols: 40, ..Default::default() });
2162 ctx.leave();
2163 let btn2 = ctx.enter(&OwnedNode::new("Button"));
2164 assert_eq!(btn2.style.color, None);
2165 }
2166
2167 #[test]
2168 fn cascade_context_media_combinator_path() {
2169 let sheet = Stylesheet::parse(
2172 "@media (min-width: 80) { Panel Button { color: green; } }",
2173 )
2174 .unwrap();
2175 assert!(sheet.has_combinators());
2176
2177 let mut ctx = CascadeContext::new(&sheet).with_media(MediaContext {
2178 cols: 100,
2179 rows: 24,
2180 ..Default::default()
2181 });
2182 let _panel = ctx.enter(&OwnedNode::new("Panel"));
2183 let btn = ctx.enter(&OwnedNode::new("Button"));
2184 assert_eq!(btn.style.color, Some(Color::literal(RC::Green)));
2185
2186 ctx.set_media(MediaContext { cols: 40, ..Default::default() });
2188 ctx.leave();
2189 ctx.leave();
2190 let _panel2 = ctx.enter(&OwnedNode::new("Panel"));
2191 let btn2 = ctx.enter(&OwnedNode::new("Button"));
2192 assert_eq!(btn2.style.color, None);
2193 }
2194
2195 fn media_token_sheet() -> Stylesheet {
2200 Stylesheet::parse(
2202 ":root { --accent: red } @media (min-width: 80) { :root { --accent: blue } } .a { color: var(--accent); }",
2203 )
2204 .unwrap()
2205 }
2206
2207 #[test]
2208 fn media_gated_token_resolves_blue_under_matching_context() {
2209 let sheet = media_token_sheet();
2210 let mut ctx = CascadeContext::new(&sheet).with_media(MediaContext {
2211 cols: 100,
2212 ..Default::default()
2213 });
2214 let a = ctx.enter(&OwnedNode::new("Div").with_classes(["a"]));
2215 assert_eq!(a.style.color, Some(Color::literal(RC::Blue)));
2216 }
2217
2218 #[test]
2219 fn media_gated_token_resolves_red_under_non_matching_context() {
2220 let sheet = media_token_sheet();
2221 let mut ctx = CascadeContext::new(&sheet).with_media(MediaContext {
2222 cols: 60,
2223 ..Default::default()
2224 });
2225 let a = ctx.enter(&OwnedNode::new("Div").with_classes(["a"]));
2226 assert_eq!(a.style.color, Some(Color::literal(RC::Red)));
2227 }
2228
2229 #[test]
2230 fn media_gated_token_resolves_default_via_one_shot_compute() {
2231 let sheet = media_token_sheet();
2234 let a = sheet.compute(&OwnedNode::new("Div").with_classes(["a"]), None);
2235 assert_eq!(a.style.color, Some(Color::literal(RC::Red)));
2236 }
2237
2238 #[test]
2239 fn media_gated_token_via_compute_with_media() {
2240 let sheet = media_token_sheet();
2241 let mut scratch = ComputeScratch::new();
2242 let node = OwnedNode::new("Div").with_classes(["a"]);
2243 let large = MediaContext { cols: 100, ..Default::default() };
2245 let c_large = sheet.compute_with_media(&node, None, &mut scratch, &large);
2246 assert_eq!(c_large.style.color, Some(Color::literal(RC::Blue)));
2247 let small = MediaContext { cols: 60, ..Default::default() };
2249 let c_small = sheet.compute_with_media(&node, None, &mut scratch, &small);
2250 assert_eq!(c_small.style.color, Some(Color::literal(RC::Red)));
2251 }
2252
2253 #[test]
2254 fn non_media_tokens_still_resolve_as_before() {
2255 let sheet = Stylesheet::parse(
2258 ":root { --c: #abcdef } .x { color: var(--c); }",
2259 )
2260 .unwrap();
2261 let node = OwnedNode::new("Div").with_classes(["x"]);
2262 let one_shot = sheet.compute(&node, None);
2263 assert_eq!(
2264 one_shot.style.color,
2265 Some(Color::literal(RC::Rgb(0xab, 0xcd, 0xef)))
2266 );
2267 let mut ctx = CascadeContext::new(&sheet);
2268 let via_ctx = ctx.enter(&node);
2269 assert_eq!(via_ctx.style.color, one_shot.style.color);
2270 }
2271
2272 fn walk_tree_cached(ctx: &mut CascadeContext<'_>, out: &mut Vec<ComputedStyle>) {
2279 out.push(ctx.enter(&OwnedNode::new("Root")));
2280 out.push(ctx.enter(&OwnedNode::new("Panel")));
2281 out.push(ctx.enter(&OwnedNode::new("Text")));
2282 ctx.leave();
2283 ctx.leave();
2284 ctx.leave();
2285 }
2286
2287 #[test]
2288 fn cache_warm_walk_produces_identical_styles() {
2289 let mut sheet = Stylesheet::new();
2293 sheet.add("Root", CssStyle::new().color(RC::Red), Origin::User).unwrap();
2294 sheet.add("Panel", CssStyle::new().padding("1"), Origin::User).unwrap();
2295 sheet.add("Text", CssStyle::new().bold(), Origin::User).unwrap();
2296
2297 let mut ctx = CascadeContext::new(&sheet).with_cache(16);
2298 let mut cold = Vec::new();
2299 walk_tree_cached(&mut ctx, &mut cold);
2300 assert_eq!(ctx.cache().unwrap().len(), 3);
2302
2303 let mut warm = Vec::new();
2305 walk_tree_cached(&mut ctx, &mut warm);
2306
2307 assert_eq!(warm.len(), cold.len());
2309 for (i, (w, c)) in warm.iter().zip(cold.iter()).enumerate() {
2310 assert_eq!(w, c, "warm walk node {i} differs from cold walk");
2311 }
2312 assert_eq!(ctx.cache().unwrap().len(), 3);
2314 }
2315
2316 #[test]
2317 fn cache_invalidated_by_stylesheet_mutation() {
2318 let mut sheet = Stylesheet::new();
2326 sheet.add("Text", CssStyle::new().color(RC::Red), Origin::User).unwrap();
2327
2328 let mut scratch = ComputeScratch::new();
2329 let mut cache = ComputeCache::new(8);
2330 let media = MediaContext::default();
2331 let node = OwnedNode::new("Text");
2332
2333 let (text1, sig1) = sheet.compute_cached(&node, None, None, &media, &mut scratch, &mut cache);
2334 assert_eq!(text1.style.color, Some(Color::literal(RC::Red)));
2335 assert_eq!(cache.len(), 1);
2336
2337 sheet.add("Text", CssStyle::new().color(RC::Blue), Origin::User).unwrap();
2339
2340 let (text2, sig2) = sheet.compute_cached(&node, None, None, &media, &mut scratch, &mut cache);
2343 assert_eq!(
2344 text2.style.color,
2345 Some(Color::literal(RC::Blue)),
2346 "mutation must invalidate the cache"
2347 );
2348 assert_eq!(sig1, sig2);
2351 assert_eq!(cache.len(), 1);
2352 }
2353
2354 #[test]
2355 fn cache_invalidated_by_tokens_mut() {
2356 let mut sheet = Stylesheet::with_tokens(
2358 crate::token::ThemeTokens::new().set("accent", Color::literal(RC::Red)),
2359 );
2360 sheet.add(".a", CssStyle::new().color(Color::var("accent")), Origin::User).unwrap();
2361
2362 let mut scratch = ComputeScratch::new();
2363 let mut cache = ComputeCache::new(8);
2364 let media = MediaContext::default();
2365 let node = OwnedNode::new("Div").with_classes(["a"]);
2366
2367 let (a1, _) = sheet.compute_cached(&node, None, None, &media, &mut scratch, &mut cache);
2368 assert_eq!(a1.style.color, Some(Color::literal(RC::Red)));
2369
2370 sheet.tokens_mut().insert("accent", Color::literal(RC::Blue));
2372
2373 let (a2, _) = sheet.compute_cached(&node, None, None, &media, &mut scratch, &mut cache);
2374 assert_eq!(
2375 a2.style.color,
2376 Some(Color::literal(RC::Blue)),
2377 "tokens_mut must invalidate the cache so the var re-resolves"
2378 );
2379 }
2380
2381 #[test]
2382 fn cache_invalidated_by_media_change() {
2383 let sheet = Stylesheet::parse(
2386 "@media (min-width: 80) { Button { color: red; } }",
2387 )
2388 .unwrap();
2389
2390 let mut ctx = CascadeContext::new(&sheet).with_cache(8).with_media(MediaContext {
2391 cols: 100,
2392 ..Default::default()
2393 });
2394 let big = ctx.enter(&OwnedNode::new("Button"));
2395 assert_eq!(big.style.color, Some(Color::literal(RC::Red)));
2396 ctx.leave();
2397
2398 ctx.set_media(MediaContext { cols: 40, ..Default::default() });
2401 let small = ctx.enter(&OwnedNode::new("Button"));
2402 assert_eq!(small.style.color, None);
2403 }
2404
2405 #[test]
2406 fn cache_parent_dependency_different_parents() {
2407 let mut sheet = Stylesheet::new();
2410 sheet.add("Red", CssStyle::new().color(RC::Red), Origin::User).unwrap();
2411 sheet.add("Blue", CssStyle::new().color(RC::Blue), Origin::User).unwrap();
2412 sheet.add("Child", CssStyle::new(), Origin::User).unwrap();
2414
2415 let mut ctx = CascadeContext::new(&sheet).with_cache(8);
2416
2417 let _red = ctx.enter(&OwnedNode::new("Red"));
2419 let child_a = ctx.enter(&OwnedNode::new("Child"));
2420 assert_eq!(child_a.style.color, Some(Color::literal(RC::Red)));
2421 ctx.leave();
2422 ctx.leave();
2423
2424 let _blue = ctx.enter(&OwnedNode::new("Blue"));
2426 let child_b = ctx.enter(&OwnedNode::new("Child"));
2427 assert_eq!(
2428 child_b.style.color,
2429 Some(Color::literal(RC::Blue)),
2430 "identical Child node with a different parent must produce a different result"
2431 );
2432 }
2433
2434 #[test]
2435 fn cache_works_with_combinator_sheet_descendant() {
2436 let mut sheet = Stylesheet::new();
2439 sheet.add("Panel Text", CssStyle::new().color(RC::Green), Origin::User).unwrap();
2440 assert!(sheet.has_combinators());
2441
2442 let mut ctx = CascadeContext::new(&sheet).with_cache(8);
2443 let _root = ctx.enter(&OwnedNode::new("Root"));
2444 let _panel = ctx.enter(&OwnedNode::new("Panel"));
2445 let text = ctx.enter(&OwnedNode::new("Text"));
2446 assert_eq!(text.style.color, Some(Color::literal(RC::Green)));
2447
2448 ctx.leave();
2450 ctx.leave();
2451 ctx.leave();
2452 let _root = ctx.enter(&OwnedNode::new("Root"));
2453 let _panel = ctx.enter(&OwnedNode::new("Panel"));
2454 let text2 = ctx.enter(&OwnedNode::new("Text"));
2455 assert_eq!(text2.style.color, Some(Color::literal(RC::Green)));
2456 assert_eq!(text2, text, "warm cached walk == cold walk for combinators");
2457 }
2458
2459 #[test]
2460 fn cache_works_with_combinator_sheet_child() {
2461 let mut sheet = Stylesheet::new();
2463 sheet.add("Panel > Text", CssStyle::new().color(RC::Blue), Origin::User).unwrap();
2464 assert!(sheet.has_combinators());
2465
2466 let mut ctx = CascadeContext::new(&sheet).with_cache(8);
2467 let _root = ctx.enter(&OwnedNode::new("Root"));
2468 let _panel = ctx.enter(&OwnedNode::new("Panel"));
2469 let text = ctx.enter(&OwnedNode::new("Text"));
2470 assert_eq!(text.style.color, Some(Color::literal(RC::Blue)));
2471 }
2472
2473 #[test]
2474 fn cache_works_with_sibling_combinator() {
2475 let mut sheet = Stylesheet::new();
2481 sheet.add("Item + Item", CssStyle::new().color(RC::Red), Origin::User).unwrap();
2482 assert!(sheet.has_combinators());
2483
2484 let mut ctx = CascadeContext::new(&sheet).with_cache(16);
2485 let _root = ctx.enter(&OwnedNode::new("Root"));
2486
2487 let first = ctx.enter(&OwnedNode::new("Item"));
2488 assert_eq!(first.style.color, None);
2489 ctx.leave();
2490 let second = ctx.enter(&OwnedNode::new("Item"));
2491 assert_eq!(second.style.color, Some(Color::literal(RC::Red)));
2492 ctx.leave();
2493 ctx.leave();
2494
2495 let _root = ctx.enter(&OwnedNode::new("Root"));
2497 let first2 = ctx.enter(&OwnedNode::new("Item"));
2498 assert_eq!(first2.style.color, None);
2499 ctx.leave();
2500 let second2 = ctx.enter(&OwnedNode::new("Item"));
2501 assert_eq!(second2.style.color, Some(Color::literal(RC::Red)));
2502 }
2503
2504 #[test]
2505 fn cache_off_context_behaves_identically() {
2506 let mut sheet = Stylesheet::new();
2510 sheet.add("Root", CssStyle::new().color(RC::Red), Origin::User).unwrap();
2511 sheet.add("Panel", CssStyle::new().padding("1"), Origin::User).unwrap();
2512 sheet.add("Text", CssStyle::new().bold(), Origin::User).unwrap();
2513
2514 let mut ctx = CascadeContext::new(&sheet);
2516 let ctx_root = ctx.enter(&OwnedNode::new("Root"));
2517 let ctx_panel = ctx.enter(&OwnedNode::new("Panel"));
2518 let ctx_text = ctx.enter(&OwnedNode::new("Text"));
2519
2520 let man_root = sheet.compute(&OwnedNode::new("Root"), None);
2522 let man_panel = sheet.compute(&OwnedNode::new("Panel"), Some(&man_root));
2523 let man_text = sheet.compute(&OwnedNode::new("Text"), Some(&man_panel));
2524
2525 assert_eq!(ctx_root, man_root);
2526 assert_eq!(ctx_panel, man_panel);
2527 assert_eq!(ctx_text, man_text);
2528
2529 assert!(ctx.cache().is_none());
2531 }
2532
2533 #[test]
2534 fn cache_recomputes_correctly_after_mixed_tree_walks() {
2535 let mut sheet = Stylesheet::new();
2540 sheet.add("A", CssStyle::new().color(RC::Red), Origin::User).unwrap();
2541 sheet.add("B", CssStyle::new().color(RC::Blue), Origin::User).unwrap();
2542
2543 let mut ctx = CascadeContext::new(&sheet).with_cache(32);
2544
2545 let _ = ctx.enter(&OwnedNode::new("A"));
2547 let b1 = ctx.enter(&OwnedNode::new("B"));
2548 assert_eq!(b1.style.color, Some(Color::literal(RC::Blue)));
2549 ctx.leave();
2550 ctx.leave();
2551
2552 let _ = ctx.enter(&OwnedNode::new("B"));
2554 let a1 = ctx.enter(&OwnedNode::new("A"));
2555 assert_eq!(a1.style.color, Some(Color::literal(RC::Red)));
2558 ctx.leave();
2559 ctx.leave();
2560
2561 let _ = ctx.enter(&OwnedNode::new("A"));
2563 let b2 = ctx.enter(&OwnedNode::new("B"));
2564 assert_eq!(b2.style.color, Some(Color::literal(RC::Blue)));
2565 assert_eq!(b2, b1, "re-walked subtree is identical to the first");
2566 }
2567
2568 fn supports_sheet() -> Stylesheet {
2573 Stylesheet::parse("@supports (truecolor) { Button { color: red; } }").unwrap()
2574 }
2575
2576 #[test]
2577 fn supports_rule_applies_when_capability_matches() {
2578 let sheet = supports_sheet();
2579 let mut scratch = ComputeScratch::new();
2580 let media = MediaContext { truecolor: true, ..Default::default() };
2581 let c = sheet.compute_with_media(&OwnedNode::new("Button"), None, &mut scratch, &media);
2582 assert_eq!(c.style.color, Some(Color::literal(RC::Red)));
2583 }
2584
2585 #[test]
2586 fn supports_rule_skipped_when_capability_does_not_match() {
2587 let sheet = supports_sheet();
2588 let mut scratch = ComputeScratch::new();
2589 let media = MediaContext { truecolor: false, ..Default::default() };
2590 let c = sheet.compute_with_media(&OwnedNode::new("Button"), None, &mut scratch, &media);
2591 assert_eq!(c.style.color, None, "supports-gated rule must not apply when truecolor is off");
2592 }
2593
2594 #[test]
2595 fn supports_rule_skipped_by_default_context() {
2596 let sheet = supports_sheet();
2598 let c = sheet.compute(&OwnedNode::new("Button"), None);
2599 assert_eq!(c.style.color, None, "default-context compute does not apply supports-gated rules");
2600 }
2601
2602 #[test]
2603 fn supports_property_known_applies() {
2604 let sheet = Stylesheet::parse("@supports (border-style) { .x { border-style: rounded; } }").unwrap();
2606 let mut scratch = ComputeScratch::new();
2607 let media = MediaContext::default();
2608 let c = sheet.compute_with_media(&OwnedNode::new("Div").with_classes(["x"]), None, &mut scratch, &media);
2609 let border = c.style.border.expect("border present");
2610 assert_eq!(
2611 border.style,
2612 crate::box_model::BorderStyleValue::Fixed(crate::box_model::BorderStyle::Rounded)
2613 );
2614 }
2615
2616 #[test]
2617 fn supports_property_unknown_does_not_apply() {
2618 let sheet = Stylesheet::parse("@supports (future-thing) { .x { color: red; } }").unwrap();
2620 let mut scratch = ComputeScratch::new();
2621 let media = MediaContext::default();
2622 let c = sheet.compute_with_media(&OwnedNode::new("Div").with_classes(["x"]), None, &mut scratch, &media);
2623 assert_eq!(c.style.color, None, "supports rule with unknown property must not apply");
2624 }
2625
2626 #[test]
2627 fn supports_combined_with_media_requires_both() {
2628 let sheet = Stylesheet::parse(
2631 "@media (min-width: 80) { @supports (truecolor) { Button { color: green; } } }",
2632 )
2633 .unwrap();
2634
2635 let mut scratch = ComputeScratch::new();
2636 let node = OwnedNode::new("Button");
2637
2638 let both = MediaContext { cols: 100, truecolor: true, ..Default::default() };
2640 let c = sheet.compute_with_media(&node, None, &mut scratch, &both);
2641 assert_eq!(c.style.color, Some(Color::literal(RC::Green)), "both match → applies");
2642
2643 let media_only = MediaContext { cols: 100, truecolor: false, ..Default::default() };
2645 let c = sheet.compute_with_media(&node, None, &mut scratch, &media_only);
2646 assert_eq!(c.style.color, None, "supports fails → no apply");
2647
2648 let supports_only = MediaContext { cols: 40, truecolor: true, ..Default::default() };
2650 let c = sheet.compute_with_media(&node, None, &mut scratch, &supports_only);
2651 assert_eq!(c.style.color, None, "media fails → no apply");
2652 }
2653
2654 #[test]
2655 fn supports_inside_media_applies_when_both_match_via_context() {
2656 let sheet = Stylesheet::parse(
2658 "@media (min-width: 80) { @supports (truecolor) { Button { color: green; } } }",
2659 )
2660 .unwrap();
2661 let mut ctx = CascadeContext::new(&sheet).with_media(MediaContext {
2662 cols: 100,
2663 truecolor: true,
2664 ..Default::default()
2665 });
2666 let btn = ctx.enter(&OwnedNode::new("Button"));
2667 assert_eq!(btn.style.color, Some(Color::literal(RC::Green)));
2668
2669 ctx.set_media(MediaContext { cols: 100, truecolor: false, ..Default::default() });
2671 ctx.leave();
2672 let btn2 = ctx.enter(&OwnedNode::new("Button"));
2673 assert_eq!(btn2.style.color, None);
2674 }
2675
2676 #[test]
2677 fn plain_and_supports_rules_coexist() {
2678 let sheet = Stylesheet::parse(
2682 "Button { color: blue; } @supports (truecolor) { Button { color: red; } }",
2683 )
2684 .unwrap();
2685 let mut scratch = ComputeScratch::new();
2686 let node = OwnedNode::new("Button");
2687
2688 let tc_off = MediaContext { truecolor: false, ..Default::default() };
2690 let c = sheet.compute_with_media(&node, None, &mut scratch, &tc_off);
2691 assert_eq!(c.style.color, Some(Color::literal(RC::Blue)));
2692
2693 let tc_on = MediaContext { truecolor: true, ..Default::default() };
2695 let c = sheet.compute_with_media(&node, None, &mut scratch, &tc_on);
2696 assert_eq!(c.style.color, Some(Color::literal(RC::Red)));
2697 }
2698}