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}