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 FitContentWith(alloc::boxed::Box<LengthValue>),
18 MathExpr(String),
21}
22
23pub(crate) fn len_value(node: &StyledNode, name: &str) -> Option<String> {
31 let v = node.value_ref(name)?;
32 if !v.as_bytes().windows(2).any(|w| w.eq_ignore_ascii_case(b"em")) {
34 return Some(String::from(v));
35 }
36 let v = String::from(v);
37 let font_px = node
38 .value("font-size")
39 .and_then(|f| parse_font_size_u32(&f))
40 .unwrap_or(16) as f32;
41 Some(crate::os_lib::web_engine::css_units::resolve_em_lengths(&v, font_px))
42}
43
44pub(crate) fn parse_length_value<S: AsRef<str>>(value: Option<S>) -> Option<LengthValue> {
45 let v = value?;
46 let t = v.as_ref().trim();
47 if t.is_empty() {
48 return None;
49 }
50 if t.eq_ignore_ascii_case("auto") {
51 return Some(LengthValue::Auto);
52 }
53 if t.eq_ignore_ascii_case("min-content") {
54 return Some(LengthValue::MinContent);
55 }
56 if t.eq_ignore_ascii_case("max-content") {
57 return Some(LengthValue::MaxContent);
58 }
59 if t.eq_ignore_ascii_case("fit-content") {
60 return Some(LengthValue::FitContent);
61 }
62 if t.len() >= 13
63 && t.get(..12).is_some_and(|h| h.eq_ignore_ascii_case("fit-content("))
64 && t.ends_with(')')
65 {
66 let inner = t.get(12..t.len() - 1).unwrap_or("").trim();
67 if let Some(parsed) = parse_length_value(Some(inner)) {
68 return Some(LengthValue::FitContentWith(alloc::boxed::Box::new(parsed)));
69 }
70 }
71 if is_css_math_func(t) {
72 return Some(LengthValue::MathExpr(String::from(t)));
73 }
74 if let Some(num) = t.strip_suffix('%') {
75 if let Ok(pct) = num.trim().parse::<f32>() {
76 return Some(LengthValue::Percent(pct / 100.0));
77 }
78 return None;
79 }
80 parse_px_like(t).map(LengthValue::Px)
81}
82
83pub(crate) fn resolve_length_value(value: LengthValue, base: i32) -> Option<i32> {
84 resolve_length_value_with_font(value, base, 16.0)
85}
86
87pub(crate) fn resolve_length_value_with_font(value: LengthValue, base: i32, font_px: f32) -> Option<i32> {
88 match value {
89 LengthValue::Auto => None,
90 LengthValue::Px(px) => Some(px),
91 LengthValue::Percent(pct) => Some(round_f32_to_i32((base as f32) * pct)),
92 LengthValue::MinContent
95 | LengthValue::MaxContent
96 | LengthValue::FitContent
97 | LengthValue::FitContentWith(_) => None,
98 LengthValue::MathExpr(expr) => crate::os_lib::css::eval_css_math_with_font(&expr, base, font_px),
99 }
100}
101
102pub(crate) fn min_max(value: i32, min_v: Option<i32>, max_v: Option<i32>) -> i32 {
103 let mut v = value;
104 if let Some(min_v) = min_v {
105 v = v.max(min_v);
106 }
107 if let Some(max_v) = max_v {
108 v = v.min(max_v);
109 }
110 v
111}
112
113pub(crate) fn parse_aspect_ratio(s: &str) -> Option<(f32, f32)> {
116 let t = s.trim();
117 if t.is_empty() || t.eq_ignore_ascii_case("auto") {
118 return None;
119 }
120 let (num, den) = if let Some((ns, ds)) = t.split_once('/') {
121 (
122 ns.trim().parse::<f32>().ok()?,
123 ds.trim().parse::<f32>().ok()?,
124 )
125 } else {
126 (t.parse::<f32>().ok()?, 1.0)
127 };
128 if num > 0.0 && den > 0.0 {
129 Some((num, den))
130 } else {
131 None
132 }
133}
134
135pub(crate) fn round_f32_to_i32(value: f32) -> i32 {
136 if value >= 0.0 {
137 (value + 0.5) as i32
138 } else {
139 (value - 0.5) as i32
140 }
141}
142
143pub(crate) fn parse_flex_gap(node: &StyledNode, base: i32) -> (i32, i32) {
146 fn px(s: &str, base: i32) -> i32 {
147 match parse_length_value(Some(String::from(s.trim()))) {
148 Some(LengthValue::Px(p)) => p,
149 Some(LengthValue::Percent(pct)) => round_f32_to_i32(base as f32 * pct).max(0),
150 _ => 0,
151 }
152 }
153 let (mut row, mut col) = (0, 0);
154 if let Some(g) = len_value(node, "gap") {
155 let toks: Vec<&str> = g.split_whitespace().collect();
156 if toks.len() == 1 {
157 row = px(toks[0], base);
158 col = row;
159 } else if toks.len() >= 2 {
160 row = px(toks[0], base);
161 col = px(toks[1], base);
162 }
163 }
164 if let Some(rg) = len_value(node, "row-gap") {
165 row = px(&rg, base);
166 }
167 if let Some(cg) = len_value(node, "column-gap") {
168 col = px(&cg, base);
169 }
170 (row, col)
171}
172
173pub(crate) fn resolve_edge(s: &str, base: i32) -> Option<i32> {
174 match parse_length_value(Some(String::from(s))) {
175 Some(LengthValue::Px(px)) => Some(px),
176 Some(LengthValue::Percent(pct)) if base > 0 => Some(round_f32_to_i32(base as f32 * pct)),
177 _ => None,
178 }
179}
180
181pub(crate) fn is_css_math_func(s: &str) -> bool {
182 let t = s.trim();
183 t.starts_with("calc(") || t.starts_with("min(") || t.starts_with("max(") || t.starts_with("clamp(")
184 || t.starts_with("abs(") || t.starts_with("sign(") || t.starts_with("round(")
185 || t.starts_with("mod(") || t.starts_with("rem(")
186}
187
188pub(crate) fn apply_box_value(value: Option<String>, edges: &mut EdgeSizes, base: i32) {
189 let Some(v) = value else { return };
190 let vt = v.trim();
192 if is_css_math_func(vt) {
193 if let Some(px) = crate::os_lib::css::eval_css_math(vt, base) {
194 edges.top = px;
195 edges.right = px;
196 edges.bottom = px;
197 edges.left = px;
198 }
199 return;
200 }
201 let parts: Vec<&str> = v.split_whitespace().collect();
202 if parts.is_empty() {
203 return;
204 }
205 let vals: Vec<Option<i32>> = parts.iter().map(|p| resolve_edge(p, base)).collect();
211 if vals.iter().all(|v| v.is_none()) {
212 return;
213 }
214 let at = |i: usize| vals.get(i).copied().flatten();
215 let (top, right, bottom, left) = match vals.len() {
216 1 => (at(0), at(0), at(0), at(0)),
217 2 => (at(0), at(1), at(0), at(1)),
218 3 => (at(0), at(1), at(2), at(1)),
219 _ => (at(0), at(1), at(2), at(3)),
220 };
221 if let Some(v) = top {
222 edges.top = v;
223 }
224 if let Some(v) = right {
225 edges.right = v;
226 }
227 if let Some(v) = bottom {
228 edges.bottom = v;
229 }
230 if let Some(v) = left {
231 edges.left = v;
232 }
233}
234
235pub(crate) fn apply_edge_value(value: Option<String>, edge: &mut i32, base: i32) {
236 if let Some(v) = value {
237 let t = v.trim();
238 if is_css_math_func(t) {
239 if let Some(px) = crate::os_lib::css::eval_css_math(t, base) {
240 *edge = px;
241 }
242 } else if let Some(px) = resolve_edge(t, base) {
243 *edge = px;
244 }
245 }
246}
247
248pub(crate) fn parse_px_like(s: &str) -> Option<i32> {
249 let t = s.trim();
250 if t.is_empty() {
251 return None;
252 }
253 match t {
263 "thin" => return Some(1),
264 "medium" => return Some(3),
265 "thick" => return Some(5),
266 _ => {}
267 }
268 if let Some(num) = t.strip_suffix("px") {
269 return num.trim().parse::<f32>().ok().map(round_f32_to_i32);
270 }
271 if let Some(num) = t.strip_suffix("vh") {
272 let h = VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed);
273 return num
274 .trim()
275 .parse::<f32>()
276 .ok()
277 .map(|v| round_f32_to_i32((v / 100.0) * h as f32));
278 }
279 if let Some(num) = t.strip_suffix("vw") {
280 let w = VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed);
281 return num
282 .trim()
283 .parse::<f32>()
284 .ok()
285 .map(|v| round_f32_to_i32((v / 100.0) * w as f32));
286 }
287 if let Some(num) = t.strip_suffix("vmin") {
296 let w = VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed);
297 let h = VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed);
298 let m = w.min(h);
299 return num
300 .trim()
301 .parse::<f32>()
302 .ok()
303 .map(|v| round_f32_to_i32((v / 100.0) * m as f32));
304 }
305 if let Some(num) = t.strip_suffix("vmax") {
306 let w = VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed);
307 let h = VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed);
308 let m = w.max(h);
309 return num
310 .trim()
311 .parse::<f32>()
312 .ok()
313 .map(|v| round_f32_to_i32((v / 100.0) * m as f32));
314 }
315 if let Some(num) = t.strip_suffix("rem") {
316 return num
317 .trim()
318 .parse::<f32>()
319 .ok()
320 .map(|v| round_f32_to_i32(v * 16.0));
321 }
322 if let Some(num) = t.strip_suffix("em") {
323 return num
324 .trim()
325 .parse::<f32>()
326 .ok()
327 .map(|v| round_f32_to_i32(v * 16.0));
328 }
329 if let Some(num) = t.strip_suffix("in") {
334 return num.trim().parse::<f32>().ok().map(|v| round_f32_to_i32(v * 96.0));
335 }
336 if let Some(num) = t.strip_suffix("cm") {
337 return num
338 .trim()
339 .parse::<f32>()
340 .ok()
341 .map(|v| round_f32_to_i32(v * 96.0 / 2.54));
342 }
343 if let Some(num) = t.strip_suffix("mm") {
344 return num
345 .trim()
346 .parse::<f32>()
347 .ok()
348 .map(|v| round_f32_to_i32(v * 96.0 / 25.4));
349 }
350 if let Some(num) = t.strip_suffix("pt") {
351 return num
352 .trim()
353 .parse::<f32>()
354 .ok()
355 .map(|v| round_f32_to_i32(v * 96.0 / 72.0));
356 }
357 if let Some(num) = t.strip_suffix("pc") {
358 return num.trim().parse::<f32>().ok().map(|v| round_f32_to_i32(v * 16.0));
359 }
360 if let Some(num) = t.strip_suffix('q') {
361 return num
362 .trim()
363 .parse::<f32>()
364 .ok()
365 .map(|v| round_f32_to_i32(v * 96.0 / 101.6));
366 }
367 t.parse::<f32>().ok().map(round_f32_to_i32)
368}
369
370pub(crate) fn parse_letter_spacing_with_font(val: Option<&str>, containing_width: i32, font_px: f32) -> i32 {
375 match val {
376 None => 0,
377 Some(s) => {
378 let t = s.trim();
379 if t.is_empty() || t.eq_ignore_ascii_case("normal") {
380 0
381 } else if is_css_math_func(t) {
382 crate::os_lib::css::eval_css_math_with_font(t, containing_width, font_px).unwrap_or(0)
383 } else if let Some(num) = t.strip_suffix("em") {
384 num.trim()
385 .parse::<f32>()
386 .ok()
387 .map(|v| round_f32_to_i32(v * font_px))
388 .unwrap_or(0)
389 } else {
390 parse_px_like(t).unwrap_or(0)
391 }
392 }
393 }
394}
395
396pub(crate) fn parse_letter_spacing_ctx(val: Option<&str>, containing_width: i32) -> i32 {
397 parse_letter_spacing_with_font(val, containing_width, 16.0)
398}
399
400pub(crate) fn parse_letter_spacing(val: Option<&str>) -> i32 {
402 parse_letter_spacing_ctx(val, 0)
403}
404
405pub(crate) fn parse_word_spacing_with_font(val: Option<&str>, containing_width: i32, font_px: f32) -> i32 {
407 parse_letter_spacing_with_font(val, containing_width, font_px)
408}
409
410pub(crate) fn parse_word_spacing_ctx(val: Option<&str>, containing_width: i32) -> i32 {
412 parse_letter_spacing_ctx(val, containing_width)
413}
414
415pub(crate) fn parse_word_spacing(val: Option<&str>) -> i32 {
417 parse_letter_spacing_ctx(val, 0)
418}
419
420pub(crate) fn parse_font_size_u32(s: &str) -> Option<u32> {
432 crate::os_lib::web_engine::css_units::parse_font_size(s)
433}
434
435pub(crate) fn shift_layout_box(layout: &mut LayoutBox, dx: i32, dy: i32) {
436 layout.dimensions.content.x += dx;
437 layout.dimensions.content.y += dy;
438 for child in &mut layout.children {
439 shift_layout_box(child, dx, dy);
440 }
441}
442
443pub(crate) fn get_image_size(data: &[u8]) -> Option<(i32, i32)> {
444 if data.len() >= 24 && &data[0..8] == b"\x89PNG\r\n\x1a\n" && &data[12..16] == b"IHDR" {
445 let w = u32::from_be_bytes(data[16..20].try_into().ok()?) as i32;
446 let h = u32::from_be_bytes(data[20..24].try_into().ok()?) as i32;
447 return Some((w, h));
448 }
449 if data.len() >= 26 && &data[0..2] == b"BM" {
450 let w = i32::from_le_bytes(data[18..22].try_into().ok()?);
451 let h = i32::from_le_bytes(data[22..26].try_into().ok()?);
452 if w > 0 && h != 0 {
453 return Some((w, h.abs()));
454 }
455 }
456 if data.len() >= 4 && data[0] == 0xFF && data[1] == 0xD8 {
457 let mut i = 2;
458 while i + 8 < data.len() {
459 if data[i] != 0xFF {
460 i += 1;
461 continue;
462 }
463 let marker = data[i + 1];
464 if marker == 0xD9 || marker == 0xDA {
465 break;
466 }
467 if marker == 0x00 || (0xD0..=0xD7).contains(&marker) {
468 i += 2;
469 continue;
470 }
471 let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
472 if (0xC0..=0xCF).contains(&marker)
473 && marker != 0xC4
474 && marker != 0xC8
475 && marker != 0xCC
476 && i + 9 < data.len()
477 {
478 let h = u16::from_be_bytes([data[i + 5], data[i + 6]]) as i32;
479 let w = u16::from_be_bytes([data[i + 7], data[i + 8]]) as i32;
480 return Some((w, h));
481 }
482 i += 2 + len;
483 }
484 }
485 None
486}
487
488pub(crate) fn get_styled_node_text(node: &StyledNode) -> String {
489 let mut text = String::new();
490 if let NodeType::Text(t) = &node.node.node_type {
491 text.push_str(t);
492 }
493 for child in &node.children {
494 text.push_str(&get_styled_node_text(child));
495 }
496 text
497}
498
499pub(crate) fn parse_length_val_simple(s: &str) -> Option<i32> {
500 if let Some(LengthValue::Px(px)) = parse_length_value(Some(alloc::string::String::from(s))) {
501 Some(px)
502 } else {
503 None
504 }
505}
506
507pub(crate) use core::sync::atomic::{AtomicBool, AtomicI32, Ordering as AtomicOrdering};
515pub static VIEWPORT_HINT_W: AtomicI32 = AtomicI32::new(800);
516pub static VIEWPORT_HINT_H: AtomicI32 = AtomicI32::new(600);
517
518pub static CONTAINER_HINT_W: AtomicI32 = AtomicI32::new(0);
531pub static CONTAINER_HINT_H: AtomicI32 = AtomicI32::new(0);
532
533pub static PREFERS_DARK: AtomicBool = AtomicBool::new(false);
537
538pub static CURRENT_TARGET_ID: spin::Mutex<alloc::string::String> =
541 spin::Mutex::new(alloc::string::String::new());
542
543pub static FOCUS_WITHIN_PTRS: spin::Mutex<alloc::collections::BTreeSet<usize>> =
550 spin::Mutex::new(alloc::collections::BTreeSet::new());
551
552pub static DOCUMENT_LANG: spin::Mutex<alloc::string::String> =
557 spin::Mutex::new(alloc::string::String::new());
558
559pub(crate) static PCT_BASE_H: AtomicI32 = AtomicI32::new(-1);
566
567pub(crate) struct PctBaseHGuard(i32);
569
570impl PctBaseHGuard {
571 pub(crate) fn set(h: i32) -> Self {
572 PctBaseHGuard(PCT_BASE_H.swap(h, AtomicOrdering::Relaxed))
573 }
574}
575
576impl Drop for PctBaseHGuard {
577 fn drop(&mut self) {
578 PCT_BASE_H.store(self.0, AtomicOrdering::Relaxed);
579 }
580}
581
582pub(crate) static FORCED_BLOCK_H: AtomicI32 = AtomicI32::new(-1);
586
587pub(crate) static ABS_CB_X: AtomicI32 = AtomicI32::new(0);
588pub(crate) static ABS_CB_Y: AtomicI32 = AtomicI32::new(0);
589pub(crate) static ABS_CB_W: AtomicI32 = AtomicI32::new(800);
590pub(crate) static ABS_CB_H: AtomicI32 = AtomicI32::new(600);
591pub(crate) static VP_ORIGIN_X: AtomicI32 = AtomicI32::new(0);
593pub(crate) static VP_ORIGIN_Y: AtomicI32 = AtomicI32::new(0);
594
595pub(crate) fn abs_cb_get() -> (i32, i32, i32, i32) {
596 (
597 ABS_CB_X.load(AtomicOrdering::Relaxed),
598 ABS_CB_Y.load(AtomicOrdering::Relaxed),
599 ABS_CB_W.load(AtomicOrdering::Relaxed),
600 ABS_CB_H.load(AtomicOrdering::Relaxed),
601 )
602}
603pub(crate) fn abs_cb_set(x: i32, y: i32, w: i32, h: i32) {
604 ABS_CB_X.store(x, AtomicOrdering::Relaxed);
605 ABS_CB_Y.store(y, AtomicOrdering::Relaxed);
606 ABS_CB_W.store(w, AtomicOrdering::Relaxed);
607 ABS_CB_H.store(h, AtomicOrdering::Relaxed);
608}
609pub(crate) fn vp_origin_get() -> (i32, i32) {
610 (
611 VP_ORIGIN_X.load(AtomicOrdering::Relaxed),
612 VP_ORIGIN_Y.load(AtomicOrdering::Relaxed),
613 )
614}
615
616pub fn set_viewport_hint(w: i32, h: i32) {
618 VIEWPORT_HINT_W.store(w.max(1), AtomicOrdering::Relaxed);
619 VIEWPORT_HINT_H.store(h.max(1), AtomicOrdering::Relaxed);
620}
621
622pub(crate) fn resolve_len_or_pct(s: &str, base: i32) -> Option<i32> {
624 match parse_length_value(Some(alloc::string::String::from(s))) {
625 Some(LengthValue::Px(px)) => Some(px),
626 Some(LengthValue::Percent(pct)) => Some(round_f32_to_i32(base as f32 * pct)),
627 _ => None,
628 }
629}
630
631fn scale_dim(a: i32, num: i32, den: i32) -> i32 {
633 ((a as i64 * num as i64 + den as i64 / 2) / den as i64) as i32
634}
635
636pub(crate) fn get_replaced_element_size(node: &StyledNode) -> Option<(i32, i32)> {
639 get_replaced_element_size_in(node, Some(VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed)))
640}
641
642pub(crate) fn get_replaced_element_size_in(
657 node: &StyledNode,
658 pct_base_w: Option<i32>,
659) -> Option<(i32, i32)> {
660 let NodeType::Element {
661 tag_name,
662 attributes,
663 ..
664 } = &node.node.node_type
665 else {
666 return None;
667 };
668 if !matches!(tag_name.as_str(), "img" | "video" | "input" | "textarea" | "button") {
672 return None;
673 }
674
675 let len_w = |v: &str| -> Option<i32> {
676 match parse_length_value(Some(String::from(v))) {
677 Some(LengthValue::Px(px)) => Some(px),
678 Some(LengthValue::Percent(pct)) => pct_base_w.map(|b| round_f32_to_i32(b as f32 * pct)),
679 _ => None,
680 }
681 };
682 let base_h = PCT_BASE_H.load(AtomicOrdering::Relaxed);
684 let len_h = |v: &str| -> Option<i32> {
685 match parse_length_value(Some(String::from(v))) {
686 Some(LengthValue::Px(px)) => Some(px),
687 Some(LengthValue::Percent(pct)) if base_h >= 0 => {
688 Some(round_f32_to_i32(base_h as f32 * pct))
689 }
690 _ => None,
691 }
692 };
693 let css_w = len_value(node, "width").and_then(|v| len_w(&v));
694 let css_h = len_value(node, "height").and_then(|v| len_h(&v));
695 let max_w = len_value(node, "max-width").and_then(|v| len_w(&v));
696 let max_h = len_value(node, "max-height").and_then(|v| len_h(&v));
697
698 let attr_w = attributes
699 .get("width")
700 .and_then(|v| v.trim().parse::<i32>().ok());
701 let attr_h = attributes
702 .get("height")
703 .and_then(|v| v.trim().parse::<i32>().ok());
704
705 let target_w = css_w.or(attr_w);
706 let target_h = css_h.or(attr_h);
707
708 match tag_name.as_str() {
709 "img" => {
710 if let (Some(w), Some(h)) = (target_w, target_h) {
711 return Some((w, h));
712 }
713 let mut resolved_w = 400;
714 let mut resolved_h = 200;
715 if let Some(src) = attributes.get("src") {
716 let registered = {
724 let cache = IMAGE_SIZE_CACHE.lock();
725 cache.as_ref().and_then(|c| c.get(src).copied())
726 };
727 if let Some((w, h)) = registered {
728 resolved_w = w;
729 resolved_h = h;
730 } else if src.starts_with('/') {
731 {
732 let mut cache = IMAGE_SIZE_CACHE.lock();
733 if cache.is_none() {
734 *cache = Some(BTreeMap::new());
735 }
736 }
737 let mut cached = None;
738 if let Some(ref cache) = *IMAGE_SIZE_CACHE.lock() {
739 if let Some(&size) = cache.get(src) {
740 cached = Some(size);
741 }
742 }
743 if let Some((w, h)) = cached {
744 resolved_w = w;
745 resolved_h = h;
746 } else {
747 let content = {
757 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
758 crate::kernel::fs::get_fs().read_file(src)
759 };
760 if let Some((_meta, content)) = content {
761 if let Some((w, h)) = get_image_size(&content) {
762 resolved_w = w;
763 resolved_h = h;
764 if let Some(ref mut cache) = *IMAGE_SIZE_CACHE.lock() {
765 cache.insert(String::from(src), (w, h));
766 }
767 }
768 }
769 }
770 }
771 }
772 let (mut w, mut h) = match (target_w, target_h) {
774 (Some(w), Some(h)) => (w, h),
775 (Some(w), None) if resolved_w > 0 => (w, scale_dim(w, resolved_h, resolved_w)),
776 (None, Some(h)) if resolved_h > 0 => (scale_dim(h, resolved_w, resolved_h), h),
777 (w, h) => (w.unwrap_or(resolved_w), h.unwrap_or(resolved_h)),
778 };
779 if let Some(mw) = max_w {
781 if mw >= 0 && w > mw {
782 if target_h.is_none() && w > 0 {
783 h = scale_dim(mw, h, w);
784 }
785 w = mw;
786 }
787 }
788 if let Some(mh) = max_h {
789 if mh >= 0 && h > mh {
790 if target_w.is_none() && h > 0 {
791 w = scale_dim(mh, w, h);
792 }
793 h = mh;
794 }
795 }
796 Some((w.max(0), h.max(0)))
797 }
798 "video" => {
799 let default_w = 400;
800 let default_h = 240;
801 Some((target_w.unwrap_or(default_w), target_h.unwrap_or(default_h)))
802 }
803 "input" => {
804 let default_w = 300;
805 let default_h = 32;
806 Some((target_w.unwrap_or(default_w), target_h.unwrap_or(default_h)))
807 }
808 "textarea" => {
809 let default_w = 400;
810 let default_h = 200;
811 Some((target_w.unwrap_or(default_w), target_h.unwrap_or(default_h)))
812 }
813 "button" => {
814 let text = get_styled_node_text(node);
815 let font_size = node
816 .value("font-size")
817 .and_then(|v| parse_font_size_u32(&v))
818 .unwrap_or(16);
819 let text_w =
820 crate::kernel::vector_font::get_vector_string_width(&text, font_size) as i32;
821
822 let class_val = attributes.get("class").map(|s| s.as_str()).unwrap_or("");
823 let has_icon =
824 class_val == "save_btn" || class_val == "load_btn" || class_val == "wrap_btn";
825 let extra_w = if has_icon { 24 } else { 0 };
826 let button_w = (text_w + 24 + extra_w).max(120);
827 let button_h = 36;
828 Some((target_w.unwrap_or(button_w), target_h.unwrap_or(button_h)))
829 }
830 _ => None,
831 }
832}
833
834pub fn parse_line_height(val: Option<String>, font_size: u32) -> i32 {
835 let Some(s) = val else {
836 return (font_size as i32 + 4).max(12);
837 };
838 let t = s.trim();
839 if t.is_empty() || t == "normal" {
840 return (font_size as i32 + 4).max(12);
841 }
842 if let Some(num) = t.strip_suffix("px") {
843 return num
844 .trim()
845 .parse::<i32>()
846 .unwrap_or((font_size as i32 + 4).max(12));
847 }
848 if let Ok(factor) = t.parse::<f32>() {
849 return (font_size as f32 * factor) as i32;
850 }
851 (font_size as i32 + 4).max(12)
852}
853
854pub fn resolve_border_radius_with_font(s: &str, container_w: i32, font_px: f32) -> i32 {
857 let s = s.trim();
858 if let Some(num) = s.strip_suffix("px") {
859 return num.trim().parse::<f32>().map(|v| v as i32).unwrap_or(0);
860 }
861 if let Some(num) = s.strip_suffix('%') {
862 return num
863 .trim()
864 .parse::<f32>()
865 .map(|pct| ((pct / 100.0) * container_w as f32) as i32)
866 .unwrap_or(0);
867 }
868 if let Some(num) = s.strip_suffix("rem") {
869 return num.trim().parse::<f32>().map(|v| (v * 16.0) as i32).unwrap_or(0);
870 }
871 if let Some(num) = s.strip_suffix("em") {
872 return num.trim().parse::<f32>().map(|v| (v * font_px) as i32).unwrap_or(0);
873 }
874 s.parse::<f32>().map(|v| v as i32).unwrap_or(0)
875}
876
877pub fn resolve_border_radius(s: &str, container_w: i32) -> i32 {
878 resolve_border_radius_with_font(s, container_w, 16.0)
879}
880
881pub fn parse_pos_len(v: &str, font_px: f32) -> Option<i32> {
884 let s = v.trim();
885 if let Some(num) = s.strip_suffix("px") {
886 num.trim().parse::<f32>().map(|v| v as i32).ok()
887 } else if let Some(num) = s.strip_suffix("rem") {
888 num.trim().parse::<f32>().map(|v| (v * 16.0) as i32).ok()
889 } else if let Some(num) = s.strip_suffix("em") {
890 num.trim().parse::<f32>().map(|v| (v * font_px) as i32).ok()
891 } else {
892 s.parse::<i32>().ok()
893 }
894}
895