Browse Source

Merge branch 'master' into fix-stackoverflow-with-redefining-resource

pull/10897/head
Max Katz 3 years ago
committed by GitHub
parent
commit
bbaaaa7726
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. 10
      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. 2
      src/Avalonia.X11/X11Window.Ime.cs
  15. 69
      src/Avalonia.X11/X11Window.cs
  16. 135
      src/iOS/Avalonia.iOS/DispatcherImpl.cs
  17. 55
      src/iOS/Avalonia.iOS/Interop.cs
  18. 3
      src/iOS/Avalonia.iOS/Platform.cs
  19. 31
      src/iOS/Avalonia.iOS/PlatformSettings.cs
  20. 38
      src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs
  21. 14
      tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs
  22. 6
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  23. 9
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  24. 6
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  25. 118
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  26. 10
      tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs
  27. 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)

10
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);
} }
@ -246,11 +246,11 @@ namespace Avalonia.Controls
break; break;
case Key.Home: case Key.Home:
Value = Minimum; SetCurrentValue(ValueProperty, Minimum);
break; break;
case Key.End: case Key.End:
Value = Maximum; SetCurrentValue(ValueProperty, Maximum);
break; break;
default: default:
@ -313,7 +313,7 @@ namespace Avalonia.Controls
// Update if we've found a better value // Update if we've found a better value
if (Math.Abs(next - value) > Tolerance) if (Math.Abs(next - value) > Tolerance)
{ {
Value = next; SetCurrentValue(ValueProperty, next);
} }
} }
@ -366,7 +366,7 @@ namespace Avalonia.Controls
var range = Maximum - Minimum; var range = Maximum - Minimum;
var finalValue = calcVal * range + Minimum; var finalValue = calcVal * range + Minimum;
Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue; SetCurrentValue(ValueProperty, IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue);
} }
/// <inheritdoc /> /// <inheritdoc />

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>

2
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) private void HandleKeyEvent(ref XEvent ev)
{ {

69
src/Avalonia.X11/X11Window.cs

@ -124,7 +124,7 @@ namespace Avalonia.X11
if (!_popup && Screen != null) if (!_popup && Screen != null)
{ {
var monitor = Screen.AllScreens.OrderBy(x => x.Scaling) var monitor = Screen.AllScreens.OrderBy(x => x.Scaling)
.FirstOrDefault(m => m.Bounds.Contains(Position)); .FirstOrDefault(m => m.Bounds.Contains(_position ?? default));
if (monitor != null) if (monitor != null)
{ {
@ -326,23 +326,16 @@ namespace Avalonia.X11
{ {
get get
{ {
XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_FRAME_EXTENTS, IntPtr.Zero, var extents = GetFrameExtents();
new IntPtr(4), false, (IntPtr)Atom.AnyPropertyType, out var _,
out var _, out var nitems, out var _, out var prop);
if (nitems.ToInt64() != 4) if(extents == null)
{ {
// Window hasn't been mapped by the WM yet, so can't get the extents.
return null; 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( return new Size(
(_realSize.Width + extents.Left + extents.Right) / RenderScaling, (_realSize.Width + extents.Value.Left + extents.Value.Right) / RenderScaling,
(_realSize.Height + extents.Top + extents.Bottom) / 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) private bool UpdateScaling(bool skipResize = false)
{ {
double newScaling; double newScaling;
@ -564,7 +576,7 @@ namespace Avalonia.X11
else else
{ {
var monitor = _platform.X11Screens.Screens.OrderBy(x => x.Scaling) 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; newScaling = monitor?.Scaling ?? RenderScaling;
} }
@ -916,11 +928,11 @@ namespace Avalonia.X11
public void Hide() => XUnmapWindow(_x11.Display, _handle); 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( public PixelPoint PointToScreen(Point point) => new PixelPoint(
(int)(point.X * RenderScaling + Position.X), (int)(point.X * RenderScaling + (_position ?? default).X),
(int)(point.Y * RenderScaling + Position.Y)); (int)(point.Y * RenderScaling + (_position ?? default).Y));
public void SetSystemDecorations(SystemDecorations enabled) public void SetSystemDecorations(SystemDecorations enabled)
{ {
@ -984,20 +996,36 @@ namespace Avalonia.X11
public PixelPoint Position 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 set
{ {
if(!_usePositioningFlags) if (!_usePositioningFlags)
{ {
_usePositioningFlags = true; _usePositioningFlags = true;
UpdateSizeHints(null); UpdateSizeHints(null);
} }
var changes = new XWindowChanges var changes = new XWindowChanges
{ {
x = (int)value.X, x = value.X,
y = (int)value.Y y = (int)value.Y
}; };
XConfigureWindow(_x11.Display, _handle, ChangeWindowFlags.CWX | ChangeWindowFlags.CWY, XConfigureWindow(_x11.Display, _handle, ChangeWindowFlags.CWX | ChangeWindowFlags.CWY,
ref changes); ref changes);
XFlush(_x11.Display); XFlush(_x11.Display);
@ -1006,7 +1034,6 @@ namespace Avalonia.X11
_position = value; _position = value;
PositionChanged?.Invoke(value); PositionChanged?.Invoke(value);
} }
} }
} }

135
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();
}
}

55
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();
}

3
src/iOS/Avalonia.iOS/Platform.cs

@ -7,6 +7,7 @@ using Avalonia.OpenGL;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia namespace Avalonia
{ {
@ -44,7 +45,7 @@ namespace Avalonia.iOS
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IRenderLoop>().ToSingleton<RenderLoop>() .Bind<IRenderLoop>().ToSingleton<RenderLoop>()
.Bind<IRenderTimer>().ToConstant(Timer) .Bind<IRenderTimer>().ToConstant(Timer)
.Bind<IPlatformThreadingInterface>().ToConstant(new PlatformThreadingInterface()) .Bind<IDispatcherImpl>().ToConstant(DispatcherImpl.Instance)
.Bind<IKeyboardDevice>().ToConstant(keyboard); .Bind<IKeyboardDevice>().ToConstant(keyboard);
Compositor = new Compositor( Compositor = new Compositor(

31
src/iOS/Avalonia.iOS/PlatformSettings.cs

@ -31,24 +31,25 @@ internal class PlatformSettings : DefaultPlatformSettings
if (tintColor is not null) if (tintColor is not null)
{ {
tintColor.GetRGBA(out var red, out var green, out var blue, out var alpha); 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, return _lastColorValues = new PlatformColorValues
ContrastPreference = contrastPreference, {
AccentColor1 = new Color( ThemeVariant = themeVariant,
(byte)(alpha * 255), ContrastPreference = contrastPreference,
(byte)(red * 255), AccentColor1 = new Color(
(byte)(green * 255), (byte)(alpha * 255),
(byte)(blue * 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() public void TraitCollectionDidChange()

38
src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs

@ -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<DispatcherPriority?> 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);
});
}
}
}

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