diff --git a/readme.md b/readme.md index 6a04c7e31e..19a9a8420d 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,7 @@ To see the status of some of our features, please see our [Roadmap](https://gith ## 🚀 Getting Started -The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). +The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index d812818ed8..fa4fd7dd07 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -56,6 +56,7 @@ + diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml b/samples/ControlCatalog/Pages/RelativePanelPage.axaml new file mode 100644 index 0000000000..3657d52bd9 --- /dev/null +++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs new file mode 100644 index 0000000000..11d0a5152e --- /dev/null +++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class RelativePanelPage : UserControl + { + public RelativePanelPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 09f86f462c..6e534bbb2a 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -273,7 +273,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Constraint = constraint, - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, @@ -490,7 +490,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index dd33023e38..89f672deaa 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -97,9 +97,7 @@ namespace Avalonia.Controls.Primitives { var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1]; - var offsetX = lastLine.LineMetrics.BaselineOrigin.X; - - var lineX = offsetX + lastLine.LineMetrics.Size.Width; + var lineX = lastLine.LineMetrics.Size.Width; var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height; @@ -117,7 +115,7 @@ namespace Avalonia.Controls.Primitives continue; } - var currentX = textLine.LineMetrics.BaselineOrigin.X; + var currentX = 0.0; foreach (var textRun in textLine.TextRuns) { diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs new file mode 100644 index 0000000000..f93de5ca15 --- /dev/null +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -0,0 +1,546 @@ +using Avalonia.Layout; + +#nullable enable + +namespace Avalonia.Controls +{ + public partial class RelativePanel + { + private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + if (d is Layoutable layoutable && layoutable.Parent is Layoutable layoutableParent) + { + layoutableParent.InvalidateArrange(); + } + } + + static RelativePanel() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + + AboveProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignBottomWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignBottomWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignHorizontalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignLeftWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignLeftWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignRightWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignRightWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignTopWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignTopWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + } + + /// + /// Gets the value of the RelativePanel.Above XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.Above XAML attached property value of the specified object. + /// (The element to position this element above.) + /// + [ResolveByName] + public static object GetAbove(AvaloniaObject obj) + { + return (object)obj.GetValue(AboveProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element above.) + public static void SetAbove(AvaloniaObject obj, object value) + { + obj.SetValue(AboveProperty, value); + } + + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AboveProperty = + AvaloniaProperty.RegisterAttached("Above", typeof(RelativePanel)); + + + /// + /// Gets the value of the RelativePanel.AlignBottomWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignBottomWithPanel XAML attached property value of the specified + /// object. (true to align this element's bottom edge with the panel's bottom edge; + /// otherwise, false.) + /// + public static bool GetAlignBottomWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignBottomWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's bottom edge with the panel's + /// bottom edge; otherwise, false.) + /// + public static void SetAlignBottomWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignBottomWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignBottomWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignBottomWithPanel", typeof(RelativePanel)); + + /// + /// Gets the value of the RelativePanel.AlignBottomWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignBottomWith XAML attached property value of the specified object. + /// (The element to align this element's bottom edge with.) + /// + [ResolveByName] + public static object GetAlignBottomWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignBottomWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's bottom edge with.) + public static void SetAlignBottomWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignBottomWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignBottomWithProperty = + AvaloniaProperty.RegisterAttached("AlignBottomWith", typeof(RelativePanel)); + + /// + /// Gets the value of the RelativePanel.AlignHorizontalCenterWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignHorizontalCenterWithPanel XAML attached property value + /// of the specified object. (true to horizontally center this element in the panel; + /// otherwise, false.) + /// + public static bool GetAlignHorizontalCenterWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignHorizontalCenterWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to horizontally center this element in the panel; otherwise, + /// false.) + /// + public static void SetAlignHorizontalCenterWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignHorizontalCenterWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignHorizontalCenterWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignHorizontalCenterWithPanel", typeof(RelativePanel), false); + + /// + /// Gets the value of the RelativePanel.AlignHorizontalCenterWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignHorizontalCenterWith XAML attached property value of the + /// specified object. (The element to align this element's horizontal center with.) + /// + [ResolveByName] + public static object GetAlignHorizontalCenterWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignHorizontalCenterWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's horizontal center with.) + public static void SetAlignHorizontalCenterWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignHorizontalCenterWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignHorizontalCenterWithProperty = + AvaloniaProperty.RegisterAttached("AlignHorizontalCenterWith", typeof(object), typeof(RelativePanel)); + + /// + /// Gets the value of the RelativePanel.AlignLeftWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignLeftWithPanel XAML attached property value of the specified + /// object. (true to align this element's left edge with the panel's left edge; otherwise, + /// false.) + /// + public static bool GetAlignLeftWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignLeftWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's left edge with the panel's left + /// edge; otherwise, false.) + /// + public static void SetAlignLeftWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignLeftWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignLeftWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignLeftWithPanel", typeof(RelativePanel), false); + + + /// + /// Gets the value of the RelativePanel.AlignLeftWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignLeftWith XAML attached property value of the specified + /// object. (The element to align this element's left edge with.) + /// + [ResolveByName] + public static object GetAlignLeftWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignLeftWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's left edge with.) + public static void SetAlignLeftWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignLeftWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignLeftWithProperty = + AvaloniaProperty.RegisterAttached("AlignLeftWith"); + + + /// + /// Gets the value of the RelativePanel.AlignRightWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignRightWithPanel XAML attached property value of the specified + /// object. (true to align this element's right edge with the panel's right edge; + /// otherwise, false.) + /// + public static bool GetAlignRightWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignRightWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's right edge with the panel's right + /// edge; otherwise, false.) + /// + public static void SetAlignRightWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignRightWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignRightWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignRightWithPanel", false); + + /// + /// Gets the value of the RelativePanel.AlignRightWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignRightWith XAML attached property value of the specified + /// object. (The element to align this element's right edge with.) + /// + [ResolveByName] + public static object GetAlignRightWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignRightWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignRightWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's right edge with.) + public static void SetAlignRightWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignRightWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignRightWithProperty = + AvaloniaProperty.RegisterAttached("AlignRightWith"); + + /// + /// Gets the value of the RelativePanel.AlignTopWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignTopWithPanel XAML attached property value of the specified + /// object. (true to align this element's top edge with the panel's top edge; otherwise, + /// false.) + /// + public static bool GetAlignTopWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignTopWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignTopWithPanel XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's top edge with the panel's top + /// edge; otherwise, false.) + /// + public static void SetAlignTopWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignTopWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignTopWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignTopWithPanel", false); + + /// + /// Gets the value of the RelativePanel.AlignTopWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// The value to set. (The element to align this element's top edge with.) + [ResolveByName] + public static object GetAlignTopWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignTopWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignTopWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's top edge with.) + public static void SetAlignTopWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignTopWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty AlignTopWithProperty = + AvaloniaProperty.RegisterAttached("AlignTopWith"); + + /// + /// Gets the value of the RelativePanel.AlignVerticalCenterWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignVerticalCenterWithPanel XAML attached property value of + /// the specified object. (true to vertically center this element in the panel; otherwise, + /// false.) + /// + public static bool GetAlignVerticalCenterWithPanel(AvaloniaObject obj) + { + return (bool)obj.GetValue(AlignVerticalCenterWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignVerticalCenterWithPanel XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to vertically center this element in the panel; otherwise, + /// false.) + /// + public static void SetAlignVerticalCenterWithPanel(AvaloniaObject obj, bool value) + { + obj.SetValue(AlignVerticalCenterWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignVerticalCenterWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignVerticalCenterWithPanel", false); + + /// + /// Gets the value of the RelativePanel.AlignVerticalCenterWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// The value to set. (The element to align this element's vertical center with.) + [ResolveByName] + public static object GetAlignVerticalCenterWith(AvaloniaObject obj) + { + return (object)obj.GetValue(AlignVerticalCenterWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignVerticalCenterWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's horizontal center with.) + + public static void SetAlignVerticalCenterWith(AvaloniaObject obj, object value) + { + obj.SetValue(AlignVerticalCenterWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly AttachedProperty AlignVerticalCenterWithProperty = + AvaloniaProperty.RegisterAttached("AlignVerticalCenterWith"); + + /// + /// Gets the value of the RelativePanel.Below XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.Below XAML attached property value of the specified object. + /// (The element to position this element below.) + /// + [ResolveByName] + public static object GetBelow(AvaloniaObject obj) + { + return (object)obj.GetValue(BelowProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element below.) + + public static void SetBelow(AvaloniaObject obj, object value) + { + obj.SetValue(BelowProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty BelowProperty = + AvaloniaProperty.RegisterAttached("Below"); + + /// + /// Gets the value of the RelativePanel.LeftOf XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.LeftOf XAML attached property value of the specified object. + /// (The element to position this element to the left of.) + /// + [ResolveByName] + public static object GetLeftOf(AvaloniaObject obj) + { + return (object)obj.GetValue(LeftOfProperty); + } + + /// + /// Sets the value of the RelativePanel.LeftOf XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element to the left of.) + public static void SetLeftOf(AvaloniaObject obj, object value) + { + obj.SetValue(LeftOfProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty LeftOfProperty = + AvaloniaProperty.RegisterAttached("LeftOf"); + + /// + /// Gets the value of the RelativePanel.RightOf XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.RightOf XAML attached property value of the specified object. + /// (The element to position this element to the right of.) + /// + [ResolveByName] + public static object GetRightOf(AvaloniaObject obj) + { + return (object)obj.GetValue(RightOfProperty); + } + + /// + /// Sets the value of the RelativePanel.RightOf XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element to the right of.) + public static void SetRightOf(AvaloniaObject obj, object value) + { + obj.SetValue(RightOfProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + + public static readonly AttachedProperty RightOfProperty = + AvaloniaProperty.RegisterAttached("RightOf"); + } +} diff --git a/src/Avalonia.Controls/RelativePanel.cs b/src/Avalonia.Controls/RelativePanel.cs new file mode 100644 index 0000000000..033a5559f5 --- /dev/null +++ b/src/Avalonia.Controls/RelativePanel.cs @@ -0,0 +1,353 @@ +/// Ported from https://github.com/HandyOrg/HandyControl/blob/master/src/Shared/HandyControl_Shared/Controls/Panel/RelativePanel.cs +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Layout; + +#nullable enable + +namespace Avalonia.Controls +{ + public partial class RelativePanel : Panel + { + private readonly Graph _childGraph; + + public RelativePanel() => _childGraph = new Graph(); + + protected override Size MeasureOverride(Size availableSize) + { + foreach (var child in Children) + { + child?.Measure(availableSize); + } + + return availableSize; + } + + protected override Size ArrangeOverride(Size arrangeSize) + { + _childGraph.Reset(arrangeSize); + + foreach (var child in Children.OfType()) + { + if (child == null) + continue; + + var node = _childGraph.AddNode(child); + + node.AlignLeftWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignLeftWithProperty, child)); + node.AlignTopWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignTopWithProperty, child)); + node.AlignRightWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignRightWithProperty, child)); + node.AlignBottomWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignBottomWithProperty, child)); + + node.LeftOfNode = _childGraph.AddLink(node, GetDependencyElement(LeftOfProperty, child)); + node.AboveNode = _childGraph.AddLink(node, GetDependencyElement(AboveProperty, child)); + node.RightOfNode = _childGraph.AddLink(node, GetDependencyElement(RightOfProperty, child)); + node.BelowNode = _childGraph.AddLink(node, GetDependencyElement(BelowProperty, child)); + + node.AlignHorizontalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignHorizontalCenterWithProperty, child)); + node.AlignVerticalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignVerticalCenterWithProperty, child)); + } + + if (_childGraph.CheckCyclic()) + { + throw new Exception("RelativePanel error: Circular dependency detected. Layout could not complete."); + } + + var size = new Size(); + + foreach (var child in Children) + { + if (child.Bounds.Bottom > size.Height) + { + size = size.WithHeight(child.Bounds.Bottom); + } + + if (child.Bounds.Right > size.Width) + { + size = size.WithWidth(child.Bounds.Right); + } + } + + if (VerticalAlignment == VerticalAlignment.Stretch) + { + size = size.WithHeight(arrangeSize.Height); + } + + if (HorizontalAlignment == HorizontalAlignment.Stretch) + { + size = size.WithWidth(arrangeSize.Width); + } + + return size; + } + + private Layoutable? GetDependencyElement(AvaloniaProperty property, AvaloniaObject child) + { + var dependency = child.GetValue(property); + + if (dependency is Layoutable layoutable) + { + if (Children.Contains((ILayoutable)layoutable)) + return layoutable; + + throw new ArgumentException($"RelativePanel error: Element does not exist in the current context: {property.Name}"); + } + + return null; + } + + private class GraphNode + { + public Point Position { get; set; } + + public bool Arranged { get; set; } + + public Layoutable Element { get; } + + public HashSet OutgoingNodes { get; } + + public GraphNode? AlignLeftWithNode { get; set; } + + public GraphNode? AlignTopWithNode { get; set; } + + public GraphNode? AlignRightWithNode { get; set; } + + public GraphNode? AlignBottomWithNode { get; set; } + + public GraphNode? LeftOfNode { get; set; } + + public GraphNode? AboveNode { get; set; } + + public GraphNode? RightOfNode { get; set; } + + public GraphNode? BelowNode { get; set; } + + public GraphNode? AlignHorizontalCenterWith { get; set; } + + public GraphNode? AlignVerticalCenterWith { get; set; } + + public GraphNode(Layoutable element) + { + OutgoingNodes = new HashSet(); + Element = element; + } + } + + private class Graph + { + private readonly Dictionary _nodeDic; + + private Size _arrangeSize; + + public Graph() + { + _nodeDic = new Dictionary(); + } + + public GraphNode? AddLink(GraphNode from, Layoutable? to) + { + if (to == null) + return null; + + GraphNode nodeTo; + if (_nodeDic.ContainsKey(to)) + { + nodeTo = _nodeDic[to]; + } + else + { + nodeTo = new GraphNode(to); + _nodeDic[to] = nodeTo; + } + + from.OutgoingNodes.Add(nodeTo); + return nodeTo; + } + + public GraphNode AddNode(Layoutable value) + { + if (!_nodeDic.ContainsKey(value)) + { + var node = new GraphNode(value); + _nodeDic.Add(value, node); + return node; + } + + return _nodeDic[value]; + } + + public void Reset(Size arrangeSize) + { + _arrangeSize = arrangeSize; + _nodeDic.Clear(); + } + + public bool CheckCyclic() => CheckCyclic(_nodeDic.Values, null); + + private bool CheckCyclic(IEnumerable nodes, HashSet? set) + { + set ??= new HashSet(); + + foreach (var node in nodes) + { + if (!node.Arranged && node.OutgoingNodes.Count == 0) + { + ArrangeChild(node, true); + continue; + } + + if (node.OutgoingNodes.All(item => item.Arranged)) + { + ArrangeChild(node); + continue; + } + + if (!set.Add(node.Element)) + return true; + + return CheckCyclic(node.OutgoingNodes, set); + } + + return false; + } + + private void ArrangeChild(GraphNode node, bool ignoneSibling = false) + { + var child = node.Element; + var childSize = child.DesiredSize; + var childPos = new Point(); + + if (GetAlignHorizontalCenterWithPanel(child)) + { + childPos = childPos.WithX((_arrangeSize.Width - childSize.Width) / 2); + } + + if (GetAlignVerticalCenterWithPanel(child)) + { + childPos = childPos.WithY((_arrangeSize.Height - childSize.Height) / 2); + } + + var alignLeftWithPanel = GetAlignLeftWithPanel(child); + var alignTopWithPanel = GetAlignTopWithPanel(child); + var alignRightWithPanel = GetAlignRightWithPanel(child); + var alignBottomWithPanel = GetAlignBottomWithPanel(child); + + if (!ignoneSibling) + { + if (node.LeftOfNode != null) + { + childPos = childPos.WithX(node.LeftOfNode.Position.X - childSize.Width); + } + + if (node.AboveNode != null) + { + childPos = childPos.WithY(node.AboveNode.Position.Y - childSize.Height); + } + + if (node.RightOfNode != null) + { + childPos = childPos.WithX(node.RightOfNode.Position.X + node.RightOfNode.Element.DesiredSize.Width); + } + + if (node.BelowNode != null) + { + childPos = childPos.WithY(node.BelowNode.Position.Y + node.BelowNode.Element.DesiredSize.Height); + } + + if (node.AlignHorizontalCenterWith != null) + { + childPos = childPos.WithX(node.AlignHorizontalCenterWith.Position.X + + (node.AlignHorizontalCenterWith.Element.DesiredSize.Width - childSize.Width) / 2); + } + + if (node.AlignVerticalCenterWith != null) + { + childPos = childPos.WithY(node.AlignVerticalCenterWith.Position.Y + + (node.AlignVerticalCenterWith.Element.DesiredSize.Height - childSize.Height) / 2); + } + + if (node.AlignLeftWithNode != null) + { + childPos = childPos.WithX(node.AlignLeftWithNode.Position.X); + } + + if (node.AlignTopWithNode != null) + { + childPos = childPos.WithY(node.AlignTopWithNode.Position.Y); + } + + if (node.AlignRightWithNode != null) + { + childPos = childPos.WithX(node.AlignRightWithNode.Element.DesiredSize.Width + node.AlignRightWithNode.Position.X - childSize.Width); + } + + if (node.AlignBottomWithNode != null) + { + childPos = childPos.WithY(node.AlignBottomWithNode.Element.DesiredSize.Height + node.AlignBottomWithNode.Position.Y - childSize.Height); + } + } + + if (alignLeftWithPanel) + { + if (node.AlignRightWithNode != null) + { + childPos = childPos.WithX((node.AlignRightWithNode.Element.DesiredSize.Width + node.AlignRightWithNode.Position.X - childSize.Width) / 2); + } + else + { + childPos = childPos.WithX(0); + } + } + + if (alignTopWithPanel) + { + if (node.AlignBottomWithNode != null) + { + childPos = childPos.WithY((node.AlignBottomWithNode.Element.DesiredSize.Height + node.AlignBottomWithNode.Position.Y - childSize.Height) / 2); + } + else + { + childPos = childPos.WithY(0); + } + } + + if (alignRightWithPanel) + { + if (alignLeftWithPanel) + { + childPos = childPos.WithX((_arrangeSize.Width - childSize.Width) / 2); + } + else if (node.AlignLeftWithNode == null) + { + childPos = childPos.WithX(_arrangeSize.Width - childSize.Width); + } + else + { + childPos = childPos.WithX((_arrangeSize.Width + node.AlignLeftWithNode.Position.X - childSize.Width) / 2); + } + } + + if (alignBottomWithPanel) + { + if (alignTopWithPanel) + { + childPos = childPos.WithY((_arrangeSize.Height - childSize.Height) / 2); + } + else if (node.AlignTopWithNode == null) + { + childPos = childPos.WithY(_arrangeSize.Height - childSize.Height); + } + else + { + childPos = childPos.WithY((_arrangeSize.Height + node.AlignTopWithNode.Position.Y - childSize.Height) / 2); + } + } + + child.Arrange(new Rect(childPos.X, childPos.Y, childSize.Width, childSize.Height)); + node.Position = childPos; + node.Arranged = true; + } + } + } +} diff --git a/src/Avalonia.Controls/ResolveByNameAttribute.cs b/src/Avalonia.Controls/ResolveByNameAttribute.cs new file mode 100644 index 0000000000..a00e3f8a1b --- /dev/null +++ b/src/Avalonia.Controls/ResolveByNameAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Controls +{ + public class ResolveByNameAttribute : Attribute + { + + } +} diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 2361ea9011..7e5287f81f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -411,9 +411,30 @@ namespace Avalonia.Controls context.FillRectangle(background, new Rect(Bounds.Size)); } + if (TextLayout is null) + { + return; + } + + var textAlignment = TextAlignment; + + var width = Bounds.Size.Width; + + var offsetX = 0.0; + + switch (textAlignment) + { + case TextAlignment.Center: + offsetX = (width - TextLayout.Size.Width) / 2; + break; + case TextAlignment.Right: + offsetX = width - TextLayout.Size.Width; + break; + } + var padding = Padding; - TextLayout?.Draw(context, new Point(padding.Left, padding.Top)); + TextLayout.Draw(context, new Point(padding.Left + offsetX, padding.Top)); } /// @@ -431,7 +452,7 @@ namespace Avalonia.Controls return new TextLayout( text ?? string.Empty, - FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), + FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), FontSize, Foreground, TextAlignment, @@ -470,12 +491,12 @@ namespace Avalonia.Controls if (_constraint != availableSize) { + _constraint = availableSize; + InvalidateTextLayout(); } - _constraint = availableSize; - - var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty; + var measuredSize = TextLayout?.Size ?? Size.Empty; return measuredSize.Inflate(padding); } diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index bc17fb3faa..763d192693 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -54,7 +54,7 @@ namespace Avalonia.Headless class HeadlessCursorFactoryStub : IStandardCursorFactory { - + public IPlatformHandle GetCursor(StandardCursorType cursorType) { return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); @@ -101,7 +101,7 @@ namespace Avalonia.Headless public bool IsFixedPitch => true; public void Dispose() - { + { } public ushort GetGlyph(uint codepoint) @@ -155,9 +155,9 @@ namespace Avalonia.Headless return new List { "Arial" }; } - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { - fontKey = new FontKey("Arial", fontWeight, fontStyle); + fontKey = new FontKey("Arial", fontStyle, fontWeight); return true; } } @@ -169,7 +169,7 @@ namespace Avalonia.Headless { public void Save(Stream outputStream) { - + } } public IWindowIconImpl LoadIcon(string fileName) diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index bc979c15ee..ad3fee7eb7 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -79,12 +79,13 @@ namespace Avalonia.Media /// Returns a new typeface, or an existing one if a matching typeface exists. /// /// The font family. - /// The font weight. /// The font style. + /// The font weight. /// /// The typeface. /// - public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal) + public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal, + FontWeight fontWeight = FontWeight.Normal) { while (true) { @@ -93,7 +94,7 @@ namespace Avalonia.Media fontFamily = _defaultFontFamily; } - var key = new FontKey(fontFamily.Name, fontWeight, fontStyle); + var key = new FontKey(fontFamily.Name, fontStyle, fontWeight); if (_typefaceCache.TryGetValue(key, out var typeface)) { @@ -121,15 +122,16 @@ namespace Avalonia.Media /// Returns null if no fallback was found. /// /// The codepoint to match against. - /// The font weight. /// The font style. + /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. /// /// The matched typeface. /// - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal, + public Typeface MatchCharacter(int codepoint, FontStyle fontStyle = FontStyle.Normal, + FontWeight fontWeight = FontWeight.Normal, FontFamily fontFamily = null, CultureInfo culture = null) { foreach (var cachedTypeface in _typefaceCache.Values) @@ -142,7 +144,7 @@ namespace Avalonia.Media } } - var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out var key) ? _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) : null; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs index a8d81648ba..b330db8462 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -4,7 +4,7 @@ namespace Avalonia.Media.Fonts { public readonly struct FontKey : IEquatable { - public FontKey(string familyName, FontWeight weight, FontStyle style) + public FontKey(string familyName, FontStyle style, FontWeight weight) { FamilyName = familyName; Style = style; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 2e7e7aceb1..b71fe5bc3c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -89,16 +89,7 @@ namespace Avalonia.Media.TextFormatting /// The split result. public SplitTextCharactersResult Split(int length) { - var glyphCount = 0; - - var firstCharacters = GlyphRun.Characters.Take(length); - - var codepointEnumerator = new CodepointEnumerator(firstCharacters); - - while (codepointEnumerator.MoveNext()) - { - glyphCount++; - } + var glyphCount = GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length); if (GlyphRun.Characters.Length == length) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index b35882fc0e..47e716982c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -71,7 +71,7 @@ namespace Avalonia.Media.TextFormatting //ToDo: Fix FontFamily fallback currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultTypeface.FontFamily); + FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily); if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 793707d0b2..3ad23f3504 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -72,6 +72,11 @@ namespace Avalonia.Media.TextFormatting { foreach (var shapedCharacters in previousLineBreak.RemainingCharacters) { + if (shapedCharacters == null) + { + continue; + } + textRuns.Add(shapedCharacters); if (TryGetLineBreak(shapedCharacters, out var runLineBreak)) @@ -106,7 +111,7 @@ namespace Avalonia.Media.TextFormatting var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface, run.Properties.FontRenderingEmSize, run.Properties.CultureInfo); - var shapedCharacters = new ShapedTextCharacters(glyphRun, textRun.Properties); + var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties); textRuns.Add(shapedCharacters); } @@ -355,9 +360,67 @@ namespace Avalonia.Media.TextFormatting { var glyphRun = textCharacters.GlyphRun; - var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _); + if (glyphRun.Bounds.Width < availableWidth) + { + return glyphRun.Characters.Length; + } + + var glyphCount = 0; + + var currentWidth = 0.0; + + if (glyphRun.GlyphAdvances.IsEmpty) + { + var glyphTypeface = glyphRun.GlyphTypeface; + + for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + { + var glyph = glyphRun.GlyphIndices[i]; + + var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; + } + } + else + { + for (var i = 0; i < glyphRun.GlyphAdvances.Length; i++) + { + var advance = glyphRun.GlyphAdvances[i]; + + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; + } + } + + if (glyphCount == glyphRun.GlyphIndices.Length) + { + return glyphRun.Characters.Length; + } + + if (glyphRun.GlyphClusters.IsEmpty) + { + return glyphCount; + } + + var firstCluster = glyphRun.GlyphClusters[0]; + + var lastCluster = glyphRun.GlyphClusters[glyphCount]; - return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textCharacters.Text.Start; + return lastCluster - firstCluster; } /// diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 2e2e4a8c68..54745144c8 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -103,12 +103,12 @@ namespace Avalonia.Media.TextFormatting public IReadOnlyList TextLines { get; private set; } /// - /// Gets the bounds of the layout. + /// Gets the size of the layout. /// /// /// The bounds. /// - public Rect Bounds { get; private set; } + public Size Size { get; private set; } /// /// Draws the text layout. @@ -126,7 +126,10 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in TextLines) { - textLine.Draw(context, new Point(origin.X, currentY)); + var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, + _paragraphProperties.TextAlignment); + + textLine.Draw(context, new Point(origin.X + offsetX, currentY)); currentY += textLine.LineMetrics.Size.Height; } @@ -158,22 +161,16 @@ namespace Avalonia.Media.TextFormatting /// Updates the current bounds. /// /// The text line. - /// The left. - /// The right. - /// The bottom. - private static void UpdateBounds(TextLine textLine, ref double left, ref double right, ref double bottom) + /// The current width. + /// The current height. + private static void UpdateBounds(TextLine textLine, ref double width, ref double height) { - if (right < textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width) - { - right = textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width; - } - - if (left < textLine.LineMetrics.BaselineOrigin.X) + if (width < textLine.LineMetrics.Size.Width) { - left = textLine.LineMetrics.BaselineOrigin.X; + width = textLine.LineMetrics.Size.Width; } - bottom += textLine.LineMetrics.Size.Height; + height += textLine.LineMetrics.Size.Height; } /// @@ -204,13 +201,13 @@ namespace Avalonia.Media.TextFormatting TextLines = new List { textLine }; - Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height); + Size = new Size(0, textLine.LineMetrics.Size.Height); } else { var textLines = new List(); - double left = 0.0, right = 0.0, bottom = 0.0; + double width = 0.0, height = 0.0; var currentPosition = 0; @@ -228,9 +225,9 @@ namespace Avalonia.Media.TextFormatting textLines.Add(textLine); - UpdateBounds(textLine, ref left, ref right, ref bottom); + UpdateBounds(textLine, ref width, ref height); - if (!double.IsPositiveInfinity(MaxHeight) && bottom > MaxHeight) + if (!double.IsPositiveInfinity(MaxHeight) && height > MaxHeight) { break; } @@ -247,7 +244,7 @@ namespace Avalonia.Media.TextFormatting textLines.Add(emptyTextLine); } - Bounds = new Rect(left, 0, right, bottom); + Size = new Size(width, height); TextLines = textLines; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index cf00399b8a..a1a9b50793 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -33,8 +33,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in _textRuns) { - var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, - origin.Y + LineMetrics.BaselineOrigin.Y); + var baselineOrigin = new Point(currentX, origin.Y + LineMetrics.TextBaseline); textRun.Draw(drawingContext, baselineOrigin); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index d47cc0c394..2f7809ff35 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -9,10 +9,10 @@ namespace Avalonia.Media.TextFormatting /// public readonly struct TextLineMetrics { - public TextLineMetrics(Size size, Point baselineOrigin, TextRange textRange) + public TextLineMetrics(Size size, double textBaseline, TextRange textRange) { Size = size; - BaselineOrigin = baselineOrigin; + TextBaseline = textBaseline; TextRange = textRange; } @@ -33,12 +33,9 @@ namespace Avalonia.Media.TextFormatting public Size Size { get; } /// - /// Gets the baseline origin. + /// Gets the distance from the top to the baseline of the line of text. /// - /// - /// The baseline origin. - /// - public Point BaselineOrigin { get; } + public double TextBaseline { get; } /// /// Creates the text line metrics. @@ -81,16 +78,12 @@ namespace Avalonia.Media.TextFormatting } } - var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, paragraphProperties.TextAlignment); - - var baselineOrigin = new Point(xOrigin, -ascent); - var size = new Size(lineWidth, double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ? descent - ascent + lineGap : paragraphProperties.LineHeight); - return new TextLineMetrics(size, baselineOrigin, textRange); + return new TextLineMetrics(size, -ascent, textRange); } } } diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 3d0bea8c80..59b08aae0a 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -22,15 +22,16 @@ namespace Avalonia.Platform /// 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 weight. /// The font family. This is optional and used for fallback lookup. /// The culture. /// The matching font key. /// /// True, if the could match the character to specified parameters, False otherwise. /// - bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); /// diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index da345835c8..3979312ce0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -25,6 +25,7 @@ + @@ -57,6 +58,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs new file mode 100644 index 0000000000..8561aa898b --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls; +using Avalonia.Data.Core; + +#nullable enable + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class ResolveByNameExtension + { + public ResolveByNameExtension(string name) + { + Name = name; + } + + public string Name { get; } + + public object? ProvideValue(IServiceProvider serviceProvider) + { + var nameScope = serviceProvider.GetService(); + + var value = nameScope.FindAsync(Name); + + if(value.IsCompleted) + return value.GetResult(); + + var provideValueTarget = serviceProvider.GetService(); + var target = provideValueTarget.TargetObject; + + if (provideValueTarget.TargetProperty is IPropertyInfo property) + value.OnCompleted(() => property.Set(target, value.GetResult())); + + return null; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index f3b8559d45..abff763bb1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -40,22 +40,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions // Targeted - InsertBefore( new AvaloniaXamlIlTransformInstanceAttachedProperties(), new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); - InsertAfter(new AvaloniaXamlIlAvaloniaPropertyResolver()); - - + InsertAfter( + new AvaloniaXamlIlAvaloniaPropertyResolver()); - InsertBefore( + InsertBefore( new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlSelectorTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), - new AvaloniaXamlIlTransitionsTypeMetadataTransformer() + new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() ); // After everything else diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs new file mode 100644 index 0000000000..c0ac841b7f --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs @@ -0,0 +1,49 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +#nullable enable +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlResolveByNameMarkupExtensionReplacer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlAstXamlPropertyValueNode propertyValueNode)) return node; + + if (!(propertyValueNode.Property is XamlAstClrProperty clrProperty)) return node; + + IEnumerable attributes = propertyValueNode.Property.GetClrProperty().CustomAttributes; + + if (propertyValueNode.Property is XamlAstClrProperty referenceNode && + referenceNode.Getter != null) + { + attributes = attributes.Concat(referenceNode.Getter.CustomAttributes); + } + + if (attributes.All(attribute => attribute.Type.FullName != "Avalonia.Controls.ResolveByNameAttribute")) + return node; + + if (propertyValueNode.Values.Count != 1 || !(propertyValueNode.Values.First() is XamlAstTextNode)) + return node; + + var newNode = new XamlAstObjectNode( + propertyValueNode.Values[0], + new XamlAstClrTypeReference(propertyValueNode.Values[0], + context.GetAvaloniaTypes().ResolveByNameExtension, true)) + { + Arguments = new List { propertyValueNode.Values[0] } + }; + + if (XamlTransformHelpers.TryConvertMarkupExtension(context, newNode, out var extensionNode)) + { + propertyValueNode.Values[0] = extensionNode; + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index 8e194e9385..d78ceeb918 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -174,6 +174,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers other is GetterMethod m && m.Name == Name && m.DeclaringType.Equals(DeclaringType); public IXamlType ReturnType => Parent.PropertyType; public IReadOnlyList Parameters { get; } + + public IReadOnlyList CustomAttributes => DeclaringType.CustomAttributes; + public void EmitCall(IXamlILEmitter emitter) { var method = Parent._avaloniaObject diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index bf1fb4a6fc..58ea11aa8f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -37,6 +37,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType CompiledBindingPathBuilder { get; } public IXamlType CompiledBindingPath { get; } public IXamlType CompiledBindingExtension { get; } + + public IXamlType ResolveByNameExtension { get; } + public IXamlType DataTemplate { get; } public IXamlType IItemsPresenterHost { get; } public IXamlType ReflectionBindingExtension { get; } @@ -92,6 +95,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder"); CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath"); CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension"); + ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension"); DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate"); IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost"); ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension"); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 0028377ce7..7b8b3013bd 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 0028377ce7c7dc21f9fe71b45f62a95991b1ab58 +Subproject commit 7b8b3013bd42e1992838a525c991f44191da55be diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 415a89e1c1..91bc937475 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -29,7 +29,8 @@ namespace Avalonia.Skia [ThreadStatic] private static string[] t_languageTagBuffer; - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { SKFontStyle skFontStyle; @@ -80,7 +81,7 @@ namespace Avalonia.Skia continue; } - fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); return true; } @@ -91,7 +92,7 @@ namespace Avalonia.Skia if (skTypeface != null) { - fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); return true; } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 0fdea5ed40..f59a0a32c2 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Platform; +using JetBrains.Annotations; using SkiaSharp; namespace Avalonia.Skia @@ -7,9 +8,9 @@ namespace Avalonia.Skia /// public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKTextBlob textBlob) + public GlyphRunImpl([NotNull] SKTextBlob textBlob) { - TextBlob = textBlob; + TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); } /// diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 7aea90e61e..6c2ac17923 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -19,42 +19,49 @@ namespace Avalonia.Skia public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style); + var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight); return GetNearestMatch(_typefaces, key); } private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) { - if (typefaces.ContainsKey(key)) + if (typefaces.TryGetValue(new FontKey(key.FamilyName, key.Style, key.Weight), out var typeface)) { - return typefaces[key]; + return typeface; } - var keys = typefaces.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); + var weight = (int)key.Weight; - if (!keys.Any()) - { - keys = typefaces.Keys.Where( - x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); + weight -= weight % 100; // make sure we start at a full weight - if (!keys.Any()) + for (var i = (int)key.Style; i < 2; i++) + { + // only try 2 font weights in each direction + for (var j = 0; j < 200; j += 100) { - keys = typefaces.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && - (x.Style >= key.Style || x.Style < key.Style)).ToArray(); - } - } + if (weight - j >= 100) + { + if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) + { + return typeface; + } + } - if (keys.Length == 0) - { - return null; - } + if (weight + j > 900) + { + continue; + } - key = keys[0]; + if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + { + return typeface; + } + } + } - return typefaces[key]; + //Nothing was found so we use the first typeface we can get. + return typefaces.Values.FirstOrDefault(); } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index a9aed80a04..d36baf331d 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -43,18 +43,20 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); - if (assetStream == null) throw new InvalidOperationException("Asset could not be loaded."); + if (assetStream == null) + throw new InvalidOperationException("Asset could not be loaded."); var typeface = SKTypeface.FromStream(assetStream); - if(typeface == null) throw new InvalidOperationException("Typeface could not be loaded."); + if (typeface == null) + throw new InvalidOperationException("Typeface could not be loaded."); if (typeface.FamilyName != fontFamily.Name) { continue; } - var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); + var key = new FontKey(fontFamily.Name, (FontStyle)typeface.FontSlant, (FontWeight)typeface.FontWeight); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 786af7726c..ffe1175567 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -15,51 +15,7 @@ namespace Avalonia.Skia { using (var buffer = new Buffer()) { - buffer.ContentType = ContentType.Unicode; - - var breakCharPosition = text.Length - 1; - - var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count); - - if (codepoint.IsBreakChar) - { - var breakCharCount = 1; - - if (text.Length > 1) - { - var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _); - - if (codepoint == '\r' && previousCodepoint == '\n' - || codepoint == '\n' && previousCodepoint == '\r') - { - breakCharCount = 2; - } - } - - if (breakCharPosition != text.Start) - { - buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount)); - } - - var cluster = buffer.GlyphInfos.Length > 0 ? - buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 : - (uint)text.Start; - - switch (breakCharCount) - { - case 1: - buffer.Add('\u200C', cluster); - break; - case 2: - buffer.Add('\u200C', cluster); - buffer.Add('\u200D', cluster); - break; - } - } - else - { - buffer.AddUtf16(text.Buffer.Span); - } + FillBuffer(buffer, text); buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); @@ -93,7 +49,7 @@ namespace Avalonia.Skia { glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; - clusters[i] = (ushort)(text.Start + glyphInfos[i].Cluster); + clusters[i] = (ushort)glyphInfos[i].Cluster; if (!glyphTypeface.IsFixedPitch) { @@ -112,6 +68,51 @@ namespace Avalonia.Skia } } + private static void FillBuffer(Buffer buffer, ReadOnlySlice text) + { + buffer.ContentType = ContentType.Unicode; + + var i = 0; + + while (i < text.Length) + { + var codepoint = Codepoint.ReadAt(text, i, out var count); + + var cluster = (uint)(text.Start + i); + + if (codepoint.IsBreakChar) + { + if (i < text.End) + { + var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); + + if (nextCodepoint == '\r' && codepoint == '\n' || nextCodepoint == '\n' && codepoint == '\r') + { + count++; + + buffer.Add('\u200C', cluster); + + buffer.Add('\u200D', cluster); + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add(codepoint, cluster); + } + + i += count; + } + } + private static void SetOffset(ReadOnlySpan glyphPositions, int index, double textScale, ref Vector[] offsetBuffer) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 253a373106..33af15076d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -32,7 +32,8 @@ namespace Avalonia.Direct2D1.Media return fontFamilies; } - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -50,7 +51,7 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle); + fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight); return true; } diff --git a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs new file mode 100644 index 0000000000..4248e643eb --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs @@ -0,0 +1,59 @@ +using Avalonia.Controls.Shapes; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class RelativePanelTests + { + [Fact] + public void Lays_Out_1_Child_Next_the_other() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Top, + HorizontalAlignment = Layout.HorizontalAlignment.Left, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAlignLeftWithPanel(rect1 , true); + RelativePanel.SetRightOf(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(40, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(20, 0, 20, 20), target.Children[1].Bounds); + } + + public void Lays_Out_1_Child_Below_the_other() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Top, + HorizontalAlignment = Layout.HorizontalAlignment.Left, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAlignLeftWithPanel(rect1, true); + RelativePanel.SetBelow(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 40), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(0, 20, 20, 20), target.Children[1].Bounds); + } + } +} diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index e65cdf0312..8683da9a01 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -51,7 +51,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle), + FontManager.Current.GetOrAddTypeface(fontFamily, fontStyle, fontWeight), fontSize, textAlignment, wrapping, diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index feed1179ef..f36d6d9e4a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -38,7 +38,7 @@ namespace Avalonia.Skia.UnitTests.Media private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { foreach (var customTypeface in _customTypefaces) @@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media continue; } - fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle); + fontKey = new FontKey(customTypeface.FontFamily.Name, fontStyle, fontWeight); return true; } @@ -56,7 +56,7 @@ namespace Avalonia.Skia.UnitTests.Media var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle); + fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); return true; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 697cc4fec7..4a88b259bc 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -260,6 +260,39 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Not_Produce_TextLine_Wider_Than_ParagraphWidth() + { + using (Start()) + { + const string text = + "Multiline TextBlock with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. " + + "Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. " + + "Vivamus pretium ornare est."; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textSourceIndex = 0; + + while (textSourceIndex < text.Length) + { + var textLine = + formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties); + + Assert.True(textLine.LineMetrics.Size.Width <= 200); + + textSourceIndex += textLine.TextRange.Length; + } + } + } + public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 5d9aa2cf97..43a791b2cb 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -512,7 +512,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(numberOfLines, layout.TextLines.Count); - Assert.Equal(numberOfLines * lineHeight, layout.Bounds.Height); + Assert.Equal(numberOfLines * lineHeight, layout.Size.Height); } } diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index 55656fcfc0..e614c60310 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -25,7 +25,7 @@ namespace Avalonia.UnitTests return new[] { _defaultFamilyName }; } - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { fontKey = default; diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index e219682fa6..42e573c8a5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -34,6 +34,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Background = Brushes.Red, Child = textBlock = new TextBlock { + TextWrapping = TextWrapping.NoWrap, Text = "Hello World", } } diff --git a/tests/Avalonia.Visuals.UnitTests/Utilities/ReadOnlySpanTests.cs b/tests/Avalonia.Visuals.UnitTests/Utilities/ReadOnlySpanTests.cs new file mode 100644 index 0000000000..1afd84e546 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Utilities/ReadOnlySpanTests.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Utilities +{ + public class ReadOnlySpanTests + { + [Fact] + public void Should_Skip() + { + var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var slice = new ReadOnlySlice(buffer); + + var skipped = slice.Skip(2); + + var expected = buffer.Skip(2); + + Assert.Equal(expected, skipped); + } + + [Fact] + public void Should_Take() + { + var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var slice = new ReadOnlySlice(buffer); + + var taken = slice.Take(8); + + var expected = buffer.Take(8); + + Assert.Equal(expected, taken); + } + } +}