⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
layout.rs

1//! Layout Engine
2//!
3//! Transforms a styled DOM tree into a layout box tree with computed
4//! positions and dimensions. Supports block layout, inline layout with
5//! line boxes and word wrapping, float layout with exclusion areas,
6//! positioned layout (relative, absolute, fixed), and margin collapsing.
7//! All measurements use 26.6 fixed-point (i32).
8
9#![allow(dead_code)]
10
11use alloc::{
12    string::{String, ToString},
13    vec::Vec,
14};
15
16use super::{
17    css_parser::{fp_to_px, px_to_fp, FixedPoint},
18    dom::{Document, NodeId, NodeType},
19    style::{Clear, ComputedStyle, Display, Float, Position, StyleResolver, TextAlign, WhiteSpace},
20};
21
22/// A rectangle with position and size in fixed-point units
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24pub struct Rect {
25    pub x: FixedPoint,
26    pub y: FixedPoint,
27    pub width: FixedPoint,
28    pub height: FixedPoint,
29}
30
31/// Edge sizes (margin, padding, border)
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33pub struct EdgeSizes {
34    pub top: FixedPoint,
35    pub right: FixedPoint,
36    pub bottom: FixedPoint,
37    pub left: FixedPoint,
38}
39
40/// Complete dimensions of a layout box
41#[derive(Debug, Clone, Copy, Default)]
42pub struct Dimensions {
43    pub content: Rect,
44    pub padding: EdgeSizes,
45    pub border: EdgeSizes,
46    pub margin: EdgeSizes,
47}
48
49impl Dimensions {
50    /// Get the padding box (content + padding)
51    pub fn padding_box(&self) -> Rect {
52        Rect {
53            x: self.content.x - self.padding.left,
54            y: self.content.y - self.padding.top,
55            width: self.content.width + self.padding.left + self.padding.right,
56            height: self.content.height + self.padding.top + self.padding.bottom,
57        }
58    }
59
60    /// Get the border box (content + padding + border)
61    pub fn border_box(&self) -> Rect {
62        let pb = self.padding_box();
63        Rect {
64            x: pb.x - self.border.left,
65            y: pb.y - self.border.top,
66            width: pb.width + self.border.left + self.border.right,
67            height: pb.height + self.border.top + self.border.bottom,
68        }
69    }
70
71    /// Get the margin box (content + padding + border + margin)
72    pub fn margin_box(&self) -> Rect {
73        let bb = self.border_box();
74        Rect {
75            x: bb.x - self.margin.left,
76            y: bb.y - self.margin.top,
77            width: bb.width + self.margin.left + self.margin.right,
78            height: bb.height + self.margin.top + self.margin.bottom,
79        }
80    }
81}
82
83/// Type of layout box
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
85pub enum BoxType {
86    #[default]
87    Block,
88    Inline,
89    Anonymous,
90}
91
92/// A fragment within an inline line box
93#[derive(Debug, Clone)]
94pub struct InlineFragment {
95    pub node_id: Option<NodeId>,
96    pub text: String,
97    pub width: FixedPoint,
98    pub ascent: FixedPoint,
99    pub descent: FixedPoint,
100    pub color: u32,
101    pub font_size: FixedPoint,
102    pub font_weight: u16,
103    pub underline: bool,
104    pub line_through: bool,
105}
106
107/// A line box containing inline fragments
108#[derive(Debug, Clone, Default)]
109pub struct LineBox {
110    pub baseline: FixedPoint,
111    pub height: FixedPoint,
112    pub width: FixedPoint,
113    pub x: FixedPoint,
114    pub y: FixedPoint,
115    pub fragments: Vec<InlineFragment>,
116}
117
118/// A float exclusion area
119#[derive(Debug, Clone, Copy)]
120struct FloatExclusion {
121    x: FixedPoint,
122    y: FixedPoint,
123    width: FixedPoint,
124    height: FixedPoint,
125    float_type: Float,
126}
127
128/// A layout box in the layout tree
129#[derive(Debug, Clone)]
130pub struct LayoutBox {
131    pub box_type: BoxType,
132    pub dimensions: Dimensions,
133    pub style: ComputedStyle,
134    pub children: Vec<LayoutBox>,
135    pub node_id: Option<NodeId>,
136    pub line_boxes: Vec<LineBox>,
137}
138
139impl Default for LayoutBox {
140    fn default() -> Self {
141        Self {
142            box_type: BoxType::Block,
143            dimensions: Dimensions::default(),
144            style: ComputedStyle::default(),
145            children: Vec::new(),
146            node_id: None,
147            line_boxes: Vec::new(),
148        }
149    }
150}
151
152impl LayoutBox {
153    /// Create a new layout box
154    pub fn new(box_type: BoxType, style: ComputedStyle, node_id: Option<NodeId>) -> Self {
155        Self {
156            box_type,
157            dimensions: Dimensions::default(),
158            style,
159            children: Vec::new(),
160            node_id,
161            line_boxes: Vec::new(),
162        }
163    }
164
165    /// Get the margin box height
166    pub fn margin_box_height(&self) -> FixedPoint {
167        self.dimensions.margin_box().height
168    }
169}
170
171/// Character width in fixed-point (8 pixels per char for 8x16 font)
172const CHAR_WIDTH: FixedPoint = 8 * 64; // 8px in 26.6 FP
173
174/// Layout context for tracking float exclusions and state
175struct LayoutContext {
176    left_floats: Vec<FloatExclusion>,
177    right_floats: Vec<FloatExclusion>,
178    viewport_width: FixedPoint,
179    viewport_height: FixedPoint,
180}
181
182impl LayoutContext {
183    fn new(viewport_width: i32, viewport_height: i32) -> Self {
184        Self {
185            left_floats: Vec::new(),
186            right_floats: Vec::new(),
187            viewport_width: px_to_fp(viewport_width),
188            viewport_height: px_to_fp(viewport_height),
189        }
190    }
191
192    /// Get available width at a given y position
193    fn available_width_at(
194        &self,
195        y: FixedPoint,
196        container_width: FixedPoint,
197    ) -> (FixedPoint, FixedPoint) {
198        let mut left_edge = 0;
199        let mut right_edge = container_width;
200
201        for float in &self.left_floats {
202            if y >= float.y && y < float.y + float.height {
203                let edge = float.x + float.width;
204                if edge > left_edge {
205                    left_edge = edge;
206                }
207            }
208        }
209
210        for float in &self.right_floats {
211            if y >= float.y && y < float.y + float.height && float.x < right_edge {
212                right_edge = float.x;
213            }
214        }
215
216        (left_edge, right_edge)
217    }
218
219    /// Clear floats up to a given type
220    fn clear_y(&self, clear: Clear, current_y: FixedPoint) -> FixedPoint {
221        let mut y = current_y;
222
223        if matches!(clear, Clear::Left | Clear::Both) {
224            for float in &self.left_floats {
225                let bottom = float.y + float.height;
226                if bottom > y {
227                    y = bottom;
228                }
229            }
230        }
231
232        if matches!(clear, Clear::Right | Clear::Both) {
233            for float in &self.right_floats {
234                let bottom = float.y + float.height;
235                if bottom > y {
236                    y = bottom;
237                }
238            }
239        }
240
241        y
242    }
243}
244
245/// Build a layout tree from a styled DOM
246pub fn build_layout_tree(
247    doc: &Document,
248    resolver: &StyleResolver,
249    viewport_width: i32,
250    viewport_height: i32,
251) -> LayoutBox {
252    let mut ctx = LayoutContext::new(viewport_width, viewport_height);
253    let root_style = resolver.resolve(doc, doc.root, None);
254    let mut root = build_layout_box(doc, doc.root, resolver, None);
255    root.dimensions.content.width = px_to_fp(viewport_width);
256    layout_box(&mut root, &mut ctx);
257    let _ = root_style;
258    root
259}
260
261/// Recursively build layout boxes from DOM nodes
262fn build_layout_box(
263    doc: &Document,
264    node_id: NodeId,
265    resolver: &StyleResolver,
266    parent_style: Option<&ComputedStyle>,
267) -> LayoutBox {
268    let node = match doc.arena.get(node_id) {
269        Some(n) => n,
270        None => return LayoutBox::default(),
271    };
272
273    let style = resolver.resolve(doc, node_id, parent_style);
274
275    // Skip display:none
276    if style.display == Display::None {
277        let mut lb = LayoutBox::new(BoxType::Block, style, Some(node_id));
278        lb.style.display = Display::None;
279        return lb;
280    }
281
282    let box_type = match node.node_type {
283        NodeType::Text => BoxType::Inline,
284        NodeType::Element => match style.display {
285            Display::Block | Display::ListItem | Display::Flex | Display::Table => BoxType::Block,
286            Display::Inline | Display::InlineBlock => BoxType::Inline,
287            Display::TableRow
288            | Display::TableCell
289            | Display::TableHeaderGroup
290            | Display::TableRowGroup
291            | Display::TableFooterGroup => BoxType::Block,
292            Display::None => BoxType::Block,
293        },
294        _ => BoxType::Block,
295    };
296
297    let mut layout_box = LayoutBox::new(box_type, style.clone(), Some(node_id));
298
299    // Process children
300    let children_ids: Vec<NodeId> = node.children.clone();
301    let mut has_block = false;
302    let mut has_inline = false;
303
304    // First pass: determine if we have mixed content
305    for &child_id in &children_ids {
306        if let Some(child_node) = doc.arena.get(child_id) {
307            let child_style = resolver.resolve(doc, child_id, Some(&style));
308            if child_style.display == Display::None {
309                continue;
310            }
311            match child_node.node_type {
312                NodeType::Text => has_inline = true,
313                NodeType::Element => match child_style.display {
314                    Display::Block
315                    | Display::ListItem
316                    | Display::Flex
317                    | Display::Table
318                    | Display::TableRow
319                    | Display::TableCell
320                    | Display::TableHeaderGroup
321                    | Display::TableRowGroup
322                    | Display::TableFooterGroup => has_block = true,
323                    Display::None => {}
324                    _ => has_inline = true,
325                },
326                _ => {}
327            }
328        }
329    }
330
331    // Build child layout boxes
332    if has_block && has_inline {
333        // Mixed content: wrap inline runs in anonymous blocks
334        let mut current_anon: Option<LayoutBox> = None;
335
336        for &child_id in &children_ids {
337            let child_box = build_layout_box(doc, child_id, resolver, Some(&style));
338            if child_box.style.display == Display::None {
339                continue;
340            }
341            if child_box.box_type == BoxType::Inline {
342                if current_anon.is_none() {
343                    current_anon = Some(LayoutBox::new(
344                        BoxType::Anonymous,
345                        ComputedStyle::default(),
346                        None,
347                    ));
348                }
349                if let Some(ref mut anon) = current_anon {
350                    anon.children.push(child_box);
351                }
352            } else {
353                if let Some(anon) = current_anon.take() {
354                    layout_box.children.push(anon);
355                }
356                layout_box.children.push(child_box);
357            }
358        }
359        if let Some(anon) = current_anon.take() {
360            layout_box.children.push(anon);
361        }
362    } else {
363        for &child_id in &children_ids {
364            let child_box = build_layout_box(doc, child_id, resolver, Some(&style));
365            if child_box.style.display == Display::None {
366                continue;
367            }
368            layout_box.children.push(child_box);
369        }
370    }
371
372    layout_box
373}
374
375/// Layout a box and its children
376fn layout_box(layout: &mut LayoutBox, ctx: &mut LayoutContext) {
377    if layout.style.display == Display::None {
378        return;
379    }
380
381    match layout.box_type {
382        BoxType::Block | BoxType::Anonymous => layout_block(layout, ctx),
383        BoxType::Inline => {
384            // Inline boxes are laid out as part of their parent's line boxes
385        }
386    }
387}
388
389/// Layout a block-level box
390fn layout_block(layout: &mut LayoutBox, ctx: &mut LayoutContext) {
391    // Calculate width
392    calculate_width(layout, ctx);
393
394    // Calculate padding, border, margin
395    calculate_box_model(layout);
396
397    // Handle clear
398    if layout.style.clear != Clear::None {
399        let new_y = ctx.clear_y(layout.style.clear, layout.dimensions.content.y);
400        layout.dimensions.content.y = new_y;
401    }
402
403    // Layout children
404    let mut cursor_y: FixedPoint = 0;
405    let mut prev_margin_bottom: FixedPoint = 0;
406    let has_inline_children = layout
407        .children
408        .iter()
409        .any(|c| c.box_type == BoxType::Inline);
410
411    if has_inline_children {
412        // Inline layout: build line boxes
413        let line_boxes = layout_inline_children(layout, ctx);
414        for lb in &line_boxes {
415            cursor_y += lb.height;
416        }
417        layout.line_boxes = line_boxes;
418    } else {
419        // Block layout: stack children vertically
420        for i in 0..layout.children.len() {
421            // Handle float children
422            if layout.children[i].style.float != Float::None {
423                let container_width = layout.dimensions.content.width;
424                layout_float(&mut layout.children[i], ctx, cursor_y, container_width);
425                continue;
426            }
427
428            // Position child
429            layout.children[i].dimensions.content.x =
430                layout.dimensions.content.x + layout.children[i].dimensions.margin.left;
431            layout.children[i].dimensions.content.y = layout.dimensions.content.y + cursor_y;
432
433            // Margin collapsing
434            let child_margin_top = layout.children[i].dimensions.margin.top;
435            if i > 0 && prev_margin_bottom > 0 && child_margin_top > 0 {
436                let collapsed = core::cmp::max(prev_margin_bottom, child_margin_top);
437                let overlap = prev_margin_bottom + child_margin_top - collapsed;
438                layout.children[i].dimensions.content.y -= overlap;
439                cursor_y -= overlap;
440            }
441
442            // Recursively layout child
443            layout_box(&mut layout.children[i], ctx);
444
445            // Advance cursor
446            cursor_y += layout.children[i].dimensions.margin_box().height;
447            prev_margin_bottom = layout.children[i].dimensions.margin.bottom;
448        }
449    }
450
451    // Calculate height
452    calculate_height(layout, cursor_y);
453
454    // Handle positioned elements
455    position_children(layout, ctx);
456}
457
458/// Calculate the width of a block-level box
459fn calculate_width(layout: &mut LayoutBox, ctx: &LayoutContext) {
460    let parent_width = if layout.dimensions.content.width > 0 {
461        layout.dimensions.content.width
462    } else {
463        ctx.viewport_width
464    };
465
466    if let Some(w) = layout.style.width {
467        layout.dimensions.content.width = w;
468    } else if layout.box_type == BoxType::Block || layout.box_type == BoxType::Anonymous {
469        // Block boxes fill available width minus margins/padding/border
470        let margin_lr = layout.style.margin_left + layout.style.margin_right;
471        let padding_lr = layout.style.padding_left + layout.style.padding_right;
472        let border_lr = layout.style.border_left_width + layout.style.border_right_width;
473        layout.dimensions.content.width = parent_width - margin_lr - padding_lr - border_lr;
474        if layout.dimensions.content.width < 0 {
475            layout.dimensions.content.width = 0;
476        }
477    }
478
479    // Apply min/max width constraints
480    if layout.dimensions.content.width < layout.style.min_width {
481        layout.dimensions.content.width = layout.style.min_width;
482    }
483    if let Some(max) = layout.style.max_width {
484        if layout.dimensions.content.width > max {
485            layout.dimensions.content.width = max;
486        }
487    }
488}
489
490/// Calculate padding, border, margin from style
491fn calculate_box_model(layout: &mut LayoutBox) {
492    layout.dimensions.padding = EdgeSizes {
493        top: layout.style.padding_top,
494        right: layout.style.padding_right,
495        bottom: layout.style.padding_bottom,
496        left: layout.style.padding_left,
497    };
498    layout.dimensions.border = EdgeSizes {
499        top: layout.style.border_top_width,
500        right: layout.style.border_right_width,
501        bottom: layout.style.border_bottom_width,
502        left: layout.style.border_left_width,
503    };
504    layout.dimensions.margin = EdgeSizes {
505        top: layout.style.margin_top,
506        right: layout.style.margin_right,
507        bottom: layout.style.margin_bottom,
508        left: layout.style.margin_left,
509    };
510}
511
512/// Calculate the height of a block box
513fn calculate_height(layout: &mut LayoutBox, content_height: FixedPoint) {
514    if let Some(h) = layout.style.height {
515        layout.dimensions.content.height = h;
516    } else {
517        layout.dimensions.content.height = content_height;
518    }
519
520    // Apply min/max height constraints
521    if layout.dimensions.content.height < layout.style.min_height {
522        layout.dimensions.content.height = layout.style.min_height;
523    }
524    if let Some(max) = layout.style.max_height {
525        if layout.dimensions.content.height > max {
526            layout.dimensions.content.height = max;
527        }
528    }
529}
530
531/// Layout inline children into line boxes
532fn layout_inline_children(parent: &LayoutBox, ctx: &LayoutContext) -> Vec<LineBox> {
533    let container_width = parent.dimensions.content.width;
534    let mut line_boxes: Vec<LineBox> = Vec::new();
535    let mut current_line = LineBox {
536        x: parent.dimensions.content.x,
537        y: parent.dimensions.content.y,
538        width: 0,
539        height: parent.style.line_height,
540        baseline: parent.style.line_height,
541        fragments: Vec::new(),
542    };
543
544    for child in &parent.children {
545        collect_inline_fragments(
546            child,
547            &mut current_line,
548            &mut line_boxes,
549            container_width,
550            ctx,
551            parent,
552        );
553    }
554
555    // Flush last line
556    if !current_line.fragments.is_empty() {
557        apply_text_align(&mut current_line, container_width, &parent.style);
558        line_boxes.push(current_line);
559    }
560
561    // Set y positions
562    let mut y = parent.dimensions.content.y;
563    for lb in &mut line_boxes {
564        lb.y = y;
565        y += lb.height;
566    }
567
568    line_boxes
569}
570
571/// Collect inline fragments from a layout box
572#[allow(dead_code, clippy::only_used_in_recursion)]
573fn collect_inline_fragments(
574    layout: &LayoutBox,
575    current_line: &mut LineBox,
576    line_boxes: &mut Vec<LineBox>,
577    container_width: FixedPoint,
578    ctx: &LayoutContext,
579    parent: &LayoutBox,
580) {
581    if layout.style.display == Display::None {
582        return;
583    }
584
585    // If this box has text content (via node_id), extract it
586    if let Some(node_id) = layout.node_id {
587        let _ = node_id; // Text is in children
588    }
589
590    // Check for inline children with text
591    for child in &layout.children {
592        collect_inline_fragments(
593            child,
594            current_line,
595            line_boxes,
596            container_width,
597            ctx,
598            parent,
599        );
600    }
601
602    // If no children, this might be a text node represented directly
603    if layout.children.is_empty() && layout.box_type == BoxType::Inline {
604        // This is a leaf inline node - treat as a text placeholder
605        // Text content would come from the DOM; for now produce a fragment marker
606        let _ = (ctx, parent);
607    }
608}
609
610/// Apply text alignment to a line box
611fn apply_text_align(line: &mut LineBox, container_width: FixedPoint, style: &ComputedStyle) {
612    let remaining = container_width - line.width;
613    if remaining <= 0 {
614        return;
615    }
616
617    match style.text_align {
618        TextAlign::Center => {
619            line.x += remaining / 2;
620        }
621        TextAlign::Right => {
622            line.x += remaining;
623        }
624        TextAlign::Justify => {
625            // Space out fragments
626            if line.fragments.len() > 1 {
627                let gap = remaining / (line.fragments.len() as i32 - 1);
628                let mut offset = 0;
629                for (i, frag) in line.fragments.iter_mut().enumerate() {
630                    let _ = frag;
631                    offset += if i > 0 { gap } else { 0 };
632                    let _ = offset;
633                }
634            }
635        }
636        TextAlign::Left => {
637            // Default, no adjustment
638        }
639    }
640}
641
642/// Layout a floated element
643fn layout_float(
644    layout: &mut LayoutBox,
645    ctx: &mut LayoutContext,
646    current_y: FixedPoint,
647    container_width: FixedPoint,
648) {
649    // Calculate dimensions
650    calculate_width(layout, ctx);
651    calculate_box_model(layout);
652
653    let float_width = layout.dimensions.content.width
654        + layout.dimensions.padding.left
655        + layout.dimensions.padding.right
656        + layout.dimensions.border.left
657        + layout.dimensions.border.right;
658
659    let (left_edge, right_edge) = ctx.available_width_at(current_y, container_width);
660
661    match layout.style.float {
662        Float::Left => {
663            layout.dimensions.content.x = left_edge + layout.dimensions.margin.left;
664            layout.dimensions.content.y = current_y + layout.dimensions.margin.top;
665            ctx.left_floats.push(FloatExclusion {
666                x: left_edge,
667                y: current_y,
668                width: float_width + layout.dimensions.margin.left + layout.dimensions.margin.right,
669                height: layout.dimensions.content.height
670                    + layout.dimensions.padding.top
671                    + layout.dimensions.padding.bottom
672                    + layout.dimensions.border.top
673                    + layout.dimensions.border.bottom
674                    + layout.dimensions.margin.top
675                    + layout.dimensions.margin.bottom,
676                float_type: Float::Left,
677            });
678        }
679        Float::Right => {
680            layout.dimensions.content.x = right_edge - float_width - layout.dimensions.margin.right;
681            layout.dimensions.content.y = current_y + layout.dimensions.margin.top;
682            ctx.right_floats.push(FloatExclusion {
683                x: layout.dimensions.content.x - layout.dimensions.margin.left,
684                y: current_y,
685                width: float_width + layout.dimensions.margin.left + layout.dimensions.margin.right,
686                height: layout.dimensions.content.height
687                    + layout.dimensions.padding.top
688                    + layout.dimensions.padding.bottom
689                    + layout.dimensions.border.top
690                    + layout.dimensions.border.bottom
691                    + layout.dimensions.margin.top
692                    + layout.dimensions.margin.bottom,
693                float_type: Float::Right,
694            });
695        }
696        Float::None => {}
697    }
698
699    // Layout float's children
700    layout_box(layout, ctx);
701}
702
703/// Position absolutely/fixed positioned children
704fn position_children(parent: &mut LayoutBox, ctx: &LayoutContext) {
705    for child in &mut parent.children {
706        match child.style.position {
707            Position::Relative => {
708                // Offset from normal flow position
709                if let Some(left) = child.style.width {
710                    let _ = left; // Not an offset
711                }
712                // Relative positioning uses top/left style offsets
713                // which aren't implemented in the simple ComputedStyle
714            }
715            Position::Absolute => {
716                // Position relative to nearest positioned ancestor
717                // Simplified: just place at content origin
718                child.dimensions.content.x = parent.dimensions.content.x;
719                child.dimensions.content.y = parent.dimensions.content.y;
720            }
721            Position::Fixed => {
722                // Position relative to viewport
723                child.dimensions.content.x = 0;
724                child.dimensions.content.y = 0;
725            }
726            Position::Static => {
727                // Normal flow (already handled)
728            }
729        }
730        let _ = ctx;
731    }
732}
733
734/// Generate text content from a DOM node for inline layout
735pub fn get_text_for_layout(doc: &Document, node_id: NodeId) -> String {
736    let mut text = String::new();
737    doc.walk(node_id, &mut |id| {
738        if let Some(node) = doc.arena.get(id) {
739            if node.node_type == NodeType::Text {
740                if let Some(ref t) = node.text_content {
741                    text.push_str(t);
742                }
743            }
744        }
745    });
746    text
747}
748
749/// Word wrap: split text into lines that fit within a given width
750pub fn word_wrap(text: &str, max_width: FixedPoint, white_space: WhiteSpace) -> Vec<String> {
751    if max_width <= 0 {
752        return Vec::new();
753    }
754
755    let char_width = CHAR_WIDTH;
756    let max_chars = fp_to_px(max_width) / fp_to_px(char_width);
757    if max_chars <= 0 {
758        return Vec::new();
759    }
760    let max_chars = max_chars as usize;
761
762    match white_space {
763        WhiteSpace::Pre | WhiteSpace::PreWrap => {
764            // Preserve whitespace and newlines
765            let mut lines = Vec::new();
766            for line in text.split('\n') {
767                if white_space == WhiteSpace::PreWrap && line.len() > max_chars {
768                    // Wrap long lines
769                    let mut pos = 0;
770                    while pos < line.len() {
771                        let end = core::cmp::min(pos + max_chars, line.len());
772                        lines.push(line[pos..end].to_string());
773                        pos = end;
774                    }
775                } else {
776                    lines.push(line.to_string());
777                }
778            }
779            if lines.is_empty() {
780                lines.push(String::new());
781            }
782            lines
783        }
784        WhiteSpace::Nowrap => {
785            // No wrapping
786            let collapsed = collapse_whitespace(text);
787            alloc::vec![collapsed]
788        }
789        _ => {
790            // Normal wrapping at word boundaries
791            let collapsed = collapse_whitespace(text);
792            let words: Vec<&str> = collapsed.split_whitespace().collect();
793            let mut lines = Vec::new();
794            let mut current_line = String::new();
795
796            for word in words {
797                if current_line.is_empty() {
798                    current_line = word.to_string();
799                } else if current_line.len() + 1 + word.len() <= max_chars {
800                    current_line.push(' ');
801                    current_line.push_str(word);
802                } else {
803                    lines.push(current_line);
804                    current_line = word.to_string();
805                }
806            }
807
808            if !current_line.is_empty() {
809                lines.push(current_line);
810            }
811
812            if lines.is_empty() {
813                lines.push(String::new());
814            }
815
816            lines
817        }
818    }
819}
820
821/// Collapse whitespace according to CSS rules
822fn collapse_whitespace(text: &str) -> String {
823    let mut result = String::new();
824    let mut last_was_space = false;
825    for c in text.chars() {
826        if c.is_whitespace() {
827            if !last_was_space {
828                result.push(' ');
829                last_was_space = true;
830            }
831        } else {
832            result.push(c);
833            last_was_space = false;
834        }
835    }
836    result
837}
838
839/// Measure text width in fixed-point units (8px per char)
840pub fn measure_text_width(text: &str) -> FixedPoint {
841    (text.len() as i32) * CHAR_WIDTH
842}
843
844/// Measure text height for a given font size
845pub fn measure_text_height(font_size: FixedPoint) -> FixedPoint {
846    // Using 8x16 font, height scales with font size
847    // Default 16px font = 16px height
848    let _ = font_size;
849    px_to_fp(16) // Fixed height for 8x16 font
850}
851
852#[cfg(test)]
853mod tests {
854    #[allow(unused_imports)]
855    use alloc::vec;
856
857    use super::{
858        super::{css_parser::CssParser, tree_builder::TreeBuilder},
859        *,
860    };
861
862    fn layout_html(html: &str, css: &str, width: i32, height: i32) -> LayoutBox {
863        let doc = TreeBuilder::build(html);
864        let stylesheet = CssParser::parse(css);
865        let mut resolver = StyleResolver::new();
866        resolver.add_stylesheet(stylesheet);
867        build_layout_tree(&doc, &resolver, width, height)
868    }
869
870    #[test]
871    fn test_rect_default() {
872        let r = Rect::default();
873        assert_eq!(r.x, 0);
874        assert_eq!(r.y, 0);
875        assert_eq!(r.width, 0);
876        assert_eq!(r.height, 0);
877    }
878
879    #[test]
880    fn test_edge_sizes_default() {
881        let e = EdgeSizes::default();
882        assert_eq!(e.top, 0);
883    }
884
885    #[test]
886    fn test_dimensions_padding_box() {
887        let d = Dimensions {
888            content: Rect {
889                x: px_to_fp(10),
890                y: px_to_fp(10),
891                width: px_to_fp(100),
892                height: px_to_fp(50),
893            },
894            padding: EdgeSizes {
895                top: px_to_fp(5),
896                right: px_to_fp(5),
897                bottom: px_to_fp(5),
898                left: px_to_fp(5),
899            },
900            border: EdgeSizes::default(),
901            margin: EdgeSizes::default(),
902        };
903        let pb = d.padding_box();
904        assert_eq!(pb.width, px_to_fp(110));
905        assert_eq!(pb.height, px_to_fp(60));
906    }
907
908    #[test]
909    fn test_dimensions_border_box() {
910        let d = Dimensions {
911            content: Rect {
912                x: px_to_fp(10),
913                y: px_to_fp(10),
914                width: px_to_fp(100),
915                height: px_to_fp(50),
916            },
917            padding: EdgeSizes {
918                top: px_to_fp(5),
919                right: px_to_fp(5),
920                bottom: px_to_fp(5),
921                left: px_to_fp(5),
922            },
923            border: EdgeSizes {
924                top: px_to_fp(1),
925                right: px_to_fp(1),
926                bottom: px_to_fp(1),
927                left: px_to_fp(1),
928            },
929            margin: EdgeSizes::default(),
930        };
931        let bb = d.border_box();
932        assert_eq!(bb.width, px_to_fp(112));
933    }
934
935    #[test]
936    fn test_dimensions_margin_box() {
937        let d = Dimensions {
938            content: Rect {
939                x: px_to_fp(20),
940                y: px_to_fp(20),
941                width: px_to_fp(100),
942                height: px_to_fp(50),
943            },
944            padding: EdgeSizes::default(),
945            border: EdgeSizes::default(),
946            margin: EdgeSizes {
947                top: px_to_fp(10),
948                right: px_to_fp(10),
949                bottom: px_to_fp(10),
950                left: px_to_fp(10),
951            },
952        };
953        let mb = d.margin_box();
954        assert_eq!(mb.width, px_to_fp(120));
955        assert_eq!(mb.height, px_to_fp(70));
956    }
957
958    #[test]
959    fn test_layout_empty() {
960        let root = layout_html("", "", 800, 600);
961        assert_eq!(root.box_type, BoxType::Block);
962    }
963
964    #[test]
965    fn test_layout_single_block() {
966        let root = layout_html("<div>Hello</div>", "", 800, 600);
967        assert!(root.dimensions.content.width > 0);
968    }
969
970    #[test]
971    fn test_layout_nested_blocks() {
972        let root = layout_html(
973            "<div><div>Inner</div></div>",
974            "div { padding: 10px; }",
975            800,
976            600,
977        );
978        assert!(root.dimensions.content.width > 0);
979    }
980
981    #[test]
982    fn test_layout_width_set() {
983        let root = layout_html("<div>text</div>", "div { width: 200px; }", 800, 600);
984        // The div should be inside the body which is inside html
985        // Find the div
986        fn find_box_with_width(lb: &LayoutBox, target: FixedPoint) -> bool {
987            if lb.dimensions.content.width == target {
988                return true;
989            }
990            for child in &lb.children {
991                if find_box_with_width(child, target) {
992                    return true;
993                }
994            }
995            false
996        }
997        assert!(find_box_with_width(&root, px_to_fp(200)));
998    }
999
1000    #[test]
1001    fn test_layout_with_padding() {
1002        let root = layout_html(
1003            "<div>text</div>",
1004            "div { padding: 20px; width: 100px; }",
1005            800,
1006            600,
1007        );
1008        assert!(root.dimensions.content.width > 0);
1009    }
1010
1011    #[test]
1012    fn test_layout_with_margin() {
1013        let root = layout_html("<div>text</div>", "div { margin: 10px; }", 800, 600);
1014        assert!(root.dimensions.content.width > 0);
1015    }
1016
1017    #[test]
1018    fn test_layout_display_none() {
1019        let root = layout_html(
1020            "<div>visible</div><div style=\"display:none\">hidden</div>",
1021            "",
1022            800,
1023            600,
1024        );
1025        assert!(root.dimensions.content.width > 0);
1026    }
1027
1028    #[test]
1029    fn test_word_wrap_normal() {
1030        let lines = word_wrap("hello world foo bar", px_to_fp(80), WhiteSpace::Normal);
1031        assert!(!lines.is_empty());
1032    }
1033
1034    #[test]
1035    fn test_word_wrap_nowrap() {
1036        let lines = word_wrap("hello world", px_to_fp(40), WhiteSpace::Nowrap);
1037        assert_eq!(lines.len(), 1);
1038    }
1039
1040    #[test]
1041    fn test_word_wrap_pre() {
1042        let lines = word_wrap("line1\nline2\nline3", px_to_fp(800), WhiteSpace::Pre);
1043        assert_eq!(lines.len(), 3);
1044    }
1045
1046    #[test]
1047    fn test_word_wrap_empty() {
1048        let lines = word_wrap("", px_to_fp(100), WhiteSpace::Normal);
1049        assert_eq!(lines.len(), 1);
1050    }
1051
1052    #[test]
1053    fn test_collapse_whitespace() {
1054        assert_eq!(collapse_whitespace("  hello   world  "), " hello world ");
1055    }
1056
1057    #[test]
1058    fn test_measure_text_width() {
1059        let w = measure_text_width("hello");
1060        assert_eq!(w, 5 * CHAR_WIDTH);
1061    }
1062
1063    #[test]
1064    fn test_measure_text_height() {
1065        let h = measure_text_height(px_to_fp(16));
1066        assert_eq!(h, px_to_fp(16));
1067    }
1068
1069    #[test]
1070    fn test_layout_box_default() {
1071        let lb = LayoutBox::default();
1072        assert_eq!(lb.box_type, BoxType::Block);
1073        assert!(lb.children.is_empty());
1074    }
1075
1076    #[test]
1077    fn test_margin_box_height() {
1078        let mut lb = LayoutBox::default();
1079        lb.dimensions.content.height = px_to_fp(100);
1080        lb.dimensions.margin.top = px_to_fp(10);
1081        lb.dimensions.margin.bottom = px_to_fp(10);
1082        assert_eq!(lb.margin_box_height(), px_to_fp(120));
1083    }
1084
1085    #[test]
1086    fn test_layout_multiple_blocks() {
1087        let root = layout_html(
1088            "<div>A</div><div>B</div><div>C</div>",
1089            "div { height: 50px; }",
1090            800,
1091            600,
1092        );
1093        assert!(root.dimensions.content.width > 0);
1094    }
1095
1096    #[test]
1097    fn test_word_wrap_single_long_word() {
1098        let lines = word_wrap("superlongword", px_to_fp(40), WhiteSpace::Normal);
1099        assert!(!lines.is_empty());
1100    }
1101
1102    #[test]
1103    fn test_layout_headings() {
1104        let root = layout_html("<h1>Title</h1><h2>Subtitle</h2><p>Text</p>", "", 800, 600);
1105        assert!(root.dimensions.content.width > 0);
1106    }
1107
1108    #[test]
1109    fn test_get_text_for_layout() {
1110        let doc = TreeBuilder::build("<p>Hello <b>world</b></p>");
1111        let ps = doc.get_elements_by_tag_name("p");
1112        let text = get_text_for_layout(&doc, ps[0]);
1113        assert_eq!(text, "Hello world");
1114    }
1115
1116    #[test]
1117    fn test_layout_context_available_width() {
1118        let ctx = LayoutContext::new(800, 600);
1119        let (left, right) = ctx.available_width_at(0, px_to_fp(800));
1120        assert_eq!(left, 0);
1121        assert_eq!(right, px_to_fp(800));
1122    }
1123
1124    #[test]
1125    fn test_layout_context_clear() {
1126        let mut ctx = LayoutContext::new(800, 600);
1127        ctx.left_floats.push(FloatExclusion {
1128            x: 0,
1129            y: 0,
1130            width: px_to_fp(200),
1131            height: px_to_fp(100),
1132            float_type: Float::Left,
1133        });
1134        let y = ctx.clear_y(Clear::Left, 0);
1135        assert_eq!(y, px_to_fp(100));
1136    }
1137
1138    #[test]
1139    fn test_word_wrap_prewrap() {
1140        let lines = word_wrap("abc\ndef", px_to_fp(800), WhiteSpace::PreWrap);
1141        assert_eq!(lines.len(), 2);
1142    }
1143
1144    #[test]
1145    fn test_word_wrap_zero_width() {
1146        let lines = word_wrap("hello", 0, WhiteSpace::Normal);
1147        assert!(lines.is_empty());
1148    }
1149
1150    // Float layout tests
1151    #[test]
1152    fn test_float_left() {
1153        let root = layout_html(
1154            "<div><div>floated</div><div>content</div></div>",
1155            "div div:first-child { float: left; width: 100px; }",
1156            800,
1157            600,
1158        );
1159        assert!(root.dimensions.content.width > 0);
1160    }
1161
1162    // Positioned layout tests
1163    #[test]
1164    fn test_relative_position() {
1165        let root = layout_html("<div>text</div>", "div { position: relative; }", 800, 600);
1166        assert!(root.dimensions.content.width > 0);
1167    }
1168
1169    #[test]
1170    fn test_layout_table() {
1171        let root = layout_html("<table><tr><td>cell</td></tr></table>", "", 800, 600);
1172        assert!(root.dimensions.content.width > 0);
1173    }
1174
1175    #[test]
1176    fn test_layout_list() {
1177        let root = layout_html("<ul><li>item 1</li><li>item 2</li></ul>", "", 800, 600);
1178        assert!(root.dimensions.content.width > 0);
1179    }
1180
1181    #[test]
1182    fn test_layout_with_border() {
1183        let root = layout_html(
1184            "<div>text</div>",
1185            "div { border-width: 2px; border-style: solid; width: 200px; }",
1186            800,
1187            600,
1188        );
1189        assert!(root.dimensions.content.width > 0);
1190    }
1191
1192    // Additional tests
1193    #[test]
1194    fn test_layout_height_set() {
1195        let root = layout_html("<div>text</div>", "div { height: 300px; }", 800, 600);
1196        fn find_height(lb: &LayoutBox, target: FixedPoint) -> bool {
1197            if lb.dimensions.content.height == target {
1198                return true;
1199            }
1200            for child in &lb.children {
1201                if find_height(child, target) {
1202                    return true;
1203                }
1204            }
1205            false
1206        }
1207        assert!(find_height(&root, px_to_fp(300)));
1208    }
1209
1210    #[test]
1211    fn test_layout_mixed_content() {
1212        let root = layout_html("<div>text <span>inline</span> more</div>", "", 800, 600);
1213        assert!(root.dimensions.content.width > 0);
1214    }
1215
1216    #[test]
1217    fn test_text_align_center() {
1218        let root = layout_html("<div>text</div>", "div { text-align: center; }", 800, 600);
1219        assert!(root.dimensions.content.width > 0);
1220    }
1221
1222    #[test]
1223    fn test_layout_deeply_nested() {
1224        let root = layout_html(
1225            "<div><div><div><div>deep</div></div></div></div>",
1226            "",
1227            800,
1228            600,
1229        );
1230        assert!(root.dimensions.content.width > 0);
1231    }
1232
1233    #[test]
1234    fn test_word_wrap_exact_fit() {
1235        // 10 chars at 8px each = 80px, container = 80px
1236        let lines = word_wrap("1234567890", px_to_fp(80), WhiteSpace::Normal);
1237        assert_eq!(lines.len(), 1);
1238    }
1239
1240    #[test]
1241    fn test_collapse_whitespace_tabs() {
1242        assert_eq!(collapse_whitespace("a\t\tb"), "a b");
1243    }
1244
1245    #[test]
1246    fn test_collapse_whitespace_newlines() {
1247        assert_eq!(collapse_whitespace("a\n\nb"), "a b");
1248    }
1249
1250    #[test]
1251    fn test_layout_wide_content() {
1252        let root = layout_html("<div>text</div>", "div { width: 1000px; }", 800, 600);
1253        assert!(root.dimensions.content.width > 0);
1254    }
1255
1256    #[test]
1257    fn test_layout_overflow_hidden() {
1258        let root = layout_html(
1259            "<div>text</div>",
1260            "div { overflow: hidden; height: 50px; }",
1261            800,
1262            600,
1263        );
1264        let _ = root.style.overflow;
1265    }
1266
1267    #[test]
1268    fn test_layout_visibility() {
1269        let root = layout_html("<div>text</div>", "div { visibility: hidden; }", 800, 600);
1270        assert!(root.dimensions.content.width > 0);
1271    }
1272
1273    #[test]
1274    fn test_layout_min_width() {
1275        let root = layout_html(
1276            "<div>text</div>",
1277            "div { min-width: 500px; width: 100px; }",
1278            800,
1279            600,
1280        );
1281        fn find_min(lb: &LayoutBox, min: FixedPoint) -> bool {
1282            if lb.dimensions.content.width >= min {
1283                return true;
1284            }
1285            for child in &lb.children {
1286                if find_min(child, min) {
1287                    return true;
1288                }
1289            }
1290            false
1291        }
1292        assert!(find_min(&root, px_to_fp(500)));
1293    }
1294
1295    #[test]
1296    fn test_layout_max_height() {
1297        let root = layout_html(
1298            "<div>text</div>",
1299            "div { max-height: 50px; height: 200px; }",
1300            800,
1301            600,
1302        );
1303        fn find_max(lb: &LayoutBox, max: FixedPoint) -> bool {
1304            if lb.dimensions.content.height == max {
1305                return true;
1306            }
1307            for child in &lb.children {
1308                if find_max(child, max) {
1309                    return true;
1310                }
1311            }
1312            false
1313        }
1314        assert!(find_max(&root, px_to_fp(50)));
1315    }
1316}