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}" + ); +}