fix(mtext): tab stops are column-relative and align-aware when wrapping

Two defects in tabbed fields inside a multi-column MText:

- Stop positions are relative to the paragraph's own left edge, but the
  tab logic compared them against the column-absolute `cursor_x`. In a
  second column `cursor_x` already carries the column offset, so no stop
  ever matched and the field shot off to the default 4-em grid, way past
  the page. Measure against the column's `box_left` instead.
- A field after a center/right/decimal tab aligns on the stop, reaching
  only part of its width to the right, but word-wrap flushed it left and
  wrapped it off the line by its full width — dropping a wide decimal
  number onto a fresh, un-tabbed line. Don't wrap the field that follows
  an aligning tab.

Picks up acadrust b16137f (decimal stop position parse).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-19 01:10:56 +03:00
1 parent 3da678b2ca
commit f808030eb6
2 files changed
+24 -5

No files matched your search

Generated
+1 -1
View File
@@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.0"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#162f1940ef9f7faaa9b6597e4b190be913b70d37"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#b16137ff0143bf480edfbc8e12f89068a76316de"
dependencies = [
"ahash 0.8.12",
"anyhow",
+23 -4
View File
@@ -774,6 +774,10 @@ pub fn wrap_paragraph(
let mut cur: Vec<LayoutAtom> = Vec::new();
let mut cur_w = 0.0_f32;
let mut subline_idx: usize = 0;
// The field after a center/right/decimal tab is aligned on the stop, so it
// reaches only part of its width to the right — don't wrap it off the line
// by its full width as a left-flushed word would be.
let mut after_align_tab = false;
let line_start_x = |idx: usize| if idx == 0 { indent_first } else { indent_left };
let line_max_w = |idx: usize| (rect_w - indent_right - line_start_x(idx)).max(0.0);
@@ -783,7 +787,7 @@ pub fn wrap_paragraph(
AtomKind::Word(_) | AtomKind::Stack { .. } => {
let w = atom_width(&atom, entity_h, base_wf, base_font);
let max_w = line_max_w(subline_idx);
if !cur.is_empty() && cur_w + w > max_w {
if !cur.is_empty() && cur_w + w > max_w && !after_align_tab {
while matches!(cur.last().map(|a| &a.kind), Some(AtomKind::Space)) {
cur.pop();
}
@@ -791,10 +795,12 @@ pub fn wrap_paragraph(
cur_w = 0.0;
subline_idx += 1;
}
after_align_tab = false;
cur.push(atom);
cur_w += w;
}
AtomKind::Space => {
after_align_tab = false;
if cur.is_empty() {
continue;
}
@@ -803,6 +809,12 @@ pub fn wrap_paragraph(
}
AtomKind::Tab => {
let start_x = line_start_x(subline_idx);
let local = cur_w + start_x - indent_left;
let is_align = tab_stops
.iter()
.find(|ts| ts.position > local + 1e-4)
.map(|ts| !matches!(ts.kind, TabKind::Left))
.unwrap_or(false);
let new_w = next_tab_position(cur_w + start_x, tab_stops, indent_left, entity_h)
- start_x;
let max_w = line_max_w(subline_idx);
@@ -814,6 +826,7 @@ pub fn wrap_paragraph(
cur.push(atom);
cur_w = new_w.min(max_w);
}
after_align_tab = is_align;
}
}
}
@@ -1759,7 +1772,13 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
// the tab (the run of words up to the next space/tab) rather
// than just advancing the pen. Decimal places the first `.`
// on the stop; right the field's end; center its middle.
let local = cursor_x - sub.indent_left;
//
// Stop positions are relative to the paragraph's own left
// edge, so measure against `box_left` (the column's left,
// which `cursor_x` already carries) — not the raw indent, or
// a second column's tabs miss every stop and shoot off right.
let tab_base = box_left + sub.indent_left;
let local = cursor_x - tab_base;
let stop = sub
.tab_stops
.iter()
@@ -1767,7 +1786,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
.copied();
match stop.map(|s| s.kind) {
Some(TabKind::Center | TabKind::Right | TabKind::Decimal) => {
let sx = sub.indent_left + stop.unwrap().position;
let sx = tab_base + stop.unwrap().position;
let mut field_w = 0.0_f32;
let mut before_dot: Option<f32> = None;
for next in &sub.atoms[ai + 1..] {
@@ -1824,7 +1843,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
cursor_x = next_tab_position(
cursor_x,
&sub.tab_stops,
sub.indent_left,
tab_base,
entity_h,
);
}