mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 11:50:20 -06:00
Layout: keep a cell's floats laid out in the cell inside it (layoutInCell) (#4545)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
334494a39f
commit
2f49c8ad2e
@@ -775,6 +775,7 @@ MATH glyphs that reference the same construction now share its immutable variant
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-05 | trivial | PR #4545: eligible cell floats retain in-bounds offsets and touch the outer cell edge only on overflow, ignoring wrap distances for object placement. Front-of-text, turned-cell and outside-cell opt-outs remain. Synthetic geometry controls; native front/behind-text parity remains unqualified. |
|
||||
| 2026-11-05 | trivial | DOCX paragraph `w:adjustRightInd` is preserved with true, false and absent values; OOXML properties follow schema order. Grid layout remains open. |
|
||||
| 2026-10-28 | trivial | Per-page ink object lookups, #3614. Drawing order, geometry and saved data retain their behavior; no readiness estimate change |
|
||||
| 2026-10-28 | trivial | Shared MATH construction records reuse variants and assembly data without changing glyph mappings or existing limits (#3630); readiness estimates unchanged |
|
||||
|
||||
@@ -13,7 +13,14 @@ use super::{
|
||||
/// Cell identity is independent of optional table-style formatting.
|
||||
pub(crate) enum BoxContext<'a> {
|
||||
Other,
|
||||
Cell(Option<&'a para::CellText>),
|
||||
/// 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> {
|
||||
@@ -32,7 +39,7 @@ impl<'a> BoxInput<'a> {
|
||||
}
|
||||
|
||||
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 }
|
||||
Self { story, blocks, prefix, width, context: BoxContext::Cell { text: cell, left: 0.0, right: width, clamp: false }, depth, frame: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,9 +198,9 @@ pub(crate) fn layout_box(ctx: &mut Ctx, input: BoxInput<'_>) -> BoxLayout {
|
||||
// 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),
|
||||
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 =
|
||||
@@ -290,7 +297,16 @@ pub(crate) fn layout_box(ctx: &mut Ctx, input: BoxInput<'_>) -> BoxLayout {
|
||||
// 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 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);
|
||||
@@ -507,6 +523,19 @@ pub(crate) fn clear_of_float_tables(mut r: Rect, f: &TableFloat, legacy: f32, te
|
||||
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)]
|
||||
|
||||
@@ -8,6 +8,7 @@ use wordcraft_doc::styles::{PartSet, TablePart, TableStyleProps};
|
||||
use wordcraft_doc::{Block, Blocks, Paragraph, Row, StoryRef, Table};
|
||||
use wordcraft_geom::Rect;
|
||||
|
||||
use crate::boxes::BoxContext;
|
||||
use crate::para::{CellText, ClKind, ParaLayout};
|
||||
use crate::{BoxInput, BoxLayout, Ctx, Placed, layout_box};
|
||||
|
||||
@@ -621,8 +622,13 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
||||
turned = Some(Turned { turn, len, across, snap, region, ci, top: margins[0] + band_t });
|
||||
(items, len)
|
||||
} else {
|
||||
let BoxLayout { mut items, mut height, floats_bottom, last_top, .. } =
|
||||
layout_box(ctx, BoxInput::cell(story, &cell.blocks, &cpath, cw, text, depth));
|
||||
let BoxLayout { mut items, mut height, floats_bottom, last_top, .. } = layout_box(
|
||||
ctx,
|
||||
BoxInput {
|
||||
context: BoxContext::Cell { text, left: -margins[1], right: x1 - x0 - margins[1], clamp: true },
|
||||
..BoxInput::cell(story, &cell.blocks, &cpath, cw, text, depth)
|
||||
},
|
||||
);
|
||||
// A hidden end-of-cell mark: an empty last paragraph takes no room (Word for
|
||||
// Windows: a row of empty such cells is only its borders).
|
||||
if cell.props.hide_mark && cell.blocks.last().and_then(|b| b.as_para()).is_some_and(|p| p.text.is_empty()) {
|
||||
|
||||
@@ -7,6 +7,7 @@ use wordcraft_doc::{Pos, Table};
|
||||
mod autospace;
|
||||
mod bidi;
|
||||
mod budget;
|
||||
mod cell_floats;
|
||||
mod complex;
|
||||
mod grid;
|
||||
mod ligature;
|
||||
@@ -5843,6 +5844,110 @@ fn floats_in_headers_and_table_cells_are_drawn() {
|
||||
assert!(c[0].x > 72.0 && c[0].x < 72.0 + 234.0, "inside the first cell: {:?}", c[0]);
|
||||
}
|
||||
|
||||
/// A float laid out in its cell (`layoutInCell`) whose offset would take it out of the cell is
|
||||
/// moved in to the cell's outer side (a letterhead logo anchored left of its header cell). One laid out on the page (`layoutInCell="0"`) keeps its offset.
|
||||
#[test]
|
||||
fn cell_floats_laid_out_in_the_cell_stay_in_it() {
|
||||
use wordcraft_doc::para::{Anchor, Float, Wrap};
|
||||
let cell_w = 234.0;
|
||||
let place = |x: f32, outside_cell: bool, wrap: Wrap| {
|
||||
let mut d = Document::from_text("Body text.");
|
||||
let mut t = Table::new(1, 2, 468.0);
|
||||
let mut cp = wordcraft_doc::Paragraph::with_text("cell", Default::default());
|
||||
let f = Float { wrap, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, x, dist: 9.0, outside_cell, ..Default::default() };
|
||||
cp.insert_object(0, rect_shape(80.0, 48.0, f), &Default::default()).unwrap();
|
||||
t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(cp)];
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let c = shapes(&lay(&d).pages[0].items);
|
||||
assert_eq!(c.len(), 1, "cell float drawn");
|
||||
c[0].x
|
||||
};
|
||||
// An offset inside the cell is kept: the cell's text starts 20 pt left of this one.
|
||||
let text_left = place(20.0, false, Wrap::Tight) - 20.0;
|
||||
assert!(text_left > 72.0 && text_left < 72.0 + 12.0, "the cell's text starts past its margin: {text_left}");
|
||||
let cell_left = text_left - 5.4;
|
||||
// Anchored 95 pt left of the column: moved in to the cell's outer left side.
|
||||
let left = place(-95.0, false, Wrap::Tight);
|
||||
assert!((left - cell_left).abs() < 0.5, "pulled in to the left side: {left} vs {text_left}");
|
||||
// Past the right side: moved in to it.
|
||||
let right = place(cell_w + 40.0, false, Wrap::Tight);
|
||||
// (within a point: the cell's border room is not split evenly between its two sides)
|
||||
assert!((right - (cell_left + cell_w - 80.0)).abs() < 1.0, "pulled in to the right side: {right}");
|
||||
// Not laid out in the cell: the offset is kept, off the cell or not.
|
||||
let out = place(-95.0, true, Wrap::Tight);
|
||||
assert!((out - (text_left - 95.0)).abs() < 0.5, "layoutInCell=0 keeps the offset: {out}");
|
||||
// Front-of-text shapes do not wrap and keep their offsets even with layoutInCell on.
|
||||
for offset in [-95.0, cell_w + 40.0] {
|
||||
let front = place(offset, false, Wrap::InFrontOfText);
|
||||
assert!((front - (text_left + offset)).abs() < 0.5, "front-of-text offset preserved: {front}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell confinement uses the finite, nonnegative width that float placement actually draws.
|
||||
#[test]
|
||||
fn cell_float_bounds_use_the_placed_width() {
|
||||
use wordcraft_doc::para::{Anchor, Float, Wrap};
|
||||
let place = |width, x, wrap, outside_cell| {
|
||||
let mut d = Document::from_text("Body");
|
||||
let mut t = Table::new(1, 2, 468.0);
|
||||
let mut p = wordcraft_doc::Paragraph::with_text("cell", Default::default());
|
||||
let f = Float { wrap, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, x, dist: 9.0, outside_cell, ..Default::default() };
|
||||
p.insert_object(0, rect_shape(width, 48.0, f), &Default::default()).unwrap();
|
||||
t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(p)];
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let objects = shapes(&lay(&d).pages[0].items);
|
||||
assert_eq!(objects.len(), 1);
|
||||
objects[0]
|
||||
};
|
||||
// Invalid stored widths are placed as zero-width objects; offsets at either side must
|
||||
// match an explicitly zero-width object, rather than inventing extra room in the cell.
|
||||
for offset in [-95.0, 300.0] {
|
||||
let zero = place(0.0, offset, Wrap::Tight, false);
|
||||
for stored in [-100.0, f32::NEG_INFINITY, f32::NAN, f32::INFINITY] {
|
||||
let placed = place(stored, offset, Wrap::Tight, false);
|
||||
assert_eq!(placed.w, 0.0);
|
||||
assert!((placed.x - zero.x).abs() < 0.01, "stored width {stored}, offset {offset}: {placed:?} vs {zero:?}");
|
||||
}
|
||||
// Neither opt-out path gains confinement from the width normalization.
|
||||
for (wrap, outside_cell) in [(Wrap::InFrontOfText, false), (Wrap::Tight, true)] {
|
||||
let zero = place(0.0, offset, wrap, outside_cell);
|
||||
let negative = place(-100.0, offset, wrap, outside_cell);
|
||||
assert_eq!(negative.w, zero.w);
|
||||
assert!((negative.x - zero.x).abs() < 0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keep_in_cell_bounds() {
|
||||
use super::boxes::keep_in_cell;
|
||||
assert_eq!(keep_in_cell(-95.0, 80.0, 0.0, 234.0), 0.0);
|
||||
assert_eq!(keep_in_cell(300.0, 80.0, 0.0, 234.0), 234.0 - 80.0);
|
||||
assert_eq!(keep_in_cell(20.0, 80.0, 0.0, 234.0), 20.0);
|
||||
// Wider than the cell, or exactly its width: starts at the outer left side.
|
||||
assert_eq!(keep_in_cell(50.0, 300.0, 0.0, 234.0), 0.0);
|
||||
assert_eq!(keep_in_cell(50.0, 234.0, 0.0, 234.0), 0.0);
|
||||
// Nearly full: no clearance is added to the object's position.
|
||||
assert_eq!(keep_in_cell(-95.0, 230.0, 0.0, 234.0), 0.0);
|
||||
assert_eq!(keep_in_cell(300.0, 230.0, 0.0, 234.0), 4.0);
|
||||
// Cell edges include its margins; negative offsets can be wholly inside the cell.
|
||||
assert_eq!(keep_in_cell(-5.0, 80.0, 0.0, 234.0), 0.0);
|
||||
assert_eq!(keep_in_cell(-5.0, 80.0, -4.0, 234.0), -4.0);
|
||||
assert_eq!(keep_in_cell(-5.15, 63.75, -5.4, 448.2), -5.15);
|
||||
assert_eq!(keep_in_cell(395.95, 47.25, -5.4, 448.2), 395.95);
|
||||
// Clamp limits stay finite and ordered even at small scales or with malformed geometry.
|
||||
for width in [0.1_f32, 0.3, 1.0, 234.0] {
|
||||
let object = width * 0.98;
|
||||
for offset in [-width, 0.0, width, f32::NAN, f32::INFINITY] {
|
||||
let x = keep_in_cell(offset, object, 0.0, width);
|
||||
assert!(x.is_finite() && x >= 0.0 && x + object <= width + f32::EPSILON * width.max(1.0));
|
||||
}
|
||||
}
|
||||
assert_eq!(keep_in_cell(10.0, 80.0, 0.0, f32::NAN), 0.0);
|
||||
assert_eq!(keep_in_cell(10.0, f32::NAN, 0.0, 234.0), 0.0);
|
||||
assert_eq!(keep_in_cell(10.0, 80.0, f32::NAN, 234.0), 10.0);
|
||||
}
|
||||
|
||||
/// Paragraph anchors exclude space-before; line and frameless page/margin anchors retain it.
|
||||
#[test]
|
||||
fn box_float_origins_respect_vertical_anchor_with_space_before() {
|
||||
@@ -13435,3 +13540,29 @@ fn a_table_moved_by_a_page_break_shows_its_new_page_number() {
|
||||
.collect();
|
||||
assert_eq!(shown, ["2"]);
|
||||
}
|
||||
|
||||
/// Cell confinement must not change page-vertical placement through the rotated text axis.
|
||||
#[test]
|
||||
fn turned_cell_float_keeps_its_along_text_offset() {
|
||||
use wordcraft_doc::para::{Anchor, Float, Wrap};
|
||||
let place = |direction, outside_cell| {
|
||||
let mut doc = Document::from_text("Synthetic body");
|
||||
let mut table = Table::new(1, 2, 468.0);
|
||||
table.rows[0].props.height = Some(100.0);
|
||||
table.rows[0].props.height_rule = wordcraft_doc::props::HeightRule::Exact;
|
||||
table.rows[0].cells[0].props.text_direction = direction;
|
||||
let mut para = Paragraph::with_text("Synthetic cell", Default::default());
|
||||
let float = Float { wrap: Wrap::Tight, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, x: 150.0, outside_cell, ..Default::default() };
|
||||
para.insert_object(0, rect_shape(20.0, 10.0, float), &Default::default()).unwrap();
|
||||
table.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(para)];
|
||||
doc.insert_block(StoryRef::Body, &Path::top(1), Block::Table(table)).unwrap();
|
||||
let objects = shapes(&lay(&doc).pages[0].items);
|
||||
assert_eq!(objects.len(), 1);
|
||||
objects[0]
|
||||
};
|
||||
for direction in [TextDirection::Down, TextDirection::Up] {
|
||||
let confined = place(direction, false);
|
||||
let reference = place(direction, true);
|
||||
assert!((confined.y - reference.y).abs() < 0.01, "{direction:?}: {confined:?} vs {reference:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//! Synthetic letterhead geometry. No external documents or image assets are used.
|
||||
use super::*;
|
||||
use wordcraft_doc::para::{Anchor, Float, Wrap};
|
||||
use wordcraft_doc::props::VAlign;
|
||||
use wordcraft_doc::{Paragraph, PartKind};
|
||||
|
||||
fn picture(name: &str, w: f32, h: f32, x: f32, y: f32, wrap: Wrap, dist: f32) -> InlineObject {
|
||||
InlineObject::Image {
|
||||
media: name.into(),
|
||||
w,
|
||||
h,
|
||||
alt: name.into(),
|
||||
float: Float { wrap, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, x, y, dist, ..Default::default() },
|
||||
crop: [0.0; 4],
|
||||
fx: Default::default(),
|
||||
ole: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_rect(items: &[Placed]) -> Rect {
|
||||
items.iter().find_map(|p| if let Placed::Cell { rect, .. } = p { Some(*rect) } else { None }).unwrap()
|
||||
}
|
||||
|
||||
/// A body-table letterhead: a central front-of-text box and two pictures close to the cell's
|
||||
/// outer edges. Both picture boxes fit; neither their offsets nor the box may move inward.
|
||||
#[test]
|
||||
fn cell_letterhead_keeps_picture_offsets_inside_the_outer_edges() {
|
||||
let mut d = Document::from_text("Synthetic body");
|
||||
d.last_section.page_w = 612.0;
|
||||
d.last_section.page_h = 792.0;
|
||||
d.last_section.margin_left = 70.9;
|
||||
d.last_section.margin_right = 31.5;
|
||||
d.last_section.margin_top = 31.2;
|
||||
let mut t = Table::new(1, 1, 453.6);
|
||||
t.props.style = None;
|
||||
t.props.width = Some(453.6);
|
||||
t.props.indent = Some(21.05);
|
||||
let border = Some(Border { style: BorderStyle::Single, width: 0.5, ..Default::default() });
|
||||
t.props.borders = Some(Borders { top: border, left: border, bottom: border, right: border, ..Default::default() });
|
||||
t.rows[0].props.height = Some(63.75);
|
||||
let mut p = Paragraph::default();
|
||||
let box_story = d.add_part(PartKind::TextBox, vec![wordcraft_doc::para_block(Paragraph::with_text("Synthetic letterhead", Default::default()))]);
|
||||
let mut central = rect_shape(332.25, 52.5, Float { wrap: Wrap::InFrontOfText, x: 59.9, y: 5.8, dist: 9.0, ..Default::default() });
|
||||
if let InlineObject::Shape { story, .. } = &mut central {
|
||||
*story = Some(box_story);
|
||||
}
|
||||
p.insert_object(0, central, &Default::default()).unwrap();
|
||||
p.insert_object(0, picture("right", 47.25, 61.6, 395.95, 1.4, Wrap::Through, 9.0), &Default::default()).unwrap();
|
||||
p.insert_object(0, picture("left", 63.75, 56.25, -5.15, 2.0, Wrap::Through, 9.0), &Default::default()).unwrap();
|
||||
t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(p)];
|
||||
d.insert_block(StoryRef::Body, &Path::top(0), Block::Table(t)).unwrap();
|
||||
let layout = lay(&d);
|
||||
let items = &layout.pages[0].items;
|
||||
let cell = cell_rect(items);
|
||||
let mut checked = 0;
|
||||
for item in items {
|
||||
if let Placed::Object { rect, origin, text_box, .. } = item {
|
||||
let (x, y) = if text_box.is_some() {
|
||||
(59.9, 5.8)
|
||||
} else if rect.w > 60.0 {
|
||||
(-5.15, 2.0)
|
||||
} else {
|
||||
(395.95, 1.4)
|
||||
};
|
||||
assert!(rect.x >= cell.x - 0.01 && rect.right() <= cell.right() + 0.01, "the original box fits the outer cell: {rect:?}, {cell:?}");
|
||||
assert!((rect.x - origin.x - x).abs() < 0.5, "offset {x} was moved: {rect:?}, origin {origin:?}");
|
||||
assert!((rect.y - origin.y - y).abs() < 0.01, "vertical placement is unchanged");
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(checked, 3, "two pictures and one central text box");
|
||||
}
|
||||
|
||||
/// A centred header table wider than the text column, with a picture anchored far to its left.
|
||||
/// The picture touches the outer cell edge, not the text inset or the wrap-distance boundary.
|
||||
#[test]
|
||||
fn cell_letterhead_pulls_an_outside_picture_to_the_outer_left_edge() {
|
||||
for dist in [0.0, 9.0, 24.0] {
|
||||
let mut d = Document::from_text("Synthetic body");
|
||||
d.last_section.page_w = 612.0;
|
||||
d.last_section.page_h = 792.0;
|
||||
d.last_section.margin_left = 70.85;
|
||||
d.last_section.margin_right = 70.85;
|
||||
d.last_section.header = 5.65;
|
||||
let mut t = Table::new(1, 1, 498.05);
|
||||
t.props.style = None;
|
||||
t.props.width_pct = Some(105.9);
|
||||
t.props.align = Some(Align::Center);
|
||||
t.rows[0].props.height = Some(67.95);
|
||||
t.rows[0].cells[0].props.valign = Some(VAlign::Center);
|
||||
let mut p = Paragraph::with_text("Synthetic header", Default::default());
|
||||
p.insert_object(0, picture("logo", 79.0, 47.55, -94.7, 2.3, Wrap::BehindText, dist), &Default::default()).unwrap();
|
||||
t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(p)];
|
||||
let header = d.add_part(PartKind::Header, vec![Arc::new(Block::Table(t))]);
|
||||
d.last_section.headers.default = Some(header);
|
||||
let layout = lay(&d);
|
||||
let items = &layout.pages[0].header;
|
||||
let cell = cell_rect(items);
|
||||
assert!((cell.x - 56.976).abs() < 0.5, "the centred outer cell begins at {cell:?}");
|
||||
let objects: Vec<_> =
|
||||
items.iter().filter_map(|p| if let Placed::Object { rect, origin, .. } = p { Some((*rect, *origin)) } else { None }).collect();
|
||||
assert_eq!(objects.len(), 1);
|
||||
let (rect, origin) = objects[0];
|
||||
assert!((rect.x - cell.x).abs() < 0.5, "wrap distance {dist}: picture {rect:?}, outer cell {cell:?}");
|
||||
assert!((rect.y - origin.y - 2.3).abs() < 0.01, "vertical placement is unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
/// Confinement follows the particular cell, including asymmetric margins mirrored for RTL.
|
||||
#[test]
|
||||
fn cell_float_outer_edges_include_asymmetric_and_mirrored_margins() {
|
||||
for rtl in [false, true] {
|
||||
for (offset, at_left) in [(-100.0, true), (300.0, false)] {
|
||||
let mut d = Document::from_text("Body");
|
||||
let mut t = Table::new(1, 2, 468.0);
|
||||
t.props.bidi_visual = rtl;
|
||||
t.props.cell_margins = Some([0.0, 12.0, 0.0, 3.0]);
|
||||
let mut p = Paragraph::default();
|
||||
p.insert_object(0, picture("logo", 40.0, 20.0, offset, 2.0, Wrap::BehindText, 9.0), &Default::default()).unwrap();
|
||||
t.rows[0].cells[1].blocks = vec![wordcraft_doc::para_block(p)];
|
||||
d.insert_block(StoryRef::Body, &Path::top(0), Block::Table(t)).unwrap();
|
||||
let layout = lay(&d);
|
||||
let items = &layout.pages[0].items;
|
||||
let cell = items.iter().find_map(|p| if let Placed::Cell { rect, cell: 1, .. } = p { Some(*rect) } else { None }).unwrap();
|
||||
let picture = items.iter().find_map(|p| if let Placed::Image { rect, .. } = p { Some(*rect) } else { None }).unwrap();
|
||||
let edge = if at_left { picture.x - cell.x } else { picture.right() - cell.right() };
|
||||
assert!(edge.abs() < 0.5, "rtl {rtl}, offset {offset}: picture {picture:?}, cell {cell:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,7 @@ Table rows and cells inside `mc:AlternateContent` use the same selected-branch p
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-05 | trivial | PR #4545: eligible cell floats retain in-bounds offsets and touch the outer cell edge only on overflow, ignoring wrap distances for object placement. Front-of-text, turned-cell and outside-cell opt-outs remain. Synthetic geometry controls; native front/behind-text parity remains unqualified. |
|
||||
| 2026-11-05 | trivial | DOCX preserves `w:adjustRightInd` with its true default and writes it in schema order; grid layout remains open |
|
||||
| 2026-10-28 | trivial | Ink page lookups reuse scanned paragraph prefixes, #3614. Drawing order, geometry and saved data retain their behavior; no readiness estimate change |
|
||||
| 2026-10-28 | trivial | Shared MATH construction records reuse variants and assembly data without changing glyph mappings or existing limits (#3630); readiness estimates unchanged |
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user