diff --git a/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.Properties.cs b/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.Properties.cs index 88fb70b62..c84848c82 100644 --- a/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.Properties.cs +++ b/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.Properties.cs @@ -100,23 +100,6 @@ private double HorizontalZoomCenter set => SetValue(HorizontalZoomCenterProperty, value); } - #endregion - #region DependencyProperty: [Private] IsHorizontalScrollBarVisible - - /// Identifies the IsHorizontalScrollBarVisible dependency property. - private static DependencyProperty IsHorizontalScrollBarVisibleProperty { get; } = DependencyProperty.Register( - nameof(IsHorizontalScrollBarVisible), - typeof(bool), - typeof(ZoomContentControl), - new PropertyMetadata(true)); - - /// Gets or sets a value indicating whether the horizontal scrollbar is visible. - private bool IsHorizontalScrollBarVisible - { - get => (bool)GetValue(IsHorizontalScrollBarVisibleProperty); - set => SetValue(IsHorizontalScrollBarVisibleProperty, value); - } - #endregion #region DependencyProperty: [Private] VerticalScrollValue @@ -184,23 +167,6 @@ private double VerticalZoomCenter set => SetValue(VerticalZoomCenterProperty, value); } - #endregion - #region DependencyProperty: [Private] IsVerticalScrollBarVisible - - /// Identifies the IsVerticalScrollBarVisible dependency property. - private static DependencyProperty IsVerticalScrollBarVisibleProperty { get; } = DependencyProperty.Register( - nameof(IsVerticalScrollBarVisible), - typeof(bool), - typeof(ZoomContentControl), - new PropertyMetadata(true)); - - /// Gets or sets a value indicating whether the vertical scrollbar is visible. - private bool IsVerticalScrollBarVisible - { - get => (bool)GetValue(IsVerticalScrollBarVisibleProperty); - set => SetValue(IsVerticalScrollBarVisibleProperty, value); - } - #endregion #region DependencyProperty: [Private] ZoomLevel diff --git a/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.cs b/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.cs index 78a2d47d5..90a76e83d 100644 --- a/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.cs +++ b/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.cs @@ -159,8 +159,20 @@ private void UpdateScrollVisibility() { if (Viewport is { } vp) { - IsHorizontalScrollBarVisible = vp.ActualWidth < ScrollExtentWidth; - IsVerticalScrollBarVisible = vp.ActualHeight < ScrollExtentHeight; + ToggleScrollBarVisibility(_scrollH, vp.ActualWidth < ScrollExtentWidth); + ToggleScrollBarVisibility(_scrollV, vp.ActualWidth < ScrollExtentWidth); + } + + void ToggleScrollBarVisibility(ScrollBar? sb, bool value) + { + if (sb is null) return; + + // Showing/hiding the ScrollBar(s)could cause the ContentPresenter to move as it re-centers. + // This adds unnecessary complexity for the zooming logics as we need to preserve the focal point + // under the cursor position or the pinch center point after zooming. + // To avoid all that, we just make them permanently there for layout calculation. + sb.IsEnabled = value; + sb.Opacity = value ? 1 : 0; } } @@ -271,7 +283,7 @@ private void OnPointerMoved(object sender, PointerRoutedEventArgs e) var delta = context.Position - position; delta.X *= -1; - ScrollValue = context.ScrollOffset + delta; + SetScrollValue(context.ScrollOffset + delta); } } @@ -289,23 +301,41 @@ private void OnPointerWheelChanged(object sender, PointerRoutedEventArgs e) #endif ) return; - // MouseWheel + Ctrl: Zoom if (e.KeyModifiers.HasFlag(Windows.System.VirtualKeyModifiers.Control)) { if (!IsZoomAllowed) return; - return; // todo + var oldPosition = (p.Position - vp.ActualSize.ToPoint().DivideBy(2)) - ScrollValue.MultiplyBy(1, -1); + var basePosition = oldPosition.DivideBy(ZoomLevel); + + var newZoom = ZoomLevel * (1 + p.Properties.MouseWheelDelta * ScaleWheelRatio); + //var newZoom = ZoomLevel + Math.Sign(p.Properties.MouseWheelDelta); + newZoom = Math.Clamp(newZoom, MinZoomLevel, MaxZoomLevel); + + var newPosition = basePosition.MultiplyBy(newZoom); + var delta = (newPosition - oldPosition).MultiplyBy(-1, 1); + var offset = ScrollValue + delta; + + // note: updating ZoomLevel can have side effects on ScrollValue: + // ZoomLevel --UpdateScrollBars-> ScrollBar.Maximum --clamp-> ScrollBar.Value --bound-> H/VScrollValue + // before we set the ZoomLevel, make sure to snapshot ScrollValue or finish the calculation using ScrollValue + ZoomLevel = newZoom; + SetScrollValue(offset, shouldClamp: false); + + e.Handled = true; } // MouseWheel + Shift: Scroll Horizontally // MouseWheel: Scroll Vertically else { - var delta = p.Properties.MouseWheelDelta * PanWheelRatio; - ScrollValue += e.KeyModifiers.HasFlag(Windows.System.VirtualKeyModifiers.Shift) - ? new (delta, 0) - : new (0, -delta); + var magnitude = p.Properties.MouseWheelDelta * PanWheelRatio; + var delta = e.KeyModifiers.HasFlag(Windows.System.VirtualKeyModifiers.Shift) + ? new Point(magnitude, 0) + : new Point(0, -magnitude); + var offset = ScrollValue + delta; + SetScrollValue(offset); e.Handled = true; } } @@ -337,6 +367,20 @@ public void FitToCanvas() } } + private void SetScrollValue(Point value, bool shouldClamp = true) + { + // we allow unconstrained panning for MouseWheelZoom(desktop) and PinchToZoom(mobile) + // where the focal point should remain stationary after zooming. + if (shouldClamp) + { + value.X = Math.Clamp(value.X, HorizontalMinScroll, HorizontalMaxScroll); + value.Y = Math.Clamp(value.Y, VerticalMinScroll, VerticalMaxScroll); + } + + HorizontalScrollValue = value.X; + VerticalScrollValue = value.Y; + } + // Helper private bool IsAllowedToWork => (IsLoaded && IsActive && _contentPresenter is not null); @@ -345,15 +389,7 @@ public void FitToCanvas() private FrameworkElement? Viewport => _contentGrid; - private Point ScrollValue - { - get => new Point(HorizontalScrollValue, VerticalScrollValue); - set - { - HorizontalScrollValue = Math.Clamp(value.X, HorizontalMinScroll, HorizontalMaxScroll); - VerticalScrollValue = Math.Clamp(value.Y, VerticalMinScroll, VerticalMaxScroll); - } - } + private Point ScrollValue => new Point(HorizontalScrollValue, VerticalScrollValue); private double ScrollExtentWidth => (ContentWidth + AdditionalMargin.Left + AdditionalMargin.Right) * ZoomLevel; private double ScrollExtentHeight => (ContentHeight + AdditionalMargin.Top + AdditionalMargin.Bottom) * ZoomLevel; diff --git a/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.xaml b/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.xaml index 22b61e151..ad874f0f2 100644 --- a/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.xaml +++ b/src/Uno.Toolkit.UI/Controls/ZoomContentControl/ZoomContentControl.xaml @@ -1,4 +1,4 @@ - @@ -19,7 +19,7 @@ - + @@ -51,7 +51,6 @@ Minimum="{TemplateBinding VerticalMinScroll}" Orientation="Vertical" SmallChange="1" - Visibility="{TemplateBinding IsVerticalScrollBarVisible}" ViewportSize="{TemplateBinding ViewportHeight}" Value="{Binding VerticalScrollValue, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" /> diff --git a/src/Uno.Toolkit.UI/Extensions/TwoDExtensions.cs b/src/Uno.Toolkit.UI/Extensions/TwoDExtensions.cs new file mode 100644 index 000000000..4033cf7f8 --- /dev/null +++ b/src/Uno.Toolkit.UI/Extensions/TwoDExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.UI.WebUI; + +namespace Uno.Toolkit.UI; + +internal static partial class TwoDExtensions // Point Mathematics +{ + public static Point ToPoint(this Size x) => new Point(x.Width, x.Height); + public static Point ToPoint(this Vector2 x) => new Point(x.X, x.Y); + + public static Point MultiplyBy(this Point x, double scale) => new Point(x.X * scale, x.Y * scale); + public static Point MultiplyBy(this Point x, double scaleX, double scaleY) => new Point(x.X * scaleX, x.Y * scaleY); + public static Point DivideBy(this Point x, double scale) => new Point(x.X / scale, x.Y / scale); +} + +internal static partial class TwoDExtensions // Size Mathematics +{ + public static Size ToSize(this Point x) => new Size(x.X, x.Y); + public static Size ToSize(this Vector2 x) => new Size(x.X, x.Y); + + public static Size MultiplyBy(this Size x, double scale) => new Size(x.Width * scale, x.Height * scale); + public static Size DivideBy(this Size x, double scale) => new Size(x.Width / scale, x.Height / scale); +}