Browse Source

Merge branch 'master' into shape-fix

pull/3233/head
Dariusz Komosiński 7 years ago
committed by GitHub
parent
commit
9ae3a54ca2
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\Binding.props = build\Binding.props
build\BuildTargets.targets = build\BuildTargets.targets
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
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
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution

2
azure-pipelines.yml

@ -52,7 +52,7 @@ jobs:
sdk: 'macosx10.14'
configuration: 'Release'
xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
xcodeVersion: 'default' # Options: 8, 9, default, specifyPath
xcodeVersion: '10' # Options: 8, 9, default, specifyPath
args: '-derivedDataPath ./'
- 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">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" />
<PackageReference Include="SkiaSharp" Version="1.68.1-rc.153" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1-rc.153" />
</ItemGroup>
</Project>

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

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

12
samples/ControlCatalog/App.xaml

@ -17,16 +17,4 @@
</Style>
<StyleInclude Source="/SideBar.xaml"/>
</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>

11
samples/ControlCatalog/App.xaml.cs

@ -8,20 +8,9 @@ namespace ControlCatalog
{
public class App : Application
{
private NativeMenu _recentMenu;
public override void Initialize()
{
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()

24
samples/ControlCatalog/MainWindow.xaml

@ -37,12 +37,20 @@
</NativeMenu>
</NativeMenu.Menu>
<Window.DataTemplates>
<DataTemplate DataType="vm:NotificationViewModel">
<v:CustomNotificationView />
</DataTemplate>
</Window.DataTemplates>
<Panel>
<local:MainView/>
</Panel>
<Window.DataTemplates>
<DataTemplate DataType="vm:NotificationViewModel">
<v:CustomNotificationView />
</DataTemplate>
</Window.DataTemplates>
<DockPanel LastChildFill="True">
<Menu Name="MainMenu" DockPanel.Dock="Top">
<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>

14
samples/ControlCatalog/MainWindow.xaml.cs

@ -31,20 +31,28 @@ namespace ControlCatalog
DataContext = new MainWindowViewModel(_notificationArea);
_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)
{
_recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
}
public void OnCloseClicked(object sender, EventArgs args)
{
Close();
}
private void InitializeComponent()
{
// TODO: iOS does not support dynamically loading assemblies

20
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,5 +1,7 @@
using System.Reactive;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Dialogs;
using ReactiveUI;
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));
});
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
@ -39,5 +55,9 @@ namespace ControlCatalog.ViewModels
public ReactiveCommand<Unit, Unit> ShowManagedNotificationCommand { 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>
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)
{
return min;

8
src/Avalonia.Controls/Application.cs

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

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

@ -19,8 +19,6 @@ namespace Avalonia.Controls.Generators
{
var tabItem = (TabItem)base.CreateContainer(item);
tabItem.ParentTabControl = Owner;
tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty];
if (tabItem.HeaderTemplate == null)
@ -48,11 +46,6 @@ namespace Avalonia.Controls.Generators
tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
}
if (tabItem.Content == null)
{
tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty];
}
return tabItem;
}
}

867
src/Avalonia.Controls/GridSplitter.cs

@ -1,210 +1,841 @@
// 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.
// This source file is adapted from the Windows Presentation Foundation project.
// (https://github.com/dotnet/wpf/)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.VisualTree;
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <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>
/// <remarks>
/// Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext.
/// </remarks>
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);
var prevDefinitionMin = GetMinLength(_prevDefinition);
var prevDefinitionMax = GetMaxLength(_prevDefinition);
get => GetValue(ResizeDirectionProperty);
set => SetValue(ResizeDirectionProperty, value);
}
var nextDefinitionLen = GetActualLength(_nextDefinition);
var nextDefinitionMin = GetMinLength(_nextDefinition);
var nextDefinitionMax = GetMaxLength(_nextDefinition);
// Determine the minimum and maximum the columns can be resized
min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen);
max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin);
/// <summary>
/// Indicates which Columns or Rows the Splitter resizes.
/// </summary>
public GridResizeBehavior ResizeBehavior
{
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
// but resizes the splitter row/column when it's the first one.
// this is different, but more internally consistent.
if (_prevDefinition == null || _nextDefinition == null)
return;
get => GetValue(ShowsPreviewProperty);
set => SetValue(ShowsPreviewProperty, value);
}
/// <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;
double max;
double min;
GetDeltaConstraints(out min, out max);
delta = Math.Min(Math.Max(delta, min), max);
return direction;
}
var prevIsStar = IsStar(_prevDefinition);
var nextIsStar = IsStar(_nextDefinition);
/// <summary>
/// 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);
SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
CancelResize();
}
}
private double GetActualLength(DefinitionBase definition)
protected override void OnDragStarted(VectorEventArgs e)
{
if (definition == null)
return 0;
var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
base.OnDragStarted(e);
// TODO: Looks like that sometimes thumb will raise multiple drag started events.
// 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)
return 0;
var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight;
base.OnDragDelta(e);
if (_resizeData != null)
{
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)
return 0;
var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight;
base.OnDragCompleted(e);
if (_resizeData != null)
{
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;
return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar;
Key key = e.Key;
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;
if (columnDefinition != null)
// Restore original column/row lengths.
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;
if (columnDefinition != null)
return direction == GridResizeDirection.Columns ?
(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);
_grid = this.GetVisualParent<Grid>();
// For the case where both definition1 and 2 are stars, update all star values to match their current pixel values.
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
if (_orientation == Orientation.Vertical)
// For each definition, if it is a star, set is value to ActualLength in stars
// 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);
_definitions = _grid.ColumnDefinitions.Cast<DefinitionBase>().ToList();
definitionIndex = GetValue(Grid.ColumnProperty);
PseudoClasses.Add(":vertical");
SetDefinitionLength(_resizeData.Definition1, new GridLength(definition1Pixels));
}
else
{
Cursor = new Cursor(StandardCursorType.SizeNorthSouth);
definitionIndex = GetValue(Grid.RowProperty);
_definitions = _grid.RowDefinitions.Cast<DefinitionBase>().ToList();
PseudoClasses.Add(":horizontal");
SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels));
}
}
/// <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)
_prevDefinition = _definitions[definitionIndex - 1];
/// <summary>
/// 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)
_nextDefinition = _definitions[definitionIndex + 1];
MoveSplitter(horizontalChange, verticalChange);
_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())
return Orientation.Horizontal;
if (!_grid.RowDefinitions.Any())
return Orientation.Vertical;
private readonly TranslateTransform _translation;
private readonly Decorator _decorator;
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);
var row = GetValue(Grid.RowProperty);
var width = _grid.ColumnDefinitions[col].Width;
var height = _grid.RowDefinitions[row].Height;
if (width.IsAuto && !height.IsAuto)
/// <summary>
/// The Preview's Offset in the X direction from the GridSplitter.
/// </summary>
public double OffsetX
{
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)
.Any(c => c.GetType() != typeof(GridSplitter)))
protected override Size ArrangeOverride(Size finalSize)
{
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();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
if (Presenter != null)
{
Presenter.Items = newValue;
}
SubscribeToItems(newValue);
}
@ -370,6 +376,8 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdateItemCount();
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@ -381,7 +389,7 @@ namespace Avalonia.Controls
break;
}
UpdateItemCount();
Presenter?.ItemsChanged(e);
var collection = sender as ICollection;
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.Templates;
using Avalonia.Input;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -132,21 +133,26 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
if (e.Source is IVisual source)
{
e.Handled = UpdateSelectionFromEventSource(
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0,
e.MouseButton == MouseButton.Right);
var point = e.GetCurrentPoint(source);
if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed)
{
e.Handled = UpdateSelectionFromEventSource(
e.Source,
true,
(e.KeyModifiers & KeyModifiers.Shift) != 0,
(e.KeyModifiers & KeyModifiers.Control) != 0,
point.Properties.IsRightButtonPressed);
}
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
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)
{
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.
// 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
{
public interface IItemsPresenter : IPresenter
{
IEnumerable Items { get; set; }
IPanel Panel { get; }
void ItemsChanged(NotifyCollectionChangedEventArgs e);
void ScrollIntoView(object item);
}
}

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

@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters
_itemsSubscription?.Dispose();
_itemsSubscription = null;
if (_createdPanel && value is INotifyCollectionChanged incc)
if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc)
{
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
}
@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters
private set;
}
protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
/// <inheritdoc/>
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>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters
_createdPanel = true;
if (_itemsSubscription == null && Items is INotifyCollectionChanged incc)
if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc)
{
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
}

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

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

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

@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/>
protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
base.ItemsCollectionChanged(sender, e);
if (_updateCount > 0)
{
base.ItemsCollectionChanged(sender, e);
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)
{
case NotifyCollectionChangedAction.Add:
@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives
}
else
{
_selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
UpdateSelectedItem(_selection.First(), false);
}
break;
case NotifyCollectionChangedAction.Remove:
_selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
UpdateSelectedItem(_selection.First(), false);
ResetSelectedItems();
break;
@ -358,17 +367,17 @@ namespace Avalonia.Controls.Primitives
{
if ((container.ContainerControl as ISelectable)?.IsSelected == true)
{
if (SelectedIndex == -1)
{
SelectedIndex = container.Index;
}
else
if (SelectionMode.HasFlag(SelectionMode.Multiple))
{
if (_selection.Add(container.Index))
{
resetSelectedItems = true;
}
}
else
{
SelectedIndex = container.Index;
}
MarkContainerSelected(container.ContainerControl, true);
}
@ -1088,9 +1097,15 @@ namespace Avalonia.Controls.Primitives
}
else
{
SelectedIndex = _updateSelectedIndex != int.MinValue ?
_updateSelectedIndex :
AlwaysSelected ? 0 : -1;
if (_updateSelectedIndex != int.MinValue)
{
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.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
@ -44,9 +45,14 @@ namespace Avalonia.Controls.Primitives
{
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)
{
e.Device.Capture(this);
e.Handled = true;
_lastPoint = e.GetPosition(this);
@ -92,7 +91,6 @@ namespace Avalonia.Controls.Primitives
{
if (_lastPoint.HasValue)
{
e.Device.Capture(null);
e.Handled = true;
_lastPoint = null;

57
src/Avalonia.Controls/TabControl.cs

@ -4,7 +4,6 @@
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -70,6 +69,7 @@ namespace Avalonia.Controls
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
AffectsMeasure<TabControl>(TabStripPlacementProperty);
SelectedIndexProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent(e));
}
/// <summary>
@ -145,6 +145,61 @@ namespace Avalonia.Controls
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>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// </summary>

21
src/Avalonia.Controls/TabItem.cs

@ -30,7 +30,6 @@ namespace Avalonia.Controls
{
SelectableMixin.Attach<TabItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue(typeof(TabItem), true);
IsSelectedProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateSelectedContent(e));
DataContextProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateHeader(e));
}
@ -54,8 +53,6 @@ namespace Avalonia.Controls
set { SetValue(IsSelectedProperty, value); }
}
internal TabControl ParentTabControl { get; set; }
private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{
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
{
Constraint = constraint,
Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
Text = text ?? string.Empty,
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;
}
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);
@ -694,9 +694,9 @@ namespace Avalonia.Controls
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 Avalonia.Input;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -79,7 +80,10 @@ namespace Avalonia.Controls
{
StopTimer();
ToolTip.SetIsOpen(control, true);
if ((control as IVisual).IsAttachedToVisualTree)
{
ToolTip.SetIsOpen(control, true);
}
}
private void Close(Control control)

19
src/Avalonia.Controls/TreeView.cs

@ -507,14 +507,19 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
if (e.Source is IVisual source)
{
e.Handled = UpdateSelectionFromEventSource(
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0,
e.MouseButton == MouseButton.Right);
var point = e.GetCurrentPoint(source);
if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed)
{
e.Handled = UpdateSelectionFromEventSource(
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>
static WrapPanel()
{
AffectsMeasure<WrapPanel>(OrientationProperty);
AffectsMeasure<WrapPanel>(OrientationProperty, ItemWidthProperty, ItemHeightProperty);
}
/// <summary>

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

@ -2,7 +2,7 @@
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode"
@ -20,7 +20,7 @@
</TreeView.Styles>
</TreeView>
<GridSplitter Width="4" Grid.Column="1" />
<GridSplitter Grid.Column="1" />
<ContentControl Content="{Binding Details}" Grid.Column="2" />
</Grid>
</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">
<SubType>Designer</SubType>
</AvaloniaResource>
<AvaloniaResource Include="Assets\*" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />

7
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@ -210,7 +210,12 @@ namespace Avalonia.Dialogs
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;
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,
// 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)
{
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))
{

13
src/Avalonia.Input/MouseDevice.cs

@ -14,13 +14,14 @@ namespace Avalonia.Input
/// <summary>
/// Represents a mouse device.
/// </summary>
public class MouseDevice : IMouseDevice
public class MouseDevice : IMouseDevice, IDisposable
{
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
private readonly Pointer _pointer;
private bool _disposed;
public MouseDevice(Pointer pointer = null)
{
@ -126,7 +127,9 @@ namespace Avalonia.Input
{
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);
var props = CreateProperties(e);
@ -441,5 +444,11 @@ namespace Avalonia.Input
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)
Captured.DetachedFromVisualTree -= OnCaptureDetached;
var oldCapture = control;
var oldCapture = Captured;
Captured = control;
PlatformCapture(control);
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
{
public sealed class PointerPoint
@ -27,9 +30,9 @@ namespace Avalonia.Input
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
{
PointerUpdateKind = kind;
IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton);
IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton);
IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton);
IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton);
IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton);
IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton);
// The underlying input source might be reporting the previous 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.Linq;
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
/// </remarks>
/// </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)(modifiers & RawInputModifiers.KeyboardMask);
@ -28,6 +30,8 @@ namespace Avalonia.Input
public void ProcessRawEvent(RawInputEventArgs ev)
{
if(_disposed)
return;
var args = (RawTouchEventArgs)ev;
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="SharpGen.Runtime.COM" Version="1.1.0" />
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.Dialogs.csproj" />
</ItemGroup>
</Project>

34
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -3,12 +3,15 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Native.Interop;
using Avalonia.Platform.Interop;
using Avalonia.Threading;
using Avalonia.Dialogs;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Native
{
@ -211,6 +214,29 @@ namespace Avalonia.Native
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)
{
QueueReset();
@ -241,6 +267,10 @@ namespace Avalonia.Native
{
SetMenu(_menu);
}
else
{
SetMenu(CreateDefaultAppMenu());
}
}
else
{
@ -321,7 +351,7 @@ namespace Avalonia.Native
}), new MenuActionCallback(() => { item.RaiseClick(); }));
menu.AddItem(menuItem);
if (item.Menu?.Items?.Count > 0)
if (item.Menu?.Items?.Count >= 0)
{
var submenu = _factory.CreateMenu();
@ -362,7 +392,7 @@ namespace Avalonia.Native
return false;
}), new MenuActionCallback(() => { item.RaiseClick(); }));
if (item.Menu?.Items.Count > 0 || isMainMenu)
if (item.Menu?.Items.Count >= 0 || isMainMenu)
{
var subMenu = CreateSubmenu(item.Menu?.Items);

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -21,7 +21,6 @@ namespace Avalonia.Native
[DllImport("libAvaloniaNative")]
static extern IntPtr CreateAvaloniaNative();
internal static readonly MouseDevice MouseDevice = new MouseDevice();
internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice();
public Size DoubleClickSize => new Size(4, 4);
@ -95,7 +94,6 @@ namespace Avalonia.Native
.Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IMouseDevice>().ToConstant(MouseDevice)
.Bind<IPlatformSettings>().ToConstant(this)
.Bind<IWindowingPlatform>().ToConstant(this)
.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 bool _deferredRendering = false;
private bool _gpu = false;
private readonly IMouseDevice _mouse;
private readonly MouseDevice _mouse;
private readonly IKeyboardDevice _keyboard;
private readonly IStandardCursorFactory _cursorFactory;
private Size _savedLogicalSize;
@ -38,7 +38,7 @@ namespace Avalonia.Native
_deferredRendering = opts.UseDeferredRendering;
_keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
_mouse = AvaloniaLocator.Current.GetService<IMouseDevice>();
_mouse = new MouseDevice();
_cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
}
@ -96,7 +96,7 @@ namespace Avalonia.Native
public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; }
public Action Closed { get; set; }
public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice;
public IMouseDevice MouseDevice => _mouse;
public abstract IPopupImpl CreatePopup();
@ -142,6 +142,7 @@ namespace Avalonia.Native
{
n?.Dispose();
}
_parent._mouse.Dispose();
}
void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke();

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

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

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

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

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
{
public class FamilyNameCollection : IEnumerable<string>
public sealed class FamilyNameCollection : IReadOnlyList<string>
{
/// <summary>
/// Initializes a new instance of the <see cref="FamilyNameCollection"/> class.
@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts
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 IReadOnlyList<FormattedTextStyleSpan> _spans;
private Typeface _typeface;
private double _fontSize;
private string _text;
private TextAlignment _textAlignment;
private TextWrapping _wrapping;
private TextWrapping _textWrapping;
/// <summary>
/// Initializes a new instance of the <see cref="FormattedText"/> class.
@ -37,6 +38,31 @@ namespace Avalonia.Media
_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>
/// Gets the bounds of the text within the <see cref="Constraint"/>.
/// </summary>
@ -61,6 +87,16 @@ namespace Avalonia.Media
set => Set(ref _typeface, value);
}
/// <summary>
/// Gets or sets the font size.
/// </summary>
public double FontSize
{
get => _fontSize;
set => Set(ref _fontSize, value);
}
/// <summary>
/// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text.
@ -92,10 +128,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the text wrapping.
/// </summary>
public TextWrapping Wrapping
public TextWrapping TextWrapping
{
get => _wrapping;
set => Set(ref _wrapping, value);
get => _textWrapping;
set => Set(ref _textWrapping, value);
}
/// <summary>
@ -110,8 +146,9 @@ namespace Avalonia.Media
_platformImpl = _platform.CreateFormattedText(
_text,
_typeface,
_fontSize,
_textAlignment,
_wrapping,
_textWrapping,
_constraint,
_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
{
/// <summary>
/// Represents a typeface.
/// </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);
private GlyphTypeface _glyphTypeface;
/// <summary>
/// Initializes a new instance of the <see cref="Typeface"/> class.
/// </summary>
/// <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>
public Typeface(
FontFamily fontFamily,
double fontSize = 12,
FontStyle style = FontStyle.Normal,
FontWeight weight = FontWeight.Normal)
/// <param name="style">The font style.</param>
public Typeface([NotNull]FontFamily fontFamily,
FontWeight weight = FontWeight.Normal,
FontStyle style = FontStyle.Normal)
{
if (fontSize <= 0)
{
throw new ArgumentException("Font size must be > 0.");
}
if (weight <= 0)
{
throw new ArgumentException("Font weight must be > 0.");
}
FontFamily = fontFamily;
FontSize = fontSize;
Style = style;
Weight = weight;
}
@ -42,15 +41,12 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="Typeface"/> class.
/// </summary>
/// <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="weight">The font weight.</param>
public Typeface(
string fontFamilyName,
double fontSize = 12,
FontStyle style = FontStyle.Normal,
FontWeight weight = FontWeight.Normal)
: this(new FontFamily(fontFamilyName), fontSize, style, weight)
public Typeface(string fontFamilyName,
FontWeight weight = FontWeight.Normal,
FontStyle style = FontStyle.Normal)
: this(new FontFamily(fontFamilyName), weight, style)
{
}
@ -59,11 +55,6 @@ namespace Avalonia.Media
/// </summary>
public FontFamily FontFamily { get; }
/// <summary>
/// Gets the size of the font in DIPs.
/// </summary>
public double FontSize { get; }
/// <summary>
/// Gets the font style.
/// </summary>
@ -73,5 +64,59 @@ namespace Avalonia.Media
/// Gets the font weight.
/// </summary>
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>
public interface IPlatformRenderInterface
{
/// <summary>
/// Get all installed fonts in the system
/// </summary>
IEnumerable<string> InstalledFontNames { get; }
/// <summary>
/// Creates a formatted text implementation.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="typeface">The base typeface.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="textAlignment">The text alignment.</param>
/// <param name="wrapping">The text wrapping mode.</param>
/// <param name="constraint">The text layout constraints.</param>
@ -31,6 +27,7 @@ namespace Avalonia.Platform
IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@ -114,5 +111,14 @@ namespace Avalonia.Platform
/// <param name="stride">The number of bytes per row.</param>
/// <returns>An <see cref="IBitmapImpl"/>.</returns>
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
{
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 int _framesThisSecond;
private int _fps;
@ -18,7 +19,8 @@ namespace Avalonia.Rendering
{
_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);
}
if (visual.VisualRoot != null)
if (visual.VisualRoot == scene.Root.Visual)
{
if (node?.Parent != null &&
visual.VisualParent != null &&

2
src/Avalonia.X11/X11Platform.cs

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

10
src/Avalonia.X11/X11Window.cs

@ -32,7 +32,8 @@ namespace Avalonia.X11
private PixelPoint? _configurePoint;
private bool _triggeredExpose;
private IInputRoot _inputRoot;
private readonly IMouseDevice _mouse;
private readonly MouseDevice _mouse;
private readonly TouchDevice _touch;
private readonly IKeyboardDevice _keyboard;
private PixelPoint? _position;
private PixelSize _realSize;
@ -57,7 +58,8 @@ namespace Avalonia.X11
_platform = platform;
_popup = popupParent != null;
_x11 = platform.Info;
_mouse = platform.MouseDevice;
_mouse = new MouseDevice();
_touch = new TouchDevice();
_keyboard = platform.KeyboardDevice;
var glfeature = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
@ -702,6 +704,8 @@ namespace Avalonia.X11
_platform.XI2?.OnWindowDestroyed(_handle);
_handle = IntPtr.Zero;
Closed?.Invoke();
_mouse.Dispose();
_touch.Dispose();
}
if (_useRenderWindow && _renderHandle != IntPtr.Zero)
@ -830,6 +834,8 @@ namespace Avalonia.X11
}
public IMouseDevice MouseDevice => _mouse;
public TouchDevice TouchDevice => _touch;
public IPopupImpl CreatePopup()
=> _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 AvaloniaX11Platform _platform;
private readonly TouchDevice _touchDevice = new TouchDevice();
public bool Init(AvaloniaX11Platform platform)
{
@ -198,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd);
client.ScheduleInput(new RawTouchEventArgs(_touchDevice,
client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return;
}
@ -232,10 +230,10 @@ namespace Avalonia.X11
}
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));
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));
}
@ -248,7 +246,7 @@ namespace Avalonia.X11
: ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp)
: (RawPointerEventType?)null;
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));
}
@ -310,5 +308,7 @@ namespace Avalonia.X11
{
IInputRoot InputRoot { get; }
void ScheduleInput(RawInputEventArgs args);
IMouseDevice MouseDevice { get; }
TouchDevice TouchDevice { get; }
}
}

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

@ -12,5 +12,6 @@
</ItemGroup>
<Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
</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(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@ -28,47 +29,22 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B)
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);
skiaTypeface = typefaces.GetTypeFace(typeface);
}
else
{
if (typeface.FontFamily.FamilyNames.HasFallbacks)
{
foreach (var familyName in typeface.FontFamily.FamilyNames)
{
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();
TextEncoding = SKTextEncoding.Utf16,
IsStroke = false,
IsAntialias = true,
LcdRenderText = true,
SubpixelText = true,
Typeface = entry.SKTypeface,
TextSize = (float)fontSize,
TextAlign = textAlignment.ToSKTextAlign()
};
//currently Skia does not measure properly with 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;
_constraint = constraint;
@ -99,7 +75,24 @@ namespace Avalonia.Skia
public TextHitTestResult HitTestPoint(Point point)
{
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)))
{
@ -127,12 +120,15 @@ namespace Avalonia.Skia
line.Length : (line.Length - 1);
}
return new TextHitTestResult
if (y < nextTop)
{
IsInside = false,
TextPosition = line.Start + offset,
IsTrailing = Text.Length == (line.Start + offset + 1)
};
return new TextHitTestResult
{
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);

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.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls.Platform.Surfaces;
@ -17,12 +18,13 @@ namespace Avalonia.Skia
/// </summary>
internal class PlatformRenderInterface : IPlatformRenderInterface
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private readonly ICustomSkiaGpu _customSkiaGpu;
private GRContext GrContext { get; }
public IEnumerable<string> InstalledFontNames => SKFontManager.Default.FontFamilies;
public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
{
if (customSkiaGpu != null)
@ -52,12 +54,13 @@ namespace Avalonia.Skia
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
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);
@ -151,5 +154,10 @@ namespace Avalonia.Skia
{
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.Generic;
using System.Linq;
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, SKTypeface>> _fontFamilies =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, SKTypeface>>();
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> _fontFamilies =
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(typeface.FamilyName, out var fontFamily))
if (!_fontFamilies.TryGetValue(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;
switch (typeface.Style)
{
case FontStyle.Italic:
styleSlant = SKFontStyleSlant.Italic;
break;
case FontStyle.Oblique:
styleSlant = SKFontStyleSlant.Oblique;
break;
}
var key = new FontKey(fontWeight, fontStyle);
if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily))
{
return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight);
}
var weight = (SKFontStyleWeight)typeface.Weight;
var key = new FontKey(weight, styleSlant);
return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key));
return _fontFamilies.TryGetValue(familyName, out var fontFamily) ?
fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) :
new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default);
}
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(
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())
{
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())
{
keys = fontFamily.Keys.Where(
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();
fontFamily.TryGetValue(key, out var typeface);
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;
}
fontFamily.TryGetValue(key, out var entry);
public override bool Equals(object other)
{
return other is FontKey key && this.Equals(key);
}
private bool Equals(FontKey other)
{
return Slant == other.Slant &&
Weight == other.Weight;
}
return entry;
}
}
}

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

@ -45,9 +45,13 @@ namespace Avalonia.Skia
{
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;

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

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

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

@ -1,7 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Collections.Concurrent;
using Avalonia.Media;
using SkiaSharp;
@ -12,88 +12,36 @@ namespace Avalonia.Skia
/// </summary>
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 =
new Dictionary<string, Dictionary<FontKey, SKTypeface>>();
struct FontKey
public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
public readonly SKFontStyleSlant Slant;
public readonly SKFontStyleWeight Weight;
public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
if (fontFamily.Key != null)
{
Slant = slant;
Weight = weight;
return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily)
.Get(fontFamily.Name, fontWeight, fontStyle);
}
public override int GetHashCode()
{
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();
var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>());
return defaultTypeface.FamilyName;
}
var key = new FontKey(fontWeight, fontStyle);
private static SKTypeface GetTypeface(string name, FontKey key)
{
var familyKey = name;
if (!s_cache.TryGetValue(familyKey, out var entry))
if (typefaceCollection.TryGetValue(key, out var entry))
{
s_cache[familyKey] = entry = new Dictionary<FontKey, SKTypeface>();
return entry;
}
if (!entry.TryGetValue(key, out var typeface))
{
typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ??
GetTypeface(DefaultFamilyName, key);
var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default;
entry[key] = typeface;
}
var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle);
return typeface;
}
public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight)
{
var skStyle = SKFontStyleSlant.Upright;
entry = new TypefaceCollectionEntry(typeface, skTypeface);
switch (style)
{
case FontStyle.Italic:
skStyle = SKFontStyleSlant.Italic;
break;
case FontStyle.Oblique:
skStyle = SKFontStyleSlant.Oblique;
break;
}
typefaceCollection[key] = entry;
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>
<Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\SharpDX.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
<Import Project="..\..\..\build\JetBrains.Annotations.props" />
</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.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls;
@ -27,6 +28,8 @@ namespace Avalonia.Direct2D1
{
public class Direct2D1Platform : IPlatformRenderInterface
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private static readonly Direct2D1Platform s_instance = new Direct2D1Platform();
public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; }
@ -41,20 +44,6 @@ namespace Avalonia.Direct2D1
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 bool s_initialized = false;
@ -120,6 +109,7 @@ namespace Avalonia.Direct2D1
{
InitializeDirect2D();
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(s_instance);
AvaloniaLocator.CurrentMutable.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true;
}
@ -131,6 +121,7 @@ namespace Avalonia.Direct2D1
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@ -139,6 +130,7 @@ namespace Avalonia.Direct2D1
return new FormattedTextImpl(
text,
typeface,
fontSize,
textAlignment,
wrapping,
constraint,
@ -201,5 +193,10 @@ namespace Avalonia.Direct2D1
{
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 Avalonia.Media;
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
{
internal static class Direct2D1FontCollectionCache
{
private static readonly ConcurrentDictionary<FontFamilyKey, SharpDX.DirectWrite.FontCollection> s_cachedCollections;
internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection;
private static readonly ConcurrentDictionary<FontFamilyKey, FontCollection> s_cachedCollections;
internal static readonly FontCollection InstalledFontCollection;
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 fontCollection = GetOrAddFontCollection(fontFamily);
var fontFamilyName = FontFamily.Default.Name;
// Should this be cached?
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(
Direct2D1Platform.DirectWriteFactory,
fontFamilyName,
fontCollection,
(SharpDX.DirectWrite.FontWeight)typeface.Weight,
(SharpDX.DirectWrite.FontStyle)typeface.Style,
SharpDX.DirectWrite.FontStretch.Normal,
(float)typeface.FontSize);
InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
FontStretch.Normal,
(FontStyle)typeface.Style);
}
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 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(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@ -21,20 +22,20 @@ namespace Avalonia.Direct2D1.Media
{
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 =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
TextLayout = new DWrite.TextLayout(
Direct2D1Platform.DirectWriteFactory,
Text ?? string.Empty,
textFormat,
(float)constraint.Width,
(float)constraint.Height)
{
TextAlignment = textAlignment.ToDirect2D()
};
Direct2D1Platform.DirectWriteFactory,
Text ?? string.Empty,
textFormat,
(float)constraint.Width,
(float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() };
}
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
{
public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice();
public WindowsMouseDevice() : base(new WindowsMousePointer())
{
}
public WindowImpl CurrentWindow
{
get;
set;
}
class WindowsMousePointer : Pointer
{
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 bool _multitouch;
private TouchDevice _touchDevice = new TouchDevice();
private MouseDevice _mouseDevice = new WindowsMouseDevice();
private IInputRoot _owner;
private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock();
private bool _trackingMouse;
@ -205,7 +206,7 @@ namespace Avalonia.Win32
}
}
public IMouseDevice MouseDevice => WindowsMouseDevice.Instance;
public IMouseDevice MouseDevice => _mouseDevice;
public WindowState WindowState
{
@ -333,7 +334,7 @@ namespace Avalonia.Win32
public void BeginMoveDrag(PointerPressedEventArgs e)
{
WindowsMouseDevice.Instance.Capture(null);
_mouseDevice.Capture(null);
UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN,
new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero);
e.Pointer.Capture(null);
@ -356,7 +357,7 @@ namespace Avalonia.Win32
#if USE_MANAGED_DRAG
_managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position));
#else
WindowsMouseDevice.Instance.Capture(null);
_mouseDevice.Capture(null);
UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN,
new IntPtr((int)EdgeDic[edge]), IntPtr.Zero);
#endif
@ -437,9 +438,7 @@ namespace Avalonia.Win32
uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime());
RawInputEventArgs e = null;
WindowsMouseDevice.Instance.CurrentWindow = this;
switch ((UnmanagedMethods.WindowsMessage)msg)
{
case UnmanagedMethods.WindowsMessage.WM_ACTIVATE:
@ -485,6 +484,8 @@ namespace Avalonia.Win32
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
}
_mouseDevice.Dispose();
_touchDevice?.Dispose();
//Free other resources
Dispose();
return IntPtr.Zero;
@ -542,7 +543,7 @@ namespace Avalonia.Win32
if(ShouldIgnoreTouchEmulatedMessage())
break;
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
_mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN
@ -559,7 +560,7 @@ namespace Avalonia.Win32
if(ShouldIgnoreTouchEmulatedMessage())
break;
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
_mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP
@ -587,7 +588,7 @@ namespace Avalonia.Win32
}
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.Move,
@ -597,7 +598,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSEWHEEL:
e = new RawMouseWheelEventArgs(
WindowsMouseDevice.Instance,
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
@ -606,7 +607,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSEHWHEEL:
e = new RawMouseWheelEventArgs(
WindowsMouseDevice.Instance,
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
@ -616,18 +617,18 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE:
_trackingMouse = false;
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.LeaveWindow,
new Point(), WindowsKeyboardDevice.Instance.Modifiers);
new Point(-1,-1), WindowsKeyboardDevice.Instance.Modifiers);
break;
case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN:
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
_mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN
@ -635,7 +636,7 @@ namespace Avalonia.Win32
: msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN
? RawPointerEventType.RightButtonDown
: RawPointerEventType.MiddleButtonDown,
new Point(0, 0), GetMouseModifiers(wParam));
PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam));
break;
case WindowsMessage.WM_TOUCH:
var touchInputCount = wParam.ToInt32();
@ -649,9 +650,9 @@ namespace Avalonia.Win32
{
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner,
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ?
touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
RawPointerEventType.TouchEnd :
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ?
touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
@ -771,11 +772,11 @@ namespace Avalonia.Win32
{
var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam);
var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
modifiers |= RawInputModifiers.LeftMouseButton;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
modifiers |= RawInputModifiers.RightMouseButton;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
modifiers |= RawInputModifiers.MiddleMouseButton;
return modifiers;
}
@ -785,7 +786,7 @@ namespace Avalonia.Win32
// Ensure that the delegate doesn't get garbage collected by storing it as a field.
_wndProcDelegate = new UnmanagedMethods.WndProc(WndProc);
_className = "Avalonia-" + Guid.NewGuid();
_className = $"Avalonia-{Guid.NewGuid().ToString()}";
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.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -21,185 +19,366 @@ namespace Avalonia.Controls.UnitTests
public void Detects_Horizontal_Orientation()
{
GridSplitter splitter;
var grid = new Grid()
{
RowDefinitions = new RowDefinitions("*,Auto,*"),
ColumnDefinitions = new ColumnDefinitions("*,*"),
Children =
{
new Border { [Grid.RowProperty] = 0 },
(splitter = new GridSplitter { [Grid.RowProperty] = 1 }),
new Border { [Grid.RowProperty] = 2 }
}
};
var grid = new Grid
{
RowDefinitions = new RowDefinitions("*,Auto,*"),
ColumnDefinitions = new ColumnDefinitions("*,*"),
Children =
{
new Border { [Grid.RowProperty] = 0 },
(splitter = new GridSplitter { [Grid.RowProperty] = 1 }),
new Border { [Grid.RowProperty] = 2 }
}
};
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
Assert.Contains(splitter.Classes, ":horizontal".Equals);
Assert.Equal(GridResizeDirection.Rows, splitter.GetEffectiveResizeDirection());
}
[Fact]
public void Detects_Vertical_Orientation()
{
GridSplitter splitter;
var grid = new Grid()
{
ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
RowDefinitions = new RowDefinitions("*,*"),
Children =
{
new Border { [Grid.ColumnProperty] = 0 },
(splitter = new GridSplitter { [Grid.ColumnProperty] = 1}),
new Border { [Grid.ColumnProperty] = 2 },
}
};
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
RowDefinitions = new RowDefinitions("*,*"),
Children =
{
new Border { [Grid.ColumnProperty] = 0 },
(splitter = new GridSplitter { [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));
Assert.Contains(splitter.Classes, ":vertical".Equals);
Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection());
}
[Fact]
public void Detects_With_Both_Auto()
{
GridSplitter splitter;
var grid = new Grid()
{
ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
RowDefinitions = new RowDefinitions("Auto,Auto"),
Children =
{
new Border { [Grid.ColumnProperty] = 0 },
(splitter = new GridSplitter { [Grid.ColumnProperty] = 1}),
new Border { [Grid.ColumnProperty] = 2 },
}
};
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
RowDefinitions = new RowDefinitions("Auto,Auto"),
Children =
{
new Border { [Grid.ColumnProperty] = 0 },
(splitter = new GridSplitter { [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));
Assert.Contains(splitter.Classes, ":vertical".Equals);
Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection());
}
[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 splitter = new GridSplitter
{
[Grid.RowProperty] = 1,
};
var splitter = new GridSplitter { [Grid.RowProperty] = 1, ShowsPreview = showsPreview};
var control2 = new Border { [Grid.RowProperty] = 2 };
var rowDefinitions = new RowDefinitions()
{
new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 },
new RowDefinition(GridLength.Auto),
new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 },
};
var grid = new Grid()
{
RowDefinitions = rowDefinitions,
Children =
{
control1, splitter, control2
}
};
var rowDefinitions = new RowDefinitions
{
new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 },
new RowDefinition(GridLength.Auto),
new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 },
};
var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } };
var root = new TestRoot
{
Child = new VisualLayerManager
{
Child = grid
}
};
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 200));
root.Arrange(new Rect(0, 0, 100, 200));
splitter.RaiseEvent(
new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
splitter.RaiseEvent(new VectorEventArgs
{
RoutedEvent = Thumb.DragDeltaEvent,
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));
{
RoutedEvent = Thumb.DragDeltaEvent,
Vector = new Vector(0, -100)
});
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
{
RoutedEvent = Thumb.DragDeltaEvent,
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));
}
{
RoutedEvent = Thumb.DragDeltaEvent,
Vector = new Vector(0, 100)
});
[Fact]
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 },
}
};
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(110, GridUnitType.Star));
Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
}
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.DragDeltaEvent,
Vector = new Vector(100, 1000)
});
{
RoutedEvent = Thumb.DragCompletedEvent
});
Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
}
[Fact]
public void Vertical_Stays_Within_Constraints()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Vertical_Stays_Within_Constraints(bool showsPreview)
{
var control1 = new Border { [Grid.ColumnProperty] = 0 };
var splitter = new GridSplitter
{
[Grid.ColumnProperty] = 1,
};
var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, ShowsPreview = showsPreview};
var control2 = new Border { [Grid.ColumnProperty] = 2 };
var columnDefinitions = new ColumnDefinitions()
{
new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 },
new ColumnDefinition(GridLength.Auto),
new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 },
};
var grid = new Grid()
{
ColumnDefinitions = columnDefinitions,
Children =
{
control1, splitter, control2
}
};
var columnDefinitions = new ColumnDefinitions
{
new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 },
new ColumnDefinition(GridLength.Auto),
new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 },
};
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.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
{
RoutedEvent = Thumb.DragDeltaEvent,
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));
{
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(120, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star));
}
splitter.RaiseEvent(new VectorEventArgs
{
RoutedEvent = Thumb.DragDeltaEvent,
Vector = new Vector(100, 0)
});
{
RoutedEvent = Thumb.DragCompletedEvent
});
Assert.Equal(columnDefinitions[0].Width, new GridLength(120, 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 Avalonia.UnitTests;
using Avalonia.Input;
using System.Collections.Generic;
namespace Avalonia.Controls.UnitTests
{
@ -104,6 +105,28 @@ namespace Avalonia.Controls.UnitTests
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]
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
{
public Item(string value)

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

@ -47,6 +47,26 @@ namespace Avalonia.Controls.UnitTests
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]
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.
// 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.ObjectModel;
using System.Collections.Specialized;
@ -14,6 +15,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Data;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -23,7 +25,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
public class SelectingItemsControlTests
{
private MouseTestHelper _helper = new MouseTestHelper();
[Fact]
public void SelectedIndex_Should_Initially_Be_Minus_1()
{
@ -168,6 +170,130 @@ namespace Avalonia.Controls.UnitTests.Primitives
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]
public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True()
{
@ -849,7 +975,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"},
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
};
target.ApplyTemplate();
@ -980,6 +1106,45 @@ namespace Avalonia.Controls.UnitTests.Primitives
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]
public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization()
{
@ -1020,6 +1185,33 @@ namespace Avalonia.Controls.UnitTests.Primitives
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()
{
return new FuncControlTemplate<SelectingItemsControl>((control, scope) =>
@ -1028,6 +1220,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
[~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
[~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty],
}.RegisterInNameScope(scope));
}
@ -1067,10 +1260,39 @@ namespace Avalonia.Controls.UnitTests.Primitives
private class TestSelector : SelectingItemsControl
{
public TestSelector()
{
}
public TestSelector(SelectionMode selectionMode)
{
SelectionMode = selectionMode;
}
public new bool MoveSelection(NavigationDirection direction, bool 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
{
Items = items,
SelectionMode = SelectionMode.Multiple,
Template = Template(),
};

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

@ -4,6 +4,7 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -44,6 +45,29 @@ namespace Avalonia.Controls.UnitTests
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]
public void Logical_Children_Should_Be_TabItems()
{
@ -287,6 +311,25 @@ namespace Avalonia.Controls.UnitTests
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()
{
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()
{
var target = new WrapPanel()
{
Width = 100,
Children =
{
Width = 100,
Children =
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
};
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
@ -33,14 +33,14 @@ namespace Avalonia.Controls.UnitTests
public void Lays_Out_Horizontally_On_A_Single_Line()
{
var target = new WrapPanel()
{
Width = 200,
Children =
{
Width = 200,
Children =
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
};
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
@ -54,15 +54,15 @@ namespace Avalonia.Controls.UnitTests
public void Lays_Out_Vertically_Children_On_A_Single_Line()
{
var target = new WrapPanel()
{
Orientation = Orientation.Vertical,
Height = 120,
Children =
{
Orientation = Orientation.Vertical,
Height = 120,
Children =
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
};
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
@ -76,15 +76,15 @@ namespace Avalonia.Controls.UnitTests
public void Lays_Out_Vertically_On_Separate_Lines()
{
var target = new WrapPanel()
{
Orientation = Orientation.Vertical,
Height = 60,
Children =
{
Orientation = Orientation.Vertical,
Height = 60,
Children =
{
new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 },
}
};
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
@ -98,17 +98,17 @@ namespace Avalonia.Controls.UnitTests
public void Applies_ItemWidth_And_ItemHeight_Properties()
{
var target = new WrapPanel()
{
Orientation = Orientation.Horizontal,
Width = 50,
ItemWidth = 20,
ItemHeight = 15,
Children =
{
Orientation = Orientation.Horizontal,
Width = 50,
ItemWidth = 20,
ItemHeight = 15,
Children =
{
new Border(),
new Border(),
}
};
};
target.Measure(Size.Infinity);
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(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(
It.IsAny<string>(),
It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(),
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>();
return r.CreateFormattedText(text,
new Typeface(fontFamily, fontSize, fontStyle, fontWeight),
new Typeface(fontFamily, fontWeight, fontStyle),
fontSize,
textAlignment,
wrapping,
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 IEnumerable<string> InstalledFontNames => new string[0];
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@ -79,5 +78,10 @@ namespace Avalonia.UnitTests
{
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)
{
_pointer.Capture(null);
target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position,
Timestamp(), props, GetModifiers(modifiers), _pressedButton));
_pointer.Capture(null);
}
else
Move(target, source, position);

11
tests/Avalonia.UnitTests/TestRoot.cs

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

1
tests/Avalonia.UnitTests/TestServices.cs

@ -169,6 +169,7 @@ namespace Avalonia.UnitTests
x.CreateFormattedText(
It.IsAny<string>(),
It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(),
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);
}
[Fact]
public void Should_Be_Equal()
[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_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]

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

@ -7,15 +7,21 @@ namespace Avalonia.Visuals.UnitTests.Media
public class TypefaceTests
{
[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]
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]
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(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@ -51,6 +52,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
throw new NotImplementedException();
}
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt)
{
throw new NotImplementedException();

Loading…
Cancel
Save