From 2bc5467b27c2e9d61d55822d33098a06edd79557 Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Thu, 5 Mar 2026 22:07:01 -0500 Subject: [PATCH 1/4] Fix dropdown scrolling behavior in FADownloadableComboBox --- .../Controls/FADownloadableComboBox.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs b/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs index c9f2cb6a..a617ff0b 100644 --- a/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs +++ b/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs @@ -2,6 +2,10 @@ using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.DependencyInjection; @@ -17,6 +21,65 @@ public partial class FADownloadableComboBox : FAComboBox { protected override Type StyleKeyOverride => typeof(FAComboBox); + private Popup? dropDownPopup; + private IDisposable? openSubscription; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + DropDownOpened -= OnDropDownOpenedHandler; + DropDownClosed -= OnDropDownClosedHandler; + + // Template part name is "Popup" per FAComboBox.properties.cs (s_tpPopup = "Popup") + dropDownPopup = e.NameScope.Find("Popup"); + + DropDownOpened += OnDropDownOpenedHandler; + DropDownClosed += OnDropDownClosedHandler; + } + + private void OnDropDownOpenedHandler(object? sender, EventArgs e) + { + openSubscription?.Dispose(); + openSubscription = null; + + if (dropDownPopup?.Child is not Control popupChild) + return; + + var scrollViewer = popupChild.GetVisualDescendants().OfType().FirstOrDefault(); + + if (scrollViewer == null) + return; + + // On Linux, FAComboBox popups are rendered as overlay popups that share the + // same TopLevel visual root as the main window. FAComboBox.OnPopupOpened adds + // a TopLevel tunnel handler that marks ALL pointer-wheel events as handled + // (when dropdown is open and source.VisualRoot == TopLevel) to prevent parent + // ScrollViewers from stealing the event. The side-effect is that the popup's + // own internal ScrollViewer also never receives the event. + // + // Fix: add our own tunnel handler directly on the popup ScrollViewer + // (which runs after the TopLevel handler in tunnel order) that resets + // e.Handled = false, allowing the ScrollViewer's normal bubble handler + // to process the scroll and move the dropdown list. + openSubscription = scrollViewer.AddDisposableHandler( + PointerWheelChangedEvent, + static (_, ev) => + { + if (ev.Handled) + ev.Handled = false; + }, + RoutingStrategies.Tunnel, + handledEventsToo: true + ); + } + + private void OnDropDownClosedHandler(object? sender, EventArgs e) + { + openSubscription?.Dispose(); + openSubscription = null; + } + static FADownloadableComboBox() { SelectionChangedEvent.AddClassHandler( From e0552f99fa83bbb073adef62b29f7d6c72705f28 Mon Sep 17 00:00:00 2001 From: NeuralFault <65365345+NeuralFault@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:23:42 -0500 Subject: [PATCH 2/4] Removed comment to move to PR description --- .../Controls/FADownloadableComboBox.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs b/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs index a617ff0b..0ccf85e3 100644 --- a/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs +++ b/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs @@ -51,17 +51,6 @@ private void OnDropDownOpenedHandler(object? sender, EventArgs e) if (scrollViewer == null) return; - // On Linux, FAComboBox popups are rendered as overlay popups that share the - // same TopLevel visual root as the main window. FAComboBox.OnPopupOpened adds - // a TopLevel tunnel handler that marks ALL pointer-wheel events as handled - // (when dropdown is open and source.VisualRoot == TopLevel) to prevent parent - // ScrollViewers from stealing the event. The side-effect is that the popup's - // own internal ScrollViewer also never receives the event. - // - // Fix: add our own tunnel handler directly on the popup ScrollViewer - // (which runs after the TopLevel handler in tunnel order) that resets - // e.Handled = false, allowing the ScrollViewer's normal bubble handler - // to process the scroll and move the dropdown list. openSubscription = scrollViewer.AddDisposableHandler( PointerWheelChangedEvent, static (_, ev) => From 1c0f3d73d8829a05909d4ef88ed7f5944fbeac4c Mon Sep 17 00:00:00 2001 From: NeuralFault <65365345+NeuralFault@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:54:40 -0500 Subject: [PATCH 3/4] Add CleanupSubscription method for better code organization Refactor subscription cleanup into a separate method. --- .../Controls/FADownloadableComboBox.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs b/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs index 0ccf85e3..74f10565 100644 --- a/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs +++ b/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs @@ -28,6 +28,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); + CleanupSubscription(); DropDownOpened -= OnDropDownOpenedHandler; DropDownClosed -= OnDropDownClosedHandler; @@ -40,8 +41,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) private void OnDropDownOpenedHandler(object? sender, EventArgs e) { - openSubscription?.Dispose(); - openSubscription = null; + CleanupSubscription(); if (dropDownPopup?.Child is not Control popupChild) return; @@ -64,6 +64,11 @@ private void OnDropDownOpenedHandler(object? sender, EventArgs e) } private void OnDropDownClosedHandler(object? sender, EventArgs e) + { + CleanupSubscription(); + } + + private void CleanupSubscription() { openSubscription?.Dispose(); openSubscription = null; From c7d4f7ad8ffed02f7253dfa97be4e8df27e908dc Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Sat, 7 Mar 2026 03:47:57 -0500 Subject: [PATCH 4/4] Added IsUnix condition and comment. --- .../Controls/FADownloadableComboBox.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs b/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs index 74f10565..a4b987d0 100644 --- a/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs +++ b/StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs @@ -12,6 +12,7 @@ using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Controls; @@ -51,6 +52,13 @@ private void OnDropDownOpenedHandler(object? sender, EventArgs e) if (scrollViewer == null) return; + // On Unix-like systems, overlay popups share the same TopLevel visual root as the main window. + // FAComboBox.OnPopupOpened adds a TopLevel tunnel handler that marks all wheel eventsas handled while the dropdown is open, + // which inadvertently blocks scroll-wheelevents in popup menus in Inference model cards. + // Resetting e.Handled on the ScrollViewer's tunnel phase counters this. + if (!Compat.IsUnix) + return; + openSubscription = scrollViewer.AddDisposableHandler( PointerWheelChangedEvent, static (_, ev) =>