From 9478302228267943b6e673365be047487114d345 Mon Sep 17 00:00:00 2001 From: Steffen Carlsen Date: Fri, 5 Jun 2026 03:44:59 +0200 Subject: [PATCH] Fix button auto-size text measurement --- Lessons.md | 1 + src/ModernOverlay.UI/Controls.cs | 10 +++++-- .../OverlayUiButtonControlTests.cs | 27 ++++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Lessons.md b/Lessons.md index 6011e5c..c93a3f8 100644 --- a/Lessons.md +++ b/Lessons.md @@ -22,6 +22,7 @@ - Treat screenshot-reported UI precision bugs as shared geometry suspects first. Hit testing, caret placement, slider bounds, and popup placement usually belong in shared measurement, transform, bounds, or z-layer code rather than one-off component fixes. - Keep the UI A/B sample useful as a validation tool, not just a showcase. Add visible state, labels, and layout previews when controls otherwise look inert or ambiguous. - For retained text input, caret, selection, and scrolling should share measured text advances. Any fallback heuristic must be treated as a temporary approximation and tested against proportional text. +- For retained UI controls that render text with measured glyph widths, auto-size measurement should use the same measured text path. Character-count heuristics are only safe as fallbacks when no render measurement context is available. ## PR Review And Triage diff --git a/src/ModernOverlay.UI/Controls.cs b/src/ModernOverlay.UI/Controls.cs index 22b6a73..155ec20 100644 --- a/src/ModernOverlay.UI/Controls.cs +++ b/src/ModernOverlay.UI/Controls.cs @@ -553,11 +553,17 @@ protected override SizeF MeasureCore(SizeF availableSize) } float fontSize = Root?.ThemeResources.Theme.FontSize ?? UiTheme.Default.FontSize; - float width = MathF.Min(availableSize.Width, Text.Length * fontSize * 0.6f + Padding.Horizontal); - float height = fontSize * 1.35f + Padding.Vertical; + SizeF textSize = MeasureButtonText(Text, fontSize); + float width = MathF.Min(availableSize.Width, textSize.Width + Padding.Horizontal); + float height = textSize.Height * 1.35f + Padding.Vertical; return new SizeF(MathF.Max(MinWidth, width), MathF.Max(MinHeight, height)); } + private SizeF MeasureButtonText(string value, float fontSize) + => Root is { } root && root.TryMeasureText(value, root.ThemeResources.Font, out SizeF measured) + ? measured + : new SizeF(value.Length * fontSize * 0.6f, fontSize); + protected override void ArrangeCore(RectF finalRect) { ArrangeContent(); diff --git a/tests/ModernOverlay.Tests/OverlayUiButtonControlTests.cs b/tests/ModernOverlay.Tests/OverlayUiButtonControlTests.cs index c474ccf..f69caa9 100644 --- a/tests/ModernOverlay.Tests/OverlayUiButtonControlTests.cs +++ b/tests/ModernOverlay.Tests/OverlayUiButtonControlTests.cs @@ -153,6 +153,29 @@ public async Task ButtonTextAlignmentControlsTextOrigin() Assert.AreEqual(left.ContentBounds.X, leftOrigin.X, 0.001f); } + [TestMethod] + [TestCategory("WindowsIntegration")] + public async Task ButtonAutoSizeUsesMeasuredTextWidth() + { + await using OverlayWindow overlay = await CreateOverlayAsync(); + using OverlayUiRoot ui = OverlayUi.Attach(overlay, new OverlayUiOptions { RegisterInputRegions = false }); + UiButton button = new() + { + Text = "WWW", + Height = 28f, + HorizontalAlignment = UiHorizontalAlignment.Left, + VerticalAlignment = UiVerticalAlignment.Top, + }; + Canvas.SetLeft(button, 10f); + Canvas.SetTop(button, 10f); + ui.Root.Children.Add(button); + + ui.Render(new DrawContext(new RecordingDrawCommandSink())); + + Assert.AreEqual(140f, button.DesiredSize.Width, 0.001f); + Assert.AreEqual(140f, button.Bounds.Width, 0.001f); + } + [TestMethod] [TestCategory("WindowsIntegration")] public async Task RadioButtonClearsPeersAndHonorsDisabledDynamicGroupChanges() @@ -336,7 +359,9 @@ public void DrawTextLayout(TextLayoutHandle layout, BrushHandle brush, PointF or => AddPrimitive(); public SizeF MeasureText(string text, FontHandle font) - => new(text.Length, font.Options.Size); + => text == "WWW" + ? new SizeF(120f, font.Options.Size) + : new SizeF(text.Length, font.Options.Size); public SizeF MeasureTextLayout(TextLayoutHandle layout) => new(layout.Text.Length, layout.Font.Options.Size);