
# Fragmentation

## Overview

CSS Fragmentation Module Level 3
([css-break-3](https://www.w3.org/TR/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". Defines `FragmentationLayoutContext`,
  `BoxBreakBehavior`, and `BreakPoint`.
- **`layout/src/paged.rs`.** Active container model. The
  `FragmentationContext` enum has `Continuous`, `Paged`, `MultiColumn`, and
  `Regions` variants. Defines `Fragmentainer` and `FragmentationState`.
- **`layout/src/solver3/pagination.rs`.** Active. `PageGeometer` is the
  infinite-canvas coordinate model. `FakePageConfig` handles header and
  footer config without `@page` parsing.
- **`layout/src/solver3/paged_layout.rs`.** Active.
  `layout_document_paged` drives 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:

```text
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

```rust,ignore
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`.

```rust,ignore
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

```rust,ignore
#[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:

1. Run normal `layout_document` against a `viewport` whose height is
   `f32::MAX` (the infinite canvas).
2. Compute `page_size` and per-page geometry from
   `FragmentationContext::Paged.page_size` plus `FakePageConfig`.
3. Walk `display_list.items` and split by `page_for_y(item.bounds.origin.y)`.
   Y coordinates are converted to page-relative by subtracting
   `page_y_offset(page)`.
4. 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 between
  `slots`, `left_page_slots`, and `right_page_slots`.
- `header_on_first_page` / `footer_on_first_page` toggles 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:

```rust,ignore
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-before` and `break-after` accept `PageBreak`. Honoured by
  `BreakPoint::is_forced()` in `fragmentation.rs`. Not consumed by the
  paged splitter.
- `break-inside` accepts `BreakInside` (`Auto` or `Avoid`). Honoured by
  `FragmentationLayoutContext::break_inside_avoid_depth` (the counter
  increments on entry). Not consumed by the paged splitter.
- `orphans` accepts a `u32` (default 2). Defined but not enforced by
  Knuth-Plass.
- `widows` accepts a `u32` (default 2). Defined but not enforced by
  Knuth-Plass.
- `box-decoration-break` accepts `BoxDecorationBreak` (`Slice` or `Clone`).
  Defined but not honoured.

`BoxBreakBehavior` classifies a box's break behaviour:

```rust,ignore
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:

```rust,ignore
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:

```rust,ignore
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](inline-text.md) 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:

1. Wire `FragmentationContext` into `LayoutContext` (already done —
   `LayoutContext::fragmentation_context: Option<&'a mut FragmentationContext>`).
2. In `layout_bfc`, before placing each child, check
   `ctx.fragmentation_context` and call `can_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.
3. In `layout_ifc`, plumb the fragment list through to
   `text_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](inline-text.md) — text3, shaping, line breaking, BiDi, hyphenation
- [Layout](../layout.md) — solver3, formatting contexts, the per-frame relayout cycle
- [Text Pipeline](../rendering/text-pipeline.md) — font discovery, parsing, fallback chains
