Browse Source

Merge branch 'master' into fixes/3919-empty-tabcontrol

pull/3990/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
37105fa645
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Documentation/build.md
  2. 6
      native/Avalonia.Native/inc/avalonia-native.h
  3. 6
      native/Avalonia.Native/src/OSX/window.h
  4. 103
      native/Avalonia.Native/src/OSX/window.mm
  5. 8
      samples/ControlCatalog/MainView.xaml
  6. 7
      samples/ControlCatalog/MainView.xaml.cs
  7. 3
      samples/ControlCatalog/MainWindow.xaml
  8. 1
      samples/Directory.Build.props
  9. 2
      samples/RenderDemo/Controls/LineBoundsDemoControl.cs
  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. 60
      src/Avalonia.Controls/Application.cs
  32. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  33. 12
      src/Avalonia.Controls/Button.cs
  34. 12
      src/Avalonia.Controls/ButtonSpinner.cs
  35. 12
      src/Avalonia.Controls/Calendar/DatePicker.cs
  36. 39
      src/Avalonia.Controls/ContextMenu.cs
  37. 8
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  38. 12
      src/Avalonia.Controls/Expander.cs
  39. 12
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  40. 16
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  41. 16
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  42. 12
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  43. 15
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  44. 20
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  45. 8
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  46. 15
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  47. 12
      src/Avalonia.Controls/Primitives/Track.cs
  48. 12
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  49. 16
      src/Avalonia.Controls/ProgressBar.cs
  50. 56
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  51. 36
      src/Avalonia.Controls/Repeater/ViewManager.cs
  52. 35
      src/Avalonia.Controls/SelectionModel.cs
  53. 34
      src/Avalonia.Controls/Shapes/Path.cs
  54. 111
      src/Avalonia.Controls/Shapes/Shape.cs
  55. 12
      src/Avalonia.Controls/Slider.cs
  56. 110
      src/Avalonia.Controls/TopLevel.cs
  57. 8
      src/Avalonia.Controls/TreeView.cs
  58. 121
      src/Avalonia.Controls/Window.cs
  59. 2
      src/Avalonia.Controls/WindowBase.cs
  60. 25
      src/Avalonia.Controls/WindowTransparencyLevel.cs
  61. 9
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  62. 10
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  63. 16
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  64. 12
      src/Avalonia.Input/InputElement.cs
  65. 10
      src/Avalonia.Layout/FlowLayoutAlgorithm.cs
  66. 58
      src/Avalonia.Layout/LayoutHelper.cs
  67. 17
      src/Avalonia.Layout/Layoutable.cs
  68. 26
      src/Avalonia.Layout/StackLayout.cs
  69. 2
      src/Avalonia.Layout/StackLayoutState.cs
  70. 36
      src/Avalonia.Layout/UniformGridLayout.cs
  71. 5
      src/Avalonia.Native/PopupImpl.cs
  72. 26
      src/Avalonia.Native/WindowImpl.cs
  73. 25
      src/Avalonia.Native/WindowImplBase.cs
  74. 24
      src/Avalonia.Remote.Protocol/DesignMessages.cs
  75. 4
      src/Avalonia.Styling/Controls/IResourceDictionary.cs
  76. 32
      src/Avalonia.Styling/Controls/IResourceHost.cs
  77. 28
      src/Avalonia.Styling/Controls/IResourceNode.cs
  78. 41
      src/Avalonia.Styling/Controls/IResourceProvider.cs
  79. 27
      src/Avalonia.Styling/Controls/ISetResourceParent.cs
  80. 164
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  81. 103
      src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs
  82. 1
      src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs
  83. 8
      src/Avalonia.Styling/IStyledElement.cs
  84. 123
      src/Avalonia.Styling/StyledElement.cs
  85. 2
      src/Avalonia.Styling/Styling/IStyle.cs
  86. 2
      src/Avalonia.Styling/Styling/IStyleHost.cs
  87. 12
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  88. 71
      src/Avalonia.Styling/Styling/Style.cs
  89. 181
      src/Avalonia.Styling/Styling/Styles.cs
  90. 19
      src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml
  91. 17
      src/Avalonia.Themes.Default/OverlayPopupHost.xaml
  92. 17
      src/Avalonia.Themes.Default/PopupRoot.xaml
  93. 6
      src/Avalonia.Themes.Default/ScrollBar.xaml
  94. 23
      src/Avalonia.Themes.Default/Window.xaml
  95. 4
      src/Avalonia.Visuals/Media/BoxShadows.cs
  96. 8
      src/Avalonia.Visuals/Media/DrawingContext.cs
  97. 10
      src/Avalonia.Visuals/Media/DrawingImage.cs
  98. 22
      src/Avalonia.Visuals/Media/GlyphRun.cs
  99. 44
      src/Avalonia.Visuals/Media/Imaging/Bitmap.cs
  100. 26
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

2
Documentation/build.md

@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi
On macOS: 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): On Debian based Linux (Debian, Ubuntu, Mint, etc):

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

@ -258,6 +258,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0;
virtual HRESULT SetBlurEnabled (bool enable) = 0;
}; };
AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
@ -267,7 +268,8 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
AVNCOM(IAvnWindow, 04) : 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 SetCanResize(bool value) = 0;
virtual HRESULT SetDecorations(SystemDecorations value) = 0; virtual HRESULT SetDecorations(SystemDecorations value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0;
@ -309,6 +311,8 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents
virtual bool Closing () = 0; virtual bool Closing () = 0;
virtual void WindowStateChanged (AvnWindowState state) = 0; virtual void WindowStateChanged (AvnWindowState state) = 0;
virtual void GotInputWhenDisabled () = 0;
}; };
AVNCOM(IAvnMacOptions, 07) : IUnknown AVNCOM(IAvnMacOptions, 07) : IUnknown

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

@ -3,6 +3,9 @@
class WindowBaseImpl; class WindowBaseImpl;
@interface AutoFitContentVisualEffectView : NSVisualEffectView
@end
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination> @interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent; -(NSEvent* _Nonnull) lastMouseDownEvent;
@ -19,8 +22,7 @@ class WindowBaseImpl;
-(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow; -(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents; -(bool) shouldTryToHandleEvents;
-(bool) isModal; -(void) setEnabled: (bool) enable;
-(void) setModal: (bool) isModal;
-(void) showAppMenuOnly; -(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu; -(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu; -(void) applyMenu:(NSMenu* _Nullable)menu;

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

@ -20,6 +20,7 @@ public:
View = NULL; View = NULL;
Window = NULL; Window = NULL;
} }
NSVisualEffectView* VisualEffect;
AvnView* View; AvnView* View;
AvnWindow* Window; AvnWindow* Window;
ComPtr<IAvnWindowBaseEvents> BaseEvents; ComPtr<IAvnWindowBaseEvents> BaseEvents;
@ -47,6 +48,12 @@ public:
[Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setStyleMask:NSWindowStyleMaskBorderless];
[Window setBackingType:NSBackingStoreBuffered]; [Window setBackingType:NSBackingStoreBuffered];
VisualEffect = [AutoFitContentVisualEffectView new];
[VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
[VisualEffect setMaterial:NSVisualEffectMaterialLight];
[VisualEffect setAutoresizesSubviews:true];
[Window setContentView: View]; [Window setContentView: View];
} }
@ -383,6 +390,18 @@ public:
return *ppv == nil ? E_FAIL : S_OK; return *ppv == nil ? E_FAIL : S_OK;
} }
virtual HRESULT SetBlurEnabled (bool enable) override
{
[Window setContentView: enable ? VisualEffect : View];
if(enable)
{
[VisualEffect addSubview:View];
}
return S_OK;
}
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, IAvnClipboard* clipboard, IAvnDndResultCallback* cb,
void* sourceHandle) override void* sourceHandle) override
@ -497,12 +516,7 @@ private:
virtual HRESULT Show () override virtual HRESULT Show () override
{ {
@autoreleasepool @autoreleasepool
{ {
if([Window parentWindow] != nil)
[[Window parentWindow] removeChildWindow:Window];
[Window setModal:FALSE];
WindowBaseImpl::Show(); WindowBaseImpl::Show();
HideOrShowTrafficLights(); HideOrShowTrafficLights();
@ -511,7 +525,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 @autoreleasepool
{ {
@ -522,12 +545,9 @@ private:
if(cparent == nullptr) if(cparent == nullptr)
return E_INVALIDARG; return E_INVALIDARG;
[Window setModal:TRUE];
[cparent->Window addChildWindow:Window ordered:NSWindowAbove]; [cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show();
HideOrShowTrafficLights(); UpdateStyle();
return S_OK; return S_OK;
} }
@ -883,15 +903,15 @@ protected:
switch (_decorations) switch (_decorations)
{ {
case SystemDecorationsNone: case SystemDecorationsNone:
s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; s = s | NSWindowStyleMaskFullSizeContentView;
break; break;
case SystemDecorationsBorderOnly: case SystemDecorationsBorderOnly:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
break; break;
case SystemDecorationsFull: case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless;
if(_canResize) if(_canResize)
{ {
@ -900,12 +920,26 @@ protected:
break; break;
} }
if([Window parentWindow] == nullptr)
{
s |= NSWindowStyleMaskMiniaturizable;
}
return s; return s;
} }
}; };
NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil];
@implementation AutoFitContentVisualEffectView
-(void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
if([[self subviews] count] == 0)
return;
[[self subviews][0] setFrameSize: newSize];
}
@end
@implementation AvnView @implementation AvnView
{ {
ComPtr<WindowBaseImpl> _parent; ComPtr<WindowBaseImpl> _parent;
@ -1081,15 +1115,28 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (bool) ignoreUserInput - (bool) ignoreUserInput
{ {
auto parentWindow = objc_cast<AvnWindow>([self window]); auto parentWindow = objc_cast<AvnWindow>([self window]);
if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents])
{
auto window = dynamic_cast<WindowImpl*>(_parent.getRaw());
if(window != nullptr)
{
window->WindowEvents->GotInputWhenDisabled();
}
return TRUE; return TRUE;
}
return FALSE; return FALSE;
} }
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
{ {
if([self ignoreUserInput]) if([self ignoreUserInput])
{
return; return;
}
[self becomeFirstResponder]; [self becomeFirstResponder];
auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
@ -1234,7 +1281,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type
{ {
if([self ignoreUserInput]) if([self ignoreUserInput])
{
return; return;
}
auto key = s_KeyMap[[event keyCode]]; auto key = s_KeyMap[[event keyCode]];
auto timestamp = [event timestamp] * 1000; auto timestamp = [event timestamp] * 1000;
@ -1416,7 +1466,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
ComPtr<WindowBaseImpl> _parent; ComPtr<WindowBaseImpl> _parent;
bool _canBecomeKeyAndMain; bool _canBecomeKeyAndMain;
bool _closed; bool _closed;
bool _isModal; bool _isEnabled;
AvnMenu* _menu; AvnMenu* _menu;
double _lastScaling; double _lastScaling;
} }
@ -1538,6 +1588,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_parent = parent; _parent = parent;
[self setDelegate:self]; [self setDelegate:self];
_closed = false; _closed = false;
_isEnabled = true;
_lastScaling = [self backingScaleFactor]; _lastScaling = [self backingScaleFactor];
[self setOpaque:NO]; [self setOpaque:NO];
@ -1604,28 +1655,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
-(bool)shouldTryToHandleEvents -(bool)shouldTryToHandleEvents
{ {
for(NSWindow* uch in [self childWindows]) return _isEnabled;
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
if(![ch isModal])
continue;
return FALSE;
}
return TRUE;
}
-(bool) isModal
{
return _isModal;
} }
-(void) setModal: (bool) isModal -(void) setEnabled:(bool)enable
{ {
_isModal = isModal; _isEnabled = enable;
} }
-(void)makeKeyWindow -(void)makeKeyWindow

8
samples/ControlCatalog/MainView.xaml

@ -2,7 +2,7 @@
xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:pages="clr-namespace:ControlCatalog.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.MainView" x:Class="ControlCatalog.MainView"
Background="{DynamicResource ThemeBackgroundBrush}" Background="Transparent"
Foreground="{DynamicResource ThemeForegroundBrush}" Foreground="{DynamicResource ThemeForegroundBrush}"
FontSize="{DynamicResource FontSizeNormal}"> FontSize="{DynamicResource FontSizeNormal}">
<Grid> <Grid>
@ -70,6 +70,12 @@
<ComboBoxItem>Light</ComboBoxItem> <ComboBoxItem>Light</ComboBoxItem>
<ComboBoxItem>Dark</ComboBoxItem> <ComboBoxItem>Dark</ComboBoxItem>
</ComboBox> </ComboBox>
<ComboBox x:Name="TransparencyLevels" SelectedIndex="0">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Transparent</ComboBoxItem>
<ComboBoxItem>Blur</ComboBoxItem>
<ComboBoxItem>AcrylicBlur</ComboBoxItem>
</ComboBox>
<ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" /> <ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" />
</StackPanel> </StackPanel>
</TabControl.Tag> </TabControl.Tag>

7
samples/ControlCatalog/MainView.xaml.cs

@ -63,6 +63,13 @@ namespace ControlCatalog
if (VisualRoot is Window window) if (VisualRoot is Window window)
window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
}; };
var transparencyLevels = this.Find<ComboBox>("TransparencyLevels");
transparencyLevels.SelectionChanged += (sender, e) =>
{
if (VisualRoot is Window window)
window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex;
};
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)

3
samples/ControlCatalog/MainWindow.xaml

@ -7,8 +7,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views" xmlns:v="clr-namespace:ControlCatalog.Views"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="Transparent">
<NativeMenu.Menu> <NativeMenu.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="File"> <NativeMenuItem Header="File">

1
samples/Directory.Build.props

@ -1,6 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
</PropertyGroup> </PropertyGroup>
<Import Project="..\build\SharedVersion.props" /> <Import Project="..\build\SharedVersion.props" />
</Project> </Project>

2
samples/RenderDemo/Controls/LineBoundsDemoControl.cs

@ -17,7 +17,7 @@ namespace RenderDemo.Controls
public LineBoundsDemoControl() public LineBoundsDemoControl()
{ {
var timer = new DispatcherTimer(); var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1 / 60); timer.Interval = TimeSpan.FromSeconds(1 / 60.0);
timer.Tick += (sender, e) => Angle += Math.PI / 360; timer.Tick += (sender, e) => Angle += Math.PI / 360;
timer.Start(); timer.Start();
} }

225
src/Avalonia.Animation/Animatable.cs

@ -1,10 +1,10 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Collections.Specialized;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Animation.Animators;
#nullable enable
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -13,9 +13,24 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class Animatable : AvaloniaObject public class Animatable : AvaloniaObject
{ {
/// <summary>
/// Defines the <see cref="Clock"/> property.
/// </summary>
public static readonly StyledProperty<IClock> ClockProperty = public static readonly StyledProperty<IClock> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true); 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 public IClock Clock
{ {
get => GetValue(ClockProperty); get => GetValue(ClockProperty);
@ -23,72 +38,194 @@ namespace Avalonia.Animation
} }
/// <summary> /// <summary>
/// Defines the <see cref="Transitions"/> property. /// Gets or sets the property transitions for the control.
/// </summary> /// </summary>
public static readonly DirectProperty<Animatable, Transitions> TransitionsProperty = public Transitions? Transitions
AvaloniaProperty.RegisterDirect<Animatable, Transitions>( {
nameof(Transitions), get => GetValue(TransitionsProperty);
o => o.Transitions, set => SetValue(TransitionsProperty, value);
(o, v) => o.Transitions = v); }
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> /// <summary>
/// Gets or sets the property transitions for the control. /// Disables transitions for the control.
/// </summary> /// </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) var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
_transitions = new Transitions(); var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
if (_previousTransitions is null) if (oldTransitions is object)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>(); {
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) foreach (var transition in Transitions)
return; {
if (transition.Property == change.Property)
{
var state = _transitionState[transition];
var oldValue = state.BaseValue;
var newValue = GetAnimationBaseValue(transition.Property);
if (_previousTransitions is null) if (!Equals(oldValue, newValue))
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>(); {
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>( private void AddTransitions(IList items)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation) if (!_transitionsEnabled)
{
return; return;
}
_transitionState ??= new Dictionary<ITransition, TransitionState>();
// PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). for (var i = 0; i < items.Count; ++i)
foreach (var transition in _transitions)
{ {
if (transition.Property == property) var t = (ITransition)items[i];
_transitionState.Add(t, new TransitionState
{ {
if (_previousTransitions.TryGetValue(property, out var dispose)) BaseValue = GetAnimationBaseValue(t.Property),
dispose.Dispose(); });
}
}
var instance = transition.Apply( private void RemoveTransitions(IList items)
this, {
Clock ?? Avalonia.Animation.Clock.GlobalClock, if (_transitionState is null)
oldValue.GetValueOrDefault(), {
newValue.GetValueOrDefault()); return;
}
_previousTransitions[property] = instance; for (var i = 0; i < items.Count; ++i)
return; {
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) public TransitionInstance(IClock clock, TimeSpan Duration)
{ {
clock = clock ?? throw new ArgumentNullException(nameof(clock));
_duration = Duration; _duration = Duration;
_baseClock = clock; _baseClock = clock;
} }

13
src/Avalonia.Animation/Transitions.cs

@ -1,4 +1,6 @@
using System;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Threading;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -13,6 +15,17 @@ namespace Avalonia.Animation
public Transitions() public Transitions()
{ {
ResetBehavior = ResetBehavior.Remove; 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); 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> /// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating. /// Checks whether a <see cref="AvaloniaProperty"/> is animating.
/// </summary> /// </summary>
@ -458,29 +473,43 @@ namespace Avalonia
return _propertyChanged?.GetInvocationList(); return _propertyChanged?.GetInvocationList();
} }
void IValueSink.ValueChanged<T>( void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue)
{ {
oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property); var property = (StyledPropertyBase<T>)change.Property;
newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(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( if (change.IsEffectiveValueChange && !change.NewValue.HasValue)
LogArea.Property, {
this, change.SetNewValue(GetInheritedOrDefault(property));
"{Property} changed from {$Old} to {$Value} with priority {Priority}", }
property,
oldValue, if (!change.IsEffectiveValueChange ||
newValue, !EqualityComparer<T>.Default.Equals(change.OldValue.Value, change.NewValue.Value))
(BindingPriority)priority); {
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, IPriorityValueEntry entry,
Optional<T> oldValue) 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> /// <summary>
@ -575,15 +610,20 @@ namespace Avalonia
/// <summary> /// <summary>
/// Called when a avalonia property changes on the object. /// Called when a avalonia property changes on the object.
/// </summary> /// </summary>
/// <param name="property">The property whose value has changed.</param> /// <param name="change">The property change details.</param>
/// <param name="oldValue">The old value of the property.</param> protected virtual void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change)
/// <param name="newValue">The new value of the property.</param> {
/// <param name="priority">The priority of the new value.</param> if (change.IsEffectiveValueChange)
protected virtual void OnPropertyChanged<T>( {
AvaloniaProperty<T> property, OnPropertyChanged(change);
Optional<T> oldValue, }
BindingValue<T> newValue, }
BindingPriority priority)
/// <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, BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs<T>(
this,
VerifyAccess(); property,
oldValue,
property.Notifying?.Invoke(this, true); newValue,
priority));
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);
}
} }
/// <summary> /// <summary>
@ -689,7 +684,9 @@ namespace Avalonia
return property.GetDefaultValue(GetType()); 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 o = this;
var inherits = property.Inherits; var inherits = property.Inherits;
@ -699,7 +696,7 @@ namespace Avalonia
{ {
var values = o._values; var values = o._values;
if (values?.TryGetValue(property, out value) == true) if (values?.TryGetValue(property, maxPriority, out value) == true)
{ {
return value; return value;
} }
@ -715,6 +712,51 @@ namespace Avalonia
return property.GetDefaultValue(GetType()); 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> /// <summary>
/// Sets the value of a direct property. /// Sets the value of a direct property.
/// </summary> /// </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> /// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value. /// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary> /// </summary>

7
src/Avalonia.Base/AvaloniaProperty.cs

@ -496,6 +496,13 @@ namespace Avalonia
/// <param name="o">The object instance.</param> /// <param name="o">The object instance.</param>
internal abstract object RouteGetValue(IAvaloniaObject o); 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> /// <summary>
/// Routes an untyped SetValue call to a typed call. /// Routes an untyped SetValue call to a typed call.
/// </summary> /// </summary>

23
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -16,6 +16,7 @@ namespace Avalonia
{ {
Sender = sender; Sender = sender;
Priority = priority; Priority = priority;
IsEffectiveValueChange = true;
} }
/// <summary> /// <summary>
@ -35,19 +36,11 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the old value of the property. /// Gets the old value of the property.
/// </summary> /// </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(); public object? OldValue => GetOldValue();
/// <summary> /// <summary>
/// Gets the new value of the property. /// Gets the new value of the property.
/// </summary> /// </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(); public object? NewValue => GetNewValue();
/// <summary> /// <summary>
@ -58,6 +51,20 @@ namespace Avalonia
/// </value> /// </value>
public BindingPriority Priority { get; private set; } 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 AvaloniaProperty GetProperty();
protected abstract object? GetOldValue(); protected abstract object? GetOldValue();
protected abstract object? GetNewValue(); protected abstract object? GetNewValue();

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

@ -1,4 +1,3 @@
using System;
using Avalonia.Data; using Avalonia.Data;
#nullable enable #nullable enable
@ -6,7 +5,7 @@ using Avalonia.Data;
namespace Avalonia namespace Avalonia
{ {
/// <summary> /// <summary>
/// Provides information for a avalonia property change. /// Provides information for an Avalonia property change.
/// </summary> /// </summary>
public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs
{ {
@ -42,19 +41,28 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the old value of the property. /// Gets the old value of the property.
/// </summary> /// </summary>
/// <value> /// <remarks>
/// The old value of the property. /// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// </value> /// 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; } public new Optional<T> OldValue { get; private set; }
/// <summary> /// <summary>
/// Gets the new value of the property. /// Gets the new value of the property.
/// </summary> /// </summary>
/// <value> /// <remarks>
/// The new value of the property. /// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// </value> /// 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; } 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 AvaloniaProperty GetProperty() => Property;
protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); 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>( return new BindingValue<T>(
fallbackValue.HasValue ? fallbackValue.HasValue ?
BindingValueType.DataValidationError : BindingValueType.DataValidationErrorWithFallback :
BindingValueType.DataValidationErrorWithFallback, BindingValueType.DataValidationError,
fallbackValue.HasValue ? fallbackValue.Value : default, fallbackValue.HasValue ? fallbackValue.Value : default,
e); e);
} }

5
src/Avalonia.Base/DirectPropertyBase.cs

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

13
src/Avalonia.Base/IAvaloniaObject.cs

@ -41,6 +41,19 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
T GetValue<T>(DirectPropertyBase<T> property); 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> /// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating. /// Checks whether a <see cref="AvaloniaProperty"/> is animating.
/// </summary> /// </summary>

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

@ -22,6 +22,7 @@ namespace Avalonia.PropertyStore
private readonly IAvaloniaObject _owner; private readonly IAvaloniaObject _owner;
private IValueSink _sink; private IValueSink _sink;
private IDisposable? _subscription; private IDisposable? _subscription;
private Optional<T> _value;
public BindingEntry( public BindingEntry(
IAvaloniaObject owner, IAvaloniaObject owner,
@ -40,18 +41,21 @@ namespace Avalonia.PropertyStore
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; } public BindingPriority Priority { get; }
public IObservable<BindingValue<T>> Source { get; } public IObservable<BindingValue<T>> Source { get; }
public Optional<T> Value { get; private set; } Optional<object> IValue.GetValue() => _value.ToObject();
Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority; public Optional<T> GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose() public void Dispose()
{ {
_subscription?.Dispose(); _subscription?.Dispose();
_subscription = null; _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) public void OnError(Exception error)
{ {
@ -94,14 +98,14 @@ namespace Avalonia.PropertyStore
return; return;
} }
var old = Value; var old = _value;
if (value.Type != BindingValueType.DataValidationError) 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 internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
{ {
private IValueSink _sink; private IValueSink _sink;
private Optional<T> _value;
public ConstantValueEntry( public ConstantValueEntry(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
@ -21,18 +22,21 @@ namespace Avalonia.PropertyStore
IValueSink sink) IValueSink sink)
{ {
Property = property; Property = property;
Value = value; _value = value;
Priority = priority; Priority = priority;
_sink = sink; _sink = sink;
} }
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; } public BindingPriority Priority { get; }
public Optional<T> Value { get; } Optional<object> IValue.GetValue() => _value.ToObject();
Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority;
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; public void Reparent(IValueSink sink) => _sink = sink;
} }
} }

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

@ -9,8 +9,8 @@ namespace Avalonia.PropertyStore
/// </summary> /// </summary>
internal interface IValue internal interface IValue
{ {
Optional<object> Value { get; } Optional<object> GetValue();
BindingPriority ValuePriority { get; } BindingPriority Priority { get; }
} }
/// <summary> /// <summary>
@ -19,6 +19,6 @@ namespace Avalonia.PropertyStore
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
internal interface IValue<T> : IValue 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> /// </summary>
internal interface IValueSink internal interface IValueSink
{ {
void ValueChanged<T>( void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change);
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue);
void Completed<T>( void Completed<T>(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,

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

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

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

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

7
src/Avalonia.Base/StyledPropertyBase.cs

@ -197,6 +197,13 @@ namespace Avalonia
return o.GetValue<TValue>(this); 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/> /// <inheritdoc/>
internal override IDisposable RouteSetValue( internal override IDisposable RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,

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

@ -115,45 +115,46 @@ namespace Avalonia.Utilities
return true; return true;
} }
var toUnderl = Nullable.GetUnderlyingType(to) ?? to;
var from = value.GetType(); var from = value.GetType();
if (to.IsAssignableFrom(from)) if (toUnderl.IsAssignableFrom(from))
{ {
result = value; result = value;
return true; return true;
} }
if (to == typeof(string)) if (toUnderl == typeof(string))
{ {
result = Convert.ToString(value); result = Convert.ToString(value, culture);
return true; 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; return true;
} }
} }
if (!from.IsEnum && to.IsEnum) if (!from.IsEnum && toUnderl.IsEnum)
{ {
result = null; 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; return true;
} }
} }
if (from.IsEnum && IsNumeric(to)) if (from.IsEnum && IsNumeric(toUnderl))
{ {
try try
{ {
result = Convert.ChangeType((int)value, to, culture); result = Convert.ChangeType((int)value, toUnderl, culture);
return true; return true;
} }
catch catch
@ -164,7 +165,7 @@ namespace Avalonia.Utilities
} }
var convertableFrom = Array.IndexOf(InbuiltTypes, from); var convertableFrom = Array.IndexOf(InbuiltTypes, from);
var convertableTo = Array.IndexOf(InbuiltTypes, to); var convertableTo = Array.IndexOf(InbuiltTypes, toUnderl);
if (convertableFrom != -1 && convertableTo != -1) if (convertableFrom != -1 && convertableTo != -1)
{ {
@ -172,7 +173,7 @@ namespace Avalonia.Utilities
{ {
try try
{ {
result = Convert.ChangeType(value, to, culture); result = Convert.ChangeType(value, toUnderl, culture);
return true; return true;
} }
catch 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; return true;
} }
var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); var cast = FindTypeConversionOperatorMethod(from, toUnderl, OperatorType.Implicit | OperatorType.Explicit);
if (cast != null) if (cast != null)
{ {

69
src/Avalonia.Base/ValueStore.cs

@ -37,7 +37,7 @@ namespace Avalonia
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
return slot.ValuePriority < BindingPriority.LocalValue; return slot.Priority < BindingPriority.LocalValue;
} }
return false; return false;
@ -47,21 +47,24 @@ namespace Avalonia
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
return slot.Value.HasValue; return slot.GetValue().HasValue;
} }
return false; 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)) 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; return true;
} }
} }
@ -90,17 +93,22 @@ namespace Avalonia
_values.AddValue(property, entry); _values.AddValue(property, entry);
result = entry.SetValue(value, priority); result = entry.SetValue(value, priority);
} }
else if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, new LocalValueEntry<T>(value));
_sink.ValueChanged(property, priority, default, value);
}
else else
{ {
var entry = new ConstantValueEntry<T>(property, value, priority, this); var change = new AvaloniaPropertyChangedEventArgs<T>(_owner, property, default, value, priority);
_values.AddValue(property, entry);
_sink.ValueChanged(property, priority, default, value); if (priority == BindingPriority.LocalValue)
result = entry; {
_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; return result;
@ -149,13 +157,14 @@ namespace Avalonia
if (remove) 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); _values.Remove(property);
_sink.ValueChanged( _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property, property,
BindingPriority.Unset,
old, old,
BindingValue<T>.Unset); default,
BindingPriority.Unset));
} }
} }
} }
@ -176,23 +185,20 @@ namespace Avalonia
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
var slotValue = slot.GetValue();
return new Diagnostics.AvaloniaPropertyValue( return new Diagnostics.AvaloniaPropertyValue(
property, property,
slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue, slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue,
slot.ValuePriority, slot.Priority,
null); null);
} }
return null; return null;
} }
void IValueSink.ValueChanged<T>( void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue)
{ {
_sink.ValueChanged(property, priority, oldValue, newValue); _sink.ValueChanged(change);
} }
void IValueSink.Completed<T>( void IValueSink.Completed<T>(
@ -232,9 +238,14 @@ namespace Avalonia
{ {
if (priority == BindingPriority.LocalValue) if (priority == BindingPriority.LocalValue)
{ {
var old = l.Value; var old = l.GetValue(BindingPriority.LocalValue);
l.SetValue(value); l.SetValue(value);
_sink.ValueChanged(property, priority, old, value); _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
old,
value,
priority));
} }
else 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. // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved. // All other rights reserved.
@ -767,7 +766,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// ItemsProperty property changed handler. /// ItemsProperty property changed handler.
/// </summary> /// </summary>
/// <param name="e">AvaloniaPropertyChangedEventArgs.</param> /// <param name="e">The event arguments.</param>
private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{ {
if (!_areHandlersSuspended) if (!_areHandlersSuspended)

60
src/Avalonia.Controls/Application.cs

@ -30,7 +30,7 @@ namespace Avalonia
/// method. /// method.
/// - Tracks the lifetime of the application. /// - Tracks the lifetime of the application.
/// </remarks> /// </remarks>
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost
{ {
/// <summary> /// <summary>
/// The application-global data templates. /// The application-global data templates.
@ -129,26 +129,13 @@ namespace Avalonia
/// </summary> /// </summary>
public IResourceDictionary Resources public IResourceDictionary Resources
{ {
get => _resources ?? (Resources = new ResourceDictionary()); get => _resources ??= new ResourceDictionary(this);
set set
{ {
Contract.Requires<ArgumentNullException>(value != null); value = value ?? throw new ArgumentNullException(nameof(value));
_resources?.RemoveOwner(this);
var hadResources = false;
if (_resources != null)
{
hadResources = _resources.Count > 0;
_resources.ResourcesChanged -= ThisResourcesChanged;
}
_resources = value; _resources = value;
_resources.ResourcesChanged += ThisResourcesChanged; _resources.AddOwner(this);
if (hadResources || _resources.Count > 0)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
} }
} }
@ -161,23 +148,15 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// Global styles apply to all windows in the application. /// Global styles apply to all windows in the application.
/// </remarks> /// </remarks>
public Styles Styles public Styles Styles => _styles ??= new Styles(this);
{
get
{
if (_styles == null)
{
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
}
return _styles;
}
}
/// <inheritdoc/> /// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <inheritdoc/>
bool IResourceNode.HasResources => (_resources?.HasResources ?? false) ||
(((IResourceNode?)_styles)?.HasResources ?? false);
/// <summary> /// <summary>
/// Gets the styling parent of the application, which is null. /// Gets the styling parent of the application, which is null.
/// </summary> /// </summary>
@ -185,13 +164,7 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
bool IStyleHost.IsStylesInitialized => _styles != null; bool IStyleHost.IsStylesInitialized => _styles != null;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => null;
/// <summary> /// <summary>
/// Application lifetime, use it for things like setting the main window and exiting the app from code /// Application lifetime, use it for things like setting the main window and exiting the app from code
/// Currently supported lifetimes are: /// Currently supported lifetimes are:
@ -219,13 +192,18 @@ namespace Avalonia
public virtual void Initialize() { } public virtual void Initialize() { }
/// <inheritdoc/> /// <inheritdoc/>
bool IResourceProvider.TryGetResource(object key, out object value) bool IResourceNode.TryGetResource(object key, out object value)
{ {
value = null; value = null;
return (_resources?.TryGetResource(key, out value) ?? false) || return (_resources?.TryGetResource(key, out value) ?? false) ||
Styles.TryGetResource(key, out value); Styles.TryGetResource(key, out value);
} }
void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
}
void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles) void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
{ {
_stylesAdded?.Invoke(styles); _stylesAdded?.Invoke(styles);
@ -282,9 +260,7 @@ namespace Avalonia
try try
{ {
_notifyingResourcesChanged = true; _notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e); ResourcesChanged?.Invoke(this, ResourcesChangedEventArgs.Empty);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
} }
finally finally
{ {

2
src/Avalonia.Controls/AutoCompleteBox.cs

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

12
src/Avalonia.Controls/Button.cs

@ -313,17 +313,13 @@ namespace Avalonia.Controls
IsPressed = false; IsPressed = false;
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
@ -9,18 +10,19 @@ using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// A control context menu. /// A control context menu.
/// </summary> /// </summary>
public class ContextMenu : MenuBase public class ContextMenu : MenuBase, ISetterValue
{ {
private static readonly ITemplate<IPanel> DefaultPanel = private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical }); new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup; private Popup _popup;
private Control _attachedControl; private List<Control> _attachedControls;
private IInputElement _previousFocus; private IInputElement _previousFocus;
/// <summary> /// <summary>
@ -74,13 +76,14 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu) if (e.OldValue is ContextMenu oldMenu)
{ {
control.PointerReleased -= ControlPointerReleased; control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null; oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null); ((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
} }
if (e.NewValue is ContextMenu newMenu) if (e.NewValue is ContextMenu newMenu)
{ {
newMenu._attachedControl = control; newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased; control.PointerReleased += ControlPointerReleased;
} }
} }
@ -96,18 +99,22 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
public void Open(Control control) 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)); 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( throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.", "Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control)); nameof(control));
} }
control ??= _attachedControls[0];
if (IsOpen) if (IsOpen)
{ {
return; return;
@ -126,7 +133,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed; _popup.Closed += PopupClosed;
} }
((ISetLogicalParent)_popup).SetParent(control); if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this; _popup.Child = this;
_popup.IsOpen = true; _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() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
return new MenuItemContainerGenerator(this); return new MenuItemContainerGenerator(this);
@ -179,7 +202,7 @@ namespace Avalonia.Controls
SelectedIndex = -1; SelectedIndex = -1;
IsOpen = false; IsOpen = false;
if (_attachedControl is null) if (_attachedControls is null || _attachedControls.Count == 0)
{ {
((ISetLogicalParent)_popup).SetParent(null); ((ISetLogicalParent)_popup).SetParent(null);
} }

8
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -49,6 +49,9 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action<Rect> Paint { get; set; } public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; } public Action<Size> Resized { get; set; }
public Action<double> ScalingChanged { get; set; } public Action<double> ScalingChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot; public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot;
public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1); public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1);
@ -61,6 +64,11 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action Closed { get; set; } public Action Closed { get; set; }
public abstract IMouseDevice MouseDevice { get; } public abstract IMouseDevice MouseDevice { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { }
public WindowTransparencyLevel TransparencyLevel { get; private set; }
public IPopupImpl CreatePopup() => null; public IPopupImpl CreatePopup() => null;
} }
} }

12
src/Avalonia.Controls/Expander.cs

@ -78,17 +78,13 @@ namespace Avalonia.Controls
} }
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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(); notificationControl.Close();
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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/ITopLevelImpl.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Rendering; using Avalonia.Rendering;
@ -58,6 +59,11 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
Action<double> ScalingChanged { get; set; } Action<double> ScalingChanged { get; set; }
/// <summary>
/// Gets or sets a method called when the toplevel's TransparencyLevel changes.
/// </summary>
Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
/// <summary> /// <summary>
/// Creates a new renderer for the toplevel. /// Creates a new renderer for the toplevel.
/// </summary> /// </summary>
@ -106,5 +112,15 @@ namespace Avalonia.Platform
IMouseDevice MouseDevice { get; } IMouseDevice MouseDevice { get; }
IPopupImpl CreatePopup(); IPopupImpl CreatePopup();
/// <summary>
/// Sets the <see cref="WindowTransparencyLevel"/> hint of the TopLevel.
/// </summary>
void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel);
/// <summary>
/// Gets the current <see cref="WindowTransparencyLevel"/> of the TopLevel.
/// </summary>
WindowTransparencyLevel TransparencyLevel { get; }
} }
} }

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

@ -26,9 +26,21 @@ namespace Avalonia.Platform
void SetTitle(string title); void SetTitle(string title);
/// <summary> /// <summary>
/// Shows the window as a dialog. /// Sets the parent of the window.
/// </summary> /// </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> /// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc) /// Enables or disables system window decorations (title bar, buttons, etc)

12
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -365,12 +365,8 @@ namespace Avalonia.Controls.Presenters
if (useLayoutRounding) if (useLayoutRounding)
{ {
sizeForChild = new Size( sizeForChild = LayoutHelper.RoundLayoutSize(sizeForChild, scale, scale);
Math.Ceiling(sizeForChild.Width * scale) / scale, availableSize = LayoutHelper.RoundLayoutSize(availableSize, scale, scale);
Math.Ceiling(sizeForChild.Height * scale) / scale);
availableSize = new Size(
Math.Ceiling(availableSize.Width * scale) / scale,
Math.Ceiling(availableSize.Height * scale) / scale);
} }
switch (horizontalContentAlignment) switch (horizontalContentAlignment)
@ -395,8 +391,8 @@ namespace Avalonia.Controls.Presenters
if (useLayoutRounding) if (useLayoutRounding)
{ {
originX = Math.Floor(originX * scale) / scale; originX = LayoutHelper.RoundLayoutValue(originX, scale);
originY = Math.Floor(originY * scale) / scale; originY = LayoutHelper.RoundLayoutValue(originY, scale);
} }
var boundsForChild = var boundsForChild =

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

@ -75,6 +75,9 @@ namespace Avalonia.Controls.Presenters
static TextPresenter() static TextPresenter()
{ {
AffectsRender<TextPresenter>(SelectionBrushProperty); AffectsRender<TextPresenter>(SelectionBrushProperty);
AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
@ -284,8 +287,6 @@ namespace Avalonia.Controls.Presenters
protected void InvalidateFormattedText() protected void InvalidateFormattedText()
{ {
_formattedText = null; _formattedText = null;
InvalidateMeasure();
} }
/// <summary> /// <summary>
@ -301,13 +302,15 @@ namespace Avalonia.Controls.Presenters
context.FillRectangle(background, new Rect(Bounds.Size)); context.FillRectangle(background, new Rect(Bounds.Size));
} }
FormattedText.Constraint = Bounds.Size;
context.DrawText(Foreground, new Point(), FormattedText); context.DrawText(Foreground, new Point(), FormattedText);
} }
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
FormattedText.Constraint = Bounds.Size;
_constraint = Bounds.Size;
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
@ -316,10 +319,6 @@ namespace Avalonia.Controls.Presenters
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start; 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); var rects = FormattedText.HitTestTextRange(start, length);
foreach (var rect in rects) 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>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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 else
{ {
if (property == MinimumProperty || if (change.Property == MinimumProperty ||
property == MaximumProperty || change.Property == MaximumProperty ||
property == ViewportSizeProperty || change.Property == ViewportSizeProperty ||
property == VisibilityProperty) change.Property == VisibilityProperty)
{ {
UpdateIsVisible(); UpdateIsVisible();
} }

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

@ -455,13 +455,13 @@ namespace Avalonia.Controls.Primitives
InternalEndInit(); 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.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
} }

15
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -287,6 +287,21 @@ namespace Avalonia.Controls.Primitives
return this; return this;
} }
protected sealed override void NotifyChildResourcesChanged(ResourcesChangedEventArgs e)
{
var count = VisualChildren.Count;
for (var i = 0; i < count; ++i)
{
if (VisualChildren[i] is ILogical logical)
{
logical.NotifyResourcesChanged(e);
}
}
base.NotifyChildResourcesChanged(e);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {

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

@ -280,17 +280,13 @@ namespace Avalonia.Controls.Primitives
return arrangeSize; return arrangeSize;
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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>());
} }
} }

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

@ -55,8 +55,15 @@ namespace Avalonia.Controls.Primitives
((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this)); ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this));
InvalidateArrange(); InvalidateArrange();
} }
protected override void NotifyChildResourcesChanged(ResourcesChangedEventArgs e)
{
foreach (var l in _layers)
((ILogical)l).NotifyResourcesChanged(e);
base.NotifyChildResourcesChanged(e);
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
base.OnAttachedToLogicalTree(e); base.OnAttachedToLogicalTree(e);
@ -74,7 +81,6 @@ namespace Avalonia.Controls.Primitives
((ILogical)l).NotifyDetachedFromLogicalTree(e); ((ILogical)l).NotifyDetachedFromLogicalTree(e);
} }
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
foreach (var l in _layers) foreach (var l in _layers)

16
src/Avalonia.Controls/ProgressBar.cs

@ -83,21 +83,17 @@ namespace Avalonia.Controls
return base.ArrangeOverride(finalSize); return base.ArrangeOverride(finalSize);
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -375,37 +376,46 @@ namespace Avalonia.Controls
_viewportManager.ResetScrollers(); _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 oldEnumerable = change.OldValue.GetValueOrDefault<IEnumerable>();
var newDataSource = newEnumerable as ItemsSourceView; var newEnumerable = change.NewValue.GetValueOrDefault<IEnumerable>();
if (newEnumerable != null && newDataSource == null)
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) internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
@ -431,8 +441,16 @@ namespace Avalonia.Controls
private int GetElementIndexImpl(IControl element) private int GetElementIndexImpl(IControl element)
{ {
var virtInfo = TryGetVirtualizationInfo(element); // Verify that element is actually a child of this ItemsRepeater
return _viewManager.GetElementIndex(virtInfo); var parent = element.GetVisualParent();
if (parent == this)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
return -1;
} }
private IControl GetElementFromIndexImpl(int index) private IControl GetElementFromIndexImpl(int index)

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

@ -388,19 +388,24 @@ namespace Avalonia.Controls
} }
case NotifyCollectionChangedAction.Reset: 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 // Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool. // the stable id reset pool.
foreach (var element in _owner.Children) foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{ {
_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(); _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); var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool(); virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index); 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); _pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement; element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool(); elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
break; break;
} }
} }

35
src/Avalonia.Controls/SelectionModel.cs

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

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

@ -1,5 +1,4 @@
using System; using System;
using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
namespace Avalonia.Controls.Shapes namespace Avalonia.Controls.Shapes
@ -9,6 +8,8 @@ namespace Avalonia.Controls.Shapes
public static readonly StyledProperty<Geometry> DataProperty = public static readonly StyledProperty<Geometry> DataProperty =
AvaloniaProperty.Register<Path, Geometry>(nameof(Data)); AvaloniaProperty.Register<Path, Geometry>(nameof(Data));
private EventHandler _geometryChangedHandler;
static Path() static Path()
{ {
AffectsGeometry<Path>(DataProperty); AffectsGeometry<Path>(DataProperty);
@ -21,21 +22,48 @@ namespace Avalonia.Controls.Shapes
set { SetValue(DataProperty, value); } set { SetValue(DataProperty, value); }
} }
private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= GeometryChanged;
protected override Geometry CreateDefiningGeometry() => Data; 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) private void DataChanged(AvaloniaPropertyChangedEventArgs e)
{ {
if (VisualRoot is null)
{
return;
}
var oldGeometry = (Geometry)e.OldValue; var oldGeometry = (Geometry)e.OldValue;
var newGeometry = (Geometry)e.NewValue; var newGeometry = (Geometry)e.NewValue;
if (oldGeometry is object) if (oldGeometry is object)
{ {
oldGeometry.Changed -= GeometryChanged; oldGeometry.Changed -= GeometryChangedHandler;
} }
if (newGeometry is object) 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.Collections;
using Avalonia.Media; using Avalonia.Media;
#nullable enable
namespace Avalonia.Controls.Shapes 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 abstract class Shape : Control
{ {
public static readonly StyledProperty<IBrush> FillProperty = /// <summary>
AvaloniaProperty.Register<Shape, IBrush>(nameof(Fill)); /// 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 = public static readonly StyledProperty<Stretch> StretchProperty =
AvaloniaProperty.Register<Shape, Stretch>(nameof(Stretch)); AvaloniaProperty.Register<Shape, Stretch>(nameof(Stretch));
public static readonly StyledProperty<IBrush> StrokeProperty = /// <summary>
AvaloniaProperty.Register<Shape, IBrush>(nameof(Stroke)); /// 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 = /// <summary>
AvaloniaProperty.Register<Shape, AvaloniaList<double>>(nameof(StrokeDashArray)); /// 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 = public static readonly StyledProperty<double> StrokeDashOffsetProperty =
AvaloniaProperty.Register<Shape, double>(nameof(StrokeDashOffset)); AvaloniaProperty.Register<Shape, double>(nameof(StrokeDashOffset));
/// <summary>
/// Defines the <see cref="StrokeThickness"/> property.
/// </summary>
public static readonly StyledProperty<double> StrokeThicknessProperty = public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<Shape, double>(nameof(StrokeThickness)); AvaloniaProperty.Register<Shape, double>(nameof(StrokeThickness));
/// <summary>
/// Defines the <see cref="StrokeLineCap"/> property.
/// </summary>
public static readonly StyledProperty<PenLineCap> StrokeLineCapProperty = public static readonly StyledProperty<PenLineCap> StrokeLineCapProperty =
AvaloniaProperty.Register<Shape, PenLineCap>(nameof(StrokeLineCap), PenLineCap.Flat); AvaloniaProperty.Register<Shape, PenLineCap>(nameof(StrokeLineCap), PenLineCap.Flat);
/// <summary>
/// Defines the <see cref="StrokeJoin"/> property.
/// </summary>
public static readonly StyledProperty<PenLineJoin> StrokeJoinProperty = public static readonly StyledProperty<PenLineJoin> StrokeJoinProperty =
AvaloniaProperty.Register<Shape, PenLineJoin>(nameof(StrokeJoin), PenLineJoin.Miter); AvaloniaProperty.Register<Shape, PenLineJoin>(nameof(StrokeJoin), PenLineJoin.Miter);
private Matrix _transform = Matrix.Identity; private Matrix _transform = Matrix.Identity;
private Geometry _definingGeometry; private Geometry? _definingGeometry;
private Geometry _renderedGeometry; private Geometry? _renderedGeometry;
bool _calculateTransformOnArrange = false; private bool _calculateTransformOnArrange;
static Shape() static Shape()
{ {
@ -43,7 +72,10 @@ namespace Avalonia.Controls.Shapes
StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty); StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty);
} }
public Geometry DefiningGeometry /// <summary>
/// Gets a value that represents the <see cref="Geometry"/> of the shape.
/// </summary>
public Geometry? DefiningGeometry
{ {
get get
{ {
@ -56,13 +88,10 @@ namespace Avalonia.Controls.Shapes
} }
} }
public IBrush Fill /// <summary>
{ /// Gets a value that represents the final rendered <see cref="Geometry"/> of the shape.
get { return GetValue(FillProperty); } /// </summary>
set { SetValue(FillProperty, value); } public Geometry? RenderedGeometry
}
public Geometry RenderedGeometry
{ {
get 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 public Stretch Stretch
{ {
get { return GetValue(StretchProperty); } get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); } 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); } get { return GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); } 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); } get { return GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); } 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 public double StrokeDashOffset
{ {
get { return GetValue(StrokeDashOffsetProperty); } get { return GetValue(StrokeDashOffsetProperty); }
set { SetValue(StrokeDashOffsetProperty, value); } set { SetValue(StrokeDashOffsetProperty, value); }
} }
/// <summary>
/// Gets or sets the width of the shape outline.
/// </summary>
public double StrokeThickness public double StrokeThickness
{ {
get { return GetValue(StrokeThicknessProperty); } get { return GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); } 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 public PenLineCap StrokeLineCap
{ {
get { return GetValue(StrokeLineCapProperty); } get { return GetValue(StrokeLineCapProperty); }
set { SetValue(StrokeLineCapProperty, value); } 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 public PenLineJoin StrokeJoin
{ {
get { return GetValue(StrokeJoinProperty); } 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() protected void InvalidateGeometry()
{ {
_renderedGeometry = null; _renderedGeometry = null;
_definingGeometry = null; _definingGeometry = null;
InvalidateMeasure(); InvalidateMeasure();
} }
@ -321,8 +388,8 @@ namespace Avalonia.Controls.Shapes
// portion changes. // portion changes.
if (e.Property == BoundsProperty) if (e.Property == BoundsProperty)
{ {
var oldBounds = (Rect)e.OldValue; var oldBounds = (Rect)e.OldValue!;
var newBounds = (Rect)e.NewValue; var newBounds = (Rect)e.NewValue!;
if (oldBounds.Size == newBounds.Size) if (oldBounds.Size == newBounds.Size)
{ {

12
src/Avalonia.Controls/Slider.cs

@ -134,17 +134,13 @@ namespace Avalonia.Controls
} }
} }
protected override void OnPropertyChanged<T>( protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{ {
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>());
} }
} }

110
src/Avalonia.Controls/TopLevel.cs

@ -6,6 +6,7 @@ using Avalonia.Input.Raw;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
@ -43,13 +44,35 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IInputElement> PointerOverElementProperty = public static readonly StyledProperty<IInputElement> PointerOverElementProperty =
AvaloniaProperty.Register<TopLevel, IInputElement>(nameof(IInputRoot.PointerOverElement)); AvaloniaProperty.Register<TopLevel, IInputElement>(nameof(IInputRoot.PointerOverElement));
/// <summary>
/// Defines the <see cref="TransparencyLevelHint"/> property.
/// </summary>
public static readonly StyledProperty<WindowTransparencyLevel> TransparencyLevelHintProperty =
AvaloniaProperty.Register<TopLevel, WindowTransparencyLevel>(nameof(TransparencyLevelHint), WindowTransparencyLevel.None);
/// <summary>
/// Defines the <see cref="ActualTransparencyLevel"/> property.
/// </summary>
public static readonly DirectProperty<TopLevel, WindowTransparencyLevel> ActualTransparencyLevelProperty =
AvaloniaProperty.RegisterDirect<TopLevel, WindowTransparencyLevel>(nameof(ActualTransparencyLevel),
o => o.ActualTransparencyLevel,
unsetValue: WindowTransparencyLevel.None);
/// <summary>
/// Defines the <see cref="TransparencyBackgroundFallbackProperty"/> property.
/// </summary>
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);
private readonly IInputManager _inputManager; private readonly IInputManager _inputManager;
private readonly IAccessKeyHandler _accessKeyHandler; private readonly IAccessKeyHandler _accessKeyHandler;
private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler;
private readonly IPlatformRenderInterface _renderInterface; private readonly IPlatformRenderInterface _renderInterface;
private readonly IGlobalStyles _globalStyles; private readonly IGlobalStyles _globalStyles;
private Size _clientSize; private Size _clientSize;
private WindowTransparencyLevel _actualTransparencyLevel;
private ILayoutManager _layoutManager; private ILayoutManager _layoutManager;
private Border _transparencyFallbackBorder;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class. /// Initializes static members of the <see cref="TopLevel"/> class.
@ -57,6 +80,16 @@ namespace Avalonia.Controls
static TopLevel() static TopLevel()
{ {
AffectsMeasure<TopLevel>(ClientSizeProperty); AffectsMeasure<TopLevel>(ClientSizeProperty);
TransparencyLevelHintProperty.Changed.AddClassHandler<TopLevel>(
(tl, e) =>
{
if (tl.PlatformImpl != null)
{
tl.PlatformImpl.SetTransparencyLevelHint((WindowTransparencyLevel)e.NewValue);
tl.HandleTransparencyLevelChanged(tl.PlatformImpl.TransparencyLevel);
}
});
} }
/// <summary> /// <summary>
@ -85,6 +118,8 @@ namespace Avalonia.Controls
PlatformImpl = impl; PlatformImpl = impl;
_actualTransparencyLevel = PlatformImpl.TransparencyLevel;
dependencyResolver = dependencyResolver ?? AvaloniaLocator.Current; dependencyResolver = dependencyResolver ?? AvaloniaLocator.Current;
var styler = TryGetService<IStyler>(dependencyResolver); var styler = TryGetService<IStyler>(dependencyResolver);
@ -108,6 +143,7 @@ namespace Avalonia.Controls
impl.Paint = HandlePaint; impl.Paint = HandlePaint;
impl.Resized = HandleResized; impl.Resized = HandleResized;
impl.ScalingChanged = HandleScalingChanged; impl.ScalingChanged = HandleScalingChanged;
impl.TransparencyLevelChanged = HandleTransparencyLevelChanged;
_keyboardNavigationHandler?.SetOwner(this); _keyboardNavigationHandler?.SetOwner(this);
_accessKeyHandler?.SetOwner(this); _accessKeyHandler?.SetOwner(this);
@ -127,11 +163,11 @@ namespace Avalonia.Controls
x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>()) x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>())
.Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor)); .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor));
if (((IStyleHost)this).StylingParent is IResourceProvider applicationResources) if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
{ {
WeakSubscriptionManager.Subscribe( WeakSubscriptionManager.Subscribe(
applicationResources, applicationResources,
nameof(IResourceProvider.ResourcesChanged), nameof(IResourceHost.ResourcesChanged),
this); this);
} }
} }
@ -155,6 +191,34 @@ namespace Avalonia.Controls
protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); }
} }
/// <summary>
/// Gets or sets the <see cref="WindowTransparencyLevel"/> that the TopLevel should use when possible.
/// </summary>
public WindowTransparencyLevel TransparencyLevelHint
{
get { return GetValue(TransparencyLevelHintProperty); }
set { SetValue(TransparencyLevelHintProperty, value); }
}
/// <summary>
/// Gets the acheived <see cref="WindowTransparencyLevel"/> that the platform was able to provide.
/// </summary>
public WindowTransparencyLevel ActualTransparencyLevel
{
get => _actualTransparencyLevel;
private set => SetAndRaise(ActualTransparencyLevelProperty, ref _actualTransparencyLevel, value);
}
/// <summary>
/// Gets or sets the <see cref="IBrush"/> that transparency will blend with when transparency is not supported.
/// By default this is a solid white brush.
/// </summary>
public IBrush TransparencyBackgroundFallback
{
get => GetValue(TransparencyBackgroundFallbackProperty);
set => SetValue(TransparencyBackgroundFallbackProperty, value);
}
public ILayoutManager LayoutManager public ILayoutManager LayoutManager
{ {
get get
@ -312,6 +376,39 @@ namespace Avalonia.Controls
LayoutHelper.InvalidateSelfAndChildrenMeasure(this); LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
} }
private bool TransparencyLevelsMatch (WindowTransparencyLevel requested, WindowTransparencyLevel received)
{
if(requested == received)
{
return true;
}
else if(requested >= WindowTransparencyLevel.Blur && received >= WindowTransparencyLevel.Blur)
{
return true;
}
return false;
}
protected virtual void HandleTransparencyLevelChanged(WindowTransparencyLevel transparencyLevel)
{
if(_transparencyFallbackBorder != null)
{
if(transparencyLevel == WindowTransparencyLevel.None ||
TransparencyLevelHint == WindowTransparencyLevel.None ||
!TransparencyLevelsMatch(TransparencyLevelHint, transparencyLevel))
{
_transparencyFallbackBorder.Background = TransparencyBackgroundFallback;
}
else
{
_transparencyFallbackBorder.Background = Brushes.Transparent;
}
}
ActualTransparencyLevel = transparencyLevel;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
@ -321,6 +418,15 @@ namespace Avalonia.Controls
$"Control '{GetType().Name}' is a top level control and cannot be added as a child."); $"Control '{GetType().Name}' is a top level control and cannot be added as a child.");
} }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_transparencyFallbackBorder = e.NameScope.Find<Border>("PART_TransparencyFallback");
HandleTransparencyLevelChanged(PlatformImpl.TransparencyLevel);
}
/// <summary> /// <summary>
/// Raises the <see cref="Opened"/> event. /// Raises the <see cref="Opened"/> event.
/// </summary> /// </summary>

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.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
} }

121
src/Avalonia.Controls/Window.cs

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

2
src/Avalonia.Controls/WindowBase.cs

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

25
src/Avalonia.Controls/WindowTransparencyLevel.cs

@ -0,0 +1,25 @@
namespace Avalonia.Controls
{
public enum WindowTransparencyLevel
{
/// <summary>
/// The window background is Black where nothing is drawn in the window.
/// </summary>
None,
/// <summary>
/// The window background is Transparent where nothing is drawn in the window.
/// </summary>
Transparent,
/// <summary>
/// The window background is a blur-behind where nothing is drawn in the window.
/// </summary>
Blur,
/// <summary>
/// The window background is a blur-behind with a high blur radius. This level may fallback to Blur.
/// </summary>
AcrylicBlur
}
}

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

@ -83,6 +83,7 @@ namespace Avalonia.DesignerSupport.Remote
} }
public IScreenImpl Screen { get; } = new ScreenStub(); public IScreenImpl Screen { get; } = new ScreenStub();
public Action GotInputWhenDisabled { get; set; }
public void Activate() public void Activate()
{ {
@ -115,5 +116,13 @@ namespace Avalonia.DesignerSupport.Remote
public void SetTopmost(bool value) 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) catch (Exception e)
{ {
var xmlException = e as XmlException;
s_transport.Send(new UpdateXamlResultMessage s_transport.Send(new UpdateXamlResultMessage
{ {
Error = e.ToString(), Error = e.ToString(),
Exception = new ExceptionDetails Exception = new ExceptionDetails(e),
{
ExceptionType = e.GetType().FullName,
Message = e.Message.ToString(),
LineNumber = xmlException?.LineNumber,
LinePosition = xmlException?.LinePosition,
}
}); });
} }
} }

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

@ -37,6 +37,8 @@ namespace Avalonia.DesignerSupport.Remote
public WindowState WindowState { get; set; } public WindowState WindowState { get; set; }
public Action<WindowState> WindowStateChanged { get; set; } public Action<WindowState> WindowStateChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public WindowStub(IWindowImpl parent = null) public WindowStub(IWindowImpl parent = null)
{ {
if (parent != null) if (parent != null)
@ -130,7 +132,21 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public void SetParent(IWindowImpl parent)
{
}
public void SetEnabled(bool enable)
{
}
public IPopupPositioner PopupPositioner { get; } public IPopupPositioner PopupPositioner { get; }
public Action GotInputWhenDisabled { get; set; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { }
public WindowTransparencyLevel TransparencyLevel { get; private set; }
} }
class ClipboardStub : IClipboard 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, double lineSpacing,
int maxItemsPerLine, int maxItemsPerLine,
ScrollOrientation orientation, ScrollOrientation orientation,
bool disableVirtualization,
string layoutId) string layoutId)
{ {
_orientation.ScrollOrientation = orientation; _orientation.ScrollOrientation = orientation;
@ -95,14 +96,14 @@ namespace Avalonia.Layout
_elementManager.OnBeginMeasure(orientation); _elementManager.OnBeginMeasure(orientation);
int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
if (isWrapping && IsReflowRequired()) if (isWrapping && IsReflowRequired())
{ {
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0); _orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); _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(); RaiseLineArranged();
@ -273,6 +274,7 @@ namespace Avalonia.Layout
double minItemSpacing, double minItemSpacing,
double lineSpacing, double lineSpacing,
int maxItemsPerLine, int maxItemsPerLine,
bool disableVirtualization,
string layoutId) string layoutId)
{ {
if (anchorIndex != -1) if (anchorIndex != -1)
@ -288,7 +290,7 @@ namespace Avalonia.Layout
bool lineNeedsReposition = false; bool lineNeedsReposition = false;
while (_elementManager.IsIndexValidInData(currentIndex) && while (_elementManager.IsIndexValidInData(currentIndex) &&
ShouldContinueFillingUpSpace(previousIndex, direction)) (disableVirtualization || ShouldContinueFillingUpSpace(previousIndex, direction)))
{ {
// Ensure layout element. // Ensure layout element.
_elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId); _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);

58
src/Avalonia.Layout/LayoutHelper.cs

@ -82,6 +82,64 @@ namespace Avalonia.Layout
InnerInvalidateMeasure(control); InnerInvalidateMeasure(control);
} }
/// <summary>
/// Rounds a size to integer values for layout purposes, compensating for high DPI screen
/// coordinates.
/// </summary>
/// <param name="size">Input size.</param>
/// <param name="dpiScaleX">DPI along x-dimension.</param>
/// <param name="dpiScaleY">DPI along y-dimension.</param>
/// <returns>Value of size that will be rounded under screen DPI.</returns>
/// <remarks>
/// This is a layout helper method. It takes DPI into account and also does not return
/// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper
/// associated with the UseLayoutRounding property and should not be used as a general rounding
/// utility.
/// </remarks>
public static Size RoundLayoutSize(Size size, double dpiScaleX, double dpiScaleY)
{
return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY));
}
/// <summary>
/// Calculates the value to be used for layout rounding at high DPI.
/// </summary>
/// <param name="value">Input value to be rounded.</param>
/// <param name="dpiScale">Ratio of screen's DPI to layout DPI</param>
/// <returns>Adjusted value that will produce layout rounding on screen at high dpi.</returns>
/// <remarks>
/// This is a layout helper method. It takes DPI into account and also does not return
/// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper
/// associated with the UseLayoutRounding property and should not be used as a general rounding
/// utility.
/// </remarks>
public static double RoundLayoutValue(double value, double dpiScale)
{
double newValue;
// If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.AreClose(dpiScale, 1.0))
{
newValue = Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue),
// use the original value.
if (double.IsNaN(newValue) ||
double.IsInfinity(newValue) ||
MathUtilities.AreClose(newValue, double.MaxValue))
{
newValue = value;
}
}
else
{
newValue = Math.Round(value);
}
return newValue;
}
/// <summary> /// <summary>
/// Calculates the min and max height for a control. Ported from WPF. /// Calculates the min and max height for a control. Ported from WPF.
/// </summary> /// </summary>

17
src/Avalonia.Layout/Layoutable.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Layout namespace Avalonia.Layout
@ -545,8 +546,8 @@ namespace Avalonia.Layout
if (UseLayoutRounding) if (UseLayoutRounding)
{ {
var scale = GetLayoutScale(); var scale = GetLayoutScale();
width = Math.Ceiling(width * scale) / scale; width = LayoutHelper.RoundLayoutValue(width, scale);
height = Math.Ceiling(height * scale) / scale; height = LayoutHelper.RoundLayoutValue(height, scale);
} }
return NonNegative(new Size(width, height).Inflate(margin)); return NonNegative(new Size(width, height).Inflate(margin));
@ -623,12 +624,8 @@ namespace Avalonia.Layout
if (useLayoutRounding) if (useLayoutRounding)
{ {
size = new Size( size = LayoutHelper.RoundLayoutSize(size, scale, scale);
Math.Ceiling(size.Width * scale) / scale, availableSizeMinusMargins = LayoutHelper.RoundLayoutSize(availableSizeMinusMargins, scale, scale);
Math.Ceiling(size.Height * scale) / scale);
availableSizeMinusMargins = new Size(
Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
} }
size = ArrangeOverride(size).Constrain(size); size = ArrangeOverride(size).Constrain(size);
@ -657,8 +654,8 @@ namespace Avalonia.Layout
if (useLayoutRounding) if (useLayoutRounding)
{ {
originX = Math.Floor(originX * scale) / scale; originX = LayoutHelper.RoundLayoutValue(originX, scale);
originY = Math.Floor(originY * scale) / scale; originY = LayoutHelper.RoundLayoutValue(originY, scale);
} }
Bounds = new Rect(originX, originY, size.Width, size.Height); Bounds = new Rect(originX, originY, size.Width, size.Height);

26
src/Avalonia.Layout/StackLayout.cs

@ -14,6 +14,12 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates 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> /// <summary>
/// Defines the <see cref="Orientation"/> property. /// Defines the <see cref="Orientation"/> property.
/// </summary> /// </summary>
@ -36,6 +42,15 @@ namespace Avalonia.Layout
LayoutId = "StackLayout"; 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> /// <summary>
/// Gets or sets the axis along which items are laid out. /// Gets or sets the axis along which items are laid out.
/// </summary> /// </summary>
@ -262,6 +277,8 @@ namespace Avalonia.Layout
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{ {
((StackLayoutState)context.LayoutState).OnMeasureStart();
var desiredSize = GetFlowAlgorithm(context).Measure( var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize, availableSize,
context, context,
@ -270,6 +287,7 @@ namespace Avalonia.Layout
Spacing, Spacing,
int.MaxValue, int.MaxValue,
_orientation.ScrollOrientation, _orientation.ScrollOrientation,
DisableVirtualization,
LayoutId); LayoutId);
return new Size(desiredSize.Width, desiredSize.Height); return new Size(desiredSize.Width, desiredSize.Height);
@ -284,8 +302,6 @@ namespace Avalonia.Layout
FlowLayoutAlgorithm.LineAlignment.Start, FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId); LayoutId);
((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
return new Size(value.Width, value.Height); return new Size(value.Width, value.Height);
} }
@ -296,11 +312,11 @@ namespace Avalonia.Layout
InvalidateLayout(); 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. //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
//Horizontal Orientation means we have a Horizontal 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); 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, LineSpacing,
_maximumRowsOrColumns, _maximumRowsOrColumns,
_orientation.ScrollOrientation, _orientation.ScrollOrientation,
false,
LayoutId); LayoutId);
// If after Measure the first item is in the realization rect, then we revoke grid state's ownership, // 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); 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. //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. //i.e. the properties are the inverse of each other.
var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal;
_orientation.ScrollOrientation = scrollOrientation; _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(); InvalidateLayout();

5
src/Avalonia.Native/PopupImpl.cs

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

26
src/Avalonia.Native/WindowImpl.cs

@ -40,7 +40,7 @@ namespace Avalonia.Native
bool IAvnWindowEvents.Closing() bool IAvnWindowEvents.Closing()
{ {
if(_parent.Closing != null) if (_parent.Closing != null)
{ {
return _parent.Closing(); return _parent.Closing();
} }
@ -52,15 +52,15 @@ namespace Avalonia.Native
{ {
_parent.WindowStateChanged?.Invoke((WindowState)state); _parent.WindowStateChanged?.Invoke((WindowState)state);
} }
void IAvnWindowEvents.GotInputWhenDisabled()
{
_parent.GotInputWhenDisabled?.Invoke();
}
} }
public IAvnWindow Native => _native; public IAvnWindow Native => _native;
public void ShowDialog(IWindowImpl window)
{
_native.ShowDialog(((WindowImpl)window).Native);
}
public void CanResize(bool value) public void CanResize(bool value)
{ {
_native.CanResize = value; _native.CanResize = value;
@ -71,7 +71,7 @@ namespace Avalonia.Native
_native.Decorations = (Interop.SystemDecorations)enabled; _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 }); _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() => public override IPopupImpl CreatePopup() =>
_opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, _glFeature, this); _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);
}
} }
} }

25
src/Avalonia.Native/WindowImplBase.cs

@ -369,6 +369,8 @@ namespace Avalonia.Native
Action<double> ITopLevelImpl.ScalingChanged { get; set; } Action<double> ITopLevelImpl.ScalingChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public IScreenImpl Screen { get; private set; } public IScreenImpl Screen { get; private set; }
// TODO // TODO
@ -389,6 +391,29 @@ namespace Avalonia.Native
_native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); _native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle);
} }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
if (TransparencyLevel != transparencyLevel)
{
if (transparencyLevel >= WindowTransparencyLevel.Blur)
{
transparencyLevel = WindowTransparencyLevel.AcrylicBlur;
}
if(transparencyLevel == WindowTransparencyLevel.None)
{
transparencyLevel = WindowTransparencyLevel.Transparent;
}
TransparencyLevel = transparencyLevel;
_native.SetBlurEnabled(TransparencyLevel >= WindowTransparencyLevel.Blur);
TransparencyLevelChanged?.Invoke(TransparencyLevel);
}
}
public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent;
public IPlatformHandle Handle { get; private set; } public IPlatformHandle Handle { get; private set; }
} }
} }

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

@ -1,4 +1,7 @@
using System; using System;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Xml;
namespace Avalonia.Remote.Protocol.Designer namespace Avalonia.Remote.Protocol.Designer
{ {
@ -26,6 +29,27 @@ namespace Avalonia.Remote.Protocol.Designer
public class ExceptionDetails 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 ExceptionType { get; set; }
public string Message { get; set; } public string Message { get; set; }
public int? LineNumber { get; set; } public int? LineNumber { get; set; }

4
src/Avalonia.Styling/Controls/IResourceDictionary.cs

@ -1,11 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// An indexed dictionary of resources. /// An indexed dictionary of resources.
/// </summary> /// </summary>
public interface IResourceDictionary : IResourceProvider, IDictionary<object, object> public interface IResourceDictionary : IResourceProvider, IDictionary<object, object?>
{ {
/// <summary> /// <summary>
/// Gets a collection of child resource dictionaries. /// Gets a collection of child resource dictionaries.

32
src/Avalonia.Styling/Controls/IResourceHost.cs

@ -0,0 +1,32 @@
using System;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Represents an element which hosts resources.
/// </summary>
/// <remarks>
/// This interface is implemented by <see cref="StyledElement"/> and `Application`.
/// </remarks>
public interface IResourceHost : IResourceNode
{
/// <summary>
/// Raised when the resources change on the element or an ancestor of the element.
/// </summary>
event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
/// <summary>
/// Notifies the resource host that one or more of its hosted resources has changed.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method will be called automatically by the framework, you should not need to call
/// this method yourself. It is called when the resources hosted by this element have
/// changed, and is usually called by a resource dictionary or style hosted by the element
/// in response to a resource being added or removed.
/// </remarks>
void NotifyHostedResourcesChanged(ResourcesChangedEventArgs e);
}
}

28
src/Avalonia.Styling/Controls/IResourceNode.cs

@ -1,15 +1,35 @@
using System; using System;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Represents resource provider in a tree. /// Represents an object that can be queried for resources.
/// </summary> /// </summary>
public interface IResourceNode : IResourceProvider /// <remarks>
/// The interface represents a common interface for both controls that host resources
/// (<see cref="IResourceHost"/>) and resource providers such as <see cref="ResourceDictionary"/>
/// (see <see cref="IResourceProvider"/>).
/// </remarks>
public interface IResourceNode
{ {
/// <summary> /// <summary>
/// Gets the parent resource node, if any. /// Gets a value indicating whether the object has resources.
/// </summary>
bool HasResources { get; }
/// <summary>
/// Tries to find a resource within the object.
/// </summary> /// </summary>
IResourceNode ResourceParent { get; } /// <param name="key">The resource key.</param>
/// <param name="value">
/// When this method returns, contains the value associated with the specified key,
/// if the key is found; otherwise, null.
/// </param>
/// <returns>
/// True if the resource if found, otherwise false.
/// </returns>
bool TryGetResource(object key, out object? value);
} }
} }

41
src/Avalonia.Styling/Controls/IResourceProvider.cs

@ -1,33 +1,42 @@
using System; using System;
using Avalonia.Styling;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Represents an object that can be queried for resources. /// Represents an object that can be queried for resources but does not appear in the logical tree.
/// </summary> /// </summary>
public interface IResourceProvider /// <remarks>
/// This interface is implemented by <see cref="ResourceDictionary"/>, <see cref="Style"/> and
/// <see cref="Styles"/>
/// </remarks>
public interface IResourceProvider : IResourceNode
{ {
/// <summary> /// <summary>
/// Raised when resources in the provider are changed. /// Gets the owner of the resource provider.
/// </summary>
/// <remarks>
/// If multiple owners are added, returns the first.
/// </remarks>
IResourceHost? Owner { get; }
/// <summary>
/// Raised when the <see cref="Owner"/> of the resource provider changes.
/// </summary> /// </summary>
event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; event EventHandler OwnerChanged;
/// <summary> /// <summary>
/// Gets a value indicating whether the element has resources. /// Adds an owner to the resource provider.
/// </summary> /// </summary>
bool HasResources { get; } /// <param name="owner">The owner.</param>
void AddOwner(IResourceHost owner);
/// <summary> /// <summary>
/// Tries to find a resource within the provider. /// Removes a resource provider owner.
/// </summary> /// </summary>
/// <param name="key">The resource key.</param> /// <param name="owner">The owner.</param>
/// <param name="value"> void RemoveOwner(IResourceHost owner);
/// When this method returns, contains the value associated with the specified key,
/// if the key is found; otherwise, null.
/// </param>
/// <returns>
/// True if the resource if found, otherwise false.
/// </returns>
bool TryGetResource(object key, out object value);
} }
} }

27
src/Avalonia.Styling/Controls/ISetResourceParent.cs

@ -1,27 +0,0 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines an interface through which an <see cref="IResourceNode"/>'s parent can be set.
/// </summary>
/// <remarks>
/// You should not usually need to use this interface - it is for internal use only.
/// </remarks>
public interface ISetResourceParent : IResourceNode
{
/// <summary>
/// Sets the resource parent.
/// </summary>
/// <param name="parent">The parent.</param>
void SetParent(IResourceNode parent);
/// <summary>
/// Notifies the resource node that a change has been made to the resources in its parent.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method will be called automatically by the framework, you should not need to call
/// this method yourself.
/// </remarks>
void ParentResourcesChanged(ResourcesChangedEventArgs e);
}
}

164
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@ -1,21 +1,20 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// An indexed dictionary of resources. /// An indexed dictionary of resources.
/// </summary> /// </summary>
public class ResourceDictionary : AvaloniaDictionary<object, object>, public class ResourceDictionary : AvaloniaDictionary<object, object?>, IResourceDictionary
IResourceDictionary,
IResourceNode,
ISetResourceParent
{ {
private IResourceNode _parent; private IResourceHost? _owner;
private AvaloniaList<IResourceProvider> _mergedDictionaries; private AvaloniaList<IResourceProvider>? _mergedDictionaries;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ResourceDictionary"/> class. /// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
@ -25,10 +24,28 @@ namespace Avalonia.Controls
CollectionChanged += OnCollectionChanged; CollectionChanged += OnCollectionChanged;
} }
/// <inheritdoc/> /// <summary>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; /// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
/// </summary>
public ResourceDictionary(IResourceHost owner)
: this()
{
Owner = owner;
}
public IResourceHost? Owner
{
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}
/// <inheritdoc/>
public IList<IResourceProvider> MergedDictionaries public IList<IResourceProvider> MergedDictionaries
{ {
get get
@ -40,71 +57,51 @@ namespace Avalonia.Controls
_mergedDictionaries.ForEachItem( _mergedDictionaries.ForEachItem(
x => x =>
{ {
if (x is ISetResourceParent setParent) if (Owner is object)
{
setParent.SetParent(this);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
{ {
OnResourcesChanged(); x.AddOwner(Owner);
} }
x.ResourcesChanged += MergedDictionaryResourcesChanged;
}, },
x => x =>
{ {
if (x is ISetResourceParent setParent) if (Owner is object)
{
setParent.SetParent(null);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
{ {
OnResourcesChanged(); x.RemoveOwner(Owner);
} }
}, null);
(x as ISetResourceParent)?.SetParent(null);
x.ResourcesChanged -= MergedDictionaryResourcesChanged;
},
() => { });
} }
return _mergedDictionaries; return _mergedDictionaries;
} }
} }
/// <inheritdoc/> bool IResourceNode.HasResources
bool IResourceProvider.HasResources
{ {
get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); get
} {
if (Count > 0)
/// <inheritdoc/> {
IResourceNode IResourceNode.ResourceParent => _parent; return true;
}
/// <inheritdoc/> if (_mergedDictionaries?.Count > 0)
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) {
{ foreach (var i in _mergedDictionaries)
NotifyMergedDictionariesResourcesChanged(e); {
ResourcesChanged?.Invoke(this, e); if (i.HasResources)
} {
return true;
}
}
}
/// <inheritdoc/> return false;
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{
throw new InvalidOperationException("The ResourceDictionary already has a parent.");
} }
_parent = parent;
} }
/// <inheritdoc/> public event EventHandler? OwnerChanged;
public bool TryGetResource(object key, out object value)
public bool TryGetResource(object key, out object? value)
{ {
if (TryGetValue(key, out value)) if (TryGetValue(key, out value))
{ {
@ -125,32 +122,63 @@ namespace Avalonia.Controls
return false; return false;
} }
private void OnResourcesChanged() void IResourceProvider.AddOwner(IResourceHost owner)
{ {
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner != null)
{
throw new InvalidOperationException("The ResourceDictionary already has a parent.");
}
Owner = owner;
var hasResources = Count > 0;
if (_mergedDictionaries is object)
{
foreach (var i in _mergedDictionaries)
{
i.AddOwner(owner);
hasResources |= i.HasResources;
}
}
if (hasResources)
{
owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
} }
private void NotifyMergedDictionariesResourcesChanged(ResourcesChangedEventArgs e) void IResourceProvider.RemoveOwner(IResourceHost owner)
{ {
if (_mergedDictionaries != null) owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner == owner)
{ {
for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) Owner = null;
var hasResources = Count > 0;
if (_mergedDictionaries is object)
{ {
if (_mergedDictionaries[i] is ISetResourceParent merged) foreach (var i in _mergedDictionaries)
{ {
merged.ParentResourcesChanged(e); i.RemoveOwner(owner);
hasResources |= i.HasResources;
} }
} }
if (hasResources)
{
owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
} }
} }
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
var ev = new ResourcesChangedEventArgs(); Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
NotifyMergedDictionariesResourcesChanged(ev);
OnResourcesChanged();
} }
private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged();
} }
} }

103
src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs

@ -1,6 +1,9 @@
using System; using System;
using Avalonia.LogicalTree;
using Avalonia.Reactive; using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public static class ResourceNodeExtensions public static class ResourceNodeExtensions
@ -11,8 +14,11 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
/// <param name="key">The resource key.</param> /// <param name="key">The resource key.</param>
/// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns> /// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
public static object FindResource(this IResourceNode control, object key) public static object? FindResource(this IResourceHost control, object key)
{ {
control = control ?? throw new ArgumentNullException(nameof(control));
key = key ?? throw new ArgumentNullException(nameof(key));
if (control.TryFindResource(key, out var value)) if (control.TryFindResource(key, out var value))
{ {
return value; return value;
@ -28,16 +34,16 @@ namespace Avalonia.Controls
/// <param name="key">The resource key.</param> /// <param name="key">The resource key.</param>
/// <param name="value">On return, contains the resource if found, otherwise null.</param> /// <param name="value">On return, contains the resource if found, otherwise null.</param>
/// <returns>True if the resource was found; otherwise false.</returns> /// <returns>True if the resource was found; otherwise false.</returns>
public static bool TryFindResource(this IResourceNode control, object key, out object value) public static bool TryFindResource(this IResourceHost control, object key, out object? value)
{ {
Contract.Requires<ArgumentNullException>(control != null); control = control ?? throw new ArgumentNullException(nameof(control));
Contract.Requires<ArgumentNullException>(key != null); key = key ?? throw new ArgumentNullException(nameof(key));
var current = control; IResourceHost? current = control;
while (current != null) while (current != null)
{ {
if (current is IResourceNode host) if (current is IResourceHost host)
{ {
if (host.TryGetResource(key, out value)) if (host.TryGetResource(key, out value))
{ {
@ -45,24 +51,35 @@ namespace Avalonia.Controls
} }
} }
current = current.ResourceParent; current = (current as IStyledElement)?.StylingParent as IResourceHost;
} }
value = null; value = null;
return false; return false;
} }
public static IObservable<object> GetResourceObservable(this IResourceNode target, object key) public static IObservable<object?> GetResourceObservable(this IStyledElement control, object key)
{
control = control ?? throw new ArgumentNullException(nameof(control));
key = key ?? throw new ArgumentNullException(nameof(key));
return new ResourceObservable(control, key);
}
public static IObservable<object?> GetResourceObservable(this IResourceProvider resourceProvider, object key)
{ {
return new ResourceObservable(target, key); resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider));
key = key ?? throw new ArgumentNullException(nameof(key));
return new FloatingResourceObservable(resourceProvider, key);
} }
private class ResourceObservable : LightweightObservableBase<object> private class ResourceObservable : LightweightObservableBase<object?>
{ {
private readonly IResourceNode _target; private readonly IStyledElement _target;
private readonly object _key; private readonly object _key;
public ResourceObservable(IResourceNode target, object key) public ResourceObservable(IStyledElement target, object key)
{ {
_target = target; _target = target;
_key = key; _key = key;
@ -78,7 +95,7 @@ namespace Avalonia.Controls
_target.ResourcesChanged -= ResourcesChanged; _target.ResourcesChanged -= ResourcesChanged;
} }
protected override void Subscribed(IObserver<object> observer, bool first) protected override void Subscribed(IObserver<object?> observer, bool first)
{ {
observer.OnNext(_target.FindResource(_key)); observer.OnNext(_target.FindResource(_key));
} }
@ -88,5 +105,65 @@ namespace Avalonia.Controls
PublishNext(_target.FindResource(_key)); PublishNext(_target.FindResource(_key));
} }
} }
private class FloatingResourceObservable : LightweightObservableBase<object?>
{
private readonly IResourceProvider _target;
private readonly object _key;
private IResourceHost? _owner;
public FloatingResourceObservable(IResourceProvider target, object key)
{
_target = target;
_key = key;
}
protected override void Initialize()
{
_target.OwnerChanged += OwnerChanged;
_owner = _target.Owner;
}
protected override void Deinitialize()
{
_target.OwnerChanged -= OwnerChanged;
_owner = null;
}
protected override void Subscribed(IObserver<object?> observer, bool first)
{
if (_target.Owner is object)
{
observer.OnNext(_target.Owner?.FindResource(_key));
}
}
private void PublishNext()
{
PublishNext(_target.Owner?.FindResource(_key));
}
private void OwnerChanged(object sender, EventArgs e)
{
if (_owner is object)
{
_owner.ResourcesChanged -= ResourcesChanged;
}
_owner = _target.Owner;
if (_owner is object)
{
_owner.ResourcesChanged += ResourcesChanged;
}
PublishNext();
}
private void ResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
PublishNext();
}
}
} }
} }

1
src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs

@ -4,5 +4,6 @@ namespace Avalonia.Controls
{ {
public class ResourcesChangedEventArgs : EventArgs public class ResourcesChangedEventArgs : EventArgs
{ {
public static new readonly ResourcesChangedEventArgs Empty = new ResourcesChangedEventArgs();
} }
} }

8
src/Avalonia.Styling/IStyledElement.cs

@ -9,8 +9,7 @@ namespace Avalonia
IStyleable, IStyleable,
IStyleHost, IStyleHost,
ILogical, ILogical,
IResourceProvider, IResourceHost,
IResourceNode,
IDataContextProvider IDataContextProvider
{ {
/// <summary> /// <summary>
@ -18,6 +17,11 @@ namespace Avalonia
/// </summary> /// </summary>
event EventHandler Initialized; event EventHandler Initialized;
/// <summary>
/// Raised when resources on the element are changed.
/// </summary>
event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
/// <summary> /// <summary>
/// Gets a value that indicates whether the element has finished initialization. /// Gets a value that indicates whether the element has finished initialization.
/// </summary> /// </summary>

123
src/Avalonia.Styling/StyledElement.cs

@ -205,52 +205,20 @@ namespace Avalonia
/// each styled element may in addition define its own styles which are applied to the styled element /// each styled element may in addition define its own styles which are applied to the styled element
/// itself and its children. /// itself and its children.
/// </remarks> /// </remarks>
public Styles Styles public Styles Styles => _styles ??= new Styles(this);
{
get
{
if (_styles is null)
{
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
NotifyResourcesChanged(new ResourcesChangedEventArgs());
}
return _styles;
}
}
/// <summary> /// <summary>
/// Gets or sets the styled element's resource dictionary. /// Gets or sets the styled element's resource dictionary.
/// </summary> /// </summary>
public IResourceDictionary Resources public IResourceDictionary Resources
{ {
get => _resources ?? (Resources = new ResourceDictionary()); get => _resources ??= new ResourceDictionary(this);
set set
{ {
value = value ?? throw new ArgumentNullException(nameof(value)); value = value ?? throw new ArgumentNullException(nameof(value));
_resources?.RemoveOwner(this);
var hadResources = false;
if (_resources != null)
{
(_resources as ISetResourceParent)?.SetParent(null);
hadResources = _resources.Count > 0;
_resources.ResourcesChanged -= ThisResourcesChanged;
}
_resources = value; _resources = value;
_resources.ResourcesChanged += ThisResourcesChanged; _resources.AddOwner(this);
if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
{
setParent.SetParent(this);
}
if (hadResources || _resources.Count > 0)
{
NotifyResourcesChanged(new ResourcesChangedEventArgs());
}
} }
} }
@ -312,10 +280,8 @@ namespace Avalonia
IAvaloniaReadOnlyList<ILogical> ILogical.LogicalChildren => LogicalChildren; IAvaloniaReadOnlyList<ILogical> ILogical.LogicalChildren => LogicalChildren;
/// <inheritdoc/> /// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; bool IResourceNode.HasResources => (_resources?.HasResources ?? false) ||
(((IResourceNode?)_styles)?.HasResources ?? false);
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode;
/// <inheritdoc/> /// <inheritdoc/>
IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes; IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
@ -407,7 +373,10 @@ namespace Avalonia
void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
/// <inheritdoc/> /// <inheritdoc/>
bool IResourceProvider.TryGetResource(object key, out object? value) void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
/// <inheritdoc/>
bool IResourceNode.TryGetResource(object key, out object? value)
{ {
value = null; value = null;
return (_resources?.TryGetResource(key, out value) ?? false) || return (_resources?.TryGetResource(key, out value) ?? false) ||
@ -442,17 +411,6 @@ namespace Avalonia
OnDetachedFromLogicalTreeCore(e); OnDetachedFromLogicalTreeCore(e);
} }
if (old != null)
{
old.ResourcesChanged -= ThisResourcesChanged;
}
if (Parent != null)
{
Parent.ResourcesChanged += ThisResourcesChanged;
}
NotifyResourcesChanged(new ResourcesChangedEventArgs());
var newRoot = FindLogicalRoot(this); var newRoot = FindLogicalRoot(this);
if (newRoot is object) if (newRoot is object)
@ -460,6 +418,18 @@ namespace Avalonia
var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent); var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent);
OnAttachedToLogicalTreeCore(e); OnAttachedToLogicalTreeCore(e);
} }
else if (parent is null)
{
// If we were attached to the logical tree, we piggyback on the tree traversal
// there to raise resources changed notifications. If we're being removed from
// the logical tree, then traverse the tree raising notifications now.
//
// We don't raise resources changed notifications if we're being attached to a
// non-rooted control beacuse it's unlikely that dynamic resources need to be
// correct until the control is added to the tree, and it causes a *lot* of
// notifications.
NotifyResourcesChanged();
}
#nullable disable #nullable disable
RaisePropertyChanged( RaisePropertyChanged(
@ -527,6 +497,28 @@ namespace Avalonia
} }
} }
/// <summary>
/// Notifies child controls that a change has been made to resources that apply to them.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void NotifyChildResourcesChanged(ResourcesChangedEventArgs e)
{
if (_logicalChildren is object)
{
var count = _logicalChildren.Count;
if (count > 0)
{
e ??= ResourcesChangedEventArgs.Empty;
for (var i = 0; i < count; ++i)
{
_logicalChildren[i].NotifyResourcesChanged(e);
}
}
}
}
/// <summary> /// <summary>
/// Called when the styled element is added to a rooted logical tree. /// Called when the styled element is added to a rooted logical tree.
/// </summary> /// </summary>
@ -656,6 +648,7 @@ namespace Avalonia
_logicalRoot = e.Root; _logicalRoot = e.Root;
ApplyStyling(); ApplyStyling();
NotifyResourcesChanged(propagate: false);
OnAttachedToLogicalTree(e); OnAttachedToLogicalTree(e);
AttachedToLogicalTree?.Invoke(this, e); AttachedToLogicalTree?.Invoke(this, e);
@ -804,31 +797,23 @@ namespace Avalonia
} }
} }
private void NotifyResourcesChanged(ResourcesChangedEventArgs e) private void NotifyResourcesChanged(
ResourcesChangedEventArgs? e = null,
bool propagate = true)
{ {
if (_notifyingResourcesChanged) if (ResourcesChanged is object)
{ {
return; e ??= ResourcesChangedEventArgs.Empty;
ResourcesChanged(this, e);
} }
try if (propagate)
{
_notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
finally
{ {
_notifyingResourcesChanged = false; e ??= ResourcesChangedEventArgs.Empty;
NotifyChildResourcesChanged(e);
} }
} }
private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
NotifyResourcesChanged(e);
}
private static IReadOnlyList<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles) private static IReadOnlyList<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles)
{ {
var count = styles.Count; var count = styles.Count;

2
src/Avalonia.Styling/Styling/IStyle.cs

@ -8,7 +8,7 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Defines the interface for styles. /// Defines the interface for styles.
/// </summary> /// </summary>
public interface IStyle : IResourceNode public interface IStyle
{ {
/// <summary> /// <summary>
/// Gets a collection of child styles. /// Gets a collection of child styles.

2
src/Avalonia.Styling/Styling/IStyleHost.cs

@ -27,7 +27,7 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Gets the parent style host element. /// Gets the parent style host element.
/// </summary> /// </summary>
IStyleHost StylingParent { get; } IStyleHost? StylingParent { get; }
/// <summary> /// <summary>
/// Called when styles are added to <see cref="Styles"/> or a nested styles collection. /// Called when styles are added to <see cref="Styles"/> or a nested styles collection.

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

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

71
src/Avalonia.Styling/Styling/Style.cs

@ -11,9 +11,9 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Defines a style. /// Defines a style.
/// </summary> /// </summary>
public class Style : AvaloniaObject, IStyle, ISetResourceParent public class Style : AvaloniaObject, IStyle, IResourceProvider
{ {
private IResourceNode? _parent; private IResourceHost? _owner;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private List<ISetter>? _setters; private List<ISetter>? _setters;
private List<IAnimation>? _animations; private List<IAnimation>? _animations;
@ -34,8 +34,18 @@ namespace Avalonia.Styling
Selector = selector(null); Selector = selector(null);
} }
/// <inheritdoc/> public IResourceHost? Owner
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged; {
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}
/// <summary> /// <summary>
/// Gets or sets a dictionary of style resources. /// Gets or sets a dictionary of style resources.
@ -47,20 +57,18 @@ namespace Avalonia.Styling
{ {
value = value ?? throw new ArgumentNullException(nameof(value)); value = value ?? throw new ArgumentNullException(nameof(value));
var hadResources = false; var hadResources = _resources?.HasResources ?? false;
if (_resources != null)
{
hadResources = _resources.HasResources;
_resources.ResourcesChanged -= ResourceDictionaryChanged;
}
_resources = value; _resources = value;
_resources.ResourcesChanged += ResourceDictionaryChanged;
if (hadResources || _resources.HasResources) if (Owner is object)
{ {
((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); _resources.AddOwner(Owner);
if (hadResources || _resources.HasResources)
{
Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
} }
} }
} }
@ -81,15 +89,11 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>(); public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
/// <inheritdoc/> bool IResourceNode.HasResources => _resources?.Count > 0;
IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>(); IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>();
/// <inheritdoc/> public event EventHandler? OwnerChanged;
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{ {
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
@ -107,7 +111,6 @@ namespace Avalonia.Styling
return match.Result; return match.Result;
} }
/// <inheritdoc/>
public bool TryGetResource(object key, out object? result) public bool TryGetResource(object key, out object? result)
{ {
result = null; result = null;
@ -130,26 +133,28 @@ namespace Avalonia.Styling
} }
} }
/// <inheritdoc/> void IResourceProvider.AddOwner(IResourceHost owner)
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{ {
ResourcesChanged?.Invoke(this, e); owner = owner ?? throw new ArgumentNullException(nameof(owner));
}
/// <inheritdoc/> if (Owner != null)
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{ {
throw new InvalidOperationException("The Style already has a parent."); throw new InvalidOperationException("The Style already has a parent.");
} }
_parent = parent; Owner = owner;
_resources?.AddOwner(owner);
} }
private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) void IResourceProvider.RemoveOwner(IResourceHost owner)
{ {
ResourcesChanged?.Invoke(this, e); owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner == owner)
{
Owner = null;
_resources?.RemoveOwner(owner);
}
} }
} }
} }

181
src/Avalonia.Styling/Styling/Styles.cs

@ -2,7 +2,6 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
@ -13,10 +12,13 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// A style that consists of a number of child styles. /// A style that consists of a number of child styles.
/// </summary> /// </summary>
public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetResourceParent public class Styles : AvaloniaObject,
IAvaloniaList<IStyle>,
IStyle,
IResourceProvider
{ {
private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>(); private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
private IResourceNode? _parent; private IResourceHost? _owner;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private Dictionary<Type, List<IStyle>?>? _cache; private Dictionary<Type, List<IStyle>?>? _cache;
private bool _notifyingResourcesChanged; private bool _notifyingResourcesChanged;
@ -27,22 +29,29 @@ namespace Avalonia.Styling
_styles.CollectionChanged += OnCollectionChanged; _styles.CollectionChanged += OnCollectionChanged;
} }
public Styles(IResourceNode parent) public Styles(IResourceHost owner)
: this() : this()
{ {
_parent = parent; Owner = owner;
} }
public event NotifyCollectionChangedEventHandler? CollectionChanged; public event NotifyCollectionChangedEventHandler? CollectionChanged;
public event EventHandler? OwnerChanged;
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <inheritdoc/>
public int Count => _styles.Count; public int Count => _styles.Count;
/// <inheritdoc/> public IResourceHost? Owner
public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources); {
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}
/// <summary> /// <summary>
/// Gets or sets a dictionary of style resources. /// Gets or sets a dictionary of style resources.
@ -54,43 +63,53 @@ namespace Avalonia.Styling
{ {
value = value ?? throw new ArgumentNullException(nameof(Resources)); value = value ?? throw new ArgumentNullException(nameof(Resources));
var hadResources = false; if (Owner is object)
if (_resources != null)
{ {
hadResources = _resources.Count > 0; _resources?.RemoveOwner(Owner);
_resources.ResourcesChanged -= NotifyResourcesChanged;
} }
_resources = value; _resources = value;
_resources.ResourcesChanged += NotifyResourcesChanged;
if (hadResources || _resources.Count > 0) if (Owner is object)
{ {
((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); _resources.AddOwner(Owner);
} }
} }
} }
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
bool ICollection<IStyle>.IsReadOnly => false; bool ICollection<IStyle>.IsReadOnly => false;
/// <inheritdoc/> bool IResourceNode.HasResources
{
get
{
if (_resources?.Count > 0)
{
return true;
}
foreach (var i in this)
{
if (i is IResourceProvider p && p.HasResources)
{
return true;
}
}
return false;
}
}
IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index]; IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index];
IReadOnlyList<IStyle> IStyle.Children => this; IReadOnlyList<IStyle> IStyle.Children => this;
/// <inheritdoc/>
public IStyle this[int index] public IStyle this[int index]
{ {
get => _styles[index]; get => _styles[index];
set => _styles[index] = value; set => _styles[index] = value;
} }
/// <inheritdoc/>
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{ {
_cache ??= new Dictionary<Type, List<IStyle>?>(); _cache ??= new Dictionary<Type, List<IStyle>?>();
@ -142,7 +161,7 @@ namespace Avalonia.Styling
for (var i = Count - 1; i >= 0; --i) for (var i = Count - 1; i >= 0; --i)
{ {
if (this[i].TryGetResource(key, out value)) if (this[i] is IResourceProvider p && p.TryGetResource(key, out value))
{ {
return true; return true;
} }
@ -203,20 +222,45 @@ namespace Avalonia.Styling
IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();
/// <inheritdoc/> /// <inheritdoc/>
void ISetResourceParent.SetParent(IResourceNode parent) void IResourceProvider.AddOwner(IResourceHost owner)
{ {
if (_parent != null && parent != null) owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner != null)
{ {
throw new InvalidOperationException("The Style already has a parent."); throw new InvalidOperationException("The Styles already has a owner.");
} }
_parent = parent; Owner = owner;
_resources?.AddOwner(owner);
foreach (var child in this)
{
if (child is IResourceProvider r)
{
r.AddOwner(owner);
}
}
} }
/// <inheritdoc/> /// <inheritdoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) void IResourceProvider.RemoveOwner(IResourceHost owner)
{ {
NotifyResourcesChanged(e); owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner == owner)
{
Owner = null;
_resources?.RemoveOwner(owner);
foreach (var child in this)
{
if (child is IResourceProvider r)
{
r.RemoveOwner(owner);
}
}
}
} }
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
@ -241,22 +285,15 @@ namespace Avalonia.Styling
{ {
var style = (IStyle)items[i]; var style = (IStyle)items[i];
if (style.ResourceParent == null && style is ISetResourceParent setParent) if (Owner is object && style is IResourceProvider resourceProvider)
{ {
setParent.SetParent(this); resourceProvider.AddOwner(Owner);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
} }
if (style.HasResources)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
style.ResourcesChanged += NotifyResourcesChanged;
_cache = null; _cache = null;
} }
GetHost()?.StylesAdded(ToReadOnlyList<IStyle>(items)); (Owner as IStyleHost)?.StylesAdded(ToReadOnlyList<IStyle>(items));
} }
void Remove(IList items) void Remove(IList items)
@ -265,22 +302,15 @@ namespace Avalonia.Styling
{ {
var style = (IStyle)items[i]; var style = (IStyle)items[i];
if (style.ResourceParent == this && style is ISetResourceParent setParent) if (Owner is object && style is IResourceProvider resourceProvider)
{
setParent.SetParent(null);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (style.HasResources)
{ {
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); resourceProvider.RemoveOwner(Owner);
} }
style.ResourcesChanged -= NotifyResourcesChanged;
_cache = null; _cache = null;
} }
GetHost()?.StylesRemoved(ToReadOnlyList<IStyle>(items)); (Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList<IStyle>(items));
} }
switch (e.Action) switch (e.Action)
@ -301,50 +331,5 @@ namespace Avalonia.Styling
CollectionChanged?.Invoke(this, e); CollectionChanged?.Invoke(this, e);
} }
private IStyleHost? GetHost()
{
var node = _parent;
while (node != null)
{
if (node is IStyleHost host)
{
return host;
}
node = node.ResourceParent;
}
return null;
}
private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
NotifyResourcesChanged(e);
}
private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
if (_notifyingResourcesChanged)
{
return;
}
try
{
_notifyingResourcesChanged = true;
foreach (var child in this)
{
(child as ISetResourceParent)?.ParentResourcesChanged(e);
}
ResourcesChanged?.Invoke(this, e);
}
finally
{
_notifyingResourcesChanged = false;
}
}
} }
} }

19
src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml

@ -3,14 +3,17 @@
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/> <Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{TemplateBinding Background}"> <Panel>
<VisualLayerManager> <Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<ContentPresenter Name="PART_ContentPresenter" <Border Background="{TemplateBinding Background}">
ContentTemplate="{TemplateBinding ContentTemplate}" <VisualLayerManager>
Content="{TemplateBinding Content}" <ContentPresenter Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}"/> ContentTemplate="{TemplateBinding ContentTemplate}"
</VisualLayerManager> Content="{TemplateBinding Content}"
</Border> Margin="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Border>
</Panel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

17
src/Avalonia.Themes.Default/OverlayPopupHost.xaml

@ -2,13 +2,16 @@
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/> <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<VisualLayerManager IsPopup="True"> <Panel>
<ContentPresenter Name="PART_ContentPresenter" <Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
Background="{TemplateBinding Background}" <VisualLayerManager IsPopup="True">
ContentTemplate="{TemplateBinding ContentTemplate}" <ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}" Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"/> ContentTemplate="{TemplateBinding ContentTemplate}"
</VisualLayerManager> Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

17
src/Avalonia.Themes.Default/PopupRoot.xaml

@ -2,13 +2,16 @@
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/> <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<VisualLayerManager IsPopup="True"> <Panel>
<ContentPresenter Name="PART_ContentPresenter" <Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
Background="{TemplateBinding Background}" <VisualLayerManager IsPopup="True">
ContentTemplate="{TemplateBinding ContentTemplate}" <ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}" Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"/> ContentTemplate="{TemplateBinding ContentTemplate}"
</VisualLayerManager> Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

6
src/Avalonia.Themes.Default/ScrollBar.xaml

@ -3,7 +3,8 @@
<Setter Property="Cursor" Value="Arrow" /> <Setter Property="Cursor" Value="Arrow" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{DynamicResource ThemeControlMidBrush}"> <Border Background="{DynamicResource ThemeControlMidBrush}"
UseLayoutRounding="False">
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">
<RepeatButton Name="PART_LineUpButton" HorizontalAlignment="Center" <RepeatButton Name="PART_LineUpButton" HorizontalAlignment="Center"
Classes="repeat" Classes="repeat"
@ -49,7 +50,8 @@
<Setter Property="Height" Value="{DynamicResource ScrollBarThickness}" /> <Setter Property="Height" Value="{DynamicResource ScrollBarThickness}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{DynamicResource ThemeControlMidBrush}"> <Border Background="{DynamicResource ThemeControlMidBrush}"
UseLayoutRounding="False">
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<RepeatButton Name="PART_LineUpButton" VerticalAlignment="Center" <RepeatButton Name="PART_LineUpButton" VerticalAlignment="Center"
Classes="repeat" Classes="repeat"

23
src/Avalonia.Themes.Default/Window.xaml

@ -4,16 +4,19 @@
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/> <Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{TemplateBinding Background}"> <Panel>
<VisualLayerManager> <Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<ContentPresenter Name="PART_ContentPresenter" <Border Background="{TemplateBinding Background}">
ContentTemplate="{TemplateBinding ContentTemplate}" <VisualLayerManager>
Content="{TemplateBinding Content}" <ContentPresenter Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}" ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Content="{TemplateBinding Content}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> Margin="{TemplateBinding Padding}"
</VisualLayerManager> HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
</Border> VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</VisualLayerManager>
</Border>
</Panel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

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

@ -21,7 +21,7 @@ namespace Avalonia.Media
{ {
_first = shadow; _first = shadow;
_list = null; _list = null;
Count = 1; Count = _first.IsEmpty ? 0 : 1;
} }
public BoxShadows(BoxShadow first, BoxShadow[] rest) public BoxShadows(BoxShadow first, BoxShadow[] rest)
@ -105,8 +105,6 @@ namespace Avalonia.Media
return false; return false;
} }
} }
public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow);
public bool Equals(BoxShadows other) 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. /// <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 /// This value will be clamped to the range of 0 to Height/2
/// </param> /// </param>
/// <param name="boxShadow">Box shadow effect parameters</param> /// <param name="boxShadows">Box shadow effect parameters</param>
/// <remarks> /// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// 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. /// 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> /// </remarks>
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0, 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)) if (brush == null && !PenIsVisible(pen))
{ {
@ -164,7 +164,7 @@ namespace Avalonia.Media
radiusY = Math.Min(radiusY, rect.Height / 2); 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> /// <summary>
@ -350,7 +350,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
/// <param name="matrix">The matrix</param> /// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns> /// <returns>A disposable used to undo the transformation.</returns>
PushedState PushSetTransform(Matrix matrix) public PushedState PushSetTransform(Matrix matrix)
{ {
var oldMatrix = CurrentTransform; var oldMatrix = CurrentTransform;
CurrentTransform = matrix; CurrentTransform = matrix;

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

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

22
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -44,7 +44,6 @@ namespace Avalonia.Media
/// <param name="characters">The characters.</param> /// <param name="characters">The characters.</param>
/// <param name="glyphClusters">The glyph clusters.</param> /// <param name="glyphClusters">The glyph clusters.</param>
/// <param name="biDiLevel">The bidi level.</param> /// <param name="biDiLevel">The bidi level.</param>
/// <param name="bounds">The bound.</param>
public GlyphRun( public GlyphRun(
GlyphTypeface glyphTypeface, GlyphTypeface glyphTypeface,
double fontRenderingEmSize, double fontRenderingEmSize,
@ -53,8 +52,7 @@ namespace Avalonia.Media
ReadOnlySlice<Vector> glyphOffsets = default, ReadOnlySlice<Vector> glyphOffsets = default,
ReadOnlySlice<char> characters = default, ReadOnlySlice<char> characters = default,
ReadOnlySlice<ushort> glyphClusters = default, ReadOnlySlice<ushort> glyphClusters = default,
int biDiLevel = 0, int biDiLevel = 0)
Rect? bounds = null)
{ {
GlyphTypeface = glyphTypeface; GlyphTypeface = glyphTypeface;
@ -71,8 +69,6 @@ namespace Avalonia.Media
GlyphClusters = glyphClusters; GlyphClusters = glyphClusters;
BiDiLevel = biDiLevel; BiDiLevel = biDiLevel;
Initialize(bounds);
} }
/// <summary> /// <summary>
@ -182,7 +178,7 @@ namespace Avalonia.Media
{ {
if (_glyphRunImpl == null) if (_glyphRunImpl == null)
{ {
Initialize(null); Initialize();
} }
return _glyphRunImpl; return _glyphRunImpl;
@ -517,8 +513,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Initializes the <see cref="GlyphRun"/>. /// Initializes the <see cref="GlyphRun"/>.
/// </summary> /// </summary>
/// <param name="bounds">Optional pre computed bounds.</param> private void Initialize()
private void Initialize(Rect? bounds)
{ {
if (GlyphIndices.Length == 0) if (GlyphIndices.Length == 0)
{ {
@ -541,16 +536,9 @@ namespace Avalonia.Media
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width); _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width);
if (bounds.HasValue) var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
{
_bounds = bounds;
}
else
{
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
_bounds = new Rect(0, 0, width, height); _bounds = new Rect(0, 0, width, height);
}
} }
void IDisposable.Dispose() void IDisposable.Dispose()

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

@ -11,6 +11,46 @@ namespace Avalonia.Media.Imaging
/// </summary> /// </summary>
public class Bitmap : IBitmap 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> /// <summary>
/// Initializes a new instance of the <see cref="Bitmap"/> class. /// Initializes a new instance of the <see cref="Bitmap"/> class.
/// </summary> /// </summary>
@ -39,7 +79,7 @@ namespace Avalonia.Media.Imaging
{ {
PlatformImpl = impl.Clone(); PlatformImpl = impl.Clone();
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Bitmap"/> class. /// Initializes a new instance of the <see cref="Bitmap"/> class.
/// </summary> /// </summary>
@ -48,7 +88,7 @@ namespace Avalonia.Media.Imaging
{ {
PlatformImpl = RefCountable.Create(impl); PlatformImpl = RefCountable.Create(impl);
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual void Dispose() public virtual void Dispose()
{ {

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

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
@ -87,17 +89,37 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Loads a bitmap implementation from a file.. /// Loads a bitmap implementation from a file..
/// </summary> /// </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> /// <returns>An <see cref="IBitmapImpl"/>.</returns>
IBitmapImpl LoadBitmap(string fileName); IBitmapImpl LoadBitmap(string fileName);
/// <summary> /// <summary>
/// Loads a bitmap implementation from a file.. /// Loads a bitmap implementation from a file..
/// </summary> /// </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> /// <returns>An <see cref="IBitmapImpl"/>.</returns>
IBitmapImpl LoadBitmap(Stream stream); 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> /// <summary>
/// Loads a bitmap implementation from a pixels in memory. /// Loads a bitmap implementation from a pixels in memory.
/// </summary> /// </summary>

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

Loading…
Cancel
Save