From d4b647cde444a2141e37ba93eaec3d64efd91c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Fri, 13 Nov 2026 17:17:46 +0100 Subject: [PATCH] Measure the content of a float before its shrink-to-fit width The words of the descendants were only measured once their own layout ran, so on the first layout pass an auto width float or absolutely positioned box got a width of 0. --- Source/HtmlRenderer/Core/Dom/CssBox.cs | 13 ++++++++++ .../Core/Dom/CssLayoutEngineTable.cs | 19 +------------- .../BoxModel/WidthIntegrationTests.cs | 25 +++++++++++++++++-- .../TestSupport/LayoutHarness.cs | 14 ++++++++--- Spec/css-position-3.yml | 1 - Spec/css-sizing-3.yml | 1 - Spec/css-sizing-4.yml | 2 +- Spec/css21.yml | 5 +--- 8 files changed, 49 insertions(+), 31 deletions(-) diff --git a/Source/HtmlRenderer/Core/Dom/CssBox.cs b/Source/HtmlRenderer/Core/Dom/CssBox.cs index fbb830828..9c28f4e10 100644 --- a/Source/HtmlRenderer/Core/Dom/CssBox.cs +++ b/Source/HtmlRenderer/Core/Dom/CssBox.cs @@ -811,6 +811,7 @@ protected virtual void PerformLayoutImp(RGraphics g) // Shrink-to-fit, CSS 2.1 §10.3.5 and §10.3.7. GetMinMaxWidth already includes the // padding and border. double minWidth, maxWidth; + MeasureDescendantWordsSize(g); GetMinMaxWidth(out minWidth, out maxWidth); width = Math.Min(Math.Max(minWidth, width), maxWidth); } @@ -1111,6 +1112,18 @@ internal virtual void MeasureWordsSize(RGraphics g) } } + /// + /// Measures the words of all descendants, which needs before their own layout ran. + /// + internal void MeasureDescendantWordsSize(RGraphics g) + { + foreach (var childBox in Boxes) + { + childBox.MeasureWordsSize(g); + childBox.MeasureDescendantWordsSize(g); + } + } + /// /// Get the parent of this css properties instance. /// diff --git a/Source/HtmlRenderer/Core/Dom/CssLayoutEngineTable.cs b/Source/HtmlRenderer/Core/Dom/CssLayoutEngineTable.cs index 7ec074be1..c654f43c9 100644 --- a/Source/HtmlRenderer/Core/Dom/CssLayoutEngineTable.cs +++ b/Source/HtmlRenderer/Core/Dom/CssLayoutEngineTable.cs @@ -141,7 +141,7 @@ public static void PerformLayout(RGraphics g, CssBox tableBox) /// private void Layout(RGraphics g) { - MeasureWords(_tableBox, g); + _tableBox.MeasureDescendantWordsSize(g); // get the table boxes into the proper fields AssignBoxKinds(); @@ -890,23 +890,6 @@ private static bool RowHasCellSpanningIntoSubsequentRow(CssBox row, int currentr return false; } - /// - /// Recursively measures words inside the box - /// - /// the box to measure - /// Device to use - private static void MeasureWords(CssBox box, RGraphics g) - { - if (box != null) - { - foreach (var childBox in box.Boxes) - { - childBox.MeasureWordsSize(g); - MeasureWords(childBox, g); - } - } - } - /// /// Tells if the columns widths can be reduced, /// by checking the minimum widths of all cells diff --git a/Source/Test/HtmlRenderer.IntegrationTest/BoxModel/WidthIntegrationTests.cs b/Source/Test/HtmlRenderer.IntegrationTest/BoxModel/WidthIntegrationTests.cs index 20d72ccc3..22222e32b 100644 --- a/Source/Test/HtmlRenderer.IntegrationTest/BoxModel/WidthIntegrationTests.cs +++ b/Source/Test/HtmlRenderer.IntegrationTest/BoxModel/WidthIntegrationTests.cs @@ -195,7 +195,6 @@ public void Float_WithAutoMargins_HasZeroMargins() Assert.AreEqual(50, f.Size.Width, Delta); } - [Ignore("Gap, see Spec/css21.yml")] [TestMethod] [Spec("css21", "visudet.html#float-width")] [Spec("css-sizing-3", "#intrinsic-sizes")] @@ -210,6 +209,29 @@ public void AutoWidthFloat_ShrinksToFitItsContent() Assert.AreEqual(words[0].Top, words[^1].Top, Delta); } + [TestMethod] + [Spec("css21", "visudet.html#float-width")] + public void AutoWidthFloat_IsSizedByTheFirstLayout() + { + var (root, container) = LayoutHarness.Layout(LayoutHarness.Wrap( + "
" + + "
8.6
" + + "

line one

line two

"), maxWidth: 400); + var f = Box(root, "f"); + var words = LayoutHarness.Descendants(f).SelectMany(b => b.Words).ToList(); + var size = container.ActualSize; + + Assert.AreEqual(words[^1].Right - words[0].Left, f.Size.Width, 1); + Assert.AreEqual(1, f.Location.X, Delta); + Assert.AreEqual(f.ActualRight + 24, FirstWord(Box(root, "p"))!.Left, 1); + + // Another layout pass must not change anything + LayoutHarness.PerformLayout(container); + + Assert.AreEqual(size.Width, container.ActualSize.Width, Delta); + Assert.AreEqual(size.Height, container.ActualSize.Height, Delta); + } + [TestMethod] [Spec("css21", "visudet.html#float-replaced-width")] public void FloatedImage_KeepsItsIntrinsicWidth() @@ -255,7 +277,6 @@ public void Absolute_WithLeftAndRight_StretchesTheAutoWidth() Assert.AreEqual(160, a.Size.Width, Delta); } - [Ignore("Gap, see Spec/css21.yml")] [TestMethod] [Spec("css21", "visudet.html#abs-non-replaced-width")] [Spec("css-position-3", "#abs-non-replaced-width")] diff --git a/Source/Test/HtmlRenderer.IntegrationTest/TestSupport/LayoutHarness.cs b/Source/Test/HtmlRenderer.IntegrationTest/TestSupport/LayoutHarness.cs index f5c2c0b45..74ab3fd81 100644 --- a/Source/Test/HtmlRenderer.IntegrationTest/TestSupport/LayoutHarness.cs +++ b/Source/Test/HtmlRenderer.IntegrationTest/TestSupport/LayoutHarness.cs @@ -57,16 +57,22 @@ internal static (CssBox Root, HtmlContainerInt Container) Layout( prepare(container.Root!); } - using var bitmap = new Bitmap(1, 1, PixelFormat.Format32bppArgb); - using var g = Graphics.FromImage(bitmap); - using var graphics = new GraphicsAdapter(g, false); - container.PerformLayout(graphics); + PerformLayout(container); Assert.IsNotNull(container.Root); return (container.Root!, container); } + /// Runs one layout pass of , e.g. a second one after . + internal static void PerformLayout(HtmlContainerInt container) + { + using var bitmap = new Bitmap(1, 1, PixelFormat.Format32bppArgb); + using var g = Graphics.FromImage(bitmap); + using var graphics = new GraphicsAdapter(g, false); + container.PerformLayout(graphics); + } + /// Wraps a body fragment in a minimal document, so a test can state only the markup it cares about. internal static string Wrap(string body) => $"{body}"; diff --git a/Spec/css-position-3.yml b/Spec/css-position-3.yml index 47f4025d5..8df041b5b 100644 --- a/Spec/css-position-3.yml +++ b/Spec/css-position-3.yml @@ -81,7 +81,6 @@ sections: status: partial gaps: - With both insets set, an auto size is its content size instead of the stretch-fit size - - The fit-content width is computed before the content is measured, see CSS 2.1 10.3.7 '#abspos-margins': title: 4.2 Auto Margins of Absolutely-Positioned Boxes status: partial diff --git a/Spec/css-sizing-3.yml b/Spec/css-sizing-3.yml index afc52669c..722679842 100644 --- a/Spec/css-sizing-3.yml +++ b/Spec/css-sizing-3.yml @@ -74,7 +74,6 @@ sections: title: 5.1 Intrinsic Sizes status: partial gaps: - - The fit-content width of an auto width float is computed before its content is measured, see CSS 2.1 10.3.5 - An auto width inline-block wider than the line wraps across lines instead of shrinking, see CSS 2.1 10.3.9 '#intrinsic-contribution': title: 5.2 Intrinsic Contributions diff --git a/Spec/css-sizing-4.yml b/Spec/css-sizing-4.yml index 791a968b7..19961c670 100644 --- a/Spec/css-sizing-4.yml +++ b/Spec/css-sizing-4.yml @@ -50,7 +50,7 @@ sections: title: 5.1 Intrinsic Sizes status: partial gaps: - - Floats and inline-blocks aren't sized to their content as specified, see css-sizing-3 5.1 + - Inline-blocks aren't sized to their content as specified, see css-sizing-3 5.1 '#intrinsic-size-override': title: '5.2 Overriding Contained Intrinsic Sizes: the contain-intrinsic-* properties' status: parsed-only diff --git a/Spec/css21.yml b/Spec/css21.yml index 6da038eb2..573a22f2e 100644 --- a/Spec/css21.yml +++ b/Spec/css21.yml @@ -503,9 +503,7 @@ sections: - Auto margins compute to 0, a block image is never centered by them visudet.html#float-width: title: 10.3.5 Floating, non-replaced elements - status: partial - gaps: - - The shrink-to-fit width of an auto width float is computed before its content is measured, on the first layout it's 0 or NaN and the text wraps after every word + status: supported visudet.html#float-replaced-width: title: 10.3.6 Floating, replaced elements status: supported @@ -514,7 +512,6 @@ sections: status: partial gaps: - With 'left' and 'right' set, an auto width is 0 instead of filling the space between them - - The shrink-to-fit width of an auto width box is computed before its content is measured, on the first layout it's 0 or NaN - With 'left', 'right' and 'width' set, auto margins compute to 0 instead of centering the box - With 'left' and 'right' auto, the box is placed at the containing block's content edge instead of its static position visudet.html#abs-replaced-width: