diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 908b9e1022..3acc5e365b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1283,6 +1283,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _closed = false; _lastScaling = [self backingScaleFactor]; + [self setOpaque:NO]; + [self setBackgroundColor: [NSColor clearColor]]; + [self invalidateShadow]; return self; } diff --git a/readme.md b/readme.md index 512b35a454..97c6509362 100644 --- a/readme.md +++ b/readme.md @@ -22,7 +22,7 @@ Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?it For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). -Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) +Avalonia is delivered via NuGet package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/) Use these commands in the Package Manager console to install Avalonia manually: ``` diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 874560a294..cbe2c62890 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -32,7 +32,11 @@ - + + + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index b44fac27cb..9b8f8af765 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -1,45 +1,52 @@ - - Image - Displays an image - - - - No Stretch - - - - - Fill - - + + + Image + Displays an image + - - Uniform - - + + + + Bitmap + + None + Fill + Uniform + UniformToFill + + + - - UniformToFill - - - - - Window Icon as an Image - - - + + Drawing + + None + Fill + Uniform + UniformToFill + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index 792b25963e..bbe89d1dfd 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,40 +1,41 @@ -using System.IO; -using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Media.Imaging; +using Avalonia.Media; namespace ControlCatalog.Pages { public class ImagePage : UserControl { - private Image iconImage; + private readonly Image _bitmapImage; + private readonly Image _drawingImage; + public ImagePage() { - this.InitializeComponent(); + InitializeComponent(); + _bitmapImage = this.FindControl("bitmapImage"); + _drawingImage = this.FindControl("drawingImage"); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - iconImage = this.Get("Icon"); } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + public void BitmapStretchChanged(object sender, SelectionChangedEventArgs e) + { + if (_bitmapImage != null) + { + var comboxBox = (ComboBox)sender; + _bitmapImage.Stretch = (Stretch)comboxBox.SelectedIndex; + } + } + + public void DrawingStretchChanged(object sender, SelectionChangedEventArgs e) { - base.OnAttachedToVisualTree(e); - if (iconImage.Source == null) + if (_drawingImage != null) { - var windowRoot = e.Root as Window; - if (windowRoot != null) - { - using (var stream = new MemoryStream()) - { - windowRoot.Icon.Save(stream); - stream.Seek(0, SeekOrigin.Begin); - iconImage.Source = new Bitmap(stream); - } - } + var comboxBox = (ComboBox)sender; + _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; } } } diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index 3eb2276c48..f263786ab7 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -39,7 +39,7 @@ namespace RenderDemo.Pages ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); } - context.DrawImage(_bitmap, 1, + context.DrawImage(_bitmap, new Rect(0, 0, 200, 200), new Rect(0, 0, 200, 200)); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); diff --git a/scripts/avalonia-rename.ps1 b/scripts/avalonia-rename.ps1 deleted file mode 100644 index c77dffb55d..0000000000 --- a/scripts/avalonia-rename.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -function Get-NewDirectoryName { - param ([System.IO.DirectoryInfo]$item) - - $name = $item.Name.Replace("perspex", "avalonia") - $name = $name.Replace("Perspex", "Avalonia") - Join-Path $item.Parent.FullName $name -} - -function Get-NewFileName { - param ([System.IO.FileInfo]$item) - - $name = $item.Name.Replace("perspex", "avalonia") - $name = $name.Replace("Perspex", "Avalonia") - Join-Path $item.DirectoryName $name -} - -function Rename-Contents { - param ([System.IO.FileInfo] $file) - - $extensions = @(".cs",".xaml",".csproj",".sln",".md",".json",".yml",".partial",".ps1",".nuspec",".htm",".html",".gitmodules".".xml",".plist",".targets",".projitems",".shproj",".xib") - - if ($extensions.Contains($file.Extension)) { - $text = [IO.File]::ReadAllText($file.FullName) - $text = $text.Replace("github.com/perspex", "github.com/avaloniaui") - $text = $text.Replace("github.com/Perspex", "github.com/AvaloniaUI") - $text = $text.Replace("perspex", "avalonia") - $text = $text.Replace("Perspex", "Avalonia") - $text = $text.Replace("PERSPEX", "AVALONIA") - [IO.File]::WriteAllText($file.FullName, $text) - } -} - -function Process-Files { - param ([System.IO.DirectoryInfo] $item) - - $dirs = Get-ChildItem -Path $item.FullName -Directory - $files = Get-ChildItem -Path $item.FullName -File - - foreach ($dir in $dirs) { - Process-Files $dir.FullName - } - - foreach ($file in $files) { - Rename-Contents $file - - $renamed = Get-NewFileName $file - - if ($file.FullName -ne $renamed) { - Write-Host git mv $file.FullName $renamed - & git mv $file.FullName $renamed - } - } - - $renamed = Get-NewDirectoryName $item - - if ($item.FullName -ne $renamed) { - Write-Host git mv $item.FullName $renamed - & git mv $item.FullName $renamed - } -} - -& git submodule deinit . -& git clean -xdf -Process-Files . diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs index e9cd0686d8..9f57455639 100644 --- a/src/Avalonia.Animation/IterationCount.cs +++ b/src/Avalonia.Animation/IterationCount.cs @@ -63,7 +63,7 @@ namespace Avalonia.Animation public IterationType RepeatType => _type; /// - /// Gets a value that indicates whether the is set to loop. + /// Gets a value that indicates whether the is set to Infinite. /// public bool IsInfinite => _type == IterationType.Infinite; diff --git a/src/Avalonia.Controls/DrawingPresenter.cs b/src/Avalonia.Controls/DrawingPresenter.cs index b30a8668fd..ee27aa7ec1 100644 --- a/src/Avalonia.Controls/DrawingPresenter.cs +++ b/src/Avalonia.Controls/DrawingPresenter.cs @@ -1,9 +1,11 @@ -using Avalonia.Controls.Shapes; +using System; +using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Metadata; namespace Avalonia.Controls { + [Obsolete("Use Image control with DrawingImage source")] public class DrawingPresenter : Control { static DrawingPresenter() diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index ff6cd482df..41b6e5449a 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -14,8 +14,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); /// /// Defines the property. @@ -23,6 +23,14 @@ namespace Avalonia.Controls public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + /// + /// Defines the property. + /// + public static readonly StyledProperty StretchDirectionProperty = + AvaloniaProperty.Register( + nameof(StretchDirection), + StretchDirection.Both); + static Image() { AffectsRender(SourceProperty, StretchProperty); @@ -30,9 +38,9 @@ namespace Avalonia.Controls } /// - /// Gets or sets the bitmap image that will be displayed. + /// Gets or sets the image that will be displayed. /// - public IBitmap Source + public IImage Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } @@ -43,10 +51,19 @@ namespace Avalonia.Controls /// public Stretch Stretch { - get { return (Stretch)GetValue(StretchProperty); } + get { return GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } + /// + /// Gets or sets a value controlling in what direction the image will be stretched. + /// + public StretchDirection StretchDirection + { + get { return GetValue(StretchDirectionProperty); } + set { SetValue(StretchDirectionProperty, value); } + } + /// /// Renders the control. /// @@ -58,8 +75,8 @@ namespace Avalonia.Controls if (source != null) { Rect viewPort = new Rect(Bounds.Size); - Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); - Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize); + Size sourceSize = source.Size; + Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); Size scaledSize = sourceSize * scale; Rect destRect = viewPort .CenterRect(new Rect(scaledSize)) @@ -69,7 +86,7 @@ namespace Avalonia.Controls var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this); - context.DrawImage(source, 1, sourceRect, destRect, interpolationMode); + context.DrawImage(source, sourceRect, destRect, interpolationMode); } } @@ -85,15 +102,7 @@ namespace Avalonia.Controls if (source != null) { - Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); - if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) - { - result = sourceSize; - } - else - { - result = Stretch.CalculateSize(availableSize, sourceSize); - } + result = Stretch.CalculateSize(availableSize, source.Size, StretchDirection); } return result; @@ -106,7 +115,7 @@ namespace Avalonia.Controls if (source != null) { - var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); + var sourceSize = source.Size; var result = Stretch.CalculateSize(finalSize, sourceSize); return result; } diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 539fe1ec4b..c7a1a24c25 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -83,7 +83,7 @@ namespace Avalonia.Controls.Remote Marshal.Copy(_lastFrame.Data, y * _lastFrame.Stride, new IntPtr(l.Address.ToInt64() + l.RowBytes * y), lineLen); } - context.DrawImage(_bitmap, 1, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height), + context.DrawImage(_bitmap, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height), new Rect(Bounds.Size)); } base.Render(context); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3d472fca18..fbac2e02ec 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -390,8 +390,10 @@ namespace Avalonia.Controls { return; } + _undoRedoHelper.Snapshot(); HandleTextInput(text); + _undoRedoHelper.Snapshot(); } protected override void OnKeyDown(KeyEventArgs e) @@ -401,12 +403,12 @@ namespace Avalonia.Controls bool movement = false; bool selection = false; bool handled = false; - var modifiers = e.Modifiers; + var modifiers = e.KeyModifiers; var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - bool DetectSelection() => e.Modifiers.HasFlag(keymap.SelectionModifiers); + bool DetectSelection() => e.KeyModifiers.HasFlag(keymap.SelectionModifiers); if (Match(keymap.SelectAll)) { diff --git a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs index a758a328be..053f894755 100644 --- a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs +++ b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs @@ -4,14 +4,14 @@ namespace Avalonia.Input.Platform { public class PlatformHotkeyConfiguration { - public PlatformHotkeyConfiguration() : this(InputModifiers.Control) + public PlatformHotkeyConfiguration() : this(KeyModifiers.Control) { } - public PlatformHotkeyConfiguration(InputModifiers commandModifiers, - InputModifiers selectionModifiers = InputModifiers.Shift, - InputModifiers wholeWordTextActionModifiers = InputModifiers.Control) + public PlatformHotkeyConfiguration(KeyModifiers commandModifiers, + KeyModifiers selectionModifiers = KeyModifiers.Shift, + KeyModifiers wholeWordTextActionModifiers = KeyModifiers.Control) { CommandModifiers = commandModifiers; SelectionModifiers = selectionModifiers; @@ -75,9 +75,9 @@ namespace Avalonia.Input.Platform }; } - public InputModifiers CommandModifiers { get; set; } - public InputModifiers WholeWordTextActionModifiers { get; set; } - public InputModifiers SelectionModifiers { get; set; } + public KeyModifiers CommandModifiers { get; set; } + public KeyModifiers WholeWordTextActionModifiers { get; set; } + public KeyModifiers SelectionModifiers { get; set; } public List Copy { get; set; } public List Cut { get; set; } public List Paste { get; set; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 571475c7ea..756619fa9f 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -101,7 +101,7 @@ namespace Avalonia.Native .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) - .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); } diff --git a/src/Avalonia.Styling/Styling/ISetStyleParent.cs b/src/Avalonia.Styling/Controls/ISetResourceParent.cs similarity index 61% rename from src/Avalonia.Styling/Styling/ISetStyleParent.cs rename to src/Avalonia.Styling/Controls/ISetResourceParent.cs index bca3d9d714..a1264adc34 100644 --- a/src/Avalonia.Styling/Styling/ISetStyleParent.cs +++ b/src/Avalonia.Styling/Controls/ISetResourceParent.cs @@ -1,29 +1,27 @@ -using Avalonia.Controls; - -namespace Avalonia.Styling +namespace Avalonia.Controls { /// - /// Defines an interface through which a 's parent can be set. + /// Defines an interface through which an 's parent can be set. /// /// /// You should not usually need to use this interface - it is for internal use only. /// - public interface ISetStyleParent : IStyle + public interface ISetResourceParent : IResourceNode { /// - /// Sets the style parent. + /// Sets the resource parent. /// /// The parent. void SetParent(IResourceNode parent); /// - /// Notifies the style that a change has been made to resources that apply to it. + /// Notifies the resource node that a change has been made to the resources in its parent. /// /// The event args. /// /// This method will be called automatically by the framework, you should not need to call /// this method yourself. /// - void NotifyResourcesChanged(ResourcesChangedEventArgs e); + void ParentResourcesChanged(ResourcesChangedEventArgs e); } } diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 901e27b7b7..acc2db1ff7 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -12,8 +12,12 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary + public class ResourceDictionary : AvaloniaDictionary, + IResourceDictionary, + IResourceNode, + ISetResourceParent { + private IResourceNode _parent; private AvaloniaList _mergedDictionaries; /// @@ -39,6 +43,12 @@ namespace Avalonia.Controls _mergedDictionaries.ForEachItem( x => { + if (x is ISetResourceParent setParent) + { + setParent.SetParent(this); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + if (x.HasResources) { OnResourcesChanged(); @@ -48,11 +58,18 @@ namespace Avalonia.Controls }, x => { + if (x is ISetResourceParent setParent) + { + setParent.SetParent(null); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + if (x.HasResources) { OnResourcesChanged(); } + (x as ISetResourceParent)?.SetParent(null); x.ResourcesChanged -= MergedDictionaryResourcesChanged; }, () => { }); @@ -68,6 +85,27 @@ namespace Avalonia.Controls get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); } + /// + IResourceNode IResourceNode.ResourceParent => _parent; + + /// + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) + { + NotifyMergedDictionariesResourcesChanged(e); + ResourcesChanged?.Invoke(this, e); + } + + /// + void ISetResourceParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The ResourceDictionary already has a parent."); + } + + _parent = parent; + } + /// public bool TryGetResource(object key, out object value) { @@ -95,7 +133,27 @@ namespace Avalonia.Controls ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); } - private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged(); + private void NotifyMergedDictionariesResourcesChanged(ResourcesChangedEventArgs e) + { + if (_mergedDictionaries != null) + { + for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) + { + if (_mergedDictionaries[i] is ISetResourceParent merged) + { + merged.ParentResourcesChanged(e); + } + } + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + var ev = new ResourcesChangedEventArgs(); + NotifyMergedDictionariesResourcesChanged(ev); + OnResourcesChanged(); + } + private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged(); } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 5e1bcde2f6..b5deb9a4a1 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -223,13 +223,13 @@ namespace Avalonia { if (_styles != null) { - (_styles as ISetStyleParent)?.SetParent(null); + (_styles as ISetResourceParent)?.SetParent(null); _styles.ResourcesChanged -= ThisResourcesChanged; } _styles = value; - if (value is ISetStyleParent setParent && setParent.ResourceParent == null) + if (value is ISetResourceParent setParent && setParent.ResourceParent == null) { setParent.SetParent(this); } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 3ce82b4160..99ee8d8563 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -14,7 +14,7 @@ namespace Avalonia.Styling /// /// Defines a style. /// - public class Style : AvaloniaObject, IStyle, ISetStyleParent + public class Style : AvaloniaObject, IStyle, ISetResourceParent { private static Dictionary _applied = new Dictionary(); @@ -59,16 +59,16 @@ namespace Avalonia.Styling if (_resources != null) { - hadResources = _resources.Count > 0; + hadResources = _resources.HasResources; _resources.ResourcesChanged -= ResourceDictionaryChanged; } _resources = value; _resources.ResourcesChanged += ResourceDictionaryChanged; - if (hadResources || _resources.Count > 0) + if (hadResources || _resources.HasResources) { - ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); } } } @@ -194,13 +194,13 @@ namespace Avalonia.Styling } /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); } /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index a4563110a9..0226288998 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -14,7 +14,7 @@ namespace Avalonia.Styling /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetStyleParent + public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetResourceParent { private IResourceNode _parent; private IResourceDictionary _resources; @@ -27,10 +27,10 @@ namespace Avalonia.Styling _styles.ForEachItem( x => { - if (x.ResourceParent == null && x is ISetStyleParent setParent) + if (x.ResourceParent == null && x is ISetResourceParent setParent) { setParent.SetParent(this); - setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); } if (x.HasResources) @@ -43,10 +43,10 @@ namespace Avalonia.Styling }, x => { - if (x.ResourceParent == this && x is ISetStyleParent setParent) + if (x.ResourceParent == this && x is ISetResourceParent setParent) { setParent.SetParent(null); - setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); } if (x.HasResources) @@ -98,7 +98,7 @@ namespace Avalonia.Styling if (hadResources || _resources.Count > 0) { - ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); } } } @@ -246,7 +246,7 @@ namespace Avalonia.Styling IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { @@ -257,7 +257,7 @@ namespace Avalonia.Styling } /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); } @@ -266,7 +266,7 @@ namespace Avalonia.Styling { foreach (var child in this) { - (child as ISetStyleParent)?.NotifyResourcesChanged(e); + (child as ISetResourceParent)?.ParentResourcesChanged(e); } ResourcesChanged?.Invoke(this, e); @@ -280,7 +280,7 @@ namespace Avalonia.Styling { if (foundSource) { - (child as ISetStyleParent)?.NotifyResourcesChanged(e); + (child as ISetResourceParent)?.ParentResourcesChanged(e); } foundSource |= child == sender; diff --git a/src/Avalonia.Visuals/Media/Drawing.cs b/src/Avalonia.Visuals/Media/Drawing.cs index a60c591edc..6bc808e407 100644 --- a/src/Avalonia.Visuals/Media/Drawing.cs +++ b/src/Avalonia.Visuals/Media/Drawing.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media +using Avalonia.Platform; + +namespace Avalonia.Media { public abstract class Drawing : AvaloniaObject { @@ -6,4 +8,4 @@ public abstract Rect GetBounds(); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index df69ab6fd5..4045b92c0c 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -74,18 +74,29 @@ namespace Avalonia.Media public Matrix CurrentContainerTransform => _currentContainerTransform; /// - /// Draws a bitmap image. + /// Draws an image. /// - /// The bitmap image. - /// The opacity to draw with. + /// The image. + /// The rect in the output to draw to. + public void DrawImage(IImage source, Rect rect) + { + Contract.Requires(source != null); + + DrawImage(source, new Rect(source.Size), rect); + } + + /// + /// Draws an image. + /// + /// The image. /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) { Contract.Requires(source != null); - PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode); + source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } /// diff --git a/src/Avalonia.Visuals/Media/DrawingGroup.cs b/src/Avalonia.Visuals/Media/DrawingGroup.cs index 744ff2af03..e581c8c553 100644 --- a/src/Avalonia.Visuals/Media/DrawingGroup.cs +++ b/src/Avalonia.Visuals/Media/DrawingGroup.cs @@ -1,5 +1,6 @@ using Avalonia.Collections; using Avalonia.Metadata; +using Avalonia.Platform; namespace Avalonia.Media { @@ -55,4 +56,4 @@ namespace Avalonia.Media return rect; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs new file mode 100644 index 0000000000..d6ab004dd7 --- /dev/null +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -0,0 +1,81 @@ +using System; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// An that uses a for content. + /// + public class DrawingImage : AvaloniaObject, IImage, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + /// + public event EventHandler Invalidated; + + /// + /// Gets or sets the drawing content. + /// + [Content] + public Drawing Drawing + { + get => GetValue(DrawingProperty); + set => SetValue(DrawingProperty, value); + } + + /// + public Size Size => Drawing?.GetBounds().Size ?? default; + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var drawing = Drawing; + + if (drawing == null) + { + return; + } + + var bounds = drawing.GetBounds(); + var scale = Matrix.CreateScale( + destRect.Width / sourceRect.Width, + destRect.Height / sourceRect.Height); + var translate = Matrix.CreateTranslation( + -sourceRect.X + destRect.X - bounds.X, + -sourceRect.Y + destRect.Y - bounds.Y); + + using (context.PushClip(destRect)) + using (context.PushPreTransform(translate * scale)) + { + Drawing?.Draw(context); + } + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == DrawingProperty) + { + RaiseInvalidated(EventArgs.Empty); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + } +} diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index 3dad10fb8f..4df3aa8ae2 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -1,10 +1,13 @@ -namespace Avalonia.Media +using Avalonia.Metadata; + +namespace Avalonia.Media { public class GeometryDrawing : Drawing { public static readonly StyledProperty GeometryProperty = AvaloniaProperty.Register(nameof(Geometry)); + [Content] public Geometry Geometry { get => GetValue(GeometryProperty); diff --git a/src/Avalonia.Visuals/Media/IImage.cs b/src/Avalonia.Visuals/Media/IImage.cs new file mode 100644 index 0000000000..aff2a9ddf9 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IImage.cs @@ -0,0 +1,29 @@ +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// Represents a raster or vector image. + /// + public interface IImage + { + /// + /// Gets the size of the image, in device independent pixels. + /// + Size Size { get; } + + /// + /// Draws the image to a . + /// + /// The drawing context. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + void Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode); + } +} diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 8dd75d2374..14ac4261dc 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -5,6 +5,7 @@ using System; using System.IO; using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media.Imaging { @@ -94,9 +95,28 @@ namespace Avalonia.Media.Imaging PlatformImpl.Item.Save(fileName); } + /// + /// Saves the bitmap to a stream. + /// + /// The stream. public void Save(Stream stream) { PlatformImpl.Item.Save(stream); } + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + context.PlatformImpl.DrawBitmap( + PlatformImpl, + 1, + sourceRect, + destRect, + bitmapInterpolationMode); + } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs index 90b13088e1..4c3203a95b 100644 --- a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media.Imaging /// /// Represents a bitmap image. /// - public interface IBitmap : IDisposable + public interface IBitmap : IImage, IDisposable { /// /// Gets the dots per inch (DPI) of the image. @@ -32,15 +32,6 @@ namespace Avalonia.Media.Imaging /// IRef PlatformImpl { get; } - /// - /// Gets the size of the image, in device independent pixels. - /// - /// - /// Note that Skia does not currently support reading the DPI of an image so this value - /// will equal on Skia. - /// - Size Size { get; } - /// /// Saves the bitmap to a file. /// diff --git a/src/Avalonia.Visuals/Media/MediaExtensions.cs b/src/Avalonia.Visuals/Media/MediaExtensions.cs index 95d17b454e..36bda5f483 100644 --- a/src/Avalonia.Visuals/Media/MediaExtensions.cs +++ b/src/Avalonia.Visuals/Media/MediaExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -16,24 +17,82 @@ namespace Avalonia.Media /// The stretch mode. /// The size of the destination viewport. /// The size of the source. + /// The stretch direction. /// A vector with the X and Y scaling factors. - public static Vector CalculateScaling(this Stretch stretch, Size destinationSize, Size sourceSize) + public static Vector CalculateScaling( + this Stretch stretch, + Size destinationSize, + Size sourceSize, + StretchDirection stretchDirection = StretchDirection.Both) { - double scaleX = 1; - double scaleY = 1; + var scaleX = 1.0; + var scaleY = 1.0; - if (stretch != Stretch.None) + bool isConstrainedWidth = !double.IsPositiveInfinity(destinationSize.Width); + bool isConstrainedHeight = !double.IsPositiveInfinity(destinationSize.Height); + + if ((stretch == Stretch.Uniform || stretch == Stretch.UniformToFill || stretch == Stretch.Fill) + && (isConstrainedWidth || isConstrainedHeight)) { - scaleX = destinationSize.Width / sourceSize.Width; - scaleY = destinationSize.Height / sourceSize.Height; + // Compute scaling factors for both axes + scaleX = MathUtilities.IsZero(sourceSize.Width) ? 0.0 : destinationSize.Width / sourceSize.Width; + scaleY = MathUtilities.IsZero(sourceSize.Height) ? 0.0 : destinationSize.Height / sourceSize.Height; - switch (stretch) + if (!isConstrainedWidth) + { + scaleX = scaleY; + } + else if (!isConstrainedHeight) + { + scaleY = scaleX; + } + else { - case Stretch.Uniform: - scaleX = scaleY = Math.Min(scaleX, scaleY); + // If not preserving aspect ratio, then just apply transform to fit + switch (stretch) + { + case Stretch.Uniform: + // Find minimum scale that we use for both axes + double minscale = scaleX < scaleY ? scaleX : scaleY; + scaleX = scaleY = minscale; + break; + + case Stretch.UniformToFill: + // Find maximum scale that we use for both axes + double maxscale = scaleX > scaleY ? scaleX : scaleY; + scaleX = scaleY = maxscale; + break; + + case Stretch.Fill: + // We already computed the fill scale factors above, so just use them + break; + } + } + + // Apply stretch direction by bounding scales. + // In the uniform case, scaleX=scaleY, so this sort of clamping will maintain aspect ratio + // In the uniform fill case, we have the same result too. + // In the fill case, note that we change aspect ratio, but that is okay + switch (stretchDirection) + { + case StretchDirection.UpOnly: + if (scaleX < 1.0) + scaleX = 1.0; + if (scaleY < 1.0) + scaleY = 1.0; + break; + + case StretchDirection.DownOnly: + if (scaleX > 1.0) + scaleX = 1.0; + if (scaleY > 1.0) + scaleY = 1.0; break; - case Stretch.UniformToFill: - scaleX = scaleY = Math.Max(scaleX, scaleY); + + case StretchDirection.Both: + break; + + default: break; } } @@ -47,10 +106,15 @@ namespace Avalonia.Media /// The stretch mode. /// The size of the destination viewport. /// The size of the source. + /// The stretch direction. /// The size of the stretched source. - public static Size CalculateSize(this Stretch stretch, Size destinationSize, Size sourceSize) + public static Size CalculateSize( + this Stretch stretch, + Size destinationSize, + Size sourceSize, + StretchDirection stretchDirection = StretchDirection.Both) { - return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize); + return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize, stretchDirection); } } } diff --git a/src/Avalonia.Visuals/Media/StretchDirection.cs b/src/Avalonia.Visuals/Media/StretchDirection.cs new file mode 100644 index 0000000000..a4be26f6cd --- /dev/null +++ b/src/Avalonia.Visuals/Media/StretchDirection.cs @@ -0,0 +1,25 @@ +namespace Avalonia.Media +{ + /// + /// Describes the type of scaling that can be used when scaling content. + /// + public enum StretchDirection + { + /// + /// Only scales the content upwards when the content is smaller than the available space. + /// If the content is larger, no scaling downwards is done. + /// + UpOnly, + + /// + /// Only scales the content downwards when the content is larger than the available space. + /// If the content is smaller, no scaling upwards is done. + /// + DownOnly, + + /// + /// Always stretches to fit the available space according to the stretch mode. + /// + Both, + } +} diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index f2309c271d..7d142b0759 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -33,7 +33,7 @@ namespace Avalonia.Platform /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); + void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a bitmap image. @@ -42,7 +42,7 @@ namespace Avalonia.Platform /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); + void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); /// /// Draws a line. diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 2fa249f101..585b132bd3 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -469,11 +469,11 @@ namespace Avalonia.Rendering if (layer.OpacityMask == null) { - context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect); + context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); } else { - context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); + context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); } if (layer.GeometryClip != null) @@ -485,7 +485,7 @@ namespace Avalonia.Rendering if (_overlay != null) { var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height); - context.DrawImage(_overlay, 0.5, sourceRect, clientRect); + context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect); } if (DrawFps) diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 68d56eeedd..5914bcb1fe 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -307,7 +307,9 @@ namespace Avalonia.Rendering if (!child.ClipToBounds || clipRect.Intersects(childBounds)) { - var childClipRect = clipRect.Translate(-childBounds.Position); + var childClipRect = child.RenderTransform == null + ? clipRect.Translate(-childBounds.Position) + : clipRect; Render(context, child, childClipRect); } else diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index a169a629be..b362321745 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -115,7 +115,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var next = NextDrawAs(); @@ -130,7 +130,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) { // This method is currently only used to composite layers so shouldn't be called here. throw new NotSupportedException(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index e1bdcaab3b..054f33c95d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawImage(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); + context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// diff --git a/src/Avalonia.X11/Glx/Glx.cs b/src/Avalonia.X11/Glx/Glx.cs index c3a2fd2050..714a592f2b 100644 --- a/src/Avalonia.X11/Glx/Glx.cs +++ b/src/Avalonia.X11/Glx/Glx.cs @@ -84,8 +84,24 @@ namespace Avalonia.X11.Glx [GlEntryPoint("glGetError")] public GlGetError GetError { get; } - public GlxInterface() : base(GlxGetProcAddress) + public GlxInterface() : base(SafeGetProcAddress) { } + + // Ignores egl functions. + // On some Linux systems, glXGetProcAddress will return valid pointers for even EGL functions. + // This makes Skia try to load some data from EGL, + // which can then cause segmentation faults because they return garbage. + public static IntPtr SafeGetProcAddress(string proc, bool optional) + { + if (proc.StartsWith("egl", StringComparison.InvariantCulture)) + { + return IntPtr.Zero; + } + + return GlxConverted(proc, optional); + } + + private static readonly Func GlxConverted = ConvertNative(GlxGetProcAddress); } } diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 04f2a7137c..22eb0792e8 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -87,7 +87,7 @@ namespace Avalonia.X11.Glx ImmediateContext.MakeCurrent(); var err = Glx.GetError(); - GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); + GlInterface = new GlInterface(GlxInterface.SafeGetProcAddress); if (GlInterface.Version == null) throw new OpenGlException("GL version string is null, aborting"); if (GlInterface.Renderer == null) diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index f0e75536d0..093f2b12c1 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -59,7 +59,7 @@ namespace Avalonia.X11 } using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) - ctx.DrawImage(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), + ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; Data[0] = new UIntPtr((uint)_width); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 6ba562bb69..8b531bd9c5 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -44,7 +44,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11PlatformThreading(this)) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Control)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 20f68df820..c77ccd64f2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -41,6 +41,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions StringFormat = StringFormat, RelativeSource = RelativeSource, DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)), + TargetNullValue = TargetNullValue, NameScope = new WeakReference(serviceProvider.GetService()) }; } @@ -86,5 +87,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public string StringFormat { get; set; } public RelativeSource RelativeSource { get; set; } + + public object TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 3525628a79..0d56942645 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -7,8 +7,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions /// /// Loads a resource dictionary from a specified URL. /// - public class ResourceInclude :IResourceProvider + public class ResourceInclude : IResourceNode, ISetResourceParent { + private IResourceNode _parent; private Uri _baseUri; private IResourceDictionary _loaded; @@ -26,6 +27,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var loader = new AvaloniaXamlLoader(); _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + (_loaded as ISetResourceParent)?.SetParent(this); + _loaded.ResourcesChanged += ResourcesChanged; + if (_loaded.HasResources) { ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); @@ -44,12 +48,32 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions /// bool IResourceProvider.HasResources => Loaded.HasResources; + /// + IResourceNode IResourceNode.ResourceParent => _parent; + /// bool IResourceProvider.TryGetResource(object key, out object value) { return Loaded.TryGetResource(key, out value); } + /// + void ISetResourceParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The ResourceInclude already has a parent."); + } + + _parent = parent; + } + + /// + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) + { + (_loaded as ISetResourceParent)?.ParentResourcesChanged(e); + } + public ResourceInclude ProvideValue(IServiceProvider serviceProvider) { var tdc = (ITypeDescriptorContext)serviceProvider; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 7acee50d80..41eab79ed8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Includes a style from a URL. /// - public class StyleInclude : IStyle, ISetStyleParent + public class StyleInclude : IStyle, ISetResourceParent { private Uri _baseUri; private IStyle _loaded; @@ -53,7 +53,7 @@ namespace Avalonia.Markup.Xaml.Styling { var loader = new AvaloniaXamlLoader(); _loaded = (IStyle)loader.Load(Source, _baseUri); - (_loaded as ISetStyleParent)?.SetParent(this); + (_loaded as ISetResourceParent)?.SetParent(this); } return _loaded; @@ -89,13 +89,13 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { - (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e); + (Loaded as ISetResourceParent)?.ParentResourcesChanged(e); } /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 63c8b1c074..ebe4035ed6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -99,7 +99,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions void Add(string type, string conv) => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); - Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); + Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index d06cfa69a7..1c05f8ac9f 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -110,7 +110,7 @@ namespace Avalonia.Skia } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var drawableImage = (IDrawableBitmapImpl)source.Item; var s = sourceRect.ToSKRect(); @@ -146,10 +146,10 @@ namespace Avalonia.Skia } /// - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); - DrawImage(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); + DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); PopOpacityMask(); } @@ -437,7 +437,7 @@ namespace Avalonia.Skia context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage( + context.DrawBitmap( RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, sourceRect, diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 727947e59d..60d6ecaabc 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -89,12 +89,15 @@ namespace Avalonia.Skia if (typeface.FontFamily.Key == null) { + var defaultName = SKTypeface.Default.FamilyName; + foreach (var familyName in typeface.FontFamily.FamilyNames) { skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); - if (skTypeface == SKTypeface.Default) + if (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal) && + defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) { continue; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index aa13003643..81d869f3b8 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1.Media /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) { @@ -149,7 +149,7 @@ namespace Avalonia.Direct2D1.Media /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index fbc6d21cb7..6632e2b3e7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -107,7 +107,7 @@ namespace Avalonia.Direct2D1.Media context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode); + context.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode); context.PopClip(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs index 1ee869ecb9..29c9280af6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs @@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.Media _direct2DBitmap = d2DBitmap ?? throw new ArgumentNullException(nameof(d2DBitmap)); } - public override Vector Dpi => _direct2DBitmap.DotsPerInch.ToAvaloniaVector(); + public override Vector Dpi => new Vector(96, 96); public override PixelSize PixelSize => _direct2DBitmap.PixelSize.ToAvalonia(); public override void Dispose() @@ -58,3 +58,4 @@ namespace Avalonia.Direct2D1.Media } } } +; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 8ec368c999..b96441e357 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -58,7 +58,7 @@ namespace Avalonia.Direct2D1.Media.Imaging { using (var dc = wic.CreateDrawingContext(null)) { - dc.DrawImage( + dc.DrawBitmap( RefCountable.CreateUnownedNotClonable(this), 1, new Rect(PixelSize.ToSizeWithDpi(Dpi.X)), diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 176c3e0e23..4e0853c20c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -26,6 +26,7 @@ namespace Avalonia.Direct2D1.Media using (BitmapDecoder decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand)) { WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); + Dpi = new Vector(96, 96); } } @@ -39,6 +40,7 @@ namespace Avalonia.Direct2D1.Media _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, _decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); + Dpi = new Vector(96, 96); } /// @@ -62,6 +64,7 @@ namespace Avalonia.Direct2D1.Media pixelFormat.Value.ToWic(), BitmapCreateCacheOption.CacheOnLoad); WicImpl.SetResolution(dpi.X, dpi.Y); + Dpi = dpi; } public WicBitmapImpl(APixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) @@ -70,6 +73,8 @@ namespace Avalonia.Direct2D1.Media WicImpl.SetResolution(dpi.X, dpi.Y); PixelFormat = format; + Dpi = dpi; + using (var l = WicImpl.Lock(BitmapLockFlags.Write)) { for (var row = 0; row < size.Height; row++) @@ -82,14 +87,7 @@ namespace Avalonia.Direct2D1.Media } } - public override Vector Dpi - { - get - { - WicImpl.GetResolution(out double x, out double y); - return new Vector(x, y); - } - } + public override Vector Dpi { get; } public override PixelSize PixelSize => WicImpl.Size.ToAvalonia(); diff --git a/tests/Avalonia.Controls.UnitTests/ImageTests.cs b/tests/Avalonia.Controls.UnitTests/ImageTests.cs index 71d0d1e328..d3c0e29eca 100644 --- a/tests/Avalonia.Controls.UnitTests/ImageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ImageTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_No_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.None; target.Source = bitmap; @@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_Fill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Fill; target.Source = bitmap; @@ -39,7 +39,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_Uniform_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Uniform; target.Source = bitmap; @@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_UniformToFill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.UniformToFill; target.Source = bitmap; @@ -62,10 +62,59 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(50, 50), target.DesiredSize); } + [Fact] + public void Measure_Should_Return_Correct_Size_With_StretchDirection_DownOnly() + { + var bitmap = CreateBitmap(50, 100); + var target = new Image(); + target.StretchDirection = StretchDirection.DownOnly; + target.Source = bitmap; + + target.Measure(new Size(150, 150)); + + Assert.Equal(new Size(50, 100), target.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Height() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(200, double.PositiveInfinity)); + + Assert.Equal(new Size(200, 400), image.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Width() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(double.PositiveInfinity, 400)); + + Assert.Equal(new Size(200, 400), image.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Width_Height() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(new Size(50, 100), image.DesiredSize); + } + [Fact] public void Arrange_Should_Return_Correct_Size_For_No_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.None; target.Source = bitmap; @@ -79,7 +128,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Fill; target.Source = bitmap; @@ -93,7 +142,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Uniform; target.Source = bitmap; @@ -107,7 +156,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.UniformToFill; target.Source = bitmap; @@ -117,5 +166,10 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(25, 100), target.Bounds.Size); } + + private IBitmap CreateBitmap(int width, int height) + { + return Mock.Of(x => x.Size == new Size(width, height)); + } } } diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index 82471915f4..3320bcebca 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -36,6 +36,30 @@ namespace Avalonia.Direct2D1.UnitTests.Media } } + [Fact] + public void Should_Create_Typeface_From_Fallback_Bold() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var fontManager = new FontManagerImpl(); + + var defaultName = fontManager.GetDefaultFontFamilyName(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("A, B, Arial"), FontWeight.Bold)); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + [Fact] public void Should_Create_Typeface_For_Unknown_Font() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index c3bc649abb..76edf9a17a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -39,6 +39,59 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void BindingExtension_Binds_To_TargetNullValue() + { + using (StyledWindow()) + { + var xaml = @" + + + foobar + + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = new FooBar(); + window.Show(); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void BindingExtension_TargetNullValue_UnsetByDefault() + { + using (StyledWindow()) + { + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = new FooBar(); + window.Show(); + + Assert.Equal(false, textBlock.IsVisible); + } + } + + private class FooBar + { + public object Foo { get; } = null; + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs new file mode 100644 index 0000000000..d0cdef3c0b --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class ResourceDictionaryTests : XamlTestBase + { + [Fact] + public void StaticResource_Works_In_ResourceDictionary() + { + using (StyledWindow()) + { + var xaml = @" + + Red + +"; + var loader = new AvaloniaXamlLoader(); + var resources = (ResourceDictionary)loader.Load(xaml); + var brush = (SolidColorBrush)resources["RedBrush"]; + + Assert.Equal(Colors.Red, brush.Color); + } + } + + [Fact] + public void DynamicResource_Works_In_ResourceDictionary() + { + using (StyledWindow()) + { + var xaml = @" + + Red + +"; + var loader = new AvaloniaXamlLoader(); + var resources = (ResourceDictionary)loader.Load(xaml); + var brush = (SolidColorBrush)resources["RedBrush"]; + + Assert.Equal(Colors.Red, brush.Color); + } + } + + [Fact] + public void DynamicResource_Finds_Resource_In_Parent_Dictionary() + { + var dictionaryXaml = @" + + +"; + + using (StyledWindow(assets: ("test:dict.xaml", dictionaryXaml))) + { + var xaml = @" + + + + + + + + Red + +