From a252cd526cd22bc2fc76c1eec9be75f1a2778978 Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Tue, 22 Sep 2026 11:04:34 +0200 Subject: [PATCH] fix(geometry): measure text overflow against the content box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bbox` here is `raw_bbox = bbox_of(layout)` (geometry.rs:272, 376-383), i.e. `layout.width` — the BORDER box. But `LegacyPaintDispatcher::dispatch` (crates/rustmotion-components/src/legacy_dispatch.rs) hands every non-codeblock painter a synthetic `BoxLayout { width: cw, height: ch, border/padding: zero }` taken from `layout.content_box()` and translates the canvas to the content-box origin. So `Text::paint` wraps and draws inside `cw`, not `layout.width`. A `white-space: nowrap` text with `style.padding: "0 24px"` in a 300px box has `cw = 252`; a natural line width of 280px paints 28px past the box's right edge, yet `280 > 300` is false and nothing is reported. The sibling check `check_content_overflows_box` (geometry.rs:848) does this correctly via `layout.content_box()` — the two checks disagree about which box text lives in, and the flagship `unwrappable_text_overflow` is the one that is wrong. The gap equals `padding.left + padding.right + border.left + border.right`. Refs #220 --- .../rustmotion/src/cli/commands/geometry.rs | 51 ++++++++++++------- crates/rustmotion/tests/audit_ws_b.rs | 51 +++++++++++++++++++ 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/crates/rustmotion/src/cli/commands/geometry.rs b/crates/rustmotion/src/cli/commands/geometry.rs index e1c10bf4..b502d461 100644 --- a/crates/rustmotion/src/cli/commands/geometry.rs +++ b/crates/rustmotion/src/cli/commands/geometry.rs @@ -300,7 +300,7 @@ fn walk( check_unwrappable_text( &child.component, &child_path, - &raw_bbox, + layout, viewport, vi, si, @@ -791,10 +791,32 @@ fn measurer_and_nowrap(component: &Component) -> Option<(Box bbox.w + 0.5 { + if natural_w > cw + 0.5 { let kind = component_kind(component); out.push(GeometryViolation { view_index: vi, @@ -830,11 +842,16 @@ fn check_unwrappable_text( component: kind.to_string(), axis: Axis::X, kind: ViolationKind::UnwrappableTextOverflow, - bbox: *bbox, + bbox: BBox { + x: cx, + y: cy, + w: cw, + h: ch, + }, viewport, hint: format!( "{kind} natural width is {natural_w:.0}px but only {:.0}px available — remove style.white-space: nowrap (or set it to normal) so it can wrap, or reduce style.font-size", - bbox.w + cw ), }); } diff --git a/crates/rustmotion/tests/audit_ws_b.rs b/crates/rustmotion/tests/audit_ws_b.rs index 0d4923e0..7d550394 100644 --- a/crates/rustmotion/tests/audit_ws_b.rs +++ b/crates/rustmotion/tests/audit_ws_b.rs @@ -354,3 +354,54 @@ fn fix_leaves_relative_asset_paths_untouched() { "the actual violation --fix targeted must still be fixed: {fixed}" ); } + +// ─── unwrappable_text_overflow must measure the CONTENT box ──────── + +/// A nowrap text's own painter draws inside its CONTENT box +/// (`LegacyPaintDispatcher` hands it `layout.content_box()`, not the raw +/// layout box, for every component except `codeblock`) — so the geometry +/// check must compare the natural line width against the content box too. +/// Content box width here is 2000 - 1900 = 100px (950px of padding on each +/// side); the border box is 2000px. Any real natural width for this +/// string/font-size sits comfortably in between, so the violation fires if +/// and only if the content box is used. +#[test] +fn unwrappable_text_overflow_is_measured_against_the_content_box() { + let scenario = ScratchFile::new("rm31-scenario"); + let report = ScratchFile::new("rm31-report"); + let json = r##"{ + "video": { "width": 2400, "height": 1080 }, + "scenes": [{ + "duration": 1.0, + "children": [{ + "type": "text", + "content": "Hello World Example", + "position": "absolute", + "x": 50, "y": 50, + "style": { + "width": "2000px", "height": "300px", + "padding": { "top": "20px", "right": "950px", "bottom": "20px", "left": "950px" }, + "white-space": "nowrap", + "font-size": "48px", + "color": "#ffffff" + } + }] + }] + }"##; + std::fs::write(&scenario.0, json).expect("write scenario"); + + let output = run_validate(&scenario.0, Some(&report.0), false, false); + let report_json = read_report(&report.0); + assert!( + !output.status.success(), + "the 100px content box (2000px border box minus 1900px of padding) is too narrow \ + for this nowrap line; report={report_json}" + ); + let violation = find_kind(&report_json, "unwrappable_text_overflow") + .expect("expected an unwrappable_text_overflow violation"); + let width = violation["bbox"]["w"].as_f64().expect("bbox.w is a number"); + assert!( + (width - 100.0).abs() < 1.0, + "violation bbox should be the 100px CONTENT box, not the 2000px border box: {report_json}" + ); +}