Skip to content

Fix System.OverflowException in CustomScrollViewerEx during Mouse Wheel scroll#4497

Open
4yinn wants to merge 2 commits into
Flow-Launcher:devfrom
4yinn:hotfix/fix-overflow-scrolling
Open

Fix System.OverflowException in CustomScrollViewerEx during Mouse Wheel scroll#4497
4yinn wants to merge 2 commits into
Flow-Launcher:devfrom
4yinn:hotfix/fix-overflow-scrolling

Conversation

@4yinn
Copy link
Copy Markdown
Contributor

@4yinn 4yinn commented May 29, 2026

This PR fixes the OverflowException reported in #4481, which occurs when the user scrolls on a ScrollViewer that has not been fully rendered yet or whose ActualHeight is 0.

Root Cause

The issue originates from the following calculation inside OnMouseWheel:

var wheelChange = e.Delta * (ViewportHeight / 1.5) / ActualHeight;

When ActualHeight is 0, the result becomes double.Infinity.
This value is later passed into the animation logic, which eventually converts it into a TimeSpan using TimeSpan.FromTicks. Since Infinity cannot be represented as a valid duration, a System.OverflowException is thrown.

Changes
Added a guard clause in CustomScrollViewerEx.cs to return early when:

ActualHeight <= 0
ActualWidth <= 0

Prevented invalid Infinity/NaN scroll offsets from reaching the animation pipeline.
Ensured the animation logic only receives finite values.
Testing
Added MouseWheelTest.cs to simulate a MouseWheelEvent on a CustomScrollViewerEx with zero dimensions.
Verified that:
without the fix, the scroll logic attempts to process an infinite offset;
with the fix, the method exits safely without triggering an exception.

I’m also open to feedback if you think there’s a cleaner or more robust approach for handling this scenario.


Summary by cubic

Fixes an OverflowException in CustomScrollViewerEx when scrolling before layout completes by exiting early on zero width/height and preventing infinite deltas from reaching animation. Adds an STA unit test to cover zero-size scroll scenarios.

Summary of changes

  • Changed: OnMouseWheel now returns early when ActualHeight <= 0 or ActualWidth <= 0; only finite deltas are sent to animation.
  • Added: MouseWheelTest to simulate wheel scroll on a zero-size control; test project now references Flow.Launcher.
  • Removed: None.
  • Memory: No change.
  • Security: No new risks; stricter input validation reduces crash surface.
  • Tests: Yes—MouseWheelTest ensures no exception is thrown on zero-size control.

Release Note
Fixes a crash when using the mouse wheel to scroll before the window finishes rendering.

Written for commit 4fa1f3d. Summary will update on new commits.

Review in cubic

@github-actions github-actions Bot added this to the 2.2.0 milestone May 29, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds early-return guards in CustomScrollViewerEx.OnMouseWheel to avoid division-by-zero when layout sizes are non-positive, and adds a test (MouseWheelTest) plus a test-project ProjectReference to invoke the handler via reflection and assert it does not throw.

Changes

MouseWheel Handler Robustness

Layer / File(s) Summary
CustomScrollViewerEx OnMouseWheel safety
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
OnMouseWheel now early-returns when ActualHeight <= 0 (vertical) or ActualWidth <= 0 (horizontal) to prevent division-by-zero in wheel-delta calculations. Includes non-functional reformatting of AutoPanningMode base value retrieval, AutoHideScrollBarsProperty.AddOwner call formatting, and ChangeView signature line breaks.
Test project setup and OnMouseWheel test
Flow.Launcher.Test/Flow.Launcher.Test.csproj, Flow.Launcher.Test/MouseWheelTest.cs
Adds a ProjectReference from the test project to the main Flow.Launcher project. Introduces MouseWheelTest.Test_Scroll_MouseWheel which constructs a CustomScrollViewerEx, builds a MouseWheelEventArgs, and invokes the non-public OnMouseWheel via reflection, asserting no exception is thrown.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

  • #4481: Related to the same OnMouseWheel implementation and scroll animation/guarding behavior.

Suggested reviewers

  • jjw24
  • taooceros
  • JohnTheGr8
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: preventing an OverflowException in CustomScrollViewerEx during mouse wheel scrolling, which is the primary change across all modified files.
Description check ✅ Passed The description is directly related to the changeset, providing clear context about the OverflowException bug, root cause analysis, and the specific guards added to prevent it.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs (1)

91-101: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset animation state on guard-path returns.

SetIsAnimating(this, true) is set before guard checks, but both guard returns exit without resetting it. That can leave animation state stuck as true and suppress offset state updates in OnScrollChanged.

Suggested fix
-            ScrollViewerBehavior.SetIsAnimating(this, true);
-
             if (Direction == Orientation.Vertical)
             {
+                if (ActualHeight <= 0)
+                    return;
+
+                ScrollViewerBehavior.SetIsAnimating(this, true);
+
                 if (ScrollableHeight > 0)
                 {
                     e.Handled = true;
                 }
-
-                if (ActualHeight <= 0)
-                    return;
             else
             {
+                if (ActualWidth <= 0)
+                    return;
+
+                ScrollViewerBehavior.SetIsAnimating(this, true);
+
                 if (ScrollableWidth > 0)
                 {
                     e.Handled = true;
                 }
-
-                if (ActualHeight <= 0)
-                    return;

Also applies to: 133-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs` around lines 91 -
101, The handler sets ScrollViewerBehavior.SetIsAnimating(this, true) before
guard checks but returns on two guard paths without resetting it; update the
method in CustomScrollViewerEx (the scroll event handler that calls
SetIsAnimating) so that you either move the SetIsAnimating(this, true) call to
after the guard checks or ensure you call
ScrollViewerBehavior.SetIsAnimating(this, false) before each early return
(including the guard near the Vertical Orientation block and the other guard
around lines ~133-134) so the animation flag is never left true when exiting
early.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Flow.Launcher.Test/MouseWheelTest.cs`:
- Around line 26-34: The reflection lookup for CustomScrollViewerEx's
"OnMouseWheel" is not asserted before Invoke, which causes a confusing
null-reference if the method isn't found; add an explicit null assertion (e.g.
Assert.IsNotNull or Assert.NotNull) for onMouseWheelMethod after the GetMethod
call with a clear message like "OnMouseWheel method not found on
CustomScrollViewerEx" so failures are diagnostic, then proceed to call
onMouseWheelMethod.Invoke(scrollView, new object[] { e }) inside the existing
Assert.DoesNotThrow block.

In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 133-136: In CustomScrollViewerEx (the mouse wheel/scroll handling
code that computes WheelChange using e.Delta * (ViewportWidth / 1.5) /
ActualWidth), change the early-return guard to validate ActualWidth instead of
ActualHeight (i.e., if (ActualWidth <= 0) return;) so you never divide by zero
when computing WheelChange; update the check in the same method where
WheelChange is declared to prevent the infinite offset on horizontal scrolling.

---

Outside diff comments:
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 91-101: The handler sets ScrollViewerBehavior.SetIsAnimating(this,
true) before guard checks but returns on two guard paths without resetting it;
update the method in CustomScrollViewerEx (the scroll event handler that calls
SetIsAnimating) so that you either move the SetIsAnimating(this, true) call to
after the guard checks or ensure you call
ScrollViewerBehavior.SetIsAnimating(this, false) before each early return
(including the guard near the Vertical Orientation block and the other guard
around lines ~133-134) so the animation flag is never left true when exiting
early.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e62e2923-df1f-435a-97a3-52643a176be2

📥 Commits

Reviewing files that changed from the base of the PR and between ef7d89d and 57c4eb6.

📒 Files selected for processing (3)
  • Flow.Launcher.Test/Flow.Launcher.Test.csproj
  • Flow.Launcher.Test/MouseWheelTest.cs
  • Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs

Comment on lines +26 to +34
var onMouseWheelMethod = typeof(CustomScrollViewerEx).GetMethod(
"OnMouseWheel",
BindingFlags.NonPublic | BindingFlags.Instance
);

Assert.DoesNotThrow(() =>
{
onMouseWheelMethod.Invoke(scrollView, new object[] { e });
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert reflected method lookup before invocation.

Add an explicit null assertion for onMouseWheelMethod so failures are diagnostic (method not found) instead of a generic null-reference during invoke.

Suggested fix
         var onMouseWheelMethod = typeof(CustomScrollViewerEx).GetMethod(
             "OnMouseWheel",
             BindingFlags.NonPublic | BindingFlags.Instance
         );
+        Assert.That(onMouseWheelMethod, Is.Not.Null, "OnMouseWheel method was not found via reflection.");
 
         Assert.DoesNotThrow(() =>
         {
             onMouseWheelMethod.Invoke(scrollView, new object[] { e });
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var onMouseWheelMethod = typeof(CustomScrollViewerEx).GetMethod(
"OnMouseWheel",
BindingFlags.NonPublic | BindingFlags.Instance
);
Assert.DoesNotThrow(() =>
{
onMouseWheelMethod.Invoke(scrollView, new object[] { e });
});
var onMouseWheelMethod = typeof(CustomScrollViewerEx).GetMethod(
"OnMouseWheel",
BindingFlags.NonPublic | BindingFlags.Instance
);
Assert.That(onMouseWheelMethod, Is.Not.Null, "OnMouseWheel method was not found via reflection.");
Assert.DoesNotThrow(() =>
{
onMouseWheelMethod.Invoke(scrollView, new object[] { e });
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Flow.Launcher.Test/MouseWheelTest.cs` around lines 26 - 34, The reflection
lookup for CustomScrollViewerEx's "OnMouseWheel" is not asserted before Invoke,
which causes a confusing null-reference if the method isn't found; add an
explicit null assertion (e.g. Assert.IsNotNull or Assert.NotNull) for
onMouseWheelMethod after the GetMethod call with a clear message like
"OnMouseWheel method not found on CustomScrollViewerEx" so failures are
diagnostic, then proceed to call onMouseWheelMethod.Invoke(scrollView, new
object[] { e }) inside the existing Assert.DoesNotThrow block.

Comment thread Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 133-134: In OnMouseWheel of CustomScrollViewerEx the method sets
the animating flag (IsAnimating) earlier but returns early when ActualWidth <= 0
without clearing it, which can leave IsAnimating stuck; modify OnMouseWheel to
reset IsAnimating (or the animating field) to false before the early return so
the animation state is cleared and OnScrollChanged can continue syncing offsets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ea371989-0f85-4582-a0d5-ae6ec2cb5aef

📥 Commits

Reviewing files that changed from the base of the PR and between 57c4eb6 and 4fa1f3d.

📒 Files selected for processing (1)
  • Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs

Comment on lines +133 to +134
if (ActualWidth <= 0)
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset animation state before guard returns.

OnMouseWheel sets animating true at Line 91, but this new early return exits without resetting it. That can leave IsAnimating stuck and block OnScrollChanged from syncing offsets.

Proposed minimal fix
 protected override void OnMouseWheel(MouseWheelEventArgs e)
 {
     var Direction = GetDirection();
     ScrollViewerBehavior.SetIsAnimating(this, true);

     if (Direction == Orientation.Vertical)
     {
         if (ScrollableHeight > 0)
         {
             e.Handled = true;
         }

-        if (ActualHeight <= 0)
-            return;
+        if (ActualHeight <= 0)
+        {
+            ScrollViewerBehavior.SetIsAnimating(this, false);
+            return;
+        }
@@
     else
     {
         if (ScrollableWidth > 0)
         {
             e.Handled = true;
         }

-        if (ActualWidth <= 0)
-            return;
+        if (ActualWidth <= 0)
+        {
+            ScrollViewerBehavior.SetIsAnimating(this, false);
+            return;
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs` around lines 133 -
134, In OnMouseWheel of CustomScrollViewerEx the method sets the animating flag
(IsAnimating) earlier but returns early when ActualWidth <= 0 without clearing
it, which can leave IsAnimating stuck; modify OnMouseWheel to reset IsAnimating
(or the animating field) to false before the early return so the animation state
is cleared and OnScrollChanged can continue syncing offsets.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs">

<violation number="1" location="Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs:133">
P1: Animation state `IsAnimating` is set to `true` before dimension guard-clauses, so an early return when `ActualWidth <= 0` or `ActualHeight <= 0` leaves the flag permanently stuck. `ScrollToValue` is the only place in the file that resets it to `false`, and it is never reached on these early-exit paths. When stuck true, `OnScrollChanged` stops updating `LastVerticalLocation`/`LastHorizontalLocation`, leading to stale offsets and incorrect subsequent wheel behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment on lines +133 to +134
if (ActualWidth <= 0)
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Animation state IsAnimating is set to true before dimension guard-clauses, so an early return when ActualWidth <= 0 or ActualHeight <= 0 leaves the flag permanently stuck. ScrollToValue is the only place in the file that resets it to false, and it is never reached on these early-exit paths. When stuck true, OnScrollChanged stops updating LastVerticalLocation/LastHorizontalLocation, leading to stale offsets and incorrect subsequent wheel behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs, line 133:

<comment>Animation state `IsAnimating` is set to `true` before dimension guard-clauses, so an early return when `ActualWidth <= 0` or `ActualHeight <= 0` leaves the flag permanently stuck. `ScrollToValue` is the only place in the file that resets it to `false`, and it is never reached on these early-exit paths. When stuck true, `OnScrollChanged` stops updating `LastVerticalLocation`/`LastHorizontalLocation`, leading to stale offsets and incorrect subsequent wheel behavior.</comment>

<file context>
@@ -130,7 +130,7 @@ protected override void OnMouseWheel(MouseWheelEventArgs e)
                 }
 
-                if (ActualHeight <= 0)
+                if (ActualWidth <= 0)
                     return;
 
</file context>
Suggested change
if (ActualWidth <= 0)
return;
if (ActualWidth <= 0)
{
ScrollViewerBehavior.SetIsAnimating(this, false);
return;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: System.OverflowException: TimeSpan overflowed in ScrollViewerEx.AnimateScroll

1 participant