From d64076358766bc7625e74e0955880c47c3283416 Mon Sep 17 00:00:00 2001 From: Steffen Carlsen Date: Fri, 5 Jun 2026 03:54:58 +0200 Subject: [PATCH] Reject non-finite range values --- Lessons.md | 1 + src/ModernOverlay.UI/AdvancedControls.cs | 11 +++++++++++ src/ModernOverlay.UI/Controls.cs | 11 +++++++++++ .../OverlayUiRangeNumericTests.cs | 16 ++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/Lessons.md b/Lessons.md index 6011e5c..6ed12bc 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. +- Numeric UI controls should reject non-finite `Minimum`, `Maximum`, `Value`, and step values before clamping or render math runs. Letting `NaN` or infinities enter range state can poison slider/progress geometry. ## PR Review And Triage diff --git a/src/ModernOverlay.UI/AdvancedControls.cs b/src/ModernOverlay.UI/AdvancedControls.cs index 88668d2..c6d53b9 100644 --- a/src/ModernOverlay.UI/AdvancedControls.cs +++ b/src/ModernOverlay.UI/AdvancedControls.cs @@ -54,6 +54,7 @@ public double Minimum get => minimum; set { + ValidateFinite(value, nameof(value)); minimum = value; if (maximum < minimum) { @@ -72,6 +73,7 @@ public double Maximum get => maximum; set { + ValidateFinite(value, nameof(value)); maximum = Math.Max(value, Minimum); Value = this.value; } @@ -102,6 +104,7 @@ public double Value get => value; set { + ValidateFinite(value, nameof(value)); double next = Math.Clamp(value, Minimum, Maximum); if (this.value.Equals(next)) { @@ -185,6 +188,14 @@ private void RevertText() private static bool IsPartialNumberText(string text) => text.Length == 0 || text is "-" or "+" or "." or "-." or "+."; + + private static void ValidateFinite(double value, string parameterName) + { + if (!double.IsFinite(value)) + { + throw new ArgumentOutOfRangeException(parameterName, "Numeric values must be finite."); + } + } } /// diff --git a/src/ModernOverlay.UI/Controls.cs b/src/ModernOverlay.UI/Controls.cs index 22b6a73..a6ee88c 100644 --- a/src/ModernOverlay.UI/Controls.cs +++ b/src/ModernOverlay.UI/Controls.cs @@ -988,6 +988,7 @@ public float Minimum get => minimum; set { + ValidateFinite(value, nameof(value)); SetProperty(ref minimum, value, UiInvalidation.Render); if (maximum < minimum) { @@ -1006,6 +1007,7 @@ public float Maximum get => maximum; set { + ValidateFinite(value, nameof(value)); SetProperty(ref maximum, MathF.Max(value, minimum), UiInvalidation.Render); Value = this.value; } @@ -1019,6 +1021,7 @@ public float Value get => value; set { + ValidateFinite(value, nameof(value)); if (SetProperty(ref this.value, Math.Clamp(value, Minimum, Maximum), UiInvalidation.Render)) { ValueChanged?.Invoke(this, EventArgs.Empty); @@ -1070,6 +1073,14 @@ protected float ValueRatio } protected void ChangeValueBy(float delta) => Value += delta; + + private static void ValidateFinite(float value, string parameterName) + { + if (!float.IsFinite(value)) + { + throw new ArgumentOutOfRangeException(parameterName, "Range values must be finite."); + } + } } /// diff --git a/tests/ModernOverlay.Tests/OverlayUiRangeNumericTests.cs b/tests/ModernOverlay.Tests/OverlayUiRangeNumericTests.cs index f794b03..9a3fd0d 100644 --- a/tests/ModernOverlay.Tests/OverlayUiRangeNumericTests.cs +++ b/tests/ModernOverlay.Tests/OverlayUiRangeNumericTests.cs @@ -136,6 +136,22 @@ public async Task NumberBoxParsesInvariantTextRejectsInvalidInputAndUsesStepButt } } + [TestMethod] + public void RangeControlsRejectNonFiniteNumericValues() + { + Slider slider = new(); + + Assert.ThrowsExactly(() => slider.Minimum = float.NaN); + Assert.ThrowsExactly(() => slider.Maximum = float.PositiveInfinity); + Assert.ThrowsExactly(() => slider.Value = float.NegativeInfinity); + + NumberBox number = new(); + + Assert.ThrowsExactly(() => number.Minimum = double.NaN); + Assert.ThrowsExactly(() => number.Maximum = double.PositiveInfinity); + Assert.ThrowsExactly(() => number.Value = double.NegativeInfinity); + } + private static async ValueTask CreateOverlayAsync() => await OverlayWindow.CreateAsync(new OverlayWindowOptions {