Browse Source

Merge branch 'master' into dotnet-core-sdk-update

pull/3034/head
jp2masa 6 years ago
committed by GitHub
parent
commit
1f4c9dc3da
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. 3
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  11. 9
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  12. 36
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  13. 20
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  14. 1
      src/Avalonia.Animation/Properties/AssemblyInfo.cs
  15. 2
      src/Avalonia.Animation/TransitionInstance.cs
  16. 11
      src/Avalonia.Base/AvaloniaObject.cs
  17. 63
      src/Avalonia.Base/AvaloniaProperty.cs
  18. 23
      src/Avalonia.Base/EnumExtensions.cs
  19. 5
      src/Avalonia.Base/Utilities/MathUtilities.cs
  20. 12
      src/Avalonia.Base/ValueStore.cs
  21. 5
      src/Avalonia.Controls/AppBuilderBase.cs
  22. 8
      src/Avalonia.Controls/Application.cs
  23. 9
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  24. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  25. 13
      src/Avalonia.Controls/ColumnDefinition.cs
  26. 54
      src/Avalonia.Controls/DefinitionBase.cs
  27. 1
      src/Avalonia.Controls/DesktopApplicationExtensions.cs
  28. 7
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  29. 12
      src/Avalonia.Controls/Grid.cs
  30. 867
      src/Avalonia.Controls/GridSplitter.cs
  31. 10
      src/Avalonia.Controls/ItemsControl.cs
  32. 22
      src/Avalonia.Controls/ListBox.cs
  33. 4
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  34. 7
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  35. 15
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  36. 3
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  37. 2
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  38. 39
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  39. 10
      src/Avalonia.Controls/Primitives/TabStrip.cs
  40. 2
      src/Avalonia.Controls/Primitives/Thumb.cs
  41. 12
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  42. 13
      src/Avalonia.Controls/RowDefinition.cs
  43. 15
      src/Avalonia.Controls/Shapes/Shape.cs
  44. 57
      src/Avalonia.Controls/TabControl.cs
  45. 21
      src/Avalonia.Controls/TabItem.cs
  46. 5
      src/Avalonia.Controls/TextBlock.cs
  47. 8
      src/Avalonia.Controls/TextBox.cs
  48. 6
      src/Avalonia.Controls/ToolTipService.cs
  49. 19
      src/Avalonia.Controls/TreeView.cs
  50. 2
      src/Avalonia.Controls/WrapPanel.cs
  51. 4
      src/Avalonia.Diagnostics/Views/TreePageView.xaml
  52. 105
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
  53. 62
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  54. BIN
      src/Avalonia.Dialogs/Assets/Roboto-Light.ttf
  55. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  56. 7
      src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
  57. 2
      src/Avalonia.Input/AccessKeyHandler.cs
  58. 2
      src/Avalonia.Input/FocusManager.cs
  59. 13
      src/Avalonia.Input/MouseDevice.cs
  60. 2
      src/Avalonia.Input/Pointer.cs
  61. 9
      src/Avalonia.Input/PointerPoint.cs
  62. 21
      src/Avalonia.Input/TouchDevice.cs
  63. 7
      src/Avalonia.Layout/LayoutHelper.cs
  64. 8
      src/Avalonia.Layout/LayoutManager.cs
  65. 25
      src/Avalonia.Layout/LayoutQueue.cs
  66. 51
      src/Avalonia.Layout/Layoutable.cs
  67. 1
      src/Avalonia.Native/Avalonia.Native.csproj
  68. 34
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  69. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  70. 7
      src/Avalonia.Native/WindowImplBase.cs
  71. 58
      src/Avalonia.Themes.Default/GridSplitter.xaml
  72. 28
      src/Avalonia.Visuals/Media/FontFamily.cs
  73. 112
      src/Avalonia.Visuals/Media/FontManager.cs
  74. 6
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  75. 47
      src/Avalonia.Visuals/Media/FormattedText.cs
  76. 111
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  77. 99
      src/Avalonia.Visuals/Media/Typeface.cs
  78. 48
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  79. 89
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  80. 16
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  81. 6
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  82. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  83. 2
      src/Avalonia.X11/X11Platform.cs
  84. 10
      src/Avalonia.X11/X11Window.cs
  85. 12
      src/Avalonia.X11/XI2Manager.cs
  86. 1
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  87. 40
      src/Skia/Avalonia.Skia/FontKey.cs
  88. 82
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  89. 80
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  90. 179
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  91. 14
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  92. 91
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  93. 8
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  94. 5
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  95. 86
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  96. 19
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
  97. 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  98. 25
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  99. 49
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  100. 71
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

3
Avalonia.sln

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

2
azure-pipelines.yml

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

6
build/HarfBuzzSharp.props

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

4
build/SkiaSharp.props

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

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

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

12
samples/ControlCatalog/App.xaml

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

11
samples/ControlCatalog/App.xaml.cs

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

24
samples/ControlCatalog/MainWindow.xaml

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

14
samples/ControlCatalog/MainWindow.xaml.cs

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

3
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

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

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

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

36
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

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

20
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

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

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

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

2
src/Avalonia.Animation/TransitionInstance.cs

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

11
src/Avalonia.Base/AvaloniaObject.cs

@ -210,7 +210,11 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
public object GetValue(AvaloniaProperty property) public object GetValue(AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(property != null); if (property is null)
{
throw new ArgumentNullException(nameof(property));
}
VerifyAccess(); VerifyAccess();
if (property.IsDirect) if (property.IsDirect)
@ -231,7 +235,10 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
public T GetValue<T>(AvaloniaProperty<T> property) public T GetValue<T>(AvaloniaProperty<T> property)
{ {
Contract.Requires<ArgumentNullException>(property != null); if (property is null)
{
throw new ArgumentNullException(nameof(property));
}
return (T)GetValue((AvaloniaProperty)property); return (T)GetValue((AvaloniaProperty)property);
} }

63
src/Avalonia.Base/AvaloniaProperty.cs

@ -28,6 +28,8 @@ namespace Avalonia
private readonly Dictionary<Type, PropertyMetadata> _metadata; private readonly Dictionary<Type, PropertyMetadata> _metadata;
private readonly Dictionary<Type, PropertyMetadata> _metadataCache = new Dictionary<Type, PropertyMetadata>(); private readonly Dictionary<Type, PropertyMetadata> _metadataCache = new Dictionary<Type, PropertyMetadata>();
private bool _hasMetadataOverrides;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class. /// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
/// </summary> /// </summary>
@ -92,6 +94,9 @@ namespace Avalonia
Id = source.Id; Id = source.Id;
_defaultMetadata = source._defaultMetadata; _defaultMetadata = source._defaultMetadata;
// Properties that have different owner can't use fast path for metadata.
_hasMetadataOverrides = true;
if (metadata != null) if (metadata != null)
{ {
_metadata.Add(ownerType, metadata); _metadata.Add(ownerType, metadata);
@ -446,31 +451,12 @@ namespace Avalonia
/// ///
public PropertyMetadata GetMetadata(Type type) public PropertyMetadata GetMetadata(Type type)
{ {
Contract.Requires<ArgumentNullException>(type != null); if (!_hasMetadataOverrides)
PropertyMetadata result;
Type currentType = type;
if (_metadataCache.TryGetValue(type, out result))
{ {
return result; return _defaultMetadata;
} }
while (currentType != null) return GetMetadataWithOverrides(type);
{
if (_metadata.TryGetValue(currentType, out result))
{
_metadataCache[type] = result;
return result;
}
currentType = currentType.GetTypeInfo().BaseType;
}
_metadataCache[type] = _defaultMetadata;
return _defaultMetadata;
} }
/// <summary> /// <summary>
@ -535,6 +521,39 @@ namespace Avalonia
metadata.Merge(baseMetadata, this); metadata.Merge(baseMetadata, this);
_metadata.Add(type, metadata); _metadata.Add(type, metadata);
_metadataCache.Clear(); _metadataCache.Clear();
_hasMetadataOverrides = true;
}
private PropertyMetadata GetMetadataWithOverrides(Type type)
{
if (type is null)
{
throw new ArgumentNullException(nameof(type));
}
if (_metadataCache.TryGetValue(type, out PropertyMetadata result))
{
return result;
}
Type currentType = type;
while (currentType != null)
{
if (_metadata.TryGetValue(currentType, out result))
{
_metadataCache[type] = result;
return result;
}
currentType = currentType.GetTypeInfo().BaseType;
}
_metadataCache[type] = _defaultMetadata;
return _defaultMetadata;
} }
[DebuggerHidden] [DebuggerHidden]

23
src/Avalonia.Base/EnumExtensions.cs

@ -0,0 +1,23 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Runtime.CompilerServices;
namespace Avalonia
{
/// <summary>
/// Provides extension methods for enums.
/// </summary>
public static class EnumExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
{
var intValue = *(int*)&value;
var intFlag = *(int*)&flag;
return (intValue & intFlag) == intFlag;
}
}
}

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

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

12
src/Avalonia.Base/ValueStore.cs

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

5
src/Avalonia.Controls/AppBuilderBase.cs

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

8
src/Avalonia.Controls/Application.cs

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

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

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

2
src/Avalonia.Controls/AutoCompleteBox.cs

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

13
src/Avalonia.Controls/ColumnDefinition.cs

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

54
src/Avalonia.Controls/DefinitionBase.cs

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

1
src/Avalonia.Controls/DesktopApplicationExtensions.cs

@ -50,6 +50,7 @@ namespace Avalonia.Controls
/// On desktop-style platforms runs the application's main loop with custom CancellationToken /// On desktop-style platforms runs the application's main loop with custom CancellationToken
/// without setting a lifetime. /// without setting a lifetime.
/// </summary> /// </summary>
/// <param name="app">The application.</param>
/// <param name="token">The token to track.</param> /// <param name="token">The token to track.</param>
public static void Run(this Application app, CancellationToken token) public static void Run(this Application app, CancellationToken token)
{ {

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

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

12
src/Avalonia.Controls/Grid.cs

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

867
src/Avalonia.Controls/GridSplitter.cs

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

10
src/Avalonia.Controls/ItemsControl.cs

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

22
src/Avalonia.Controls/ListBox.cs

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

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

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

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

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

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

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

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

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

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

@ -41,6 +41,8 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PopupRoot"/> class. /// Initializes a new instance of the <see cref="PopupRoot"/> class.
/// </summary> /// </summary>
/// <param name="parent">The popup parent.</param>
/// <param name="impl">The popup implementation.</param>
/// <param name="dependencyResolver"> /// <param name="dependencyResolver">
/// The dependency resolver to use. If null the default dependency resolver will be used. /// The dependency resolver to use. If null the default dependency resolver will be used.
/// </param> /// </param>

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

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

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

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

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

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

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

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

13
src/Avalonia.Controls/RowDefinition.cs

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

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

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

57
src/Avalonia.Controls/TabControl.cs

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

21
src/Avalonia.Controls/TabItem.cs

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

5
src/Avalonia.Controls/TextBlock.cs

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

8
src/Avalonia.Controls/TextBox.cs

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

6
src/Avalonia.Controls/ToolTipService.cs

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

19
src/Avalonia.Controls/TreeView.cs

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

2
src/Avalonia.Controls/WrapPanel.cs

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

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

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

105
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml

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

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

@ -0,0 +1,62 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Diagnostics;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Avalonia.Dialogs
{
public class AboutAvaloniaDialog : Window
{
public AboutAvaloniaDialog()
{
AvaloniaXamlLoader.Load(this);
DataContext = this;
}
public static void OpenBrowser(string url)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// If no associated application/json MimeType is found xdg-open opens retrun error
// but it tries to open it anyway using the console editor (nano, vim, other..)
ShellExec($"xdg-open {url}", waitForExit: false);
}
else
{
using (Process process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"-e {url}" : "",
CreateNoWindow = true,
UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
}));
}
}
private static void ShellExec(string cmd, bool waitForExit = true)
{
var escapedArgs = cmd.Replace("\"", "\\\"");
using (var process = Process.Start(
new ProcessStartInfo
{
FileName = "/bin/sh",
Arguments = $"-c \"{escapedArgs}\"",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}
))
{
if (waitForExit)
{
process.WaitForExit();
}
}
}
}
}

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

Binary file not shown.

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

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

7
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

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

2
src/Avalonia.Input/AccessKeyHandler.cs

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

2
src/Avalonia.Input/FocusManager.cs

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

13
src/Avalonia.Input/MouseDevice.cs

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

2
src/Avalonia.Input/Pointer.cs

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

9
src/Avalonia.Input/PointerPoint.cs

@ -1,3 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Input namespace Avalonia.Input
{ {
public sealed class PointerPoint public sealed class PointerPoint
@ -27,9 +30,9 @@ namespace Avalonia.Input
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
{ {
PointerUpdateKind = kind; PointerUpdateKind = kind;
IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton); IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton);
IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton); IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton);
IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton); IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton);
// The underlying input source might be reporting the previous state, // The underlying input source might be reporting the previous state,
// so make sure that we reflect the current state // so make sure that we reflect the current state

21
src/Avalonia.Input/TouchDevice.cs

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

7
src/Avalonia.Layout/LayoutHelper.cs

@ -21,8 +21,11 @@ namespace Avalonia.Layout
/// <returns>The control's size.</returns> /// <returns>The control's size.</returns>
public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints) public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints)
{ {
double width = (control.Width > 0) ? control.Width : constraints.Width; var controlWidth = control.Width;
double height = (control.Height > 0) ? control.Height : constraints.Height; var controlHeight = control.Height;
double width = (controlWidth > 0) ? controlWidth : constraints.Width;
double height = (controlHeight > 0) ? controlHeight : constraints.Height;
width = Math.Min(width, control.MaxWidth); width = Math.Min(width, control.MaxWidth);
width = Math.Max(width, control.MinWidth); width = Math.Max(width, control.MinWidth);
height = Math.Min(height, control.MaxHeight); height = Math.Min(height, control.MaxHeight);

8
src/Avalonia.Layout/LayoutManager.cs

@ -15,9 +15,15 @@ namespace Avalonia.Layout
{ {
private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid); private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid);
private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid); private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid);
private readonly Action _executeLayoutPass;
private bool _queued; private bool _queued;
private bool _running; private bool _running;
public LayoutManager()
{
_executeLayoutPass = ExecuteLayoutPass;
}
/// <inheritdoc/> /// <inheritdoc/>
public void InvalidateMeasure(ILayoutable control) public void InvalidateMeasure(ILayoutable control)
{ {
@ -215,7 +221,7 @@ namespace Avalonia.Layout
{ {
if (!_queued && !_running) if (!_queued && !_running)
{ {
Dispatcher.UIThread.Post(ExecuteLayoutPass, DispatcherPriority.Layout); Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout);
_queued = true; _queued = true;
} }
} }

25
src/Avalonia.Layout/LayoutQueue.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Layout namespace Avalonia.Layout
{ {
@ -18,9 +17,11 @@ namespace Avalonia.Layout
_shouldEnqueue = shouldEnqueue; _shouldEnqueue = shouldEnqueue;
} }
private Func<T, bool> _shouldEnqueue; private readonly Func<T, bool> _shouldEnqueue;
private Queue<T> _inner = new Queue<T>(); private readonly Queue<T> _inner = new Queue<T>();
private Dictionary<T, Info> _loopQueueInfo = new Dictionary<T, Info>(); private readonly Dictionary<T, Info> _loopQueueInfo = new Dictionary<T, Info>();
private readonly List<KeyValuePair<T, Info>> _notFinalizedBuffer = new List<KeyValuePair<T, Info>>();
private int _maxEnqueueCountPerLoop = 1; private int _maxEnqueueCountPerLoop = 1;
public int Count => _inner.Count; public int Count => _inner.Count;
@ -60,13 +61,19 @@ namespace Avalonia.Layout
public void EndLoop() public void EndLoop()
{ {
var notfinalized = _loopQueueInfo.Where(v => v.Value.Count >= _maxEnqueueCountPerLoop).ToArray(); foreach (KeyValuePair<T, Info> info in _loopQueueInfo)
{
if (info.Value.Count >= _maxEnqueueCountPerLoop)
{
_notFinalizedBuffer.Add(info);
}
}
_loopQueueInfo.Clear(); _loopQueueInfo.Clear();
//prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle // Prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle
//one more time as a final attempt // one more time as a final attempt.
foreach (var item in notfinalized) foreach (var item in _notFinalizedBuffer)
{ {
if (_shouldEnqueue(item.Key)) if (_shouldEnqueue(item.Key))
{ {
@ -74,6 +81,8 @@ namespace Avalonia.Layout
_inner.Enqueue(item.Key); _inner.Enqueue(item.Key);
} }
} }
_notFinalizedBuffer.Clear();
} }
} }
} }

51
src/Avalonia.Layout/Layoutable.cs

@ -518,17 +518,25 @@ namespace Avalonia.Layout
var width = measured.Width; var width = measured.Width;
var height = measured.Height; var height = measured.Height;
if (!double.IsNaN(Width))
{ {
width = Width; double widthCache = Width;
if (!double.IsNaN(widthCache))
{
width = widthCache;
}
} }
width = Math.Min(width, MaxWidth); width = Math.Min(width, MaxWidth);
width = Math.Max(width, MinWidth); width = Math.Max(width, MinWidth);
if (!double.IsNaN(Height))
{ {
height = Height; double heightCache = Height;
if (!double.IsNaN(heightCache))
{
height = heightCache;
}
} }
height = Math.Min(height, MaxHeight); height = Math.Min(height, MaxHeight);
@ -562,11 +570,19 @@ namespace Avalonia.Layout
double width = 0; double width = 0;
double height = 0; double height = 0;
foreach (ILayoutable child in this.GetVisualChildren()) var visualChildren = VisualChildren;
var visualCount = visualChildren.Count;
for (var i = 0; i < visualCount; i++)
{ {
child.Measure(availableSize); IVisual visual = visualChildren[i];
width = Math.Max(width, child.DesiredSize.Width);
height = Math.Max(height, child.DesiredSize.Height); if (visual is ILayoutable layoutable)
{
layoutable.Measure(availableSize);
width = Math.Max(width, layoutable.DesiredSize.Width);
height = Math.Max(height, layoutable.DesiredSize.Height);
}
} }
return new Size(width, height); return new Size(width, height);
@ -594,6 +610,7 @@ namespace Avalonia.Layout
var verticalAlignment = VerticalAlignment; var verticalAlignment = VerticalAlignment;
var size = availableSizeMinusMargins; var size = availableSizeMinusMargins;
var scale = GetLayoutScale(); var scale = GetLayoutScale();
var useLayoutRounding = UseLayoutRounding;
if (horizontalAlignment != HorizontalAlignment.Stretch) if (horizontalAlignment != HorizontalAlignment.Stretch)
{ {
@ -607,7 +624,7 @@ namespace Avalonia.Layout
size = LayoutHelper.ApplyLayoutConstraints(this, size); size = LayoutHelper.ApplyLayoutConstraints(this, size);
if (UseLayoutRounding) if (useLayoutRounding)
{ {
size = new Size( size = new Size(
Math.Ceiling(size.Width * scale) / scale, Math.Ceiling(size.Width * scale) / scale,
@ -641,7 +658,7 @@ namespace Avalonia.Layout
break; break;
} }
if (UseLayoutRounding) if (useLayoutRounding)
{ {
originX = Math.Floor(originX * scale) / scale; originX = Math.Floor(originX * scale) / scale;
originY = Math.Floor(originY * scale) / scale; originY = Math.Floor(originY * scale) / scale;
@ -658,9 +675,19 @@ namespace Avalonia.Layout
/// <returns>The actual size used.</returns> /// <returns>The actual size used.</returns>
protected virtual Size ArrangeOverride(Size finalSize) protected virtual Size ArrangeOverride(Size finalSize)
{ {
foreach (ILayoutable child in this.GetVisualChildren().OfType<ILayoutable>()) var arrangeRect = new Rect(finalSize);
var visualChildren = VisualChildren;
var visualCount = visualChildren.Count;
for (var i = 0; i < visualCount; i++)
{ {
child.Arrange(new Rect(finalSize)); IVisual visual = visualChildren[i];
if (visual is ILayoutable layoutable)
{
layoutable.Arrange(arrangeRect);
}
} }
return finalSize; return finalSize;

1
src/Avalonia.Native/Avalonia.Native.csproj

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

34
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

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

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -21,7 +21,6 @@ namespace Avalonia.Native
[DllImport("libAvaloniaNative")] [DllImport("libAvaloniaNative")]
static extern IntPtr CreateAvaloniaNative(); static extern IntPtr CreateAvaloniaNative();
internal static readonly MouseDevice MouseDevice = new MouseDevice();
internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice();
public Size DoubleClickSize => new Size(4, 4); public Size DoubleClickSize => new Size(4, 4);
@ -95,7 +94,6 @@ namespace Avalonia.Native
.Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind<IPlatformIconLoader>().ToSingleton<IconLoader>() .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice) .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IMouseDevice>().ToConstant(MouseDevice)
.Bind<IPlatformSettings>().ToConstant(this) .Bind<IPlatformSettings>().ToConstant(this)
.Bind<IWindowingPlatform>().ToConstant(this) .Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))

7
src/Avalonia.Native/WindowImplBase.cs

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

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

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

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

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

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

@ -0,0 +1,112 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media
{
/// <summary>
/// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts.
/// It is also responsible for the font fallback.
/// </summary>
public abstract class FontManager
{
public static readonly FontManager Default = CreateDefault();
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
public string DefaultFontFamilyName
{
get;
protected set;
}
/// <summary>
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary>
public abstract IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Get a cached typeface from specified parameters.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <returns>
/// The cached typeface.
/// </returns>
public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
/// Returns <c>null</c> if no fallback was found.
/// </summary>
/// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param>
/// <returns>
/// The matched typeface.
/// </returns>
public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
public static FontManager CreateDefault()
{
var platformImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
if (platformImpl != null)
{
return new PlatformFontManager(platformImpl);
}
return new EmptyFontManager();
}
private class PlatformFontManager : FontManager
{
private readonly IFontManagerImpl _platformImpl;
public PlatformFontManager(IFontManagerImpl platformImpl)
{
_platformImpl = platformImpl;
DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName;
}
public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
_platformImpl.GetInstalledFontFamilyNames(checkForUpdates);
public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) =>
_platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle);
public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null) =>
_platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture);
}
private class EmptyFontManager : FontManager
{
public EmptyFontManager()
{
DefaultFontFamilyName = "Empty";
}
public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
new[] { DefaultFontFamilyName };
public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle);
public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null) => null;
}
}
}

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

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

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

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

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

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

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

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

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

@ -0,0 +1,48 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
namespace Avalonia.Platform
{
public interface IFontManagerImpl
{
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
string DefaultFontFamilyName { get; }
/// <summary>
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Get a typeface from specified parameters.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <returns>
/// The typeface.
/// </returns>
Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
/// </summary>
/// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param>
/// <returns>
/// The typeface.
/// </returns>
Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
}
}

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

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

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

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

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

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

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

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

2
src/Avalonia.X11/X11Platform.cs

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

10
src/Avalonia.X11/X11Window.cs

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

12
src/Avalonia.X11/XI2Manager.cs

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

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

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

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

@ -0,0 +1,40 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
namespace Avalonia.Skia
{
internal readonly struct FontKey : IEquatable<FontKey>
{
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontWeight weight, FontStyle style)
{
Style = style;
Weight = weight;
}
public override int GetHashCode()
{
var hash = 17;
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
return hash;
}
public override bool Equals(object other)
{
return other is FontKey key && Equals(key);
}
public bool Equals(FontKey other)
{
return Style == other.Style &&
Weight == other.Weight;
}
}
}

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

@ -0,0 +1,82 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class FontManagerImpl : IFontManagerImpl
{
private SKFontManager _skFontManager = SKFontManager.Default;
public FontManagerImpl()
{
DefaultFontFamilyName = SKTypeface.Default.FamilyName;
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
{
_skFontManager = SKFontManager.CreateDefault();
}
return _skFontManager.FontFamilies;
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
}
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
{
var fontFamilyName = FontFamily.Default.Name;
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
if (fontFamily != null)
{
foreach (var familyName in fontFamily.FamilyNames)
{
var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal,
(SKFontStyleSlant)fontStyle,
new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
if (skTypeface == null)
{
continue;
}
fontFamilyName = familyName;
break;
}
}
else
{
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
(SKFontStyleSlant)fontStyle,
new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
if (skTypeface != null)
{
fontFamilyName = skTypeface.FamilyName;
}
}
return GetTypeface(fontFamilyName, fontWeight, fontStyle);
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,19 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class TypefaceCollectionEntry
{
public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface)
{
Typeface = typeface;
SKTypeface = skTypeface;
}
public Typeface Typeface { get; }
public SKTypeface SKTypeface { get; }
}
}

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

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

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

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

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

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

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

@ -0,0 +1,71 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
using FontStyle = Avalonia.Media.FontStyle;
using FontWeight = Avalonia.Media.FontWeight;
namespace Avalonia.Direct2D1.Media
{
internal class FontManagerImpl : IFontManagerImpl
{
public FontManagerImpl()
{
//ToDo: Implement a real lookup of the system's default font.
DefaultFontFamilyName = "segoe ui";
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
var fontFamilies = new string[familyCount];
for (var i = 0; i < familyCount; i++)
{
fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0);
}
return fontFamilies;
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
//ToDo: Implement caching.
return new Typeface(fontFamily, fontWeight, fontStyle);
}
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
{
var fontFamilyName = FontFamily.Default.Name;
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
for (var i = 0; i < familyCount; i++)
{
var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i)
.GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal,
(SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0);
if (!font.HasCharacter(codepoint))
{
continue;
}
fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
break;
}
return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
}
}
}

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

Loading…
Cancel
Save