From a4f3e36a8f80041379992f526d8bc1d084f9aa94 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Wed, 7 Oct 2026 13:11:37 -0500 Subject: [PATCH] Keep percentage flex items beside their gaps --- MarkupString.Html/Emitters/LayoutHtml.cs | 9 +++++++-- MarkupString.Html/LayoutCss.cs | 2 +- MarkupString.Tests/Layout/BlockLayoutTests.cs | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/MarkupString.Html/Emitters/LayoutHtml.cs b/MarkupString.Html/Emitters/LayoutHtml.cs index 4169b22..9c3d5ab 100644 --- a/MarkupString.Html/Emitters/LayoutHtml.cs +++ b/MarkupString.Html/Emitters/LayoutHtml.cs @@ -107,11 +107,14 @@ private static void Rule(Rule rule, string kind, BorderStyle? border, HtmlLayout private static void Flex(Flex flex, HtmlLayoutWriter html) { + var items = flex.Items.IsDefault ? [] : flex.Items; + var spacing = flex.Separator is { } separator ? separator.DisplayWidth : Math.Max(0, flex.Gap); + var reserved = flex.Vertical ? 0 : spacing * Math.Max(0, items.Length - 1); html.Write("
"); - foreach (var block in flex.Items.IsDefault ? [] : flex.Items) + foreach (var block in items) { // Each item asks for its terminal width and grows to fill its row, so items that do not fit // side by side wrap onto rows of their own. Spare width is left alone only when the layout @@ -145,6 +148,8 @@ private static void Flex(Flex flex, HtmlLayoutWriter html) html.Write(item.Basis.Kind switch { BlockSizeKind.Cells => Number(item.Basis.Value) + "ch", + BlockSizeKind.Percent when reserved > 0 && item.Basis.Value > 0 => + "calc(" + Number(item.Basis.Value) + "% - " + Decimal(reserved * item.Basis.Value / 100.0) + "ch)", BlockSizeKind.Percent => Number(item.Basis.Value) + "%", _ => item.Min > 1 ? Number(item.Min) + "ch" : "0", }); diff --git a/MarkupString.Html/LayoutCss.cs b/MarkupString.Html/LayoutCss.cs index 2354df8..f983a2d 100644 --- a/MarkupString.Html/LayoutCss.cs +++ b/MarkupString.Html/LayoutCss.cs @@ -38,7 +38,7 @@ public static class LayoutCss ".ms-border-none.ms-rule::before, .ms-border-none.ms-rule::after { border-top: none; }\n" + ".ms-flex { display: flex; flex-wrap: wrap; }\n" + ".ms-flex.ms-vertical { flex-direction: column; }\n" + - ".ms-item { min-width: 0; }\n" + + ".ms-item { min-width: 0; box-sizing: border-box; }\n" + ".ms-flex.ms-divided > .ms-item + .ms-item { border-left: 1px solid var(--ms-separator, var(--ms-separator-default, currentColor)); padding-left: 1ch; }\n" + ".ms-figure { display: flow-root; }\n" + ".ms-figure-image { max-width: 100%; height: auto; }\n" + diff --git a/MarkupString.Tests/Layout/BlockLayoutTests.cs b/MarkupString.Tests/Layout/BlockLayoutTests.cs index 4f2087d..1f6483a 100644 --- a/MarkupString.Tests/Layout/BlockLayoutTests.cs +++ b/MarkupString.Tests/Layout/BlockLayoutTests.cs @@ -238,6 +238,22 @@ public async Task Flex_AutoItemsShareTheRest() await Assert.That(BlockLayout.Lines(flex, 20)[0].ToPlainText()).IsEqualTo("a b c "); } + [Test] + public async Task Flex_PercentageItemsLeaveRoomForTheirSeparatorInHtml() + { + var flex = new Flex( + [ + T("left").Sized(BlockSize.Percent(50)), + T("right").Sized(BlockSize.Percent(50)), + ]) + { Separator = P(" │ ") }; + + var html = BlockLayout.Build(flex, 40).Render(MarkupFormat.Html, Registry); + + await Assert.That(html).Contains("flex:1 1 calc(50% - 1.5ch)"); + await Assert.That(LayoutCss.Fixed).Contains(".ms-item { min-width: 0; box-sizing: border-box; }"); + } + [Test] public async Task Figure_TextFlowsRoundTheArt() {