1use super::*;
4
5#[derive(Debug, Clone)]
6pub(crate) enum LengthValue {
7 Auto,
8 Px(i32),
9 Percent(f32),
10 MinContent,
12 MaxContent,
14 FitContent,
16 MathExpr(String),
19}
20
21pub(crate) fn parse_length_value<S: AsRef<str>>(value: Option<S>) -> Option<LengthValue> {
22 let v = value?;
23 let t = v.as_ref().trim();
24 if t.is_empty() {
25 return None;
26 }
27 if t.eq_ignore_ascii_case("auto") {
28 return Some(LengthValue::Auto);
29 }
30 if t.eq_ignore_ascii_case("min-content") {
31 return Some(LengthValue::MinContent);
32 }
33 if t.eq_ignore_ascii_case("max-content") {
34 return Some(LengthValue::MaxContent);
35 }
36 if t.eq_ignore_ascii_case("fit-content") {
37 return Some(LengthValue::FitContent);
38 }
39 if t.starts_with("calc(") || t.starts_with("min(") || t.starts_with("max(") || t.starts_with("clamp(")
40 {
41 return Some(LengthValue::MathExpr(String::from(t)));
42 }
43 if let Some(num) = t.strip_suffix('%') {
44 if let Ok(pct) = num.trim().parse::<f32>() {
45 return Some(LengthValue::Percent(pct / 100.0));
46 }
47 return None;
48 }
49 parse_px_like(t).map(LengthValue::Px)
50}
51
52pub(crate) fn resolve_length_value(value: LengthValue, base: i32) -> Option<i32> {
53 match value {
54 LengthValue::Auto => None,
55 LengthValue::Px(px) => Some(px),
56 LengthValue::Percent(pct) => Some(round_f32_to_i32((base as f32) * pct)),
57 LengthValue::MinContent | LengthValue::MaxContent | LengthValue::FitContent => None,
60 LengthValue::MathExpr(expr) => crate::os_lib::css::eval_css_math(&expr, base),
61 }
62}
63
64pub(crate) fn min_max(value: i32, min_v: Option<i32>, max_v: Option<i32>) -> i32 {
65 let mut v = value;
66 if let Some(min_v) = min_v {
67 v = v.max(min_v);
68 }
69 if let Some(max_v) = max_v {
70 v = v.min(max_v);
71 }
72 v
73}
74
75pub(crate) fn parse_aspect_ratio(s: &str) -> Option<(f32, f32)> {
78 let t = s.trim();
79 if t.is_empty() || t.eq_ignore_ascii_case("auto") {
80 return None;
81 }
82 let (num, den) = if let Some((ns, ds)) = t.split_once('/') {
83 (
84 ns.trim().parse::<f32>().ok()?,
85 ds.trim().parse::<f32>().ok()?,
86 )
87 } else {
88 (t.parse::<f32>().ok()?, 1.0)
89 };
90 if num > 0.0 && den > 0.0 {
91 Some((num, den))
92 } else {
93 None
94 }
95}
96
97pub(crate) fn round_f32_to_i32(value: f32) -> i32 {
98 if value >= 0.0 {
99 (value + 0.5) as i32
100 } else {
101 (value - 0.5) as i32
102 }
103}
104
105pub(crate) fn parse_flex_gap(node: &StyledNode, base: i32) -> (i32, i32) {
108 fn px(s: &str, base: i32) -> i32 {
109 match parse_length_value(Some(String::from(s.trim()))) {
110 Some(LengthValue::Px(p)) => p,
111 Some(LengthValue::Percent(pct)) => round_f32_to_i32(base as f32 * pct).max(0),
112 _ => 0,
113 }
114 }
115 let (mut row, mut col) = (0, 0);
116 if let Some(g) = node.value("gap") {
117 let toks: Vec<&str> = g.split_whitespace().collect();
118 if toks.len() == 1 {
119 row = px(toks[0], base);
120 col = row;
121 } else if toks.len() >= 2 {
122 row = px(toks[0], base);
123 col = px(toks[1], base);
124 }
125 }
126 if let Some(rg) = node.value("row-gap") {
127 row = px(&rg, base);
128 }
129 if let Some(cg) = node.value("column-gap") {
130 col = px(&cg, base);
131 }
132 (row, col)
133}
134
135pub(crate) fn resolve_edge(s: &str, base: i32) -> Option<i32> {
136 match parse_length_value(Some(String::from(s))) {
137 Some(LengthValue::Px(px)) => Some(px),
138 Some(LengthValue::Percent(pct)) if base > 0 => Some(round_f32_to_i32(base as f32 * pct)),
139 _ => None,
140 }
141}
142
143pub(crate) fn apply_box_value(value: Option<String>, edges: &mut EdgeSizes, base: i32) {
144 let Some(v) = value else { return };
145 let vt = v.trim();
148 if vt.starts_with("calc(") || vt.starts_with("min(") || vt.starts_with("max(") || vt.starts_with("clamp(")
149 {
150 if let Some(px) = crate::os_lib::css::eval_css_math(vt, base) {
151 edges.top = px;
152 edges.right = px;
153 edges.bottom = px;
154 edges.left = px;
155 }
156 return;
157 }
158 let parts: Vec<&str> = v.split_whitespace().collect();
159 if parts.is_empty() {
160 return;
161 }
162 let vals: Vec<i32> = parts.iter().filter_map(|p| resolve_edge(p, base)).collect();
163 if vals.is_empty() {
164 return;
165 }
166
167 match vals.len() {
168 1 => {
169 edges.top = vals[0];
170 edges.right = vals[0];
171 edges.bottom = vals[0];
172 edges.left = vals[0];
173 }
174 2 => {
175 edges.top = vals[0];
176 edges.bottom = vals[0];
177 edges.right = vals[1];
178 edges.left = vals[1];
179 }
180 3 => {
181 edges.top = vals[0];
182 edges.right = vals[1];
183 edges.left = vals[1];
184 edges.bottom = vals[2];
185 }
186 _ => {
187 edges.top = vals[0];
188 edges.right = vals[1];
189 edges.bottom = vals[2];
190 edges.left = vals[3];
191 }
192 }
193}
194
195pub(crate) fn apply_edge_value(value: Option<String>, edge: &mut i32, base: i32) {
196 if let Some(v) = value {
197 let t = v.trim();
198 if t.starts_with("calc(") || t.starts_with("min(") || t.starts_with("max(") || t.starts_with("clamp(")
199 {
200 if let Some(px) = crate::os_lib::css::eval_css_math(t, base) {
201 *edge = px;
202 }
203 } else if let Some(px) = resolve_edge(t, base) {
204 *edge = px;
205 }
206 }
207}
208
209pub(crate) fn parse_px_like(s: &str) -> Option<i32> {
210 let t = s.trim();
211 if t.is_empty() {
212 return None;
213 }
214 match t {
224 "thin" => return Some(1),
225 "medium" => return Some(3),
226 "thick" => return Some(5),
227 _ => {}
228 }
229 if let Some(num) = t.strip_suffix("px") {
230 return num.trim().parse::<f32>().ok().map(round_f32_to_i32);
231 }
232 if let Some(num) = t.strip_suffix("vh") {
233 let h = VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed);
234 return num
235 .trim()
236 .parse::<f32>()
237 .ok()
238 .map(|v| round_f32_to_i32((v / 100.0) * h as f32));
239 }
240 if let Some(num) = t.strip_suffix("vw") {
241 let w = VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed);
242 return num
243 .trim()
244 .parse::<f32>()
245 .ok()
246 .map(|v| round_f32_to_i32((v / 100.0) * w as f32));
247 }
248 if let Some(num) = t.strip_suffix("vmin") {
257 let w = VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed);
258 let h = VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed);
259 let m = w.min(h);
260 return num
261 .trim()
262 .parse::<f32>()
263 .ok()
264 .map(|v| round_f32_to_i32((v / 100.0) * m as f32));
265 }
266 if let Some(num) = t.strip_suffix("vmax") {
267 let w = VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed);
268 let h = VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed);
269 let m = w.max(h);
270 return num
271 .trim()
272 .parse::<f32>()
273 .ok()
274 .map(|v| round_f32_to_i32((v / 100.0) * m as f32));
275 }
276 if let Some(num) = t.strip_suffix("rem") {
277 return num
278 .trim()
279 .parse::<f32>()
280 .ok()
281 .map(|v| round_f32_to_i32(v * 16.0));
282 }
283 if let Some(num) = t.strip_suffix("em") {
284 return num
285 .trim()
286 .parse::<f32>()
287 .ok()
288 .map(|v| round_f32_to_i32(v * 16.0));
289 }
290 if let Some(num) = t.strip_suffix("in") {
295 return num.trim().parse::<f32>().ok().map(|v| round_f32_to_i32(v * 96.0));
296 }
297 if let Some(num) = t.strip_suffix("cm") {
298 return num
299 .trim()
300 .parse::<f32>()
301 .ok()
302 .map(|v| round_f32_to_i32(v * 96.0 / 2.54));
303 }
304 if let Some(num) = t.strip_suffix("mm") {
305 return num
306 .trim()
307 .parse::<f32>()
308 .ok()
309 .map(|v| round_f32_to_i32(v * 96.0 / 25.4));
310 }
311 if let Some(num) = t.strip_suffix("pt") {
312 return num
313 .trim()
314 .parse::<f32>()
315 .ok()
316 .map(|v| round_f32_to_i32(v * 96.0 / 72.0));
317 }
318 if let Some(num) = t.strip_suffix("pc") {
319 return num.trim().parse::<f32>().ok().map(|v| round_f32_to_i32(v * 16.0));
320 }
321 if let Some(num) = t.strip_suffix('q') {
322 return num
323 .trim()
324 .parse::<f32>()
325 .ok()
326 .map(|v| round_f32_to_i32(v * 96.0 / 101.6));
327 }
328 t.parse::<f32>().ok().map(round_f32_to_i32)
329}
330
331pub(crate) fn parse_letter_spacing_ctx(val: Option<&str>, containing_width: i32) -> i32 {
335 match val {
336 None => 0,
337 Some(s) => {
338 let t = s.trim();
339 if t.is_empty() || t.eq_ignore_ascii_case("normal") {
340 0
341 } else if t.starts_with("calc(")
342 || t.starts_with("min(")
343 || t.starts_with("max(")
344 || t.starts_with("clamp(")
345 {
346 crate::os_lib::css::eval_css_math(t, containing_width).unwrap_or(0)
347 } else {
348 parse_px_like(t).unwrap_or(0)
349 }
350 }
351 }
352}
353
354pub(crate) fn parse_letter_spacing(val: Option<&str>) -> i32 {
356 parse_letter_spacing_ctx(val, 0)
357}
358
359pub(crate) fn parse_word_spacing_ctx(val: Option<&str>, containing_width: i32) -> i32 {
361 parse_letter_spacing_ctx(val, containing_width)
362}
363
364pub(crate) fn parse_word_spacing(val: Option<&str>) -> i32 {
366 parse_letter_spacing_ctx(val, 0)
367}
368
369pub(crate) fn parse_font_size_u32(s: &str) -> Option<u32> {
381 crate::os_lib::web_engine::css_units::parse_font_size(s)
382}
383
384pub(crate) fn shift_layout_box(layout: &mut LayoutBox, dx: i32, dy: i32) {
385 layout.dimensions.content.x += dx;
386 layout.dimensions.content.y += dy;
387 for child in &mut layout.children {
388 shift_layout_box(child, dx, dy);
389 }
390}
391
392pub(crate) fn get_image_size(data: &[u8]) -> Option<(i32, i32)> {
393 if data.len() >= 24 && &data[0..8] == b"\x89PNG\r\n\x1a\n" && &data[12..16] == b"IHDR" {
394 let w = u32::from_be_bytes(data[16..20].try_into().ok()?) as i32;
395 let h = u32::from_be_bytes(data[20..24].try_into().ok()?) as i32;
396 return Some((w, h));
397 }
398 if data.len() >= 26 && &data[0..2] == b"BM" {
399 let w = i32::from_le_bytes(data[18..22].try_into().ok()?);
400 let h = i32::from_le_bytes(data[22..26].try_into().ok()?);
401 if w > 0 && h != 0 {
402 return Some((w, h.abs()));
403 }
404 }
405 if data.len() >= 4 && data[0] == 0xFF && data[1] == 0xD8 {
406 let mut i = 2;
407 while i + 8 < data.len() {
408 if data[i] != 0xFF {
409 i += 1;
410 continue;
411 }
412 let marker = data[i + 1];
413 if marker == 0xD9 || marker == 0xDA {
414 break;
415 }
416 if marker == 0x00 || (0xD0..=0xD7).contains(&marker) {
417 i += 2;
418 continue;
419 }
420 let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
421 if (0xC0..=0xCF).contains(&marker)
422 && marker != 0xC4
423 && marker != 0xC8
424 && marker != 0xCC
425 && i + 9 < data.len()
426 {
427 let h = u16::from_be_bytes([data[i + 5], data[i + 6]]) as i32;
428 let w = u16::from_be_bytes([data[i + 7], data[i + 8]]) as i32;
429 return Some((w, h));
430 }
431 i += 2 + len;
432 }
433 }
434 None
435}
436
437pub(crate) fn get_styled_node_text(node: &StyledNode) -> String {
438 let mut text = String::new();
439 if let NodeType::Text(t) = &node.node.node_type {
440 text.push_str(t);
441 }
442 for child in &node.children {
443 text.push_str(&get_styled_node_text(child));
444 }
445 text
446}
447
448pub(crate) fn parse_length_val_simple(s: &str) -> Option<i32> {
449 if let Some(LengthValue::Px(px)) = parse_length_value(Some(alloc::string::String::from(s))) {
450 Some(px)
451 } else {
452 None
453 }
454}
455
456pub(crate) use core::sync::atomic::{AtomicBool, AtomicI32, Ordering as AtomicOrdering};
464pub static VIEWPORT_HINT_W: AtomicI32 = AtomicI32::new(800);
465pub static VIEWPORT_HINT_H: AtomicI32 = AtomicI32::new(600);
466
467pub static CONTAINER_HINT_W: AtomicI32 = AtomicI32::new(0);
480pub static CONTAINER_HINT_H: AtomicI32 = AtomicI32::new(0);
481
482pub static PREFERS_DARK: AtomicBool = AtomicBool::new(false);
486
487pub static CURRENT_TARGET_ID: spin::Mutex<alloc::string::String> =
490 spin::Mutex::new(alloc::string::String::new());
491
492pub static FOCUS_WITHIN_PTRS: spin::Mutex<alloc::collections::BTreeSet<usize>> =
499 spin::Mutex::new(alloc::collections::BTreeSet::new());
500
501pub static DOCUMENT_LANG: spin::Mutex<alloc::string::String> =
506 spin::Mutex::new(alloc::string::String::new());
507
508pub(crate) static ABS_CB_X: AtomicI32 = AtomicI32::new(0);
511pub(crate) static ABS_CB_Y: AtomicI32 = AtomicI32::new(0);
512pub(crate) static ABS_CB_W: AtomicI32 = AtomicI32::new(800);
513pub(crate) static ABS_CB_H: AtomicI32 = AtomicI32::new(600);
514pub(crate) static VP_ORIGIN_X: AtomicI32 = AtomicI32::new(0);
516pub(crate) static VP_ORIGIN_Y: AtomicI32 = AtomicI32::new(0);
517
518pub(crate) fn abs_cb_get() -> (i32, i32, i32, i32) {
519 (
520 ABS_CB_X.load(AtomicOrdering::Relaxed),
521 ABS_CB_Y.load(AtomicOrdering::Relaxed),
522 ABS_CB_W.load(AtomicOrdering::Relaxed),
523 ABS_CB_H.load(AtomicOrdering::Relaxed),
524 )
525}
526pub(crate) fn abs_cb_set(x: i32, y: i32, w: i32, h: i32) {
527 ABS_CB_X.store(x, AtomicOrdering::Relaxed);
528 ABS_CB_Y.store(y, AtomicOrdering::Relaxed);
529 ABS_CB_W.store(w, AtomicOrdering::Relaxed);
530 ABS_CB_H.store(h, AtomicOrdering::Relaxed);
531}
532pub(crate) fn vp_origin_get() -> (i32, i32) {
533 (
534 VP_ORIGIN_X.load(AtomicOrdering::Relaxed),
535 VP_ORIGIN_Y.load(AtomicOrdering::Relaxed),
536 )
537}
538
539pub fn set_viewport_hint(w: i32, h: i32) {
541 VIEWPORT_HINT_W.store(w.max(1), AtomicOrdering::Relaxed);
542 VIEWPORT_HINT_H.store(h.max(1), AtomicOrdering::Relaxed);
543}
544
545pub(crate) fn resolve_len_or_pct(s: &str, base: i32) -> Option<i32> {
547 match parse_length_value(Some(alloc::string::String::from(s))) {
548 Some(LengthValue::Px(px)) => Some(px),
549 Some(LengthValue::Percent(pct)) => Some(round_f32_to_i32(base as f32 * pct)),
550 _ => None,
551 }
552}
553
554pub(crate) fn get_replaced_element_size(node: &StyledNode) -> Option<(i32, i32)> {
555 let NodeType::Element {
556 tag_name,
557 attributes,
558 ..
559 } = &node.node.node_type
560 else {
561 return None;
562 };
563
564 let css_w = node
566 .value("width")
567 .and_then(|v| resolve_len_or_pct(&v, VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed)));
568 let css_h = node
569 .value("height")
570 .and_then(|v| resolve_len_or_pct(&v, VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed)));
571
572 let attr_w = attributes
573 .get("width")
574 .and_then(|v| v.trim().parse::<i32>().ok());
575 let attr_h = attributes
576 .get("height")
577 .and_then(|v| v.trim().parse::<i32>().ok());
578
579 let target_w = css_w.or(attr_w);
580 let target_h = css_h.or(attr_h);
581
582 match tag_name.as_str() {
583 "img" => {
584 if let (Some(w), Some(h)) = (target_w, target_h) {
585 return Some((w, h));
586 }
587 let mut resolved_w = 400;
588 let mut resolved_h = 200;
589 if let Some(src) = attributes.get("src") {
590 let registered = {
598 let cache = IMAGE_SIZE_CACHE.lock();
599 cache.as_ref().and_then(|c| c.get(src).copied())
600 };
601 if let Some((w, h)) = registered {
602 resolved_w = w;
603 resolved_h = h;
604 } else if src.starts_with('/') {
605 {
606 let mut cache = IMAGE_SIZE_CACHE.lock();
607 if cache.is_none() {
608 *cache = Some(BTreeMap::new());
609 }
610 }
611 let mut cached = None;
612 if let Some(ref cache) = *IMAGE_SIZE_CACHE.lock() {
613 if let Some(&size) = cache.get(src) {
614 cached = Some(size);
615 }
616 }
617 if let Some((w, h)) = cached {
618 resolved_w = w;
619 resolved_h = h;
620 } else {
621 let content = {
631 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
632 crate::kernel::fs::get_fs().read_file(src)
633 };
634 if let Some((_meta, content)) = content {
635 if let Some((w, h)) = get_image_size(&content) {
636 resolved_w = w;
637 resolved_h = h;
638 if let Some(ref mut cache) = *IMAGE_SIZE_CACHE.lock() {
639 cache.insert(String::from(src), (w, h));
640 }
641 }
642 }
643 }
644 }
645 }
646 Some((
647 target_w.unwrap_or(resolved_w),
648 target_h.unwrap_or(resolved_h),
649 ))
650 }
651 "video" => {
652 let default_w = 400;
653 let default_h = 240;
654 Some((target_w.unwrap_or(default_w), target_h.unwrap_or(default_h)))
655 }
656 "input" => {
657 let default_w = 300;
658 let default_h = 32;
659 Some((target_w.unwrap_or(default_w), target_h.unwrap_or(default_h)))
660 }
661 "textarea" => {
662 let default_w = 400;
663 let default_h = 200;
664 Some((target_w.unwrap_or(default_w), target_h.unwrap_or(default_h)))
665 }
666 "button" => {
667 let text = get_styled_node_text(node);
668 let font_size = node
669 .value("font-size")
670 .and_then(|v| parse_font_size_u32(&v))
671 .unwrap_or(16);
672 let text_w =
673 crate::kernel::vector_font::get_vector_string_width(&text, font_size) as i32;
674
675 let class_val = attributes.get("class").map(|s| s.as_str()).unwrap_or("");
676 let has_icon =
677 class_val == "save_btn" || class_val == "load_btn" || class_val == "wrap_btn";
678 let extra_w = if has_icon { 24 } else { 0 };
679 let button_w = (text_w + 24 + extra_w).max(120);
680 let button_h = 36;
681 Some((target_w.unwrap_or(button_w), target_h.unwrap_or(button_h)))
682 }
683 _ => None,
684 }
685}
686
687pub fn parse_line_height(val: Option<String>, font_size: u32) -> i32 {
688 let Some(s) = val else {
689 return (font_size as i32 + 4).max(12);
690 };
691 let t = s.trim();
692 if t.is_empty() || t == "normal" {
693 return (font_size as i32 + 4).max(12);
694 }
695 if let Some(num) = t.strip_suffix("px") {
696 return num
697 .trim()
698 .parse::<i32>()
699 .unwrap_or((font_size as i32 + 4).max(12));
700 }
701 if let Ok(factor) = t.parse::<f32>() {
702 return (font_size as f32 * factor) as i32;
703 }
704 (font_size as i32 + 4).max(12)
705}
706