Skip to content

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

Open
4yinn wants to merge 5 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 5 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

Prevents an OverflowException in CustomScrollViewerEx when mouse-wheel scrolling before layout. Adds an early height guard and blocks non-finite deltas from reaching animation.

Summary of changes

  • Changed: OnMouseWheel now returns immediately when ActualHeight <= 0; guard moved to method start to avoid computing WheelChange on zero height; only finite deltas proceed to animation. Minor formatting updates from merging dev with no behavior changes.
  • Added: MouseWheelTest that invokes OnMouseWheel on an unrendered control; test project now references Flow.Launcher.
  • Removed: None.
  • Memory: No change.
  • Security: No new risks; stricter validation reduces crash surface.
  • Tests: Yes — unit test verifies no exception before layout.

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

Written for commit 7e3aa4a. 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

Adds an early-return guard in CustomScrollViewerEx.OnMouseWheel when ActualHeight <= 0, minor non-functional formatting in the control, a ProjectReference from the test project to the main project, and a new NUnit STA test that invokes the non-public OnMouseWheel via reflection and asserts it does not throw.

Changes

MouseWheel Handler Robustness

Layer / File(s) Summary
OnMouseWheel guard and formatting
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
Adds an early return in OnMouseWheel when ActualHeight <= 0 to avoid division-by-zero in wheel-delta math. Also includes non-functional reformatting of AutoPanningMode base value retrieval, AutoHideScrollBarsProperty.AddOwner call formatting, and ChangeView signature line breaks.
Test project reference 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 project and 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

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Suggested reviewers

  • jjw24
  • 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 pull request title directly and clearly describes the main change: fixing a System.OverflowException in CustomScrollViewerEx during mouse wheel scrolling.
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.
Description check ✅ Passed The pull request description clearly relates to the changeset, explaining the OverflowException fix and detailing the root cause, changes made, and testing approach.

✏️ 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 Outdated
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 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 1 file (changes from recent commits).

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

Re-trigger cubic

Comment thread Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs Outdated
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
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.

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)

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

Vertical guard must also reset animation state.

The horizontal path (lines 133-137) correctly resets IsAnimating to false before returning, but the vertical path here does not. Since SetIsAnimating(true) is called at line 91, this early return will leave IsAnimating stuck, preventing OnScrollChanged (line 168) from syncing offsets.

Proposed fix
-                if (ActualHeight <= 0)
-                    return;
+                if (ActualHeight <= 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 100 -
101, The vertical early-return when ActualHeight <= 0 fails to reset the
animation flag and leaves IsAnimating true; update the vertical guard in the
same method that calls SetIsAnimating(true) to call SetIsAnimating(false) (or
otherwise reset IsAnimating) before returning so the animation state is cleared
and OnScrollChanged can resync offsets; mirror the horizontal path's behavior
that resets IsAnimating to false prior to returning.
🤖 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.

Outside diff comments:
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 100-101: The vertical early-return when ActualHeight <= 0 fails to
reset the animation flag and leaves IsAnimating true; update the vertical guard
in the same method that calls SetIsAnimating(true) to call SetIsAnimating(false)
(or otherwise reset IsAnimating) before returning so the animation state is
cleared and OnScrollChanged can resync offsets; mirror the horizontal path's
behavior that resets IsAnimating to false prior to returning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9ed14a09-ed58-4711-9720-2e30a96598f7

📥 Commits

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

📒 Files selected for processing (1)
  • 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.

♻️ Duplicate comments (1)
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs (1)

90-91: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Direction-agnostic guard reintroduces horizontal divide-by-zero path.

The new guard checks only height, but horizontal scrolling still divides by ActualWidth (Line 133). If ActualWidth == 0, WheelChange can become infinite again and propagate into animation timing.

Suggested minimal fix
 protected override void OnMouseWheel(MouseWheelEventArgs e)
 {
-    if (ActualHeight <= 0)
-        return;
-
-    var Direction = GetDirection();
+    var Direction = GetDirection();
+    if ((Direction == Orientation.Vertical && ActualHeight <= 0) ||
+        (Direction == Orientation.Horizontal && ActualWidth <= 0))
+    {
+        return;
+    }
+
     ScrollViewerBehavior.SetIsAnimating(this, true);
🤖 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 90 -
91, The early return only checks ActualHeight but the code later computes
WheelChange by dividing by ActualWidth (risking divide-by-zero); update the
guard in CustomScrollViewerEx (the method containing the WheelChange
calculation) to check both ActualHeight and ActualWidth (return if either <= 0)
or otherwise protect the WheelChange computation by ensuring ActualWidth is
non-zero before dividing; reference ActualWidth, ActualHeight and WheelChange to
locate and fix the division-by-zero path.
🤖 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.

Duplicate comments:
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 90-91: The early return only checks ActualHeight but the code
later computes WheelChange by dividing by ActualWidth (risking divide-by-zero);
update the guard in CustomScrollViewerEx (the method containing the WheelChange
calculation) to check both ActualHeight and ActualWidth (return if either <= 0)
or otherwise protect the WheelChange computation by ensuring ActualWidth is
non-zero before dividing; reference ActualWidth, ActualHeight and WheelChange to
locate and fix the division-by-zero path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 326c3eca-b827-4876-857e-e9b013a3b57e

📥 Commits

Reviewing files that changed from the base of the PR and between fc0fd3c and ff7d7f7.

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

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