Files
wordcraft/crates/layout/src/boxes.rs
T
alehelandClaude Opus 5.5 56c85834f1 Merge origin/main into the ODT branch (gap 19)
Conflicts kept both sides. Where the two branches met:
- Para.mark: both added it. It stays CharProps (ODT needs it); HTML's
  `<p>&nbsp;</p>` converts its Fmt, and to_doc reads it in one place.
- Arrowheads: the PR's line ends (top left to bottom right) with main's
  arrowhead styles.
- inline_rect: the PR's depth below the baseline with main's rule room.
- Spacing and Cell literals take main's new fields.
- The zip entry guard (#3660) also covers the encrypted ODT paths, which
  parsed a package before it; regression test in tests_more/encryption.rs.
- chunks_exact to as_chunks in odt/frames.rs and odt/path.rs (clippy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:51:35 +01:00

663 lines
29 KiB
Rust

use super::ctx::{Ctx, page_anchored};
use super::floats::{
ObjFrame, ObjectPlacement, WrapArea, align_in, float_rect, floating, left_out_objects, place_objects, rel_exclusions, wrap_area,
};
use super::page::PageFrame;
use super::placed::Placed;
use super::table_flow::below;
use super::{
Anchor, Arc, Block, Blocks, Border, Document, Float, FloatAlign, HashMap, ParaLayout, Paragraph, Path, Rect, StoryRef, TableFloat, TextDirection,
Wrap, WrapText, para, table,
};
/// Cell identity is independent of optional table-style formatting.
pub(crate) enum BoxContext<'a> {
Other,
Cell(Option<&'a para::CellText>),
}
pub(crate) struct BoxInput<'a> {
pub(crate) story: StoryRef,
pub(crate) blocks: &'a Blocks,
pub(crate) prefix: &'a [u32],
pub(crate) width: f32,
pub(crate) context: BoxContext<'a>,
pub(crate) depth: usize,
pub(crate) frame: Option<PageFrame<'a>>,
}
impl<'a> BoxInput<'a> {
pub(crate) fn other(story: StoryRef, blocks: &'a Blocks, width: f32, depth: usize, frame: Option<PageFrame<'a>>) -> Self {
Self { story, blocks, prefix: &[], width, context: BoxContext::Other, depth, frame }
}
pub(crate) fn cell(story: StoryRef, blocks: &'a Blocks, prefix: &'a [u32], width: f32, cell: Option<&'a para::CellText>, depth: usize) -> Self {
Self { story, blocks, prefix, width, context: BoxContext::Cell(cell), depth, frame: None }
}
}
struct ParagraphSpacing<'a> {
index: usize,
listed: bool,
style: &'a str,
contextual: bool,
before_auto: bool,
space_before: f32,
allow_same_style_space: bool,
previous_style: Option<&'a (String, bool)>,
previous_listed: bool,
previous_after_auto: bool,
previous_after: &'a mut f32,
y: &'a mut f32,
sum_paragraph_spacing: bool,
}
struct TableBlockContext<'ctx, 'state, 'doc> {
ctx: &'ctx mut Ctx<'doc>,
story: StoryRef,
table: &'state wordcraft_doc::Table,
path: &'state [u32],
width: f32,
frame: Option<PageFrame<'state>>,
in_cell: bool,
depth: usize,
y: &'state mut f32,
items: &'state mut Vec<Placed>,
exclusions: &'state mut Vec<WrapArea>,
float_tables: &'state mut Vec<Rect>,
table_areas: &'state mut Vec<Rect>,
page_table_wraps: &'state mut Vec<WrapArea>,
previous_after: &'state mut f32,
previous_style: &'state mut Option<(String, bool)>,
}
fn paragraph_spacing(input: ParagraphSpacing<'_>) -> (bool, f32) {
let ParagraphSpacing {
index,
listed,
style,
contextual,
before_auto,
space_before,
allow_same_style_space,
previous_style,
previous_listed,
previous_after_auto,
previous_after,
y,
sum_paragraph_spacing,
} = input;
let contextual = contextual && !allow_same_style_space;
let mut before = space_before;
if let Some((previous, previous_contextual)) = previous_style
&& previous == style
&& !allow_same_style_space
{
if *previous_contextual {
*y -= *previous_after;
before = (before - *previous_after).max(0.0);
*previous_after = 0.0;
}
if contextual {
before = 0.0;
}
}
if index > 0 && previous_listed && listed {
if previous_after_auto {
*y -= *previous_after;
*previous_after = 0.0;
}
if before_auto {
before = 0.0;
}
}
if before_auto && index == 0 {
before = 0.0;
} else if index > 0 && before > 0.0 && *previous_after > 0.0 && !sum_paragraph_spacing {
before = (before - *previous_after).max(0.0);
}
(contextual, before)
}
fn layout_table_block(input: TableBlockContext<'_, '_, '_>) -> bool {
let TableBlockContext {
ctx,
story,
table: t,
path,
width,
frame,
in_cell,
depth,
y,
items,
exclusions,
float_tables,
table_areas,
page_table_wraps,
previous_after,
previous_style,
} = input;
if depth > 8 {
return false;
}
// Page tables do not contribute to the footer text's intrinsic height.
if frame.is_none()
&& !in_cell
&& matches!(story, StoryRef::Part(id) if ctx.doc.parts.get(&id).is_some_and(|p| p.kind == wordcraft_doc::PartKind::Footer))
&& t.props.float.is_some_and(|f| f.v_rel == Anchor::Page)
{
return false;
}
let layout = table::layout_table(ctx, story, t, path, width, depth + 1);
let (tx, ty) = match t.props.float {
Some(float) => {
let legacy = if ctx.doc.settings.compat_mode < 15 { table::first_cell_left_margin(ctx, t) } else { 0.0 };
let (rect, area) = box_float_table_rect(frame, width, *y, &layout, &float, legacy, float_tables);
exclusions.push((area, false, None, WrapText::BothSides, true));
float_tables.push(area);
table_areas.push(area);
if float.v_rel == Anchor::Page {
page_table_wraps.push((area, false, None, WrapText::BothSides, true));
}
(rect.x, rect.y)
}
None => (layout.x, below(exclusions, *y, layout.x, layout.x + layout.width)),
};
let mut row_y = ty;
for row in &layout.rows {
for item in &row.items {
let mut item = item.clone();
item.translate(tx, row_y);
items.push(item);
}
row_y += row.height;
}
if t.props.float.is_none() {
*y = row_y;
*previous_after = 0.0;
*previous_style = None;
}
true
}
/// Lay out a block list into a free-standing box of `width` (no page breaks): table cells,
/// headers, footers, notes, text boxes. Returns items relative to (0, 0) and the height. `frame`
/// says where the box sits on its page, for floating objects positioned relative to the page or
/// its margins; without it (cells, notes, text boxes) they're positioned in the box.
pub(crate) fn layout_box(ctx: &mut Ctx, input: BoxInput<'_>) -> BoxLayout {
let BoxInput { story, blocks, prefix, width, context, depth, frame } = input;
// Deletions in balloons leave the body's text only: a header, note or text box keeps its own
// inline (restored for the story around this box when it's done).
let outer_hide = std::mem::replace(&mut ctx.hide_deleted, ctx.opts.hides_deleted(story));
let empty_cell = para::CellText::default();
let (table, in_cell) = match context {
BoxContext::Other => (None, false),
BoxContext::Cell(table) => (Some(table.unwrap_or(&empty_cell)), true),
};
// Word 16.113.3: the override applies in modes 11/12/14, but not mode 15.
let allow_same_style_space =
in_cell && ctx.doc.settings.allow_space_of_same_style_in_table && matches!(ctx.doc.settings.compat_mode, 11 | 12 | 14);
let mut items = Vec::new();
// Pictures behind the text go first; `behind` counts them.
let mut behind = 0;
// Wrap areas of the box's floating objects.
let mut excl: Vec<WrapArea> = Vec::new();
// Areas of the box's floating tables, which other floating tables may have to avoid.
let mut float_tables: Vec<Rect> = Vec::new();
// Deepest a floating picture or shape that takes part in the text flow reaches, so the box
// (a table cell especially) grows to hold it instead of letting it hang past the box's own
// bottom edge (#3108). Behind-text and in-front-of-text objects don't: a page-anchored logo or
// watermark behind a cell's text must not stretch its row (see `align_h` in table.rs).
let mut float_objects_bottom = 0.0f32;
let mut table_areas = Vec::new();
let mut page_table_wraps = Vec::new();
let mut y = 0.0f32;
let mut prev_after = 0.0f32;
let mut prev_after_auto = false;
let mut prev_listed = false;
let mut prev_style: Option<(String, bool)> = None;
let mut last_top = 0.0f32;
// Word has no frames in a text box: a framed paragraph there (its style's frame, usually)
// runs with the box's text (#2357).
let frames = !matches!(story, StoryRef::Part(id) if ctx.doc.parts.get(&id).is_some_and(|p| p.kind == wordcraft_doc::PartKind::TextBox));
// The bottom of the last paragraph's lines.
let mut prev_end = 0.0f32;
for (i, b) in blocks.iter().enumerate() {
last_top = y;
let mut path = prefix.to_vec();
path.push(i as u32);
match &**b {
Block::Para(p) => {
let label = ctx.next_label(p);
let listed = label.is_some();
let mut pl = ctx.para_labelled(p, width, table, &[], label.clone());
// A text frame (a footer's page number in a frame, say) is placed apart, like a
// floating object, and the text after it wraps around it; it takes no room in
// the flow.
if let Some(fr) = pl.rp.frame.clone().filter(|_| frames) {
let fw = fr.w.unwrap_or_else(|| natural_width(&pl)).clamp(1.0, 1584.0);
let pf = if (fw - width).abs() > 0.01 { ctx.para_labelled(p, fw, table, &[], label) } else { pl };
let h = match fr.h {
Some(h) if fr.h_exact => h,
Some(h) => h.max(pf.height),
None => pf.height,
};
let float = frame_float(&fr);
let r = float_rect(frame, (0.0, width), y, fw, h, &float);
push_para(&mut items, (story, &path), &pf, (0, pf.lines.len()), (r.x, r.y), fw, Edges::own(&pf, 0, pf.lines.len()));
excl.extend(wrap_area(r, &float, None));
float_tables.push(r);
continue;
}
let (ctxl, before) = paragraph_spacing(ParagraphSpacing {
index: i,
listed,
style: &pl.rp.style,
contextual: pl.rp.contextual_spacing,
before_auto: pl.rp.before_auto,
space_before: pl.rp.space_before,
allow_same_style_space,
previous_style: prev_style.as_ref(),
previous_listed: prev_listed,
previous_after_auto: prev_after_auto,
previous_after: &mut prev_after,
y: &mut y,
sum_paragraph_spacing: ctx.doc.settings.sum_paragraph_spacing,
});
// Paragraph-relative floats start above the effective space before, as in the
// body path. Keep text and wrap exclusions relative to the text's actual top.
let para_top = y;
y += if i == 0 { before } else { before.max(0.0) };
// Floating objects anchored here: place them, then wrap the text around them.
// One left out of the layout (hidden, or deleted in the final text) takes no room.
let mut floats = HashMap::new();
// A header or footer cell's floats placed on the page: no room taken in the cell.
let mut pinned = HashMap::new();
let left_out = left_out_objects(ctx.doc, p, table.map(|t| &t.chr), ctx.opts.show_hidden, ctx.hide_deleted);
for (oi, o) in p.objects.iter().enumerate() {
let Some((w, h, float)) = floating(o) else { continue };
if left_out(oi) {
continue;
}
let h = ctx.float_height(o, w, h);
if let Some(pin) = ctx.pin.as_ref().filter(|pin| in_cell && pin.story == story && float.outside_cell && page_anchored(float)) {
let page =
PageFrame { sect: &pin.sect, margin_top: pin.margin_top, origin: (0.0, 0.0), number: pin.number, mirror: pin.mirror };
pinned.insert(oi, float_rect(Some(page), (0.0, width), para_top, w, h, float));
continue;
}
// Only paragraph-relative placement excludes space-before. Line anchors
// and frameless page/margin fallbacks retain the text-top origin.
let float_y = if float.v_rel == Anchor::Paragraph { para_top } else { y };
let r = float_rect(frame, (0.0, width), float_y, w, h, float);
let outline = ctx.wrap_outline(o);
excl.extend(wrap_area(r, float, outline.as_deref().map(Vec::as_slice)));
floats.insert(oi, r);
}
let rel = rel_exclusions(&excl, 0.0, y);
if !rel.is_empty() {
pl = ctx.para_labelled(p, width, table, &rel, label);
}
// Bordered paragraphs: room for their borders, those of a group joined in one box.
let cell = table.map(|t| &t.para);
let (above, below) = (joined(ctx.doc, blocks, i, i.checked_sub(1), cell), joined(ctx.doc, blocks, i, Some(i + 1), cell));
let mut edges = Edges::own(&pl, 0, pl.lines.len());
if above {
edges.top = 0.0;
}
if below {
edges.bottom = 0.0;
}
let lead = pl.lines.first().map_or(0.0, |l| l.top.max(0.0)) + edges.top;
if above {
edges.up = (y + lead - prev_end).max(0.0);
}
prev_end = push_para(&mut items, (story, &path), &pl, (0, pl.lines.len()), (0.0, y + lead), width, edges);
// Pictures, shapes and text boxes: drawn, their areas and text boxes' text.
if !p.objects.is_empty() {
let at = ObjFrame { page: frame, col: (0.0, width), para_y: para_top, table_chr: table.map(|t| &t.chr), pinned: Some(&pinned) };
let (back, front) = place_objects(
ctx,
&ObjectPlacement {
story,
path: &path,
p,
pl: &pl,
lines: (0, pl.lines.len()),
origin: (0.0, y + lead),
at: &at,
floats: &floats,
depth,
},
);
for placed in back.iter().chain(front.iter()) {
if let Placed::Object { rect, spin, wrap, .. } = placed
&& !matches!(wrap, Wrap::Inline | Wrap::BehindText | Wrap::InFrontOfText)
{
float_objects_bottom = float_objects_bottom.max(spin.bounds(*rect).bottom());
}
}
let at = behind.min(items.len());
behind += back.len();
items.splice(at..at, back);
items.extend(front);
}
// Automatic space after: none after the last paragraph of a cell or box.
let after = if pl.rp.after_auto && i + 1 == blocks.len() { 0.0 } else { pl.rp.space_after };
y += edges.top + pl.height + edges.bottom + after;
prev_after = after;
prev_after_auto = pl.rp.after_auto;
prev_listed = listed;
prev_style = Some((pl.rp.style.clone(), ctxl));
}
Block::Table(t) => {
if !layout_table_block(TableBlockContext {
ctx,
story,
table: t,
path: &path,
width,
frame,
in_cell,
depth,
y: &mut y,
items: &mut items,
exclusions: &mut excl,
float_tables: &mut float_tables,
table_areas: &mut table_areas,
page_table_wraps: &mut page_table_wraps,
previous_after: &mut prev_after,
previous_style: &mut prev_style,
}) {
continue;
}
}
}
}
let floats_bottom = float_tables.iter().map(|r| r.bottom()).fold(float_objects_bottom, f32::max);
ctx.hide_deleted = outer_hide;
BoxLayout { items, height: y.max(0.0), floats_bottom, last_top: last_top.max(0.0), table_areas, page_table_wraps }
}
/// The width a text frame without one takes: its widest line, with the paragraph's indents.
fn natural_width(pl: &ParaLayout) -> f32 {
let widest = (0..pl.lines.len())
.filter_map(|li| {
let l = pl.lines.get(li)?;
Some((pl.x_of(li, l.stop)? - pl.x_of(li, l.start)?).abs())
})
.fold(0.0f32, f32::max);
widest + pl.rp.indent_left.max(0.0) + pl.rp.indent_right.max(0.0)
}
/// A text frame's placement as a floating object's (`w:framePr` → anchors, alignment, wrap).
fn frame_float(fr: &wordcraft_doc::props::TextFrame) -> Float {
let anchor = |a: &str, text: Anchor| match a {
"page" => Anchor::Page,
"margin" => Anchor::Margin,
_ => text,
};
let align = |a: Option<&str>| match a {
Some("left" | "top") => Some(FloatAlign::Start),
Some("center") => Some(FloatAlign::Center),
Some("right" | "bottom") => Some(FloatAlign::End),
Some("inside") => Some(FloatAlign::Inside),
Some("outside") => Some(FloatAlign::Outside),
_ => None,
};
Float {
wrap: match fr.wrap.as_str() {
"notBeside" => Wrap::TopAndBottom,
"none" => Wrap::InFrontOfText,
"tight" => Wrap::Tight,
"through" => Wrap::Through,
_ => Wrap::Square,
},
h_rel: anchor(&fr.h_anchor, Anchor::Column),
v_rel: anchor(&fr.v_anchor, Anchor::Paragraph),
x: fr.x,
y: fr.y,
h_align: align(fr.x_align.as_deref()),
v_align: align(fr.y_align.as_deref()),
dist: fr.h_space,
dist_top: fr.v_space,
dist_bottom: fr.v_space,
..Default::default()
}
}
/// A laid-out box: items relative to its top-left, the height of its text flow, where its
/// floating tables end (a table cell grows to hold them) and where its last block starts (the
/// height without it).
pub(crate) struct BoxLayout {
pub(crate) items: Vec<Placed>,
pub(crate) table_areas: Vec<Rect>,
pub(crate) page_table_wraps: Vec<WrapArea>,
pub(crate) height: f32,
pub(crate) floats_bottom: f32,
pub(crate) last_top: f32,
}
/// Where floating table `tl` goes in a box `width` wide whose next paragraph starts at `y`, and
/// the area text keeps clear of. Positions relative to the page or its margins need the box's
/// `frame`; without one (a table cell) they're relative to the box.
fn box_float_table_rect(
frame: Option<PageFrame>,
width: f32,
y: f32,
tl: &table::TableLayout,
f: &TableFloat,
legacy: f32,
float_tables: &[Rect],
) -> (Rect, Rect) {
let h = 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);
// Across, as laid out before an even page is mirrored (see `float_rect`).
let page_x = |fr: &PageFrame| {
let (left, _, shift) = fr.sides();
match f.h_rel {
Anchor::Page => Some((-shift, fr.sect.page_w)),
Anchor::Margin => Some((left - shift, fr.sect.text_width())),
_ => None,
}
};
let page_y = |fr: &PageFrame| {
let s = fr.sect;
match f.v_rel {
Anchor::Page => Some((0.0, s.page_h)),
Anchor::Margin => {
let mut reference = s.clone();
reference.margin_top = fr.margin_top.unwrap_or(s.margin_top);
Some((reference.margin_top, reference.text_height()))
}
_ => None,
}
};
let h_area = frame.and_then(|fr| page_x(&fr).map(|(a, e)| (a - fr.origin.0, e))).unwrap_or((0.0, width));
// In a cell an offset from the page or margin has nothing to measure from: the table stands
// where the text is.
let (v_area, y_off) = match frame.and_then(|fr| page_y(&fr).map(|(a, e)| (a - fr.origin.1, e))) {
Some(area) => (area, f.y),
None if matches!(f.v_rel, Anchor::Page | Anchor::Margin) => ((y, 0.0), 0.0),
None => ((y, 0.0), f.y),
};
// Nor is there an area to align in: the table starts where the text is.
let v_align = if frame.is_none() && matches!(f.v_rel, Anchor::Page | Anchor::Margin) { None } else { f.v_align };
let x = align_in(h_area, f.x, w, PageFrame::h_align(frame.as_ref(), f.h_align)) - if f.h_align.is_none() { legacy } else { 0.0 };
let r = clear_of_float_tables(Rect::new(x, align_in(v_area, y_off, h, v_align), w, h), f, legacy, h_area.0, float_tables);
let [dl, dt, dr, db] = f.dist_from_text();
(r, r.inset(-dl, -dt, -dr, -db))
}
/// Floating table `f` at `r`, moved below the floating tables in its way when it may not
/// overlap them. In Word 2013+ layout one placed by an offset keeps its distance from the text's
/// left edge (`text_left`).
pub(crate) fn clear_of_float_tables(mut r: Rect, f: &TableFloat, legacy: f32, text_left: f32, others: &[Rect]) -> Rect {
let [dl, dt, dr, db] = f.dist_from_text();
let keeps_text_distance = legacy == 0.0 && f.h_align.is_none() && f.x >= 0.0 && matches!(f.h_rel, Anchor::Margin | Anchor::Column);
for _ in 0..64 {
let area = r.inset(-dl, -dt, -dr, -db);
let Some(below) = others.iter().filter(|o| !f.overlap && o.intersects(&area)).map(|o| o.bottom()).reduce(f32::max) else { break };
r.y = below + dt;
if keeps_text_distance {
r.x = r.x.max(text_left + dl);
}
}
r
}
/// A text box's default internal margins (Word's: 0.1" left/right, 0.05" top/bottom); a box's
/// own come from its [`wordcraft_doc::TextBody`].
#[cfg(test)]
pub(crate) const BOX_INSET_X: f32 = 7.2;
#[cfg(test)]
pub(crate) const BOX_INSET_Y: f32 = 3.6;
/// Keep what fits in a box `height` tall (items relative to its top): lines that don't fit are
/// dropped (whole lines; a line that only partly fits too) and so is anything starting below.
/// The first line always stays, so even a tiny box shows the start of its text.
pub(crate) fn fit_box(items: Vec<Placed>, height: f32) -> Vec<Placed> {
let limit = height + 0.5;
let mut out = Vec::with_capacity(items.len());
let mut kept_line = false;
for it in items {
if let Some(r) = it.turned_bounds() {
// Turned text (in a table cell) stays whole.
if r.y < limit {
out.push(it);
}
continue;
}
match it {
Placed::Lines { story, path, para, l0, l1, x, y, turn } => {
let Some(first) = para.lines.get(l0) else { continue };
let mut end = l0;
for k in l0..l1 {
let Some(l) = para.lines.get(k) else { break };
if y + (l.top - first.top) + l.height > limit && kept_line {
break;
}
end = k + 1;
kept_line = true;
}
if end > l0 {
out.push(Placed::Lines { story, path, para, l0, l1: end, x, y, turn });
}
}
Placed::Fill { rect, color } if rect.y < limit => {
out.push(Placed::Fill { rect: Rect::new(rect.x, rect.y, rect.w, rect.h.min(limit - rect.y)), color });
}
Placed::Rule { x0, y0, x1, y1, border } if y0.min(y1) < limit => {
out.push(Placed::Rule { x0, y0: y0.min(limit), x1, y1: y1.min(limit), border });
}
Placed::Image { rect, .. }
| Placed::Shape { rect, .. }
| Placed::Graphic { rect, .. }
| Placed::Clip { rect, .. }
| Placed::Cell { rect, .. }
| Placed::Object { rect, .. }
if rect.y < limit =>
{
out.push(it);
}
_ => {}
}
}
out
}
/// Word joins neighbouring paragraphs with the same borders and indents into one box: the top
/// border goes above the first of them only, the bottom one below the last, and the shading and
/// side borders run on through the space between them (`table`: the cell's paragraph properties).
fn border_group(doc: &Document, a: &Paragraph, b: &Paragraph, table: Option<&wordcraft_doc::props::ParaProps>) -> bool {
let (a, b) = (doc.styles.resolve_para_in(&a.props, table), doc.styles.resolve_para_in(&b.props, table));
a.borders.is_some() && a.borders == b.borders && a.indent_left == b.indent_left && a.indent_right == b.indent_right
}
/// Whether paragraph `i` of `blocks` and the one at `j` are of one border group.
pub(crate) fn joined(doc: &Document, blocks: &Blocks, i: usize, j: Option<usize>, table: Option<&wordcraft_doc::props::ParaProps>) -> bool {
match (blocks.get(i).map(|b| &**b), j.and_then(|j| blocks.get(j)).map(|b| &**b)) {
(Some(Block::Para(a)), Some(Block::Para(b))) => border_group(doc, a, b, table),
_ => false,
}
}
/// The room a paragraph's top or bottom border takes in the text flow: its distance from the
/// text plus its width (Word).
fn border_room(b: Option<Border>) -> f32 {
b.filter(Border::is_visible).map_or(0.0, |b| b.space + b.width)
}
/// How a piece of a paragraph meets the paragraphs around it: the room taken by its top and
/// bottom borders (0: none drawn there — the paragraph goes on from or into one of its border
/// group, or it is split there) and how far its shading and side borders reach up to join the
/// paragraph above in its group.
#[derive(Clone, Copy, Default)]
pub(crate) struct Edges {
pub(crate) top: f32,
pub(crate) bottom: f32,
pub(crate) up: f32,
}
impl Edges {
/// A paragraph on its own: its top and bottom borders at its ends.
pub(crate) fn own(pl: &ParaLayout, l0: usize, l1: usize) -> Edges {
let b = pl.rp.borders.unwrap_or_default();
Edges { top: if l0 == 0 { border_room(b.top) } else { 0.0 }, bottom: if l1 >= pl.lines.len() { border_room(b.bottom) } else { 0.0 }, up: 0.0 }
}
}
/// Paragraph lines plus their shading and borders; returns the bottom of the lines. The
/// shading fills the box out to the borders, the room they take included (Word).
pub(crate) fn push_para(
items: &mut Vec<Placed>,
(story, path): (StoryRef, &[u32]),
pl: &Arc<ParaLayout>,
(l0, l1): (usize, usize),
(x, y): (f32, f32),
width: f32,
e: Edges,
) -> f32 {
let (Some(first), Some(last)) = (pl.lines.get(l0), l1.checked_sub(1).and_then(|k| pl.lines.get(k))) else {
return y;
};
let h = last.top + last.height - first.top;
// Indents are logical: a right-to-left paragraph's start indent (and hanging indent) is on the right.
let start = pl.rp.indent_left.min(pl.rp.indent_left + pl.rp.indent_first);
let (li, ri) = if pl.rp.bidi { (pl.rp.indent_right, start) } else { (start, pl.rp.indent_right) };
let left = x + li;
let right = x + width - ri;
let (top, bottom) = (y - e.top - e.up, y + h + e.bottom);
// The outer edges of the side borders.
let sides = pl.rp.borders.unwrap_or_default();
let (lx, rx) = (left - border_room(sides.left), right + border_room(sides.right));
if let Some(c) = pl.rp.shading {
let pad = pl.rp.borders.as_ref().map(|b| b.left.map(|l| l.space).unwrap_or(4.0)).unwrap_or(0.0);
let (sl, sr) = (lx.min(left - pad), rx.max(right + pad));
items.push(Placed::Fill { rect: Rect::new(sl, top, sr - sl, bottom - top), color: c });
}
items.push(Placed::Lines { story, path: Path(path.to_vec()), para: pl.clone(), l0, l1, x, y, turn: TextDirection::Horizontal });
if let Some(b) = &pl.rp.borders {
// All four lines lie beyond their distance from the text; top and bottom reach the corners.
if let Some(t) = b.top.filter(|t| e.top > 0.0 && t.is_visible()) {
let ty = y - t.space - t.width / 2.0;
items.push(Placed::Rule { x0: lx, y0: ty, x1: rx, y1: ty, border: t });
}
if let Some(t) = b.bottom.filter(|t| e.bottom > 0.0 && t.is_visible()) {
let by = y + h + t.space + t.width / 2.0;
items.push(Placed::Rule { x0: lx, y0: by, x1: rx, y1: by, border: t });
}
if let Some(t) = b.left.filter(Border::is_visible) {
let bx = lx + t.width / 2.0;
items.push(Placed::Rule { x0: bx, y0: top, x1: bx, y1: bottom, border: t });
}
if let Some(t) = b.right.filter(Border::is_visible) {
let bx = rx - t.width / 2.0;
items.push(Placed::Rule { x0: bx, y0: top, x1: bx, y1: bottom, border: t });
}
}
y + h
}