diff --git a/native/Avalonia.Native/src/OSX/gl.mm b/native/Avalonia.Native/src/OSX/gl.mm index 083adc927d..feb0643654 100644 --- a/native/Avalonia.Native/src/OSX/gl.mm +++ b/native/Avalonia.Native/src/OSX/gl.mm @@ -1,6 +1,7 @@ #include "common.h" #include #include +#include "window.h" template char (&ArrayCounter(T (&a)[N]))[N]; #define ARRAY_COUNT(a) (sizeof(ArrayCounter(a))) @@ -181,12 +182,12 @@ extern IAvnGlFeature* GetGlFeature() class AvnGlRenderingSession : public ComSingleObject { - NSView* _view; - NSWindow* _window; + AvnView* _view; + AvnWindow* _window; NSOpenGLContext* _context; public: FORWARD_IUNKNOWN() - AvnGlRenderingSession(NSWindow*window, NSView* view, NSOpenGLContext* context) + AvnGlRenderingSession(AvnWindow*window, AvnView* view, NSOpenGLContext* context) { _context = context; _window = window; @@ -195,14 +196,12 @@ public: virtual HRESULT GetPixelSize(AvnPixelSize* ret) override { - auto fsize = [_view convertSizeToBacking: [_view frame].size]; - ret->Width = (int)fsize.width; - ret->Height = (int)fsize.height; + *ret = [_view getPixelSize]; return S_OK; } virtual HRESULT GetScaling(double* ret) override { - *ret = [_window backingScaleFactor]; + *ret = [_window getScaling]; return S_OK; } @@ -234,8 +233,17 @@ public: auto f = GetFeature(); if(f == NULL) return E_FAIL; - if(![_view lockFocusIfCanDraw]) + + @try + { + if(![_view lockFocusIfCanDraw]) + return E_ABORT; + } + @catch(NSException* exception) + { return E_ABORT; + } + auto gl = _context; CGLLockContext([_context CGLContextObj]); diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 932bc56a2e..3e626675d2 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -12,6 +12,7 @@ class WindowBaseImpl; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) onClosed; +-(AvnPixelSize) getPixelSize; @end @interface AvnWindow : NSWindow @@ -22,6 +23,7 @@ class WindowBaseImpl; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; -(void) applyMenu:(NSMenu *)menu; +-(double) getScaling; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index c54829d750..3acc5e365b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -195,7 +195,11 @@ public: { @autoreleasepool { - [Window close]; + if (Window != nullptr) + { + [Window close]; + } + return S_OK; } } @@ -291,7 +295,14 @@ public: { @autoreleasepool { - return [View lockFocusIfCanDraw] == YES; + @try + { + return [View lockFocusIfCanDraw] == YES; + } + @catch (NSException*) + { + return NO; + } } } @@ -719,15 +730,33 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; NSEvent* _lastMouseDownEvent; bool _lastKeyHandled; + AvnPixelSize _lastPixelSize; } -- (void)dealloc +- (void)onClosed { + @synchronized (self) + { + _parent = nullptr; + } } -- (void)onClosed +- (BOOL)lockFocusIfCanDraw +{ + @synchronized (self) + { + if(_parent == nullptr) + { + return NO; + } + } + + return [super lockFocusIfCanDraw]; +} + +-(AvnPixelSize) getPixelSize { - _parent = NULL; + return _lastPixelSize; } - (NSEvent*) lastMouseDownEvent @@ -742,6 +771,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setWantsLayer:YES]; _parent = parent; _area = nullptr; + _lastPixelSize.Height = 100; + _lastPixelSize.Width = 100; return self; } @@ -783,6 +814,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self addTrackingArea:_area]; _parent->UpdateCursor(); + + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } @@ -812,7 +847,13 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)drawRect:(NSRect)dirtyRect { + if (_parent == nullptr) + { + return; + } + _parent->BaseEvents->RunRenderPriorityJobs(); + @synchronized (self) { if(_swRenderedFrame != NULL) { @@ -879,7 +920,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) viewDidChangeBackingProperties { + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + [super viewDidChangeBackingProperties]; } @@ -1161,6 +1207,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _closed; NSMenu* _menu; bool _isAppMenuApplied; + double _lastScaling; +} + +-(double) getScaling +{ + return _lastScaling; } +(void)closeAll @@ -1174,10 +1226,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } -- (void)dealloc -{ -} - - (void)pollModalSession:(nonnull NSModalSession)session { auto response = [NSApp runModalSession:session]; @@ -1232,6 +1280,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setReleasedWhenClosed:false]; _parent = parent; [self setDelegate:self]; + _closed = false; + + _lastScaling = [self backingScaleFactor]; + [self setOpaque:NO]; + [self setBackgroundColor: [NSColor clearColor]]; + [self invalidateShadow]; return self; } @@ -1247,6 +1301,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return true; } +- (void)windowDidChangeBackingProperties:(NSNotification *)notification +{ + _lastScaling = [self backingScaleFactor]; +} + - (void)windowWillClose:(NSNotification *)notification { _closed = true; @@ -1257,9 +1316,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self restoreParentWindow]; parent->BaseEvents->Closed(); [parent->View onClosed]; - dispatch_async(dispatch_get_main_queue(), ^{ - [self setContentView: nil]; - }); } } @@ -1406,18 +1462,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->GetPosition(&position); _parent->BaseEvents->PositionChanged(position); } - -// TODO this breaks resizing. -/*- (void)windowDidResize:(NSNotification *)notification -{ - - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->WindowStateChanged(); - } -}*/ @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup 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/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/Image.cs b/src/Avalonia.Controls/Image.cs index ff6cd482df..c2cdcb4e69 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -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); @@ -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. /// @@ -59,7 +76,7 @@ namespace Avalonia.Controls { Rect viewPort = new Rect(Bounds.Size); Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); - Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize); + Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); Size scaledSize = sourceSize * scale; Rect destRect = viewPort .CenterRect(new Rect(scaledSize)) @@ -85,15 +102,8 @@ 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); - } + var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); + result = Stretch.CalculateSize(availableSize, sourceSize, StretchDirection); } return result; diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 3010a3d8a8..f0358ec04f 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -306,7 +306,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (placement == PlacementMode.Pointer) { positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); - positionerParameters.Anchor = PopupPositioningEdge.BottomRight; + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; positionerParameters.Gravity = PopupPositioningEdge.BottomRight; } else 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.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index 8081528e55..f9737b461d 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -47,7 +47,8 @@ namespace Avalonia.FreeDesktop var fProcMounts = File.ReadAllLines(ProcMountsDir) .Select(x => x.Split(' ')) - .Select(x => (x[0], x[1])); + .Select(x => (x[0], x[1])) + .Where(x => !x.Item2.StartsWith("/snap/", StringComparison.InvariantCultureIgnoreCase)); var labelDirEnum = Directory.Exists(DevByLabelDir) ? new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); 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.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index e72fefe3ce..5d701dc8df 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -353,6 +353,11 @@ namespace Avalonia.Native public void SetCursor(IPlatformHandle cursor) { + if (_native == null) + { + return; + } + var newCursor = cursor as AvaloniaNativeCursor; newCursor = newCursor ?? (_cursorFactory.GetCursor(StandardCursorType.Arrow) as AvaloniaNativeCursor); _native.Cursor = newCursor.Cursor; 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/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.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/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/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/tests/Avalonia.Controls.UnitTests/ImageTests.cs b/tests/Avalonia.Controls.UnitTests/ImageTests.cs index 71d0d1e328..5102085fbf 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.PixelSize == new PixelSize(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 + +