Fragmentation
Overview
CSS Fragmentation Module Level 3
(css-break-3) covers breaking content
across pages, columns, and regions. Azul implements paged media for PDF
generation; column and region fragmentation are scaffolded but not active.
WIP — two parallel pagination paths exist. The active production path is
the „infinite-canvas with physical spacers“ model, which honours @page
margins and headers/footers but not break-inside, orphans, or widows. The
older integrated-splitting design remains in the code as public types but is
no longer driven by any caller.
The reader takeaway: paged output today lays one tall page out, splits the display list by Y-coordinate, and prepends headers and footers per page. This is simple and reuses the existing block/inline solver unchanged. The trade-off is that CSS break properties are parsed but largely advisory.
Two implementations, briefly
layout/src/fragmentation.rs. Partially superseded. CSS-spec-style „decide breaks during layout“. DefinesFragmentationLayoutContext,BoxBreakBehavior, andBreakPoint.layout/src/paged.rs. Active container model. TheFragmentationContextenum hasContinuous,Paged,MultiColumn, andRegionsvariants. DefinesFragmentainerandFragmentationState.layout/src/solver3/pagination.rs. Active.PageGeometeris the infinite-canvas coordinate model.FakePageConfighandles header and footer config without@pageparsing.layout/src/solver3/paged_layout.rs. Active.layout_document_pageddrives the paged path. It lays out once on a tall canvas, splits into pages by Y position, and filters the display list.
Active path: infinite canvas with physical spacers
solver3/pagination.rs lays content out on a single tall canvas with „dead
zones“ between pages representing margins, headers, and footers:
0px ─────────────────────────────
│ Page 1 Content │
1000px ─────────────────────────────
│ Dead Space (Footer+Margin) │ ← page break zone
1100px ─────────────────────────────
│ Page 2 Content │
2100px ─────────────────────────────
│ Dead Space (Footer+Margin) │
2200px ─────────────────────────────
The advantage: the existing block/inline solver runs unchanged on the tall
canvas. The downside: break-inside: avoid and orphans/widows aren't
honoured by the layout — the splitter has to do its best after the fact.
CSS @page rules aren't parsed yet (FakePageConfig is the programmatic
surrogate).
FragmentationContext
pub enum FragmentationContext {
Continuous {
width: f32,
container: Fragmentainer,
},
Paged {
page_size: LogicalSize,
pages: Vec<Fragmentainer>,
},
MultiColumn {
column_width: f32,
column_height: f32,
gap: f32,
columns: Vec<Fragmentainer>,
},
Regions {
regions: Vec<Fragmentainer>,
},
}
MultiColumn and Regions exist as enum variants but are not yet driven by
any layout path. Continuous and Paged are the only ones that production
code constructs.
A Fragmentainer tracks size, used_block_size, and is_fixed_size.
remaining_space() returns f32::MAX for non-fixed (continuous) and
(size.height - used).max(0.0) for fixed (pages). advance() creates the
next fragmentainer; for Continuous it's a no-op (containers grow), for
Regions it returns Err if no more regions exist.
FragmentationState
The simpler per-layout-pass tracker for paged mode. Doesn't own
fragmentainers; just tracks current_page, current_page_y,
available_height, and page_content_height.
pub struct FragmentationState {
pub current_page: usize,
pub current_page_y: f32,
pub available_height: f32,
pub page_content_height: f32,
pub margins_top: f32,
pub margins_bottom: f32,
pub total_pages: usize,
}
Helpers: can_fit(height), would_fit_on_empty_page(height),
use_space(height), advance_page(), page_for_y(y) -> usize,
page_y_offset(page) -> f32.
page_for_y is the splitter: given the absolute Y position of a display-list
item, it computes which page the item belongs on.
layout_document_paged uses this to build per-page display lists from
the single tall layout pass.
Driving paged layout
#[cfg(feature = "text_layout")]
pub fn layout_document_paged<T, F>(
cache: &mut LayoutCache,
text_cache: &mut TextLayoutCache,
fragmentation_context: FragmentationContext,
new_dom: &StyledDom,
viewport: LogicalRect,
font_manager: &mut FontManager<T>,
scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
gpu_value_cache: Option<&GpuValueCache>,
renderer_resources: &RendererResources,
id_namespace: IdNamespace,
dom_id: DomId,
font_loader: F,
image_cache: &ImageCache,
get_system_time_fn: GetSystemTimeCallback,
) -> Result<Vec<DisplayList>>
where
T: ParsedFontTrait + Sync + 'static,
F: Fn(Arc<FontBytes>, usize) -> std::result::Result<T, LayoutError>;
Returns one DisplayList per page. Internally:
- Run normal
layout_documentagainst aviewportwhose height isf32::MAX(the infinite canvas). - Compute
page_sizeand per-page geometry fromFragmentationContext::Paged.page_sizeplusFakePageConfig. - Walk
display_list.itemsand split bypage_for_y(item.bounds.origin.y). Y coordinates are converted to page-relative by subtractingpage_y_offset(page). - For each page, append header/footer items from
FakePageConfig.
FakePageConfig and page templates
FakePageConfig is the programmatic substitute for unparsed @page rules.
It configures:
- Page size, margins, header height, footer height.
- Per-slot dynamic content via
PageSlotContent(Text, PageNumber, PageOfTotal, RunningHeader, Dynamic closure). - Six slot positions (
PageSlotPosition::TopLeft,TopCenter,TopRight,BottomLeft,BottomCenter,BottomRight). - Optional left/right (verso/recto) overrides via
PageTemplate::slots_for_page(page_number), which selects betweenslots,left_page_slots, andright_page_slots. header_on_first_page/footer_on_first_pagetoggles for cover-page styling.
PageNumberStyle covers Decimal, LowerRoman, UpperRoman,
LowerAlpha, UpperAlpha. PageCounter::format_page_number(style)
produces the string; format_page_of_total() renders „Page X of Y“.
PageSlotContent::Dynamic(Arc<DynamicSlotContentFn>) lets a caller produce
per-page content from a closure:
let func = DynamicSlotContentFn::new(|counter| {
format!("Page {}", counter.page_number)
});
let content = PageSlotContent::Dynamic(Arc::new(func));
Send + Sync is required because the function is Arc-shared across
pages.
CSS break properties
From azul_css::props::layout::fragmentation, defined and parseable, but
only partially consumed:
break-beforeandbreak-afteracceptPageBreak. Honoured byBreakPoint::is_forced()infragmentation.rs. Not consumed by the paged splitter.break-insideacceptsBreakInside(AutoorAvoid). Honoured byFragmentationLayoutContext::break_inside_avoid_depth(the counter increments on entry). Not consumed by the paged splitter.orphansaccepts au32(default 2). Defined but not enforced by Knuth-Plass.widowsaccepts au32(default 2). Defined but not enforced by Knuth-Plass.box-decoration-breakacceptsBoxDecorationBreak(SliceorClone). Defined but not honoured.
BoxBreakBehavior classifies a box's break behaviour:
pub enum BoxBreakBehavior {
Splittable {
min_before_break: f32,
min_after_break: f32,
},
KeepTogether {
estimated_height: f32,
priority: KeepTogetherPriority,
},
Monolithic {
height: f32,
},
}
KeepTogetherPriority: Low | Normal | High | Critical. Headers with
following content are High, figures with captions are Critical. The
original design used these to drive break decisions during layout; the
active path doesn't read them yet.
FragmentationDefaults
The „smart“ defaults that the integrated splitter would apply when CSS doesn't dictate otherwise:
pub struct FragmentationDefaults {
pub keep_headers_with_content: bool,
pub min_paragraph_lines: u32,
pub keep_figures_together: bool,
pub keep_table_headers: bool,
pub keep_list_markers: bool,
pub small_block_threshold_lines: u32,
pub default_orphans: u32,
pub default_widows: u32,
}
These are exposed but unused by paged_layout.rs. Implementing them
requires switching to integrated splitting (the original design) or layering
them on top of the post-hoc splitter.
Page templates and verso/recto
PageTemplate::slots_for_page(page_number) picks the slot list:
let override_slots = if page_number % 2 == 0 {
self.left_page_slots.as_deref() // verso (even)
} else {
self.right_page_slots.as_deref() // recto (odd)
};
override_slots.unwrap_or(&self.slots)
FragmentationLayoutContext::advance_to_left_page and
advance_to_right_page insert blank pages as needed to land on an even or
odd page (chapter-start convention in print typography).
Inline content flow across fragments
The text engine's stage-5 line breaker accepts flow_chain: &[LayoutFragment]. A BreakCursor records where one fragment stopped; the
next fragment continues from there. This is how text would flow across
columns or pages without re-shaping. Today only the paged splitter uses it
indirectly (one big fragment per layout pass, then split by Y); column
layout is not wired up. See Inline Text for the
fragment-aware text pipeline.
Activating fragmentation in LayoutWindow
LayoutWindow::new_paged(fc_cache, page_size) (behind feature = "pdf")
constructs a window with fragmentation_context: FragmentationContext::new_paged(page_size). The paged layout path then
calls layout_document_paged instead of layout_document. The screen path
(new and new_with_shared_fonts) uses FragmentationContext::new_continuous(800.0),
which makes paged_layout.rs a no-op (one page, infinite height).
Toward integrated splitting
The original design (layout/src/fragmentation.rs) called for break
decisions made during layout: as content is laid out, check
can_fit(height), apply break-before/break-after rules, defer or split
KeepTogether blocks, enforce orphans/widows. The implementation in
solver3/paged_layout.rs lays content out continuously and splits
afterwards, which is simpler but cannot honour break-inside: avoid or
orphans/widows correctly. The original design proposed BreakDecision and
BreakPoint::is_allowed() to drive integrated splitting; those types
remain public and re-exported but no caller consumes them in the paged
path.
If a contributor wants integrated splitting, the path forward is:
- Wire
FragmentationContextintoLayoutContext(already done —LayoutContext::fragmentation_context: Option<&'a mut FragmentationContext>). - In
layout_bfc, before placing each child, checkctx.fragmentation_contextand callcan_fit(child_height). If false, advance the fragmentainer, leave a gap, and re-issue the layout with the child placed at the new page Y. - In
layout_ifc, plumb the fragment list through totext_cache.layout_flow(flow_chain: &[LayoutFragment])so Knuth–Plass produces fragment-aware breaks.
Today neither step is done; LayoutContext.fragmentation_context is always
None in production calls.
Coming Up Next
- Inline Text — text3, shaping, line breaking, BiDi, hyphenation
- Layout — solver3, formatting contexts, the per-frame relayout cycle
- Text Pipeline — font discovery, parsing, fallback chains