From 0f6343b90e2e026d9f2e8a49515bdc7e2a2263c3 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sat, 18 Jul 2026 18:37:37 +0800 Subject: [PATCH] Fix border-box flex relayout for Opera header References isu issue 281; follow-up tracked as isu issue 408. --- .isu/issues.json | 16 ++++++++++- crates/layout/src/lib.rs | 59 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/.isu/issues.json b/.isu/issues.json index a43dfbb..ffbbf70 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 408, + "next_id": 409, "issues": [ { "id": 1, @@ -4992,6 +4992,20 @@ "author": "piefev", "state": "open", "created_at": "2026-07-18T09:59:47Z" + }, + { + "id": 408, + "repo": "we", + "title": "Opera parity remains blocked after border-box flex header fix", + "body": "Parent: isu issue 281. Follow-up after fixing border-box flex item relayout so a padded header wrapper does not subtract its horizontal padding twice.\n\nThe Opera desktop header download button now aligns with the Chromium golden, improving the direct scenario desktop assertion, but the scenario remains xfailed and issue 281 should stay open.\n\nRepro:\n cargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/opera.com.we --out-dir crates/e2e/artifacts\n\nCurrent direct-run results after this pass:\n - desktop screenshot assertion at opera.com.we L96: 85.17% match; 182137/1228500 pixels differ at tolerance 4\n - mobile screenshot assertion at opera.com.we L111: 69.85% match; 99256/329160 pixels differ at tolerance 4\n\nArtifacts:\n - crates/e2e/artifacts/real-web/opera.com/desktop.png\n - crates/e2e/artifacts/real-web/opera.com/desktop.png.diff.png\n - crates/e2e/artifacts/real-web/opera.com/mobile.png\n - crates/e2e/artifacts/real-web/opera.com/mobile.png.diff.png\n - crates/e2e/artifacts/real-web/opera.com/desktop_dom.txt\n - crates/e2e/artifacts/real-web/opera.com/mobile_dom.txt\n - crates/e2e/artifacts/real-web/opera.com/desktop_console.txt\n - crates/e2e/artifacts/real-web/opera.com/mobile_console.txt\n\nRemaining visible blockers are still concentrated in first-viewport image parity, text rasterization/antialiasing, and cookie-panel shadow/text fidelity. Keep crates/e2e/scenarios/real-web/opera.com.we xfail until both Chromium screenshot assertions pass within the committed threshold.", + "labels": [ + "real-web", + "layout" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-07-18T10:37:20Z" } ] } diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index d5f941f..67ae969 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -3695,6 +3695,13 @@ fn fixed_horizontal_margins(b: &LayoutBox) -> f32 { left + right } +fn css_length_for_content_size(content_size: f32, box_sizing: BoxSizing, extra: f32) -> f32 { + match box_sizing { + BoxSizing::ContentBox => content_size, + BoxSizing::BorderBox => content_size + extra, + } +} + /// Recursively measure max-content width of a layout box tree. fn measure_box_content_width( b: &LayoutBox, @@ -5631,7 +5638,15 @@ fn layout_flex_children( // Set up the child for layout at the resolved main size. if is_row { - child.css_width = LengthOrAuto::Length(target_main); + let horizontal_extra = child.border.left + + child.border.right + + child.padding.left + + child.padding.right; + child.css_width = LengthOrAuto::Length(css_length_for_content_size( + target_main, + child.box_sizing, + horizontal_extra, + )); compute_layout( child, 0.0, @@ -12336,6 +12351,48 @@ header.hf__header { ); } + #[test] + fn border_box_flex_item_padding_is_not_subtracted_twice_for_child_layout() { + // A flex item is tracked internally by content-box main size. When + // re-laying it out at the resolved flex size, a border-box CSS width + // must include padding; otherwise the child layout sees the content + // width with padding subtracted a second time. + let mut doc = Document::new(); + let (_, _, body) = make_html_body(&mut doc); + let outer = doc.create_element("div"); + let wrapper = doc.create_element("div"); + let brand = doc.create_element("div"); + let menu = doc.create_element("div"); + doc.append_child(body, outer); + doc.append_child(outer, wrapper); + doc.append_child(wrapper, brand); + doc.append_child(wrapper, menu); + doc.set_attribute(body, "style", "margin:0;"); + doc.set_attribute(outer, "style", "display:flex;width:600px;"); + doc.set_attribute( + wrapper, + "style", + "box-sizing:border-box;display:flex;width:100%;padding-left:50px;padding-right:50px;", + ); + doc.set_attribute(brand, "style", "width:100px;height:20px;"); + doc.set_attribute(menu, "style", "flex:1;height:20px;"); + + let tree = layout_doc_viewport(&doc, 600.0, 300.0); + let wrapper_box = find_box(&tree.root, wrapper).expect("wrapper box"); + let menu_box = find_box(&tree.root, menu).expect("menu box"); + + assert!( + (wrapper_box.rect.width - 500.0).abs() < 0.01, + "wrapper content width should be 600px minus 100px padding, got {}", + wrapper_box.rect.width + ); + assert!( + (menu_box.rect.width - 400.0).abs() < 0.01, + "menu should receive remaining wrapper content width, got {}", + menu_box.rect.width + ); + } + #[test] fn absolute_bottom_right_positioning() { let mut doc = Document::new(); -- 2.51.2