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.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/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bc86558ab3..0efa8d38f8 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,8 @@ namespace Avalonia.Controls.Presenters private double _verticalSnapPoint; private double _verticalSnapPointOffset; private double _horizontalSnapPointOffset; + private CompositeDisposable? _ownerSubscriptions; + private ScrollViewer? _owner; /// /// Initializes static members of the class. @@ -116,7 +106,6 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); - ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); } /// @@ -137,8 +126,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 +135,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 +153,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 +284,60 @@ 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() + { + 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)), + 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)), + }.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 => IsSet(property) ? null : func(property); + } + /// void IScrollAnchorProvider.RegisterAnchorCandidate(Control element) { @@ -410,7 +447,7 @@ namespace Avalonia.Controls.Presenters try { _arranging = true; - Offset = newOffset; + SetCurrentValue(OffsetProperty, newOffset); } finally { @@ -427,7 +464,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 +552,7 @@ namespace Avalonia.Controls.Presenters } bool offsetChanged = newOffset != Offset; - Offset = newOffset; + SetCurrentValue(OffsetProperty, newOffset); e.Handled = !IsScrollChainingEnabled || offsetChanged; @@ -529,7 +565,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 +659,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; } @@ -631,9 +667,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) { @@ -651,11 +692,31 @@ namespace Avalonia.Controls.Presenters UpdateSnapPoints(); } + else if (change.Property == ChildProperty) + { + ChildChanged(change); + } else if (change.Property == HorizontalSnapPointsAlignmentProperty || change.Property == VerticalSnapPointsAlignmentProperty) { UpdateSnapPoints(); } + else if (change.Property == ExtentProperty) + { + if (_owner != null) + { + _owner.Extent = change.GetNewValue(); + } + CoerceValue(OffsetProperty); + } + else if (change.Property == ViewportProperty) + { + if (_owner != null) + { + _owner.Viewport = change.GetNewValue(); + } + CoerceValue(OffsetProperty); + } base.OnPropertyChanged(change); } @@ -677,7 +738,7 @@ namespace Avalonia.Controls.Presenters if (e.OldValue != null) { - Offset = default; + SetCurrentValue(OffsetProperty, default); } } @@ -719,14 +780,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..37aa1ebffd 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,8 @@ namespace Avalonia.Controls.Primitives private Button? _pageDownButton; private DispatcherTimer? _timer; private bool _isExpanded; + private CompositeDisposable? _ownerSubscriptions; + private ScrollViewer? _owner; /// /// Initializes static members of the class. @@ -88,6 +93,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,9 +185,62 @@ 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() + { + 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?[] + { + 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)), + 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 => 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 OnKeyDown(KeyEventArgs e) { if (e.Key == Key.PageUp) @@ -202,11 +262,20 @@ namespace Avalonia.Controls.Primitives if (change.Property == OrientationProperty) { UpdatePseudoClasses(change.GetNewValue()); + if (IsAttachedToVisualTree) + { + AttachToScrollViewer(); // there's no way to manually refresh bindings, so reapply them + } } else if (change.Property == AllowAutoHideProperty) { 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 || @@ -373,25 +442,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..a7188c6226 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. - /// - protected double HorizontalScrollBarMaximum - { - get { return Max(_extent.Width - _viewport.Width, 0); } - } - - /// - /// Gets or sets the horizontal scrollbar value. + /// Gets the maximum scrolling distance (which is - ). /// - 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,14 @@ namespace Avalonia.Controls CalculatedPropertiesChanged(); } - private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) + private void CalculatedPropertiesChanged() { - var wasEnabled = e.OldValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; - var isEnabled = e.NewValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; - - if (wasEnabled != isEnabled) + var newMaximum = ScrollBarMaximum; + if (newMaximum != _oldMaximum) { - if (e.Property == HorizontalScrollBarVisibilityProperty) - { - RaisePropertyChanged( - CanHorizontallyScrollProperty, - wasEnabled, - isEnabled); - } - else if (e.Property == VerticalScrollBarVisibilityProperty) - { - RaisePropertyChanged( - CanVerticallyScrollProperty, - wasEnabled, - isEnabled); - } + RaisePropertyChanged(ScrollBarMaximumProperty, _oldMaximum, newMaximum); + _oldMaximum = newMaximum; } - } - - private void CalculatedPropertiesChanged() - { - // Pass old values of 0 here because we don't have the old values at this point, - // and it shouldn't matter as only the template uses these properties. - RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum); - RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue); - RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize); - RaisePropertyChanged(VerticalScrollBarMaximumProperty, 0, VerticalScrollBarMaximum); - RaisePropertyChanged(VerticalScrollBarValueProperty, 0, VerticalScrollBarValue); - RaisePropertyChanged(VerticalScrollBarViewportSizeProperty, 0, VerticalScrollBarViewportSize); if (_logicalScrollable?.IsLogicalScrollEnabled == true) { @@ -906,6 +678,24 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == OffsetProperty) + { + CalculatedPropertiesChanged(); + } + else if (change.Property == ExtentProperty) + { + CoerceValue(OffsetProperty); + } + else if (change.Property == ViewportProperty) + { + CoerceValue(OffsetProperty); + } + } + 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..b0dff5be79 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); } @@ -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); } /// 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..1d9815713c 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 +80,7 @@ + Margin="{TemplateBinding Padding}" /> 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..b1e8d3fa71 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,36 @@ namespace Avalonia.X11 public PixelPoint Position { - get => _position ?? default; + get + { + if(_position == null) + { + return default; + } + + var extents = GetFrameExtents(); + + if(extents == null) + { + extents = default(Thickness); + } + + 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 +1034,6 @@ namespace Avalonia.X11 _position = value; PositionChanged?.Invoke(value); } - } } diff --git a/src/iOS/Avalonia.iOS/DispatcherImpl.cs b/src/iOS/Avalonia.iOS/DispatcherImpl.cs new file mode 100644 index 0000000000..8b3c747b5a --- /dev/null +++ b/src/iOS/Avalonia.iOS/DispatcherImpl.cs @@ -0,0 +1,135 @@ +#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 observer = Interop.CFRunLoopObserverCreate(IntPtr.Zero, + Interop.CFOptionFlags.kCFRunLoopAfterWaiting | Interop.CFOptionFlags.kCFRunLoopBeforeSources | + Interop.CFOptionFlags.kCFRunLoopBeforeWaiting, + true, 0, ObserverCallback, IntPtr.Zero); + Interop.CFRunLoopAddObserver(Interop.CFRunLoopGetMain(), observer, Interop.kCFRunLoopCommonModes); + + _timer = Interop.CFRunLoopTimerCreate(IntPtr.Zero, + Interop.CFAbsoluteTimeGetCurrent() + DistantFutureInterval, + DistantFutureInterval, 0, 0, TimerCallback, IntPtr.Zero); + Interop.CFRunLoopAddTimer(Interop.CFRunLoopGetMain(), _timer, Interop.kCFRunLoopCommonModes); + } + + 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, IntPtr info) + { + 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, IntPtr info) + { + 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..c0b3506936 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Interop.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics.CodeAnalysis; +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] + internal enum CFOptionFlags : ulong + { + kCFRunLoopBeforeSources = (1UL << 2), + kCFRunLoopAfterWaiting = (1UL << 6), + kCFRunLoopBeforeWaiting = (1UL << 5) + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate void CFRunLoopObserverCallback(IntPtr observer, CFOptionFlags activity, IntPtr info); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate void CFRunLoopTimerCallback(IntPtr timer, IntPtr info); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopGetMain(); + + [DllImport(CoreFoundationLibrary)] + 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 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(); +} 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/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() 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 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/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); } } diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index d3eb42f147..a7bde551e9 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); @@ -247,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(); @@ -261,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); @@ -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)); }