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);
+}