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, /// Outer cell edges relative to its text origin (not including wrap distances). Cell { text: Option<&'a para::CellText>, left: f32, right: f32, /// False for a turned text frame: its x axis becomes page y after rotation. clamp: bool, }, } 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>, } impl<'a> BoxInput<'a> { pub(crate) fn other(story: StoryRef, blocks: &'a Blocks, width: f32, depth: usize, frame: Option>) -> 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 { text: cell, left: 0.0, right: width, clamp: false }, 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>, in_cell: bool, depth: usize, y: &'state mut f32, items: &'state mut Vec, exclusions: &'state mut Vec, float_tables: &'state mut Vec, table_areas: &'state mut Vec, page_table_wraps: &'state mut Vec, 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 (in_cell, table, cell_edges) = match context { BoxContext::Other => (false, None, None), BoxContext::Cell { text, left, right, clamp } => (true, Some(text.unwrap_or(&empty_cell)), clamp.then_some((left, right))), }; // 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 = Vec::new(); // Areas of the box's floating tables, which other floating tables may have to avoid. let mut float_tables: Vec = 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 mut r = float_rect(frame, (0.0, width), float_y, w, h, float); // Only move an offset-positioned float when its box crosses the outer // cell edge. Cell text margins and wrap distances do not move the object. if let Some((left, right)) = cell_edges && !float.outside_cell && float.h_align.is_none() && float.wrap != Wrap::InFrontOfText { r.x = keep_in_cell(r.x, r.w, left, right); } 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), late: None, }; 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, pub(crate) table_areas: Vec, pub(crate) page_table_wraps: Vec, 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, 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::().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 } /// Keep a float's box within the outer cell edges, relative to the cell's text origin. /// Offsets that fit are unchanged, including those inside the text margins. Wrap distances /// affect surrounding text only. An oversized object starts at the outer left edge. pub(crate) fn keep_in_cell(x: f32, w: f32, left: f32, right: f32) -> f32 { let left = if left.is_finite() { left } else { 0.0 }; let end = right - w; if !end.is_finite() || end <= left { return left; } // max/min also place a non-finite offset at a finite edge without panicking. x.max(left).min(end) } /// 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, height: f32) -> Vec { 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, 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) -> f32 { b.filter(Border::is_visible).map_or(0.0, |b| { let extent = b.space + b.width; if extent.is_finite() { extent.max(0.0) } else { 0.0 } }) } /// 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, (story, path): (StoryRef, &[u32]), pl: &Arc, (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 }