use crate::model::TableOverlap;
use crate::render::dimension::Pt;
use crate::render::layout::float::ActiveFloat;
use crate::render::layout::table::TableSlice;
#[derive(Debug, PartialEq)]
pub(super) enum FloatingTableAnchor {
OnCurrentPage(Pt),
Shifted { from: Pt, to: Pt },
Spillover,
}
pub(super) fn resolve_floating_anchor(
requested: Pt,
height: Pt,
overlap: Option<TableOverlap>,
prior: &[ActiveFloat],
page_bottom: Pt,
) -> FloatingTableAnchor {
if !matches!(overlap, Some(TableOverlap::Never)) {
return FloatingTableAnchor::OnCurrentPage(requested);
}
let mut anchor = requested;
let mut shifted_any = false;
loop {
let mut max_blocking_end: Option<Pt> = None;
for f in prior {
let overlaps = anchor < f.page_y_end && anchor + height > f.page_y_start;
if overlaps {
let candidate = f.page_y_end;
max_blocking_end = Some(match max_blocking_end {
Some(prev) if prev >= candidate => prev,
_ => candidate,
});
}
}
match max_blocking_end {
None => break,
Some(new_anchor) => {
anchor = new_anchor;
shifted_any = true;
}
}
}
if anchor + height > page_bottom {
FloatingTableAnchor::Spillover
} else if shifted_any {
FloatingTableAnchor::Shifted {
from: requested,
to: anchor,
}
} else {
FloatingTableAnchor::OnCurrentPage(anchor)
}
}
#[derive(Debug)]
pub(super) enum FloatingTablePagePlacement {
Anchor { y_start: Pt, slice: TableSlice },
Continuation { y_start: Pt, slice: TableSlice },
}
impl FloatingTablePagePlacement {
#[cfg(test)]
pub(super) fn y_start(&self) -> Pt {
match self {
Self::Anchor { y_start, .. } | Self::Continuation { y_start, .. } => *y_start,
}
}
#[cfg(test)]
pub(super) fn slice(&self) -> &TableSlice {
match self {
Self::Anchor { slice, .. } | Self::Continuation { slice, .. } => slice,
}
}
}
#[derive(Debug)]
pub(super) struct FloatingTablePlan {
pub(super) pages: Vec<FloatingTablePagePlacement>,
}
pub(super) fn plan_floating_table_pages(
slices: Vec<TableSlice>,
anchor_y: Pt,
continuation_y: Pt,
) -> FloatingTablePlan {
let pages = slices
.into_iter()
.enumerate()
.map(|(idx, slice)| {
if idx == 0 {
FloatingTablePagePlacement::Anchor {
y_start: anchor_y,
slice,
}
} else {
FloatingTablePagePlacement::Continuation {
y_start: continuation_y,
slice,
}
}
})
.collect();
FloatingTablePlan { pages }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::geometry::PtSize;
use crate::render::layout::float::{FloatSource, WrapTextSide};
fn slice(height: f32) -> TableSlice {
TableSlice {
commands: Vec::new(),
size: PtSize::new(Pt::new(100.0), Pt::new(height)),
}
}
fn float_at(y_start: f32, y_end: f32) -> ActiveFloat {
ActiveFloat {
page_x: Pt::ZERO,
page_y_start: Pt::new(y_start),
page_y_end: Pt::new(y_end),
width: Pt::new(100.0),
source: FloatSource::Table { owner_block_idx: 0 },
wrap_text: WrapTextSide::BothSides,
}
}
#[test]
fn plan_single_slice_is_one_anchor_placement() {
let plan = plan_floating_table_pages(vec![slice(50.0)], Pt::new(100.0), Pt::new(40.0));
assert_eq!(plan.pages.len(), 1);
assert!(matches!(
plan.pages[0],
FloatingTablePagePlacement::Anchor { .. }
));
assert_eq!(plan.pages[0].y_start().raw(), 100.0);
}
#[test]
fn plan_two_slices_anchor_then_continuation() {
let plan = plan_floating_table_pages(
vec![slice(700.0), slice(100.0)],
Pt::new(100.0),
Pt::new(40.0),
);
assert_eq!(plan.pages.len(), 2);
assert!(matches!(
plan.pages[0],
FloatingTablePagePlacement::Anchor { .. }
));
assert!(matches!(
plan.pages[1],
FloatingTablePagePlacement::Continuation { .. }
));
assert_eq!(plan.pages[0].y_start().raw(), 100.0);
assert_eq!(plan.pages[1].y_start().raw(), 40.0);
}
#[test]
fn plan_n_slices_only_first_is_anchor_page() {
let plan = plan_floating_table_pages(
vec![slice(700.0), slice(700.0), slice(100.0)],
Pt::new(150.0),
Pt::new(40.0),
);
assert_eq!(plan.pages.len(), 3);
let anchor_count = plan
.pages
.iter()
.filter(|p| matches!(p, FloatingTablePagePlacement::Anchor { .. }))
.count();
assert_eq!(anchor_count, 1, "at most one Anchor in a plan");
assert_eq!(plan.pages[0].y_start().raw(), 150.0);
for p in &plan.pages[1..] {
assert_eq!(p.y_start().raw(), 40.0);
assert!(matches!(p, FloatingTablePagePlacement::Continuation { .. }));
}
}
#[test]
fn plan_empty_slices_returns_empty_plan() {
let plan = plan_floating_table_pages(Vec::new(), Pt::new(100.0), Pt::new(40.0));
assert!(plan.pages.is_empty());
}
#[test]
fn plan_preserves_slice_size_through_placement() {
let plan = plan_floating_table_pages(
vec![slice(123.4), slice(56.7)],
Pt::new(100.0),
Pt::new(40.0),
);
assert_eq!(plan.pages[0].slice().size.height.raw(), 123.4);
assert_eq!(plan.pages[1].slice().size.height.raw(), 56.7);
}
#[test]
fn anchor_accepts_overlap_when_overlap_permitted() {
let prior = vec![float_at(80.0, 130.0)];
let resolved =
resolve_floating_anchor(Pt::new(100.0), Pt::new(50.0), None, &prior, Pt::new(700.0));
assert_eq!(resolved, FloatingTableAnchor::OnCurrentPage(Pt::new(100.0)));
let resolved = resolve_floating_anchor(
Pt::new(100.0),
Pt::new(50.0),
Some(TableOverlap::Overlap),
&prior,
Pt::new(700.0),
);
assert_eq!(resolved, FloatingTableAnchor::OnCurrentPage(Pt::new(100.0)));
}
#[test]
fn anchor_no_priors_returns_requested_y() {
let resolved = resolve_floating_anchor(
Pt::new(100.0),
Pt::new(50.0),
Some(TableOverlap::Never),
&[],
Pt::new(700.0),
);
assert_eq!(resolved, FloatingTableAnchor::OnCurrentPage(Pt::new(100.0)));
}
#[test]
fn anchor_shifts_below_overlapping_float() {
let prior = vec![float_at(80.0, 130.0)];
let resolved = resolve_floating_anchor(
Pt::new(100.0),
Pt::new(50.0),
Some(TableOverlap::Never),
&prior,
Pt::new(700.0),
);
assert_eq!(
resolved,
FloatingTableAnchor::Shifted {
from: Pt::new(100.0),
to: Pt::new(130.0),
}
);
}
#[test]
fn anchor_below_priors_is_accepted_unchanged() {
let prior = vec![float_at(80.0, 130.0)];
let resolved = resolve_floating_anchor(
Pt::new(200.0),
Pt::new(50.0),
Some(TableOverlap::Never),
&prior,
Pt::new(700.0),
);
assert_eq!(resolved, FloatingTableAnchor::OnCurrentPage(Pt::new(200.0)));
}
#[test]
fn anchor_shifts_past_chain_of_floats() {
let prior = vec![
float_at(80.0, 130.0),
float_at(125.0, 180.0), ];
let resolved = resolve_floating_anchor(
Pt::new(100.0),
Pt::new(50.0),
Some(TableOverlap::Never),
&prior,
Pt::new(700.0),
);
assert_eq!(
resolved,
FloatingTableAnchor::Shifted {
from: Pt::new(100.0),
to: Pt::new(180.0),
}
);
}
#[test]
fn anchor_spillover_when_shift_exceeds_page_bottom() {
let prior = vec![float_at(80.0, 680.0)]; let resolved = resolve_floating_anchor(
Pt::new(100.0),
Pt::new(50.0), Some(TableOverlap::Never),
&prior,
Pt::new(700.0),
);
assert_eq!(resolved, FloatingTableAnchor::Spillover);
}
#[test]
fn anchor_at_prior_y_end_is_not_overlap() {
let prior = vec![float_at(80.0, 130.0)];
let resolved = resolve_floating_anchor(
Pt::new(130.0),
Pt::new(50.0),
Some(TableOverlap::Never),
&prior,
Pt::new(700.0),
);
assert_eq!(resolved, FloatingTableAnchor::OnCurrentPage(Pt::new(130.0)));
}
}