Browse Source

Merge branch 'master' into issues/xamlilpathgeneration

pull/3151/head
Andrey Kunchev 7 years ago
committed by GitHub
parent
commit
797c5c97df
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      azure-pipelines.yml
  2. 4
      build/HarfBuzzSharp.props
  3. 2
      build/SharedVersion.props
  4. 4
      build/SkiaSharp.props
  5. 18
      native/Avalonia.Native/inc/avalonia-native.h
  6. 14
      native/Avalonia.Native/src/OSX/app.mm
  7. 90
      native/Avalonia.Native/src/OSX/window.mm
  8. 4
      samples/ControlCatalog/App.xaml.cs
  9. 4
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  10. 3
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  11. 9
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  12. 36
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  13. 3
      samples/RenderDemo/MainWindow.xaml
  14. 14
      samples/RenderDemo/Pages/GlyphRunPage.xaml
  15. 80
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  16. 12
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  17. 17
      src/Avalonia.Base/Data/BindingOperations.cs
  18. 13
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  19. 6
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  20. 20
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  21. 14
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  22. 15
      src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs
  23. 25
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  24. 2
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  25. 48
      src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
  26. 21
      src/Avalonia.Controls/Application.cs
  27. 5
      src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs
  28. 49
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  29. 25
      src/Avalonia.Controls/ItemsControl.cs
  30. 10
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  31. 4
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  32. 2
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  33. 25
      src/Avalonia.Controls/Primitives/RangeBase.cs
  34. 18
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  35. 12
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  36. 15
      src/Avalonia.Controls/Shapes/Shape.cs
  37. 2
      src/Avalonia.Controls/TextBlock.cs
  38. 14
      src/Avalonia.Controls/TopLevel.cs
  39. 3
      src/Avalonia.Controls/TreeView.cs
  40. 9
      src/Avalonia.Controls/TreeViewItem.cs
  41. 20
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  42. 1
      src/Avalonia.Controls/Window.cs
  43. 18
      src/Avalonia.Input/FocusManager.cs
  44. 2
      src/Avalonia.Input/IKeyboardDevice.cs
  45. 4
      src/Avalonia.Input/InputElement.cs
  46. 16
      src/Avalonia.Input/MouseDevice.cs
  47. 8
      src/Avalonia.Input/Pointer.cs
  48. 22
      src/Avalonia.Input/PointerPoint.cs
  49. 4
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  50. 27
      src/Avalonia.Layout/LayoutHelper.cs
  51. 8
      src/Avalonia.Layout/Layoutable.cs
  52. 2
      src/Avalonia.Native/Avalonia.Native.csproj
  53. 33
      src/Avalonia.Native/WindowImplBase.cs
  54. 13
      src/Avalonia.Styling/IDataContextProvider.cs
  55. 8
      src/Avalonia.Styling/IStyledElement.cs
  56. 4
      src/Avalonia.Styling/StyledElement.cs
  57. 5
      src/Avalonia.Themes.Default/RadioButton.xaml
  58. 68
      src/Avalonia.Visuals/Media/CharacterHit.cs
  59. 62
      src/Avalonia.Visuals/Media/DrawingContext.cs
  60. 61
      src/Avalonia.Visuals/Media/FontFamily.cs
  61. 153
      src/Avalonia.Visuals/Media/FontManager.cs
  62. 72
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  63. 15
      src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
  64. 15
      src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
  65. 15
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  66. 457
      src/Avalonia.Visuals/Media/GlyphRun.cs
  67. 50
      src/Avalonia.Visuals/Media/GlyphRunDrawing.cs
  68. 11
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  69. 4
      src/Avalonia.Visuals/Media/Typeface.cs
  70. 28
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  71. 30
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  72. 12
      src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs
  73. 5
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  74. 17
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  75. 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  76. 5
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  77. 16
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  78. 91
      src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
  79. 43
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  80. 154
      src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
  81. 22
      src/Avalonia.Visuals/Visual.cs
  82. 155
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  83. 2
      src/Avalonia.X11/X11Clipboard.cs
  84. 36
      src/Avalonia.X11/X11Window.cs
  85. 20
      src/Avalonia.X11/XI2Manager.cs
  86. 7
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  87. 11
      src/Markup/Avalonia.Markup/Data/Binding.cs
  88. 16
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  89. 63
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  90. 77
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  91. 4
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  92. 35
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  93. 10
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  94. 96
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  95. 4
      src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs
  96. 47
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  97. 8
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  98. 5
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  99. 47
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  100. 19
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs

12
azure-pipelines.yml

@ -34,9 +34,17 @@ jobs:
pool:
vmImage: 'macOS-10.14'
steps:
- task: DotNetCoreInstaller@0
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.0.x'
inputs:
version: '2.1.403'
packageType: sdk
version: 3.0.x
- task: UseDotNet@2
displayName: 'Use .NET Core Runtime 2.1.x'
inputs:
packageType: runtime
version: 2.1.x
- task: CmdLine@2
displayName: 'Install Mono 5.18'

4
build/HarfBuzzSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.6.1-rc.153" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1-rc.153" />
<PackageReference Include="HarfBuzzSharp" Version="2.6.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1" />
</ItemGroup>
</Project>

2
build/SharedVersion.props

@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Product>Avalonia</Product>
<Version>0.8.999</Version>
<Version>0.9.999</Version>
<Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.1-rc.153" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1-rc.153" />
<PackageReference Include="SkiaSharp" Version="1.68.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
</ItemGroup>
</Project>

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

@ -92,9 +92,17 @@ enum AvnRawMouseEventType
RightButtonUp,
MiddleButtonDown,
MiddleButtonUp,
XButton1Down,
XButton1Up,
XButton2Down,
XButton2Up,
Move,
Wheel,
NonClientLeftButtonDown
NonClientLeftButtonDown,
TouchBegin,
TouchUpdate,
TouchEnd,
TouchCancel
};
enum AvnRawKeyEventType
@ -112,7 +120,9 @@ enum AvnInputModifiers
Windows = 8,
LeftMouseButton = 16,
RightMouseButton = 32,
MiddleMouseButton = 64
MiddleMouseButton = 64,
XButton1MouseButton = 128,
XButton2MouseButton = 256
};
enum AvnWindowState
@ -212,6 +222,10 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0;
virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0;
virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
virtual bool TryLock() = 0;
virtual void Unlock() = 0;
};

14
native/Avalonia.Native/src/OSX/app.mm

@ -1,16 +1,25 @@
#include "common.h"
@interface AvnAppDelegate : NSObject<NSApplicationDelegate>
@end
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@implementation AvnAppDelegate
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
{
for (NSRunningApplication * app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) {
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
break;
}
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
}
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
[NSApp activateIgnoringOtherApps:true];
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
}
@end
@ -20,5 +29,4 @@ extern void InitializeAvnApp()
NSApplication* app = [NSApplication sharedApplication];
id delegate = [AvnAppDelegate new];
[app setDelegate:delegate];
}

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

@ -83,6 +83,54 @@ public:
[Window setContentView: View];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
{
if (ret == nullptr)
{
return E_POINTER;
}
*ret = (__bridge void*)Window;
return S_OK;
}
virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override
{
if (ret == nullptr)
{
return E_POINTER;
}
*ret = (__bridge_retained void*)Window;
return S_OK;
}
virtual HRESULT ObtainNSViewHandle(void** ret) override
{
if (ret == nullptr)
{
return E_POINTER;
}
*ret = (__bridge void*)View;
return S_OK;
}
virtual HRESULT ObtainNSViewHandleRetained(void** ret) override
{
if (ret == nullptr)
{
return E_POINTER;
}
*ret = (__bridge_retained void*)View;
return S_OK;
}
virtual AvnWindow* GetNSWindow() override
{
return Window;
@ -668,7 +716,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
AvnFramebuffer _swRenderedFrameBuffer;
bool _queuedDisplayFromThread;
NSTrackingArea* _area;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isMouseOver;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver;
NSEvent* _lastMouseDownEvent;
bool _lastKeyHandled;
}
@ -691,6 +739,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
self = [super init];
[self setWantsBestResolutionOpenGLSurface:true];
[self setWantsLayer:YES];
_parent = parent;
_area = nullptr;
return self;
@ -893,9 +942,23 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)otherMouseDown:(NSEvent *)event
{
_isMiddlePressed = true;
_lastMouseDownEvent = event;
[self mouseEvent:event withType:MiddleButtonDown];
switch(event.buttonNumber)
{
case 3:
_isMiddlePressed = true;
[self mouseEvent:event withType:MiddleButtonDown];
break;
case 4:
_isXButton1Pressed = true;
[self mouseEvent:event withType:XButton1Down];
break;
case 5:
_isXButton2Pressed = true;
[self mouseEvent:event withType:XButton2Down];
break;
}
}
- (void)rightMouseDown:(NSEvent *)event
@ -913,8 +976,21 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)otherMouseUp:(NSEvent *)event
{
_isMiddlePressed = false;
[self mouseEvent:event withType:MiddleButtonUp];
switch(event.buttonNumber)
{
case 3:
_isMiddlePressed = false;
[self mouseEvent:event withType:MiddleButtonUp];
break;
case 4:
_isXButton1Pressed = false;
[self mouseEvent:event withType:XButton1Up];
break;
case 5:
_isXButton2Pressed = false;
[self mouseEvent:event withType:XButton2Up];
break;
}
}
- (void)rightMouseUp:(NSEvent *)event
@ -1013,6 +1089,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
rv |= MiddleMouseButton;
if (_isRightPressed)
rv |= RightMouseButton;
if (_isXButton1Pressed)
rv |= XButton1MouseButton;
if (_isXButton2Pressed)
rv |= XButton2MouseButton;
return (AvnInputModifiers)rv;
}

4
samples/ControlCatalog/App.xaml.cs

@ -1,6 +1,4 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@ -19,7 +17,7 @@ namespace ControlCatalog
desktopLifetime.MainWindow = new MainWindow();
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
singleViewLifetime.MainView = new MainView();
base.OnFrameworkInitializationCompleted();
}
}

4
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@ -1,5 +1,7 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace ControlCatalog.Pages
{
@ -14,7 +16,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Find<ComboBox>("fontComboBox");
fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies;
fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.SelectedIndex = 0;
}
}

3
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -15,6 +15,7 @@
</ComboBox>
<Button Command="{Binding AddItem}">Add Item</Button>
<Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
<Button Command="{Binding ResetItems}">Reset items</Button>
</StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
<ScrollViewer Name="scroller"
@ -23,7 +24,7 @@
<ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<TextBlock Height="{Binding Height}" Text="{Binding Text}"/>
<TextBlock Focusable="True" Height="{Binding Height}" Text="{Binding Text}"/>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>

9
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -20,6 +20,7 @@ namespace ControlCatalog.Pages
_repeater = this.FindControl<ItemsRepeater>("repeater");
_scroller = this.FindControl<ScrollViewer>("scroller");
_repeater.PointerPressed += RepeaterClick;
_repeater.KeyDown += RepeaterOnKeyDown;
DataContext = new ItemsRepeaterPageViewModel();
}
@ -77,5 +78,13 @@ namespace ControlCatalog.Pages
var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item;
((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
}
private void RepeaterOnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.F5)
{
((ItemsRepeaterPageViewModel)DataContext).ResetItems();
}
}
}
}

36
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@ -7,25 +7,27 @@ namespace ControlCatalog.ViewModels
{
public class ItemsRepeaterPageViewModel : ReactiveObject
{
private int newItemIndex = 1;
private int _newItemIndex = 1;
private int _newGenerationIndex = 0;
private ObservableCollection<Item> _items;
public ItemsRepeaterPageViewModel()
{
Items = new ObservableCollection<Item>(
Enumerable.Range(1, 100000).Select(i => new Item
{
Text = $"Item {i.ToString()}",
}));
Items = CreateItems();
}
public ObservableCollection<Item> Items { get; }
public ObservableCollection<Item> Items
{
get => _items;
set => this.RaiseAndSetIfChanged(ref _items, value);
}
public Item SelectedItem { get; set; }
public void AddItem()
{
var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
Items.Insert(index + 1, new Item { Text = $"New Item {newItemIndex++}" });
Items.Insert(index + 1, new Item { Text = $"New Item {_newItemIndex++}" });
}
public void RandomizeHeights()
@ -38,6 +40,24 @@ namespace ControlCatalog.ViewModels
}
}
public void ResetItems()
{
Items = CreateItems();
}
private ObservableCollection<Item> CreateItems()
{
var suffix = _newGenerationIndex == 0 ? string.Empty : $"[{_newGenerationIndex.ToString()}]";
_newGenerationIndex++;
return new ObservableCollection<Item>(
Enumerable.Range(1, 100000).Select(i => new Item
{
Text = $"Item {i.ToString()} {suffix}"
}));
}
public class Item : ReactiveObject
{
private double _height = double.NaN;

3
samples/RenderDemo/MainWindow.xaml

@ -41,6 +41,9 @@
<TabItem Header="RenderTargetBitmap">
<pages:RenderTargetBitmapPage/>
</TabItem>
<TabItem Header="GlyphRun">
<pages:GlyphRunPage/>
</TabItem>
</TabControl>
</DockPanel>
</Window>

14
samples/RenderDemo/Pages/GlyphRunPage.xaml

@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="RenderDemo.Pages.GlyphRunPage">
<Border
Background="White">
<DrawingPresenter
x:Name="drawingPresenter"
Stretch="None">
</DrawingPresenter>
</Border>
</UserControl>

80
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@ -0,0 +1,80 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
namespace RenderDemo.Pages
{
public class GlyphRunPage : UserControl
{
private DrawingPresenter _drawingPresenter;
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
private float _fontSize = 20;
private int _direction = 10;
public GlyphRunPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_drawingPresenter = this.FindControl<DrawingPresenter>("drawingPresenter");
DispatcherTimer.Run(() =>
{
UpdateGlyphRun();
return true;
}, TimeSpan.FromSeconds(1));
}
private void UpdateGlyphRun()
{
var c = (uint)_rand.Next(65, 90);
if (_fontSize + _direction > 200)
{
_direction = -10;
}
if (_fontSize + _direction < 20)
{
_direction = 10;
}
_fontSize += _direction;
_glyphIndices[0] = _glyphTypeface.GetGlyph(c);
var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
var drawingGroup = new DrawingGroup();
var glyphRunDrawing = new GlyphRunDrawing
{
Foreground = Brushes.Black,
GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale)
};
drawingGroup.Children.Add(glyphRunDrawing);
var geometryDrawing = new GeometryDrawing
{
Pen = new Pen(Brushes.Black),
Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds }
};
drawingGroup.Children.Add(geometryDrawing);
_drawingPresenter.Drawing = drawingGroup;
}
}
}

12
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -173,12 +173,20 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(name != null);
if (name.Contains('.'))
if (name.Contains("."))
{
throw new InvalidOperationException("Attached properties not supported.");
}
return GetRegistered(type).FirstOrDefault(x => x.Name == name);
foreach (AvaloniaProperty x in GetRegistered(type))
{
if (x.Name == name)
{
return x;
}
}
return null;
}
/// <summary>

17
src/Avalonia.Base/Data/BindingOperations.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@ -56,22 +55,34 @@ namespace Avalonia.Data
if (source != null)
{
// Perf: Avoid allocating closure in the outer scope.
var targetCopy = target;
var propertyCopy = property;
var bindingCopy = binding;
return source
.Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
.Take(1)
.Subscribe(x => target.SetValue(property, x, binding.Priority));
.Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority));
}
else
{
target.SetValue(property, binding.Value, binding.Priority);
return Disposable.Empty;
}
case BindingMode.OneWayToSource:
{
// Perf: Avoid allocating closure in the outer scope.
var bindingCopy = binding;
return Observable.CombineLatest(
binding.Observable,
target.GetObservable(property),
(_, v) => v)
.Subscribe(x => binding.Subject.OnNext(x));
.Subscribe(x => bindingCopy.Subject.OnNext(x));
}
default:
throw new ArgumentException("Invalid binding mode.");
}

13
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -21,6 +21,7 @@ namespace Avalonia.Data.Core
private readonly ExpressionObserver _inner;
private readonly Type _targetType;
private readonly object _fallbackValue;
private readonly object _targetNullValue;
private readonly BindingPriority _priority;
InnerListener _innerListener;
WeakReference<object> _value;
@ -51,7 +52,7 @@ namespace Avalonia.Data.Core
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
: this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
: this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
{
}
@ -63,6 +64,9 @@ namespace Avalonia.Data.Core
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="targetNullValue">
/// The value to use when the binding result is null.
/// </param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
@ -72,6 +76,7 @@ namespace Avalonia.Data.Core
ExpressionObserver inner,
Type targetType,
object fallbackValue,
object targetNullValue,
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
@ -85,6 +90,7 @@ namespace Avalonia.Data.Core
Converter = converter;
ConverterParameter = converterParameter;
_fallbackValue = fallbackValue;
_targetNullValue = targetNullValue;
_priority = priority;
}
@ -196,6 +202,11 @@ namespace Avalonia.Data.Core
/// <inheritdoc/>
private object ConvertValue(object value)
{
if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue)
{
return _targetNullValue;
}
if (value == BindingOperations.DoNothing)
{
return value;

6
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -21,7 +21,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of property accessor plugins that can be used to customize
/// the reading and subscription of property values on a type.
/// </summary>
public static readonly IList<IPropertyAccessorPlugin> PropertyAccessors =
public static readonly List<IPropertyAccessorPlugin> PropertyAccessors =
new List<IPropertyAccessorPlugin>
{
new AvaloniaPropertyAccessorPlugin(),
@ -33,7 +33,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
/// </summary>
public static readonly IList<IDataValidationPlugin> DataValidators =
public static readonly List<IDataValidationPlugin> DataValidators =
new List<IDataValidationPlugin>
{
new DataAnnotationsValidationPlugin(),
@ -45,7 +45,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of stream plugins that can be used to customize the behavior
/// of the '^' stream binding operator.
/// </summary>
public static readonly IList<IStreamPlugin> StreamHandlers =
public static readonly List<IStreamPlugin> StreamHandlers =
new List<IStreamPlugin>
{
new TaskStreamPlugin(),

20
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -2,7 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Runtime.ExceptionServices;
namespace Avalonia.Data.Core.Plugins
{
@ -76,7 +76,7 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
private class Accessor : PropertyAccessorBase
private class Accessor : PropertyAccessorBase, IObserver<object>
{
private readonly WeakReference<AvaloniaObject> _reference;
private readonly AvaloniaProperty _property;
@ -117,7 +117,7 @@ namespace Avalonia.Data.Core.Plugins
protected override void SubscribeCore()
{
_subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
_subscription = Instance?.GetObservable(_property).Subscribe(this);
}
protected override void UnsubscribeCore()
@ -125,6 +125,20 @@ namespace Avalonia.Data.Core.Plugins
_subscription?.Dispose();
_subscription = null;
}
void IObserver<object>.OnCompleted()
{
}
void IObserver<object>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
void IObserver<object>.OnNext(object value)
{
PublishValue(value);
}
}
}
}

14
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -2,8 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
@ -41,7 +39,17 @@ namespace Avalonia.Data.Core
{
reference.TryGetTarget(out object target);
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName));
IPropertyAccessorPlugin plugin = null;
foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors)
{
if (x.Match(target, PropertyName))
{
plugin = x;
break;
}
}
var accessor = plugin?.Start(reference, PropertyName);
if (_enableValidation && Next == null)

15
src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs

@ -0,0 +1,15 @@
// 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;
namespace Avalonia.Platform
{
public interface IMacOSTopLevelPlatformHandle
{
IntPtr NSView { get; }
IntPtr GetNSViewRetained();
IntPtr NSWindow { get; }
IntPtr GetNSWindowRetained();
}
}

25
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@ -116,20 +116,33 @@ namespace Avalonia.Reactive
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
IObserver<T>[] observers = null;
IObserver<T> singleObserver = null;
lock (this)
{
if (_observers == null)
{
return;
}
observers = _observers.ToArray();
if (_observers.Count == 1)
{
singleObserver = _observers[0];
}
else
{
observers = _observers.ToArray();
}
}
foreach (var observer in observers)
if (singleObserver != null)
{
observer.OnNext(value);
singleObserver.OnNext(value);
}
else
{
foreach (var observer in observers)
{
observer.OnNext(value);
}
}
}
}

2
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -15,7 +15,7 @@ namespace Avalonia.Utilities
{
if (IsValidIdentifierStart(r.Peek))
{
return r.TakeWhile(IsValidIdentifierChar);
return r.TakeWhile(c => IsValidIdentifierChar(c));
}
else
{

48
src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs

@ -0,0 +1,48 @@
using System.Collections;
using System.Collections.Generic;
namespace Avalonia.Utilities
{
public struct ImmutableReadOnlyListStructEnumerator<T> : IEnumerator, IEnumerator<T>
{
private readonly IReadOnlyList<T> _readOnlyList;
private int _pos;
public ImmutableReadOnlyListStructEnumerator(IReadOnlyList<T> readOnlyList)
{
_readOnlyList = readOnlyList;
_pos = -1;
Current = default;
}
public T Current
{
get;
private set;
}
object IEnumerator.Current => Current;
public void Dispose() { }
public bool MoveNext()
{
if (_pos >= _readOnlyList.Count - 1)
{
return false;
}
Current = _readOnlyList[++_pos];
return true;
}
public void Reset()
{
_pos = -1;
Current = default;
}
}
}

21
src/Avalonia.Controls/Application.cs

@ -32,7 +32,7 @@ namespace Avalonia
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
{
/// <summary>
/// The application-global data templates.
@ -45,6 +45,12 @@ namespace Avalonia
private Styles _styles;
private IResourceDictionary _resources;
/// <summary>
/// Defines the <see cref="DataContext"/> property.
/// </summary>
public static readonly StyledProperty<object> DataContextProperty =
StyledElement.DataContextProperty.AddOwner<Application>();
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
@ -56,6 +62,19 @@ namespace Avalonia
Name = "Avalonia Application";
}
/// <summary>
/// Gets or sets the Applications's data context.
/// </summary>
/// <remarks>
/// The data context property specifies the default object that will
/// be used for data binding.
/// </remarks>
public object DataContext
{
get { return GetValue(DataContextProperty); }
set { SetValue(DataContextProperty, value); }
}
/// <summary>
/// Gets the current instance of the <see cref="Application"/> class.
/// </summary>

5
src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs

@ -12,5 +12,10 @@ namespace Avalonia.Controls.Generators
/// Gets the container index for the tree.
/// </summary>
TreeContainerIndex Index { get; }
/// <summary>
/// Updates the index based on the parent <see cref="TreeView"/>.
/// </summary>
void UpdateIndex();
}
}

49
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Generators
{
@ -15,6 +17,8 @@ namespace Avalonia.Controls.Generators
public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
where T : class, IControl, new()
{
private TreeView _treeView;
/// <summary>
/// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
/// </summary>
@ -23,31 +27,28 @@ namespace Avalonia.Controls.Generators
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
/// <param name="itemsProperty">The container's Items property.</param>
/// <param name="isExpandedProperty">The container's IsExpanded property.</param>
/// <param name="index">The container index for the tree</param>
public TreeItemContainerGenerator(
IControl owner,
AvaloniaProperty contentProperty,
AvaloniaProperty contentTemplateProperty,
AvaloniaProperty itemsProperty,
AvaloniaProperty isExpandedProperty,
TreeContainerIndex index)
AvaloniaProperty isExpandedProperty)
: base(owner, contentProperty, contentTemplateProperty)
{
Contract.Requires<ArgumentNullException>(owner != null);
Contract.Requires<ArgumentNullException>(contentProperty != null);
Contract.Requires<ArgumentNullException>(itemsProperty != null);
Contract.Requires<ArgumentNullException>(isExpandedProperty != null);
Contract.Requires<ArgumentNullException>(index != null);
ItemsProperty = itemsProperty;
IsExpandedProperty = isExpandedProperty;
Index = index;
UpdateIndex();
}
/// <summary>
/// Gets the container index for the tree.
/// </summary>
public TreeContainerIndex Index { get; }
public TreeContainerIndex Index { get; private set; }
/// <summary>
/// Gets the item container's Items property.
@ -70,7 +71,7 @@ namespace Avalonia.Controls.Generators
}
else if (container != null)
{
Index.Add(item, container);
Index?.Add(item, container);
return container;
}
else
@ -92,7 +93,7 @@ namespace Avalonia.Controls.Generators
result.DataContext = item;
}
Index.Add(item, result);
Index?.Add(item, result);
return result;
}
@ -101,24 +102,50 @@ namespace Avalonia.Controls.Generators
public override IEnumerable<ItemContainerInfo> Clear()
{
var items = base.Clear();
Index.Remove(0, items);
Index?.Remove(0, items);
return items;
}
public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
Index.Remove(startingIndex, GetContainerRange(startingIndex, count));
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
Index.Remove(startingIndex, GetContainerRange(startingIndex, count));
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count);
}
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
public void UpdateIndex()
{
if (Owner is TreeView treeViewOwner && Index == null)
{
Index = new TreeContainerIndex();
_treeView = treeViewOwner;
}
else if (Owner.IsAttachedToLogicalTree)
{
var treeView = Owner.GetSelfAndLogicalAncestors().OfType<TreeView>().FirstOrDefault();
if (treeView != _treeView)
{
Clear();
Index = treeView?.ItemContainerGenerator?.Index;
_treeView = treeView;
}
}
else
{
Clear();
Index = null;
_treeView = null;
}
}
class WrapperTreeDataTemplate : ITreeDataTemplate
{
private readonly IDataTemplate _inner;

25
src/Avalonia.Controls/ItemsControl.cs

@ -5,7 +5,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
@ -324,20 +323,24 @@ namespace Avalonia.Controls
return;
}
var current = focus.Current
.GetSelfAndVisualAncestors()
.OfType<IInputElement>()
.FirstOrDefault(x => x.VisualParent == container);
IVisual current = focus.Current;
if (current != null)
while (current != null)
{
var next = GetNextControl(container, direction.Value, current, false);
if (next != null)
if (current.VisualParent == container && current is IInputElement inputElement)
{
focus.Focus(next, NavigationMethod.Directional);
e.Handled = true;
IInputElement next = GetNextControl(container, direction.Value, inputElement, false);
if (next != null)
{
focus.Focus(next, NavigationMethod.Directional);
e.Handled = true;
}
break;
}
current = current.VisualParent;
}
}

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

@ -149,15 +149,9 @@ namespace Avalonia.Controls.Notifications
/// <param name="host">The <see cref="Window"/> that will be the host.</param>
private void Install(Window host)
{
var adornerLayer = host.GetVisualDescendants()
.OfType<VisualLayerManager>()
.FirstOrDefault()
?.AdornerLayer;
var adornerLayer = host.FindDescendantOfType<VisualLayerManager>()?.AdornerLayer;
if (adornerLayer != null)
{
adornerLayer.Children.Add(this);
}
adornerLayer?.Children.Add(this);
}
}
}

4
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -188,8 +188,8 @@ namespace Avalonia.Controls.Presenters
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex >= FirstIndex &&
e.OldStartingIndex < NextIndex)
if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) ||
panel.Children.Count > ItemCount)
{
RecycleContainersOnRemove();
}

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

@ -297,7 +297,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText
{
Text = "X",
Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
TextAlignment = TextAlignment,
Constraint = availableSize,

25
src/Avalonia.Controls/Primitives/RangeBase.cs

@ -75,7 +75,10 @@ namespace Avalonia.Controls.Primitives
set
{
ValidateDouble(value, "Minimum");
if (!ValidateDouble(value))
{
return;
}
if (IsInitialized)
{
@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives
set
{
ValidateDouble(value, "Maximum");
if (!ValidateDouble(value))
{
return;
}
if (IsInitialized)
{
@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives
set
{
ValidateDouble(value, "Value");
if (!ValidateDouble(value))
{
return;
}
if (IsInitialized)
{
@ -164,16 +173,12 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Throws an exception if the double value is NaN or Inf.
/// Checks if the double value is not inifinity nor NaN.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="property">The name of the property being set.</param>
private static void ValidateDouble(double value, string property)
private static bool ValidateDouble(double value)
{
if (double.IsInfinity(value) || double.IsNaN(value))
{
throw new ArgumentException($"{value} is not a valid value for {property}.");
}
return !double.IsInfinity(value) || !double.IsNaN(value);
}
/// <summary>

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

@ -13,7 +13,6 @@ using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Logging;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
@ -269,11 +268,20 @@ namespace Avalonia.Controls.Primitives
/// <returns>The container or null if the event did not originate in a container.</returns>
protected IControl GetContainerFromEventSource(IInteractive eventSource)
{
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
.OfType<IControl>()
.FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1);
var parent = (IVisual)eventSource;
return item;
while (parent != null)
{
if (parent is IControl control && control.LogicalParent == this
&& ItemContainerGenerator?.IndexFromContainer(control) != -1)
{
return control;
}
parent = parent.VisualParent;
}
return null;
}
/// <inheritdoc/>

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

@ -565,7 +565,17 @@ namespace Avalonia.Controls
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
_processingItemsSourceChange = args;
try
{
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
}
finally
{
_processingItemsSourceChange = null;
}
}
else if (Layout is NonVirtualizingLayout nonVirtualLayout)
{

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

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reflection;
using Avalonia.Collections;
using Avalonia.Media;
@ -166,12 +165,9 @@ namespace Avalonia.Controls.Shapes
{
property.Changed.Subscribe(e =>
{
var senderType = e.Sender.GetType().GetTypeInfo();
var affectedType = typeof(TShape).GetTypeInfo();
if (affectedType.IsAssignableFrom(senderType))
if (e.Sender is TShape shape)
{
AffectsGeometryInvalidate(e);
AffectsGeometryInvalidate(shape, e);
}
});
}
@ -322,13 +318,8 @@ namespace Avalonia.Controls.Shapes
return (size, transform);
}
private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e)
private static void AffectsGeometryInvalidate(Shape control, AvaloniaPropertyChangedEventArgs e)
{
if (!(e.Sender is Shape control))
{
return;
}
// If the geometry is invalidated when Bounds changes, only invalidate when the Size
// portion changes.
if (e.Property == BoundsProperty)

2
src/Avalonia.Controls/TextBlock.cs

@ -352,7 +352,7 @@ namespace Avalonia.Controls
return new FormattedText
{
Constraint = constraint,
Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,

14
src/Avalonia.Controls/TopLevel.cs

@ -2,9 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.Raw;
@ -15,7 +13,6 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace Avalonia.Controls
@ -269,6 +266,12 @@ namespace Avalonia.Controls
/// </summary>
protected virtual void HandleClosed()
{
var logicalArgs = new LogicalTreeAttachmentEventArgs(this);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
var visualArgs = new VisualTreeAttachmentEventArgs(this, this);
OnDetachedFromVisualTreeCore(visualArgs);
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null;
OnClosed(EventArgs.Empty);
@ -296,10 +299,7 @@ namespace Avalonia.Controls
/// <param name="scaling">The window scaling.</param>
protected virtual void HandleScalingChanged(double scaling)
{
foreach (ILayoutable control in this.GetSelfAndVisualDescendants())
{
control.InvalidateMeasure();
}
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
}
/// <inheritdoc/>

3
src/Avalonia.Controls/TreeView.cs

@ -393,8 +393,7 @@ namespace Avalonia.Controls
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
new TreeContainerIndex());
TreeViewItem.IsExpandedProperty);
result.Index.Materialized += ContainerMaterialized;
return result;
}

9
src/Avalonia.Controls/TreeViewItem.cs

@ -98,17 +98,18 @@ namespace Avalonia.Controls
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
_treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex());
TreeViewItem.IsExpandedProperty);
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_treeView = this.GetLogicalAncestors().OfType<TreeView>().FirstOrDefault();
Level = CalculateDistanceFromLogicalParent<TreeView>(this) - 1;
ItemContainerGenerator.UpdateIndex();
if (ItemTemplate == null && _treeView?.ItemTemplate != null)
{
@ -119,7 +120,7 @@ namespace Avalonia.Controls
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
ItemContainerGenerator.Clear();
ItemContainerGenerator.UpdateIndex();
}
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)

20
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -90,23 +90,17 @@ namespace Avalonia.Controls.Utils
{
var borderThickness = borders.Top;
var top = borderThickness * 0.5;
var cornerRadius = (float)Math.Max(0, radii.TopLeft - borderThickness - top);
if (background != null)
{
var topLeft = new Point(borders.Left, borders.Top);
var bottomRight = new Point(size.Width - borders.Right, size.Height - borders.Bottom);
var innerRect = new Rect(topLeft, bottomRight);
context.FillRectangle(background, innerRect, cornerRadius);
}
IPen pen = null;
if (borderBrush != null && borderThickness > 0)
if (borderThickness > 0)
{
var topLeft = new Point(top, top);
var bottomRight = new Point(size.Width - top, size.Height - top);
var outerRect = new Rect(topLeft, bottomRight);
context.DrawRectangle(new Pen(borderBrush, borderThickness), outerRect, (float)radii.TopLeft);
pen = new Pen(borderBrush, borderThickness);
}
var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness);
context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft);
}
}

1
src/Avalonia.Controls/Window.cs

@ -336,7 +336,6 @@ namespace Avalonia.Controls
if (close)
{
PlatformImpl?.Dispose();
HandleClosed();
}
}
}

18
src/Avalonia.Input/FocusManager.cs

@ -180,18 +180,18 @@ namespace Avalonia.Input
if (sender == e.Source && ev.MouseButton == MouseButton.Left)
{
var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement);
IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement;
if (element == null || !CanFocus(element))
while (element != null)
{
element = element.GetSelfAndVisualAncestors()
.OfType<IInputElement>()
.FirstOrDefault(CanFocus);
}
if (element is IInputElement inputElement && CanFocus(inputElement))
{
Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers);
if (element != null)
{
Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers);
break;
}
element = element.VisualParent;
}
}
}

2
src/Avalonia.Input/IKeyboardDevice.cs

@ -48,6 +48,8 @@ namespace Avalonia.Input
LeftMouseButton = 16,
RightMouseButton = 32,
MiddleMouseButton = 64,
XButton1MouseButton = 128,
XButton2MouseButton = 256,
KeyboardMask = Alt | Control | Shift | Meta
}

4
src/Avalonia.Input/InputElement.cs

@ -342,7 +342,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Gets or sets a value indicating whether the control is focused.
/// Gets a value indicating whether the control is focused.
/// </summary>
public bool IsFocused
{
@ -360,7 +360,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Gets or sets a value indicating whether the pointer is currently over the control.
/// Gets a value indicating whether the pointer is currently over the control.
/// </summary>
public bool IsPointerOver
{

16
src/Avalonia.Input/MouseDevice.cs

@ -120,6 +120,10 @@ namespace Avalonia.Input
rv++;
if (props.IsRightButtonPressed)
rv++;
if (props.IsXButton1Pressed)
rv++;
if (props.IsXButton2Pressed)
rv++;
return rv;
}
@ -142,6 +146,8 @@ namespace Avalonia.Input
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown:
case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
else
@ -151,6 +157,8 @@ namespace Avalonia.Input
case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp:
case RawPointerEventType.MiddleButtonUp:
case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
else
@ -186,12 +194,20 @@ namespace Avalonia.Input
kind = PointerUpdateKind.MiddleButtonPressed;
if (args.Type == RawPointerEventType.RightButtonDown)
kind = PointerUpdateKind.RightButtonPressed;
if (args.Type == RawPointerEventType.XButton1Down)
kind = PointerUpdateKind.XButton1Pressed;
if (args.Type == RawPointerEventType.XButton2Down)
kind = PointerUpdateKind.XButton2Pressed;
if (args.Type == RawPointerEventType.LeftButtonUp)
kind = PointerUpdateKind.LeftButtonReleased;
if (args.Type == RawPointerEventType.MiddleButtonUp)
kind = PointerUpdateKind.MiddleButtonReleased;
if (args.Type == RawPointerEventType.RightButtonUp)
kind = PointerUpdateKind.RightButtonReleased;
if (args.Type == RawPointerEventType.XButton1Up)
kind = PointerUpdateKind.XButton1Released;
if (args.Type == RawPointerEventType.XButton2Up)
kind = PointerUpdateKind.XButton2Released;
return new PointerPointProperties(args.InputModifiers, kind);
}

8
src/Avalonia.Input/Pointer.cs

@ -55,9 +55,11 @@ namespace Avalonia.Input
Captured.DetachedFromVisualTree += OnCaptureDetached;
}
IInputElement GetNextCapture(IVisual parent) =>
parent as IInputElement ?? parent.GetVisualAncestors().OfType<IInputElement>().FirstOrDefault();
IInputElement GetNextCapture(IVisual parent)
{
return parent as IInputElement ?? parent.FindAncestorOfType<IInputElement>();
}
private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
{
Capture(GetNextCapture(e.Parent));

22
src/Avalonia.Input/PointerPoint.cs

@ -21,18 +21,24 @@ namespace Avalonia.Input
public bool IsLeftButtonPressed { get; }
public bool IsMiddleButtonPressed { get; }
public bool IsRightButtonPressed { get; }
public bool IsXButton1Pressed { get; }
public bool IsXButton2Pressed { get; }
public PointerUpdateKind PointerUpdateKind { get; }
private PointerPointProperties()
{
{
}
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
{
PointerUpdateKind = kind;
IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton);
IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton);
IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton);
IsXButton1Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton1MouseButton);
IsXButton2Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton2MouseButton);
// The underlying input source might be reporting the previous state,
// so make sure that we reflect the current state
@ -49,6 +55,14 @@ namespace Avalonia.Input
IsRightButtonPressed = true;
if (kind == PointerUpdateKind.RightButtonReleased)
IsRightButtonPressed = false;
if (kind == PointerUpdateKind.XButton1Pressed)
IsXButton1Pressed = true;
if (kind == PointerUpdateKind.XButton1Released)
IsXButton1Pressed = false;
if (kind == PointerUpdateKind.XButton2Pressed)
IsXButton2Pressed = true;
if (kind == PointerUpdateKind.XButton2Released)
IsXButton2Pressed = false;
}
public static PointerPointProperties None { get; } = new PointerPointProperties();
@ -59,9 +73,13 @@ namespace Avalonia.Input
LeftButtonPressed,
MiddleButtonPressed,
RightButtonPressed,
XButton1Pressed,
XButton2Pressed,
LeftButtonReleased,
MiddleButtonReleased,
RightButtonReleased,
XButton1Released,
XButton2Released,
Other
}

4
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -14,6 +14,10 @@ namespace Avalonia.Input.Raw
RightButtonUp,
MiddleButtonDown,
MiddleButtonUp,
XButton1Down,
XButton1Up,
XButton2Down,
XButton2Up,
Move,
Wheel,
NonClientLeftButtonDown,

27
src/Avalonia.Layout/LayoutHelper.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.VisualTree;
namespace Avalonia.Layout
{
@ -61,5 +62,31 @@ namespace Avalonia.Layout
return availableSize;
}
/// <summary>
/// Invalidates measure for given control and all visual children recursively.
/// </summary>
public static void InvalidateSelfAndChildrenMeasure(ILayoutable control)
{
void InnerInvalidateMeasure(IVisual target)
{
if (target is ILayoutable targetLayoutable)
{
targetLayoutable.InvalidateMeasure();
}
var visualChildren = target.VisualChildren;
var visualChildrenCount = visualChildren.Count;
for (int i = 0; i < visualChildrenCount; i++)
{
IVisual child = visualChildren[i];
InnerInvalidateMeasure(child);
}
}
InnerInvalidateMeasure(control);
}
}
}

8
src/Avalonia.Layout/Layoutable.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Avalonia.Logging;
using Avalonia.VisualTree;
@ -694,12 +693,9 @@ namespace Avalonia.Layout
}
/// <inheritdoc/>
protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{
foreach (ILayoutable i in this.GetSelfAndVisualDescendants())
{
i.InvalidateMeasure();
}
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
base.OnVisualParentChanged(oldParent, newParent);
}

2
src/Avalonia.Native/Avalonia.Native.csproj

@ -22,6 +22,6 @@
<PackageReference Include="SharpGenTools.Sdk" Version="1.1.2" PrivateAssets="all" />
<PackageReference Include="SharpGen.Runtime.COM" Version="1.1.0" />
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.Dialogs.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
</ItemGroup>
</Project>

33
src/Avalonia.Native/WindowImplBase.cs

@ -16,6 +16,34 @@ using Avalonia.Threading;
namespace Avalonia.Native
{
public class MacOSTopLevelWindowHandle : IPlatformHandle, IMacOSTopLevelPlatformHandle
{
IAvnWindowBase _native;
public MacOSTopLevelWindowHandle(IAvnWindowBase native)
{
_native = native;
}
public IntPtr Handle => NSWindow;
public string HandleDescriptor => "NSWindow";
public IntPtr NSView => _native.ObtainNSViewHandle();
public IntPtr NSWindow => _native.ObtainNSWindowHandle();
public IntPtr GetNSViewRetained()
{
return _native.ObtainNSViewHandleRetained();
}
public IntPtr GetNSWindowRetained()
{
return _native.ObtainNSWindowHandleRetained();
}
}
public abstract class WindowBaseImpl : IWindowBaseImpl,
IFramebufferPlatformSurface
{
@ -45,6 +73,9 @@ namespace Avalonia.Native
protected void Init(IAvnWindowBase window, IAvnScreens screens)
{
_native = window;
Handle = new MacOSTopLevelWindowHandle(window);
_glSurface = new GlPlatformSurface(window);
Screen = new ScreenImpl(screens);
_savedLogicalSize = ClientSize;
@ -349,6 +380,6 @@ namespace Avalonia.Native
}
public IPlatformHandle Handle => new PlatformHandle(IntPtr.Zero, "NOT SUPPORTED");
public IPlatformHandle Handle { get; private set; }
}
}

13
src/Avalonia.Styling/IDataContextProvider.cs

@ -0,0 +1,13 @@
namespace Avalonia
{
/// <summary>
/// Defines an element with a data context that can be used for binding.
/// </summary>
public interface IDataContextProvider : IAvaloniaObject
{
/// <summary>
/// Gets or sets the element's data context.
/// </summary>
object DataContext { get; set; }
}
}

8
src/Avalonia.Styling/IStyledElement.cs

@ -10,7 +10,8 @@ namespace Avalonia
IStyleHost,
ILogical,
IResourceProvider,
IResourceNode
IResourceNode,
IDataContextProvider
{
/// <summary>
/// Occurs when the control has finished initialization.
@ -27,11 +28,6 @@ namespace Avalonia
/// </summary>
new Classes Classes { get; set; }
/// <summary>
/// Gets or sets the control's data context.
/// </summary>
object DataContext { get; set; }
/// <summary>
/// Gets the control's logical parent.
/// </summary>

4
src/Avalonia.Styling/StyledElement.cs

@ -24,7 +24,7 @@ namespace Avalonia
/// - Implements <see cref="ILogical"/> to form part of a logical tree.
/// - A collection of class strings for custom styling.
/// </summary>
public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent
public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent
{
/// <summary>
/// Defines the <see cref="DataContext"/> property.
@ -288,7 +288,7 @@ namespace Avalonia
var list = new AvaloniaList<ILogical>
{
ResetBehavior = ResetBehavior.Remove,
Validate = ValidateLogicalChild
Validate = logical => ValidateLogicalChild(logical)
};
list.CollectionChanged += LogicalChildrenCollectionChanged;
_logicalChildren = list;

5
src/Avalonia.Themes.Default/RadioButton.xaml

@ -13,7 +13,6 @@
Height="18"
VerticalAlignment="Center"/>
<Ellipse Name="checkMark"
Fill="{DynamicResource HighlightBrush}"
Width="10"
Height="10"
Stretch="Uniform"
@ -38,10 +37,12 @@
</ControlTemplate>
</Setter>
</Style>
<Style Selector="RadioButton:pointerover /template/ Ellipse#border">
<Setter Property="Stroke" Value="{DynamicResource ThemeBorderHighBrush}"/>
</Style>
<Style Selector="RadioButton /template/ Ellipse#checkMark">
<Setter Property="Fill" Value="{DynamicResource HighlightBrush}"/>
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="RadioButton /template/ Ellipse#indeterminateMark">
@ -56,4 +57,4 @@
<Style Selector="RadioButton:disabled /template/ Ellipse#border">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
</Style>
</Styles>
</Styles>

68
src/Avalonia.Visuals/Media/CharacterHit.cs

@ -0,0 +1,68 @@
// 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;
namespace Avalonia.Media
{
/// <summary>
/// Represents information about a character hit within a glyph run.
/// </summary>
/// <remarks>
/// The CharacterHit structure provides information about the index of the first
/// character that got hit as well as information about leading or trailing edge.
/// </remarks>
public readonly struct CharacterHit : IEquatable<CharacterHit>
{
/// <summary>
/// Initializes a new instance of the <see cref="CharacterHit"/> structure.
/// </summary>
/// <param name="firstCharacterIndex">Index of the first character that got hit.</param>
/// <param name="trailingLength">In the case of a leading edge, this value is 0. In the case of a trailing edge,
/// this value is the number of code points until the next valid caret position.</param>
public CharacterHit(int firstCharacterIndex, int trailingLength = 0)
{
FirstCharacterIndex = firstCharacterIndex;
TrailingLength = trailingLength;
}
/// <summary>
/// Gets the index of the first character that got hit.
/// </summary>
public int FirstCharacterIndex { get; }
/// <summary>
/// Gets the trailing length value for the character that got hit.
/// </summary>
public int TrailingLength { get; }
public bool Equals(CharacterHit other)
{
return FirstCharacterIndex == other.FirstCharacterIndex && TrailingLength == other.TrailingLength;
}
public override bool Equals(object obj)
{
return obj is CharacterHit other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return FirstCharacterIndex * 397 ^ TrailingLength;
}
}
public static bool operator ==(CharacterHit left, CharacterHit right)
{
return left.Equals(right);
}
public static bool operator !=(CharacterHit left, CharacterHit right)
{
return !left.Equals(right);
}
}
}

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

@ -118,6 +118,42 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Draws a rectangle with the specified Brush and Pen.
/// </summary>
/// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="radiusX">The radius in the X dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Width/2
/// </param>
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
/// </param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0)
{
if (brush == null && !PenIsVisible(pen))
{
return;
}
if (Math.Abs(radiusX) > double.Epsilon)
{
radiusX = Math.Min(radiusX, rect.Width / 2);
}
if (Math.Abs(radiusY) > double.Epsilon)
{
radiusY = Math.Min(radiusY, rect.Height / 2);
}
PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY);
}
/// <summary>
/// Draws the outline of a rectangle.
/// </summary>
@ -126,10 +162,7 @@ namespace Avalonia.Media
/// <param name="cornerRadius">The corner radius.</param>
public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f)
{
if (PenIsVisible(pen))
{
PlatformImpl.DrawRectangle(pen, rect, cornerRadius);
}
DrawRectangle(null, pen, rect, cornerRadius, cornerRadius);
}
/// <summary>
@ -154,6 +187,22 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Draws a glyph run.
/// </summary>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
Contract.Requires<ArgumentNullException>(glyphRun != null);
if (foreground != null)
{
PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin);
}
}
/// <summary>
/// Draws a filled rectangle.
/// </summary>
@ -162,10 +211,7 @@ namespace Avalonia.Media
/// <param name="cornerRadius">The corner radius.</param>
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f)
{
if (brush != null && rect != Rect.Empty)
{
PlatformImpl.FillRectangle(brush, rect, cornerRadius);
}
DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
}
public readonly struct PushedState : IDisposable

61
src/Avalonia.Visuals/Media/FontFamily.cs

@ -2,17 +2,17 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.Fonts;
namespace Avalonia.Media
{
public sealed class FontFamily
{
public const string DefaultFontFamilyName = "$Default";
static FontFamily()
{
Default = new FontFamily(FontManager.Default.DefaultFontFamilyName);
Default = new FontFamily(DefaultFontFamilyName);
}
/// <inheritdoc />
@ -57,15 +57,6 @@ namespace Avalonia.Media
/// </summary>
public static FontFamily Default { get; }
/// <summary>
/// Represents all font families in the system. This can be an expensive call depending on platform implementation.
/// </summary>
/// <remarks>
/// Consider using the new <see cref="FontManager"/> instead.
/// </remarks>
public static IEnumerable<FontFamily> SystemFontFamilies =>
FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
/// <summary>
/// Gets the primary family name of the font family.
/// </summary>
@ -86,10 +77,16 @@ namespace Avalonia.Media
/// Gets the key for associated assets.
/// </summary>
/// <value>
/// The family familyNames.
/// The family key.
/// </value>
/// <remarks>Key is only used for custom fonts.</remarks>
public FontFamilyKey Key { get; }
/// <summary>
/// Returns <c>True</c> if this instance is the system's default.
/// </summary>
public bool IsDefault => Name.Equals(DefaultFontFamilyName);
/// <summary>
/// Implicit conversion of string to FontFamily
/// </summary>
@ -184,36 +181,40 @@ namespace Avalonia.Media
{
unchecked
{
var hash = (int)2186146271;
if (Key != null)
{
hash = (hash * 15768619) ^ Key.GetHashCode();
}
else
{
hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
}
return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0);
}
}
if (Key != null)
{
hash = (hash * 15768619) ^ Key.GetHashCode();
}
public static bool operator !=(FontFamily a, FontFamily b)
{
return !(a == b);
}
return hash;
public static bool operator ==(FontFamily a, FontFamily b)
{
if (ReferenceEquals(a, b))
{
return true;
}
return !(a is null) && a.Equals(b);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}
if (!(obj is FontFamily other))
{
return false;
}
if (Key != null)
if (!Equals(Key, other.Key))
{
return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key);
return false;
}
return other.FamilyNames.Equals(FamilyNames);

153
src/Avalonia.Visuals/Media/FontManager.cs

@ -1,8 +1,10 @@
// 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
namespace Avalonia.Media
@ -11,9 +13,53 @@ namespace Avalonia.Media
/// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts.
/// It is also responsible for the font fallback.
/// </summary>
public abstract class FontManager
public sealed class FontManager
{
public static readonly FontManager Default = CreateDefault();
private readonly ConcurrentDictionary<FontKey, Typeface> _typefaceCache =
new ConcurrentDictionary<FontKey, Typeface>();
private readonly FontFamily _defaultFontFamily;
private FontManager(IFontManagerImpl platformImpl)
{
PlatformImpl = platformImpl;
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
_defaultFontFamily = new FontFamily(DefaultFontFamilyName);
}
public static FontManager Current
{
get
{
var current = AvaloniaLocator.Current.GetService<FontManager>();
if (current != null)
{
return current;
}
var renderInterface = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var fontManagerImpl = renderInterface?.CreateFontManager();
if (fontManagerImpl == null)
{
return null;
}
current = new FontManager(fontManagerImpl);
AvaloniaLocator.CurrentMutable.Bind<FontManager>().ToConstant(current);
return current;
}
}
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Gets the system's default font family's name.
@ -21,25 +67,55 @@ namespace Avalonia.Media
public string DefaultFontFamilyName
{
get;
protected set;
}
/// <summary>
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// Get all installed font family names.
/// </summary>
public abstract IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
/// <summary>
/// Get a cached typeface from specified parameters.
/// Returns a new typeface, or an existing one if a matching typeface exists.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <returns>
/// The cached typeface.
/// The typeface.
/// </returns>
public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal)
{
while (true)
{
if (fontFamily.IsDefault)
{
fontFamily = _defaultFontFamily;
}
var key = new FontKey(fontFamily, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface))
{
return typeface;
}
typeface = new Typeface(fontFamily, fontWeight, fontStyle);
if (_typefaceCache.TryAdd(key, typeface))
{
return typeface;
}
if (fontFamily == _defaultFontFamily)
{
return null;
}
fontFamily = _defaultFontFamily;
}
}
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
@ -53,60 +129,13 @@ namespace Avalonia.Media
/// <returns>
/// The matched typeface.
/// </returns>
public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
public static FontManager CreateDefault()
{
var platformImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
if (platformImpl != null)
{
return new PlatformFontManager(platformImpl);
}
return new EmptyFontManager();
}
private class PlatformFontManager : FontManager
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal,
FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null)
{
private readonly IFontManagerImpl _platformImpl;
public PlatformFontManager(IFontManagerImpl platformImpl)
{
_platformImpl = platformImpl;
DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName;
}
public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
_platformImpl.GetInstalledFontFamilyNames(checkForUpdates);
public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) =>
_platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle);
public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null) =>
_platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture);
}
private class EmptyFontManager : FontManager
{
public EmptyFontManager()
{
DefaultFontFamilyName = "Empty";
}
public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
new[] { DefaultFontFamilyName };
public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle);
public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null) => null;
return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
_typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) :
null;
}
}
}

72
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
@ -54,25 +55,19 @@ namespace Avalonia.Media.Fonts
/// </value>
internal IReadOnlyList<string> Names { get; }
/// <inheritdoc />
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// Returns an enumerator for the name collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<string> GetEnumerator()
public ImmutableReadOnlyListStructEnumerator<string> GetEnumerator()
{
return Names.GetEnumerator();
return new ImmutableReadOnlyListStructEnumerator<string>(this);
}
IEnumerator<string> IEnumerable<string>.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc />
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
@ -111,7 +106,39 @@ namespace Avalonia.Media.Fonts
/// </returns>
public override int GetHashCode()
{
return ToString().GetHashCode();
if (Count == 0)
{
return 0;
}
unchecked
{
int hash = 17;
for (var i = 0; i < Names.Count; i++)
{
string name = Names[i];
hash = hash * 23 + name.GetHashCode();
}
return hash;
}
}
public static bool operator !=(FamilyNameCollection a, FamilyNameCollection b)
{
return !(a == b);
}
public static bool operator ==(FamilyNameCollection a, FamilyNameCollection b)
{
if (ReferenceEquals(a, b))
{
return true;
}
return !(a is null) && a.Equals(b);
}
/// <summary>
@ -128,7 +155,20 @@ namespace Avalonia.Media.Fonts
return false;
}
return other.ToString().Equals(ToString());
if (other.Count != Count)
{
return false;
}
for (int i = 0; i < Count; i++)
{
if (Names[i] != other.Names[i])
{
return false;
}
}
return true;
}
public int Count => Names.Count;

15
src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs

@ -58,6 +58,21 @@ namespace Avalonia.Media.Fonts
}
}
public static bool operator !=(FontFamilyKey a, FontFamilyKey b)
{
return !(a == b);
}
public static bool operator ==(FontFamilyKey a, FontFamilyKey b)
{
if (ReferenceEquals(a, b))
{
return true;
}
return !(a is null) && a.Equals(b);
}
/// <summary>
/// Determines whether the specified <see cref="object" />, is equal to this instance.
/// </summary>

15
src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs

@ -10,13 +10,6 @@ namespace Avalonia.Media.Fonts
{
public static class FontFamilyLoader
{
private static readonly IAssetLoader s_assetLoader;
static FontFamilyLoader()
{
s_assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
}
/// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary>
@ -42,7 +35,9 @@ namespace Avalonia.Media.Fonts
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
{
var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var matchingAssets =
availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf"));
@ -58,9 +53,11 @@ namespace Avalonia.Media.Fonts
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
{
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location);
var availableResources = s_assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
string compareTo;

15
src/Skia/Avalonia.Skia/FontKey.cs → src/Avalonia.Visuals/Media/Fonts/FontKey.cs

@ -2,24 +2,26 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
namespace Avalonia.Skia
namespace Avalonia.Media.Fonts
{
internal readonly struct FontKey : IEquatable<FontKey>
public readonly struct FontKey : IEquatable<FontKey>
{
public readonly FontFamily FontFamily;
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontWeight weight, FontStyle style)
public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
{
FontFamily = fontFamily;
Style = style;
Weight = weight;
}
public override int GetHashCode()
{
var hash = 17;
var hash = FontFamily.GetHashCode();
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
@ -33,7 +35,8 @@ namespace Avalonia.Skia
public bool Equals(FontKey other)
{
return Style == other.Style &&
return FontFamily == other.FontFamily &&
Style == other.Style &&
Weight == other.Weight;
}
}

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

@ -0,0 +1,457 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using Avalonia.Platform;
using Avalonia.Utility;
namespace Avalonia.Media
{
/// <summary>
/// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style.
/// </summary>
public sealed class GlyphRun : IDisposable
{
private static readonly IPlatformRenderInterface s_platformRenderInterface =
AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
private IGlyphRunImpl _glyphRunImpl;
private GlyphTypeface _glyphTypeface;
private double _fontRenderingEmSize;
private Rect? _bounds;
private ReadOnlySlice<ushort> _glyphIndices;
private ReadOnlySlice<double> _glyphAdvances;
private ReadOnlySlice<Vector> _glyphOffsets;
private ReadOnlySlice<ushort> _glyphClusters;
private ReadOnlySlice<char> _characters;
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class.
/// </summary>
public GlyphRun()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="glyphAdvances">The glyph advances.</param>
/// <param name="glyphOffsets">The glyph offsets.</param>
/// <param name="characters">The characters.</param>
/// <param name="glyphClusters">The glyph clusters.</param>
/// <param name="bidiLevel">The bidi level.</param>
/// <param name="bounds">The bound.</param>
public GlyphRun(
GlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlySlice<ushort> glyphIndices,
ReadOnlySlice<double> glyphAdvances = default,
ReadOnlySlice<Vector> glyphOffsets = default,
ReadOnlySlice<char> characters = default,
ReadOnlySlice<ushort> glyphClusters = default,
int bidiLevel = 0,
Rect? bounds = null)
{
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
GlyphIndices = glyphIndices;
GlyphAdvances = glyphAdvances;
GlyphOffsets = glyphOffsets;
Characters = characters;
GlyphClusters = glyphClusters;
BidiLevel = bidiLevel;
Initialize(bounds);
}
/// <summary>
/// Gets or sets the <see cref="Media.GlyphTypeface"/> for the <see cref="GlyphRun"/>.
/// </summary>
public GlyphTypeface GlyphTypeface
{
get => _glyphTypeface;
set => Set(ref _glyphTypeface, value);
}
/// <summary>
/// Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
/// </summary>
public double FontRenderingEmSize
{
get => _fontRenderingEmSize;
set => Set(ref _fontRenderingEmSize, value);
}
/// <summary>
/// Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
/// </summary>
public ReadOnlySlice<ushort> GlyphIndices
{
get => _glyphIndices;
set => Set(ref _glyphIndices, value);
}
/// <summary>
/// Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices.
/// </summary>
public ReadOnlySlice<double> GlyphAdvances
{
get => _glyphAdvances;
set => Set(ref _glyphAdvances, value);
}
/// <summary>
/// Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>.
/// </summary>
public ReadOnlySlice<Vector> GlyphOffsets
{
get => _glyphOffsets;
set => Set(ref _glyphOffsets, value);
}
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public ReadOnlySlice<char> Characters
{
get => _characters;
set => Set(ref _characters, value);
}
/// <summary>
/// Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index.
/// </summary>
public ReadOnlySlice<ushort> GlyphClusters
{
get => _glyphClusters;
set => Set(ref _glyphClusters, value);
}
/// <summary>
/// Gets or sets the bidirectional nesting level of the <see cref="GlyphRun"/>.
/// </summary>
public int BidiLevel
{
get;
set;
}
/// <summary>
///
/// </summary>
internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
/// <summary>
///
/// </summary>
internal bool IsLeftToRight => ((BidiLevel & 1) == 0);
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Rect Bounds
{
get
{
if (_bounds == null)
{
_bounds = CalculateBounds();
}
return _bounds.Value;
}
set => _bounds = value;
}
public IGlyphRunImpl GlyphRunImpl
{
get
{
if (_glyphRunImpl == null)
{
Initialize(null);
}
return _glyphRunImpl;
}
}
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var distance = 0.0;
var end = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
for (var i = 0; i < _glyphClusters.Length; i++)
{
if (_glyphClusters[i] >= end)
{
break;
}
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[i];
distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
distance += GlyphAdvances[i];
}
}
return distance;
}
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
// Before
if (distance < 0)
{
isInside = false;
var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _);
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
}
//After
if (distance > Bounds.Size.Width)
{
isInside = false;
var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
}
//Within
var currentX = 0.0;
var index = 0;
for (; index < GlyphIndices.Length; index++)
{
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[index];
currentX += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
currentX += GlyphAdvances[index];
}
if (currentX > distance)
{
break;
}
}
if (index == GlyphIndices.Length)
{
index--;
}
var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width);
isInside = distance < currentX && width > 0;
var isTrailing = distance > currentX - width / 2;
return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
}
public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
if (characterHit.TrailingLength == 0)
{
return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
}
var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
}
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{
return characterHit.TrailingLength == 0 ?
FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _) :
new CharacterHit(characterHit.FirstCharacterIndex);
}
private class ReverseComparer<T> : IComparer<T>
{
public int Compare(T x, T y)
{
return Comparer<T>.Default.Compare(y, x);
}
}
private static readonly IComparer<ushort> s_ascendingComparer = Comparer<ushort>.Default;
private static readonly IComparer<ushort> s_descendingComparer = new ReverseComparer<ushort>();
internal CharacterHit FindNearestCharacterHit(int index, out double width)
{
width = 0.0;
if (index < 0)
{
return default;
}
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
var clusters = _glyphClusters.AsSpan();
int start;
if (index == 0 && clusters[0] == 0)
{
start = 0;
}
else
{
// Find the start of the cluster at the character index.
start = clusters.BinarySearch((ushort)index, comparer);
}
// No cluster found.
if (start < 0)
{
while (index > 0 && start < 0)
{
index--;
start = clusters.BinarySearch((ushort)index, comparer);
}
if (start < 0)
{
return default;
}
}
var trailingLength = 0;
var currentCluster = clusters[start];
while (start > 0 && clusters[start - 1] == currentCluster)
{
start--;
}
for (var lastIndex = start; lastIndex < _glyphClusters.Length; ++lastIndex)
{
if (_glyphClusters[lastIndex] != currentCluster)
{
break;
}
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[lastIndex];
width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
width += GlyphAdvances[lastIndex];
}
trailingLength++;
}
return new CharacterHit(currentCluster, trailingLength);
}
private Rect CalculateBounds()
{
var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale;
var width = 0.0;
if (GlyphAdvances.IsEmpty)
{
foreach (var glyph in GlyphIndices)
{
width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
}
else
{
foreach (var advance in GlyphAdvances)
{
width += advance;
}
}
return new Rect(0, 0, width, height);
}
private void Set<T>(ref T field, T value)
{
if (_glyphRunImpl != null)
{
throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'");
}
field = value;
}
private void Initialize(Rect? bounds)
{
if (GlyphIndices.Length == 0)
{
throw new InvalidOperationException();
}
var glyphCount = GlyphIndices.Length;
if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount)
{
throw new InvalidOperationException();
}
if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount)
{
throw new InvalidOperationException();
}
_glyphRunImpl = s_platformRenderInterface.CreateGlyphRun(this, out var width);
if (bounds.HasValue)
{
_bounds = bounds;
}
else
{
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
_bounds = new Rect(0, 0, width, height);
}
}
void IDisposable.Dispose()
{
_glyphRunImpl?.Dispose();
}
}
}

50
src/Avalonia.Visuals/Media/GlyphRunDrawing.cs

@ -0,0 +1,50 @@
// 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.
namespace Avalonia.Media
{
public class GlyphRunDrawing : Drawing
{
public static readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<GlyphRunDrawing, IBrush>(nameof(Foreground));
public static readonly StyledProperty<GlyphRun> GlyphRunProperty =
AvaloniaProperty.Register<GlyphRunDrawing, GlyphRun>(nameof(GlyphRun));
public static readonly StyledProperty<Point> BaselineOriginProperty =
AvaloniaProperty.Register<GlyphRunDrawing, Point>(nameof(BaselineOrigin));
public IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public GlyphRun GlyphRun
{
get => GetValue(GlyphRunProperty);
set => SetValue(GlyphRunProperty, value);
}
public Point BaselineOrigin
{
get => GetValue(BaselineOriginProperty);
set => SetValue(BaselineOriginProperty, value);
}
public override void Draw(DrawingContext context)
{
if (GlyphRun == null)
{
return;
}
context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin);
}
public override Rect GetBounds()
{
return GlyphRun?.Bounds ?? default;
}
}
}

11
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@ -9,11 +9,9 @@ namespace Avalonia.Media
{
public sealed class GlyphTypeface : IDisposable
{
private static readonly IPlatformRenderInterface s_platformRenderInterface =
AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface))
public GlyphTypeface(Typeface typeface)
{
PlatformImpl = FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface);
}
public GlyphTypeface(IGlyphTypefaceImpl platformImpl)
@ -68,6 +66,11 @@ namespace Avalonia.Media
/// </summary>
public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
/// <summary>
/// A <see cref="bool"/> value indicating whether all glyphs in the font have the same advancement.
/// </summary>
public bool IsFixedPitch => PlatformImpl.IsFixedPitch;
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>

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

@ -13,8 +13,6 @@ namespace Avalonia.Media
[DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")]
public class Typeface : IEquatable<Typeface>
{
public static readonly Typeface Default = new Typeface(FontFamily.Default);
private GlyphTypeface _glyphTypeface;
/// <summary>
@ -50,6 +48,8 @@ namespace Avalonia.Media
{
}
public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default);
/// <summary>
/// Gets the font family.
/// </summary>

28
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -61,12 +61,22 @@ namespace Avalonia.Platform
void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry);
/// <summary>
/// Draws the outline of a rectangle.
/// Draws a rectangle with the specified Brush and Pen.
/// </summary>
/// <param name="pen">The pen.</param>
/// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f);
/// <param name="radiusX">The radius in the X dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Width/2
/// </param>
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
/// </param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0);
/// <summary>
/// Draws text.
@ -77,12 +87,12 @@ namespace Avalonia.Platform
void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text);
/// <summary>
/// Draws a filled rectangle.
/// Draws a glyph run.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f);
/// <param name="foreground">The foreground.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin);
/// <summary>
/// Creates a new <see cref="IRenderTargetBitmapImpl"/> that can be used as a render layer

30
src/Avalonia.Visuals/Platform/IFontManagerImpl.cs

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
namespace Avalonia.Platform
{
@ -12,7 +13,7 @@ namespace Avalonia.Platform
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
string DefaultFontFamilyName { get; }
string GetDefaultFontFamilyName();
/// <summary>
/// Get all installed fonts in the system.
@ -20,17 +21,6 @@ namespace Avalonia.Platform
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Get a typeface from specified parameters.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <returns>
/// The typeface.
/// </returns>
Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
/// </summary>
@ -39,10 +29,20 @@ namespace Avalonia.Platform
/// <param name="fontStyle">The font style.</param>
/// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param>
/// <param name="fontKey">The matching font key.</param>
/// <returns>
/// The typeface.
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey);
/// <summary>
/// Creates a glyph typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>0
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
/// </returns>
Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
}
}

12
src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs

@ -0,0 +1,12 @@
// 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;
namespace Avalonia.Platform
{
/// <summary>
/// Actual implementation of a glyph run that stores platform dependent resources.
/// </summary>
public interface IGlyphRunImpl : IDisposable { }
}

5
src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs

@ -47,6 +47,11 @@ namespace Avalonia.Platform
/// </summary>
int StrikethroughThickness { get; }
/// <summary>
/// A <see cref="bool"/> value indicating whether all glyphs in the font have the same advancement.
/// </summary>
bool IsFixedPitch { get; }
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>

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

@ -113,12 +113,17 @@ namespace Avalonia.Platform
IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride);
/// <summary>
/// Creates a glyph typeface for specified typeface.
/// Creates a font manager implementation.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>
/// The glyph typeface implementation.
/// </returns>
IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
/// <returns>The font manager.</returns>
IFontManagerImpl CreateFontManager();
/// <summary>
/// Creates a platform implementation of a glyph run.
/// </summary>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="width">The glyph run's width.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
}
}

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

@ -445,7 +445,7 @@ namespace Avalonia.Rendering
foreach (var r in _dirtyRectsDisplay)
{
var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity);
context.FillRectangle(brush, r.Rect);
context.DrawRectangle(brush,null, r.Rect);
}
}

5
src/Avalonia.Visuals/Rendering/RendererBase.cs

@ -7,7 +7,6 @@ namespace Avalonia.Rendering
{
public class RendererBase
{
private static readonly Typeface s_fpsTypeface = new Typeface("Arial");
private static int s_fontSize = 18;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond;
@ -19,7 +18,7 @@ namespace Avalonia.Rendering
{
_fpsText = new FormattedText
{
Typeface = s_fpsTypeface,
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default),
FontSize = s_fontSize
};
}
@ -51,7 +50,7 @@ namespace Avalonia.Rendering
var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
context.Transform = Matrix.Identity;
context.FillRectangle(Brushes.Black, rect);
context.DrawRectangle(Brushes.Black,null, rect);
context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl);
}
}

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

@ -152,13 +152,13 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0)
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, null, pen, rect, cornerRadius))
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, radiusX, radiusY))
{
Add(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush)));
Add(new RectangleNode(Transform, brush, pen, rect, radiusX, radiusY, CreateChildScene(brush)));
}
else
{
@ -191,20 +191,20 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
var next = NextDrawAs<RectangleNode>();
var next = NextDrawAs<GlyphRunNode>();
if (next == null || !next.Item.Equals(Transform, brush, null, rect, cornerRadius))
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
{
Add(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush)));
Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground)));
}
else
{
++_drawOperationindex;
}
}
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
throw new NotSupportedException("Creating layers on a deferred drawing context not supported");

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

@ -0,0 +1,91 @@
// 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.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a glyph run draw.
/// </summary>
internal class GlyphRunNode : BrushDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRunNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run to draw.</param>
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public GlyphRunNode(
Matrix transform,
IBrush foreground,
GlyphRun glyphRun,
Point baselineOrigin,
IDictionary<IVisual, Scene> childScenes = null)
: base(glyphRun.Bounds, transform, null)
{
Transform = transform;
Foreground = foreground?.ToImmutable();
GlyphRun = glyphRun;
BaselineOrigin = baselineOrigin;
ChildScenes = childScenes;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the foreground brush.
/// </summary>
public IBrush Foreground { get; }
/// <summary>
/// Gets the glyph run to draw.
/// </summary>
public GlyphRun GlyphRun { get; }
/// <summary>
/// Gets the baseline origin.
/// </summary>
public Point BaselineOrigin { get; set; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin);
}
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="foreground">The foreground of the other draw operation.</param>
/// <param name="glyphRun">The glyph run of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun)
{
return transform == Transform &&
Equals(foreground, Foreground) &&
Equals(glyphRun, GlyphRun);
}
/// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.Contains(p);
}
}

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

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@ -21,14 +22,16 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="rect">The rectangle to draw.</param>
/// <param name="cornerRadius">The rectangle corner radius.</param>
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.</param>
/// <param name="radiusX">The radius in the X dimension of the rounded corners.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public RectangleNode(
Matrix transform,
IBrush brush,
IPen pen,
Rect rect,
float cornerRadius,
double radiusX,
double radiusY,
IDictionary<IVisual, Scene> childScenes = null)
: base(rect, transform, pen)
{
@ -36,7 +39,8 @@ namespace Avalonia.Rendering.SceneGraph
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
CornerRadius = cornerRadius;
RadiusX = radiusX;
RadiusY = radiusY;
ChildScenes = childScenes;
}
@ -61,9 +65,14 @@ namespace Avalonia.Rendering.SceneGraph
public Rect Rect { get; }
/// <summary>
/// Gets the rectangle corner radius.
/// The radius in the X dimension of the rounded corners.
/// </summary>
public float CornerRadius { get; }
public double RadiusX { get; }
/// <summary>
/// The radius in the Y dimension of the rounded corners.
/// </summary>
public double RadiusY { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
@ -75,19 +84,21 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill of the other draw operation.</param>
/// <param name="pen">The stroke of the other draw operation.</param>
/// <param name="rect">The rectangle of the other draw operation.</param>
/// <param name="cornerRadius">The rectangle corner radius of the other draw operation.</param>
/// <param name="radiusX"></param>
/// <param name="radiusY"></param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, float cornerRadius)
public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY)
{
return transform == Transform &&
Equals(brush, Brush) &&
Equals(Pen, pen) &&
rect == Rect &&
cornerRadius == CornerRadius;
Equals(brush, Brush) &&
Equals(Pen, pen) &&
rect == Rect &&
Math.Abs(radiusX - RadiusX) < double.Epsilon &&
Math.Abs(radiusY - RadiusY) < double.Epsilon;
}
/// <inheritdoc/>
@ -95,15 +106,7 @@ namespace Avalonia.Rendering.SceneGraph
{
context.Transform = Transform;
if (Brush != null)
{
context.FillRectangle(Brush, Rect, CornerRadius);
}
if (Pen != null)
{
context.DrawRectangle(Pen, Rect, CornerRadius);
}
context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY);
}
/// <inheritdoc/>

154
src/Avalonia.Visuals/Utility/ReadOnlySlice.cs

@ -0,0 +1,154 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Utility
{
/// <summary>
/// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
/// </summary>
/// <typeparam name="T">The type of elements in the slice.</typeparam>
public readonly struct ReadOnlySlice<T> : IReadOnlyList<T>
{
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length)
{
Buffer = buffer;
Start = start;
Length = length;
}
/// <summary>
/// Gets the start.
/// </summary>
/// <value>
/// The start.
/// </value>
public int Start { get; }
/// <summary>
/// Gets the end.
/// </summary>
/// <value>
/// The end.
/// </value>
public int End => Start + Length - 1;
/// <summary>
/// Gets the length.
/// </summary>
/// <value>
/// The length.
/// </value>
public int Length { get; }
/// <summary>
/// Gets a value that indicates whether this instance of <see cref="ReadOnlySpan{T}"/> is Empty.
/// </summary>
public bool IsEmpty => Length == 0;
/// <summary>
/// The buffer.
/// </summary>
public ReadOnlyMemory<T> Buffer { get; }
public T this[int index] => Buffer.Span[Start + index];
/// <summary>
/// Returns a span of the underlying buffer.
/// </summary>
/// <returns>The <see cref="ReadOnlySpan{T}"/> of the underlying buffer.</returns>
public ReadOnlySpan<T> AsSpan()
{
return Buffer.Span.Slice(Start, Length);
}
/// <summary>
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
/// </summary>
/// <param name="start">The start of the sub slice.</param>
/// <param name="length">The length of the sub slice.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
public ReadOnlySlice<T> AsSlice(int start, int length)
{
if (start < 0 || start >= Length)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
if (Start + start > End)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(Buffer, Start + start, length);
}
/// <summary>
/// Returns a specified number of contiguous elements from the start of the slice.
/// </summary>
/// <param name="length">The number of elements to return.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
public ReadOnlySlice<T> Take(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(Buffer, Start, length);
}
/// <summary>
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
/// </summary>
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
public ReadOnlySlice<T> Skip(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(Buffer, Start + length, Length - length);
}
/// <summary>
/// Returns an enumerator for the slice.
/// </summary>
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<T>(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
int IReadOnlyCollection<T>.Count => Length;
T IReadOnlyList<T>.this[int index] => this[index];
public static implicit operator ReadOnlySlice<T>(T[] array)
{
return new ReadOnlySlice<T>(array);
}
public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory)
{
return new ReadOnlySlice<T>(memory);
}
}
}

22
src/Avalonia.Visuals/Visual.cs

@ -4,7 +4,6 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Logging;
@ -121,7 +120,7 @@ namespace Avalonia
{
var visualChildren = new AvaloniaList<IVisual>();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.Validate = ValidateVisualChild;
visualChildren.Validate = visual => ValidateVisualChild(visual);
visualChildren.CollectionChanged += VisualChildrenChanged;
VisualChildren = visualChildren;
}
@ -173,7 +172,22 @@ namespace Avalonia
/// </summary>
public bool IsEffectivelyVisible
{
get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); }
get
{
IVisual node = this;
while (node != null)
{
if (!node.IsVisible)
{
return false;
}
node = node.VisualParent;
}
return true;
}
}
/// <summary>
@ -552,7 +566,7 @@ namespace Avalonia
if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true)
{
var root = this.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
var root = this.FindAncestorOfType<IRenderRoot>();
var e = new VisualTreeAttachmentEventArgs(_visualParent, root);
OnAttachedToVisualTreeCore(e);
}

155
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@ -14,7 +14,7 @@ namespace Avalonia.VisualTree
public static class VisualExtensions
{
/// <summary>
/// Calculates the distance from a visual's <see cref="IRenderRoot"/>.
/// Calculates the distance from a visual's ancestor.
/// </summary>
/// <param name="visual">The visual.</param>
/// <param name="ancestor">The ancestor visual.</param>
@ -30,13 +30,39 @@ namespace Avalonia.VisualTree
while (visual != null && visual != ancestor)
{
++result;
visual = visual.VisualParent;
result++;
}
return visual != null ? result : -1;
}
/// <summary>
/// Calculates the distance from a visual's root.
/// </summary>
/// <param name="visual">The visual.</param>
/// <returns>
/// The number of steps from the visual to the root.
/// </returns>
public static int CalculateDistanceFromRoot(IVisual visual)
{
Contract.Requires<ArgumentNullException>(visual != null);
var result = 0;
visual = visual?.VisualParent;
while (visual != null)
{
visual = visual.VisualParent;
result++;
}
return result;
}
/// <summary>
/// Tries to get the first common ancestor of two visuals.
/// </summary>
@ -47,8 +73,53 @@ namespace Avalonia.VisualTree
{
Contract.Requires<ArgumentNullException>(visual != null);
return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors())
.FirstOrDefault();
if (target is null)
{
return null;
}
void GoUpwards(ref IVisual node, int count)
{
for (int i = 0; i < count; ++i)
{
node = node.VisualParent;
}
}
// We want to find lowest node first, then make sure that both nodes are at the same height.
// By doing that we can sometimes find out that other node is our lowest common ancestor.
var firstHeight = CalculateDistanceFromRoot(visual);
var secondHeight = CalculateDistanceFromRoot(target);
if (firstHeight > secondHeight)
{
GoUpwards(ref visual, firstHeight - secondHeight);
}
else
{
GoUpwards(ref target, secondHeight - firstHeight);
}
if (visual == target)
{
return visual;
}
while (visual != null && target != null)
{
IVisual firstParent = visual.VisualParent;
IVisual secondParent = target.VisualParent;
if (firstParent == secondParent)
{
return firstParent;
}
visual = visual.VisualParent;
target = target.VisualParent;
}
return null;
}
/// <summary>
@ -69,6 +140,57 @@ namespace Avalonia.VisualTree
}
}
/// <summary>
/// Finds first ancestor of given type.
/// </summary>
/// <typeparam name="T">Ancestor type.</typeparam>
/// <param name="visual">The visual.</param>
/// <param name="includeSelf">If given visual should be included in search.</param>
/// <returns>First ancestor of given type.</returns>
public static T FindAncestorOfType<T>(this IVisual visual, bool includeSelf = false) where T : class
{
if (visual is null)
{
return null;
}
IVisual parent = includeSelf ? visual : visual.VisualParent;
while (parent != null)
{
if (parent is T result)
{
return result;
}
parent = parent.VisualParent;
}
return null;
}
/// <summary>
/// Finds first descendant of given type.
/// </summary>
/// <typeparam name="T">Descendant type.</typeparam>
/// <param name="visual">The visual.</param>
/// <param name="includeSelf">If given visual should be included in search.</param>
/// <returns>First descendant of given type.</returns>
public static T FindDescendantOfType<T>(this IVisual visual, bool includeSelf = false) where T : class
{
if (visual is null)
{
return null;
}
if (includeSelf && visual is T result)
{
return result;
}
return FindDescendantOfTypeCore<T>(visual);
}
/// <summary>
/// Enumerates an <see cref="IVisual"/> and its ancestors in the visual tree.
/// </summary>
@ -249,6 +371,31 @@ namespace Avalonia.VisualTree
.Select(x => x.Element);
}
private static T FindDescendantOfTypeCore<T>(IVisual visual) where T : class
{
var visualChildren = visual.VisualChildren;
var visualChildrenCount = visualChildren.Count;
for (var i = 0; i < visualChildrenCount; i++)
{
IVisual child = visualChildren[i];
if (child is T result)
{
return result;
}
var childResult = FindDescendantOfTypeCore<T>(child);
if (!(childResult is null))
{
return childResult;
}
}
return null;
}
private class ZOrderElement : IComparable<ZOrderElement>
{
public IVisual Element { get; set; }

2
src/Avalonia.X11/X11Clipboard.cs

@ -79,7 +79,7 @@ namespace Avalonia.X11
atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE})
.ToArray();
XChangeProperty(_x11.Display, window, property,
target, 32, PropertyMode.Replace, atoms, atoms.Length);
_x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
return property;
}
else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)

36
src/Avalonia.X11/X11Window.cs

@ -351,10 +351,17 @@ namespace Avalonia.X11
{
if (ActivateTransientChildIfNeeded())
return;
if (ev.ButtonEvent.button < 4)
MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonDown
: ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonDown
: RawPointerEventType.RightButtonDown, ref ev, ev.ButtonEvent.state);
if (ev.ButtonEvent.button < 4 || ev.ButtonEvent.button == 8 || ev.ButtonEvent.button == 9)
MouseEvent(
ev.ButtonEvent.button switch
{
1 => RawPointerEventType.LeftButtonDown,
2 => RawPointerEventType.MiddleButtonDown,
3 => RawPointerEventType.RightButtonDown,
8 => RawPointerEventType.XButton1Down,
9 => RawPointerEventType.XButton2Down
},
ref ev, ev.ButtonEvent.state);
else
{
var delta = ev.ButtonEvent.button == 4
@ -372,10 +379,17 @@ namespace Avalonia.X11
}
else if (ev.type == XEventName.ButtonRelease)
{
if (ev.ButtonEvent.button < 4)
MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonUp
: ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonUp
: RawPointerEventType.RightButtonUp, ref ev, ev.ButtonEvent.state);
if (ev.ButtonEvent.button < 4 || ev.ButtonEvent.button == 8 || ev.ButtonEvent.button == 9)
MouseEvent(
ev.ButtonEvent.button switch
{
1 => RawPointerEventType.LeftButtonUp,
2 => RawPointerEventType.MiddleButtonUp,
3 => RawPointerEventType.RightButtonUp,
8 => RawPointerEventType.XButton1Up,
9 => RawPointerEventType.XButton2Up
},
ref ev, ev.ButtonEvent.state);
}
else if (ev.type == XEventName.ConfigureNotify)
{
@ -586,8 +600,12 @@ namespace Avalonia.X11
rv |= RawInputModifiers.LeftMouseButton;
if (state.HasFlag(XModifierMask.Button2Mask))
rv |= RawInputModifiers.RightMouseButton;
if (state.HasFlag(XModifierMask.Button2Mask))
if (state.HasFlag(XModifierMask.Button3Mask))
rv |= RawInputModifiers.MiddleMouseButton;
if (state.HasFlag(XModifierMask.Button4Mask))
rv |= RawInputModifiers.XButton1MouseButton;
if (state.HasFlag(XModifierMask.Button5Mask))
rv |= RawInputModifiers.XButton2MouseButton;
if (state.HasFlag(XModifierMask.ShiftMask))
rv |= RawInputModifiers.Shift;
if (state.HasFlag(XModifierMask.ControlMask))

20
src/Avalonia.X11/XI2Manager.cs

@ -240,11 +240,15 @@ namespace Avalonia.X11
if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease)
{
var down = ev.Type == XiEventType.XI_ButtonPress;
var type =
ev.Button == 1 ? (down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp)
: ev.Button == 2 ? (down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp)
: ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp)
: (RawPointerEventType?)null;
var type = ev.Button switch
{
1 => down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp,
2 => down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp,
3 => down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp,
8 => down ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton1Up,
9 => down ? RawPointerEventType.XButton2Down : RawPointerEventType.XButton2Up,
_ => (RawPointerEventType?)null
};
if (type.HasValue)
client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers));
@ -283,12 +287,14 @@ namespace Avalonia.X11
var buttons = ev->buttons.Mask;
if (XIMaskIsSet(buttons, 1))
Modifiers |= RawInputModifiers.LeftMouseButton;
if (XIMaskIsSet(buttons, 2))
Modifiers |= RawInputModifiers.MiddleMouseButton;
if (XIMaskIsSet(buttons, 3))
Modifiers |= RawInputModifiers.RightMouseButton;
if (XIMaskIsSet(buttons, 8))
Modifiers |= RawInputModifiers.XButton1MouseButton;
if (XIMaskIsSet(buttons, 9))
Modifiers |= RawInputModifiers.XButton2MouseButton;
}
Valuators = new Dictionary<int, double>();

7
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -52,6 +52,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
// the context.
object anchor = context.GetFirstParent<IControl>();
if(anchor is null)
{
// Try to find IDataContextProvider, this was added to allow us to find
// a datacontext for Application class when using NativeMenuItems.
anchor = context.GetFirstParent<IDataContextProvider>();
}
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
return anchor ??

11
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -26,6 +26,7 @@ namespace Avalonia.Data
public Binding()
{
FallbackValue = AvaloniaProperty.UnsetValue;
TargetNullValue = AvaloniaProperty.UnsetValue;
}
/// <summary>
@ -60,6 +61,11 @@ namespace Avalonia.Data
/// </summary>
public object FallbackValue { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding result is null.
/// </summary>
public object TargetNullValue { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
@ -209,6 +215,7 @@ namespace Avalonia.Data
observer,
targetType,
fallback,
TargetNullValue,
converter ?? DefaultValueConverter.Instance,
ConverterParameter,
Priority);
@ -224,9 +231,9 @@ namespace Avalonia.Data
{
Contract.Requires<ArgumentNullException>(target != null);
if (!(target is IStyledElement))
if (!(target is IDataContextProvider))
{
target = anchor as IStyledElement;
target = anchor as IDataContextProvider;
if (target == null)
{

16
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -37,6 +37,11 @@ namespace Avalonia.Data
/// </summary>
public object FallbackValue { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding result is null.
/// </summary>
public object TargetNullValue { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
@ -57,6 +62,12 @@ namespace Avalonia.Data
/// </summary>
public string StringFormat { get; set; }
public MultiBinding()
{
FallbackValue = AvaloniaProperty.UnsetValue;
TargetNullValue = AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
public InstancedBinding Initiate(
IAvaloniaObject target,
@ -102,6 +113,11 @@ namespace Avalonia.Data
var culture = CultureInfo.CurrentCulture;
var converted = converter.Convert(values, targetType, ConverterParameter, culture);
if (converted == null)
{
converted = TargetNullValue;
}
if (converted == AvaloniaProperty.UnsetValue)
{
converted = FallbackValue;

63
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -184,37 +184,40 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0)
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY)
{
using (var paint = CreatePaint(pen, rect.Size))
{
var rc = rect.ToSKRect();
var rc = rect.ToSKRect();
var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
if (Math.Abs(cornerRadius) < float.Epsilon)
{
Canvas.DrawRect(rc, paint.Paint);
}
else
if (brush != null)
{
using (var paint = CreatePaint(brush, rect.Size))
{
Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
if (isRounded)
{
Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint);
}
else
{
Canvas.DrawRect(rc, paint.Paint);
}
}
}
}
/// <inheritdoc />
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
{
using (var paint = CreatePaint(brush, rect.Size))
if (pen?.Brush != null)
{
var rc = rect.ToSKRect();
if (Math.Abs(cornerRadius) < float.Epsilon)
{
Canvas.DrawRect(rc, paint.Paint);
}
else
using (var paint = CreatePaint(pen, rect.Size))
{
Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
if (isRounded)
{
Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint);
}
else
{
Canvas.DrawRect(rc, paint.Paint);
}
}
}
}
@ -229,6 +232,20 @@ namespace Avalonia.Skia
}
}
/// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
paint.ApplyTo(glyphRunImpl.Paint);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
(float)baselineOrigin.Y, glyphRunImpl.Paint);
}
}
/// <inheritdoc />
public IRenderTargetBitmapImpl CreateLayer(Size size)
{

77
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -1,9 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
@ -13,13 +15,11 @@ namespace Avalonia.Skia
{
private SKFontManager _skFontManager = SKFontManager.Default;
public FontManagerImpl()
public string GetDefaultFontFamilyName()
{
DefaultFontFamilyName = SKTypeface.Default.FamilyName;
return SKTypeface.Default.FamilyName;
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
@ -30,53 +30,86 @@ namespace Avalonia.Skia
return _skFontManager.FontFamilies;
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
}
[ThreadStatic] private static string[] t_languageTagBuffer;
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
var fontFamilyName = FontFamily.Default.Name;
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
if (t_languageTagBuffer == null)
{
t_languageTagBuffer = new string[2];
}
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
if (fontFamily != null)
{
foreach (var familyName in fontFamily.FamilyNames)
{
var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal,
(SKFontStyleSlant)fontStyle,
new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null)
{
continue;
}
fontFamilyName = familyName;
fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle);
break;
return true;
}
}
else
{
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
(SKFontStyleSlant)fontStyle,
new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null)
{
fontFamilyName = skTypeface.FamilyName;
fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle);
return true;
}
}
fontKey = default;
return false;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
var skTypeface = SKTypeface.Default;
if (typeface.FontFamily.Key == null)
{
foreach (var familyName in typeface.FontFamily.FamilyNames)
{
skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
if (skTypeface == SKTypeface.Default)
{
continue;
}
break;
}
}
else
{
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
skTypeface = fontCollection.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
}
return GetTypeface(fontFamilyName, fontWeight, fontStyle);
return new GlyphTypefaceImpl(skTypeface);
}
}
}

4
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -29,7 +29,7 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl;
_paint = new SKPaint
{
@ -38,7 +38,7 @@ namespace Avalonia.Skia
IsAntialias = true,
LcdRenderText = true,
SubpixelText = true,
Typeface = entry.SKTypeface,
Typeface = glyphTypeface.Typeface,
TextSize = (float)fontSize,
TextAlign = textAlignment.ToSKTextAlign()
};

35
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -0,0 +1,35 @@
// 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.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <inheritdoc />
public class GlyphRunImpl : IGlyphRunImpl
{
public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
{
Paint = paint;
TextBlob = textBlob;
}
/// <summary>
/// Gets the paint to draw with.
/// </summary>
public SKPaint Paint { get; }
/// <summary>
/// Gets the text blob to draw.
/// </summary>
public SKTextBlob TextBlob { get; }
void IDisposable.Dispose()
{
TextBlob.Dispose();
Paint.Dispose();
}
}
}

10
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -14,9 +14,9 @@ namespace Avalonia.Skia
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
public GlyphTypefaceImpl(SKTypeface typeface)
{
Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface;
Typeface = typeface;
Face = new Face(GetTable)
{
@ -61,6 +61,8 @@ namespace Avalonia.Skia
{
StrikethroughThickness = strikethroughThickness;
}
IsFixedPitch = Typeface.IsFixedPitch;
}
public Face Face { get; }
@ -81,7 +83,6 @@ namespace Avalonia.Skia
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int LineGap { get; }
//ToDo: Get these values from HarfBuzz
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlinePosition { get; }
@ -94,6 +95,9 @@ namespace Avalonia.Skia
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public bool IsFixedPitch { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{

96
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls.Platform.Surfaces;
@ -18,9 +17,6 @@ namespace Avalonia.Skia
/// </summary>
internal class PlatformRenderInterface : IPlatformRenderInterface
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private readonly ICustomSkiaGpu _customSkiaGpu;
private GRContext GrContext { get; }
@ -60,7 +56,7 @@ namespace Avalonia.Skia
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{
return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans);
return new FormattedTextImpl(text, typeface, fontSize, textAlignment, wrapping, constraint, spans);
}
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
@ -155,9 +151,95 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format);
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
/// <inheritdoc />
public IFontManagerImpl CreateFontManager()
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
return new FontManagerImpl();
}
/// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
var count = glyphRun.GlyphIndices.Length;
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
var typeface = glyphTypeface.Typeface;
var paint = new SKPaint
{
TextSize = (float)glyphRun.FontRenderingEmSize,
Typeface = typeface,
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
using (var textBlobBuilder = new SKTextBlobBuilder())
{
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphOffsets.IsEmpty)
{
width = 0;
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
if (!glyphTypeface.IsFixedPitch)
{
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
if (glyphRun.GlyphAdvances.IsEmpty)
{
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
width += glyphRun.GlyphAdvances[i];
}
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
}
else
{
var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0;
for (var i = 0; i < count; i++)
{
var glyphOffset = glyphRun.GlyphOffsets[i];
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (glyphRun.GlyphAdvances.IsEmpty)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
currentX += glyphRun.GlyphAdvances[i];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
width = currentX;
}
var textBlob = textBlobBuilder.Build();
return new GlyphRunImpl(paint, textBlob);
}
}
}
}

4
src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")]

47
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -5,58 +5,59 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> _fontFamilies =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
private readonly ConcurrentDictionary<FontKey, SKTypeface> _typefaces =
new ConcurrentDictionary<FontKey, SKTypeface>();
public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry)
public void AddTypeface(FontKey key, SKTypeface typeface)
{
if (!_fontFamilies.TryGetValue(familyName, out var fontFamily))
{
fontFamily = new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>();
_fontFamilies.TryAdd(familyName, fontFamily);
}
fontFamily.TryAdd(key, entry);
_typefaces.TryAdd(key, typeface);
}
public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle)
public SKTypeface Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
var key = new FontKey(fontWeight, fontStyle);
var key = new FontKey(fontFamily, fontWeight, fontStyle);
return _fontFamilies.TryGetValue(familyName, out var fontFamily) ?
fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) :
new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default);
return GetNearestMatch(_typefaces, key);
}
private static TypefaceCollectionEntry GetFallback(IDictionary<FontKey, TypefaceCollectionEntry> fontFamily, FontKey key)
private static SKTypeface GetNearestMatch(IDictionary<FontKey, SKTypeface> typefaces, FontKey key)
{
var keys = fontFamily.Keys.Where(
if (typefaces.ContainsKey(key))
{
return typefaces[key];
}
var keys = typefaces.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray();
if (!keys.Any())
{
keys = fontFamily.Keys.Where(
keys = typefaces.Keys.Where(
x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray();
if (!keys.Any())
{
keys = fontFamily.Keys.Where(
keys = typefaces.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) &&
(x.Style >= key.Style || x.Style < key.Style)).ToArray();
}
}
key = keys.FirstOrDefault();
if (keys.Length == 0)
{
return SKTypeface.Default;
}
fontFamily.TryGetValue(key, out var entry);
key = keys[0];
return entry;
return typefaces[key];
}
}
}

8
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -45,13 +45,11 @@ namespace Avalonia.Skia
{
var assetStream = assetLoader.Open(asset);
var skTypeface = SKTypeface.FromStream(assetStream);
var typeface = SKTypeface.FromStream(assetStream);
var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant);
var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
var entry = new TypefaceCollectionEntry(typeface, skTypeface);
typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry);
typeFaceCollection.AddTypeface(key, typeface);
}
return typeFaceCollection;

5
src/Skia/Avalonia.Skia/SkiaPlatform.cs

@ -25,11 +25,6 @@ namespace Avalonia.Skia
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface);
var fontManager = new FontManagerImpl();
AvaloniaLocator.CurrentMutable
.Bind<IFontManagerImpl>().ToConstant(fontManager);
}
/// <summary>

47
src/Skia/Avalonia.Skia/TypefaceCache.cs

@ -1,47 +0,0 @@
// 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.Collections.Concurrent;
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Cache for Skia typefaces.
/// </summary>
internal static class TypefaceCache
{
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> s_cache =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
if (fontFamily.Key != null)
{
return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily)
.Get(fontFamily.Name, fontWeight, fontStyle);
}
var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>());
var key = new FontKey(fontWeight, fontStyle);
if (typefaceCollection.TryGetValue(key, out var entry))
{
return entry;
}
var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default;
var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle);
entry = new TypefaceCollectionEntry(typeface, skTypeface);
typefaceCollection[key] = entry;
return entry;
}
}
}

19
src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs

@ -1,19 +0,0 @@
// 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 Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class TypefaceCollectionEntry
{
public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface)
{
Typeface = typeface;
SKTypeface = skTypeface;
}
public Typeface Typeface { get; }
public SKTypeface SKTypeface { get; }
}
}

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

Loading…
Cancel
Save