diff --git a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/ScrollContentPresenter.cs b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/ScrollContentPresenter.cs index 4fc89a79859a..1cff6cb9c9d7 100644 --- a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/ScrollContentPresenter.cs +++ b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/ScrollContentPresenter.cs @@ -176,14 +176,14 @@ public void MouseWheelRight() global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.Controls.ScrollContentPresenter", "void ScrollContentPresenter.MouseWheelRight()"); } #endif - #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || false || __NETSTD_REFERENCE__ || false + #if __ANDROID__ || __IOS__ || NET461 || false || false || __NETSTD_REFERENCE__ || false [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "NET461", "__WASM__", "__NETSTD_REFERENCE__")] public void SetHorizontalOffset( double offset) { global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.Controls.ScrollContentPresenter", "void ScrollContentPresenter.SetHorizontalOffset(double offset)"); } #endif - #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || false || __NETSTD_REFERENCE__ || false + #if __ANDROID__ || __IOS__ || NET461 || false || false || __NETSTD_REFERENCE__ || false [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "NET461", "__WASM__", "__NETSTD_REFERENCE__")] public void SetVerticalOffset( double offset) { diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.Managed.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.Managed.cs index 6e0bd2f7932c..35d3b337f8c1 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.Managed.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.Managed.cs @@ -19,61 +19,6 @@ namespace Windows.UI.Xaml.Controls { public partial class ScrollContentPresenter : ContentPresenter, ICustomClippingElement { - // Default physical amount to scroll with Up/Down/Left/Right key - //const double ScrollViewerLineDelta = 16.0; - - // This value comes from WHEEL_DELTA defined in WinUser.h. It represents the universal default mouse wheel delta. - const int ScrollViewerDefaultMouseWheelDelta = 120; - - // These macros compute how many integral pixels need to be scrolled based on the viewport size and mouse wheel delta. - // - First the maximum between 48 and 15% of the viewport size is picked. - // - Then that number is multiplied by (mouse wheel delta/120), 120 being the universal default value. - // - Finally if the resulting number is larger than the viewport size, then that viewport size is picked instead. - private static double GetVerticalScrollWheelDelta(Size size, double delta) - => Math.Min(Math.Floor(size.Height), Math.Round(delta * Math.Max(48.0, Math.Round(size.Height * 0.15, 0)) / ScrollViewerDefaultMouseWheelDelta, 0)); - private static double GetHorizontalScrollWheelDelta(Size size, double delta) - => Math.Min(Math.Floor(size.Width), Math.Round(delta * Math.Max(48.0, Math.Round(size.Width * 0.15, 0)) / ScrollViewerDefaultMouseWheelDelta, 0)); - - // Minimum value of MinZoomFactor, ZoomFactor and MaxZoomFactor - // ZoomFactor can be manipulated to a slightly smaller value, but - // will jump back to 0.1 when the manipulation completes. - //const double ScrollViewerMinimumZoomFactor = 0.1f; - - // Tolerated rounding delta in pixels between requested scroll offset and - // effective value. Used to handle non-DM-driven scrolls. - //const double ScrollViewerScrollRoundingTolerance = 0.05f; - - // Tolerated rounding delta in pixels between requested scroll offset and - // effective value for cases where IScrollInfo is implemented by a - // IManipulationDataProvider provider. Used to handle non-DM-driven scrolls. - //const double ScrollViewerScrollRoundingToleranceForProvider = 1.0f; - - // Delta required between the current scroll offsets and target scroll offsets - // in order to warrant a call to BringIntoViewport instead of - // SetOffsetsWithExtents, SetHorizontalOffset, SetVerticalOffset. - //const double ScrollViewerScrollRoundingToleranceForBringIntoViewport = 0.001f; - - // Tolerated rounding delta in between requested zoom factor and - // effective value. Used to handle non-DM-driven zooms. - //const double ScrollViewerZoomExtentRoundingTolerance = 0.001f; - - // Tolerated rounding delta in between old and new zoom factor - // in DM delta handling. - //const double ScrollViewerZoomRoundingTolerance = 0.000001f; - - // Delta required between the current zoom factor and target zoom factor - // in order to warrant a call to BringIntoViewport instead of ZoomToFactor. - //const double ScrollViewerZoomRoundingToleranceForBringIntoViewport = 0.00001f; - - // When a snap point is within this tolerance of the scrollviewer's extent - // minus its viewport we nudge the snap point back into place. - //const double ScrollViewerSnapPointLocationTolerance = 0.0001f; - - // If a ScrollViewer is going to reflow around docked CoreInputView occlussions - // by shrinking its viewport, we want to at least guarantee that it will keep - // an appropriate size. - //const double ScrollViewerMinHeightToReflowAroundOcclusions = 32.0f; - private /*readonly - partial*/ IScrollStrategy _strategy; private bool _canHorizontallyScroll; @@ -123,7 +68,7 @@ partial void InitializePartial() _strategy.Initialize(this); // Mouse wheel support - PointerWheelChanged += ScrollContentPresenter_PointerWheelChanged; + PointerWheelChanged += PointerWheelScroll; // Touch scroll support ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY; // Updated in PrepareTouchScroll! @@ -132,12 +77,6 @@ partial void InitializePartial() ManipulationCompleted += CompleteTouchScroll; } - public void SetVerticalOffset(double offset) - => Set(verticalOffset: offset); - - public void SetHorizontalOffset(double offset) - => Set(horizontalOffset: offset); - /// protected override void OnContentChanged(object oldValue, object newValue) { @@ -162,7 +101,7 @@ internal bool Set( double? horizontalOffset = null, double? verticalOffset = null, float? zoomFactor = null, - bool disableAnimation = true, + bool disableAnimation = false, bool isIntermediate = false) { var success = true; @@ -215,44 +154,6 @@ private void Apply(bool disableAnimation, bool isIntermediate) InvalidateViewport(); } - // Ensure the offset we're scrolling to is valid. - private double ValidateInputOffset(double offset, int minOffset, double maxOffset) - { - if (offset.IsNaN()) - { - throw new InvalidOperationException($"Invalid scroll offset value"); - } - - return Math.Max(minOffset, Math.Min(offset, maxOffset)); - } - - private void ScrollContentPresenter_PointerWheelChanged(object sender, Input.PointerRoutedEventArgs e) - { - var properties = e.GetCurrentPoint(null).Properties; - - if (Content is UIElement) - { - var canScrollHorizontally = CanHorizontallyScroll; - var canScrollVertically = CanVerticallyScroll; - - if (e.KeyModifiers == VirtualKeyModifiers.Control) - { - // TODO: Handle zoom https://github.com/unoplatform/uno/issues/4309 - } - else if (!canScrollVertically || properties.IsHorizontalMouseWheel || e.KeyModifiers == VirtualKeyModifiers.Shift) - { - if (canScrollHorizontally) - { - SetHorizontalOffset(HorizontalOffset + GetHorizontalScrollWheelDelta(DesiredSize, -properties.MouseWheelDelta * ScrollViewerDefaultMouseWheelDelta)); - } - } - else - { - SetVerticalOffset(VerticalOffset + GetVerticalScrollWheelDelta(DesiredSize, properties.MouseWheelDelta * ScrollViewerDefaultMouseWheelDelta)); - } - } - } - private void PrepareTouchScroll(object sender, ManipulationStartingRoutedEventArgs e) { if (e.Container != this) @@ -289,6 +190,7 @@ private void UpdateTouchScroll(object sender, ManipulationDeltaRoutedEventArgs e Set( horizontalOffset: HorizontalOffset - e.Delta.Translation.X, verticalOffset: VerticalOffset - e.Delta.Translation.Y, + disableAnimation: true, isIntermediate: true); } @@ -299,7 +201,7 @@ private void CompleteTouchScroll(object sender, ManipulationCompletedRoutedEvent return; } - Set(isIntermediate: false); + Set(disableAnimation: true, isIntermediate: false); } bool ICustomClippingElement.AllowClippingToLayoutSlot => true; diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.cs index 055c3e49b67e..f4ceecc569c9 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.cs @@ -2,6 +2,7 @@ using System; using Windows.Foundation; using Uno.UI; +using Windows.System; #if XAMARIN_ANDROID using View = Android.Views.View; using Font = Android.Graphics.Typeface; @@ -182,7 +183,71 @@ protected override Size ArrangeOverride(Size finalSize) internal override bool IsViewHit() => true; -#elif __IOS__ // Note: No __ANDROID__, the ICustomScrollInfo support is made directly in the NativeScrollContentPresenter + + private void PointerWheelScroll(object sender, Input.PointerRoutedEventArgs e) + { + var properties = e.GetCurrentPoint(null).Properties; + + if (Content is UIElement) + { + var canScrollHorizontally = CanHorizontallyScroll; + var canScrollVertically = CanVerticallyScroll; + var delta = IsPointerWheelReversed + ? -properties.MouseWheelDelta + : properties.MouseWheelDelta; + + if (e.KeyModifiers == VirtualKeyModifiers.Control) + { + // TODO: Handle zoom https://github.com/unoplatform/uno/issues/4309 + } + else if (!canScrollVertically || properties.IsHorizontalMouseWheel || e.KeyModifiers == VirtualKeyModifiers.Shift) + { + if (canScrollHorizontally) + { +#if __WASM__ // On wasm the scroll might be async (especially with disableAnimation: false), so we need to use the pending value to support high speed multiple wheel events + var horizontalOffset = _pendingScrollTo?.horizontal ?? HorizontalOffset; +#else + var horizontalOffset = HorizontalOffset; +#endif + + Set( + horizontalOffset: horizontalOffset + GetHorizontalScrollWheelDelta(DesiredSize, delta), + disableAnimation: false); + } + } + else + { +#if __WASM__ // On wasm the scroll might be async (especially with disableAnimation: false), so we need to use the pending value to support high speed multiple wheel events + var verticalOffset = _pendingScrollTo?.vertical ?? VerticalOffset; +#else + var verticalOffset = VerticalOffset; +#endif + + Set( + verticalOffset: verticalOffset + GetVerticalScrollWheelDelta(DesiredSize, -delta), + disableAnimation: false); + } + } + } + + public void SetVerticalOffset(double offset) + => Set(verticalOffset: offset, disableAnimation: true); + + public void SetHorizontalOffset(double offset) + => Set(horizontalOffset: offset, disableAnimation: true); + + // Ensure the offset we're scrolling to is valid. + private double ValidateInputOffset(double offset, int minOffset, double maxOffset) + { + if (offset.IsNaN()) + { + throw new InvalidOperationException($"Invalid scroll offset value"); + } + + return Math.Max(minOffset, Math.Min(offset, maxOffset)); + } + +#elif __IOS__ // Note: No __ANDROID__, the ICustomScrollInfo support is made directly in the NativeScrollContentPresenter protected override Size MeasureOverride(Size size) { var result = base.MeasureOverride(size); diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.mux.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.mux.cs index f0b04eac8c6d..7d772fea20df 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.mux.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.mux.cs @@ -10,6 +10,62 @@ namespace Windows.UI.Xaml.Controls; public partial class ScrollContentPresenter { + // Default physical amount to scroll with Up/Down/Left/Right key + //const double ScrollViewerLineDelta = 16.0; + + // This value comes from WHEEL_DELTA defined in WinUser.h. It represents the universal default mouse wheel delta. + internal const int ScrollViewerDefaultMouseWheelDelta = 120; + + // These macros compute how many integral pixels need to be scrolled based on the viewport size and mouse wheel delta. + // - First the maximum between 48 and 15% of the viewport size is picked. + // - Then that number is multiplied by (mouse wheel delta/120), 120 being the universal default value. + // - Finally if the resulting number is larger than the viewport size, then that viewport size is picked instead. + private static double GetVerticalScrollWheelDelta(Size size, double delta) + => Math.Min(Math.Floor(size.Height), Math.Round(delta * Math.Max(48.0, Math.Round(size.Height * 0.15, 0)) / ScrollViewerDefaultMouseWheelDelta, 0)); + private static double GetHorizontalScrollWheelDelta(Size size, double delta) + => Math.Min(Math.Floor(size.Width), Math.Round(delta * Math.Max(48.0, Math.Round(size.Width * 0.15, 0)) / ScrollViewerDefaultMouseWheelDelta, 0)); + + // Minimum value of MinZoomFactor, ZoomFactor and MaxZoomFactor + // ZoomFactor can be manipulated to a slightly smaller value, but + // will jump back to 0.1 when the manipulation completes. + //const double ScrollViewerMinimumZoomFactor = 0.1f; + + // Tolerated rounding delta in pixels between requested scroll offset and + // effective value. Used to handle non-DM-driven scrolls. + //const double ScrollViewerScrollRoundingTolerance = 0.05f; + + // Tolerated rounding delta in pixels between requested scroll offset and + // effective value for cases where IScrollInfo is implemented by a + // IManipulationDataProvider provider. Used to handle non-DM-driven scrolls. + //const double ScrollViewerScrollRoundingToleranceForProvider = 1.0f; + + // Delta required between the current scroll offsets and target scroll offsets + // in order to warrant a call to BringIntoViewport instead of + // SetOffsetsWithExtents, SetHorizontalOffset, SetVerticalOffset. + //const double ScrollViewerScrollRoundingToleranceForBringIntoViewport = 0.001f; + + // Tolerated rounding delta in between requested zoom factor and + // effective value. Used to handle non-DM-driven zooms. + //const double ScrollViewerZoomExtentRoundingTolerance = 0.001f; + + // Tolerated rounding delta in between old and new zoom factor + // in DM delta handling. + //const double ScrollViewerZoomRoundingTolerance = 0.000001f; + + // Delta required between the current zoom factor and target zoom factor + // in order to warrant a call to BringIntoViewport instead of ZoomToFactor. + //const double ScrollViewerZoomRoundingToleranceForBringIntoViewport = 0.00001f; + + // When a snap point is within this tolerance of the scrollviewer's extent + // minus its viewport we nudge the snap point back into place. + //const double ScrollViewerSnapPointLocationTolerance = 0.0001f; + + // If a ScrollViewer is going to reflow around docked CoreInputView occlussions + // by shrinking its viewport, we want to at least guarantee that it will keep + // an appropriate size. + //const double ScrollViewerMinHeightToReflowAroundOcclusions = 32.0f; + + // BringIntoView functionality is ported from WinUI ScrollPresenter // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/ScrollPresenter/ScrollPresenter.cpp // with partial modifications to match the ScrollViewer control behavior. diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.uno.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.uno.cs new file mode 100644 index 000000000000..a95a7b90577c --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.uno.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.UI.Xaml; + +namespace Uno.UI.Xaml.Controls +{ + public class ScrollContentPresenter + { + /// + /// Backing property for the IsPointerWheelReversed attached property. + /// + public static readonly DependencyProperty IsPointerWheelReversedProperty = DependencyProperty.RegisterAttached( + "IsPointerWheelReversed", + typeof(bool), + typeof(Windows.UI.Xaml.Controls.ScrollContentPresenter), + new FrameworkPropertyMetadata((snd, e) => ((Windows.UI.Xaml.Controls.ScrollContentPresenter)snd).IsPointerWheelReversed = (bool)e.NewValue)); + + /// + /// Gets a boolean which indicates if the pointer wheel should be reversed or not for the . + /// + /// + /// + public static bool GetIsPointerWheelReversed(Windows.UI.Xaml.Controls.ScrollContentPresenter scrollViewer) + => (bool)scrollViewer.GetValue(IsPointerWheelReversedProperty); + + /// + /// Sets if the pointer wheel should be reversed or not for the . + /// + /// The target ScrollViewer to configure + /// A boolean which indicates if the wheel should be reversed of not. + public static void SetIsPointerWheelReversed(Windows.UI.Xaml.Controls.ScrollContentPresenter scrollViewer, bool isReversed) + => scrollViewer.SetValue(IsPointerWheelReversedProperty, isReversed); + + } +} + +namespace Windows.UI.Xaml.Controls +{ + partial class ScrollContentPresenter + { + private bool _isPointerWheelReversed; + + /// + /// Cached value of , + /// in order to not access the DP on each scroll (perf considerations) + /// + internal bool IsPointerWheelReversed + { + get => _isPointerWheelReversed; + set + { + if (_isPointerWheelReversed != value) + { + _isPointerWheelReversed = value; + OnIsPointerWheelReversedChanged(value); + } + } + } + + partial void OnIsPointerWheelReversedChanged(bool isReversed); + } +} diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.wasm.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.wasm.cs index de86c17b4f75..6dcdaf14504c 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.wasm.cs @@ -13,6 +13,7 @@ using Uno.UI.Xaml; using Uno.UI.Extensions; +using Uno.UI.Xaml.Input; namespace Windows.UI.Xaml.Controls { @@ -37,6 +38,24 @@ internal Size ScrollBarSize private object RealContent => Content; + partial void OnIsPointerWheelReversedChanged(bool isReversed) + { + PointerWheelChanged -= ManagedScroll; + if (isReversed) + { + PointerWheelChanged += ManagedScroll; + } + + static void ManagedScroll(object sender, Input.PointerRoutedEventArgs e) + { + // When pointer wheel is reversed, we scroll in managed code and prevent the browser to scroll (PreventDefault) + e.Handled = true; + ((IHtmlHandleableRoutedEventArgs)e).HandledResult |= HtmlEventDispatchResult.PreventDefault; + + ((ScrollContentPresenter)sender).PointerWheelScroll(sender, e); + } + } + private void TryRegisterEvents(ScrollBarVisibility visibility) { if ( @@ -49,20 +68,20 @@ private void TryRegisterEvents(ScrollBarVisibility visibility) _eventsRegistered = true; - PointerReleased += HandlePointerEvent; - PointerPressed += HandlePointerEvent; - PointerCanceled += HandlePointerEvent; - PointerMoved += HandlePointerEvent; - PointerEntered += HandlePointerEvent; - PointerExited += HandlePointerEvent; - PointerWheelChanged += HandlePointerEvent; + PointerReleased += HandlePointerEventIfOverNativeScrollbars; + PointerPressed += HandlePointerEventIfOverNativeScrollbars; + PointerCanceled += HandlePointerEventIfOverNativeScrollbars; + PointerMoved += HandlePointerEventIfOverNativeScrollbars; + PointerEntered += HandlePointerEventIfOverNativeScrollbars; + PointerExited += HandlePointerEventIfOverNativeScrollbars; + PointerWheelChanged += HandlePointerEventIfOverNativeScrollbars; } } - private static void HandlePointerEvent(object sender, Input.PointerRoutedEventArgs e) - => ((ScrollContentPresenter)sender).HandlePointerEvent(e); + private static void HandlePointerEventIfOverNativeScrollbars(object sender, Input.PointerRoutedEventArgs e) + => ((ScrollContentPresenter)sender).HandlePointerEventIfOverNativeScrollbars(e); - private void HandlePointerEvent(Input.PointerRoutedEventArgs e) + private void HandlePointerEventIfOverNativeScrollbars(Input.PointerRoutedEventArgs e) { var (clientSize, offsetSize) = WindowManagerInterop.GetClientViewSize(HtmlId); @@ -166,7 +185,10 @@ private void RestoreScroll() { if (sv.HorizontalOffset > 0 || sv.VerticalOffset > 0) { - ScrollTo(sv.HorizontalOffset, sv.VerticalOffset, disableAnimation: true); + Set( + horizontalOffset: sv.HorizontalOffset, + verticalOffset: sv.VerticalOffset, + disableAnimation: true); } } } @@ -191,8 +213,32 @@ internal override void OnLayoutUpdated() TryProcessScrollTo(); } - public void ScrollTo(double? horizontalOffset, double? verticalOffset, bool disableAnimation) + internal bool Set( + double? horizontalOffset = null, + double? verticalOffset = null, + float? zoomFactor = null, // Not supported yet + bool disableAnimation = false, + bool isIntermediate = false) // Not supported yet { + var success = true; + if (horizontalOffset is double hOffset) + { + var extentWidth = ExtentWidth; + var viewportWidth = ViewportWidth; + + horizontalOffset = ValidateInputOffset(hOffset, 0, extentWidth - viewportWidth); + success &= horizontalOffset == hOffset; + } + + if (verticalOffset is double vOffset) + { + var extentHeight = ExtentHeight; + var viewportHeight = ViewportHeight; + + verticalOffset = ValidateInputOffset(vOffset, 0, extentHeight - viewportHeight); + success &= verticalOffset == vOffset; + } + _pendingScrollTo = (horizontalOffset, verticalOffset); WindowManagerInterop.ScrollTo(HtmlId, horizontalOffset, verticalOffset, disableAnimation); @@ -228,7 +274,11 @@ public void ScrollTo(double? horizontalOffset, double? verticalOffset, bool disa var willNotScroll = horizontalOffset < 0 && nativeHorizontalOffset == 0 || verticalOffset < 0 && nativeVerticalOffset == 0; - if (!willNotScroll) + if (willNotScroll) + { + return false; + } + else { // As the native ScrollTo is going to be async, we manually raise the event with the provided values. // If those values are invalid, the browser will raise the final event anyway. @@ -242,8 +292,17 @@ public void ScrollTo(double? horizontalOffset, double? verticalOffset, bool disa } } } + + return success; // If if not yet processed, we assume that it will be. } + // Backward compat, use the shared "Set" method instead. + public void ScrollTo(double? horizontalOffset, double? verticalOffset, bool disableAnimation) + => Set( + horizontalOffset: horizontalOffset, + verticalOffset: verticalOffset, + disableAnimation: disableAnimation); + private void TryProcessScrollTo(object sender, object e) => TryProcessScrollTo();