From f808030eb6ac8e65b3b00e80d6ffdd98d1146a4a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 19 Jul 2026 01:10:56 +0300 Subject: [PATCH] fix(mtext): tab stops are column-relative and align-aware when wrapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Cargo.lock | 2 +- src/entities/text_support.rs | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cfff5261..5026a3b72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 0a851fd41..bdadd05b5 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -774,6 +774,10 @@ pub fn wrap_paragraph( let mut cur: Vec = 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 = 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, ); }