Browse Source

Merge branch 'master' into initializeGlyphRunOnRender

pull/3922/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
2c2b958a46
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Documentation/build.md
  2. 5
      native/Avalonia.Native/inc/avalonia-native.h
  3. 3
      native/Avalonia.Native/src/OSX/window.h
  4. 74
      native/Avalonia.Native/src/OSX/window.mm
  5. 53
      samples/RenderDemo/Controls/LineBoundsDemoControl.cs
  6. 3
      samples/RenderDemo/MainWindow.xaml
  7. 9
      samples/RenderDemo/Pages/LineBoundsPage.xaml
  8. 19
      samples/RenderDemo/Pages/LineBoundsPage.xaml.cs
  9. 3
      samples/RenderDemo/RenderDemo.csproj
  10. 225
      src/Avalonia.Animation/Animatable.cs
  11. 2
      src/Avalonia.Animation/TransitionInstance.cs
  12. 13
      src/Avalonia.Animation/Transitions.cs
  13. 204
      src/Avalonia.Base/AvaloniaObject.cs
  14. 59
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  15. 7
      src/Avalonia.Base/AvaloniaProperty.cs
  16. 23
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  17. 24
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  18. 4
      src/Avalonia.Base/Data/BindingValue.cs
  19. 5
      src/Avalonia.Base/DirectPropertyBase.cs
  20. 13
      src/Avalonia.Base/IAvaloniaObject.cs
  21. 20
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  22. 14
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  23. 6
      src/Avalonia.Base/PropertyStore/IValue.cs
  24. 6
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  25. 11
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  26. 161
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  27. 7
      src/Avalonia.Base/StyledPropertyBase.cs
  28. 43
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  29. 69
      src/Avalonia.Base/ValueStore.cs
  30. 5
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  31. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  32. 12
      src/Avalonia.Controls/Button.cs
  33. 12
      src/Avalonia.Controls/ButtonSpinner.cs
  34. 12
      src/Avalonia.Controls/Calendar/DatePicker.cs
  35. 39
      src/Avalonia.Controls/ContextMenu.cs
  36. 12
      src/Avalonia.Controls/Expander.cs
  37. 12
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  38. 16
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  39. 15
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  40. 20
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  41. 8
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  42. 12
      src/Avalonia.Controls/Primitives/Track.cs
  43. 16
      src/Avalonia.Controls/ProgressBar.cs
  44. 56
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  45. 36
      src/Avalonia.Controls/Repeater/ViewManager.cs
  46. 35
      src/Avalonia.Controls/SelectionModel.cs
  47. 34
      src/Avalonia.Controls/Shapes/Path.cs
  48. 111
      src/Avalonia.Controls/Shapes/Shape.cs
  49. 12
      src/Avalonia.Controls/Slider.cs
  50. 8
      src/Avalonia.Controls/TreeView.cs
  51. 121
      src/Avalonia.Controls/Window.cs
  52. 2
      src/Avalonia.Controls/WindowBase.cs
  53. 9
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  54. 10
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  55. 10
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  56. 12
      src/Avalonia.Input/InputElement.cs
  57. 10
      src/Avalonia.Layout/FlowLayoutAlgorithm.cs
  58. 26
      src/Avalonia.Layout/StackLayout.cs
  59. 2
      src/Avalonia.Layout/StackLayoutState.cs
  60. 36
      src/Avalonia.Layout/UniformGridLayout.cs
  61. 5
      src/Avalonia.Native/PopupImpl.cs
  62. 26
      src/Avalonia.Native/WindowImpl.cs
  63. 24
      src/Avalonia.Remote.Protocol/DesignMessages.cs
  64. 12
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  65. 4
      src/Avalonia.Visuals/Media/BoxShadows.cs
  66. 8
      src/Avalonia.Visuals/Media/DrawingContext.cs
  67. 10
      src/Avalonia.Visuals/Media/DrawingImage.cs
  68. 44
      src/Avalonia.Visuals/Media/Imaging/Bitmap.cs
  69. 15
      src/Avalonia.Visuals/Media/PixelRect.cs
  70. 26
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  71. 49
      src/Avalonia.Visuals/Rect.cs
  72. 11
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  73. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
  74. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs
  75. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  76. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
  77. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  78. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
  79. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  80. 68
      src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs
  81. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  82. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs
  83. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  84. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
  85. 5
      src/Avalonia.Visuals/Visual.cs
  86. 49
      src/Avalonia.X11/X11Window.cs
  87. 19
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  88. 2
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  89. 73
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  90. 42
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  91. 18
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  92. 24
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  93. 66
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
  94. 6
      src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs
  95. 29
      src/Windows/Avalonia.Win32/WindowImpl.cs
  96. 339
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  97. 4
      tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
  98. 51
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs
  99. 142
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs
  100. 39
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

2
Documentation/build.md

@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi
On macOS:
```
brew install castxml
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
```
On Debian based Linux (Debian, Ubuntu, Mint, etc):

5
native/Avalonia.Native/inc/avalonia-native.h

@ -267,7 +267,8 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
{
virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
virtual HRESULT SetEnabled (bool enable) = 0;
virtual HRESULT SetParent (IAvnWindow* parent) = 0;
virtual HRESULT SetCanResize(bool value) = 0;
virtual HRESULT SetDecorations(SystemDecorations value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 0;
@ -309,6 +310,8 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents
virtual bool Closing () = 0;
virtual void WindowStateChanged (AvnWindowState state) = 0;
virtual void GotInputWhenDisabled () = 0;
};
AVNCOM(IAvnMacOptions, 07) : IUnknown

3
native/Avalonia.Native/src/OSX/window.h

@ -19,8 +19,7 @@ class WindowBaseImpl;
-(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents;
-(bool) isModal;
-(void) setModal: (bool) isModal;
-(void) setEnabled: (bool) enable;
-(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu;

74
native/Avalonia.Native/src/OSX/window.mm

@ -497,12 +497,7 @@ private:
virtual HRESULT Show () override
{
@autoreleasepool
{
if([Window parentWindow] != nil)
[[Window parentWindow] removeChildWindow:Window];
[Window setModal:FALSE];
{
WindowBaseImpl::Show();
HideOrShowTrafficLights();
@ -511,7 +506,16 @@ private:
}
}
virtual HRESULT ShowDialog (IAvnWindow* parent) override
virtual HRESULT SetEnabled (bool enable) override
{
@autoreleasepool
{
[Window setEnabled:enable];
return S_OK;
}
}
virtual HRESULT SetParent (IAvnWindow* parent) override
{
@autoreleasepool
{
@ -522,12 +526,9 @@ private:
if(cparent == nullptr)
return E_INVALIDARG;
[Window setModal:TRUE];
[cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show();
HideOrShowTrafficLights();
UpdateStyle();
return S_OK;
}
@ -883,15 +884,15 @@ protected:
switch (_decorations)
{
case SystemDecorationsNone:
s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
s = s | NSWindowStyleMaskFullSizeContentView;
break;
case SystemDecorationsBorderOnly:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
break;
case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless;
if(_canResize)
{
@ -900,6 +901,10 @@ protected:
break;
}
if([Window parentWindow] == nullptr)
{
s |= NSWindowStyleMaskMiniaturizable;
}
return s;
}
};
@ -1081,15 +1086,28 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (bool) ignoreUserInput
{
auto parentWindow = objc_cast<AvnWindow>([self window]);
if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents])
{
auto window = dynamic_cast<WindowImpl*>(_parent.getRaw());
if(window != nullptr)
{
window->WindowEvents->GotInputWhenDisabled();
}
return TRUE;
}
return FALSE;
}
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
{
if([self ignoreUserInput])
{
return;
}
[self becomeFirstResponder];
auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
@ -1234,7 +1252,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type
{
if([self ignoreUserInput])
{
return;
}
auto key = s_KeyMap[[event keyCode]];
auto timestamp = [event timestamp] * 1000;
@ -1416,7 +1437,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
ComPtr<WindowBaseImpl> _parent;
bool _canBecomeKeyAndMain;
bool _closed;
bool _isModal;
bool _isEnabled;
AvnMenu* _menu;
double _lastScaling;
}
@ -1538,6 +1559,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_parent = parent;
[self setDelegate:self];
_closed = false;
_isEnabled = true;
_lastScaling = [self backingScaleFactor];
[self setOpaque:NO];
@ -1604,28 +1626,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
-(bool)shouldTryToHandleEvents
{
for(NSWindow* uch in [self childWindows])
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
if(![ch isModal])
continue;
return FALSE;
}
return TRUE;
}
-(bool) isModal
{
return _isModal;
return _isEnabled;
}
-(void) setModal: (bool) isModal
-(void) setEnabled:(bool)enable
{
_isModal = isModal;
_isEnabled = enable;
}
-(void)makeKeyWindow

53
samples/RenderDemo/Controls/LineBoundsDemoControl.cs

@ -0,0 +1,53 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
namespace RenderDemo.Controls
{
public class LineBoundsDemoControl : Control
{
static LineBoundsDemoControl()
{
AffectsRender<LineBoundsDemoControl>(AngleProperty);
}
public LineBoundsDemoControl()
{
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1 / 60.0);
timer.Tick += (sender, e) => Angle += Math.PI / 360;
timer.Start();
}
public static readonly StyledProperty<double> AngleProperty =
AvaloniaProperty.Register<LineBoundsDemoControl, double>(nameof(Angle));
public double Angle
{
get => GetValue(AngleProperty);
set => SetValue(AngleProperty, value);
}
public override void Render(DrawingContext drawingContext)
{
var lineLength = Math.Sqrt((100 * 100) + (100 * 100));
var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength);
var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength);
var p1 = new Point(200, 200);
var p2 = new Point(p1.X + diffX, p1.Y + diffY);
var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square);
var boundPen = new Pen(Brushes.Black);
drawingContext.DrawLine(pen, p1, p2);
drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen));
}
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -44,6 +44,9 @@
<TabItem Header="GlyphRun">
<pages:GlyphRunPage/>
</TabItem>
<TabItem Header="LineBounds">
<pages:LineBoundsPage />
</TabItem>
</TabControl>
</DockPanel>
</Window>

9
samples/RenderDemo/Pages/LineBoundsPage.xaml

@ -0,0 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:controls="clr-namespace:RenderDemo.Controls"
x:Class="RenderDemo.Pages.LineBoundsPage">
<controls:LineBoundsDemoControl />
</UserControl>

19
samples/RenderDemo/Pages/LineBoundsPage.xaml.cs

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace RenderDemo.Pages
{
public class LineBoundsPage : UserControl
{
public LineBoundsPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

3
samples/RenderDemo/RenderDemo.csproj

@ -3,6 +3,9 @@
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\Avalonia.Visuals\Rendering\SceneGraph\LineBoundsHelper.cs" Link="LineBoundsHelper.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />

225
src/Avalonia.Animation/Animatable.cs

@ -1,10 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Collections;
using System.Collections.Specialized;
using Avalonia.Data;
using Avalonia.Animation.Animators;
#nullable enable
namespace Avalonia.Animation
{
@ -13,9 +13,24 @@ namespace Avalonia.Animation
/// </summary>
public class Animatable : AvaloniaObject
{
/// <summary>
/// Defines the <see cref="Clock"/> property.
/// </summary>
public static readonly StyledProperty<IClock> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true);
/// <summary>
/// Defines the <see cref="Transitions"/> property.
/// </summary>
public static readonly StyledProperty<Transitions?> TransitionsProperty =
AvaloniaProperty.Register<Animatable, Transitions?>(nameof(Transitions));
private bool _transitionsEnabled = true;
private Dictionary<ITransition, TransitionState>? _transitionState;
/// <summary>
/// Gets or sets the clock which controls the animations on the control.
/// </summary>
public IClock Clock
{
get => GetValue(ClockProperty);
@ -23,72 +38,194 @@ namespace Avalonia.Animation
}
/// <summary>
/// Defines the <see cref="Transitions"/> property.
/// Gets or sets the property transitions for the control.
/// </summary>
public static readonly DirectProperty<Animatable, Transitions> TransitionsProperty =
AvaloniaProperty.RegisterDirect<Animatable, Transitions>(
nameof(Transitions),
o => o.Transitions,
(o, v) => o.Transitions = v);
public Transitions? Transitions
{
get => GetValue(TransitionsProperty);
set => SetValue(TransitionsProperty, value);
}
private Transitions _transitions;
/// <summary>
/// Enables transitions for the control.
/// </summary>
/// <remarks>
/// This method should not be called from user code, it will be called automatically by the framework
/// when a control is added to the visual tree.
/// </remarks>
protected void EnableTransitions()
{
if (!_transitionsEnabled)
{
_transitionsEnabled = true;
private Dictionary<AvaloniaProperty, IDisposable> _previousTransitions;
if (Transitions is object)
{
AddTransitions(Transitions);
}
}
}
/// <summary>
/// Gets or sets the property transitions for the control.
/// Disables transitions for the control.
/// </summary>
public Transitions Transitions
/// <remarks>
/// This method should not be called from user code, it will be called automatically by the framework
/// when a control is added to the visual tree.
/// </remarks>
protected void DisableTransitions()
{
if (_transitionsEnabled)
{
_transitionsEnabled = false;
if (Transitions is object)
{
RemoveTransitions(Transitions);
}
}
}
protected sealed override void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
get
if (change.Property == TransitionsProperty && change.IsEffectiveValueChange)
{
if (_transitions is null)
_transitions = new Transitions();
var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
if (_previousTransitions is null)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>();
if (oldTransitions is object)
{
oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
RemoveTransitions(oldTransitions);
}
return _transitions;
if (newTransitions is object)
{
newTransitions.CollectionChanged += TransitionsCollectionChanged;
AddTransitions(newTransitions);
}
}
set
else if (_transitionsEnabled &&
Transitions is object &&
_transitionState is object &&
!change.Property.IsDirect &&
change.Priority > BindingPriority.Animation)
{
if (value is null)
return;
foreach (var transition in Transitions)
{
if (transition.Property == change.Property)
{
var state = _transitionState[transition];
var oldValue = state.BaseValue;
var newValue = GetAnimationBaseValue(transition.Property);
if (_previousTransitions is null)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>();
if (!Equals(oldValue, newValue))
{
state.BaseValue = newValue;
SetAndRaise(TransitionsProperty, ref _transitions, value);
// We need to transition from the current animated value if present,
// instead of the old base value.
var animatedValue = GetValue(transition.Property);
if (!Equals(newValue, animatedValue))
{
oldValue = animatedValue;
}
state.Instance?.Dispose();
state.Instance = transition.Apply(
this,
Clock ?? AvaloniaLocator.Current.GetService<IGlobalClock>(),
oldValue,
newValue);
return;
}
}
}
}
base.OnPropertyChangedCore(change);
}
private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (!_transitionsEnabled)
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddTransitions(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveTransitions(e.OldItems);
break;
case NotifyCollectionChangedAction.Replace:
RemoveTransitions(e.OldItems);
AddTransitions(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
throw new NotSupportedException("Transitions collection cannot be reset.");
}
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
private void AddTransitions(IList items)
{
if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation)
if (!_transitionsEnabled)
{
return;
}
_transitionState ??= new Dictionary<ITransition, TransitionState>();
// PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
foreach (var transition in _transitions)
for (var i = 0; i < items.Count; ++i)
{
if (transition.Property == property)
var t = (ITransition)items[i];
_transitionState.Add(t, new TransitionState
{
if (_previousTransitions.TryGetValue(property, out var dispose))
dispose.Dispose();
BaseValue = GetAnimationBaseValue(t.Property),
});
}
}
var instance = transition.Apply(
this,
Clock ?? Avalonia.Animation.Clock.GlobalClock,
oldValue.GetValueOrDefault(),
newValue.GetValueOrDefault());
private void RemoveTransitions(IList items)
{
if (_transitionState is null)
{
return;
}
_previousTransitions[property] = instance;
return;
for (var i = 0; i < items.Count; ++i)
{
var t = (ITransition)items[i];
if (_transitionState.TryGetValue(t, out var state))
{
state.Instance?.Dispose();
_transitionState.Remove(t);
}
}
}
private object GetAnimationBaseValue(AvaloniaProperty property)
{
var value = this.GetBaseValue(property, BindingPriority.LocalValue);
if (value == AvaloniaProperty.UnsetValue)
{
value = GetValue(property);
}
return value;
}
private class TransitionState
{
public IDisposable? Instance { get; set; }
public object? BaseValue { get; set; }
}
}
}

2
src/Avalonia.Animation/TransitionInstance.cs

@ -19,6 +19,8 @@ namespace Avalonia.Animation
public TransitionInstance(IClock clock, TimeSpan Duration)
{
clock = clock ?? throw new ArgumentNullException(nameof(clock));
_duration = Duration;
_baseClock = clock;
}

13
src/Avalonia.Animation/Transitions.cs

@ -1,4 +1,6 @@
using System;
using Avalonia.Collections;
using Avalonia.Threading;
namespace Avalonia.Animation
{
@ -13,6 +15,17 @@ namespace Avalonia.Animation
public Transitions()
{
ResetBehavior = ResetBehavior.Remove;
Validate = ValidateTransition;
}
private void ValidateTransition(ITransition obj)
{
Dispatcher.UIThread.VerifyAccess();
if (obj.Property.IsDirect)
{
throw new InvalidOperationException("Cannot animate a direct property.");
}
}
}
}

204
src/Avalonia.Base/AvaloniaObject.cs

@ -259,6 +259,21 @@ namespace Avalonia
return registered.InvokeGetter(this);
}
/// <inheritdoc/>
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property, BindingPriority maxPriority)
{
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
if (_values is object &&
_values.TryGetValue(property, maxPriority, out var value))
{
return value;
}
return default;
}
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating.
/// </summary>
@ -458,29 +473,43 @@ namespace Avalonia
return _propertyChanged?.GetInvocationList();
}
void IValueSink.ValueChanged<T>(
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue)
void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property);
newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property));
var property = (StyledPropertyBase<T>)change.Property;
LogIfError(property, newValue);
LogIfError(property, change.NewValue);
if (!EqualityComparer<T>.Default.Equals(oldValue.Value, newValue.Value))
// If the change is to the effective value of the property and no old/new value is set
// then fill in the old/new value from property inheritance/default value. We don't do
// this for non-effective value changes because these are only needed for property
// transitions, where knowing e.g. that an inherited value is active at an arbitrary
// priority isn't of any use and would introduce overhead.
if (change.IsEffectiveValueChange && !change.OldValue.HasValue)
{
RaisePropertyChanged(property, oldValue, newValue, priority);
change.SetOldValue(GetInheritedOrDefault<T>(property));
}
Logger.TryGet(LogEventLevel.Verbose)?.Log(
LogArea.Property,
this,
"{Property} changed from {$Old} to {$Value} with priority {Priority}",
property,
oldValue,
newValue,
(BindingPriority)priority);
if (change.IsEffectiveValueChange && !change.NewValue.HasValue)
{
change.SetNewValue(GetInheritedOrDefault(property));
}
if (!change.IsEffectiveValueChange ||
!EqualityComparer<T>.Default.Equals(change.OldValue.Value, change.NewValue.Value))
{
RaisePropertyChanged(change);
if (change.IsEffectiveValueChange)
{
Logger.TryGet(LogEventLevel.Verbose)?.Log(
LogArea.Property,
this,
"{Property} changed from {$Old} to {$Value} with priority {Priority}",
property,
change.OldValue,
change.NewValue,
change.Priority);
}
}
}
@ -489,7 +518,13 @@ namespace Avalonia
IPriorityValueEntry entry,
Optional<T> oldValue)
{
((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default);
var change = new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
default,
BindingPriority.Unset);
((IValueSink)this).ValueChanged(change);
}
/// <summary>
@ -575,15 +610,20 @@ namespace Avalonia
/// <summary>
/// Called when a avalonia property changes on the object.
/// </summary>
/// <param name="property">The property whose value has changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
/// <param name="priority">The priority of the new value.</param>
protected virtual void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
/// <param name="change">The property change details.</param>
protected virtual void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (change.IsEffectiveValueChange)
{
OnPropertyChanged(change);
}
}
/// <summary>
/// Called when a avalonia property changes on the object.
/// </summary>
/// <param name="change">The property change details.</param>
protected virtual void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
}
@ -600,57 +640,12 @@ namespace Avalonia
BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
property.Notifying?.Invoke(this, true);
try
{
AvaloniaPropertyChangedEventArgs<T> e = null;
var hasChanged = property.HasChangedSubscriptions;
if (hasChanged || _propertyChanged != null)
{
e = new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
newValue,
priority);
}
OnPropertyChanged(property, oldValue, newValue, priority);
if (hasChanged)
{
property.NotifyChanged(e);
}
_propertyChanged?.Invoke(this, e);
if (_inpcChanged != null)
{
var inpce = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, inpce);
}
if (property.Inherits && _inheritanceChildren != null)
{
foreach (var child in _inheritanceChildren)
{
child.InheritedPropertyChanged(
property,
oldValue,
newValue.ToOptional());
}
}
}
finally
{
property.Notifying?.Invoke(this, false);
}
RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
newValue,
priority));
}
/// <summary>
@ -689,7 +684,9 @@ namespace Avalonia
return property.GetDefaultValue(GetType());
}
private T GetValueOrInheritedOrDefault<T>(StyledPropertyBase<T> property)
private T GetValueOrInheritedOrDefault<T>(
StyledPropertyBase<T> property,
BindingPriority maxPriority = BindingPriority.Animation)
{
var o = this;
var inherits = property.Inherits;
@ -699,7 +696,7 @@ namespace Avalonia
{
var values = o._values;
if (values?.TryGetValue(property, out value) == true)
if (values?.TryGetValue(property, maxPriority, out value) == true)
{
return value;
}
@ -715,6 +712,51 @@ namespace Avalonia
return property.GetDefaultValue(GetType());
}
protected internal void RaisePropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
VerifyAccess();
if (change.IsEffectiveValueChange)
{
change.Property.Notifying?.Invoke(this, true);
}
try
{
OnPropertyChangedCore(change);
if (change.IsEffectiveValueChange)
{
change.Property.NotifyChanged(change);
_propertyChanged?.Invoke(this, change);
if (_inpcChanged != null)
{
var inpce = new PropertyChangedEventArgs(change.Property.Name);
_inpcChanged(this, inpce);
}
if (change.Property.Inherits && _inheritanceChildren != null)
{
foreach (var child in _inheritanceChildren)
{
child.InheritedPropertyChanged(
change.Property,
change.OldValue,
change.NewValue.ToOptional());
}
}
}
}
finally
{
if (change.IsEffectiveValueChange)
{
change.Property.Notifying?.Invoke(this, false);
}
}
}
/// <summary>
/// Sets the value of a direct property.
/// </summary>

59
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -448,6 +448,65 @@ namespace Avalonia
};
}
/// <summary>
/// Gets an <see cref="AvaloniaProperty"/> base value.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// For styled properties, gets the value of the property if set on the object with a
/// priority equal or lower to <paramref name="maxPriority"/>, otherwise
/// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return
/// property values that come from inherited or default values.
///
/// For direct properties returns <see cref="GetValue(IAvaloniaObject, AvaloniaProperty)"/>.
/// </remarks>
public static object GetBaseValue(
this IAvaloniaObject target,
AvaloniaProperty property,
BindingPriority maxPriority)
{
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
return property.RouteGetBaseValue(target, maxPriority);
}
/// <summary>
/// Gets an <see cref="AvaloniaProperty"/> base value.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// For styled properties, gets the value of the property if set on the object with a
/// priority equal or lower to <paramref name="maxPriority"/>, otherwise
/// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values
/// that come from inherited or default values.
///
/// For direct properties returns
/// <see cref="IAvaloniaObject.GetValue{T}(DirectPropertyBase{T})"/>.
/// </remarks>
public static Optional<T> GetBaseValue<T>(
this IAvaloniaObject target,
AvaloniaProperty<T> property,
BindingPriority maxPriority)
{
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
return property switch
{
StyledPropertyBase<T> styled => target.GetBaseValue(styled, maxPriority),
DirectPropertyBase<T> direct => target.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};
}
/// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary>

7
src/Avalonia.Base/AvaloniaProperty.cs

@ -496,6 +496,13 @@ namespace Avalonia
/// <param name="o">The object instance.</param>
internal abstract object RouteGetValue(IAvaloniaObject o);
/// <summary>
/// Routes an untyped GetBaseValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
internal abstract object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority);
/// <summary>
/// Routes an untyped SetValue call to a typed call.
/// </summary>

23
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -16,6 +16,7 @@ namespace Avalonia
{
Sender = sender;
Priority = priority;
IsEffectiveValueChange = true;
}
/// <summary>
@ -35,19 +36,11 @@ namespace Avalonia
/// <summary>
/// Gets the old value of the property.
/// </summary>
/// <value>
/// The old value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
/// property previously had no value.
/// </value>
public object? OldValue => GetOldValue();
/// <summary>
/// Gets the new value of the property.
/// </summary>
/// <value>
/// The new value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
/// property previously had no value.
/// </value>
public object? NewValue => GetNewValue();
/// <summary>
@ -58,6 +51,20 @@ namespace Avalonia
/// </value>
public BindingPriority Priority { get; private set; }
/// <summary>
/// Gets a value indicating whether the change represents a change to the effective value of
/// the property.
/// </summary>
/// <remarks>
/// This will usually be true, except in
/// <see cref="AvaloniaObject.OnPropertyChangedCore{T}(AvaloniaPropertyChangedEventArgs{T})"/>
/// which recieves notifications for all changes to property values, whether a value with a higher
/// priority is present or not. When this property is false, the change that is being signalled
/// has not resulted in a change to the property value on the object.
/// </remarks>
public bool IsEffectiveValueChange { get; private set; }
internal void MarkNonEffectiveValue() => IsEffectiveValueChange = false;
protected abstract AvaloniaProperty GetProperty();
protected abstract object? GetOldValue();
protected abstract object? GetNewValue();

24
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs

@ -1,4 +1,3 @@
using System;
using Avalonia.Data;
#nullable enable
@ -6,7 +5,7 @@ using Avalonia.Data;
namespace Avalonia
{
/// <summary>
/// Provides information for a avalonia property change.
/// Provides information for an Avalonia property change.
/// </summary>
public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs
{
@ -42,19 +41,28 @@ namespace Avalonia
/// <summary>
/// Gets the old value of the property.
/// </summary>
/// <value>
/// The old value of the property.
/// </value>
/// <remarks>
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// old value of the property on the object.
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false, returns
/// <see cref="Optional{T}.Empty"/>.
/// </remarks>
public new Optional<T> OldValue { get; private set; }
/// <summary>
/// Gets the new value of the property.
/// </summary>
/// <value>
/// The new value of the property.
/// </value>
/// <remarks>
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// value of the property on the object.
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false returns the
/// changed value, or <see cref="Optional{T}.Empty"/> if the value was removed.
/// </remarks>
public new BindingValue<T> NewValue { get; private set; }
internal void SetOldValue(Optional<T> value) => OldValue = value;
internal void SetNewValue(BindingValue<T> value) => NewValue = value;
protected override AvaloniaProperty GetProperty() => Property;
protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);

4
src/Avalonia.Base/Data/BindingValue.cs

@ -348,8 +348,8 @@ namespace Avalonia.Data
return new BindingValue<T>(
fallbackValue.HasValue ?
BindingValueType.DataValidationError :
BindingValueType.DataValidationErrorWithFallback,
BindingValueType.DataValidationErrorWithFallback :
BindingValueType.DataValidationError,
fallbackValue.HasValue ? fallbackValue.Value : default,
e);
}

5
src/Avalonia.Base/DirectPropertyBase.cs

@ -120,6 +120,11 @@ namespace Avalonia
return o.GetValue<TValue>(this);
}
internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority)
{
return o.GetValue<TValue>(this);
}
/// <inheritdoc/>
internal override IDisposable? RouteSetValue(
IAvaloniaObject o,

13
src/Avalonia.Base/IAvaloniaObject.cs

@ -41,6 +41,19 @@ namespace Avalonia
/// <returns>The value.</returns>
T GetValue<T>(DirectPropertyBase<T> property);
/// <summary>
/// Gets an <see cref="AvaloniaProperty"/> base value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// Gets the value of the property, if set on this object with a priority equal or lower to
/// <paramref name="maxPriority"/>, otherwise <see cref="Optional{T}.Empty"/>. Note that
/// this method does not return property values that come from inherited or default values.
/// </remarks>
Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property, BindingPriority maxPriority);
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating.
/// </summary>

20
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -22,6 +22,7 @@ namespace Avalonia.PropertyStore
private readonly IAvaloniaObject _owner;
private IValueSink _sink;
private IDisposable? _subscription;
private Optional<T> _value;
public BindingEntry(
IAvaloniaObject owner,
@ -40,18 +41,21 @@ namespace Avalonia.PropertyStore
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; }
public IObservable<BindingValue<T>> Source { get; }
public Optional<T> Value { get; private set; }
Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority;
Optional<object> IValue.GetValue() => _value.ToObject();
public Optional<T> GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_sink.Completed(Property, this, Value);
_sink.Completed(Property, this, _value);
}
public void OnCompleted() => _sink.Completed(Property, this, Value);
public void OnCompleted() => _sink.Completed(Property, this, _value);
public void OnError(Exception error)
{
@ -94,14 +98,14 @@ namespace Avalonia.PropertyStore
return;
}
var old = Value;
var old = _value;
if (value.Type != BindingValueType.DataValidationError)
{
Value = value.ToOptional();
_value = value.ToOptional();
}
_sink.ValueChanged(Property, Priority, old, value);
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(_owner, Property, old, value, Priority));
}
}
}

14
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -13,6 +13,7 @@ namespace Avalonia.PropertyStore
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
{
private IValueSink _sink;
private Optional<T> _value;
public ConstantValueEntry(
StyledPropertyBase<T> property,
@ -21,18 +22,21 @@ namespace Avalonia.PropertyStore
IValueSink sink)
{
Property = property;
Value = value;
_value = value;
Priority = priority;
_sink = sink;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; }
public Optional<T> Value { get; }
Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority;
Optional<object> IValue.GetValue() => _value.ToObject();
public void Dispose() => _sink.Completed(Property, this, Value);
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose() => _sink.Completed(Property, this, _value);
public void Reparent(IValueSink sink) => _sink = sink;
}
}

6
src/Avalonia.Base/PropertyStore/IValue.cs

@ -9,8 +9,8 @@ namespace Avalonia.PropertyStore
/// </summary>
internal interface IValue
{
Optional<object> Value { get; }
BindingPriority ValuePriority { get; }
Optional<object> GetValue();
BindingPriority Priority { get; }
}
/// <summary>
@ -19,6 +19,6 @@ namespace Avalonia.PropertyStore
/// <typeparam name="T">The property type.</typeparam>
internal interface IValue<T> : IValue
{
new Optional<T> Value { get; }
Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation);
}
}

6
src/Avalonia.Base/PropertyStore/IValueSink.cs

@ -9,11 +9,7 @@ namespace Avalonia.PropertyStore
/// </summary>
internal interface IValueSink
{
void ValueChanged<T>(
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue);
void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change);
void Completed<T>(
StyledPropertyBase<T> property,

11
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -14,9 +14,14 @@ namespace Avalonia.PropertyStore
private T _value;
public LocalValueEntry(T value) => _value = value;
public Optional<T> Value => _value;
public BindingPriority ValuePriority => BindingPriority.LocalValue;
Optional<object> IValue.Value => Value.ToObject();
public BindingPriority Priority => BindingPriority.LocalValue;
Optional<object> IValue.GetValue() => new Optional<object>(_value);
public Optional<T> GetValue(BindingPriority maxPriority)
{
return BindingPriority.LocalValue >= maxPriority ? _value : Optional<T>.Empty;
}
public void SetValue(T value) => _value = value;
}
}

161
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Data;
#nullable enable
@ -24,6 +25,7 @@ namespace Avalonia.PropertyStore
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue;
private Optional<T> _value;
public PriorityValue(
IAvaloniaObject owner,
@ -50,11 +52,13 @@ namespace Avalonia.PropertyStore
{
existing.Reparent(this);
_entries.Add(existing);
var v = existing.GetValue();
if (existing.Value.HasValue)
if (v.HasValue)
{
Value = existing.Value;
ValuePriority = existing.Priority;
_value = v;
Priority = existing.Priority;
}
}
@ -65,18 +69,39 @@ namespace Avalonia.PropertyStore
LocalValueEntry<T> existing)
: this(owner, property, sink)
{
_localValue = existing.Value;
Value = _localValue;
ValuePriority = BindingPriority.LocalValue;
_value = _localValue = existing.GetValue(BindingPriority.LocalValue);
Priority = BindingPriority.LocalValue;
}
public StyledPropertyBase<T> Property { get; }
public Optional<T> Value { get; private set; }
public BindingPriority ValuePriority { get; private set; }
public BindingPriority Priority { get; private set; } = BindingPriority.Unset;
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
Optional<object> IValue.Value => Value.ToObject();
Optional<object> IValue.GetValue() => _value.ToObject();
public void ClearLocalValue()
{
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
default,
default,
BindingPriority.LocalValue));
}
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
{
if (Priority == BindingPriority.Unset)
{
return default;
}
public void ClearLocalValue() => UpdateEffectiveValue();
if (Priority >= maxPriority)
{
return _value;
}
return CalculateValue(maxPriority).Item1;
}
public IDisposable? SetValue(T value, BindingPriority priority)
{
@ -94,7 +119,13 @@ namespace Avalonia.PropertyStore
result = entry;
}
UpdateEffectiveValue();
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
default,
value,
priority));
return result;
}
@ -106,20 +137,19 @@ namespace Avalonia.PropertyStore
return binding;
}
public void CoerceValue() => UpdateEffectiveValue();
public void CoerceValue() => UpdateEffectiveValue(null);
void IValueSink.ValueChanged<TValue>(
StyledPropertyBase<TValue> property,
BindingPriority priority,
Optional<TValue> oldValue,
BindingValue<TValue> newValue)
void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
{
if (priority == BindingPriority.LocalValue)
if (change.Priority == BindingPriority.LocalValue)
{
_localValue = default;
}
UpdateEffectiveValue();
if (change is AvaloniaPropertyChangedEventArgs<T> c)
{
UpdateEffectiveValue(c);
}
}
void IValueSink.Completed<TValue>(
@ -128,7 +158,16 @@ namespace Avalonia.PropertyStore
Optional<TValue> oldValue)
{
_entries.Remove((IPriorityValueEntry<T>)entry);
UpdateEffectiveValue();
if (oldValue is Optional<T> o)
{
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
o,
default,
entry.Priority));
}
}
private int FindInsertPoint(BindingPriority priority)
@ -147,53 +186,73 @@ namespace Avalonia.PropertyStore
return result;
}
private void UpdateEffectiveValue()
public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
{
var reachedLocalValues = false;
var value = default(Optional<T>);
if (_entries.Count > 0)
for (var i = _entries.Count - 1; i >= 0; --i)
{
for (var i = _entries.Count - 1; i >= 0; --i)
var entry = _entries[i];
if (entry.Priority < maxPriority)
{
continue;
}
if (!reachedLocalValues &&
entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
var entryValue = entry.GetValue();
if (entryValue.HasValue)
{
var entry = _entries[i];
if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue)
{
reachedLocalValues = true;
if (_localValue.HasValue)
{
value = _localValue;
ValuePriority = BindingPriority.LocalValue;
break;
}
}
if (entry.Value.HasValue)
{
value = entry.Value;
ValuePriority = entry.Priority;
break;
}
return (entryValue, entry.Priority);
}
}
else if (_localValue.HasValue)
if (!reachedLocalValues &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{
value = _localValue;
ValuePriority = BindingPriority.LocalValue;
return (_localValue, BindingPriority.LocalValue);
}
return (default, BindingPriority.Unset);
}
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)
{
var (value, priority) = CalculateValue(BindingPriority.Animation);
if (value.HasValue && _coerceValue != null)
{
value = _coerceValue(_owner, value.Value);
}
if (value != Value)
Priority = priority;
if (value != _value)
{
var old = _value;
_value = value;
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
old,
value,
Priority));
}
else if (change is object)
{
var old = Value;
Value = value;
_sink.ValueChanged(Property, ValuePriority, old, value);
change.MarkNonEffectiveValue();
change.SetOldValue(default);
_sink.ValueChanged(change);
}
}
}

7
src/Avalonia.Base/StyledPropertyBase.cs

@ -197,6 +197,13 @@ namespace Avalonia
return o.GetValue<TValue>(this);
}
/// <inheritdoc/>
internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority)
{
var value = o.GetBaseValue<TValue>(this, maxPriority);
return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
internal override IDisposable RouteSetValue(
IAvaloniaObject o,

43
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -115,45 +115,46 @@ namespace Avalonia.Utilities
return true;
}
var toUnderl = Nullable.GetUnderlyingType(to) ?? to;
var from = value.GetType();
if (to.IsAssignableFrom(from))
if (toUnderl.IsAssignableFrom(from))
{
result = value;
return true;
}
if (to == typeof(string))
if (toUnderl == typeof(string))
{
result = Convert.ToString(value);
result = Convert.ToString(value, culture);
return true;
}
if (to.IsEnum && from == typeof(string))
if (toUnderl.IsEnum && from == typeof(string))
{
if (Enum.IsDefined(to, (string)value))
if (Enum.IsDefined(toUnderl, (string)value))
{
result = Enum.Parse(to, (string)value);
result = Enum.Parse(toUnderl, (string)value);
return true;
}
}
if (!from.IsEnum && to.IsEnum)
if (!from.IsEnum && toUnderl.IsEnum)
{
result = null;
if (TryConvert(Enum.GetUnderlyingType(to), value, culture, out object enumValue))
if (TryConvert(Enum.GetUnderlyingType(toUnderl), value, culture, out object enumValue))
{
result = Enum.ToObject(to, enumValue);
result = Enum.ToObject(toUnderl, enumValue);
return true;
}
}
if (from.IsEnum && IsNumeric(to))
if (from.IsEnum && IsNumeric(toUnderl))
{
try
{
result = Convert.ChangeType((int)value, to, culture);
result = Convert.ChangeType((int)value, toUnderl, culture);
return true;
}
catch
@ -164,7 +165,7 @@ namespace Avalonia.Utilities
}
var convertableFrom = Array.IndexOf(InbuiltTypes, from);
var convertableTo = Array.IndexOf(InbuiltTypes, to);
var convertableTo = Array.IndexOf(InbuiltTypes, toUnderl);
if (convertableFrom != -1 && convertableTo != -1)
{
@ -172,7 +173,7 @@ namespace Avalonia.Utilities
{
try
{
result = Convert.ChangeType(value, to, culture);
result = Convert.ChangeType(value, toUnderl, culture);
return true;
}
catch
@ -183,15 +184,23 @@ namespace Avalonia.Utilities
}
}
var typeConverter = TypeDescriptor.GetConverter(to);
var toTypeConverter = TypeDescriptor.GetConverter(toUnderl);
if (toTypeConverter.CanConvertFrom(from) == true)
{
result = toTypeConverter.ConvertFrom(null, culture, value);
return true;
}
var fromTypeConverter = TypeDescriptor.GetConverter(from);
if (typeConverter.CanConvertFrom(from) == true)
if (fromTypeConverter.CanConvertTo(toUnderl) == true)
{
result = typeConverter.ConvertFrom(null, culture, value);
result = fromTypeConverter.ConvertTo(null, culture, value, toUnderl);
return true;
}
var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit);
var cast = FindTypeConversionOperatorMethod(from, toUnderl, OperatorType.Implicit | OperatorType.Explicit);
if (cast != null)
{

69
src/Avalonia.Base/ValueStore.cs

@ -37,7 +37,7 @@ namespace Avalonia
{
if (_values.TryGetValue(property, out var slot))
{
return slot.ValuePriority < BindingPriority.LocalValue;
return slot.Priority < BindingPriority.LocalValue;
}
return false;
@ -47,21 +47,24 @@ namespace Avalonia
{
if (_values.TryGetValue(property, out var slot))
{
return slot.Value.HasValue;
return slot.GetValue().HasValue;
}
return false;
}
public bool TryGetValue<T>(StyledPropertyBase<T> property, out T value)
public bool TryGetValue<T>(
StyledPropertyBase<T> property,
BindingPriority maxPriority,
out T value)
{
if (_values.TryGetValue(property, out var slot))
{
var v = (IValue<T>)slot;
var v = ((IValue<T>)slot).GetValue(maxPriority);
if (v.Value.HasValue)
if (v.HasValue)
{
value = v.Value.Value;
value = v.Value;
return true;
}
}
@ -90,17 +93,22 @@ namespace Avalonia
_values.AddValue(property, entry);
result = entry.SetValue(value, priority);
}
else if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, new LocalValueEntry<T>(value));
_sink.ValueChanged(property, priority, default, value);
}
else
{
var entry = new ConstantValueEntry<T>(property, value, priority, this);
_values.AddValue(property, entry);
_sink.ValueChanged(property, priority, default, value);
result = entry;
var change = new AvaloniaPropertyChangedEventArgs<T>(_owner, property, default, value, priority);
if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, new LocalValueEntry<T>(value));
_sink.ValueChanged(change);
}
else
{
var entry = new ConstantValueEntry<T>(property, value, priority, this);
_values.AddValue(property, entry);
_sink.ValueChanged(change);
result = entry;
}
}
return result;
@ -149,13 +157,14 @@ namespace Avalonia
if (remove)
{
var old = TryGetValue(property, out var value) ? value : default;
var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
_values.Remove(property);
_sink.ValueChanged(
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
BindingPriority.Unset,
old,
BindingValue<T>.Unset);
default,
BindingPriority.Unset));
}
}
}
@ -176,23 +185,20 @@ namespace Avalonia
{
if (_values.TryGetValue(property, out var slot))
{
var slotValue = slot.GetValue();
return new Diagnostics.AvaloniaPropertyValue(
property,
slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue,
slot.ValuePriority,
slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue,
slot.Priority,
null);
}
return null;
}
void IValueSink.ValueChanged<T>(
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue)
void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
_sink.ValueChanged(property, priority, oldValue, newValue);
_sink.ValueChanged(change);
}
void IValueSink.Completed<T>(
@ -232,9 +238,14 @@ namespace Avalonia
{
if (priority == BindingPriority.LocalValue)
{
var old = l.Value;
var old = l.GetValue(BindingPriority.LocalValue);
l.SetValue(value);
_sink.ValueChanged(property, priority, old, value);
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
old,
value,
priority));
}
else
{

5
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -1,5 +1,4 @@
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
@ -767,7 +766,7 @@ namespace Avalonia.Controls
/// <summary>
/// ItemsProperty property changed handler.
/// </summary>
/// <param name="e">AvaloniaPropertyChangedEventArgs.</param>
/// <param name="e">The event arguments.</param>
private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)

2
src/Avalonia.Controls/AutoCompleteBox.cs

@ -10,6 +10,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Collections;
@ -1100,6 +1101,7 @@ namespace Avalonia.Controls
{
_textBoxSubscriptions =
_textBox.GetObservable(TextBox.TextProperty)
.Skip(1)
.Subscribe(_ => OnTextBoxTextChanged());
if (Text != null)

12
src/Avalonia.Controls/Button.cs

@ -313,17 +313,13 @@ namespace Avalonia.Controls
IsPressed = false;
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == IsPressedProperty)
if (change.Property == IsPressedProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<bool>());
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
}
}

12
src/Avalonia.Controls/ButtonSpinner.cs

@ -205,17 +205,13 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == ButtonSpinnerLocationProperty)
if (change.Property == ButtonSpinnerLocationProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<Location>());
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<Location>());
}
}

12
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -510,17 +510,13 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == SelectedDateProperty)
if (change.Property == SelectedDateProperty)
{
DataValidationErrors.SetError(this, newValue.Error);
DataValidationErrors.SetError(this, change.NewValue.Error);
}
}

39
src/Avalonia.Controls/ContextMenu.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators;
@ -9,18 +10,19 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls
{
/// <summary>
/// A control context menu.
/// </summary>
public class ContextMenu : MenuBase
public class ContextMenu : MenuBase, ISetterValue
{
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
private Control _attachedControl;
private List<Control> _attachedControls;
private IInputElement _previousFocus;
/// <summary>
@ -74,13 +76,14 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu)
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
{
newMenu._attachedControl = control;
newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased;
}
}
@ -96,18 +99,22 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (control is null && _attachedControl is null)
if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
throw new ArgumentNullException(nameof(control));
}
if (control is object && _attachedControl is object && control != _attachedControl)
if (control is object &&
_attachedControls is object &&
!_attachedControls.Contains(control))
{
throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control));
}
control ??= _attachedControls[0];
if (IsOpen)
{
return;
@ -126,7 +133,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed;
}
((ISetLogicalParent)_popup).SetParent(control);
if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this;
_popup.IsOpen = true;
@ -155,6 +167,17 @@ namespace Avalonia.Controls
}
}
void ISetterValue.Initialize(ISetter setter)
{
// ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
// the behavior defined in Control which requires controls to be wrapped in a <template>.
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
@ -179,7 +202,7 @@ namespace Avalonia.Controls
SelectedIndex = -1;
IsOpen = false;
if (_attachedControl is null)
if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup).SetParent(null);
}

12
src/Avalonia.Controls/Expander.cs

@ -78,17 +78,13 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == ExpandDirectionProperty)
if (change.Property == ExpandDirectionProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<ExpandDirection>());
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<ExpandDirection>());
}
}

12
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -136,17 +136,13 @@ namespace Avalonia.Controls.Notifications
notificationControl.Close();
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == PositionProperty)
if (change.Property == PositionProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<NotificationPosition>());
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<NotificationPosition>());
}
}

16
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -26,9 +26,21 @@ namespace Avalonia.Platform
void SetTitle(string title);
/// <summary>
/// Shows the window as a dialog.
/// Sets the parent of the window.
/// </summary>
void ShowDialog(IWindowImpl parent);
/// <param name="parent">The parent <see cref="IWindowImpl"/>.</param>
void SetParent(IWindowImpl parent);
/// <summary>
/// Disables the window for example when a modal dialog is open.
/// </summary>
/// <param name="enable">true if the window is enabled, or false if it is disabled.</param>
void SetEnabled(bool enable);
/// <summary>
/// Called when a disabled window received input. Can be used to activate child windows.
/// </summary>
Action GotInputWhenDisabled { get; set; }
/// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc)

15
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -75,6 +75,9 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
AffectsRender<TextPresenter>(SelectionBrushProperty);
AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
@ -284,8 +287,6 @@ namespace Avalonia.Controls.Presenters
protected void InvalidateFormattedText()
{
_formattedText = null;
InvalidateMeasure();
}
/// <summary>
@ -301,13 +302,15 @@ namespace Avalonia.Controls.Presenters
context.FillRectangle(background, new Rect(Bounds.Size));
}
FormattedText.Constraint = Bounds.Size;
context.DrawText(Foreground, new Point(), FormattedText);
}
public override void Render(DrawingContext context)
{
FormattedText.Constraint = Bounds.Size;
_constraint = Bounds.Size;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
@ -316,10 +319,6 @@ namespace Avalonia.Controls.Presenters
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
// issue #600: set constraint before any FormattedText manipulation
// see base.Render(...) implementation
FormattedText.Constraint = _constraint;
var rects = FormattedText.HitTestTextRange(start, length);
foreach (var rect in rects)

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

@ -123,24 +123,20 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == OrientationProperty)
if (change.Property == OrientationProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<Orientation>());
}
else
{
if (property == MinimumProperty ||
property == MaximumProperty ||
property == ViewportSizeProperty ||
property == VisibilityProperty)
if (change.Property == MinimumProperty ||
change.Property == MaximumProperty ||
change.Property == ViewportSizeProperty ||
change.Property == VisibilityProperty)
{
UpdateIsVisible();
}

8
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -455,13 +455,13 @@ namespace Avalonia.Controls.Primitives
InternalEndInit();
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == SelectionModeProperty)
if (change.Property == SelectionModeProperty)
{
var mode = newValue.GetValueOrDefault<SelectionMode>();
var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
}

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

@ -280,17 +280,13 @@ namespace Avalonia.Controls.Primitives
return arrangeSize;
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == OrientationProperty)
if (change.Property == OrientationProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<Orientation>());
}
}

16
src/Avalonia.Controls/ProgressBar.cs

@ -83,21 +83,17 @@ namespace Avalonia.Controls
return base.ArrangeOverride(finalSize);
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == IsIndeterminateProperty)
if (change.Property == IsIndeterminateProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<bool>(), null);
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>(), null);
}
else if (property == OrientationProperty)
else if (change.Property == OrientationProperty)
{
UpdatePseudoClasses(null, newValue.GetValueOrDefault<Orientation>());
UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault<Orientation>());
}
}

56
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -375,37 +376,46 @@ namespace Avalonia.Controls
_viewportManager.ResetScrollers();
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (property == ItemsProperty)
if (change.Property == ItemsProperty)
{
var newEnumerable = newValue.GetValueOrDefault<IEnumerable>();
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
var oldEnumerable = change.OldValue.GetValueOrDefault<IEnumerable>();
var newEnumerable = change.NewValue.GetValueOrDefault<IEnumerable>();
if (oldEnumerable != newEnumerable)
{
newDataSource = new ItemsSourceView(newEnumerable);
}
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newEnumerable);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
}
else if (property == ItemTemplateProperty)
else if (change.Property == ItemTemplateProperty)
{
OnItemTemplateChanged(oldValue.GetValueOrDefault<IDataTemplate>(), newValue.GetValueOrDefault<IDataTemplate>());
OnItemTemplateChanged(
change.OldValue.GetValueOrDefault<IDataTemplate>(),
change.NewValue.GetValueOrDefault<IDataTemplate>());
}
else if (property == LayoutProperty)
else if (change.Property == LayoutProperty)
{
OnLayoutChanged(oldValue.GetValueOrDefault<AttachedLayout>(), newValue.GetValueOrDefault<AttachedLayout>());
OnLayoutChanged(
change.OldValue.GetValueOrDefault<AttachedLayout>(),
change.NewValue.GetValueOrDefault<AttachedLayout>());
}
else if (property == HorizontalCacheLengthProperty)
else if (change.Property == HorizontalCacheLengthProperty)
{
_viewportManager.HorizontalCacheLength = newValue.GetValueOrDefault<double>();
_viewportManager.HorizontalCacheLength = change.NewValue.GetValueOrDefault<double>();
}
else if (property == VerticalCacheLengthProperty)
else if (change.Property == VerticalCacheLengthProperty)
{
_viewportManager.VerticalCacheLength = newValue.GetValueOrDefault<double>();
_viewportManager.VerticalCacheLength = change.NewValue.GetValueOrDefault<double>();
}
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
}
internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
@ -431,8 +441,16 @@ namespace Avalonia.Controls
private int GetElementIndexImpl(IControl element)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
// Verify that element is actually a child of this ItemsRepeater
var parent = element.GetVisualParent();
if (parent == this)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
return -1;
}
private IControl GetElementFromIndexImpl(int index)

36
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -388,19 +388,24 @@ namespace Avalonia.Controls
}
case NotifyCollectionChangedAction.Reset:
if (_owner.ItemsSourceView.HasKeyIndexMapping)
// If we get multiple resets back to back before
// running layout, we dont have to clear all the elements again.
if (!_isDataSourceStableResetPending)
{
_isDataSourceStableResetPending = true;
}
if (_owner.ItemsSourceView.HasKeyIndexMapping)
{
_isDataSourceStableResetPending = true;
}
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
_owner.ClearElementImpl(element);
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{
_owner.ClearElementImpl(element);
}
}
}
@ -441,6 +446,9 @@ namespace Avalonia.Controls
}
_resetPool.Clear();
// Flush the realized indices once the stable reset pool is cleared to start fresh.
InvalidateRealizedIndicesHeldByLayout();
}
}
@ -498,6 +506,10 @@ namespace Avalonia.Controls
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
}
}
@ -519,6 +531,10 @@ namespace Avalonia.Controls
_pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
break;
}
}

35
src/Avalonia.Controls/SelectionModel.cs

@ -20,6 +20,7 @@ namespace Avalonia.Controls
private bool _singleSelect;
private bool _autoSelect;
private int _operationCount;
private IndexPath _oldAnchorIndex;
private IReadOnlyList<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _selectedItemsCached;
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
@ -142,6 +143,8 @@ namespace Avalonia.Controls
}
set
{
var oldValue = AnchorIndex;
if (value != null)
{
SelectionTreeHelper.TraverseIndexPath(
@ -155,7 +158,10 @@ namespace Avalonia.Controls
_rootNode.AnchorIndex = -1;
}
RaisePropertyChanged("AnchorIndex");
if (_operationCount == 0 && oldValue != AnchorIndex)
{
RaisePropertyChanged("AnchorIndex");
}
}
}
@ -633,19 +639,18 @@ namespace Avalonia.Controls
_selectedIndicesCached = null;
_selectedItemsCached = null;
// Raise SelectionChanged event
if (e != null)
{
SelectionChanged?.Invoke(this, e);
}
RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(SelectedIndices));
RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(SelectedIndices));
if (_rootNode.Source != null)
{
RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(SelectedItems));
if (_rootNode.Source != null)
{
RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(SelectedItems));
}
}
}
@ -785,6 +790,7 @@ namespace Avalonia.Controls
{
if (_operationCount++ == 0)
{
_oldAnchorIndex = AnchorIndex;
_rootNode.BeginOperation();
}
}
@ -808,13 +814,16 @@ namespace Avalonia.Controls
var changeSet = new SelectionModelChangeSet(changes);
e = changeSet.CreateEventArgs();
}
}
OnSelectionChanged(e);
OnSelectionChanged(e);
if (_oldAnchorIndex != AnchorIndex)
{
RaisePropertyChanged(nameof(AnchorIndex));
}
if (_operationCount == 0)
{
_rootNode.Cleanup();
_oldAnchorIndex = default;
}
}

34
src/Avalonia.Controls/Shapes/Path.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Controls.Shapes
@ -9,6 +8,8 @@ namespace Avalonia.Controls.Shapes
public static readonly StyledProperty<Geometry> DataProperty =
AvaloniaProperty.Register<Path, Geometry>(nameof(Data));
private EventHandler _geometryChangedHandler;
static Path()
{
AffectsGeometry<Path>(DataProperty);
@ -21,21 +22,48 @@ namespace Avalonia.Controls.Shapes
set { SetValue(DataProperty, value); }
}
private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= GeometryChanged;
protected override Geometry CreateDefiningGeometry() => Data;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (Data is object)
{
Data.Changed += GeometryChangedHandler;
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (Data is object)
{
Data.Changed -= GeometryChangedHandler;
}
}
private void DataChanged(AvaloniaPropertyChangedEventArgs e)
{
if (VisualRoot is null)
{
return;
}
var oldGeometry = (Geometry)e.OldValue;
var newGeometry = (Geometry)e.NewValue;
if (oldGeometry is object)
{
oldGeometry.Changed -= GeometryChanged;
oldGeometry.Changed -= GeometryChangedHandler;
}
if (newGeometry is object)
{
newGeometry.Changed += GeometryChanged;
newGeometry.Changed += GeometryChangedHandler;
}
}

111
src/Avalonia.Controls/Shapes/Shape.cs

@ -2,38 +2,67 @@ using System;
using Avalonia.Collections;
using Avalonia.Media;
#nullable enable
namespace Avalonia.Controls.Shapes
{
/// <summary>
/// Provides a base class for shape elements, such as <see cref="Ellipse"/>, <see cref="Polygon"/> and <see cref="Rectangle"/>.
/// </summary>
public abstract class Shape : Control
{
public static readonly StyledProperty<IBrush> FillProperty =
AvaloniaProperty.Register<Shape, IBrush>(nameof(Fill));
/// <summary>
/// Defines the <see cref="Fill"/> property.
/// </summary>
public static readonly StyledProperty<IBrush?> FillProperty =
AvaloniaProperty.Register<Shape, IBrush?>(nameof(Fill));
/// <summary>
/// Defines the <see cref="Stretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> StretchProperty =
AvaloniaProperty.Register<Shape, Stretch>(nameof(Stretch));
public static readonly StyledProperty<IBrush> StrokeProperty =
AvaloniaProperty.Register<Shape, IBrush>(nameof(Stroke));
/// <summary>
/// Defines the <see cref="Stroke"/> property.
/// </summary>
public static readonly StyledProperty<IBrush?> StrokeProperty =
AvaloniaProperty.Register<Shape, IBrush?>(nameof(Stroke));
public static readonly StyledProperty<AvaloniaList<double>> StrokeDashArrayProperty =
AvaloniaProperty.Register<Shape, AvaloniaList<double>>(nameof(StrokeDashArray));
/// <summary>
/// Defines the <see cref="StrokeDashArray"/> property.
/// </summary>
public static readonly StyledProperty<AvaloniaList<double>?> StrokeDashArrayProperty =
AvaloniaProperty.Register<Shape, AvaloniaList<double>?>(nameof(StrokeDashArray));
/// <summary>
/// Defines the <see cref="StrokeDashOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> StrokeDashOffsetProperty =
AvaloniaProperty.Register<Shape, double>(nameof(StrokeDashOffset));
/// <summary>
/// Defines the <see cref="StrokeThickness"/> property.
/// </summary>
public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<Shape, double>(nameof(StrokeThickness));
/// <summary>
/// Defines the <see cref="StrokeLineCap"/> property.
/// </summary>
public static readonly StyledProperty<PenLineCap> StrokeLineCapProperty =
AvaloniaProperty.Register<Shape, PenLineCap>(nameof(StrokeLineCap), PenLineCap.Flat);
/// <summary>
/// Defines the <see cref="StrokeJoin"/> property.
/// </summary>
public static readonly StyledProperty<PenLineJoin> StrokeJoinProperty =
AvaloniaProperty.Register<Shape, PenLineJoin>(nameof(StrokeJoin), PenLineJoin.Miter);
private Matrix _transform = Matrix.Identity;
private Geometry _definingGeometry;
private Geometry _renderedGeometry;
bool _calculateTransformOnArrange = false;
private Geometry? _definingGeometry;
private Geometry? _renderedGeometry;
private bool _calculateTransformOnArrange;
static Shape()
{
@ -43,7 +72,10 @@ namespace Avalonia.Controls.Shapes
StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty);
}
public Geometry DefiningGeometry
/// <summary>
/// Gets a value that represents the <see cref="Geometry"/> of the shape.
/// </summary>
public Geometry? DefiningGeometry
{
get
{
@ -56,13 +88,10 @@ namespace Avalonia.Controls.Shapes
}
}
public IBrush Fill
{
get { return GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
public Geometry RenderedGeometry
/// <summary>
/// Gets a value that represents the final rendered <see cref="Geometry"/> of the shape.
/// </summary>
public Geometry? RenderedGeometry
{
get
{
@ -93,42 +122,72 @@ namespace Avalonia.Controls.Shapes
}
}
/// <summary>
/// Gets or sets the <see cref="IBrush"/> that specifies how the shape's interior is painted.
/// </summary>
public IBrush? Fill
{
get { return GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="Stretch"/> enumeration value that describes how the shape fills its allocated space.
/// </summary>
public Stretch Stretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
public IBrush Stroke
/// <summary>
/// Gets or sets the <see cref="IBrush"/> that specifies how the shape's outline is painted.
/// </summary>
public IBrush? Stroke
{
get { return GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); }
}
public AvaloniaList<double> StrokeDashArray
/// <summary>
/// Gets or sets a collection of <see cref="double"/> values that indicate the pattern of dashes and gaps that is used to outline shapes.
/// </summary>
public AvaloniaList<double>? StrokeDashArray
{
get { return GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); }
}
/// <summary>
/// Gets or sets a value that specifies the distance within the dash pattern where a dash begins.
/// </summary>
public double StrokeDashOffset
{
get { return GetValue(StrokeDashOffsetProperty); }
set { SetValue(StrokeDashOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the width of the shape outline.
/// </summary>
public double StrokeThickness
{
get { return GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineCap"/> enumeration value that describes the shape at the ends of a line.
/// </summary>
public PenLineCap StrokeLineCap
{
get { return GetValue(StrokeLineCapProperty); }
set { SetValue(StrokeLineCapProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineJoin"/> enumeration value that specifies the type of join that is used at the vertices of a Shape.
/// </summary>
public PenLineJoin StrokeJoin
{
get { return GetValue(StrokeJoinProperty); }
@ -170,12 +229,20 @@ namespace Avalonia.Controls.Shapes
}
}
protected abstract Geometry CreateDefiningGeometry();
/// <summary>
/// Creates the shape's defining geometry.
/// </summary>
/// <returns>Defining <see cref="Geometry"/> of the shape.</returns>
protected abstract Geometry? CreateDefiningGeometry();
/// <summary>
/// Invalidates the geometry of this shape.
/// </summary>
protected void InvalidateGeometry()
{
_renderedGeometry = null;
_definingGeometry = null;
InvalidateMeasure();
}
@ -321,8 +388,8 @@ namespace Avalonia.Controls.Shapes
// portion changes.
if (e.Property == BoundsProperty)
{
var oldBounds = (Rect)e.OldValue;
var newBounds = (Rect)e.NewValue;
var oldBounds = (Rect)e.OldValue!;
var newBounds = (Rect)e.NewValue!;
if (oldBounds.Size == newBounds.Size)
{

12
src/Avalonia.Controls/Slider.cs

@ -134,17 +134,13 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == OrientationProperty)
if (change.Property == OrientationProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<Orientation>());
}
}

8
src/Avalonia.Controls/TreeView.cs

@ -486,13 +486,13 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == SelectionModeProperty)
if (change.Property == SelectionModeProperty)
{
var mode = newValue.GetValueOrDefault<SelectionMode>();
var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
}

121
src/Avalonia.Controls/Window.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
@ -68,6 +69,8 @@ namespace Avalonia.Controls
/// </summary>
public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot
{
private List<Window> _children = new List<Window>();
/// <summary>
/// Defines the <see cref="SizeToContent"/> property.
/// </summary>
@ -131,7 +134,7 @@ namespace Avalonia.Controls
/// </summary>
public static readonly RoutedEvent WindowClosedEvent =
RoutedEvent.Register<Window, RoutedEventArgs>("WindowClosed", RoutingStrategies.Direct);
/// <summary>
/// Routed event that can be used for global tracking of opening windows
/// </summary>
@ -183,6 +186,7 @@ namespace Avalonia.Controls
: base(impl)
{
impl.Closing = HandleClosing;
impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
@ -302,7 +306,7 @@ namespace Avalonia.Controls
PlatformImpl?.Move(value);
}
}
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler
/// </summary>
@ -323,7 +327,7 @@ namespace Avalonia.Controls
/// <summary>
/// Fired before a window is closed.
/// </summary>
public event EventHandler<CancelEventArgs> Closing;
public event EventHandler<CancelEventArgs> Closing;
/// <summary>
/// Closes the window.
@ -365,19 +369,59 @@ namespace Avalonia.Controls
{
if (close)
{
PlatformImpl?.Dispose();
CloseInternal();
}
}
}
private void CloseInternal()
{
foreach (var child in _children.ToList())
{
// if we HandleClosing() before then there will be no children.
child.CloseInternal();
}
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
Owner = null;
PlatformImpl?.Dispose();
}
/// <summary>
/// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
/// </summary>
protected virtual bool HandleClosing()
{
var args = new CancelEventArgs();
OnClosing(args);
return args.Cancel;
bool canClose = true;
foreach (var child in _children.ToList())
{
if (!child.HandleClosing())
{
child.CloseInternal();
}
else
{
canClose = false;
}
}
if (canClose)
{
var args = new CancelEventArgs();
OnClosing(args);
return args.Cancel;
}
else
{
return !canClose;
}
}
protected virtual void HandleWindowStateChanged(WindowState state)
@ -407,6 +451,14 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
Renderer?.Stop();
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
Owner = null;
PlatformImpl?.Hide();
}
@ -519,7 +571,10 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
PlatformImpl?.ShowDialog(owner.PlatformImpl);
PlatformImpl.SetParent(owner.PlatformImpl);
Owner = owner;
owner.AddChild(this);
PlatformImpl?.Show();
Renderer?.Start();
@ -541,6 +596,37 @@ namespace Avalonia.Controls
return result.Task;
}
private void UpdateEnabled()
{
PlatformImpl.SetEnabled(_children.Count == 0);
}
private void AddChild(Window window)
{
_children.Add(window);
UpdateEnabled();
}
private void RemoveChild(Window window)
{
_children.Remove(window);
UpdateEnabled();
}
private void OnGotInputWhenDisabled()
{
var firstChild = _children.FirstOrDefault();
if (firstChild != null)
{
firstChild.OnGotInputWhenDisabled();
}
else
{
Activate();
}
}
private void SetWindowStartupLocation(IWindowBaseImpl owner = null)
{
var scaling = owner?.Scaling ?? PlatformImpl?.Scaling ?? 1;
@ -631,6 +717,13 @@ namespace Avalonia.Controls
RaiseEvent(new RoutedEventArgs(WindowClosedEvent));
base.HandleClosed();
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
Owner = null;
}
/// <inheritdoc/>
@ -658,19 +751,15 @@ namespace Avalonia.Controls
/// </remarks>
protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e);
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (property == SystemDecorationsProperty)
if (change.Property == SystemDecorationsProperty)
{
var typedNewValue = newValue.GetValueOrDefault<SystemDecorations>();
var typedNewValue = change.NewValue.GetValueOrDefault<SystemDecorations>();
PlatformImpl?.SetSystemDecorations(typedNewValue);
var o = oldValue.GetValueOrDefault<SystemDecorations>() == SystemDecorations.Full;
var o = change.OldValue.GetValueOrDefault<SystemDecorations>() == SystemDecorations.Full;
var n = typedNewValue == SystemDecorations.Full;
if (o != n)

2
src/Avalonia.Controls/WindowBase.cs

@ -109,7 +109,7 @@ namespace Avalonia.Controls
public WindowBase Owner
{
get { return _owner; }
set { SetAndRaise(OwnerProperty, ref _owner, value); }
protected set { SetAndRaise(OwnerProperty, ref _owner, value); }
}
/// <summary>

9
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -83,6 +83,7 @@ namespace Avalonia.DesignerSupport.Remote
}
public IScreenImpl Screen { get; } = new ScreenStub();
public Action GotInputWhenDisabled { get; set; }
public void Activate()
{
@ -115,5 +116,13 @@ namespace Avalonia.DesignerSupport.Remote
public void SetTopmost(bool value)
{
}
public void SetParent(IWindowImpl parent)
{
}
public void SetEnabled(bool enable)
{
}
}
}

10
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -234,18 +234,10 @@ namespace Avalonia.DesignerSupport.Remote
}
catch (Exception e)
{
var xmlException = e as XmlException;
s_transport.Send(new UpdateXamlResultMessage
{
Error = e.ToString(),
Exception = new ExceptionDetails
{
ExceptionType = e.GetType().FullName,
Message = e.Message.ToString(),
LineNumber = xmlException?.LineNumber,
LinePosition = xmlException?.LinePosition,
}
Exception = new ExceptionDetails(e),
});
}
}

10
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -130,7 +130,17 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetParent(IWindowImpl parent)
{
}
public void SetEnabled(bool enable)
{
}
public IPopupPositioner PopupPositioner { get; }
public Action GotInputWhenDisabled { get; set; }
}
class ClipboardStub : IClipboard

12
src/Avalonia.Input/InputElement.cs

@ -526,17 +526,17 @@ namespace Avalonia.Input
{
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == IsFocusedProperty)
if (change.Property == IsFocusedProperty)
{
UpdatePseudoClasses(newValue.GetValueOrDefault<bool>(), null);
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>(), null);
}
else if (property == IsPointerOverProperty)
else if (change.Property == IsPointerOverProperty)
{
UpdatePseudoClasses(null, newValue.GetValueOrDefault<bool>());
UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault<bool>());
}
}

10
src/Avalonia.Layout/FlowLayoutAlgorithm.cs

@ -74,6 +74,7 @@ namespace Avalonia.Layout
double lineSpacing,
int maxItemsPerLine,
ScrollOrientation orientation,
bool disableVirtualization,
string layoutId)
{
_orientation.ScrollOrientation = orientation;
@ -95,14 +96,14 @@ namespace Avalonia.Layout
_elementManager.OnBeginMeasure(orientation);
int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
if (isWrapping && IsReflowRequired())
{
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
}
RaiseLineArranged();
@ -273,6 +274,7 @@ namespace Avalonia.Layout
double minItemSpacing,
double lineSpacing,
int maxItemsPerLine,
bool disableVirtualization,
string layoutId)
{
if (anchorIndex != -1)
@ -288,7 +290,7 @@ namespace Avalonia.Layout
bool lineNeedsReposition = false;
while (_elementManager.IsIndexValidInData(currentIndex) &&
ShouldContinueFillingUpSpace(previousIndex, direction))
(disableVirtualization || ShouldContinueFillingUpSpace(previousIndex, direction)))
{
// Ensure layout element.
_elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);

26
src/Avalonia.Layout/StackLayout.cs

@ -14,6 +14,12 @@ namespace Avalonia.Layout
/// </summary>
public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
/// <summary>
/// Defines the <see cref="DisableVirtualization"/> property.
/// </summary>
public static readonly StyledProperty<bool> DisableVirtualizationProperty =
AvaloniaProperty.Register<StackLayout, bool>(nameof(DisableVirtualization));
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
@ -36,6 +42,15 @@ namespace Avalonia.Layout
LayoutId = "StackLayout";
}
/// <summary>
/// Gets or sets a value indicating whether virtualization is disabled on the layout.
/// </summary>
public bool DisableVirtualization
{
get => GetValue(DisableVirtualizationProperty);
set => SetValue(DisableVirtualizationProperty, value);
}
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
@ -262,6 +277,8 @@ namespace Avalonia.Layout
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
((StackLayoutState)context.LayoutState).OnMeasureStart();
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
@ -270,6 +287,7 @@ namespace Avalonia.Layout
Spacing,
int.MaxValue,
_orientation.ScrollOrientation,
DisableVirtualization,
LayoutId);
return new Size(desiredSize.Width, desiredSize.Height);
@ -284,8 +302,6 @@ namespace Avalonia.Layout
FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId);
((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
return new Size(value.Width, value.Height);
}
@ -296,11 +312,11 @@ namespace Avalonia.Layout
InvalidateLayout();
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (property == OrientationProperty)
if (change.Property == OrientationProperty)
{
var orientation = newValue.GetValueOrDefault<Orientation>();
var orientation = change.NewValue.GetValueOrDefault<Orientation>();
//Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
//Horizontal Orientation means we have a Horizontal ScrollOrientation.

2
src/Avalonia.Layout/StackLayoutState.cs

@ -56,6 +56,6 @@ namespace Avalonia.Layout
MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize);
}
internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0;
internal void OnMeasureStart() => MaxArrangeBounds = 0;
}
}

36
src/Avalonia.Layout/UniformGridLayout.cs

@ -433,6 +433,7 @@ namespace Avalonia.Layout
LineSpacing,
_maximumRowsOrColumns,
_orientation.ScrollOrientation,
false,
LayoutId);
// If after Measure the first item is in the realization rect, then we revoke grid state's ownership,
@ -463,45 +464,44 @@ namespace Avalonia.Layout
gridState.ClearElementOnDataSourceChange(context, args);
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (property == OrientationProperty)
if (change.Property == OrientationProperty)
{
var orientation = newValue.GetValueOrDefault<Orientation>();
var orientation = change.NewValue.GetValueOrDefault<Orientation>();
//Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation.
//i.e. the properties are the inverse of each other.
var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal;
_orientation.ScrollOrientation = scrollOrientation;
}
else if (property == MinColumnSpacingProperty)
else if (change.Property == MinColumnSpacingProperty)
{
_minColumnSpacing = newValue.GetValueOrDefault<double>();
_minColumnSpacing = change.NewValue.GetValueOrDefault<double>();
}
else if (property == MinRowSpacingProperty)
else if (change.Property == MinRowSpacingProperty)
{
_minRowSpacing = newValue.GetValueOrDefault<double>();
_minRowSpacing = change.NewValue.GetValueOrDefault<double>();
}
else if (property == ItemsJustificationProperty)
else if (change.Property == ItemsJustificationProperty)
{
_itemsJustification = newValue.GetValueOrDefault<UniformGridLayoutItemsJustification>();
;
_itemsJustification = change.NewValue.GetValueOrDefault<UniformGridLayoutItemsJustification>();
}
else if (property == ItemsStretchProperty)
else if (change.Property == ItemsStretchProperty)
{
_itemsStretch = newValue.GetValueOrDefault<UniformGridLayoutItemsStretch>();
_itemsStretch = change.NewValue.GetValueOrDefault<UniformGridLayoutItemsStretch>();
}
else if (property == MinItemWidthProperty)
else if (change.Property == MinItemWidthProperty)
{
_minItemWidth = newValue.GetValueOrDefault<double>();
_minItemWidth = change.NewValue.GetValueOrDefault<double>();
}
else if (property == MinItemHeightProperty)
else if (change.Property == MinItemHeightProperty)
{
_minItemHeight = newValue.GetValueOrDefault<double>();
_minItemHeight = change.NewValue.GetValueOrDefault<double>();
}
else if (property == MaximumRowsOrColumnsProperty)
else if (change.Property == MaximumRowsOrColumnsProperty)
{
_maximumRowsOrColumns = newValue.GetValueOrDefault<int>();
_maximumRowsOrColumns = change.NewValue.GetValueOrDefault<int>();
}
InvalidateLayout();

5
src/Avalonia.Native/PopupImpl.cs

@ -43,6 +43,11 @@ namespace Avalonia.Native
_parent = parent;
}
public void GotInputWhenDisabled()
{
// NOP on Popup
}
bool IAvnWindowEvents.Closing()
{
return true;

26
src/Avalonia.Native/WindowImpl.cs

@ -40,7 +40,7 @@ namespace Avalonia.Native
bool IAvnWindowEvents.Closing()
{
if(_parent.Closing != null)
if (_parent.Closing != null)
{
return _parent.Closing();
}
@ -52,15 +52,15 @@ namespace Avalonia.Native
{
_parent.WindowStateChanged?.Invoke((WindowState)state);
}
void IAvnWindowEvents.GotInputWhenDisabled()
{
_parent.GotInputWhenDisabled?.Invoke();
}
}
public IAvnWindow Native => _native;
public void ShowDialog(IWindowImpl window)
{
_native.ShowDialog(((WindowImpl)window).Native);
}
public void CanResize(bool value)
{
_native.CanResize = value;
@ -71,7 +71,7 @@ namespace Avalonia.Native
_native.Decorations = (Interop.SystemDecorations)enabled;
}
public void SetTitleBarColor (Avalonia.Media.Color color)
public void SetTitleBarColor(Avalonia.Media.Color color)
{
_native.SetTitleBarColor(new AvnColor { Alpha = color.A, Red = color.R, Green = color.G, Blue = color.B });
}
@ -116,5 +116,17 @@ namespace Avalonia.Native
public override IPopupImpl CreatePopup() =>
_opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, _glFeature, this);
public Action GotInputWhenDisabled { get; set; }
public void SetParent(IWindowImpl parent)
{
_native.SetParent(((WindowImpl)parent).Native);
}
public void SetEnabled(bool enable)
{
_native.SetEnabled(enable);
}
}
}

24
src/Avalonia.Remote.Protocol/DesignMessages.cs

@ -1,4 +1,7 @@
using System;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Xml;
namespace Avalonia.Remote.Protocol.Designer
{
@ -26,6 +29,27 @@ namespace Avalonia.Remote.Protocol.Designer
public class ExceptionDetails
{
public ExceptionDetails()
{
}
public ExceptionDetails(Exception e)
{
if (e is TargetInvocationException)
{
e = e.InnerException;
}
ExceptionType = e.GetType().Name;
Message = e.Message;
if (e is XmlException xml)
{
LineNumber = xml.LineNumber;
LinePosition = xml.LinePosition;
}
}
public string ExceptionType { get; set; }
public string Message { get; set; }
public int? LineNumber { get; set; }

12
src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs

@ -24,6 +24,7 @@ namespace Avalonia.Styling
private BindingValue<T> _value;
private IDisposable? _subscription;
private IDisposable? _subscriptionTwoWay;
private IDisposable? _innerSubscription;
private bool _isActive;
public PropertySetterBindingInstance(
@ -121,6 +122,9 @@ namespace Avalonia.Styling
sub.Dispose();
}
_innerSubscription?.Dispose();
_innerSubscription = null;
base.Dispose();
}
@ -144,13 +148,13 @@ namespace Avalonia.Styling
protected override void Subscribed()
{
_subscription = _binding.Observable.Subscribe(_inner);
_innerSubscription = _binding.Observable.Subscribe(_inner);
}
protected override void Unsubscribed()
{
_subscription?.Dispose();
_subscription = null;
_innerSubscription?.Dispose();
_innerSubscription = null;
}
private void PublishNext()
@ -160,7 +164,7 @@ namespace Avalonia.Styling
private void ConvertAndPublishNext(object? value)
{
_value = value is T v ? v : BindingValue<object>.FromUntyped(value).Convert<T>();
_value = value is T v ? v : BindingValue<T>.FromUntyped(value);
if (_isActive)
{

4
src/Avalonia.Visuals/Media/BoxShadows.cs

@ -21,7 +21,7 @@ namespace Avalonia.Media
{
_first = shadow;
_list = null;
Count = 1;
Count = _first.IsEmpty ? 0 : 1;
}
public BoxShadows(BoxShadow first, BoxShadow[] rest)
@ -105,8 +105,6 @@ namespace Avalonia.Media
return false;
}
}
public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow);
public bool Equals(BoxShadows other)
{

8
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -141,13 +141,13 @@ namespace Avalonia.Media
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
/// </param>
/// <param name="boxShadow">Box shadow effect parameters</param>
/// <param name="boxShadows">Box shadow effect parameters</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0,
BoxShadow boxShadow = default)
BoxShadows boxShadows = default)
{
if (brush == null && !PenIsVisible(pen))
{
@ -164,7 +164,7 @@ namespace Avalonia.Media
radiusY = Math.Min(radiusY, rect.Height / 2);
}
PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadow);
PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows);
}
/// <summary>
@ -350,7 +350,7 @@ namespace Avalonia.Media
/// </summary>
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
PushedState PushSetTransform(Matrix matrix)
public PushedState PushSetTransform(Matrix matrix)
{
var oldMatrix = CurrentTransform;
CurrentTransform = matrix;

10
src/Avalonia.Visuals/Media/DrawingImage.cs

@ -63,15 +63,11 @@ namespace Avalonia.Media
}
/// <inheritdoc/>
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == DrawingProperty)
if (change.Property == DrawingProperty)
{
RaiseInvalidated(EventArgs.Empty);
}

44
src/Avalonia.Visuals/Media/Imaging/Bitmap.cs

@ -11,6 +11,46 @@ namespace Avalonia.Media.Imaging
/// </summary>
public class Bitmap : IBitmap
{
/// <summary>
/// Loads a Bitmap from a stream and decodes at the desired width. Aspect ratio is maintained.
/// This is more efficient than loading and then resizing.
/// </summary>
/// <param name="stream">The stream to read the bitmap from. This can be any supported image format.</param>
/// <param name="width">The desired width of the resulting bitmap.</param>
/// <param name="interpolationMode">The <see cref="BitmapInterpolationMode"/> to use should any scaling be required.</param>
/// <returns>An instance of the <see cref="Bitmap"/> class.</returns>
public static Bitmap DecodeToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return new Bitmap(factory.LoadBitmapToWidth(stream, width, interpolationMode));
}
/// <summary>
/// Loads a Bitmap from a stream and decodes at the desired height. Aspect ratio is maintained.
/// This is more efficient than loading and then resizing.
/// </summary>
/// <param name="stream">The stream to read the bitmap from. This can be any supported image format.</param>
/// <param name="height">The desired height of the resulting bitmap.</param>
/// <param name="interpolationMode">The <see cref="BitmapInterpolationMode"/> to use should any scaling be required.</param>
/// <returns>An instance of the <see cref="Bitmap"/> class.</returns>
public static Bitmap DecodeToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return new Bitmap(factory.LoadBitmapToHeight(stream, height, interpolationMode));
}
/// <summary>
/// Creates a Bitmap scaled to a specified size from the current bitmap.
/// </summary>
/// <param name="destinationSize">The destination size.</param>
/// <param name="interpolationMode">The <see cref="BitmapInterpolationMode"/> to use should any scaling be required.</param>
/// <returns>An instance of the <see cref="Bitmap"/> class.</returns>
public Bitmap CreateScaledBitmap(PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return new Bitmap(factory.ResizeBitmap(PlatformImpl.Item, destinationSize, interpolationMode));
}
/// <summary>
/// Initializes a new instance of the <see cref="Bitmap"/> class.
/// </summary>
@ -39,7 +79,7 @@ namespace Avalonia.Media.Imaging
{
PlatformImpl = impl.Clone();
}
/// <summary>
/// Initializes a new instance of the <see cref="Bitmap"/> class.
/// </summary>
@ -48,7 +88,7 @@ namespace Avalonia.Media.Imaging
{
PlatformImpl = RefCountable.Create(impl);
}
/// <inheritdoc/>
public virtual void Dispose()
{

15
src/Avalonia.Visuals/Media/PixelRect.cs

@ -377,7 +377,7 @@ namespace Avalonia
/// <returns>The device-independent rect.</returns>
public static PixelRect FromRect(Rect rect, double scale) => new PixelRect(
PixelPoint.FromPoint(rect.Position, scale),
PixelSize.FromSize(rect.Size, scale));
FromPointCeiling(rect.BottomRight, new Vector(scale, scale)));
/// <summary>
/// Converts a <see cref="Rect"/> to device pixels using the specified scaling factor.
@ -387,7 +387,7 @@ namespace Avalonia
/// <returns>The device-independent point.</returns>
public static PixelRect FromRect(Rect rect, Vector scale) => new PixelRect(
PixelPoint.FromPoint(rect.Position, scale),
PixelSize.FromSize(rect.Size, scale));
FromPointCeiling(rect.BottomRight, scale));
/// <summary>
/// Converts a <see cref="Rect"/> to device pixels using the specified dots per inch (DPI).
@ -397,7 +397,7 @@ namespace Avalonia
/// <returns>The device-independent point.</returns>
public static PixelRect FromRectWithDpi(Rect rect, double dpi) => new PixelRect(
PixelPoint.FromPointWithDpi(rect.Position, dpi),
PixelSize.FromSizeWithDpi(rect.Size, dpi));
FromPointCeiling(rect.BottomRight, new Vector(dpi / 96, dpi / 96)));
/// <summary>
/// Converts a <see cref="Rect"/> to device pixels using the specified dots per inch (DPI).
@ -407,7 +407,7 @@ namespace Avalonia
/// <returns>The device-independent point.</returns>
public static PixelRect FromRectWithDpi(Rect rect, Vector dpi) => new PixelRect(
PixelPoint.FromPointWithDpi(rect.Position, dpi),
PixelSize.FromSizeWithDpi(rect.Size, dpi));
FromPointCeiling(rect.BottomRight, dpi / 96));
/// <summary>
/// Returns the string representation of the rectangle.
@ -441,5 +441,12 @@ namespace Avalonia
);
}
}
private static PixelPoint FromPointCeiling(Point point, Vector scale)
{
return new PixelPoint(
(int)Math.Ceiling(point.X * scale.X),
(int)Math.Ceiling(point.Y * scale.Y));
}
}
}

26
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Platform
{
@ -87,17 +89,37 @@ namespace Avalonia.Platform
/// <summary>
/// Loads a bitmap implementation from a file..
/// </summary>
/// <param name="fileName">The filename of the bitmap.</param>
/// <param name="fileName">The filename of the bitmap.</param>
/// <returns>An <see cref="IBitmapImpl"/>.</returns>
IBitmapImpl LoadBitmap(string fileName);
/// <summary>
/// Loads a bitmap implementation from a file..
/// </summary>
/// <param name="stream">The stream to read the bitmap from.</param>
/// <param name="stream">The stream to read the bitmap from.</param>
/// <returns>An <see cref="IBitmapImpl"/>.</returns>
IBitmapImpl LoadBitmap(Stream stream);
/// <summary>
/// Loads a bitmap implementation from a stream to a specified width maintaining aspect ratio.
/// </summary>
/// <param name="stream">The stream to read the bitmap from.</param>
/// <param name="width">The desired width of the resulting bitmap.</param>
/// <param name="interpolationMode">The <see cref="BitmapInterpolationMode"/> to use should resizing be required.</param>
/// <returns>An <see cref="IBitmapImpl"/>.</returns>
IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality);
/// <summary>
/// Loads a bitmap implementation from a stream to a specified height maintaining aspect ratio.
/// </summary>
/// <param name="stream">The stream to read the bitmap from.</param>
/// <param name="height">The desired height of the resulting bitmap.</param>
/// <param name="interpolationMode">The <see cref="BitmapInterpolationMode"/> to use should resizing be required.</param>
/// <returns>An <see cref="IBitmapImpl"/>.</returns>
IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality);
IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality);
/// <summary>
/// Loads a bitmap implementation from a pixels in memory.
/// </summary>

49
src/Avalonia.Visuals/Rect.cs

@ -421,11 +421,50 @@ namespace Avalonia
}
/// <summary>
/// Gets the union of two rectangles.
/// </summary>
/// <param name="rect">The other rectangle.</param>
/// <returns>The union.</returns>
public Rect Union(Rect rect)
/// Normalizes the rectangle so both the <see cref="Width"/> and <see
/// cref="Height"/> are positive, without changing the location of the rectangle
/// </summary>
/// <returns>Normalized Rect</returns>
/// <remarks>
/// Empty rect will be return when Rect contains invalid values. Like NaN.
/// </remarks>
public Rect Normalize()
{
Rect rect = this;
if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) ||
double.IsNaN(rect.X) || double.IsNaN(rect.Y) ||
double.IsNaN(Height) || double.IsNaN(Width))
{
return Rect.Empty;
}
if (rect.Width < 0)
{
var x = X + Width;
var width = X - x;
rect = rect.WithX(x).WithWidth(width);
}
if (rect.Height < 0)
{
var y = Y + Height;
var height = Y - y;
rect = rect.WithY(y).WithHeight(height);
}
return rect;
}
/// <summary>
/// Gets the union of two rectangles.
/// </summary>
/// <param name="rect">The other rectangle.</param>
/// <returns>The union.</returns>
public Rect Union(Rect rect)
{
if (IsEmpty)
{

11
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -443,11 +443,12 @@ namespace Avalonia.Rendering
private static Rect SnapToDevicePixels(Rect rect, double scale)
{
return new Rect(
Math.Floor(rect.X * scale) / scale,
Math.Floor(rect.Y * scale) / scale,
Math.Ceiling(rect.Width * scale) / scale,
Math.Ceiling(rect.Height * scale) / scale);
new Point(
Math.Floor(rect.X * scale) / scale,
Math.Floor(rect.Y * scale) / scale),
new Point(
Math.Ceiling(rect.Right * scale) / scale,
Math.Ceiling(rect.Bottom * scale) / scale));
}
private void RenderOverlay(Scene scene, ref IDrawingContextImpl parentContent)

4
src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs

@ -9,8 +9,8 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
internal abstract class BrushDrawOperation : DrawOperation
{
public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen)
: base(bounds, transform, pen)
public BrushDrawOperation(Rect bounds, Matrix transform)
: base(bounds, transform)
{
}

2
src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs

@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph
public Matrix Transform { get; }
public ICustomDrawOperation Custom { get; }
public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform)
: base(custom.Bounds, transform, null)
: base(custom.Bounds, transform)
{
Transform = transform;
Custom = custom;

2
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -364,7 +364,7 @@ namespace Avalonia.Rendering.SceneGraph
public int DrawOperationIndex { get; }
}
private void Add(IDrawOperation node)
private void Add<T>(T node) where T : class, IDrawOperation
{
using (var refCounted = RefCountable.Create(node))
{

5
src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs

@ -9,9 +9,10 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
internal abstract class DrawOperation : IDrawOperation
{
public DrawOperation(Rect bounds, Matrix transform, IPen pen)
public DrawOperation(Rect bounds, Matrix transform)
{
bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform);
bounds = bounds.Normalize().TransformToAABB(transform);
Bounds = new Rect(
new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)),
new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom)));

2
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@ -24,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph
IPen pen,
IGeometryImpl geometry,
IDictionary<IVisual, Scene> childScenes = null)
: base(geometry.GetRenderBounds(pen), transform, null)
: base(geometry.GetRenderBounds(pen), transform)
{
Transform = transform;
Brush = brush?.ToImmutable();

2
src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs

@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
GlyphRun glyphRun,
Point baselineOrigin,
IDictionary<IVisual, Scene> childScenes = null)
: base(glyphRun.Bounds, transform, null)
: base(glyphRun.Bounds, transform)
{
Transform = transform;
Foreground = foreground?.ToImmutable();

2
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="destRect">The destination rect.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
: base(destRect, transform, null)
: base(destRect, transform)
{
Transform = transform;
Source = source.Clone();

68
src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs

@ -0,0 +1,68 @@
using System;
using Avalonia.Media;
namespace Avalonia.Rendering.SceneGraph
{
internal static class LineBoundsHelper
{
private static double CalculateAngle(Point p1, Point p2)
{
var xDiff = p2.X - p1.X;
var yDiff = p2.Y - p1.Y;
return Math.Atan2(yDiff, xDiff);
}
internal static double CalculateOppSide(double angle, double hyp)
{
return Math.Sin(angle) * hyp;
}
internal static double CalculateAdjSide(double angle, double hyp)
{
return Math.Cos(angle) * hyp;
}
private static (Point p1, Point p2) TranslatePointsAlongTangent(Point p1, Point p2, double angle, double distance)
{
var xDiff = CalculateOppSide(angle, distance);
var yDiff = CalculateAdjSide(angle, distance);
var c1 = new Point(p1.X + xDiff, p1.Y - yDiff);
var c2 = new Point(p1.X - xDiff, p1.Y + yDiff);
var c3 = new Point(p2.X + xDiff, p2.Y - yDiff);
var c4 = new Point(p2.X - xDiff, p2.Y + yDiff);
var minX = Math.Min(c1.X, Math.Min(c2.X, Math.Min(c3.X, c4.X)));
var minY = Math.Min(c1.Y, Math.Min(c2.Y, Math.Min(c3.Y, c4.Y)));
var maxX = Math.Max(c1.X, Math.Max(c2.X, Math.Max(c3.X, c4.X)));
var maxY = Math.Max(c1.Y, Math.Max(c2.Y, Math.Max(c3.Y, c4.Y)));
return (new Point(minX, minY), new Point(maxX, maxY));
}
private static Rect CalculateBounds(Point p1, Point p2, double thickness, double angleToCorner)
{
var pts = TranslatePointsAlongTangent(p1, p2, angleToCorner, thickness / 2);
return new Rect(pts.p1, pts.p2);
}
public static Rect CalculateBounds(Point p1, Point p2, IPen p)
{
var radians = CalculateAngle(p1, p2);
if (p.LineCap != PenLineCap.Flat)
{
var pts = TranslatePointsAlongTangent(p1, p2, radians - Math.PI / 2, p.Thickness / 2);
return CalculateBounds(pts.p1, pts.p2, p.Thickness, radians);
}
else
{
return CalculateBounds(p1, p2, p.Thickness, radians);
}
}
}
}

2
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
Point p1,
Point p2,
IDictionary<IVisual, Scene> childScenes = null)
: base(new Rect(p1, p2), transform, pen)
: base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform)
{
Transform = transform;
Pen = pen?.ToImmutable();

4
src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs

@ -18,7 +18,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="bounds">The bounds of the mask.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary<IVisual, Scene> childScenes = null)
: base(Rect.Empty, Matrix.Identity, null)
: base(Rect.Empty, Matrix.Identity)
{
Mask = mask?.ToImmutable();
MaskBounds = bounds;
@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph
/// opacity mask pop.
/// </summary>
public OpacityMaskNode()
: base(Rect.Empty, Matrix.Identity, null)
: base(Rect.Empty, Matrix.Identity)
{
}

2
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
RoundedRect rect,
BoxShadows boxShadows,
IDictionary<IVisual, Scene> childScenes = null)
: base(boxShadows.TransformBounds(rect.Rect), transform, pen)
: base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform)
{
Transform = transform;
Brush = brush?.ToImmutable();

2
src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs

@ -24,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph
Point origin,
IFormattedTextImpl text,
IDictionary<IVisual, Scene> childScenes = null)
: base(text.Bounds.Translate(origin), transform, null)
: base(text.Bounds.Translate(origin), transform)
{
Transform = transform;
Foreground = foreground?.ToImmutable();

5
src/Avalonia.Visuals/Visual.cs

@ -114,6 +114,9 @@ namespace Avalonia
/// </summary>
public Visual()
{
// Disable transitions until we're added to the visual tree.
DisableTransitions();
var visualChildren = new AvaloniaList<IVisual>();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.Validate = visual => ValidateVisualChild(visual);
@ -393,6 +396,7 @@ namespace Avalonia
RenderTransform.Changed += RenderTransformChanged;
}
EnableTransitions();
OnAttachedToVisualTree(e);
AttachedToVisualTree?.Invoke(this, e);
InvalidateVisual();
@ -429,6 +433,7 @@ namespace Avalonia
RenderTransform.Changed -= RenderTransformChanged;
}
DisableTransitions();
OnDetachedFromVisualTree(e);
DetachedFromVisualTree?.Invoke(this, e);
e.Root?.Renderer?.AddDirty(this);

49
src/Avalonia.X11/X11Window.cs

@ -41,9 +41,9 @@ namespace Avalonia.X11
private IntPtr _renderHandle;
private bool _mapped;
private bool _wasMappedAtLeastOnce = false;
private HashSet<X11Window> _transientChildren = new HashSet<X11Window>();
private X11Window _transientParent;
private double? _scalingOverride;
private bool _disabled;
public object SyncRoot { get; } = new object();
class InputEventContainer
@ -746,7 +746,6 @@ namespace Avalonia.X11
void Cleanup()
{
SetTransientParent(null, false);
if (_xic != IntPtr.Zero)
{
XDestroyIC(_xic);
@ -773,38 +772,24 @@ namespace Avalonia.X11
bool ActivateTransientChildIfNeeded()
{
if (_transientChildren.Count == 0)
return false;
var child = _transientChildren.First();
if (!child.ActivateTransientChildIfNeeded())
child.Activate();
return true;
}
void SetTransientParent(X11Window window, bool informServer = true)
{
_transientParent?._transientChildren.Remove(this);
_transientParent = window;
_transientParent?._transientChildren.Add(this);
if (informServer)
SetTransientForHint(_transientParent?._handle);
if (_disabled)
{
GotInputWhenDisabled?.Invoke();
return true;
}
return false;
}
void SetTransientForHint(IntPtr? parent)
public void SetParent(IWindowImpl parent)
{
if (parent == null || parent == IntPtr.Zero)
if (parent == null || parent.Handle == null || parent.Handle.Handle == IntPtr.Zero)
XDeleteProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_TRANSIENT_FOR);
else
XSetTransientForHint(_x11.Display, _handle, parent.Value);
XSetTransientForHint(_x11.Display, _handle, parent.Handle.Handle);
}
public void Show()
{
SetTransientParent(null);
ShowCore();
}
void ShowCore()
{
_wasMappedAtLeastOnce = true;
XMapWindow(_x11.Display, _handle);
@ -813,7 +798,6 @@ namespace Avalonia.X11
public void Hide() => XUnmapWindow(_x11.Display, _handle);
public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / Scaling, (point.Y - Position.Y) / Scaling);
public PixelPoint PointToScreen(Point point) => new PixelPoint(
@ -1034,13 +1018,14 @@ namespace Avalonia.X11
{
ChangeWMAtoms(value, _x11.Atoms._NET_WM_STATE_ABOVE);
}
public void ShowDialog(IWindowImpl parent)
public void SetEnabled(bool enable)
{
SetTransientParent((X11Window)parent);
ShowCore();
_disabled = !enable;
}
public Action GotInputWhenDisabled { get; set; }
public void SetIcon(IWindowIconImpl icon)
{
if (icon != null)

19
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -124,29 +124,12 @@ namespace Avalonia.Skia
Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity))
})
{
paint.FilterQuality = GetInterpolationMode(bitmapInterpolationMode);
paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality();
drawableImage.Draw(this, s, d, paint);
}
}
private static SKFilterQuality GetInterpolationMode(BitmapInterpolationMode interpolationMode)
{
switch (interpolationMode)
{
case BitmapInterpolationMode.LowQuality:
return SKFilterQuality.Low;
case BitmapInterpolationMode.MediumQuality:
return SKFilterQuality.Medium;
case BitmapInterpolationMode.HighQuality:
return SKFilterQuality.High;
case BitmapInterpolationMode.Default:
return SKFilterQuality.None;
default:
throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null);
}
}
/// <inheritdoc />
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{

2
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -95,7 +95,7 @@ namespace Avalonia.Skia
UpdatePathCache(strokeWidth);
}
return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0);
return _pathCache.CachedGeometryRenderBounds;
}
/// <inheritdoc />

73
src/Skia/Avalonia.Skia/ImmutableBitmap.cs

@ -1,7 +1,11 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Skia.Helpers;
using Avalonia.Visuals.Media.Imaging;
using SkiaSharp;
namespace Avalonia.Skia
@ -36,6 +40,75 @@ namespace Avalonia.Skia
}
}
public ImmutableBitmap(ImmutableBitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode)
{
SKImageInfo info = new SKImageInfo(destinationSize.Width, destinationSize.Height, SKColorType.Bgra8888);
SKImage output = SKImage.Create(info);
src._image.ScalePixels(output.PeekPixels(), interpolationMode.ToSKFilterQuality());
_image = output;
PixelSize = new PixelSize(_image.Width, _image.Height);
// TODO: Skia doesn't have an API for DPI.
Dpi = new Vector(96, 96);
}
public ImmutableBitmap(Stream stream, int decodeSize, bool horizontal, BitmapInterpolationMode interpolationMode)
{
using (var skStream = new SKManagedStream(stream))
using (var codec = SKCodec.Create(skStream))
{
var info = codec.Info;
// get the scale that is nearest to what we want (eg: jpg returned 512)
var supportedScale = codec.GetScaledDimensions(horizontal ? ((float)decodeSize / info.Width) : ((float)decodeSize / info.Height));
// decode the bitmap at the nearest size
var nearest = new SKImageInfo(supportedScale.Width, supportedScale.Height);
var bmp = SKBitmap.Decode(codec, nearest);
// now scale that to the size that we want
var realScale = horizontal ? ((double)info.Height / info.Width) : ((double)info.Width / info.Height);
SKImageInfo desired;
if (horizontal)
{
desired = new SKImageInfo(decodeSize, (int)(realScale * decodeSize));
}
else
{
desired = new SKImageInfo((int)(realScale * decodeSize), decodeSize);
}
if (bmp.Width != desired.Width || bmp.Height != desired.Height)
{
if (bmp.Height != bmp.Width)
{
}
var scaledBmp = bmp.Resize(desired, interpolationMode.ToSKFilterQuality());
bmp.Dispose();
bmp = scaledBmp;
}
_image = SKImage.FromBitmap(bmp);
bmp.Dispose();
if (_image == null)
{
throw new ArgumentException("Unable to load bitmap from provided data");
}
PixelSize = new PixelSize(_image.Width, _image.Height);
// TODO: Skia doesn't have an API for DPI.
Dpi = new Vector(96, 96);
}
}
/// <summary>
/// Create immutable bitmap from given pixel data copy.
/// </summary>

42
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -1,13 +1,16 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO;
using System.Security.Cryptography;
using System.Linq;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Imaging;
using Avalonia.Platform;
using Avalonia.Visuals.Media.Imaging;
using SkiaSharp;
namespace Avalonia.Skia
@ -57,12 +60,6 @@ namespace Avalonia.Skia
return new StreamGeometryImpl();
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(Stream stream)
{
return new ImmutableBitmap(stream);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)
{
@ -72,12 +69,43 @@ namespace Avalonia.Skia
}
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(Stream stream)
{
return new ImmutableBitmap(stream);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
{
return new ImmutableBitmap(size, dpi, stride, format, data);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
return new ImmutableBitmap(stream, width, true, interpolationMode);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
return new ImmutableBitmap(stream, height, false, interpolationMode);
}
/// <inheritdoc />
public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
if (bitmapImpl is ImmutableBitmap ibmp)
{
return new ImmutableBitmap(ibmp, destinationSize, interpolationMode);
}
else
{
throw new Exception("Invalid source bitmap type.");
}
}
/// <inheritdoc />
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
{

18
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -1,12 +1,30 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Visuals.Media.Imaging;
using SkiaSharp;
namespace Avalonia.Skia
{
public static class SkiaSharpExtensions
{
public static SKFilterQuality ToSKFilterQuality(this BitmapInterpolationMode interpolationMode)
{
switch (interpolationMode)
{
case BitmapInterpolationMode.LowQuality:
return SKFilterQuality.Low;
case BitmapInterpolationMode.MediumQuality:
return SKFilterQuality.Medium;
case BitmapInterpolationMode.HighQuality:
return SKFilterQuality.High;
case BitmapInterpolationMode.Default:
return SKFilterQuality.None;
default:
throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null);
}
}
public static SKPoint ToSKPoint(this Point p)
{
return new SKPoint((float)p.X, (float)p.Y);

24
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -7,7 +7,9 @@ using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Visuals.Media.Imaging;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment;
@ -179,16 +181,38 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect);
public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl();
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)
{
return new WicBitmapImpl(fileName);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(Stream stream)
{
return new WicBitmapImpl(stream);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
return new WicBitmapImpl(stream, width, true, interpolationMode);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
return new WicBitmapImpl(stream, height, false, interpolationMode);
}
/// <inheritdoc />
public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
// https://github.com/sharpdx/SharpDX/issues/959 blocks implementation.
throw new NotImplementedException();
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
{
return new WicBitmapImpl(format, data, size, dpi, stride);

66
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Security.Cryptography;
using Avalonia.Win32.Interop;
using SharpDX.WIC;
using APixelFormat = Avalonia.Platform.PixelFormat;
@ -14,6 +15,26 @@ namespace Avalonia.Direct2D1.Media
{
private BitmapDecoder _decoder;
private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode)
{
switch (interpolationMode)
{
case Visuals.Media.Imaging.BitmapInterpolationMode.Default:
return BitmapInterpolationMode.Fant;
case Visuals.Media.Imaging.BitmapInterpolationMode.LowQuality:
return BitmapInterpolationMode.NearestNeighbor;
case Visuals.Media.Imaging.BitmapInterpolationMode.MediumQuality:
return BitmapInterpolationMode.Fant;
default:
case Visuals.Media.Imaging.BitmapInterpolationMode.HighQuality:
return BitmapInterpolationMode.HighQualityCubic;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="WicBitmapImpl"/> class.
/// </summary>
@ -27,6 +48,12 @@ namespace Avalonia.Direct2D1.Media
}
}
private WicBitmapImpl(Bitmap bmp)
{
WicImpl = bmp;
Dpi = new Vector(96, 96);
}
/// <summary>
/// Initializes a new instance of the <see cref="WicBitmapImpl"/> class.
/// </summary>
@ -60,7 +87,7 @@ namespace Avalonia.Direct2D1.Media
size.Height,
pixelFormat.Value.ToWic(),
BitmapCreateCacheOption.CacheOnLoad);
WicImpl.SetResolution(dpi.X, dpi.Y);
Dpi = dpi;
}
@ -84,6 +111,43 @@ namespace Avalonia.Direct2D1.Media
}
}
public WicBitmapImpl(Stream stream, int decodeSize, bool horizontal, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode)
{
_decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad);
var frame = _decoder.GetFrame(0);
// now scale that to the size that we want
var realScale = horizontal ? ((double)frame.Size.Height / frame.Size.Width) : ((double)frame.Size.Width / frame.Size.Height);
PixelSize desired;
if (horizontal)
{
desired = new PixelSize(decodeSize, (int)(realScale * decodeSize));
}
else
{
desired = new PixelSize((int)(realScale * decodeSize), decodeSize);
}
if (frame.Size.Width != desired.Width || frame.Size.Height != desired.Height)
{
using (var scaler = new BitmapScaler(Direct2D1Platform.ImagingFactory))
{
scaler.Initialize(frame, desired.Width, desired.Height, ConvertInterpolationMode(interpolationMode));
WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, scaler, BitmapCreateCacheOption.CacheOnLoad);
}
}
else
{
WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, frame, BitmapCreateCacheOption.CacheOnLoad);
}
Dpi = new Vector(96, 96);
}
public override Vector Dpi { get; }
public override PixelSize PixelSize => WicImpl.Size.ToAvalonia();

6
src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs

@ -77,12 +77,6 @@ namespace Avalonia.Win32
s_instances.Remove(this);
Closed?.Invoke();
if (_parent != null)
{
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
}
_mouseDevice.Dispose();
_touchDevice?.Dispose();
//Free other resources

29
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -45,7 +45,6 @@ namespace Avalonia.Win32
#endif
private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE);
private readonly List<WindowImpl> _disabledBy;
private readonly TouchDevice _touchDevice;
private readonly MouseDevice _mouseDevice;
private readonly ManagedDeferredRendererLock _rendererLock;
@ -70,7 +69,6 @@ namespace Avalonia.Win32
public WindowImpl()
{
_disabledBy = new List<WindowImpl>();
_touchDevice = new TouchDevice();
_mouseDevice = new WindowsMouseDevice();
@ -342,31 +340,25 @@ namespace Avalonia.Win32
public void Hide()
{
if (_parent != null)
{
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
_parent = null;
}
UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide);
}
public virtual void Show()
{
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, IntPtr.Zero);
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent != null ? _parent._hwnd : IntPtr.Zero);
ShowWindow(_showWindowState);
}
public void ShowDialog(IWindowImpl parent)
public Action GotInputWhenDisabled { get; set; }
public void SetParent(IWindowImpl parent)
{
_parent = (WindowImpl)parent;
_parent._disabledBy.Add(this);
_parent.UpdateEnabled();
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, ((WindowImpl)parent)._hwnd);
ShowWindow(_showWindowState);
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent._hwnd);
}
public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable);
public void BeginMoveDrag(PointerPressedEventArgs e)
{
_mouseDevice.Capture(null);
@ -666,7 +658,7 @@ namespace Avalonia.Win32
}
}
private WindowStyles GetWindowStateStyles ()
private WindowStyles GetWindowStateStyles()
{
return GetStyle() & WindowStateMask;
}
@ -721,11 +713,6 @@ namespace Avalonia.Win32
}
}
private void UpdateEnabled()
{
EnableWindow(_hwnd, _disabledBy.Count == 0);
}
private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false)
{
var oldProperties = _windowProperties;

339
tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

@ -0,0 +1,339 @@
using System;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Animation.UnitTests
{
public class AnimatableTests
{
[Fact]
public void Transition_Is_Not_Applied_When_Not_Attached_To_Visual_Tree()
{
var target = CreateTarget();
var control = new Control
{
Transitions = new Transitions { target.Object },
};
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5),
Times.Never);
}
[Fact]
public void Transition_Is_Not_Applied_To_Initial_Style()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
{
var target = CreateTarget();
var control = new Control
{
Transitions = new Transitions { target.Object },
};
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Control>())
{
Setters =
{
new Setter(Visual.OpacityProperty, 0.8),
}
}
}
};
root.Child = control;
Assert.Equal(0.8, control.Opacity);
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
}
[Fact]
public void Transition_Is_Applied_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
}
[Fact]
public void Transition_Is_Not_Applied_When_Animated_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.SetValue(Visual.OpacityProperty, 0.5, BindingPriority.Animation);
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5),
Times.Never);
}
[Fact]
public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.SetValue(Visual.OpacityProperty, 0.5);
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
target.ResetCalls();
control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger);
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
[Fact]
public void Transition_Is_Disposed_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
var sub = new Mock<IDisposable>();
target.Setup(x => x.Apply(control, It.IsAny<IClock>(), 1.0, 0.5)).Returns(sub.Object);
control.Opacity = 0.5;
sub.ResetCalls();
control.Opacity = 0.4;
sub.Verify(x => x.Dispose());
}
[Fact]
public void New_Transition_Is_Applied_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
target.Setup(x => x.Property).Returns(Visual.OpacityProperty);
target.Setup(x => x.Apply(control, It.IsAny<IClock>(), 1.0, 0.5))
.Callback(() =>
{
control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation);
})
.Returns(Mock.Of<IDisposable>());
control.Opacity = 0.5;
Assert.Equal(0.9, control.Opacity);
target.ResetCalls();
control.Opacity = 0.4;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
0.9,
0.4));
}
[Fact]
public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
target.ResetCalls();
var root = (TestRoot)control.Parent;
root.Child = null;
control.Opacity = 0.8;
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
[Fact]
public void Animation_Is_Cancelled_When_Transition_Removed()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
var sub = new Mock<IDisposable>();
target.Setup(x => x.Apply(
It.IsAny<Animatable>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>())).Returns(sub.Object);
control.Opacity = 0.5;
control.Transitions.RemoveAt(0);
sub.Verify(x => x.Dispose());
}
[Fact]
public void Animation_Is_Cancelled_When_New_Style_Activates()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
{
var target = CreateTarget();
var control = CreateStyledControl(target.Object);
var sub = new Mock<IDisposable>();
target.Setup(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5)).Returns(sub.Object);
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<Clock>(),
1.0,
0.5),
Times.Once);
control.Classes.Add("foo");
sub.Verify(x => x.Dispose());
}
}
[Fact]
public void Transition_From_Style_Trigger_Is_Applied()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
{
var target = CreateTransition(Control.WidthProperty);
var control = CreateStyledControl(transition2: target.Object);
var sub = new Mock<IDisposable>();
control.Classes.Add("foo");
control.Width = 100;
target.Verify(x => x.Apply(
control,
It.IsAny<Clock>(),
double.NaN,
100.0),
Times.Once);
}
}
private static Mock<ITransition> CreateTarget()
{
return CreateTransition(Visual.OpacityProperty);
}
private static Control CreateControl(ITransition transition)
{
var control = new Control
{
Transitions = new Transitions { transition },
};
var root = new TestRoot(control);
return control;
}
private static Control CreateStyledControl(
ITransition transition1 = null,
ITransition transition2 = null)
{
transition1 = transition1 ?? CreateTarget().Object;
transition2 = transition2 ?? CreateTransition(Control.WidthProperty).Object;
var control = new Control
{
Styles =
{
new Style(x => x.OfType<Control>())
{
Setters =
{
new Setter
{
Property = Control.TransitionsProperty,
Value = new Transitions { transition1 },
}
}
},
new Style(x => x.OfType<Control>().Class("foo"))
{
Setters =
{
new Setter
{
Property = Control.TransitionsProperty,
Value = new Transitions { transition2 },
}
}
}
}
};
var root = new TestRoot(control);
return control;
}
private static Mock<ITransition> CreateTransition(AvaloniaProperty property)
{
var target = new Mock<ITransition>();
var sub = new Mock<IDisposable>();
target.Setup(x => x.Property).Returns(property);
target.Setup(x => x.Apply(
It.IsAny<Animatable>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>())).Returns(sub.Object);
return target;
}
}
}

4
tests/Avalonia.Animation.UnitTests/TransitionsTests.cs

@ -16,7 +16,7 @@ namespace Avalonia.Animation.UnitTests
{
var border = new Border
{
Transitions =
Transitions = new Transitions
{
new DoubleTransition
{
@ -44,7 +44,7 @@ namespace Avalonia.Animation.UnitTests
{
var border = new Border
{
Transitions =
Transitions = new Transitions
{
new DoubleTransition
{

51
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs

@ -1,5 +1,6 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -63,6 +64,56 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
[Fact]
public void GetBaseValue_LocalValue_Ignores_Default_Value()
{
var target = new Class3();
target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation);
Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).HasValue);
}
[Fact]
public void GetBaseValue_LocalValue_Returns_Local_Value()
{
var target = new Class3();
target.SetValue(Class1.FooProperty, "local");
target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation);
Assert.Equal("local", target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).Value);
}
[Fact]
public void GetBaseValue_LocalValue_Returns_Style_Value()
{
var target = new Class3();
target.SetValue(Class1.FooProperty, "style", BindingPriority.Style);
target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation);
Assert.Equal("style", target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).Value);
}
[Fact]
public void GetBaseValue_Style_Ignores_LocalValue_Animated_Value()
{
var target = new Class3();
target.Bind(Class1.FooProperty, new BehaviorSubject<string>("animated"), BindingPriority.Animation);
target.SetValue(Class1.FooProperty, "local");
Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.Style).HasValue);
}
[Fact]
public void GetBaseValue_Style_Returns_Style_Value()
{
var target = new Class3();
target.SetValue(Class1.FooProperty, "local");
target.SetValue(Class1.FooProperty, "style", BindingPriority.Style);
target.Bind(Class1.FooProperty, new BehaviorSubject<string>("animated"), BindingPriority.Animation);
Assert.Equal("style", target.GetBaseValue(Class1.FooProperty, BindingPriority.Style));
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =

142
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs

@ -0,0 +1,142 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_OnPropertyChanged
{
[Fact]
public void OnPropertyChangedCore_Is_Called_On_Property_Change()
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "newvalue");
Assert.Equal(1, target.CoreChanges.Count);
var change = (AvaloniaPropertyChangedEventArgs<string>)target.CoreChanges[0];
Assert.Equal("newvalue", change.NewValue.Value);
Assert.Equal("foodefault", change.OldValue.Value);
Assert.Equal(BindingPriority.LocalValue, change.Priority);
Assert.True(change.IsEffectiveValueChange);
}
[Fact]
public void OnPropertyChangedCore_Is_Called_On_Non_Effective_Property_Value_Change()
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "newvalue");
target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style);
Assert.Equal(2, target.CoreChanges.Count);
var change = (AvaloniaPropertyChangedEventArgs<string>)target.CoreChanges[1];
Assert.Equal("styled", change.NewValue.Value);
Assert.False(change.OldValue.HasValue);
Assert.Equal(BindingPriority.Style, change.Priority);
Assert.False(change.IsEffectiveValueChange);
}
[Fact]
public void OnPropertyChangedCore_Is_Called_On_All_Binding_Property_Changes()
{
var target = new Class1();
var style = new Subject<BindingValue<string>>();
var animation = new Subject<BindingValue<string>>();
var templatedParent = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, style, BindingPriority.Style);
target.Bind(Class1.FooProperty, animation, BindingPriority.Animation);
target.Bind(Class1.FooProperty, templatedParent, BindingPriority.TemplatedParent);
style.OnNext("style1");
templatedParent.OnNext("tp1");
animation.OnNext("a1");
templatedParent.OnNext("tp2");
templatedParent.OnCompleted();
animation.OnNext("a2");
style.OnNext("style2");
style.OnCompleted();
animation.OnCompleted();
var changes = target.CoreChanges.Cast<AvaloniaPropertyChangedEventArgs<string>>();
Assert.Equal(
new[] { true, true, true, false, false, true, false, false, true },
changes.Select(x => x.IsEffectiveValueChange).ToList());
Assert.Equal(
new[] { "style1", "tp1", "a1", "tp2", "$unset", "a2", "style2", "$unset", "foodefault" },
changes.Select(x => x.NewValue.GetValueOrDefault("$unset")).ToList());
Assert.Equal(
new[] { "foodefault", "style1", "tp1", "$unset", "$unset", "a1", "$unset", "$unset", "a2" },
changes.Select(x => x.OldValue.GetValueOrDefault("$unset")).ToList());
}
[Fact]
public void OnPropertyChanged_Is_Called_Only_For_Effective_Value_Changes()
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "newvalue");
target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style);
Assert.Equal(1, target.Changes.Count);
Assert.Equal(2, target.CoreChanges.Count);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", "foodefault");
public Class1()
{
Changes = new List<AvaloniaPropertyChangedEventArgs>();
CoreChanges = new List<AvaloniaPropertyChangedEventArgs>();
}
public List<AvaloniaPropertyChangedEventArgs> Changes { get; }
public List<AvaloniaPropertyChangedEventArgs> CoreChanges { get; }
protected override void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
CoreChanges.Add(Clone(change));
base.OnPropertyChangedCore(change);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
Changes.Add(Clone(change));
base.OnPropertyChanged(change);
}
private static AvaloniaPropertyChangedEventArgs<T> Clone<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
var result = new AvaloniaPropertyChangedEventArgs<T>(
change.Sender,
change.Property,
change.OldValue,
change.NewValue,
change.Priority);
if (!change.IsEffectiveValueChange)
{
result.MarkNonEffectiveValue();
}
return result;
}
}
}
}

39
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.Utilities;
using Xunit;
@ -88,6 +89,30 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("newvalue", value);
}
[Fact]
public void Changed_Observable_Fired_Only_On_Effective_Value_Change()
{
var target = new Class1();
var result = new List<string>();
Class1.FooProperty.Changed.Subscribe(x => result.Add((string)x.NewValue));
target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation);
target.SetValue(Class1.FooProperty, "local");
Assert.Equal(new[] { "animated" }, result);
}
[Fact]
public void Notify_Fired_Only_On_Effective_Value_Change()
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation);
target.SetValue(Class1.FooProperty, "local");
Assert.Equal(2, target.NotifyCount);
}
[Fact]
public void Property_Equals_Should_Handle_Null()
{
@ -144,6 +169,11 @@ namespace Avalonia.Base.UnitTests
throw new NotImplementedException();
}
internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority)
{
throw new NotImplementedException();
}
internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent)
{
throw new NotImplementedException();
@ -161,7 +191,14 @@ namespace Avalonia.Base.UnitTests
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", "default");
AvaloniaProperty.Register<Class1, string>("Foo", "default", notifying: FooNotifying);
public int NotifyCount { get; private set; }
private static void FooNotifying(IAvaloniaObject o, bool n)
{
++((Class1)o).NotifyCount;
}
}
private class Class2 : Class1

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save