tangled
alpha
login
or
join now
pierrelf.com
/
we
3
fork
atom
web engine - experimental web browser
3
fork
atom
overview
issues
55
pulls
pipelines
Merge branch 'block-layout': Basic block layout engine
pierrelf.com
1 week ago
0e48251a
4ae938c9
+976
-1
1 changed file
expand all
collapse all
unified
split
crates
layout
src
lib.rs
+976
-1
crates/layout/src/lib.rs
···
1
1
-
//! Box generation, block/inline/flex/grid/table layout.
1
1
+
//! Block layout engine: box generation, block/inline layout, and text wrapping.
2
2
+
//!
3
3
+
//! Builds a layout tree from a DOM document and positions block-level elements
4
4
+
//! vertically with text wrapping. Uses hardcoded default styles (no CSS yet).
5
5
+
6
6
+
use we_dom::{Document, NodeData, NodeId};
7
7
+
use we_text::font::Font;
8
8
+
9
9
+
/// Edge sizes for box model (margin, padding, border).
10
10
+
#[derive(Debug, Clone, Copy, Default, PartialEq)]
11
11
+
pub struct EdgeSizes {
12
12
+
pub top: f32,
13
13
+
pub right: f32,
14
14
+
pub bottom: f32,
15
15
+
pub left: f32,
16
16
+
}
17
17
+
18
18
+
/// A positioned rectangle with content area dimensions.
19
19
+
#[derive(Debug, Clone, Copy, Default, PartialEq)]
20
20
+
pub struct Rect {
21
21
+
pub x: f32,
22
22
+
pub y: f32,
23
23
+
pub width: f32,
24
24
+
pub height: f32,
25
25
+
}
26
26
+
27
27
+
/// The type of layout box.
28
28
+
#[derive(Debug)]
29
29
+
pub enum BoxType {
30
30
+
/// Block-level box from an element.
31
31
+
Block(NodeId),
32
32
+
/// Inline-level box from an element.
33
33
+
Inline(NodeId),
34
34
+
/// A run of text from a text node.
35
35
+
TextRun { node: NodeId, text: String },
36
36
+
/// Anonymous block wrapping inline content within a block container.
37
37
+
Anonymous,
38
38
+
}
39
39
+
40
40
+
/// A single line of wrapped text.
41
41
+
#[derive(Debug, Clone, PartialEq)]
42
42
+
pub struct TextLine {
43
43
+
pub text: String,
44
44
+
pub x: f32,
45
45
+
pub y: f32,
46
46
+
pub width: f32,
47
47
+
}
48
48
+
49
49
+
/// A box in the layout tree with dimensions and child boxes.
50
50
+
#[derive(Debug)]
51
51
+
pub struct LayoutBox {
52
52
+
pub box_type: BoxType,
53
53
+
pub rect: Rect,
54
54
+
pub margin: EdgeSizes,
55
55
+
pub padding: EdgeSizes,
56
56
+
pub border: EdgeSizes,
57
57
+
pub children: Vec<LayoutBox>,
58
58
+
pub font_size: f32,
59
59
+
/// Wrapped text lines (populated for boxes with inline content).
60
60
+
pub lines: Vec<TextLine>,
61
61
+
}
62
62
+
63
63
+
impl LayoutBox {
64
64
+
fn new(box_type: BoxType, font_size: f32) -> Self {
65
65
+
LayoutBox {
66
66
+
box_type,
67
67
+
rect: Rect::default(),
68
68
+
margin: EdgeSizes::default(),
69
69
+
padding: EdgeSizes::default(),
70
70
+
border: EdgeSizes::default(),
71
71
+
children: Vec::new(),
72
72
+
font_size,
73
73
+
lines: Vec::new(),
74
74
+
}
75
75
+
}
76
76
+
77
77
+
/// Total height including margin, border, and padding.
78
78
+
pub fn margin_box_height(&self) -> f32 {
79
79
+
self.margin.top
80
80
+
+ self.border.top
81
81
+
+ self.padding.top
82
82
+
+ self.rect.height
83
83
+
+ self.padding.bottom
84
84
+
+ self.border.bottom
85
85
+
+ self.margin.bottom
86
86
+
}
87
87
+
88
88
+
/// Iterate over all boxes in depth-first pre-order.
89
89
+
pub fn iter(&self) -> LayoutBoxIter<'_> {
90
90
+
LayoutBoxIter { stack: vec![self] }
91
91
+
}
92
92
+
}
93
93
+
94
94
+
/// Depth-first pre-order iterator over layout boxes.
95
95
+
pub struct LayoutBoxIter<'a> {
96
96
+
stack: Vec<&'a LayoutBox>,
97
97
+
}
98
98
+
99
99
+
impl<'a> Iterator for LayoutBoxIter<'a> {
100
100
+
type Item = &'a LayoutBox;
101
101
+
102
102
+
fn next(&mut self) -> Option<&'a LayoutBox> {
103
103
+
let node = self.stack.pop()?;
104
104
+
// Push children in reverse so leftmost child is visited first.
105
105
+
for child in node.children.iter().rev() {
106
106
+
self.stack.push(child);
107
107
+
}
108
108
+
Some(node)
109
109
+
}
110
110
+
}
111
111
+
112
112
+
/// The result of laying out a document.
113
113
+
#[derive(Debug)]
114
114
+
pub struct LayoutTree {
115
115
+
pub root: LayoutBox,
116
116
+
pub width: f32,
117
117
+
pub height: f32,
118
118
+
}
119
119
+
120
120
+
impl LayoutTree {
121
121
+
/// Iterate over all layout boxes in depth-first pre-order.
122
122
+
pub fn iter(&self) -> LayoutBoxIter<'_> {
123
123
+
self.root.iter()
124
124
+
}
125
125
+
}
126
126
+
127
127
+
// ---------------------------------------------------------------------------
128
128
+
// Display type classification
129
129
+
// ---------------------------------------------------------------------------
130
130
+
131
131
+
#[derive(Debug, Clone, Copy, PartialEq)]
132
132
+
enum DisplayType {
133
133
+
Block,
134
134
+
Inline,
135
135
+
None,
136
136
+
}
137
137
+
138
138
+
fn display_type(tag: &str) -> DisplayType {
139
139
+
match tag {
140
140
+
"html" | "body" | "div" | "p" | "pre" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul"
141
141
+
| "ol" | "li" | "blockquote" | "section" | "article" | "nav" | "header" | "footer"
142
142
+
| "main" | "hr" => DisplayType::Block,
143
143
+
144
144
+
"span" | "a" | "em" | "strong" | "b" | "i" | "u" | "code" | "small" | "sub" | "sup"
145
145
+
| "br" => DisplayType::Inline,
146
146
+
147
147
+
"head" | "title" | "script" | "style" | "link" | "meta" => DisplayType::None,
148
148
+
149
149
+
_ => DisplayType::Block,
150
150
+
}
151
151
+
}
152
152
+
153
153
+
// ---------------------------------------------------------------------------
154
154
+
// Default styles (hardcoded for Phase 3)
155
155
+
// ---------------------------------------------------------------------------
156
156
+
157
157
+
fn default_font_size(tag: &str, parent_size: f32) -> f32 {
158
158
+
match tag {
159
159
+
"h1" => parent_size * 2.0,
160
160
+
"h2" => parent_size * 1.5,
161
161
+
"h3" => parent_size * 1.17,
162
162
+
"h4" => parent_size,
163
163
+
"h5" => parent_size * 0.83,
164
164
+
"h6" => parent_size * 0.67,
165
165
+
_ => parent_size,
166
166
+
}
167
167
+
}
168
168
+
169
169
+
fn default_margin(tag: &str, font_size: f32) -> EdgeSizes {
170
170
+
match tag {
171
171
+
"body" => EdgeSizes {
172
172
+
top: 8.0,
173
173
+
right: 8.0,
174
174
+
bottom: 8.0,
175
175
+
left: 8.0,
176
176
+
},
177
177
+
"p" => EdgeSizes {
178
178
+
top: font_size,
179
179
+
bottom: font_size,
180
180
+
..EdgeSizes::default()
181
181
+
},
182
182
+
"h1" => EdgeSizes {
183
183
+
top: font_size * 0.67,
184
184
+
bottom: font_size * 0.67,
185
185
+
..EdgeSizes::default()
186
186
+
},
187
187
+
"h2" => EdgeSizes {
188
188
+
top: font_size * 0.83,
189
189
+
bottom: font_size * 0.83,
190
190
+
..EdgeSizes::default()
191
191
+
},
192
192
+
"h3" | "h4" => EdgeSizes {
193
193
+
top: font_size,
194
194
+
bottom: font_size,
195
195
+
..EdgeSizes::default()
196
196
+
},
197
197
+
"h5" | "h6" => EdgeSizes {
198
198
+
top: font_size * 1.67,
199
199
+
bottom: font_size * 1.67,
200
200
+
..EdgeSizes::default()
201
201
+
},
202
202
+
_ => EdgeSizes::default(),
203
203
+
}
204
204
+
}
205
205
+
206
206
+
// ---------------------------------------------------------------------------
207
207
+
// Build layout tree from DOM
208
208
+
// ---------------------------------------------------------------------------
209
209
+
210
210
+
fn build_box(doc: &Document, node: NodeId, parent_font_size: f32) -> Option<LayoutBox> {
211
211
+
match doc.node_data(node) {
212
212
+
NodeData::Document => {
213
213
+
let mut children = Vec::new();
214
214
+
for child in doc.children(node) {
215
215
+
if let Some(child_box) = build_box(doc, child, parent_font_size) {
216
216
+
children.push(child_box);
217
217
+
}
218
218
+
}
219
219
+
// Unwrap single root element (typically <html>).
220
220
+
if children.len() == 1 {
221
221
+
children.into_iter().next()
222
222
+
} else if children.is_empty() {
223
223
+
None
224
224
+
} else {
225
225
+
let mut b = LayoutBox::new(BoxType::Anonymous, parent_font_size);
226
226
+
b.children = children;
227
227
+
Some(b)
228
228
+
}
229
229
+
}
230
230
+
NodeData::Element { tag_name, .. } => {
231
231
+
let dt = display_type(tag_name);
232
232
+
if dt == DisplayType::None {
233
233
+
return None;
234
234
+
}
235
235
+
236
236
+
let font_size = default_font_size(tag_name, parent_font_size);
237
237
+
let margin = default_margin(tag_name, font_size);
238
238
+
239
239
+
let mut children = Vec::new();
240
240
+
for child in doc.children(node) {
241
241
+
if let Some(child_box) = build_box(doc, child, font_size) {
242
242
+
children.push(child_box);
243
243
+
}
244
244
+
}
245
245
+
246
246
+
let box_type = match dt {
247
247
+
DisplayType::Block => BoxType::Block(node),
248
248
+
DisplayType::Inline => BoxType::Inline(node),
249
249
+
DisplayType::None => unreachable!(),
250
250
+
};
251
251
+
252
252
+
// For block containers, ensure children are uniformly block or inline.
253
253
+
if dt == DisplayType::Block {
254
254
+
children = normalize_children(children, font_size);
255
255
+
}
256
256
+
257
257
+
let mut b = LayoutBox::new(box_type, font_size);
258
258
+
b.margin = margin;
259
259
+
b.children = children;
260
260
+
Some(b)
261
261
+
}
262
262
+
NodeData::Text { data } => {
263
263
+
let collapsed = collapse_whitespace(data);
264
264
+
if collapsed.is_empty() {
265
265
+
return None;
266
266
+
}
267
267
+
Some(LayoutBox::new(
268
268
+
BoxType::TextRun {
269
269
+
node,
270
270
+
text: collapsed,
271
271
+
},
272
272
+
parent_font_size,
273
273
+
))
274
274
+
}
275
275
+
NodeData::Comment { .. } => None,
276
276
+
}
277
277
+
}
278
278
+
279
279
+
/// Collapse runs of whitespace to a single space. Preserves non-whitespace content.
280
280
+
fn collapse_whitespace(s: &str) -> String {
281
281
+
let mut result = String::new();
282
282
+
let mut in_ws = false;
283
283
+
for ch in s.chars() {
284
284
+
if ch.is_whitespace() {
285
285
+
if !in_ws {
286
286
+
result.push(' ');
287
287
+
}
288
288
+
in_ws = true;
289
289
+
} else {
290
290
+
in_ws = false;
291
291
+
result.push(ch);
292
292
+
}
293
293
+
}
294
294
+
result
295
295
+
}
296
296
+
297
297
+
/// If a block container has a mix of block-level and inline-level children,
298
298
+
/// wrap consecutive inline runs in anonymous block boxes.
299
299
+
fn normalize_children(children: Vec<LayoutBox>, font_size: f32) -> Vec<LayoutBox> {
300
300
+
if children.is_empty() {
301
301
+
return children;
302
302
+
}
303
303
+
304
304
+
let has_block = children.iter().any(is_block_level);
305
305
+
if !has_block {
306
306
+
// All inline — parent will do inline layout directly.
307
307
+
return children;
308
308
+
}
309
309
+
310
310
+
let has_inline = children.iter().any(|c| !is_block_level(c));
311
311
+
if !has_inline {
312
312
+
// All block — no wrapping needed.
313
313
+
return children;
314
314
+
}
315
315
+
316
316
+
// Mixed: wrap consecutive inline runs in anonymous blocks.
317
317
+
let mut result = Vec::new();
318
318
+
let mut inline_group: Vec<LayoutBox> = Vec::new();
319
319
+
320
320
+
for child in children {
321
321
+
if is_block_level(&child) {
322
322
+
if !inline_group.is_empty() {
323
323
+
let mut anon = LayoutBox::new(BoxType::Anonymous, font_size);
324
324
+
anon.children = std::mem::take(&mut inline_group);
325
325
+
result.push(anon);
326
326
+
}
327
327
+
result.push(child);
328
328
+
} else {
329
329
+
inline_group.push(child);
330
330
+
}
331
331
+
}
332
332
+
333
333
+
if !inline_group.is_empty() {
334
334
+
let mut anon = LayoutBox::new(BoxType::Anonymous, font_size);
335
335
+
anon.children = inline_group;
336
336
+
result.push(anon);
337
337
+
}
338
338
+
339
339
+
result
340
340
+
}
341
341
+
342
342
+
fn is_block_level(b: &LayoutBox) -> bool {
343
343
+
matches!(b.box_type, BoxType::Block(_) | BoxType::Anonymous)
344
344
+
}
345
345
+
346
346
+
// ---------------------------------------------------------------------------
347
347
+
// Layout algorithm
348
348
+
// ---------------------------------------------------------------------------
349
349
+
350
350
+
/// Position and size a layout box within `available_width` at position (`x`, `y`).
351
351
+
///
352
352
+
/// `x` and `y` mark the top-left corner of the box's margin area.
353
353
+
fn compute_layout(b: &mut LayoutBox, x: f32, y: f32, available_width: f32, font: &Font) {
354
354
+
let content_x = x + b.margin.left + b.border.left + b.padding.left;
355
355
+
let content_y = y + b.margin.top + b.border.top + b.padding.top;
356
356
+
let content_width = (available_width
357
357
+
- b.margin.left
358
358
+
- b.margin.right
359
359
+
- b.border.left
360
360
+
- b.border.right
361
361
+
- b.padding.left
362
362
+
- b.padding.right)
363
363
+
.max(0.0);
364
364
+
365
365
+
b.rect.x = content_x;
366
366
+
b.rect.y = content_y;
367
367
+
b.rect.width = content_width;
368
368
+
369
369
+
match &b.box_type {
370
370
+
BoxType::Block(_) | BoxType::Anonymous => {
371
371
+
if has_block_children(b) {
372
372
+
layout_block_children(b, font);
373
373
+
} else {
374
374
+
layout_inline_children(b, font);
375
375
+
}
376
376
+
}
377
377
+
BoxType::TextRun { .. } | BoxType::Inline(_) => {
378
378
+
// Handled by the parent's inline layout.
379
379
+
}
380
380
+
}
381
381
+
}
382
382
+
383
383
+
fn has_block_children(b: &LayoutBox) -> bool {
384
384
+
b.children.iter().any(is_block_level)
385
385
+
}
386
386
+
387
387
+
/// Lay out block-level children: stack them vertically.
388
388
+
fn layout_block_children(parent: &mut LayoutBox, font: &Font) {
389
389
+
let content_x = parent.rect.x;
390
390
+
let content_width = parent.rect.width;
391
391
+
let mut cursor_y = parent.rect.y;
392
392
+
393
393
+
for child in &mut parent.children {
394
394
+
compute_layout(child, content_x, cursor_y, content_width, font);
395
395
+
cursor_y += child.margin_box_height();
396
396
+
}
397
397
+
398
398
+
parent.rect.height = cursor_y - parent.rect.y;
399
399
+
}
400
400
+
401
401
+
/// Lay out inline children: collect text, word-wrap, and compute height.
402
402
+
fn layout_inline_children(parent: &mut LayoutBox, font: &Font) {
403
403
+
let text = collect_inline_text(&parent.children);
404
404
+
if text.is_empty() {
405
405
+
parent.rect.height = 0.0;
406
406
+
return;
407
407
+
}
408
408
+
409
409
+
let line_height = parent.font_size * 1.2;
410
410
+
let wrapped = wrap_text(&text, parent.rect.width, font, parent.font_size);
411
411
+
412
412
+
let mut y = parent.rect.y;
413
413
+
let mut positioned = Vec::with_capacity(wrapped.len());
414
414
+
for line in wrapped {
415
415
+
positioned.push(TextLine {
416
416
+
text: line.text,
417
417
+
x: parent.rect.x,
418
418
+
y,
419
419
+
width: line.width,
420
420
+
});
421
421
+
y += line_height;
422
422
+
}
423
423
+
424
424
+
parent.rect.height = positioned.len() as f32 * line_height;
425
425
+
parent.lines = positioned;
426
426
+
}
427
427
+
428
428
+
/// Recursively collect all text from inline children.
429
429
+
fn collect_inline_text(children: &[LayoutBox]) -> String {
430
430
+
let mut result = String::new();
431
431
+
collect_text_recursive(children, &mut result);
432
432
+
result
433
433
+
}
434
434
+
435
435
+
fn collect_text_recursive(children: &[LayoutBox], result: &mut String) {
436
436
+
for child in children {
437
437
+
match &child.box_type {
438
438
+
BoxType::TextRun { text, .. } => {
439
439
+
result.push_str(text);
440
440
+
}
441
441
+
BoxType::Inline(_) => {
442
442
+
collect_text_recursive(&child.children, result);
443
443
+
}
444
444
+
_ => {}
445
445
+
}
446
446
+
}
447
447
+
}
448
448
+
449
449
+
// ---------------------------------------------------------------------------
450
450
+
// Text measurement and word wrapping
451
451
+
// ---------------------------------------------------------------------------
452
452
+
453
453
+
/// Measure the total advance width of a text string at the given font size.
454
454
+
fn measure_text_width(font: &Font, text: &str, font_size: f32) -> f32 {
455
455
+
let shaped = font.shape_text(text, font_size);
456
456
+
match shaped.last() {
457
457
+
Some(last) => last.x_offset + last.x_advance,
458
458
+
None => 0.0,
459
459
+
}
460
460
+
}
461
461
+
462
462
+
/// Word-wrap text to fit within `max_width`.
463
463
+
fn wrap_text(text: &str, max_width: f32, font: &Font, font_size: f32) -> Vec<TextLine> {
464
464
+
let words: Vec<&str> = text.split_whitespace().collect();
465
465
+
if words.is_empty() {
466
466
+
return Vec::new();
467
467
+
}
468
468
+
469
469
+
let space_width = measure_text_width(font, " ", font_size);
470
470
+
let mut lines = Vec::new();
471
471
+
let mut line_text = String::new();
472
472
+
let mut line_width: f32 = 0.0;
473
473
+
474
474
+
for word in &words {
475
475
+
let word_width = measure_text_width(font, word, font_size);
476
476
+
477
477
+
if line_text.is_empty() {
478
478
+
// First word on line — always accept.
479
479
+
line_text.push_str(word);
480
480
+
line_width = word_width;
481
481
+
} else if line_width + space_width + word_width <= max_width {
482
482
+
line_text.push(' ');
483
483
+
line_text.push_str(word);
484
484
+
line_width += space_width + word_width;
485
485
+
} else {
486
486
+
// Emit current line, start new one.
487
487
+
lines.push(TextLine {
488
488
+
text: line_text,
489
489
+
x: 0.0,
490
490
+
y: 0.0,
491
491
+
width: line_width,
492
492
+
});
493
493
+
line_text = word.to_string();
494
494
+
line_width = word_width;
495
495
+
}
496
496
+
}
497
497
+
498
498
+
if !line_text.is_empty() {
499
499
+
lines.push(TextLine {
500
500
+
text: line_text,
501
501
+
x: 0.0,
502
502
+
y: 0.0,
503
503
+
width: line_width,
504
504
+
});
505
505
+
}
506
506
+
507
507
+
lines
508
508
+
}
509
509
+
510
510
+
// ---------------------------------------------------------------------------
511
511
+
// Public API
512
512
+
// ---------------------------------------------------------------------------
513
513
+
514
514
+
const BASE_FONT_SIZE: f32 = 16.0;
515
515
+
516
516
+
/// Build and lay out a DOM document.
517
517
+
///
518
518
+
/// Returns a `LayoutTree` with positioned boxes ready for rendering.
519
519
+
pub fn layout(
520
520
+
document: &Document,
521
521
+
viewport_width: f32,
522
522
+
_viewport_height: f32,
523
523
+
font: &Font,
524
524
+
) -> LayoutTree {
525
525
+
let mut root = match build_box(document, document.root(), BASE_FONT_SIZE) {
526
526
+
Some(b) => b,
527
527
+
None => {
528
528
+
return LayoutTree {
529
529
+
root: LayoutBox::new(BoxType::Anonymous, BASE_FONT_SIZE),
530
530
+
width: viewport_width,
531
531
+
height: 0.0,
532
532
+
};
533
533
+
}
534
534
+
};
535
535
+
536
536
+
compute_layout(&mut root, 0.0, 0.0, viewport_width, font);
537
537
+
538
538
+
let height = root.margin_box_height();
539
539
+
LayoutTree {
540
540
+
root,
541
541
+
width: viewport_width,
542
542
+
height,
543
543
+
}
544
544
+
}
545
545
+
546
546
+
#[cfg(test)]
547
547
+
mod tests {
548
548
+
use super::*;
549
549
+
use we_dom::Document;
550
550
+
551
551
+
// Helper: load a system font for testing.
552
552
+
fn test_font() -> Font {
553
553
+
let paths = [
554
554
+
"/System/Library/Fonts/Geneva.ttf",
555
555
+
"/System/Library/Fonts/Monaco.ttf",
556
556
+
];
557
557
+
for path in &paths {
558
558
+
let p = std::path::Path::new(path);
559
559
+
if p.exists() {
560
560
+
return Font::from_file(p).expect("failed to parse font");
561
561
+
}
562
562
+
}
563
563
+
panic!("no test font found");
564
564
+
}
565
565
+
566
566
+
// Helper: build a simple DOM and lay it out.
567
567
+
fn layout_simple_html(html_element: NodeId, doc: &Document) -> LayoutTree {
568
568
+
let _ = html_element; // doc.root() already wraps it
569
569
+
let font = test_font();
570
570
+
layout(doc, 800.0, 600.0, &font)
571
571
+
}
572
572
+
573
573
+
#[test]
574
574
+
fn empty_document() {
575
575
+
let doc = Document::new();
576
576
+
let font = test_font();
577
577
+
let tree = layout(&doc, 800.0, 600.0, &font);
578
578
+
// Empty document should produce a minimal layout.
579
579
+
assert_eq!(tree.width, 800.0);
580
580
+
}
581
581
+
582
582
+
#[test]
583
583
+
fn single_paragraph() {
584
584
+
// Build: <html><body><p>Hello world</p></body></html>
585
585
+
let mut doc = Document::new();
586
586
+
let root = doc.root();
587
587
+
let html = doc.create_element("html");
588
588
+
let body = doc.create_element("body");
589
589
+
let p = doc.create_element("p");
590
590
+
let text = doc.create_text("Hello world");
591
591
+
doc.append_child(root, html);
592
592
+
doc.append_child(html, body);
593
593
+
doc.append_child(body, p);
594
594
+
doc.append_child(p, text);
595
595
+
596
596
+
let tree = layout_simple_html(html, &doc);
597
597
+
598
598
+
// Root should be the html element box.
599
599
+
assert!(matches!(tree.root.box_type, BoxType::Block(_)));
600
600
+
601
601
+
// Find the p box (html > body > p).
602
602
+
let body_box = &tree.root.children[0];
603
603
+
assert!(matches!(body_box.box_type, BoxType::Block(_)));
604
604
+
605
605
+
let p_box = &body_box.children[0];
606
606
+
assert!(matches!(p_box.box_type, BoxType::Block(_)));
607
607
+
608
608
+
// p should have text lines.
609
609
+
assert!(!p_box.lines.is_empty(), "p should have wrapped text lines");
610
610
+
assert_eq!(p_box.lines[0].text, "Hello world");
611
611
+
612
612
+
// p should have vertical margins (1em = 16px default).
613
613
+
assert_eq!(p_box.margin.top, 16.0);
614
614
+
assert_eq!(p_box.margin.bottom, 16.0);
615
615
+
}
616
616
+
617
617
+
#[test]
618
618
+
fn blocks_stack_vertically() {
619
619
+
// <html><body><p>First</p><p>Second</p></body></html>
620
620
+
let mut doc = Document::new();
621
621
+
let root = doc.root();
622
622
+
let html = doc.create_element("html");
623
623
+
let body = doc.create_element("body");
624
624
+
let p1 = doc.create_element("p");
625
625
+
let t1 = doc.create_text("First");
626
626
+
let p2 = doc.create_element("p");
627
627
+
let t2 = doc.create_text("Second");
628
628
+
doc.append_child(root, html);
629
629
+
doc.append_child(html, body);
630
630
+
doc.append_child(body, p1);
631
631
+
doc.append_child(p1, t1);
632
632
+
doc.append_child(body, p2);
633
633
+
doc.append_child(p2, t2);
634
634
+
635
635
+
let tree = layout_simple_html(html, &doc);
636
636
+
let body_box = &tree.root.children[0];
637
637
+
let first = &body_box.children[0];
638
638
+
let second = &body_box.children[1];
639
639
+
640
640
+
// Second paragraph should be below the first.
641
641
+
assert!(
642
642
+
second.rect.y > first.rect.y,
643
643
+
"second p (y={}) should be below first p (y={})",
644
644
+
second.rect.y,
645
645
+
first.rect.y
646
646
+
);
647
647
+
}
648
648
+
649
649
+
#[test]
650
650
+
fn heading_larger_than_body() {
651
651
+
// <html><body><h1>Title</h1><p>Text</p></body></html>
652
652
+
let mut doc = Document::new();
653
653
+
let root = doc.root();
654
654
+
let html = doc.create_element("html");
655
655
+
let body = doc.create_element("body");
656
656
+
let h1 = doc.create_element("h1");
657
657
+
let h1_text = doc.create_text("Title");
658
658
+
let p = doc.create_element("p");
659
659
+
let p_text = doc.create_text("Text");
660
660
+
doc.append_child(root, html);
661
661
+
doc.append_child(html, body);
662
662
+
doc.append_child(body, h1);
663
663
+
doc.append_child(h1, h1_text);
664
664
+
doc.append_child(body, p);
665
665
+
doc.append_child(p, p_text);
666
666
+
667
667
+
let tree = layout_simple_html(html, &doc);
668
668
+
let body_box = &tree.root.children[0];
669
669
+
let h1_box = &body_box.children[0];
670
670
+
let p_box = &body_box.children[1];
671
671
+
672
672
+
// h1 should have a larger font size (2em = 32px).
673
673
+
assert!(
674
674
+
h1_box.font_size > p_box.font_size,
675
675
+
"h1 font_size ({}) should be > p font_size ({})",
676
676
+
h1_box.font_size,
677
677
+
p_box.font_size
678
678
+
);
679
679
+
assert_eq!(h1_box.font_size, 32.0);
680
680
+
681
681
+
// h1 should take more vertical space.
682
682
+
assert!(
683
683
+
h1_box.rect.height > p_box.rect.height,
684
684
+
"h1 height ({}) should be > p height ({})",
685
685
+
h1_box.rect.height,
686
686
+
p_box.rect.height
687
687
+
);
688
688
+
}
689
689
+
690
690
+
#[test]
691
691
+
fn body_has_default_margin() {
692
692
+
let mut doc = Document::new();
693
693
+
let root = doc.root();
694
694
+
let html = doc.create_element("html");
695
695
+
let body = doc.create_element("body");
696
696
+
let p = doc.create_element("p");
697
697
+
let text = doc.create_text("Test");
698
698
+
doc.append_child(root, html);
699
699
+
doc.append_child(html, body);
700
700
+
doc.append_child(body, p);
701
701
+
doc.append_child(p, text);
702
702
+
703
703
+
let tree = layout_simple_html(html, &doc);
704
704
+
let body_box = &tree.root.children[0];
705
705
+
706
706
+
assert_eq!(body_box.margin.top, 8.0);
707
707
+
assert_eq!(body_box.margin.right, 8.0);
708
708
+
assert_eq!(body_box.margin.bottom, 8.0);
709
709
+
assert_eq!(body_box.margin.left, 8.0);
710
710
+
711
711
+
// Body content should be offset by 8px from html content edge.
712
712
+
assert_eq!(body_box.rect.x, 8.0);
713
713
+
assert_eq!(body_box.rect.y, 8.0);
714
714
+
}
715
715
+
716
716
+
#[test]
717
717
+
fn text_wraps_at_container_width() {
718
718
+
// Use a narrow viewport to force wrapping.
719
719
+
let mut doc = Document::new();
720
720
+
let root = doc.root();
721
721
+
let html = doc.create_element("html");
722
722
+
let body = doc.create_element("body");
723
723
+
let p = doc.create_element("p");
724
724
+
let text =
725
725
+
doc.create_text("The quick brown fox jumps over the lazy dog and more words to wrap");
726
726
+
doc.append_child(root, html);
727
727
+
doc.append_child(html, body);
728
728
+
doc.append_child(body, p);
729
729
+
doc.append_child(p, text);
730
730
+
731
731
+
let font = test_font();
732
732
+
// Narrow viewport: 100px (minus body margin 8+8 = 84px content width).
733
733
+
let tree = layout(&doc, 100.0, 600.0, &font);
734
734
+
let body_box = &tree.root.children[0];
735
735
+
let p_box = &body_box.children[0];
736
736
+
737
737
+
// With a 84px content width, long text should wrap to multiple lines.
738
738
+
assert!(
739
739
+
p_box.lines.len() > 1,
740
740
+
"text should wrap to multiple lines, got {} lines",
741
741
+
p_box.lines.len()
742
742
+
);
743
743
+
}
744
744
+
745
745
+
#[test]
746
746
+
fn layout_produces_positive_dimensions() {
747
747
+
let mut doc = Document::new();
748
748
+
let root = doc.root();
749
749
+
let html = doc.create_element("html");
750
750
+
let body = doc.create_element("body");
751
751
+
let div = doc.create_element("div");
752
752
+
let text = doc.create_text("Content");
753
753
+
doc.append_child(root, html);
754
754
+
doc.append_child(html, body);
755
755
+
doc.append_child(body, div);
756
756
+
doc.append_child(div, text);
757
757
+
758
758
+
let tree = layout_simple_html(html, &doc);
759
759
+
760
760
+
// All boxes should have non-negative dimensions.
761
761
+
for b in tree.iter() {
762
762
+
assert!(b.rect.width >= 0.0, "width should be >= 0");
763
763
+
assert!(b.rect.height >= 0.0, "height should be >= 0");
764
764
+
}
765
765
+
766
766
+
// Overall layout should have positive height.
767
767
+
assert!(tree.height > 0.0, "layout height should be > 0");
768
768
+
}
769
769
+
770
770
+
#[test]
771
771
+
fn head_is_hidden() {
772
772
+
let mut doc = Document::new();
773
773
+
let root = doc.root();
774
774
+
let html = doc.create_element("html");
775
775
+
let head = doc.create_element("head");
776
776
+
let title = doc.create_element("title");
777
777
+
let title_text = doc.create_text("Page Title");
778
778
+
let body = doc.create_element("body");
779
779
+
let p = doc.create_element("p");
780
780
+
let p_text = doc.create_text("Visible");
781
781
+
doc.append_child(root, html);
782
782
+
doc.append_child(html, head);
783
783
+
doc.append_child(head, title);
784
784
+
doc.append_child(title, title_text);
785
785
+
doc.append_child(html, body);
786
786
+
doc.append_child(body, p);
787
787
+
doc.append_child(p, p_text);
788
788
+
789
789
+
let tree = layout_simple_html(html, &doc);
790
790
+
791
791
+
// html should have one child (body), head is display:none.
792
792
+
assert_eq!(
793
793
+
tree.root.children.len(),
794
794
+
1,
795
795
+
"html should have 1 child (body), head should be hidden"
796
796
+
);
797
797
+
}
798
798
+
799
799
+
#[test]
800
800
+
fn mixed_block_and_inline() {
801
801
+
// <html><body><div>Text<p>Block</p>More</div></body></html>
802
802
+
let mut doc = Document::new();
803
803
+
let root = doc.root();
804
804
+
let html = doc.create_element("html");
805
805
+
let body = doc.create_element("body");
806
806
+
let div = doc.create_element("div");
807
807
+
let text1 = doc.create_text("Text");
808
808
+
let p = doc.create_element("p");
809
809
+
let p_text = doc.create_text("Block");
810
810
+
let text2 = doc.create_text("More");
811
811
+
doc.append_child(root, html);
812
812
+
doc.append_child(html, body);
813
813
+
doc.append_child(body, div);
814
814
+
doc.append_child(div, text1);
815
815
+
doc.append_child(div, p);
816
816
+
doc.append_child(p, p_text);
817
817
+
doc.append_child(div, text2);
818
818
+
819
819
+
let tree = layout_simple_html(html, &doc);
820
820
+
let body_box = &tree.root.children[0];
821
821
+
let div_box = &body_box.children[0];
822
822
+
823
823
+
// div should have 3 children: anonymous(Text), block(p), anonymous(More).
824
824
+
assert_eq!(
825
825
+
div_box.children.len(),
826
826
+
3,
827
827
+
"div should have 3 children (anon, block, anon), got {}",
828
828
+
div_box.children.len()
829
829
+
);
830
830
+
831
831
+
assert!(matches!(div_box.children[0].box_type, BoxType::Anonymous));
832
832
+
assert!(matches!(div_box.children[1].box_type, BoxType::Block(_)));
833
833
+
assert!(matches!(div_box.children[2].box_type, BoxType::Anonymous));
834
834
+
}
835
835
+
836
836
+
#[test]
837
837
+
fn inline_elements_contribute_text() {
838
838
+
// <html><body><p>Hello <em>world</em>!</p></body></html>
839
839
+
let mut doc = Document::new();
840
840
+
let root = doc.root();
841
841
+
let html = doc.create_element("html");
842
842
+
let body = doc.create_element("body");
843
843
+
let p = doc.create_element("p");
844
844
+
let t1 = doc.create_text("Hello ");
845
845
+
let em = doc.create_element("em");
846
846
+
let t2 = doc.create_text("world");
847
847
+
let t3 = doc.create_text("!");
848
848
+
doc.append_child(root, html);
849
849
+
doc.append_child(html, body);
850
850
+
doc.append_child(body, p);
851
851
+
doc.append_child(p, t1);
852
852
+
doc.append_child(p, em);
853
853
+
doc.append_child(em, t2);
854
854
+
doc.append_child(p, t3);
855
855
+
856
856
+
let tree = layout_simple_html(html, &doc);
857
857
+
let body_box = &tree.root.children[0];
858
858
+
let p_box = &body_box.children[0];
859
859
+
860
860
+
// Text should be collected from inline children.
861
861
+
assert!(!p_box.lines.is_empty());
862
862
+
assert_eq!(p_box.lines[0].text, "Hello world!");
863
863
+
}
864
864
+
865
865
+
#[test]
866
866
+
fn collapse_whitespace_works() {
867
867
+
assert_eq!(collapse_whitespace("hello world"), "hello world");
868
868
+
assert_eq!(collapse_whitespace(" spaces "), " spaces ");
869
869
+
assert_eq!(collapse_whitespace("\n\ttabs\n"), " tabs ");
870
870
+
assert_eq!(collapse_whitespace("no-extra"), "no-extra");
871
871
+
assert_eq!(collapse_whitespace(" "), " ");
872
872
+
}
873
873
+
874
874
+
#[test]
875
875
+
fn display_type_classification() {
876
876
+
assert_eq!(display_type("div"), DisplayType::Block);
877
877
+
assert_eq!(display_type("p"), DisplayType::Block);
878
878
+
assert_eq!(display_type("h1"), DisplayType::Block);
879
879
+
assert_eq!(display_type("span"), DisplayType::Inline);
880
880
+
assert_eq!(display_type("a"), DisplayType::Inline);
881
881
+
assert_eq!(display_type("head"), DisplayType::None);
882
882
+
assert_eq!(display_type("script"), DisplayType::None);
883
883
+
assert_eq!(display_type("unknown-tag"), DisplayType::Block);
884
884
+
}
885
885
+
886
886
+
#[test]
887
887
+
fn default_font_sizes() {
888
888
+
assert_eq!(default_font_size("h1", 16.0), 32.0);
889
889
+
assert_eq!(default_font_size("h2", 16.0), 24.0);
890
890
+
assert_eq!(default_font_size("p", 16.0), 16.0);
891
891
+
assert_eq!(default_font_size("div", 16.0), 16.0);
892
892
+
}
893
893
+
894
894
+
#[test]
895
895
+
fn heading_margins() {
896
896
+
let m = default_margin("h1", 32.0);
897
897
+
let expected = 32.0 * 0.67;
898
898
+
assert!((m.top - expected).abs() < 0.01);
899
899
+
assert!((m.bottom - expected).abs() < 0.01);
900
900
+
}
901
901
+
902
902
+
#[test]
903
903
+
fn layout_tree_iteration() {
904
904
+
let mut doc = Document::new();
905
905
+
let root = doc.root();
906
906
+
let html = doc.create_element("html");
907
907
+
let body = doc.create_element("body");
908
908
+
let p = doc.create_element("p");
909
909
+
let text = doc.create_text("Test");
910
910
+
doc.append_child(root, html);
911
911
+
doc.append_child(html, body);
912
912
+
doc.append_child(body, p);
913
913
+
doc.append_child(p, text);
914
914
+
915
915
+
let tree = layout_simple_html(html, &doc);
916
916
+
let count = tree.iter().count();
917
917
+
assert!(count >= 3, "should have at least html, body, p boxes");
918
918
+
}
919
919
+
920
920
+
#[test]
921
921
+
fn content_width_respects_body_margin() {
922
922
+
let mut doc = Document::new();
923
923
+
let root = doc.root();
924
924
+
let html = doc.create_element("html");
925
925
+
let body = doc.create_element("body");
926
926
+
let div = doc.create_element("div");
927
927
+
let text = doc.create_text("Content");
928
928
+
doc.append_child(root, html);
929
929
+
doc.append_child(html, body);
930
930
+
doc.append_child(body, div);
931
931
+
doc.append_child(div, text);
932
932
+
933
933
+
let font = test_font();
934
934
+
let tree = layout(&doc, 800.0, 600.0, &font);
935
935
+
let body_box = &tree.root.children[0];
936
936
+
937
937
+
// body content width = 800 - 8 - 8 = 784
938
938
+
assert_eq!(body_box.rect.width, 784.0);
939
939
+
940
940
+
// div inside body should also be 784px wide.
941
941
+
let div_box = &body_box.children[0];
942
942
+
assert_eq!(div_box.rect.width, 784.0);
943
943
+
}
944
944
+
945
945
+
#[test]
946
946
+
fn multiple_heading_levels() {
947
947
+
let mut doc = Document::new();
948
948
+
let root = doc.root();
949
949
+
let html = doc.create_element("html");
950
950
+
let body = doc.create_element("body");
951
951
+
doc.append_child(root, html);
952
952
+
doc.append_child(html, body);
953
953
+
954
954
+
let tags = ["h1", "h2", "h3"];
955
955
+
for tag in &tags {
956
956
+
let h = doc.create_element(tag);
957
957
+
let t = doc.create_text(tag);
958
958
+
doc.append_child(body, h);
959
959
+
doc.append_child(h, t);
960
960
+
}
961
961
+
962
962
+
let tree = layout_simple_html(html, &doc);
963
963
+
let body_box = &tree.root.children[0];
964
964
+
965
965
+
// h1 font_size > h2 font_size > h3 font_size
966
966
+
let h1 = &body_box.children[0];
967
967
+
let h2 = &body_box.children[1];
968
968
+
let h3 = &body_box.children[2];
969
969
+
assert!(h1.font_size > h2.font_size);
970
970
+
assert!(h2.font_size > h3.font_size);
971
971
+
972
972
+
// All should stack vertically.
973
973
+
assert!(h2.rect.y > h1.rect.y);
974
974
+
assert!(h3.rect.y > h2.rect.y);
975
975
+
}
976
976
+
}