mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 20:00:11 -06:00
A floating table with tblOverlap=never that is moved below another floating table steps in to its distance from the text's left edge only in Word 2013 layout (compatibility mode 15). clear_of_float_tables read the mode from the legacy shift, the first cell's left margin before mode 15, being zero, so a mode 12 table whose first cell has no left margin was taken for Word 2013 layout and moved 9 pt right (docxide displaced-anchor-*-compat12-overlap0-*: Word 72 pt, WordCraft 81 pt). Pass the mode itself. Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
461 lines
23 KiB
Rust
461 lines
23 KiB
Rust
use super::boxes::{BoxInput, clear_of_float_tables, layout_box};
|
|
use super::ctx::{Ctx, with_pct_base};
|
|
use super::floats::{WrapArea, align_in, body_wraps};
|
|
use super::flow::again;
|
|
use super::headers::{header_items_cap, header_table_areas, placed_cost};
|
|
use super::notes::table_notes;
|
|
use super::page::{PageBuilder, PageFrame, page_frame};
|
|
use super::placed::Placed;
|
|
use super::{Anchor, Arc, HeightRule, Path, Rect, StoryRef, TableFloat, ViewMode, WrapText, table};
|
|
|
|
/// Footer page tables wrap only the intersecting body lines, which resume below them.
|
|
pub(crate) fn footer_table_wraps(ctx: &mut Ctx, pb: &PageBuilder) -> Vec<WrapArea> {
|
|
if pb.web || ctx.opts.view == ViewMode::Read {
|
|
return Vec::new();
|
|
}
|
|
let Some(page) = pb.pages.last() else { return Vec::new() };
|
|
let first = pb.pages.iter().rev().nth(1).is_none_or(|prev| prev.section != page.section);
|
|
let key = (page.section, page.number, first);
|
|
if let Some(areas) = ctx.footer_wraps.get(&key) {
|
|
return areas.clone();
|
|
}
|
|
ctx.footer_wraps.insert(key, Vec::new());
|
|
let doc = ctx.doc;
|
|
let hf = Arc::clone(&ctx.hf);
|
|
let Some((_, sect)) = hf.sections.get(page.section) else { return Vec::new() };
|
|
if sect.text_direction.is_turned() {
|
|
return Vec::new();
|
|
}
|
|
let set = hf.feet.set(page.section);
|
|
let id = if sect.title_page && first {
|
|
set.first
|
|
} else if doc.settings.even_odd_headers && page.number.is_multiple_of(2) {
|
|
set.even.or(set.default)
|
|
} else {
|
|
set.default
|
|
};
|
|
let Some(id) = id else { return Vec::new() };
|
|
let Some(part) = doc.parts.get(&id) else { return Vec::new() };
|
|
let blocks = part.blocks.clone();
|
|
let x = sect.margin_left + sect.gutter;
|
|
let base = hf.defining_width(true, page.section, Some(id));
|
|
let counters = std::mem::take(&mut ctx.counters);
|
|
let boxes = std::mem::take(&mut ctx.boxes);
|
|
let chains = std::mem::take(&mut ctx.chains);
|
|
let fields = ctx.fields.clone();
|
|
let grid = std::mem::take(&mut ctx.grid);
|
|
let track = std::mem::replace(&mut ctx.track_totals, false);
|
|
ctx.fields.page = page.number;
|
|
let areas = again(ctx, 0, |ctx| {
|
|
with_pct_base(ctx, base, |ctx| {
|
|
let story = StoryRef::Part(id);
|
|
let h = layout_box(ctx, BoxInput::other(story, &blocks, sect.text_width(), 0, None)).height;
|
|
let y = sect.page_h - sect.footer - h;
|
|
let frame = PageFrame { sect, margin_top: None, origin: (x, y), number: page.number, mirror: ctx.mirror };
|
|
let mut areas = layout_box(ctx, BoxInput::other(story, &blocks, sect.text_width(), 0, Some(frame))).page_table_wraps;
|
|
for (rect, ..) in &mut areas {
|
|
rect.x += x;
|
|
rect.y += y;
|
|
}
|
|
areas
|
|
})
|
|
});
|
|
ctx.counters = counters;
|
|
ctx.boxes = boxes;
|
|
ctx.chains = chains;
|
|
ctx.fields = fields;
|
|
ctx.grid = grid;
|
|
ctx.track_totals = track;
|
|
ctx.footer_wraps.insert(key, areas.clone());
|
|
areas
|
|
}
|
|
|
|
fn row_keeps_next(row: &table::RowLayout) -> bool {
|
|
let Some(items) = row.cells.first().and_then(|cell| row.items.get(cell.items.clone())) else { return false };
|
|
let Some((depth, story)) = items
|
|
.iter()
|
|
.filter_map(|item| match item {
|
|
Placed::Cell { table, story, .. } => Some((table.0.len() + 3, *story)),
|
|
_ => None,
|
|
})
|
|
.min_by_key(|(depth, _)| *depth)
|
|
else {
|
|
return false;
|
|
};
|
|
// Word takes the row's keep flag from the first paragraph of its first cell.
|
|
items
|
|
.iter()
|
|
.find_map(|item| match item {
|
|
Placed::Lines { path, story: own, para, .. } if path.0.len() == depth && path.0.last() == Some(&0) && *own == story => {
|
|
Some(para.rp.keep_next)
|
|
}
|
|
_ => None,
|
|
})
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// The cut that leaves every row of a nested table marked "can't split" whole: just below the
|
|
/// top of the highest such row the cut at `room` runs through, moved up again while it runs
|
|
/// through another one (Word moves such a row on with the rest of the outer row, observed in the
|
|
/// docxide-pdf `nested-first-row-*-cant1` cases). Like the split itself, a row "runs through"
|
|
/// the cut when it ends below the room left for text there, above the row's bottom band; the cut
|
|
/// goes that band below the row's top, so the text and rows ending at its top stay.
|
|
/// shortcut: the moved row's side borders and shading keep that band's height on the page, and
|
|
/// on the next page its cell area and shading start that band less its top margin below its
|
|
/// first line (clicks there miss the cell). Only an explicit bottom cell margin or a thick outer
|
|
/// border shows it, never Word's defaults; upgrade with a separate drawing cut in split_row that
|
|
/// moves a nested cell starting between the limit and the cut whole from its top.
|
|
fn nested_keep_cut(doc: &wordcraft_doc::Document, row: &table::RowLayout, room: f32) -> f32 {
|
|
let own = row.items.iter().filter_map(|it| if let Placed::Cell { table, .. } = it { Some(table.0.len()) } else { None }).min();
|
|
// Each nested row's top and bottom when it can't split. A vertically merged cell runs past
|
|
// its own row, so a row ends where its shortest cell does.
|
|
let mut bands: std::collections::BTreeMap<(StoryRef, &[u32], usize), Option<(f32, f32)>> = std::collections::BTreeMap::new();
|
|
// The split runs a cell aligned to the middle or bottom from its top: so do its bands.
|
|
let mut shift = vec![0.0f32; row.items.len()];
|
|
for al in row.cells.iter().filter_map(|c| c.align.as_ref()) {
|
|
shift.get_mut(al.content.clone()).into_iter().flatten().for_each(|d| *d = al.dy);
|
|
}
|
|
for (it, &dy) in row.items.iter().zip(&shift) {
|
|
let Placed::Cell { rect, table, row: r, story, .. } = it else { continue };
|
|
if Some(table.0.len()) <= own {
|
|
continue;
|
|
}
|
|
let rect = Rect { y: rect.y - dy, ..*rect };
|
|
let band = bands
|
|
.entry((*story, table.0.as_slice(), *r))
|
|
.or_insert_with(|| doc.table(*story, table).and_then(|t| t.rows.get(*r)).filter(|r| r.props.cant_split).map(|_| (rect.y, rect.bottom())));
|
|
if let Some((top, bottom)) = band {
|
|
*top = top.min(rect.y);
|
|
*bottom = bottom.min(rect.bottom());
|
|
}
|
|
}
|
|
let below = row.bottom.clamp(0.0, room.max(0.0));
|
|
let mut bands: Vec<(f32, f32)> = bands.into_values().flatten().collect();
|
|
// From the lowest top up: once the cut moves up to a row, the rows starting below it are
|
|
// wholly under it, and a row the old cut ran through still runs through the new one.
|
|
bands.sort_by(|a, b| b.0.total_cmp(&a.0));
|
|
let mut cut = room;
|
|
for (top, bottom) in bands {
|
|
let limit = cut - below;
|
|
if top < limit && bottom > limit + 0.01 {
|
|
cut = top + below;
|
|
}
|
|
}
|
|
cut
|
|
}
|
|
|
|
/// [`table::split_row_keeping_cells`] that doesn't split a nested "can't split" row either.
|
|
fn split_keeping_nested(doc: &wordcraft_doc::Document, row: &table::RowLayout, room: f32) -> Option<(table::RowLayout, table::RowLayout)> {
|
|
table::split_row_keeping_cells(row, nested_keep_cut(doc, row, room))
|
|
}
|
|
|
|
fn row_can_start(doc: &wordcraft_doc::Document, row: &wordcraft_doc::Row, laid: &table::RowLayout, room: f32, page_height: f32) -> bool {
|
|
if laid.height <= room + 0.01 {
|
|
return true;
|
|
}
|
|
if row.props.header || row.props.cant_split || (row.props.height_rule == HeightRule::Exact && row.props.height.is_some_and(|h| h > 0.0)) {
|
|
return false;
|
|
}
|
|
if row.props.height_rule == HeightRule::AtLeast && row.props.height.is_some_and(|h| h > room + 0.01 && h <= page_height + 0.01) {
|
|
return false;
|
|
}
|
|
split_keeping_nested(doc, laid, room).is_some()
|
|
}
|
|
|
|
pub(crate) fn place_table(ctx: &mut Ctx, pb: &mut PageBuilder, t: &wordcraft_doc::Table, block: usize, body_top: f32) {
|
|
pb.prev = None;
|
|
// A page break before the first row's paragraph moves the whole table to a new page, as
|
|
// Word does (and as an ODT table starting a page opens). Before the table is laid out, so
|
|
// its cells' fields (PAGE) are those of the page it lands on.
|
|
let first = t.rows.first().and_then(|r| r.cells.first()).and_then(|c| c.blocks.first()).and_then(|b| b.as_para());
|
|
if t.props.float.is_none() && !pb.at_top() && !pb.web && first.is_some_and(|p| ctx.doc.styles.resolve_para(&p.props).page_break_before) {
|
|
pb.new_page(block, body_top);
|
|
ctx.fields.page = pb.number;
|
|
}
|
|
let width = pb.col_w();
|
|
let mut tl = table::layout_table(ctx, StoryRef::Body, t, &[block as u32], width, 1);
|
|
if let Some(f) = t.props.float.filter(|_| !pb.web) {
|
|
// Before Word 2013 layout (compatibility mode 15) an offset places the first cell's text,
|
|
// so the edge sits a cell margin further out.
|
|
let legacy = if ctx.doc.settings.compat_mode < 15 { table::first_cell_left_margin(ctx, t) } else { 0.0 };
|
|
let h: f32 = tl.rows.iter().map(|r| r.height).sum();
|
|
if h <= pb.uncapped_bottom() - pb.top + 0.01 {
|
|
place_floating_table(ctx, pb, &tl, &f, legacy, block, body_top);
|
|
return;
|
|
}
|
|
// Too tall for a page: Word lets it run across pages like a table in the text, from its
|
|
// own left edge.
|
|
tl.x = float_table_x(pb, tl.width, &f, legacy, ctx.mirror) - pb.col_x();
|
|
}
|
|
// Its own space above (ODT), not at the top of a page.
|
|
if let Some(s) = t.props.space_before.filter(|s| s.is_finite() && *s > 0.0 && !pb.at_top()) {
|
|
pb.y += s.min(MAX_TABLE_SPACE);
|
|
}
|
|
// A table in the text flow starts below the floating objects in its way.
|
|
below_floats(ctx, pb, pb.col_x() + tl.x, pb.col_x() + tl.x + tl.width);
|
|
let header_rows: Vec<usize> = (0..t.rows.len()).take_while(|r| t.rows.get(*r).is_some_and(|row| row.props.header)).collect();
|
|
// What one more repeat of the header rows costs (see `MAX_HEADER_ITEMS`; the items repeated
|
|
// so far are counted in the whole layout).
|
|
let header_cap = header_items_cap();
|
|
let header_cost = header_rows
|
|
.iter()
|
|
.filter_map(|i| tl.rows.get(*i))
|
|
.map(|r| r.items.iter().map(|it| placed_cost(it, header_cap)).fold(0, usize::saturating_add).max(1))
|
|
.fold(0, usize::saturating_add);
|
|
// Resolve kept chains once; nested paragraphs cannot keep their enclosing row.
|
|
let mut kept: Vec<(usize, f32)> = (0..tl.rows.len()).map(|i| (i, 0.0)).collect();
|
|
for i in (0..tl.rows.len().saturating_sub(1)).rev() {
|
|
let Some(row) = tl.rows.get(i) else { continue };
|
|
if i < header_rows.len() || !row_keeps_next(row) {
|
|
continue;
|
|
}
|
|
let Some((end, height)) = kept.get(i + 1).copied() else { continue };
|
|
if let Some(group) = kept.get_mut(i) {
|
|
*group = (end, row.height + height);
|
|
}
|
|
}
|
|
let repeated_height: f32 = header_rows.iter().filter_map(|i| tl.rows.get(*i)).map(|r| r.height).sum();
|
|
let place = |pb: &mut PageBuilder, row: &table::RowLayout| {
|
|
let (x, y) = (pb.col_x() + tl.x, pb.y);
|
|
if let Some(pg) = pb.page() {
|
|
for it in &row.items {
|
|
let mut it = it.clone();
|
|
it.translate(x, y);
|
|
pg.items.push(it);
|
|
}
|
|
}
|
|
pb.y += row.height;
|
|
pb.content_bottom = pb.content_bottom.max(pb.y);
|
|
};
|
|
// Measured in Word: an at-least row's minimum height is reserved, not split. Where less room
|
|
// than that is left on a page that already holds something, the row starts the next page
|
|
// whole; a row taller than a page then splits from the top of that page.
|
|
let short_of_min = |ri: usize, room: f32| {
|
|
t.rows.get(ri).is_some_and(|r| r.props.height_rule == HeightRule::AtLeast && r.props.height.is_some_and(|h| h > room + 0.01))
|
|
};
|
|
// Like Word, repeated header rows don't stay alone at the bottom of a page: when the first row
|
|
// after them can't start under them there, the table starts on the next page.
|
|
let first = header_rows.len();
|
|
if !header_rows.is_empty()
|
|
&& !pb.at_top()
|
|
&& let Some(row) = tl.rows.get(first)
|
|
{
|
|
let room = pb.bottom - pb.y - tl.rows.iter().take(first).map(|r| r.height).sum::<f32>();
|
|
let splittable = !t.rows.get(first).is_some_and(|r| r.props.cant_split);
|
|
if row.height + row.edge > room + 0.01 && (short_of_min(first, room) || !(splittable && split_keeping_nested(ctx.doc, row, room).is_some())) {
|
|
pb.advance(block, body_top);
|
|
}
|
|
}
|
|
// For the move-on rule, the table's own header rows at the top of a page leave it empty.
|
|
let table_at_top = pb.at_top();
|
|
for (ri, row) in tl.rows.iter().enumerate() {
|
|
// Word never splits a row of an exact height: it moves on whole.
|
|
let splittable = ri >= header_rows.len()
|
|
&& !t
|
|
.rows
|
|
.get(ri)
|
|
.is_some_and(|r| r.props.cant_split || (r.props.height_rule == HeightRule::Exact && r.props.height.is_some_and(|h| h > 0.0)));
|
|
let mut rest: Option<table::RowLayout> = None;
|
|
// On a new page holding only the repeated header rows.
|
|
let mut fresh = false;
|
|
// Nothing but this table's header rows above it on a page it started at the top of.
|
|
let under_top_headers = ri == first && table_at_top;
|
|
// Each pass places the part of the row that fits, then breaks the page.
|
|
for _ in 0..1000 {
|
|
let cur = rest.as_ref().unwrap_or(row);
|
|
// A fitting kept row follows the next row when that row cannot start below it.
|
|
// Move only the prefix's start; an oversized follower cannot separate its kept rows.
|
|
let defer_kept = pb.y + cur.height <= pb.bottom + 0.01
|
|
&& !pb.web
|
|
&& !pb.at_top()
|
|
&& !fresh
|
|
&& rest.is_none()
|
|
&& ri.checked_sub(1).and_then(|prev| kept.get(prev)).is_none_or(|&(end, _)| end < ri)
|
|
&& kept.get(ri).is_some_and(|&(end, height)| {
|
|
if end == ri {
|
|
return false;
|
|
}
|
|
let (Some(next), Some(laid)) = (t.rows.get(end), tl.rows.get(end)) else { return false };
|
|
let page_height = pb.bottom - body_top - repeated_height;
|
|
height <= page_height + 0.01 && !row_can_start(ctx.doc, next, laid, pb.bottom - pb.y - height, page_height)
|
|
});
|
|
// A row another row follows needs room for the border below it too (measured in Word).
|
|
if pb.y + cur.height + cur.edge <= pb.bottom + 0.01 && !defer_kept {
|
|
break;
|
|
}
|
|
// An at-least row with less room than its minimum moves on whole, unless nothing
|
|
// is above it on this page (moving on would gain nothing).
|
|
let room = pb.bottom - pb.y;
|
|
// Its first part on a page that already holds something leaves no cell empty
|
|
// (#4272): moving on can give the cell room there.
|
|
let first_part = rest.is_none() && !pb.at_top() && !fresh && !under_top_headers;
|
|
let moves_on = first_part && short_of_min(ri, room);
|
|
let split = match (splittable && !moves_on && !defer_kept, first_part) {
|
|
(false, _) => None,
|
|
(true, true) => split_keeping_nested(ctx.doc, cur, room),
|
|
// On a fresh page, a nested row taller than the room splits rather than overflowing.
|
|
(true, false) => table::split_row(cur, nested_keep_cut(ctx.doc, cur, room)).or_else(|| table::split_row(cur, room)),
|
|
};
|
|
match split {
|
|
Some((a, b)) => {
|
|
place(pb, &a);
|
|
table_notes(ctx, pb, &a.items);
|
|
rest = Some(b);
|
|
}
|
|
// Like Word, a row that does not fit on an empty page, or under the header rows
|
|
// repeated there, is placed anyway and runs into the bottom margin.
|
|
None if pb.at_top() || fresh => break,
|
|
None => {}
|
|
}
|
|
pb.advance(block, body_top);
|
|
// Repeat header rows, up to the layout's cap.
|
|
if ri >= header_rows.len() && ctx.header_repeats.saturating_add(header_cost) <= header_cap {
|
|
ctx.header_repeats = ctx.header_repeats.saturating_add(header_cost);
|
|
for h in header_rows.iter().filter_map(|hr| tl.rows.get(*hr)) {
|
|
place(pb, h);
|
|
}
|
|
}
|
|
fresh = true;
|
|
}
|
|
let last = rest.as_ref().unwrap_or(row);
|
|
place(pb, last);
|
|
table_notes(ctx, pb, &last.items);
|
|
}
|
|
// Its own space below (ODT).
|
|
if let Some(s) = t.props.space_after.filter(|s| s.is_finite() && *s > 0.0) {
|
|
pb.y += s.min(MAX_TABLE_SPACE);
|
|
}
|
|
}
|
|
|
|
/// The most space a table's own space above or below can add, points (22 inches, Word's
|
|
/// largest paragraph spacing).
|
|
const MAX_TABLE_SPACE: f32 = 1584.0;
|
|
|
|
/// Place floating table `tl` (`w:tblpPr`): at its own position, which takes no room in the text
|
|
/// flow; the text after it wraps around it.
|
|
fn place_floating_table(ctx: &mut Ctx, pb: &mut PageBuilder, tl: &table::TableLayout, f: &TableFloat, legacy: f32, block: usize, body_top: f32) {
|
|
let h: f32 = tl.rows.iter().map(|r| r.height).sum::<f32>().clamp(0.0, 100_000.0);
|
|
let w = tl.width.clamp(1.0, 100_000.0);
|
|
let [dl, dt, dr, db] = f.dist_from_text();
|
|
for attempt in 0..2 {
|
|
let s = pb.sect;
|
|
let h_area = float_table_h_area(pb, f, ctx.mirror);
|
|
// The text a table anchored to it stands in: where the next paragraph starts.
|
|
let v_area = match f.v_rel {
|
|
Anchor::Page => (0.0, s.page_h),
|
|
Anchor::Margin => (s.margin_top, s.text_height()),
|
|
_ => (pb.y, 0.0),
|
|
};
|
|
let x = float_table_x(pb, w, f, legacy, ctx.mirror);
|
|
let mut obstacles = if f.overlap { Vec::new() } else { header_table_areas(ctx, pb) };
|
|
obstacles.extend_from_slice(&pb.float_tables);
|
|
let r = clear_of_float_tables(
|
|
Rect::new(x, align_in(v_area, f.y, h, f.v_align), w, h),
|
|
f,
|
|
ctx.doc.settings.compat_mode >= 15,
|
|
h_area.0,
|
|
&obstacles,
|
|
);
|
|
// Like Word, a table that doesn't fit below the text before it moves to the next page with
|
|
// its anchor: one placed relative to the paragraph when it would run off the page, one
|
|
// placed on the page or margin when it would also cover the text already on the page.
|
|
let moves = if f.v_rel == Anchor::Paragraph {
|
|
r.bottom() > pb.bottom + 0.01
|
|
} else {
|
|
let covers_text = pb.page().is_some_and(|page| page.items.iter().any(|item| placed_intersects(item, &r)));
|
|
covers_text && pb.y + h > pb.bottom + 0.01
|
|
};
|
|
if attempt == 0 && moves && !pb.at_top() {
|
|
pb.advance(block, body_top);
|
|
continue;
|
|
}
|
|
if let Some(pg) = pb.page() {
|
|
let mut y = r.y;
|
|
for row in &tl.rows {
|
|
for it in &row.items {
|
|
let mut it = it.clone();
|
|
it.translate(r.x, y);
|
|
pg.items.push(it);
|
|
}
|
|
y += row.height;
|
|
}
|
|
pg.float_tables.push(Path::top(block));
|
|
}
|
|
let area = r.inset(-dl, -dt, -dr, -db);
|
|
pb.excl.push((area, false, None, WrapText::BothSides, true));
|
|
pb.float_tables.push(area);
|
|
return;
|
|
}
|
|
}
|
|
|
|
fn placed_intersects(item: &Placed, rect: &Rect) -> bool {
|
|
match item {
|
|
Placed::Lines { para, l0, l1, x, y, .. } => para.lines.get(*l0..*l1).is_some_and(|lines| {
|
|
lines.iter().enumerate().any(|(offset, line)| {
|
|
if para.label.is_some() && *l0 + offset == 0 {
|
|
return Rect::new(x + line.left, y + line.top, line.right - line.left, line.height).intersects(rect);
|
|
}
|
|
para.clusters.get(line.c0..line.c1).is_some_and(|clusters| {
|
|
clusters.iter().enumerate().any(|(index, cluster)| {
|
|
if !matches!(
|
|
cluster.kind,
|
|
crate::para::ClKind::Text | crate::para::ClKind::Space | crate::para::ClKind::Tab | crate::para::ClKind::Object(_)
|
|
) {
|
|
return false;
|
|
}
|
|
let ci = line.c0 + index;
|
|
let Some(left) = line.cl_left(ci) else { return false };
|
|
let Some(right) = line.cl_right(ci) else { return false };
|
|
Rect::new(x + left, y + line.top, right - left, line.height).intersects(rect)
|
|
})
|
|
})
|
|
})
|
|
}),
|
|
Placed::Fill { rect: bounds, .. }
|
|
| Placed::Image { rect: bounds, .. }
|
|
| Placed::Shape { rect: bounds, .. }
|
|
| Placed::Graphic { rect: bounds, .. }
|
|
| Placed::Cell { rect: bounds, .. }
|
|
| Placed::Object { rect: bounds, .. }
|
|
| Placed::Clip { rect: bounds, .. } => bounds.intersects(rect),
|
|
Placed::Rule { x0, y0, x1, y1, .. } => Rect::new(x0.min(*x1), y0.min(*y1), (x1 - x0).abs(), (y1 - y0).abs()).intersects(rect),
|
|
}
|
|
}
|
|
|
|
/// The area a floating table's `x` is measured in: its start and width on the page, as laid out
|
|
/// before an even page is mirrored (see [`float_rect`]).
|
|
fn float_table_h_area(pb: &PageBuilder, f: &TableFloat, mirror: bool) -> (f32, f32) {
|
|
let s = pb.sect;
|
|
let (left, _, shift) = page_frame(pb, mirror).sides();
|
|
match f.h_rel {
|
|
Anchor::Page => (-shift, s.page_w),
|
|
Anchor::Margin => (left - shift, s.text_width()),
|
|
_ => (pb.col_x(), pb.col_w()),
|
|
}
|
|
}
|
|
|
|
/// A floating table's left edge on the page (`legacy`: the first cell's left margin before Word
|
|
/// 2013 layout, when an offset places the cell's text).
|
|
fn float_table_x(pb: &PageBuilder, w: f32, f: &TableFloat, legacy: f32, mirror: bool) -> f32 {
|
|
let align = PageFrame::h_align(Some(&page_frame(pb, mirror)), f.h_align);
|
|
let x = align_in(float_table_h_area(pb, f, mirror), f.x, w, align);
|
|
if f.h_align.is_none() { x - legacy } else { x }
|
|
}
|
|
|
|
/// Move `pb.y` below the floating objects in the way of something spanning `x0..x1` that
|
|
/// starts there (a table in the text flow doesn't wrap around them).
|
|
fn below_floats(ctx: &mut Ctx, pb: &mut PageBuilder, x0: f32, x1: f32) {
|
|
pb.y = below(&body_wraps(ctx, pb), pb.y, x0, x1);
|
|
}
|
|
|
|
/// `y`, moved below the wrap areas in `excl` in the way of something spanning `x0..x1` that
|
|
/// starts there.
|
|
pub(crate) fn below(excl: &[WrapArea], mut y: f32, x0: f32, x1: f32) -> f32 {
|
|
for _ in 0..64 {
|
|
let in_the_way = excl.iter().filter(|(r, ..)| r.y <= y + 0.01 && r.bottom() > y && r.x < x1 && r.right() > x0);
|
|
let Some(bottom) = in_the_way.map(|(r, ..)| r.bottom()).reduce(f32::max) else { break };
|
|
y = bottom;
|
|
}
|
|
y
|
|
}
|