atmos/os_lib/css/
container_query.rs1extern crate alloc;
24use alloc::string::String;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct ContainerSize {
29 pub width: i32,
30 pub height: i32,
31}
32
33pub fn split_name_and_condition(prelude: &str) -> (Option<String>, String) {
37 let s = prelude.trim();
38 if s.is_empty() {
39 return (None, String::new());
40 }
41 let first = s.split_whitespace().next().unwrap_or("");
42 if !first.starts_with('(') && !first.eq_ignore_ascii_case("not") {
44 let rest = s.get(first.len()..).unwrap_or("").trim();
48 return (Some(String::from(first)), String::from(rest));
49 }
50 (None, String::from(s))
51}
52
53pub fn condition_matches(cond: &str, size: ContainerSize) -> bool {
61 let s = cond.trim();
62 if s.is_empty() {
63 return false;
64 }
65 if let Some(parts) = split_top_level(s, " or ") {
67 return parts.iter().any(|p| condition_matches(p, size));
68 }
69 if let Some(parts) = split_top_level(s, " and ") {
70 return parts.iter().all(|p| condition_matches(p, size));
71 }
72 if let Some(rest) = strip_kw_prefix(s, "not") {
73 return !condition_matches(rest, size);
74 }
75 if s.starts_with('(') && s.ends_with(')') && s.len() >= 2 {
77 let Some(inner) = s.get(1..s.len() - 1) else {
78 return false;
79 };
80 if inner.contains(" and ") || inner.contains(" or ") || starts_with_kw(inner, "not") {
81 return condition_matches(inner, size);
82 }
83 return eval_single(inner.trim(), size);
84 }
85 eval_single(s, size)
86}
87
88fn split_top_level<'a>(s: &'a str, sep: &str) -> Option<alloc::vec::Vec<&'a str>> {
90 let bytes = s.as_bytes();
91 let sep_bytes = sep.as_bytes();
92 let mut depth = 0i32;
93 let mut parts = alloc::vec::Vec::new();
94 let mut start = 0usize;
95 let mut i = 0usize;
96 while i < bytes.len() {
97 match bytes[i] {
98 b'(' => depth += 1,
99 b')' => depth -= 1,
100 _ => {}
101 }
102 if depth == 0
103 && i + sep_bytes.len() <= bytes.len()
104 && bytes.get(i..i + sep_bytes.len()) == Some(sep_bytes)
105 {
106 if let Some(part) = s.get(start..i) {
109 parts.push(part.trim());
110 }
111 i += sep_bytes.len();
112 start = i;
113 continue;
114 }
115 i += 1;
116 }
117 if parts.is_empty() {
118 return None;
119 }
120 if let Some(tail) = s.get(start..) {
121 parts.push(tail.trim());
122 }
123 Some(parts)
124}
125
126fn starts_with_kw(s: &str, kw: &str) -> bool {
128 let s = s.trim_start();
129 s.get(..kw.len()).is_some_and(|h| h.eq_ignore_ascii_case(kw))
130 && s.get(kw.len()..)
131 .and_then(|r| r.chars().next())
132 .is_some_and(|c| c.is_whitespace() || c == '(')
133}
134
135fn strip_kw_prefix<'a>(s: &'a str, kw: &str) -> Option<&'a str> {
137 if starts_with_kw(s, kw) {
138 s.trim_start().get(kw.len()..).map(|r| r.trim())
139 } else {
140 None
141 }
142}
143
144fn eval_single(cond: &str, size: ContainerSize) -> bool {
146 let c = cond.trim().trim_matches(|ch| ch == '(' || ch == ')').trim();
147 if c.is_empty() {
148 return false;
149 }
150 for (prefix, is_w, is_min) in [
153 ("min-width:", true, true),
154 ("max-width:", true, false),
155 ("min-height:", false, true),
156 ("max-height:", false, false),
157 ("min-inline-size:", true, true),
158 ("max-inline-size:", true, false),
159 ("min-block-size:", false, true),
160 ("max-block-size:", false, false),
161 ] {
162 if let Some(rest) = strip_ci_prefix(c, prefix) {
163 let v = if is_w { size.width } else { size.height };
164 return parse_px(rest).is_some_and(|px| if is_min { v >= px } else { v <= px });
165 }
166 }
167 for op in [">=", "<=", ">", "<", "="] {
169 if let Some((lhs, rhs)) = c.split_once(op) {
170 let axis = lhs.trim().to_ascii_lowercase();
171 let v = match axis.as_str() {
172 "width" | "inline-size" => size.width,
173 "height" | "block-size" => size.height,
174 _ => return false,
175 };
176 let Some(px) = parse_px(rhs) else {
177 return false;
178 };
179 return match op {
180 ">=" => v >= px,
181 "<=" => v <= px,
182 ">" => v > px,
183 "<" => v < px,
184 _ => v == px,
185 };
186 }
187 }
188 false
190}
191
192fn strip_ci_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
194 if s.get(..prefix.len())
195 .is_some_and(|h| h.eq_ignore_ascii_case(prefix))
196 {
197 return s.get(prefix.len()..);
198 }
199 let name = prefix.trim_end_matches(':');
201 let s_trim = s.trim_start();
202 if s_trim
203 .get(..name.len())
204 .is_some_and(|h| h.eq_ignore_ascii_case(name))
205 {
206 let rest = s_trim.get(name.len()..).unwrap_or("").trim_start();
207 if let Some(after) = rest.strip_prefix(':') {
208 return Some(after);
209 }
210 }
211 None
212}
213
214fn parse_px(s: &str) -> Option<i32> {
216 let t = s.trim().trim_end_matches("px").trim();
217 if t.is_empty() {
218 return None;
219 }
220 t.parse::<f32>()
221 .ok()
222 .filter(|v| v.is_finite())
223 .map(|v| v as i32)
224}