Browse Source

Merge branch 'master' into refactor/value-store

pull/3255/head
Steven Kirk 7 years ago
parent
commit
f0301d5a10
  1. 4
      build/HarfBuzzSharp.props
  2. 4
      build/SkiaSharp.props
  3. 14
      native/Avalonia.Native/inc/avalonia-native.h
  4. 24
      native/Avalonia.Native/src/OSX/gl.mm
  5. 2
      native/Avalonia.Native/src/OSX/window.h
  6. 130
      native/Avalonia.Native/src/OSX/window.mm
  7. 4
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  8. 3
      samples/RenderDemo/MainWindow.xaml
  9. 14
      samples/RenderDemo/Pages/GlyphRunPage.xaml
  10. 80
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  11. 48
      src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
  12. 4
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  13. 2
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  14. 2
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  15. 2
      src/Avalonia.Controls/TextBlock.cs
  16. 20
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  17. 3
      src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs
  18. 18
      src/Avalonia.Input/FocusManager.cs
  19. 2
      src/Avalonia.Input/IKeyboardDevice.cs
  20. 16
      src/Avalonia.Input/MouseDevice.cs
  21. 22
      src/Avalonia.Input/PointerPoint.cs
  22. 4
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  23. 5
      src/Avalonia.Native/WindowImplBase.cs
  24. 2
      src/Avalonia.Visuals/Matrix.cs
  25. 68
      src/Avalonia.Visuals/Media/CharacterHit.cs
  26. 62
      src/Avalonia.Visuals/Media/DrawingContext.cs
  27. 38
      src/Avalonia.Visuals/Media/FontFamily.cs
  28. 153
      src/Avalonia.Visuals/Media/FontManager.cs
  29. 38
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  30. 15
      src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
  31. 15
      src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
  32. 15
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  33. 457
      src/Avalonia.Visuals/Media/GlyphRun.cs
  34. 50
      src/Avalonia.Visuals/Media/GlyphRunDrawing.cs
  35. 11
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  36. 19
      src/Avalonia.Visuals/Media/Transform.cs
  37. 4
      src/Avalonia.Visuals/Media/Typeface.cs
  38. 28
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  39. 30
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  40. 12
      src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs
  41. 5
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  42. 17
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  43. 2
      src/Avalonia.Visuals/Point.cs
  44. 11
      src/Avalonia.Visuals/RelativePoint.cs
  45. 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  46. 5
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  47. 16
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  48. 91
      src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
  49. 43
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  50. 154
      src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
  51. 21
      src/Avalonia.Visuals/Vector.cs
  52. 36
      src/Avalonia.X11/X11Window.cs
  53. 20
      src/Avalonia.X11/XI2Manager.cs
  54. 3
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  55. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
  56. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
  57. 63
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  58. 77
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  59. 4
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  60. 35
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  61. 10
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  62. 96
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  63. 4
      src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs
  64. 47
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  65. 8
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  66. 5
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  67. 47
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  68. 19
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
  69. 58
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  70. 9
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  71. 111
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  72. 34
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  73. 14
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  74. 19
      src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
  75. 9
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  76. 4
      src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs
  77. 52
      src/Windows/Avalonia.Win32/WindowImpl.cs
  78. 44
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  79. 111
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  80. 2
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  81. 53
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
  82. 23
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  83. 52
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  84. 10
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  85. 2
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  86. 93
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  87. BIN
      tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf
  88. 5
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  89. 35
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  90. 47
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs
  91. 10
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  92. 25
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
  93. 130
      tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
  94. 8
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  95. 26
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  96. 10
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

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>

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>

14
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

24
native/Avalonia.Native/src/OSX/gl.mm

@ -1,6 +1,7 @@
#include "common.h"
#include <OpenGL/gl.h>
#include <dlfcn.h>
#include "window.h"
template <typename T, size_t N> char (&ArrayCounter(T (&a)[N]))[N];
#define ARRAY_COUNT(a) (sizeof(ArrayCounter(a)))
@ -181,12 +182,12 @@ extern IAvnGlFeature* GetGlFeature()
class AvnGlRenderingSession : public ComSingleObject<IAvnGlSurfaceRenderingSession, &IID_IAvnGlSurfaceRenderingSession>
{
NSView* _view;
NSWindow* _window;
AvnView* _view;
AvnWindow* _window;
NSOpenGLContext* _context;
public:
FORWARD_IUNKNOWN()
AvnGlRenderingSession(NSWindow*window, NSView* view, NSOpenGLContext* context)
AvnGlRenderingSession(AvnWindow*window, AvnView* view, NSOpenGLContext* context)
{
_context = context;
_window = window;
@ -195,14 +196,12 @@ public:
virtual HRESULT GetPixelSize(AvnPixelSize* ret) override
{
auto fsize = [_view convertSizeToBacking: [_view frame].size];
ret->Width = (int)fsize.width;
ret->Height = (int)fsize.height;
*ret = [_view getPixelSize];
return S_OK;
}
virtual HRESULT GetScaling(double* ret) override
{
*ret = [_window backingScaleFactor];
*ret = [_window getScaling];
return S_OK;
}
@ -234,8 +233,17 @@ public:
auto f = GetFeature();
if(f == NULL)
return E_FAIL;
if(![_view lockFocusIfCanDraw])
@try
{
if(![_view lockFocusIfCanDraw])
return E_ABORT;
}
@catch(NSException* exception)
{
return E_ABORT;
}
auto gl = _context;
CGLLockContext([_context CGLContextObj]);

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

@ -12,6 +12,7 @@ class WindowBaseImpl;
-(AvnPoint) translateLocalPoint:(AvnPoint)pt;
-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose;
-(void) onClosed;
-(AvnPixelSize) getPixelSize;
@end
@interface AvnWindow : NSWindow <NSWindowDelegate>
@ -22,6 +23,7 @@ class WindowBaseImpl;
-(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents;
-(void) applyMenu:(NSMenu *)menu;
-(double) getScaling;
@end
struct INSWindowHolder

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

@ -195,7 +195,11 @@ public:
{
@autoreleasepool
{
[Window close];
if (Window != nullptr)
{
[Window close];
}
return S_OK;
}
}
@ -291,7 +295,14 @@ public:
{
@autoreleasepool
{
return [View lockFocusIfCanDraw] == YES;
@try
{
return [View lockFocusIfCanDraw] == YES;
}
@catch (NSException*)
{
return NO;
}
}
}
@ -716,18 +727,36 @@ 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;
AvnPixelSize _lastPixelSize;
}
- (void)dealloc
- (void)onClosed
{
@synchronized (self)
{
_parent = nullptr;
}
}
- (void)onClosed
- (BOOL)lockFocusIfCanDraw
{
_parent = NULL;
@synchronized (self)
{
if(_parent == nullptr)
{
return NO;
}
}
return [super lockFocusIfCanDraw];
}
-(AvnPixelSize) getPixelSize
{
return _lastPixelSize;
}
- (NSEvent*) lastMouseDownEvent
@ -742,6 +771,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[self setWantsLayer:YES];
_parent = parent;
_area = nullptr;
_lastPixelSize.Height = 100;
_lastPixelSize.Width = 100;
return self;
}
@ -783,6 +814,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[self addTrackingArea:_area];
_parent->UpdateCursor();
auto fsize = [self convertSizeToBacking: [self frame].size];
_lastPixelSize.Width = (int)fsize.width;
_lastPixelSize.Height = (int)fsize.height;
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
}
@ -812,7 +847,13 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)drawRect:(NSRect)dirtyRect
{
if (_parent == nullptr)
{
return;
}
_parent->BaseEvents->RunRenderPriorityJobs();
@synchronized (self) {
if(_swRenderedFrame != NULL)
{
@ -879,7 +920,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void) viewDidChangeBackingProperties
{
auto fsize = [self convertSizeToBacking: [self frame].size];
_lastPixelSize.Width = (int)fsize.width;
_lastPixelSize.Height = (int)fsize.height;
_parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]);
[super viewDidChangeBackingProperties];
}
@ -942,9 +988,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
@ -962,8 +1022,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
@ -1062,6 +1135,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
rv |= MiddleMouseButton;
if (_isRightPressed)
rv |= RightMouseButton;
if (_isXButton1Pressed)
rv |= XButton1MouseButton;
if (_isXButton2Pressed)
rv |= XButton2MouseButton;
return (AvnInputModifiers)rv;
}
@ -1130,6 +1207,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _closed;
NSMenu* _menu;
bool _isAppMenuApplied;
double _lastScaling;
}
-(double) getScaling
{
return _lastScaling;
}
+(void)closeAll
@ -1143,10 +1226,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
}
}
- (void)dealloc
{
}
- (void)pollModalSession:(nonnull NSModalSession)session
{
auto response = [NSApp runModalSession:session];
@ -1201,6 +1280,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[self setReleasedWhenClosed:false];
_parent = parent;
[self setDelegate:self];
_closed = false;
_lastScaling = [self backingScaleFactor];
return self;
}
@ -1216,6 +1298,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return true;
}
- (void)windowDidChangeBackingProperties:(NSNotification *)notification
{
_lastScaling = [self backingScaleFactor];
}
- (void)windowWillClose:(NSNotification *)notification
{
_closed = true;
@ -1226,9 +1313,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[self restoreParentWindow];
parent->BaseEvents->Closed();
[parent->View onClosed];
dispatch_async(dispatch_get_main_queue(), ^{
[self setContentView: nil];
});
}
}
@ -1375,18 +1459,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_parent->GetPosition(&position);
_parent->BaseEvents->PositionChanged(position);
}
// TODO this breaks resizing.
/*- (void)windowDidResize:(NSNotification *)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->WindowStateChanged();
}
}*/
@end
class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup

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/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;
}
}
}

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;
}
}
}

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,

2
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -306,7 +306,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
if (placement == PlacementMode.Pointer)
{
positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1));
positionerParameters.Anchor = PopupPositioningEdge.BottomRight;
positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
}
else

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,

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);
}
}

3
src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs

@ -47,7 +47,8 @@ namespace Avalonia.FreeDesktop
var fProcMounts = File.ReadAllLines(ProcMountsDir)
.Select(x => x.Split(' '))
.Select(x => (x[0], x[1]));
.Select(x => (x[0], x[1]))
.Where(x => !x.Item2.StartsWith("/snap/", StringComparison.InvariantCultureIgnoreCase));
var labelDirEnum = Directory.Exists(DevByLabelDir) ?
new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty<FileInfo>();

18
src/Avalonia.Input/FocusManager.cs

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -17,8 +18,8 @@ namespace Avalonia.Input
/// <summary>
/// The focus scopes in which the focus is currently defined.
/// </summary>
private readonly Dictionary<IFocusScope, IInputElement> _focusScopes =
new Dictionary<IFocusScope, IInputElement>();
private readonly ConditionalWeakTable<IFocusScope, IInputElement> _focusScopes =
new ConditionalWeakTable<IFocusScope, IInputElement>();
/// <summary>
/// Initializes a new instance of the <see cref="FocusManager"/> class.
@ -110,7 +111,18 @@ namespace Avalonia.Input
{
Contract.Requires<ArgumentNullException>(scope != null);
_focusScopes[scope] = element;
if (_focusScopes.TryGetValue(scope, out IInputElement existingElement))
{
if (element != existingElement)
{
_focusScopes.Remove(scope);
_focusScopes.Add(scope, element);
}
}
else
{
_focusScopes.Add(scope, element);
}
if (Scope == scope)
{

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
}

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);
}

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,

5
src/Avalonia.Native/WindowImplBase.cs

@ -353,6 +353,11 @@ namespace Avalonia.Native
public void SetCursor(IPlatformHandle cursor)
{
if (_native == null)
{
return;
}
var newCursor = cursor as AvaloniaNativeCursor;
newCursor = newCursor ?? (_cursorFactory.GetCursor(StandardCursorType.Arrow) as AvaloniaNativeCursor);
_native.Cursor = newCursor.Cursor;

2
src/Avalonia.Visuals/Matrix.cs

@ -306,7 +306,7 @@ namespace Avalonia
/// <summary>
/// Parses a <see cref="Matrix"/> string.
/// </summary>
/// <param name="s">The string.</param>
/// <param name="s">Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new <see cref="Matrix"/></param>
/// <returns>The <see cref="Matrix"/>.</returns>
public static Matrix Parse(string s)
{

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

38
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>
@ -188,6 +185,21 @@ namespace Avalonia.Media
}
}
public static bool operator !=(FontFamily a, FontFamily b)
{
return !(a == b);
}
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))

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;
}
}
}

38
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();
@ -131,6 +126,21 @@ namespace Avalonia.Media.Fonts
}
}
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>
/// Determines whether the specified <see cref="object" />, is equal to this instance.
/// </summary>

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>

19
src/Avalonia.Visuals/Media/Transform.cs

@ -28,6 +28,16 @@ namespace Avalonia.Media
/// </summary>
public abstract Matrix Value { get; }
/// <summary>
/// Parses a <see cref="Transform"/> string.
/// </summary>
/// <param name="s">Six comma-delimited double values that describe the new <see cref="Transform"/>. For details check <see cref="Matrix.Parse(string)"/> </param>
/// <returns>The <see cref="Transform"/>.</returns>
public static Transform Parse(string s)
{
return new MatrixTransform(Matrix.Parse(s));
}
/// <summary>
/// Raises the <see cref="Changed"/> event.
/// </summary>
@ -35,5 +45,14 @@ namespace Avalonia.Media
{
Changed?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Returns a String representing this transform matrix instance.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString()
{
return Value.ToString();
}
}
}

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/Point.cs

@ -175,7 +175,7 @@ namespace Avalonia
/// Parses a <see cref="Point"/> string.
/// </summary>
/// <param name="s">The string.</param>
/// <returns>The <see cref="Thickness"/>.</returns>
/// <returns>The <see cref="Point"/>.</returns>
public static Point Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point."))

11
src/Avalonia.Visuals/RelativePoint.cs

@ -177,5 +177,16 @@ namespace Avalonia
unit);
}
}
/// <summary>
/// Returns a String representing this RelativePoint instance.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString()
{
return _unit == RelativeUnit.Absolute ?
_point.ToString() :
string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100);
}
}
}

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);
}
}
}

21
src/Avalonia.Visuals/Vector.cs

@ -4,6 +4,7 @@
using System;
using System.Globalization;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
using JetBrains.Annotations;
namespace Avalonia
@ -85,6 +86,22 @@ namespace Avalonia
public static Vector operator /(Vector vector, double scale)
=> Divide(vector, scale);
/// <summary>
/// Parses a <see cref="Vector"/> string.
/// </summary>
/// <param name="s">The string.</param>
/// <returns>The <see cref="Vector"/>.</returns>
public static Vector Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector."))
{
return new Vector(
tokenizer.ReadDouble(),
tokenizer.ReadDouble()
);
}
}
/// <summary>
/// Length of the vector
/// </summary>
@ -166,9 +183,9 @@ namespace Avalonia
}
/// <summary>
/// Returns the string representation of the point.
/// Returns the string representation of the vector.
/// </summary>
/// <returns>The string representation of the point.</returns>
/// <returns>The string representation of the vector.</returns>
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y);

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>();

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

@ -41,6 +41,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
StringFormat = StringFormat,
RelativeSource = RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)),
TargetNullValue = TargetNullValue,
NameScope = new WeakReference<INameScope>(serviceProvider.GetService<INameScope>())
};
}
@ -86,5 +87,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public string StringFormat { get; set; }
public RelativeSource RelativeSource { get; set; }
public object TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue;
}
}

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs

@ -25,7 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if ((tt?.Values.FirstOrDefault() is XamlIlTypeExtensionNode tn))
{
targetType = tn.Type;
targetType = tn.Value;
}
else
{

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github

@ -1 +1 @@
Subproject commit ad9915e19398a49c5a11b66000c361659ca692b3
Subproject commit 4c4b6cf8ff0894c925d87b27d4fc7a064440c218

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; }
}
}

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

@ -11,6 +11,9 @@ using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment;
namespace Avalonia
{
@ -28,8 +31,6 @@ namespace Avalonia.Direct2D1
{
public class Direct2D1Platform : IPlatformRenderInterface
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private static readonly Direct2D1Platform s_instance = new Direct2D1Platform();
public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; }
@ -109,7 +110,6 @@ namespace Avalonia.Direct2D1
{
InitializeDirect2D();
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(s_instance);
AvaloniaLocator.CurrentMutable.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true;
}
@ -194,9 +194,57 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, data, size, dpi, stride);
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
/// <inheritdoc />
public IFontManagerImpl CreateFontManager()
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
return new FontManagerImpl();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
var glyphCount = glyphRun.GlyphIndices.Length;
var run = new SharpDX.DirectWrite.GlyphRun
{
FontFace = glyphTypeface.FontFace,
FontSize = (float)glyphRun.FontRenderingEmSize
};
var indices = new short[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
indices[i] = (short)glyphRun.GlyphIndices[i];
}
run.Indices = indices;
run.Advances = new float[glyphCount];
width = 0;
for (var i = 0; i < glyphCount; i++)
{
run.Advances[i] = (float)glyphRun.GlyphAdvances[i];
width += run.Advances[i];
}
run.Offsets = new GlyphOffset[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
var offset = glyphRun.GlyphOffsets[i];
run.Offsets[i] = new GlyphOffset
{
AdvanceOffset = (float)offset.X,
AscenderOffset = (float)offset.Y
};
}
return new GlyphRunImpl(run);
}
}
}

9
src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs

@ -24,10 +24,11 @@ namespace Avalonia.Direct2D1.Media
{
var fontFamily = typeface.FontFamily;
var fontCollection = GetOrAddFontCollection(fontFamily);
int index;
foreach (var familyName in fontFamily.FamilyNames)
foreach (var name in fontFamily.FamilyNames)
{
if (fontCollection.FindFamilyName(familyName, out var index))
if (fontCollection.FindFamilyName(name, out index))
{
return fontCollection.GetFontFamily(index).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
@ -36,9 +37,9 @@ namespace Avalonia.Direct2D1.Media
}
}
InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
InstalledFontCollection.FindFamilyName("Segoe UI", out index);
return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
FontStretch.Normal,
(FontStyle)typeface.Style);

111
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -230,34 +230,64 @@ namespace Avalonia.Direct2D1.Media
}
}
/// <summary>
/// Draws the outline of a rectangle.
/// </summary>
/// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
public void DrawRectangle(IPen pen, Rect rect, float cornerRadius)
/// <inheritdoc />
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY)
{
using (var brush = CreateBrush(pen.Brush, rect.Size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
var rc = rect.ToDirect2D();
var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
if (brush != null)
{
if (brush.PlatformBrush != null)
using (var b = CreateBrush(brush, rect.Size))
{
if (cornerRadius == 0)
if (b.PlatformBrush != null)
{
_deviceContext.DrawRectangle(
rect.ToDirect2D(),
brush.PlatformBrush,
(float)pen.Thickness,
d2dStroke);
if (isRounded)
{
_deviceContext.FillRoundedRectangle(
new RoundedRectangle
{
Rect = new RawRectangleF(
(float)rect.X,
(float)rect.Y,
(float)rect.Right,
(float)rect.Bottom),
RadiusX = (float)radiusX,
RadiusY = (float)radiusY
},
b.PlatformBrush);
}
else
{
_deviceContext.FillRectangle(rc, b.PlatformBrush);
}
}
else
}
}
if (pen?.Brush != null)
{
using (var wrapper = CreateBrush(pen.Brush, rect.Size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (wrapper.PlatformBrush != null)
{
_deviceContext.DrawRoundedRectangle(
new RoundedRectangle { Rect = rect.ToDirect2D(), RadiusX = cornerRadius, RadiusY = cornerRadius },
brush.PlatformBrush,
(float)pen.Thickness,
d2dStroke);
if (isRounded)
{
_deviceContext.DrawRoundedRectangle(
new RoundedRectangle { Rect = rc, RadiusX = (float)radiusX, RadiusY = (float)radiusY },
wrapper.PlatformBrush,
(float)pen.Thickness,
d2dStroke);
}
else
{
_deviceContext.DrawRectangle(
rc,
wrapper.PlatformBrush,
(float)pen.Thickness,
d2dStroke);
}
}
}
}
@ -287,37 +317,18 @@ namespace Avalonia.Direct2D1.Media
}
/// <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>
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius)
/// <param name="foreground">The foreground.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin"></param>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
using (var b = CreateBrush(brush, rect.Size))
using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size))
{
if (b.PlatformBrush != null)
{
if (cornerRadius == 0)
{
_deviceContext.FillRectangle(rect.ToDirect2D(), b.PlatformBrush);
}
else
{
_deviceContext.FillRoundedRectangle(
new RoundedRectangle
{
Rect = new RawRectangleF(
(float)rect.X,
(float)rect.Y,
(float)rect.Right,
(float)rect.Bottom),
RadiusX = cornerRadius,
RadiusY = cornerRadius
},
b.PlatformBrush);
}
}
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
_renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural);
}
}

34
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
@ -14,14 +15,12 @@ namespace Avalonia.Direct2D1.Media
{
internal class FontManagerImpl : IFontManagerImpl
{
public FontManagerImpl()
public string GetDefaultFontFamilyName()
{
//ToDo: Implement a real lookup of the system's default font.
DefaultFontFamilyName = "segoe ui";
return "Segoe UI";
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -36,17 +35,9 @@ namespace Avalonia.Direct2D1.Media
return fontFamilies;
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
//ToDo: Implement caching.
return new Typeface(fontFamily, fontWeight, fontStyle);
}
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
{
var fontFamilyName = FontFamily.Default.Name;
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
for (var i = 0; i < familyCount; i++)
@ -60,12 +51,21 @@ namespace Avalonia.Direct2D1.Media
continue;
}
fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
break;
fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle);
return true;
}
return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
fontKey = default;
return false;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return new GlyphTypefaceImpl(typeface);
}
}
}

14
src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs

@ -22,10 +22,16 @@ namespace Avalonia.Direct2D1.Media
{
Text = text;
using (var font = Direct2D1FontCollectionCache.GetFont(typeface))
using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory,
typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight,
(DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize))
var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont;
var familyName = font.FontFamily.FamilyNames.GetString(0);
using (var textFormat = new DWrite.TextFormat(
Direct2D1Platform.DirectWriteFactory,
familyName,
font.FontFamily.FontCollection,
(DWrite.FontWeight)typeface.Weight,
(DWrite.FontStyle)typeface.Style,
DWrite.FontStretch.Normal,
(float)fontSize))
{
textFormat.WordWrapping =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;

19
src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs

@ -0,0 +1,19 @@
using Avalonia.Platform;
namespace Avalonia.Direct2D1.Media
{
internal class GlyphRunImpl : IGlyphRunImpl
{
public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun)
{
GlyphRun = glyphRun;
}
public SharpDX.DirectWrite.GlyphRun GlyphRun { get; }
public void Dispose()
{
GlyphRun?.Dispose();
}
}
}

9
src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs

@ -17,7 +17,7 @@ namespace Avalonia.Direct2D1.Media
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
FontFace = new FontFace(DWFont);
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
Face = new Face(GetTable);
@ -59,6 +59,8 @@ namespace Avalonia.Direct2D1.Media
{
StrikethroughThickness = strikethroughThickness;
}
IsFixedPitch = FontFace.IsMonospacedFont;
}
private Blob GetTable(Face face, Tag tag)
@ -82,7 +84,7 @@ namespace Avalonia.Direct2D1.Media
public SharpDX.DirectWrite.Font DWFont { get; }
public FontFace FontFace { get; }
public FontFace1 FontFace { get; }
public Face Face { get; }
@ -113,6 +115,9 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public bool IsFixedPitch { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{

4
src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs

@ -2,9 +2,13 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Platform;
using Avalonia.Direct2D1;
[assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize),
typeof(Direct2DChecker))]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")]

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

@ -545,34 +545,42 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN:
if(ShouldIgnoreTouchEmulatedMessage())
case UnmanagedMethods.WindowsMessage.WM_XBUTTONDOWN:
if (ShouldIgnoreTouchEmulatedMessage())
break;
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN
? RawPointerEventType.LeftButtonDown
: msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN
? RawPointerEventType.RightButtonDown
: RawPointerEventType.MiddleButtonDown,
(UnmanagedMethods.WindowsMessage)msg switch
{
UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown,
UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown,
UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown,
UnmanagedMethods.WindowsMessage.WM_XBUTTONDOWN =>
HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton2Down
},
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
case UnmanagedMethods.WindowsMessage.WM_LBUTTONUP:
case UnmanagedMethods.WindowsMessage.WM_RBUTTONUP:
case UnmanagedMethods.WindowsMessage.WM_MBUTTONUP:
if(ShouldIgnoreTouchEmulatedMessage())
case UnmanagedMethods.WindowsMessage.WM_XBUTTONUP:
if (ShouldIgnoreTouchEmulatedMessage())
break;
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP
? RawPointerEventType.LeftButtonUp
: msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONUP
? RawPointerEventType.RightButtonUp
: RawPointerEventType.MiddleButtonUp,
(UnmanagedMethods.WindowsMessage)msg switch
{
UnmanagedMethods.WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp,
UnmanagedMethods.WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp,
UnmanagedMethods.WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp,
UnmanagedMethods.WindowsMessage.WM_XBUTTONUP =>
HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Up : RawPointerEventType.XButton2Up,
},
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
@ -632,15 +640,19 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCXBUTTONDOWN:
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN
? RawPointerEventType.NonClientLeftButtonDown
: msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN
? RawPointerEventType.RightButtonDown
: RawPointerEventType.MiddleButtonDown,
(UnmanagedMethods.WindowsMessage)msg switch
{
UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType.NonClientLeftButtonDown,
UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown,
UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown,
UnmanagedMethods.WindowsMessage.WM_NCXBUTTONDOWN =>
HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton2Down,
},
PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam));
break;
case WindowsMessage.WM_TOUCH:
@ -783,6 +795,10 @@ namespace Avalonia.Win32
modifiers |= RawInputModifiers.RightMouseButton;
if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
modifiers |= RawInputModifiers.MiddleMouseButton;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_XBUTTON1))
modifiers |= RawInputModifiers.XButton1MouseButton;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_XBUTTON2))
modifiers |= RawInputModifiers.XButton2MouseButton;
return modifiers;
}
@ -1061,5 +1077,7 @@ namespace Avalonia.Win32
}
}
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle;
private static int HighWord(int param) => param >> 16;
}
}

44
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -4,6 +4,7 @@
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
@ -245,6 +246,23 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(false, item.IsSelected);
}
[Fact]
public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection()
{
var items = new AvaloniaList<string>(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
};
Prepare(target);
target.Scroll.Offset = new Vector(0, 1);
items.RemoveRange(0, 11);
}
private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
{
_mouse.Click(listBox, item, mouseButton);
@ -383,14 +401,26 @@ namespace Avalonia.Controls.UnitTests
private FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
new ScrollContentPresenter
new Panel
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
}.RegisterInNameScope(scope));
Children =
{
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
}.RegisterInNameScope(scope),
new ScrollBar
{
Name = "verticalScrollBar",
[~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty],
[~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty],
}
}
});
}
private void Prepare(ListBox target)

111
tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs

@ -0,0 +1,111 @@
using System;
using System.Reflection;
using Avalonia.Direct2D1.Media;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Direct2D1.UnitTests.Media
{
public class FontManagerImplTests
{
private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono";
[Fact]
public void Should_Create_Typeface_From_Fallback()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var defaultName = fontManager.GetDefaultFontFamilyName();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial")));
var font = glyphTypeface.DWFont;
Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
}
}
[Fact]
public void Should_Create_Typeface_For_Unknown_Font()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
var font = glyphTypeface.DWFont;
var defaultName = fontManager.GetDefaultFontFamilyName();
Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
}
}
[Fact]
public void Should_Load_Typeface_From_Resource()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(assetLoader);
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily(s_fontUri)));
var font = glyphTypeface.DWFont;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
}
}
[Fact]
public void Should_Load_Nearest_Matching_Font()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(assetLoader);
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic));
var font = glyphTypeface.DWFont;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
}
}
}
}

2
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@ -182,6 +182,8 @@ namespace Avalonia.Layout.UnitTests
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
.Returns(new FormattedTextMock("TEST"));
renderInterface.Setup(x => x.CreateFontManager()).Returns(new MockFontManagerImpl());
var streamGeometry = new Mock<IStreamGeometryImpl>();
streamGeometry.Setup(x =>
x.Open())

53
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs

@ -39,6 +39,59 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
[Fact]
public void BindingExtension_Binds_To_TargetNullValue()
{
using (StyledWindow())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Resources>
<x:String x:Key='text'>foobar</x:String>
</Window.Resources>
<TextBlock Name='textBlock' Text='{Binding Foo, TargetNullValue={StaticResource text}}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
window.DataContext = new FooBar();
window.Show();
Assert.Equal("foobar", textBlock.Text);
}
}
[Fact]
public void BindingExtension_TargetNullValue_UnsetByDefault()
{
using (StyledWindow())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Name='textBlock' IsVisible='{Binding Foo, Converter={x:Static ObjectConverters.IsNotNull}}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
window.DataContext = new FooBar();
window.Show();
Assert.Equal(false, textBlock.IsVisible);
}
}
private class FooBar
{
public object Foo { get; } = null;
}
private IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(

23
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -307,6 +307,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("child", child.Name);
}
[Fact]
public void ControlTemplate_With_TargetType_Is_Operational()
{
var xaml = @"
<ControlTemplate xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
TargetType='{x:Type ContentControl}'>
<ContentPresenter Content='{TemplateBinding Content}' />
</ControlTemplate>
";
var template = AvaloniaXamlLoader.Parse<ControlTemplate>(xaml);
Assert.Equal(typeof(ContentControl), template.TargetType);
Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control);
}
[Fact]
public void ControlTemplate_With_Panel_Children_Are_Added()
{
@ -704,11 +721,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact(Skip =
@"Doesn't work with Portable.xaml, it's working in different creation order -
Handled in test 'Control_Is_Added_To_Parent_Before_Final_EndInit'
do we need it?")]
[Fact]
public void Control_Is_Added_To_Parent_Before_Properties_Are_Set()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))

52
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -5,6 +5,7 @@ using System.Xml;
using Avalonia.Controls;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
@ -38,6 +39,57 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void DataTemplate_Can_Be_Added_To_Style_Resources()
{
using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
{
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Styles>
<Style>
<Style.Resources>
<DataTemplate x:Key='dataTemplate'><TextBlock/></DataTemplate>
</Style.Resources>
</Style>
</UserControl.Styles>
</UserControl>";
var loader = new AvaloniaXamlLoader();
var userControl = (UserControl)loader.Load(xaml);
var dataTemplate = (DataTemplate)((Style)userControl.Styles[0]).Resources["dataTemplate"];
Assert.NotNull(dataTemplate);
}
}
[Fact]
public void ControlTemplate_Can_Be_Added_To_Style_Resources()
{
using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
{
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Styles>
<Style>
<Style.Resources>
<ControlTemplate x:Key='controlTemplate' TargetType='{x:Type Button}'>
<ContentPresenter Content='{TemplateBinding Content}'/>
</ControlTemplate>
</Style.Resources>
</Style>
</UserControl.Styles>
</UserControl>";
var loader = new AvaloniaXamlLoader();
var userControl = (UserControl)loader.Load(xaml);
var controlTemplate = (ControlTemplate)((Style)userControl.Styles[0]).Resources["controlTemplate"];
Assert.NotNull(controlTemplate);
Assert.Equal(typeof(Button), controlTemplate.TargetType);
}
}
[Fact]
public void SolidColorBrush_Can_Be_Added_To_Style_Resources()
{

10
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@ -78,9 +78,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
ctx.Clear(Colors.Transparent);
ctx.PushOpacity(0.8);
ctx.FillRectangle(Brushes.Chartreuse, new Rect(0, 0, 20, 100));
ctx.FillRectangle(Brushes.Crimson, new Rect(20, 0, 20, 100));
ctx.FillRectangle(Brushes.Gold, new Rect(40, 0, 20, 100));
ctx.DrawRectangle(Brushes.Chartreuse, null, new Rect(0, 0, 20, 100));
ctx.DrawRectangle(Brushes.Crimson, null, new Rect(20, 0, 20, 100));
ctx.DrawRectangle(Brushes.Gold,null, new Rect(40, 0, 20, 100));
ctx.PopOpacity();
}
@ -90,8 +90,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
using (var ctx = rtb.CreateDrawingContext(null))
{
ctx.FillRectangle(Brushes.Blue, new Rect(0, 0, 100, 100));
ctx.FillRectangle(Brushes.Pink, new Rect(0, 20, 100, 10));
ctx.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 100, 100));
ctx.DrawRectangle(Brushes.Pink, null, new Rect(0, 20, 100, 10));
var rc = new Rect(0, 0, 60, 60);
ctx.DrawImage(bmp.PlatformImpl, 1, rc, rc);

2
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@ -53,7 +53,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text,
new Typeface(fontFamily, fontWeight, fontStyle),
FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle),
fontSize,
textAlignment,
wrapping,

93
tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs

@ -0,0 +1,93 @@
using System;
using System.Reflection;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using SkiaSharp;
using Xunit;
namespace Avalonia.Skia.UnitTests
{
public class FontManagerImplTests
{
private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono";
[Fact]
public void Should_Create_Typeface_From_Fallback()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName())));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
}
[Fact]
public void Should_Create_Typeface_For_Unknown_Font()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
}
[Fact]
public void Should_Load_Typeface_From_Resource()
{
using (AvaloniaLocator.EnterScope())
{
var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(assetLoader);
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily(s_fontUri)));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName);
}
}
[Fact]
public void Should_Load_Nearest_Matching_Font()
{
using (AvaloniaLocator.EnterScope())
{
var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(assetLoader);
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName);
}
}
}
}

BIN
tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf

Binary file not shown.

5
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@ -7,6 +7,9 @@
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.UnitTests\Assets\*.ttf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
@ -20,7 +23,7 @@
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
</ItemGroup>
<Import Project="..\..\src\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\src\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
</Project>

35
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using Moq;
namespace Avalonia.UnitTests
{
public class MockFontManagerImpl : IFontManagerImpl
{
public string GetDefaultFontFamilyName()
{
return "Default";
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return new[] { "Default" };
}
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
CultureInfo culture, out FontKey fontKey)
{
fontKey = default;
return false;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return Mock.Of<IGlyphTypefaceImpl>();
}
}
}

47
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@ -0,0 +1,47 @@
using System;
using Avalonia.Platform;
namespace Avalonia.UnitTests
{
public class MockGlyphTypeface : IGlyphTypefaceImpl
{
public short DesignEmHeight => 10;
public int Ascent => 100;
public int Descent => 0;
public int LineGap { get; }
public int UnderlinePosition { get; }
public int UnderlineThickness { get; }
public int StrikethroughPosition { get; }
public int StrikethroughThickness { get; }
public bool IsFixedPitch { get; }
public ushort GetGlyph(uint codepoint)
{
return 0;
}
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
return new ushort[codepoints.Length];
}
public int GetGlyphAdvance(ushort glyph)
{
return 100;
}
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var advances = new int[glyphs.Length];
for (var i = 0; i < advances.Length; i++)
{
advances[i] = 100;
}
return advances;
}
public void Dispose() { }
}
}

10
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -79,9 +79,15 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
public IFontManagerImpl CreateFontManager()
{
return Mock.Of<IGlyphTypefaceImpl>();
return new MockFontManagerImpl();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
width = 0;
return Mock.Of<IGlyphRunImpl>();
}
}
}

25
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@ -0,0 +1,25 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class FontManagerTests
{
[Fact]
public void Should_Create_Single_Instance_Typeface()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(new MockPlatformRenderInterface());
var fontFamily = new FontFamily("MyFont");
var typeface = FontManager.Current.GetOrAddTypeface(fontFamily);
Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
}
}
}
}

130
tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

@ -0,0 +1,130 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class GlyphRunTests : TestWithServicesBase
{
public GlyphRunTests()
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToSingleton<MockPlatformRenderInterface>();
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 3, 30)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 1, 0, 10)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 2, 0, 20)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 2, 1, 30)]
[Theory]
public void Should_Get_Distance_From_CharacterHit(double[] advances, ushort[] clusters, int start, int trailingLength, double expectedDistance)
{
using (var glyphRun = CreateGlyphRun(advances, clusters))
{
var characterHit = new CharacterHit(start, trailingLength);
var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
Assert.Equal(expectedDistance, distance);
}
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 25.0, 0, 3, true)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 20.0, 2, 0, true)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 26.0, 2, 1, true)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 35.0, 2, 1, false)]
[Theory]
public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start,
int trailingLengthExpected, bool isInsideExpected)
{
using (var glyphRun = CreateGlyphRun(advances, clusters))
{
var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside);
Assert.Equal(start, textBounds.FirstCharacterIndex);
Assert.Equal(trailingLengthExpected, textBounds.TrailingLength);
Assert.Equal(isInsideExpected, isInside);
}
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 30.0)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 0, 1, 1, 1, 10.0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 2, 1, 2, 20.0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 1, 1, 2, 20.0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 1, 1, 0 }, 1, 1, 1, 2, 20.0)]
[Theory]
public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel,
int index, int expectedIndex, int expectedLength, double expectedWidth)
{
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var textBounds = glyphRun.FindNearestCharacterHit(index, out var width);
Assert.Equal(expectedIndex, textBounds.FirstCharacterIndex);
Assert.Equal(expectedLength, textBounds.TrailingLength);
Assert.Equal(expectedWidth, width, 2);
}
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 0)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 1)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 0, 3, 1, 0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 0, 3, 1, 1)]
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 0, 4, 1, 0)]
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 0, 4, 1, 1)]
[Theory]
public void Should_Get_Next_CharacterHit(double[] advances, ushort[] clusters,
int currentIndex, int currentLength,
int nextIndex, int nextLength,
int bidiLevel)
{
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
Assert.Equal(nextIndex, characterHit.FirstCharacterIndex);
Assert.Equal(nextLength, characterHit.TrailingLength);
}
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 0)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 1)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 1, 3, 0, 0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 1, 3, 0, 1)]
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 1, 4, 0, 0)]
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 1, 4, 0, 1)]
[Theory]
public void Should_Get_Previous_CharacterHit(double[] advances, ushort[] clusters,
int currentIndex, int currentLength,
int previousIndex, int previousLength,
int bidiLevel)
{
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
Assert.Equal(previousIndex, characterHit.FirstCharacterIndex);
Assert.Equal(previousLength, characterHit.TrailingLength);
}
}
private static GlyphRun CreateGlyphRun(double[] glyphAdvances, ushort[] glyphClusters, int bidiLevel = 0)
{
var count = glyphAdvances.Length;
var glyphIndices = new ushort[count];
var bounds = new Rect(0, 0, count * 10, 10);
return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, glyphIndices, glyphAdvances,
glyphClusters: glyphClusters, bidiLevel: bidiLevel, bounds: bounds);
}
}
}

8
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@ -466,7 +466,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacity(0.5), Times.Once);
context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once);
context.Verify(x => x.PopOpacity(), Times.Once);
}
@ -496,7 +496,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacity(0.5), Times.Never);
context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Never);
context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Never);
context.Verify(x => x.PopOpacity(), Times.Never);
}
@ -522,7 +522,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacityMask(Brushes.Green, new Rect(0, 0, 100, 100)), Times.Once);
context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once);
context.Verify(x => x.PopOpacityMask(), Times.Once);
}
@ -641,7 +641,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var context = GetLayerContext(target, border);
context.Verify(x => x.PushOpacity(0.5), Times.Never);
context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once);
context.Verify(x => x.PopOpacity(), Times.Never);
}

26
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs

@ -100,20 +100,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100));
target.DrawRectangle(new Pen(Brushes.Green, 1), new Rect(0, 0, 100, 100));
target.DrawRectangle(Brushes.Red, new Pen(Brushes.Green, 1), new Rect(0, 0, 100, 100));
}
Assert.Equal(2, node.DrawOperations.Count);
Assert.Equal(1, node.DrawOperations.Count);
Assert.IsType<RectangleNode>(node.DrawOperations[0].Item);
Assert.IsType<RectangleNode>(node.DrawOperations[1].Item);
}
[Fact]
public void Should_Not_Replace_Identical_DrawOperation()
{
var node = new VisualNode(new TestRoot(), null);
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0));
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
@ -122,7 +120,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100));
target.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
@ -135,7 +133,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
public void Should_Replace_Different_DrawOperation()
{
var node = new VisualNode(new TestRoot(), null);
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0));
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
@ -144,7 +142,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
@ -157,7 +155,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
public void Should_Update_DirtyRects()
{
var node = new VisualNode(new TestRoot(), null);
var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0);
var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0);
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
@ -165,7 +163,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(new Rect(0, 0, 100, 100), layers.Single().Dirty.Single());
@ -192,8 +190,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
target.FillRectangle(Brushes.Green, new Rect(0, 0, 10, 100));
target.FillRectangle(Brushes.Blue, new Rect(0, 0, 20, 100));
target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 10, 100));
target.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 20, 100));
}
Assert.Equal(2, node.DrawOperations.Count);
@ -208,7 +206,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
public void Trimmed_DrawOperations_Releases_Reference()
{
var node = new VisualNode(new TestRoot(), null);
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0));
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
@ -218,7 +216,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);

10
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -3,13 +3,12 @@ using System.Collections.Generic;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
namespace Avalonia.Visuals.UnitTests.VisualTree
{
class MockRenderInterface : IPlatformRenderInterface
{
public IEnumerable<string> InstalledFontNames => new string[0];
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
@ -52,11 +51,16 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
throw new NotImplementedException();
}
public IFontManagerImpl CreateFontManager()
{
return new MockFontManagerImpl();
}
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt)
{
throw new NotImplementedException();

Loading…
Cancel
Save