From 00263f71e4b7e3a9acb93444e07b10b5e35fa1a4 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 26 Oct 2019 16:49:10 +0200 Subject: [PATCH 01/14] Port GridSplitter from WPF. --- src/Avalonia.Controls/GridSplitter.cs | 872 +++++++++++++++--- src/Avalonia.Themes.Default/GridSplitter.xaml | 56 +- .../GridSplitterTests.cs | 313 ++++--- 3 files changed, 955 insertions(+), 286 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 28b9b3a38f..e5749c8ed9 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -1,210 +1,848 @@ -// 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. + /// Enum to indicate whether GridSplitter 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 GridSplitter 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 + } + + /// + /// Represents the control that redistributes space between columns or rows of a Grid 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 + { + 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 { - // 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(PreviewContentProperty); + set => SetValue(PreviewContentProperty, value); + } - 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); + /// + /// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height. + /// + internal GridResizeDirection GetEffectiveResizeDirection() + { + GridResizeDirection direction = ResizeDirection; - var prevIsStar = IsStar(_prevDefinition); - var nextIsStar = IsStar(_nextDefinition); + if (direction != GridResizeDirection.Auto) + { + return direction; + } - if (prevIsStar && nextIsStar) + // When HorizontalAlignment is Left, Right or Center, resize Columns. + if (HorizontalAlignment != HorizontalAlignment.Stretch) + { + direction = GridResizeDirection.Columns; + } + else if (VerticalAlignment != VerticalAlignment.Stretch) { - foreach (var definition in _definitions) + direction = GridResizeDirection.Rows; + } + else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height. + { + direction = GridResizeDirection.Columns; + } + else + { + direction = GridResizeDirection.Rows; + } + + return direction; + } + + /// + /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction. + /// + private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction) + { + GridResizeBehavior resizeBehavior = ResizeBehavior; + + if (resizeBehavior == GridResizeBehavior.BasedOnAlignment) + { + 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); + + Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + + 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; } - private void SetLength(DefinitionBase definition, double value) + /// + /// Returns true if the row/column has a star length. + /// + private static bool IsStar(DefinitionBase definition) { - var columnDefinition = definition as ColumnDefinition; - if (columnDefinition != null) + return definition.UserSizeValueCache.IsStar; + } + + /// + /// Gets Column or Row definition at index from grid based on resize direction. + /// + private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction) + { + 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) + { + definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength); + } + else if (_resizeData.SplitterIndex == _resizeData.Definition2Index) + { + definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength); + } + + if (_resizeData.SplitBehavior == SplitBehavior.Split) { - columnDefinition.Width = new GridLength(value); + // Determine the minimum and maximum the columns can be resized. + minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); + maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); + } + else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) + { + minDelta = definition1Min - definition1Len; + maxDelta = definition1Max - definition1Len; } else { - ((RowDefinition)definition).Height = new GridLength(value); + minDelta = definition2Len - definition2Max; + maxDelta = 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; - int definitionIndex; //row or col - if (_orientation == Orientation.Vertical) + for (var i = 0; i < definitionsCount; i++) + { + DefinitionBase definition = definitions[i]; + + // 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)); } + } - if (definitionIndex > 0) - _prevDefinition = _definitions[definitionIndex - 1]; + /// + /// 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(); - if (definitionIndex < _definitions.Count - 1) - _nextDefinition = _definitions[definitionIndex + 1]; + 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); + } } - private Orientation DetectOrientation() + /// + /// Move the splitter using the Keyboard (Don't show preview). + /// + private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange) { - if (!_grid.ColumnDefinitions.Any()) - return Orientation.Horizontal; - if (!_grid.RowDefinitions.Any()) - return Orientation.Vertical; + // If moving with the mouse, ignore keyboard motion. + if (_resizeData != null) + { + return false; // Don't handle the event. + } - 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) + // Don't show preview. + InitializeData(false); + + // Check that we are actually able to resize. + if (_resizeData == null) { - return Orientation.Vertical; + return false; // Don't handle the event. } - if (!width.IsAuto && height.IsAuto) + + MoveSplitter(horizontalChange, verticalChange); + + _resizeData = null; + + return true; + } + + /// + /// This adorner draws the preview for the . + /// It also positions the adorner. + /// + private sealed class PreviewAdorner : Decorator + { + private readonly TranslateTransform _translation; + private readonly Decorator _decorator; + + public PreviewAdorner(IControl previewControl) { - return Orientation.Horizontal; + // Add a decorator to perform translations. + _translation = new TranslateTransform(); + + _decorator = new Decorator + { + Child = previewControl, + RenderTransform = _translation + }; + + Child = _decorator; + } + + /// + /// The Preview's Offset in the X direction from the GridSplitter. + /// + public double OffsetX + { + get => _translation.X; + set => _translation.X = value; + } + + /// + /// The Preview's Offset in the Y direction from the GridSplitter. + /// + public double OffsetY + { + 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; } } } diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index 64349222ea..cfab5dab56 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -1,51 +1,21 @@ - - - - - - - - + + - - - - - - - + - + diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index a790d2fca1..15d62e9140 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,183 +19,246 @@ 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) - }); - 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(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) - }); + { + 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.DragCompletedEvent + }); + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } From bf04d22856d36ffeded87fedd211de21c36a66f3 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 19:49:20 +0100 Subject: [PATCH 02/14] Comment fixes. --- src/Avalonia.Controls/GridSplitter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index e5749c8ed9..3fc1ed3ea5 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -16,7 +16,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Enum to indicate whether GridSplitter resizes Columns or Rows. + /// Enum to indicate whether resizes Columns or Rows. /// public enum GridResizeDirection { @@ -38,7 +38,7 @@ namespace Avalonia.Controls } /// - /// Enum to indicate what Columns or Rows the GridSplitter resizes. + /// Enum to indicate what Columns or Rows the resizes. /// public enum GridResizeBehavior { From b315d5024f28d82f8eb51b0a46a86085d13d24d9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 21:42:59 +0100 Subject: [PATCH 03/14] Move enums lower. Add workaround for thumb raising multiple started events. --- src/Avalonia.Controls/GridSplitter.cs | 106 ++++++++++++++------------ 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 3fc1ed3ea5..56a28e15c2 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -16,55 +16,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// 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 - } - - /// - /// 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. /// public class GridSplitter : Thumb { @@ -427,7 +379,13 @@ namespace Avalonia.Controls { base.OnDragStarted(e); - Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + // 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); } @@ -845,4 +803,52 @@ namespace Avalonia.Controls 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 + } } From 1d7feade1b6b8dffbb83487a0f0c588988200467 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 23:32:20 +0100 Subject: [PATCH 04/14] Add keyboard input tests. --- .../GridSplitterTests.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index 15d62e9140..f2b6b0db4b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -262,5 +262,123 @@ namespace Avalonia.Controls.UnitTests 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)); + } } } From 6dd6f336da5509d517c3e0c1935ea322ca2e7f0e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 23:59:17 +0100 Subject: [PATCH 05/14] Restore original delta constraints algorithm. --- src/Avalonia.Controls/GridSplitter.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 56a28e15c2..a2fefa0548 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -554,22 +554,9 @@ namespace Avalonia.Controls definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength); } - if (_resizeData.SplitBehavior == SplitBehavior.Split) - { - // Determine the minimum and maximum the columns can be resized. - minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); - maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); - } - else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) - { - minDelta = definition1Min - definition1Len; - maxDelta = definition1Max - definition1Len; - } - else - { - minDelta = definition2Len - definition2Max; - maxDelta = definition2Len - definition2Min; - } + // Determine the minimum and maximum the columns can be resized. + minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); + maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); } /// From 83bfb8ec70837d260527b3724c68f4da0eef9243 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 11:07:08 +0100 Subject: [PATCH 06/14] Initial implementation of a font manager --- Avalonia.sln | 3 +- build/HarfBuzzSharp.props | 6 + build/SkiaSharp.props | 4 +- .../Presenters/TextPresenter.cs | 3 +- src/Avalonia.Controls/TextBlock.cs | 5 +- src/Avalonia.Visuals/Media/FontFamily.cs | 23 ++- src/Avalonia.Visuals/Media/FontManager.cs | 100 ++++++++++ .../Media/Fonts/FamilyNameCollection.cs | 6 +- src/Avalonia.Visuals/Media/FormattedText.cs | 47 ++++- src/Avalonia.Visuals/Media/GlyphTypeface.cs | 108 ++++++++++ src/Avalonia.Visuals/Media/Typeface.cs | 99 ++++++--- .../Platform/IFontManagerImpl.cs | 57 ++++++ .../Platform/IGlyphTypefaceImpl.cs | 89 +++++++++ .../Platform/IPlatformRenderInterface.cs | 7 +- .../Rendering/RendererBase.cs | 6 +- src/Skia/Avalonia.Skia/Avalonia.Skia.csproj | 1 + src/Skia/Avalonia.Skia/FontKey.cs | 40 ++++ src/Skia/Avalonia.Skia/FontManagerImpl.cs | 91 +++++++++ src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 48 ++--- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 179 +++++++++++++++++ .../Avalonia.Skia/PlatformRenderInterface.cs | 5 +- .../Avalonia.Skia/SKTypefaceCollection.cs | 91 ++------- .../SKTypefaceCollectionCache.cs | 8 +- src/Skia/Avalonia.Skia/SkiaPlatform.cs | 5 + src/Skia/Avalonia.Skia/TypefaceCache.cs | 86 ++------ .../Avalonia.Skia/TypefaceCollectionEntry.cs | 19 ++ .../Avalonia.Direct2D1.csproj | 1 + .../Avalonia.Direct2D1/Direct2D1Platform.cs | 17 +- .../Media/Direct2D1FontCollectionCache.cs | 49 +++-- .../Media/FontManagerImpl.cs | 80 ++++++++ .../Media/FormattedTextImpl.cs | 19 +- .../Media/GlyphTypefaceImpl.cs | 188 ++++++++++++++++++ .../FullLayoutTests.cs | 1 + .../Media/FormattedTextImplTests.cs | 3 +- .../MockPlatformRenderInterface.cs | 3 +- tests/Avalonia.UnitTests/TestServices.cs | 1 + .../Media/FontFamilyTests.cs | 44 +++- .../Media/TypefaceTests.cs | 14 +- .../VisualTree/MockRenderInterface.cs | 1 + 39 files changed, 1262 insertions(+), 295 deletions(-) create mode 100644 build/HarfBuzzSharp.props create mode 100644 src/Avalonia.Visuals/Media/FontManager.cs create mode 100644 src/Avalonia.Visuals/Media/GlyphTypeface.cs create mode 100644 src/Avalonia.Visuals/Platform/IFontManagerImpl.cs create mode 100644 src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs create mode 100644 src/Skia/Avalonia.Skia/FontKey.cs create mode 100644 src/Skia/Avalonia.Skia/FontManagerImpl.cs create mode 100644 src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs create mode 100644 src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs 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/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/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/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.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index a486723d86..665dfc1129 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 { + 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,13 @@ 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. /// 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 +183,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..e89471ede8 --- /dev/null +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -0,0 +1,100 @@ +// 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.Collections.Generic; +using System.Globalization; +using Avalonia.Platform; + +namespace Avalonia.Media +{ + public abstract class FontManager : IFontManagerImpl + { + public static readonly FontManager Default = CreateDefaultFontManger(); + + /// + public string DefaultFontFamilyName { get; protected set; } + + private static FontManager CreateDefaultFontManger() + { + var platformImpl = AvaloniaLocator.Current.GetService(); + + if(platformImpl == null) + { + return new EmptyFontManager(); + } + + return new PlatformFontManger(platformImpl); + } + + /// + public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + + /// + public abstract IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + + /// + public abstract Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + + /// + public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null); + + private class PlatformFontManger : FontManager + { + private readonly IFontManagerImpl _platformImpl; + + public PlatformFontManger(IFontManagerImpl platformImpl) + { + _platformImpl = platformImpl; + + DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; + } + + public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); + + public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) => _platformImpl.CreateGlyphTypeface(typeface); + + public override Typeface GetTypeface(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 + { + private readonly string[] _defaultFontFamilies = { "Arial" }; + + public EmptyFontManager() + { + DefaultFontFamilyName = "Arial"; + } + + public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + return _defaultFontFamilies; + } + + public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + throw new NotSupportedException(); + } + + public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + { + throw new NotSupportedException(); + } + + public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index acf0bbdb11..0c161131dc 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 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..1c959a86c5 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -0,0 +1,108 @@ +// 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 class GlyphTypeface : IDisposable + { + public GlyphTypeface(Typeface typeface) : this(FontManager.Default.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..2d3c7e6ffa 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}, Style = {Style}, Weight = {Weight}")] + 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..236631edde --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -0,0 +1,57 @@ +// 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); + + /// + /// Creates a glyph typeface for specified typeface. + /// + /// The typeface. + /// + /// The glyph typeface implementation. + /// + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + + /// + /// 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..619d3088b4 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, 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/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..6c67438533 --- /dev/null +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -0,0 +1,91 @@ +// 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.Concurrent; +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; + + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + + 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 IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } + + 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..78ff785bdc 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; diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..b9b5b07d7d --- /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 +{ + internal 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..e4ad0c1b24 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -21,8 +21,6 @@ namespace Avalonia.Skia private GRContext GrContext { get; } - public IEnumerable InstalledFontNames => SKFontManager.Default.FontFamilies; - public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) { if (customSkiaGpu != null) @@ -52,12 +50,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); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 17448127b0..8ec2a9c3f8 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)) : + null; } - 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..e76596e925 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -41,20 +41,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 +106,7 @@ namespace Avalonia.Direct2D1 { InitializeDirect2D(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -131,6 +118,7 @@ namespace Avalonia.Direct2D1 public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -139,6 +127,7 @@ namespace Avalonia.Direct2D1 return new FormattedTextImpl( text, typeface, + fontSize, textAlignment, wrapping, constraint, 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..de1a4cf2d1 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -0,0 +1,80 @@ +// 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.Concurrent; +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 + { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + + 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 IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } + + 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..66cf397110 --- /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 +{ + internal 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/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..b3f0af55f4 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, 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/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index d31210bc71..335cdf4597 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, From 9d105c7dbbb026cb7d3d362f96aa24cb70e3e9d2 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 12:01:02 +0100 Subject: [PATCH 07/14] Introduce a static FontManager that uses a platform implementation under the hood. --- src/Avalonia.Visuals/Media/FontFamily.cs | 7 +- src/Avalonia.Visuals/Media/FontManager.cs | 92 +++++-------------- src/Avalonia.Visuals/Media/GlyphTypeface.cs | 5 +- .../Platform/IFontManagerImpl.cs | 9 -- .../Platform/IPlatformRenderInterface.cs | 9 ++ src/Skia/Avalonia.Skia/FontManagerImpl.cs | 9 -- .../Avalonia.Skia/PlatformRenderInterface.cs | 9 ++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 8 ++ .../Media/FontManagerImpl.cs | 9 -- 9 files changed, 59 insertions(+), 98 deletions(-) diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index 665dfc1129..d263097e6a 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media { static FontFamily() { - Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + Default = new FontFamily(FontManager.DefaultFontFamilyName); } /// @@ -60,8 +60,11 @@ namespace Avalonia.Media /// /// 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 => - FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); + FontManager.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); /// /// Gets the primary family name of the font family. diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index e89471ede8..95d91e7df1 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -1,99 +1,55 @@ // 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.Collections.Generic; using System.Globalization; using Avalonia.Platform; namespace Avalonia.Media { - public abstract class FontManager : IFontManagerImpl + public static class FontManager { - public static readonly FontManager Default = CreateDefaultFontManger(); + private static readonly IFontManagerImpl s_platformImpl = GetPlatformImpl(); - /// - public string DefaultFontFamilyName { get; protected set; } + /// + public static string DefaultFontFamilyName => s_platformImpl.DefaultFontFamilyName; - private static FontManager CreateDefaultFontManger() - { - var platformImpl = AvaloniaLocator.Current.GetService(); - - if(platformImpl == null) - { - return new EmptyFontManager(); - } - - return new PlatformFontManger(platformImpl); - } + /// + public static IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + s_platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - /// - public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + /// + public static Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => + s_platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); - /// - public abstract IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); - - /// - public abstract Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); - - /// - public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + /// + public static Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); + FontFamily fontFamily = null, CultureInfo culture = null) => + s_platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); - private class PlatformFontManger : FontManager + private static IFontManagerImpl GetPlatformImpl() { - private readonly IFontManagerImpl _platformImpl; - - public PlatformFontManger(IFontManagerImpl platformImpl) - { - _platformImpl = platformImpl; - - DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - - public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) => _platformImpl.CreateGlyphTypeface(typeface); - - public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => - _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); + var platformImpl = AvaloniaLocator.Current.GetService(); - 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); + return platformImpl ?? new EmptyFontManagerImpl(); } - private class EmptyFontManager : FontManager + private class EmptyFontManagerImpl : IFontManagerImpl { - private readonly string[] _defaultFontFamilies = { "Arial" }; - - public EmptyFontManager() - { - DefaultFontFamilyName = "Arial"; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) - { - return _defaultFontFamilies; - } + public string DefaultFontFamilyName => "Arial"; - public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - throw new NotSupportedException(); - } + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => new[] { "Arial" }; - public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - throw new NotSupportedException(); + return new Typeface(fontFamily, fontWeight, fontStyle); } - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, FontFamily fontFamily = null, CultureInfo culture = null) { - throw new NotSupportedException(); + return null; } } } diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 1c959a86c5..cba7c8c795 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -9,7 +9,10 @@ namespace Avalonia.Media { public class GlyphTypeface : IDisposable { - public GlyphTypeface(Typeface typeface) : this(FontManager.Default.CreateGlyphTypeface(typeface)) + private static readonly IPlatformRenderInterface s_platformRenderInterface = + AvaloniaLocator.Current.GetService(); + + public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface)) { } diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 236631edde..254b5d07d1 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -20,15 +20,6 @@ namespace Avalonia.Platform /// IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); - /// - /// Creates a glyph typeface for specified typeface. - /// - /// The typeface. - /// - /// The glyph typeface implementation. - /// - IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); - /// /// Get a typeface from specified parameters. /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 619d3088b4..5a0a7b2f19 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -111,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/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 6c67438533..03de82178a 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; @@ -14,9 +13,6 @@ namespace Avalonia.Skia { private SKFontManager _skFontManager = SKFontManager.Default; - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - public FontManagerImpl() { DefaultFontFamilyName = SKTypeface.Default.FamilyName; @@ -34,11 +30,6 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); - } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index e4ad0c1b24..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,6 +18,9 @@ namespace Avalonia.Skia /// internal class PlatformRenderInterface : IPlatformRenderInterface { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + private readonly ICustomSkiaGpu _customSkiaGpu; private GRContext GrContext { get; } @@ -150,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/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index e76596e925..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; } @@ -190,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/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index de1a4cf2d1..94de397652 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; @@ -15,9 +14,6 @@ namespace Avalonia.Direct2D1.Media { internal class FontManagerImpl : IFontManagerImpl { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - public FontManagerImpl() { //ToDo: Implement a real lookup of the system's default font. @@ -40,11 +36,6 @@ namespace Avalonia.Direct2D1.Media return fontFamilies; } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); - } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { //ToDo: Implement caching. From 46d3a916527e791c0c16ba1ec99b39d828ffbd1a Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 12:08:54 +0100 Subject: [PATCH 08/14] Add missing mocks --- tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs | 5 +++++ .../VisualTree/MockRenderInterface.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index b3f0af55f4..187853283f 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -78,5 +78,10 @@ namespace Avalonia.UnitTests { throw new NotImplementedException(); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return Mock.Of(); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 335cdf4597..032b6582a9 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -52,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(); From 75142ce5fa71fc89991eeb4753352b50a6648eec Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 2 Nov 2019 21:43:01 +0100 Subject: [PATCH 09/14] Set grid splitter min width and height to avoid invisible splitters. Make splitter background lighter. --- src/Avalonia.Diagnostics/Views/TreePageView.xaml | 4 ++-- src/Avalonia.Themes.Default/GridSplitter.xaml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) 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.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index cfab5dab56..dc5cd002dc 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -2,7 +2,9 @@