Browse Source

Merge branch 'master' into bench-measure

pull/3243/head
Dariusz Komosiński 7 years ago
committed by GitHub
parent
commit
91158f51f7
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. 11
      native/Avalonia.Native/src/OSX/window.mm
  6. 12
      samples/ControlCatalog/App.xaml
  7. 11
      samples/ControlCatalog/App.xaml.cs
  8. 24
      samples/ControlCatalog/MainWindow.xaml
  9. 14
      samples/ControlCatalog/MainWindow.xaml.cs
  10. 20
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  11. 23
      src/Avalonia.Base/EnumExtensions.cs
  12. 5
      src/Avalonia.Base/Utilities/MathUtilities.cs
  13. 8
      src/Avalonia.Controls/Application.cs
  14. 7
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  15. 867
      src/Avalonia.Controls/GridSplitter.cs
  16. 10
      src/Avalonia.Controls/ItemsControl.cs
  17. 22
      src/Avalonia.Controls/ListBox.cs
  18. 4
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.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. 39
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  23. 10
      src/Avalonia.Controls/Primitives/TabStrip.cs
  24. 2
      src/Avalonia.Controls/Primitives/Thumb.cs
  25. 57
      src/Avalonia.Controls/TabControl.cs
  26. 21
      src/Avalonia.Controls/TabItem.cs
  27. 5
      src/Avalonia.Controls/TextBlock.cs
  28. 8
      src/Avalonia.Controls/TextBox.cs
  29. 6
      src/Avalonia.Controls/ToolTipService.cs
  30. 19
      src/Avalonia.Controls/TreeView.cs
  31. 2
      src/Avalonia.Controls/WrapPanel.cs
  32. 4
      src/Avalonia.Diagnostics/Views/TreePageView.xaml
  33. 105
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
  34. 62
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  35. BIN
      src/Avalonia.Dialogs/Assets/Roboto-Light.ttf
  36. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  37. 7
      src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
  38. 2
      src/Avalonia.Input/AccessKeyHandler.cs
  39. 2
      src/Avalonia.Input/FocusManager.cs
  40. 13
      src/Avalonia.Input/MouseDevice.cs
  41. 2
      src/Avalonia.Input/Pointer.cs
  42. 9
      src/Avalonia.Input/PointerPoint.cs
  43. 21
      src/Avalonia.Input/TouchDevice.cs
  44. 1
      src/Avalonia.Native/Avalonia.Native.csproj
  45. 34
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  46. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  47. 7
      src/Avalonia.Native/WindowImplBase.cs
  48. 58
      src/Avalonia.Themes.Default/GridSplitter.xaml
  49. 28
      src/Avalonia.Visuals/Media/FontFamily.cs
  50. 112
      src/Avalonia.Visuals/Media/FontManager.cs
  51. 6
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  52. 47
      src/Avalonia.Visuals/Media/FormattedText.cs
  53. 111
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  54. 99
      src/Avalonia.Visuals/Media/Typeface.cs
  55. 48
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  56. 89
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  57. 16
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  58. 6
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  59. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  60. 2
      src/Avalonia.X11/X11Platform.cs
  61. 10
      src/Avalonia.X11/X11Window.cs
  62. 12
      src/Avalonia.X11/XI2Manager.cs
  63. 1
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  64. 40
      src/Skia/Avalonia.Skia/FontKey.cs
  65. 82
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  66. 80
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  67. 179
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  68. 14
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  69. 91
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  70. 8
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  71. 5
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  72. 86
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  73. 19
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
  74. 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  75. 25
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  76. 49
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  77. 71
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  78. 19
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  79. 188
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  80. 8
      src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
  81. 43
      src/Windows/Avalonia.Win32/WindowImpl.cs
  82. 431
      tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
  83. 53
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  84. 20
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  85. 226
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  86. 1
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  87. 43
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  88. 109
      tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
  89. 78
      tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
  90. 39
      tests/Avalonia.Input.UnitTests/PointerTests.cs
  91. 1
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  92. 3
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  93. 8
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  94. 2
      tests/Avalonia.UnitTests/MouseTestHelper.cs
  95. 11
      tests/Avalonia.UnitTests/TestRoot.cs
  96. 1
      tests/Avalonia.UnitTests/TestServices.cs
  97. 44
      tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs
  98. 14
      tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
  99. 75
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  100. 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>

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

@ -855,8 +855,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
if(type == Wheel) if(type == Wheel)
{ {
delta.X = [event scrollingDeltaX] / 50; auto speed = 5;
delta.Y = [event scrollingDeltaY] / 50;
if([event hasPreciseScrollingDeltas])
{
speed = 50;
}
delta.X = [event scrollingDeltaX] / speed;
delta.Y = [event scrollingDeltaY] / speed;
if(delta.X == 0 && delta.Y == 0) if(delta.X == 0 && delta.Y == 0)
{ {

12
samples/ControlCatalog/App.xaml

@ -17,16 +17,4 @@
</Style> </Style>
<StyleInclude Source="/SideBar.xaml"/> <StyleInclude Source="/SideBar.xaml"/>
</Application.Styles> </Application.Styles>
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
<NativeMenuItem Header="Recent">
<NativeMenuItem.Menu>
<NativeMenu/>
</NativeMenuItem.Menu>
</NativeMenuItem>
<NativeMenuItem Header="Quit Avalonia" Gesture="CMD+Q"/>
</NativeMenu>
</NativeMenu.Menu>
</Application> </Application>

11
samples/ControlCatalog/App.xaml.cs

@ -8,20 +8,9 @@ namespace ControlCatalog
{ {
public class App : Application public class App : Application
{ {
private NativeMenu _recentMenu;
public override void Initialize() public override void Initialize()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
Name = "Avalonia";
_recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu;
}
public void OnOpenClicked(object sender, EventArgs args)
{
_recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()

24
samples/ControlCatalog/MainWindow.xaml

@ -37,12 +37,20 @@
</NativeMenu> </NativeMenu>
</NativeMenu.Menu> </NativeMenu.Menu>
<Window.DataTemplates> <Window.DataTemplates>
<DataTemplate DataType="vm:NotificationViewModel"> <DataTemplate DataType="vm:NotificationViewModel">
<v:CustomNotificationView /> <v:CustomNotificationView />
</DataTemplate> </DataTemplate>
</Window.DataTemplates> </Window.DataTemplates>
<Panel> <DockPanel LastChildFill="True">
<local:MainView/> <Menu Name="MainMenu" DockPanel.Dock="Top">
</Panel> <MenuItem Header="File">
<MenuItem Header="Exit" Command="{Binding ExitCommand}" />
</MenuItem>
<MenuItem Header="Help">
<MenuItem Header="About" Command="{Binding AboutCommand}" />
</MenuItem>
</Menu>
<local:MainView />
</DockPanel>
</Window> </Window>

14
samples/ControlCatalog/MainWindow.xaml.cs

@ -31,20 +31,28 @@ namespace ControlCatalog
DataContext = new MainWindowViewModel(_notificationArea); DataContext = new MainWindowViewModel(_notificationArea);
_recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu;
var mainMenu = this.FindControl<Menu>("MainMenu");
mainMenu.AttachedToVisualTree += MenuAttached;
}
public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e)
{
if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu)
{
mainMenu.IsVisible = false;
}
} }
public void OnOpenClicked(object sender, EventArgs args) public void OnOpenClicked(object sender, EventArgs args)
{ {
_recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
} }
public void OnCloseClicked(object sender, EventArgs args) public void OnCloseClicked(object sender, EventArgs args)
{ {
Close(); Close();
} }
private void InitializeComponent() private void InitializeComponent()
{ {
// TODO: iOS does not support dynamically loading assemblies // TODO: iOS does not support dynamically loading assemblies

20
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,5 +1,7 @@
using System.Reactive; using System.Reactive;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Dialogs;
using ReactiveUI; using ReactiveUI;
namespace ControlCatalog.ViewModels namespace ControlCatalog.ViewModels
@ -26,6 +28,20 @@ namespace ControlCatalog.ViewModels
{ {
NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error)); NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error));
}); });
AboutCommand = ReactiveCommand.CreateFromTask(async () =>
{
var dialog = new AboutAvaloniaDialog();
var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
await dialog.ShowDialog(mainWindow);
});
ExitCommand = ReactiveCommand.Create(() =>
{
(App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
});
} }
public IManagedNotificationManager NotificationManager public IManagedNotificationManager NotificationManager
@ -39,5 +55,9 @@ namespace ControlCatalog.ViewModels
public ReactiveCommand<Unit, Unit> ShowManagedNotificationCommand { get; } public ReactiveCommand<Unit, Unit> ShowManagedNotificationCommand { get; }
public ReactiveCommand<Unit, Unit> ShowNativeNotificationCommand { get; } public ReactiveCommand<Unit, Unit> ShowNativeNotificationCommand { get; }
public ReactiveCommand<Unit, Unit> AboutCommand { get; }
public ReactiveCommand<Unit, Unit> ExitCommand { get; }
} }
} }

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;

8
src/Avalonia.Controls/Application.cs

@ -48,6 +48,14 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
/// <summary>
/// Creates an instance of the <see cref="Application"/> class.
/// </summary>
public Application()
{
Name = "Avalonia Application";
}
/// <summary> /// <summary>
/// Gets the current instance of the <see cref="Application"/> class. /// Gets the current instance of the <see cref="Application"/> class.
/// </summary> /// </summary>

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

@ -19,8 +19,6 @@ namespace Avalonia.Controls.Generators
{ {
var tabItem = (TabItem)base.CreateContainer(item); var tabItem = (TabItem)base.CreateContainer(item);
tabItem.ParentTabControl = Owner;
tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty];
if (tabItem.HeaderTemplate == null) if (tabItem.HeaderTemplate == null)
@ -48,11 +46,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;
} }
} }

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

22
src/Avalonia.Controls/ListBox.cs

@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -132,21 +133,26 @@ namespace Avalonia.Controls
{ {
base.OnPointerPressed(e); base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) if (e.Source is IVisual source)
{ {
e.Handled = UpdateSelectionFromEventSource( var point = e.GetCurrentPoint(source);
e.Source,
true, if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed)
(e.InputModifiers & InputModifiers.Shift) != 0, {
(e.InputModifiers & InputModifiers.Control) != 0, e.Handled = UpdateSelectionFromEventSource(
e.MouseButton == MouseButton.Right); e.Source,
true,
(e.KeyModifiers & KeyModifiers.Shift) != 0,
(e.KeyModifiers & KeyModifiers.Control) != 0,
point.Properties.IsRightButtonPressed);
}
} }
} }
protected override void OnTemplateApplied(TemplateAppliedEventArgs e) protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{ {
base.OnTemplateApplied(e);
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer"); Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
base.OnTemplateApplied(e);
} }
} }
} }

4
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -804,9 +804,9 @@ namespace Avalonia.Controls
private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e)
{ {
if (e.Device.Captured != Spinner) if (e.Pointer.Captured != Spinner)
{ {
Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); Dispatcher.UIThread.InvokeAsync(() => { e.Pointer.Capture(Spinner); }, DispatcherPriority.Input);
} }
} }

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;

39
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;
@ -358,17 +367,17 @@ namespace Avalonia.Controls.Primitives
{ {
if ((container.ContainerControl as ISelectable)?.IsSelected == true) if ((container.ContainerControl as ISelectable)?.IsSelected == true)
{ {
if (SelectedIndex == -1) if (SelectionMode.HasFlag(SelectionMode.Multiple))
{
SelectedIndex = container.Index;
}
else
{ {
if (_selection.Add(container.Index)) if (_selection.Add(container.Index))
{ {
resetSelectedItems = true; resetSelectedItems = true;
} }
} }
else
{
SelectedIndex = container.Index;
}
MarkContainerSelected(container.ContainerControl, true); MarkContainerSelected(container.ContainerControl, true);
} }
@ -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;
}
} }
} }
} }

10
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -44,9 +45,14 @@ namespace Avalonia.Controls.Primitives
{ {
base.OnPointerPressed(e); base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left) if (e.Source is IVisual source)
{ {
e.Handled = UpdateSelectionFromEventSource(e.Source); var point = e.GetCurrentPoint(source);
if (point.Properties.IsLeftButtonPressed)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
} }
} }
} }

2
src/Avalonia.Controls/Primitives/Thumb.cs

@ -73,7 +73,6 @@ namespace Avalonia.Controls.Primitives
protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
e.Device.Capture(this);
e.Handled = true; e.Handled = true;
_lastPoint = e.GetPosition(this); _lastPoint = e.GetPosition(this);
@ -92,7 +91,6 @@ namespace Avalonia.Controls.Primitives
{ {
if (_lastPoint.HasValue) if (_lastPoint.HasValue)
{ {
e.Device.Capture(null);
e.Handled = true; e.Handled = true;
_lastPoint = null; _lastPoint = null;

57
src/Avalonia.Controls/TabControl.cs

@ -4,7 +4,6 @@
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -70,6 +69,7 @@ namespace Avalonia.Controls
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected); SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
AffectsMeasure<TabControl>(TabStripPlacementProperty); AffectsMeasure<TabControl>(TabStripPlacementProperty);
SelectedIndexProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent(e));
} }
/// <summary> /// <summary>
@ -145,6 +145,61 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter); return RegisterContentPresenter(presenter);
} }
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
{
base.OnContainersMaterialized(e);
if (SelectedContent != null || SelectedIndex == -1)
{
return;
}
var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex);
if (container == null)
{
return;
}
UpdateSelectedContent(container);
}
private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e)
{
var index = (int)e.NewValue;
if (index == -1)
{
SelectedContentTemplate = null;
SelectedContent = null;
return;
}
var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index);
if (container == null)
{
return;
}
UpdateSelectedContent(container);
}
private void UpdateSelectedContent(IContentControl item)
{
if (SelectedContentTemplate != item.ContentTemplate)
{
SelectedContentTemplate = item.ContentTemplate;
}
if (SelectedContent != item.Content)
{
SelectedContent = item.Content;
}
}
/// <summary> /// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control. /// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// </summary> /// </summary>

21
src/Avalonia.Controls/TabItem.cs

@ -30,7 +30,6 @@ namespace Avalonia.Controls
{ {
SelectableMixin.Attach<TabItem>(IsSelectedProperty); SelectableMixin.Attach<TabItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true);
IsSelectedProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateSelectedContent(e));
DataContextProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateHeader(e)); DataContextProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateHeader(e));
} }
@ -54,8 +53,6 @@ namespace Avalonia.Controls
set { SetValue(IsSelectedProperty, value); } set { SetValue(IsSelectedProperty, value); }
} }
internal TabControl ParentTabControl { get; set; }
private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{ {
if (Header == null) if (Header == null)
@ -83,23 +80,5 @@ namespace Avalonia.Controls
} }
} }
} }
private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e)
{
if (!IsSelected)
{
return;
}
if (ParentTabControl.SelectedContentTemplate != ContentTemplate)
{
ParentTabControl.SelectedContentTemplate = ContentTemplate;
}
if (ParentTabControl.SelectedContent != Content)
{
ParentTabControl.SelectedContent = Content;
}
}
} }
} }

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

8
src/Avalonia.Controls/TextBox.cs

@ -677,13 +677,13 @@ namespace Avalonia.Controls
} }
} }
e.Device.Capture(_presenter); e.Pointer.Capture(_presenter);
e.Handled = true; e.Handled = true;
} }
protected override void OnPointerMoved(PointerEventArgs e) protected override void OnPointerMoved(PointerEventArgs e)
{ {
if (_presenter != null && e.Device.Captured == _presenter) if (_presenter != null && e.Pointer.Captured == _presenter)
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
@ -694,9 +694,9 @@ namespace Avalonia.Controls
protected override void OnPointerReleased(PointerReleasedEventArgs e) protected override void OnPointerReleased(PointerReleasedEventArgs e)
{ {
if (_presenter != null && e.Device.Captured == _presenter) if (_presenter != null && e.Pointer.Captured == _presenter)
{ {
e.Device.Capture(null); e.Pointer.Capture(null);
} }
} }

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)

19
src/Avalonia.Controls/TreeView.cs

@ -507,14 +507,19 @@ namespace Avalonia.Controls
{ {
base.OnPointerPressed(e); base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) if (e.Source is IVisual source)
{ {
e.Handled = UpdateSelectionFromEventSource( var point = e.GetCurrentPoint(source);
e.Source,
true, if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed)
(e.InputModifiers & InputModifiers.Shift) != 0, {
(e.InputModifiers & InputModifiers.Control) != 0, e.Handled = UpdateSelectionFromEventSource(
e.MouseButton == MouseButton.Right); e.Source,
true,
(e.KeyModifiers & KeyModifiers.Shift) != 0,
(e.KeyModifiers & KeyModifiers.Control) != 0,
point.Properties.IsRightButtonPressed);
}
} }
} }

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>

105
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml

@ -0,0 +1,105 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
MaxWidth="400"
MaxHeight="475"
MinWidth="430"
MinHeight="475"
Title="About Avalonia"
Background="Purple"
FontFamily="/Assets/Roboto-Light.ttf#Roboto"
x:Class="Avalonia.Dialogs.AboutAvaloniaDialog">
<Window.Styles>
<Style>
<Style.Resources>
<DrawingGroup x:Key="AvaloniaLogo">
<GeometryDrawing Geometry="m 150.66581 0.66454769 c -54.77764 0 -101.0652 38.86360031 -112.62109 90.33008031 a 26.1 26.1 0 0 1 18.92187 25.070312 26.1 26.1 0 0 1 -18.91992 25.08202 c 11.56024 51.46073 57.8456 90.31837 112.61914 90.31837 63.37832 0 115.40039 -52.02207 115.40039 -115.40039 0 -63.378322 -52.02207 -115.40039231 -115.40039 -115.40039231 z m 0 60.00000031 c 30.95192 0 55.40039 24.44847 55.40039 55.400392 0 30.9519 -24.44847 55.40039 -55.40039 55.40039 -30.95191 0 -55.40039 -24.44848 -55.40039 -55.40039 0 -30.951922 24.44848 -55.400392 55.40039 -55.400392 z">
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="272,411" EndPoint="435,248">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#B0B0B0" Offset="0" />
<GradientStop Color="#FFFFFF" Offset="0.6784" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Brush="#B0B0B0">
<GeometryDrawing.Geometry>
<EllipseGeometry Rect="9.6,95.8,40.6,40.6" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="206.06355, 114.56503,60,116.2" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</Style.Resources>
</Style>
<Style Selector="Rectangle.Abstract">
<Setter Property="Fill" Value="White" />
<Setter Property="Width" Value="750" />
<Setter Property="Height" Value="700" />
</Style>
<Style Selector="Button.Hyperlink">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Margin" Value="-5"/>
<Setter Property="Foreground" Value="#419df2" />
<Setter Property="Command" Value="{Binding OpenBrowser}" />
<Setter Property="Content" Value="{Binding $self.CommandParameter}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</Window.Styles>
<Grid Background="#4A255D">
<Canvas>
<Rectangle Classes="Abstract" Canvas.Top="90" Opacity="0.132">
<Rectangle.RenderTransform>
<RotateTransform Angle="-2" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Classes="Abstract" Canvas.Top="95" Opacity="0.3">
<Rectangle.RenderTransform>
<RotateTransform Angle="-4" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Classes="Abstract" Canvas.Top="100" Opacity="0.3">
<Rectangle.RenderTransform>
<RotateTransform Angle="-8" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Classes="Abstract" Canvas.Top="105" Opacity="0.7">
<Rectangle.RenderTransform>
<RotateTransform Angle="-12" />
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="18">
<Border Height="70" Width="70">
<DrawingPresenter Drawing="{DynamicResource AvaloniaLogo}" />
</Border>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10,-10,0,0">
<TextBlock Text="Avalonia 0.9" FontSize="40" Foreground="White" />
<TextBlock Text="Development Build" Margin="0,-10,0,0" FontSize="15" Foreground="White" />
</StackPanel>
</StackPanel>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Spacing="20" Margin="10 60 10 0">
<TextBlock Text="This product is built with the Avalonia cross-platform UI Framework. &#x0A;&#x0A;Avalonia is made possible by the generous support of it's contributors and community." TextWrapping="Wrap" TextAlignment="Center" HorizontalAlignment="Center" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
<TextBlock Text="Main source repository | " />
<Button Classes="Hyperlink" CommandParameter="https://github.com/AvaloniaUI/Avalonia/" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
<TextBlock Text="Documentation and Information | " />
<Button Classes="Hyperlink" CommandParameter="https://avaloniaui.net/" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
<TextBlock Text="Chat Room | " />
<Button Classes="Hyperlink" CommandParameter="https://gitter.im/AvaloniaUI/Avalonia/" />
</StackPanel>
</StackPanel>
<StackPanel VerticalAlignment="Bottom" Margin="10">
<TextBlock Text="© 2019 The Avalonia Project" TextWrapping="Wrap" HorizontalAlignment="Center" />
</StackPanel>
</Grid>
</Window>

62
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@ -0,0 +1,62 @@
// 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.Diagnostics;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Avalonia.Dialogs
{
public class AboutAvaloniaDialog : Window
{
public AboutAvaloniaDialog()
{
AvaloniaXamlLoader.Load(this);
DataContext = this;
}
public static void OpenBrowser(string url)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// If no associated application/json MimeType is found xdg-open opens retrun error
// but it tries to open it anyway using the console editor (nano, vim, other..)
ShellExec($"xdg-open {url}", waitForExit: false);
}
else
{
using (Process process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"-e {url}" : "",
CreateNoWindow = true,
UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
}));
}
}
private static void ShellExec(string cmd, bool waitForExit = true)
{
var escapedArgs = cmd.Replace("\"", "\\\"");
using (var process = Process.Start(
new ProcessStartInfo
{
FileName = "/bin/sh",
Arguments = $"-c \"{escapedArgs}\"",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}
))
{
if (waitForExit)
{
process.WaitForExit();
}
}
}
}
}

BIN
src/Avalonia.Dialogs/Assets/Roboto-Light.ttf

Binary file not shown.

1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -7,6 +7,7 @@
<AvaloniaResource Include="**\*.xaml"> <AvaloniaResource Include="**\*.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</AvaloniaResource> </AvaloniaResource>
<AvaloniaResource Include="Assets\*" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />

7
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@ -210,7 +210,12 @@ namespace Avalonia.Dialogs
if (!_selectingDirectory) if (!_selectingDirectory)
{ {
FileName = SelectedItems.FirstOrDefault()?.DisplayName; var selectedItem = SelectedItems.FirstOrDefault();
if (selectedItem != null)
{
FileName = selectedItem.DisplayName;
}
} }
} }
} }

2
src/Avalonia.Input/AccessKeyHandler.cs

@ -182,7 +182,7 @@ namespace Avalonia.Input
{ {
bool menuIsOpen = MainMenu?.IsOpen == true; bool menuIsOpen = MainMenu?.IsOpen == true;
if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen)
{ {
// If any other key is pressed with the Alt key held down, or the main menu is open, // If any other key is pressed with the Alt key held down, or the main menu is open,
// find all controls who have registered that access key. // find all controls who have registered that access key.

2
src/Avalonia.Input/FocusManager.cs

@ -180,7 +180,7 @@ namespace Avalonia.Input
if (sender == e.Source && ev.MouseButton == MouseButton.Left) if (sender == e.Source && ev.MouseButton == MouseButton.Left)
{ {
var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement); var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement);
if (element == null || !CanFocus(element)) if (element == null || !CanFocus(element))
{ {

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/Avalonia.Native.csproj

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

34
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -3,12 +3,15 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Dialogs;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Native namespace Avalonia.Native
{ {
@ -211,6 +214,29 @@ namespace Avalonia.Native
DoLayoutReset(); DoLayoutReset();
} }
private static NativeMenu CreateDefaultAppMenu()
{
var result = new NativeMenu();
var aboutItem = new NativeMenuItem
{
Header = "About Avalonia",
};
aboutItem.Clicked += async (sender, e) =>
{
var dialog = new AboutAvaloniaDialog();
var mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
await dialog.ShowDialog(mainWindow);
};
result.Add(aboutItem);
return result;
}
private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{ {
QueueReset(); QueueReset();
@ -241,6 +267,10 @@ namespace Avalonia.Native
{ {
SetMenu(_menu); SetMenu(_menu);
} }
else
{
SetMenu(CreateDefaultAppMenu());
}
} }
else else
{ {
@ -321,7 +351,7 @@ namespace Avalonia.Native
}), new MenuActionCallback(() => { item.RaiseClick(); })); }), new MenuActionCallback(() => { item.RaiseClick(); }));
menu.AddItem(menuItem); menu.AddItem(menuItem);
if (item.Menu?.Items?.Count > 0) if (item.Menu?.Items?.Count >= 0)
{ {
var submenu = _factory.CreateMenu(); var submenu = _factory.CreateMenu();
@ -362,7 +392,7 @@ namespace Avalonia.Native
return false; return false;
}), new MenuActionCallback(() => { item.RaiseClick(); })); }), new MenuActionCallback(() => { item.RaiseClick(); }));
if (item.Menu?.Items.Count > 0 || isMainMenu) if (item.Menu?.Items.Count >= 0 || isMainMenu)
{ {
var subMenu = CreateSubmenu(item.Menu?.Items); var subMenu = CreateSubmenu(item.Menu?.Items);

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -21,7 +21,6 @@ namespace Avalonia.Native
[DllImport("libAvaloniaNative")] [DllImport("libAvaloniaNative")]
static extern IntPtr CreateAvaloniaNative(); static extern IntPtr CreateAvaloniaNative();
internal static readonly MouseDevice MouseDevice = new MouseDevice();
internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice();
public Size DoubleClickSize => new Size(4, 4); public Size DoubleClickSize => new Size(4, 4);
@ -95,7 +94,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()))

7
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>();
} }
@ -96,7 +96,7 @@ namespace Avalonia.Native
public Action<Rect> Paint { get; set; } public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; } public Action<Size> Resized { get; set; }
public Action Closed { get; set; } public Action Closed { get; set; }
public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice; public IMouseDevice MouseDevice => _mouse;
public abstract IPopupImpl CreatePopup(); public abstract IPopupImpl CreatePopup();
@ -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); new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default);
}
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)

43
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,18 +617,18 @@ 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,
new Point(), WindowsKeyboardDevice.Instance.Modifiers); new Point(-1,-1), WindowsKeyboardDevice.Instance.Modifiers);
break; break;
case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN:
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
@ -635,7 +636,7 @@ namespace Avalonia.Win32
: msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN
? RawPointerEventType.RightButtonDown ? RawPointerEventType.RightButtonDown
: RawPointerEventType.MiddleButtonDown, : RawPointerEventType.MiddleButtonDown,
new Point(0, 0), GetMouseModifiers(wParam)); PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam));
break; break;
case WindowsMessage.WM_TOUCH: case WindowsMessage.WM_TOUCH:
var touchInputCount = wParam.ToInt32(); var touchInputCount = wParam.ToInt32();
@ -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
{ {

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

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)

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

@ -47,6 +47,26 @@ namespace Avalonia.Controls.UnitTests
Assert.IsType<ItemsPresenter>(target.Presenter); Assert.IsType<ItemsPresenter>(target.Presenter);
} }
[Fact]
public void ListBox_Should_Find_Scrollviewer_In_Template()
{
var target = new ListBox
{
Template = ListBoxTemplate(),
};
ScrollViewer viewer = null;
target.TemplateApplied += (sender, e) =>
{
viewer = target.Scroll as ScrollViewer;
};
Prepare(target);
Assert.NotNull(viewer);
}
[Fact] [Fact]
public void ListBoxItem_Containers_Should_Be_Generated() public void ListBoxItem_Containers_Should_Be_Generated()
{ {

226
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()
{ {
@ -1020,6 +1185,33 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.MoveSelection(NavigationDirection.Next, true); target.MoveSelection(NavigationDirection.Next, true);
} }
[Fact]
public void Pre_Selecting_Item_Should_Set_Selection_After_It_Was_Added_When_AlwaysSelected()
{
var target = new TestSelector(SelectionMode.AlwaysSelected)
{
Template = Template()
};
var second = new Item { IsSelected = true };
var items = new AvaloniaList<object>
{
new Item(),
second
};
target.Items = items;
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Assert.Equal(second, target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
private FuncControlTemplate Template() private FuncControlTemplate Template()
{ {
return new FuncControlTemplate<SelectingItemsControl>((control, scope) => return new FuncControlTemplate<SelectingItemsControl>((control, scope) =>
@ -1028,6 +1220,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));
} }
@ -1067,10 +1260,39 @@ namespace Avalonia.Controls.UnitTests.Primitives
private class TestSelector : SelectingItemsControl private class TestSelector : SelectingItemsControl
{ {
public TestSelector()
{
}
public TestSelector(SelectionMode selectionMode)
{
SelectionMode = selectionMode;
}
public new bool MoveSelection(NavigationDirection direction, bool wrap) public new bool MoveSelection(NavigationDirection direction, bool wrap)
{ {
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;
}
} }
} }

1
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1080,6 +1080,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
var target = new TestSelector var target = new TestSelector
{ {
Items = items, Items = items,
SelectionMode = SelectionMode.Multiple,
Template = Template(), Template = Template(),
}; };

43
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;
@ -44,6 +45,29 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(selected, target.SelectedItem); Assert.Equal(selected, target.SelectedItem);
} }
[Fact]
public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added()
{
var target = new TabControl
{
Template = TabControlTemplate(),
};
const string secondContent = "Second";
var items = new AvaloniaList<object>
{
new TabItem { Header = "First"},
new TabItem { Header = "Second", Content = secondContent, IsSelected = true }
};
target.Items = items;
ApplyTemplate(target);
Assert.Equal(secondContent, target.SelectedContent);
}
[Fact] [Fact]
public void Logical_Children_Should_Be_TabItems() public void Logical_Children_Should_Be_TabItems()
{ {
@ -287,6 +311,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>(),

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