1#![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#[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#[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#[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 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 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 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
85pub enum BoxType {
86 #[default]
87 Block,
88 Inline,
89 Anonymous,
90}
91
92#[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#[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#[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#[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 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 pub fn margin_box_height(&self) -> FixedPoint {
167 self.dimensions.margin_box().height
168 }
169}
170
171const CHAR_WIDTH: FixedPoint = 8 * 64; struct 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 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 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
245pub 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
261fn 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 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 let children_ids: Vec<NodeId> = node.children.clone();
301 let mut has_block = false;
302 let mut has_inline = false;
303
304 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 if has_block && has_inline {
333 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
375fn 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 }
386 }
387}
388
389fn layout_block(layout: &mut LayoutBox, ctx: &mut LayoutContext) {
391 calculate_width(layout, ctx);
393
394 calculate_box_model(layout);
396
397 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 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 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 for i in 0..layout.children.len() {
421 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 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 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 layout_box(&mut layout.children[i], ctx);
444
445 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(layout, cursor_y);
453
454 position_children(layout, ctx);
456}
457
458fn 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 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 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
490fn 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
512fn 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 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
531fn 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 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 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#[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 let Some(node_id) = layout.node_id {
587 let _ = node_id; }
589
590 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 layout.children.is_empty() && layout.box_type == BoxType::Inline {
604 let _ = (ctx, parent);
607 }
608}
609
610fn 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 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 }
639 }
640}
641
642fn layout_float(
644 layout: &mut LayoutBox,
645 ctx: &mut LayoutContext,
646 current_y: FixedPoint,
647 container_width: FixedPoint,
648) {
649 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_box(layout, ctx);
701}
702
703fn position_children(parent: &mut LayoutBox, ctx: &LayoutContext) {
705 for child in &mut parent.children {
706 match child.style.position {
707 Position::Relative => {
708 if let Some(left) = child.style.width {
710 let _ = left; }
712 }
715 Position::Absolute => {
716 child.dimensions.content.x = parent.dimensions.content.x;
719 child.dimensions.content.y = parent.dimensions.content.y;
720 }
721 Position::Fixed => {
722 child.dimensions.content.x = 0;
724 child.dimensions.content.y = 0;
725 }
726 Position::Static => {
727 }
729 }
730 let _ = ctx;
731 }
732}
733
734pub 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
749pub 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 let mut lines = Vec::new();
766 for line in text.split('\n') {
767 if white_space == WhiteSpace::PreWrap && line.len() > max_chars {
768 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 let collapsed = collapse_whitespace(text);
787 alloc::vec![collapsed]
788 }
789 _ => {
790 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
821fn 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
839pub fn measure_text_width(text: &str) -> FixedPoint {
841 (text.len() as i32) * CHAR_WIDTH
842}
843
844pub fn measure_text_height(font_size: FixedPoint) -> FixedPoint {
846 let _ = font_size;
849 px_to_fp(16) }
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 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 #[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 #[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 #[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 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}