Skip to main content

kozan_core/layout/
fragment.rs

1//! Fragment — the immutable output of layout.
2//!
3//! Chrome equivalent: `NGPhysicalFragment` + `NGPhysicalBoxFragment` +
4//! `NGPhysicalTextFragment`.
5//!
6//! # Architecture
7//!
8//! Fragments are the ONLY output of layout. They flow UP the tree
9//! (children produce fragments, parents position them). Once created,
10//! fragments are **immutable** — wrapped in `Arc` for safe sharing
11//! between layout, paint, hit-testing, and the compositor.
12//!
13//! # Why immutable?
14//!
15//! Chrome's `LayoutNG` learned from legacy's bugs:
16//! - **No hysteresis**: same inputs = same outputs, always.
17//! - **Safe concurrent access**: paint reads while layout runs on next frame.
18//! - **Fragment caching**: if `ConstraintSpace` matches, reuse the fragment.
19//! - **No stale reads**: impossible to read half-updated data.
20//!
21//! # Fragment types
22//!
23//! ```text
24//! Fragment
25//! ├── BoxFragment    — block, flex, grid, inline-block containers
26//! ├── TextFragment   — shaped text content (glyph runs)
27//! └── LineFragment   — a line box in inline formatting context
28//! ```
29
30use std::sync::Arc;
31
32use kozan_primitives::geometry::{Point, Size};
33use style::properties::ComputedValues;
34
35/// A positioned child within a parent fragment.
36///
37/// Chrome equivalent: `NGLink` — stores the offset separately from
38/// the fragment, so the same fragment can appear at different positions
39/// (e.g., in different columns) without cloning.
40#[derive(Debug, Clone)]
41pub struct ChildFragment {
42    /// Offset relative to the parent fragment's top-left corner.
43    pub offset: Point,
44    /// The child's fragment (shared, immutable).
45    pub fragment: Arc<Fragment>,
46}
47
48/// The immutable output of a layout algorithm.
49///
50/// Chrome equivalent: `NGPhysicalFragment`. Created by layout algorithms,
51/// never modified afterwards. Wrapped in `Arc` for zero-cost sharing.
52///
53/// # Coordinate system
54///
55/// All coordinates are **physical** (not logical). Writing-mode conversion
56/// happens at the algorithm level, not in the fragment.
57/// Chrome uses physical fragments too (`NGPhysicalFragment`, not `NGLogicalFragment`).
58#[derive(Debug, Clone)]
59pub struct Fragment {
60    /// The border-box size of this fragment.
61    pub size: Size,
62    /// What kind of fragment this is.
63    pub kind: FragmentKind,
64    /// The computed style for this fragment.
65    /// Chrome: `NGPhysicalFragment::Style()`.
66    /// Used by the paint phase to determine background, border, text color, etc.
67    /// `None` for fragments not yet connected to paint (e.g., anonymous, line boxes).
68    pub style: Option<servo_arc::Arc<ComputedValues>>,
69    /// The DOM node index this fragment was generated from.
70    /// `None` for anonymous boxes and line fragments.
71    /// Used by the paint phase to look up additional data (text content, etc.).
72    pub dom_node: Option<u32>,
73}
74
75/// The specific type of a fragment.
76///
77/// Chrome equivalent: `NGPhysicalFragment::Type` + subclass data.
78#[derive(Debug, Clone)]
79pub enum FragmentKind {
80    /// A box fragment (block, flex, grid, inline-block container).
81    /// Chrome: `NGPhysicalBoxFragment`.
82    Box(BoxFragmentData),
83
84    /// A text fragment (shaped glyph run).
85    /// Chrome: `NGPhysicalTextFragment`.
86    Text(TextFragmentData),
87
88    /// A line box in an inline formatting context.
89    /// Chrome: `NGPhysicalLineBoxFragment`.
90    Line(LineFragmentData),
91}
92
93/// Data specific to box fragments (containers).
94///
95/// Chrome equivalent: `NGPhysicalBoxFragment` fields.
96#[derive(Debug, Clone, Default)]
97pub struct BoxFragmentData {
98    /// Positioned children within this box.
99    pub children: Vec<ChildFragment>,
100    /// Padding box insets (for hit-testing and paint).
101    pub padding: PhysicalInsets,
102    /// Border box insets (for border painting).
103    pub border: PhysicalInsets,
104    /// Content overflow extent (for scrolling).
105    /// If larger than the box's size, there's scrollable overflow.
106    pub scrollable_overflow: Size,
107    /// Whether this box establishes a new stacking context.
108    pub is_stacking_context: bool,
109    /// Overflow behavior on inline axis.
110    pub overflow_x: OverflowClip,
111    /// Overflow behavior on block axis.
112    pub overflow_y: OverflowClip,
113}
114
115/// How overflow content is handled for a box fragment.
116///
117/// Chrome equivalent: part of `NGPhysicalBoxFragment` overflow handling.
118/// This is the RESOLVED overflow — after layout determines if there IS overflow.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
120pub enum OverflowClip {
121    /// Content overflows visibly (default).
122    #[default]
123    Visible,
124    /// Content is clipped to the box. No scroll mechanism.
125    Hidden,
126    /// Content is clipped. Scroll mechanism provided if overflow exists.
127    Scroll,
128    /// Like scroll, but scroll mechanism only shown when needed.
129    Auto,
130}
131
132impl OverflowClip {
133    /// Whether this mode clips content (for paint + hit-test).
134    pub fn clips(self) -> bool {
135        matches!(self, Self::Hidden | Self::Scroll | Self::Auto)
136    }
137
138    /// Whether the user can scroll this axis (wheel/touch).
139    /// `Hidden` clips but does NOT respond to user input.
140    pub fn is_user_scrollable(self) -> bool {
141        matches!(self, Self::Scroll | Self::Auto)
142    }
143}
144
145/// Data specific to text fragments (glyph runs).
146///
147/// Chrome equivalent: `NGPhysicalTextFragment` fields.
148/// The actual glyphs and positions will come from Parley's shaping output.
149#[derive(Debug, Clone)]
150pub struct TextFragmentData {
151    /// The text content this fragment represents.
152    pub text_range: std::ops::Range<usize>,
153    /// Baseline offset from the fragment's top edge.
154    pub baseline: f32,
155    /// The raw text content.
156    pub text: Option<Arc<str>>,
157    /// Pre-shaped glyph runs from Parley (`HarfRust`).
158    /// Chrome: `ShapeResult` on `NGPhysicalTextFragment`.
159    /// Shaped ONCE during layout, read by paint + renderer.
160    /// Font data is `parley::FontData` = `peniko::Font` — zero conversion to vello.
161    pub shaped_runs: Vec<crate::layout::inline::font_system::ShapedTextRun>,
162}
163
164/// Data specific to line box fragments.
165///
166/// Chrome equivalent: `NGPhysicalLineBoxFragment`.
167/// A line box contains inline-level children (text, inline boxes).
168#[derive(Debug, Clone)]
169pub struct LineFragmentData {
170    /// Inline-level children positioned within this line.
171    pub children: Vec<ChildFragment>,
172    /// The baseline of this line (from top of line box).
173    pub baseline: f32,
174}
175
176/// Physical edge insets (top, right, bottom, left).
177///
178/// Used for padding and border widths on box fragments.
179/// "Physical" means not affected by writing-mode.
180#[derive(Debug, Clone, Copy, Default)]
181pub struct PhysicalInsets {
182    pub top: f32,
183    pub right: f32,
184    pub bottom: f32,
185    pub left: f32,
186}
187
188impl PhysicalInsets {
189    pub const ZERO: Self = Self {
190        top: 0.0,
191        right: 0.0,
192        bottom: 0.0,
193        left: 0.0,
194    };
195
196    #[must_use]
197    pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
198        Self {
199            top,
200            right,
201            bottom,
202            left,
203        }
204    }
205
206    /// Total inline (horizontal) insets.
207    #[inline]
208    #[must_use]
209    pub fn inline_sum(&self) -> f32 {
210        self.left + self.right
211    }
212
213    /// Total block (vertical) insets.
214    #[inline]
215    #[must_use]
216    pub fn block_sum(&self) -> f32 {
217        self.top + self.bottom
218    }
219}
220
221impl Fragment {
222    /// Create a box fragment.
223    #[must_use]
224    pub fn new_box(size: Size, data: BoxFragmentData) -> Arc<Self> {
225        Arc::new(Self {
226            size,
227            kind: FragmentKind::Box(data),
228            style: None,
229            dom_node: None,
230        })
231    }
232
233    /// Create a box fragment with computed style and DOM node reference.
234    ///
235    /// Chrome: `NGPhysicalFragment` always has a style + layout object pointer.
236    /// The style is needed by the paint phase for background, border, text color.
237    /// The `dom_node` is needed for looking up text content and element data.
238    #[must_use]
239    pub fn new_box_styled(
240        size: Size,
241        data: BoxFragmentData,
242        style: servo_arc::Arc<ComputedValues>,
243        dom_node: Option<u32>,
244    ) -> Arc<Self> {
245        Arc::new(Self {
246            size,
247            kind: FragmentKind::Box(data),
248            style: Some(style),
249            dom_node,
250        })
251    }
252
253    /// Create a text fragment.
254    #[must_use]
255    pub fn new_text(size: Size, data: TextFragmentData) -> Arc<Self> {
256        Arc::new(Self {
257            size,
258            kind: FragmentKind::Text(data),
259            style: None,
260            dom_node: None,
261        })
262    }
263
264    /// Create a text fragment with style (for font-size, color inheritance).
265    #[must_use]
266    pub fn new_text_styled(
267        size: Size,
268        data: TextFragmentData,
269        style: servo_arc::Arc<style::properties::ComputedValues>,
270        dom_node: Option<u32>,
271    ) -> Arc<Self> {
272        Arc::new(Self {
273            size,
274            kind: FragmentKind::Text(data),
275            style: Some(style),
276            dom_node,
277        })
278    }
279
280    /// Create a line fragment.
281    #[must_use]
282    pub fn new_line(size: Size, data: LineFragmentData) -> Arc<Self> {
283        Arc::new(Self {
284            size,
285            kind: FragmentKind::Line(data),
286            style: None,
287            dom_node: None,
288        })
289    }
290
291    /// Whether this is a box fragment.
292    #[must_use]
293    pub fn is_box(&self) -> bool {
294        matches!(self.kind, FragmentKind::Box(_))
295    }
296
297    /// Whether this is a text fragment.
298    #[must_use]
299    pub fn is_text(&self) -> bool {
300        matches!(self.kind, FragmentKind::Text(_))
301    }
302
303    /// Whether this is a line fragment.
304    #[must_use]
305    pub fn is_line(&self) -> bool {
306        matches!(self.kind, FragmentKind::Line(_))
307    }
308
309    /// Panics if this is not a box fragment. Use `try_as_box()` when unsure.
310    #[must_use]
311    pub fn unwrap_box(&self) -> &BoxFragmentData {
312        match &self.kind {
313            FragmentKind::Box(data) => data,
314            _ => panic!("Fragment is not a box"),
315        }
316    }
317
318    /// Get line fragment data (panics if not a line).
319    #[must_use]
320    pub fn as_line(&self) -> &LineFragmentData {
321        match &self.kind {
322            FragmentKind::Line(data) => data,
323            _ => panic!("Fragment is not a line"),
324        }
325    }
326
327    /// Get box fragment data if this is a box.
328    #[must_use]
329    pub fn try_as_box(&self) -> Option<&BoxFragmentData> {
330        match &self.kind {
331            FragmentKind::Box(data) => Some(data),
332            _ => None,
333        }
334    }
335
336    /// Get text fragment data if this is text.
337    #[must_use]
338    pub fn try_as_text(&self) -> Option<&TextFragmentData> {
339        match &self.kind {
340            FragmentKind::Text(data) => Some(data),
341            _ => None,
342        }
343    }
344
345    /// Get line fragment data if this is a line.
346    #[must_use]
347    pub fn try_as_line(&self) -> Option<&LineFragmentData> {
348        match &self.kind {
349            FragmentKind::Line(data) => Some(data),
350            _ => None,
351        }
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn box_fragment_immutable_via_arc() {
361        let fragment = Fragment::new_box(Size::new(100.0, 50.0), BoxFragmentData::default());
362        // Arc means shared + immutable.
363        let shared = Arc::clone(&fragment);
364        assert_eq!(fragment.size.width, 100.0);
365        assert_eq!(shared.size.height, 50.0);
366        assert!(fragment.is_box());
367    }
368
369    #[test]
370    fn text_fragment() {
371        let fragment = Fragment::new_text(
372            Size::new(80.0, 16.0),
373            TextFragmentData {
374                text_range: 0..5,
375                baseline: 12.0,
376                text: Some(Arc::from("Hello")),
377                shaped_runs: Vec::new(),
378            },
379        );
380        assert!(fragment.is_text());
381        assert_eq!(fragment.try_as_text().unwrap().baseline, 12.0);
382    }
383
384    #[test]
385    fn line_fragment_with_children() {
386        let text = Fragment::new_text(
387            Size::new(40.0, 16.0),
388            TextFragmentData {
389                text_range: 0..3,
390                baseline: 12.0,
391                text: Some(Arc::from("abc")),
392                shaped_runs: Vec::new(),
393            },
394        );
395
396        let line = Fragment::new_line(
397            Size::new(200.0, 20.0),
398            LineFragmentData {
399                children: vec![ChildFragment {
400                    offset: Point::new(0.0, 2.0),
401                    fragment: text,
402                }],
403                baseline: 16.0,
404            },
405        );
406        assert!(line.is_line());
407        assert_eq!(line.try_as_line().unwrap().children.len(), 1);
408    }
409
410    #[test]
411    fn nested_box_fragments() {
412        let inner = Fragment::new_box(
413            Size::new(50.0, 30.0),
414            BoxFragmentData {
415                padding: PhysicalInsets::new(5.0, 5.0, 5.0, 5.0),
416                border: PhysicalInsets::new(1.0, 1.0, 1.0, 1.0),
417                ..Default::default()
418            },
419        );
420
421        let outer = Fragment::new_box(
422            Size::new(200.0, 100.0),
423            BoxFragmentData {
424                children: vec![ChildFragment {
425                    offset: Point::new(10.0, 10.0),
426                    fragment: inner,
427                }],
428                ..Default::default()
429            },
430        );
431
432        let children = &outer.unwrap_box().children;
433        assert_eq!(children.len(), 1);
434        assert_eq!(children[0].offset.x, 10.0);
435        assert_eq!(children[0].fragment.size.width, 50.0);
436    }
437
438    #[test]
439    fn physical_insets() {
440        let insets = PhysicalInsets::new(10.0, 20.0, 10.0, 20.0);
441        assert_eq!(insets.inline_sum(), 40.0);
442        assert_eq!(insets.block_sum(), 20.0);
443    }
444
445    #[test]
446    fn fragment_is_send_sync() {
447        fn assert_send_sync<T: Send + Sync>() {}
448        assert_send_sync::<Arc<Fragment>>();
449    }
450}