From 74f75b052518eb9c729d14b3bfd7801144d97cb0 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Thu, 16 Mar 2023 21:04:32 +0100 Subject: [PATCH 01/12] Converted RangeBase to StyledProperty Normalised ScrollViewer internal properties and bindings --- .../Presenters/ScrollContentPresenter.cs | 117 ++++--- src/Avalonia.Controls/Primitives/RangeBase.cs | 165 ++++------ src/Avalonia.Controls/Primitives/ScrollBar.cs | 70 +++- src/Avalonia.Controls/Primitives/Track.cs | 34 +- src/Avalonia.Controls/ProgressBar.cs | 30 +- src/Avalonia.Controls/ScrollViewer.cs | 309 +++--------------- src/Avalonia.Controls/Slider.cs | 2 +- .../Controls/MenuScrollViewer.xaml | 13 +- .../Controls/ScrollViewer.xaml | 35 +- .../Controls/ScrollViewer.xaml | 51 +-- ...ayoutableTests_EffectiveViewportChanged.cs | 14 - .../CarouselTests.cs | 6 - .../ListBoxTests.cs | 9 +- .../ScrollViewerTests.cs | 112 +++---- .../VirtualizingCarouselPanelTests.cs | 10 +- .../VirtualizingStackPanelTests.cs | 6 - 16 files changed, 337 insertions(+), 646 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bc86558ab3..0cde765f76 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -6,6 +6,7 @@ using Avalonia.Input; using Avalonia.Input.GestureRecognizers; using Avalonia.Utilities; using Avalonia.VisualTree; +using System.Linq; namespace Avalonia.Controls.Presenters { @@ -19,44 +20,34 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly DirectProperty CanHorizontallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanHorizontallyScroll), - o => o.CanHorizontallyScroll, - (o, v) => o.CanHorizontallyScroll = v); + public static readonly StyledProperty CanHorizontallyScrollProperty = + AvaloniaProperty.Register(nameof(CanHorizontallyScroll)); /// /// Defines the property. /// - public static readonly DirectProperty CanVerticallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanVerticallyScroll), - o => o.CanVerticallyScroll, - (o, v) => o.CanVerticallyScroll = v); + public static readonly StyledProperty CanVerticallyScrollProperty = + AvaloniaProperty.Register(nameof(CanVerticallyScroll)); /// /// Defines the property. /// public static readonly DirectProperty ExtentProperty = ScrollViewer.ExtentProperty.AddOwner( - o => o.Extent, - (o, v) => o.Extent = v); + o => o.Extent); /// /// Defines the property. /// - public static readonly DirectProperty OffsetProperty = - ScrollViewer.OffsetProperty.AddOwner( - o => o.Offset, - (o, v) => o.Offset = v); + public static readonly StyledProperty OffsetProperty = + ScrollViewer.OffsetProperty.AddOwner(new(coerce: ScrollViewer.CoerceOffset)); /// /// Defines the property. /// public static readonly DirectProperty ViewportProperty = ScrollViewer.ViewportProperty.AddOwner( - o => o.Viewport, - (o, v) => o.Viewport = v); + o => o.Viewport); /// /// Defines the property. @@ -88,11 +79,8 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty IsScrollChainingEnabledProperty = ScrollViewer.IsScrollChainingEnabledProperty.AddOwner(); - private bool _canHorizontallyScroll; - private bool _canVerticallyScroll; private bool _arranging; private Size _extent; - private Vector _offset; private IDisposable? _logicalScrollSubscription; private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; @@ -109,6 +97,7 @@ namespace Avalonia.Controls.Presenters private double _verticalSnapPoint; private double _verticalSnapPointOffset; private double _horizontalSnapPointOffset; + private CompositeDisposable? _ownerSubscriptions; /// /// Initializes static members of the class. @@ -116,7 +105,6 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); - ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); } /// @@ -137,8 +125,8 @@ namespace Avalonia.Controls.Presenters /// public bool CanHorizontallyScroll { - get { return _canHorizontallyScroll; } - set { SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); } + get => GetValue(CanHorizontallyScrollProperty); + set => SetValue(CanHorizontallyScrollProperty, value); } /// @@ -146,8 +134,8 @@ namespace Avalonia.Controls.Presenters /// public bool CanVerticallyScroll { - get { return _canVerticallyScroll; } - set { SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } + get => GetValue(CanVerticallyScrollProperty); + set => SetValue(CanVerticallyScrollProperty, value); } /// @@ -164,8 +152,8 @@ namespace Avalonia.Controls.Presenters /// public Vector Offset { - get { return _offset; } - set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); } + get => GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); } /// @@ -295,12 +283,64 @@ namespace Avalonia.Controls.Presenters if (result) { - Offset = offset; + SetCurrentValue(OffsetProperty, offset); } return result; } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + AttachToScrollViewer(); + } + + /// + /// Locates the first ancestor and binds to it. Properties which have been set through other means are not bound. + /// + /// + /// This method is automatically called when the control is attached to a visual tree. + /// + protected internal virtual void AttachToScrollViewer() + { + _ownerSubscriptions?.Dispose(); + + var owner = this.FindAncestorOfType(); + + if (owner == null) + { + return; + } + + var subscriptionDisposables = new IDisposable?[] + { + IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty).Select(NotDisabled), Data.BindingPriority.Template)), + IfUnset(CanVerticallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.VerticalScrollBarVisibilityProperty).Select(NotDisabled), Data.BindingPriority.Template)), + IfUnset(OffsetProperty, p => new CompositeDisposable( + Bind(p, owner.GetBindingObservable(ScrollViewer.OffsetProperty), Data.BindingPriority.Template), + this.GetObservable(OffsetProperty).Subscribe(v => owner.SetCurrentValue(OffsetProperty, v)))), + IfUnset(IsScrollChainingEnabledProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.IsScrollChainingEnabledProperty), Data.BindingPriority.Template)), + IfUnset(ContentProperty, p => Bind(p, owner.GetBindingObservable(ContentProperty), Data.BindingPriority.Template)), + + // read-only properties on ScrollViewer with internal setters: + this.GetObservable(ExtentProperty).Subscribe(v => owner.Extent = v), + this.GetObservable(ViewportProperty).Subscribe(v => owner.Viewport = v) + }.Where(d => d != null).Cast().ToArray(); + + _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables); + + static bool NotDisabled(ScrollBarVisibility v) => v != ScrollBarVisibility.Disabled; + + IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => GetValueStore().IsSet(property) ? null : func(property); + } + + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _ownerSubscriptions?.Dispose(); + base.OnDetachedFromVisualTree(e); + } + /// void IScrollAnchorProvider.RegisterAnchorCandidate(Control element) { @@ -410,7 +450,7 @@ namespace Avalonia.Controls.Presenters try { _arranging = true; - Offset = newOffset; + SetCurrentValue(OffsetProperty, newOffset); } finally { @@ -427,7 +467,6 @@ namespace Avalonia.Controls.Presenters Viewport = finalSize; Extent = Child!.Bounds.Size.Inflate(Child.Margin); - Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset); _isAnchorElementDirty = true; return finalSize; @@ -516,7 +555,7 @@ namespace Avalonia.Controls.Presenters } bool offsetChanged = newOffset != Offset; - Offset = newOffset; + SetCurrentValue(OffsetProperty, newOffset); e.Handled = !IsScrollChainingEnabled || offsetChanged; @@ -529,7 +568,7 @@ namespace Avalonia.Controls.Presenters _activeLogicalGestureScrolls?.Remove(e.Id); _scrollGestureSnapPoints?.Remove(e.Id); - Offset = SnapOffset(Offset); + SetCurrentValue(OffsetProperty, SnapOffset(Offset)); } private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e) @@ -623,7 +662,7 @@ namespace Avalonia.Controls.Presenters Vector newOffset = SnapOffset(new Vector(x, y)); bool offsetChanged = newOffset != Offset; - Offset = newOffset; + SetCurrentValue(OffsetProperty, newOffset); e.Handled = !IsScrollChainingEnabled || offsetChanged; } @@ -651,6 +690,10 @@ namespace Avalonia.Controls.Presenters UpdateSnapPoints(); } + else if (change.Property == ChildProperty) + { + ChildChanged(change); + } else if (change.Property == HorizontalSnapPointsAlignmentProperty || change.Property == VerticalSnapPointsAlignmentProperty) { @@ -677,7 +720,7 @@ namespace Avalonia.Controls.Presenters if (e.OldValue != null) { - Offset = default; + SetCurrentValue(OffsetProperty, default); } } @@ -719,14 +762,14 @@ namespace Avalonia.Controls.Presenters if (logicalScroll != scrollable.IsLogicalScrollEnabled) { UpdateScrollableSubscription(Child); - Offset = default; + SetCurrentValue(OffsetProperty, default); InvalidateMeasure(); } else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; Extent = scrollable.Extent; - Offset = scrollable.Offset; + SetCurrentValue(OffsetProperty, scrollable.Offset); } } diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 38d848d69b..fd9de47236 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -12,30 +12,22 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly DirectProperty MinimumProperty = - AvaloniaProperty.RegisterDirect( - nameof(Minimum), - o => o.Minimum, - (o, v) => o.Minimum = v); + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), coerce: CoerceMinimum); /// /// Defines the property. /// - public static readonly DirectProperty MaximumProperty = - AvaloniaProperty.RegisterDirect( - nameof(Maximum), - o => o.Maximum, - (o, v) => o.Maximum = v); + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), 100, coerce: CoerceMaximum); /// /// Defines the property. /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect( - nameof(Value), - o => o.Value, - (o, v) => o.Value = v, - defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(nameof(Value), + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceValue); /// /// Defines the property. @@ -49,44 +41,26 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty LargeChangeProperty = AvaloniaProperty.Register(nameof(LargeChange), 10); - private double _minimum; - private double _maximum = 100.0; - private double _value; - /// - /// Initializes a new instance of the class. + /// Gets or sets the minimum value. /// - public RangeBase() + public double Minimum { + get => GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); } - /// - /// Gets or sets the minimum value. - /// - public double Minimum + private static double CoerceMinimum(AvaloniaObject sender, double value) { - get - { - return _minimum; - } + return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty); + } - set + private void OnMinimumChanged() + { + if (IsInitialized) { - if (!ValidateDouble(value)) - { - return; - } - - if (IsInitialized) - { - SetAndRaise(MinimumProperty, ref _minimum, value); - Maximum = ValidateMaximum(Maximum); - Value = ValidateValue(Value); - } - else - { - SetAndRaise(MinimumProperty, ref _minimum, value); - } + CoerceValue(MaximumProperty); + CoerceValue(ValueProperty); } } @@ -95,28 +69,22 @@ namespace Avalonia.Controls.Primitives /// public double Maximum { - get - { - return _maximum; - } + get => GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } + + private static double CoerceMaximum(AvaloniaObject sender, double value) + { + return ValidateDouble(value) + ? Math.Max(value, sender.GetValue(MinimumProperty)) + : sender.GetValue(MaximumProperty); + } - set + private void OnMaximumChanged() + { + if (IsInitialized) { - if (!ValidateDouble(value)) - { - return; - } - - if (IsInitialized) - { - value = ValidateMaximum(value); - SetAndRaise(MaximumProperty, ref _maximum, value); - Value = ValidateValue(Value); - } - else - { - SetAndRaise(MaximumProperty, ref _maximum, value); - } + CoerceValue(ValueProperty); } } @@ -125,28 +93,15 @@ namespace Avalonia.Controls.Primitives /// public double Value { - get - { - return _value; - } + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } - set - { - if (!ValidateDouble(value)) - { - return; - } - - if (IsInitialized) - { - value = ValidateValue(value); - SetAndRaise(ValueProperty, ref _value, value); - } - else - { - SetAndRaise(ValueProperty, ref _value, value); - } - } + private static double CoerceValue(AvaloniaObject sender, double value) + { + return ValidateDouble(value) + ? MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty)) + : sender.GetValue(ValueProperty); } public double SmallChange @@ -165,37 +120,31 @@ namespace Avalonia.Controls.Primitives { base.OnInitialized(); - Maximum = ValidateMaximum(Maximum); - Value = ValidateValue(Value); + CoerceValue(MaximumProperty); + CoerceValue(ValueProperty); } - /// - /// Checks if the double value is not infinity nor NaN. - /// - /// The value. - private static bool ValidateDouble(double value) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - return !double.IsInfinity(value) && !double.IsNaN(value); - } + base.OnPropertyChanged(change); - /// - /// Validates/coerces the property. - /// - /// The value. - /// The coerced value. - private double ValidateMaximum(double value) - { - return Math.Max(value, Minimum); + if (change.Property == MinimumProperty) + { + OnMinimumChanged(); + } + else if (change.Property == MaximumProperty) + { + OnMaximumChanged(); + } } /// - /// Validates/coerces the property. + /// Checks if the double value is not infinity nor NaN. /// /// The value. - /// The coerced value. - private double ValidateValue(double value) + private static bool ValidateDouble(double value) { - return MathUtilities.Clamp(value, Minimum, Maximum); + return !double.IsInfinity(value) && !double.IsNaN(value); } } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index b4a8408901..df69baae0a 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -6,6 +6,9 @@ using Avalonia.Layout; using Avalonia.Threading; using Avalonia.Controls.Metadata; using Avalonia.Automation.Peers; +using Avalonia.VisualTree; +using Avalonia.Reactive; +using System.Linq; namespace Avalonia.Controls.Primitives { @@ -80,6 +83,7 @@ namespace Avalonia.Controls.Primitives private Button? _pageDownButton; private DispatcherTimer? _timer; private bool _isExpanded; + private CompositeDisposable? _ownerSubscriptions; /// /// Initializes static members of the class. @@ -88,6 +92,8 @@ namespace Avalonia.Controls.Primitives { Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble); + + FocusableProperty.OverrideMetadata(new(false)); } /// @@ -178,7 +184,60 @@ namespace Avalonia.Controls.Primitives _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.") }; - SetValue(IsVisibleProperty, isVisible); + SetCurrentValue(IsVisibleProperty, isVisible); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + AttachToScrollViewer(); + } + + /// + /// Locates the first ancestor and binds to its properties. Properties which have been set through other means are not bound. + /// + /// + /// This method is automatically called when the control is attached to a visual tree. + /// + protected internal virtual void AttachToScrollViewer() + { + _ownerSubscriptions?.Dispose(); + + var owner = this.FindAncestorOfType(); + + if (owner == null) + { + return; + } + + var visibilitySource = Orientation == Orientation.Horizontal ? ScrollViewer.HorizontalScrollBarVisibilityProperty : ScrollViewer.VerticalScrollBarVisibilityProperty; + + var subscriptionDisposables = new IDisposable?[] + { + IfUnset(MaximumProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ScrollBarMaximumProperty).Select(ExtractOrdinate), BindingPriority.Template)), + IfUnset(ValueProperty, p => new CompositeDisposable( + Bind(p, owner.GetObservable(ScrollViewer.OffsetProperty).Select(ExtractOrdinate), BindingPriority.Template), + this.GetObservable(ValueProperty).Subscribe(v => SetScrollViewerOffset(owner, v)))), + IfUnset(ViewportSizeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ViewportProperty).Select(ExtractOrdinate), BindingPriority.Template)), + IfUnset(VisibilityProperty, p => Bind(p, owner.GetObservable(visibilitySource), BindingPriority.Template)), + IfUnset(AllowAutoHideProperty, p => Bind(p, owner.GetObservable(ScrollViewer.AllowAutoHideProperty), BindingPriority.Template)), + IfUnset(LargeChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.LargeChangeProperty).Select(ExtractOrdinate), BindingPriority.Template)), + IfUnset(SmallChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.SmallChangeProperty).Select(ExtractOrdinate), BindingPriority.Template)) + }.Where(d => d != null).Cast().ToArray(); + + _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables); + + IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => GetValueStore().IsSet(property) ? null : func(property); + } + + private double ExtractOrdinate(Vector v) => Orientation == Orientation.Horizontal ? v.X : v.Y; + private double ExtractOrdinate(Size v) => Orientation == Orientation.Horizontal ? v.Width : v.Height; + private void SetScrollViewerOffset(ScrollViewer viewer, double value) => viewer.SetCurrentValue(ScrollViewer.OffsetProperty, Orientation == Orientation.Horizontal ? viewer.Offset.WithX(value) : viewer.Offset.WithY(value)); + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _ownerSubscriptions?.Dispose(); + base.OnDetachedFromVisualTree(e); } protected override void OnKeyDown(KeyEventArgs e) @@ -202,6 +261,7 @@ namespace Avalonia.Controls.Primitives if (change.Property == OrientationProperty) { UpdatePseudoClasses(change.GetNewValue()); + AttachToScrollViewer(); // there's no way to manually refresh bindings, so reapply them } else if (change.Property == AllowAutoHideProperty) { @@ -373,25 +433,25 @@ namespace Avalonia.Controls.Primitives private void SmallDecrement() { - Value = Math.Max(Value - SmallChange, Minimum); + SetCurrentValue(ValueProperty, Math.Max(Value - SmallChange, Minimum)); OnScroll(ScrollEventType.SmallDecrement); } private void SmallIncrement() { - Value = Math.Min(Value + SmallChange, Maximum); + SetCurrentValue(ValueProperty, Math.Min(Value + SmallChange, Maximum)); OnScroll(ScrollEventType.SmallIncrement); } private void LargeDecrement() { - Value = Math.Max(Value - LargeChange, Minimum); + SetCurrentValue(ValueProperty, Math.Max(Value - LargeChange, Minimum)); OnScroll(ScrollEventType.LargeDecrement); } private void LargeIncrement() { - Value = Math.Min(Value + LargeChange, Maximum); + SetCurrentValue(ValueProperty, Math.Min(Value + LargeChange, Maximum)); OnScroll(ScrollEventType.LargeIncrement); } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 9e8d1478fa..fe4912a33c 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -15,14 +15,14 @@ namespace Avalonia.Controls.Primitives [PseudoClasses(":vertical", ":horizontal")] public class Track : Control { - public static readonly DirectProperty MinimumProperty = - RangeBase.MinimumProperty.AddOwner(o => o.Minimum, (o, v) => o.Minimum = v); + public static readonly StyledProperty MinimumProperty = + RangeBase.MinimumProperty.AddOwner(); - public static readonly DirectProperty MaximumProperty = - RangeBase.MaximumProperty.AddOwner(o => o.Maximum, (o, v) => o.Maximum = v); + public static readonly StyledProperty MaximumProperty = + RangeBase.MaximumProperty.AddOwner(); - public static readonly DirectProperty ValueProperty = - RangeBase.ValueProperty.AddOwner(o => o.Value, (o, v) => o.Value = v); + public static readonly StyledProperty ValueProperty = + RangeBase.ValueProperty.AddOwner(); public static readonly StyledProperty ViewportSizeProperty = ScrollBar.ViewportSizeProperty.AddOwner(); @@ -45,10 +45,6 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty IgnoreThumbDragProperty = AvaloniaProperty.Register(nameof(IgnoreThumbDrag)); - private double _minimum; - private double _maximum = 100.0; - private double _value; - static Track() { ThumbProperty.Changed.AddClassHandler((x, e) => x.ThumbChanged(e)); @@ -64,20 +60,20 @@ namespace Avalonia.Controls.Primitives public double Minimum { - get { return _minimum; } - set { SetAndRaise(MinimumProperty, ref _minimum, value); } + get => GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); } public double Maximum { - get { return _maximum; } - set { SetAndRaise(MaximumProperty, ref _maximum, value); } + get => GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); } public double Value { - get { return _value; } - set { SetAndRaise(ValueProperty, ref _value, value); } + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); } public double ViewportSize @@ -443,11 +439,11 @@ namespace Avalonia.Controls.Primitives { if (IgnoreThumbDrag) return; - - Value = MathUtilities.Clamp( + + SetCurrentValue(ValueProperty, MathUtilities.Clamp( Value + ValueFromDistance(e.Vector.X, e.Vector.Y), Minimum, - Maximum); + Maximum)); } private void ShowChildren(bool visible) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 98a9ec60bf..daf6be12d2 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -100,8 +100,6 @@ namespace Avalonia.Controls } private double _percentage; - private double _indeterminateStartingOffset; - private double _indeterminateEndingOffset; private Border? _indicator; private IDisposable? _trackSizeChangedListener; @@ -122,17 +120,11 @@ namespace Avalonia.Controls nameof(Percentage), o => o.Percentage); - public static readonly DirectProperty IndeterminateStartingOffsetProperty = - AvaloniaProperty.RegisterDirect( - nameof(IndeterminateStartingOffset), - p => p.IndeterminateStartingOffset, - (p, o) => p.IndeterminateStartingOffset = o); + public static readonly StyledProperty IndeterminateStartingOffsetProperty = + AvaloniaProperty.Register(nameof(IndeterminateStartingOffset)); - public static readonly DirectProperty IndeterminateEndingOffsetProperty = - AvaloniaProperty.RegisterDirect( - nameof(IndeterminateEndingOffset), - p => p.IndeterminateEndingOffset, - (p, o) => p.IndeterminateEndingOffset = o); + public static readonly StyledProperty IndeterminateEndingOffsetProperty = + AvaloniaProperty.Register(nameof(IndeterminateEndingOffset)); public double Percentage { @@ -142,19 +134,19 @@ namespace Avalonia.Controls public double IndeterminateStartingOffset { - get => _indeterminateStartingOffset; - set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); + get => GetValue(IndeterminateStartingOffsetProperty); + set => SetValue(IndeterminateStartingOffsetProperty, value); } public double IndeterminateEndingOffset { - get => _indeterminateEndingOffset; - set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); + get => GetValue(IndeterminateEndingOffsetProperty); + set => SetValue(IndeterminateEndingOffsetProperty, value); } static ProgressBar() { - ValueProperty.OverrideMetadata(new DirectPropertyMetadata(defaultBindingMode: BindingMode.OneWay)); + ValueProperty.OverrideMetadata(new(defaultBindingMode: BindingMode.OneWay)); ValueProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MinimumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MaximumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); @@ -261,8 +253,8 @@ namespace Avalonia.Controls // Remove these properties when we switch to fluent as default and removed the old one. - IndeterminateStartingOffset = -dim; - IndeterminateEndingOffset = dim; + SetCurrentValue(IndeterminateStartingOffsetProperty,-dim); + SetCurrentValue(IndeterminateEndingOffsetProperty,dim); var padding = Padding; var rectangle = new RectangleGeometry( diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 0f1b8f388c..f469e4948d 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -16,54 +16,25 @@ namespace Avalonia.Controls [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); + o => o.Extent); /// /// Defines the property. /// - public static readonly DirectProperty OffsetProperty = - AvaloniaProperty.RegisterDirect( - nameof(Offset), - o => o.Offset, - (o, v) => o.Offset = v); + public static readonly StyledProperty OffsetProperty = + AvaloniaProperty.Register(nameof(Offset), coerce: CoerceOffset); /// /// Defines the property. /// public static readonly DirectProperty ViewportProperty = AvaloniaProperty.RegisterDirect(nameof(Viewport), - o => o.Viewport, - (o, v) => o.Viewport = v); + o => o.Viewport); /// /// Defines the property. @@ -82,41 +53,12 @@ namespace Avalonia.Controls 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. + /// 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 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); + public static readonly DirectProperty ScrollBarMaximumProperty = + AvaloniaProperty.RegisterDirect( + nameof(ScrollBarMaximum), + o => o.ScrollBarMaximum); /// /// Defines the property. @@ -126,31 +68,6 @@ namespace Avalonia.Controls 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. /// @@ -179,18 +96,6 @@ namespace Avalonia.Controls 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. /// @@ -242,25 +147,16 @@ namespace Avalonia.Controls private IDisposable? _childSubscription; private ILogicalScrollable? _logicalScrollable; private Size _extent; - private Vector _offset; private Size _viewport; private Size _oldExtent; private Vector _oldOffset; + private Vector _oldMaximum; 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. /// @@ -288,7 +184,7 @@ namespace Avalonia.Controls return _extent; } - private set + internal set { if (SetAndRaise(ExtentProperty, ref _extent, value)) { @@ -302,18 +198,8 @@ namespace Avalonia.Controls /// public Vector Offset { - get - { - return _offset; - } - - set - { - if (SetAndRaise(OffsetProperty, ref _offset, CoerceOffset(Extent, Viewport, value))) - { - CalculatedPropertiesChanged(); - } - } + get => GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); } /// @@ -326,7 +212,7 @@ namespace Avalonia.Controls return _viewport; } - private set + internal set { if (SetAndRaise(ViewportProperty, ref _viewport, value)) { @@ -383,70 +269,9 @@ namespace Avalonia.Controls public Control? CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor; /// - /// Gets the maximum horizontal scrollbar value. + /// Gets the maximum scrolling distance (which is - ). /// - 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; } - } + public Vector ScrollBarMaximum => new(Max(_extent.Width - _viewport.Width, 0), Max(_extent.Height - _viewport.Height, 0)); /// /// Gets a value that indicates whether any scrollbar is expanded. @@ -528,82 +353,52 @@ namespace Avalonia.Controls /// /// Scrolls the content up one line. /// - public void LineUp() - { - Offset -= new Vector(0, _smallChange.Height); - } + public void LineUp() => SetCurrentValue(OffsetProperty, Offset - new Vector(0, _smallChange.Height)); /// /// Scrolls the content down one line. /// - public void LineDown() - { - Offset += new Vector(0, _smallChange.Height); - } + public void LineDown() => SetCurrentValue(OffsetProperty, Offset + new Vector(0, _smallChange.Height)); /// /// Scrolls the content left one line. /// - public void LineLeft() - { - Offset -= new Vector(_smallChange.Width, 0); - } + public void LineLeft() => SetCurrentValue(OffsetProperty, Offset - new Vector(_smallChange.Width, 0)); /// /// Scrolls the content right one line. /// - public void LineRight() - { - Offset += new Vector(_smallChange.Width, 0); - } + public void LineRight() => SetCurrentValue(OffsetProperty, Offset + new Vector(_smallChange.Width, 0)); /// /// Scrolls the content upward by one page. /// - public void PageUp() - { - VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); - } + public void PageUp() => SetCurrentValue(OffsetProperty, Offset.WithY(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); - } + public void PageDown() => SetCurrentValue(OffsetProperty, Offset.WithY(Math.Min(Offset.Y + _viewport.Height, ScrollBarMaximum.Y))); /// /// Scrolls the content left by one page. /// - public void PageLeft() - { - HorizontalScrollBarValue = Math.Max(_offset.X - _viewport.Width, 0); - } + public void PageLeft() => SetCurrentValue(OffsetProperty, Offset.WithX(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); - } + public void PageRight() => SetCurrentValue(OffsetProperty, Offset.WithX(Math.Min(Offset.X + _viewport.Width, ScrollBarMaximum.X))); /// /// Scrolls to the top-left corner of the content. /// - public void ScrollToHome() - { - Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity); - } + public void ScrollToHome() => SetCurrentValue(OffsetProperty, 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); - } + public void ScrollToEnd() => SetCurrentValue(OffsetProperty, new Vector(double.NegativeInfinity, double.PositiveInfinity)); /// /// Gets the value of the HorizontalScrollBarVisibility attached property. @@ -819,11 +614,14 @@ namespace Avalonia.Controls return false; } - internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset) + internal static Vector CoerceOffset(AvaloniaObject sender, Vector value) { + var extent = sender.GetValue(ExtentProperty); + var viewport = sender.GetValue(ViewportProperty); + 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)); + return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); } private static double Clamp(double value, double min, double max) @@ -859,40 +657,11 @@ namespace Avalonia.Controls 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); + var newMaximum = ScrollBarMaximum; + RaisePropertyChanged(ScrollBarMaximumProperty, _oldMaximum, newMaximum); + _oldMaximum = newMaximum; if (_logicalScrollable?.IsLogicalScrollEnabled == true) { @@ -906,6 +675,16 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == OffsetProperty) + { + CalculatedPropertiesChanged(); + } + } + protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.PageUp) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 7e43f6682c..ed9075c155 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -110,7 +110,7 @@ namespace Avalonia.Controls Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); - ValueProperty.OverrideMetadata(new DirectPropertyMetadata(enableDataValidation: true)); + ValueProperty.OverrideMetadata(new(enableDataValidation: true)); AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Slider); } diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml index e8508935d2..adca099a10 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml @@ -75,17 +75,10 @@ Height="20" /> - + - + diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml index 7a8c15bf60..04b8f80f82 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml @@ -29,46 +29,23 @@ Grid.RowSpan="2" Grid.ColumnSpan="2" Background="{TemplateBinding Background}" - CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" - CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" - Content="{TemplateBinding Content}" - Extent="{TemplateBinding Extent, Mode=TwoWay}" - Padding="{TemplateBinding Padding}" HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}" VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}" HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}" VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}" - Offset="{TemplateBinding Offset, Mode=TwoWay}" - Viewport="{TemplateBinding Viewport, Mode=TwoWay}" - IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> + Padding="{TemplateBinding Padding}"> - + + Grid.Row="1" /> + Grid.Column="1" /> + Background="{TemplateBinding Background}"> - + - - + + Orientation="Vertical"/> @@ -105,16 +79,7 @@ + Margin="{TemplateBinding Padding}" /> diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs index 98a2d76b48..536c766a7d 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs @@ -384,31 +384,17 @@ namespace Avalonia.Base.UnitTests.Layout new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - [~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty], - [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], - [~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty], - [~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty], - [~ScrollContentPresenter.CanVerticallyScrollProperty] = control[~ScrollViewer.CanVerticallyScrollProperty], }.RegisterInNameScope(scope), new ScrollBar { Name = "horizontalScrollBar", Orientation = Orientation.Horizontal, - [~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty], - [~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty], - [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty], - [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], [Grid.RowProperty] = 1, }.RegisterInNameScope(scope), new ScrollBar { Name = "verticalScrollBar", Orientation = Orientation.Vertical, - [~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty], - [~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty], - [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty], - [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], [Grid.ColumnProperty] = 1, }.RegisterInNameScope(scope), }, diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index c6237c2fa8..6624d13165 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -301,12 +301,6 @@ namespace Avalonia.Controls.UnitTests new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), - [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], - [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], - [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], - [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty], - [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty], }.RegisterInNameScope(scope), } }); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index f42185a59c..7a227a48ab 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -590,18 +590,11 @@ namespace Avalonia.Controls.UnitTests new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), - [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], - [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], - [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], - [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty], - [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty], }.RegisterInNameScope(scope), new ScrollBar { Name = "verticalScrollBar", - [~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty], - [~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty], + Orientation = Orientation.Vertical, } } }); diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index d3eb42f147..407bd40c37 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -21,45 +21,20 @@ namespace Avalonia.Controls.UnitTests Content = "Foo", }; - target.ApplyTemplate(); - ((ContentPresenter)target.Presenter).UpdateChild(); + InitializeScrollViewer(target); Assert.IsType(target.Presenter.Child); } - [Fact] - public void CanHorizontallyScroll_Should_Track_HorizontalScrollBarVisibility() - { - var target = new ScrollViewer(); - var values = new List(); - - target.GetObservable(ScrollViewer.CanHorizontallyScrollProperty).Subscribe(x => values.Add(x)); - target.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; - target.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; - - Assert.Equal(new[] { false, true }, values); - } - - [Fact] - public void CanVerticallyScroll_Should_Track_VerticalScrollBarVisibility() - { - var target = new ScrollViewer(); - var values = new List(); - - target.GetObservable(ScrollViewer.CanVerticallyScrollProperty).Subscribe(x => values.Add(x)); - target.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; - target.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; - - Assert.Equal(new[] { true, false, true }, values); - } - [Fact] public void Offset_Should_Be_Coerced_To_Viewport() { - var target = new ScrollViewer(); - target.SetValue(ScrollViewer.ExtentProperty, new Size(20, 20)); - target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10)); - target.Offset = new Vector(12, 12); + var target = new ScrollViewer + { + Extent = new Size(20, 20), + Viewport = new Size(10, 10), + Offset = new Vector(12, 12) + }; Assert.Equal(new Vector(10, 10), target.Offset); } @@ -67,10 +42,12 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Test_ScrollToHome() { - var target = new ScrollViewer(); - target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50)); - target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10)); - target.Offset = new Vector(25, 25); + var target = new ScrollViewer + { + Extent = new Size(50, 50), + Viewport = new Size(10, 10), + Offset = new Vector(25, 25) + }; target.ScrollToHome(); Assert.Equal(new Vector(0, 0), target.Offset); @@ -79,10 +56,12 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Test_ScrollToEnd() { - var target = new ScrollViewer(); - target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50)); - target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10)); - target.Offset = new Vector(25, 25); + var target = new ScrollViewer + { + Extent = new Size(50, 50), + Viewport = new Size(10, 10), + Offset = new Vector(25, 25) + }; target.ScrollToEnd(); Assert.Equal(new Vector(0, 40), target.Offset); @@ -99,9 +78,10 @@ namespace Avalonia.Controls.UnitTests [Fact] public void LargeChange_Should_Be_Viewport() { - var target = new ScrollViewer(); - - target.SetValue(ScrollViewer.ViewportProperty, new Size(104, 143)); + var target = new ScrollViewer + { + Viewport = new Size(104, 143) + }; Assert.Equal(new Size(104, 143), target.LargeChange); } @@ -120,8 +100,7 @@ namespace Avalonia.Controls.UnitTests Content = child.Object, }; - target.ApplyTemplate(); - ((ContentPresenter)target.Presenter).UpdateChild(); + InitializeScrollViewer(target); Assert.Equal(new Size(12, 43), target.SmallChange); } @@ -141,8 +120,7 @@ namespace Avalonia.Controls.UnitTests Content = child.Object, }; - target.ApplyTemplate(); - ((ContentPresenter)target.Presenter).UpdateChild(); + InitializeScrollViewer(target); Assert.Equal(new Size(45, 67), target.LargeChange); } @@ -154,8 +132,8 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot(target); var raised = 0; - target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); - target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Extent = new Size(100, 100); + target.Viewport = new Size(50, 50); target.Offset = new Vector(10, 10); root.LayoutManager.ExecuteInitialLayoutPass(); @@ -168,7 +146,7 @@ namespace Avalonia.Controls.UnitTests ++raised; }; - target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112)); + target.Extent = new Size(111, 112); Assert.Equal(0, raised); @@ -184,8 +162,8 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot(target); var raised = 0; - target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); - target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Extent = new Size(100, 100); + target.Viewport = new Size(50, 50); target.Offset = new Vector(10, 10); root.LayoutManager.ExecuteInitialLayoutPass(); @@ -214,8 +192,8 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot(target); var raised = 0; - target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); - target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Extent = new Size(100, 100); + target.Viewport = new Size(50, 50); target.Offset = new Vector(10, 10); root.LayoutManager.ExecuteInitialLayoutPass(); @@ -228,7 +206,7 @@ namespace Avalonia.Controls.UnitTests ++raised; }; - target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58)); + target.Viewport = new Size(56, 58); Assert.Equal(0, raised); @@ -290,34 +268,32 @@ namespace Avalonia.Controls.UnitTests new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - [~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty], - [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], - [~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty], - [~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty], }.RegisterInNameScope(scope), new ScrollBar { - Name = "horizontalScrollBar", + Name = "PART_HorizontalScrollBar", Orientation = Orientation.Horizontal, - [~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty], - [~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty], - [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], [Grid.RowProperty] = 1, }.RegisterInNameScope(scope), new ScrollBar { - Name = "verticalScrollBar", + Name = "PART_VerticalScrollBar", Orientation = Orientation.Vertical, - [~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty], - [~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty], - [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], [Grid.ColumnProperty] = 1, }.RegisterInNameScope(scope), }, }; } + + private static void InitializeScrollViewer(ScrollViewer target) + { + target.ApplyTemplate(); + + var presenter = (ScrollContentPresenter)target.Presenter; + presenter.AttachToScrollViewer(); + presenter.UpdateChild(); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs index 721e8bde68..5e276b5911 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs @@ -163,13 +163,13 @@ namespace Avalonia.Controls.UnitTests true, It.IsAny())) .Returns(() => transitionTask.Task); - + carousel.SelectedIndex = 1; Layout(target); Assert.Equal(items, target.Children); Assert.All(items, x => Assert.True(x.IsVisible)); - + transitionTask.SetResult(); sync.ExecutePostedCallbacks(); @@ -255,12 +255,6 @@ namespace Avalonia.Controls.UnitTests new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), - [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], - [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], - [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], - [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty], - [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty], }.RegisterInNameScope(scope), } }); diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 7f5c0eb134..aa3d90b20f 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -565,12 +565,6 @@ namespace Avalonia.Controls.UnitTests new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = x[~ContentControl.ContentProperty], - [~~ScrollContentPresenter.ExtentProperty] = x[~~ScrollViewer.ExtentProperty], - [~~ScrollContentPresenter.OffsetProperty] = x[~~ScrollViewer.OffsetProperty], - [~~ScrollContentPresenter.ViewportProperty] = x[~~ScrollViewer.ViewportProperty], - [~ScrollContentPresenter.CanHorizontallyScrollProperty] = x[~ScrollViewer.CanHorizontallyScrollProperty], - [~ScrollContentPresenter.CanVerticallyScrollProperty] = x[~ScrollViewer.CanVerticallyScrollProperty], }.RegisterInNameScope(ns)); } From 2a5a5d229f6e8935bbb62b6be0e3f90e62dcc71d Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sun, 19 Mar 2023 15:13:42 +0100 Subject: [PATCH 02/12] Fix tests --- .../Presenters/ScrollContentPresenter.cs | 41 +++++++++++++------ src/Avalonia.Controls/Primitives/ScrollBar.cs | 18 ++++---- src/Avalonia.Controls/ScrollViewer.cs | 8 ++++ .../ScrollViewerTests.cs | 6 +-- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0cde765f76..6552ee6dbd 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -98,6 +98,7 @@ namespace Avalonia.Controls.Presenters private double _verticalSnapPointOffset; private double _horizontalSnapPointOffset; private CompositeDisposable? _ownerSubscriptions; + private ScrollViewer? _owner; /// /// Initializes static members of the class. @@ -305,7 +306,7 @@ namespace Avalonia.Controls.Presenters { _ownerSubscriptions?.Dispose(); - var owner = this.FindAncestorOfType(); + var owner = _owner = this.FindAncestorOfType(); if (owner == null) { @@ -314,17 +315,11 @@ namespace Avalonia.Controls.Presenters var subscriptionDisposables = new IDisposable?[] { - IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty).Select(NotDisabled), Data.BindingPriority.Template)), - IfUnset(CanVerticallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.VerticalScrollBarVisibilityProperty).Select(NotDisabled), Data.BindingPriority.Template)), - IfUnset(OffsetProperty, p => new CompositeDisposable( - Bind(p, owner.GetBindingObservable(ScrollViewer.OffsetProperty), Data.BindingPriority.Template), - this.GetObservable(OffsetProperty).Subscribe(v => owner.SetCurrentValue(OffsetProperty, v)))), + IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)), + IfUnset(CanVerticallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.VerticalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)), + IfUnset(OffsetProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.OffsetProperty), Data.BindingPriority.Template)), IfUnset(IsScrollChainingEnabledProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.IsScrollChainingEnabledProperty), Data.BindingPriority.Template)), IfUnset(ContentProperty, p => Bind(p, owner.GetBindingObservable(ContentProperty), Data.BindingPriority.Template)), - - // read-only properties on ScrollViewer with internal setters: - this.GetObservable(ExtentProperty).Subscribe(v => owner.Extent = v), - this.GetObservable(ViewportProperty).Subscribe(v => owner.Viewport = v) }.Where(d => d != null).Cast().ToArray(); _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables); @@ -338,6 +333,7 @@ namespace Avalonia.Controls.Presenters protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _ownerSubscriptions?.Dispose(); + _owner = null; base.OnDetachedFromVisualTree(e); } @@ -670,9 +666,14 @@ namespace Avalonia.Controls.Presenters protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (change.Property == OffsetProperty && !_arranging) + if (change.Property == OffsetProperty) { - InvalidateArrange(); + if (!_arranging) + { + InvalidateArrange(); + } + + _owner?.SetCurrentValue(OffsetProperty, change.GetNewValue()); } else if (change.Property == ContentProperty) { @@ -699,6 +700,22 @@ namespace Avalonia.Controls.Presenters { UpdateSnapPoints(); } + else if (change.Property == ExtentProperty) + { + if (_owner != null) + { + _owner.Extent = change.GetNewValue(); + } + CoerceValue(OffsetProperty); + } + else if (change.Property == ViewportProperty && _owner != null) + { + if (_owner != null) + { + _owner.Viewport = change.GetNewValue(); + } + CoerceValue(OffsetProperty); + } base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index df69baae0a..29775f4526 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -84,6 +84,7 @@ namespace Avalonia.Controls.Primitives private DispatcherTimer? _timer; private bool _isExpanded; private CompositeDisposable? _ownerSubscriptions; + private ScrollViewer? _owner; /// /// Initializes static members of the class. @@ -203,7 +204,7 @@ namespace Avalonia.Controls.Primitives { _ownerSubscriptions?.Dispose(); - var owner = this.FindAncestorOfType(); + var owner = _owner = this.FindAncestorOfType(); if (owner == null) { @@ -214,11 +215,9 @@ namespace Avalonia.Controls.Primitives var subscriptionDisposables = new IDisposable?[] { - IfUnset(MaximumProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ScrollBarMaximumProperty).Select(ExtractOrdinate), BindingPriority.Template)), - IfUnset(ValueProperty, p => new CompositeDisposable( - Bind(p, owner.GetObservable(ScrollViewer.OffsetProperty).Select(ExtractOrdinate), BindingPriority.Template), - this.GetObservable(ValueProperty).Subscribe(v => SetScrollViewerOffset(owner, v)))), - IfUnset(ViewportSizeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ViewportProperty).Select(ExtractOrdinate), BindingPriority.Template)), + IfUnset(MaximumProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ScrollBarMaximumProperty, ExtractOrdinate), BindingPriority.Template)), + IfUnset(ValueProperty, p => Bind(p, owner.GetObservable(ScrollViewer.OffsetProperty, ExtractOrdinate), BindingPriority.Template)), + IfUnset(ViewportSizeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ViewportProperty, ExtractOrdinate), BindingPriority.Template)), IfUnset(VisibilityProperty, p => Bind(p, owner.GetObservable(visibilitySource), BindingPriority.Template)), IfUnset(AllowAutoHideProperty, p => Bind(p, owner.GetObservable(ScrollViewer.AllowAutoHideProperty), BindingPriority.Template)), IfUnset(LargeChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.LargeChangeProperty).Select(ExtractOrdinate), BindingPriority.Template)), @@ -232,11 +231,11 @@ namespace Avalonia.Controls.Primitives private double ExtractOrdinate(Vector v) => Orientation == Orientation.Horizontal ? v.X : v.Y; private double ExtractOrdinate(Size v) => Orientation == Orientation.Horizontal ? v.Width : v.Height; - private void SetScrollViewerOffset(ScrollViewer viewer, double value) => viewer.SetCurrentValue(ScrollViewer.OffsetProperty, Orientation == Orientation.Horizontal ? viewer.Offset.WithX(value) : viewer.Offset.WithY(value)); protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _ownerSubscriptions?.Dispose(); + _owner = null; base.OnDetachedFromVisualTree(e); } @@ -267,6 +266,11 @@ namespace Avalonia.Controls.Primitives { UpdateIsExpandedState(); } + else if (change.Property == ValueProperty) + { + var value = change.GetNewValue(); + _owner?.SetCurrentValue(ScrollViewer.OffsetProperty, Orientation == Orientation.Horizontal ? _owner.Offset.WithX(value) : _owner.Offset.WithY(value)); + } else { if (change.Property == MinimumProperty || diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index f469e4948d..26b2a030c5 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -683,6 +683,14 @@ namespace Avalonia.Controls { CalculatedPropertiesChanged(); } + else if (change.Property == ExtentProperty) + { + CoerceValue(OffsetProperty); + } + else if (change.Property == ViewportProperty) + { + CoerceValue(OffsetProperty); + } } protected override void OnKeyDown(KeyEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index 407bd40c37..a7bde551e9 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -225,8 +225,8 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot(target); var raised = 0; - target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); - target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Extent = new (100, 100); + target.Viewport = new(50, 50); target.Offset = new Vector(50, 50); root.LayoutManager.ExecuteInitialLayoutPass(); @@ -239,7 +239,7 @@ namespace Avalonia.Controls.UnitTests ++raised; }; - target.SetValue(ScrollViewer.ExtentProperty, new Size(70, 70)); + target.Extent = new(70, 70); Assert.Equal(0, raised); From b400a1f72d72b59ee5c1c9c3bf10918db22ac510 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sun, 26 Mar 2023 16:32:45 +0200 Subject: [PATCH 03/12] Fixed children being attached to the visual or logical tree multiple times --- src/Avalonia.Base/StyledElement.cs | 2 +- src/Avalonia.Base/Visual.cs | 2 +- tests/Avalonia.Controls.UnitTests/MenuItemTests.cs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index b51093b40c..731cb97161 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -891,7 +891,7 @@ namespace Avalonia for (var i = 0; i < logicalChildrenCount; i++) { - if (logicalChildren[i] is StyledElement child) + if (logicalChildren[i] is StyledElement child && child._logicalRoot != e.Root) // child may already have been attached within an event handler { child.OnAttachedToLogicalTreeCore(e); } diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index b4c5b2a1d2..05159eb4ae 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -487,7 +487,7 @@ namespace Avalonia for (var i = 0; i < visualChildrenCount; i++) { - if (visualChildren[i] is { } child) + if (visualChildren[i] is { } child && child._visualRoot != e.Root) // child may already have been attached within an event handler { child.OnAttachedToVisualTreeCore(e); } diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index fc189fb3c3..bbe46b2b21 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -250,13 +250,13 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, canExecuteCallCount); flyout.ShowAt(button); - Assert.Equal(2, canExecuteCallCount); + Assert.Equal(1, canExecuteCallCount); command.RaiseCanExecuteChanged(); - Assert.Equal(3, canExecuteCallCount); + Assert.Equal(2, canExecuteCallCount); target.CommandParameter = true; - Assert.Equal(4, canExecuteCallCount); + Assert.Equal(3, canExecuteCallCount); } } From e3c6ee0da3ee703e32c956d1521a37a7e0c86165 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Fri, 31 Mar 2023 21:08:06 +0200 Subject: [PATCH 04/12] Convert ScrollGestureRecognizer to an unstyled element Added missing binding of IsScrollInertiaEnabled in the simple theme --- .../ScrollGestureRecognizer.cs | 53 +++++++++++-------- .../Controls/ScrollViewer.xaml | 2 +- .../Controls/ScrollViewer.xaml | 3 +- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 1ad2f292ca..b510d44e63 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -4,14 +4,17 @@ using Avalonia.Threading; namespace Avalonia.Input.GestureRecognizers { - public class ScrollGestureRecognizer - : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise - IGestureRecognizer + public class ScrollGestureRecognizer : AvaloniaObject, IGestureRecognizer { // Pixels per second speed that is considered to be the stop of inertial scroll internal const double InertialScrollSpeedEnd = 5; public const double InertialResistance = 0.15; + private bool _canHorizontallyScroll; + private bool _canVerticallyScroll; + private bool _isScrollInertiaEnabled; + private int _scrollStartDistance = 30; + private bool _scrolling; private Point _trackedRootPoint; private IPointer? _tracking; @@ -28,34 +31,39 @@ namespace Avalonia.Input.GestureRecognizers /// /// Defines the property. /// - public static readonly StyledProperty CanHorizontallyScrollProperty = - AvaloniaProperty.Register(nameof(CanHorizontallyScroll)); + public static readonly DirectProperty CanHorizontallyScrollProperty = + AvaloniaProperty.RegisterDirect(nameof(CanHorizontallyScroll), + o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v); /// /// Defines the property. /// - public static readonly StyledProperty CanVerticallyScrollProperty = - AvaloniaProperty.Register(nameof(CanVerticallyScroll)); + public static readonly DirectProperty CanVerticallyScrollProperty = + AvaloniaProperty.RegisterDirect(nameof(CanVerticallyScroll), + o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v); /// /// Defines the property. /// - public static readonly StyledProperty IsScrollInertiaEnabledProperty = - AvaloniaProperty.Register(nameof(IsScrollInertiaEnabled)); + public static readonly DirectProperty IsScrollInertiaEnabledProperty = + AvaloniaProperty.RegisterDirect(nameof(IsScrollInertiaEnabled), + o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v); /// /// Defines the property. /// - public static readonly StyledProperty ScrollStartDistanceProperty = - AvaloniaProperty.Register(nameof(ScrollStartDistance), 30); - + public static readonly DirectProperty ScrollStartDistanceProperty = + AvaloniaProperty.RegisterDirect(nameof(ScrollStartDistance), + o => o.ScrollStartDistance, (o, v) => o.ScrollStartDistance = v, + unsetValue: 30); + /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// public bool CanHorizontallyScroll { - get => GetValue(CanHorizontallyScrollProperty); - set => SetValue(CanHorizontallyScrollProperty, value); + get => _canHorizontallyScroll; + set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); } /// @@ -63,17 +71,17 @@ namespace Avalonia.Input.GestureRecognizers /// public bool CanVerticallyScroll { - get => GetValue(CanVerticallyScrollProperty); - set => SetValue(CanVerticallyScrollProperty, value); + get => _canVerticallyScroll; + set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } - + /// /// Gets or sets whether the gesture should include inertia in it's behavior. /// public bool IsScrollInertiaEnabled { - get => GetValue(IsScrollInertiaEnabledProperty); - set => SetValue(IsScrollInertiaEnabledProperty, value); + get => _isScrollInertiaEnabled; + set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value); } /// @@ -81,10 +89,9 @@ namespace Avalonia.Input.GestureRecognizers /// public int ScrollStartDistance { - get => GetValue(ScrollStartDistanceProperty); - set => SetValue(ScrollStartDistanceProperty, value); - } - + get => _scrollStartDistance; + set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value); + } public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) { diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml index 04b8f80f82..1d9815713c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml @@ -37,7 +37,7 @@ + IsScrollInertiaEnabled="{Binding IsScrollInertiaEnabled, RelativeSource={RelativeSource TemplatedParent}}" /> + CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}" + IsScrollInertiaEnabled="{Binding IsScrollInertiaEnabled, RelativeSource={RelativeSource TemplatedParent}}" /> Date: Sat, 1 Apr 2023 13:00:52 +0200 Subject: [PATCH 05/12] Don't detach from ScrollViewer when detached from visual tree Fixes offset resetting when switching between tabs --- .../Presenters/ScrollContentPresenter.cs | 27 ++++++++--------- src/Avalonia.Controls/Primitives/ScrollBar.cs | 29 +++++++++++-------- src/Avalonia.Controls/ScrollViewer.cs | 7 +++-- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 6552ee6dbd..0efa8d38f8 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -304,15 +304,23 @@ namespace Avalonia.Controls.Presenters /// protected internal virtual void AttachToScrollViewer() { - _ownerSubscriptions?.Dispose(); - - var owner = _owner = this.FindAncestorOfType(); + var owner = this.FindAncestorOfType(); if (owner == null) { + _owner = null; + _ownerSubscriptions?.Dispose(); + _ownerSubscriptions = null; return; } + if (owner == _owner) + { + return; + } + + _ownerSubscriptions?.Dispose(); + var subscriptionDisposables = new IDisposable?[] { IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)), @@ -322,19 +330,12 @@ namespace Avalonia.Controls.Presenters IfUnset(ContentProperty, p => Bind(p, owner.GetBindingObservable(ContentProperty), Data.BindingPriority.Template)), }.Where(d => d != null).Cast().ToArray(); + _owner = owner; _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables); static bool NotDisabled(ScrollBarVisibility v) => v != ScrollBarVisibility.Disabled; - IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => GetValueStore().IsSet(property) ? null : func(property); - } - - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - _ownerSubscriptions?.Dispose(); - _owner = null; - base.OnDetachedFromVisualTree(e); + IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => IsSet(property) ? null : func(property); } /// @@ -708,7 +709,7 @@ namespace Avalonia.Controls.Presenters } CoerceValue(OffsetProperty); } - else if (change.Property == ViewportProperty && _owner != null) + else if (change.Property == ViewportProperty) { if (_owner != null) { diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 29775f4526..37aa1ebffd 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -202,15 +202,23 @@ namespace Avalonia.Controls.Primitives /// protected internal virtual void AttachToScrollViewer() { - _ownerSubscriptions?.Dispose(); - - var owner = _owner = this.FindAncestorOfType(); + var owner = this.FindAncestorOfType(); if (owner == null) { + _owner = null; + _ownerSubscriptions?.Dispose(); + _ownerSubscriptions = null; return; } + if (owner == _owner) + { + return; + } + + _ownerSubscriptions?.Dispose(); + var visibilitySource = Orientation == Orientation.Horizontal ? ScrollViewer.HorizontalScrollBarVisibilityProperty : ScrollViewer.VerticalScrollBarVisibilityProperty; var subscriptionDisposables = new IDisposable?[] @@ -224,21 +232,15 @@ namespace Avalonia.Controls.Primitives IfUnset(SmallChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.SmallChangeProperty).Select(ExtractOrdinate), BindingPriority.Template)) }.Where(d => d != null).Cast().ToArray(); + _owner = owner; _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables); - IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => GetValueStore().IsSet(property) ? null : func(property); + IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => IsSet(property) ? null : func(property); } private double ExtractOrdinate(Vector v) => Orientation == Orientation.Horizontal ? v.X : v.Y; private double ExtractOrdinate(Size v) => Orientation == Orientation.Horizontal ? v.Width : v.Height; - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - _ownerSubscriptions?.Dispose(); - _owner = null; - base.OnDetachedFromVisualTree(e); - } - protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.PageUp) @@ -260,7 +262,10 @@ namespace Avalonia.Controls.Primitives if (change.Property == OrientationProperty) { UpdatePseudoClasses(change.GetNewValue()); - AttachToScrollViewer(); // there's no way to manually refresh bindings, so reapply them + if (IsAttachedToVisualTree) + { + AttachToScrollViewer(); // there's no way to manually refresh bindings, so reapply them + } } else if (change.Property == AllowAutoHideProperty) { diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 26b2a030c5..a7188c6226 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -660,8 +660,11 @@ namespace Avalonia.Controls private void CalculatedPropertiesChanged() { var newMaximum = ScrollBarMaximum; - RaisePropertyChanged(ScrollBarMaximumProperty, _oldMaximum, newMaximum); - _oldMaximum = newMaximum; + if (newMaximum != _oldMaximum) + { + RaisePropertyChanged(ScrollBarMaximumProperty, _oldMaximum, newMaximum); + _oldMaximum = newMaximum; + } if (_logicalScrollable?.IsLogicalScrollEnabled == true) { From d0be3f2d7bf1224d695311f913742aa35fbce150 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 5 Apr 2023 15:54:16 -0400 Subject: [PATCH 06/12] Proper implementation of DispatcherImpl for iOS --- src/iOS/Avalonia.iOS/DispatcherImpl.cs | 139 ++++++++++++++++++ src/iOS/Avalonia.iOS/Interop.cs | 47 ++++++ src/iOS/Avalonia.iOS/Platform.cs | 3 +- .../PlatformThreadingInterface.cs | 38 ----- 4 files changed, 188 insertions(+), 39 deletions(-) create mode 100644 src/iOS/Avalonia.iOS/DispatcherImpl.cs create mode 100644 src/iOS/Avalonia.iOS/Interop.cs delete mode 100644 src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs diff --git a/src/iOS/Avalonia.iOS/DispatcherImpl.cs b/src/iOS/Avalonia.iOS/DispatcherImpl.cs new file mode 100644 index 0000000000..e9448c8757 --- /dev/null +++ b/src/iOS/Avalonia.iOS/DispatcherImpl.cs @@ -0,0 +1,139 @@ +#nullable enable + +using System; +using System.Diagnostics; +using System.Threading; +using Avalonia.Threading; +using CoreFoundation; +using Foundation; +using ObjCRuntime; +using CFIndex = System.IntPtr; + +namespace Avalonia.iOS; + +internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing +{ + // CFRunLoopTimerSetNextFireDate docs recommend to "create a repeating timer with an initial + // firing time in the distant future (or the initial firing time) and a very large repeat + // interval—on the order of decades or more" + private const double DistantFutureInterval = (double)50*365*24*3600; + internal static readonly DispatcherImpl Instance = new(); + + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private readonly Action _checkSignaledAction; + private readonly Action _wakeUpLoopAction; + private readonly IntPtr _timer; + private Thread? _loopThread; + private bool _backgroundProcessingRequested, _signaled; + + private DispatcherImpl() + { + _checkSignaledAction = CheckSignaled; + _wakeUpLoopAction = () => + { + // This is needed to wakeup the loop if we are called from inside of BeforeWait hook + }; + + var observerBlock = new BlockLiteral(); + observerBlock.SetupBlock((Interop.CFRunLoopObserverCallback)ObserverCallback, null); + var observer = Interop.CFRunLoopObserverCreateWithHandler(IntPtr.Zero, + Interop.CFOptionFlags.kCFRunLoopAfterWaiting | Interop.CFOptionFlags.kCFRunLoopBeforeSources | + Interop.CFOptionFlags.kCFRunLoopBeforeWaiting, + true, + 0, + ref observerBlock); + Interop.CFRunLoopAddObserver(CFRunLoop.Main.Handle, observer, Interop.kCFRunLoopCommonModes); + + var timerBlock = new BlockLiteral(); + timerBlock.SetupBlock((Interop.CFRunLoopTimerCallback)TimerCallback, null); + _timer = Interop.CFRunLoopTimerCreateWithHandler(IntPtr.Zero, + Interop.CFAbsoluteTimeGetCurrent() + DistantFutureInterval, + DistantFutureInterval, 0, 0, ref timerBlock); + } + + public event Action? Signaled; + public event Action? Timer; + public event Action? ReadyForBackgroundProcessing; + + public bool CurrentThreadIsLoopThread + { + get + { + if (_loopThread != null) + return Thread.CurrentThread == _loopThread; + if (!NSThread.IsMain) + return false; + _loopThread = Thread.CurrentThread; + return true; + } + } + + public void Signal() + { + lock (this) { + if(_signaled) + return; + _signaled = true; + + DispatchQueue.MainQueue.DispatchAsync(_checkSignaledAction); + CFRunLoop.Main.WakeUp(); + } + } + + public void UpdateTimer(long? dueTimeInMs) + { + var ms = dueTimeInMs == null ? -1 : (int)Math.Min(int.MaxValue - 10, Math.Max(1, dueTimeInMs.Value - Now)); + var interval = ms < 0 ? DistantFutureInterval : ((double)ms / 1000); + Interop.CFRunLoopTimerSetTolerance(_timer, 0); + Interop.CFRunLoopTimerSetNextFireDate(_timer, Interop.CFAbsoluteTimeGetCurrent() + interval); + } + + public long Now => _clock.ElapsedMilliseconds; + + public void RequestBackgroundProcessing() + { + if(_backgroundProcessingRequested) + return; + _backgroundProcessingRequested = true; + DispatchQueue.MainQueue.DispatchAsync(_wakeUpLoopAction); + } + + private void CheckSignaled() + { + bool signaled; + lock (this) + { + signaled = _signaled; + _signaled = false; + } + + if (signaled) + { + Signaled?.Invoke(); + } + } + + [MonoPInvokeCallback(typeof(Interop.CFRunLoopObserverCallback))] + private static void ObserverCallback(IntPtr observer, Interop.CFOptionFlags activity) + { + if (activity == Interop.CFOptionFlags.kCFRunLoopBeforeWaiting) + { + bool triggerProcessing; + lock (Instance) + { + triggerProcessing = Instance._backgroundProcessingRequested; + Instance._backgroundProcessingRequested = false; + } + + if (triggerProcessing) Instance.ReadyForBackgroundProcessing?.Invoke(); + } + + Instance.CheckSignaled(); + } + + [MonoPInvokeCallback(typeof(Interop.CFRunLoopTimerCallback))] + private static void TimerCallback(IntPtr timer) + { + Instance.Timer?.Invoke(); + } +} diff --git a/src/iOS/Avalonia.iOS/Interop.cs b/src/iOS/Avalonia.iOS/Interop.cs new file mode 100644 index 0000000000..2758faa2f1 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Interop.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.InteropServices; +using CoreFoundation; +using Foundation; +using ObjCRuntime; + +namespace Avalonia.iOS; + +// TODO: use LibraryImport in NET7 +internal class Interop +{ + internal const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + internal static NativeHandle kCFRunLoopCommonModes = CFString.CreateNative("kCFRunLoopCommonModes"); + + [Flags] + public enum CFOptionFlags : ulong + { + kCFRunLoopBeforeSources = (1UL << 2), + kCFRunLoopAfterWaiting = (1UL << 6), + kCFRunLoopBeforeWaiting = (1UL << 5) + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void CFRunLoopObserverCallback(IntPtr observer, CFOptionFlags activity); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void CFRunLoopTimerCallback(IntPtr timer); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopObserverCreateWithHandler(IntPtr allocator, CFOptionFlags activities, bool repeats, int index, ref BlockLiteral block); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopAddObserver(IntPtr loop, IntPtr observer, IntPtr mode); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopTimerCreateWithHandler(IntPtr allocator, double firstDate, double interval, + CFOptionFlags flags, int order, ref BlockLiteral block); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFRunLoopTimerSetTolerance(IntPtr timer, double tolerance); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFRunLoopTimerSetNextFireDate(IntPtr timer, double fireDate); + + [DllImport(CoreFoundationLibrary)] + internal static extern double CFAbsoluteTimeGetCurrent(); +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 9187187648..cf5c89cf5f 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -7,6 +7,7 @@ using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.Threading; namespace Avalonia { @@ -44,7 +45,7 @@ namespace Avalonia.iOS .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(Timer) - .Bind().ToConstant(new PlatformThreadingInterface()) + .Bind().ToConstant(DispatcherImpl.Instance) .Bind().ToConstant(keyboard); Compositor = new Compositor( diff --git a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs b/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs deleted file mode 100644 index 93caecf711..0000000000 --- a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Threading; -using Avalonia.Platform; -using Avalonia.Threading; -using CoreFoundation; -using Foundation; - -namespace Avalonia.iOS -{ - class PlatformThreadingInterface : IPlatformThreadingInterface - { - private bool _signaled; - public static PlatformThreadingInterface Instance { get; } = new PlatformThreadingInterface(); - public bool CurrentThreadIsLoopThread => NSThread.Current.IsMainThread; - - public event Action Signaled; - - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - => NSTimer.CreateRepeatingScheduledTimer(interval, _ => tick()); - - public void Signal(DispatcherPriority prio) - { - lock (this) - { - if(_signaled) - return; - _signaled = true; - } - - DispatchQueue.MainQueue.DispatchAsync(() => - { - lock (this) - _signaled = false; - Signaled?.Invoke(null); - }); - } - } -} \ No newline at end of file From 76eeca8f1249908b89a9c2a19529e9e030bc6b06 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 5 Apr 2023 16:13:36 -0400 Subject: [PATCH 07/12] Format code --- src/iOS/Avalonia.iOS/DispatcherImpl.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/iOS/Avalonia.iOS/DispatcherImpl.cs b/src/iOS/Avalonia.iOS/DispatcherImpl.cs index e9448c8757..0c11aa473b 100644 --- a/src/iOS/Avalonia.iOS/DispatcherImpl.cs +++ b/src/iOS/Avalonia.iOS/DispatcherImpl.cs @@ -16,7 +16,7 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing // CFRunLoopTimerSetNextFireDate docs recommend to "create a repeating timer with an initial // firing time in the distant future (or the initial firing time) and a very large repeat // interval—on the order of decades or more" - private const double DistantFutureInterval = (double)50*365*24*3600; + private const double DistantFutureInterval = (double)50 * 365 * 24 * 3600; internal static readonly DispatcherImpl Instance = new(); private readonly Stopwatch _clock = Stopwatch.StartNew(); @@ -54,7 +54,7 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing public event Action? Signaled; public event Action? Timer; public event Action? ReadyForBackgroundProcessing; - + public bool CurrentThreadIsLoopThread { get @@ -70,8 +70,9 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing public void Signal() { - lock (this) { - if(_signaled) + lock (this) + { + if (_signaled) return; _signaled = true; @@ -89,10 +90,10 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing } public long Now => _clock.ElapsedMilliseconds; - + public void RequestBackgroundProcessing() { - if(_backgroundProcessingRequested) + if (_backgroundProcessingRequested) return; _backgroundProcessingRequested = true; DispatchQueue.MainQueue.DispatchAsync(_wakeUpLoopAction); From 3a6b44a5cf3a89d364080362fc7abfdd87dfb10f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 5 Apr 2023 16:13:58 -0400 Subject: [PATCH 08/12] Fix platform settings returning empty color --- src/iOS/Avalonia.iOS/PlatformSettings.cs | 31 ++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs index 082966f5b2..07e366e79e 100644 --- a/src/iOS/Avalonia.iOS/PlatformSettings.cs +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -31,24 +31,25 @@ internal class PlatformSettings : DefaultPlatformSettings if (tintColor is not null) { tintColor.GetRGBA(out var red, out var green, out var blue, out var alpha); - return _lastColorValues = new PlatformColorValues + if (red != 0 && green != 0 && blue != 0 && alpha != 0) { - ThemeVariant = themeVariant, - ContrastPreference = contrastPreference, - AccentColor1 = new Color( - (byte)(alpha * 255), - (byte)(red * 255), - (byte)(green * 255), - (byte)(blue * 255)) - }; + return _lastColorValues = new PlatformColorValues + { + ThemeVariant = themeVariant, + ContrastPreference = contrastPreference, + AccentColor1 = new Color( + (byte)(alpha * 255), + (byte)(red * 255), + (byte)(green * 255), + (byte)(blue * 255)) + }; + } } - else + + return _lastColorValues = new PlatformColorValues { - return _lastColorValues = new PlatformColorValues - { - ThemeVariant = themeVariant, ContrastPreference = contrastPreference - }; - } + ThemeVariant = themeVariant, ContrastPreference = contrastPreference + }; } public void TraitCollectionDidChange() From cf4154505bf2803201c3335197c7fe138420bb54 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 5 Apr 2023 22:55:24 -0400 Subject: [PATCH 09/12] Avoid BlockLiteral based APIs for simplicity --- src/iOS/Avalonia.iOS/DispatcherImpl.cs | 23 +++++++++-------------- src/iOS/Avalonia.iOS/Interop.cs | 24 ++++++++++++++++-------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/iOS/Avalonia.iOS/DispatcherImpl.cs b/src/iOS/Avalonia.iOS/DispatcherImpl.cs index 0c11aa473b..8b3c747b5a 100644 --- a/src/iOS/Avalonia.iOS/DispatcherImpl.cs +++ b/src/iOS/Avalonia.iOS/DispatcherImpl.cs @@ -34,21 +34,16 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing // This is needed to wakeup the loop if we are called from inside of BeforeWait hook }; - var observerBlock = new BlockLiteral(); - observerBlock.SetupBlock((Interop.CFRunLoopObserverCallback)ObserverCallback, null); - var observer = Interop.CFRunLoopObserverCreateWithHandler(IntPtr.Zero, + var observer = Interop.CFRunLoopObserverCreate(IntPtr.Zero, Interop.CFOptionFlags.kCFRunLoopAfterWaiting | Interop.CFOptionFlags.kCFRunLoopBeforeSources | Interop.CFOptionFlags.kCFRunLoopBeforeWaiting, - true, - 0, - ref observerBlock); - Interop.CFRunLoopAddObserver(CFRunLoop.Main.Handle, observer, Interop.kCFRunLoopCommonModes); - - var timerBlock = new BlockLiteral(); - timerBlock.SetupBlock((Interop.CFRunLoopTimerCallback)TimerCallback, null); - _timer = Interop.CFRunLoopTimerCreateWithHandler(IntPtr.Zero, + true, 0, ObserverCallback, IntPtr.Zero); + Interop.CFRunLoopAddObserver(Interop.CFRunLoopGetMain(), observer, Interop.kCFRunLoopCommonModes); + + _timer = Interop.CFRunLoopTimerCreate(IntPtr.Zero, Interop.CFAbsoluteTimeGetCurrent() + DistantFutureInterval, - DistantFutureInterval, 0, 0, ref timerBlock); + DistantFutureInterval, 0, 0, TimerCallback, IntPtr.Zero); + Interop.CFRunLoopAddTimer(Interop.CFRunLoopGetMain(), _timer, Interop.kCFRunLoopCommonModes); } public event Action? Signaled; @@ -115,7 +110,7 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing } [MonoPInvokeCallback(typeof(Interop.CFRunLoopObserverCallback))] - private static void ObserverCallback(IntPtr observer, Interop.CFOptionFlags activity) + private static void ObserverCallback(IntPtr observer, Interop.CFOptionFlags activity, IntPtr info) { if (activity == Interop.CFOptionFlags.kCFRunLoopBeforeWaiting) { @@ -133,7 +128,7 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing } [MonoPInvokeCallback(typeof(Interop.CFRunLoopTimerCallback))] - private static void TimerCallback(IntPtr timer) + private static void TimerCallback(IntPtr timer, IntPtr info) { Instance.Timer?.Invoke(); } diff --git a/src/iOS/Avalonia.iOS/Interop.cs b/src/iOS/Avalonia.iOS/Interop.cs index 2758faa2f1..c0b3506936 100644 --- a/src/iOS/Avalonia.iOS/Interop.cs +++ b/src/iOS/Avalonia.iOS/Interop.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using CoreFoundation; using Foundation; @@ -13,7 +14,7 @@ internal class Interop internal static NativeHandle kCFRunLoopCommonModes = CFString.CreateNative("kCFRunLoopCommonModes"); [Flags] - public enum CFOptionFlags : ulong + internal enum CFOptionFlags : ulong { kCFRunLoopBeforeSources = (1UL << 2), kCFRunLoopAfterWaiting = (1UL << 6), @@ -21,27 +22,34 @@ internal class Interop } [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void CFRunLoopObserverCallback(IntPtr observer, CFOptionFlags activity); - + internal delegate void CFRunLoopObserverCallback(IntPtr observer, CFOptionFlags activity, IntPtr info); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void CFRunLoopTimerCallback(IntPtr timer); + internal delegate void CFRunLoopTimerCallback(IntPtr timer, IntPtr info); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopGetMain(); [DllImport(CoreFoundationLibrary)] - internal static extern IntPtr CFRunLoopObserverCreateWithHandler(IntPtr allocator, CFOptionFlags activities, bool repeats, int index, ref BlockLiteral block); + internal static extern IntPtr CFRunLoopObserverCreate(IntPtr allocator, CFOptionFlags activities, + bool repeats, int index, CFRunLoopObserverCallback callout, IntPtr context); [DllImport(CoreFoundationLibrary)] internal static extern IntPtr CFRunLoopAddObserver(IntPtr loop, IntPtr observer, IntPtr mode); [DllImport(CoreFoundationLibrary)] - internal static extern IntPtr CFRunLoopTimerCreateWithHandler(IntPtr allocator, double firstDate, double interval, - CFOptionFlags flags, int order, ref BlockLiteral block); + internal static extern IntPtr CFRunLoopTimerCreate(IntPtr allocator, double firstDate, double interval, + CFOptionFlags flags, int order, CFRunLoopTimerCallback callout, IntPtr context); [DllImport(CoreFoundationLibrary)] internal static extern void CFRunLoopTimerSetTolerance(IntPtr timer, double tolerance); [DllImport(CoreFoundationLibrary)] internal static extern void CFRunLoopTimerSetNextFireDate(IntPtr timer, double fireDate); - + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFRunLoopAddTimer(IntPtr loop, IntPtr timer, IntPtr mode); + [DllImport(CoreFoundationLibrary)] internal static extern double CFAbsoluteTimeGetCurrent(); } From b7cf688f867055570ec2a5aca0fe0272544302fa Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Thu, 6 Apr 2023 11:12:04 +0200 Subject: [PATCH 10/12] Use SetCurrentValue in Slider --- src/Avalonia.Controls/Slider.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index ed9075c155..b0dff5be79 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -246,11 +246,11 @@ namespace Avalonia.Controls break; case Key.Home: - Value = Minimum; + SetCurrentValue(ValueProperty, Minimum); break; case Key.End: - Value = Maximum; + SetCurrentValue(ValueProperty, Maximum); break; default: @@ -313,7 +313,7 @@ namespace Avalonia.Controls // Update if we've found a better value if (Math.Abs(next - value) > Tolerance) { - Value = next; + SetCurrentValue(ValueProperty, next); } } @@ -366,7 +366,7 @@ namespace Avalonia.Controls var range = Maximum - Minimum; var finalValue = calcVal * range + Minimum; - Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue; + SetCurrentValue(ValueProperty, IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue); } /// From 6b98400f87d48bbdc7efc41e966f6e304953d702 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 3 Apr 2023 15:08:07 +0000 Subject: [PATCH 11/12] report frame position for linux --- src/Avalonia.X11/X11Window.Ime.cs | 2 +- src/Avalonia.X11/X11Window.cs | 64 +++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.X11/X11Window.Ime.cs b/src/Avalonia.X11/X11Window.Ime.cs index 26ead5e6b8..4ee418fd0a 100644 --- a/src/Avalonia.X11/X11Window.Ime.cs +++ b/src/Avalonia.X11/X11Window.Ime.cs @@ -89,7 +89,7 @@ namespace Avalonia.X11 } } - private void UpdateImePosition() => _imeControl?.UpdateWindowInfo(Position, RenderScaling); + private void UpdateImePosition() => _imeControl?.UpdateWindowInfo(_position ?? default, RenderScaling); private void HandleKeyEvent(ref XEvent ev) { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 8bd84215ed..3d6e349f82 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -124,7 +124,7 @@ namespace Avalonia.X11 if (!_popup && Screen != null) { var monitor = Screen.AllScreens.OrderBy(x => x.Scaling) - .FirstOrDefault(m => m.Bounds.Contains(Position)); + .FirstOrDefault(m => m.Bounds.Contains(_position ?? default)); if (monitor != null) { @@ -326,23 +326,16 @@ namespace Avalonia.X11 { get { - XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_FRAME_EXTENTS, IntPtr.Zero, - new IntPtr(4), false, (IntPtr)Atom.AnyPropertyType, out var _, - out var _, out var nitems, out var _, out var prop); + var extents = GetFrameExtents(); - if (nitems.ToInt64() != 4) + if(extents == null) { - // Window hasn't been mapped by the WM yet, so can't get the extents. return null; } - var data = (IntPtr*)prop.ToPointer(); - var extents = new Thickness(data[0].ToInt32(), data[2].ToInt32(), data[1].ToInt32(), data[3].ToInt32()); - XFree(prop); - return new Size( - (_realSize.Width + extents.Left + extents.Right) / RenderScaling, - (_realSize.Height + extents.Top + extents.Bottom) / RenderScaling); + (_realSize.Width + extents.Value.Left + extents.Value.Right) / RenderScaling, + (_realSize.Height + extents.Value.Top + extents.Value.Bottom) / RenderScaling); } } @@ -556,6 +549,25 @@ namespace Avalonia.X11 } } + private Thickness? GetFrameExtents() + { + XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_FRAME_EXTENTS, IntPtr.Zero, + new IntPtr(4), false, (IntPtr)Atom.AnyPropertyType, out var _, + out var _, out var nitems, out var _, out var prop); + + if (nitems.ToInt64() != 4) + { + // Window hasn't been mapped by the WM yet, so can't get the extents. + return null; + } + + var data = (IntPtr*)prop.ToPointer(); + var extents = new Thickness(data[0].ToInt32(), data[2].ToInt32(), data[1].ToInt32(), data[3].ToInt32()); + XFree(prop); + + return extents; + } + private bool UpdateScaling(bool skipResize = false) { double newScaling; @@ -564,7 +576,7 @@ namespace Avalonia.X11 else { var monitor = _platform.X11Screens.Screens.OrderBy(x => x.Scaling) - .FirstOrDefault(m => m.Bounds.Contains(Position)); + .FirstOrDefault(m => m.Bounds.Contains(_position ?? default)); newScaling = monitor?.Scaling ?? RenderScaling; } @@ -916,11 +928,11 @@ namespace Avalonia.X11 public void Hide() => XUnmapWindow(_x11.Display, _handle); - public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / RenderScaling, (point.Y - Position.Y) / RenderScaling); + public Point PointToClient(PixelPoint point) => new Point((point.X - (_position ?? default).X) / RenderScaling, (point.Y - (_position ?? default).Y) / RenderScaling); public PixelPoint PointToScreen(Point point) => new PixelPoint( - (int)(point.X * RenderScaling + Position.X), - (int)(point.Y * RenderScaling + Position.Y)); + (int)(point.X * RenderScaling + (_position ?? default).X), + (int)(point.Y * RenderScaling + (_position ?? default).Y)); public void SetSystemDecorations(SystemDecorations enabled) { @@ -984,20 +996,31 @@ namespace Avalonia.X11 public PixelPoint Position { - get => _position ?? default; + get + { + var extents = GetFrameExtents(); + + if(extents == null || _position == null) + { + return default; + } + + return new PixelPoint(_position.Value.X - (int)extents.Value.Left, _position.Value.Y - (int)extents.Value.Top); + } set { - if(!_usePositioningFlags) + if (!_usePositioningFlags) { _usePositioningFlags = true; UpdateSizeHints(null); } - + var changes = new XWindowChanges { - x = (int)value.X, + x = value.X, y = (int)value.Y }; + XConfigureWindow(_x11.Display, _handle, ChangeWindowFlags.CWX | ChangeWindowFlags.CWY, ref changes); XFlush(_x11.Display); @@ -1006,7 +1029,6 @@ namespace Avalonia.X11 _position = value; PositionChanged?.Invoke(value); } - } } From f53498a90e8d7080c5b3aa86768c53573c63bc13 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 3 Apr 2023 16:50:59 +0000 Subject: [PATCH 12/12] use empty extents if frame extents isn't available --- src/Avalonia.X11/X11Window.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 3d6e349f82..b1e8d3fa71 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -998,11 +998,16 @@ namespace Avalonia.X11 { get { + if(_position == null) + { + return default; + } + var extents = GetFrameExtents(); - if(extents == null || _position == null) + if(extents == null) { - return default; + extents = default(Thickness); } return new PixelPoint(_position.Value.X - (int)extents.Value.Left, _position.Value.Y - (int)extents.Value.Top);