1use super::*;
4
5pub(crate) static IMAGE_SIZE_CACHE: Mutex<Option<BTreeMap<String, (i32, i32)>>> = Mutex::new(None);
6
7pub fn register_image_natural_size(src: &str, w: i32, h: i32) {
17 let mut cache = IMAGE_SIZE_CACHE.lock();
18 if cache.is_none() {
19 *cache = Some(BTreeMap::new());
20 }
21 if let Some(ref mut c) = *cache {
22 c.insert(alloc::string::String::from(src), (w, h));
23 }
24}
25
26#[derive(Debug, Clone, Default)]
27pub struct Rect {
28 pub x: i32,
29 pub y: i32,
30 pub width: i32,
31 pub height: i32,
32}
33
34#[derive(Debug, Clone, Default)]
35pub struct EdgeSizes {
36 pub left: i32,
37 pub right: i32,
38 pub top: i32,
39 pub bottom: i32,
40}
41
42#[derive(Debug, Clone, Default)]
43pub struct Dimensions {
44 pub content: Rect,
45 pub padding: EdgeSizes,
46 pub border: EdgeSizes,
47 pub margin: EdgeSizes,
48}
49
50impl Dimensions {
51 pub fn padding_box(&self) -> Rect {
53 Rect {
54 x: self.content.x - self.padding.left,
55 y: self.content.y - self.padding.top,
56 width: self.content.width + self.padding.left + self.padding.right,
57 height: self.content.height + self.padding.top + self.padding.bottom,
58 }
59 }
60
61 pub fn margin_box(&self) -> Rect {
62 Rect {
63 x: self.content.x - self.padding.left - self.border.left - self.margin.left,
64 y: self.content.y - self.padding.top - self.border.top - self.margin.top,
65 width: self.content.width
66 + self.padding.left
67 + self.padding.right
68 + self.border.left
69 + self.border.right
70 + self.margin.left
71 + self.margin.right,
72 height: self.content.height
73 + self.padding.top
74 + self.padding.bottom
75 + self.border.top
76 + self.border.bottom
77 + self.margin.top
78 + self.margin.bottom,
79 }
80 }
81}
82
83#[derive(Clone)]
85pub struct LayoutBox<'a> {
86 pub dimensions: Dimensions,
87 pub box_type: BoxType<'a>,
88 pub children: Vec<LayoutBox<'a>>,
89 pub trim_leading_space: bool,
94}
95
96#[derive(Clone, Copy)]
97pub enum BoxType<'a> {
98 BlockNode(&'a StyledNode<'a>),
99 InlineNode(&'a StyledNode<'a>),
100 InlineBlockNode(&'a StyledNode<'a>),
101 FlexNode(&'a StyledNode<'a>),
102 GridNode(&'a StyledNode<'a>),
103 TableNode(&'a StyledNode<'a>),
104 TableRowGroupNode(&'a StyledNode<'a>),
105 TableRowNode(&'a StyledNode<'a>),
106 TableCellNode(&'a StyledNode<'a>),
107 AnonymousBlock,
108}
109
110pub(crate) fn default_display_for(tag_name: &str) -> &'static str {
113 match tag_name {
114 "head" | "title" | "meta" | "link" | "style" | "script" => "none",
115 "html" | "body" | "div" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol"
116 | "li" | "form" | "center" | "header" | "footer" | "main" | "section" | "nav" | "hr"
117 | "caption" | "details" | "summary" => "block",
118 "table" => "table",
119 "thead" | "tbody" | "tfoot" => "table-row-group",
120 "tr" => "table-row",
121 "td" | "th" => "table-cell",
122 _ => "inline",
123 }
124}
125
126pub(crate) fn cell_spans(cell: &LayoutBox) -> (usize, usize) {
128 if let BoxType::TableCellNode(styled) = &cell.box_type {
129 if let NodeType::Element { attributes, .. } = &styled.node.node_type {
130 let cs = attributes
131 .get("colspan")
132 .and_then(|v| v.trim().parse::<usize>().ok())
133 .unwrap_or(1)
134 .clamp(1, 256);
135 let rs = attributes
136 .get("rowspan")
137 .and_then(|v| v.trim().parse::<usize>().ok())
138 .unwrap_or(1)
139 .clamp(1, 1024);
140 return (cs, rs);
141 }
142 }
143 (1, 1)
144}
145
146pub(crate) fn box_tag_name<'a>(b: &LayoutBox<'a>) -> Option<&'a str> {
148 let styled = match &b.box_type {
149 BoxType::BlockNode(s)
150 | BoxType::InlineNode(s)
151 | BoxType::InlineBlockNode(s)
152 | BoxType::FlexNode(s)
153 | BoxType::GridNode(s)
154 | BoxType::TableNode(s)
155 | BoxType::TableRowGroupNode(s)
156 | BoxType::TableRowNode(s)
157 | BoxType::TableCellNode(s) => *s,
158 BoxType::AnonymousBlock => return None,
159 };
160 match &styled.node.node_type {
161 NodeType::Element { tag_name, .. } => Some(tag_name.as_str()),
162 NodeType::Text(_) => None,
163 }
164}
165
166pub(crate) fn box_style_value<'a>(b: &LayoutBox<'a>, prop: &str) -> Option<String> {
168 let styled = match &b.box_type {
169 BoxType::BlockNode(s)
170 | BoxType::InlineNode(s)
171 | BoxType::InlineBlockNode(s)
172 | BoxType::FlexNode(s)
173 | BoxType::GridNode(s)
174 | BoxType::TableNode(s)
175 | BoxType::TableRowGroupNode(s)
176 | BoxType::TableRowNode(s)
177 | BoxType::TableCellNode(s) => *s,
178 BoxType::AnonymousBlock => return None,
179 };
180 styled.value(prop)
181}
182
183pub(crate) fn box_style_value_ref<'a>(b: &LayoutBox<'a>, prop: &str) -> Option<&'a str> {
185 match &b.box_type {
186 BoxType::BlockNode(s)
187 | BoxType::InlineNode(s)
188 | BoxType::InlineBlockNode(s)
189 | BoxType::FlexNode(s)
190 | BoxType::GridNode(s)
191 | BoxType::TableNode(s)
192 | BoxType::TableRowGroupNode(s)
193 | BoxType::TableRowNode(s)
194 | BoxType::TableCellNode(s) => s.value_ref(prop),
195 BoxType::AnonymousBlock => None,
196 }
197}
198
199pub(crate) fn box_font_size(b: &LayoutBox) -> f32 {
201 box_style_value(b, "font-size")
202 .and_then(|f| parse_font_size_u32(&f))
203 .unwrap_or(16) as f32
204}
205
206impl<'a> LayoutBox<'a> {
207 pub(crate) fn font_size(&self) -> f32 {
208 box_font_size(self)
209 }
210
211 pub fn client_width(&self) -> i32 {
213 self.dimensions.padding_box().width
214 }
215
216 pub fn client_height(&self) -> i32 {
218 self.dimensions.padding_box().height
219 }
220
221 pub fn scroll_dimensions(&self) -> (i32, i32, i32, i32) {
225 let cw = self.client_width();
226 let ch = self.client_height();
227 let pbox = self.dimensions.padding_box();
228
229 let mut max_right = pbox.width;
231 let mut max_bottom = pbox.height;
232
233 fn collect_max_bounds(parent_pbox: &Rect, b: &LayoutBox, max_r: &mut i32, max_b: &mut i32) {
234 for child in &b.children {
235 let mbox = child.dimensions.margin_box();
236 let r = mbox.x + mbox.width - parent_pbox.x;
237 let b_edge = mbox.y + mbox.height - parent_pbox.y;
238 if r > *max_r {
239 *max_r = r;
240 }
241 if b_edge > *max_b {
242 *max_b = b_edge;
243 }
244 let is_child_scroll = match &child.box_type {
247 BoxType::BlockNode(s)
248 | BoxType::InlineNode(s)
249 | BoxType::InlineBlockNode(s)
250 | BoxType::FlexNode(s)
251 | BoxType::GridNode(s)
252 | BoxType::TableNode(s)
253 | BoxType::TableRowGroupNode(s)
254 | BoxType::TableRowNode(s)
255 | BoxType::TableCellNode(s) => {
256 ["overflow", "overflow-x", "overflow-y"].iter().any(|k| {
257 s.value(k).is_some_and(|v| matches!(v.trim(), "hidden" | "scroll" | "auto"))
258 })
259 }
260 BoxType::AnonymousBlock => false,
261 };
262 if !is_child_scroll {
263 collect_max_bounds(parent_pbox, child, max_r, max_b);
264 }
265 }
266 }
267
268 collect_max_bounds(&pbox, self, &mut max_right, &mut max_bottom);
269
270 (cw, ch, max_right.max(cw), max_bottom.max(ch))
271 }
272
273 pub fn scroll_width(&self) -> i32 {
275 self.scroll_dimensions().2
276 }
277
278 pub fn scroll_height(&self) -> i32 {
280 self.scroll_dimensions().3
281 }
282}
283
284pub(crate) fn box_has_explicit(b: &LayoutBox, prop: &str) -> bool {
286 box_style_value(b, prop)
287 .map(|v| {
288 let t = v.trim().to_lowercase();
289 !t.is_empty() && t != "auto"
290 })
291 .unwrap_or(false)
292}
293
294pub(crate) fn box_aspect_ratio(b: &LayoutBox) -> Option<(f32, f32)> {
296 box_style_value(b, "aspect-ratio").and_then(|v| parse_aspect_ratio(&v))
297}
298
299pub(crate) fn box_flex_grow(b: &LayoutBox) -> i32 {
301 if let Some(g) = box_style_value(b, "flex-grow") {
302 return g.trim().parse::<i32>().unwrap_or(0).max(0);
303 }
304 if let Some(f) = box_style_value(b, "flex") {
305 let t = f.trim().to_lowercase();
306 if t == "none" || t == "initial" {
307 return 0;
308 }
309 if t == "auto" {
310 return 1; }
312 if let Some(first) = t.split_whitespace().next() {
313 return first.parse::<i32>().unwrap_or(0).max(0);
314 }
315 }
316 0
317}
318
319pub(crate) fn is_flex_length_token(tok: &str) -> bool {
329 for suffix in ["px", "%", "rem", "em", "vh", "vw"] {
330 if let Some(n) = tok.strip_suffix(suffix) {
331 if n.trim().parse::<f32>().is_ok() {
332 return true;
333 }
334 }
335 }
336 false
337}
338
339pub(crate) fn box_flex_shrink(b: &LayoutBox) -> i32 {
340 if let Some(s) = box_style_value(b, "flex-shrink") {
341 return s.trim().parse::<i32>().unwrap_or(1).max(0);
342 }
343 if let Some(f) = box_style_value(b, "flex") {
344 let t = f.trim().to_lowercase();
345 if t == "none" {
346 return 0; }
348 if t == "initial" {
349 return 1; }
351 if t == "auto" {
352 return 1; }
354 let parts: Vec<&str> = t.split_whitespace().collect();
356 if let Some(second) = parts.get(1) {
357 if !is_flex_length_token(second) && *second != "auto" {
359 return second.parse::<i32>().unwrap_or(1).max(0);
360 }
361 }
362 return 1;
364 }
365 1
366}
367
368pub(crate) fn box_order(b: &LayoutBox) -> i32 {
370 box_style_value(b, "order")
371 .and_then(|v| v.trim().parse::<i32>().ok())
372 .unwrap_or(0)
373}
374
375pub(crate) fn box_flex_basis(b: &LayoutBox, main_size: i32) -> Option<i32> {
379 let font_px = box_style_value(b, "font-size")
380 .and_then(|f| parse_font_size_u32(&f))
381 .unwrap_or(16) as f32;
382 if let Some(v) = box_style_value(b, "flex-basis") {
384 let t = v.trim().to_lowercase();
385 if t.is_empty() || t == "auto" || t == "content" {
386 return None;
387 }
388 if t == "min-content" {
389 return Some(b.block_intrinsic_widths().0);
390 }
391 if t == "max-content" {
392 return Some(b.block_intrinsic_widths().1);
393 }
394 if t == "fit-content" {
395 let (minc, maxc) = b.block_intrinsic_widths();
396 return Some(maxc.min(main_size.max(0)).max(minc));
397 }
398 if let Some(fit) = t.strip_prefix("fit-content(").and_then(|s| s.strip_suffix(')')) {
399 let limit = resolve_basis_len_with_font(fit, main_size, font_px).unwrap_or(main_size);
400 let (minc, maxc) = b.block_intrinsic_widths();
401 return Some(maxc.min(limit.max(0)).max(minc));
402 }
403 return resolve_basis_len_with_font(&t, main_size, font_px);
404 }
405 if let Some(f) = box_style_value(b, "flex") {
407 let t = f.trim().to_lowercase();
408 if t == "none" || t == "auto" || t == "initial" {
409 return None; }
411 for tok in t.split_whitespace() {
413 if tok == "min-content" {
414 return Some(b.block_intrinsic_widths().0);
415 }
416 if tok == "max-content" {
417 return Some(b.block_intrinsic_widths().1);
418 }
419 if tok == "fit-content" {
420 let (minc, maxc) = b.block_intrinsic_widths();
421 return Some(maxc.min(main_size.max(0)).max(minc));
422 }
423 if is_flex_length_token(tok) {
424 return resolve_basis_len_with_font(tok, main_size, font_px);
425 }
426 }
427 if t.split_whitespace().count() == 1 && t.parse::<i32>().is_ok() {
429 return Some(0);
430 }
431 }
432 None
433}
434
435pub(crate) fn resolve_basis_len_with_font(tok: &str, main_size: i32, font_px: f32) -> Option<i32> {
438 let t = tok.trim();
439 if let Some(p) = t.strip_suffix('%') {
440 return p
441 .trim()
442 .parse::<f32>()
443 .ok()
444 .map(|pct| round_f32_to_i32(main_size as f32 * pct / 100.0).max(0));
445 }
446 if let Some(num) = t.strip_suffix("em") {
447 return num
448 .trim()
449 .parse::<f32>()
450 .ok()
451 .map(|v| round_f32_to_i32(v * font_px).max(0));
452 }
453 parse_px_like(t).map(|v| v.max(0))
454}
455
456pub(crate) fn resolve_basis_len(tok: &str, main_size: i32) -> Option<i32> {
457 resolve_basis_len_with_font(tok, main_size, 16.0)
458}
459
460pub(crate) fn box_align_self(b: &LayoutBox) -> Option<String> {
462 box_style_value(b, "align-self").and_then(|v| {
463 let t = v.trim().to_lowercase();
464 if t.is_empty() || t == "auto" {
465 None
466 } else {
467 Some(t)
468 }
469 })
470}
471
472pub(crate) fn grid_item_align(b: &LayoutBox, self_prop: &str, items_default: &str) -> &'static str {
477 let raw = match box_style_value(b, self_prop) {
478 Some(v) => {
479 let t = v.trim().to_lowercase();
480 if t.is_empty() || t == "auto" {
481 String::from(items_default)
482 } else {
483 t
484 }
485 }
486 None => String::from(items_default),
487 };
488 match raw.as_str() {
489 "center" => "center",
490 "end" | "flex-end" | "self-end" | "right" => "end",
491 "start" | "flex-start" | "self-start" | "left" => "start",
492 _ => "stretch",
493 }
494}
495