mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-10 04:09:53 -06:00
Word drops the space before of an empty paragraph that follows a document's leading floating table: its PDFs of docxide-pdf's leading-empty-before-float-0 and -12 put the text after the empty anchor (beside the table) at the same place for 0 pt and 12 pt before. Flow::gap_before (crates/layout/src/flow.rs) kept it: a floating table takes no room in the flow and clears `pb.prev`, so the paragraph after it was laid out like a document's first paragraph, with its full space before. It now drops it for body block 1 when block 0 was placed floating on this page (the page's float_tables), the page is fresh and the paragraph holds nothing but markers (bookmarks, comment and permission ranges). It doesn't check that the paragraph sits beside the table, so below a full-width leading float an empty paragraph loses its space before too; that case is untested. Later pages and columns, and paragraphs with text, keep their space before. 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>
1063 lines
50 KiB
Rust
1063 lines
50 KiB
Rust
use super::boxes::{BoxInput, BoxLayout, Edges, joined, layout_box, push_para};
|
|
use super::cache::has_page_number_fields;
|
|
use super::ctx::Ctx;
|
|
use super::floats::{
|
|
FloatAnchor, LateFloats, ObjFrame, ObjectPlacement, anchor_floats, body_wraps, continued_exclusions, fitting, late_floats, place_late,
|
|
place_objects,
|
|
};
|
|
use super::notes::{DECOR_STORY, NoteBox, para_notes};
|
|
use super::page::{PageBuilder, PageFrame, item_bottom};
|
|
use super::placed::Placed;
|
|
use super::{
|
|
Arc, Block, Counters, Document, HashMap, InlineObject, Level, LineEnd, Page, ParaLayout, Paragraph, Path, Rect, StoryRef, TextDirection,
|
|
ViewMode, outline, para,
|
|
};
|
|
#[cfg(test)]
|
|
use crate::WRAP_RELAYS;
|
|
|
|
/// A closing mark before a continuous section takes no room after content. An otherwise empty
|
|
/// section starting a new page retains its paragraph line.
|
|
pub(crate) fn place_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize, body_top: f32) {
|
|
if !pb.web && ctx.quiet_marks.binary_search(&block).is_ok() {
|
|
let follows_section = block.checked_sub(1).and_then(|i| ctx.doc.body.get(i)).and_then(|b| b.as_para()).is_some_and(|p| p.section.is_some());
|
|
let section = pb.sect_idx;
|
|
let own_page = pb.page().is_some_and(|page| page.section == section);
|
|
// Word keeps the line when a new page's section contains only this mark.
|
|
if p.section.is_some()
|
|
&& p.objects.is_empty()
|
|
&& (ctx.opts.show_hidden || !ctx.doc.styles.resolve_char(p.props.style.as_deref(), &p.mark).hidden)
|
|
&& !ctx.has_label(p)
|
|
&& (block == 0 || follows_section)
|
|
&& pb.at_top()
|
|
&& own_page
|
|
{
|
|
place_para_in_flow(ctx, pb, p, block, body_top);
|
|
return;
|
|
}
|
|
// Its (empty) line stays where the text ended, even at the bottom of a full column.
|
|
let (y, prev, bottom, content_bottom, at) = (pb.y, pb.prev.clone(), pb.bottom, pb.content_bottom, (pb.pages.len(), pb.col));
|
|
let drawn = pb.page().map_or(0, |page| page.items.len());
|
|
pb.bottom = f32::MAX / 4.0;
|
|
place_para_in_flow(ctx, pb, p, block, body_top);
|
|
pb.bottom = bottom;
|
|
if (pb.pages.len(), pb.col) == at {
|
|
(pb.y, pb.prev, pb.content_bottom) = (y, prev, content_bottom);
|
|
if let Some(page) = pb.page() {
|
|
strip_mark(page, drawn, true);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if !pb.web && ctx.stacked_marks.binary_search(&block).is_ok() {
|
|
// A run of empty continuous-section marks uses each remaining line, but never starts a
|
|
// new page just for a mark; later text starts there instead.
|
|
let (y, prev, bottom, content_bottom, at) = (pb.y, pb.prev.clone(), pb.bottom, pb.content_bottom, (pb.pages.len(), pb.col));
|
|
let drawn = pb.page().map_or(0, |page| page.items.len());
|
|
pb.bottom = f32::MAX / 4.0;
|
|
place_para_in_flow(ctx, pb, p, block, body_top);
|
|
if pb.pages.len() == at.0 {
|
|
pb.bottom = bottom;
|
|
}
|
|
if (pb.pages.len(), pb.col) == at && pb.y > bottom + 0.01 {
|
|
(pb.y, pb.prev, pb.content_bottom) = (bottom.max(y), prev, content_bottom);
|
|
if p.objects.is_empty()
|
|
&& let Some(page) = pb.page()
|
|
{
|
|
strip_mark(page, drawn, false);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
place_para_in_flow(ctx, pb, p, block, body_top);
|
|
}
|
|
|
|
/// Remove the list label from a mark's lines; optionally keep its other drawing items.
|
|
fn strip_mark(page: &mut Page, drawn: usize, keep_others: bool) {
|
|
let tail = page.items.split_off(drawn.min(page.items.len()));
|
|
page.items.extend(tail.into_iter().filter_map(|item| match item {
|
|
Placed::Lines { story, path, para, l0, l1, x, y, turn } if para.label.is_some() => {
|
|
let mut bare = (*para).clone();
|
|
bare.label = None;
|
|
Some(Placed::Lines { story, path, para: Arc::new(bare), l0, l1, x, y, turn })
|
|
}
|
|
Placed::Lines { story, .. } if story == DECOR_STORY && !keep_others => None,
|
|
line @ Placed::Lines { .. } => Some(line),
|
|
other => keep_others.then_some(other),
|
|
}));
|
|
}
|
|
|
|
/// How many times one body paragraph is laid out again for the page numbers of the later pages
|
|
/// it runs onto (#1352).
|
|
const MAX_PAGE_RELAYS: usize = 64;
|
|
|
|
/// Run `f`, a layout of a paragraph again, with the automatic equation numbers it counted from
|
|
/// (`eq_start`) the first time, and leave the count where it was: laying it out again doesn't
|
|
/// count its numbered equations twice.
|
|
pub(crate) fn again<T>(ctx: &mut Ctx, eq_start: u32, f: impl FnOnce(&mut Ctx) -> T) -> T {
|
|
let now = std::mem::replace(&mut ctx.eq_count, eq_start);
|
|
let out = f(ctx);
|
|
ctx.eq_count = now;
|
|
out
|
|
}
|
|
|
|
/// How many times one body paragraph's lines are broken again for the wrap areas of the later
|
|
/// columns and pages it runs onto (#1374).
|
|
const MAX_WRAP_RELAYS: usize = 64;
|
|
|
|
/// Run `f` with `map` as the field context's page map (`FieldCtx::page_from`), then take it back.
|
|
fn with_page_map<T>(ctx: &mut Ctx, map: &mut Vec<(usize, u32)>, f: impl FnOnce(&mut Ctx) -> T) -> T {
|
|
ctx.fields.page_from = std::mem::take(map);
|
|
let out = f(ctx);
|
|
*map = std::mem::take(&mut ctx.fields.page_from);
|
|
out
|
|
}
|
|
|
|
/// The room for a paragraph's top and bottom borders; those of a group joined in one box are
|
|
/// drawn by its first and last paragraph.
|
|
#[derive(Clone, Copy)]
|
|
struct Rooms {
|
|
/// Joined to the paragraph above.
|
|
above: bool,
|
|
top: f32,
|
|
bottom: f32,
|
|
}
|
|
|
|
impl Rooms {
|
|
fn of(ctx: &Ctx, block: usize, pl: &ParaLayout) -> Self {
|
|
let above = joined(ctx.doc, &ctx.doc.body, block, block.checked_sub(1), None);
|
|
let below = joined(ctx.doc, &ctx.doc.body, block, Some(block + 1), None);
|
|
let own = Edges::own(pl, 0, pl.lines.len());
|
|
Rooms { above, top: if above { 0.0 } else { own.top }, bottom: if below { 0.0 } else { own.bottom } }
|
|
}
|
|
}
|
|
|
|
/// The lines of a paragraph that go on one page, and what goes with them.
|
|
struct Fit {
|
|
/// The end of the lines (exclusive).
|
|
l1: usize,
|
|
/// How the last line ends, if it forces a break.
|
|
forced: Option<LineEnd>,
|
|
/// A page break followed only by the paragraph mark: the mark's (empty) line stays on this
|
|
/// page with the break, so nothing of the paragraph goes to the next page (Word).
|
|
mark_with_break: bool,
|
|
/// The footnotes referenced on those lines.
|
|
notes: Vec<NoteBox>,
|
|
}
|
|
|
|
/// A body paragraph being placed: its layout and what it was laid out for, which change as it is
|
|
/// laid out again for the columns and pages it runs onto.
|
|
struct Flow<'a> {
|
|
p: &'a Paragraph,
|
|
block: usize,
|
|
body_top: f32,
|
|
width: f32,
|
|
label: Option<(String, Level)>,
|
|
/// Where it was expected to start; floats anchored here are placed again if it starts elsewhere.
|
|
guess: f32,
|
|
excl_mark: usize,
|
|
/// Where its automatic equation numbers start: each layout of it again counts from here.
|
|
eq_start: u32,
|
|
float_rects: HashMap<usize, Rect>,
|
|
pl: Arc<ParaLayout>,
|
|
rel: Vec<para::Exclusion>,
|
|
/// The page its fields were laid out for, and the width.
|
|
laid_page: u32,
|
|
laid_width: f32,
|
|
/// The anchor paragraph's top (above its space before) the floats were placed from.
|
|
anchor_y: f32,
|
|
/// The pages this paragraph's fields are on after its first (`FieldCtx::page_from`), kept here
|
|
/// and lent to the field context only while this paragraph itself is laid out, so footnotes,
|
|
/// text boxes and headers laid out in between never see it (#1352).
|
|
page_map: Vec<(usize, u32)>,
|
|
/// How many times it was laid out again for a later page (bounded: a paragraph over hundreds
|
|
/// of pages keeps the last number it got), and for the wrap areas of a later column (#1374).
|
|
page_relays: usize,
|
|
wrap_relays: usize,
|
|
/// The first line not placed yet.
|
|
l0: usize,
|
|
/// Its floats anchored on a later page or column than its first piece (#4250).
|
|
late: LateFloats,
|
|
/// Whether the floats past the first piece were checked ([`Flow::recheck_late`]), and those
|
|
/// it held back.
|
|
rechecked: bool,
|
|
forced: Vec<usize>,
|
|
/// The space before it at the top of the piece being placed (a paragraph starting with a page
|
|
/// break has it on the new page): late floats there are positioned from above it.
|
|
space_here: f32,
|
|
}
|
|
|
|
impl<'a> Flow<'a> {
|
|
fn new(ctx: &mut Ctx, pb: &mut PageBuilder, p: &'a Paragraph, (block, body_top): (usize, f32)) -> Self {
|
|
let width = pb.col_w();
|
|
// Floating objects anchored here join the page's wrap areas before the text is laid out; if
|
|
// the paragraph then starts somewhere else (another column or page, or contextual spacing
|
|
// drops its space before), they are placed again there.
|
|
let space_before = ctx.doc.styles.resolve_para(&p.props).space_before;
|
|
let excl_mark = pb.excl.len();
|
|
let guess = pb.y + space_before;
|
|
let label = ctx.next_label(p);
|
|
let eq_start = ctx.eq_count;
|
|
let (float_rects, pl, rel, late) =
|
|
anchor_floats(ctx, pb, FloatAnchor { p, block, y0: guess, before: space_before, excl_mark, label: &label, place: true }, &[]);
|
|
Flow {
|
|
p,
|
|
block,
|
|
body_top,
|
|
width,
|
|
label,
|
|
guess,
|
|
excl_mark,
|
|
eq_start,
|
|
float_rects,
|
|
pl,
|
|
rel,
|
|
laid_page: pb.number,
|
|
laid_width: pb.col_w(),
|
|
anchor_y: pb.y,
|
|
page_map: Vec::new(),
|
|
page_relays: 0,
|
|
wrap_relays: 0,
|
|
l0: 0,
|
|
late: LateFloats::new(&late),
|
|
rechecked: false,
|
|
forced: Vec::new(),
|
|
space_here: 0.0,
|
|
}
|
|
}
|
|
|
|
fn n(&self) -> usize {
|
|
self.pl.lines.len()
|
|
}
|
|
|
|
/// Place the floats and lay the paragraph out again from `y0`, `before` the space before it,
|
|
/// leaving those in `force` for a later piece.
|
|
fn reanchor(&mut self, ctx: &mut Ctx, pb: &mut PageBuilder, (y0, before): (f32, f32), force: &[usize]) {
|
|
let (rects, pl, rel, late) = again(ctx, self.eq_start, |ctx| {
|
|
anchor_floats(
|
|
ctx,
|
|
pb,
|
|
FloatAnchor { p: self.p, block: self.block, y0, before, excl_mark: self.excl_mark, label: &self.label, place: true },
|
|
force,
|
|
)
|
|
});
|
|
(self.float_rects, self.pl, self.rel) = (rects, pl, rel);
|
|
self.late = LateFloats::new(&late);
|
|
(self.laid_page, self.laid_width) = (pb.number, pb.col_w());
|
|
}
|
|
|
|
/// Lay the paragraph out again around the wrap areas `rel`, with its fields on their pages.
|
|
fn relay(&mut self, ctx: &mut Ctx, rel: &[para::Exclusion]) -> Arc<ParaLayout> {
|
|
let (p, width, eq_start, label) = (self.p, self.laid_width, self.eq_start, &self.label);
|
|
with_page_map(ctx, &mut self.page_map, |ctx| again(ctx, eq_start, |ctx| ctx.para_labelled(p, width, None, rel, label.clone())))
|
|
}
|
|
|
|
/// The space before the paragraph, after the one between it and the previous paragraph is
|
|
/// settled: same-style contextual spacing removes it, or subtracts the previous paragraph's
|
|
/// after-space from the merged gap (ISO/IEC 29500-1 §17.3.1.9).
|
|
fn gap_before(&self, ctx: &Ctx, pb: &mut PageBuilder, broke_before: bool) -> f32 {
|
|
let mut before = self.pl.rp.space_before;
|
|
let prev_para = pb.prev.clone();
|
|
// At the top of a page, measured in Word for Windows: no space before after a paragraph that
|
|
// ended with a page break, nor (from Word 2013's layout, compatibility mode 15) for a page
|
|
// break before; a section break or, in older modes, a page break before keeps it.
|
|
let after_break_mark = std::mem::take(&mut pb.after_break_mark);
|
|
if pb.at_top() && (after_break_mark || (broke_before && ctx.doc.settings.compat_mode >= 15)) {
|
|
before = 0.0;
|
|
} else if pb.at_top()
|
|
&& !ctx.doc.settings.sum_paragraph_spacing
|
|
&& let Some((_, _, after, _, _)) = &prev_para
|
|
{
|
|
// After a page break before or a section break the space between the paragraphs is
|
|
// still counted as one: the previous paragraph's space after (left on its page) comes
|
|
// off the space before here (Word: 24 pt before after 10 pt after leaves 14 pt).
|
|
before = (before - after.max(0.0)).max(0.0);
|
|
}
|
|
if let Some(prev) = pb.prev.take()
|
|
&& !pb.at_top()
|
|
{
|
|
before = self.merge_gap(ctx, pb, before, prev);
|
|
}
|
|
// Automatic spacing: none at the top of the document.
|
|
if self.pl.rp.before_auto && self.block == 0 {
|
|
before = 0.0;
|
|
}
|
|
// An empty paragraph after a document's leading table, placed floating on this page, has no
|
|
// space before (Word's PDFs: 0 pt and 12 pt before put the text after it at the same place).
|
|
// Only that shape is observed: later pages and columns keep theirs.
|
|
if self.block == 1
|
|
&& pb.fresh()
|
|
&& !pb.web
|
|
&& pb.pages.last().is_some_and(|pg| pg.float_tables.contains(&Path::top(0)))
|
|
&& only_markers(self.p, 0..self.p.text.len())
|
|
{
|
|
before = 0.0;
|
|
}
|
|
before
|
|
}
|
|
|
|
/// `before`, the space before the paragraph, with the previous paragraph (`prev`) taken into account.
|
|
fn merge_gap(&self, ctx: &Ctx, pb: &mut PageBuilder, mut before: f32, prev: (String, bool, f32, bool, bool)) -> f32 {
|
|
let (style, ctxl, mut after, after_auto, listed) = prev;
|
|
let rp = &self.pl.rp;
|
|
// Contextual spacing suppresses each paragraph's own side of the gap.
|
|
if style == rp.style {
|
|
if ctxl {
|
|
pb.y -= after;
|
|
before = (before - after).max(0.0);
|
|
after = 0.0;
|
|
}
|
|
if rp.contextual_spacing {
|
|
before = 0.0;
|
|
}
|
|
}
|
|
// Automatic spacing: none between two list items (as in an HTML list; measured in Word:
|
|
// any two items, of one list or two, at any level).
|
|
if listed && self.label.is_some() {
|
|
if after_auto {
|
|
pb.y -= after;
|
|
after = 0.0;
|
|
}
|
|
if rp.before_auto {
|
|
before = 0.0;
|
|
}
|
|
}
|
|
// The space between two paragraphs is the larger of the first's space after and the
|
|
// second's space before, not their sum.
|
|
if before > 0.0 && after > 0.0 && !ctx.doc.settings.sum_paragraph_spacing {
|
|
before = (before - after).max(0.0);
|
|
}
|
|
before
|
|
}
|
|
|
|
/// Keep lines together: if it doesn't fit but would on an empty column, move it. Keep with
|
|
/// next alone moves a paragraph only when it would fit here by itself (Word for Windows): one
|
|
/// that doesn't breaks as usual, its last lines going over with the next paragraph. Returns
|
|
/// the space before applied (`before`, or what is left of it at the top of the next column).
|
|
fn keep_together(&self, ctx: &mut Ctx, pb: &mut PageBuilder, rooms: Rooms, before: f32) -> f32 {
|
|
let rp = &self.pl.rp;
|
|
// A terminal page-break mark stays with the break and draws no bottom border there.
|
|
let break_mark = !pb.web
|
|
&& self.pl.lines.len() >= 2
|
|
&& self.pl.lines.get(self.pl.lines.len() - 2).is_some_and(|line| {
|
|
line.end == LineEnd::PageBreak && self.pl.lines.last().is_some_and(|mark| only_markers(self.p, mark.start..mark.stop))
|
|
});
|
|
let height = self.pl.height + rooms.top + if break_mark { 0.0 } else { rooms.bottom };
|
|
if !(rp.keep_lines || (rp.keep_next && pb.y + height <= pb.bottom + 0.01)) {
|
|
return before;
|
|
}
|
|
let need = height + if rp.keep_next { next_first_line(ctx, self.block, self.width, 0, &mut ctx.counters.clone()) } else { 0.0 };
|
|
// (Against the whole column: balanced columns grow to hold it.)
|
|
// With the 0.01 pt of slack the other fit checks take.
|
|
if pb.y + need > pb.bottom + 0.01 && !pb.fresh() && need <= pb.next_room(self.body_top) {
|
|
pb.advance(self.block, self.body_top);
|
|
pb.y += rp.space_before.min(0.0);
|
|
return rp.space_before.min(0.0);
|
|
}
|
|
before
|
|
}
|
|
|
|
/// The lines going into a new column (or page) were broken around the wrap areas where the
|
|
/// paragraph started: break them again around this column's (the lines before stay as they
|
|
/// were, and so do the paragraph's own floating objects), when they differ (#1374).
|
|
fn relay_wrap(&mut self, ctx: &mut Ctx, pb: &PageBuilder) {
|
|
if self.l0 == 0 || pb.web || self.wrap_relays >= MAX_WRAP_RELAYS {
|
|
return;
|
|
}
|
|
let Some((resume, t)) = self.pl.lines.get(self.l0).map(|l| (l.start, l.top)) else { return };
|
|
let here = continued_exclusions(&self.rel, &body_wraps(ctx, pb), pb.col_x(), pb.y, t, self.laid_width);
|
|
if here == self.rel {
|
|
return;
|
|
}
|
|
self.wrap_relays += 1;
|
|
ctx.fields.page = self.laid_page;
|
|
#[cfg(test)]
|
|
WRAP_RELAYS.with(|c| c.set(c.get() + 1));
|
|
let relaid = self.relay(ctx, &here);
|
|
// Only if the rest still starts at the same line, at the same height (an outline
|
|
// reaching above it could move the lines before down, and the column's areas with them).
|
|
if let Some(k) = relaid.lines.iter().position(|l| l.start == resume)
|
|
&& relaid.lines.get(k).is_some_and(|l| (l.top - t).abs() < 0.01)
|
|
{
|
|
(self.pl, self.rel, self.l0) = (relaid, here, k);
|
|
}
|
|
}
|
|
|
|
/// Page fields in the lines going onto a new page show that page's number: lay the
|
|
/// paragraph out again with the fields from there on on this page (the lines before stay as
|
|
/// they were, so the line the rest starts with is still there), breaking the rest again for
|
|
/// the new results' widths (#1352). Only a paragraph with page fields pays for this.
|
|
fn relay_pages(&mut self, ctx: &mut Ctx, pb: &PageBuilder) {
|
|
if self.l0 == 0 || !self.pl.has_page_fields || pb.web || self.page_relays >= MAX_PAGE_RELAYS {
|
|
return;
|
|
}
|
|
let Some(resume) = self.pl.lines.get(self.l0).map(|l| l.start) else { return };
|
|
if self.page_map.last().map_or(self.laid_page, |e| e.1) == pb.number || !has_page_number_fields(self.p) {
|
|
return;
|
|
}
|
|
self.page_relays += 1;
|
|
self.page_map.push((resume, pb.number));
|
|
ctx.fields.page = self.laid_page;
|
|
let rel = self.rel.clone();
|
|
let relaid = self.relay(ctx, &rel);
|
|
match relaid.lines.iter().position(|l| l.start == resume) {
|
|
Some(k) => (self.pl, self.l0) = (relaid, k),
|
|
None => {
|
|
self.page_map.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The lines from `l0` that go on this page (`lead`: the room above the first line, from a
|
|
/// float pushing it down or its border), with the footnotes on them.
|
|
fn fit_lines(&self, ctx: &mut Ctx, pb: &PageBuilder, notes: &[(usize, u32)], lead: f32) -> Fit {
|
|
let (pl, n, l0) = (&self.pl, self.n(), self.l0);
|
|
let first_top = pl.lines.get(l0).map_or(0.0, |f| f.top);
|
|
let mut fit = Fit { l1: l0, forced: None, mark_with_break: false, notes: Vec::new() };
|
|
// Worked out only for a last line on a page with notes.
|
|
let mut after_on_page: Option<f32> = None;
|
|
while let Some(l) = pl.lines.get(fit.l1) {
|
|
let line_notes = notes_on_line(ctx, pb, l, notes, &fit.notes);
|
|
let limit = notes_limit(pb, &fit.notes, &line_notes);
|
|
let free = pb.notes.is_empty() && fit.notes.is_empty() && line_notes.is_empty();
|
|
let bottom = pb.y + lead + (l.top + fit_height(pb, l, &pl.rp, free) - first_top);
|
|
// On a page with footnotes the paragraph's last line keeps its space after clear of
|
|
// the separator (Word): if the space doesn't fit, the line goes to the next page (and,
|
|
// with widow control, one more with it). Without notes the space after may run past
|
|
// the bottom margin as usual.
|
|
let tail = if !free && fit.l1 + 1 == n {
|
|
*after_on_page.get_or_insert_with(|| space_after_kept(ctx, self.block, &pl.rp, self.label.is_some()))
|
|
} else {
|
|
0.0
|
|
};
|
|
let over = bottom + tail > limit + 0.01;
|
|
let split = bottom > limit + 0.01 && notes_split(pb, (&fit.notes, &line_notes), bottom);
|
|
if over && fit.l1 > l0 && !split {
|
|
break;
|
|
}
|
|
if over && fit.l1 == l0 && !pb.fresh() && !split && !self.stays_at_bottom(pb, l) {
|
|
break;
|
|
}
|
|
fit.notes.extend(line_notes);
|
|
fit.l1 += 1;
|
|
if self.ends_page(pb, l, &mut fit) || split {
|
|
break;
|
|
}
|
|
}
|
|
fit
|
|
}
|
|
|
|
/// Whether the line may overflow the page: one holding nothing but a page or column break stays
|
|
/// at the bottom of the page even when it doesn't fit (it only ends the page, and moving it
|
|
/// would leave a blank page behind it, as in Word), and so does an empty paragraph closing a
|
|
/// section before a new page (measured in Word: it stays on a full page instead of starting
|
|
/// one of its own).
|
|
fn stays_at_bottom(&self, pb: &PageBuilder, l: ¶::Line) -> bool {
|
|
let p = self.p;
|
|
let only_break = matches!(l.end, LineEnd::PageBreak | LineEnd::ColumnBreak)
|
|
&& p.text
|
|
.get(l.start..l.stop)
|
|
.is_some_and(|t| t.chars().all(|c| matches!(c, wordcraft_doc::para::PAGE_BREAK | wordcraft_doc::para::COLUMN_BREAK)));
|
|
only_break || (pb.next_starts_page && p.section.is_some() && p.text.is_empty())
|
|
}
|
|
|
|
/// Whether the line (just taken, `fit.l1` is after it) forces a break, noted in `fit`.
|
|
fn ends_page(&self, pb: &PageBuilder, l: ¶::Line, fit: &mut Fit) -> bool {
|
|
if !matches!(l.end, LineEnd::PageBreak | LineEnd::ColumnBreak) || pb.web {
|
|
return false;
|
|
}
|
|
fit.forced = Some(l.end);
|
|
let mark_follows = self.pl.lines.get(fit.l1).is_some_and(|m| only_markers(self.p, m.start..m.stop));
|
|
if l.end == LineEnd::PageBreak && fit.l1 + 1 == self.n() && mark_follows {
|
|
fit.l1 += 1;
|
|
fit.mark_with_break = true;
|
|
}
|
|
true
|
|
}
|
|
|
|
/// The end of the lines on this page after widow/orphan control, and keeping the rows beside
|
|
/// floating objects together.
|
|
fn settled_end(&self, pb: &PageBuilder, fit: &Fit) -> usize {
|
|
let (n, l0, mut l1) = (self.n(), self.l0, fit.l1);
|
|
// Widow/orphan control (2-line minimum at either end).
|
|
if fit.forced.is_none() && l1 < n && self.pl.rp.widow_control && n >= 2 {
|
|
if l1 - l0 == 1 && l0 == 0 && !pb.fresh() {
|
|
l1 = l0; // orphan: move the first line to the next page
|
|
} else if n - l1 == 1 && (l1 - l0 >= 3 || (l0 > 0 && l1 - l0 >= 2)) {
|
|
l1 -= 1; // widow: take one more line along
|
|
} else if n - l1 == 1 && l0 == 0 && l1 - l0 == 2 && !pb.fresh() {
|
|
// Neither split of a three-line paragraph is allowed (two and a widow, or an
|
|
// orphan and two): it moves whole, as in Word.
|
|
l1 = l0;
|
|
}
|
|
}
|
|
// A row (text both sides of a floating object) stays together.
|
|
while l1 > l0 && l1 < n && self.pl.lines.get(l1).is_some_and(|l| l.beside) {
|
|
l1 -= 1;
|
|
}
|
|
l1
|
|
}
|
|
|
|
/// Nothing fits on this page: move to the next column or page, and place the floats again
|
|
/// there if this is the paragraph's start.
|
|
fn move_on(&mut self, ctx: &mut Ctx, pb: &mut PageBuilder) {
|
|
pb.advance(self.block, self.body_top);
|
|
if self.l0 == 0 {
|
|
let y0 = pb.y;
|
|
self.reanchor(ctx, pb, (y0, 0.0), &[]);
|
|
self.anchor_y = y0;
|
|
// Its floats are checked again on the new page.
|
|
(self.rechecked, self.forced) = (false, Vec::new());
|
|
}
|
|
}
|
|
|
|
/// Put the lines `l0..fit.l1` on the page, with their notes, objects and line numbers.
|
|
fn emit(&mut self, ctx: &mut Ctx, pb: &mut PageBuilder, rooms: Rooms, (lead, fit, notes): (f32, Fit, &[(usize, u32)])) {
|
|
let (l0, l1, n) = (self.l0, fit.l1, self.n());
|
|
add_notes(pb, fit.notes, notes, self.pl.lines.get(l1.saturating_sub(1)).map_or(0, |l| l.c1));
|
|
// Mirror indents: on an even (left-hand) page the inside indent is on the right, so the
|
|
// paragraph's lines (broken for the same width) move over by the indents' difference.
|
|
let x = pb.col_x() + mirror_shift(&self.pl.rp, pb.number, pb.web);
|
|
let y = pb.y + lead;
|
|
// Notes that no longer fit below the text (a long one, or the rest of one continued from
|
|
// the page before) get what's left under it and continue on the next page.
|
|
if !pb.notes.is_empty()
|
|
&& !pb.web
|
|
&& let Some(placed_bottom) = item_bottom(y, &self.pl, l0, l1)
|
|
&& placed_bottom > pb.bottom + 0.01
|
|
{
|
|
pb.bottom = pb.body_floor().max(placed_bottom).min(pb.orig_bottom);
|
|
}
|
|
let mut items = Vec::new();
|
|
let mut edges = Edges { top: if l0 == 0 { rooms.top } else { 0.0 }, bottom: if l1 >= n { rooms.bottom } else { 0.0 }, up: 0.0 };
|
|
if l0 == 0
|
|
&& rooms.above
|
|
&& let Some((page, col, end)) = pb.para_end
|
|
&& page == pb.pages.len()
|
|
&& col == pb.col
|
|
{
|
|
edges.up = (y - end).max(0.0);
|
|
}
|
|
let end = push_para(&mut items, (StoryRef::Body, &[self.block as u32]), &self.pl, (l0, l1), (x, y), self.width, edges);
|
|
pb.para_end = Some((pb.pages.len(), pb.col, end));
|
|
// Late floats whose anchor line came here after all are placed with these lines.
|
|
self.late.piece = (pb.pages.len(), pb.col);
|
|
if !pb.web && !self.late.placed.is_empty() {
|
|
let top = if l0 == 0 { self.anchor_y } else { y - self.space_here };
|
|
self.place_due(ctx, pb, 0..l1, top);
|
|
}
|
|
self.space_here = 0.0;
|
|
// Pictures, shapes and text boxes in these lines (web view: floats placed by this piece).
|
|
let at = ObjFrame {
|
|
page: Some(PageFrame { sect: pb.sect, margin_top: None, origin: (0.0, 0.0), number: pb.number, mirror: ctx.mirror }),
|
|
col: (x, self.width),
|
|
para_y: if pb.web { y } else { self.anchor_y },
|
|
table_chr: None,
|
|
pinned: None,
|
|
late: Some(&self.late),
|
|
};
|
|
let (behind, front) = place_objects(
|
|
ctx,
|
|
&ObjectPlacement {
|
|
story: StoryRef::Body,
|
|
path: &[self.block as u32],
|
|
p: self.p,
|
|
pl: &self.pl,
|
|
lines: (l0, l1),
|
|
origin: (x, y),
|
|
at: &at,
|
|
floats: &self.float_rects,
|
|
depth: 0,
|
|
},
|
|
);
|
|
self.late.drew();
|
|
items.extend(front);
|
|
items.extend(line_numbers(ctx, pb, &self.pl, (l0, l1), y));
|
|
add_items(pb, behind, items);
|
|
if let (Some(f), Some(l)) = (self.pl.lines.get(l0), self.pl.lines.get(l1 - 1)) {
|
|
pb.y = y + (l.top + l.height - f.top);
|
|
pb.content_bottom = pb.content_bottom.max(pb.y);
|
|
}
|
|
self.l0 = l1;
|
|
}
|
|
|
|
/// Start the next page or column if the lines just placed end one, or are not the last.
|
|
fn after_lines(&mut self, ctx: &mut Ctx, pb: &mut PageBuilder, forced: Option<LineEnd>, mark_with_break: bool) {
|
|
let (block, body_top) = (self.block, self.body_top);
|
|
// A paragraph that starts with a page break starts on the new page, its space before
|
|
// there (Word).
|
|
let starts_with_break = self.l0 == 1
|
|
&& self.pl.lines.first().is_some_and(|l| {
|
|
self.p.text.get(l.start..l.stop).is_some_and(|t| !t.is_empty() && t.chars().all(|c| c == wordcraft_doc::para::PAGE_BREAK))
|
|
});
|
|
match forced {
|
|
Some(LineEnd::PageBreak) if mark_with_break => {
|
|
pb.new_page(block, body_top);
|
|
pb.after_break_mark = true;
|
|
}
|
|
Some(LineEnd::PageBreak) if starts_with_break && !pb.web => {
|
|
pb.new_page(block, body_top);
|
|
self.space_here = self.pl.rp.space_before.max(0.0);
|
|
pb.y += self.space_here;
|
|
}
|
|
Some(LineEnd::PageBreak) => pb.new_page(block, body_top),
|
|
Some(LineEnd::ColumnBreak) => {
|
|
pb.col_break_page = Some(pb.pages.len().saturating_sub(1));
|
|
pb.advance(block, body_top);
|
|
self.rewidth_after_column_break(ctx, pb);
|
|
}
|
|
_ if self.l0 < self.n() => pb.advance(block, body_top),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
/// Unequal columns: the text after a column break is broken for its own column's width (Word),
|
|
/// not the width of the column it started in.
|
|
fn rewidth_after_column_break(&mut self, ctx: &mut Ctx, pb: &mut PageBuilder) {
|
|
if (pb.col_w() - self.width).abs() <= 0.5 {
|
|
return;
|
|
}
|
|
let Some(resume) = self.pl.lines.get(self.l0).map(|l| l.start) else { return };
|
|
let y0 = pb.y;
|
|
// The floats were placed and drawn with the first piece: they stay there.
|
|
let (p, block, excl_mark, eq_start, label) = (self.p, self.block, self.excl_mark, self.eq_start, &self.label);
|
|
let (rects, relaid, relaid_rel, _) = with_page_map(ctx, &mut self.page_map, |ctx| {
|
|
again(ctx, eq_start, |ctx| anchor_floats(ctx, pb, FloatAnchor { p, block, y0, before: 0.0, excl_mark, label, place: false }, &[]))
|
|
});
|
|
if let Some(k) = relaid.lines.iter().position(|l| l.start == resume) {
|
|
(self.float_rects, self.pl, self.rel) = (rects, relaid, relaid_rel);
|
|
(self.laid_page, self.laid_width) = (pb.number, pb.col_w());
|
|
self.anchor_y = y0;
|
|
self.l0 = k;
|
|
}
|
|
}
|
|
|
|
/// Late floats anchored in the lines that fit in this column are placed here, so these lines
|
|
/// are broken again around them ([`Flow::relay_wrap`]).
|
|
fn place_due_late(&mut self, ctx: &mut Ctx, pb: &mut PageBuilder) {
|
|
if self.l0 == 0 || pb.web || self.late.placed.is_empty() {
|
|
return;
|
|
}
|
|
self.late.piece = (pb.pages.len(), pb.col);
|
|
let lines = self.l0..fitting(&self.pl, self.l0, pb.y, pb.bottom, (0, pb.top));
|
|
self.place_due(ctx, pb, lines, pb.y - self.space_here);
|
|
}
|
|
|
|
/// Place the late floats due with this piece whose anchor line is in `lines`, positioned from
|
|
/// `top`.
|
|
fn place_due(&mut self, ctx: &mut Ctx, pb: &mut PageBuilder, lines: std::ops::Range<usize>, top: f32) {
|
|
for oi in self.late.due(self.p, &self.pl, lines) {
|
|
place_late(ctx, pb, self.p, oi, top, &mut self.late);
|
|
}
|
|
}
|
|
|
|
/// Late floats placed for lines that went on after all (past `l1`) wait for the next piece;
|
|
/// one whose own wrap pushed its line on stays, as before. Whether the lines from `l0` are to
|
|
/// be broken again without them. Does nothing once the wrap relays are used up
|
|
/// ([`MAX_WRAP_RELAYS`]).
|
|
fn settle_late(&mut self, pb: &mut PageBuilder, l1: usize) -> bool {
|
|
let l0 = self.l0;
|
|
if l0 == 0 || pb.web || self.late.placed.is_empty() || self.wrap_relays >= MAX_WRAP_RELAYS {
|
|
return false;
|
|
}
|
|
let end = if l1 == l0 && pb.fresh() { l0 + 1 } else { l1 }; // what the overflow keeps
|
|
let moved = self.late.release(self.p, &self.pl, end, &mut pb.excl) || self.late.keep_back(self.p, &self.pl, l0..end);
|
|
moved && l1 > l0
|
|
}
|
|
|
|
/// The first piece's lines end at `l1` on this page. Floats anchored past them, on the next
|
|
/// page, though [`anchor_floats`] thought they would fit here (footnotes, widow control…),
|
|
/// are held back after all, once; and those held back whose anchor came back once they were
|
|
/// out of the way are placed here as before, with the text wrapping around them rather than
|
|
/// over it (before the notes of these lines are kept: the wrap can push their references on).
|
|
/// Whether the paragraph was laid out again.
|
|
fn recheck_late(&mut self, ctx: &mut Ctx, pb: &mut PageBuilder, l1: usize) -> bool {
|
|
if self.l0 != 0 || pb.web {
|
|
return false;
|
|
}
|
|
if l1 < self.n() && !self.rechecked && pb.col + 1 >= pb.cols.len() {
|
|
self.rechecked = true;
|
|
self.forced = late_floats(self.p, &self.pl, l1).into_iter().filter(|oi| !self.late.placed.contains_key(oi)).collect();
|
|
if !self.forced.is_empty() {
|
|
let mut force = self.forced.clone();
|
|
force.extend(self.late.placed.keys());
|
|
force.sort_unstable();
|
|
let top = (pb.y, pb.y - self.anchor_y);
|
|
self.reanchor(ctx, pb, top, &force);
|
|
return true;
|
|
}
|
|
}
|
|
let back = if self.forced.is_empty() { Vec::new() } else { self.late.due(self.p, &self.pl, 0..l1) };
|
|
if !back.iter().any(|oi| self.forced.contains(oi)) {
|
|
return false;
|
|
}
|
|
// The others are still held back.
|
|
self.forced.retain(|oi| !back.contains(oi));
|
|
let (keep, top) = (std::mem::take(&mut self.forced), (pb.y, pb.y - self.anchor_y));
|
|
self.reanchor(ctx, pb, top, &keep);
|
|
true
|
|
}
|
|
|
|
/// Leave the space after the paragraph, for the next one to merge with.
|
|
fn finish(&self, pb: &mut PageBuilder, rooms: Rooms) {
|
|
// A mark that stayed with its page break keeps its space after on that page too, and its
|
|
// bottom border, drawn there: the next page starts with the next paragraph (Word,
|
|
// compatibility modes 12 to 15).
|
|
let rp = &self.pl.rp;
|
|
let (bottom_room, space_after) = if pb.after_break_mark { (0.0, 0.0) } else { (rooms.bottom, rp.space_after) };
|
|
pb.y += bottom_room + space_after;
|
|
pb.prev = Some((rp.style.clone(), rp.contextual_spacing, space_after, rp.after_auto, self.label.is_some()));
|
|
}
|
|
}
|
|
|
|
/// The footnotes referenced on line `l` that aren't on the page yet, laid out.
|
|
fn notes_on_line(ctx: &mut Ctx, pb: &PageBuilder, l: ¶::Line, notes: &[(usize, u32)], new_notes: &[NoteBox]) -> Vec<NoteBox> {
|
|
let mut out = Vec::new();
|
|
if pb.web {
|
|
return out;
|
|
}
|
|
for (ci, id) in notes {
|
|
if *ci >= l.c0
|
|
&& *ci < l.c1
|
|
&& !pb.notes.iter().chain(new_notes).any(|x| x.id == *id)
|
|
&& let Some(part) = ctx.doc.parts.get(id).filter(|p| p.kind == wordcraft_doc::PartKind::Footnote)
|
|
{
|
|
let blocks = part.blocks.clone();
|
|
let BoxLayout { items, height: h, .. } = layout_box(ctx, BoxInput::other(StoryRef::Part(*id), &blocks, pb.sect.text_width(), 0, None));
|
|
out.push(NoteBox { id: *id, items, h, cont: false });
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The bottom of the room for text on a page with the notes of the lines before (`new_notes`)
|
|
/// and those of this line (`line_notes`) at its foot.
|
|
fn notes_limit(pb: &PageBuilder, new_notes: &[NoteBox], line_notes: &[NoteBox]) -> f32 {
|
|
let height = |notes: &[NoteBox]| notes.iter().map(|n| n.h).sum::<f32>();
|
|
let extra = height(line_notes) + if pb.notes.is_empty() && new_notes.is_empty() && !line_notes.is_empty() { pb.seps.sep_h(false) } else { 0.0 };
|
|
pb.bottom - height(new_notes) - if pb.notes.is_empty() && !new_notes.is_empty() { pb.seps.sep_h(false) } else { 0.0 } - extra
|
|
}
|
|
|
|
/// How much of the line has to fit. Multiple spacing above single puts its extra leading below
|
|
/// the text (see `baseline_offset`), and Word lets that leading of a page's last line run into the
|
|
/// bottom margin: only the text has to fit (observed: a double-spaced 12pt line stays on the page
|
|
/// while its text ends above the margin, about half a line lower than when the whole line must
|
|
/// fit). Only the bottom margin takes that leading: a footer taller than the margin, or
|
|
/// footnotes (`free` is false), keep the whole line clear of them.
|
|
fn fit_height(pb: &PageBuilder, l: ¶::Line, rp: &wordcraft_doc::resolve::ResolvedPara, free: bool) -> f32 {
|
|
let at_margin = pb.bottom >= pb.sect.page_h - pb.sect.margin_bottom - 0.01;
|
|
match rp.line_spacing {
|
|
// `baseline` is from the paragraph's top; the text ends a descent below it.
|
|
wordcraft_doc::props::LineSpacing::Multiple(m) if m > 1.0 && at_margin && free => (l.baseline - l.top + l.text_desc).clamp(0.0, l.height),
|
|
_ => l.height,
|
|
}
|
|
}
|
|
|
|
/// Notes too long for the rest of the page continue on the next one: the line still fits if the
|
|
/// notes before its last one, and that one's first line, fit below it. The notes then take the
|
|
/// rest of the page.
|
|
fn notes_split(pb: &PageBuilder, (new_notes, line_notes): (&[NoteBox], &[NoteBox]), bottom: f32) -> bool {
|
|
line_notes.split_last().is_some_and(|(last, others)| {
|
|
let before: f32 = pb.notes.iter().chain(new_notes).chain(others).map(|n| n.h).sum();
|
|
let sep = pb.seps.sep_h(pb.notes.first().is_some_and(|n| n.cont));
|
|
pb.body_floor().max(bottom) + sep + before + last.first_piece() + pb.seps.notice_h() <= pb.orig_bottom + 0.01
|
|
})
|
|
}
|
|
|
|
/// Keep the notes of the lines that are placed (those referenced before `placed_end`) on the page.
|
|
fn add_notes(pb: &mut PageBuilder, mut new_notes: Vec<NoteBox>, notes: &[(usize, u32)], placed_end: usize) {
|
|
new_notes.retain(|n| notes.iter().any(|(ci, nid)| *nid == n.id && *ci < placed_end));
|
|
let before = pb.notes_h();
|
|
pb.notes.extend(new_notes);
|
|
if pb.balance.is_some() {
|
|
// Balanced columns end where the balance puts them unless the notes reach higher.
|
|
pb.cap_columns();
|
|
} else {
|
|
// Relative, so a bottom moved down for notes continuing on the next page stays put.
|
|
pb.bottom -= pb.notes_h() - before;
|
|
}
|
|
}
|
|
|
|
/// Line numbers in the left margin for lines `l0..l1` of `pl` laid out from `y`.
|
|
fn line_numbers(ctx: &mut Ctx, pb: &mut PageBuilder, pl: &ParaLayout, (l0, l1): (usize, usize), y: f32) -> Vec<Placed> {
|
|
let mut items = Vec::new();
|
|
if let Some(ln) = pb.sect.line_numbers.clone().filter(|_| !pb.web && pl.rp.style != "Header" && !pl.rp.suppress_line_numbers)
|
|
&& let Some(first) = pl.lines.get(l0)
|
|
{
|
|
for li in l0..l1 {
|
|
let Some(line) = pl.lines.get(li).filter(|l| !l.beside) else { continue };
|
|
pb.line_no = pb.line_no.saturating_add(1);
|
|
let n = pb.line_no.saturating_add(ln.start.saturating_sub(1));
|
|
if ln.count_by > 1 && !n.is_multiple_of(ln.count_by) {
|
|
continue;
|
|
}
|
|
let num = ctx.number_para(n);
|
|
let w = num.lines.first().and_then(|l| l.xs.last().copied()).unwrap_or(10.0);
|
|
let dist = if ln.distance > 0.0 { ln.distance } else { 18.0 };
|
|
let ly = y + (line.top - first.top) + (line.baseline - line.top) - num.lines.first().map(|l| l.baseline).unwrap_or(10.0);
|
|
items.push(Placed::Lines {
|
|
story: DECOR_STORY,
|
|
path: Path(vec![]),
|
|
para: num.clone(),
|
|
l0: 0,
|
|
l1: 1,
|
|
x: pb.sect.margin_left + pb.sect.gutter - dist - w,
|
|
y: ly,
|
|
turn: TextDirection::Horizontal,
|
|
});
|
|
}
|
|
}
|
|
items
|
|
}
|
|
|
|
/// Add a piece's items to the last page: those `behind` the text at the front, the rest at the end.
|
|
fn add_items(pb: &mut PageBuilder, behind: Vec<Placed>, items: Vec<Placed>) {
|
|
let n_behind = behind.len();
|
|
if let Some(pg) = pb.pages.last_mut() {
|
|
for (i, it) in behind.into_iter().enumerate() {
|
|
pg.items.insert(i.min(pg.items.len()), it);
|
|
}
|
|
pg.items.extend(items);
|
|
}
|
|
if let Some(b) = pb.behind.last_mut() {
|
|
*b += n_behind;
|
|
}
|
|
}
|
|
|
|
fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize, body_top: f32) {
|
|
let mut f = Flow::new(ctx, pb, p, (block, body_top));
|
|
let spot = |pb: &PageBuilder| (pb.pages.len(), pb.col);
|
|
let placed_at = spot(pb);
|
|
// A continuous section's top mid-page is not a page top, so the break still applies.
|
|
let broke_before = f.pl.rp.page_break_before && !pb.fresh() && !pb.web;
|
|
if broke_before {
|
|
pb.new_page(block, body_top);
|
|
}
|
|
let before = f.gap_before(ctx, pb, broke_before);
|
|
pb.y += before;
|
|
let rooms = Rooms::of(ctx, block, &f.pl);
|
|
let applied_before = f.keep_together(ctx, pb, rooms, before);
|
|
// A different top only matters when there is something to wrap around.
|
|
if spot(pb) != placed_at || ((pb.y - f.guess).abs() > 0.01 && !pb.excl.is_empty()) {
|
|
let y0 = pb.y;
|
|
f.reanchor(ctx, pb, (y0, applied_before), &[]);
|
|
f.anchor_y = y0 - applied_before;
|
|
}
|
|
while f.l0 < f.n() {
|
|
f.place_due_late(ctx, pb);
|
|
f.relay_wrap(ctx, pb);
|
|
f.relay_pages(ctx, pb);
|
|
// What is laid out for these lines from here (their footnotes) is on this page.
|
|
ctx.fields.page = pb.number;
|
|
// A paragraph's first line may start below its top (pushed down by a floating object).
|
|
let Some(first) = f.pl.lines.get(f.l0) else { break };
|
|
let lead = if f.l0 == 0 { first.top.max(0.0) + rooms.top } else { 0.0 };
|
|
let notes = para_notes(ctx.doc, p, &f.pl);
|
|
let mut fit = f.fit_lines(ctx, pb, ¬es, lead);
|
|
fit.l1 = f.settled_end(pb, &fit);
|
|
if f.settle_late(pb, fit.l1) {
|
|
continue;
|
|
}
|
|
if fit.l1 == f.l0 {
|
|
if !pb.fresh() {
|
|
f.move_on(ctx, pb);
|
|
continue;
|
|
}
|
|
fit.l1 = f.l0 + 1; // can't fit even one line on an empty page: overflow
|
|
}
|
|
if f.recheck_late(ctx, pb, fit.l1) {
|
|
continue;
|
|
}
|
|
let (forced, mark_with_break) = (fit.forced, fit.mark_with_break);
|
|
f.emit(ctx, pb, rooms, (lead, fit, ¬es));
|
|
f.after_lines(ctx, pb, forced, mark_with_break);
|
|
}
|
|
f.finish(pb, rooms);
|
|
}
|
|
|
|
/// Whether `range` of `p`'s text holds nothing but zero-width markers (bookmarks, field and
|
|
/// control boundaries…), or nothing at all: a line of them takes no room after a page break.
|
|
fn only_markers(p: &Paragraph, range: std::ops::Range<usize>) -> bool {
|
|
let Some(text) = p.text.get(range.clone()) else { return false };
|
|
text.char_indices().all(|(i, c)| c == wordcraft_doc::para::OBJ && p.object_at(range.start + i).is_some_and(InlineObject::is_marker))
|
|
}
|
|
|
|
/// The outline of the body for an Outline view pass.
|
|
pub(crate) struct OutlinePass {
|
|
view: outline::OutlineView,
|
|
levels: Vec<u8>,
|
|
pub(crate) hidden: Vec<bool>,
|
|
}
|
|
|
|
impl OutlinePass {
|
|
pub(crate) fn new(doc: &Document, view: outline::OutlineView) -> Self {
|
|
let levels = outline::levels(doc);
|
|
let hidden = outline::hidden(&levels, &view);
|
|
OutlinePass { view, levels, hidden }
|
|
}
|
|
}
|
|
|
|
/// A body paragraph in Outline view (one long sheet: no page or column breaks, no floats
|
|
/// wrapping): indented by its level behind its symbol; hidden when collapsed or deeper than Show
|
|
/// Level; body text cut to its first line with Show First Line Only.
|
|
pub(crate) fn place_outline_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize, o: &OutlinePass) {
|
|
// List numbers count the hidden paragraphs too.
|
|
let label = ctx.next_label(p);
|
|
if o.hidden.get(block).copied().unwrap_or(false) {
|
|
return;
|
|
}
|
|
let level = o.levels.get(block).copied().unwrap_or(outline::BODY);
|
|
let indent = outline::indent(&o.levels, block);
|
|
let x = pb.col_x() + indent + outline::SYMBOL_W;
|
|
let width = (pb.col_w() - indent - outline::SYMBOL_W).max(72.0);
|
|
let q = outline::outline_para(ctx.doc, p, o.view.plain);
|
|
ctx.fields.page = pb.number;
|
|
let pl = ctx.para_labelled(&q, width, None, &[], label);
|
|
pb.y += pl.rp.space_before.max(0.0);
|
|
let n = pl.lines.len();
|
|
let l1 = if o.view.first_line_only && level >= outline::BODY { n.min(1) } else { n };
|
|
// Its borders take their room above and below its lines.
|
|
let edges = Edges::own(&pl, 0, l1);
|
|
let y = pb.y + pl.lines.first().map(|f| f.top.max(0.0)).unwrap_or(0.0) + edges.top;
|
|
let bottom_room = edges.bottom;
|
|
let mut items = Vec::new();
|
|
push_para(&mut items, (StoryRef::Body, &[block as u32]), &pl, (0, l1), (x, y), width, edges);
|
|
let at = ObjFrame {
|
|
page: Some(PageFrame { sect: pb.sect, margin_top: None, origin: (0.0, 0.0), number: pb.number, mirror: ctx.mirror }),
|
|
col: (x, width),
|
|
para_y: y,
|
|
table_chr: None,
|
|
pinned: None,
|
|
late: None,
|
|
};
|
|
let (behind, front) = place_objects(
|
|
ctx,
|
|
&ObjectPlacement {
|
|
story: StoryRef::Body,
|
|
path: &[block as u32],
|
|
p: &q,
|
|
pl: &pl,
|
|
lines: (0, l1),
|
|
origin: (x, y),
|
|
at: &at,
|
|
floats: &HashMap::new(),
|
|
depth: 0,
|
|
},
|
|
);
|
|
items.extend(front);
|
|
let children = level < outline::BODY && outline::has_children(&o.levels, block);
|
|
// Collapsed, or with something under it hidden by Show Level: the wavy line says so.
|
|
let collapsed = children && o.hidden.get(block + 1).copied().unwrap_or(false);
|
|
items.extend(outline::symbol_items(&pl, x, y, level, children, collapsed));
|
|
if let Some(pg) = pb.page() {
|
|
for (i, it) in behind.into_iter().enumerate() {
|
|
pg.items.insert(i.min(pg.items.len()), it);
|
|
}
|
|
pg.items.extend(items);
|
|
}
|
|
if let (Some(f), Some(l)) = (pl.lines.first(), l1.checked_sub(1).and_then(|k| pl.lines.get(k))) {
|
|
pb.y = y + (l.top + l.height - f.top);
|
|
}
|
|
pb.y += bottom_room + pl.rp.space_after.max(0.0);
|
|
}
|
|
|
|
/// How far a paragraph with mirrored indents (`w:mirrorIndents`) moves right on page `number`:
|
|
/// its left and right indents are the inside and outside ones, which swap sides on even pages.
|
|
fn mirror_shift(rp: &wordcraft_doc::resolve::ResolvedPara, number: u32, web: bool) -> f32 {
|
|
if !rp.mirror_indents || web || !number.is_multiple_of(2) {
|
|
return 0.0;
|
|
}
|
|
let (left, right) = rp.visual_indents();
|
|
right - left
|
|
}
|
|
|
|
/// The space after body paragraph `block` (resolved as `rp`) that is really left below it: none
|
|
/// when contextual spacing or automatic spacing between list items drops it before the next
|
|
/// paragraph. Same tests as the spacing between paragraphs in `place_para_in_flow`: contextual
|
|
/// spacing removes this paragraph's space after only when this one asks for it, and the next
|
|
/// counts as a list item when it gets a label.
|
|
fn space_after_kept(ctx: &Ctx, block: usize, rp: &wordcraft_doc::resolve::ResolvedPara, listed: bool) -> f32 {
|
|
let after = rp.space_after.max(0.0);
|
|
if after <= 0.0 {
|
|
return 0.0;
|
|
}
|
|
if let Some(Block::Para(next)) = ctx.doc.body.get(block + 1).map(|b| &**b) {
|
|
let nrp = ctx.doc.styles.resolve_para(&next.props);
|
|
if nrp.style == rp.style && rp.contextual_spacing {
|
|
return 0.0;
|
|
}
|
|
if listed && rp.after_auto && ctx.has_label(next) {
|
|
return 0.0;
|
|
}
|
|
}
|
|
after
|
|
}
|
|
|
|
/// The room the paragraphs after `block` need on the page for keep with next: enough of the
|
|
/// next one to start there — its first line, two of its lines with widow/orphan control (it
|
|
/// doesn't leave a single line at the bottom of a page; it moves over whole instead), all three
|
|
/// of a three-line one under that control (two would leave a widow), or all
|
|
/// of it under keep lines together — or all of it and what it keeps with in turn when it is
|
|
/// kept with the next too (a chain moves together in Word). Word carries a kept heading over
|
|
/// when the paragraph after it cannot start on the page (#966).
|
|
fn next_first_line(ctx: &mut Ctx, block: usize, width: f32, depth: usize, counters: &mut Counters) -> f32 {
|
|
match ctx.doc.body.get(block + 1).map(|b| &**b) {
|
|
Some(Block::Para(p)) => {
|
|
// Keep list indents and label metrics without consuming the document counters.
|
|
let rp = ctx.doc.styles.resolve_para(&p.props);
|
|
let label =
|
|
p.props.numbering.or(rp.numbering).filter(|n| n.num != 0).and_then(|n| counters.next_label(&ctx.doc.numbering, n.num, n.level));
|
|
let pl = match ctx.para_ahead(p, width, label.clone()) {
|
|
Some(pl) => pl,
|
|
None => {
|
|
let env = para::ParaEnv {
|
|
doc: ctx.doc,
|
|
width,
|
|
label,
|
|
fields: &ctx.fields,
|
|
show_hidden: ctx.opts.show_hidden,
|
|
field_codes: ctx.opts.field_codes,
|
|
hide_deleted: ctx.opts.hides_deleted(StoryRef::Body),
|
|
table: None,
|
|
proofing: None,
|
|
typing_word: None,
|
|
numeral: ctx.opts.numeral,
|
|
exclusions: &[],
|
|
eq_number: ctx.eq_count,
|
|
read_line_spacing: if ctx.opts.view == ViewMode::Read { ctx.opts.read_line_spacing } else { None },
|
|
grid: ctx.grid,
|
|
fit_objects: false,
|
|
fitted: &[],
|
|
};
|
|
Arc::new(para::layout_para(p, &env))
|
|
}
|
|
};
|
|
// Its border room, unless it joins the paragraph before (or after) in one box.
|
|
let own = Edges::own(&pl, 0, pl.lines.len());
|
|
let top = if joined(ctx.doc, &ctx.doc.body, block + 1, Some(block), None) { 0.0 } else { own.top };
|
|
if pl.rp.keep_next && depth < 8 {
|
|
let bottom = if joined(ctx.doc, &ctx.doc.body, block + 1, Some(block + 2), None) { 0.0 } else { own.bottom };
|
|
pl.rp.space_before + top + pl.height + bottom + pl.rp.space_after + next_first_line(ctx, block + 1, width, depth + 1, counters)
|
|
} else {
|
|
// The room this one needs to start on the page with the kept paragraph before
|
|
// it (#966).
|
|
let to = |k: usize| match (pl.lines.first(), pl.lines.get(k)) {
|
|
(Some(first), Some(last)) => last.top + last.height - first.top,
|
|
_ => 0.0,
|
|
};
|
|
pl.rp.space_before
|
|
+ top
|
|
+ if pl.rp.keep_lines {
|
|
pl.height
|
|
} else if pl.rp.widow_control && pl.lines.len() >= 2 {
|
|
// Neither split of three lines is allowed (`settled_end`).
|
|
to(if pl.lines.len() == 3 { 2 } else { 1 })
|
|
} else {
|
|
to(0)
|
|
}
|
|
}
|
|
}
|
|
Some(Block::Table(_)) => 24.0,
|
|
None => 0.0,
|
|
}
|
|
}
|