Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Lessons.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- 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
Expand Down
11 changes: 11 additions & 0 deletions src/ModernOverlay.UI/AdvancedControls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public double Minimum
get => minimum;
set
{
ValidateFinite(value, nameof(value));
minimum = value;
if (maximum < minimum)
{
Expand All @@ -72,6 +73,7 @@ public double Maximum
get => maximum;
set
{
ValidateFinite(value, nameof(value));
maximum = Math.Max(value, Minimum);
Value = this.value;
}
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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.");
}
}
}

/// <summary>
Expand Down
11 changes: 11 additions & 0 deletions src/ModernOverlay.UI/Controls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ public float Minimum
get => minimum;
set
{
ValidateFinite(value, nameof(value));
SetProperty(ref minimum, value, UiInvalidation.Render);
if (maximum < minimum)
{
Expand All @@ -1012,6 +1013,7 @@ public float Maximum
get => maximum;
set
{
ValidateFinite(value, nameof(value));
SetProperty(ref maximum, MathF.Max(value, minimum), UiInvalidation.Render);
Value = this.value;
}
Expand All @@ -1025,6 +1027,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);
Expand Down Expand Up @@ -1076,6 +1079,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.");
}
}
}

/// <summary>
Expand Down
16 changes: 16 additions & 0 deletions tests/ModernOverlay.Tests/OverlayUiRangeNumericTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,22 @@ public async Task NumberBoxParsesInvariantTextRejectsInvalidInputAndUsesStepButt
}
}

[TestMethod]
public void RangeControlsRejectNonFiniteNumericValues()
{
Slider slider = new();

Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => slider.Minimum = float.NaN);
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => slider.Maximum = float.PositiveInfinity);
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => slider.Value = float.NegativeInfinity);

NumberBox number = new();

Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => number.Minimum = double.NaN);
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => number.Maximum = double.PositiveInfinity);
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => number.Value = double.NegativeInfinity);
}

private static async ValueTask<OverlayWindow> CreateOverlayAsync()
=> await OverlayWindow.CreateAsync(new OverlayWindowOptions
{
Expand Down
Loading