Browse Source

Merge branch 'master' into replace-obsolete-api-usages

pull/3168/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
6698f74c70
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Avalonia.sln
  2. 2
      azure-pipelines.yml
  3. 6
      build/HarfBuzzSharp.props
  4. 4
      build/SkiaSharp.props
  5. 1
      src/Avalonia.Animation/Properties/AssemblyInfo.cs
  6. 2
      src/Avalonia.Animation/TransitionInstance.cs
  7. 23
      src/Avalonia.Base/EnumExtensions.cs
  8. 5
      src/Avalonia.Base/Utilities/MathUtilities.cs
  9. 12
      src/Avalonia.Base/ValueStore.cs
  10. 5
      src/Avalonia.Controls/AppBuilderBase.cs
  11. 9
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  12. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  13. 13
      src/Avalonia.Controls/ColumnDefinition.cs
  14. 54
      src/Avalonia.Controls/DefinitionBase.cs
  15. 5
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  16. 12
      src/Avalonia.Controls/Grid.cs
  17. 867
      src/Avalonia.Controls/GridSplitter.cs
  18. 10
      src/Avalonia.Controls/ItemsControl.cs
  19. 7
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  20. 15
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  21. 3
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  22. 29
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  23. 13
      src/Avalonia.Controls/RowDefinition.cs
  24. 5
      src/Avalonia.Controls/TextBlock.cs
  25. 6
      src/Avalonia.Controls/ToolTipService.cs
  26. 2
      src/Avalonia.Controls/WrapPanel.cs
  27. 4
      src/Avalonia.Diagnostics/Views/TreePageView.xaml
  28. 13
      src/Avalonia.Input/MouseDevice.cs
  29. 2
      src/Avalonia.Input/Pointer.cs
  30. 9
      src/Avalonia.Input/PointerPoint.cs
  31. 21
      src/Avalonia.Input/TouchDevice.cs
  32. 1
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  33. 5
      src/Avalonia.Native/WindowImplBase.cs
  34. 58
      src/Avalonia.Themes.Default/GridSplitter.xaml
  35. 28
      src/Avalonia.Visuals/Media/FontFamily.cs
  36. 112
      src/Avalonia.Visuals/Media/FontManager.cs
  37. 6
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  38. 47
      src/Avalonia.Visuals/Media/FormattedText.cs
  39. 111
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  40. 99
      src/Avalonia.Visuals/Media/Typeface.cs
  41. 48
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  42. 89
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  43. 16
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  44. 6
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  45. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  46. 2
      src/Avalonia.X11/X11Platform.cs
  47. 10
      src/Avalonia.X11/X11Window.cs
  48. 12
      src/Avalonia.X11/XI2Manager.cs
  49. 1
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  50. 40
      src/Skia/Avalonia.Skia/FontKey.cs
  51. 82
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  52. 80
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  53. 179
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  54. 14
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  55. 91
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  56. 8
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  57. 5
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  58. 86
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  59. 19
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
  60. 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  61. 25
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  62. 49
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  63. 71
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  64. 19
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  65. 188
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  66. 8
      src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
  67. 39
      src/Windows/Avalonia.Win32/WindowImpl.cs
  68. 28
      tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
  69. 12
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
  70. 18
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
  71. 431
      tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
  72. 286
      tests/Avalonia.Controls.UnitTests/GridTests.cs
  73. 53
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  74. 189
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  75. 20
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  76. 109
      tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
  77. 78
      tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
  78. 39
      tests/Avalonia.Input.UnitTests/PointerTests.cs
  79. 1
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  80. 6
      tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs
  81. 3
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  82. 8
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  83. 2
      tests/Avalonia.UnitTests/MouseTestHelper.cs
  84. 11
      tests/Avalonia.UnitTests/TestRoot.cs
  85. 1
      tests/Avalonia.UnitTests/TestServices.cs
  86. 44
      tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs
  87. 14
      tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
  88. 75
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  89. 6
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

3
Avalonia.sln

@ -128,6 +128,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\Base.props = build\Base.props build\Base.props = build\Base.props
build\Binding.props = build\Binding.props build\Binding.props = build\Binding.props
build\BuildTargets.targets = build\BuildTargets.targets build\BuildTargets.targets = build\BuildTargets.targets
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props
@ -201,7 +202,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
EndProject EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution

2
azure-pipelines.yml

@ -52,7 +52,7 @@ jobs:
sdk: 'macosx10.14' sdk: 'macosx10.14'
configuration: 'Release' configuration: 'Release'
xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
xcodeVersion: 'default' # Options: 8, 9, default, specifyPath xcodeVersion: '10' # Options: 8, 9, default, specifyPath
args: '-derivedDataPath ./' args: '-derivedDataPath ./'
- task: CmdLine@2 - task: CmdLine@2

6
build/HarfBuzzSharp.props

@ -0,0 +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" />
</ItemGroup>
</Project>

4
build/SkiaSharp.props

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

1
src/Avalonia.Animation/Properties/AssemblyInfo.cs

@ -10,3 +10,4 @@ using System.Runtime.CompilerServices;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")]
[assembly: InternalsVisibleTo("Avalonia.LeakTests")] [assembly: InternalsVisibleTo("Avalonia.LeakTests")]
[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")]

2
src/Avalonia.Animation/TransitionInstance.cs

@ -28,7 +28,7 @@ namespace Avalonia.Animation
private void TimerTick(TimeSpan t) private void TimerTick(TimeSpan t)
{ {
var interpVal = (double)t.Ticks / _duration.Ticks; var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks;
// Clamp interpolation value. // Clamp interpolation value.
if (interpVal >= 1d | interpVal < 0d) if (interpVal >= 1d | interpVal < 0d)

23
src/Avalonia.Base/EnumExtensions.cs

@ -0,0 +1,23 @@
// 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.Runtime.CompilerServices;
namespace Avalonia
{
/// <summary>
/// Provides extension methods for enums.
/// </summary>
public static class EnumExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
{
var intValue = *(int*)&value;
var intFlag = *(int*)&flag;
return (intValue & intFlag) == intFlag;
}
}
}

5
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -159,6 +159,11 @@ namespace Avalonia.Utilities
/// <returns>The clamped value.</returns> /// <returns>The clamped value.</returns>
public static int Clamp(int val, int min, int max) public static int Clamp(int val, int min, int max)
{ {
if (min > max)
{
throw new ArgumentException($"{min} cannot be greater than {max}.");
}
if (val < min) if (val < min)
{ {
return min; return min;

12
src/Avalonia.Base/ValueStore.cs

@ -57,7 +57,8 @@ namespace Avalonia
{ {
if (priority == (int)BindingPriority.LocalValue) if (priority == (int)BindingPriority.LocalValue)
{ {
_propertyValues.SetValue(property, Validate(property, value)); Validate(property, ref value);
_propertyValues.SetValue(property, value);
Changed(property, priority, v, value); Changed(property, priority, v, value);
return; return;
} }
@ -78,7 +79,8 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue) if (priority == (int)BindingPriority.LocalValue)
{ {
_propertyValues.AddValue(property, Validate(property, value)); Validate(property, ref value);
_propertyValues.AddValue(property, value);
Changed(property, priority, AvaloniaProperty.UnsetValue, value); Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return; return;
} }
@ -166,16 +168,14 @@ namespace Avalonia
validate2); validate2);
} }
private object Validate(AvaloniaProperty property, object value) private void Validate(AvaloniaProperty property, ref object value)
{ {
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
if (validate != null && value != AvaloniaProperty.UnsetValue) if (validate != null && value != AvaloniaProperty.UnsetValue)
{ {
return validate(_owner, value); value = validate(_owner, value);
} }
return value;
} }
private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property) private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)

5
src/Avalonia.Controls/AppBuilderBase.cs

@ -125,9 +125,8 @@ namespace Avalonia.Controls
}); });
// Copy-pasted because we can't call extension methods due to generic constraints // Copy-pasted because we can't call extension methods due to generic constraints
var lifetime = new ClassicDesktopStyleApplicationLifetime(Instance) {ShutdownMode = ShutdownMode.OnMainWindowClose}; var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = ShutdownMode.OnMainWindowClose};
Instance.ApplicationLifetime = lifetime; SetupWithLifetime(lifetime);
SetupWithoutStarting();
lifetime.Start(Array.Empty<string>()); lifetime.Start(Array.Empty<string>());
} }

9
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -5,12 +5,12 @@ using System.Threading;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading;
namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia.Controls.ApplicationLifetimes
{ {
public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable
{ {
private readonly Application _app;
private int _exitCode; private int _exitCode;
private CancellationTokenSource _cts; private CancellationTokenSource _cts;
private bool _isShuttingDown; private bool _isShuttingDown;
@ -34,12 +34,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
_activeLifetime?._windows.Add((Window)sender); _activeLifetime?._windows.Add((Window)sender);
} }
public ClassicDesktopStyleApplicationLifetime(Application app) public ClassicDesktopStyleApplicationLifetime()
{ {
if (_activeLifetime != null) if (_activeLifetime != null)
throw new InvalidOperationException( throw new InvalidOperationException(
"Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed");
_app = app;
_activeLifetime = this; _activeLifetime = this;
} }
@ -103,7 +102,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
MainWindow?.Show(); MainWindow?.Show();
_app.Run(_cts.Token); Dispatcher.UIThread.MainLoop(_cts.Token);
Environment.ExitCode = _exitCode; Environment.ExitCode = _exitCode;
return _exitCode; return _exitCode;
} }
@ -124,7 +123,7 @@ namespace Avalonia
this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
where T : AppBuilderBase<T>, new() where T : AppBuilderBase<T>, new()
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode}; var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode};
builder.SetupWithLifetime(lifetime); builder.SetupWithLifetime(lifetime);
return lifetime.Start(args); return lifetime.Start(args);
} }

2
src/Avalonia.Controls/AutoCompleteBox.cs

@ -704,7 +704,7 @@ namespace Avalonia.Controls
added.Add(e.NewValue); added.Add(e.NewValue);
} }
OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added)); OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, added, removed));
} }
/// <summary> /// <summary>

13
src/Avalonia.Controls/ColumnDefinition.cs

@ -26,6 +26,16 @@ namespace Avalonia.Controls
public static readonly StyledProperty<GridLength> WidthProperty = public static readonly StyledProperty<GridLength> WidthProperty =
AvaloniaProperty.Register<ColumnDefinition, GridLength>(nameof(Width), new GridLength(1, GridUnitType.Star)); AvaloniaProperty.Register<ColumnDefinition, GridLength>(nameof(Width), new GridLength(1, GridUnitType.Star));
/// <summary>
/// Initializes static members of the <see cref="ColumnDefinition"/> class.
/// </summary>
static ColumnDefinition()
{
AffectsParentMeasure(MinWidthProperty, MaxWidthProperty);
WidthProperty.Changed.AddClassHandler<DefinitionBase>(OnUserSizePropertyChanged);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ColumnDefinition"/> class. /// Initializes a new instance of the <see cref="ColumnDefinition"/> class.
/// </summary> /// </summary>
@ -68,7 +78,6 @@ namespace Avalonia.Controls
} }
set set
{ {
Parent?.InvalidateMeasure();
SetValue(MaxWidthProperty, value); SetValue(MaxWidthProperty, value);
} }
} }
@ -84,7 +93,6 @@ namespace Avalonia.Controls
} }
set set
{ {
Parent?.InvalidateMeasure();
SetValue(MinWidthProperty, value); SetValue(MinWidthProperty, value);
} }
} }
@ -100,7 +108,6 @@ namespace Avalonia.Controls
} }
set set
{ {
Parent?.InvalidateMeasure();
SetValue(WidthProperty, value); SetValue(WidthProperty, value);
} }
} }

54
src/Avalonia.Controls/DefinitionBase.cs

@ -7,9 +7,6 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -50,6 +47,8 @@ namespace Avalonia.Controls
} }
} }
} }
Parent?.InvalidateMeasure();
} }
/// <summary> /// <summary>
@ -63,6 +62,8 @@ namespace Avalonia.Controls
_sharedState.RemoveMember(this); _sharedState.RemoveMember(this);
_sharedState = null; _sharedState = null;
} }
Parent?.InvalidateMeasure();
} }
/// <summary> /// <summary>
@ -114,6 +115,36 @@ namespace Avalonia.Controls
} }
} }
/// <remarks>
/// Notifies parent <see cref="Grid"/> or size scope that definition size has been changed.
/// </remarks>
internal static void OnUserSizePropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e)
{
if (definition.Parent == null)
{
return;
}
if (definition._sharedState != null)
{
definition._sharedState.Invalidate();
}
else
{
GridUnitType oldUnitType = ((GridLength)e.OldValue).GridUnitType;
GridUnitType newUnitType = ((GridLength)e.NewValue).GridUnitType;
if (oldUnitType != newUnitType)
{
definition.Parent.Invalidate();
}
else
{
definition.Parent.InvalidateMeasure();
}
}
}
/// <summary> /// <summary>
/// Returns <c>true</c> if this definition is a part of shared group. /// Returns <c>true</c> if this definition is a part of shared group.
/// </summary> /// </summary>
@ -730,5 +761,22 @@ namespace Avalonia.Controls
SharedSizeGroupProperty.Changed.AddClassHandler<DefinitionBase>(OnSharedSizeGroupPropertyChanged); SharedSizeGroupProperty.Changed.AddClassHandler<DefinitionBase>(OnSharedSizeGroupPropertyChanged);
PrivateSharedSizeScopeProperty.Changed.AddClassHandler<DefinitionBase>(OnPrivateSharedSizeScopePropertyChanged); PrivateSharedSizeScopeProperty.Changed.AddClassHandler<DefinitionBase>(OnPrivateSharedSizeScopePropertyChanged);
} }
/// <summary>
/// Marks a property on a definition as affecting the parent grid's measurement.
/// </summary>
/// <param name="properties">The properties.</param>
protected static void AffectsParentMeasure(params AvaloniaProperty[] properties)
{
void Invalidate(AvaloniaPropertyChangedEventArgs e)
{
(e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure();
}
foreach (var property in properties)
{
property.Changed.Subscribe(Invalidate);
}
}
} }
} }

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

@ -48,11 +48,6 @@ namespace Avalonia.Controls.Generators
tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
} }
if (tabItem.Content == null)
{
tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty];
}
return tabItem; return tabItem;
} }
} }

12
src/Avalonia.Controls/Grid.cs

@ -6,6 +6,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -178,6 +179,7 @@ namespace Avalonia.Controls
if (_data == null) { _data = new ExtendedData(); } if (_data == null) { _data = new ExtendedData(); }
_data.ColumnDefinitions = value; _data.ColumnDefinitions = value;
_data.ColumnDefinitions.Parent = this; _data.ColumnDefinitions.Parent = this;
InvalidateMeasure();
} }
} }
@ -198,6 +200,7 @@ namespace Avalonia.Controls
if (_data == null) { _data = new ExtendedData(); } if (_data == null) { _data = new ExtendedData(); }
_data.RowDefinitions = value; _data.RowDefinitions = value;
_data.RowDefinitions.Parent = this; _data.RowDefinitions.Parent = this;
InvalidateMeasure();
} }
} }
@ -569,6 +572,15 @@ namespace Avalonia.Controls
return (arrangeSize); return (arrangeSize);
} }
/// <summary>
/// <see cref="Panel.ChildrenChanged"/>
/// </summary>
protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
{
CellsStructureDirty = true;
base.ChildrenChanged(sender, e);
}
/// <summary> /// <summary>
/// Invalidates grid caches and makes the grid dirty for measure. /// Invalidates grid caches and makes the grid dirty for measure.
/// </summary> /// </summary>

867
src/Avalonia.Controls/GridSplitter.cs

@ -1,210 +1,841 @@
// Copyright (c) The Avalonia Project. All rights reserved. // This source file is adapted from the Windows Presentation Foundation project.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // (https://github.com/dotnet/wpf/)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System; using System;
using System.Collections.Generic; using System.Diagnostics;
using System.Linq; using Avalonia.Collections;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree; using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Represents the control that redistributes space between columns or rows of a Grid control. /// Represents the control that redistributes space between columns or rows of a <see cref="Grid"/> control.
/// </summary> /// </summary>
/// <remarks>
/// Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext.
/// </remarks>
public class GridSplitter : Thumb public class GridSplitter : Thumb
{ {
private List<DefinitionBase> _definitions; /// <summary>
/// Defines the <see cref="ResizeDirection"/> property.
/// </summary>
public static readonly AvaloniaProperty<GridResizeDirection> ResizeDirectionProperty =
AvaloniaProperty.Register<GridSplitter, GridResizeDirection>(nameof(ResizeDirection));
private Grid _grid; /// <summary>
/// Defines the <see cref="ResizeBehavior"/> property.
/// </summary>
public static readonly AvaloniaProperty<GridResizeBehavior> ResizeBehaviorProperty =
AvaloniaProperty.Register<GridSplitter, GridResizeBehavior>(nameof(ResizeBehavior));
private DefinitionBase _nextDefinition; /// <summary>
/// Defines the <see cref="ShowsPreview"/> property.
/// </summary>
public static readonly AvaloniaProperty<bool> ShowsPreviewProperty =
AvaloniaProperty.Register<GridSplitter, bool>(nameof(ShowsPreview));
private Orientation _orientation; /// <summary>
/// Defines the <see cref="KeyboardIncrement"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> KeyboardIncrementProperty =
AvaloniaProperty.Register<GridSplitter, double>(nameof(KeyboardIncrement), 10d);
private DefinitionBase _prevDefinition; /// <summary>
/// Defines the <see cref="DragIncrement"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> DragIncrementProperty =
AvaloniaProperty.Register<GridSplitter, double>(nameof(DragIncrement), 1d);
private void GetDeltaConstraints(out double min, out double max) /// <summary>
/// Defines the <see cref="PreviewContent"/> property.
/// </summary>
public static readonly AvaloniaProperty<ITemplate<IControl>> PreviewContentProperty =
AvaloniaProperty.Register<GridSplitter, ITemplate<IControl>>(nameof(PreviewContent));
private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast);
private static readonly Cursor s_rowSplitterCursor = new Cursor(StandardCursorType.SizeNorthSouth);
private ResizeData _resizeData;
/// <summary>
/// Indicates whether the Splitter resizes the Columns, Rows, or Both.
/// </summary>
public GridResizeDirection ResizeDirection
{ {
var prevDefinitionLen = GetActualLength(_prevDefinition); get => GetValue(ResizeDirectionProperty);
var prevDefinitionMin = GetMinLength(_prevDefinition); set => SetValue(ResizeDirectionProperty, value);
var prevDefinitionMax = GetMaxLength(_prevDefinition); }
var nextDefinitionLen = GetActualLength(_nextDefinition); /// <summary>
var nextDefinitionMin = GetMinLength(_nextDefinition); /// Indicates which Columns or Rows the Splitter resizes.
var nextDefinitionMax = GetMaxLength(_nextDefinition); /// </summary>
// Determine the minimum and maximum the columns can be resized public GridResizeBehavior ResizeBehavior
min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen); {
max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin); get => GetValue(ResizeBehaviorProperty);
set => SetValue(ResizeBehaviorProperty, value);
} }
protected override void OnDragDelta(VectorEventArgs e) /// <summary>
/// Indicates whether to Preview the column resizing without updating layout.
/// </summary>
public bool ShowsPreview
{ {
// WPF doesn't change anything when spliter is in the last row/column get => GetValue(ShowsPreviewProperty);
// but resizes the splitter row/column when it's the first one. set => SetValue(ShowsPreviewProperty, value);
// this is different, but more internally consistent. }
if (_prevDefinition == null || _nextDefinition == null)
return; /// <summary>
/// The Distance to move the splitter when pressing the keyboard arrow keys.
/// </summary>
public double KeyboardIncrement
{
get => GetValue(KeyboardIncrementProperty);
set => SetValue(KeyboardIncrementProperty, value);
}
/// <summary>
/// Restricts splitter to move a multiple of the specified units.
/// </summary>
public double DragIncrement
{
get => GetValue(DragIncrementProperty);
set => SetValue(DragIncrementProperty, value);
}
/// <summary>
/// Gets or sets content that will be shown when <see cref="ShowsPreview"/> is enabled and user starts resize operation.
/// </summary>
public ITemplate<IControl> PreviewContent
{
get => GetValue(PreviewContentProperty);
set => SetValue(PreviewContentProperty, value);
}
/// <summary>
/// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height.
/// </summary>
internal GridResizeDirection GetEffectiveResizeDirection()
{
GridResizeDirection direction = ResizeDirection;
if (direction != GridResizeDirection.Auto)
{
return direction;
}
// When HorizontalAlignment is Left, Right or Center, resize Columns.
if (HorizontalAlignment != HorizontalAlignment.Stretch)
{
direction = GridResizeDirection.Columns;
}
else if (VerticalAlignment != VerticalAlignment.Stretch)
{
direction = GridResizeDirection.Rows;
}
else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height.
{
direction = GridResizeDirection.Columns;
}
else
{
direction = GridResizeDirection.Rows;
}
var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; return direction;
double max; }
double min;
GetDeltaConstraints(out min, out max);
delta = Math.Min(Math.Max(delta, min), max);
var prevIsStar = IsStar(_prevDefinition); /// <summary>
var nextIsStar = IsStar(_nextDefinition); /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction.
/// </summary>
private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction)
{
GridResizeBehavior resizeBehavior = ResizeBehavior;
if (prevIsStar && nextIsStar) if (resizeBehavior == GridResizeBehavior.BasedOnAlignment)
{ {
foreach (var definition in _definitions) if (direction == GridResizeDirection.Columns)
{ {
if (definition == _prevDefinition) switch (HorizontalAlignment)
{ {
SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta); case HorizontalAlignment.Left:
resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
break;
case HorizontalAlignment.Right:
resizeBehavior = GridResizeBehavior.CurrentAndNext;
break;
default:
resizeBehavior = GridResizeBehavior.PreviousAndNext;
break;
} }
else if (definition == _nextDefinition) }
else
{
switch (VerticalAlignment)
{ {
SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta); case VerticalAlignment.Top:
resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
break;
case VerticalAlignment.Bottom:
resizeBehavior = GridResizeBehavior.CurrentAndNext;
break;
default:
resizeBehavior = GridResizeBehavior.PreviousAndNext;
break;
} }
else if (IsStar(definition)) }
}
return resizeBehavior;
}
/// <summary>
/// Removes preview adorner from the grid.
/// </summary>
private void RemovePreviewAdorner()
{
if (_resizeData.Adorner != null)
{
AdornerLayer layer = AdornerLayer.GetAdornerLayer(this);
layer.Children.Remove(_resizeData.Adorner);
}
}
/// <summary>
/// Initialize the data needed for resizing.
/// </summary>
private void InitializeData(bool showsPreview)
{
// If not in a grid or can't resize, do nothing.
if (Parent is Grid grid)
{
GridResizeDirection resizeDirection = GetEffectiveResizeDirection();
// Setup data used for resizing.
_resizeData = new ResizeData
{
Grid = grid,
ShowsPreview = showsPreview,
ResizeDirection = resizeDirection,
SplitterLength = Math.Min(Bounds.Width, Bounds.Height),
ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection)
};
// Store the rows and columns to resize on drag events.
if (!SetupDefinitionsToResize())
{
// Unable to resize, clear data.
_resizeData = null;
return;
}
// Setup the preview in the adorner if ShowsPreview is true.
SetupPreviewAdorner();
}
}
/// <summary>
/// Returns true if GridSplitter can resize rows/columns.
/// </summary>
private bool SetupDefinitionsToResize()
{
int gridSpan = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
Grid.ColumnSpanProperty :
Grid.RowSpanProperty);
if (gridSpan == 1)
{
var splitterIndex = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
Grid.ColumnProperty :
Grid.RowProperty);
// Select the columns based on behavior.
int index1, index2;
switch (_resizeData.ResizeBehavior)
{
case GridResizeBehavior.PreviousAndCurrent:
// Get current and previous.
index1 = splitterIndex - 1;
index2 = splitterIndex;
break;
case GridResizeBehavior.CurrentAndNext:
// Get current and next.
index1 = splitterIndex;
index2 = splitterIndex + 1;
break;
default: // GridResizeBehavior.PreviousAndNext.
// Get previous and next.
index1 = splitterIndex - 1;
index2 = splitterIndex + 1;
break;
}
// Get count of rows/columns in the resize direction.
int count = _resizeData.ResizeDirection == GridResizeDirection.Columns ?
_resizeData.Grid.ColumnDefinitions.Count :
_resizeData.Grid.RowDefinitions.Count;
if (index1 >= 0 && index2 < count)
{
_resizeData.SplitterIndex = splitterIndex;
_resizeData.Definition1Index = index1;
_resizeData.Definition1 = GetGridDefinition(_resizeData.Grid, index1, _resizeData.ResizeDirection);
_resizeData.OriginalDefinition1Length =
_resizeData.Definition1.UserSizeValueCache; // Save Size if user cancels.
_resizeData.OriginalDefinition1ActualLength = GetActualLength(_resizeData.Definition1);
_resizeData.Definition2Index = index2;
_resizeData.Definition2 = GetGridDefinition(_resizeData.Grid, index2, _resizeData.ResizeDirection);
_resizeData.OriginalDefinition2Length =
_resizeData.Definition2.UserSizeValueCache; // Save Size if user cancels.
_resizeData.OriginalDefinition2ActualLength = GetActualLength(_resizeData.Definition2);
// Determine how to resize the columns.
bool isStar1 = IsStar(_resizeData.Definition1);
bool isStar2 = IsStar(_resizeData.Definition2);
if (isStar1 && isStar2)
{ {
SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars. // If they are both stars, resize both.
_resizeData.SplitBehavior = SplitBehavior.Split;
} }
else
{
// One column is fixed width, resize the first one that is fixed.
_resizeData.SplitBehavior = !isStar1 ? SplitBehavior.Resize1 : SplitBehavior.Resize2;
}
return true;
} }
} }
else if (prevIsStar)
return false;
}
/// <summary>
/// Create the preview adorner and add it to the adorner layer.
/// </summary>
private void SetupPreviewAdorner()
{
if (_resizeData.ShowsPreview)
{ {
SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); // Get the adorner layer and add an adorner to it.
var adornerLayer = AdornerLayer.GetAdornerLayer(_resizeData.Grid);
var previewContent = PreviewContent;
// Can't display preview.
if (adornerLayer == null)
{
return;
}
IControl builtPreviewContent = previewContent?.Build();
_resizeData.Adorner = new PreviewAdorner(builtPreviewContent);
AdornerLayer.SetAdornedElement(_resizeData.Adorner, this);
adornerLayer.Children.Add(_resizeData.Adorner);
// Get constraints on preview's translation.
GetDeltaConstraints(out _resizeData.MinChange, out _resizeData.MaxChange);
} }
else if (nextIsStar) }
protected override void OnPointerEnter(PointerEventArgs e)
{
base.OnPointerEnter(e);
GridResizeDirection direction = GetEffectiveResizeDirection();
switch (direction)
{ {
SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); case GridResizeDirection.Columns:
Cursor = s_columnSplitterCursor;
break;
case GridResizeDirection.Rows:
Cursor = s_rowSplitterCursor;
break;
} }
else }
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
if (_resizeData != null)
{ {
SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); CancelResize();
SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
} }
} }
private double GetActualLength(DefinitionBase definition) protected override void OnDragStarted(VectorEventArgs e)
{ {
if (definition == null) base.OnDragStarted(e);
return 0;
var columnDefinition = definition as ColumnDefinition; // TODO: Looks like that sometimes thumb will raise multiple drag started events.
return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; // Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called");
if (_resizeData != null)
{
return;
}
InitializeData(ShowsPreview);
} }
private double GetMinLength(DefinitionBase definition) protected override void OnDragDelta(VectorEventArgs e)
{ {
if (definition == null) base.OnDragDelta(e);
return 0;
var columnDefinition = definition as ColumnDefinition; if (_resizeData != null)
return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight; {
double horizontalChange = e.Vector.X;
double verticalChange = e.Vector.Y;
// Round change to nearest multiple of DragIncrement.
double dragIncrement = DragIncrement;
horizontalChange = Math.Round(horizontalChange / dragIncrement) * dragIncrement;
verticalChange = Math.Round(verticalChange / dragIncrement) * dragIncrement;
if (_resizeData.ShowsPreview)
{
// Set the Translation of the Adorner to the distance from the thumb.
if (_resizeData.ResizeDirection == GridResizeDirection.Columns)
{
_resizeData.Adorner.OffsetX = Math.Min(
Math.Max(horizontalChange, _resizeData.MinChange),
_resizeData.MaxChange);
}
else
{
_resizeData.Adorner.OffsetY = Math.Min(
Math.Max(verticalChange, _resizeData.MinChange),
_resizeData.MaxChange);
}
}
else
{
// Directly update the grid.
MoveSplitter(horizontalChange, verticalChange);
}
}
} }
private double GetMaxLength(DefinitionBase definition) protected override void OnDragCompleted(VectorEventArgs e)
{ {
if (definition == null) base.OnDragCompleted(e);
return 0;
var columnDefinition = definition as ColumnDefinition; if (_resizeData != null)
return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight; {
if (_resizeData.ShowsPreview)
{
// Update the grid.
MoveSplitter(_resizeData.Adorner.OffsetX, _resizeData.Adorner.OffsetY);
RemovePreviewAdorner();
}
_resizeData = null;
}
} }
private bool IsStar(DefinitionBase definition) protected override void OnKeyDown(KeyEventArgs e)
{ {
var columnDefinition = definition as ColumnDefinition; Key key = e.Key;
return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar;
switch (key)
{
case Key.Escape:
if (_resizeData != null)
{
CancelResize();
e.Handled = true;
}
break;
case Key.Left:
e.Handled = KeyboardMoveSplitter(-KeyboardIncrement, 0);
break;
case Key.Right:
e.Handled = KeyboardMoveSplitter(KeyboardIncrement, 0);
break;
case Key.Up:
e.Handled = KeyboardMoveSplitter(0, -KeyboardIncrement);
break;
case Key.Down:
e.Handled = KeyboardMoveSplitter(0, KeyboardIncrement);
break;
}
} }
private void SetLengthInStars(DefinitionBase definition, double value) /// <summary>
/// Cancels the resize operation.
/// </summary>
private void CancelResize()
{ {
var columnDefinition = definition as ColumnDefinition; // Restore original column/row lengths.
if (columnDefinition != null) if (_resizeData.ShowsPreview)
{ {
columnDefinition.Width = new GridLength(value, GridUnitType.Star); RemovePreviewAdorner();
} }
else else // Reset the columns/rows lengths to the saved values.
{ {
((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star); SetDefinitionLength(_resizeData.Definition1, _resizeData.OriginalDefinition1Length);
SetDefinitionLength(_resizeData.Definition2, _resizeData.OriginalDefinition2Length);
} }
_resizeData = null;
}
/// <summary>
/// Returns true if the row/column has a star length.
/// </summary>
private static bool IsStar(DefinitionBase definition)
{
return definition.UserSizeValueCache.IsStar;
} }
private void SetLength(DefinitionBase definition, double value) /// <summary>
/// Gets Column or Row definition at index from grid based on resize direction.
/// </summary>
private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction)
{ {
var columnDefinition = definition as ColumnDefinition; return direction == GridResizeDirection.Columns ?
if (columnDefinition != null) (DefinitionBase)grid.ColumnDefinitions[index] :
(DefinitionBase)grid.RowDefinitions[index];
}
/// <summary>
/// Retrieves the ActualWidth or ActualHeight of the definition depending on its type Column or Row.
/// </summary>
private double GetActualLength(DefinitionBase definition)
{
var column = definition as ColumnDefinition;
return column?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
}
/// <summary>
/// Gets Column or Row definition at index from grid based on resize direction.
/// </summary>
private static void SetDefinitionLength(DefinitionBase definition, GridLength length)
{
definition.SetValue(
definition is ColumnDefinition ? ColumnDefinition.WidthProperty : RowDefinition.HeightProperty, length);
}
/// <summary>
/// Get the minimum and maximum Delta can be given definition constraints (MinWidth/MaxWidth).
/// </summary>
private void GetDeltaConstraints(out double minDelta, out double maxDelta)
{
double definition1Len = GetActualLength(_resizeData.Definition1);
double definition1Min = _resizeData.Definition1.UserMinSizeValueCache;
double definition1Max = _resizeData.Definition1.UserMaxSizeValueCache;
double definition2Len = GetActualLength(_resizeData.Definition2);
double definition2Min = _resizeData.Definition2.UserMinSizeValueCache;
double definition2Max = _resizeData.Definition2.UserMaxSizeValueCache;
// Set MinWidths to be greater than width of splitter.
if (_resizeData.SplitterIndex == _resizeData.Definition1Index)
{ {
columnDefinition.Width = new GridLength(value); definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength);
} }
else else if (_resizeData.SplitterIndex == _resizeData.Definition2Index)
{ {
((RowDefinition)definition).Height = new GridLength(value); definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength);
} }
// Determine the minimum and maximum the columns can be resized.
minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len);
maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min);
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) /// <summary>
/// Sets the length of definition1 and definition2.
/// </summary>
private void SetLengths(double definition1Pixels, double definition2Pixels)
{ {
base.OnAttachedToVisualTree(e); // For the case where both definition1 and 2 are stars, update all star values to match their current pixel values.
_grid = this.GetVisualParent<Grid>(); if (_resizeData.SplitBehavior == SplitBehavior.Split)
{
var definitions = _resizeData.ResizeDirection == GridResizeDirection.Columns ?
(IAvaloniaReadOnlyList<DefinitionBase>)_resizeData.Grid.ColumnDefinitions :
(IAvaloniaReadOnlyList<DefinitionBase>)_resizeData.Grid.RowDefinitions;
_orientation = DetectOrientation(); var definitionsCount = definitions.Count;
for (var i = 0; i < definitionsCount; i++)
{
DefinitionBase definition = definitions[i];
int definitionIndex; //row or col // For each definition, if it is a star, set is value to ActualLength in stars
if (_orientation == Orientation.Vertical) // This makes 1 star == 1 pixel in length
if (i == _resizeData.Definition1Index)
{
SetDefinitionLength(definition, new GridLength(definition1Pixels, GridUnitType.Star));
}
else if (i == _resizeData.Definition2Index)
{
SetDefinitionLength(definition, new GridLength(definition2Pixels, GridUnitType.Star));
}
else if (IsStar(definition))
{
SetDefinitionLength(definition, new GridLength(GetActualLength(definition), GridUnitType.Star));
}
}
}
else if (_resizeData.SplitBehavior == SplitBehavior.Resize1)
{ {
Cursor = new Cursor(StandardCursorType.SizeWestEast); SetDefinitionLength(_resizeData.Definition1, new GridLength(definition1Pixels));
_definitions = _grid.ColumnDefinitions.Cast<DefinitionBase>().ToList();
definitionIndex = GetValue(Grid.ColumnProperty);
PseudoClasses.Add(":vertical");
} }
else else
{ {
Cursor = new Cursor(StandardCursorType.SizeNorthSouth); SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels));
definitionIndex = GetValue(Grid.RowProperty); }
_definitions = _grid.RowDefinitions.Cast<DefinitionBase>().ToList(); }
PseudoClasses.Add(":horizontal");
/// <summary>
/// Move the splitter by the given Delta's in the horizontal and vertical directions.
/// </summary>
private void MoveSplitter(double horizontalChange, double verticalChange)
{
Debug.Assert(_resizeData != null, "_resizeData should not be null when calling MoveSplitter");
// Calculate the offset to adjust the splitter.
var delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange;
DefinitionBase definition1 = _resizeData.Definition1;
DefinitionBase definition2 = _resizeData.Definition2;
if (definition1 != null && definition2 != null)
{
double actualLength1 = GetActualLength(definition1);
double actualLength2 = GetActualLength(definition2);
// When splitting, Check to see if the total pixels spanned by the definitions
// is the same asbefore starting resize. If not cancel the drag
if (_resizeData.SplitBehavior == SplitBehavior.Split &&
!MathUtilities.AreClose(
actualLength1 + actualLength2,
_resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength))
{
CancelResize();
return;
}
GetDeltaConstraints(out var min, out var max);
// Constrain Delta to Min/MaxWidth of columns
delta = Math.Min(Math.Max(delta, min), max);
double definition1LengthNew = actualLength1 + delta;
double definition2LengthNew = actualLength1 + actualLength2 - definition1LengthNew;
SetLengths(definition1LengthNew, definition2LengthNew);
} }
}
if (definitionIndex > 0) /// <summary>
_prevDefinition = _definitions[definitionIndex - 1]; /// Move the splitter using the Keyboard (Don't show preview).
/// </summary>
private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange)
{
// If moving with the mouse, ignore keyboard motion.
if (_resizeData != null)
{
return false; // Don't handle the event.
}
// Don't show preview.
InitializeData(false);
// Check that we are actually able to resize.
if (_resizeData == null)
{
return false; // Don't handle the event.
}
if (definitionIndex < _definitions.Count - 1) MoveSplitter(horizontalChange, verticalChange);
_nextDefinition = _definitions[definitionIndex + 1];
_resizeData = null;
return true;
} }
private Orientation DetectOrientation() /// <summary>
/// This adorner draws the preview for the <see cref="GridSplitter"/>.
/// It also positions the adorner.
/// </summary>
private sealed class PreviewAdorner : Decorator
{ {
if (!_grid.ColumnDefinitions.Any()) private readonly TranslateTransform _translation;
return Orientation.Horizontal; private readonly Decorator _decorator;
if (!_grid.RowDefinitions.Any())
return Orientation.Vertical; public PreviewAdorner(IControl previewControl)
{
// Add a decorator to perform translations.
_translation = new TranslateTransform();
_decorator = new Decorator
{
Child = previewControl,
RenderTransform = _translation
};
Child = _decorator;
}
var col = GetValue(Grid.ColumnProperty); /// <summary>
var row = GetValue(Grid.RowProperty); /// The Preview's Offset in the X direction from the GridSplitter.
var width = _grid.ColumnDefinitions[col].Width; /// </summary>
var height = _grid.RowDefinitions[row].Height; public double OffsetX
if (width.IsAuto && !height.IsAuto)
{ {
return Orientation.Vertical; get => _translation.X;
set => _translation.X = value;
} }
if (!width.IsAuto && height.IsAuto)
/// <summary>
/// The Preview's Offset in the Y direction from the GridSplitter.
/// </summary>
public double OffsetY
{ {
return Orientation.Horizontal; get => _translation.Y;
set => _translation.Y = value;
} }
if (_grid.Children.OfType<Control>() // Decision based on other controls in the same column
.Where(c => Grid.GetColumn(c) == col) protected override Size ArrangeOverride(Size finalSize)
.Any(c => c.GetType() != typeof(GridSplitter)))
{ {
return Orientation.Horizontal; // Adorners always get clipped to the owner control. In this case we want
// to constrain size to the splitter size but draw on top of the parent grid.
Clip = null;
return base.ArrangeOverride(finalSize);
} }
return Orientation.Vertical;
} }
/// <summary>
/// <see cref="GridSplitter"/> has special Behavior when columns are fixed.
/// If the left column is fixed, splitter will only resize that column.
/// Else if the right column is fixed, splitter will only resize the right column.
/// </summary>
private enum SplitBehavior
{
/// <summary>
/// Both columns/rows are star lengths.
/// </summary>
Split,
/// <summary>
/// Resize 1 only.
/// </summary>
Resize1,
/// <summary>
/// Resize 2 only.
/// </summary>
Resize2
}
/// <summary>
/// Stores data during the resizing operation.
/// </summary>
private class ResizeData
{
public bool ShowsPreview;
public PreviewAdorner Adorner;
// The constraints to keep the Preview within valid ranges.
public double MinChange;
public double MaxChange;
// The grid to Resize.
public Grid Grid;
// Cache of Resize Direction and Behavior.
public GridResizeDirection ResizeDirection;
public GridResizeBehavior ResizeBehavior;
// The columns/rows to resize.
public DefinitionBase Definition1;
public DefinitionBase Definition2;
// Are the columns/rows star lengths.
public SplitBehavior SplitBehavior;
// The index of the splitter.
public int SplitterIndex;
// The indices of the columns/rows.
public int Definition1Index;
public int Definition2Index;
// The original lengths of Definition1 and Definition2 (to restore lengths if user cancels resize).
public GridLength OriginalDefinition1Length;
public GridLength OriginalDefinition2Length;
public double OriginalDefinition1ActualLength;
public double OriginalDefinition2ActualLength;
// The minimum of Width/Height of Splitter. Used to ensure splitter
// isn't hidden by resizing a row/column smaller than the splitter.
public double SplitterLength;
}
}
/// <summary>
/// Enum to indicate whether <see cref="GridSplitter"/> resizes Columns or Rows.
/// </summary>
public enum GridResizeDirection
{
/// <summary>
/// Determines whether to resize rows or columns based on its Alignment and
/// width compared to height.
/// </summary>
Auto,
/// <summary>
/// Resize columns when dragging Splitter.
/// </summary>
Columns,
/// <summary>
/// Resize rows when dragging Splitter.
/// </summary>
Rows
}
/// <summary>
/// Enum to indicate what Columns or Rows the <see cref="GridSplitter"/> resizes.
/// </summary>
public enum GridResizeBehavior
{
/// <summary>
/// Determine which columns or rows to resize based on its Alignment.
/// </summary>
BasedOnAlignment,
/// <summary>
/// Resize the current and next Columns or Rows.
/// </summary>
CurrentAndNext,
/// <summary>
/// Resize the previous and current Columns or Rows.
/// </summary>
PreviousAndCurrent,
/// <summary>
/// Resize the previous and next Columns or Rows.
/// </summary>
PreviousAndNext
} }
} }

10
src/Avalonia.Controls/ItemsControl.cs

@ -359,6 +359,12 @@ namespace Avalonia.Controls
UpdateItemCount(); UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue); RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue); AddControlItemsToLogicalChildren(newValue);
if (Presenter != null)
{
Presenter.Items = newValue;
}
SubscribeToItems(newValue); SubscribeToItems(newValue);
} }
@ -370,6 +376,8 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
UpdateItemCount();
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
@ -381,7 +389,7 @@ namespace Avalonia.Controls
break; break;
} }
UpdateItemCount(); Presenter?.ItemsChanged(e);
var collection = sender as ICollection; var collection = sender as ICollection;
PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":empty", collection == null || collection.Count == 0);

7
src/Avalonia.Controls/Presenters/IItemsPresenter.cs

@ -1,12 +1,19 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections;
using System.Collections.Specialized;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
public interface IItemsPresenter : IPresenter public interface IItemsPresenter : IPresenter
{ {
IEnumerable Items { get; set; }
IPanel Panel { get; } IPanel Panel { get; }
void ItemsChanged(NotifyCollectionChangedEventArgs e);
void ScrollIntoView(object item); void ScrollIntoView(object item);
} }
} }

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

@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters
_itemsSubscription?.Dispose(); _itemsSubscription?.Dispose();
_itemsSubscription = null; _itemsSubscription = null;
if (_createdPanel && value is INotifyCollectionChanged incc) if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc)
{ {
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
} }
@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters
private set; private set;
} }
protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
/// <inheritdoc/> /// <inheritdoc/>
public override sealed void ApplyTemplate() public override sealed void ApplyTemplate()
{ {
@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters
{ {
} }
/// <inheritdoc/>
void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e)
{
if (Panel != null)
{
ItemsChanged(e);
}
}
/// <summary> /// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control. /// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary> /// </summary>
@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters
_createdPanel = true; _createdPanel = true;
if (_itemsSubscription == null && Items is INotifyCollectionChanged incc) if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc)
{ {
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
} }

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

@ -297,7 +297,8 @@ namespace Avalonia.Controls.Presenters
return new FormattedText return new FormattedText
{ {
Text = "X", Text = "X",
Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
TextAlignment = TextAlignment, TextAlignment = TextAlignment,
Constraint = availableSize, Constraint = availableSize,
}.Bounds.Size; }.Bounds.Size;

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

@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/> /// <inheritdoc/>
protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
base.ItemsCollectionChanged(sender, e);
if (_updateCount > 0) if (_updateCount > 0)
{ {
base.ItemsCollectionChanged(sender, e);
return; return;
} }
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
_selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
break;
case NotifyCollectionChangedAction.Remove:
_selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
break;
}
base.ItemsCollectionChanged(sender, e);
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives
} }
else else
{ {
_selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
UpdateSelectedItem(_selection.First(), false); UpdateSelectedItem(_selection.First(), false);
} }
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
_selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
UpdateSelectedItem(_selection.First(), false); UpdateSelectedItem(_selection.First(), false);
ResetSelectedItems(); ResetSelectedItems();
break; break;
@ -1088,9 +1097,15 @@ namespace Avalonia.Controls.Primitives
} }
else else
{ {
SelectedIndex = _updateSelectedIndex != int.MinValue ? if (_updateSelectedIndex != int.MinValue)
_updateSelectedIndex : {
AlwaysSelected ? 0 : -1; SelectedIndex = _updateSelectedIndex;
}
if (AlwaysSelected && SelectedIndex == -1)
{
SelectedIndex = 0;
}
} }
} }
} }

13
src/Avalonia.Controls/RowDefinition.cs

@ -26,6 +26,16 @@ namespace Avalonia.Controls
public static readonly StyledProperty<GridLength> HeightProperty = public static readonly StyledProperty<GridLength> HeightProperty =
AvaloniaProperty.Register<RowDefinition, GridLength>(nameof(Height), new GridLength(1, GridUnitType.Star)); AvaloniaProperty.Register<RowDefinition, GridLength>(nameof(Height), new GridLength(1, GridUnitType.Star));
/// <summary>
/// Initializes static members of the <see cref="RowDefinition"/> class.
/// </summary>
static RowDefinition()
{
AffectsParentMeasure(MaxHeightProperty, MinHeightProperty);
HeightProperty.Changed.AddClassHandler<DefinitionBase>(OnUserSizePropertyChanged);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RowDefinition"/> class. /// Initializes a new instance of the <see cref="RowDefinition"/> class.
/// </summary> /// </summary>
@ -68,7 +78,6 @@ namespace Avalonia.Controls
} }
set set
{ {
Parent?.InvalidateMeasure();
SetValue(MaxHeightProperty, value); SetValue(MaxHeightProperty, value);
} }
} }
@ -84,7 +93,6 @@ namespace Avalonia.Controls
} }
set set
{ {
Parent?.InvalidateMeasure();
SetValue(MinHeightProperty, value); SetValue(MinHeightProperty, value);
} }
} }
@ -100,7 +108,6 @@ namespace Avalonia.Controls
} }
set set
{ {
Parent?.InvalidateMeasure();
SetValue(HeightProperty, value); SetValue(HeightProperty, value);
} }
} }

5
src/Avalonia.Controls/TextBlock.cs

@ -352,10 +352,11 @@ namespace Avalonia.Controls
return new FormattedText return new FormattedText
{ {
Constraint = constraint, Constraint = constraint,
Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
Text = text ?? string.Empty, Text = text ?? string.Empty,
TextAlignment = TextAlignment, TextAlignment = TextAlignment,
Wrapping = TextWrapping, TextWrapping = TextWrapping,
}; };
} }

6
src/Avalonia.Controls/ToolTipService.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -79,7 +80,10 @@ namespace Avalonia.Controls
{ {
StopTimer(); StopTimer();
ToolTip.SetIsOpen(control, true); if ((control as IVisual).IsAttachedToVisualTree)
{
ToolTip.SetIsOpen(control, true);
}
} }
private void Close(Control control) private void Close(Control control)

2
src/Avalonia.Controls/WrapPanel.cs

@ -42,7 +42,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static WrapPanel() static WrapPanel()
{ {
AffectsMeasure<WrapPanel>(OrientationProperty); AffectsMeasure<WrapPanel>(OrientationProperty, ItemWidthProperty, ItemHeightProperty);
} }
/// <summary> /// <summary>

4
src/Avalonia.Diagnostics/Views/TreePageView.xaml

@ -2,7 +2,7 @@
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.Views.TreePageView"> x:Class="Avalonia.Diagnostics.Views.TreePageView">
<Grid ColumnDefinitions="*,4,3*"> <Grid ColumnDefinitions="*,Auto,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"> <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<TreeView.DataTemplates> <TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode" <TreeDataTemplate DataType="vm:TreeNode"
@ -20,7 +20,7 @@
</TreeView.Styles> </TreeView.Styles>
</TreeView> </TreeView>
<GridSplitter Width="4" Grid.Column="1" /> <GridSplitter Grid.Column="1" />
<ContentControl Content="{Binding Details}" Grid.Column="2" /> <ContentControl Content="{Binding Details}" Grid.Column="2" />
</Grid> </Grid>
</UserControl> </UserControl>

13
src/Avalonia.Input/MouseDevice.cs

@ -14,13 +14,14 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Represents a mouse device. /// Represents a mouse device.
/// </summary> /// </summary>
public class MouseDevice : IMouseDevice public class MouseDevice : IMouseDevice, IDisposable
{ {
private int _clickCount; private int _clickCount;
private Rect _lastClickRect; private Rect _lastClickRect;
private ulong _lastClickTime; private ulong _lastClickTime;
private readonly Pointer _pointer; private readonly Pointer _pointer;
private bool _disposed;
public MouseDevice(Pointer pointer = null) public MouseDevice(Pointer pointer = null)
{ {
@ -126,7 +127,9 @@ namespace Avalonia.Input
{ {
Contract.Requires<ArgumentNullException>(e != null); Contract.Requires<ArgumentNullException>(e != null);
var mouse = (IMouseDevice)e.Device; var mouse = (MouseDevice)e.Device;
if(mouse._disposed)
return;
Position = e.Root.PointToScreen(e.Position); Position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e); var props = CreateProperties(e);
@ -441,5 +444,11 @@ namespace Avalonia.Input
el = (IInputElement)el.VisualParent; el = (IInputElement)el.VisualParent;
} }
} }
public void Dispose()
{
_disposed = true;
_pointer?.Dispose();
}
} }
} }

2
src/Avalonia.Input/Pointer.cs

@ -37,7 +37,7 @@ namespace Avalonia.Input
{ {
if (Captured != null) if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached; Captured.DetachedFromVisualTree -= OnCaptureDetached;
var oldCapture = control; var oldCapture = Captured;
Captured = control; Captured = control;
PlatformCapture(control); PlatformCapture(control);
if (oldCapture != null) if (oldCapture != null)

9
src/Avalonia.Input/PointerPoint.cs

@ -1,3 +1,6 @@
// 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.Input namespace Avalonia.Input
{ {
public sealed class PointerPoint public sealed class PointerPoint
@ -27,9 +30,9 @@ namespace Avalonia.Input
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
{ {
PointerUpdateKind = kind; PointerUpdateKind = kind;
IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton); IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton);
IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton); IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton);
IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton); IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton);
// The underlying input source might be reporting the previous state, // The underlying input source might be reporting the previous state,
// so make sure that we reflect the current state // so make sure that we reflect the current state

21
src/Avalonia.Input/TouchDevice.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
@ -11,10 +12,11 @@ namespace Avalonia.Input
/// This class is supposed to be used on per-toplevel basis, don't use a shared one /// This class is supposed to be used on per-toplevel basis, don't use a shared one
/// </remarks> /// </remarks>
/// </summary> /// </summary>
public class TouchDevice : IInputDevice public class TouchDevice : IInputDevice, IDisposable
{ {
Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>(); private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
private bool _disposed;
KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) =>
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
@ -28,6 +30,8 @@ namespace Avalonia.Input
public void ProcessRawEvent(RawInputEventArgs ev) public void ProcessRawEvent(RawInputEventArgs ev)
{ {
if(_disposed)
return;
var args = (RawTouchEventArgs)ev; var args = (RawTouchEventArgs)ev;
if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
{ {
@ -82,6 +86,17 @@ namespace Avalonia.Input
} }
public void Dispose()
{
if(_disposed)
return;
var values = _pointers.Values.ToList();
_pointers.Clear();
_disposed = true;
foreach (var p in values)
p.Dispose();
}
} }
} }

1
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -95,7 +95,6 @@ namespace Avalonia.Native
.Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind<IPlatformIconLoader>().ToSingleton<IconLoader>() .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice) .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IMouseDevice>().ToConstant(MouseDevice)
.Bind<IPlatformSettings>().ToConstant(this) .Bind<IPlatformSettings>().ToConstant(this)
.Bind<IWindowingPlatform>().ToConstant(this) .Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))

5
src/Avalonia.Native/WindowImplBase.cs

@ -24,7 +24,7 @@ namespace Avalonia.Native
private object _syncRoot = new object(); private object _syncRoot = new object();
private bool _deferredRendering = false; private bool _deferredRendering = false;
private bool _gpu = false; private bool _gpu = false;
private readonly IMouseDevice _mouse; private readonly MouseDevice _mouse;
private readonly IKeyboardDevice _keyboard; private readonly IKeyboardDevice _keyboard;
private readonly IStandardCursorFactory _cursorFactory; private readonly IStandardCursorFactory _cursorFactory;
private Size _savedLogicalSize; private Size _savedLogicalSize;
@ -38,7 +38,7 @@ namespace Avalonia.Native
_deferredRendering = opts.UseDeferredRendering; _deferredRendering = opts.UseDeferredRendering;
_keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>(); _keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
_mouse = AvaloniaLocator.Current.GetService<IMouseDevice>(); _mouse = new MouseDevice();
_cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>(); _cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
} }
@ -142,6 +142,7 @@ namespace Avalonia.Native
{ {
n?.Dispose(); n?.Dispose();
} }
_parent._mouse.Dispose();
} }
void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke(); void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke();

58
src/Avalonia.Themes.Default/GridSplitter.xaml

@ -1,51 +1,23 @@
<Styles xmlns="https://github.com/avaloniaui"> <Styles xmlns="https://github.com/avaloniaui">
<Style Selector="GridSplitter:vertical">
<Setter Property="Width" Value="6"/> <Style Selector="GridSplitter">
<Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/> <Setter Property="Focusable" Value="True" />
<Setter Property="Template"> <Setter Property="MinWidth" Value="6" />
<ControlTemplate> <Setter Property="MinHeight" Value="6" />
<Border Background="{TemplateBinding Background}"> <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}" />
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center"> <Setter Property="PreviewContent">
<StackPanel.Styles> <Template>
<Style Selector="Ellipse"> <Rectangle Fill="{DynamicResource HighlightBrush}" />
<Setter Property="HorizontalAlignment" Value="Center"/> </Template>
<Setter Property="Width" Value="4"/>
<Setter Property="Height" Value="4"/>
<Setter Property="Fill" Value="{DynamicResource ThemeControlMidBrush}"/>
<Setter Property="Margin" Value="1"/>
</Style>
</StackPanel.Styles>
<Ellipse/>
<Ellipse/>
<Ellipse/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter> </Setter>
</Style>
<Style Selector="GridSplitter:horizontal">
<Setter Property="Height" Value="6"/>
<Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{TemplateBinding Background}"> <Border
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> BorderBrush="{TemplateBinding BorderBrush}"
<StackPanel.Styles> BorderThickness="{TemplateBinding BorderThickness}"
<Style Selector="Ellipse"> Background="{TemplateBinding Background}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Width" Value="4"/>
<Setter Property="Height" Value="4"/>
<Setter Property="Fill" Value="{DynamicResource ThemeControlMidBrush}"/>
<Setter Property="Margin" Value="1"/>
</Style>
</StackPanel.Styles>
<Ellipse/>
<Ellipse/>
<Ellipse/>
</StackPanel>
</Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>
</Styles>
</Styles>

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

@ -5,12 +5,16 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Platform;
namespace Avalonia.Media namespace Avalonia.Media
{ {
public class FontFamily public sealed class FontFamily
{ {
static FontFamily()
{
Default = new FontFamily(FontManager.Default.DefaultFontFamilyName);
}
/// <inheritdoc /> /// <inheritdoc />
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="T:Avalonia.Media.FontFamily" /> class. /// Initializes a new instance of the <see cref="T:Avalonia.Media.FontFamily" /> class.
@ -30,9 +34,7 @@ namespace Avalonia.Media
{ {
if (string.IsNullOrEmpty(name)) if (string.IsNullOrEmpty(name))
{ {
FamilyNames = new FamilyNameCollection(string.Empty); throw new ArgumentNullException(nameof(name));
return;
} }
var fontFamilySegment = GetFontFamilyIdentifier(name); var fontFamilySegment = GetFontFamilyIdentifier(name);
@ -53,13 +55,16 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Represents the default font family /// Represents the default font family
/// </summary> /// </summary>
public static FontFamily Default => new FontFamily(string.Empty); public static FontFamily Default { get; }
/// <summary> /// <summary>
/// Represents all font families in the system. This can be an expensive call depending on platform implementation. /// Represents all font families in the system. This can be an expensive call depending on platform implementation.
/// </summary> /// </summary>
/// <remarks>
/// Consider using the new <see cref="FontManager"/> instead.
/// </remarks>
public static IEnumerable<FontFamily> SystemFontFamilies => public static IEnumerable<FontFamily> SystemFontFamilies =>
AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().InstalledFontNames.Select(name => new FontFamily(name)); FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
/// <summary> /// <summary>
/// Gets the primary family name of the font family. /// Gets the primary family name of the font family.
@ -181,7 +186,14 @@ namespace Avalonia.Media
{ {
var hash = (int)2186146271; var hash = (int)2186146271;
hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); if (Key != null)
{
hash = (hash * 15768619) ^ Key.GetHashCode();
}
else
{
hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
}
if (Key != null) if (Key != null)
{ {

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

@ -0,0 +1,112 @@
// 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 System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media
{
/// <summary>
/// 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 static readonly FontManager Default = CreateDefault();
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
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>
/// </summary>
public abstract IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Get a cached 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 cached typeface.
/// </returns>
public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
/// Returns <c>null</c> if no fallback was found.
/// </summary>
/// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontWeight">The font weight.</param>
/// <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>
/// <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
{
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;
}
}
}

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

@ -9,7 +9,7 @@ using System.Text;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
{ {
public class FamilyNameCollection : IEnumerable<string> public sealed class FamilyNameCollection : IReadOnlyList<string>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FamilyNameCollection"/> class. /// Initializes a new instance of the <see cref="FamilyNameCollection"/> class.
@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts
return other.ToString().Equals(ToString()); return other.ToString().Equals(ToString());
} }
public int Count => Names.Count;
public string this[int index] => Names[index];
} }
} }

47
src/Avalonia.Visuals/Media/FormattedText.cs

@ -16,9 +16,10 @@ namespace Avalonia.Media
private IFormattedTextImpl _platformImpl; private IFormattedTextImpl _platformImpl;
private IReadOnlyList<FormattedTextStyleSpan> _spans; private IReadOnlyList<FormattedTextStyleSpan> _spans;
private Typeface _typeface; private Typeface _typeface;
private double _fontSize;
private string _text; private string _text;
private TextAlignment _textAlignment; private TextAlignment _textAlignment;
private TextWrapping _wrapping; private TextWrapping _textWrapping;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FormattedText"/> class. /// Initializes a new instance of the <see cref="FormattedText"/> class.
@ -37,6 +38,31 @@ namespace Avalonia.Media
_platform = platform; _platform = platform;
} }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="typeface"></param>
/// <param name="fontSize"></param>
/// <param name="textAlignment"></param>
/// <param name="textWrapping"></param>
/// <param name="constraint"></param>
public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
TextWrapping textWrapping, Size constraint)
{
_text = text;
_typeface = typeface;
_fontSize = fontSize;
_textAlignment = textAlignment;
_textWrapping = textWrapping;
_constraint = constraint;
}
/// <summary> /// <summary>
/// Gets the bounds of the text within the <see cref="Constraint"/>. /// Gets the bounds of the text within the <see cref="Constraint"/>.
/// </summary> /// </summary>
@ -61,6 +87,16 @@ namespace Avalonia.Media
set => Set(ref _typeface, value); set => Set(ref _typeface, value);
} }
/// <summary>
/// Gets or sets the font size.
/// </summary>
public double FontSize
{
get => _fontSize;
set => Set(ref _fontSize, value);
}
/// <summary> /// <summary>
/// Gets or sets a collection of spans that describe the formatting of subsections of the /// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text. /// text.
@ -92,10 +128,10 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the text wrapping. /// Gets or sets the text wrapping.
/// </summary> /// </summary>
public TextWrapping Wrapping public TextWrapping TextWrapping
{ {
get => _wrapping; get => _textWrapping;
set => Set(ref _wrapping, value); set => Set(ref _textWrapping, value);
} }
/// <summary> /// <summary>
@ -110,8 +146,9 @@ namespace Avalonia.Media
_platformImpl = _platform.CreateFormattedText( _platformImpl = _platform.CreateFormattedText(
_text, _text,
_typeface, _typeface,
_fontSize,
_textAlignment, _textAlignment,
_wrapping, _textWrapping,
_constraint, _constraint,
_spans); _spans);
} }

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

@ -0,0 +1,111 @@
// 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;
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(IGlyphTypefaceImpl platformImpl)
{
PlatformImpl = platformImpl;
}
public IGlyphTypefaceImpl PlatformImpl { get; }
/// <summary>
/// Gets the font design units per em.
/// </summary>
public short DesignEmHeight => PlatformImpl.DesignEmHeight;
/// <summary>
/// Gets the recommended distance above the baseline in design em size.
/// </summary>
public int Ascent => PlatformImpl.Ascent;
/// <summary>
/// Gets the recommended distance under the baseline in design em size.
/// </summary>
public int Descent => PlatformImpl.Descent;
/// <summary>
/// Gets the recommended additional space between two lines of text in design em size.
/// </summary>
public int LineGap => PlatformImpl.LineGap;
/// <summary>
/// Gets the recommended line height.
/// </summary>
public int LineHeight => Descent - Ascent + LineGap;
/// <summary>
/// Gets a value that indicates the distance of the underline from the baseline in design em size.
/// </summary>
public int UnderlinePosition => PlatformImpl.UnderlinePosition;
/// <summary>
/// Gets a value that indicates the thickness of the underline in design em size.
/// </summary>
public int UnderlineThickness => PlatformImpl.UnderlineThickness;
/// <summary>
/// Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
/// </summary>
public int StrikethroughPosition => PlatformImpl.StrikethroughPosition;
/// <summary>
/// Gets a value that indicates the thickness of the underline in design em size.
/// </summary>
public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>
/// <remarks>
/// Returns <c>0</c> if a glyph isn't found.
/// </remarks>
/// <param name="codepoint">The codepoint.</param>
/// <returns>
/// A glyph index.
/// </returns>
public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint);
/// <summary>
/// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
/// </summary>
/// <param name="codepoints">The codepoints to map.</param>
/// <returns></returns>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) => PlatformImpl.GetGlyphs(codepoints);
/// <summary>
/// Returns the glyph advance for the specified glyph.
/// </summary>
/// <param name="glyph">The glyph.</param>
/// <returns>
/// The advance.
/// </returns>
public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph);
/// <summary>
/// Returns an array of glyph advances in design em size.
/// </summary>
/// <param name="glyphs">The glyph indices.</param>
/// <returns></returns>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) => PlatformImpl.GetGlyphAdvances(glyphs);
void IDisposable.Dispose()
{
PlatformImpl?.Dispose();
}
}
}

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

@ -1,39 +1,38 @@
using System; // 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.Diagnostics;
using JetBrains.Annotations;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Represents a typeface. /// Represents a typeface.
/// </summary> /// </summary>
public class Typeface [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")]
public class Typeface : IEquatable<Typeface>
{ {
public static readonly Typeface Default = new Typeface(FontFamily.Default); public static readonly Typeface Default = new Typeface(FontFamily.Default);
private GlyphTypeface _glyphTypeface;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Typeface"/> class. /// Initializes a new instance of the <see cref="Typeface"/> class.
/// </summary> /// </summary>
/// <param name="fontFamily">The font family.</param> /// <param name="fontFamily">The font family.</param>
/// <param name="fontSize">The font size, in DIPs.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param> /// <param name="weight">The font weight.</param>
public Typeface( /// <param name="style">The font style.</param>
FontFamily fontFamily, public Typeface([NotNull]FontFamily fontFamily,
double fontSize = 12, FontWeight weight = FontWeight.Normal,
FontStyle style = FontStyle.Normal, FontStyle style = FontStyle.Normal)
FontWeight weight = FontWeight.Normal)
{ {
if (fontSize <= 0)
{
throw new ArgumentException("Font size must be > 0.");
}
if (weight <= 0) if (weight <= 0)
{ {
throw new ArgumentException("Font weight must be > 0."); throw new ArgumentException("Font weight must be > 0.");
} }
FontFamily = fontFamily; FontFamily = fontFamily;
FontSize = fontSize;
Style = style; Style = style;
Weight = weight; Weight = weight;
} }
@ -42,15 +41,12 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="Typeface"/> class. /// Initializes a new instance of the <see cref="Typeface"/> class.
/// </summary> /// </summary>
/// <param name="fontFamilyName">The name of the font family.</param> /// <param name="fontFamilyName">The name of the font family.</param>
/// <param name="fontSize">The font size, in DIPs.</param>
/// <param name="style">The font style.</param> /// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param> /// <param name="weight">The font weight.</param>
public Typeface( public Typeface(string fontFamilyName,
string fontFamilyName, FontWeight weight = FontWeight.Normal,
double fontSize = 12, FontStyle style = FontStyle.Normal)
FontStyle style = FontStyle.Normal, : this(new FontFamily(fontFamilyName), weight, style)
FontWeight weight = FontWeight.Normal)
: this(new FontFamily(fontFamilyName), fontSize, style, weight)
{ {
} }
@ -59,11 +55,6 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public FontFamily FontFamily { get; } public FontFamily FontFamily { get; }
/// <summary>
/// Gets the size of the font in DIPs.
/// </summary>
public double FontSize { get; }
/// <summary> /// <summary>
/// Gets the font style. /// Gets the font style.
/// </summary> /// </summary>
@ -73,5 +64,59 @@ namespace Avalonia.Media
/// Gets the font weight. /// Gets the font weight.
/// </summary> /// </summary>
public FontWeight Weight { get; } public FontWeight Weight { get; }
/// <summary>
/// Gets the glyph typeface.
/// </summary>
/// <value>
/// The glyph typeface.
/// </value>
public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this));
public static bool operator !=(Typeface a, Typeface b)
{
return !(a == b);
}
public static bool operator ==(Typeface a, Typeface b)
{
if (ReferenceEquals(a, b))
{
return true;
}
return !(a is null) && a.Equals(b);
}
public override bool Equals(object obj)
{
if (obj is Typeface typeface)
{
return Equals(typeface);
}
return false;
}
public bool Equals(Typeface other)
{
if (other is null)
{
return false;
}
return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight;
}
public override int GetHashCode()
{
unchecked
{
var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (int)Style;
hashCode = (hashCode * 397) ^ (int)Weight;
return hashCode;
}
}
} }
} }

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

@ -0,0 +1,48 @@
// 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 System.Globalization;
using Avalonia.Media;
namespace Avalonia.Platform
{
public interface IFontManagerImpl
{
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
string DefaultFontFamilyName { get; }
/// <summary>
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </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>
/// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontWeight">The font weight.</param>
/// <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>
/// <returns>
/// The typeface.
/// </returns>
Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
}
}

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

@ -0,0 +1,89 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Platform
{
public interface IGlyphTypefaceImpl : IDisposable
{
/// <summary>
/// Gets the font design units per em.
/// </summary>
short DesignEmHeight { get; }
/// <summary>
/// Gets the recommended distance above the baseline in design em size.
/// </summary>
int Ascent { get; }
/// <summary>
/// Gets the recommended distance under the baseline in design em size.
/// </summary>
int Descent { get; }
/// <summary>
/// Gets the recommended additional space between two lines of text in design em size.
/// </summary>
int LineGap { get; }
/// <summary>
/// Gets a value that indicates the distance of the underline from the baseline in design em size.
/// </summary>
int UnderlinePosition { get; }
/// <summary>
/// Gets a value that indicates the thickness of the underline in design em size.
/// </summary>
int UnderlineThickness { get; }
/// <summary>
/// Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
/// </summary>
int StrikethroughPosition { get; }
/// <summary>
/// Gets a value that indicates the thickness of the underline in design em size.
/// </summary>
int StrikethroughThickness { get; }
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>
/// <remarks>
/// Returns <c>0</c> if a glyph isn't found.
/// </remarks>
/// <param name="codepoint">The codepoint.</param>
/// <returns>
/// A glyph index.
/// </returns>
ushort GetGlyph(uint codepoint);
/// <summary>
/// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
/// </summary>
/// <param name="codepoints">The codepoints to map.</param>
/// <returns>
/// An array of glyph indices.
/// </returns>
ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints);
/// <summary>
/// Returns the glyph advance for the specified glyph.
/// </summary>
/// <param name="glyph">The glyph.</param>
/// <returns>
/// The advance.
/// </returns>
int GetGlyphAdvance(ushort glyph);
/// <summary>
/// Returns an array of glyph advances in design em size.
/// </summary>
/// <param name="glyphs">The glyph indices.</param>
/// <returns>
/// An array of glyph advances.
/// </returns>
int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs);
}
}

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

@ -13,16 +13,12 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
public interface IPlatformRenderInterface public interface IPlatformRenderInterface
{ {
/// <summary>
/// Get all installed fonts in the system
/// </summary>
IEnumerable<string> InstalledFontNames { get; }
/// <summary> /// <summary>
/// Creates a formatted text implementation. /// Creates a formatted text implementation.
/// </summary> /// </summary>
/// <param name="text">The text.</param> /// <param name="text">The text.</param>
/// <param name="typeface">The base typeface.</param> /// <param name="typeface">The base typeface.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="textAlignment">The text alignment.</param> /// <param name="textAlignment">The text alignment.</param>
/// <param name="wrapping">The text wrapping mode.</param> /// <param name="wrapping">The text wrapping mode.</param>
/// <param name="constraint">The text layout constraints.</param> /// <param name="constraint">The text layout constraints.</param>
@ -31,6 +27,7 @@ namespace Avalonia.Platform
IFormattedTextImpl CreateFormattedText( IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -114,5 +111,14 @@ namespace Avalonia.Platform
/// <param name="stride">The number of bytes per row.</param> /// <param name="stride">The number of bytes per row.</param>
/// <returns>An <see cref="IBitmapImpl"/>.</returns> /// <returns>An <see cref="IBitmapImpl"/>.</returns>
IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride); IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride);
/// <summary>
/// Creates a glyph typeface for specified typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>
/// The glyph typeface implementation.
/// </returns>
IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
} }
} }

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

@ -7,7 +7,8 @@ namespace Avalonia.Rendering
{ {
public class RendererBase public class RendererBase
{ {
private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18); private static readonly Typeface s_fpsTypeface = new Typeface("Arial");
private static int s_fontSize = 18;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond; private int _framesThisSecond;
private int _fps; private int _fps;
@ -18,7 +19,8 @@ namespace Avalonia.Rendering
{ {
_fpsText = new FormattedText _fpsText = new FormattedText
{ {
Typeface = s_fpsTypeface Typeface = s_fpsTypeface,
FontSize = s_fontSize
}; };
} }

2
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -51,7 +51,7 @@ namespace Avalonia.Rendering.SceneGraph
UpdateSize(scene); UpdateSize(scene);
} }
if (visual.VisualRoot != null) if (visual.VisualRoot == scene.Root.Visual)
{ {
if (node?.Parent != null && if (node?.Parent != null &&
visual.VisualParent != null && visual.VisualParent != null &&

2
src/Avalonia.X11/X11Platform.cs

@ -19,9 +19,7 @@ namespace Avalonia.X11
class AvaloniaX11Platform : IWindowingPlatform class AvaloniaX11Platform : IWindowingPlatform
{ {
private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice()); private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
private Lazy<MouseDevice> _mouseDevice = new Lazy<MouseDevice>(() => new MouseDevice());
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value; public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
public MouseDevice MouseDevice => _mouseDevice.Value;
public Dictionary<IntPtr, Action<XEvent>> Windows = new Dictionary<IntPtr, Action<XEvent>>(); public Dictionary<IntPtr, Action<XEvent>> Windows = new Dictionary<IntPtr, Action<XEvent>>();
public XI2Manager XI2; public XI2Manager XI2;
public X11Info Info { get; private set; } public X11Info Info { get; private set; }

10
src/Avalonia.X11/X11Window.cs

@ -32,7 +32,8 @@ namespace Avalonia.X11
private PixelPoint? _configurePoint; private PixelPoint? _configurePoint;
private bool _triggeredExpose; private bool _triggeredExpose;
private IInputRoot _inputRoot; private IInputRoot _inputRoot;
private readonly IMouseDevice _mouse; private readonly MouseDevice _mouse;
private readonly TouchDevice _touch;
private readonly IKeyboardDevice _keyboard; private readonly IKeyboardDevice _keyboard;
private PixelPoint? _position; private PixelPoint? _position;
private PixelSize _realSize; private PixelSize _realSize;
@ -57,7 +58,8 @@ namespace Avalonia.X11
_platform = platform; _platform = platform;
_popup = popupParent != null; _popup = popupParent != null;
_x11 = platform.Info; _x11 = platform.Info;
_mouse = platform.MouseDevice; _mouse = new MouseDevice();
_touch = new TouchDevice();
_keyboard = platform.KeyboardDevice; _keyboard = platform.KeyboardDevice;
var glfeature = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>(); var glfeature = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
@ -702,6 +704,8 @@ namespace Avalonia.X11
_platform.XI2?.OnWindowDestroyed(_handle); _platform.XI2?.OnWindowDestroyed(_handle);
_handle = IntPtr.Zero; _handle = IntPtr.Zero;
Closed?.Invoke(); Closed?.Invoke();
_mouse.Dispose();
_touch.Dispose();
} }
if (_useRenderWindow && _renderHandle != IntPtr.Zero) if (_useRenderWindow && _renderHandle != IntPtr.Zero)
@ -830,6 +834,8 @@ namespace Avalonia.X11
} }
public IMouseDevice MouseDevice => _mouse; public IMouseDevice MouseDevice => _mouse;
public TouchDevice TouchDevice => _touch;
public IPopupImpl CreatePopup() public IPopupImpl CreatePopup()
=> _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this);

12
src/Avalonia.X11/XI2Manager.cs

@ -92,8 +92,6 @@ namespace Avalonia.X11
private PointerDeviceInfo _pointerDevice; private PointerDeviceInfo _pointerDevice;
private AvaloniaX11Platform _platform; private AvaloniaX11Platform _platform;
private readonly TouchDevice _touchDevice = new TouchDevice();
public bool Init(AvaloniaX11Platform platform) public bool Init(AvaloniaX11Platform platform)
{ {
@ -198,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ? (ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate : RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd); RawPointerEventType.TouchEnd);
client.ScheduleInput(new RawTouchEventArgs(_touchDevice, client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return; return;
} }
@ -232,10 +230,10 @@ namespace Avalonia.X11
} }
if (scrollDelta != default) if (scrollDelta != default)
client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp, client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
if (_pointerDevice.HasMotion(ev)) if (_pointerDevice.HasMotion(ev))
client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
RawPointerEventType.Move, ev.Position, ev.Modifiers)); RawPointerEventType.Move, ev.Position, ev.Modifiers));
} }
@ -248,7 +246,7 @@ namespace Avalonia.X11
: ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp) : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp)
: (RawPointerEventType?)null; : (RawPointerEventType?)null;
if (type.HasValue) if (type.HasValue)
client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers)); type.Value, ev.Position, ev.Modifiers));
} }
@ -310,5 +308,7 @@ namespace Avalonia.X11
{ {
IInputRoot InputRoot { get; } IInputRoot InputRoot { get; }
void ScheduleInput(RawInputEventArgs args); void ScheduleInput(RawInputEventArgs args);
IMouseDevice MouseDevice { get; }
TouchDevice TouchDevice { get; }
} }
} }

1
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@ -12,5 +12,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\SkiaSharp.props" /> <Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" /> <Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
</Project> </Project>

40
src/Skia/Avalonia.Skia/FontKey.cs

@ -0,0 +1,40 @@
// 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.Media;
namespace Avalonia.Skia
{
internal readonly struct FontKey : IEquatable<FontKey>
{
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontWeight weight, FontStyle style)
{
Style = style;
Weight = weight;
}
public override int GetHashCode()
{
var hash = 17;
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
return hash;
}
public override bool Equals(object other)
{
return other is FontKey key && Equals(key);
}
public bool Equals(FontKey other)
{
return Style == other.Style &&
Weight == other.Weight;
}
}
}

82
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -0,0 +1,82 @@
// 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 System.Globalization;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class FontManagerImpl : IFontManagerImpl
{
private SKFontManager _skFontManager = SKFontManager.Default;
public FontManagerImpl()
{
DefaultFontFamilyName = SKTypeface.Default.FamilyName;
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
{
_skFontManager = SKFontManager.CreateDefault();
}
return _skFontManager.FontFamilies;
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
}
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
{
var fontFamilyName = FontFamily.Default.Name;
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
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);
if (skTypeface == null)
{
continue;
}
fontFamilyName = familyName;
break;
}
}
else
{
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
(SKFontStyleSlant)fontStyle,
new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
if (skTypeface != null)
{
fontFamilyName = skTypeface.FamilyName;
}
}
return GetTypeface(fontFamilyName, fontWeight, fontStyle);
}
}
}

80
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -18,6 +18,7 @@ namespace Avalonia.Skia
public FormattedTextImpl( public FormattedTextImpl(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -28,47 +29,22 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B) // Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B); Text = Text.Replace((char)0, (char)0x200B);
SKTypeface skiaTypeface = null; var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
if (typeface.FontFamily.Key != null) _paint = new SKPaint
{ {
var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); TextEncoding = SKTextEncoding.Utf16,
skiaTypeface = typefaces.GetTypeFace(typeface); IsStroke = false,
} IsAntialias = true,
else LcdRenderText = true,
{ SubpixelText = true,
if (typeface.FontFamily.FamilyNames.HasFallbacks) Typeface = entry.SKTypeface,
{ TextSize = (float)fontSize,
foreach (var familyName in typeface.FontFamily.FamilyNames) TextAlign = textAlignment.ToSKTextAlign()
{ };
skiaTypeface = TypefaceCache.GetTypeface(
familyName,
typeface.Style,
typeface.Weight);
if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break;
}
}
else
{
skiaTypeface = TypefaceCache.GetTypeface(
typeface.FontFamily.Name,
typeface.Style,
typeface.Weight);
}
}
_paint = new SKPaint();
//currently Skia does not measure properly with Utf8 !!! //currently Skia does not measure properly with Utf8 !!!
//Paint.TextEncoding = SKTextEncoding.Utf8; //Paint.TextEncoding = SKTextEncoding.Utf8;
_paint.TextEncoding = SKTextEncoding.Utf16;
_paint.IsStroke = false;
_paint.IsAntialias = true;
_paint.LcdRenderText = true;
_paint.SubpixelText = true;
_paint.Typeface = skiaTypeface;
_paint.TextSize = (float)typeface.FontSize;
_paint.TextAlign = textAlignment.ToSKTextAlign();
_wrapping = wrapping; _wrapping = wrapping;
_constraint = constraint; _constraint = constraint;
@ -99,7 +75,24 @@ namespace Avalonia.Skia
public TextHitTestResult HitTestPoint(Point point) public TextHitTestResult HitTestPoint(Point point)
{ {
float y = (float)point.Y; float y = (float)point.Y;
var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y);
AvaloniaFormattedTextLine line = default;
float nextTop = 0;
foreach(var currentLine in _skiaLines)
{
if(currentLine.Top <= y)
{
line = currentLine;
nextTop = currentLine.Top + currentLine.Height;
}
else
{
nextTop = currentLine.Top;
break;
}
}
if (!line.Equals(default(AvaloniaFormattedTextLine))) if (!line.Equals(default(AvaloniaFormattedTextLine)))
{ {
@ -127,12 +120,15 @@ namespace Avalonia.Skia
line.Length : (line.Length - 1); line.Length : (line.Length - 1);
} }
return new TextHitTestResult if (y < nextTop)
{ {
IsInside = false, return new TextHitTestResult
TextPosition = line.Start + offset, {
IsTrailing = Text.Length == (line.Start + offset + 1) IsInside = false,
}; TextPosition = line.Start + offset,
IsTrailing = Text.Length == (line.Start + offset + 1)
};
}
} }
bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height); bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height);

179
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -0,0 +1,179 @@
// 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.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Platform;
using HarfBuzzSharp;
using SkiaSharp;
namespace Avalonia.Skia
{
public class GlyphTypefaceImpl : IGlyphTypefaceImpl
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
{
Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface;
Face = new Face(GetTable)
{
UnitsPerEm = Typeface.UnitsPerEm
};
Font = new Font(Face);
Font.SetFunctionsOpenType();
Font.GetScale(out var xScale, out _);
DesignEmHeight = (short)xScale;
if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
{
Font.TryGetVerticalFontExtents(out fontExtents);
}
Ascent = -fontExtents.Ascender;
Descent = -fontExtents.Descender;
LineGap = fontExtents.LineGap;
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
{
UnderlinePosition = underlinePosition;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
{
UnderlineThickness = underlineThickness;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
{
StrikethroughPosition = strikethroughPosition;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
{
StrikethroughThickness = strikethroughThickness;
}
}
public Face Face { get; }
public Font Font { get; }
public SKTypeface Typeface { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public short DesignEmHeight { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Ascent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Descent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int LineGap { get; }
//ToDo: Get these values from HarfBuzz
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlinePosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlineThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughPosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{
if (Font.TryGetGlyph(codepoint, out var glyph))
{
return (ushort)glyph;
}
return 0;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
var glyphs = new ushort[codepoints.Length];
for (var i = 0; i < codepoints.Length; i++)
{
if (Font.TryGetGlyph(codepoints[i], out var glyph))
{
glyphs[i] = (ushort)glyph;
}
}
return glyphs;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int GetGlyphAdvance(ushort glyph)
{
return Font.GetHorizontalGlyphAdvance(glyph);
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var glyphIndices = new uint[glyphs.Length];
for (var i = 0; i < glyphs.Length; i++)
{
glyphIndices[i] = glyphs[i];
}
return Font.GetHorizontalGlyphAdvances(glyphIndices);
}
private Blob GetTable(Face face, Tag tag)
{
var size = Typeface.GetTableSize(tag);
var data = Marshal.AllocCoTaskMem(size);
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
return Typeface.TryGetTableData(tag, 0, size, data) ?
new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
}
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
Font?.Dispose();
Face?.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

14
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Platform.Surfaces;
@ -17,12 +18,13 @@ namespace Avalonia.Skia
/// </summary> /// </summary>
internal class PlatformRenderInterface : IPlatformRenderInterface internal class PlatformRenderInterface : IPlatformRenderInterface
{ {
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private readonly ICustomSkiaGpu _customSkiaGpu; private readonly ICustomSkiaGpu _customSkiaGpu;
private GRContext GrContext { get; } private GRContext GrContext { get; }
public IEnumerable<string> InstalledFontNames => SKFontManager.Default.FontFamilies;
public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
{ {
if (customSkiaGpu != null) if (customSkiaGpu != null)
@ -52,12 +54,13 @@ namespace Avalonia.Skia
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans) IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans);
} }
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
@ -151,5 +154,10 @@ namespace Avalonia.Skia
{ {
return new WriteableBitmapImpl(size, dpi, format); return new WriteableBitmapImpl(size, dpi, format);
} }
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
}
} }
} }

91
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -4,114 +4,59 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
internal class SKTypefaceCollection internal class SKTypefaceCollection
{ {
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, SKTypeface>> _fontFamilies = private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> _fontFamilies =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, SKTypeface>>(); new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
public void AddTypeFace(SKTypeface typeface) public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry)
{ {
var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); if (!_fontFamilies.TryGetValue(familyName, out var fontFamily))
if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily))
{ {
fontFamily = new ConcurrentDictionary<FontKey, SKTypeface>(); fontFamily = new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>();
_fontFamilies.TryAdd(typeface.FamilyName, fontFamily); _fontFamilies.TryAdd(familyName, fontFamily);
} }
fontFamily.TryAdd(key, typeface); fontFamily.TryAdd(key, entry);
} }
public SKTypeface GetTypeFace(Typeface typeface) public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle)
{ {
var styleSlant = SKFontStyleSlant.Upright; var key = new FontKey(fontWeight, fontStyle);
switch (typeface.Style)
{
case FontStyle.Italic:
styleSlant = SKFontStyleSlant.Italic;
break;
case FontStyle.Oblique:
styleSlant = SKFontStyleSlant.Oblique;
break;
}
if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily)) return _fontFamilies.TryGetValue(familyName, out var fontFamily) ?
{ fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) :
return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight); null;
}
var weight = (SKFontStyleWeight)typeface.Weight;
var key = new FontKey(weight, styleSlant);
return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key));
} }
private static SKTypeface GetFallback(IDictionary<FontKey, SKTypeface> fontFamily, FontKey key) private static TypefaceCollectionEntry GetFallback(IDictionary<FontKey, TypefaceCollectionEntry> fontFamily, FontKey key)
{ {
var keys = fontFamily.Keys.Where( var keys = fontFamily.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Slant == key.Slant).ToArray(); x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray();
if (!keys.Any()) if (!keys.Any())
{ {
keys = fontFamily.Keys.Where( keys = fontFamily.Keys.Where(
x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray();
if (!keys.Any()) if (!keys.Any())
{ {
keys = fontFamily.Keys.Where( keys = fontFamily.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) &&
(x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); (x.Style >= key.Style || x.Style < key.Style)).ToArray();
} }
} }
key = keys.FirstOrDefault(); key = keys.FirstOrDefault();
fontFamily.TryGetValue(key, out var typeface); fontFamily.TryGetValue(key, out var entry);
return typeface;
}
private struct FontKey
{
public readonly SKFontStyleSlant Slant;
public readonly SKFontStyleWeight Weight;
public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
{
Slant = slant;
Weight = weight;
}
public override int GetHashCode()
{
var hash = 17;
hash = (hash * 31) + (int)Slant;
hash = (hash * 31) + (int)Weight;
return hash;
}
public override bool Equals(object other) return entry;
{
return other is FontKey key && this.Equals(key);
}
private bool Equals(FontKey other)
{
return Slant == other.Slant &&
Weight == other.Weight;
}
} }
} }
} }

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

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

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

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

86
src/Skia/Avalonia.Skia/TypefaceCache.cs

@ -1,7 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic; using System.Collections.Concurrent;
using Avalonia.Media; using Avalonia.Media;
using SkiaSharp; using SkiaSharp;
@ -12,88 +12,36 @@ namespace Avalonia.Skia
/// </summary> /// </summary>
internal static class TypefaceCache internal static class TypefaceCache
{ {
public static readonly string DefaultFamilyName = CreateDefaultFamilyName(); private static readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> s_cache =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
private static readonly Dictionary<string, Dictionary<FontKey, SKTypeface>> s_cache = public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
new Dictionary<string, Dictionary<FontKey, SKTypeface>>();
struct FontKey
{ {
public readonly SKFontStyleSlant Slant; if (fontFamily.Key != null)
public readonly SKFontStyleWeight Weight;
public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
{ {
Slant = slant; return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily)
Weight = weight; .Get(fontFamily.Name, fontWeight, fontStyle);
} }
public override int GetHashCode() var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>());
{
int hash = 17;
hash = hash * 31 + (int)Slant;
hash = hash * 31 + (int)Weight;
return hash;
}
public override bool Equals(object other)
{
return other is FontKey ? Equals((FontKey)other) : false;
}
public bool Equals(FontKey other)
{
return Slant == other.Slant &&
Weight == other.Weight;
}
// Equals and GetHashCode ommitted
}
private static string CreateDefaultFamilyName()
{
var defaultTypeface = SKTypeface.CreateDefault();
return defaultTypeface.FamilyName; var key = new FontKey(fontWeight, fontStyle);
}
private static SKTypeface GetTypeface(string name, FontKey key) if (typefaceCollection.TryGetValue(key, out var entry))
{
var familyKey = name;
if (!s_cache.TryGetValue(familyKey, out var entry))
{ {
s_cache[familyKey] = entry = new Dictionary<FontKey, SKTypeface>(); return entry;
} }
if (!entry.TryGetValue(key, out var typeface)) var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight,
{ SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default;
typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ??
GetTypeface(DefaultFamilyName, key);
entry[key] = typeface; var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle);
}
return typeface; entry = new TypefaceCollectionEntry(typeface, skTypeface);
}
public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight)
{
var skStyle = SKFontStyleSlant.Upright;
switch (style) typefaceCollection[key] = entry;
{
case FontStyle.Italic:
skStyle = SKFontStyleSlant.Italic;
break;
case FontStyle.Oblique:
skStyle = SKFontStyleSlant.Oblique;
break;
}
return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle)); return entry;
} }
} }
} }

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

@ -0,0 +1,19 @@
// 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; }
}
}

1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@ -14,6 +14,7 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\Rx.props" /> <Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\SharpDX.props" /> <Import Project="..\..\..\build\SharpDX.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" /> <Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
<Import Project="..\..\..\build\JetBrains.Annotations.props" /> <Import Project="..\..\..\build\JetBrains.Annotations.props" />
</Project> </Project>

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

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Avalonia.Controls; using Avalonia.Controls;
@ -27,6 +28,8 @@ namespace Avalonia.Direct2D1
{ {
public class Direct2D1Platform : IPlatformRenderInterface public class Direct2D1Platform : IPlatformRenderInterface
{ {
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private static readonly Direct2D1Platform s_instance = new Direct2D1Platform(); private static readonly Direct2D1Platform s_instance = new Direct2D1Platform();
public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; } public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; }
@ -41,20 +44,6 @@ namespace Avalonia.Direct2D1
public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; } public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; }
public IEnumerable<string> InstalledFontNames
{
get
{
var cache = Direct2D1FontCollectionCache.s_installedFontCollection;
var length = cache.FontFamilyCount;
for (int i = 0; i < length; i++)
{
var names = cache.GetFontFamily(i).FamilyNames;
yield return names.GetString(0);
}
}
}
private static readonly object s_initLock = new object(); private static readonly object s_initLock = new object();
private static bool s_initialized = false; private static bool s_initialized = false;
@ -120,6 +109,7 @@ namespace Avalonia.Direct2D1
{ {
InitializeDirect2D(); InitializeDirect2D();
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(s_instance); AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(s_instance);
AvaloniaLocator.CurrentMutable.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true; SharpDX.Configuration.EnableReleaseOnFinalizer = true;
} }
@ -131,6 +121,7 @@ namespace Avalonia.Direct2D1
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -139,6 +130,7 @@ namespace Avalonia.Direct2D1
return new FormattedTextImpl( return new FormattedTextImpl(
text, text,
typeface, typeface,
fontSize,
textAlignment, textAlignment,
wrapping, wrapping,
constraint, constraint,
@ -201,5 +193,10 @@ namespace Avalonia.Direct2D1
{ {
return new WicBitmapImpl(format, data, size, dpi, stride); return new WicBitmapImpl(format, data, size, dpi, stride);
} }
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
}
} }
} }

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

@ -1,62 +1,61 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
using FontStyle = SharpDX.DirectWrite.FontStyle;
using FontWeight = SharpDX.DirectWrite.FontWeight;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
internal static class Direct2D1FontCollectionCache internal static class Direct2D1FontCollectionCache
{ {
private static readonly ConcurrentDictionary<FontFamilyKey, SharpDX.DirectWrite.FontCollection> s_cachedCollections; private static readonly ConcurrentDictionary<FontFamilyKey, FontCollection> s_cachedCollections;
internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection; internal static readonly FontCollection InstalledFontCollection;
static Direct2D1FontCollectionCache() static Direct2D1FontCollectionCache()
{ {
s_cachedCollections = new ConcurrentDictionary<FontFamilyKey, SharpDX.DirectWrite.FontCollection>(); s_cachedCollections = new ConcurrentDictionary<FontFamilyKey, FontCollection>();
s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false);
} }
public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface) public static Font GetFont(Typeface typeface)
{ {
var fontFamily = typeface.FontFamily; var fontFamily = typeface.FontFamily;
var fontCollection = GetOrAddFontCollection(fontFamily); var fontCollection = GetOrAddFontCollection(fontFamily);
var fontFamilyName = FontFamily.Default.Name;
// Should this be cached?
foreach (var familyName in fontFamily.FamilyNames) foreach (var familyName in fontFamily.FamilyNames)
{ {
if (!fontCollection.FindFamilyName(familyName, out _)) if (fontCollection.FindFamilyName(familyName, out var index))
{ {
continue; return fontCollection.GetFontFamily(index).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
FontStretch.Normal,
(FontStyle)typeface.Style);
} }
fontFamilyName = familyName;
break;
} }
return new SharpDX.DirectWrite.TextFormat( InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
Direct2D1Platform.DirectWriteFactory,
fontFamilyName, return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
fontCollection, (FontWeight)typeface.Weight,
(SharpDX.DirectWrite.FontWeight)typeface.Weight, FontStretch.Normal,
(SharpDX.DirectWrite.FontStyle)typeface.Style, (FontStyle)typeface.Style);
SharpDX.DirectWrite.FontStretch.Normal,
(float)typeface.FontSize);
} }
private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily) private static FontCollection GetOrAddFontCollection(FontFamily fontFamily)
{ {
return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
} }
private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key) private static FontCollection CreateFontCollection(FontFamilyKey key)
{ {
var assets = FontFamilyLoader.LoadFontAssets(key); var assets = FontFamilyLoader.LoadFontAssets(key);
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
} }
} }
} }

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

@ -0,0 +1,71 @@
// 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 System.Globalization;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
using FontStyle = Avalonia.Media.FontStyle;
using FontWeight = Avalonia.Media.FontWeight;
namespace Avalonia.Direct2D1.Media
{
internal class FontManagerImpl : IFontManagerImpl
{
public FontManagerImpl()
{
//ToDo: Implement a real lookup of the system's default font.
DefaultFontFamilyName = "segoe ui";
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
var fontFamilies = new string[familyCount];
for (var i = 0; i < familyCount; i++)
{
fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0);
}
return fontFamilies;
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
//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++)
{
var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i)
.GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal,
(SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0);
if (!font.HasCharacter(codepoint))
{
continue;
}
fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
break;
}
return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
}
}
}

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

@ -14,6 +14,7 @@ namespace Avalonia.Direct2D1.Media
public FormattedTextImpl( public FormattedTextImpl(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -21,20 +22,20 @@ namespace Avalonia.Direct2D1.Media
{ {
Text = text; Text = text;
using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface)) 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))
{ {
textFormat.WordWrapping = textFormat.WordWrapping =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
TextLayout = new DWrite.TextLayout( TextLayout = new DWrite.TextLayout(
Direct2D1Platform.DirectWriteFactory, Direct2D1Platform.DirectWriteFactory,
Text ?? string.Empty, Text ?? string.Empty,
textFormat, textFormat,
(float)constraint.Width, (float)constraint.Width,
(float)constraint.Height) (float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() };
{
TextAlignment = textAlignment.ToDirect2D()
};
} }
if (spans != null) if (spans != null)

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

@ -0,0 +1,188 @@
// 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.Media;
using Avalonia.Platform;
using HarfBuzzSharp;
using SharpDX.DirectWrite;
namespace Avalonia.Direct2D1.Media
{
public class GlyphTypefaceImpl : IGlyphTypefaceImpl
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
FontFace = new FontFace(DWFont);
Face = new Face(GetTable);
Font = new HarfBuzzSharp.Font(Face);
Font.SetFunctionsOpenType();
Font.GetScale(out var xScale, out _);
DesignEmHeight = (short)xScale;
if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
{
Font.TryGetVerticalFontExtents(out fontExtents);
}
Ascent = -fontExtents.Ascender;
Descent = -fontExtents.Descender;
LineGap = fontExtents.LineGap;
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
{
UnderlinePosition = underlinePosition;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
{
UnderlineThickness = underlineThickness;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
{
StrikethroughPosition = strikethroughPosition;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
{
StrikethroughThickness = strikethroughThickness;
}
}
private Blob GetTable(Face face, Tag tag)
{
var dwTag = (int)SwapBytes(tag);
if (FontFace.TryGetFontTable(dwTag, out var tableData, out _))
{
return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { });
}
return null;
}
private static uint SwapBytes(uint x)
{
x = (x >> 16) | (x << 16);
return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8);
}
public SharpDX.DirectWrite.Font DWFont { get; }
public FontFace FontFace { get; }
public Face Face { get; }
public HarfBuzzSharp.Font Font { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public short DesignEmHeight { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Ascent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Descent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int LineGap { get; }
//ToDo: Read font table for these values
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlinePosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlineThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughPosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{
if (Font.TryGetGlyph(codepoint, out var glyph))
{
return (ushort)glyph;
}
return 0;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
var glyphs = new ushort[codepoints.Length];
for (var i = 0; i < codepoints.Length; i++)
{
if (Font.TryGetGlyph(codepoints[i], out var glyph))
{
glyphs[i] = (ushort)glyph;
}
}
return glyphs;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int GetGlyphAdvance(ushort glyph)
{
return Font.GetHorizontalGlyphAdvance(glyph);
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var glyphIndices = new uint[glyphs.Length];
for (var i = 0; i < glyphs.Length; i++)
{
glyphIndices[i] = glyphs[i];
}
return Font.GetHorizontalGlyphAdvances(glyphIndices);
}
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
Font?.Dispose();
Face?.Dispose();
FontFace?.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

8
src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs

@ -11,19 +11,11 @@ namespace Avalonia.Win32.Input
{ {
class WindowsMouseDevice : MouseDevice class WindowsMouseDevice : MouseDevice
{ {
public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice();
public WindowsMouseDevice() : base(new WindowsMousePointer()) public WindowsMouseDevice() : base(new WindowsMousePointer())
{ {
} }
public WindowImpl CurrentWindow
{
get;
set;
}
class WindowsMousePointer : Pointer class WindowsMousePointer : Pointer
{ {
public WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true) public WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true)

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

@ -30,6 +30,7 @@ namespace Avalonia.Win32
private IntPtr _hwnd; private IntPtr _hwnd;
private bool _multitouch; private bool _multitouch;
private TouchDevice _touchDevice = new TouchDevice(); private TouchDevice _touchDevice = new TouchDevice();
private MouseDevice _mouseDevice = new WindowsMouseDevice();
private IInputRoot _owner; private IInputRoot _owner;
private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock();
private bool _trackingMouse; private bool _trackingMouse;
@ -205,7 +206,7 @@ namespace Avalonia.Win32
} }
} }
public IMouseDevice MouseDevice => WindowsMouseDevice.Instance; public IMouseDevice MouseDevice => _mouseDevice;
public WindowState WindowState public WindowState WindowState
{ {
@ -333,7 +334,7 @@ namespace Avalonia.Win32
public void BeginMoveDrag(PointerPressedEventArgs e) public void BeginMoveDrag(PointerPressedEventArgs e)
{ {
WindowsMouseDevice.Instance.Capture(null); _mouseDevice.Capture(null);
UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN,
new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero); new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero);
e.Pointer.Capture(null); e.Pointer.Capture(null);
@ -356,7 +357,7 @@ namespace Avalonia.Win32
#if USE_MANAGED_DRAG #if USE_MANAGED_DRAG
_managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position)); _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position));
#else #else
WindowsMouseDevice.Instance.Capture(null); _mouseDevice.Capture(null);
UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN,
new IntPtr((int)EdgeDic[edge]), IntPtr.Zero); new IntPtr((int)EdgeDic[edge]), IntPtr.Zero);
#endif #endif
@ -437,9 +438,7 @@ namespace Avalonia.Win32
uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime()); uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime());
RawInputEventArgs e = null; RawInputEventArgs e = null;
WindowsMouseDevice.Instance.CurrentWindow = this;
switch ((UnmanagedMethods.WindowsMessage)msg) switch ((UnmanagedMethods.WindowsMessage)msg)
{ {
case UnmanagedMethods.WindowsMessage.WM_ACTIVATE: case UnmanagedMethods.WindowsMessage.WM_ACTIVATE:
@ -485,6 +484,8 @@ namespace Avalonia.Win32
_parent._disabledBy.Remove(this); _parent._disabledBy.Remove(this);
_parent.UpdateEnabled(); _parent.UpdateEnabled();
} }
_mouseDevice.Dispose();
_touchDevice?.Dispose();
//Free other resources //Free other resources
Dispose(); Dispose();
return IntPtr.Zero; return IntPtr.Zero;
@ -542,7 +543,7 @@ namespace Avalonia.Win32
if(ShouldIgnoreTouchEmulatedMessage()) if(ShouldIgnoreTouchEmulatedMessage())
break; break;
e = new RawPointerEventArgs( e = new RawPointerEventArgs(
WindowsMouseDevice.Instance, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN
@ -559,7 +560,7 @@ namespace Avalonia.Win32
if(ShouldIgnoreTouchEmulatedMessage()) if(ShouldIgnoreTouchEmulatedMessage())
break; break;
e = new RawPointerEventArgs( e = new RawPointerEventArgs(
WindowsMouseDevice.Instance, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP
@ -587,7 +588,7 @@ namespace Avalonia.Win32
} }
e = new RawPointerEventArgs( e = new RawPointerEventArgs(
WindowsMouseDevice.Instance, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
RawPointerEventType.Move, RawPointerEventType.Move,
@ -597,7 +598,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSEWHEEL: case UnmanagedMethods.WindowsMessage.WM_MOUSEWHEEL:
e = new RawMouseWheelEventArgs( e = new RawMouseWheelEventArgs(
WindowsMouseDevice.Instance, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
PointToClient(PointFromLParam(lParam)), PointToClient(PointFromLParam(lParam)),
@ -606,7 +607,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSEHWHEEL: case UnmanagedMethods.WindowsMessage.WM_MOUSEHWHEEL:
e = new RawMouseWheelEventArgs( e = new RawMouseWheelEventArgs(
WindowsMouseDevice.Instance, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
PointToClient(PointFromLParam(lParam)), PointToClient(PointFromLParam(lParam)),
@ -616,7 +617,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE: case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE:
_trackingMouse = false; _trackingMouse = false;
e = new RawPointerEventArgs( e = new RawPointerEventArgs(
WindowsMouseDevice.Instance, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
RawPointerEventType.LeaveWindow, RawPointerEventType.LeaveWindow,
@ -627,7 +628,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN:
e = new RawPointerEventArgs( e = new RawPointerEventArgs(
WindowsMouseDevice.Instance, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN
@ -649,9 +650,9 @@ namespace Avalonia.Win32
{ {
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner, _owner,
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ? touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
RawPointerEventType.TouchEnd : RawPointerEventType.TouchEnd :
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ? touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin : RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate, RawPointerEventType.TouchUpdate,
PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
@ -771,11 +772,11 @@ namespace Avalonia.Win32
{ {
var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam); var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam);
var modifiers = WindowsKeyboardDevice.Instance.Modifiers; var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
modifiers |= RawInputModifiers.LeftMouseButton; modifiers |= RawInputModifiers.LeftMouseButton;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
modifiers |= RawInputModifiers.RightMouseButton; modifiers |= RawInputModifiers.RightMouseButton;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
modifiers |= RawInputModifiers.MiddleMouseButton; modifiers |= RawInputModifiers.MiddleMouseButton;
return modifiers; return modifiers;
} }
@ -785,7 +786,7 @@ namespace Avalonia.Win32
// Ensure that the delegate doesn't get garbage collected by storing it as a field. // Ensure that the delegate doesn't get garbage collected by storing it as a field.
_wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc);
_className = "Avalonia-" + Guid.NewGuid(); _className = $"Avalonia-{Guid.NewGuid().ToString()}";
UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX
{ {

28
tests/Avalonia.Animation.UnitTests/TransitionsTests.cs

@ -1,14 +1,7 @@
using System; using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Data;
using Xunit; using Xunit;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation.UnitTests namespace Avalonia.Animation.UnitTests
{ {
@ -69,5 +62,26 @@ namespace Avalonia.Animation.UnitTests
Assert.Equal(0, border.Opacity); Assert.Equal(0, border.Opacity);
} }
} }
[Fact]
public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick()
{
var clock = new MockGlobalClock();
using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
{
int i = 0;
var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue =>
{
switch (i++)
{
case 0: Assert.Equal(0, nextValue); break;
case 1: Assert.Equal(1d, nextValue); break;
}
});
clock.Pulse(TimeSpan.FromMilliseconds(10));
}
}
} }
} }

12
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

@ -78,6 +78,18 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(10, target.GetValue(Class1.AttachedProperty)); Assert.Equal(10, target.GetValue(Class1.AttachedProperty));
} }
[Fact]
public void PropertyChanged_Event_Uses_Coerced_Value()
{
var inst = new Class1();
inst.PropertyChanged += (sender, e) =>
{
Assert.Equal(10, e.NewValue);
};
inst.SetValue(Class1.QuxProperty, 15);
}
private class Class1 : AvaloniaObject private class Class1 : AvaloniaObject
{ {
public static readonly StyledProperty<int> QuxProperty = public static readonly StyledProperty<int> QuxProperty =

18
tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

@ -16,7 +16,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Set_ExitCode_After_Shutdown() public void Should_Set_ExitCode_After_Shutdown()
{ {
using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.Shutdown(1337); lifetime.Shutdown(1337);
@ -31,7 +31,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() }; var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() };
@ -50,7 +50,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Only_Exit_On_Explicit_Exit() public void Should_Only_Exit_On_Explicit_Exit()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
@ -84,7 +84,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Exit_After_MainWindow_Closed() public void Should_Exit_After_MainWindow_Closed()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
@ -112,7 +112,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Exit_After_Last_Window_Closed() public void Should_Exit_After_Last_Window_Closed()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests
public void Show_Should_Add_Window_To_OpenWindows() public void Show_Should_Add_Window_To_OpenWindows()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
var window = new Window(); var window = new Window();
@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests
public void Window_Should_Be_Added_To_OpenWindows_Only_Once() public void Window_Should_Be_Added_To_OpenWindows_Only_Once()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
var window = new Window(); var window = new Window();
@ -174,7 +174,7 @@ namespace Avalonia.Controls.UnitTests
public void Close_Should_Remove_Window_From_OpenWindows() public void Close_Should_Remove_Window_From_OpenWindows()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
var window = new Window(); var window = new Window();
@ -197,7 +197,7 @@ namespace Avalonia.Controls.UnitTests
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object));
using (UnitTestApplication.Start(services)) using (UnitTestApplication.Start(services))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
var window = new Window(); var window = new Window();

431
tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs

@ -2,9 +2,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
@ -21,185 +19,366 @@ namespace Avalonia.Controls.UnitTests
public void Detects_Horizontal_Orientation() public void Detects_Horizontal_Orientation()
{ {
GridSplitter splitter; GridSplitter splitter;
var grid = new Grid()
{ var grid = new Grid
RowDefinitions = new RowDefinitions("*,Auto,*"), {
ColumnDefinitions = new ColumnDefinitions("*,*"), RowDefinitions = new RowDefinitions("*,Auto,*"),
Children = ColumnDefinitions = new ColumnDefinitions("*,*"),
{ Children =
new Border { [Grid.RowProperty] = 0 }, {
(splitter = new GridSplitter { [Grid.RowProperty] = 1 }), new Border { [Grid.RowProperty] = 0 },
new Border { [Grid.RowProperty] = 2 } (splitter = new GridSplitter { [Grid.RowProperty] = 1 }),
} new Border { [Grid.RowProperty] = 2 }
}; }
};
var root = new TestRoot { Child = grid }; var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300)); root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300)); root.Arrange(new Rect(0, 0, 100, 300));
Assert.Contains(splitter.Classes, ":horizontal".Equals); Assert.Equal(GridResizeDirection.Rows, splitter.GetEffectiveResizeDirection());
} }
[Fact] [Fact]
public void Detects_Vertical_Orientation() public void Detects_Vertical_Orientation()
{ {
GridSplitter splitter; GridSplitter splitter;
var grid = new Grid()
{ var grid = new Grid
ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), {
RowDefinitions = new RowDefinitions("*,*"), ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
Children = RowDefinitions = new RowDefinitions("*,*"),
{ Children =
new Border { [Grid.ColumnProperty] = 0 }, {
(splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), new Border { [Grid.ColumnProperty] = 0 },
new Border { [Grid.ColumnProperty] = 2 }, (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }),
} new Border { [Grid.ColumnProperty] = 2 },
}; }
};
var root = new TestRoot { Child = grid }; var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300)); root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300)); root.Arrange(new Rect(0, 0, 100, 300));
Assert.Contains(splitter.Classes, ":vertical".Equals); Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection());
} }
[Fact] [Fact]
public void Detects_With_Both_Auto() public void Detects_With_Both_Auto()
{ {
GridSplitter splitter; GridSplitter splitter;
var grid = new Grid()
{ var grid = new Grid
ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), {
RowDefinitions = new RowDefinitions("Auto,Auto"), ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
Children = RowDefinitions = new RowDefinitions("Auto,Auto"),
{ Children =
new Border { [Grid.ColumnProperty] = 0 }, {
(splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), new Border { [Grid.ColumnProperty] = 0 },
new Border { [Grid.ColumnProperty] = 2 }, (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }),
} new Border { [Grid.ColumnProperty] = 2 },
}; }
};
var root = new TestRoot { Child = grid }; var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300)); root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300)); root.Arrange(new Rect(0, 0, 100, 300));
Assert.Contains(splitter.Classes, ":vertical".Equals); Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection());
} }
[Fact] [Fact]
public void Horizontal_Stays_Within_Constraints() public void In_First_Position_Doesnt_Throw_Exception()
{
GridSplitter splitter;
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,*"),
RowDefinitions = new RowDefinitions("*,*"),
Children =
{
(splitter = new GridSplitter { [Grid.ColumnProperty] = 0 }),
new Border { [Grid.ColumnProperty] = 1 },
new Border { [Grid.ColumnProperty] = 2 },
}
};
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
splitter.RaiseEvent(
new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
splitter.RaiseEvent(new VectorEventArgs
{
RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(100, 1000)
});
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Horizontal_Stays_Within_Constraints(bool showsPreview)
{ {
var control1 = new Border { [Grid.RowProperty] = 0 }; var control1 = new Border { [Grid.RowProperty] = 0 };
var splitter = new GridSplitter var splitter = new GridSplitter { [Grid.RowProperty] = 1, ShowsPreview = showsPreview};
{
[Grid.RowProperty] = 1,
};
var control2 = new Border { [Grid.RowProperty] = 2 }; var control2 = new Border { [Grid.RowProperty] = 2 };
var rowDefinitions = new RowDefinitions() var rowDefinitions = new RowDefinitions
{ {
new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 },
new RowDefinition(GridLength.Auto), new RowDefinition(GridLength.Auto),
new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 },
}; };
var grid = new Grid() var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } };
{
RowDefinitions = rowDefinitions, var root = new TestRoot
Children = {
{ Child = new VisualLayerManager
control1, splitter, control2 {
} Child = grid
}; }
};
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 200)); root.Measure(new Size(100, 200));
root.Arrange(new Rect(0, 0, 100, 200)); root.Arrange(new Rect(0, 0, 100, 200));
splitter.RaiseEvent(
new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
splitter.RaiseEvent(new VectorEventArgs splitter.RaiseEvent(new VectorEventArgs
{ {
RoutedEvent = Thumb.DragDeltaEvent, RoutedEvent = Thumb.DragDeltaEvent,
Vector = new Vector(0, -100) Vector = new Vector(0, -100)
}); });
Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star));
Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); if (showsPreview)
{
Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star));
Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star));
}
else
{
Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star));
Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star));
}
splitter.RaiseEvent(new VectorEventArgs splitter.RaiseEvent(new VectorEventArgs
{ {
RoutedEvent = Thumb.DragDeltaEvent, RoutedEvent = Thumb.DragDeltaEvent,
Vector = new Vector(0, 100) Vector = new Vector(0, 100)
}); });
Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
}
[Fact] if (showsPreview)
public void In_First_Position_Doesnt_Throw_Exception() {
{ Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star));
GridSplitter splitter; Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star));
var grid = new Grid() }
{ else
ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), {
RowDefinitions = new RowDefinitions("*,*"), Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
Children = Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
{ }
(splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ),
new Border { [Grid.ColumnProperty] = 1 },
new Border { [Grid.ColumnProperty] = 2 },
}
};
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
splitter.RaiseEvent(new VectorEventArgs splitter.RaiseEvent(new VectorEventArgs
{ {
RoutedEvent = Thumb.DragDeltaEvent, RoutedEvent = Thumb.DragCompletedEvent
Vector = new Vector(100, 1000) });
});
Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
} }
[Fact] [Theory]
public void Vertical_Stays_Within_Constraints() [InlineData(false)]
[InlineData(true)]
public void Vertical_Stays_Within_Constraints(bool showsPreview)
{ {
var control1 = new Border { [Grid.ColumnProperty] = 0 }; var control1 = new Border { [Grid.ColumnProperty] = 0 };
var splitter = new GridSplitter var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, ShowsPreview = showsPreview};
{
[Grid.ColumnProperty] = 1,
};
var control2 = new Border { [Grid.ColumnProperty] = 2 }; var control2 = new Border { [Grid.ColumnProperty] = 2 };
var columnDefinitions = new ColumnDefinitions() var columnDefinitions = new ColumnDefinitions
{ {
new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 },
new ColumnDefinition(GridLength.Auto), new ColumnDefinition(GridLength.Auto),
new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 },
}; };
var grid = new Grid()
{
ColumnDefinitions = columnDefinitions,
Children =
{
control1, splitter, control2
}
};
var root = new TestRoot { Child = grid }; var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } };
var root = new TestRoot
{
Child = new VisualLayerManager
{
Child = grid
}
};
root.Measure(new Size(200, 100)); root.Measure(new Size(200, 100));
root.Arrange(new Rect(0, 0, 200, 100)); root.Arrange(new Rect(0, 0, 200, 100));
splitter.RaiseEvent(
new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
splitter.RaiseEvent(new VectorEventArgs
{
RoutedEvent = Thumb.DragDeltaEvent,
Vector = new Vector(-100, 0)
});
if (showsPreview)
{
Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star));
}
else
{
Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star));
}
splitter.RaiseEvent(new VectorEventArgs splitter.RaiseEvent(new VectorEventArgs
{ {
RoutedEvent = Thumb.DragDeltaEvent, RoutedEvent = Thumb.DragDeltaEvent,
Vector = new Vector(-100, 0) Vector = new Vector(100, 0)
}); });
Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); if (showsPreview)
{
Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star));
}
else
{
Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star));
}
splitter.RaiseEvent(new VectorEventArgs splitter.RaiseEvent(new VectorEventArgs
{ {
RoutedEvent = Thumb.DragDeltaEvent, RoutedEvent = Thumb.DragCompletedEvent
Vector = new Vector(100, 0) });
});
Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star));
} }
[Theory]
[InlineData(Key.Up, 90, 110)]
[InlineData(Key.Down, 110, 90)]
public void Vertical_Keyboard_Input_Can_Move_Splitter(Key key, double expectedHeightFirst, double expectedHeightSecond)
{
var control1 = new Border { [Grid.RowProperty] = 0 };
var splitter = new GridSplitter { [Grid.RowProperty] = 1, KeyboardIncrement = 10d };
var control2 = new Border { [Grid.RowProperty] = 2 };
var rowDefinitions = new RowDefinitions
{
new RowDefinition(1, GridUnitType.Star),
new RowDefinition(GridLength.Auto),
new RowDefinition(1, GridUnitType.Star)
};
var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } };
var root = new TestRoot
{
Child = grid
};
root.Measure(new Size(200, 200));
root.Arrange(new Rect(0, 0, 200, 200));
splitter.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = key
});
Assert.Equal(rowDefinitions[0].Height, new GridLength(expectedHeightFirst, GridUnitType.Star));
Assert.Equal(rowDefinitions[2].Height, new GridLength(expectedHeightSecond, GridUnitType.Star));
}
[Theory]
[InlineData(Key.Left, 90, 110)]
[InlineData(Key.Right, 110, 90)]
public void Horizontal_Keyboard_Input_Can_Move_Splitter(Key key, double expectedWidthFirst, double expectedWidthSecond)
{
var control1 = new Border { [Grid.ColumnProperty] = 0 };
var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d };
var control2 = new Border { [Grid.ColumnProperty] = 2 };
var columnDefinitions = new ColumnDefinitions
{
new ColumnDefinition(1, GridUnitType.Star),
new ColumnDefinition(GridLength.Auto),
new ColumnDefinition(1, GridUnitType.Star)
};
var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } };
var root = new TestRoot
{
Child = grid
};
root.Measure(new Size(200, 200));
root.Arrange(new Rect(0, 0, 200, 200));
splitter.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = key
});
Assert.Equal(columnDefinitions[0].Width, new GridLength(expectedWidthFirst, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(expectedWidthSecond, GridUnitType.Star));
}
[Fact]
public void Pressing_Escape_Key_Cancels_Resizing()
{
var control1 = new Border { [Grid.ColumnProperty] = 0 };
var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d };
var control2 = new Border { [Grid.ColumnProperty] = 2 };
var columnDefinitions = new ColumnDefinitions
{
new ColumnDefinition(1, GridUnitType.Star),
new ColumnDefinition(GridLength.Auto),
new ColumnDefinition(1, GridUnitType.Star)
};
var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } };
var root = new TestRoot
{
Child = grid
};
root.Measure(new Size(200, 200));
root.Arrange(new Rect(0, 0, 200, 200));
splitter.RaiseEvent(
new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
splitter.RaiseEvent(new VectorEventArgs
{
RoutedEvent = Thumb.DragDeltaEvent,
Vector = new Vector(-100, 0)
});
Assert.Equal(columnDefinitions[0].Width, new GridLength(0, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(200, GridUnitType.Star));
splitter.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Escape
});
Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star));
}
} }
} }

286
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -1,11 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -34,7 +29,6 @@ namespace Avalonia.Controls.UnitTests
private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns) private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns)
{ {
var grid = new Grid(); var grid = new Grid();
foreach (var k in columns.Select(c => new ColumnDefinition foreach (var k in columns.Select(c => new ColumnDefinition
{ {
@ -1178,6 +1172,41 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth);
} }
[Fact]
public void Size_Group_Definition_Resizes_Are_Tracked()
{
var grids = new[] {
CreateGrid(("A", new GridLength(5, GridUnitType.Pixel)), (null, new GridLength())),
CreateGrid(("A", new GridLength(5, GridUnitType.Pixel)), (null, new GridLength())) };
var scope = new Grid();
foreach (var xgrids in grids)
scope.Children.Add(xgrids);
var root = new Grid();
root.UseLayoutRounding = false;
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Children.Add(scope);
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
PrintColumnDefinitions(grids[0]);
Assert.Equal(5, grids[0].ColumnDefinitions[0].ActualWidth);
Assert.Equal(5, grids[1].ColumnDefinitions[0].ActualWidth);
grids[0].ColumnDefinitions[0].Width = new GridLength(10, GridUnitType.Pixel);
foreach (Grid grid in grids)
{
grid.Measure(new Size(50, 50));
grid.Arrange(new Rect(new Point(), new Point(50, 50)));
}
PrintColumnDefinitions(grids[0]);
Assert.Equal(10, grids[0].ColumnDefinitions[0].ActualWidth);
Assert.Equal(10, grids[1].ColumnDefinitions[0].ActualWidth);
}
[Fact] [Fact]
public void Collection_Changes_Are_Tracked() public void Collection_Changes_Are_Tracked()
{ {
@ -1270,11 +1299,11 @@ namespace Avalonia.Controls.UnitTests
// grid.Measure(new Size(100, 100)); // grid.Measure(new Size(100, 100));
// grid.Arrange(new Rect(new Point(), new Point(100, 100))); // grid.Arrange(new Rect(new Point(), new Point(100, 100)));
// PrintColumnDefinitions(grid); // PrintColumnDefinitions(grid);
// NOTE: THIS IS BROKEN IN WPF // NOTE: THIS IS BROKEN IN WPF
// all in group are equal to width (MinWidth) of the sizer in the second column // all in group are equal to width (MinWidth) of the sizer in the second column
// Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth)); // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth));
// NOTE: THIS IS BROKEN IN WPF // NOTE: THIS IS BROKEN IN WPF
// grid.ColumnDefinitions[2].SharedSizeGroup = null; // grid.ColumnDefinitions[2].SharedSizeGroup = null;
@ -1382,6 +1411,245 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Size(100, 100), grid.Bounds.Size); Assert.Equal(new Size(100, 100), grid.Bounds.Size);
} }
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Changing_Column_Width_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
{
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
if (setUsingAvaloniaProperty)
grid.ColumnDefinitions[0][ColumnDefinition.WidthProperty] = new GridLength(5);
else
grid.ColumnDefinitions[0].Width = new GridLength(5);
});
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Changing_Column_MinWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
{
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
if (setUsingAvaloniaProperty)
grid.ColumnDefinitions[0][ColumnDefinition.MinWidthProperty] = 5;
else
grid.ColumnDefinitions[0].MinWidth = 5;
});
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Changing_Column_MaxWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
{
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
if (setUsingAvaloniaProperty)
grid.ColumnDefinitions[0][ColumnDefinition.MaxWidthProperty] = 5;
else
grid.ColumnDefinitions[0].MaxWidth = 5;
});
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Changing_Row_Height_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
{
var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
if (setUsingAvaloniaProperty)
grid.RowDefinitions[0][RowDefinition.HeightProperty] = new GridLength(5);
else
grid.RowDefinitions[0].Height = new GridLength(5);
});
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Changing_Row_MinHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
{
var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
if (setUsingAvaloniaProperty)
grid.RowDefinitions[0][RowDefinition.MinHeightProperty] = 5;
else
grid.RowDefinitions[0].MinHeight = 5;
});
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Changing_Row_MaxHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
{
var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
if (setUsingAvaloniaProperty)
grid.RowDefinitions[0][RowDefinition.MaxHeightProperty] = 5;
else
grid.RowDefinitions[0].MaxHeight = 5;
});
}
[Fact]
public void Adding_Column_Should_Invalidate_Grid()
{
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
grid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(5)));
});
}
[Fact]
public void Adding_Row_Should_Invalidate_Grid()
{
var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
grid.RowDefinitions.Add(new RowDefinition(new GridLength(5)));
});
}
[Fact]
public void Replacing_Columns_Should_Invalidate_Grid()
{
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
grid.ColumnDefinitions = ColumnDefinitions.Parse("2*,1*");
});
}
[Fact]
public void Replacing_Rows_Should_Invalidate_Grid()
{
var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
grid.RowDefinitions = RowDefinitions.Parse("2*,1*");
});
}
[Fact]
public void Removing_Column_Should_Invalidate_Grid()
{
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
grid.ColumnDefinitions.RemoveAt(0);
});
}
[Fact]
public void Removing_Row_Should_Invalidate_Grid()
{
var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
Change_Property_And_Verify_Measure_Requested(grid, () =>
{
grid.RowDefinitions.RemoveAt(0);
});
}
[Fact]
public void Removing_Child_Should_Invalidate_Grid_And_Be_Operational()
{
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") };
grid.Children.Add(new Decorator() { [Grid.ColumnProperty] = 0 });
grid.Children.Add(new Decorator() { Width = 10, Height = 10, [Grid.ColumnProperty] = 1 });
var size = new Size(100, 100);
grid.Measure(size);
grid.Arrange(new Rect(size));
Assert.True(grid.IsMeasureValid);
Assert.True(grid.IsArrangeValid);
Assert.Equal(90, grid.Children[0].Bounds.Width);
Assert.Equal(10, grid.Children[1].Bounds.Width);
grid.Children.RemoveAt(1);
Assert.False(grid.IsMeasureValid);
Assert.False(grid.IsArrangeValid);
grid.Measure(size);
grid.Arrange(new Rect(size));
Assert.True(grid.IsMeasureValid);
Assert.True(grid.IsArrangeValid);
Assert.Equal(100, grid.Children[0].Bounds.Width);
}
[Fact]
public void Adding_Child_Should_Invalidate_Grid_And_Be_Operational()
{
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") };
grid.Children.Add(new Decorator() { [Grid.ColumnProperty] = 0 });
var size = new Size(100, 100);
grid.Measure(size);
grid.Arrange(new Rect(size));
Assert.True(grid.IsMeasureValid);
Assert.True(grid.IsArrangeValid);
Assert.Equal(100, grid.Children[0].Bounds.Width);
grid.Children.Add(new Decorator() { Width = 10, Height = 10, [Grid.ColumnProperty] = 1 });
Assert.False(grid.IsMeasureValid);
Assert.False(grid.IsArrangeValid);
grid.Measure(size);
grid.Arrange(new Rect(size));
Assert.True(grid.IsMeasureValid);
Assert.True(grid.IsArrangeValid);
Assert.Equal(90, grid.Children[0].Bounds.Width);
Assert.Equal(10, grid.Children[1].Bounds.Width);
}
private static void Change_Property_And_Verify_Measure_Requested(Grid grid, Action change)
{
grid.Measure(new Size(100, 100));
grid.Arrange(new Rect(grid.DesiredSize));
Assert.True(grid.IsMeasureValid);
Assert.True(grid.IsArrangeValid);
change();
Assert.False(grid.IsMeasureValid);
Assert.False(grid.IsArrangeValid);
}
private class TestControl : Control private class TestControl : Control
{ {
public Size MeasureSize { get; set; } public Size MeasureSize { get; set; }

53
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -12,6 +12,7 @@ using Xunit;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Input; using Avalonia.Input;
using System.Collections.Generic;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
{ {
@ -104,6 +105,28 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new[] { child }, target.GetLogicalChildren()); Assert.Equal(new[] { child }, target.GetLogicalChildren());
} }
[Fact]
public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl()
{
var item = new Border();
var items = new ObservableCollection<Border>();
var target = new ItemsControl
{
Template = GetTemplate(),
Items = items,
};
var root = new TestRoot(true, target);
root.Measure(new Size(100, 100));
root.Arrange(new Rect(0, 0, 100, 100));
items.Add(item);
Assert.Equal(target, item.Parent);
}
[Fact] [Fact]
public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate() public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate()
{ {
@ -522,6 +545,36 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Presenter_Items_Should_Be_In_Sync()
{
var target = new ItemsControl
{
Template = GetTemplate(),
Items = new object[]
{
new Button(),
new Button(),
},
};
var root = new TestRoot { Child = target };
var otherPanel = new StackPanel();
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.ItemContainerGenerator.Materialized += (s, e) =>
{
Assert.IsType<Canvas>(e.Containers[0].Item);
};
target.Items = new[]
{
new Canvas()
};
}
private class Item private class Item
{ {
public Item(string value) public Item(string value)

189
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
@ -14,6 +15,7 @@ using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Data; using Avalonia.Markup.Data;
using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
using Xunit; using Xunit;
@ -23,7 +25,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
public class SelectingItemsControlTests public class SelectingItemsControlTests
{ {
private MouseTestHelper _helper = new MouseTestHelper(); private MouseTestHelper _helper = new MouseTestHelper();
[Fact] [Fact]
public void SelectedIndex_Should_Initially_Be_Minus_1() public void SelectedIndex_Should_Initially_Be_Minus_1()
{ {
@ -168,6 +170,130 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal("B", listBox.SelectedItem); Assert.Equal("B", listBox.SelectedItem);
} }
[Fact]
public void Setting_SelectedIndex_Before_Initialize_Should_Retain()
{
var listBox = new ListBox
{
SelectionMode = SelectionMode.Single,
Items = new[] { "foo", "bar", "baz" },
SelectedIndex = 1
};
listBox.BeginInit();
listBox.EndInit();
Assert.Equal(1, listBox.SelectedIndex);
Assert.Equal("bar", listBox.SelectedItem);
}
[Fact]
public void Setting_SelectedIndex_During_Initialize_Should_Take_Priority_Over_Previous_Value()
{
var listBox = new ListBox
{
SelectionMode = SelectionMode.Single,
Items = new[] { "foo", "bar", "baz" },
SelectedIndex = 2
};
listBox.BeginInit();
listBox.SelectedIndex = 1;
listBox.EndInit();
Assert.Equal(1, listBox.SelectedIndex);
Assert.Equal("bar", listBox.SelectedItem);
}
[Fact]
public void Setting_SelectedItem_Before_Initialize_Should_Retain()
{
var listBox = new ListBox
{
SelectionMode = SelectionMode.Single,
Items = new[] { "foo", "bar", "baz" },
SelectedItem = "bar"
};
listBox.BeginInit();
listBox.EndInit();
Assert.Equal(1, listBox.SelectedIndex);
Assert.Equal("bar", listBox.SelectedItem);
}
[Fact]
public void Setting_SelectedItems_Before_Initialize_Should_Retain()
{
var listBox = new ListBox
{
SelectionMode = SelectionMode.Multiple,
Items = new[] { "foo", "bar", "baz" },
};
var selected = new[] { "foo", "bar" };
foreach (var v in selected)
{
listBox.SelectedItems.Add(v);
}
listBox.BeginInit();
listBox.EndInit();
Assert.Equal(selected, listBox.SelectedItems);
}
[Fact]
public void Setting_SelectedItems_During_Initialize_Should_Take_Priority_Over_Previous_Value()
{
var listBox = new ListBox
{
SelectionMode = SelectionMode.Multiple,
Items = new[] { "foo", "bar", "baz" },
};
var selected = new[] { "foo", "bar" };
foreach (var v in new[] { "bar", "baz" })
{
listBox.SelectedItems.Add(v);
}
listBox.BeginInit();
listBox.SelectedItems = new AvaloniaList<object>(selected);
listBox.EndInit();
Assert.Equal(selected, listBox.SelectedItems);
}
[Fact]
public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain()
{
var listBox = new ListBox
{
SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected,
Items = new[] { "foo", "bar", "baz" },
SelectedIndex = 1
};
listBox.BeginInit();
listBox.EndInit();
Assert.Equal(1, listBox.SelectedIndex);
Assert.Equal("bar", listBox.SelectedItem);
}
[Fact] [Fact]
public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True()
{ {
@ -849,7 +975,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
var target = new ListBox var target = new ListBox
{ {
Template = Template(), Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"}, Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
}; };
target.ApplyTemplate(); target.ApplyTemplate();
@ -980,6 +1106,45 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.True(raised); Assert.True(raised);
} }
[Fact]
public void AutoScrollToSelectedItem_On_Reset_Works()
{
// Issue #3148
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var items = new ResettingCollection(100);
var target = new ListBox
{
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) =>
new TextBlock
{
Text = x,
Width = 100,
Height = 10
}),
AutoScrollToSelectedItem = true,
VirtualizationMode = ItemVirtualizationMode.Simple,
};
var root = new TestRoot(true, target);
root.Measure(new Size(100, 100));
root.Arrange(new Rect(0, 0, 100, 100));
Assert.True(target.Presenter.Panel.Children.Count > 0);
Assert.True(target.Presenter.Panel.Children.Count < 100);
target.SelectedItem = "Item99";
// #3148 triggered here.
items.Reset(new[] { "Item99" });
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(1, target.Presenter.Panel.Children.Count);
}
}
[Fact] [Fact]
public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization()
{ {
@ -1028,6 +1193,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Name = "itemsPresenter", Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
[~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
[~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty],
}.RegisterInNameScope(scope)); }.RegisterInNameScope(scope));
} }
@ -1072,5 +1238,24 @@ namespace Avalonia.Controls.UnitTests.Primitives
return base.MoveSelection(direction, wrap); return base.MoveSelection(direction, wrap);
} }
} }
private class ResettingCollection : List<string>, INotifyCollectionChanged
{
public ResettingCollection(int itemCount)
{
AddRange(Enumerable.Range(0, itemCount).Select(x => $"Item{x}"));
}
public void Reset(IEnumerable<string> items)
{
Clear();
AddRange(items);
CollectionChanged?.Invoke(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
} }
} }

20
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -287,6 +288,25 @@ namespace Avalonia.Controls.UnitTests
Assert.Single(target.GetLogicalChildren(), content); Assert.Single(target.GetLogicalChildren(), content);
} }
[Fact]
public void Should_Not_Propagate_DataContext_To_TabItem_Content()
{
var dataContext = "DataContext";
var tabItem = new TabItem();
var target = new TabControl
{
Template = TabControlTemplate(),
DataContext = dataContext,
Items = new AvaloniaList<object> { tabItem }
};
ApplyTemplate(target);
Assert.NotEqual(dataContext, tabItem.Content);
}
private IControlTemplate TabControlTemplate() private IControlTemplate TabControlTemplate()
{ {
return new FuncControlTemplate<TabControl>((parent, scope) => return new FuncControlTemplate<TabControl>((parent, scope) =>

109
tests/Avalonia.Controls.UnitTests/ToolTipTests.cs

@ -0,0 +1,109 @@
// 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.Reactive.Disposables;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class TolTipTests
{
private MouseTestHelper _mouseHelper = new MouseTestHelper();
[Fact]
public void Should_Not_Open_On_Detached_Control()
{
//issue #3188
var control = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
Assert.False((control as IVisual).IsAttachedToVisualTree);
//here in issue #3188 exception is raised
_mouseHelper.Enter(control);
Assert.False(ToolTip.GetIsOpen(control));
}
[Fact]
public void Should_Open_On_Pointer_Enter()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
window.Content = target;
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.True((target as IVisual).IsAttachedToVisualTree);
_mouseHelper.Enter(target);
Assert.True(ToolTip.GetIsOpen(target));
}
}
[Fact]
public void Should_Open_On_Pointer_Enter_With_Delay()
{
Action timercallback = null;
var delay = TimeSpan.Zero;
var pti = Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true);
Mock.Get(pti)
.Setup(v => v.StartTimer(It.IsAny<DispatcherPriority>(), It.IsAny<TimeSpan>(), It.IsAny<Action>()))
.Callback<DispatcherPriority, TimeSpan, Action>((priority, interval, tick) =>
{
delay = interval;
timercallback = tick;
})
.Returns(Disposable.Empty);
using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti)))
{
var window = new Window();
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 1
};
window.Content = target;
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.True((target as IVisual).IsAttachedToVisualTree);
_mouseHelper.Enter(target);
Assert.Equal(TimeSpan.FromMilliseconds(1), delay);
Assert.NotNull(timercallback);
Assert.False(ToolTip.GetIsOpen(target));
timercallback();
Assert.True(ToolTip.GetIsOpen(target));
}
}
}
}

78
tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

@ -12,14 +12,14 @@ namespace Avalonia.Controls.UnitTests
public void Lays_Out_Horizontally_On_Separate_Lines() public void Lays_Out_Horizontally_On_Separate_Lines()
{ {
var target = new WrapPanel() var target = new WrapPanel()
{ {
Width = 100, Width = 100,
Children = Children =
{ {
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize)); target.Arrange(new Rect(target.DesiredSize));
@ -33,14 +33,14 @@ namespace Avalonia.Controls.UnitTests
public void Lays_Out_Horizontally_On_A_Single_Line() public void Lays_Out_Horizontally_On_A_Single_Line()
{ {
var target = new WrapPanel() var target = new WrapPanel()
{ {
Width = 200, Width = 200,
Children = Children =
{ {
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize)); target.Arrange(new Rect(target.DesiredSize));
@ -54,15 +54,15 @@ namespace Avalonia.Controls.UnitTests
public void Lays_Out_Vertically_Children_On_A_Single_Line() public void Lays_Out_Vertically_Children_On_A_Single_Line()
{ {
var target = new WrapPanel() var target = new WrapPanel()
{ {
Orientation = Orientation.Vertical, Orientation = Orientation.Vertical,
Height = 120, Height = 120,
Children = Children =
{ {
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize)); target.Arrange(new Rect(target.DesiredSize));
@ -76,15 +76,15 @@ namespace Avalonia.Controls.UnitTests
public void Lays_Out_Vertically_On_Separate_Lines() public void Lays_Out_Vertically_On_Separate_Lines()
{ {
var target = new WrapPanel() var target = new WrapPanel()
{ {
Orientation = Orientation.Vertical, Orientation = Orientation.Vertical,
Height = 60, Height = 60,
Children = Children =
{ {
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize)); target.Arrange(new Rect(target.DesiredSize));
@ -98,17 +98,17 @@ namespace Avalonia.Controls.UnitTests
public void Applies_ItemWidth_And_ItemHeight_Properties() public void Applies_ItemWidth_And_ItemHeight_Properties()
{ {
var target = new WrapPanel() var target = new WrapPanel()
{ {
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
Width = 50, Width = 50,
ItemWidth = 20, ItemWidth = 20,
ItemHeight = 15, ItemHeight = 15,
Children = Children =
{ {
new Border(), new Border(),
new Border(), new Border(),
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize)); target.Arrange(new Rect(target.DesiredSize));
@ -117,5 +117,33 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(0, 0, 20, 15), target.Children[0].Bounds); Assert.Equal(new Rect(0, 0, 20, 15), target.Children[0].Bounds);
Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds); Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds);
} }
[Fact]
void ItemWidth_Trigger_InvalidateMeasure()
{
var target = new WrapPanel();
target.Measure(new Size(10, 10));
Assert.True(target.IsMeasureValid);
target.ItemWidth = 1;
Assert.False(target.IsMeasureValid);
}
[Fact]
void ItemHeight_Trigger_InvalidateMeasure()
{
var target = new WrapPanel();
target.Measure(new Size(10, 10));
Assert.True(target.IsMeasureValid);
target.ItemHeight = 1;
Assert.False(target.IsMeasureValid);
}
} }
} }

39
tests/Avalonia.Input.UnitTests/PointerTests.cs

@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class PointerTests
{
[Fact]
public void On_Capture_Transfer_PointerCaptureLost_Should_Propagate_Up_To_The_Common_Parent()
{
Border initialParent, initialCapture, newParent, newCapture;
var el = new StackPanel
{
Children =
{
(initialParent = new Border { Child = initialCapture = new Border() }),
(newParent = new Border { Child = newCapture = new Border() })
}
};
var receivers = new List<object>();
var root = new TestRoot(el);
foreach (InputElement d in root.GetSelfAndVisualDescendants())
d.PointerCaptureLost += (s, e) => receivers.Add(s);
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
pointer.Capture(initialCapture);
pointer.Capture(newCapture);
Assert.True(receivers.SequenceEqual(new[] { initialCapture, initialParent }));
receivers.Clear();
pointer.Capture(null);
Assert.True(receivers.SequenceEqual(new object[] { newCapture, newParent, el, root }));
}
}
}

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

@ -175,6 +175,7 @@ namespace Avalonia.Layout.UnitTests
x.CreateFormattedText( x.CreateFormattedText(
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Typeface>(), It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<TextAlignment>(), It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(), It.IsAny<TextWrapping>(),
It.IsAny<Size>(), It.IsAny<Size>(),

6
tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs

@ -45,7 +45,7 @@ namespace Avalonia.ReactiveUI.UnitTests
public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew()
{ {
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using (var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
var isLaunchingReceived = false; var isLaunchingReceived = false;
var application = AvaloniaLocator.Current.GetService<Application>(); var application = AvaloniaLocator.Current.GetService<Application>();
@ -86,7 +86,7 @@ namespace Avalonia.ReactiveUI.UnitTests
public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized()
{ {
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) using (var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
var shouldPersistReceived = false; var shouldPersistReceived = false;
var application = AvaloniaLocator.Current.GetService<Application>(); var application = AvaloniaLocator.Current.GetService<Application>();
@ -105,4 +105,4 @@ namespace Avalonia.ReactiveUI.UnitTests
} }
} }
} }
} }

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

@ -53,7 +53,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{ {
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text, return r.CreateFormattedText(text,
new Typeface(fontFamily, fontSize, fontStyle, fontWeight), new Typeface(fontFamily, fontWeight, fontStyle),
fontSize,
textAlignment, textAlignment,
wrapping, wrapping,
widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity), widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity),

8
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -9,11 +9,10 @@ namespace Avalonia.UnitTests
{ {
public class MockPlatformRenderInterface : IPlatformRenderInterface public class MockPlatformRenderInterface : IPlatformRenderInterface
{ {
public IEnumerable<string> InstalledFontNames => new string[0];
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -79,5 +78,10 @@ namespace Avalonia.UnitTests
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return Mock.Of<IGlyphTypefaceImpl>();
}
} }
} }

2
tests/Avalonia.UnitTests/MouseTestHelper.cs

@ -84,9 +84,9 @@ namespace Avalonia.UnitTests
); );
if (ButtonCount(props) == 0) if (ButtonCount(props) == 0)
{ {
_pointer.Capture(null);
target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position,
Timestamp(), props, GetModifiers(modifiers), _pressedButton)); Timestamp(), props, GetModifiers(modifiers), _pressedButton));
_pointer.Capture(null);
} }
else else
Move(target, source, position); Move(target, source, position);

11
tests/Avalonia.UnitTests/TestRoot.cs

@ -24,8 +24,19 @@ namespace Avalonia.UnitTests
} }
public TestRoot(IControl child) public TestRoot(IControl child)
: this(false, child)
{
Child = child;
}
public TestRoot(bool useGlobalStyles, IControl child)
: this() : this()
{ {
if (useGlobalStyles)
{
StylingParent = UnitTestApplication.Current;
}
Child = child; Child = child;
} }

1
tests/Avalonia.UnitTests/TestServices.cs

@ -169,6 +169,7 @@ namespace Avalonia.UnitTests
x.CreateFormattedText( x.CreateFormattedText(
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Typeface>(), It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<TextAlignment>(), It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(), It.IsAny<TextWrapping>(),
It.IsAny<Size>(), It.IsAny<Size>(),

44
tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs

@ -19,12 +19,48 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(new FontFamily("Arial"), fontFamily); Assert.Equal(new FontFamily("Arial"), fontFamily);
} }
[Fact] [InlineData("Font A")]
public void Should_Be_Equal() [InlineData("Font A, Font B")]
[InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")]
[InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")]
[Theory]
public void Should_Have_Equal_Hash(string s)
{ {
var fontFamily = new FontFamily("Arial"); var fontFamily = new FontFamily(s);
Assert.Equal(new FontFamily("Arial"), fontFamily); Assert.Equal(new FontFamily(s).GetHashCode(), fontFamily.GetHashCode());
}
[InlineData("Font A, Font B", "Font B, Font A")]
[InlineData("Font A, Font B", "Font A, Font C")]
[Theory]
public void Should_Not_Have_Equal_Hash(string a, string b)
{
var fontFamily = new FontFamily(b);
Assert.NotEqual(new FontFamily(a).GetHashCode(), fontFamily.GetHashCode());
}
[InlineData("Font A")]
[InlineData("Font A, Font B")]
[InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")]
[InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")]
[Theory]
public void Should_Be_Equal(string s)
{
var fontFamily = new FontFamily(s);
Assert.Equal(new FontFamily(s), fontFamily);
}
[InlineData("Font A, Font B", "Font B, Font A")]
[InlineData("Font A, Font B", "Font A, Font C")]
[Theory]
public void Should_Not_Be_Equal(string a, string b)
{
var fontFamily = new FontFamily(b);
Assert.NotEqual(new FontFamily(a), fontFamily);
} }
[Fact] [Fact]

14
tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs

@ -7,15 +7,21 @@ namespace Avalonia.Visuals.UnitTests.Media
public class TypefaceTests public class TypefaceTests
{ {
[Fact] [Fact]
public void Exception_Should_Be_Thrown_If_FontSize_LessThanEqualTo_0() public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_Zero()
{ {
Assert.Throws<ArgumentException>(() => new Typeface("foo", 0)); Assert.Throws<ArgumentException>(() => new Typeface("foo", 0, (FontStyle)12));
} }
[Fact] [Fact]
public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_0() public void Should_Be_Equal()
{ {
Assert.Throws<ArgumentException>(() => new Typeface("foo", 12, weight: 0)); Assert.Equal(new Typeface("Font A"), new Typeface("Font A"));
}
[Fact]
public void Should_Have_Equal_Hash()
{
Assert.Equal(new Typeface("Font A").GetHashCode(), new Typeface("Font A").GetHashCode());
} }
} }
} }

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

@ -369,6 +369,81 @@ namespace Avalonia.Visuals.UnitTests.Rendering
} }
[Fact]
public void Should_Update_VisualNodes_When_Child_Moved_To_New_Parent_And_New_Root()
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
Decorator moveFrom;
Decorator moveTo;
Canvas moveMe;
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
(moveFrom = new Decorator
{
Child = moveMe = new Canvas(),
})
}
}
};
var otherRoot = new TestRoot
{
Child = new StackPanel
{
Children =
{
(moveTo = new Decorator())
}
}
};
var sceneBuilder = new SceneBuilder();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
var otherSceneBuilder = new SceneBuilder();
var otherTarget = new DeferredRenderer(
otherRoot,
loop.Object,
sceneBuilder: otherSceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
otherRoot.Renderer = otherTarget;
target.Start();
otherTarget.Start();
RunFrame(target);
RunFrame(otherTarget);
moveFrom.Child = null;
moveTo.Child = moveMe;
RunFrame(target);
RunFrame(otherTarget);
var scene = target.UnitTestScene();
var otherScene = otherTarget.UnitTestScene();
var moveFromNode = (VisualNode)scene.FindNode(moveFrom);
var moveToNode = (VisualNode)otherScene.FindNode(moveTo);
Assert.Empty(moveFromNode.Children);
Assert.Equal(1, moveToNode.Children.Count);
Assert.Same(moveMe, moveToNode.Children[0].Visual);
}
[Fact] [Fact]
public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity() public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity()
{ {

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

@ -13,6 +13,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -51,6 +52,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
throw new NotImplementedException();
}
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt) public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt)
{ {
throw new NotImplementedException(); throw new NotImplementedException();

Loading…
Cancel
Save