Browse Source

Merge pull request #10803 from Enscape/refactor/rangebase-styledproperty

Refactor `RangeBase` to use `StyledProperty`
pull/10929/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
70b6da25e5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 53
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  2. 2
      src/Avalonia.Base/StyledElement.cs
  3. 2
      src/Avalonia.Base/Visual.cs
  4. 139
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  5. 165
      src/Avalonia.Controls/Primitives/RangeBase.cs
  6. 79
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  7. 34
      src/Avalonia.Controls/Primitives/Track.cs
  8. 30
      src/Avalonia.Controls/ProgressBar.cs
  9. 318
      src/Avalonia.Controls/ScrollViewer.cs
  10. 2
      src/Avalonia.Controls/Slider.cs
  11. 13
      src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml
  12. 35
      src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
  13. 52
      src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml
  14. 14
      tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs
  15. 6
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  16. 9
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  17. 6
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  18. 118
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  19. 10
      tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs
  20. 6
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

53
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -4,14 +4,17 @@ using Avalonia.Threading;
namespace Avalonia.Input.GestureRecognizers namespace Avalonia.Input.GestureRecognizers
{ {
public class ScrollGestureRecognizer public class ScrollGestureRecognizer : AvaloniaObject, IGestureRecognizer
: StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
IGestureRecognizer
{ {
// Pixels per second speed that is considered to be the stop of inertial scroll // Pixels per second speed that is considered to be the stop of inertial scroll
internal const double InertialScrollSpeedEnd = 5; internal const double InertialScrollSpeedEnd = 5;
public const double InertialResistance = 0.15; public const double InertialResistance = 0.15;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private bool _isScrollInertiaEnabled;
private int _scrollStartDistance = 30;
private bool _scrolling; private bool _scrolling;
private Point _trackedRootPoint; private Point _trackedRootPoint;
private IPointer? _tracking; private IPointer? _tracking;
@ -28,34 +31,39 @@ namespace Avalonia.Input.GestureRecognizers
/// <summary> /// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property. /// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> CanHorizontallyScrollProperty = public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll)); AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v);
/// <summary> /// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property. /// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> CanVerticallyScrollProperty = public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll)); AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v);
/// <summary> /// <summary>
/// Defines the <see cref="IsScrollInertiaEnabled"/> property. /// Defines the <see cref="IsScrollInertiaEnabled"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsScrollInertiaEnabledProperty = public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled)); AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled),
o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v);
/// <summary> /// <summary>
/// Defines the <see cref="ScrollStartDistance"/> property. /// Defines the <see cref="ScrollStartDistance"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<int> ScrollStartDistanceProperty = public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance), 30); AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance),
o => o.ScrollStartDistance, (o, v) => o.ScrollStartDistance = v,
unsetValue: 30);
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally. /// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary> /// </summary>
public bool CanHorizontallyScroll public bool CanHorizontallyScroll
{ {
get => GetValue(CanHorizontallyScrollProperty); get => _canHorizontallyScroll;
set => SetValue(CanHorizontallyScrollProperty, value); set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
} }
/// <summary> /// <summary>
@ -63,17 +71,17 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary> /// </summary>
public bool CanVerticallyScroll public bool CanVerticallyScroll
{ {
get => GetValue(CanVerticallyScrollProperty); get => _canVerticallyScroll;
set => SetValue(CanVerticallyScrollProperty, value); set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
} }
/// <summary> /// <summary>
/// Gets or sets whether the gesture should include inertia in it's behavior. /// Gets or sets whether the gesture should include inertia in it's behavior.
/// </summary> /// </summary>
public bool IsScrollInertiaEnabled public bool IsScrollInertiaEnabled
{ {
get => GetValue(IsScrollInertiaEnabledProperty); get => _isScrollInertiaEnabled;
set => SetValue(IsScrollInertiaEnabledProperty, value); set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
} }
/// <summary> /// <summary>
@ -81,10 +89,9 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary> /// </summary>
public int ScrollStartDistance public int ScrollStartDistance
{ {
get => GetValue(ScrollStartDistanceProperty); get => _scrollStartDistance;
set => SetValue(ScrollStartDistanceProperty, value); set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
} }
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{ {

2
src/Avalonia.Base/StyledElement.cs

@ -891,7 +891,7 @@ namespace Avalonia
for (var i = 0; i < logicalChildrenCount; i++) 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); child.OnAttachedToLogicalTreeCore(e);
} }

2
src/Avalonia.Base/Visual.cs

@ -487,7 +487,7 @@ namespace Avalonia
for (var i = 0; i < visualChildrenCount; i++) 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); child.OnAttachedToVisualTreeCore(e);
} }

139
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -6,6 +6,7 @@ using Avalonia.Input;
using Avalonia.Input.GestureRecognizers; using Avalonia.Input.GestureRecognizers;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using System.Linq;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
@ -19,44 +20,34 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property. /// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollContentPresenter, bool> CanHorizontallyScrollProperty = public static readonly StyledProperty<bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollContentPresenter, bool>( AvaloniaProperty.Register<ScrollContentPresenter, bool>(nameof(CanHorizontallyScroll));
nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll,
(o, v) => o.CanHorizontallyScroll = v);
/// <summary> /// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property. /// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollContentPresenter, bool> CanVerticallyScrollProperty = public static readonly StyledProperty<bool> CanVerticallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollContentPresenter, bool>( AvaloniaProperty.Register<ScrollContentPresenter, bool>(nameof(CanVerticallyScroll));
nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll,
(o, v) => o.CanVerticallyScroll = v);
/// <summary> /// <summary>
/// Defines the <see cref="Extent"/> property. /// Defines the <see cref="Extent"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollContentPresenter, Size> ExtentProperty = public static readonly DirectProperty<ScrollContentPresenter, Size> ExtentProperty =
ScrollViewer.ExtentProperty.AddOwner<ScrollContentPresenter>( ScrollViewer.ExtentProperty.AddOwner<ScrollContentPresenter>(
o => o.Extent, o => o.Extent);
(o, v) => o.Extent = v);
/// <summary> /// <summary>
/// Defines the <see cref="Offset"/> property. /// Defines the <see cref="Offset"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollContentPresenter, Vector> OffsetProperty = public static readonly StyledProperty<Vector> OffsetProperty =
ScrollViewer.OffsetProperty.AddOwner<ScrollContentPresenter>( ScrollViewer.OffsetProperty.AddOwner<ScrollContentPresenter>(new(coerce: ScrollViewer.CoerceOffset));
o => o.Offset,
(o, v) => o.Offset = v);
/// <summary> /// <summary>
/// Defines the <see cref="Viewport"/> property. /// Defines the <see cref="Viewport"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollContentPresenter, Size> ViewportProperty = public static readonly DirectProperty<ScrollContentPresenter, Size> ViewportProperty =
ScrollViewer.ViewportProperty.AddOwner<ScrollContentPresenter>( ScrollViewer.ViewportProperty.AddOwner<ScrollContentPresenter>(
o => o.Viewport, o => o.Viewport);
(o, v) => o.Viewport = v);
/// <summary> /// <summary>
/// Defines the <see cref="HorizontalSnapPointsType"/> property. /// Defines the <see cref="HorizontalSnapPointsType"/> property.
@ -88,11 +79,8 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<bool> IsScrollChainingEnabledProperty = public static readonly StyledProperty<bool> IsScrollChainingEnabledProperty =
ScrollViewer.IsScrollChainingEnabledProperty.AddOwner<ScrollContentPresenter>(); ScrollViewer.IsScrollChainingEnabledProperty.AddOwner<ScrollContentPresenter>();
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private bool _arranging; private bool _arranging;
private Size _extent; private Size _extent;
private Vector _offset;
private IDisposable? _logicalScrollSubscription; private IDisposable? _logicalScrollSubscription;
private Size _viewport; private Size _viewport;
private Dictionary<int, Vector>? _activeLogicalGestureScrolls; private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
@ -109,6 +97,8 @@ namespace Avalonia.Controls.Presenters
private double _verticalSnapPoint; private double _verticalSnapPoint;
private double _verticalSnapPointOffset; private double _verticalSnapPointOffset;
private double _horizontalSnapPointOffset; private double _horizontalSnapPointOffset;
private CompositeDisposable? _ownerSubscriptions;
private ScrollViewer? _owner;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class. /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -116,7 +106,6 @@ namespace Avalonia.Controls.Presenters
static ScrollContentPresenter() static ScrollContentPresenter()
{ {
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>((x, e) => x.ChildChanged(e));
} }
/// <summary> /// <summary>
@ -137,8 +126,8 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
public bool CanHorizontallyScroll public bool CanHorizontallyScroll
{ {
get { return _canHorizontallyScroll; } get => GetValue(CanHorizontallyScrollProperty);
set { SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); } set => SetValue(CanHorizontallyScrollProperty, value);
} }
/// <summary> /// <summary>
@ -146,8 +135,8 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
public bool CanVerticallyScroll public bool CanVerticallyScroll
{ {
get { return _canVerticallyScroll; } get => GetValue(CanVerticallyScrollProperty);
set { SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } set => SetValue(CanVerticallyScrollProperty, value);
} }
/// <summary> /// <summary>
@ -164,8 +153,8 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
public Vector Offset public Vector Offset
{ {
get { return _offset; } get => GetValue(OffsetProperty);
set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); } set => SetValue(OffsetProperty, value);
} }
/// <summary> /// <summary>
@ -295,12 +284,60 @@ namespace Avalonia.Controls.Presenters
if (result) if (result)
{ {
Offset = offset; SetCurrentValue(OffsetProperty, offset);
} }
return result; return result;
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AttachToScrollViewer();
}
/// <summary>
/// Locates the first <see cref="ScrollViewer"/> ancestor and binds to it. Properties which have been set through other means are not bound.
/// </summary>
/// <remarks>
/// This method is automatically called when the control is attached to a visual tree.
/// </remarks>
protected internal virtual void AttachToScrollViewer()
{
var owner = this.FindAncestorOfType<ScrollViewer>();
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<IDisposable>().ToArray();
_owner = owner;
_ownerSubscriptions = new CompositeDisposable(subscriptionDisposables);
static bool NotDisabled(ScrollBarVisibility v) => v != ScrollBarVisibility.Disabled;
IDisposable? IfUnset<T>(T property, Func<T, IDisposable> func) where T : AvaloniaProperty => IsSet(property) ? null : func(property);
}
/// <inheritdoc/> /// <inheritdoc/>
void IScrollAnchorProvider.RegisterAnchorCandidate(Control element) void IScrollAnchorProvider.RegisterAnchorCandidate(Control element)
{ {
@ -410,7 +447,7 @@ namespace Avalonia.Controls.Presenters
try try
{ {
_arranging = true; _arranging = true;
Offset = newOffset; SetCurrentValue(OffsetProperty, newOffset);
} }
finally finally
{ {
@ -427,7 +464,6 @@ namespace Avalonia.Controls.Presenters
Viewport = finalSize; Viewport = finalSize;
Extent = Child!.Bounds.Size.Inflate(Child.Margin); Extent = Child!.Bounds.Size.Inflate(Child.Margin);
Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset);
_isAnchorElementDirty = true; _isAnchorElementDirty = true;
return finalSize; return finalSize;
@ -516,7 +552,7 @@ namespace Avalonia.Controls.Presenters
} }
bool offsetChanged = newOffset != Offset; bool offsetChanged = newOffset != Offset;
Offset = newOffset; SetCurrentValue(OffsetProperty, newOffset);
e.Handled = !IsScrollChainingEnabled || offsetChanged; e.Handled = !IsScrollChainingEnabled || offsetChanged;
@ -529,7 +565,7 @@ namespace Avalonia.Controls.Presenters
_activeLogicalGestureScrolls?.Remove(e.Id); _activeLogicalGestureScrolls?.Remove(e.Id);
_scrollGestureSnapPoints?.Remove(e.Id); _scrollGestureSnapPoints?.Remove(e.Id);
Offset = SnapOffset(Offset); SetCurrentValue(OffsetProperty, SnapOffset(Offset));
} }
private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e) private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e)
@ -623,7 +659,7 @@ namespace Avalonia.Controls.Presenters
Vector newOffset = SnapOffset(new Vector(x, y)); Vector newOffset = SnapOffset(new Vector(x, y));
bool offsetChanged = newOffset != Offset; bool offsetChanged = newOffset != Offset;
Offset = newOffset; SetCurrentValue(OffsetProperty, newOffset);
e.Handled = !IsScrollChainingEnabled || offsetChanged; e.Handled = !IsScrollChainingEnabled || offsetChanged;
} }
@ -631,9 +667,14 @@ namespace Avalonia.Controls.Presenters
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (change.Property == OffsetProperty && !_arranging) if (change.Property == OffsetProperty)
{ {
InvalidateArrange(); if (!_arranging)
{
InvalidateArrange();
}
_owner?.SetCurrentValue(OffsetProperty, change.GetNewValue<Vector>());
} }
else if (change.Property == ContentProperty) else if (change.Property == ContentProperty)
{ {
@ -651,11 +692,31 @@ namespace Avalonia.Controls.Presenters
UpdateSnapPoints(); UpdateSnapPoints();
} }
else if (change.Property == ChildProperty)
{
ChildChanged(change);
}
else if (change.Property == HorizontalSnapPointsAlignmentProperty || else if (change.Property == HorizontalSnapPointsAlignmentProperty ||
change.Property == VerticalSnapPointsAlignmentProperty) change.Property == VerticalSnapPointsAlignmentProperty)
{ {
UpdateSnapPoints(); UpdateSnapPoints();
} }
else if (change.Property == ExtentProperty)
{
if (_owner != null)
{
_owner.Extent = change.GetNewValue<Size>();
}
CoerceValue(OffsetProperty);
}
else if (change.Property == ViewportProperty)
{
if (_owner != null)
{
_owner.Viewport = change.GetNewValue<Size>();
}
CoerceValue(OffsetProperty);
}
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
} }
@ -677,7 +738,7 @@ namespace Avalonia.Controls.Presenters
if (e.OldValue != null) if (e.OldValue != null)
{ {
Offset = default; SetCurrentValue(OffsetProperty, default);
} }
} }
@ -719,14 +780,14 @@ namespace Avalonia.Controls.Presenters
if (logicalScroll != scrollable.IsLogicalScrollEnabled) if (logicalScroll != scrollable.IsLogicalScrollEnabled)
{ {
UpdateScrollableSubscription(Child); UpdateScrollableSubscription(Child);
Offset = default; SetCurrentValue(OffsetProperty, default);
InvalidateMeasure(); InvalidateMeasure();
} }
else if (scrollable.IsLogicalScrollEnabled) else if (scrollable.IsLogicalScrollEnabled)
{ {
Viewport = scrollable.Viewport; Viewport = scrollable.Viewport;
Extent = scrollable.Extent; Extent = scrollable.Extent;
Offset = scrollable.Offset; SetCurrentValue(OffsetProperty, scrollable.Offset);
} }
} }

165
src/Avalonia.Controls/Primitives/RangeBase.cs

@ -12,30 +12,22 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="Minimum"/> property. /// Defines the <see cref="Minimum"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<RangeBase, double> MinimumProperty = public static readonly StyledProperty<double> MinimumProperty =
AvaloniaProperty.RegisterDirect<RangeBase, double>( AvaloniaProperty.Register<RangeBase, double>(nameof(Minimum), coerce: CoerceMinimum);
nameof(Minimum),
o => o.Minimum,
(o, v) => o.Minimum = v);
/// <summary> /// <summary>
/// Defines the <see cref="Maximum"/> property. /// Defines the <see cref="Maximum"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<RangeBase, double> MaximumProperty = public static readonly StyledProperty<double> MaximumProperty =
AvaloniaProperty.RegisterDirect<RangeBase, double>( AvaloniaProperty.Register<RangeBase, double>(nameof(Maximum), 100, coerce: CoerceMaximum);
nameof(Maximum),
o => o.Maximum,
(o, v) => o.Maximum = v);
/// <summary> /// <summary>
/// Defines the <see cref="Value"/> property. /// Defines the <see cref="Value"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<RangeBase, double> ValueProperty = public static readonly StyledProperty<double> ValueProperty =
AvaloniaProperty.RegisterDirect<RangeBase, double>( AvaloniaProperty.Register<RangeBase, double>(nameof(Value),
nameof(Value), defaultBindingMode: BindingMode.TwoWay,
o => o.Value, coerce: CoerceValue);
(o, v) => o.Value = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary> /// <summary>
/// Defines the <see cref="SmallChange"/> property. /// Defines the <see cref="SmallChange"/> property.
@ -49,44 +41,26 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<double> LargeChangeProperty = public static readonly StyledProperty<double> LargeChangeProperty =
AvaloniaProperty.Register<RangeBase, double>(nameof(LargeChange), 10); AvaloniaProperty.Register<RangeBase, double>(nameof(LargeChange), 10);
private double _minimum;
private double _maximum = 100.0;
private double _value;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RangeBase"/> class. /// Gets or sets the minimum value.
/// </summary> /// </summary>
public RangeBase() public double Minimum
{ {
get => GetValue(MinimumProperty);
set => SetValue(MinimumProperty, value);
} }
/// <summary> private static double CoerceMinimum(AvaloniaObject sender, double value)
/// Gets or sets the minimum value.
/// </summary>
public double Minimum
{ {
get return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty);
{ }
return _minimum;
}
set private void OnMinimumChanged()
{
if (IsInitialized)
{ {
if (!ValidateDouble(value)) CoerceValue(MaximumProperty);
{ CoerceValue(ValueProperty);
return;
}
if (IsInitialized)
{
SetAndRaise(MinimumProperty, ref _minimum, value);
Maximum = ValidateMaximum(Maximum);
Value = ValidateValue(Value);
}
else
{
SetAndRaise(MinimumProperty, ref _minimum, value);
}
} }
} }
@ -95,28 +69,22 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public double Maximum public double Maximum
{ {
get get => GetValue(MaximumProperty);
{ set => SetValue(MaximumProperty, value);
return _maximum; }
}
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)) CoerceValue(ValueProperty);
{
return;
}
if (IsInitialized)
{
value = ValidateMaximum(value);
SetAndRaise(MaximumProperty, ref _maximum, value);
Value = ValidateValue(Value);
}
else
{
SetAndRaise(MaximumProperty, ref _maximum, value);
}
} }
} }
@ -125,28 +93,15 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public double Value public double Value
{ {
get get => GetValue(ValueProperty);
{ set => SetValue(ValueProperty, value);
return _value; }
}
set private static double CoerceValue(AvaloniaObject sender, double value)
{ {
if (!ValidateDouble(value)) return ValidateDouble(value)
{ ? MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty))
return; : sender.GetValue(ValueProperty);
}
if (IsInitialized)
{
value = ValidateValue(value);
SetAndRaise(ValueProperty, ref _value, value);
}
else
{
SetAndRaise(ValueProperty, ref _value, value);
}
}
} }
public double SmallChange public double SmallChange
@ -165,37 +120,31 @@ namespace Avalonia.Controls.Primitives
{ {
base.OnInitialized(); base.OnInitialized();
Maximum = ValidateMaximum(Maximum); CoerceValue(MaximumProperty);
Value = ValidateValue(Value); CoerceValue(ValueProperty);
} }
/// <summary> protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
/// Checks if the double value is not infinity nor NaN.
/// </summary>
/// <param name="value">The value.</param>
private static bool ValidateDouble(double value)
{ {
return !double.IsInfinity(value) && !double.IsNaN(value); base.OnPropertyChanged(change);
}
/// <summary> if (change.Property == MinimumProperty)
/// Validates/coerces the <see cref="Maximum"/> property. {
/// </summary> OnMinimumChanged();
/// <param name="value">The value.</param> }
/// <returns>The coerced value.</returns> else if (change.Property == MaximumProperty)
private double ValidateMaximum(double value) {
{ OnMaximumChanged();
return Math.Max(value, Minimum); }
} }
/// <summary> /// <summary>
/// Validates/coerces the <see cref="Value"/> property. /// Checks if the double value is not infinity nor NaN.
/// </summary> /// </summary>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <returns>The coerced value.</returns> private static bool ValidateDouble(double value)
private double ValidateValue(double value)
{ {
return MathUtilities.Clamp(value, Minimum, Maximum); return !double.IsInfinity(value) && !double.IsNaN(value);
} }
} }
} }

79
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -6,6 +6,9 @@ using Avalonia.Layout;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.VisualTree;
using Avalonia.Reactive;
using System.Linq;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -80,6 +83,8 @@ namespace Avalonia.Controls.Primitives
private Button? _pageDownButton; private Button? _pageDownButton;
private DispatcherTimer? _timer; private DispatcherTimer? _timer;
private bool _isExpanded; private bool _isExpanded;
private CompositeDisposable? _ownerSubscriptions;
private ScrollViewer? _owner;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ScrollBar"/> class. /// Initializes static members of the <see cref="ScrollBar"/> class.
@ -88,6 +93,8 @@ namespace Avalonia.Controls.Primitives
{ {
Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble);
FocusableProperty.OverrideMetadata<ScrollBar>(new(false));
} }
/// <summary> /// <summary>
@ -178,9 +185,62 @@ namespace Avalonia.Controls.Primitives
_ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.") _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.")
}; };
SetValue(IsVisibleProperty, isVisible); SetCurrentValue(IsVisibleProperty, isVisible);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AttachToScrollViewer();
}
/// <summary>
/// Locates the first <see cref="ScrollViewer"/> ancestor and binds to its properties. Properties which have been set through other means are not bound.
/// </summary>
/// <remarks>
/// This method is automatically called when the control is attached to a visual tree.
/// </remarks>
protected internal virtual void AttachToScrollViewer()
{
var owner = this.FindAncestorOfType<ScrollViewer>();
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<IDisposable>().ToArray();
_owner = owner;
_ownerSubscriptions = new CompositeDisposable(subscriptionDisposables);
IDisposable? IfUnset<T>(T property, Func<T, IDisposable> 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) protected override void OnKeyDown(KeyEventArgs e)
{ {
if (e.Key == Key.PageUp) if (e.Key == Key.PageUp)
@ -202,11 +262,20 @@ namespace Avalonia.Controls.Primitives
if (change.Property == OrientationProperty) if (change.Property == OrientationProperty)
{ {
UpdatePseudoClasses(change.GetNewValue<Orientation>()); UpdatePseudoClasses(change.GetNewValue<Orientation>());
if (IsAttachedToVisualTree)
{
AttachToScrollViewer(); // there's no way to manually refresh bindings, so reapply them
}
} }
else if (change.Property == AllowAutoHideProperty) else if (change.Property == AllowAutoHideProperty)
{ {
UpdateIsExpandedState(); UpdateIsExpandedState();
} }
else if (change.Property == ValueProperty)
{
var value = change.GetNewValue<double>();
_owner?.SetCurrentValue(ScrollViewer.OffsetProperty, Orientation == Orientation.Horizontal ? _owner.Offset.WithX(value) : _owner.Offset.WithY(value));
}
else else
{ {
if (change.Property == MinimumProperty || if (change.Property == MinimumProperty ||
@ -373,25 +442,25 @@ namespace Avalonia.Controls.Primitives
private void SmallDecrement() private void SmallDecrement()
{ {
Value = Math.Max(Value - SmallChange, Minimum); SetCurrentValue(ValueProperty, Math.Max(Value - SmallChange, Minimum));
OnScroll(ScrollEventType.SmallDecrement); OnScroll(ScrollEventType.SmallDecrement);
} }
private void SmallIncrement() private void SmallIncrement()
{ {
Value = Math.Min(Value + SmallChange, Maximum); SetCurrentValue(ValueProperty, Math.Min(Value + SmallChange, Maximum));
OnScroll(ScrollEventType.SmallIncrement); OnScroll(ScrollEventType.SmallIncrement);
} }
private void LargeDecrement() private void LargeDecrement()
{ {
Value = Math.Max(Value - LargeChange, Minimum); SetCurrentValue(ValueProperty, Math.Max(Value - LargeChange, Minimum));
OnScroll(ScrollEventType.LargeDecrement); OnScroll(ScrollEventType.LargeDecrement);
} }
private void LargeIncrement() private void LargeIncrement()
{ {
Value = Math.Min(Value + LargeChange, Maximum); SetCurrentValue(ValueProperty, Math.Min(Value + LargeChange, Maximum));
OnScroll(ScrollEventType.LargeIncrement); OnScroll(ScrollEventType.LargeIncrement);
} }

34
src/Avalonia.Controls/Primitives/Track.cs

@ -15,14 +15,14 @@ namespace Avalonia.Controls.Primitives
[PseudoClasses(":vertical", ":horizontal")] [PseudoClasses(":vertical", ":horizontal")]
public class Track : Control public class Track : Control
{ {
public static readonly DirectProperty<Track, double> MinimumProperty = public static readonly StyledProperty<double> MinimumProperty =
RangeBase.MinimumProperty.AddOwner<Track>(o => o.Minimum, (o, v) => o.Minimum = v); RangeBase.MinimumProperty.AddOwner<Track>();
public static readonly DirectProperty<Track, double> MaximumProperty = public static readonly StyledProperty<double> MaximumProperty =
RangeBase.MaximumProperty.AddOwner<Track>(o => o.Maximum, (o, v) => o.Maximum = v); RangeBase.MaximumProperty.AddOwner<Track>();
public static readonly DirectProperty<Track, double> ValueProperty = public static readonly StyledProperty<double> ValueProperty =
RangeBase.ValueProperty.AddOwner<Track>(o => o.Value, (o, v) => o.Value = v); RangeBase.ValueProperty.AddOwner<Track>();
public static readonly StyledProperty<double> ViewportSizeProperty = public static readonly StyledProperty<double> ViewportSizeProperty =
ScrollBar.ViewportSizeProperty.AddOwner<Track>(); ScrollBar.ViewportSizeProperty.AddOwner<Track>();
@ -45,10 +45,6 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<bool> IgnoreThumbDragProperty = public static readonly StyledProperty<bool> IgnoreThumbDragProperty =
AvaloniaProperty.Register<Track, bool>(nameof(IgnoreThumbDrag)); AvaloniaProperty.Register<Track, bool>(nameof(IgnoreThumbDrag));
private double _minimum;
private double _maximum = 100.0;
private double _value;
static Track() static Track()
{ {
ThumbProperty.Changed.AddClassHandler<Track>((x, e) => x.ThumbChanged(e)); ThumbProperty.Changed.AddClassHandler<Track>((x, e) => x.ThumbChanged(e));
@ -64,20 +60,20 @@ namespace Avalonia.Controls.Primitives
public double Minimum public double Minimum
{ {
get { return _minimum; } get => GetValue(MinimumProperty);
set { SetAndRaise(MinimumProperty, ref _minimum, value); } set => SetValue(MinimumProperty, value);
} }
public double Maximum public double Maximum
{ {
get { return _maximum; } get => GetValue(MaximumProperty);
set { SetAndRaise(MaximumProperty, ref _maximum, value); } set => SetValue(MaximumProperty, value);
} }
public double Value public double Value
{ {
get { return _value; } get => GetValue(ValueProperty);
set { SetAndRaise(ValueProperty, ref _value, value); } set => SetValue(ValueProperty, value);
} }
public double ViewportSize public double ViewportSize
@ -443,11 +439,11 @@ namespace Avalonia.Controls.Primitives
{ {
if (IgnoreThumbDrag) if (IgnoreThumbDrag)
return; return;
Value = MathUtilities.Clamp( SetCurrentValue(ValueProperty, MathUtilities.Clamp(
Value + ValueFromDistance(e.Vector.X, e.Vector.Y), Value + ValueFromDistance(e.Vector.X, e.Vector.Y),
Minimum, Minimum,
Maximum); Maximum));
} }
private void ShowChildren(bool visible) private void ShowChildren(bool visible)

30
src/Avalonia.Controls/ProgressBar.cs

@ -100,8 +100,6 @@ namespace Avalonia.Controls
} }
private double _percentage; private double _percentage;
private double _indeterminateStartingOffset;
private double _indeterminateEndingOffset;
private Border? _indicator; private Border? _indicator;
private IDisposable? _trackSizeChangedListener; private IDisposable? _trackSizeChangedListener;
@ -122,17 +120,11 @@ namespace Avalonia.Controls
nameof(Percentage), nameof(Percentage),
o => o.Percentage); o => o.Percentage);
public static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty = public static readonly StyledProperty<double> IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>( AvaloniaProperty.Register<ProgressBar, double>(nameof(IndeterminateStartingOffset));
nameof(IndeterminateStartingOffset),
p => p.IndeterminateStartingOffset,
(p, o) => p.IndeterminateStartingOffset = o);
public static readonly DirectProperty<ProgressBar, double> IndeterminateEndingOffsetProperty = public static readonly StyledProperty<double> IndeterminateEndingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>( AvaloniaProperty.Register<ProgressBar, double>(nameof(IndeterminateEndingOffset));
nameof(IndeterminateEndingOffset),
p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o);
public double Percentage public double Percentage
{ {
@ -142,19 +134,19 @@ namespace Avalonia.Controls
public double IndeterminateStartingOffset public double IndeterminateStartingOffset
{ {
get => _indeterminateStartingOffset; get => GetValue(IndeterminateStartingOffsetProperty);
set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); set => SetValue(IndeterminateStartingOffsetProperty, value);
} }
public double IndeterminateEndingOffset public double IndeterminateEndingOffset
{ {
get => _indeterminateEndingOffset; get => GetValue(IndeterminateEndingOffsetProperty);
set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); set => SetValue(IndeterminateEndingOffsetProperty, value);
} }
static ProgressBar() static ProgressBar()
{ {
ValueProperty.OverrideMetadata<ProgressBar>(new DirectPropertyMetadata<double>(defaultBindingMode: BindingMode.OneWay)); ValueProperty.OverrideMetadata<ProgressBar>(new(defaultBindingMode: BindingMode.OneWay));
ValueProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e)); ValueProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MinimumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MinimumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MaximumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MaximumProperty.Changed.AddClassHandler<ProgressBar>((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. // Remove these properties when we switch to fluent as default and removed the old one.
IndeterminateStartingOffset = -dim; SetCurrentValue(IndeterminateStartingOffsetProperty,-dim);
IndeterminateEndingOffset = dim; SetCurrentValue(IndeterminateEndingOffsetProperty,dim);
var padding = Padding; var padding = Padding;
var rectangle = new RectangleGeometry( var rectangle = new RectangleGeometry(

318
src/Avalonia.Controls/ScrollViewer.cs

@ -16,54 +16,25 @@ namespace Avalonia.Controls
[TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))] [TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))]
public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{ {
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
/// <remarks>
/// There is no public C# accessor for this property as it is intended to be bound to by a
/// <see cref="ScrollContentPresenter"/> in the control's template.
/// </remarks>
public static readonly DirectProperty<ScrollViewer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, bool>(
nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll);
/// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary>
/// <remarks>
/// There is no public C# accessor for this property as it is intended to be bound to by a
/// <see cref="ScrollContentPresenter"/> in the control's template.
/// </remarks>
public static readonly DirectProperty<ScrollViewer, bool> CanVerticallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, bool>(
nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll);
/// <summary> /// <summary>
/// Defines the <see cref="Extent"/> property. /// Defines the <see cref="Extent"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollViewer, Size> ExtentProperty = public static readonly DirectProperty<ScrollViewer, Size> ExtentProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, Size>(nameof(Extent), AvaloniaProperty.RegisterDirect<ScrollViewer, Size>(nameof(Extent),
o => o.Extent, o => o.Extent);
(o, v) => o.Extent = v);
/// <summary> /// <summary>
/// Defines the <see cref="Offset"/> property. /// Defines the <see cref="Offset"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollViewer, Vector> OffsetProperty = public static readonly StyledProperty<Vector> OffsetProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, Vector>( AvaloniaProperty.Register<ScrollViewer, Vector>(nameof(Offset), coerce: CoerceOffset);
nameof(Offset),
o => o.Offset,
(o, v) => o.Offset = v);
/// <summary> /// <summary>
/// Defines the <see cref="Viewport"/> property. /// Defines the <see cref="Viewport"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollViewer, Size> ViewportProperty = public static readonly DirectProperty<ScrollViewer, Size> ViewportProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, Size>(nameof(Viewport), AvaloniaProperty.RegisterDirect<ScrollViewer, Size>(nameof(Viewport),
o => o.Viewport, o => o.Viewport);
(o, v) => o.Viewport = v);
/// <summary> /// <summary>
/// Defines the <see cref="LargeChange"/> property. /// Defines the <see cref="LargeChange"/> property.
@ -82,41 +53,12 @@ namespace Avalonia.Controls
o => o.SmallChange); o => o.SmallChange);
/// <summary> /// <summary>
/// Defines the HorizontalScrollBarMaximum property. /// Defines the <see cref="ScrollBarMaximum"/> property.
/// </summary>
/// <remarks>
/// There is no public C# accessor for this property as it is intended to be bound to by a
/// <see cref="ScrollContentPresenter"/> in the control's template.
/// </remarks>
public static readonly DirectProperty<ScrollViewer, double> HorizontalScrollBarMaximumProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, double>(
nameof(HorizontalScrollBarMaximum),
o => o.HorizontalScrollBarMaximum);
/// <summary>
/// Defines the HorizontalScrollBarValue property.
/// </summary> /// </summary>
/// <remarks> public static readonly DirectProperty<ScrollViewer, Vector> ScrollBarMaximumProperty =
/// There is no public C# accessor for this property as it is intended to be bound to by a AvaloniaProperty.RegisterDirect<ScrollViewer, Vector>(
/// <see cref="ScrollContentPresenter"/> in the control's template. nameof(ScrollBarMaximum),
/// </remarks> o => o.ScrollBarMaximum);
public static readonly DirectProperty<ScrollViewer, double> HorizontalScrollBarValueProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, double>(
nameof(HorizontalScrollBarValue),
o => o.HorizontalScrollBarValue,
(o, v) => o.HorizontalScrollBarValue = v);
/// <summary>
/// Defines the HorizontalScrollBarViewportSize property.
/// </summary>
/// <remarks>
/// There is no public C# accessor for this property as it is intended to be bound to by a
/// <see cref="ScrollContentPresenter"/> in the control's template.
/// </remarks>
public static readonly DirectProperty<ScrollViewer, double> HorizontalScrollBarViewportSizeProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, double>(
nameof(HorizontalScrollBarViewportSize),
o => o.HorizontalScrollBarViewportSize);
/// <summary> /// <summary>
/// Defines the <see cref="HorizontalScrollBarVisibility"/> property. /// Defines the <see cref="HorizontalScrollBarVisibility"/> property.
@ -126,31 +68,6 @@ namespace Avalonia.Controls
nameof(HorizontalScrollBarVisibility), nameof(HorizontalScrollBarVisibility),
ScrollBarVisibility.Disabled); ScrollBarVisibility.Disabled);
/// <summary>
/// Defines the VerticalScrollBarMaximum property.
/// </summary>
/// <remarks>
/// There is no public C# accessor for this property as it is intended to be bound to by a
/// <see cref="ScrollContentPresenter"/> in the control's template.
/// </remarks>
public static readonly DirectProperty<ScrollViewer, double> VerticalScrollBarMaximumProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, double>(
nameof(VerticalScrollBarMaximum),
o => o.VerticalScrollBarMaximum);
/// <summary>
/// Defines the VerticalScrollBarValue property.
/// </summary>
/// <remarks>
/// There is no public C# accessor for this property as it is intended to be bound to by a
/// <see cref="ScrollContentPresenter"/> in the control's template.
/// </remarks>
public static readonly DirectProperty<ScrollViewer, double> VerticalScrollBarValueProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, double>(
nameof(VerticalScrollBarValue),
o => o.VerticalScrollBarValue,
(o, v) => o.VerticalScrollBarValue = v);
/// <summary> /// <summary>
/// Defines the <see cref="HorizontalSnapPointsType"/> property. /// Defines the <see cref="HorizontalSnapPointsType"/> property.
/// </summary> /// </summary>
@ -179,18 +96,6 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, SnapPointsAlignment>( AvaloniaProperty.RegisterAttached<ScrollViewer, Control, SnapPointsAlignment>(
nameof(VerticalSnapPointsAlignment)); nameof(VerticalSnapPointsAlignment));
/// <summary>
/// Defines the VerticalScrollBarViewportSize property.
/// </summary>
/// <remarks>
/// There is no public C# accessor for this property as it is intended to be bound to by a
/// <see cref="ScrollContentPresenter"/> in the control's template.
/// </remarks>
public static readonly DirectProperty<ScrollViewer, double> VerticalScrollBarViewportSizeProperty =
AvaloniaProperty.RegisterDirect<ScrollViewer, double>(
nameof(VerticalScrollBarViewportSize),
o => o.VerticalScrollBarViewportSize);
/// <summary> /// <summary>
/// Defines the <see cref="VerticalScrollBarVisibility"/> property. /// Defines the <see cref="VerticalScrollBarVisibility"/> property.
/// </summary> /// </summary>
@ -242,25 +147,16 @@ namespace Avalonia.Controls
private IDisposable? _childSubscription; private IDisposable? _childSubscription;
private ILogicalScrollable? _logicalScrollable; private ILogicalScrollable? _logicalScrollable;
private Size _extent; private Size _extent;
private Vector _offset;
private Size _viewport; private Size _viewport;
private Size _oldExtent; private Size _oldExtent;
private Vector _oldOffset; private Vector _oldOffset;
private Vector _oldMaximum;
private Size _oldViewport; private Size _oldViewport;
private Size _largeChange; private Size _largeChange;
private Size _smallChange = new Size(DefaultSmallChange, DefaultSmallChange); private Size _smallChange = new Size(DefaultSmallChange, DefaultSmallChange);
private bool _isExpanded; private bool _isExpanded;
private IDisposable? _scrollBarExpandSubscription; private IDisposable? _scrollBarExpandSubscription;
/// <summary>
/// Initializes static members of the <see cref="ScrollViewer"/> class.
/// </summary>
static ScrollViewer()
{
HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer, ScrollBarVisibility>((x, e) => x.ScrollBarVisibilityChanged(e));
VerticalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer, ScrollBarVisibility>((x, e) => x.ScrollBarVisibilityChanged(e));
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ScrollViewer"/> class. /// Initializes a new instance of the <see cref="ScrollViewer"/> class.
/// </summary> /// </summary>
@ -288,7 +184,7 @@ namespace Avalonia.Controls
return _extent; return _extent;
} }
private set internal set
{ {
if (SetAndRaise(ExtentProperty, ref _extent, value)) if (SetAndRaise(ExtentProperty, ref _extent, value))
{ {
@ -302,18 +198,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public Vector Offset public Vector Offset
{ {
get get => GetValue(OffsetProperty);
{ set => SetValue(OffsetProperty, value);
return _offset;
}
set
{
if (SetAndRaise(OffsetProperty, ref _offset, CoerceOffset(Extent, Viewport, value)))
{
CalculatedPropertiesChanged();
}
}
} }
/// <summary> /// <summary>
@ -326,7 +212,7 @@ namespace Avalonia.Controls
return _viewport; return _viewport;
} }
private set internal set
{ {
if (SetAndRaise(ViewportProperty, ref _viewport, value)) if (SetAndRaise(ViewportProperty, ref _viewport, value))
{ {
@ -383,70 +269,9 @@ namespace Avalonia.Controls
public Control? CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor; public Control? CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor;
/// <summary> /// <summary>
/// Gets the maximum horizontal scrollbar value. /// Gets the maximum scrolling distance (which is <see cref="Extent"/> - <see cref="Viewport"/>).
/// </summary>
protected double HorizontalScrollBarMaximum
{
get { return Max(_extent.Width - _viewport.Width, 0); }
}
/// <summary>
/// Gets or sets the horizontal scrollbar value.
/// </summary> /// </summary>
protected double HorizontalScrollBarValue public Vector ScrollBarMaximum => new(Max(_extent.Width - _viewport.Width, 0), Max(_extent.Height - _viewport.Height, 0));
{
get { return _offset.X; }
set
{
if (_offset.X != value)
{
var old = Offset.X;
Offset = Offset.WithX(value);
RaisePropertyChanged(HorizontalScrollBarValueProperty, old, value);
}
}
}
/// <summary>
/// Gets the size of the horizontal scrollbar viewport.
/// </summary>
protected double HorizontalScrollBarViewportSize
{
get { return _viewport.Width; }
}
/// <summary>
/// Gets the maximum vertical scrollbar value.
/// </summary>
protected double VerticalScrollBarMaximum
{
get { return Max(_extent.Height - _viewport.Height, 0); }
}
/// <summary>
/// Gets or sets the vertical scrollbar value.
/// </summary>
protected double VerticalScrollBarValue
{
get { return _offset.Y; }
set
{
if (_offset.Y != value)
{
var old = Offset.Y;
Offset = Offset.WithY(value);
RaisePropertyChanged(VerticalScrollBarValueProperty, old, value);
}
}
}
/// <summary>
/// Gets the size of the vertical scrollbar viewport.
/// </summary>
protected double VerticalScrollBarViewportSize
{
get { return _viewport.Height; }
}
/// <summary> /// <summary>
/// Gets a value that indicates whether any scrollbar is expanded. /// Gets a value that indicates whether any scrollbar is expanded.
@ -528,82 +353,52 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Scrolls the content up one line. /// Scrolls the content up one line.
/// </summary> /// </summary>
public void LineUp() public void LineUp() => SetCurrentValue(OffsetProperty, Offset - new Vector(0, _smallChange.Height));
{
Offset -= new Vector(0, _smallChange.Height);
}
/// <summary> /// <summary>
/// Scrolls the content down one line. /// Scrolls the content down one line.
/// </summary> /// </summary>
public void LineDown() public void LineDown() => SetCurrentValue(OffsetProperty, Offset + new Vector(0, _smallChange.Height));
{
Offset += new Vector(0, _smallChange.Height);
}
/// <summary> /// <summary>
/// Scrolls the content left one line. /// Scrolls the content left one line.
/// </summary> /// </summary>
public void LineLeft() public void LineLeft() => SetCurrentValue(OffsetProperty, Offset - new Vector(_smallChange.Width, 0));
{
Offset -= new Vector(_smallChange.Width, 0);
}
/// <summary> /// <summary>
/// Scrolls the content right one line. /// Scrolls the content right one line.
/// </summary> /// </summary>
public void LineRight() public void LineRight() => SetCurrentValue(OffsetProperty, Offset + new Vector(_smallChange.Width, 0));
{
Offset += new Vector(_smallChange.Width, 0);
}
/// <summary> /// <summary>
/// Scrolls the content upward by one page. /// Scrolls the content upward by one page.
/// </summary> /// </summary>
public void PageUp() public void PageUp() => SetCurrentValue(OffsetProperty, Offset.WithY(Math.Max(Offset.Y - _viewport.Height, 0)));
{
VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0);
}
/// <summary> /// <summary>
/// Scrolls the content downward by one page. /// Scrolls the content downward by one page.
/// </summary> /// </summary>
public void PageDown() public void PageDown() => SetCurrentValue(OffsetProperty, Offset.WithY(Math.Min(Offset.Y + _viewport.Height, ScrollBarMaximum.Y)));
{
VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum);
}
/// <summary> /// <summary>
/// Scrolls the content left by one page. /// Scrolls the content left by one page.
/// </summary> /// </summary>
public void PageLeft() public void PageLeft() => SetCurrentValue(OffsetProperty, Offset.WithX(Math.Max(Offset.X - _viewport.Width, 0)));
{
HorizontalScrollBarValue = Math.Max(_offset.X - _viewport.Width, 0);
}
/// <summary> /// <summary>
/// Scrolls the content tight by one page. /// Scrolls the content tight by one page.
/// </summary> /// </summary>
public void PageRight() public void PageRight() => SetCurrentValue(OffsetProperty, Offset.WithX(Math.Min(Offset.X + _viewport.Width, ScrollBarMaximum.X)));
{
HorizontalScrollBarValue = Math.Min(_offset.X + _viewport.Width, HorizontalScrollBarMaximum);
}
/// <summary> /// <summary>
/// Scrolls to the top-left corner of the content. /// Scrolls to the top-left corner of the content.
/// </summary> /// </summary>
public void ScrollToHome() public void ScrollToHome() => SetCurrentValue(OffsetProperty, new Vector(double.NegativeInfinity, double.NegativeInfinity));
{
Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity);
}
/// <summary> /// <summary>
/// Scrolls to the bottom-left corner of the content. /// Scrolls to the bottom-left corner of the content.
/// </summary> /// </summary>
public void ScrollToEnd() public void ScrollToEnd() => SetCurrentValue(OffsetProperty, new Vector(double.NegativeInfinity, double.PositiveInfinity));
{
Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity);
}
/// <summary> /// <summary>
/// Gets the value of the HorizontalScrollBarVisibility attached property. /// Gets the value of the HorizontalScrollBarVisibility attached property.
@ -819,11 +614,14 @@ namespace Avalonia.Controls
return false; 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 maxX = Math.Max(extent.Width - viewport.Width, 0);
var maxY = Math.Max(extent.Height - viewport.Height, 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) private static double Clamp(double value, double min, double max)
@ -859,40 +657,14 @@ namespace Avalonia.Controls
CalculatedPropertiesChanged(); CalculatedPropertiesChanged();
} }
private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs<ScrollBarVisibility> e) private void CalculatedPropertiesChanged()
{ {
var wasEnabled = e.OldValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; var newMaximum = ScrollBarMaximum;
var isEnabled = e.NewValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; if (newMaximum != _oldMaximum)
if (wasEnabled != isEnabled)
{ {
if (e.Property == HorizontalScrollBarVisibilityProperty) RaisePropertyChanged(ScrollBarMaximumProperty, _oldMaximum, newMaximum);
{ _oldMaximum = newMaximum;
RaisePropertyChanged(
CanHorizontallyScrollProperty,
wasEnabled,
isEnabled);
}
else if (e.Property == VerticalScrollBarVisibilityProperty)
{
RaisePropertyChanged(
CanVerticallyScrollProperty,
wasEnabled,
isEnabled);
}
} }
}
private void CalculatedPropertiesChanged()
{
// Pass old values of 0 here because we don't have the old values at this point,
// and it shouldn't matter as only the template uses these properties.
RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum);
RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue);
RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize);
RaisePropertyChanged(VerticalScrollBarMaximumProperty, 0, VerticalScrollBarMaximum);
RaisePropertyChanged(VerticalScrollBarValueProperty, 0, VerticalScrollBarValue);
RaisePropertyChanged(VerticalScrollBarViewportSizeProperty, 0, VerticalScrollBarViewportSize);
if (_logicalScrollable?.IsLogicalScrollEnabled == true) 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) protected override void OnKeyDown(KeyEventArgs e)
{ {
if (e.Key == Key.PageUp) if (e.Key == Key.PageUp)

2
src/Avalonia.Controls/Slider.cs

@ -110,7 +110,7 @@ namespace Avalonia.Controls
Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e), Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e),
RoutingStrategies.Bubble); RoutingStrategies.Bubble);
ValueProperty.OverrideMetadata<Slider>(new DirectPropertyMetadata<double>(enableDataValidation: true)); ValueProperty.OverrideMetadata<Slider>(new(enableDataValidation: true));
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Slider>(AutomationControlType.Slider); AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Slider>(AutomationControlType.Slider);
} }

13
src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml

@ -75,17 +75,10 @@
Height="20" /> Height="20" />
</Viewbox> </Viewbox>
</RepeatButton> </RepeatButton>
<ScrollContentPresenter Name="PART_ContentPresenter" <ScrollContentPresenter Name="PART_ContentPresenter">
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}">
<ScrollContentPresenter.GestureRecognizers> <ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" <ScrollGestureRecognizer CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" /> CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}" />
</ScrollContentPresenter.GestureRecognizers> </ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter> </ScrollContentPresenter>
</DockPanel> </DockPanel>

35
src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml

@ -29,46 +29,23 @@
Grid.RowSpan="2" Grid.RowSpan="2"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Padding="{TemplateBinding Padding}"
HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}" HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}" VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}"
HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}" HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}" VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
Offset="{TemplateBinding Offset, Mode=TwoWay}" Padding="{TemplateBinding Padding}">
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"
IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}">
<ScrollContentPresenter.GestureRecognizers> <ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" <ScrollGestureRecognizer CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"/> IsScrollInertiaEnabled="{Binding IsScrollInertiaEnabled, RelativeSource={RelativeSource TemplatedParent}}" />
</ScrollContentPresenter.GestureRecognizers> </ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter> </ScrollContentPresenter>
<ScrollBar Name="PART_HorizontalScrollBar" <ScrollBar Name="PART_HorizontalScrollBar"
AllowAutoHide="{TemplateBinding AllowAutoHide}"
Orientation="Horizontal" Orientation="Horizontal"
LargeChange="{Binding LargeChange.Width, RelativeSource={RelativeSource TemplatedParent}}" Grid.Row="1" />
SmallChange="{Binding SmallChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}"
Visibility="{TemplateBinding HorizontalScrollBarVisibility}"
Grid.Row="1"
Focusable="False" />
<ScrollBar Name="PART_VerticalScrollBar" <ScrollBar Name="PART_VerticalScrollBar"
AllowAutoHide="{TemplateBinding AllowAutoHide}"
Orientation="Vertical" Orientation="Vertical"
LargeChange="{Binding LargeChange.Height, RelativeSource={RelativeSource TemplatedParent}}" Grid.Column="1" />
SmallChange="{Binding SmallChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding VerticalScrollBarMaximum}"
Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}"
Visibility="{TemplateBinding VerticalScrollBarVisibility}"
Grid.Column="1"
Focusable="False" />
<Panel x:Name="PART_ScrollBarsSeparator" <Panel x:Name="PART_ScrollBarsSeparator"
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"

52
src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml

@ -10,48 +10,23 @@
RowDefinitions="*,Auto"> RowDefinitions="*,Auto">
<ScrollContentPresenter Name="PART_ContentPresenter" <ScrollContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}" Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}" HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}" VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}"
HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}" HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}" VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
Extent="{TemplateBinding Extent, Background="{TemplateBinding Background}">
Mode=TwoWay}"
IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"
Viewport="{TemplateBinding Viewport,
Mode=TwoWay}"
Offset="{TemplateBinding Offset,
Mode=TwoWay}">
<ScrollContentPresenter.GestureRecognizers> <ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" <ScrollGestureRecognizer CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" /> CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
IsScrollInertiaEnabled="{Binding IsScrollInertiaEnabled, RelativeSource={RelativeSource TemplatedParent}}" />
</ScrollContentPresenter.GestureRecognizers> </ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter> </ScrollContentPresenter>
<ScrollBar Name="horizontalScrollBar" <ScrollBar Name="PART_HorizontalScrollBar"
Grid.Row="1" Grid.Row="1"
Focusable="False" Orientation="Horizontal"/>
LargeChange="{Binding LargeChange.Width, RelativeSource={RelativeSource TemplatedParent}}" <ScrollBar Name="PART_VerticalScrollBar"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
Orientation="Horizontal"
SmallChange="{Binding SmallChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}"
Visibility="{TemplateBinding HorizontalScrollBarVisibility}"
Value="{TemplateBinding HorizontalScrollBarValue,
Mode=TwoWay}" />
<ScrollBar Name="verticalScrollBar"
Grid.Column="1" Grid.Column="1"
Focusable="False" Orientation="Vertical"/>
LargeChange="{Binding LargeChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding VerticalScrollBarMaximum}"
Orientation="Vertical"
SmallChange="{Binding SmallChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}"
Visibility="{TemplateBinding VerticalScrollBarVisibility}"
Value="{TemplateBinding VerticalScrollBarValue,
Mode=TwoWay}" />
<Panel Grid.Row="1" <Panel Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Background="{DynamicResource ThemeControlMidBrush}" /> Background="{DynamicResource ThemeControlMidBrush}" />
@ -105,16 +80,7 @@
<Path Data="M 0 0 L 4 4 L 8 0 Z" /> <Path Data="M 0 0 L 4 4 L 8 0 Z" />
</RepeatButton> </RepeatButton>
<ScrollContentPresenter Name="PART_ContentPresenter" <ScrollContentPresenter Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}" />
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent,
Mode=TwoWay}"
Viewport="{TemplateBinding Viewport,
Mode=TwoWay}"
Offset="{TemplateBinding Offset,
Mode=TwoWay}" />
</DockPanel> </DockPanel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>

14
tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs

@ -384,31 +384,17 @@ namespace Avalonia.Base.UnitTests.Layout
new ScrollContentPresenter new ScrollContentPresenter
{ {
Name = "PART_ContentPresenter", 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), }.RegisterInNameScope(scope),
new ScrollBar new ScrollBar
{ {
Name = "horizontalScrollBar", Name = "horizontalScrollBar",
Orientation = Orientation.Horizontal, 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, [Grid.RowProperty] = 1,
}.RegisterInNameScope(scope), }.RegisterInNameScope(scope),
new ScrollBar new ScrollBar
{ {
Name = "verticalScrollBar", Name = "verticalScrollBar",
Orientation = Orientation.Vertical, 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, [Grid.ColumnProperty] = 1,
}.RegisterInNameScope(scope), }.RegisterInNameScope(scope),
}, },

6
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -301,12 +301,6 @@ namespace Avalonia.Controls.UnitTests
new ScrollContentPresenter new ScrollContentPresenter
{ {
Name = "PART_ContentPresenter", 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), }.RegisterInNameScope(scope),
} }
}); });

9
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -590,18 +590,11 @@ namespace Avalonia.Controls.UnitTests
new ScrollContentPresenter new ScrollContentPresenter
{ {
Name = "PART_ContentPresenter", 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), }.RegisterInNameScope(scope),
new ScrollBar new ScrollBar
{ {
Name = "verticalScrollBar", Name = "verticalScrollBar",
[~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty], Orientation = Orientation.Vertical,
[~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty],
} }
} }
}); });

6
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@ -250,13 +250,13 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, canExecuteCallCount); Assert.Equal(0, canExecuteCallCount);
flyout.ShowAt(button); flyout.ShowAt(button);
Assert.Equal(2, canExecuteCallCount); Assert.Equal(1, canExecuteCallCount);
command.RaiseCanExecuteChanged(); command.RaiseCanExecuteChanged();
Assert.Equal(3, canExecuteCallCount); Assert.Equal(2, canExecuteCallCount);
target.CommandParameter = true; target.CommandParameter = true;
Assert.Equal(4, canExecuteCallCount); Assert.Equal(3, canExecuteCallCount);
} }
} }

118
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -21,45 +21,20 @@ namespace Avalonia.Controls.UnitTests
Content = "Foo", Content = "Foo",
}; };
target.ApplyTemplate(); InitializeScrollViewer(target);
((ContentPresenter)target.Presenter).UpdateChild();
Assert.IsType<TextBlock>(target.Presenter.Child); Assert.IsType<TextBlock>(target.Presenter.Child);
} }
[Fact]
public void CanHorizontallyScroll_Should_Track_HorizontalScrollBarVisibility()
{
var target = new ScrollViewer();
var values = new List<bool>();
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<bool>();
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] [Fact]
public void Offset_Should_Be_Coerced_To_Viewport() public void Offset_Should_Be_Coerced_To_Viewport()
{ {
var target = new ScrollViewer(); var target = new ScrollViewer
target.SetValue(ScrollViewer.ExtentProperty, new Size(20, 20)); {
target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10)); Extent = new Size(20, 20),
target.Offset = new Vector(12, 12); Viewport = new Size(10, 10),
Offset = new Vector(12, 12)
};
Assert.Equal(new Vector(10, 10), target.Offset); Assert.Equal(new Vector(10, 10), target.Offset);
} }
@ -67,10 +42,12 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Test_ScrollToHome() public void Test_ScrollToHome()
{ {
var target = new ScrollViewer(); var target = new ScrollViewer
target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50)); {
target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10)); Extent = new Size(50, 50),
target.Offset = new Vector(25, 25); Viewport = new Size(10, 10),
Offset = new Vector(25, 25)
};
target.ScrollToHome(); target.ScrollToHome();
Assert.Equal(new Vector(0, 0), target.Offset); Assert.Equal(new Vector(0, 0), target.Offset);
@ -79,10 +56,12 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Test_ScrollToEnd() public void Test_ScrollToEnd()
{ {
var target = new ScrollViewer(); var target = new ScrollViewer
target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50)); {
target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10)); Extent = new Size(50, 50),
target.Offset = new Vector(25, 25); Viewport = new Size(10, 10),
Offset = new Vector(25, 25)
};
target.ScrollToEnd(); target.ScrollToEnd();
Assert.Equal(new Vector(0, 40), target.Offset); Assert.Equal(new Vector(0, 40), target.Offset);
@ -99,9 +78,10 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void LargeChange_Should_Be_Viewport() public void LargeChange_Should_Be_Viewport()
{ {
var target = new ScrollViewer(); var target = new ScrollViewer
{
target.SetValue(ScrollViewer.ViewportProperty, new Size(104, 143)); Viewport = new Size(104, 143)
};
Assert.Equal(new Size(104, 143), target.LargeChange); Assert.Equal(new Size(104, 143), target.LargeChange);
} }
@ -120,8 +100,7 @@ namespace Avalonia.Controls.UnitTests
Content = child.Object, Content = child.Object,
}; };
target.ApplyTemplate(); InitializeScrollViewer(target);
((ContentPresenter)target.Presenter).UpdateChild();
Assert.Equal(new Size(12, 43), target.SmallChange); Assert.Equal(new Size(12, 43), target.SmallChange);
} }
@ -141,8 +120,7 @@ namespace Avalonia.Controls.UnitTests
Content = child.Object, Content = child.Object,
}; };
target.ApplyTemplate(); InitializeScrollViewer(target);
((ContentPresenter)target.Presenter).UpdateChild();
Assert.Equal(new Size(45, 67), target.LargeChange); Assert.Equal(new Size(45, 67), target.LargeChange);
} }
@ -154,8 +132,8 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot(target); var root = new TestRoot(target);
var raised = 0; var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); target.Extent = new Size(100, 100);
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Viewport = new Size(50, 50);
target.Offset = new Vector(10, 10); target.Offset = new Vector(10, 10);
root.LayoutManager.ExecuteInitialLayoutPass(); root.LayoutManager.ExecuteInitialLayoutPass();
@ -168,7 +146,7 @@ namespace Avalonia.Controls.UnitTests
++raised; ++raised;
}; };
target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112)); target.Extent = new Size(111, 112);
Assert.Equal(0, raised); Assert.Equal(0, raised);
@ -184,8 +162,8 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot(target); var root = new TestRoot(target);
var raised = 0; var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); target.Extent = new Size(100, 100);
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Viewport = new Size(50, 50);
target.Offset = new Vector(10, 10); target.Offset = new Vector(10, 10);
root.LayoutManager.ExecuteInitialLayoutPass(); root.LayoutManager.ExecuteInitialLayoutPass();
@ -214,8 +192,8 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot(target); var root = new TestRoot(target);
var raised = 0; var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); target.Extent = new Size(100, 100);
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Viewport = new Size(50, 50);
target.Offset = new Vector(10, 10); target.Offset = new Vector(10, 10);
root.LayoutManager.ExecuteInitialLayoutPass(); root.LayoutManager.ExecuteInitialLayoutPass();
@ -228,7 +206,7 @@ namespace Avalonia.Controls.UnitTests
++raised; ++raised;
}; };
target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58)); target.Viewport = new Size(56, 58);
Assert.Equal(0, raised); Assert.Equal(0, raised);
@ -247,8 +225,8 @@ namespace Avalonia.Controls.UnitTests
var root = new TestRoot(target); var root = new TestRoot(target);
var raised = 0; var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); target.Extent = new (100, 100);
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Viewport = new(50, 50);
target.Offset = new Vector(50, 50); target.Offset = new Vector(50, 50);
root.LayoutManager.ExecuteInitialLayoutPass(); root.LayoutManager.ExecuteInitialLayoutPass();
@ -261,7 +239,7 @@ namespace Avalonia.Controls.UnitTests
++raised; ++raised;
}; };
target.SetValue(ScrollViewer.ExtentProperty, new Size(70, 70)); target.Extent = new(70, 70);
Assert.Equal(0, raised); Assert.Equal(0, raised);
@ -290,34 +268,32 @@ namespace Avalonia.Controls.UnitTests
new ScrollContentPresenter new ScrollContentPresenter
{ {
Name = "PART_ContentPresenter", 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), }.RegisterInNameScope(scope),
new ScrollBar new ScrollBar
{ {
Name = "horizontalScrollBar", Name = "PART_HorizontalScrollBar",
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
[~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty],
[~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty],
[~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty],
[~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty],
[Grid.RowProperty] = 1, [Grid.RowProperty] = 1,
}.RegisterInNameScope(scope), }.RegisterInNameScope(scope),
new ScrollBar new ScrollBar
{ {
Name = "verticalScrollBar", Name = "PART_VerticalScrollBar",
Orientation = Orientation.Vertical, Orientation = Orientation.Vertical,
[~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty],
[~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty],
[~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty],
[~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty],
[Grid.ColumnProperty] = 1, [Grid.ColumnProperty] = 1,
}.RegisterInNameScope(scope), }.RegisterInNameScope(scope),
}, },
}; };
} }
private static void InitializeScrollViewer(ScrollViewer target)
{
target.ApplyTemplate();
var presenter = (ScrollContentPresenter)target.Presenter;
presenter.AttachToScrollViewer();
presenter.UpdateChild();
}
} }
} }

10
tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs

@ -163,13 +163,13 @@ namespace Avalonia.Controls.UnitTests
true, true,
It.IsAny<CancellationToken>())) It.IsAny<CancellationToken>()))
.Returns(() => transitionTask.Task); .Returns(() => transitionTask.Task);
carousel.SelectedIndex = 1; carousel.SelectedIndex = 1;
Layout(target); Layout(target);
Assert.Equal(items, target.Children); Assert.Equal(items, target.Children);
Assert.All(items, x => Assert.True(x.IsVisible)); Assert.All(items, x => Assert.True(x.IsVisible));
transitionTask.SetResult(); transitionTask.SetResult();
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();
@ -255,12 +255,6 @@ namespace Avalonia.Controls.UnitTests
new ScrollContentPresenter new ScrollContentPresenter
{ {
Name = "PART_ContentPresenter", 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), }.RegisterInNameScope(scope),
} }
}); });

6
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -565,12 +565,6 @@ namespace Avalonia.Controls.UnitTests
new ScrollContentPresenter new ScrollContentPresenter
{ {
Name = "PART_ContentPresenter", 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)); }.RegisterInNameScope(ns));
} }

Loading…
Cancel
Save