Skip to main content

kozan_core/layout/inline/
context.rs

1//! Inline formatting context — collects inline items and manages layout.
2//!
3//! Chrome equivalent: `NGInlineNode::PrepareLayout()` + `CollectInlines`.
4
5use smallvec::SmallVec;
6
7use super::item::InlineItem;
8use style::properties::ComputedValues;
9
10/// The inline formatting context for a block container.
11///
12/// Collects all inline-level content into a flat item list,
13/// then delegates to the line breaker for layout.
14#[derive(Debug, Clone)]
15pub struct InlineFormattingContext {
16    /// Flat list of inline items (text runs, open/close tags, atomics).
17    /// `SmallVec` avoids heap allocation for the common case of ≤16 items.
18    pub items: SmallVec<[InlineItem; 16]>,
19}
20
21impl InlineFormattingContext {
22    #[must_use]
23    pub fn new() -> Self {
24        Self {
25            items: SmallVec::new(),
26        }
27    }
28
29    /// Add a text run, measuring it via the provided `TextMeasurer`.
30    ///
31    /// Chrome equivalent: `InlineBoxState::ComputeTextMetrics()`.
32    ///
33    /// 1. `measure()` → `ShapeResult` (width from glyph advances)
34    /// 2. `font_metrics()` → `FontMetrics` (ascent/descent/lineGap from font)
35    /// 3. `resolve_line_height()` → CSS line-height → pixel value
36    /// 4. `FontHeight::from_metrics_and_line_height()` → leading split
37    pub fn add_text(
38        &mut self,
39        content: std::sync::Arc<str>,
40        style: servo_arc::Arc<ComputedValues>,
41        measurer: &dyn super::measurer::TextMeasurer,
42    ) {
43        let font_size = style.clone_font_size().computed_size().px();
44        let text_metrics = measurer.measure(&content, font_size);
45        let font_metrics = measurer.font_metrics(font_size);
46
47        // Resolve CSS line-height using the font's actual metrics.
48        let line_height = super::measurer::resolve_line_height(
49            &style.clone_line_height(),
50            font_size,
51            &font_metrics,
52        );
53
54        // Distribute leading equally above and below the baseline.
55        // Chrome: CalculateLeadingSpace() → AddLeading().
56        let font_height =
57            super::measurer::FontHeight::from_metrics_and_line_height(&font_metrics, line_height);
58
59        self.items.push(InlineItem::Text {
60            content,
61            style,
62            measured_width: text_metrics.width,
63            measured_height: font_height.height(),
64            baseline: font_height.ascent,
65        });
66    }
67
68    /// Add a forced line break.
69    pub fn add_break(&mut self) {
70        self.items.push(InlineItem::ForcedBreak);
71    }
72
73    /// Add an atomic inline (inline-block, image, etc.).
74    pub fn add_atomic(
75        &mut self,
76        width: f32,
77        height: f32,
78        baseline: f32,
79        layout_id: u32,
80        style: servo_arc::Arc<ComputedValues>,
81    ) {
82        self.items.push(InlineItem::AtomicInline {
83            width,
84            height,
85            baseline,
86            layout_id,
87            style,
88        });
89    }
90
91    /// Add an open tag for an inline element.
92    pub fn add_open_tag(&mut self, style: servo_arc::Arc<ComputedValues>) {
93        // Extract inline-direction margin/border/padding.
94        // TODO: support RTL (swap left↔right based on direction)
95        let margin_start = resolve_stylo_margin(&style.get_margin().margin_left);
96        let border_start = style.get_border().border_left_width.0.to_f32_px();
97        let padding_start = resolve_stylo_lp(&style.get_padding().padding_left.0);
98
99        self.items.push(InlineItem::OpenTag {
100            style,
101            margin_inline_start: margin_start,
102            border_inline_start: border_start,
103            padding_inline_start: padding_start,
104        });
105    }
106
107    /// Add a close tag for an inline element.
108    pub fn add_close_tag(&mut self, style: &ComputedValues) {
109        // TODO: support RTL (swap left↔right based on direction)
110        let margin_end = resolve_stylo_margin(&style.get_margin().margin_right);
111        let border_end = style.get_border().border_right_width.0.to_f32_px();
112        let padding_end = resolve_stylo_lp(&style.get_padding().padding_right.0);
113
114        self.items.push(InlineItem::CloseTag {
115            margin_inline_end: margin_end,
116            border_inline_end: border_end,
117            padding_inline_end: padding_end,
118        });
119    }
120
121    /// Whether this context has any items.
122    #[must_use]
123    pub fn is_empty(&self) -> bool {
124        self.items.is_empty()
125    }
126}
127
128/// Resolve a Stylo margin value to px (auto → 0).
129fn resolve_stylo_margin(
130    value: &style::values::generics::length::GenericMargin<
131        style::values::computed::LengthPercentage,
132    >,
133) -> f32 {
134    use style::values::generics::length::GenericMargin;
135    match value {
136        GenericMargin::LengthPercentage(lp) => resolve_stylo_lp(lp),
137        GenericMargin::Auto => 0.0,
138        _ => 0.0, // AnchorSizeFunction etc.
139    }
140}
141
142/// Resolve a Stylo `LengthPercentage` to px (percentages resolved against 0 for now).
143fn resolve_stylo_lp(value: &style::values::computed::LengthPercentage) -> f32 {
144    value
145        .percentage_relative_to(style::values::computed::CSSPixelLength::new(0.0))
146        .px()
147}
148
149impl Default for InlineFormattingContext {
150    fn default() -> Self {
151        Self::new()
152    }
153}