using System; using Avalonia.Reactive; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; namespace Avalonia.Controls { /// /// A control which scrolls its content if the content is bigger than the space available. /// [TemplatePart("PART_HorizontalScrollBar", typeof(ScrollBar))] [TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))] public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { /// /// Defines the property. /// /// /// There is no public C# accessor for this property as it is intended to be bound to by a /// in the control's template. /// public static readonly DirectProperty CanHorizontallyScrollProperty = AvaloniaProperty.RegisterDirect( nameof(CanHorizontallyScroll), o => o.CanHorizontallyScroll); /// /// Defines the property. /// /// /// There is no public C# accessor for this property as it is intended to be bound to by a /// in the control's template. /// public static readonly DirectProperty CanVerticallyScrollProperty = AvaloniaProperty.RegisterDirect( nameof(CanVerticallyScroll), o => o.CanVerticallyScroll); /// /// Defines the property. /// public static readonly DirectProperty ExtentProperty = AvaloniaProperty.RegisterDirect(nameof(Extent), o => o.Extent, (o, v) => o.Extent = v); /// /// Defines the property. /// public static readonly DirectProperty OffsetProperty = AvaloniaProperty.RegisterDirect( nameof(Offset), o => o.Offset, (o, v) => o.Offset = v); /// /// Defines the property. /// public static readonly DirectProperty ViewportProperty = AvaloniaProperty.RegisterDirect(nameof(Viewport), o => o.Viewport, (o, v) => o.Viewport = v); /// /// Defines the property. /// public static readonly DirectProperty LargeChangeProperty = AvaloniaProperty.RegisterDirect( nameof(LargeChange), o => o.LargeChange); /// /// Defines the property. /// public static readonly DirectProperty SmallChangeProperty = AvaloniaProperty.RegisterDirect( nameof(SmallChange), o => o.SmallChange); /// /// Defines the HorizontalScrollBarMaximum property. /// /// /// There is no public C# accessor for this property as it is intended to be bound to by a /// in the control's template. /// public static readonly DirectProperty HorizontalScrollBarMaximumProperty = AvaloniaProperty.RegisterDirect( nameof(HorizontalScrollBarMaximum), o => o.HorizontalScrollBarMaximum); /// /// Defines the HorizontalScrollBarValue property. /// /// /// There is no public C# accessor for this property as it is intended to be bound to by a /// in the control's template. /// public static readonly DirectProperty HorizontalScrollBarValueProperty = AvaloniaProperty.RegisterDirect( nameof(HorizontalScrollBarValue), o => o.HorizontalScrollBarValue, (o, v) => o.HorizontalScrollBarValue = v); /// /// Defines the HorizontalScrollBarViewportSize property. /// /// /// There is no public C# accessor for this property as it is intended to be bound to by a /// in the control's template. /// public static readonly DirectProperty HorizontalScrollBarViewportSizeProperty = AvaloniaProperty.RegisterDirect( nameof(HorizontalScrollBarViewportSize), o => o.HorizontalScrollBarViewportSize); /// /// Defines the property. /// public static readonly AttachedProperty HorizontalScrollBarVisibilityProperty = AvaloniaProperty.RegisterAttached( nameof(HorizontalScrollBarVisibility), ScrollBarVisibility.Disabled); /// /// Defines the VerticalScrollBarMaximum property. /// /// /// There is no public C# accessor for this property as it is intended to be bound to by a /// in the control's template. /// public static readonly DirectProperty VerticalScrollBarMaximumProperty = AvaloniaProperty.RegisterDirect( nameof(VerticalScrollBarMaximum), o => o.VerticalScrollBarMaximum); /// /// Defines the VerticalScrollBarValue property. /// /// /// There is no public C# accessor for this property as it is intended to be bound to by a /// in the control's template. /// public static readonly DirectProperty VerticalScrollBarValueProperty = AvaloniaProperty.RegisterDirect( nameof(VerticalScrollBarValue), o => o.VerticalScrollBarValue, (o, v) => o.VerticalScrollBarValue = v); /// /// Defines the property. /// public static readonly AttachedProperty HorizontalSnapPointsTypeProperty = AvaloniaProperty.RegisterAttached( nameof(HorizontalSnapPointsType)); /// /// Defines the property. /// public static readonly AttachedProperty VerticalSnapPointsTypeProperty = AvaloniaProperty.RegisterAttached( nameof(VerticalSnapPointsType)); /// /// Defines the property. /// public static readonly AttachedProperty HorizontalSnapPointsAlignmentProperty = AvaloniaProperty.RegisterAttached( nameof(HorizontalSnapPointsAlignment)); /// /// Defines the property. /// public static readonly AttachedProperty VerticalSnapPointsAlignmentProperty = AvaloniaProperty.RegisterAttached( nameof(VerticalSnapPointsAlignment)); /// /// Defines the VerticalScrollBarViewportSize property. /// /// /// There is no public C# accessor for this property as it is intended to be bound to by a /// in the control's template. /// public static readonly DirectProperty VerticalScrollBarViewportSizeProperty = AvaloniaProperty.RegisterDirect( nameof(VerticalScrollBarViewportSize), o => o.VerticalScrollBarViewportSize); /// /// Defines the property. /// public static readonly AttachedProperty VerticalScrollBarVisibilityProperty = AvaloniaProperty.RegisterAttached( nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); /// /// Defines the property. /// public static readonly DirectProperty IsExpandedProperty = ScrollBar.IsExpandedProperty.AddOwner(o => o.IsExpanded); /// /// Defines the property. /// public static readonly AttachedProperty AllowAutoHideProperty = AvaloniaProperty.RegisterAttached( nameof(AllowAutoHide), true); /// /// Defines the property. /// public static readonly AttachedProperty IsScrollChainingEnabledProperty = AvaloniaProperty.RegisterAttached( nameof(IsScrollChainingEnabled), defaultValue: true); /// /// Defines the property. /// public static readonly AttachedProperty IsScrollInertiaEnabledProperty = AvaloniaProperty.RegisterAttached( nameof(IsScrollInertiaEnabled), defaultValue: true); /// /// Defines the event. /// public static readonly RoutedEvent ScrollChangedEvent = RoutedEvent.Register( nameof(ScrollChanged), RoutingStrategies.Bubble); internal const double DefaultSmallChange = 16; private IDisposable? _childSubscription; private ILogicalScrollable? _logicalScrollable; private Size _extent; private Vector _offset; private Size _viewport; private Size _oldExtent; private Vector _oldOffset; private Size _oldViewport; private Size _largeChange; private Size _smallChange = new Size(DefaultSmallChange, DefaultSmallChange); private bool _isExpanded; private IDisposable? _scrollBarExpandSubscription; /// /// Initializes static members of the class. /// static ScrollViewer() { HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); } /// /// Initializes a new instance of the class. /// public ScrollViewer() { LayoutUpdated += OnLayoutUpdated; } /// /// Occurs when changes are detected to the scroll position, extent, or viewport size. /// public event EventHandler? ScrollChanged { add => AddHandler(ScrollChangedEvent, value); remove => RemoveHandler(ScrollChangedEvent, value); } /// /// Gets the extent of the scrollable content. /// public Size Extent { get { return _extent; } private set { if (SetAndRaise(ExtentProperty, ref _extent, value)) { CalculatedPropertiesChanged(); } } } /// /// Gets or sets the current scroll offset. /// public Vector Offset { get { return _offset; } set { if (SetAndRaise(OffsetProperty, ref _offset, CoerceOffset(Extent, Viewport, value))) { CalculatedPropertiesChanged(); } } } /// /// Gets the size of the viewport on the scrollable content. /// public Size Viewport { get { return _viewport; } private set { if (SetAndRaise(ViewportProperty, ref _viewport, value)) { CalculatedPropertiesChanged(); } } } /// /// Gets the large (page) change value for the scroll viewer. /// public Size LargeChange => _largeChange; /// /// Gets the small (line) change value for the scroll viewer. /// public Size SmallChange => _smallChange; /// /// Gets or sets the horizontal scrollbar visibility. /// public ScrollBarVisibility HorizontalScrollBarVisibility { get { return GetValue(HorizontalScrollBarVisibilityProperty); } set { SetValue(HorizontalScrollBarVisibilityProperty, value); } } /// /// Gets or sets the vertical scrollbar visibility. /// public ScrollBarVisibility VerticalScrollBarVisibility { get { return GetValue(VerticalScrollBarVisibilityProperty); } set { SetValue(VerticalScrollBarVisibilityProperty, value); } } /// /// Gets a value indicating whether the viewer can scroll horizontally. /// protected bool CanHorizontallyScroll { get { return HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled; } } /// /// Gets a value indicating whether the viewer can scroll vertically. /// protected bool CanVerticallyScroll { get { return VerticalScrollBarVisibility != ScrollBarVisibility.Disabled; } } /// public Control? CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor; /// /// Gets the maximum horizontal scrollbar value. /// protected double HorizontalScrollBarMaximum { get { return Max(_extent.Width - _viewport.Width, 0); } } /// /// Gets or sets the horizontal scrollbar value. /// protected double HorizontalScrollBarValue { get { return _offset.X; } set { if (_offset.X != value) { var old = Offset.X; Offset = Offset.WithX(value); RaisePropertyChanged(HorizontalScrollBarValueProperty, old, value); } } } /// /// Gets the size of the horizontal scrollbar viewport. /// protected double HorizontalScrollBarViewportSize { get { return _viewport.Width; } } /// /// Gets the maximum vertical scrollbar value. /// protected double VerticalScrollBarMaximum { get { return Max(_extent.Height - _viewport.Height, 0); } } /// /// Gets or sets the vertical scrollbar value. /// protected double VerticalScrollBarValue { get { return _offset.Y; } set { if (_offset.Y != value) { var old = Offset.Y; Offset = Offset.WithY(value); RaisePropertyChanged(VerticalScrollBarValueProperty, old, value); } } } /// /// Gets the size of the vertical scrollbar viewport. /// protected double VerticalScrollBarViewportSize { get { return _viewport.Height; } } /// /// Gets a value that indicates whether any scrollbar is expanded. /// public bool IsExpanded { get => _isExpanded; private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value); } /// /// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis. /// public SnapPointsType HorizontalSnapPointsType { get => GetValue(HorizontalSnapPointsTypeProperty); set => SetValue(HorizontalSnapPointsTypeProperty, value); } /// /// Gets or sets how scroll gesture reacts to the snap points along the vertical axis. /// public SnapPointsType VerticalSnapPointsType { get => GetValue(VerticalSnapPointsTypeProperty); set => SetValue(VerticalSnapPointsTypeProperty, value); } /// /// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport. /// public SnapPointsAlignment HorizontalSnapPointsAlignment { get => GetValue(HorizontalSnapPointsAlignmentProperty); set => SetValue(HorizontalSnapPointsAlignmentProperty, value); } /// /// Gets or sets how the existing snap points are vertically aligned versus the initial viewport. /// public SnapPointsAlignment VerticalSnapPointsAlignment { get => GetValue(VerticalSnapPointsAlignmentProperty); set => SetValue(VerticalSnapPointsAlignmentProperty, value); } /// /// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it. /// public bool AllowAutoHide { get => GetValue(AllowAutoHideProperty); set => SetValue(AllowAutoHideProperty, value); } /// /// Gets or sets if scroll chaining is enabled. The default value is true. /// /// /// After a user hits a scroll limit on an element that has been nested within another scrollable element, /// you can specify whether that parent element should continue the scrolling operation begun in its child element. /// This is called scroll chaining. /// public bool IsScrollChainingEnabled { get => GetValue(IsScrollChainingEnabledProperty); set => SetValue(IsScrollChainingEnabledProperty, value); } /// /// Gets or sets whether scroll gestures should include inertia in their behavior and value. /// public bool IsScrollInertiaEnabled { get => GetValue(IsScrollInertiaEnabledProperty); set => SetValue(IsScrollInertiaEnabledProperty, value); } /// /// Scrolls the content up one line. /// public void LineUp() { Offset -= new Vector(0, _smallChange.Height); } /// /// Scrolls the content down one line. /// public void LineDown() { Offset += new Vector(0, _smallChange.Height); } /// /// Scrolls the content left one line. /// public void LineLeft() { Offset -= new Vector(_smallChange.Width, 0); } /// /// Scrolls the content right one line. /// public void LineRight() { Offset += new Vector(_smallChange.Width, 0); } /// /// Scrolls the content upward by one page. /// public void PageUp() { VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); } /// /// Scrolls the content downward by one page. /// public void PageDown() { VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum); } /// /// Scrolls the content left by one page. /// public void PageLeft() { HorizontalScrollBarValue = Math.Max(_offset.X - _viewport.Width, 0); } /// /// Scrolls the content tight by one page. /// public void PageRight() { HorizontalScrollBarValue = Math.Min(_offset.X + _viewport.Width, HorizontalScrollBarMaximum); } /// /// Scrolls to the top-left corner of the content. /// public void ScrollToHome() { Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity); } /// /// Scrolls to the bottom-left corner of the content. /// public void ScrollToEnd() { Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity); } /// /// Gets the value of the HorizontalScrollBarVisibility attached property. /// /// The control to read the value from. /// The value of the property. public static ScrollBarVisibility GetHorizontalScrollBarVisibility(Control control) { return control.GetValue(HorizontalScrollBarVisibilityProperty); } /// /// Gets the value of the HorizontalScrollBarVisibility attached property. /// /// The control to set the value on. /// The value of the property. public static void SetHorizontalScrollBarVisibility(Control control, ScrollBarVisibility value) { control.SetValue(HorizontalScrollBarVisibilityProperty, value); } /// /// Gets the value of the HorizontalSnapPointsType attached property. /// /// The control to read the value from. /// The value of the property. public static SnapPointsType GetHorizontalSnapPointsType(Control control) { return control.GetValue(HorizontalSnapPointsTypeProperty); } /// /// Gets the value of the HorizontalSnapPointsType attached property. /// /// The control to set the value on. /// The value of the property. public static void SetHorizontalSnapPointsType(Control control, SnapPointsType value) { control.SetValue(HorizontalSnapPointsTypeProperty, value); } /// /// Gets the value of the VerticalSnapPointsType attached property. /// /// The control to read the value from. /// The value of the property. public static SnapPointsType GetVerticalSnapPointsType(Control control) { return control.GetValue(VerticalSnapPointsTypeProperty); } /// /// Gets the value of the VerticalSnapPointsType attached property. /// /// The control to set the value on. /// The value of the property. public static void SetVerticalSnapPointsType(Control control, SnapPointsType value) { control.SetValue(VerticalSnapPointsTypeProperty, value); } /// /// Gets the value of the HorizontalSnapPointsAlignment attached property. /// /// The control to read the value from. /// The value of the property. public static SnapPointsAlignment GetHorizontalSnapPointsAlignment(Control control) { return control.GetValue(HorizontalSnapPointsAlignmentProperty); } /// /// Gets the value of the HorizontalSnapPointsAlignment attached property. /// /// The control to set the value on. /// The value of the property. public static void SetHorizontalSnapPointsAlignment(Control control, SnapPointsAlignment value) { control.SetValue(HorizontalSnapPointsAlignmentProperty, value); } /// /// Gets the value of the VerticalSnapPointsAlignment attached property. /// /// The control to read the value from. /// The value of the property. public static SnapPointsAlignment GetVerticalSnapPointsAlignment(Control control) { return control.GetValue(VerticalSnapPointsAlignmentProperty); } /// /// Gets the value of the VerticalSnapPointsAlignment attached property. /// /// The control to set the value on. /// The value of the property. public static void SetVerticalSnapPointsAlignment(Control control, SnapPointsAlignment value) { control.SetValue(VerticalSnapPointsAlignmentProperty, value); } /// /// Gets the value of the VerticalScrollBarVisibility attached property. /// /// The control to read the value from. /// The value of the property. public static ScrollBarVisibility GetVerticalScrollBarVisibility(Control control) { return control.GetValue(VerticalScrollBarVisibilityProperty); } /// /// Gets the value of the AllowAutoHideProperty attached property. /// /// The control to set the value on. /// The value of the property. public static void SetAllowAutoHide(Control control, bool value) { control.SetValue(AllowAutoHideProperty, value); } /// /// Gets the value of the AllowAutoHideProperty attached property. /// /// The control to read the value from. /// The value of the property. public static bool GetAllowAutoHide(Control control) { return control.GetValue(AllowAutoHideProperty); } /// /// Sets the value of the IsScrollChainingEnabled attached property. /// /// The control to set the value on. /// The value of the property. /// /// After a user hits a scroll limit on an element that has been nested within another scrollable element, /// you can specify whether that parent element should continue the scrolling operation begun in its child element. /// This is called scroll chaining. /// public static void SetIsScrollChainingEnabled(Control control, bool value) { control.SetValue(IsScrollChainingEnabledProperty, value); } /// /// Gets the value of the IsScrollChainingEnabled attached property. /// /// The control to read the value from. /// The value of the property. /// /// After a user hits a scroll limit on an element that has been nested within another scrollable element, /// you can specify whether that parent element should continue the scrolling operation begun in its child element. /// This is called scroll chaining. /// public static bool GetIsScrollChainingEnabled(Control control) { return control.GetValue(IsScrollChainingEnabledProperty); } /// /// Gets the value of the VerticalScrollBarVisibility attached property. /// /// The control to set the value on. /// The value of the property. public static void SetVerticalScrollBarVisibility(Control control, ScrollBarVisibility value) { control.SetValue(VerticalScrollBarVisibilityProperty, value); } /// /// Gets whether scroll gestures should include inertia in their behavior and value. /// public static bool GetIsScrollInertiaEnabled(Control control) { return control.GetValue(IsScrollInertiaEnabledProperty); } /// /// Sets whether scroll gestures should include inertia in their behavior and value. /// public static void SetIsScrollInertiaEnabled(Control control, bool value) { control.SetValue(IsScrollInertiaEnabledProperty, value); } /// public void RegisterAnchorCandidate(Control element) { (Presenter as IScrollAnchorProvider)?.RegisterAnchorCandidate(element); } /// public void UnregisterAnchorCandidate(Control element) { (Presenter as IScrollAnchorProvider)?.UnregisterAnchorCandidate(element); } protected override bool RegisterContentPresenter(IContentPresenter presenter) { _childSubscription?.Dispose(); _childSubscription = null; if (base.RegisterContentPresenter(presenter)) { _childSubscription = ((Control?)Presenter)? .GetObservable(ContentPresenter.ChildProperty) .Subscribe(ChildChanged); return true; } return false; } internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset) { var maxX = Math.Max(extent.Width - viewport.Width, 0); var maxY = Math.Max(extent.Height - viewport.Height, 0); return new Vector(Clamp(offset.X, 0, maxX), Clamp(offset.Y, 0, maxY)); } private static double Clamp(double value, double min, double max) { return (value < min) ? min : (value > max) ? max : value; } private static double Max(double x, double y) { var result = Math.Max(x, y); return double.IsNaN(result) ? 0 : result; } private void ChildChanged(Control? child) { if (_logicalScrollable is object) { _logicalScrollable.ScrollInvalidated -= LogicalScrollInvalidated; _logicalScrollable = null; } if (child is ILogicalScrollable logical) { _logicalScrollable = logical; logical.ScrollInvalidated += LogicalScrollInvalidated; } CalculatedPropertiesChanged(); } private void LogicalScrollInvalidated(object? sender, EventArgs e) { CalculatedPropertiesChanged(); } private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) { var wasEnabled = e.OldValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; var isEnabled = e.NewValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; if (wasEnabled != isEnabled) { if (e.Property == HorizontalScrollBarVisibilityProperty) { RaisePropertyChanged( CanHorizontallyScrollProperty, wasEnabled, isEnabled); } else if (e.Property == VerticalScrollBarVisibilityProperty) { RaisePropertyChanged( CanVerticallyScrollProperty, wasEnabled, isEnabled); } } } private void CalculatedPropertiesChanged() { // Pass old values of 0 here because we don't have the old values at this point, // and it shouldn't matter as only the template uses these properties. RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum); RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue); RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize); RaisePropertyChanged(VerticalScrollBarMaximumProperty, 0, VerticalScrollBarMaximum); RaisePropertyChanged(VerticalScrollBarValueProperty, 0, VerticalScrollBarValue); RaisePropertyChanged(VerticalScrollBarViewportSizeProperty, 0, VerticalScrollBarViewportSize); if (_logicalScrollable?.IsLogicalScrollEnabled == true) { SetAndRaise(SmallChangeProperty, ref _smallChange, _logicalScrollable.ScrollSize); SetAndRaise(LargeChangeProperty, ref _largeChange, _logicalScrollable.PageScrollSize); } else { SetAndRaise(SmallChangeProperty, ref _smallChange, new Size(DefaultSmallChange, DefaultSmallChange)); SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport); } } protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.PageUp) { PageUp(); e.Handled = true; } else if (e.Key == Key.PageDown) { PageDown(); e.Handled = true; } } /// /// Called when a change in scrolling state is detected, such as a change in scroll /// position, extent, or viewport size. /// /// The event args. /// /// If you override this method, call `base.OnScrollChanged(ScrollChangedEventArgs)` to /// ensure that this event is raised. /// protected virtual void OnScrollChanged(ScrollChangedEventArgs e) { RaiseEvent(e); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _scrollBarExpandSubscription?.Dispose(); _scrollBarExpandSubscription = SubscribeToScrollBars(e); } protected override AutomationPeer OnCreateAutomationPeer() { return new ScrollViewerAutomationPeer(this); } private IDisposable? SubscribeToScrollBars(TemplateAppliedEventArgs e) { static IObservable? GetExpandedObservable(ScrollBar? scrollBar) { return scrollBar?.GetObservable(ScrollBar.IsExpandedProperty); } var horizontalScrollBar = e.NameScope.Find("PART_HorizontalScrollBar"); var verticalScrollBar = e.NameScope.Find("PART_VerticalScrollBar"); var horizontalExpanded = GetExpandedObservable(horizontalScrollBar); var verticalExpanded = GetExpandedObservable(verticalScrollBar); IObservable? actualExpanded = null; if (horizontalExpanded != null && verticalExpanded != null) { actualExpanded = horizontalExpanded.CombineLatest(verticalExpanded, (h, v) => h || v); } else { if (horizontalExpanded != null) { actualExpanded = horizontalExpanded; } else if (verticalExpanded != null) { actualExpanded = verticalExpanded; } } return actualExpanded?.Subscribe(OnScrollBarExpandedChanged); } private void OnScrollBarExpandedChanged(bool isExpanded) { IsExpanded = isExpanded; } private void OnLayoutUpdated(object? sender, EventArgs e) => RaiseScrollChanged(); private void RaiseScrollChanged() { var extentDelta = new Vector(Extent.Width - _oldExtent.Width, Extent.Height - _oldExtent.Height); var offsetDelta = Offset - _oldOffset; var viewportDelta = new Vector(Viewport.Width - _oldViewport.Width, Viewport.Height - _oldViewport.Height); if (!extentDelta.NearlyEquals(default) || !offsetDelta.NearlyEquals(default) || !viewportDelta.NearlyEquals(default)) { var e = new ScrollChangedEventArgs(extentDelta, offsetDelta, viewportDelta); OnScrollChanged(e); _oldExtent = Extent; _oldOffset = Offset; _oldViewport = Viewport; } } } }