Skip to main content

cssbox_core/
inline.rs

1//! Inline formatting context implementation.
2//!
3//! This module handles the layout of inline-level content, including text runs,
4//! inline boxes, and line breaking.
5
6use crate::float::FloatContext;
7use crate::fragment::{Fragment, FragmentKind};
8use crate::geometry::{Point, Size};
9use crate::layout::LayoutContext;
10use crate::style::{TextAlign, VerticalAlign, WhiteSpace};
11use crate::tree::NodeId;
12
13/// An inline item that needs to be laid out within a line.
14#[derive(Debug, Clone)]
15struct InlineItem {
16    node: NodeId,
17    size: Size,
18    baseline: f32,
19}
20
21/// A single line box containing inline items.
22#[derive(Debug)]
23struct LineBox {
24    items: Vec<InlineItem>,
25    width: f32,
26    height: f32,
27    baseline: f32,
28}
29
30impl LineBox {
31    fn new() -> Self {
32        Self {
33            items: Vec::new(),
34            width: 0.0,
35            height: 0.0,
36            baseline: 0.0,
37        }
38    }
39
40    fn add_item(&mut self, item: InlineItem) {
41        self.width += item.size.width;
42        self.height = self.height.max(item.size.height);
43        self.baseline = self.baseline.max(item.baseline);
44        self.items.push(item);
45    }
46
47    fn is_empty(&self) -> bool {
48        self.items.is_empty()
49    }
50
51    fn used_width(&self) -> f32 {
52        self.width
53    }
54}
55
56/// Layout an inline formatting context.
57///
58/// This function creates line boxes from inline-level children of the parent node,
59/// applying line breaking, text alignment, and vertical alignment.
60///
61/// # Arguments
62///
63/// * `ctx` - The layout context with tree and text measurement
64/// * `parent` - The parent node establishing the inline formatting context
65/// * `containing_width` - The available width for lines
66/// * `fragments` - Output vector to append generated fragments
67/// * `float_ctx` - Float context for handling floated elements (currently unused)
68///
69/// # Returns
70///
71/// The total height consumed by all line boxes.
72pub fn layout_inline_formatting_context(
73    ctx: &LayoutContext,
74    parent: NodeId,
75    containing_width: f32,
76    fragments: &mut Vec<Fragment>,
77    _float_ctx: &mut FloatContext,
78) -> f32 {
79    let parent_style = ctx.tree.style(parent);
80    let text_align = parent_style.text_align;
81    let line_height = parent_style.line_height;
82    let white_space = parent_style.white_space;
83
84    // Collect inline items from children
85    let mut inline_items = Vec::new();
86    collect_inline_items(ctx, parent, containing_width, &mut inline_items);
87
88    // Break into line boxes
89    let mut line_boxes = Vec::new();
90    break_into_lines(
91        &inline_items,
92        containing_width,
93        white_space,
94        &mut line_boxes,
95    );
96
97    // Position line boxes and create fragments
98    let mut current_y = 0.0;
99
100    for line_box in &line_boxes {
101        let line_height_actual = line_box.height.max(line_height);
102
103        // Apply text alignment
104        let line_offset_x = calculate_text_align_offset(
105            text_align,
106            containing_width,
107            line_box.used_width(),
108            line_box.items.len(),
109        );
110
111        // Create line box fragment
112        let mut line_fragment = Fragment::new(parent, FragmentKind::LineBox);
113        line_fragment.position = Point::new(0.0, current_y);
114        line_fragment.size = Size::new(containing_width, line_height_actual);
115
116        // Position items within line
117        let mut current_x = line_offset_x;
118
119        for (idx, item) in line_box.items.iter().enumerate() {
120            let mut item_fragment = Fragment::new(item.node, FragmentKind::TextRun);
121            item_fragment.size = item.size;
122
123            // Calculate vertical position based on vertical-align
124            let item_style = ctx.tree.style(item.node);
125            let vertical_offset = calculate_vertical_align_offset(
126                item_style.vertical_align,
127                line_box.baseline,
128                item.baseline,
129                item.size.height,
130                line_height_actual,
131            );
132
133            item_fragment.position = Point::new(current_x, vertical_offset);
134
135            // Apply justify spacing if needed
136            let extra_space = if text_align == TextAlign::Justify
137                && line_box.items.len() > 1
138                && idx < line_box.items.len() - 1
139            {
140                let remaining = containing_width - line_box.used_width();
141                let gaps = (line_box.items.len() - 1) as f32;
142                remaining / gaps
143            } else {
144                0.0
145            };
146
147            current_x += item.size.width + extra_space;
148
149            line_fragment.children.push(item_fragment);
150        }
151
152        current_y += line_height_actual;
153        fragments.push(line_fragment);
154    }
155
156    current_y
157}
158
159/// Collect inline items from the children of a parent node.
160fn collect_inline_items(
161    ctx: &LayoutContext,
162    parent: NodeId,
163    max_width: f32,
164    items: &mut Vec<InlineItem>,
165) {
166    let children = ctx.tree.children(parent);
167
168    for &child in children {
169        let node = ctx.tree.node(child);
170        let style = ctx.tree.style(child);
171
172        if node.is_text() {
173            if let Some(text) = node.text_content() {
174                // Measure text
175                let font_size = style.line_height / 1.2;
176                let size = ctx.text_measure.measure(text, font_size, max_width);
177
178                // For text, baseline is typically at bottom minus descent
179                // Simplified: baseline is at 80% of height (20% descent)
180                let baseline = size.height * 0.8;
181
182                items.push(InlineItem {
183                    node: child,
184                    size,
185                    baseline,
186                });
187            }
188        } else {
189            // Inline-level element - recursively collect or measure as atomic
190            // For now, treat as atomic inline-block
191            // In a full implementation, we'd check display type and recurse if inline
192            let font_size = style.line_height / 1.2;
193            let size = ctx.text_measure.measure("X", font_size, max_width);
194            let baseline = size.height * 0.8;
195
196            items.push(InlineItem {
197                node: child,
198                size,
199                baseline,
200            });
201        }
202    }
203}
204
205/// Break inline items into line boxes using greedy line breaking.
206fn break_into_lines(
207    items: &[InlineItem],
208    containing_width: f32,
209    white_space: WhiteSpace,
210    line_boxes: &mut Vec<LineBox>,
211) {
212    if items.is_empty() {
213        return;
214    }
215
216    let allow_wrapping = white_space.wraps();
217    let mut current_line = LineBox::new();
218
219    for item in items {
220        let item_width = item.size.width;
221
222        // Check if item fits on current line
223        let fits =
224            current_line.is_empty() || current_line.used_width() + item_width <= containing_width;
225
226        if fits || !allow_wrapping {
227            // Add to current line
228            current_line.add_item(item.clone());
229        } else {
230            // Start new line
231            if !current_line.is_empty() {
232                line_boxes.push(current_line);
233            }
234            current_line = LineBox::new();
235            current_line.add_item(item.clone());
236        }
237    }
238
239    // Push final line
240    if !current_line.is_empty() {
241        line_boxes.push(current_line);
242    }
243}
244
245/// Calculate horizontal offset for text alignment.
246fn calculate_text_align_offset(
247    align: TextAlign,
248    containing_width: f32,
249    line_width: f32,
250    _item_count: usize,
251) -> f32 {
252    match align {
253        TextAlign::Left => 0.0,
254        TextAlign::Right => (containing_width - line_width).max(0.0),
255        TextAlign::Center => ((containing_width - line_width) / 2.0).max(0.0),
256        TextAlign::Justify => {
257            // Justify only applies to spacing between items, not initial offset
258            0.0
259        }
260    }
261}
262
263/// Calculate vertical offset for an item based on vertical-align.
264fn calculate_vertical_align_offset(
265    align: VerticalAlign,
266    line_baseline: f32,
267    item_baseline: f32,
268    item_height: f32,
269    line_height: f32,
270) -> f32 {
271    match align {
272        VerticalAlign::Baseline => line_baseline - item_baseline,
273        VerticalAlign::Top => 0.0,
274        VerticalAlign::Bottom => line_height - item_height,
275        VerticalAlign::Middle => (line_height - item_height) / 2.0,
276        VerticalAlign::Length(offset) => line_baseline - item_baseline + offset,
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::layout::FixedWidthTextMeasure;
284    use crate::style::ComputedStyle;
285    use crate::tree::{BoxTreeBuilder, NodeKind, TextContent};
286
287    #[test]
288    fn test_single_line_layout() {
289        let mut builder = BoxTreeBuilder::new();
290        let root = builder.root(ComputedStyle::block());
291        builder.text(root, "Hello");
292        let tree = builder.build();
293
294        let ctx = LayoutContext {
295            tree: &tree,
296            text_measure: &FixedWidthTextMeasure,
297            viewport: Size::new(800.0, 600.0),
298        };
299
300        let mut fragments = Vec::new();
301        let mut float_ctx = FloatContext::new(800.0);
302        let height =
303            layout_inline_formatting_context(&ctx, root, 800.0, &mut fragments, &mut float_ctx);
304
305        assert_eq!(fragments.len(), 1); // One line box
306        assert!(height > 0.0);
307        assert_eq!(fragments[0].kind, FragmentKind::LineBox);
308        assert_eq!(fragments[0].children.len(), 1); // One text run
309    }
310
311    #[test]
312    fn test_line_wrapping() {
313        let mut builder = BoxTreeBuilder::new();
314        let root = builder.root(ComputedStyle::block());
315        builder.text(root, "Hello");
316        builder.text(root, "World");
317        builder.text(root, "Test");
318        let tree = builder.build();
319
320        let ctx = LayoutContext {
321            tree: &tree,
322            text_measure: &FixedWidthTextMeasure,
323            viewport: Size::new(800.0, 600.0),
324        };
325
326        let mut fragments = Vec::new();
327        let mut float_ctx = FloatContext::new(50.0);
328
329        // Narrow width to force wrapping
330        let height =
331            layout_inline_formatting_context(&ctx, root, 50.0, &mut fragments, &mut float_ctx);
332
333        // Should have multiple lines due to narrow width
334        // Default line_height is 1.2 (unitless), so each line is 1.2px tall
335        // With 3 text items wrapping to 3 lines: total height = 3.6px
336        assert!(fragments.len() >= 2);
337        assert!(height > 1.2); // More than one line height
338    }
339
340    #[test]
341    fn test_text_align_center() {
342        let mut builder = BoxTreeBuilder::new();
343        let mut style = ComputedStyle::block();
344        style.text_align = TextAlign::Center;
345        let root = builder.root(style);
346        builder.text(root, "Hi");
347        let tree = builder.build();
348
349        let ctx = LayoutContext {
350            tree: &tree,
351            text_measure: &FixedWidthTextMeasure,
352            viewport: Size::new(800.0, 600.0),
353        };
354
355        let mut fragments = Vec::new();
356        let mut float_ctx = FloatContext::new(800.0);
357        layout_inline_formatting_context(&ctx, root, 800.0, &mut fragments, &mut float_ctx);
358
359        // Check that text is not at x=0 (it's centered)
360        let line = &fragments[0];
361        let text_run = &line.children[0];
362        // "Hi" = 2 chars * 8px = 16px, centered in 800px = (800-16)/2 = 392
363        assert!((text_run.position.x - 392.0).abs() < 1.0);
364    }
365
366    #[test]
367    fn test_text_align_right() {
368        let mut builder = BoxTreeBuilder::new();
369        let mut style = ComputedStyle::block();
370        style.text_align = TextAlign::Right;
371        let root = builder.root(style);
372        builder.text(root, "Hi");
373        let tree = builder.build();
374
375        let ctx = LayoutContext {
376            tree: &tree,
377            text_measure: &FixedWidthTextMeasure,
378            viewport: Size::new(800.0, 600.0),
379        };
380
381        let mut fragments = Vec::new();
382        let mut float_ctx = FloatContext::new(800.0);
383        layout_inline_formatting_context(&ctx, root, 800.0, &mut fragments, &mut float_ctx);
384
385        // "Hi" = 2 chars * 8px = 16px, right-aligned in 800px = 800 - 16 = 784
386        let line = &fragments[0];
387        let text_run = &line.children[0];
388        assert!((text_run.position.x - 784.0).abs() < 1.0);
389    }
390
391    #[test]
392    fn test_text_align_justify() {
393        let mut builder = BoxTreeBuilder::new();
394        let mut style = ComputedStyle::block();
395        style.text_align = TextAlign::Justify;
396        let root = builder.root(style);
397        builder.text(root, "A");
398        builder.text(root, "B");
399        let tree = builder.build();
400
401        let ctx = LayoutContext {
402            tree: &tree,
403            text_measure: &FixedWidthTextMeasure,
404            viewport: Size::new(800.0, 600.0),
405        };
406
407        let mut fragments = Vec::new();
408        let mut float_ctx = FloatContext::new(100.0);
409        layout_inline_formatting_context(&ctx, root, 100.0, &mut fragments, &mut float_ctx);
410
411        let line = &fragments[0];
412        assert_eq!(line.children.len(), 2);
413
414        // First item at x=0
415        assert_eq!(line.children[0].position.x, 0.0);
416
417        // Second item should have extra space added for justification
418        // Each text is 8px wide, total 16px in 100px line
419        // Extra space = 100 - 16 = 84px distributed across 1 gap
420        // Second item x = 8 + 84 = 92
421        assert!((line.children[1].position.x - 92.0).abs() < 1.0);
422    }
423
424    #[test]
425    fn test_vertical_align_baseline() {
426        let mut builder = BoxTreeBuilder::new();
427        let root = builder.root(ComputedStyle::block());
428        builder.text(root, "Test");
429        let tree = builder.build();
430
431        let ctx = LayoutContext {
432            tree: &tree,
433            text_measure: &FixedWidthTextMeasure,
434            viewport: Size::new(800.0, 600.0),
435        };
436
437        let mut fragments = Vec::new();
438        let mut float_ctx = FloatContext::new(800.0);
439        layout_inline_formatting_context(&ctx, root, 800.0, &mut fragments, &mut float_ctx);
440
441        // Default is baseline alignment
442        let line = &fragments[0];
443        let text_run = &line.children[0];
444
445        // Vertical offset should position text on baseline
446        assert!(text_run.position.y >= 0.0);
447    }
448
449    #[test]
450    fn test_vertical_align_top() {
451        let mut builder = BoxTreeBuilder::new();
452        let style = ComputedStyle::block();
453        let root = builder.root(style);
454
455        let mut text_style = ComputedStyle::inline();
456        text_style.vertical_align = VerticalAlign::Top;
457
458        // Create text with custom style
459        let text_id = builder.tree.add_node(
460            NodeKind::Text(TextContent {
461                text: "Test".to_string(),
462            }),
463            text_style,
464        );
465        builder.tree.append_child(root, text_id);
466
467        let tree = builder.build();
468
469        let ctx = LayoutContext {
470            tree: &tree,
471            text_measure: &FixedWidthTextMeasure,
472            viewport: Size::new(800.0, 600.0),
473        };
474
475        let mut fragments = Vec::new();
476        let mut float_ctx = FloatContext::new(800.0);
477        layout_inline_formatting_context(&ctx, root, 800.0, &mut fragments, &mut float_ctx);
478
479        let line = &fragments[0];
480        let text_run = &line.children[0];
481
482        // Top alignment means y = 0
483        assert_eq!(text_run.position.y, 0.0);
484    }
485
486    #[test]
487    fn test_empty_inline_context() {
488        let mut builder = BoxTreeBuilder::new();
489        let root = builder.root(ComputedStyle::block());
490        let tree = builder.build();
491
492        let ctx = LayoutContext {
493            tree: &tree,
494            text_measure: &FixedWidthTextMeasure,
495            viewport: Size::new(800.0, 600.0),
496        };
497
498        let mut fragments = Vec::new();
499        let mut float_ctx = FloatContext::new(800.0);
500        let height =
501            layout_inline_formatting_context(&ctx, root, 800.0, &mut fragments, &mut float_ctx);
502
503        assert_eq!(height, 0.0);
504        assert_eq!(fragments.len(), 0);
505    }
506
507    #[test]
508    fn test_white_space_nowrap() {
509        let mut builder = BoxTreeBuilder::new();
510        let mut style = ComputedStyle::block();
511        style.white_space = WhiteSpace::Nowrap;
512        let root = builder.root(style);
513        builder.text(root, "A");
514        builder.text(root, "B");
515        builder.text(root, "C");
516        let tree = builder.build();
517
518        let ctx = LayoutContext {
519            tree: &tree,
520            text_measure: &FixedWidthTextMeasure,
521            viewport: Size::new(800.0, 600.0),
522        };
523
524        let mut fragments = Vec::new();
525        let mut float_ctx = FloatContext::new(10.0);
526
527        // Even with narrow width, nowrap should keep everything on one line
528        layout_inline_formatting_context(&ctx, root, 10.0, &mut fragments, &mut float_ctx);
529
530        // Should be one line despite narrow width
531        assert_eq!(fragments.len(), 1);
532        assert_eq!(fragments[0].children.len(), 3);
533    }
534}