use super::super::draw_command::{DrawCommand, LayoutedPage};
use super::super::float;
use super::super::fragment::Fragment;
use super::super::header_footer::{HeaderFooterClearance, PageBodyBounds};
use super::super::page::PageConfig;
use super::super::paragraph::{
layout_paragraph, place_paragraph, ParagraphBorderStyle, ParagraphStyle,
};
use super::super::table::{
layout_table, layout_table_paginated_with_page_heights, measure_leading_table_group_height,
TablePaginationHeights,
};
use super::super::BoxConstraints;
use super::floating_table::{
plan_floating_table_pages_with_page_tops, resolve_floating_anchor, FloatingTableAnchor,
FloatingTablePagePlacement,
};
use super::helpers::{
render_page_footnotes, split_at_column_breaks, split_at_page_breaks, table_x_offset,
};
use super::types::{
ContinuationState, FloatingImage, FloatingImageY, FloatingShape, LayoutBlock, WrapMode,
};
use super::FLOAT_DEDUP_EPSILON_PT;
use super::FOOTNOTE_SEPARATOR_GAP;
use crate::model::StyleId;
use crate::render::dimension::Pt;
use crate::render::geometry::PtRect;
struct LayoutCtx<'cx> {
config: &'cx PageConfig,
clearance: &'cx HeaderFooterClearance,
measure_text: super::super::paragraph::MeasureTextFn<'cx>,
separator_indent: Pt,
default_line_height: Pt,
}
impl LayoutCtx<'_> {
fn page_bounds(&self, section_page_index: usize) -> PageBodyBounds {
self.clearance.for_page(section_page_index)
}
}
struct PageLayoutState<'doc> {
pages: Vec<LayoutedPage>,
current_page: LayoutedPage,
cursor_y: Pt,
page_index: usize,
page_top: Pt,
current_col: usize,
column_top: Pt,
bottom: Pt,
page_footnotes: Vec<(
&'doc [super::super::fragment::Fragment],
&'doc ParagraphStyle,
)>,
first_on_section_page: bool,
prev_space_after: Pt,
prev_style_id: Option<StyleId>,
prev_borders: Option<ParagraphBorderStyle>,
page_floats: Vec<float::ActiveFloat>,
current_page_abs_floats: Vec<float::ActiveFloat>,
abs_floats_dirty: bool,
page_start_block: usize,
last_para_start_y: Pt,
prev_table_style_id: Option<StyleId>,
pending_page_break: bool,
}
impl<'doc> PageLayoutState<'doc> {
fn new(
config: &PageConfig,
continuation: Option<ContinuationState>,
bounds: PageBodyBounds,
) -> Self {
let (current_page, cursor_y) = match continuation {
Some(c) => (c.page, c.cursor_y),
None => (LayoutedPage::new(config.page_size), bounds.top),
};
PageLayoutState {
pages: Vec::new(),
column_top: cursor_y,
last_para_start_y: cursor_y,
current_page,
cursor_y,
page_index: 0,
page_top: bounds.top,
current_col: 0,
bottom: bounds.bottom,
page_footnotes: Vec::new(),
first_on_section_page: true,
prev_space_after: Pt::ZERO,
prev_style_id: None,
prev_borders: None,
page_floats: Vec::new(),
current_page_abs_floats: Vec::new(),
abs_floats_dirty: true,
page_start_block: 0,
prev_table_style_id: None,
pending_page_break: false,
}
}
fn flush_footnotes(&mut self, ctx: &LayoutCtx<'_>) {
if !self.page_footnotes.is_empty() {
render_page_footnotes(
&mut self.current_page,
ctx.config,
&self.page_footnotes,
ctx.default_line_height,
ctx.measure_text,
ctx.separator_indent,
ctx.page_bounds(self.page_index).bottom,
);
self.page_footnotes.clear();
}
}
fn push_new_page(&mut self, block_idx: usize, ctx: &LayoutCtx<'_>) {
self.flush_footnotes(ctx);
self.pages.push(std::mem::replace(
&mut self.current_page,
LayoutedPage::new(ctx.config.page_size),
));
self.page_index += 1;
let bounds = ctx.page_bounds(self.page_index);
self.page_top = bounds.top;
self.cursor_y = bounds.top;
self.column_top = bounds.top;
self.current_col = 0;
self.bottom = bounds.bottom;
self.page_start_block = block_idx;
self.abs_floats_dirty = true;
self.page_floats.clear();
}
fn finalize(mut self, ctx: &LayoutCtx<'_>) -> Vec<LayoutedPage> {
self.flush_footnotes(ctx);
self.pages.push(self.current_page);
self.pages
}
fn effective_floats_at_cursor(
&mut self,
blocks: &[LayoutBlock],
relocated_absolute_float_blocks: &std::collections::HashSet<usize>,
num_cols: usize,
space_before: Pt,
col_width: Pt,
) -> Vec<float::ActiveFloat> {
if self.abs_floats_dirty {
self.current_page_abs_floats.clear();
let mut scan_col = self.current_col;
for (fi_idx, future_block) in blocks[self.page_start_block..].iter().enumerate() {
let future_block_idx = self.page_start_block + fi_idx;
if relocated_absolute_float_blocks.contains(&future_block_idx) {
if fi_idx == 0 {
continue;
}
break;
}
if let LayoutBlock::Paragraph {
floating_images: fi_list,
page_break_before,
fragments,
..
} = future_block
{
if *page_break_before && fi_idx > 0 {
break;
}
let boundary = scan_inline_page_boundary(fragments, &mut scan_col, num_cols);
if boundary != ForwardScanBoundary::BeforeParagraphFloat {
for fi in fi_list {
if fi.is_wrap_top_and_bottom() {
continue; }
if let FloatingImageY::Absolute(img_y) = fi.y {
self.current_page_abs_floats.push(float::ActiveFloat {
page_x: fi.x - fi.dist_left,
page_y_start: img_y,
page_y_end: img_y + fi.size.height,
width: fi.size.width + fi.dist_left + fi.dist_right,
source: float::FloatSource::Image,
wrap_text: fi.wrap_mode.wrap_text().into(),
});
}
}
}
if boundary != ForwardScanBoundary::None {
break;
}
}
}
self.abs_floats_dirty = false;
}
let mut effective_floats = self.page_floats.clone();
let y_threshold = self.cursor_y + space_before;
let deduped: Vec<float::ActiveFloat> = self
.current_page_abs_floats
.iter()
.filter(|af| af.page_y_start <= y_threshold)
.filter(|af| {
!effective_floats.iter().any(|pf| {
(pf.page_x - af.page_x).raw().abs() < FLOAT_DEDUP_EPSILON_PT
&& (pf.page_y_start - af.page_y_start).raw().abs() < FLOAT_DEDUP_EPSILON_PT
&& (pf.page_y_end - af.page_y_end).raw().abs() < FLOAT_DEDUP_EPSILON_PT
})
})
.cloned()
.collect();
effective_floats.extend(deduped);
for ef in &effective_floats {
if ef.overlaps_y(self.cursor_y) && ef.width >= col_width {
self.cursor_y = self.cursor_y.max(ef.page_y_end);
}
}
float::prune_floats(&mut effective_floats, self.cursor_y);
effective_floats
}
}
struct ParagraphFloatCheckpoint {
command_count: usize,
page_floats: Vec<float::ActiveFloat>,
cursor_y: Pt,
}
struct PageReplayCheckpoint<'doc> {
current_page: LayoutedPage,
cursor_y: Pt,
page_index: usize,
page_top: Pt,
current_col: usize,
column_top: Pt,
bottom: Pt,
page_footnotes: Vec<(
&'doc [super::super::fragment::Fragment],
&'doc ParagraphStyle,
)>,
first_on_section_page: bool,
prev_space_after: Pt,
prev_style_id: Option<StyleId>,
prev_borders: Option<ParagraphBorderStyle>,
page_floats: Vec<float::ActiveFloat>,
current_page_abs_floats: Vec<float::ActiveFloat>,
abs_floats_dirty: bool,
page_start_block: usize,
last_para_start_y: Pt,
prev_table_style_id: Option<StyleId>,
pending_page_break: bool,
}
impl<'doc> PageReplayCheckpoint<'doc> {
fn capture(state: &PageLayoutState<'doc>) -> Self {
Self {
current_page: state.current_page.clone(),
cursor_y: state.cursor_y,
page_index: state.page_index,
page_top: state.page_top,
current_col: state.current_col,
column_top: state.column_top,
bottom: state.bottom,
page_footnotes: state.page_footnotes.clone(),
first_on_section_page: state.first_on_section_page,
prev_space_after: state.prev_space_after,
prev_style_id: state.prev_style_id.clone(),
prev_borders: state.prev_borders.clone(),
page_floats: state.page_floats.clone(),
current_page_abs_floats: state.current_page_abs_floats.clone(),
abs_floats_dirty: state.abs_floats_dirty,
page_start_block: state.page_start_block,
last_para_start_y: state.last_para_start_y,
prev_table_style_id: state.prev_table_style_id.clone(),
pending_page_break: state.pending_page_break,
}
}
fn restore(&self, state: &mut PageLayoutState<'doc>) {
state.current_page.clone_from(&self.current_page);
state.cursor_y = self.cursor_y;
state.page_index = self.page_index;
state.page_top = self.page_top;
state.current_col = self.current_col;
state.column_top = self.column_top;
state.bottom = self.bottom;
state.page_footnotes.clone_from(&self.page_footnotes);
state.first_on_section_page = self.first_on_section_page;
state.prev_space_after = self.prev_space_after;
state.prev_style_id.clone_from(&self.prev_style_id);
state.prev_borders.clone_from(&self.prev_borders);
state.page_floats.clone_from(&self.page_floats);
state
.current_page_abs_floats
.clone_from(&self.current_page_abs_floats);
state.abs_floats_dirty = self.abs_floats_dirty;
state.page_start_block = self.page_start_block;
state.last_para_start_y = self.last_para_start_y;
state
.prev_table_style_id
.clone_from(&self.prev_table_style_id);
state.pending_page_break = self.pending_page_break;
}
}
fn refresh_page_replay_checkpoint<'doc>(
state: &PageLayoutState<'doc>,
block_idx: usize,
checkpoint: &mut PageReplayCheckpoint<'doc>,
checkpoint_page_index: &mut usize,
replay_block_idx: &mut usize,
) {
if state.page_index != *checkpoint_page_index {
*checkpoint = PageReplayCheckpoint::capture(state);
*checkpoint_page_index = state.page_index;
*replay_block_idx = block_idx;
}
}
impl ParagraphFloatCheckpoint {
fn capture(state: &PageLayoutState<'_>) -> Self {
Self {
command_count: state.current_page.commands.len(),
page_floats: state.page_floats.clone(),
cursor_y: state.cursor_y,
}
}
fn restore(&self, state: &mut PageLayoutState<'_>) {
state.current_page.commands.truncate(self.command_count);
state.page_floats.clone_from(&self.page_floats);
state.cursor_y = self.cursor_y;
}
}
fn register_paragraph_floats(
state: &mut PageLayoutState<'_>,
floating_images: &[FloatingImage],
floating_shapes: &[FloatingShape],
content_top: Pt,
) {
for fi in floating_images {
let (y_start, y_end) = match fi.y {
FloatingImageY::RelativeToParagraph(offset) => {
(content_top + offset, content_top + offset + fi.size.height)
}
FloatingImageY::Absolute(img_y) => (img_y, img_y + fi.size.height),
};
if fi.is_wrap_top_and_bottom() {
let img_y = match fi.y {
FloatingImageY::Absolute(y) => y,
FloatingImageY::RelativeToParagraph(offset) => content_top + offset,
};
state.current_page.commands.push(DrawCommand::Image {
rect: PtRect::from_xywh(fi.x, img_y, fi.size.width, fi.size.height),
image_data: fi.image_data.clone(),
src_rect: fi.src_rect,
});
if y_end > state.cursor_y {
state.cursor_y = y_end;
}
} else {
let float_entry = float::ActiveFloat {
page_x: fi.x - fi.dist_left,
page_y_start: y_start,
page_y_end: y_end,
width: fi.size.width + fi.dist_left + fi.dist_right,
source: float::FloatSource::Image,
wrap_text: fi.wrap_mode.wrap_text().into(),
};
log::debug!(
"[layout] register image float: x={:.1} y={:.1}-{:.1} w={:.1}",
float_entry.page_x.raw(),
y_start.raw(),
y_end.raw(),
float_entry.width.raw()
);
state.page_floats.push(float_entry);
}
}
for fs in floating_shapes {
if matches!(fs.wrap_mode, WrapMode::None) {
continue;
}
let (y_start, y_end) = match fs.y {
FloatingImageY::RelativeToParagraph(offset) => {
(content_top + offset, content_top + offset + fs.size.height)
}
FloatingImageY::Absolute(y) => (y, y + fs.size.height),
};
if fs.is_wrap_top_and_bottom() {
let shape_y = match fs.y {
FloatingImageY::Absolute(y) => y,
FloatingImageY::RelativeToParagraph(offset) => content_top + offset,
};
state.current_page.commands.push(DrawCommand::Path {
origin: crate::render::geometry::PtOffset::new(fs.x, shape_y),
rotation: fs.rotation,
flip_h: fs.flip_h,
flip_v: fs.flip_v,
extent: fs.size,
paths: fs.paths.clone(),
fill: fs.fill.clone(),
stroke: fs.stroke.clone(),
effects: fs.effects.clone(),
});
if y_end > state.cursor_y {
state.cursor_y = y_end;
}
} else {
let float_entry = float::ActiveFloat {
page_x: fs.x - fs.dist_left,
page_y_start: y_start,
page_y_end: y_end,
width: fs.size.width + fs.dist_left + fs.dist_right,
source: float::FloatSource::Shape,
wrap_text: fs.wrap_mode.wrap_text().into(),
};
log::debug!(
"[layout] register shape float: x={:.1} y={:.1}-{:.1} w={:.1} mode={:?}",
float_entry.page_x.raw(),
y_start.raw(),
y_end.raw(),
float_entry.width.raw(),
fs.wrap_mode
);
state.page_floats.push(float_entry);
}
}
}
fn register_destination_paragraph_floats(
state: &mut PageLayoutState<'_>,
floating_images: &[FloatingImage],
floating_shapes: &[FloatingShape],
space_before: Pt,
col_width: Pt,
) {
let content_top = state.cursor_y + space_before;
register_paragraph_floats(state, floating_images, floating_shapes, content_top);
for active_float in &state.page_floats {
if active_float.overlaps_y(state.cursor_y) && active_float.width >= col_width {
state.cursor_y = state.cursor_y.max(active_float.page_y_end);
}
}
float::prune_floats(&mut state.page_floats, state.cursor_y);
}
fn has_absolute_wrap_float(floating_images: &[FloatingImage]) -> bool {
floating_images.iter().any(|image| {
matches!(image.y, FloatingImageY::Absolute(_)) && image.wrap_mode.registers_as_wrap_float()
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ForwardScanBoundary {
None,
BeforeParagraphFloat,
AfterParagraphFloat,
}
fn scan_inline_page_boundary(
fragments: &[super::super::fragment::Fragment],
current_col: &mut usize,
num_cols: usize,
) -> ForwardScanBoundary {
for (index, fragment) in fragments.iter().enumerate() {
match fragment {
super::super::fragment::Fragment::PageBreak { .. } => {
let has_following_content = fragments[index + 1..].iter().any(|fragment| {
!matches!(fragment, super::super::fragment::Fragment::PageBreak { .. })
});
return if has_following_content {
ForwardScanBoundary::BeforeParagraphFloat
} else {
ForwardScanBoundary::AfterParagraphFloat
};
}
super::super::fragment::Fragment::ColumnBreak => {
if *current_col + 1 < num_cols {
*current_col += 1;
} else {
*current_col = 0;
return ForwardScanBoundary::BeforeParagraphFloat;
}
}
_ => {}
}
}
ForwardScanBoundary::None
}
fn mark_absolute_float_relocation(
paragraph_content_placed: bool,
moves_to_new_page: bool,
block_idx: usize,
replay_block_idx: usize,
floating_images: &[FloatingImage],
relocated_blocks: &mut std::collections::HashSet<usize>,
) -> bool {
!paragraph_content_placed
&& moves_to_new_page
&& block_idx > replay_block_idx
&& has_absolute_wrap_float(floating_images)
&& relocated_blocks.insert(block_idx)
}
fn paragraph_keep_next(block: &LayoutBlock) -> bool {
matches!(block, LayoutBlock::Paragraph { style, .. } if style.keep_next)
}
fn starts_keep_next_chain(blocks: &[LayoutBlock], block_idx: usize) -> bool {
paragraph_keep_next(&blocks[block_idx])
&& (block_idx == 0
|| matches!(
blocks[block_idx],
LayoutBlock::Paragraph {
page_break_before: true,
..
}
)
|| !paragraph_keep_next(&blocks[block_idx - 1]))
}
fn keep_next_terminal_table(blocks: &[LayoutBlock], start: usize) -> Option<&LayoutBlock> {
let mut index = start;
while let Some(block) = blocks.get(index) {
match block {
LayoutBlock::Paragraph {
style,
page_break_before,
..
} => {
if index > start && *page_break_before {
return None;
}
if !style.keep_next {
return None;
}
index += 1;
}
LayoutBlock::Table {
float_info: None, ..
} => return Some(block),
LayoutBlock::Table {
float_info: Some(_),
..
} => return None,
}
}
None
}
fn fresh_page_contextual_space_before(
block: &LayoutBlock,
previous_style_id: &Option<StyleId>,
) -> Pt {
let LayoutBlock::Paragraph { style, .. } = block else {
return Pt::ZERO;
};
let effective = style.clone_for_layout();
if effective.contextual_spacing
&& effective.style_id.is_some()
&& effective.style_id.as_ref() == previous_style_id.as_ref()
{
effective.space_before
} else {
Pt::ZERO
}
}
struct KeepNextGroupMeasurement {
body_height: Pt,
footnote_height: Pt,
has_footnotes: bool,
}
impl KeepNextGroupMeasurement {
fn total_height(&self, separator_already_reserved: bool) -> Pt {
self.body_height
+ self.footnote_height
+ if self.has_footnotes && !separator_already_reserved {
FOOTNOTE_SEPARATOR_GAP
} else {
Pt::ZERO
}
}
}
fn measure_keep_next_group(
blocks: &[LayoutBlock],
start: usize,
constraints: &BoxConstraints,
default_line_height: Pt,
measure_text: super::super::paragraph::MeasureTextFn<'_>,
) -> Option<KeepNextGroupMeasurement> {
let mut measurement = KeepNextGroupMeasurement {
body_height: Pt::ZERO,
footnote_height: Pt::ZERO,
has_footnotes: false,
};
let mut previous_space_after = Pt::ZERO;
let mut previous_style_id = None;
let mut index = start;
while let Some(block) = blocks.get(index) {
match block {
LayoutBlock::Paragraph {
fragments,
style,
page_break_before,
footnotes,
..
} => {
if index > start && *page_break_before {
return None;
}
let effective = style.clone_for_layout();
let collapsed = if effective.contextual_spacing
&& effective.style_id.is_some()
&& effective.style_id == previous_style_id
{
previous_space_after + effective.space_before
} else {
previous_space_after.min(effective.space_before)
};
let layout = layout_paragraph(
fragments,
constraints,
&effective,
default_line_height,
measure_text,
);
measurement.body_height += layout.size.height - collapsed;
for (footnote_fragments, footnote_style) in footnotes {
let footnote = layout_paragraph(
footnote_fragments,
&BoxConstraints::tight_width(constraints.max_width, Pt::INFINITY),
footnote_style,
default_line_height,
measure_text,
);
measurement.footnote_height += footnote.size.height;
measurement.has_footnotes = true;
}
previous_space_after = effective.space_after;
previous_style_id = effective.style_id.clone();
if !effective.keep_next {
return Some(measurement);
}
index += 1;
}
LayoutBlock::Table { .. } => {
return Some(measurement);
}
}
}
None
}
fn leading_keep_next_paragraph_splittable(
block: &LayoutBlock,
constraints: &BoxConstraints,
default_line_height: Pt,
measure_text: super::super::paragraph::MeasureTextFn<'_>,
) -> bool {
let LayoutBlock::Paragraph {
fragments,
style,
floating_images,
floating_shapes,
..
} = block
else {
return false;
};
let effective = style.clone_for_layout();
if effective.keep_lines || !floating_images.is_empty() || !floating_shapes.is_empty() {
return false;
}
let placed = place_paragraph(
fragments,
constraints,
&effective,
default_line_height,
measure_text,
);
let min_lines = if effective.widow_control { 4 } else { 2 };
placed.line_count() >= min_lines
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParagraphSplit {
All,
Break { head: usize },
MoveWhole,
}
fn decide_paragraph_split(
n_fit: usize,
total: usize,
widow_control: bool,
at_page_top: bool,
) -> ParagraphSplit {
debug_assert!(total >= 1, "a paragraph has at least one line");
if n_fit >= total {
return ParagraphSplit::All;
}
let head = if widow_control {
let capped = n_fit.min(total.saturating_sub(2));
if capped >= 2 {
capped
} else {
0 }
} else {
n_fit
};
if head >= 1 {
return ParagraphSplit::Break { head };
}
if at_page_top {
ParagraphSplit::All
} else {
ParagraphSplit::MoveWhole
}
}
fn reserve_footnotes<'doc>(
state: &mut PageLayoutState<'doc>,
ctx: &LayoutCtx<'_>,
content_width: Pt,
footnotes: &'doc [(Vec<Fragment>, ParagraphStyle)],
) {
if footnotes.is_empty() {
return;
}
let fn_constraints = BoxConstraints::tight_width(content_width, Pt::INFINITY);
for (fn_frags, fn_style) in footnotes {
let fn_para = layout_paragraph(
fn_frags,
&fn_constraints,
fn_style,
ctx.default_line_height,
ctx.measure_text,
);
if state.page_footnotes.is_empty() {
state.bottom -= FOOTNOTE_SEPARATOR_GAP;
}
state.bottom -= fn_para.size.height;
state.page_footnotes.push((fn_frags, fn_style));
}
}
fn advance_column_or_page(
state: &mut PageLayoutState<'_>,
block_idx: usize,
ctx: &LayoutCtx<'_>,
num_cols: usize,
para_start_y: &mut Pt,
) {
if state.current_col + 1 < num_cols {
state.current_col += 1;
state.cursor_y = state.column_top;
} else {
state.push_new_page(block_idx, ctx);
}
*para_start_y = state.cursor_y;
}
#[allow(clippy::too_many_arguments)] fn emit_split_paragraph<'doc>(
state: &mut PageLayoutState<'doc>,
fragments: &[Fragment],
style: &ParagraphStyle,
block_idx: usize,
config: &PageConfig,
ctx: &LayoutCtx<'_>,
para_start_y: &mut Pt,
footnotes: &'doc [(Vec<Fragment>, ParagraphStyle)],
content_width: Pt,
blocks: &[LayoutBlock],
relocated_absolute_float_blocks: &std::collections::HashSet<usize>,
) {
let num_cols = config.num_columns();
let col_x = |col: usize| config.margins.left + config.columns[col].x_offset;
let widow_control = style.widow_control;
let mut remaining: Vec<Fragment> = fragments.to_vec();
let mut cont_style: ParagraphStyle = style.clone();
let mut first_segment = true;
let mut footnote_cursor = 0;
loop {
let col_width = config.columns[state.current_col].width;
let page_height = (state.bottom - state.page_top).max(Pt::ZERO);
let constraints = BoxConstraints::new(Pt::ZERO, col_width, Pt::ZERO, page_height);
let placed = place_paragraph(
&remaining,
&constraints,
&cont_style,
ctx.default_line_height,
ctx.measure_text,
);
let total = placed.line_count();
if total == 0 {
break;
}
let at_page_top = state.cursor_y <= state.column_top;
let avail = (state.bottom - state.cursor_y).max(Pt::ZERO);
let trailing_extra = cont_style.space_after + placed.bottom_border_space();
let mut used = if first_segment {
cont_style.space_before
} else {
Pt::ZERO
};
let mut n_fit = 0;
for i in 0..total {
let mut needed = used + placed.line_height(i);
if i + 1 == total {
needed += trailing_extra;
}
if needed > avail {
break;
}
used += placed.line_height(i);
n_fit += 1;
}
let head = match decide_paragraph_split(n_fit, total, widow_control, at_page_top) {
ParagraphSplit::All => Some(total),
ParagraphSplit::Break { head } => Some(head),
ParagraphSplit::MoveWhole => None,
}
.and_then(|head| {
prefix_adjusted_head(
head,
total,
placed.unbreakable_prefix_lines(),
first_segment,
at_page_top,
)
});
let Some(head) = head else {
drop(placed);
advance_column_or_page(state, block_idx, ctx, num_cols, para_start_y);
cont_style = continuation_style(
state,
blocks,
style,
config,
relocated_absolute_float_blocks,
first_segment,
);
continue;
};
let is_last = head == total;
let segment = placed.emit_split_segment(0, head, first_segment, is_last);
if !(first_segment && is_last) {
log::debug!(
"[layout] paragraph split: {head}/{total} lines at y={:.1}",
state.cursor_y.raw()
);
}
let segment_x = col_x(state.current_col);
for mut cmd in segment.commands {
cmd.shift_y(state.cursor_y);
cmd.shift_x(segment_x);
state.current_page.commands.push(cmd);
}
state.cursor_y += segment.size.height;
if !footnotes.is_empty() {
let refs = placed.footnote_refs_in(0, head);
let end = (footnote_cursor + refs).min(footnotes.len());
reserve_footnotes(state, ctx, content_width, &footnotes[footnote_cursor..end]);
footnote_cursor = end;
}
if is_last {
break;
}
let next_remaining = placed.fragments_from_line(head);
drop(placed);
first_segment = false;
advance_column_or_page(state, block_idx, ctx, num_cols, para_start_y);
cont_style = continuation_style(
state,
blocks,
style,
config,
relocated_absolute_float_blocks,
first_segment,
);
remaining = next_remaining;
}
}
fn continuation_style(
state: &mut PageLayoutState<'_>,
blocks: &[LayoutBlock],
style: &ParagraphStyle,
config: &PageConfig,
relocated_absolute_float_blocks: &std::collections::HashSet<usize>,
first_segment: bool,
) -> ParagraphStyle {
let col = state.current_col;
let col_width = config.columns[col].width;
let page_x = config.margins.left + config.columns[col].x_offset;
let space_before = if first_segment {
style.space_before
} else {
Pt::ZERO
};
let floats = state.effective_floats_at_cursor(
blocks,
relocated_absolute_float_blocks,
config.num_columns(),
space_before,
col_width,
);
let mut cs = style.clone();
if !first_segment {
cs.drop_cap = None;
cs.indent_first_line = Pt::ZERO;
cs.space_before = Pt::ZERO;
}
cs.page_floats = floats;
cs.page_y = state.cursor_y;
cs.page_x = page_x;
cs.page_content_width = col_width;
cs
}
fn prefix_adjusted_head(
head: usize,
remaining: usize,
prefix_lines: usize,
first_segment: bool,
at_page_top: bool,
) -> Option<usize> {
let breaks_prefix =
first_segment && prefix_lines > 1 && head < prefix_lines && head < remaining;
if !breaks_prefix {
return Some(head);
}
if at_page_top {
Some(prefix_lines.min(remaining))
} else {
None
}
}
pub fn layout_section(
blocks: &[LayoutBlock],
config: &PageConfig,
measure_text: super::super::paragraph::MeasureTextFn<'_>,
separator_indent: Pt,
default_line_height: Pt,
continuation: Option<ContinuationState>,
) -> Vec<LayoutedPage> {
let clearance = HeaderFooterClearance::uniform(config);
layout_section_with_clearance(
blocks,
config,
measure_text,
separator_indent,
default_line_height,
continuation,
&clearance,
)
}
pub(crate) fn layout_section_with_clearance(
blocks: &[LayoutBlock],
config: &PageConfig,
measure_text: super::super::paragraph::MeasureTextFn<'_>,
separator_indent: Pt,
default_line_height: Pt,
continuation: Option<ContinuationState>,
clearance: &HeaderFooterClearance,
) -> Vec<LayoutedPage> {
let content_width = config.content_width();
let num_cols = config.num_columns();
let ctx = LayoutCtx {
config,
clearance,
measure_text,
separator_indent,
default_line_height,
};
let mut state = PageLayoutState::new(config, continuation, clearance.for_page(0));
let col_constraints = |col: usize, page_height: Pt| -> BoxConstraints {
let col_width = config.columns[col].width;
BoxConstraints::new(Pt::ZERO, col_width, Pt::ZERO, page_height)
};
let col_x = |col: usize| -> Pt { config.margins.left + config.columns[col].x_offset };
let mut relocated_absolute_float_blocks = std::collections::HashSet::new();
let mut page_replay_state = PageReplayCheckpoint::capture(&state);
let mut checkpoint_page_index = state.page_index;
let mut replay_block_idx = 0;
let mut block_idx = 0;
'blocks: while block_idx < blocks.len() {
let block = &blocks[block_idx];
if state.pending_page_break {
state.pending_page_break = false;
if state.cursor_y > state.page_top {
state.push_new_page(block_idx, &ctx);
state.prev_space_after = Pt::ZERO;
}
}
refresh_page_replay_checkpoint(
&state,
block_idx,
&mut page_replay_state,
&mut checkpoint_page_index,
&mut replay_block_idx,
);
match block {
LayoutBlock::Paragraph {
fragments,
style,
page_break_before,
footnotes,
floating_images,
floating_shapes,
} => {
if *page_break_before && state.cursor_y > state.page_top {
state.push_new_page(block_idx, &ctx);
state.prev_space_after = Pt::ZERO;
}
if num_cols == 1
&& starts_keep_next_chain(blocks, block_idx)
&& state.page_floats.is_empty()
{
let constraints = col_constraints(
state.current_col,
(state.bottom - state.page_top).max(Pt::ZERO),
);
if let Some(group) = measure_keep_next_group(
blocks,
block_idx,
&constraints,
ctx.default_line_height,
ctx.measure_text,
) {
let current_group_height =
group.total_height(!state.page_footnotes.is_empty());
let current_group_top = match &blocks[block_idx] {
LayoutBlock::Paragraph { style, .. }
if style.contextual_spacing
&& style.style_id.is_some()
&& style.style_id == state.prev_style_id =>
{
state.cursor_y - state.prev_space_after - style.space_before
}
LayoutBlock::Paragraph { style, .. } => {
state.cursor_y - state.prev_space_after.min(style.space_before)
}
LayoutBlock::Table { .. } => state.cursor_y,
};
let full_page_height = ctx.page_bounds(state.page_index + 1).height();
let fresh_page_group_height = group.total_height(false)
- fresh_page_contextual_space_before(
&blocks[block_idx],
&state.prev_style_id,
);
let should_move = match keep_next_terminal_table(blocks, block_idx) {
Some(LayoutBlock::Table {
rows,
col_widths,
border_config,
..
}) if !rows.is_empty() => {
let current_available =
state.bottom - current_group_top - current_group_height;
let full_page_available =
full_page_height - fresh_page_group_height;
let leading_group_height = measure_leading_table_group_height(
rows,
col_widths,
ctx.default_line_height,
border_config.as_ref(),
ctx.measure_text,
false,
);
leading_group_height.is_some_and(|height| {
height <= full_page_available && height > current_available
})
}
_ => {
fresh_page_group_height <= full_page_height
&& current_group_top + current_group_height > state.bottom
&& !leading_keep_next_paragraph_splittable(
&blocks[block_idx],
&constraints,
ctx.default_line_height,
ctx.measure_text,
)
}
};
if should_move && state.cursor_y > state.column_top {
state.push_new_page(block_idx, &ctx);
state.prev_space_after = Pt::ZERO;
}
}
}
refresh_page_replay_checkpoint(
&state,
block_idx,
&mut page_replay_state,
&mut checkpoint_page_index,
&mut replay_block_idx,
);
let mut effective_style = style.clone_for_layout();
let first_text = fragments
.iter()
.find_map(|f| {
if let super::super::fragment::Fragment::Text { text, .. } = f {
Some(&**text)
} else {
None
}
})
.unwrap_or("");
log::debug!(
"[layout] block[{block_idx}] para style={:?} text={:?} cursor_y={:.1} col={} floats={} fwd_floats={}",
effective_style.style_id, &first_text[..first_text.len().min(30)],
state.cursor_y.raw(), state.current_col,
state.page_floats.len(), state.current_page_abs_floats.len()
);
if state.cursor_y <= state.column_top && state.first_on_section_page {
effective_style.space_before = Pt::ZERO;
}
if effective_style.borders.is_some()
&& effective_style.borders == state.prev_borders
{
if let Some(ref mut b) = effective_style.borders {
b.top = None;
}
}
if effective_style.contextual_spacing
&& effective_style.style_id.is_some()
&& effective_style.style_id == state.prev_style_id
{
state.cursor_y -= state.prev_space_after + effective_style.space_before;
} else {
let collapse = state.prev_space_after.min(effective_style.space_before);
state.cursor_y -= collapse;
}
let float_checkpoint = ParagraphFloatCheckpoint::capture(&state);
let content_top = state.cursor_y + effective_style.space_before;
register_paragraph_floats(
&mut state,
floating_images,
floating_shapes,
content_top,
);
float::prune_floats(&mut state.page_floats, state.cursor_y);
let col_width = config.columns[state.current_col].width;
let page_x = col_x(state.current_col);
let effective_floats = state.effective_floats_at_cursor(
blocks,
&relocated_absolute_float_blocks,
num_cols,
effective_style.space_before,
col_width,
);
effective_style.page_floats = effective_floats;
effective_style.page_y = state.cursor_y;
effective_style.page_x = page_x;
effective_style.page_content_width = col_width;
let page_chunks = split_at_page_breaks(fragments);
let mut para_start_y = state.cursor_y;
state.last_para_start_y = state.cursor_y;
let mut paragraph_content_placed = false;
let mut footnotes_reserved = false;
let mut unresolved_page_break = false;
for (page_chunk_idx, page_chunk) in page_chunks.iter().enumerate() {
if page_chunk_idx > 0 {
unresolved_page_break = true;
}
if page_chunk.is_empty() {
continue;
}
if unresolved_page_break {
if mark_absolute_float_relocation(
paragraph_content_placed,
true,
block_idx,
replay_block_idx,
floating_images,
&mut relocated_absolute_float_blocks,
) {
page_replay_state.restore(&mut state);
block_idx = replay_block_idx;
continue 'blocks;
}
if !paragraph_content_placed {
float_checkpoint.restore(&mut state);
}
state.push_new_page(block_idx, &ctx);
state.prev_space_after = Pt::ZERO;
para_start_y = state.cursor_y;
if !paragraph_content_placed {
let col_width = config.columns[state.current_col].width;
register_destination_paragraph_floats(
&mut state,
floating_images,
floating_shapes,
effective_style.space_before,
col_width,
);
}
effective_style.page_y = state.cursor_y;
effective_style.page_x = col_x(state.current_col);
effective_style.page_content_width =
config.columns[state.current_col].width;
effective_style.page_floats = state.page_floats.clone();
}
let col_chunks = split_at_column_breaks(page_chunk);
for (chunk_idx, chunk) in col_chunks.iter().enumerate() {
if chunk_idx > 0 {
let starts_new_page = state.current_col + 1 >= num_cols;
if mark_absolute_float_relocation(
paragraph_content_placed,
starts_new_page,
block_idx,
replay_block_idx,
floating_images,
&mut relocated_absolute_float_blocks,
) {
page_replay_state.restore(&mut state);
block_idx = replay_block_idx;
continue 'blocks;
}
if !paragraph_content_placed && starts_new_page {
float_checkpoint.restore(&mut state);
}
if !starts_new_page {
state.current_col += 1;
} else {
state.push_new_page(block_idx, &ctx);
}
state.cursor_y = state.column_top;
if !paragraph_content_placed && starts_new_page {
let col_width = config.columns[state.current_col].width;
register_destination_paragraph_floats(
&mut state,
floating_images,
floating_shapes,
effective_style.space_before,
col_width,
);
}
effective_style.page_y = state.cursor_y;
effective_style.page_x = col_x(state.current_col);
effective_style.page_content_width =
config.columns[state.current_col].width;
if starts_new_page {
effective_style.page_floats = state.page_floats.clone();
}
}
let constraints = col_constraints(
state.current_col,
(state.bottom - state.page_top).max(Pt::ZERO),
);
let placed = place_paragraph(
chunk,
&constraints,
&effective_style,
ctx.default_line_height,
ctx.measure_text,
);
let single_chunk = page_chunks.len() == 1 && col_chunks.len() == 1;
let can_split = !effective_style.keep_lines
&& (footnotes.is_empty() || single_chunk)
&& floating_images.is_empty()
&& floating_shapes.is_empty()
&& placed.line_count() >= 2;
if can_split {
if !footnotes.is_empty() {
footnotes_reserved = true;
}
drop(placed);
emit_split_paragraph(
&mut state,
chunk,
&effective_style,
block_idx,
config,
&ctx,
&mut para_start_y,
footnotes,
content_width,
blocks,
&relocated_absolute_float_blocks,
);
} else {
let mut para = placed.emit_full();
if state.cursor_y + para.size.height > state.bottom
&& state.cursor_y > state.column_top
{
drop(placed);
let moves_to_new_page = state.current_col + 1 >= num_cols;
if mark_absolute_float_relocation(
paragraph_content_placed,
moves_to_new_page,
block_idx,
replay_block_idx,
floating_images,
&mut relocated_absolute_float_blocks,
) {
page_replay_state.restore(&mut state);
block_idx = replay_block_idx;
continue 'blocks;
}
if !paragraph_content_placed {
float_checkpoint.restore(&mut state);
}
if state.current_col + 1 < num_cols {
state.current_col += 1;
state.cursor_y = state.column_top;
} else {
state.push_new_page(block_idx, &ctx);
}
let destination_para_start_y = state.cursor_y;
if !paragraph_content_placed {
let col_width = config.columns[state.current_col].width;
register_destination_paragraph_floats(
&mut state,
floating_images,
floating_shapes,
effective_style.space_before,
col_width,
);
}
para_start_y = destination_para_start_y;
effective_style.page_y = state.cursor_y;
effective_style.page_x = col_x(state.current_col);
effective_style.page_content_width =
config.columns[state.current_col].width;
effective_style.page_floats = state.page_floats.clone();
let destination_constraints = col_constraints(
state.current_col,
(state.bottom - state.page_top).max(Pt::ZERO),
);
para = place_paragraph(
chunk,
&destination_constraints,
&effective_style,
ctx.default_line_height,
ctx.measure_text,
)
.emit_full();
}
log::debug!(
"[layout] page_chunk[{page_chunk_idx}] col_chunk[{chunk_idx}] placed at y={:.1} x={:.1} height={:.1}",
state.cursor_y.raw(),
col_x(state.current_col).raw(),
para.size.height.raw()
);
for mut cmd in para.commands {
cmd.shift_y(state.cursor_y);
cmd.shift_x(col_x(state.current_col));
state.current_page.commands.push(cmd);
}
state.cursor_y += para.size.height;
}
paragraph_content_placed |= !chunk.is_empty();
}
unresolved_page_break = false;
}
if unresolved_page_break {
state.pending_page_break = true;
}
state.first_on_section_page = false;
state.prev_borders = style.borders.clone();
state.prev_space_after = effective_style.space_after;
state.prev_style_id = effective_style.style_id.clone();
state.prev_table_style_id = None;
for fi in floating_images {
if fi.is_wrap_top_and_bottom() {
continue;
}
let img_y = match fi.y {
FloatingImageY::Absolute(y) => y,
FloatingImageY::RelativeToParagraph(offset) => {
para_start_y + effective_style.space_before + offset
}
};
state.current_page.commands.push(DrawCommand::Image {
rect: PtRect::from_xywh(fi.x, img_y, fi.size.width, fi.size.height),
image_data: fi.image_data.clone(),
src_rect: fi.src_rect,
});
}
for fs in floating_shapes {
if fs.is_wrap_top_and_bottom() {
continue;
}
let shape_y = match fs.y {
FloatingImageY::Absolute(y) => y,
FloatingImageY::RelativeToParagraph(offset) => {
para_start_y + effective_style.space_before + offset
}
};
state.current_page.commands.push(DrawCommand::Path {
origin: crate::render::geometry::PtOffset::new(fs.x, shape_y),
rotation: fs.rotation,
flip_h: fs.flip_h,
flip_v: fs.flip_v,
extent: fs.size,
paths: fs.paths.clone(),
fill: fs.fill.clone(),
stroke: fs.stroke.clone(),
effects: fs.effects.clone(),
});
}
if !footnotes_reserved {
reserve_footnotes(&mut state, &ctx, content_width, footnotes);
}
}
LayoutBlock::Table {
rows,
col_widths,
border_config,
indent,
alignment,
float_info,
style_id,
} => {
if let Some(fi) = float_info {
let table = layout_table(
rows,
col_widths,
&col_constraints(
state.current_col,
(state.bottom - state.page_top).max(Pt::ZERO),
),
ctx.default_line_height,
border_config.as_ref(),
ctx.measure_text,
false,
);
let table_x = table_x_offset(
*alignment,
*indent,
table.size.width,
content_width,
config.margins.left,
);
let table_x = match fi.x_align {
Some(crate::model::TableXAlign::Center) => {
config.margins.left + (content_width - table.size.width) * 0.5
}
Some(crate::model::TableXAlign::Right) => {
config.margins.left + content_width - table.size.width
}
_ => table_x,
};
if state.cursor_y + table.size.height > state.bottom
&& state.cursor_y > state.page_top
{
state.push_new_page(block_idx, &ctx);
state.prev_space_after = Pt::ZERO;
}
let float_y_start = loop {
let requested_y = if fi.y_offset > Pt::ZERO {
let anchor_y = match fi.vert_anchor {
crate::model::TableAnchor::Text => {
state.last_para_start_y + fi.y_offset
}
crate::model::TableAnchor::Margin => state.page_top + fi.y_offset,
crate::model::TableAnchor::Page => fi.y_offset,
};
anchor_y.max(state.cursor_y)
} else {
state.cursor_y
};
match resolve_floating_anchor(
requested_y,
table.size.height,
fi.overlap,
&state.page_floats,
state.bottom,
) {
FloatingTableAnchor::OnCurrentPage(y) => break y,
FloatingTableAnchor::Shifted { from, to } => {
log::debug!(
"[layout] shift float past prior (overlap=Never): {:.1} -> {:.1}",
from.raw(),
to.raw(),
);
break to;
}
FloatingTableAnchor::Spillover => {
log::debug!(
"[layout] float spill to next page (overlap=Never): block_idx={block_idx}",
);
state.push_new_page(block_idx, &ctx);
state.prev_space_after = Pt::ZERO;
}
}
};
state.prev_table_style_id = None;
let available_first = (state.bottom - float_y_start).max(Pt::ZERO);
let section_page_index = state.page_index;
let slices = layout_table_paginated_with_page_heights(
rows,
col_widths,
&col_constraints(
state.current_col,
(state.bottom - state.page_top).max(Pt::ZERO),
),
ctx.default_line_height,
border_config.as_ref(),
ctx.measure_text,
TablePaginationHeights {
available_height: available_first,
suppress_first_row_top: false,
page_height_for_slice: |slice_index| {
ctx.page_bounds(section_page_index + slice_index).height()
},
},
);
let plan = plan_floating_table_pages_with_page_tops(
slices,
float_y_start,
|slice_index| ctx.page_bounds(section_page_index + slice_index).top,
);
let table_width = table.size.width;
for (page_idx, placement) in plan.pages.into_iter().enumerate() {
if page_idx > 0 {
state.push_new_page(block_idx, &ctx);
state.prev_space_after = Pt::ZERO;
}
let (y_start, slice, is_anchor) = match placement {
FloatingTablePagePlacement::Anchor { y_start, slice } => {
(y_start, slice, true)
}
FloatingTablePagePlacement::Continuation { y_start, slice } => {
(y_start, slice, false)
}
};
let slice_height = slice.size.height;
for mut cmd in slice.commands {
cmd.shift_y(y_start);
cmd.shift_x(table_x);
state.current_page.commands.push(cmd);
}
log::debug!(
"[layout] register table float ({}): x={:.1} y={:.1}-{:.1} w={:.1} block_idx={block_idx}",
if is_anchor { "anchor" } else { "continuation" },
table_x.raw(),
y_start.raw(),
(y_start + slice_height).raw(),
(table_width + fi.right_gap).raw(),
);
state.page_floats.push(float::ActiveFloat {
page_x: table_x,
page_y_start: y_start,
page_y_end: y_start + slice_height,
width: table_width + fi.right_gap,
source: float::FloatSource::Table {
owner_block_idx: block_idx,
},
wrap_text: float::WrapTextSide::BothSides,
});
let _ = is_anchor;
}
block_idx += 1;
continue;
}
let suppress_top = style_id.is_some() && *style_id == state.prev_table_style_id;
let available = state.bottom - state.cursor_y;
let section_page_index = state.page_index;
let slices = layout_table_paginated_with_page_heights(
rows,
col_widths,
&col_constraints(
state.current_col,
(state.bottom - state.page_top).max(Pt::ZERO),
),
ctx.default_line_height,
border_config.as_ref(),
ctx.measure_text,
TablePaginationHeights {
available_height: available,
suppress_first_row_top: suppress_top,
page_height_for_slice: |slice_index| {
ctx.page_bounds(section_page_index + slice_index).height()
},
},
);
let table_width: Pt = col_widths.iter().copied().sum();
let table_x = table_x_offset(
*alignment,
*indent,
table_width,
content_width,
config.margins.left,
);
for (slice_idx, slice) in slices.into_iter().enumerate() {
if slice_idx > 0 {
state.push_new_page(block_idx, &ctx);
}
for mut cmd in slice.commands {
cmd.shift_y(state.cursor_y);
cmd.shift_x(table_x);
state.current_page.commands.push(cmd);
}
state.cursor_y += slice.size.height;
}
state.first_on_section_page = false;
state.prev_borders = None; state.prev_space_after = Pt::ZERO;
state.prev_style_id = None;
state.prev_table_style_id = style_id.clone();
}
}
block_idx += 1;
}
state.finalize(&ctx)
}
#[cfg(test)]
mod keep_next_chain_tests {
use super::*;
use crate::render::layout::paragraph::ParagraphStyle;
fn paragraph(keep_next: bool, page_break_before: bool) -> LayoutBlock {
LayoutBlock::Paragraph {
fragments: Vec::new(),
style: ParagraphStyle {
keep_next,
..Default::default()
},
page_break_before,
footnotes: Vec::new(),
floating_images: Vec::new(),
floating_shapes: Vec::new(),
}
}
#[test]
fn page_break_before_starts_a_new_keep_next_chain() {
let blocks = [paragraph(true, false), paragraph(true, true)];
assert!(starts_keep_next_chain(&blocks, 1));
}
}
#[cfg(test)]
mod paragraph_split_tests {
use super::{decide_paragraph_split, prefix_adjusted_head, ParagraphSplit};
#[test]
fn all_lines_fit_is_all() {
assert_eq!(
decide_paragraph_split(5, 5, true, false),
ParagraphSplit::All
);
assert_eq!(
decide_paragraph_split(9, 5, true, false),
ParagraphSplit::All
);
}
#[test]
fn widow_control_keeps_two_lines_on_each_side() {
assert_eq!(
decide_paragraph_split(4, 6, true, false),
ParagraphSplit::Break { head: 4 }
);
}
#[test]
fn widow_control_caps_head_to_leave_a_non_widow_tail() {
assert_eq!(
decide_paragraph_split(3, 4, true, false),
ParagraphSplit::Break { head: 2 }
);
}
#[test]
fn widow_control_rejects_single_orphan_line_and_moves_whole() {
assert_eq!(
decide_paragraph_split(1, 6, true, false),
ParagraphSplit::MoveWhole
);
}
#[test]
fn widow_control_three_line_paragraph_cannot_split() {
assert_eq!(
decide_paragraph_split(2, 3, true, false),
ParagraphSplit::MoveWhole
);
}
#[test]
fn widow_control_paragraph_taller_than_page_splits_two_by_two() {
assert_eq!(
decide_paragraph_split(4, 10, true, true),
ParagraphSplit::Break { head: 4 }
);
assert_eq!(
decide_paragraph_split(4, 6, true, true),
ParagraphSplit::Break { head: 4 }
);
assert_eq!(
decide_paragraph_split(4, 2, true, true),
ParagraphSplit::All
);
}
#[test]
fn without_widow_control_a_single_line_may_split() {
assert_eq!(
decide_paragraph_split(1, 6, false, false),
ParagraphSplit::Break { head: 1 }
);
assert_eq!(
decide_paragraph_split(3, 4, false, false),
ParagraphSplit::Break { head: 3 }
);
}
#[test]
fn without_widow_control_nothing_fits_moves_whole() {
assert_eq!(
decide_paragraph_split(0, 4, false, false),
ParagraphSplit::MoveWhole
);
}
#[test]
fn at_page_top_unsplittable_paragraph_overflows_whole() {
assert_eq!(
decide_paragraph_split(2, 3, true, true),
ParagraphSplit::All
);
}
#[test]
fn at_page_top_single_oversized_line_overflows_whole() {
assert_eq!(
decide_paragraph_split(0, 1, true, true),
ParagraphSplit::All
);
assert_eq!(
decide_paragraph_split(0, 3, false, true),
ParagraphSplit::All
);
}
#[test]
fn drop_cap_head_clearing_the_prefix_is_unchanged() {
assert_eq!(prefix_adjusted_head(5, 8, 3, true, true), Some(5));
assert_eq!(prefix_adjusted_head(2, 8, 0, true, false), Some(2));
assert_eq!(prefix_adjusted_head(1, 8, 1, true, false), Some(1));
assert_eq!(prefix_adjusted_head(1, 8, 3, false, false), Some(1));
assert_eq!(prefix_adjusted_head(2, 2, 3, true, false), Some(2));
}
#[test]
fn drop_cap_break_inside_prefix_moves_whole_when_not_at_top() {
assert_eq!(prefix_adjusted_head(2, 8, 3, true, false), None);
assert_eq!(prefix_adjusted_head(1, 8, 3, true, false), None);
}
#[test]
fn drop_cap_break_inside_prefix_overflows_whole_at_top() {
assert_eq!(prefix_adjusted_head(1, 8, 3, true, true), Some(3));
assert_eq!(prefix_adjusted_head(1, 2, 3, true, true), Some(2));
}
}