diff --git a/Avalonia.sln b/Avalonia.sln index 568a16ce0e..e40ebae4d6 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -128,6 +128,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Base.props = build\Base.props build\Binding.props = build\Binding.props build\BuildTargets.targets = build\BuildTargets.targets + build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props @@ -201,7 +202,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 92e4afdca8..603308ef9a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -52,7 +52,7 @@ jobs: sdk: 'macosx10.14' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: 'default' # Options: 8, 9, default, specifyPath + xcodeVersion: '10' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props new file mode 100644 index 0000000000..f8767c7599 --- /dev/null +++ b/build/HarfBuzzSharp.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index c03ad0fefd..796bd8e596 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 2a5acaea91..0e85332555 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -855,8 +855,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(type == Wheel) { - delta.X = [event scrollingDeltaX] / 50; - delta.Y = [event scrollingDeltaY] / 50; + auto speed = 5; + + if([event hasPreciseScrollingDeltas]) + { + speed = 50; + } + + delta.X = [event scrollingDeltaX] / speed; + delta.Y = [event scrollingDeltaY] / speed; if(delta.X == 0 && delta.Y == 0) { diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 335c460b40..e40509dfda 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -17,16 +17,4 @@ - - - - - - - - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 4fc63ea054..958729e2e8 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -8,20 +8,9 @@ namespace ControlCatalog { public class App : Application { - private NativeMenu _recentMenu; - public override void Initialize() { AvaloniaXamlLoader.Load(this); - - Name = "Avalonia"; - - _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; - } - - public void OnOpenClicked(object sender, EventArgs args) - { - _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } public override void OnFrameworkInitializationCompleted() diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6088f2ec57..248f94082d 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -37,12 +37,20 @@ - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 7b0ee897c4..38cbde9d92 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -31,20 +31,28 @@ namespace ControlCatalog DataContext = new MainWindowViewModel(_notificationArea); _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; + var mainMenu = this.FindControl("MainMenu"); + mainMenu.AttachedToVisualTree += MenuAttached; + } + + public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e) + { + if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu) + { + mainMenu.IsVisible = false; + } } public void OnOpenClicked(object sender, EventArgs args) { _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } - + public void OnCloseClicked(object sender, EventArgs args) { Close(); } - - private void InitializeComponent() { // TODO: iOS does not support dynamically loading assemblies diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index adf0345a70..89e7653618 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,7 @@ using System.Reactive; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; +using Avalonia.Dialogs; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -26,6 +28,20 @@ namespace ControlCatalog.ViewModels { NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error)); }); + + AboutCommand = ReactiveCommand.CreateFromTask(async () => + { + var dialog = new AboutAvaloniaDialog(); + + var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + + await dialog.ShowDialog(mainWindow); + }); + + ExitCommand = ReactiveCommand.Create(() => + { + (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); + }); } public IManagedNotificationManager NotificationManager @@ -39,5 +55,9 @@ namespace ControlCatalog.ViewModels public ReactiveCommand ShowManagedNotificationCommand { get; } public ReactiveCommand ShowNativeNotificationCommand { get; } + + public ReactiveCommand AboutCommand { get; } + + public ReactiveCommand ExitCommand { get; } } } diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs new file mode 100644 index 0000000000..a8306c2d69 --- /dev/null +++ b/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 +{ + /// + /// Provides extension methods for enums. + /// + public static class EnumExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum + { + var intValue = *(int*)&value; + var intFlag = *(int*)&flag; + + return (intValue & intFlag) == intFlag; + } + } +} diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 41b57b6e70..027028480c 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -159,6 +159,11 @@ namespace Avalonia.Utilities /// The clamped value. public static int Clamp(int val, int min, int max) { + if (min > max) + { + throw new ArgumentException($"{min} cannot be greater than {max}."); + } + if (val < min) { return min; diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index ce60a0f0b9..59c6c47ed9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -48,6 +48,14 @@ namespace Avalonia /// public event EventHandler ResourcesChanged; + /// + /// Creates an instance of the class. + /// + public Application() + { + Name = "Avalonia Application"; + } + /// /// Gets the current instance of the class. /// diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index a6a64e570b..c216037aba 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -19,8 +19,6 @@ namespace Avalonia.Controls.Generators { var tabItem = (TabItem)base.CreateContainer(item); - tabItem.ParentTabControl = Owner; - tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; if (tabItem.HeaderTemplate == null) @@ -48,11 +46,6 @@ namespace Avalonia.Controls.Generators tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; } - if (tabItem.Content == null) - { - tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty]; - } - return tabItem; } } diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 28b9b3a38f..a2fefa0548 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -1,210 +1,841 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; -using Avalonia.VisualTree; +using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// 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 control. /// - /// - /// Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext. - /// public class GridSplitter : Thumb { - private List _definitions; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ResizeDirectionProperty = + AvaloniaProperty.Register(nameof(ResizeDirection)); - private Grid _grid; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ResizeBehaviorProperty = + AvaloniaProperty.Register(nameof(ResizeBehavior)); - private DefinitionBase _nextDefinition; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ShowsPreviewProperty = + AvaloniaProperty.Register(nameof(ShowsPreview)); - private Orientation _orientation; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty KeyboardIncrementProperty = + AvaloniaProperty.Register(nameof(KeyboardIncrement), 10d); - private DefinitionBase _prevDefinition; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty DragIncrementProperty = + AvaloniaProperty.Register(nameof(DragIncrement), 1d); - private void GetDeltaConstraints(out double min, out double max) + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty> PreviewContentProperty = + AvaloniaProperty.Register>(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; + + /// + /// Indicates whether the Splitter resizes the Columns, Rows, or Both. + /// + public GridResizeDirection ResizeDirection { - var prevDefinitionLen = GetActualLength(_prevDefinition); - var prevDefinitionMin = GetMinLength(_prevDefinition); - var prevDefinitionMax = GetMaxLength(_prevDefinition); + get => GetValue(ResizeDirectionProperty); + set => SetValue(ResizeDirectionProperty, value); + } - var nextDefinitionLen = GetActualLength(_nextDefinition); - var nextDefinitionMin = GetMinLength(_nextDefinition); - var nextDefinitionMax = GetMaxLength(_nextDefinition); - // Determine the minimum and maximum the columns can be resized - min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen); - max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin); + /// + /// Indicates which Columns or Rows the Splitter resizes. + /// + public GridResizeBehavior ResizeBehavior + { + get => GetValue(ResizeBehaviorProperty); + set => SetValue(ResizeBehaviorProperty, value); } - protected override void OnDragDelta(VectorEventArgs e) + /// + /// Indicates whether to Preview the column resizing without updating layout. + /// + public bool ShowsPreview { - // WPF doesn't change anything when spliter is in the last row/column - // but resizes the splitter row/column when it's the first one. - // this is different, but more internally consistent. - if (_prevDefinition == null || _nextDefinition == null) - return; + get => GetValue(ShowsPreviewProperty); + set => SetValue(ShowsPreviewProperty, value); + } + + /// + /// The Distance to move the splitter when pressing the keyboard arrow keys. + /// + public double KeyboardIncrement + { + get => GetValue(KeyboardIncrementProperty); + set => SetValue(KeyboardIncrementProperty, value); + } + + /// + /// Restricts splitter to move a multiple of the specified units. + /// + public double DragIncrement + { + get => GetValue(DragIncrementProperty); + set => SetValue(DragIncrementProperty, value); + } + + /// + /// Gets or sets content that will be shown when is enabled and user starts resize operation. + /// + public ITemplate PreviewContent + { + get => GetValue(PreviewContentProperty); + set => SetValue(PreviewContentProperty, value); + } + + /// + /// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height. + /// + internal GridResizeDirection GetEffectiveResizeDirection() + { + GridResizeDirection direction = ResizeDirection; + + if (direction != GridResizeDirection.Auto) + { + return direction; + } + + // When HorizontalAlignment is Left, Right or Center, resize Columns. + if (HorizontalAlignment != HorizontalAlignment.Stretch) + { + direction = GridResizeDirection.Columns; + } + else if (VerticalAlignment != VerticalAlignment.Stretch) + { + direction = GridResizeDirection.Rows; + } + else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height. + { + direction = GridResizeDirection.Columns; + } + else + { + direction = GridResizeDirection.Rows; + } - var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; - double max; - double min; - GetDeltaConstraints(out min, out max); - delta = Math.Min(Math.Max(delta, min), max); + return direction; + } - var prevIsStar = IsStar(_prevDefinition); - var nextIsStar = IsStar(_nextDefinition); + /// + /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction. + /// + 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; + } + + /// + /// Removes preview adorner from the grid. + /// + private void RemovePreviewAdorner() + { + if (_resizeData.Adorner != null) + { + AdornerLayer layer = AdornerLayer.GetAdornerLayer(this); + layer.Children.Remove(_resizeData.Adorner); + } + } + + /// + /// Initialize the data needed for resizing. + /// + 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(); + } + } + + /// + /// Returns true if GridSplitter can resize rows/columns. + /// + 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; + } + + /// + /// Create the preview adorner and add it to the adorner layer. + /// + private void SetupPreviewAdorner() + { + if (_resizeData.ShowsPreview) { - SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); + // Get the adorner layer and add an adorner to it. + var adornerLayer = AdornerLayer.GetAdornerLayer(_resizeData.Grid); + + var previewContent = PreviewContent; + + // Can't display preview. + if (adornerLayer == null) + { + return; + } + + IControl builtPreviewContent = previewContent?.Build(); + + _resizeData.Adorner = new PreviewAdorner(builtPreviewContent); + + AdornerLayer.SetAdornedElement(_resizeData.Adorner, this); + + adornerLayer.Children.Add(_resizeData.Adorner); + + // Get constraints on preview's translation. + GetDeltaConstraints(out _resizeData.MinChange, out _resizeData.MaxChange); } - else if (nextIsStar) + } + + protected override void OnPointerEnter(PointerEventArgs e) + { + base.OnPointerEnter(e); + + GridResizeDirection direction = GetEffectiveResizeDirection(); + + switch (direction) { - SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); + case GridResizeDirection.Columns: + Cursor = s_columnSplitterCursor; + break; + case GridResizeDirection.Rows: + Cursor = s_rowSplitterCursor; + break; } - else + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + + if (_resizeData != null) { - SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); - SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); + CancelResize(); } } - private double GetActualLength(DefinitionBase definition) + protected override void OnDragStarted(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; + base.OnDragStarted(e); + + // TODO: Looks like that sometimes thumb will raise multiple drag started events. + // Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + + if (_resizeData != null) + { + return; + } + + InitializeData(ShowsPreview); } - private double GetMinLength(DefinitionBase definition) + protected override void OnDragDelta(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight; + base.OnDragDelta(e); + + if (_resizeData != null) + { + double horizontalChange = e.Vector.X; + double verticalChange = e.Vector.Y; + + // Round change to nearest multiple of DragIncrement. + double dragIncrement = DragIncrement; + horizontalChange = Math.Round(horizontalChange / dragIncrement) * dragIncrement; + verticalChange = Math.Round(verticalChange / dragIncrement) * dragIncrement; + + if (_resizeData.ShowsPreview) + { + // Set the Translation of the Adorner to the distance from the thumb. + if (_resizeData.ResizeDirection == GridResizeDirection.Columns) + { + _resizeData.Adorner.OffsetX = Math.Min( + Math.Max(horizontalChange, _resizeData.MinChange), + _resizeData.MaxChange); + } + else + { + _resizeData.Adorner.OffsetY = Math.Min( + Math.Max(verticalChange, _resizeData.MinChange), + _resizeData.MaxChange); + } + } + else + { + // Directly update the grid. + MoveSplitter(horizontalChange, verticalChange); + } + } } - private double GetMaxLength(DefinitionBase definition) + protected override void OnDragCompleted(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight; + base.OnDragCompleted(e); + + if (_resizeData != null) + { + if (_resizeData.ShowsPreview) + { + // Update the grid. + MoveSplitter(_resizeData.Adorner.OffsetX, _resizeData.Adorner.OffsetY); + RemovePreviewAdorner(); + } + + _resizeData = null; + } } - private bool IsStar(DefinitionBase definition) + protected override void OnKeyDown(KeyEventArgs e) { - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar; + Key key = e.Key; + + switch (key) + { + case Key.Escape: + if (_resizeData != null) + { + CancelResize(); + e.Handled = true; + } + + break; + + case Key.Left: + e.Handled = KeyboardMoveSplitter(-KeyboardIncrement, 0); + break; + case Key.Right: + e.Handled = KeyboardMoveSplitter(KeyboardIncrement, 0); + break; + case Key.Up: + e.Handled = KeyboardMoveSplitter(0, -KeyboardIncrement); + break; + case Key.Down: + e.Handled = KeyboardMoveSplitter(0, KeyboardIncrement); + break; + } } - private void SetLengthInStars(DefinitionBase definition, double value) + /// + /// Cancels the resize operation. + /// + private void CancelResize() { - var columnDefinition = definition as ColumnDefinition; - if (columnDefinition != null) + // Restore original column/row lengths. + if (_resizeData.ShowsPreview) { - columnDefinition.Width = new GridLength(value, GridUnitType.Star); + RemovePreviewAdorner(); } - else + else // Reset the columns/rows lengths to the saved values. { - ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star); + SetDefinitionLength(_resizeData.Definition1, _resizeData.OriginalDefinition1Length); + SetDefinitionLength(_resizeData.Definition2, _resizeData.OriginalDefinition2Length); } + + _resizeData = null; + } + + /// + /// Returns true if the row/column has a star length. + /// + private static bool IsStar(DefinitionBase definition) + { + return definition.UserSizeValueCache.IsStar; } - private void SetLength(DefinitionBase definition, double value) + /// + /// Gets Column or Row definition at index from grid based on resize direction. + /// + private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction) { - var columnDefinition = definition as ColumnDefinition; - if (columnDefinition != null) + return direction == GridResizeDirection.Columns ? + (DefinitionBase)grid.ColumnDefinitions[index] : + (DefinitionBase)grid.RowDefinitions[index]; + } + + /// + /// Retrieves the ActualWidth or ActualHeight of the definition depending on its type Column or Row. + /// + private double GetActualLength(DefinitionBase definition) + { + var column = definition as ColumnDefinition; + + return column?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; + } + + /// + /// Gets Column or Row definition at index from grid based on resize direction. + /// + private static void SetDefinitionLength(DefinitionBase definition, GridLength length) + { + definition.SetValue( + definition is ColumnDefinition ? ColumnDefinition.WidthProperty : RowDefinition.HeightProperty, length); + } + + /// + /// Get the minimum and maximum Delta can be given definition constraints (MinWidth/MaxWidth). + /// + 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) + /// + /// Sets the length of definition1 and definition2. + /// + private void SetLengths(double definition1Pixels, double definition2Pixels) { - base.OnAttachedToVisualTree(e); - _grid = this.GetVisualParent(); + // For the case where both definition1 and 2 are stars, update all star values to match their current pixel values. + if (_resizeData.SplitBehavior == SplitBehavior.Split) + { + var definitions = _resizeData.ResizeDirection == GridResizeDirection.Columns ? + (IAvaloniaReadOnlyList)_resizeData.Grid.ColumnDefinitions : + (IAvaloniaReadOnlyList)_resizeData.Grid.RowDefinitions; - _orientation = DetectOrientation(); + var definitionsCount = definitions.Count; + + for (var i = 0; i < definitionsCount; i++) + { + DefinitionBase definition = definitions[i]; - int definitionIndex; //row or col - if (_orientation == Orientation.Vertical) + // For each definition, if it is a star, set is value to ActualLength in stars + // This makes 1 star == 1 pixel in length + if (i == _resizeData.Definition1Index) + { + SetDefinitionLength(definition, new GridLength(definition1Pixels, GridUnitType.Star)); + } + else if (i == _resizeData.Definition2Index) + { + SetDefinitionLength(definition, new GridLength(definition2Pixels, GridUnitType.Star)); + } + else if (IsStar(definition)) + { + SetDefinitionLength(definition, new GridLength(GetActualLength(definition), GridUnitType.Star)); + } + } + } + else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) { - Cursor = new Cursor(StandardCursorType.SizeWestEast); - _definitions = _grid.ColumnDefinitions.Cast().ToList(); - definitionIndex = GetValue(Grid.ColumnProperty); - PseudoClasses.Add(":vertical"); + SetDefinitionLength(_resizeData.Definition1, new GridLength(definition1Pixels)); } else { - Cursor = new Cursor(StandardCursorType.SizeNorthSouth); - definitionIndex = GetValue(Grid.RowProperty); - _definitions = _grid.RowDefinitions.Cast().ToList(); - PseudoClasses.Add(":horizontal"); + SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels)); + } + } + + /// + /// Move the splitter by the given Delta's in the horizontal and vertical directions. + /// + private void MoveSplitter(double horizontalChange, double verticalChange) + { + Debug.Assert(_resizeData != null, "_resizeData should not be null when calling MoveSplitter"); + + // Calculate the offset to adjust the splitter. + var delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange; + + DefinitionBase definition1 = _resizeData.Definition1; + DefinitionBase definition2 = _resizeData.Definition2; + + if (definition1 != null && definition2 != null) + { + double actualLength1 = GetActualLength(definition1); + double actualLength2 = GetActualLength(definition2); + + // When splitting, Check to see if the total pixels spanned by the definitions + // is the same asbefore starting resize. If not cancel the drag + if (_resizeData.SplitBehavior == SplitBehavior.Split && + !MathUtilities.AreClose( + actualLength1 + actualLength2, + _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength)) + { + CancelResize(); + + return; + } + + GetDeltaConstraints(out var min, out var max); + + // Constrain Delta to Min/MaxWidth of columns + delta = Math.Min(Math.Max(delta, min), max); + + double definition1LengthNew = actualLength1 + delta; + double definition2LengthNew = actualLength1 + actualLength2 - definition1LengthNew; + + SetLengths(definition1LengthNew, definition2LengthNew); } + } - if (definitionIndex > 0) - _prevDefinition = _definitions[definitionIndex - 1]; + /// + /// Move the splitter using the Keyboard (Don't show preview). + /// + private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange) + { + // If moving with the mouse, ignore keyboard motion. + if (_resizeData != null) + { + return false; // Don't handle the event. + } + + // Don't show preview. + InitializeData(false); + + // Check that we are actually able to resize. + if (_resizeData == null) + { + return false; // Don't handle the event. + } - if (definitionIndex < _definitions.Count - 1) - _nextDefinition = _definitions[definitionIndex + 1]; + MoveSplitter(horizontalChange, verticalChange); + + _resizeData = null; + + return true; } - private Orientation DetectOrientation() + /// + /// This adorner draws the preview for the . + /// It also positions the adorner. + /// + private sealed class PreviewAdorner : Decorator { - if (!_grid.ColumnDefinitions.Any()) - return Orientation.Horizontal; - if (!_grid.RowDefinitions.Any()) - return Orientation.Vertical; + private readonly TranslateTransform _translation; + private readonly Decorator _decorator; + + public PreviewAdorner(IControl previewControl) + { + // Add a decorator to perform translations. + _translation = new TranslateTransform(); + + _decorator = new Decorator + { + Child = previewControl, + RenderTransform = _translation + }; + + Child = _decorator; + } - var col = GetValue(Grid.ColumnProperty); - var row = GetValue(Grid.RowProperty); - var width = _grid.ColumnDefinitions[col].Width; - var height = _grid.RowDefinitions[row].Height; - if (width.IsAuto && !height.IsAuto) + /// + /// The Preview's Offset in the X direction from the GridSplitter. + /// + public double OffsetX { - return Orientation.Vertical; + get => _translation.X; + set => _translation.X = value; } - if (!width.IsAuto && height.IsAuto) + + /// + /// The Preview's Offset in the Y direction from the GridSplitter. + /// + public double OffsetY { - return Orientation.Horizontal; + get => _translation.Y; + set => _translation.Y = value; } - if (_grid.Children.OfType() // Decision based on other controls in the same column - .Where(c => Grid.GetColumn(c) == col) - .Any(c => c.GetType() != typeof(GridSplitter))) + + protected override Size ArrangeOverride(Size finalSize) { - return Orientation.Horizontal; + // Adorners always get clipped to the owner control. In this case we want + // to constrain size to the splitter size but draw on top of the parent grid. + Clip = null; + + return base.ArrangeOverride(finalSize); } - return Orientation.Vertical; } + + /// + /// 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. + /// + private enum SplitBehavior + { + /// + /// Both columns/rows are star lengths. + /// + Split, + + /// + /// Resize 1 only. + /// + Resize1, + + /// + /// Resize 2 only. + /// + Resize2 + } + + /// + /// Stores data during the resizing operation. + /// + 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; + } + } + + /// + /// Enum to indicate whether resizes Columns or Rows. + /// + public enum GridResizeDirection + { + /// + /// Determines whether to resize rows or columns based on its Alignment and + /// width compared to height. + /// + Auto, + + /// + /// Resize columns when dragging Splitter. + /// + Columns, + + /// + /// Resize rows when dragging Splitter. + /// + Rows + } + + /// + /// Enum to indicate what Columns or Rows the resizes. + /// + public enum GridResizeBehavior + { + /// + /// Determine which columns or rows to resize based on its Alignment. + /// + BasedOnAlignment, + + /// + /// Resize the current and next Columns or Rows. + /// + CurrentAndNext, + + /// + /// Resize the previous and current Columns or Rows. + /// + PreviousAndCurrent, + + /// + /// Resize the previous and next Columns or Rows. + /// + PreviousAndNext } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b027da6d0c..bf22f0a08a 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -359,6 +359,12 @@ namespace Avalonia.Controls UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); + + if (Presenter != null) + { + Presenter.Items = newValue; + } + SubscribeToItems(newValue); } @@ -370,6 +376,8 @@ namespace Avalonia.Controls /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + UpdateItemCount(); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -381,7 +389,7 @@ namespace Avalonia.Controls break; } - UpdateItemCount(); + Presenter?.ItemsChanged(e); var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 449ca18465..4966e669ed 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -132,21 +133,26 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0, - e.MouseButton == MouseButton.Right); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0, + point.Properties.IsRightButtonPressed); + } } } protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); Scroll = e.NameScope.Find("PART_ScrollViewer"); + base.OnTemplateApplied(e); } } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index f8ae5c9690..6d450a0155 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -804,9 +804,9 @@ namespace Avalonia.Controls private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) { - if (e.Device.Captured != Spinner) + if (e.Pointer.Captured != Spinner) { - Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); + Dispatcher.UIThread.InvokeAsync(() => { e.Pointer.Capture(Spinner); }, DispatcherPriority.Input); } } diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 42311dc781..c4acf1ebef 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +1,19 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections; +using System.Collections.Specialized; + namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { + IEnumerable Items { get; set; } + IPanel Panel { get; } + void ItemsChanged(NotifyCollectionChangedEventArgs e); + void ScrollIntoView(object item); } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 0f0cdc37cf..ef1f277162 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters _itemsSubscription?.Dispose(); _itemsSubscription = null; - if (_createdPanel && value is INotifyCollectionChanged incc) + if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc) { _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } @@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters private set; } + protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + /// public override sealed void ApplyTemplate() { @@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters { } + /// + void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) + { + if (Panel != null) + { + ItemsChanged(e); + } + } + /// /// Creates the for the control. /// @@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters _createdPanel = true; - if (_itemsSubscription == null && Items is INotifyCollectionChanged incc) + if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc) { _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 5931fec350..e0cc9aa128 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -297,7 +297,8 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, }.Bounds.Size; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..329b086a7c 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - base.ItemsCollectionChanged(sender, e); - if (_updateCount > 0) { + base.ItemsCollectionChanged(sender, e); return; } + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + break; + case NotifyCollectionChangedAction.Remove: + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + break; + } + + base.ItemsCollectionChanged(sender, e); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives } else { - _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); UpdateSelectedItem(_selection.First(), false); } break; case NotifyCollectionChangedAction.Remove: - _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); UpdateSelectedItem(_selection.First(), false); ResetSelectedItems(); break; @@ -358,17 +367,17 @@ namespace Avalonia.Controls.Primitives { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectedIndex == -1) - { - SelectedIndex = container.Index; - } - else + if (SelectionMode.HasFlag(SelectionMode.Multiple)) { if (_selection.Add(container.Index)) { resetSelectedItems = true; } } + else + { + SelectedIndex = container.Index; + } MarkContainerSelected(container.ContainerControl, true); } @@ -1088,9 +1097,15 @@ namespace Avalonia.Controls.Primitives } else { - SelectedIndex = _updateSelectedIndex != int.MinValue ? - _updateSelectedIndex : - AlwaysSelected ? 0 : -1; + if (_updateSelectedIndex != int.MinValue) + { + SelectedIndex = _updateSelectedIndex; + } + + if (AlwaysSelected && SelectedIndex == -1) + { + SelectedIndex = 0; + } } } } diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index ec0dbd124c..e1a6cf79bb 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -44,9 +45,14 @@ namespace Avalonia.Controls.Primitives { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource(e.Source); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource(e.Source); + } } } } diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 9f5ddb666c..d88018cf32 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -73,7 +73,6 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { - e.Device.Capture(this); e.Handled = true; _lastPoint = e.GetPosition(this); @@ -92,7 +91,6 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - e.Device.Capture(null); e.Handled = true; _lastPoint = null; diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 61ac0822b0..8b1cd48379 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -4,7 +4,6 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -70,6 +69,7 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); + SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); } /// @@ -145,6 +145,61 @@ namespace Avalonia.Controls return RegisterContentPresenter(presenter); } + protected override void OnContainersMaterialized(ItemContainerEventArgs e) + { + base.OnContainersMaterialized(e); + + if (SelectedContent != null || SelectedIndex == -1) + { + return; + } + + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); + + if (container == null) + { + return; + } + + UpdateSelectedContent(container); + } + + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) + { + var index = (int)e.NewValue; + + if (index == -1) + { + SelectedContentTemplate = null; + + SelectedContent = null; + + return; + } + + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); + + if (container == null) + { + return; + } + + UpdateSelectedContent(container); + } + + private void UpdateSelectedContent(IContentControl item) + { + if (SelectedContentTemplate != item.ContentTemplate) + { + SelectedContentTemplate = item.ContentTemplate; + } + + if (SelectedContent != item.Content) + { + SelectedContent = item.Content; + } + } + /// /// Called when an is registered with the control. /// diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index fca1e022aa..e27977bf3d 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -30,7 +30,6 @@ namespace Avalonia.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); - IsSelectedProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e)); } @@ -54,8 +53,6 @@ namespace Avalonia.Controls set { SetValue(IsSelectedProperty, value); } } - internal TabControl ParentTabControl { get; set; } - private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) { if (Header == null) @@ -83,23 +80,5 @@ namespace Avalonia.Controls } } } - - private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) - { - if (!IsSelected) - { - return; - } - - if (ParentTabControl.SelectedContentTemplate != ContentTemplate) - { - ParentTabControl.SelectedContentTemplate = ContentTemplate; - } - - if (ParentTabControl.SelectedContent != Content) - { - ParentTabControl.SelectedContent = Content; - } - } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index b9603b91ed..c7855ddfd1 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -352,10 +352,11 @@ namespace Avalonia.Controls return new FormattedText { Constraint = constraint, - Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, - Wrapping = TextWrapping, + TextWrapping = TextWrapping, }; } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index fdc9d153e2..3d472fca18 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -677,13 +677,13 @@ namespace Avalonia.Controls } } - e.Device.Capture(_presenter); + e.Pointer.Capture(_presenter); e.Handled = true; } protected override void OnPointerMoved(PointerEventArgs e) { - if (_presenter != null && e.Device.Captured == _presenter) + if (_presenter != null && e.Pointer.Captured == _presenter) { var point = e.GetPosition(_presenter); @@ -694,9 +694,9 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (_presenter != null && e.Device.Captured == _presenter) + if (_presenter != null && e.Pointer.Captured == _presenter) { - e.Device.Capture(null); + e.Pointer.Capture(null); } } diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 384a9db0cf..d90729e8a5 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Threading; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -79,7 +80,10 @@ namespace Avalonia.Controls { StopTimer(); - ToolTip.SetIsOpen(control, true); + if ((control as IVisual).IsAttachedToVisualTree) + { + ToolTip.SetIsOpen(control, true); + } } private void Close(Control control) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 8907137ecb..59844be8a6 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -507,14 +507,19 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0, - e.MouseButton == MouseButton.Right); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0, + point.Properties.IsRightButtonPressed); + } } } diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 7c88401615..d06a71a9f8 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls /// static WrapPanel() { - AffectsMeasure(OrientationProperty); + AffectsMeasure(OrientationProperty, ItemWidthProperty, ItemHeightProperty); } /// diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml index ca7314264a..2619fd744a 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml @@ -2,7 +2,7 @@ xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Avalonia.Diagnostics.Views.TreePageView"> - + - + diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml new file mode 100644 index 0000000000..e227fc2812 --- /dev/null +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - public class TouchDevice : IInputDevice + public class TouchDevice : IInputDevice, IDisposable { - Dictionary _pointers = new Dictionary(); - + private readonly Dictionary _pointers = new Dictionary(); + private bool _disposed; + KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); @@ -28,6 +30,8 @@ namespace Avalonia.Input public void ProcessRawEvent(RawInputEventArgs ev) { + if(_disposed) + return; var args = (RawTouchEventArgs)ev; if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) { @@ -82,6 +86,17 @@ namespace Avalonia.Input } + + public void Dispose() + { + if(_disposed) + return; + var values = _pointers.Values.ToList(); + _pointers.Clear(); + _disposed = true; + foreach (var p in values) + p.Dispose(); + } } } diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 35e50b1b36..88a6da8945 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -22,5 +22,6 @@ + diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 950943d54a..95c2aabb3d 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -3,12 +3,15 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Text; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Native.Interop; using Avalonia.Platform.Interop; using Avalonia.Threading; +using Avalonia.Dialogs; +using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Native { @@ -211,6 +214,29 @@ namespace Avalonia.Native DoLayoutReset(); } + private static NativeMenu CreateDefaultAppMenu() + { + var result = new NativeMenu(); + + var aboutItem = new NativeMenuItem + { + Header = "About Avalonia", + }; + + aboutItem.Clicked += async (sender, e) => + { + var dialog = new AboutAvaloniaDialog(); + + var mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + + await dialog.ShowDialog(mainWindow); + }; + + result.Add(aboutItem); + + return result; + } + private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { QueueReset(); @@ -241,6 +267,10 @@ namespace Avalonia.Native { SetMenu(_menu); } + else + { + SetMenu(CreateDefaultAppMenu()); + } } else { @@ -321,7 +351,7 @@ namespace Avalonia.Native }), new MenuActionCallback(() => { item.RaiseClick(); })); menu.AddItem(menuItem); - if (item.Menu?.Items?.Count > 0) + if (item.Menu?.Items?.Count >= 0) { var submenu = _factory.CreateMenu(); @@ -362,7 +392,7 @@ namespace Avalonia.Native return false; }), new MenuActionCallback(() => { item.RaiseClick(); })); - if (item.Menu?.Items.Count > 0 || isMainMenu) + if (item.Menu?.Items.Count >= 0 || isMainMenu) { var subMenu = CreateSubmenu(item.Menu?.Items); diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index ddb71b61bb..571475c7ea 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -21,7 +21,6 @@ namespace Avalonia.Native [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); - internal static readonly MouseDevice MouseDevice = new MouseDevice(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); public Size DoubleClickSize => new Size(4, 4); @@ -95,7 +94,6 @@ namespace Avalonia.Native .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) - .Bind().ToConstant(MouseDevice) .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index fe7458d583..8b397403ca 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -24,7 +24,7 @@ namespace Avalonia.Native private object _syncRoot = new object(); private bool _deferredRendering = false; private bool _gpu = false; - private readonly IMouseDevice _mouse; + private readonly MouseDevice _mouse; private readonly IKeyboardDevice _keyboard; private readonly IStandardCursorFactory _cursorFactory; private Size _savedLogicalSize; @@ -38,7 +38,7 @@ namespace Avalonia.Native _deferredRendering = opts.UseDeferredRendering; _keyboard = AvaloniaLocator.Current.GetService(); - _mouse = AvaloniaLocator.Current.GetService(); + _mouse = new MouseDevice(); _cursorFactory = AvaloniaLocator.Current.GetService(); } @@ -96,7 +96,7 @@ namespace Avalonia.Native public Action Paint { get; set; } public Action Resized { get; set; } public Action Closed { get; set; } - public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice; + public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); @@ -142,6 +142,7 @@ namespace Avalonia.Native { n?.Dispose(); } + _parent._mouse.Dispose(); } void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke(); diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index 64349222ea..dc5cd002dc 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -1,51 +1,23 @@ - - - - - - - - + + - - - - - - - + - + diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index a486723d86..b57b4a0ca8 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -5,12 +5,16 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Media.Fonts; -using Avalonia.Platform; namespace Avalonia.Media { - public class FontFamily + public sealed class FontFamily { + static FontFamily() + { + Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + } + /// /// /// Initializes a new instance of the class. @@ -30,9 +34,7 @@ namespace Avalonia.Media { if (string.IsNullOrEmpty(name)) { - FamilyNames = new FamilyNameCollection(string.Empty); - - return; + throw new ArgumentNullException(nameof(name)); } var fontFamilySegment = GetFontFamilyIdentifier(name); @@ -53,13 +55,16 @@ namespace Avalonia.Media /// /// Represents the default font family /// - public static FontFamily Default => new FontFamily(string.Empty); + public static FontFamily Default { get; } /// /// Represents all font families in the system. This can be an expensive call depending on platform implementation. /// + /// + /// Consider using the new instead. + /// public static IEnumerable SystemFontFamilies => - AvaloniaLocator.Current.GetService().InstalledFontNames.Select(name => new FontFamily(name)); + FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); /// /// Gets the primary family name of the font family. @@ -181,7 +186,14 @@ namespace Avalonia.Media { var hash = (int)2186146271; - hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); + if (Key != null) + { + hash = (hash * 15768619) ^ Key.GetHashCode(); + } + else + { + hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); + } if (Key != null) { diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs new file mode 100644 index 0000000000..be1bd269ed --- /dev/null +++ b/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 +{ + /// + /// 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. + /// + public abstract class FontManager + { + public static readonly FontManager Default = CreateDefault(); + + /// + /// Gets the system's default font family's name. + /// + public string DefaultFontFamilyName + { + get; + protected set; + } + + /// + /// Get all installed fonts in the system. + /// If true the font collection is updated. + /// + public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + + /// + /// Get a cached typeface from specified parameters. + /// + /// The font family. + /// The font weight. + /// The font style. + /// + /// The cached typeface. + /// + public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// Returns null if no fallback was found. + /// + /// The codepoint to match against. + /// The font weight. + /// The font style. + /// The font family. This is optional and used for fallback lookup. + /// The culture. + /// + /// The matched typeface. + /// + 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(); + + 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 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 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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index acf0bbdb11..e777d93315 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -9,7 +9,7 @@ using System.Text; namespace Avalonia.Media.Fonts { - public class FamilyNameCollection : IEnumerable + public sealed class FamilyNameCollection : IReadOnlyList { /// /// Initializes a new instance of the class. @@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts return other.ToString().Equals(ToString()); } + + public int Count => Names.Count; + + public string this[int index] => Names[index]; } } diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index e20e03e296..5013f925b3 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -16,9 +16,10 @@ namespace Avalonia.Media private IFormattedTextImpl _platformImpl; private IReadOnlyList _spans; private Typeface _typeface; + private double _fontSize; private string _text; private TextAlignment _textAlignment; - private TextWrapping _wrapping; + private TextWrapping _textWrapping; /// /// Initializes a new instance of the class. @@ -37,6 +38,31 @@ namespace Avalonia.Media _platform = platform; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + 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; + } + /// /// Gets the bounds of the text within the . /// @@ -61,6 +87,16 @@ namespace Avalonia.Media set => Set(ref _typeface, value); } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => _fontSize; + set => Set(ref _fontSize, value); + } + /// /// Gets or sets a collection of spans that describe the formatting of subsections of the /// text. @@ -92,10 +128,10 @@ namespace Avalonia.Media /// /// Gets or sets the text wrapping. /// - public TextWrapping Wrapping + public TextWrapping TextWrapping { - get => _wrapping; - set => Set(ref _wrapping, value); + get => _textWrapping; + set => Set(ref _textWrapping, value); } /// @@ -110,8 +146,9 @@ namespace Avalonia.Media _platformImpl = _platform.CreateFormattedText( _text, _typeface, + _fontSize, _textAlignment, - _wrapping, + _textWrapping, _constraint, _spans); } diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs new file mode 100644 index 0000000000..3ba31f7e84 --- /dev/null +++ b/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(); + + public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface)) + { + } + + public GlyphTypeface(IGlyphTypefaceImpl platformImpl) + { + PlatformImpl = platformImpl; + } + + public IGlyphTypefaceImpl PlatformImpl { get; } + + /// + /// Gets the font design units per em. + /// + public short DesignEmHeight => PlatformImpl.DesignEmHeight; + + /// + /// Gets the recommended distance above the baseline in design em size. + /// + public int Ascent => PlatformImpl.Ascent; + + /// + /// Gets the recommended distance under the baseline in design em size. + /// + public int Descent => PlatformImpl.Descent; + + /// + /// Gets the recommended additional space between two lines of text in design em size. + /// + public int LineGap => PlatformImpl.LineGap; + + /// + /// Gets the recommended line height. + /// + public int LineHeight => Descent - Ascent + LineGap; + + /// + /// Gets a value that indicates the distance of the underline from the baseline in design em size. + /// + public int UnderlinePosition => PlatformImpl.UnderlinePosition; + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + public int UnderlineThickness => PlatformImpl.UnderlineThickness; + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size. + /// + public int StrikethroughPosition => PlatformImpl.StrikethroughPosition; + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + public int StrikethroughThickness => PlatformImpl.StrikethroughThickness; + + /// + /// Returns an glyph index for the specified codepoint. + /// + /// + /// Returns 0 if a glyph isn't found. + /// + /// The codepoint. + /// + /// A glyph index. + /// + public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint); + + /// + /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// + /// The codepoints to map. + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) => PlatformImpl.GetGlyphs(codepoints); + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph); + + /// + /// Returns an array of glyph advances in design em size. + /// + /// The glyph indices. + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) => PlatformImpl.GetGlyphAdvances(glyphs); + + void IDisposable.Dispose() + { + PlatformImpl?.Dispose(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 37ac0953bf..a6d5c8a43c 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/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 { /// /// Represents a typeface. /// - public class Typeface + [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] + public class Typeface : IEquatable { public static readonly Typeface Default = new Typeface(FontFamily.Default); + private GlyphTypeface _glyphTypeface; + /// /// Initializes a new instance of the class. /// /// The font family. - /// The font size, in DIPs. - /// The font style. /// The font weight. - public Typeface( - FontFamily fontFamily, - double fontSize = 12, - FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) + /// The font style. + public Typeface([NotNull]FontFamily fontFamily, + FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal) { - if (fontSize <= 0) - { - throw new ArgumentException("Font size must be > 0."); - } - if (weight <= 0) { throw new ArgumentException("Font weight must be > 0."); } FontFamily = fontFamily; - FontSize = fontSize; Style = style; Weight = weight; } @@ -42,15 +41,12 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The name of the font family. - /// The font size, in DIPs. /// The font style. /// The font weight. - public Typeface( - string fontFamilyName, - double fontSize = 12, - FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) - : this(new FontFamily(fontFamilyName), fontSize, style, weight) + public Typeface(string fontFamilyName, + FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal) + : this(new FontFamily(fontFamilyName), weight, style) { } @@ -59,11 +55,6 @@ namespace Avalonia.Media /// public FontFamily FontFamily { get; } - /// - /// Gets the size of the font in DIPs. - /// - public double FontSize { get; } - /// /// Gets the font style. /// @@ -73,5 +64,59 @@ namespace Avalonia.Media /// Gets the font weight. /// public FontWeight Weight { get; } + + /// + /// Gets the glyph typeface. + /// + /// + /// The glyph typeface. + /// + 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; + } + } } } diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs new file mode 100644 index 0000000000..254b5d07d1 --- /dev/null +++ b/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 + { + /// + /// Gets the system's default font family's name. + /// + string DefaultFontFamilyName { get; } + + /// + /// Get all installed fonts in the system. + /// If true the font collection is updated. + /// + IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + + /// + /// Get a typeface from specified parameters. + /// + /// The font family. + /// The font weight. + /// The font style. + /// + /// The typeface. + /// + Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// + /// The codepoint to match against. + /// The font weight. + /// The font style. + /// The font family. This is optional and used for fallback lookup. + /// The culture. + /// + /// The typeface. + /// + Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null); + } +} diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs new file mode 100644 index 0000000000..8c043a5129 --- /dev/null +++ b/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 + { + /// + /// Gets the font design units per em. + /// + short DesignEmHeight { get; } + + /// + /// Gets the recommended distance above the baseline in design em size. + /// + int Ascent { get; } + + /// + /// Gets the recommended distance under the baseline in design em size. + /// + int Descent { get; } + + /// + /// Gets the recommended additional space between two lines of text in design em size. + /// + int LineGap { get; } + + /// + /// Gets a value that indicates the distance of the underline from the baseline in design em size. + /// + int UnderlinePosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + int UnderlineThickness { get; } + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size. + /// + int StrikethroughPosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + int StrikethroughThickness { get; } + + /// + /// Returns an glyph index for the specified codepoint. + /// + /// + /// Returns 0 if a glyph isn't found. + /// + /// The codepoint. + /// + /// A glyph index. + /// + ushort GetGlyph(uint codepoint); + + /// + /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// + /// The codepoints to map. + /// + /// An array of glyph indices. + /// + ushort[] GetGlyphs(ReadOnlySpan codepoints); + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + int GetGlyphAdvance(ushort glyph); + + /// + /// Returns an array of glyph advances in design em size. + /// + /// The glyph indices. + /// + /// An array of glyph advances. + /// + int[] GetGlyphAdvances(ReadOnlySpan glyphs); + } +} diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 87db9251e1..5a0a7b2f19 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -13,16 +13,12 @@ namespace Avalonia.Platform /// public interface IPlatformRenderInterface { - /// - /// Get all installed fonts in the system - /// - IEnumerable InstalledFontNames { get; } - /// /// Creates a formatted text implementation. /// /// The text. /// The base typeface. + /// The font size. /// The text alignment. /// The text wrapping mode. /// The text layout constraints. @@ -31,6 +27,7 @@ namespace Avalonia.Platform IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -114,5 +111,14 @@ namespace Avalonia.Platform /// The number of bytes per row. /// An . IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride); + + /// + /// Creates a glyph typeface for specified typeface. + /// + /// The typeface. + /// + /// The glyph typeface implementation. + /// + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 7b10fc1212..e341f02901 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -7,7 +7,8 @@ namespace Avalonia.Rendering { public class RendererBase { - private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18); + private static readonly Typeface s_fpsTypeface = new Typeface("Arial"); + private static int s_fontSize = 18; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; private int _fps; @@ -18,7 +19,8 @@ namespace Avalonia.Rendering { _fpsText = new FormattedText { - Typeface = s_fpsTypeface + Typeface = s_fpsTypeface, + FontSize = s_fontSize }; } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 558e96e132..1689f2e964 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -51,7 +51,7 @@ namespace Avalonia.Rendering.SceneGraph UpdateSize(scene); } - if (visual.VisualRoot != null) + if (visual.VisualRoot == scene.Root.Visual) { if (node?.Parent != null && visual.VisualParent != null && diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index d7a7bb97fd..6ba562bb69 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -19,9 +19,7 @@ namespace Avalonia.X11 class AvaloniaX11Platform : IWindowingPlatform { private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice()); - private Lazy _mouseDevice = new Lazy(() => new MouseDevice()); public KeyboardDevice KeyboardDevice => _keyboardDevice.Value; - public MouseDevice MouseDevice => _mouseDevice.Value; public Dictionary> Windows = new Dictionary>(); public XI2Manager XI2; public X11Info Info { get; private set; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 17471fad10..32460fed86 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -32,7 +32,8 @@ namespace Avalonia.X11 private PixelPoint? _configurePoint; private bool _triggeredExpose; private IInputRoot _inputRoot; - private readonly IMouseDevice _mouse; + private readonly MouseDevice _mouse; + private readonly TouchDevice _touch; private readonly IKeyboardDevice _keyboard; private PixelPoint? _position; private PixelSize _realSize; @@ -57,7 +58,8 @@ namespace Avalonia.X11 _platform = platform; _popup = popupParent != null; _x11 = platform.Info; - _mouse = platform.MouseDevice; + _mouse = new MouseDevice(); + _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; var glfeature = AvaloniaLocator.Current.GetService(); @@ -702,6 +704,8 @@ namespace Avalonia.X11 _platform.XI2?.OnWindowDestroyed(_handle); _handle = IntPtr.Zero; Closed?.Invoke(); + _mouse.Dispose(); + _touch.Dispose(); } if (_useRenderWindow && _renderHandle != IntPtr.Zero) @@ -830,6 +834,8 @@ namespace Avalonia.X11 } public IMouseDevice MouseDevice => _mouse; + public TouchDevice TouchDevice => _touch; + public IPopupImpl CreatePopup() => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 6989d6d26d..e37ed39bee 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -92,8 +92,6 @@ namespace Avalonia.X11 private PointerDeviceInfo _pointerDevice; private AvaloniaX11Platform _platform; - private readonly TouchDevice _touchDevice = new TouchDevice(); - public bool Init(AvaloniaX11Platform platform) { @@ -198,7 +196,7 @@ namespace Avalonia.X11 (ev.Type == XiEventType.XI_TouchUpdate ? RawPointerEventType.TouchUpdate : RawPointerEventType.TouchEnd); - client.ScheduleInput(new RawTouchEventArgs(_touchDevice, + client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice, ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); return; } @@ -232,10 +230,10 @@ namespace Avalonia.X11 } if (scrollDelta != default) - client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp, + client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); if (_pointerDevice.HasMotion(ev)) - client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, RawPointerEventType.Move, ev.Position, ev.Modifiers)); } @@ -248,7 +246,7 @@ namespace Avalonia.X11 : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp) : (RawPointerEventType?)null; if (type.HasValue) - client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); } @@ -310,5 +308,7 @@ namespace Avalonia.X11 { IInputRoot InputRoot { get; } void ScheduleInput(RawInputEventArgs args); + IMouseDevice MouseDevice { get; } + TouchDevice TouchDevice { get; } } } diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 4f884cdf33..68da513528 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -12,5 +12,6 @@ + diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Skia/Avalonia.Skia/FontKey.cs new file mode 100644 index 0000000000..bb3fe230c1 --- /dev/null +++ b/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 + { + 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; + } + } +} diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs new file mode 100644 index 0000000000..03de82178a --- /dev/null +++ b/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 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); + } + } +} diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index eb7b65cdce..a9358cb458 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -18,6 +18,7 @@ namespace Avalonia.Skia public FormattedTextImpl( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -28,47 +29,22 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - SKTypeface skiaTypeface = null; + var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style); - if (typeface.FontFamily.Key != null) + _paint = new SKPaint { - var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - skiaTypeface = typefaces.GetTypeFace(typeface); - } - else - { - if (typeface.FontFamily.FamilyNames.HasFallbacks) - { - foreach (var familyName in typeface.FontFamily.FamilyNames) - { - skiaTypeface = TypefaceCache.GetTypeface( - familyName, - typeface.Style, - typeface.Weight); - if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break; - } - } - else - { - skiaTypeface = TypefaceCache.GetTypeface( - typeface.FontFamily.Name, - typeface.Style, - typeface.Weight); - } - } - - _paint = new SKPaint(); + TextEncoding = SKTextEncoding.Utf16, + IsStroke = false, + IsAntialias = true, + LcdRenderText = true, + SubpixelText = true, + Typeface = entry.SKTypeface, + TextSize = (float)fontSize, + TextAlign = textAlignment.ToSKTextAlign() + }; //currently Skia does not measure properly with Utf8 !!! //Paint.TextEncoding = SKTextEncoding.Utf8; - _paint.TextEncoding = SKTextEncoding.Utf16; - _paint.IsStroke = false; - _paint.IsAntialias = true; - _paint.LcdRenderText = true; - _paint.SubpixelText = true; - _paint.Typeface = skiaTypeface; - _paint.TextSize = (float)typeface.FontSize; - _paint.TextAlign = textAlignment.ToSKTextAlign(); _wrapping = wrapping; _constraint = constraint; @@ -99,7 +75,24 @@ namespace Avalonia.Skia public TextHitTestResult HitTestPoint(Point point) { float y = (float)point.Y; - var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y); + + AvaloniaFormattedTextLine line = default; + + float nextTop = 0; + + foreach(var currentLine in _skiaLines) + { + if(currentLine.Top <= y) + { + line = currentLine; + nextTop = currentLine.Top + currentLine.Height; + } + else + { + nextTop = currentLine.Top; + break; + } + } if (!line.Equals(default(AvaloniaFormattedTextLine))) { @@ -127,12 +120,15 @@ namespace Avalonia.Skia line.Length : (line.Length - 1); } - return new TextHitTestResult + if (y < nextTop) { - IsInside = false, - TextPosition = line.Start + offset, - IsTrailing = Text.Length == (line.Start + offset + 1) - }; + return new TextHitTestResult + { + IsInside = false, + TextPosition = line.Start + offset, + IsTrailing = Text.Length == (line.Start + offset + 1) + }; + } } bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height); diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..e46f766255 --- /dev/null +++ b/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; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + //ToDo: Get these values from HarfBuzz + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan 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; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan 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); + } + } +} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 15f38b1c4f..ee0cfb2f06 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls.Platform.Surfaces; @@ -17,12 +18,13 @@ namespace Avalonia.Skia /// internal class PlatformRenderInterface : IPlatformRenderInterface { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + private readonly ICustomSkiaGpu _customSkiaGpu; private GRContext GrContext { get; } - public IEnumerable InstalledFontNames => SKFontManager.Default.FontFamilies; - public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) { if (customSkiaGpu != null) @@ -52,12 +54,13 @@ namespace Avalonia.Skia public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList spans) { - return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); + return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); @@ -151,5 +154,10 @@ namespace Avalonia.Skia { return new WriteableBitmapImpl(size, dpi, format); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 17448127b0..577567a8a1 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -4,114 +4,59 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; - using Avalonia.Media; -using SkiaSharp; - namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary> _fontFamilies = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _fontFamilies = + new ConcurrentDictionary>(); - public void AddTypeFace(SKTypeface typeface) + public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry) { - var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); - - if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily)) + if (!_fontFamilies.TryGetValue(familyName, out var fontFamily)) { - fontFamily = new ConcurrentDictionary(); + fontFamily = new ConcurrentDictionary(); - _fontFamilies.TryAdd(typeface.FamilyName, fontFamily); + _fontFamilies.TryAdd(familyName, fontFamily); } - fontFamily.TryAdd(key, typeface); + fontFamily.TryAdd(key, entry); } - public SKTypeface GetTypeFace(Typeface typeface) + public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle) { - var styleSlant = SKFontStyleSlant.Upright; - - switch (typeface.Style) - { - case FontStyle.Italic: - styleSlant = SKFontStyleSlant.Italic; - break; - - case FontStyle.Oblique: - styleSlant = SKFontStyleSlant.Oblique; - break; - } + var key = new FontKey(fontWeight, fontStyle); - if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily)) - { - return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight); - } - - var weight = (SKFontStyleWeight)typeface.Weight; - - var key = new FontKey(weight, styleSlant); - - return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)); + return _fontFamilies.TryGetValue(familyName, out var fontFamily) ? + fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) : + new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default); } - private static SKTypeface GetFallback(IDictionary fontFamily, FontKey key) + private static TypefaceCollectionEntry GetFallback(IDictionary fontFamily, FontKey key) { var keys = fontFamily.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Slant == key.Slant).ToArray(); + x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); if (!keys.Any()) { keys = fontFamily.Keys.Where( - x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); if (!keys.Any()) { keys = fontFamily.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && - (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + (x.Style >= key.Style || x.Style < key.Style)).ToArray(); } } key = keys.FirstOrDefault(); - fontFamily.TryGetValue(key, out var typeface); - - return typeface; - } - - private struct FontKey - { - public readonly SKFontStyleSlant Slant; - public readonly SKFontStyleWeight Weight; - - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) - { - Slant = slant; - Weight = weight; - } - - public override int GetHashCode() - { - var hash = 17; - hash = (hash * 31) + (int)Slant; - hash = (hash * 31) + (int)Weight; - - return hash; - } + fontFamily.TryGetValue(key, out var entry); - public override bool Equals(object other) - { - return other is FontKey key && this.Equals(key); - } - - private bool Equals(FontKey other) - { - return Slant == other.Slant && - Weight == other.Weight; - } + return entry; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index ab8ee85a54..4bb42c7118 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -45,9 +45,13 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); - var typeface = SKTypeface.FromStream(assetStream); + var skTypeface = SKTypeface.FromStream(assetStream); - typeFaceCollection.AddTypeFace(typeface); + var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant); + + var entry = new TypefaceCollectionEntry(typeface, skTypeface); + + typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry); } return typeFaceCollection; diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index f16e967f42..ce3aef755b 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -25,6 +25,11 @@ namespace Avalonia.Skia AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); + + var fontManager = new FontManagerImpl(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(fontManager); } /// diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index 9e270114d2..1c2b855032 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -1,7 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Generic; +using System.Collections.Concurrent; using Avalonia.Media; using SkiaSharp; @@ -12,88 +12,36 @@ namespace Avalonia.Skia /// internal static class TypefaceCache { - public static readonly string DefaultFamilyName = CreateDefaultFamilyName(); + private static readonly ConcurrentDictionary> s_cache = + new ConcurrentDictionary>(); - private static readonly Dictionary> s_cache = - new Dictionary>(); - - struct FontKey + public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - public readonly SKFontStyleSlant Slant; - public readonly SKFontStyleWeight Weight; - - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) + if (fontFamily.Key != null) { - Slant = slant; - Weight = weight; + return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily) + .Get(fontFamily.Name, fontWeight, fontStyle); } - public override int GetHashCode() - { - int hash = 17; - hash = hash * 31 + (int)Slant; - hash = hash * 31 + (int)Weight; - - return hash; - } - - public override bool Equals(object other) - { - return other is FontKey ? Equals((FontKey)other) : false; - } - - public bool Equals(FontKey other) - { - return Slant == other.Slant && - Weight == other.Weight; - } - - // Equals and GetHashCode ommitted - } - - private static string CreateDefaultFamilyName() - { - var defaultTypeface = SKTypeface.CreateDefault(); + var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary()); - return defaultTypeface.FamilyName; - } + var key = new FontKey(fontWeight, fontStyle); - private static SKTypeface GetTypeface(string name, FontKey key) - { - var familyKey = name; - - if (!s_cache.TryGetValue(familyKey, out var entry)) + if (typefaceCollection.TryGetValue(key, out var entry)) { - s_cache[familyKey] = entry = new Dictionary(); + return entry; } - if (!entry.TryGetValue(key, out var typeface)) - { - typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ?? - GetTypeface(DefaultFamilyName, key); + var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default; - entry[key] = typeface; - } + var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle); - return typeface; - } - - public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight) - { - var skStyle = SKFontStyleSlant.Upright; + entry = new TypefaceCollectionEntry(typeface, skTypeface); - switch (style) - { - case FontStyle.Italic: - skStyle = SKFontStyleSlant.Italic; - break; - - case FontStyle.Oblique: - skStyle = SKFontStyleSlant.Oblique; - break; - } + typefaceCollection[key] = entry; - return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle)); + return entry; } } } diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs new file mode 100644 index 0000000000..ef9f889819 --- /dev/null +++ b/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; } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 458d8f9cbb..7d47b95ede 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5ab9a8f74d..1bda5157a5 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls; @@ -27,6 +28,8 @@ namespace Avalonia.Direct2D1 { public class Direct2D1Platform : IPlatformRenderInterface { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); private static readonly Direct2D1Platform s_instance = new Direct2D1Platform(); public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; } @@ -41,20 +44,6 @@ namespace Avalonia.Direct2D1 public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; } - public IEnumerable InstalledFontNames - { - get - { - var cache = Direct2D1FontCollectionCache.s_installedFontCollection; - var length = cache.FontFamilyCount; - for (int i = 0; i < length; i++) - { - var names = cache.GetFontFamily(i).FamilyNames; - yield return names.GetString(0); - } - } - } - private static readonly object s_initLock = new object(); private static bool s_initialized = false; @@ -120,6 +109,7 @@ namespace Avalonia.Direct2D1 { InitializeDirect2D(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -131,6 +121,7 @@ namespace Avalonia.Direct2D1 public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -139,6 +130,7 @@ namespace Avalonia.Direct2D1 return new FormattedTextImpl( text, typeface, + fontSize, textAlignment, wrapping, constraint, @@ -201,5 +193,10 @@ namespace Avalonia.Direct2D1 { return new WicBitmapImpl(format, data, size, dpi, stride); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index d93a59d384..b455c4fbee 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -1,62 +1,61 @@ using System.Collections.Concurrent; using Avalonia.Media; using Avalonia.Media.Fonts; +using SharpDX.DirectWrite; +using FontFamily = Avalonia.Media.FontFamily; +using FontStyle = SharpDX.DirectWrite.FontStyle; +using FontWeight = SharpDX.DirectWrite.FontWeight; namespace Avalonia.Direct2D1.Media { internal static class Direct2D1FontCollectionCache { - private static readonly ConcurrentDictionary s_cachedCollections; - internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection; + private static readonly ConcurrentDictionary s_cachedCollections; + internal static readonly FontCollection InstalledFontCollection; static Direct2D1FontCollectionCache() { - s_cachedCollections = new ConcurrentDictionary(); + s_cachedCollections = new ConcurrentDictionary(); - s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); + InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); } - public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface) + public static Font GetFont(Typeface typeface) { var fontFamily = typeface.FontFamily; var fontCollection = GetOrAddFontCollection(fontFamily); - var fontFamilyName = FontFamily.Default.Name; - // Should this be cached? foreach (var familyName in fontFamily.FamilyNames) { - if (!fontCollection.FindFamilyName(familyName, out _)) + if (fontCollection.FindFamilyName(familyName, out var index)) { - continue; + return fontCollection.GetFontFamily(index).GetFirstMatchingFont( + (FontWeight)typeface.Weight, + FontStretch.Normal, + (FontStyle)typeface.Style); } - - fontFamilyName = familyName; - - break; } - return new SharpDX.DirectWrite.TextFormat( - Direct2D1Platform.DirectWriteFactory, - fontFamilyName, - fontCollection, - (SharpDX.DirectWrite.FontWeight)typeface.Weight, - (SharpDX.DirectWrite.FontStyle)typeface.Style, - SharpDX.DirectWrite.FontStretch.Normal, - (float)typeface.FontSize); + InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i); + + return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont( + (FontWeight)typeface.Weight, + FontStretch.Normal, + (FontStyle)typeface.Style); } - private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily) + private static FontCollection GetOrAddFontCollection(FontFamily fontFamily) { - return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); + return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); } - private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key) + private static FontCollection CreateFontCollection(FontFamilyKey key) { var assets = FontFamilyLoader.LoadFontAssets(key); var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); - return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); + return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs new file mode 100644 index 0000000000..94de397652 --- /dev/null +++ b/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 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); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b73deb1f0a..b1a177ad24 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -14,6 +14,7 @@ namespace Avalonia.Direct2D1.Media public FormattedTextImpl( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -21,20 +22,20 @@ namespace Avalonia.Direct2D1.Media { Text = text; - using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface)) + using (var font = Direct2D1FontCollectionCache.GetFont(typeface)) + using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory, + typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight, + (DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize)) { textFormat.WordWrapping = wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; TextLayout = new DWrite.TextLayout( - Direct2D1Platform.DirectWriteFactory, - Text ?? string.Empty, - textFormat, - (float)constraint.Width, - (float)constraint.Height) - { - TextAlignment = textAlignment.ToDirect2D() - }; + Direct2D1Platform.DirectWriteFactory, + Text ?? string.Empty, + textFormat, + (float)constraint.Width, + (float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() }; } if (spans != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..32def01c39 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -0,0 +1,188 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.Platform; +using HarfBuzzSharp; +using SharpDX.DirectWrite; + +namespace Avalonia.Direct2D1.Media +{ + public class GlyphTypefaceImpl : IGlyphTypefaceImpl + { + private bool _isDisposed; + + public GlyphTypefaceImpl(Typeface typeface) + { + DWFont = Direct2D1FontCollectionCache.GetFont(typeface); + + FontFace = new FontFace(DWFont); + + Face = new Face(GetTable); + + Font = new HarfBuzzSharp.Font(Face); + + Font.SetFunctionsOpenType(); + + Font.GetScale(out var xScale, out _); + + DesignEmHeight = (short)xScale; + + if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) + { + Font.TryGetVerticalFontExtents(out fontExtents); + } + + Ascent = -fontExtents.Ascender; + + Descent = -fontExtents.Descender; + + LineGap = fontExtents.LineGap; + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition)) + { + UnderlinePosition = underlinePosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness)) + { + UnderlineThickness = underlineThickness; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition)) + { + StrikethroughPosition = strikethroughPosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness)) + { + StrikethroughThickness = strikethroughThickness; + } + } + + private Blob GetTable(Face face, Tag tag) + { + var dwTag = (int)SwapBytes(tag); + + if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) + { + return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { }); + } + + return null; + } + + private static uint SwapBytes(uint x) + { + x = (x >> 16) | (x << 16); + + return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); + } + + public SharpDX.DirectWrite.Font DWFont { get; } + + public FontFace FontFace { get; } + + public Face Face { get; } + + public HarfBuzzSharp.Font Font { get; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + //ToDo: Read font table for these values + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan 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; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var glyphIndices = new uint[glyphs.Length]; + + for (var i = 0; i < glyphs.Length; i++) + { + glyphIndices[i] = glyphs[i]; + } + + return Font.GetHorizontalGlyphAdvances(glyphIndices); + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + FontFace?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} + diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index e7c379ad89..8f060b1b81 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -11,19 +11,11 @@ namespace Avalonia.Win32.Input { class WindowsMouseDevice : MouseDevice { - public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); - public WindowsMouseDevice() : base(new WindowsMousePointer()) { } - public WindowImpl CurrentWindow - { - get; - set; - } - class WindowsMousePointer : Pointer { public WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 04a9303d53..9d35342fb9 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -30,6 +30,7 @@ namespace Avalonia.Win32 private IntPtr _hwnd; private bool _multitouch; private TouchDevice _touchDevice = new TouchDevice(); + private MouseDevice _mouseDevice = new WindowsMouseDevice(); private IInputRoot _owner; private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; @@ -205,7 +206,7 @@ namespace Avalonia.Win32 } } - public IMouseDevice MouseDevice => WindowsMouseDevice.Instance; + public IMouseDevice MouseDevice => _mouseDevice; public WindowState WindowState { @@ -333,7 +334,7 @@ namespace Avalonia.Win32 public void BeginMoveDrag(PointerPressedEventArgs e) { - WindowsMouseDevice.Instance.Capture(null); + _mouseDevice.Capture(null); UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero); e.Pointer.Capture(null); @@ -356,7 +357,7 @@ namespace Avalonia.Win32 #if USE_MANAGED_DRAG _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position)); #else - WindowsMouseDevice.Instance.Capture(null); + _mouseDevice.Capture(null); UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, new IntPtr((int)EdgeDic[edge]), IntPtr.Zero); #endif @@ -437,9 +438,7 @@ namespace Avalonia.Win32 uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime()); RawInputEventArgs e = null; - - WindowsMouseDevice.Instance.CurrentWindow = this; - + switch ((UnmanagedMethods.WindowsMessage)msg) { case UnmanagedMethods.WindowsMessage.WM_ACTIVATE: @@ -485,6 +484,8 @@ namespace Avalonia.Win32 _parent._disabledBy.Remove(this); _parent.UpdateEnabled(); } + _mouseDevice.Dispose(); + _touchDevice?.Dispose(); //Free other resources Dispose(); return IntPtr.Zero; @@ -542,7 +543,7 @@ namespace Avalonia.Win32 if(ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN @@ -559,7 +560,7 @@ namespace Avalonia.Win32 if(ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP @@ -587,7 +588,7 @@ namespace Avalonia.Win32 } e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, RawPointerEventType.Move, @@ -597,7 +598,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOUSEWHEEL: e = new RawMouseWheelEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, PointToClient(PointFromLParam(lParam)), @@ -606,7 +607,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOUSEHWHEEL: e = new RawMouseWheelEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, PointToClient(PointFromLParam(lParam)), @@ -616,18 +617,18 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE: _trackingMouse = false; e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, RawPointerEventType.LeaveWindow, - new Point(), WindowsKeyboardDevice.Instance.Modifiers); + new Point(-1,-1), WindowsKeyboardDevice.Instance.Modifiers); break; case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN @@ -635,7 +636,7 @@ namespace Avalonia.Win32 : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN ? RawPointerEventType.RightButtonDown : RawPointerEventType.MiddleButtonDown, - new Point(0, 0), GetMouseModifiers(wParam)); + PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); break; case WindowsMessage.WM_TOUCH: var touchInputCount = wParam.ToInt32(); @@ -649,9 +650,9 @@ namespace Avalonia.Win32 { Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, _owner, - touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ? + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ? RawPointerEventType.TouchEnd : - touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ? + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ? RawPointerEventType.TouchBegin : RawPointerEventType.TouchUpdate, PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), @@ -771,11 +772,11 @@ namespace Avalonia.Win32 { var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam); var modifiers = WindowsKeyboardDevice.Instance.Modifiers; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) modifiers |= RawInputModifiers.LeftMouseButton; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) modifiers |= RawInputModifiers.RightMouseButton; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) modifiers |= RawInputModifiers.MiddleMouseButton; return modifiers; } @@ -785,7 +786,7 @@ namespace Avalonia.Win32 // Ensure that the delegate doesn't get garbage collected by storing it as a field. _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); - _className = "Avalonia-" + Guid.NewGuid(); + _className = $"Avalonia-{Guid.NewGuid().ToString()}"; UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX { diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index a790d2fca1..f2b6b0db4b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -2,9 +2,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Platform; using Avalonia.UnitTests; - using Moq; - using Xunit; namespace Avalonia.Controls.UnitTests @@ -21,185 +19,366 @@ namespace Avalonia.Controls.UnitTests public void Detects_Horizontal_Orientation() { GridSplitter splitter; - var grid = new Grid() - { - RowDefinitions = new RowDefinitions("*,Auto,*"), - ColumnDefinitions = new ColumnDefinitions("*,*"), - Children = - { - new Border { [Grid.RowProperty] = 0 }, - (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), - new Border { [Grid.RowProperty] = 2 } - } - }; + + var grid = new Grid + { + RowDefinitions = new RowDefinitions("*,Auto,*"), + ColumnDefinitions = new ColumnDefinitions("*,*"), + Children = + { + new Border { [Grid.RowProperty] = 0 }, + (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), + new Border { [Grid.RowProperty] = 2 } + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":horizontal".Equals); + Assert.Equal(GridResizeDirection.Rows, splitter.GetEffectiveResizeDirection()); } [Fact] public void Detects_Vertical_Orientation() { GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = - { - new Border { [Grid.ColumnProperty] = 0 }, - (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }), + new Border { [Grid.ColumnProperty] = 2 }, + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":vertical".Equals); + Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection()); } [Fact] public void Detects_With_Both_Auto() { GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), - RowDefinitions = new RowDefinitions("Auto,Auto"), - Children = - { - new Border { [Grid.ColumnProperty] = 0 }, - (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), + RowDefinitions = new RowDefinitions("Auto,Auto"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }), + new Border { [Grid.ColumnProperty] = 2 }, + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":vertical".Equals); + Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection()); } [Fact] - public void Horizontal_Stays_Within_Constraints() + public void In_First_Position_Doesnt_Throw_Exception() + { + GridSplitter splitter; + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + (splitter = new GridSplitter { [Grid.ColumnProperty] = 0 }), + new Border { [Grid.ColumnProperty] = 1 }, + new Border { [Grid.ColumnProperty] = 2 }, + } + }; + + var root = new TestRoot { Child = grid }; + root.Measure(new Size(100, 300)); + root.Arrange(new Rect(0, 0, 100, 300)); + + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(100, 1000) + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Horizontal_Stays_Within_Constraints(bool showsPreview) { var control1 = new Border { [Grid.RowProperty] = 0 }; - var splitter = new GridSplitter - { - [Grid.RowProperty] = 1, - }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1, ShowsPreview = showsPreview}; var control2 = new Border { [Grid.RowProperty] = 2 }; - var rowDefinitions = new RowDefinitions() - { - new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, - new RowDefinition(GridLength.Auto), - new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, - }; - - var grid = new Grid() - { - RowDefinitions = rowDefinitions, - Children = - { - control1, splitter, control2 - } - }; + var rowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, + }; + + var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = new VisualLayerManager + { + Child = grid + } + }; - var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 200)); root.Arrange(new Rect(0, 0, 100, 200)); + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(0, -100) - }); - Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star)); - Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, -100) + }); + + if (showsPreview) + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(0, 100) - }); - Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); - Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); - } + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, 100) + }); - [Fact] - public void In_First_Position_Doesnt_Throw_Exception() - { - GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = - { - (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ), - new Border { [Grid.ColumnProperty] = 1 }, - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + if (showsPreview) + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); + } - var root = new TestRoot { Child = grid }; - root.Measure(new Size(100, 300)); - root.Arrange(new Rect(0, 0, 100, 300)); splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(100, 1000) - }); + { + RoutedEvent = Thumb.DragCompletedEvent + }); + + Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); } - [Fact] - public void Vertical_Stays_Within_Constraints() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Vertical_Stays_Within_Constraints(bool showsPreview) { var control1 = new Border { [Grid.ColumnProperty] = 0 }; - var splitter = new GridSplitter - { - [Grid.ColumnProperty] = 1, - }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, ShowsPreview = showsPreview}; var control2 = new Border { [Grid.ColumnProperty] = 2 }; - var columnDefinitions = new ColumnDefinitions() - { - new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, - new ColumnDefinition(GridLength.Auto), - new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, - }; - - var grid = new Grid() - { - ColumnDefinitions = columnDefinitions, - Children = - { - control1, splitter, control2 - } - }; + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, + }; - var root = new TestRoot { Child = grid }; + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = new VisualLayerManager + { + Child = grid + } + }; root.Measure(new Size(200, 100)); root.Arrange(new Rect(0, 0, 200, 100)); + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100, 0) + }); + + if (showsPreview) + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(-100, 0) - }); - Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star)); - Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(100, 0) + }); + + if (showsPreview) + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(100, 0) - }); + { + RoutedEvent = Thumb.DragCompletedEvent + }); + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } + + [Theory] + [InlineData(Key.Up, 90, 110)] + [InlineData(Key.Down, 110, 90)] + public void Vertical_Keyboard_Input_Can_Move_Splitter(Key key, double expectedHeightFirst, double expectedHeightSecond) + { + var control1 = new Border { [Grid.RowProperty] = 0 }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.RowProperty] = 2 }; + + var rowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key + }); + + Assert.Equal(rowDefinitions[0].Height, new GridLength(expectedHeightFirst, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(expectedHeightSecond, GridUnitType.Star)); + } + + [Theory] + [InlineData(Key.Left, 90, 110)] + [InlineData(Key.Right, 110, 90)] + public void Horizontal_Keyboard_Input_Can_Move_Splitter(Key key, double expectedWidthFirst, double expectedWidthSecond) + { + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(expectedWidthFirst, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(expectedWidthSecond, GridUnitType.Star)); + } + + [Fact] + public void Pressing_Escape_Key_Cancels_Resizing() + { + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100, 0) + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(0, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(200, GridUnitType.Star)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Escape + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index b2839360ee..227d783874 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -12,6 +12,7 @@ using Xunit; using System.Collections.ObjectModel; using Avalonia.UnitTests; using Avalonia.Input; +using System.Collections.Generic; namespace Avalonia.Controls.UnitTests { @@ -104,6 +105,28 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { child }, target.GetLogicalChildren()); } + [Fact] + public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl() + { + var item = new Border(); + var items = new ObservableCollection(); + + var target = new ItemsControl + { + Template = GetTemplate(), + Items = items, + }; + + var root = new TestRoot(true, target); + + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + items.Add(item); + + Assert.Equal(target, item.Parent); + } + [Fact] public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate() { @@ -522,6 +545,36 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Presenter_Items_Should_Be_In_Sync() + { + var target = new ItemsControl + { + Template = GetTemplate(), + Items = new object[] + { + new Button(), + new Button(), + }, + }; + + var root = new TestRoot { Child = target }; + var otherPanel = new StackPanel(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.ItemContainerGenerator.Materialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + }; + + target.Items = new[] + { + new Canvas() + }; + } + private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 9a459328aa..df2508a3ed 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -47,6 +47,26 @@ namespace Avalonia.Controls.UnitTests Assert.IsType(target.Presenter); } + [Fact] + public void ListBox_Should_Find_Scrollviewer_In_Template() + { + var target = new ListBox + { + Template = ListBoxTemplate(), + }; + + ScrollViewer viewer = null; + + target.TemplateApplied += (sender, e) => + { + viewer = target.Scroll as ScrollViewer; + }; + + Prepare(target); + + Assert.NotNull(viewer); + } + [Fact] public void ListBoxItem_Containers_Should_Be_Generated() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..d819581000 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -14,6 +15,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; +using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; @@ -23,7 +25,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public class SelectingItemsControlTests { private MouseTestHelper _helper = new MouseTestHelper(); - + [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { @@ -168,6 +170,130 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("B", listBox.SelectedItem); } + [Fact] + public void Setting_SelectedIndex_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 1 + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + [Fact] + public void Setting_SelectedIndex_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 2 + }; + + listBox.BeginInit(); + + listBox.SelectedIndex = 1; + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + [Fact] + public void Setting_SelectedItem_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedItem = "bar" + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + + [Fact] + public void Setting_SelectedItems_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Multiple, + Items = new[] { "foo", "bar", "baz" }, + }; + + var selected = new[] { "foo", "bar" }; + + foreach (var v in selected) + { + listBox.SelectedItems.Add(v); + } + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(selected, listBox.SelectedItems); + } + + [Fact] + public void Setting_SelectedItems_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Multiple, + Items = new[] { "foo", "bar", "baz" }, + }; + + var selected = new[] { "foo", "bar" }; + + foreach (var v in new[] { "bar", "baz" }) + { + listBox.SelectedItems.Add(v); + } + + listBox.BeginInit(); + + listBox.SelectedItems = new AvaloniaList(selected); + + listBox.EndInit(); + + Assert.Equal(selected, listBox.SelectedItems); + } + + [Fact] + public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected, + + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 1 + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + [Fact] public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() { @@ -849,7 +975,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new ListBox { Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"}, + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; target.ApplyTemplate(); @@ -980,6 +1106,45 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(raised); } + [Fact] + public void AutoScrollToSelectedItem_On_Reset_Works() + { + // Issue #3148 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new ResettingCollection(100); + + var target = new ListBox + { + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => + new TextBlock + { + Text = x, + Width = 100, + Height = 10 + }), + AutoScrollToSelectedItem = true, + VirtualizationMode = ItemVirtualizationMode.Simple, + }; + + var root = new TestRoot(true, target); + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + Assert.True(target.Presenter.Panel.Children.Count > 0); + Assert.True(target.Presenter.Panel.Children.Count < 100); + + target.SelectedItem = "Item99"; + + // #3148 triggered here. + items.Reset(new[] { "Item99" }); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(1, target.Presenter.Panel.Children.Count); + } + } + [Fact] public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() { @@ -1020,6 +1185,33 @@ namespace Avalonia.Controls.UnitTests.Primitives target.MoveSelection(NavigationDirection.Next, true); } + [Fact] + public void Pre_Selecting_Item_Should_Set_Selection_After_It_Was_Added_When_AlwaysSelected() + { + var target = new TestSelector(SelectionMode.AlwaysSelected) + { + Template = Template() + }; + + var second = new Item { IsSelected = true }; + + var items = new AvaloniaList + { + new Item(), + second + }; + + target.Items = items; + + target.ApplyTemplate(); + + target.Presenter.ApplyTemplate(); + + Assert.Equal(second, target.SelectedItem); + + Assert.Equal(1, target.SelectedIndex); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => @@ -1028,6 +1220,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + [~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty], }.RegisterInNameScope(scope)); } @@ -1067,10 +1260,39 @@ namespace Avalonia.Controls.UnitTests.Primitives private class TestSelector : SelectingItemsControl { + public TestSelector() + { + + } + + public TestSelector(SelectionMode selectionMode) + { + SelectionMode = selectionMode; + } + public new bool MoveSelection(NavigationDirection direction, bool wrap) { return base.MoveSelection(direction, wrap); } } + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(int itemCount) + { + AddRange(Enumerable.Range(0, itemCount).Select(x => $"Item{x}")); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index be0f4272a5..952a00a14e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1080,6 +1080,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new TestSelector { Items = items, + SelectionMode = SelectionMode.Multiple, Template = Template(), }; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ddf7e7a0fa..a9e86d71ee 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -44,6 +45,29 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(selected, target.SelectedItem); } + [Fact] + public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added() + { + var target = new TabControl + { + Template = TabControlTemplate(), + }; + + const string secondContent = "Second"; + + var items = new AvaloniaList + { + new TabItem { Header = "First"}, + new TabItem { Header = "Second", Content = secondContent, IsSelected = true } + }; + + target.Items = items; + + ApplyTemplate(target); + + Assert.Equal(secondContent, target.SelectedContent); + } + [Fact] public void Logical_Children_Should_Be_TabItems() { @@ -287,6 +311,25 @@ namespace Avalonia.Controls.UnitTests Assert.Single(target.GetLogicalChildren(), content); } + [Fact] + public void Should_Not_Propagate_DataContext_To_TabItem_Content() + { + var dataContext = "DataContext"; + + var tabItem = new TabItem(); + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = dataContext, + Items = new AvaloniaList { tabItem } + }; + + ApplyTemplate(target); + + Assert.NotEqual(dataContext, tabItem.Content); + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs new file mode 100644 index 0000000000..362dd6d111 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -0,0 +1,109 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class TolTipTests + { + private MouseTestHelper _mouseHelper = new MouseTestHelper(); + + [Fact] + public void Should_Not_Open_On_Detached_Control() + { + //issue #3188 + var control = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + Assert.False((control as IVisual).IsAttachedToVisualTree); + + //here in issue #3188 exception is raised + _mouseHelper.Enter(control); + + Assert.False(ToolTip.GetIsOpen(control)); + } + + [Fact] + public void Should_Open_On_Pointer_Enter() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + window.Content = target; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.True(ToolTip.GetIsOpen(target)); + } + } + + [Fact] + public void Should_Open_On_Pointer_Enter_With_Delay() + { + Action timercallback = null; + var delay = TimeSpan.Zero; + + var pti = Mock.Of(x => x.CurrentThreadIsLoopThread == true); + + Mock.Get(pti) + .Setup(v => v.StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((priority, interval, tick) => + { + delay = interval; + timercallback = tick; + }) + .Returns(Disposable.Empty); + + using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti))) + { + var window = new Window(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 1 + }; + + window.Content = target; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.Equal(TimeSpan.FromMilliseconds(1), delay); + Assert.NotNull(timercallback); + Assert.False(ToolTip.GetIsOpen(target)); + + timercallback(); + + Assert.True(ToolTip.GetIsOpen(target)); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index fd93df46b8..82f94b4f3c 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -12,14 +12,14 @@ namespace Avalonia.Controls.UnitTests public void Lays_Out_Horizontally_On_Separate_Lines() { var target = new WrapPanel() - { - Width = 100, - Children = + { + Width = 100, + Children = { new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 }, } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -33,14 +33,14 @@ namespace Avalonia.Controls.UnitTests public void Lays_Out_Horizontally_On_A_Single_Line() { var target = new WrapPanel() - { - Width = 200, - Children = + { + Width = 200, + Children = { new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 }, } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -54,15 +54,15 @@ namespace Avalonia.Controls.UnitTests public void Lays_Out_Vertically_Children_On_A_Single_Line() { var target = new WrapPanel() - { - Orientation = Orientation.Vertical, - Height = 120, - Children = + { + Orientation = Orientation.Vertical, + Height = 120, + Children = { new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 }, } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -76,15 +76,15 @@ namespace Avalonia.Controls.UnitTests public void Lays_Out_Vertically_On_Separate_Lines() { var target = new WrapPanel() - { - Orientation = Orientation.Vertical, - Height = 60, - Children = + { + Orientation = Orientation.Vertical, + Height = 60, + Children = { new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 }, } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -98,17 +98,17 @@ namespace Avalonia.Controls.UnitTests public void Applies_ItemWidth_And_ItemHeight_Properties() { var target = new WrapPanel() - { - Orientation = Orientation.Horizontal, - Width = 50, - ItemWidth = 20, - ItemHeight = 15, - Children = + { + Orientation = Orientation.Horizontal, + Width = 50, + ItemWidth = 20, + ItemHeight = 15, + Children = { new Border(), new Border(), } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -117,5 +117,33 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 20, 15), target.Children[0].Bounds); Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds); } + + [Fact] + void ItemWidth_Trigger_InvalidateMeasure() + { + var target = new WrapPanel(); + + target.Measure(new Size(10, 10)); + + Assert.True(target.IsMeasureValid); + + target.ItemWidth = 1; + + Assert.False(target.IsMeasureValid); + } + + [Fact] + void ItemHeight_Trigger_InvalidateMeasure() + { + var target = new WrapPanel(); + + target.Measure(new Size(10, 10)); + + Assert.True(target.IsMeasureValid); + + target.ItemHeight = 1; + + Assert.False(target.IsMeasureValid); + } } } diff --git a/tests/Avalonia.Input.UnitTests/PointerTests.cs b/tests/Avalonia.Input.UnitTests/PointerTests.cs new file mode 100644 index 0000000000..e639726dbd --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/PointerTests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class PointerTests + { + [Fact] + public void On_Capture_Transfer_PointerCaptureLost_Should_Propagate_Up_To_The_Common_Parent() + { + Border initialParent, initialCapture, newParent, newCapture; + var el = new StackPanel + { + Children = + { + (initialParent = new Border { Child = initialCapture = new Border() }), + (newParent = new Border { Child = newCapture = new Border() }) + } + }; + var receivers = new List(); + var root = new TestRoot(el); + foreach (InputElement d in root.GetSelfAndVisualDescendants()) + d.PointerCaptureLost += (s, e) => receivers.Add(s); + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + + pointer.Capture(initialCapture); + pointer.Capture(newCapture); + Assert.True(receivers.SequenceEqual(new[] { initialCapture, initialParent })); + + receivers.Clear(); + pointer.Capture(null); + Assert.True(receivers.SequenceEqual(new object[] { newCapture, newParent, el, root })); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 6cf38b6121..a683e5cfca 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -175,6 +175,7 @@ namespace Avalonia.Layout.UnitTests x.CreateFormattedText( It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 353123ab2a..bca34dd69d 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -53,7 +53,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - new Typeface(fontFamily, fontSize, fontStyle, fontWeight), + new Typeface(fontFamily, fontWeight, fontStyle), + fontSize, textAlignment, wrapping, widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity), diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index a3cc3dec17..187853283f 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -9,11 +9,10 @@ namespace Avalonia.UnitTests { public class MockPlatformRenderInterface : IPlatformRenderInterface { - public IEnumerable InstalledFontNames => new string[0]; - public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -79,5 +78,10 @@ namespace Avalonia.UnitTests { throw new NotImplementedException(); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return Mock.Of(); + } } } diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index 48c4d73471..f6454a9cd2 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -84,9 +84,9 @@ namespace Avalonia.UnitTests ); if (ButtonCount(props) == 0) { - _pointer.Capture(null); target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, Timestamp(), props, GetModifiers(modifiers), _pressedButton)); + _pointer.Capture(null); } else Move(target, source, position); diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 969d7bc821..56d7f028f2 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -24,8 +24,19 @@ namespace Avalonia.UnitTests } public TestRoot(IControl child) + : this(false, child) + { + Child = child; + } + + public TestRoot(bool useGlobalStyles, IControl child) : this() { + if (useGlobalStyles) + { + StylingParent = UnitTestApplication.Current; + } + Child = child; } diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index f7a878feba..d189aa3165 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -169,6 +169,7 @@ namespace Avalonia.UnitTests x.CreateFormattedText( It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs index 75ae43a1fa..5d47333d51 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs @@ -19,12 +19,48 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(new FontFamily("Arial"), fontFamily); } - [Fact] - public void Should_Be_Equal() + [InlineData("Font A")] + [InlineData("Font A, Font B")] + [InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")] + [InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")] + [Theory] + public void Should_Have_Equal_Hash(string s) { - var fontFamily = new FontFamily("Arial"); + var fontFamily = new FontFamily(s); - Assert.Equal(new FontFamily("Arial"), fontFamily); + Assert.Equal(new FontFamily(s).GetHashCode(), fontFamily.GetHashCode()); + } + + [InlineData("Font A, Font B", "Font B, Font A")] + [InlineData("Font A, Font B", "Font A, Font C")] + [Theory] + public void Should_Not_Have_Equal_Hash(string a, string b) + { + var fontFamily = new FontFamily(b); + + Assert.NotEqual(new FontFamily(a).GetHashCode(), fontFamily.GetHashCode()); + } + + [InlineData("Font A")] + [InlineData("Font A, Font B")] + [InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")] + [InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")] + [Theory] + public void Should_Be_Equal(string s) + { + var fontFamily = new FontFamily(s); + + Assert.Equal(new FontFamily(s), fontFamily); + } + + [InlineData("Font A, Font B", "Font B, Font A")] + [InlineData("Font A, Font B", "Font A, Font C")] + [Theory] + public void Should_Not_Be_Equal(string a, string b) + { + var fontFamily = new FontFamily(b); + + Assert.NotEqual(new FontFamily(a), fontFamily); } [Fact] diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs index 2c8f8eb9b2..0e43c76da1 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs @@ -7,15 +7,21 @@ namespace Avalonia.Visuals.UnitTests.Media public class TypefaceTests { [Fact] - public void Exception_Should_Be_Thrown_If_FontSize_LessThanEqualTo_0() + public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_Zero() { - Assert.Throws(() => new Typeface("foo", 0)); + Assert.Throws(() => new Typeface("foo", 0, (FontStyle)12)); } [Fact] - public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_0() + public void Should_Be_Equal() { - Assert.Throws(() => new Typeface("foo", 12, weight: 0)); + Assert.Equal(new Typeface("Font A"), new Typeface("Font A")); + } + + [Fact] + public void Should_Have_Equal_Hash() + { + Assert.Equal(new Typeface("Font A").GetHashCode(), new Typeface("Font A").GetHashCode()); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index b4743e900d..6063a382a0 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -369,6 +369,81 @@ namespace Avalonia.Visuals.UnitTests.Rendering } + [Fact] + public void Should_Update_VisualNodes_When_Child_Moved_To_New_Parent_And_New_Root() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + Decorator moveFrom; + Decorator moveTo; + Canvas moveMe; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + (moveFrom = new Decorator + { + Child = moveMe = new Canvas(), + }) + } + } + }; + + var otherRoot = new TestRoot + { + Child = new StackPanel + { + Children = + { + (moveTo = new Decorator()) + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + var otherSceneBuilder = new SceneBuilder(); + var otherTarget = new DeferredRenderer( + otherRoot, + loop.Object, + sceneBuilder: otherSceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + otherRoot.Renderer = otherTarget; + + target.Start(); + otherTarget.Start(); + + RunFrame(target); + RunFrame(otherTarget); + + moveFrom.Child = null; + moveTo.Child = moveMe; + + RunFrame(target); + RunFrame(otherTarget); + + var scene = target.UnitTestScene(); + var otherScene = otherTarget.UnitTestScene(); + + var moveFromNode = (VisualNode)scene.FindNode(moveFrom); + var moveToNode = (VisualNode)otherScene.FindNode(moveTo); + + Assert.Empty(moveFromNode.Children); + Assert.Equal(1, moveToNode.Children.Count); + Assert.Same(moveMe, moveToNode.Children[0].Visual); + } + [Fact] public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity() { diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index d31210bc71..032b6582a9 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -13,6 +13,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -51,6 +52,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + throw new NotImplementedException(); + } + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt) { throw new NotImplementedException();