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/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs
index 28b9b3a38f..a2fefa0548 100644
--- a/src/Avalonia.Controls/GridSplitter.cs
+++ b/src/Avalonia.Controls/GridSplitter.cs
@@ -1,210 +1,841 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
+// This source file is adapted from the Windows Presentation Foundation project.
+// (https://github.com/dotnet/wpf/)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
+using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Layout;
-using Avalonia.VisualTree;
+using Avalonia.Media;
+using Avalonia.Utilities;
namespace Avalonia.Controls
{
///
- /// Represents the control that redistributes space between columns or rows of a Grid control.
+ /// Represents the control that redistributes space between columns or rows of a control.
///
- ///
- /// Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext.
- ///
public class GridSplitter : Thumb
{
- private List _definitions;
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty ResizeDirectionProperty =
+ AvaloniaProperty.Register(nameof(ResizeDirection));
- private Grid _grid;
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty ResizeBehaviorProperty =
+ AvaloniaProperty.Register(nameof(ResizeBehavior));
- private DefinitionBase _nextDefinition;
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty ShowsPreviewProperty =
+ AvaloniaProperty.Register(nameof(ShowsPreview));
- private Orientation _orientation;
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty KeyboardIncrementProperty =
+ AvaloniaProperty.Register(nameof(KeyboardIncrement), 10d);
- private DefinitionBase _prevDefinition;
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty DragIncrementProperty =
+ AvaloniaProperty.Register(nameof(DragIncrement), 1d);
- private void GetDeltaConstraints(out double min, out double max)
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty> PreviewContentProperty =
+ AvaloniaProperty.Register>(nameof(PreviewContent));
+
+ private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast);
+ private static readonly Cursor s_rowSplitterCursor = new Cursor(StandardCursorType.SizeNorthSouth);
+
+ private ResizeData _resizeData;
+
+ ///
+ /// Indicates whether the Splitter resizes the Columns, Rows, or Both.
+ ///
+ public GridResizeDirection ResizeDirection
{
- var prevDefinitionLen = GetActualLength(_prevDefinition);
- var prevDefinitionMin = GetMinLength(_prevDefinition);
- var prevDefinitionMax = GetMaxLength(_prevDefinition);
+ get => GetValue(ResizeDirectionProperty);
+ set => SetValue(ResizeDirectionProperty, value);
+ }
- var nextDefinitionLen = GetActualLength(_nextDefinition);
- var nextDefinitionMin = GetMinLength(_nextDefinition);
- var nextDefinitionMax = GetMaxLength(_nextDefinition);
- // Determine the minimum and maximum the columns can be resized
- min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen);
- max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin);
+ ///
+ /// Indicates which Columns or Rows the Splitter resizes.
+ ///
+ public GridResizeBehavior ResizeBehavior
+ {
+ get => GetValue(ResizeBehaviorProperty);
+ set => SetValue(ResizeBehaviorProperty, value);
}
- protected override void OnDragDelta(VectorEventArgs e)
+ ///
+ /// Indicates whether to Preview the column resizing without updating layout.
+ ///
+ public bool ShowsPreview
{
- // WPF doesn't change anything when spliter is in the last row/column
- // but resizes the splitter row/column when it's the first one.
- // this is different, but more internally consistent.
- if (_prevDefinition == null || _nextDefinition == null)
- return;
+ get => GetValue(ShowsPreviewProperty);
+ set => SetValue(ShowsPreviewProperty, value);
+ }
+
+ ///
+ /// The Distance to move the splitter when pressing the keyboard arrow keys.
+ ///
+ public double KeyboardIncrement
+ {
+ get => GetValue(KeyboardIncrementProperty);
+ set => SetValue(KeyboardIncrementProperty, value);
+ }
+
+ ///
+ /// Restricts splitter to move a multiple of the specified units.
+ ///
+ public double DragIncrement
+ {
+ get => GetValue(DragIncrementProperty);
+ set => SetValue(DragIncrementProperty, value);
+ }
+
+ ///
+ /// Gets or sets content that will be shown when is enabled and user starts resize operation.
+ ///
+ public ITemplate PreviewContent
+ {
+ get => GetValue(PreviewContentProperty);
+ set => SetValue(PreviewContentProperty, value);
+ }
+
+ ///
+ /// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height.
+ ///
+ internal GridResizeDirection GetEffectiveResizeDirection()
+ {
+ GridResizeDirection direction = ResizeDirection;
+
+ if (direction != GridResizeDirection.Auto)
+ {
+ return direction;
+ }
+
+ // When HorizontalAlignment is Left, Right or Center, resize Columns.
+ if (HorizontalAlignment != HorizontalAlignment.Stretch)
+ {
+ direction = GridResizeDirection.Columns;
+ }
+ else if (VerticalAlignment != VerticalAlignment.Stretch)
+ {
+ direction = GridResizeDirection.Rows;
+ }
+ else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height.
+ {
+ direction = GridResizeDirection.Columns;
+ }
+ else
+ {
+ direction = GridResizeDirection.Rows;
+ }
- var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y;
- double max;
- double min;
- GetDeltaConstraints(out min, out max);
- delta = Math.Min(Math.Max(delta, min), max);
+ return direction;
+ }
- var prevIsStar = IsStar(_prevDefinition);
- var nextIsStar = IsStar(_nextDefinition);
+ ///
+ /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction.
+ ///
+ private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction)
+ {
+ GridResizeBehavior resizeBehavior = ResizeBehavior;
- if (prevIsStar && nextIsStar)
+ if (resizeBehavior == GridResizeBehavior.BasedOnAlignment)
{
- foreach (var definition in _definitions)
+ if (direction == GridResizeDirection.Columns)
{
- if (definition == _prevDefinition)
+ switch (HorizontalAlignment)
{
- SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta);
+ case HorizontalAlignment.Left:
+ resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
+ break;
+ case HorizontalAlignment.Right:
+ resizeBehavior = GridResizeBehavior.CurrentAndNext;
+ break;
+ default:
+ resizeBehavior = GridResizeBehavior.PreviousAndNext;
+ break;
}
- else if (definition == _nextDefinition)
+ }
+ else
+ {
+ switch (VerticalAlignment)
{
- SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+ case VerticalAlignment.Top:
+ resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
+ break;
+ case VerticalAlignment.Bottom:
+ resizeBehavior = GridResizeBehavior.CurrentAndNext;
+ break;
+ default:
+ resizeBehavior = GridResizeBehavior.PreviousAndNext;
+ break;
}
- else if (IsStar(definition))
+ }
+ }
+
+ return resizeBehavior;
+ }
+
+ ///
+ /// Removes preview adorner from the grid.
+ ///
+ private void RemovePreviewAdorner()
+ {
+ if (_resizeData.Adorner != null)
+ {
+ AdornerLayer layer = AdornerLayer.GetAdornerLayer(this);
+ layer.Children.Remove(_resizeData.Adorner);
+ }
+ }
+
+ ///
+ /// Initialize the data needed for resizing.
+ ///
+ private void InitializeData(bool showsPreview)
+ {
+ // If not in a grid or can't resize, do nothing.
+ if (Parent is Grid grid)
+ {
+ GridResizeDirection resizeDirection = GetEffectiveResizeDirection();
+
+ // Setup data used for resizing.
+ _resizeData = new ResizeData
+ {
+ Grid = grid,
+ ShowsPreview = showsPreview,
+ ResizeDirection = resizeDirection,
+ SplitterLength = Math.Min(Bounds.Width, Bounds.Height),
+ ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection)
+ };
+
+ // Store the rows and columns to resize on drag events.
+ if (!SetupDefinitionsToResize())
+ {
+ // Unable to resize, clear data.
+ _resizeData = null;
+ return;
+ }
+
+ // Setup the preview in the adorner if ShowsPreview is true.
+ SetupPreviewAdorner();
+ }
+ }
+
+ ///
+ /// Returns true if GridSplitter can resize rows/columns.
+ ///
+ private bool SetupDefinitionsToResize()
+ {
+ int gridSpan = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
+ Grid.ColumnSpanProperty :
+ Grid.RowSpanProperty);
+
+ if (gridSpan == 1)
+ {
+ var splitterIndex = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
+ Grid.ColumnProperty :
+ Grid.RowProperty);
+
+ // Select the columns based on behavior.
+ int index1, index2;
+
+ switch (_resizeData.ResizeBehavior)
+ {
+ case GridResizeBehavior.PreviousAndCurrent:
+ // Get current and previous.
+ index1 = splitterIndex - 1;
+ index2 = splitterIndex;
+ break;
+ case GridResizeBehavior.CurrentAndNext:
+ // Get current and next.
+ index1 = splitterIndex;
+ index2 = splitterIndex + 1;
+ break;
+ default: // GridResizeBehavior.PreviousAndNext.
+ // Get previous and next.
+ index1 = splitterIndex - 1;
+ index2 = splitterIndex + 1;
+ break;
+ }
+
+ // Get count of rows/columns in the resize direction.
+ int count = _resizeData.ResizeDirection == GridResizeDirection.Columns ?
+ _resizeData.Grid.ColumnDefinitions.Count :
+ _resizeData.Grid.RowDefinitions.Count;
+
+ if (index1 >= 0 && index2 < count)
+ {
+ _resizeData.SplitterIndex = splitterIndex;
+
+ _resizeData.Definition1Index = index1;
+ _resizeData.Definition1 = GetGridDefinition(_resizeData.Grid, index1, _resizeData.ResizeDirection);
+ _resizeData.OriginalDefinition1Length =
+ _resizeData.Definition1.UserSizeValueCache; // Save Size if user cancels.
+ _resizeData.OriginalDefinition1ActualLength = GetActualLength(_resizeData.Definition1);
+
+ _resizeData.Definition2Index = index2;
+ _resizeData.Definition2 = GetGridDefinition(_resizeData.Grid, index2, _resizeData.ResizeDirection);
+ _resizeData.OriginalDefinition2Length =
+ _resizeData.Definition2.UserSizeValueCache; // Save Size if user cancels.
+ _resizeData.OriginalDefinition2ActualLength = GetActualLength(_resizeData.Definition2);
+
+ // Determine how to resize the columns.
+ bool isStar1 = IsStar(_resizeData.Definition1);
+ bool isStar2 = IsStar(_resizeData.Definition2);
+
+ if (isStar1 && isStar2)
{
- SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars.
+ // If they are both stars, resize both.
+ _resizeData.SplitBehavior = SplitBehavior.Split;
}
+ else
+ {
+ // One column is fixed width, resize the first one that is fixed.
+ _resizeData.SplitBehavior = !isStar1 ? SplitBehavior.Resize1 : SplitBehavior.Resize2;
+ }
+
+ return true;
}
}
- else if (prevIsStar)
+
+ return false;
+ }
+
+ ///
+ /// Create the preview adorner and add it to the adorner layer.
+ ///
+ private void SetupPreviewAdorner()
+ {
+ if (_resizeData.ShowsPreview)
{
- SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+ // Get the adorner layer and add an adorner to it.
+ var adornerLayer = AdornerLayer.GetAdornerLayer(_resizeData.Grid);
+
+ var previewContent = PreviewContent;
+
+ // Can't display preview.
+ if (adornerLayer == null)
+ {
+ return;
+ }
+
+ IControl builtPreviewContent = previewContent?.Build();
+
+ _resizeData.Adorner = new PreviewAdorner(builtPreviewContent);
+
+ AdornerLayer.SetAdornedElement(_resizeData.Adorner, this);
+
+ adornerLayer.Children.Add(_resizeData.Adorner);
+
+ // Get constraints on preview's translation.
+ GetDeltaConstraints(out _resizeData.MinChange, out _resizeData.MaxChange);
}
- else if (nextIsStar)
+ }
+
+ protected override void OnPointerEnter(PointerEventArgs e)
+ {
+ base.OnPointerEnter(e);
+
+ GridResizeDirection direction = GetEffectiveResizeDirection();
+
+ switch (direction)
{
- SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
+ case GridResizeDirection.Columns:
+ Cursor = s_columnSplitterCursor;
+ break;
+ case GridResizeDirection.Rows:
+ Cursor = s_rowSplitterCursor;
+ break;
}
- else
+ }
+
+ protected override void OnLostFocus(RoutedEventArgs e)
+ {
+ base.OnLostFocus(e);
+
+ if (_resizeData != null)
{
- SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
- SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+ CancelResize();
}
}
- private double GetActualLength(DefinitionBase definition)
+ protected override void OnDragStarted(VectorEventArgs e)
{
- if (definition == null)
- return 0;
- var columnDefinition = definition as ColumnDefinition;
- return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
+ base.OnDragStarted(e);
+
+ // TODO: Looks like that sometimes thumb will raise multiple drag started events.
+ // Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called");
+
+ if (_resizeData != null)
+ {
+ return;
+ }
+
+ InitializeData(ShowsPreview);
}
- private double GetMinLength(DefinitionBase definition)
+ protected override void OnDragDelta(VectorEventArgs e)
{
- if (definition == null)
- return 0;
- var columnDefinition = definition as ColumnDefinition;
- return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight;
+ base.OnDragDelta(e);
+
+ if (_resizeData != null)
+ {
+ double horizontalChange = e.Vector.X;
+ double verticalChange = e.Vector.Y;
+
+ // Round change to nearest multiple of DragIncrement.
+ double dragIncrement = DragIncrement;
+ horizontalChange = Math.Round(horizontalChange / dragIncrement) * dragIncrement;
+ verticalChange = Math.Round(verticalChange / dragIncrement) * dragIncrement;
+
+ if (_resizeData.ShowsPreview)
+ {
+ // Set the Translation of the Adorner to the distance from the thumb.
+ if (_resizeData.ResizeDirection == GridResizeDirection.Columns)
+ {
+ _resizeData.Adorner.OffsetX = Math.Min(
+ Math.Max(horizontalChange, _resizeData.MinChange),
+ _resizeData.MaxChange);
+ }
+ else
+ {
+ _resizeData.Adorner.OffsetY = Math.Min(
+ Math.Max(verticalChange, _resizeData.MinChange),
+ _resizeData.MaxChange);
+ }
+ }
+ else
+ {
+ // Directly update the grid.
+ MoveSplitter(horizontalChange, verticalChange);
+ }
+ }
}
- private double GetMaxLength(DefinitionBase definition)
+ protected override void OnDragCompleted(VectorEventArgs e)
{
- if (definition == null)
- return 0;
- var columnDefinition = definition as ColumnDefinition;
- return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight;
+ base.OnDragCompleted(e);
+
+ if (_resizeData != null)
+ {
+ if (_resizeData.ShowsPreview)
+ {
+ // Update the grid.
+ MoveSplitter(_resizeData.Adorner.OffsetX, _resizeData.Adorner.OffsetY);
+ RemovePreviewAdorner();
+ }
+
+ _resizeData = null;
+ }
}
- private bool IsStar(DefinitionBase definition)
+ protected override void OnKeyDown(KeyEventArgs e)
{
- var columnDefinition = definition as ColumnDefinition;
- return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar;
+ Key key = e.Key;
+
+ switch (key)
+ {
+ case Key.Escape:
+ if (_resizeData != null)
+ {
+ CancelResize();
+ e.Handled = true;
+ }
+
+ break;
+
+ case Key.Left:
+ e.Handled = KeyboardMoveSplitter(-KeyboardIncrement, 0);
+ break;
+ case Key.Right:
+ e.Handled = KeyboardMoveSplitter(KeyboardIncrement, 0);
+ break;
+ case Key.Up:
+ e.Handled = KeyboardMoveSplitter(0, -KeyboardIncrement);
+ break;
+ case Key.Down:
+ e.Handled = KeyboardMoveSplitter(0, KeyboardIncrement);
+ break;
+ }
}
- private void SetLengthInStars(DefinitionBase definition, double value)
+ ///
+ /// Cancels the resize operation.
+ ///
+ private void CancelResize()
{
- var columnDefinition = definition as ColumnDefinition;
- if (columnDefinition != null)
+ // Restore original column/row lengths.
+ if (_resizeData.ShowsPreview)
{
- columnDefinition.Width = new GridLength(value, GridUnitType.Star);
+ RemovePreviewAdorner();
}
- else
+ else // Reset the columns/rows lengths to the saved values.
{
- ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star);
+ SetDefinitionLength(_resizeData.Definition1, _resizeData.OriginalDefinition1Length);
+ SetDefinitionLength(_resizeData.Definition2, _resizeData.OriginalDefinition2Length);
}
+
+ _resizeData = null;
+ }
+
+ ///
+ /// Returns true if the row/column has a star length.
+ ///
+ private static bool IsStar(DefinitionBase definition)
+ {
+ return definition.UserSizeValueCache.IsStar;
}
- private void SetLength(DefinitionBase definition, double value)
+ ///
+ /// Gets Column or Row definition at index from grid based on resize direction.
+ ///
+ private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction)
{
- var columnDefinition = definition as ColumnDefinition;
- if (columnDefinition != null)
+ return direction == GridResizeDirection.Columns ?
+ (DefinitionBase)grid.ColumnDefinitions[index] :
+ (DefinitionBase)grid.RowDefinitions[index];
+ }
+
+ ///
+ /// Retrieves the ActualWidth or ActualHeight of the definition depending on its type Column or Row.
+ ///
+ private double GetActualLength(DefinitionBase definition)
+ {
+ var column = definition as ColumnDefinition;
+
+ return column?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
+ }
+
+ ///
+ /// Gets Column or Row definition at index from grid based on resize direction.
+ ///
+ private static void SetDefinitionLength(DefinitionBase definition, GridLength length)
+ {
+ definition.SetValue(
+ definition is ColumnDefinition ? ColumnDefinition.WidthProperty : RowDefinition.HeightProperty, length);
+ }
+
+ ///
+ /// Get the minimum and maximum Delta can be given definition constraints (MinWidth/MaxWidth).
+ ///
+ private void GetDeltaConstraints(out double minDelta, out double maxDelta)
+ {
+ double definition1Len = GetActualLength(_resizeData.Definition1);
+ double definition1Min = _resizeData.Definition1.UserMinSizeValueCache;
+ double definition1Max = _resizeData.Definition1.UserMaxSizeValueCache;
+
+ double definition2Len = GetActualLength(_resizeData.Definition2);
+ double definition2Min = _resizeData.Definition2.UserMinSizeValueCache;
+ double definition2Max = _resizeData.Definition2.UserMaxSizeValueCache;
+
+ // Set MinWidths to be greater than width of splitter.
+ if (_resizeData.SplitterIndex == _resizeData.Definition1Index)
{
- columnDefinition.Width = new GridLength(value);
+ definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength);
}
- else
+ else if (_resizeData.SplitterIndex == _resizeData.Definition2Index)
{
- ((RowDefinition)definition).Height = new GridLength(value);
+ definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength);
}
+
+ // Determine the minimum and maximum the columns can be resized.
+ minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len);
+ maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min);
}
- protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ ///
+ /// Sets the length of definition1 and definition2.
+ ///
+ private void SetLengths(double definition1Pixels, double definition2Pixels)
{
- base.OnAttachedToVisualTree(e);
- _grid = this.GetVisualParent();
+ // For the case where both definition1 and 2 are stars, update all star values to match their current pixel values.
+ if (_resizeData.SplitBehavior == SplitBehavior.Split)
+ {
+ var definitions = _resizeData.ResizeDirection == GridResizeDirection.Columns ?
+ (IAvaloniaReadOnlyList)_resizeData.Grid.ColumnDefinitions :
+ (IAvaloniaReadOnlyList)_resizeData.Grid.RowDefinitions;
- _orientation = DetectOrientation();
+ var definitionsCount = definitions.Count;
+
+ for (var i = 0; i < definitionsCount; i++)
+ {
+ DefinitionBase definition = definitions[i];
- int definitionIndex; //row or col
- if (_orientation == Orientation.Vertical)
+ // For each definition, if it is a star, set is value to ActualLength in stars
+ // This makes 1 star == 1 pixel in length
+ if (i == _resizeData.Definition1Index)
+ {
+ SetDefinitionLength(definition, new GridLength(definition1Pixels, GridUnitType.Star));
+ }
+ else if (i == _resizeData.Definition2Index)
+ {
+ SetDefinitionLength(definition, new GridLength(definition2Pixels, GridUnitType.Star));
+ }
+ else if (IsStar(definition))
+ {
+ SetDefinitionLength(definition, new GridLength(GetActualLength(definition), GridUnitType.Star));
+ }
+ }
+ }
+ else if (_resizeData.SplitBehavior == SplitBehavior.Resize1)
{
- Cursor = new Cursor(StandardCursorType.SizeWestEast);
- _definitions = _grid.ColumnDefinitions.Cast().ToList();
- definitionIndex = GetValue(Grid.ColumnProperty);
- PseudoClasses.Add(":vertical");
+ SetDefinitionLength(_resizeData.Definition1, new GridLength(definition1Pixels));
}
else
{
- Cursor = new Cursor(StandardCursorType.SizeNorthSouth);
- definitionIndex = GetValue(Grid.RowProperty);
- _definitions = _grid.RowDefinitions.Cast().ToList();
- PseudoClasses.Add(":horizontal");
+ SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels));
+ }
+ }
+
+ ///
+ /// Move the splitter by the given Delta's in the horizontal and vertical directions.
+ ///
+ private void MoveSplitter(double horizontalChange, double verticalChange)
+ {
+ Debug.Assert(_resizeData != null, "_resizeData should not be null when calling MoveSplitter");
+
+ // Calculate the offset to adjust the splitter.
+ var delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange;
+
+ DefinitionBase definition1 = _resizeData.Definition1;
+ DefinitionBase definition2 = _resizeData.Definition2;
+
+ if (definition1 != null && definition2 != null)
+ {
+ double actualLength1 = GetActualLength(definition1);
+ double actualLength2 = GetActualLength(definition2);
+
+ // When splitting, Check to see if the total pixels spanned by the definitions
+ // is the same asbefore starting resize. If not cancel the drag
+ if (_resizeData.SplitBehavior == SplitBehavior.Split &&
+ !MathUtilities.AreClose(
+ actualLength1 + actualLength2,
+ _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength))
+ {
+ CancelResize();
+
+ return;
+ }
+
+ GetDeltaConstraints(out var min, out var max);
+
+ // Constrain Delta to Min/MaxWidth of columns
+ delta = Math.Min(Math.Max(delta, min), max);
+
+ double definition1LengthNew = actualLength1 + delta;
+ double definition2LengthNew = actualLength1 + actualLength2 - definition1LengthNew;
+
+ SetLengths(definition1LengthNew, definition2LengthNew);
}
+ }
- if (definitionIndex > 0)
- _prevDefinition = _definitions[definitionIndex - 1];
+ ///
+ /// Move the splitter using the Keyboard (Don't show preview).
+ ///
+ private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange)
+ {
+ // If moving with the mouse, ignore keyboard motion.
+ if (_resizeData != null)
+ {
+ return false; // Don't handle the event.
+ }
+
+ // Don't show preview.
+ InitializeData(false);
+
+ // Check that we are actually able to resize.
+ if (_resizeData == null)
+ {
+ return false; // Don't handle the event.
+ }
- if (definitionIndex < _definitions.Count - 1)
- _nextDefinition = _definitions[definitionIndex + 1];
+ MoveSplitter(horizontalChange, verticalChange);
+
+ _resizeData = null;
+
+ return true;
}
- private Orientation DetectOrientation()
+ ///
+ /// This adorner draws the preview for the .
+ /// It also positions the adorner.
+ ///
+ private sealed class PreviewAdorner : Decorator
{
- if (!_grid.ColumnDefinitions.Any())
- return Orientation.Horizontal;
- if (!_grid.RowDefinitions.Any())
- return Orientation.Vertical;
+ private readonly TranslateTransform _translation;
+ private readonly Decorator _decorator;
+
+ public PreviewAdorner(IControl previewControl)
+ {
+ // Add a decorator to perform translations.
+ _translation = new TranslateTransform();
+
+ _decorator = new Decorator
+ {
+ Child = previewControl,
+ RenderTransform = _translation
+ };
+
+ Child = _decorator;
+ }
- var col = GetValue(Grid.ColumnProperty);
- var row = GetValue(Grid.RowProperty);
- var width = _grid.ColumnDefinitions[col].Width;
- var height = _grid.RowDefinitions[row].Height;
- if (width.IsAuto && !height.IsAuto)
+ ///
+ /// The Preview's Offset in the X direction from the GridSplitter.
+ ///
+ public double OffsetX
{
- return Orientation.Vertical;
+ get => _translation.X;
+ set => _translation.X = value;
}
- if (!width.IsAuto && height.IsAuto)
+
+ ///
+ /// The Preview's Offset in the Y direction from the GridSplitter.
+ ///
+ public double OffsetY
{
- return Orientation.Horizontal;
+ get => _translation.Y;
+ set => _translation.Y = value;
}
- if (_grid.Children.OfType() // Decision based on other controls in the same column
- .Where(c => Grid.GetColumn(c) == col)
- .Any(c => c.GetType() != typeof(GridSplitter)))
+
+ protected override Size ArrangeOverride(Size finalSize)
{
- return Orientation.Horizontal;
+ // Adorners always get clipped to the owner control. In this case we want
+ // to constrain size to the splitter size but draw on top of the parent grid.
+ Clip = null;
+
+ return base.ArrangeOverride(finalSize);
}
- return Orientation.Vertical;
}
+
+ ///
+ /// has special Behavior when columns are fixed.
+ /// If the left column is fixed, splitter will only resize that column.
+ /// Else if the right column is fixed, splitter will only resize the right column.
+ ///
+ private enum SplitBehavior
+ {
+ ///
+ /// Both columns/rows are star lengths.
+ ///
+ Split,
+
+ ///
+ /// Resize 1 only.
+ ///
+ Resize1,
+
+ ///
+ /// Resize 2 only.
+ ///
+ Resize2
+ }
+
+ ///
+ /// Stores data during the resizing operation.
+ ///
+ private class ResizeData
+ {
+ public bool ShowsPreview;
+ public PreviewAdorner Adorner;
+
+ // The constraints to keep the Preview within valid ranges.
+ public double MinChange;
+ public double MaxChange;
+
+ // The grid to Resize.
+ public Grid Grid;
+
+ // Cache of Resize Direction and Behavior.
+ public GridResizeDirection ResizeDirection;
+ public GridResizeBehavior ResizeBehavior;
+
+ // The columns/rows to resize.
+ public DefinitionBase Definition1;
+ public DefinitionBase Definition2;
+
+ // Are the columns/rows star lengths.
+ public SplitBehavior SplitBehavior;
+
+ // The index of the splitter.
+ public int SplitterIndex;
+
+ // The indices of the columns/rows.
+ public int Definition1Index;
+ public int Definition2Index;
+
+ // The original lengths of Definition1 and Definition2 (to restore lengths if user cancels resize).
+ public GridLength OriginalDefinition1Length;
+ public GridLength OriginalDefinition2Length;
+ public double OriginalDefinition1ActualLength;
+ public double OriginalDefinition2ActualLength;
+
+ // The minimum of Width/Height of Splitter. Used to ensure splitter
+ // isn't hidden by resizing a row/column smaller than the splitter.
+ public double SplitterLength;
+ }
+ }
+
+ ///
+ /// Enum to indicate whether resizes Columns or Rows.
+ ///
+ public enum GridResizeDirection
+ {
+ ///
+ /// Determines whether to resize rows or columns based on its Alignment and
+ /// width compared to height.
+ ///
+ Auto,
+
+ ///
+ /// Resize columns when dragging Splitter.
+ ///
+ Columns,
+
+ ///
+ /// Resize rows when dragging Splitter.
+ ///
+ Rows
+ }
+
+ ///
+ /// Enum to indicate what Columns or Rows the resizes.
+ ///
+ public enum GridResizeBehavior
+ {
+ ///
+ /// Determine which columns or rows to resize based on its Alignment.
+ ///
+ BasedOnAlignment,
+
+ ///
+ /// Resize the current and next Columns or Rows.
+ ///
+ CurrentAndNext,
+
+ ///
+ /// Resize the previous and current Columns or Rows.
+ ///
+ PreviousAndCurrent,
+
+ ///
+ /// Resize the previous and next Columns or Rows.
+ ///
+ PreviousAndNext
}
}
diff --git a/src/Avalonia.Controls/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.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 64349222ea..dc5cd002dc 100644
--- a/src/Avalonia.Themes.Default/GridSplitter.xaml
+++ b/src/Avalonia.Themes.Default/GridSplitter.xaml
@@ -1,51 +1,23 @@
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
-
+
diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs
index a486723d86..b57b4a0ca8 100644
--- a/src/Avalonia.Visuals/Media/FontFamily.cs
+++ b/src/Avalonia.Visuals/Media/FontFamily.cs
@@ -5,12 +5,16 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.Fonts;
-using Avalonia.Platform;
namespace Avalonia.Media
{
- public class FontFamily
+ public sealed class FontFamily
{
+ static FontFamily()
+ {
+ Default = new FontFamily(FontManager.Default.DefaultFontFamilyName);
+ }
+
///
///
/// Initializes a new instance of the class.
@@ -30,9 +34,7 @@ namespace Avalonia.Media
{
if (string.IsNullOrEmpty(name))
{
- FamilyNames = new FamilyNameCollection(string.Empty);
-
- return;
+ throw new ArgumentNullException(nameof(name));
}
var fontFamilySegment = GetFontFamilyIdentifier(name);
@@ -53,13 +55,16 @@ namespace Avalonia.Media
///
/// Represents the default font family
///
- public static FontFamily Default => new FontFamily(string.Empty);
+ public static FontFamily Default { get; }
///
/// Represents all font families in the system. This can be an expensive call depending on platform implementation.
///
+ ///
+ /// Consider using the new instead.
+ ///
public static IEnumerable SystemFontFamilies =>
- AvaloniaLocator.Current.GetService().InstalledFontNames.Select(name => new FontFamily(name));
+ FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
///
/// Gets the primary family name of the font family.
@@ -181,7 +186,14 @@ namespace Avalonia.Media
{
var hash = (int)2186146271;
- hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
+ if (Key != null)
+ {
+ hash = (hash * 15768619) ^ Key.GetHashCode();
+ }
+ else
+ {
+ hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
+ }
if (Key != null)
{
diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs
new file mode 100644
index 0000000000..be1bd269ed
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/FontManager.cs
@@ -0,0 +1,112 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Platform;
+
+namespace Avalonia.Media
+{
+ ///
+ /// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts.
+ /// It is also responsible for the font fallback.
+ ///
+ public abstract class FontManager
+ {
+ public static readonly FontManager Default = CreateDefault();
+
+ ///
+ /// Gets the system's default font family's name.
+ ///
+ public string DefaultFontFamilyName
+ {
+ get;
+ protected set;
+ }
+
+ ///
+ /// Get all installed fonts in the system.
+ /// If true the font collection is updated.
+ ///
+ public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false);
+
+ ///
+ /// Get a cached typeface from specified parameters.
+ ///
+ /// The font family.
+ /// The font weight.
+ /// The font style.
+ ///
+ /// The cached typeface.
+ ///
+ public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
+
+ ///
+ /// Tries to match a specified character to a typeface that supports specified font properties.
+ /// Returns null if no fallback was found.
+ ///
+ /// The codepoint to match against.
+ /// The font weight.
+ /// The font style.
+ /// The font family. This is optional and used for fallback lookup.
+ /// The culture.
+ ///
+ /// The matched typeface.
+ ///
+ public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
+ FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null);
+
+ public static FontManager CreateDefault()
+ {
+ var platformImpl = AvaloniaLocator.Current.GetService();
+
+ if (platformImpl != null)
+ {
+ return new PlatformFontManager(platformImpl);
+ }
+
+ return new EmptyFontManager();
+ }
+
+ private class PlatformFontManager : FontManager
+ {
+ private readonly IFontManagerImpl _platformImpl;
+
+ public PlatformFontManager(IFontManagerImpl platformImpl)
+ {
+ _platformImpl = platformImpl;
+
+ DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName;
+ }
+
+ public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
+ _platformImpl.GetInstalledFontFamilyNames(checkForUpdates);
+
+ public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) =>
+ _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle);
+
+ public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
+ FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null) =>
+ _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture);
+ }
+
+ private class EmptyFontManager : FontManager
+ {
+ public EmptyFontManager()
+ {
+ DefaultFontFamilyName = "Empty";
+ }
+
+ public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
+ new[] { DefaultFontFamilyName };
+
+ public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle);
+
+ public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
+ FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null) => null;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
index acf0bbdb11..e777d93315 100644
--- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
+++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
@@ -9,7 +9,7 @@ using System.Text;
namespace Avalonia.Media.Fonts
{
- public class FamilyNameCollection : IEnumerable
+ public sealed class FamilyNameCollection : IReadOnlyList
{
///
/// Initializes a new instance of the class.
@@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts
return other.ToString().Equals(ToString());
}
+
+ public int Count => Names.Count;
+
+ public string this[int index] => Names[index];
}
}
diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs
index e20e03e296..5013f925b3 100644
--- a/src/Avalonia.Visuals/Media/FormattedText.cs
+++ b/src/Avalonia.Visuals/Media/FormattedText.cs
@@ -16,9 +16,10 @@ namespace Avalonia.Media
private IFormattedTextImpl _platformImpl;
private IReadOnlyList _spans;
private Typeface _typeface;
+ private double _fontSize;
private string _text;
private TextAlignment _textAlignment;
- private TextWrapping _wrapping;
+ private TextWrapping _textWrapping;
///
/// Initializes a new instance of the class.
@@ -37,6 +38,31 @@ namespace Avalonia.Media
_platform = platform;
}
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
+ TextWrapping textWrapping, Size constraint)
+ {
+ _text = text;
+
+ _typeface = typeface;
+
+ _fontSize = fontSize;
+
+ _textAlignment = textAlignment;
+
+ _textWrapping = textWrapping;
+
+ _constraint = constraint;
+ }
+
///
/// Gets the bounds of the text within the .
///
@@ -61,6 +87,16 @@ namespace Avalonia.Media
set => Set(ref _typeface, value);
}
+
+ ///
+ /// Gets or sets the font size.
+ ///
+ public double FontSize
+ {
+ get => _fontSize;
+ set => Set(ref _fontSize, value);
+ }
+
///
/// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text.
@@ -92,10 +128,10 @@ namespace Avalonia.Media
///
/// Gets or sets the text wrapping.
///
- public TextWrapping Wrapping
+ public TextWrapping TextWrapping
{
- get => _wrapping;
- set => Set(ref _wrapping, value);
+ get => _textWrapping;
+ set => Set(ref _textWrapping, value);
}
///
@@ -110,8 +146,9 @@ namespace Avalonia.Media
_platformImpl = _platform.CreateFormattedText(
_text,
_typeface,
+ _fontSize,
_textAlignment,
- _wrapping,
+ _textWrapping,
_constraint,
_spans);
}
diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
new file mode 100644
index 0000000000..3ba31f7e84
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
@@ -0,0 +1,111 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+using Avalonia.Platform;
+
+namespace Avalonia.Media
+{
+ public sealed class GlyphTypeface : IDisposable
+ {
+ private static readonly IPlatformRenderInterface s_platformRenderInterface =
+ AvaloniaLocator.Current.GetService();
+
+ public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface))
+ {
+ }
+
+ public GlyphTypeface(IGlyphTypefaceImpl platformImpl)
+ {
+ PlatformImpl = platformImpl;
+ }
+
+ public IGlyphTypefaceImpl PlatformImpl { get; }
+
+ ///
+ /// Gets the font design units per em.
+ ///
+ public short DesignEmHeight => PlatformImpl.DesignEmHeight;
+
+ ///
+ /// Gets the recommended distance above the baseline in design em size.
+ ///
+ public int Ascent => PlatformImpl.Ascent;
+
+ ///
+ /// Gets the recommended distance under the baseline in design em size.
+ ///
+ public int Descent => PlatformImpl.Descent;
+
+ ///
+ /// Gets the recommended additional space between two lines of text in design em size.
+ ///
+ public int LineGap => PlatformImpl.LineGap;
+
+ ///
+ /// Gets the recommended line height.
+ ///
+ public int LineHeight => Descent - Ascent + LineGap;
+
+ ///
+ /// Gets a value that indicates the distance of the underline from the baseline in design em size.
+ ///
+ public int UnderlinePosition => PlatformImpl.UnderlinePosition;
+
+ ///
+ /// Gets a value that indicates the thickness of the underline in design em size.
+ ///
+ public int UnderlineThickness => PlatformImpl.UnderlineThickness;
+
+ ///
+ /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
+ ///
+ public int StrikethroughPosition => PlatformImpl.StrikethroughPosition;
+
+ ///
+ /// Gets a value that indicates the thickness of the underline in design em size.
+ ///
+ public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
+
+ ///
+ /// Returns an glyph index for the specified codepoint.
+ ///
+ ///
+ /// Returns 0 if a glyph isn't found.
+ ///
+ /// The codepoint.
+ ///
+ /// A glyph index.
+ ///
+ public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint);
+
+ ///
+ /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0.
+ ///
+ /// The codepoints to map.
+ ///
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints) => PlatformImpl.GetGlyphs(codepoints);
+
+ ///
+ /// Returns the glyph advance for the specified glyph.
+ ///
+ /// The glyph.
+ ///
+ /// The advance.
+ ///
+ public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph);
+
+ ///
+ /// Returns an array of glyph advances in design em size.
+ ///
+ /// The glyph indices.
+ ///
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs) => PlatformImpl.GetGlyphAdvances(glyphs);
+
+ void IDisposable.Dispose()
+ {
+ PlatformImpl?.Dispose();
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs
index 37ac0953bf..a6d5c8a43c 100644
--- a/src/Avalonia.Visuals/Media/Typeface.cs
+++ b/src/Avalonia.Visuals/Media/Typeface.cs
@@ -1,39 +1,38 @@
-using System;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Diagnostics;
+using JetBrains.Annotations;
namespace Avalonia.Media
{
///
/// Represents a typeface.
///
- public class Typeface
+ [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")]
+ public class Typeface : IEquatable
{
public static readonly Typeface Default = new Typeface(FontFamily.Default);
+ private GlyphTypeface _glyphTypeface;
+
///
/// Initializes a new instance of the class.
///
/// The font family.
- /// The font size, in DIPs.
- /// The font style.
/// The font weight.
- public Typeface(
- FontFamily fontFamily,
- double fontSize = 12,
- FontStyle style = FontStyle.Normal,
- FontWeight weight = FontWeight.Normal)
+ /// The font style.
+ public Typeface([NotNull]FontFamily fontFamily,
+ FontWeight weight = FontWeight.Normal,
+ FontStyle style = FontStyle.Normal)
{
- if (fontSize <= 0)
- {
- throw new ArgumentException("Font size must be > 0.");
- }
-
if (weight <= 0)
{
throw new ArgumentException("Font weight must be > 0.");
}
FontFamily = fontFamily;
- FontSize = fontSize;
Style = style;
Weight = weight;
}
@@ -42,15 +41,12 @@ namespace Avalonia.Media
/// Initializes a new instance of the class.
///
/// The name of the font family.
- /// The font size, in DIPs.
/// The font style.
/// The font weight.
- public Typeface(
- string fontFamilyName,
- double fontSize = 12,
- FontStyle style = FontStyle.Normal,
- FontWeight weight = FontWeight.Normal)
- : this(new FontFamily(fontFamilyName), fontSize, style, weight)
+ public Typeface(string fontFamilyName,
+ FontWeight weight = FontWeight.Normal,
+ FontStyle style = FontStyle.Normal)
+ : this(new FontFamily(fontFamilyName), weight, style)
{
}
@@ -59,11 +55,6 @@ namespace Avalonia.Media
///
public FontFamily FontFamily { get; }
- ///
- /// Gets the size of the font in DIPs.
- ///
- public double FontSize { get; }
-
///
/// Gets the font style.
///
@@ -73,5 +64,59 @@ namespace Avalonia.Media
/// Gets the font weight.
///
public FontWeight Weight { get; }
+
+ ///
+ /// Gets the glyph typeface.
+ ///
+ ///
+ /// The glyph typeface.
+ ///
+ public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this));
+
+ public static bool operator !=(Typeface a, Typeface b)
+ {
+ return !(a == b);
+ }
+
+ public static bool operator ==(Typeface a, Typeface b)
+ {
+ if (ReferenceEquals(a, b))
+ {
+ return true;
+ }
+
+ return !(a is null) && a.Equals(b);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is Typeface typeface)
+ {
+ return Equals(typeface);
+ }
+
+ return false;
+ }
+
+ public bool Equals(Typeface other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0);
+ hashCode = (hashCode * 397) ^ (int)Style;
+ hashCode = (hashCode * 397) ^ (int)Weight;
+ return hashCode;
+ }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
new file mode 100644
index 0000000000..254b5d07d1
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
@@ -0,0 +1,48 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Media;
+
+namespace Avalonia.Platform
+{
+ public interface IFontManagerImpl
+ {
+ ///
+ /// Gets the system's default font family's name.
+ ///
+ string DefaultFontFamilyName { get; }
+
+ ///
+ /// Get all installed fonts in the system.
+ /// If true the font collection is updated.
+ ///
+ IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false);
+
+ ///
+ /// Get a typeface from specified parameters.
+ ///
+ /// The font family.
+ /// The font weight.
+ /// The font style.
+ ///
+ /// The typeface.
+ ///
+ Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
+
+ ///
+ /// Tries to match a specified character to a typeface that supports specified font properties.
+ ///
+ /// The codepoint to match against.
+ /// The font weight.
+ /// The font style.
+ /// The font family. This is optional and used for fallback lookup.
+ /// The culture.
+ ///
+ /// The typeface.
+ ///
+ Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null);
+ }
+}
diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
new file mode 100644
index 0000000000..8c043a5129
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
@@ -0,0 +1,89 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Platform
+{
+ public interface IGlyphTypefaceImpl : IDisposable
+ {
+ ///
+ /// Gets the font design units per em.
+ ///
+ short DesignEmHeight { get; }
+
+ ///
+ /// Gets the recommended distance above the baseline in design em size.
+ ///
+ int Ascent { get; }
+
+ ///
+ /// Gets the recommended distance under the baseline in design em size.
+ ///
+ int Descent { get; }
+
+ ///
+ /// Gets the recommended additional space between two lines of text in design em size.
+ ///
+ int LineGap { get; }
+
+ ///
+ /// Gets a value that indicates the distance of the underline from the baseline in design em size.
+ ///
+ int UnderlinePosition { get; }
+
+ ///
+ /// Gets a value that indicates the thickness of the underline in design em size.
+ ///
+ int UnderlineThickness { get; }
+
+ ///
+ /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
+ ///
+ int StrikethroughPosition { get; }
+
+ ///
+ /// Gets a value that indicates the thickness of the underline in design em size.
+ ///
+ int StrikethroughThickness { get; }
+
+ ///
+ /// Returns an glyph index for the specified codepoint.
+ ///
+ ///
+ /// Returns 0 if a glyph isn't found.
+ ///
+ /// The codepoint.
+ ///
+ /// A glyph index.
+ ///
+ ushort GetGlyph(uint codepoint);
+
+ ///
+ /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0.
+ ///
+ /// The codepoints to map.
+ ///
+ /// An array of glyph indices.
+ ///
+ ushort[] GetGlyphs(ReadOnlySpan codepoints);
+
+ ///
+ /// Returns the glyph advance for the specified glyph.
+ ///
+ /// The glyph.
+ ///
+ /// The advance.
+ ///
+ int GetGlyphAdvance(ushort glyph);
+
+ ///
+ /// Returns an array of glyph advances in design em size.
+ ///
+ /// The glyph indices.
+ ///
+ /// An array of glyph advances.
+ ///
+ int[] GetGlyphAdvances(ReadOnlySpan glyphs);
+ }
+}
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index 87db9251e1..5a0a7b2f19 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -13,16 +13,12 @@ namespace Avalonia.Platform
///
public interface IPlatformRenderInterface
{
- ///
- /// Get all installed fonts in the system
- ///
- IEnumerable InstalledFontNames { get; }
-
///
/// Creates a formatted text implementation.
///
/// The text.
/// The base typeface.
+ /// The font size.
/// The text alignment.
/// The text wrapping mode.
/// The text layout constraints.
@@ -31,6 +27,7 @@ namespace Avalonia.Platform
IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -114,5 +111,14 @@ namespace Avalonia.Platform
/// The number of bytes per row.
/// An .
IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride);
+
+ ///
+ /// Creates a glyph typeface for specified typeface.
+ ///
+ /// The typeface.
+ ///
+ /// The glyph typeface implementation.
+ ///
+ IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
}
}
diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs
index 7b10fc1212..e341f02901 100644
--- a/src/Avalonia.Visuals/Rendering/RendererBase.cs
+++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs
@@ -7,7 +7,8 @@ namespace Avalonia.Rendering
{
public class RendererBase
{
- private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18);
+ private static readonly Typeface s_fpsTypeface = new Typeface("Arial");
+ private static int s_fontSize = 18;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond;
private int _fps;
@@ -18,7 +19,8 @@ namespace Avalonia.Rendering
{
_fpsText = new FormattedText
{
- Typeface = s_fpsTypeface
+ Typeface = s_fpsTypeface,
+ FontSize = s_fontSize
};
}
diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
index 4f884cdf33..68da513528 100644
--- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
+++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
@@ -12,5 +12,6 @@
+
diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Skia/Avalonia.Skia/FontKey.cs
new file mode 100644
index 0000000000..bb3fe230c1
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/FontKey.cs
@@ -0,0 +1,40 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Skia
+{
+ internal readonly struct FontKey : IEquatable
+ {
+ public readonly FontStyle Style;
+ public readonly FontWeight Weight;
+
+ public FontKey(FontWeight weight, FontStyle style)
+ {
+ Style = style;
+ Weight = weight;
+ }
+
+ public override int GetHashCode()
+ {
+ var hash = 17;
+ hash = hash * 31 + (int)Style;
+ hash = hash * 31 + (int)Weight;
+
+ return hash;
+ }
+
+ public override bool Equals(object other)
+ {
+ return other is FontKey key && Equals(key);
+ }
+
+ public bool Equals(FontKey other)
+ {
+ return Style == other.Style &&
+ Weight == other.Weight;
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
new file mode 100644
index 0000000000..03de82178a
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
@@ -0,0 +1,82 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ internal class FontManagerImpl : IFontManagerImpl
+ {
+ private SKFontManager _skFontManager = SKFontManager.Default;
+
+ public FontManagerImpl()
+ {
+ DefaultFontFamilyName = SKTypeface.Default.FamilyName;
+ }
+
+ public string DefaultFontFamilyName { get; }
+
+ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ {
+ if (checkForUpdates)
+ {
+ _skFontManager = SKFontManager.CreateDefault();
+ }
+
+ return _skFontManager.FontFamilies;
+ }
+
+ public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
+ {
+ return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
+ }
+
+ public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null)
+ {
+ var fontFamilyName = FontFamily.Default.Name;
+
+ if (culture == null)
+ {
+ culture = CultureInfo.CurrentUICulture;
+ }
+
+ if (fontFamily != null)
+ {
+ foreach (var familyName in fontFamily.FamilyNames)
+ {
+ var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
+ SKFontStyleWidth.Normal,
+ (SKFontStyleSlant)fontStyle,
+ new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
+
+ if (skTypeface == null)
+ {
+ continue;
+ }
+
+ fontFamilyName = familyName;
+
+ break;
+ }
+ }
+ else
+ {
+ var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
+ (SKFontStyleSlant)fontStyle,
+ new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
+
+ if (skTypeface != null)
+ {
+ fontFamilyName = skTypeface.FamilyName;
+ }
+ }
+
+ return GetTypeface(fontFamilyName, fontWeight, fontStyle);
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index a5e56b1d7a..a9358cb458 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -18,6 +18,7 @@ namespace Avalonia.Skia
public FormattedTextImpl(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -28,47 +29,22 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
- SKTypeface skiaTypeface = null;
+ var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
- if (typeface.FontFamily.Key != null)
+ _paint = new SKPaint
{
- var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
- skiaTypeface = typefaces.GetTypeFace(typeface);
- }
- else
- {
- if (typeface.FontFamily.FamilyNames.HasFallbacks)
- {
- foreach (var familyName in typeface.FontFamily.FamilyNames)
- {
- skiaTypeface = TypefaceCache.GetTypeface(
- familyName,
- typeface.Style,
- typeface.Weight);
- if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break;
- }
- }
- else
- {
- skiaTypeface = TypefaceCache.GetTypeface(
- typeface.FontFamily.Name,
- typeface.Style,
- typeface.Weight);
- }
- }
-
- _paint = new SKPaint();
+ TextEncoding = SKTextEncoding.Utf16,
+ IsStroke = false,
+ IsAntialias = true,
+ LcdRenderText = true,
+ SubpixelText = true,
+ Typeface = entry.SKTypeface,
+ TextSize = (float)fontSize,
+ TextAlign = textAlignment.ToSKTextAlign()
+ };
//currently Skia does not measure properly with Utf8 !!!
//Paint.TextEncoding = SKTextEncoding.Utf8;
- _paint.TextEncoding = SKTextEncoding.Utf16;
- _paint.IsStroke = false;
- _paint.IsAntialias = true;
- _paint.LcdRenderText = true;
- _paint.SubpixelText = true;
- _paint.Typeface = skiaTypeface;
- _paint.TextSize = (float)typeface.FontSize;
- _paint.TextAlign = textAlignment.ToSKTextAlign();
_wrapping = wrapping;
_constraint = constraint;
@@ -118,7 +94,7 @@ namespace Avalonia.Skia
}
}
- if (!line.Equals(default))
+ if (!line.Equals(default(AvaloniaFormattedTextLine)))
{
var rects = GetRects();
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
new file mode 100644
index 0000000000..e46f766255
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -0,0 +1,179 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Media;
+using Avalonia.Platform;
+using HarfBuzzSharp;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ public class GlyphTypefaceImpl : IGlyphTypefaceImpl
+ {
+ private bool _isDisposed;
+
+ public GlyphTypefaceImpl(Typeface typeface)
+ {
+ Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface;
+
+ Face = new Face(GetTable)
+ {
+ UnitsPerEm = Typeface.UnitsPerEm
+ };
+
+ Font = new Font(Face);
+
+ Font.SetFunctionsOpenType();
+
+ Font.GetScale(out var xScale, out _);
+
+ DesignEmHeight = (short)xScale;
+
+ if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
+ {
+ Font.TryGetVerticalFontExtents(out fontExtents);
+ }
+
+ Ascent = -fontExtents.Ascender;
+
+ Descent = -fontExtents.Descender;
+
+ LineGap = fontExtents.LineGap;
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
+ {
+ UnderlinePosition = underlinePosition;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
+ {
+ UnderlineThickness = underlineThickness;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
+ {
+ StrikethroughPosition = strikethroughPosition;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
+ {
+ StrikethroughThickness = strikethroughThickness;
+ }
+ }
+
+ public Face Face { get; }
+
+ public Font Font { get; }
+
+ public SKTypeface Typeface { get; }
+
+ ///
+ public short DesignEmHeight { get; }
+
+ ///
+ public int Ascent { get; }
+
+ ///
+ public int Descent { get; }
+
+ ///
+ public int LineGap { get; }
+
+ //ToDo: Get these values from HarfBuzz
+ ///
+ public int UnderlinePosition { get; }
+
+ ///
+ public int UnderlineThickness { get; }
+
+ ///
+ public int StrikethroughPosition { get; }
+
+ ///
+ public int StrikethroughThickness { get; }
+
+ ///
+ public ushort GetGlyph(uint codepoint)
+ {
+ if (Font.TryGetGlyph(codepoint, out var glyph))
+ {
+ return (ushort)glyph;
+ }
+
+ return 0;
+ }
+
+ ///
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
+ {
+ var glyphs = new ushort[codepoints.Length];
+
+ for (var i = 0; i < codepoints.Length; i++)
+ {
+ if (Font.TryGetGlyph(codepoints[i], out var glyph))
+ {
+ glyphs[i] = (ushort)glyph;
+ }
+ }
+
+ return glyphs;
+ }
+
+ ///
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return Font.GetHorizontalGlyphAdvance(glyph);
+ }
+
+ ///
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
+ {
+ var glyphIndices = new uint[glyphs.Length];
+
+ for (var i = 0; i < glyphs.Length; i++)
+ {
+ glyphIndices[i] = glyphs[i];
+ }
+
+ return Font.GetHorizontalGlyphAdvances(glyphIndices);
+ }
+
+ private Blob GetTable(Face face, Tag tag)
+ {
+ var size = Typeface.GetTableSize(tag);
+
+ var data = Marshal.AllocCoTaskMem(size);
+
+ var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
+
+ return Typeface.TryGetTableData(tag, 0, size, data) ?
+ new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+
+ if (!disposing)
+ {
+ return;
+ }
+
+ Font?.Dispose();
+ Face?.Dispose();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 15f38b1c4f..ee0cfb2f06 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls.Platform.Surfaces;
@@ -17,12 +18,13 @@ namespace Avalonia.Skia
///
internal class PlatformRenderInterface : IPlatformRenderInterface
{
+ private readonly ConcurrentDictionary _glyphTypefaceCache =
+ new ConcurrentDictionary();
+
private readonly ICustomSkiaGpu _customSkiaGpu;
private GRContext GrContext { get; }
- public IEnumerable InstalledFontNames => SKFontManager.Default.FontFamilies;
-
public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
{
if (customSkiaGpu != null)
@@ -52,12 +54,13 @@ namespace Avalonia.Skia
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
IReadOnlyList spans)
{
- return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
+ return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans);
}
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
@@ -151,5 +154,10 @@ namespace Avalonia.Skia
{
return new WriteableBitmapImpl(size, dpi, format);
}
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
+ }
}
}
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
index 17448127b0..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..1bda5157a5 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls;
@@ -27,6 +28,8 @@ namespace Avalonia.Direct2D1
{
public class Direct2D1Platform : IPlatformRenderInterface
{
+ private readonly ConcurrentDictionary _glyphTypefaceCache =
+ new ConcurrentDictionary();
private static readonly Direct2D1Platform s_instance = new Direct2D1Platform();
public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; }
@@ -41,20 +44,6 @@ namespace Avalonia.Direct2D1
public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; }
- public IEnumerable InstalledFontNames
- {
- get
- {
- var cache = Direct2D1FontCollectionCache.s_installedFontCollection;
- var length = cache.FontFamilyCount;
- for (int i = 0; i < length; i++)
- {
- var names = cache.GetFontFamily(i).FamilyNames;
- yield return names.GetString(0);
- }
- }
- }
-
private static readonly object s_initLock = new object();
private static bool s_initialized = false;
@@ -120,6 +109,7 @@ namespace Avalonia.Direct2D1
{
InitializeDirect2D();
AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance);
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true;
}
@@ -131,6 +121,7 @@ namespace Avalonia.Direct2D1
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -139,6 +130,7 @@ namespace Avalonia.Direct2D1
return new FormattedTextImpl(
text,
typeface,
+ fontSize,
textAlignment,
wrapping,
constraint,
@@ -201,5 +193,10 @@ namespace Avalonia.Direct2D1
{
return new WicBitmapImpl(format, data, size, dpi, stride);
}
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
+ }
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
index d93a59d384..b455c4fbee 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
@@ -1,62 +1,61 @@
using System.Collections.Concurrent;
using Avalonia.Media;
using Avalonia.Media.Fonts;
+using SharpDX.DirectWrite;
+using FontFamily = Avalonia.Media.FontFamily;
+using FontStyle = SharpDX.DirectWrite.FontStyle;
+using FontWeight = SharpDX.DirectWrite.FontWeight;
namespace Avalonia.Direct2D1.Media
{
internal static class Direct2D1FontCollectionCache
{
- private static readonly ConcurrentDictionary s_cachedCollections;
- internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection;
+ private static readonly ConcurrentDictionary s_cachedCollections;
+ internal static readonly FontCollection InstalledFontCollection;
static Direct2D1FontCollectionCache()
{
- s_cachedCollections = new ConcurrentDictionary();
+ s_cachedCollections = new ConcurrentDictionary();
- s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false);
+ InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false);
}
- public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface)
+ public static Font GetFont(Typeface typeface)
{
var fontFamily = typeface.FontFamily;
var fontCollection = GetOrAddFontCollection(fontFamily);
- var fontFamilyName = FontFamily.Default.Name;
- // Should this be cached?
foreach (var familyName in fontFamily.FamilyNames)
{
- if (!fontCollection.FindFamilyName(familyName, out _))
+ if (fontCollection.FindFamilyName(familyName, out var index))
{
- continue;
+ return fontCollection.GetFontFamily(index).GetFirstMatchingFont(
+ (FontWeight)typeface.Weight,
+ FontStretch.Normal,
+ (FontStyle)typeface.Style);
}
-
- fontFamilyName = familyName;
-
- break;
}
- return new SharpDX.DirectWrite.TextFormat(
- Direct2D1Platform.DirectWriteFactory,
- fontFamilyName,
- fontCollection,
- (SharpDX.DirectWrite.FontWeight)typeface.Weight,
- (SharpDX.DirectWrite.FontStyle)typeface.Style,
- SharpDX.DirectWrite.FontStretch.Normal,
- (float)typeface.FontSize);
+ InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
+
+ return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
+ (FontWeight)typeface.Weight,
+ FontStretch.Normal,
+ (FontStyle)typeface.Style);
}
- private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily)
+ private static FontCollection GetOrAddFontCollection(FontFamily fontFamily)
{
- return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
+ return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
}
- private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key)
+ private static FontCollection CreateFontCollection(FontFamilyKey key)
{
var assets = FontFamilyLoader.LoadFontAssets(key);
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
- return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
+ return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
}
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
new file mode 100644
index 0000000000..94de397652
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
@@ -0,0 +1,71 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Platform;
+using SharpDX.DirectWrite;
+using FontFamily = Avalonia.Media.FontFamily;
+using FontStyle = Avalonia.Media.FontStyle;
+using FontWeight = Avalonia.Media.FontWeight;
+
+namespace Avalonia.Direct2D1.Media
+{
+ internal class FontManagerImpl : IFontManagerImpl
+ {
+ public FontManagerImpl()
+ {
+ //ToDo: Implement a real lookup of the system's default font.
+ DefaultFontFamilyName = "segoe ui";
+ }
+
+ public string DefaultFontFamilyName { get; }
+
+ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ {
+ var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
+
+ var fontFamilies = new string[familyCount];
+
+ for (var i = 0; i < familyCount; i++)
+ {
+ fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0);
+ }
+
+ return fontFamilies;
+ }
+
+ public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
+ {
+ //ToDo: Implement caching.
+ return new Typeface(fontFamily, fontWeight, fontStyle);
+ }
+
+ public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null)
+ {
+ var fontFamilyName = FontFamily.Default.Name;
+
+ var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
+
+ for (var i = 0; i < familyCount; i++)
+ {
+ var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i)
+ .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal,
+ (SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0);
+
+ if (!font.HasCharacter(codepoint))
+ {
+ continue;
+ }
+
+ fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
+
+ break;
+ }
+
+ return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
index b73deb1f0a..b1a177ad24 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
@@ -14,6 +14,7 @@ namespace Avalonia.Direct2D1.Media
public FormattedTextImpl(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -21,20 +22,20 @@ namespace Avalonia.Direct2D1.Media
{
Text = text;
- using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface))
+ using (var font = Direct2D1FontCollectionCache.GetFont(typeface))
+ using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory,
+ typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight,
+ (DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize))
{
textFormat.WordWrapping =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
TextLayout = new DWrite.TextLayout(
- Direct2D1Platform.DirectWriteFactory,
- Text ?? string.Empty,
- textFormat,
- (float)constraint.Width,
- (float)constraint.Height)
- {
- TextAlignment = textAlignment.ToDirect2D()
- };
+ Direct2D1Platform.DirectWriteFactory,
+ Text ?? string.Empty,
+ textFormat,
+ (float)constraint.Width,
+ (float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() };
}
if (spans != null)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
new file mode 100644
index 0000000000..32def01c39
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
@@ -0,0 +1,188 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+using Avalonia.Platform;
+using HarfBuzzSharp;
+using SharpDX.DirectWrite;
+
+namespace Avalonia.Direct2D1.Media
+{
+ public class GlyphTypefaceImpl : IGlyphTypefaceImpl
+ {
+ private bool _isDisposed;
+
+ public GlyphTypefaceImpl(Typeface typeface)
+ {
+ DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
+
+ FontFace = new FontFace(DWFont);
+
+ Face = new Face(GetTable);
+
+ Font = new HarfBuzzSharp.Font(Face);
+
+ Font.SetFunctionsOpenType();
+
+ Font.GetScale(out var xScale, out _);
+
+ DesignEmHeight = (short)xScale;
+
+ if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
+ {
+ Font.TryGetVerticalFontExtents(out fontExtents);
+ }
+
+ Ascent = -fontExtents.Ascender;
+
+ Descent = -fontExtents.Descender;
+
+ LineGap = fontExtents.LineGap;
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
+ {
+ UnderlinePosition = underlinePosition;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
+ {
+ UnderlineThickness = underlineThickness;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
+ {
+ StrikethroughPosition = strikethroughPosition;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
+ {
+ StrikethroughThickness = strikethroughThickness;
+ }
+ }
+
+ private Blob GetTable(Face face, Tag tag)
+ {
+ var dwTag = (int)SwapBytes(tag);
+
+ if (FontFace.TryGetFontTable(dwTag, out var tableData, out _))
+ {
+ return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { });
+ }
+
+ return null;
+ }
+
+ private static uint SwapBytes(uint x)
+ {
+ x = (x >> 16) | (x << 16);
+
+ return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8);
+ }
+
+ public SharpDX.DirectWrite.Font DWFont { get; }
+
+ public FontFace FontFace { get; }
+
+ public Face Face { get; }
+
+ public HarfBuzzSharp.Font Font { get; }
+
+ ///
+ public short DesignEmHeight { get; }
+
+ ///
+ public int Ascent { get; }
+
+ ///
+ public int Descent { get; }
+
+ ///
+ public int LineGap { get; }
+
+ //ToDo: Read font table for these values
+ ///
+ public int UnderlinePosition { get; }
+
+ ///
+ public int UnderlineThickness { get; }
+
+ ///
+ public int StrikethroughPosition { get; }
+
+ ///
+ public int StrikethroughThickness { get; }
+
+ ///
+ public ushort GetGlyph(uint codepoint)
+ {
+ if (Font.TryGetGlyph(codepoint, out var glyph))
+ {
+ return (ushort)glyph;
+ }
+
+ return 0;
+ }
+
+ ///
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
+ {
+ var glyphs = new ushort[codepoints.Length];
+
+ for (var i = 0; i < codepoints.Length; i++)
+ {
+ if (Font.TryGetGlyph(codepoints[i], out var glyph))
+ {
+ glyphs[i] = (ushort)glyph;
+ }
+ }
+
+ return glyphs;
+ }
+
+ ///
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return Font.GetHorizontalGlyphAdvance(glyph);
+ }
+
+ ///
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
+ {
+ var glyphIndices = new uint[glyphs.Length];
+
+ for (var i = 0; i < glyphs.Length; i++)
+ {
+ glyphIndices[i] = glyphs[i];
+ }
+
+ return Font.GetHorizontalGlyphAdvances(glyphIndices);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+
+ if (!disposing)
+ {
+ return;
+ }
+
+ Font?.Dispose();
+ Face?.Dispose();
+ FontFace?.Dispose();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
+
diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
index a790d2fca1..f2b6b0db4b 100644
--- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
@@ -2,9 +2,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.UnitTests;
-
using Moq;
-
using Xunit;
namespace Avalonia.Controls.UnitTests
@@ -21,185 +19,366 @@ namespace Avalonia.Controls.UnitTests
public void Detects_Horizontal_Orientation()
{
GridSplitter splitter;
- var grid = new Grid()
- {
- RowDefinitions = new RowDefinitions("*,Auto,*"),
- ColumnDefinitions = new ColumnDefinitions("*,*"),
- Children =
- {
- new Border { [Grid.RowProperty] = 0 },
- (splitter = new GridSplitter { [Grid.RowProperty] = 1 }),
- new Border { [Grid.RowProperty] = 2 }
- }
- };
+
+ var grid = new Grid
+ {
+ RowDefinitions = new RowDefinitions("*,Auto,*"),
+ ColumnDefinitions = new ColumnDefinitions("*,*"),
+ Children =
+ {
+ new Border { [Grid.RowProperty] = 0 },
+ (splitter = new GridSplitter { [Grid.RowProperty] = 1 }),
+ new Border { [Grid.RowProperty] = 2 }
+ }
+ };
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
- Assert.Contains(splitter.Classes, ":horizontal".Equals);
+ Assert.Equal(GridResizeDirection.Rows, splitter.GetEffectiveResizeDirection());
}
[Fact]
public void Detects_Vertical_Orientation()
{
GridSplitter splitter;
- var grid = new Grid()
- {
- ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
- RowDefinitions = new RowDefinitions("*,*"),
- Children =
- {
- new Border { [Grid.ColumnProperty] = 0 },
- (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}),
- new Border { [Grid.ColumnProperty] = 2 },
- }
- };
+
+ var grid = new Grid
+ {
+ ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
+ RowDefinitions = new RowDefinitions("*,*"),
+ Children =
+ {
+ new Border { [Grid.ColumnProperty] = 0 },
+ (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }),
+ new Border { [Grid.ColumnProperty] = 2 },
+ }
+ };
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
- Assert.Contains(splitter.Classes, ":vertical".Equals);
+ Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection());
}
[Fact]
public void Detects_With_Both_Auto()
{
GridSplitter splitter;
- var grid = new Grid()
- {
- ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
- RowDefinitions = new RowDefinitions("Auto,Auto"),
- Children =
- {
- new Border { [Grid.ColumnProperty] = 0 },
- (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}),
- new Border { [Grid.ColumnProperty] = 2 },
- }
- };
+
+ var grid = new Grid
+ {
+ ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
+ RowDefinitions = new RowDefinitions("Auto,Auto"),
+ Children =
+ {
+ new Border { [Grid.ColumnProperty] = 0 },
+ (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }),
+ new Border { [Grid.ColumnProperty] = 2 },
+ }
+ };
var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 300));
root.Arrange(new Rect(0, 0, 100, 300));
- Assert.Contains(splitter.Classes, ":vertical".Equals);
+ Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection());
}
[Fact]
- public void Horizontal_Stays_Within_Constraints()
+ public void In_First_Position_Doesnt_Throw_Exception()
+ {
+ GridSplitter splitter;
+ var grid = new Grid
+ {
+ ColumnDefinitions = new ColumnDefinitions("Auto,*,*"),
+ RowDefinitions = new RowDefinitions("*,*"),
+ Children =
+ {
+ (splitter = new GridSplitter { [Grid.ColumnProperty] = 0 }),
+ new Border { [Grid.ColumnProperty] = 1 },
+ new Border { [Grid.ColumnProperty] = 2 },
+ }
+ };
+
+ var root = new TestRoot { Child = grid };
+ root.Measure(new Size(100, 300));
+ root.Arrange(new Rect(0, 0, 100, 300));
+
+ splitter.RaiseEvent(
+ new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
+
+ splitter.RaiseEvent(new VectorEventArgs
+ {
+ RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(100, 1000)
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Horizontal_Stays_Within_Constraints(bool showsPreview)
{
var control1 = new Border { [Grid.RowProperty] = 0 };
- var splitter = new GridSplitter
- {
- [Grid.RowProperty] = 1,
- };
+ var splitter = new GridSplitter { [Grid.RowProperty] = 1, ShowsPreview = showsPreview};
var control2 = new Border { [Grid.RowProperty] = 2 };
- var rowDefinitions = new RowDefinitions()
- {
- new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 },
- new RowDefinition(GridLength.Auto),
- new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 },
- };
-
- var grid = new Grid()
- {
- RowDefinitions = rowDefinitions,
- Children =
- {
- control1, splitter, control2
- }
- };
+ var rowDefinitions = new RowDefinitions
+ {
+ new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 },
+ new RowDefinition(GridLength.Auto),
+ new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 },
+ };
+
+ var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } };
+
+ var root = new TestRoot
+ {
+ Child = new VisualLayerManager
+ {
+ Child = grid
+ }
+ };
- var root = new TestRoot { Child = grid };
root.Measure(new Size(100, 200));
root.Arrange(new Rect(0, 0, 100, 200));
+ splitter.RaiseEvent(
+ new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
+
splitter.RaiseEvent(new VectorEventArgs
- {
- RoutedEvent = Thumb.DragDeltaEvent,
- Vector = new Vector(0, -100)
- });
- Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star));
- Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star));
+ {
+ RoutedEvent = Thumb.DragDeltaEvent,
+ Vector = new Vector(0, -100)
+ });
+
+ if (showsPreview)
+ {
+ Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star));
+ Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star));
+ }
+ else
+ {
+ Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star));
+ Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star));
+ }
+
splitter.RaiseEvent(new VectorEventArgs
- {
- RoutedEvent = Thumb.DragDeltaEvent,
- Vector = new Vector(0, 100)
- });
- Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
- Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
- }
+ {
+ RoutedEvent = Thumb.DragDeltaEvent,
+ Vector = new Vector(0, 100)
+ });
- [Fact]
- public void In_First_Position_Doesnt_Throw_Exception()
- {
- GridSplitter splitter;
- var grid = new Grid()
- {
- ColumnDefinitions = new ColumnDefinitions("Auto,*,*"),
- RowDefinitions = new RowDefinitions("*,*"),
- Children =
- {
- (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ),
- new Border { [Grid.ColumnProperty] = 1 },
- new Border { [Grid.ColumnProperty] = 2 },
- }
- };
+ if (showsPreview)
+ {
+ Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star));
+ Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star));
+ }
+ else
+ {
+ Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
+ Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
+ }
- var root = new TestRoot { Child = grid };
- root.Measure(new Size(100, 300));
- root.Arrange(new Rect(0, 0, 100, 300));
splitter.RaiseEvent(new VectorEventArgs
- {
- RoutedEvent = Thumb.DragDeltaEvent,
- Vector = new Vector(100, 1000)
- });
+ {
+ RoutedEvent = Thumb.DragCompletedEvent
+ });
+
+ Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
+ Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
}
- [Fact]
- public void Vertical_Stays_Within_Constraints()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Vertical_Stays_Within_Constraints(bool showsPreview)
{
var control1 = new Border { [Grid.ColumnProperty] = 0 };
- var splitter = new GridSplitter
- {
- [Grid.ColumnProperty] = 1,
- };
+ var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, ShowsPreview = showsPreview};
var control2 = new Border { [Grid.ColumnProperty] = 2 };
- var columnDefinitions = new ColumnDefinitions()
- {
- new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 },
- new ColumnDefinition(GridLength.Auto),
- new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 },
- };
-
- var grid = new Grid()
- {
- ColumnDefinitions = columnDefinitions,
- Children =
- {
- control1, splitter, control2
- }
- };
+ var columnDefinitions = new ColumnDefinitions
+ {
+ new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 },
+ new ColumnDefinition(GridLength.Auto),
+ new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 },
+ };
- var root = new TestRoot { Child = grid };
+ var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } };
+
+ var root = new TestRoot
+ {
+ Child = new VisualLayerManager
+ {
+ Child = grid
+ }
+ };
root.Measure(new Size(200, 100));
root.Arrange(new Rect(0, 0, 200, 100));
+ splitter.RaiseEvent(
+ new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
+
+ splitter.RaiseEvent(new VectorEventArgs
+ {
+ RoutedEvent = Thumb.DragDeltaEvent,
+ Vector = new Vector(-100, 0)
+ });
+
+ if (showsPreview)
+ {
+ Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star));
+ Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star));
+ }
+ else
+ {
+ Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star));
+ Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star));
+ }
+
splitter.RaiseEvent(new VectorEventArgs
- {
- RoutedEvent = Thumb.DragDeltaEvent,
- Vector = new Vector(-100, 0)
- });
- Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star));
- Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star));
+ {
+ RoutedEvent = Thumb.DragDeltaEvent,
+ Vector = new Vector(100, 0)
+ });
+
+ if (showsPreview)
+ {
+ Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star));
+ Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star));
+ }
+ else
+ {
+ Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star));
+ Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star));
+ }
+
splitter.RaiseEvent(new VectorEventArgs
- {
- RoutedEvent = Thumb.DragDeltaEvent,
- Vector = new Vector(100, 0)
- });
+ {
+ RoutedEvent = Thumb.DragCompletedEvent
+ });
+
Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star));
Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star));
}
+
+ [Theory]
+ [InlineData(Key.Up, 90, 110)]
+ [InlineData(Key.Down, 110, 90)]
+ public void Vertical_Keyboard_Input_Can_Move_Splitter(Key key, double expectedHeightFirst, double expectedHeightSecond)
+ {
+ var control1 = new Border { [Grid.RowProperty] = 0 };
+ var splitter = new GridSplitter { [Grid.RowProperty] = 1, KeyboardIncrement = 10d };
+ var control2 = new Border { [Grid.RowProperty] = 2 };
+
+ var rowDefinitions = new RowDefinitions
+ {
+ new RowDefinition(1, GridUnitType.Star),
+ new RowDefinition(GridLength.Auto),
+ new RowDefinition(1, GridUnitType.Star)
+ };
+
+ var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } };
+
+ var root = new TestRoot
+ {
+ Child = grid
+ };
+
+ root.Measure(new Size(200, 200));
+ root.Arrange(new Rect(0, 0, 200, 200));
+
+ splitter.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = key
+ });
+
+ Assert.Equal(rowDefinitions[0].Height, new GridLength(expectedHeightFirst, GridUnitType.Star));
+ Assert.Equal(rowDefinitions[2].Height, new GridLength(expectedHeightSecond, GridUnitType.Star));
+ }
+
+ [Theory]
+ [InlineData(Key.Left, 90, 110)]
+ [InlineData(Key.Right, 110, 90)]
+ public void Horizontal_Keyboard_Input_Can_Move_Splitter(Key key, double expectedWidthFirst, double expectedWidthSecond)
+ {
+ var control1 = new Border { [Grid.ColumnProperty] = 0 };
+ var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d };
+ var control2 = new Border { [Grid.ColumnProperty] = 2 };
+
+ var columnDefinitions = new ColumnDefinitions
+ {
+ new ColumnDefinition(1, GridUnitType.Star),
+ new ColumnDefinition(GridLength.Auto),
+ new ColumnDefinition(1, GridUnitType.Star)
+ };
+
+ var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } };
+
+ var root = new TestRoot
+ {
+ Child = grid
+ };
+
+ root.Measure(new Size(200, 200));
+ root.Arrange(new Rect(0, 0, 200, 200));
+
+ splitter.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = key
+ });
+
+ Assert.Equal(columnDefinitions[0].Width, new GridLength(expectedWidthFirst, GridUnitType.Star));
+ Assert.Equal(columnDefinitions[2].Width, new GridLength(expectedWidthSecond, GridUnitType.Star));
+ }
+
+ [Fact]
+ public void Pressing_Escape_Key_Cancels_Resizing()
+ {
+ var control1 = new Border { [Grid.ColumnProperty] = 0 };
+ var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d };
+ var control2 = new Border { [Grid.ColumnProperty] = 2 };
+
+ var columnDefinitions = new ColumnDefinitions
+ {
+ new ColumnDefinition(1, GridUnitType.Star),
+ new ColumnDefinition(GridLength.Auto),
+ new ColumnDefinition(1, GridUnitType.Star)
+ };
+
+ var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } };
+
+ var root = new TestRoot
+ {
+ Child = grid
+ };
+
+ root.Measure(new Size(200, 200));
+ root.Arrange(new Rect(0, 0, 200, 200));
+
+ splitter.RaiseEvent(
+ new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
+
+ splitter.RaiseEvent(new VectorEventArgs
+ {
+ RoutedEvent = Thumb.DragDeltaEvent,
+ Vector = new Vector(-100, 0)
+ });
+
+ Assert.Equal(columnDefinitions[0].Width, new GridLength(0, GridUnitType.Star));
+ Assert.Equal(columnDefinitions[2].Width, new GridLength(200, GridUnitType.Star));
+
+ splitter.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = Key.Escape
+ });
+
+ Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star));
+ Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star));
+ }
}
}
diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
index 6cf38b6121..a683e5cfca 100644
--- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
+++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
@@ -175,6 +175,7 @@ namespace Avalonia.Layout.UnitTests
x.CreateFormattedText(
It.IsAny(),
It.IsAny(),
+ It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny(),
diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
index 353123ab2a..bca34dd69d 100644
--- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
+++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
@@ -53,7 +53,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var r = AvaloniaLocator.Current.GetService();
return r.CreateFormattedText(text,
- new Typeface(fontFamily, fontSize, fontStyle, fontWeight),
+ new Typeface(fontFamily, fontWeight, fontStyle),
+ fontSize,
textAlignment,
wrapping,
widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity),
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index a3cc3dec17..187853283f 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -9,11 +9,10 @@ namespace Avalonia.UnitTests
{
public class MockPlatformRenderInterface : IPlatformRenderInterface
{
- public IEnumerable InstalledFontNames => new string[0];
-
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -79,5 +78,10 @@ namespace Avalonia.UnitTests
{
throw new NotImplementedException();
}
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ return Mock.Of();
+ }
}
}
diff --git a/tests/Avalonia.UnitTests/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..032b6582a9 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
@@ -13,6 +13,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -51,6 +52,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ throw new NotImplementedException();
+ }
+
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt)
{
throw new NotImplementedException();