diff --git a/Avalonia.v3.ncrunchsolution b/Avalonia.v3.ncrunchsolution index a2208a9a91..bef7e45524 100644 --- a/Avalonia.v3.ncrunchsolution +++ b/Avalonia.v3.ncrunchsolution @@ -3,6 +3,7 @@ tests\TestFiles\**.* src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Avalonia.Build.Tasks.dll + src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll True .ncrunch diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 873048ef21..88c4d36282 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 4def44cbd0..a8d9332c57 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + 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/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 08bb1584a9..304782dbf9 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,6 +1,30 @@ + + + + + + + + + + + + + + + + ItemsRepeater @@ -12,8 +36,6 @@ Stack - Horizontal UniformGrid - Vertical UniformGrid - Horizontal - WrapLayout - Horizontal - WrapLayout - Veritcal @@ -25,20 +47,8 @@ - - - - - - - - - - - - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index a4003cadc9..9e898c4536 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -38,6 +38,12 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e) + { + var item = (ItemsRepeaterPageViewModel.Item)e.DataContext; + e.TemplateKey = (item.Index % 2 == 0) ? "even" : "odd"; + } + private void LayoutChanged(object sender, SelectionChangedEventArgs e) { if (_repeater == null) 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/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs index 73aaeff994..f893a6e28e 100644 --- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs @@ -62,13 +62,9 @@ namespace ControlCatalog.ViewModels public class Item : ReactiveObject { private double _height = double.NaN; - private int _index; - - public Item(int index) - { - _index = index; - } + public Item(int index) => Index = index; + public int Index { get; } public string Text { get; set; } public double Height @@ -76,8 +72,6 @@ namespace ControlCatalog.ViewModels get => _height; set => this.RaiseAndSetIfChanged(ref _height, value); } - - public IBrush Background => ((_index % 2) == 0) ? Brushes.Yellow : Brushes.Wheat; } } } 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/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index c91adaa26e..29e7f28b44 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -353,6 +353,15 @@ namespace Avalonia.Controls.Primitives var trackLength = isVertical ? arrangeSize.Height : arrangeSize.Width; double thumbMinLength = 10; + StyledProperty minLengthProperty = isVertical ? MinHeightProperty : MinWidthProperty; + + var thumb = Thumb; + + if (thumb != null && thumb.IsSet(minLengthProperty)) + { + thumbMinLength = thumb.GetValue(minLengthProperty); + } + thumbLength = trackLength * viewportSize / extent; CoerceLength(ref thumbLength, trackLength); thumbLength = Math.Max(thumbMinLength, thumbLength); 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/Repeater/ElementFactory.cs b/src/Avalonia.Controls/Repeater/ElementFactory.cs new file mode 100644 index 0000000000..1c1b71af88 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ElementFactory.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + public abstract class ElementFactory : IElementFactory + { + bool IDataTemplate.SupportsRecycling => false; + + public IControl Build(object data) + { + return GetElementCore(new ElementFactoryGetArgs { Data = data }); + } + + public IControl GetElement(ElementFactoryGetArgs args) + { + return GetElementCore(args); + } + + public bool Match(object data) => true; + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + RecycleElementCore(args); + } + + protected abstract IControl GetElementCore(ElementFactoryGetArgs args); + protected abstract void RecycleElementCore(ElementFactoryRecycleArgs args); + } +} diff --git a/src/Avalonia.Controls/Repeater/IElementFactory.cs b/src/Avalonia.Controls/Repeater/IElementFactory.cs new file mode 100644 index 0000000000..6a899a6f26 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/IElementFactory.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + /// + /// Represents the optional arguments to use when calling an implementation of the + /// 's method. + /// + public class ElementFactoryGetArgs + { + /// + /// Gets or sets the data item for which an appropriate element tree should be realized + /// when calling . + /// + public object Data { get; set; } + + /// + /// Gets or sets the that is expected to be the parent of the + /// realized element from . + /// + public IControl Parent { get; set; } + + /// + /// Gets or sets the index of the item that should be realized. + /// + public int Index { get; set; } + } + + /// + /// Represents the optional arguments to use when calling an implementation of the + /// 's method. + /// + public class ElementFactoryRecycleArgs + { + /// + /// Gets or sets the to recycle when calling + /// . + /// + public IControl Element { get; set; } + + /// + /// Gets or sets the that is expected to be the parent of the + /// realized element from . + /// + public IControl Parent { get; set; } + } + + /// + /// A data template that supports creating and recyling elements for an . + /// + public interface IElementFactory : IDataTemplate + { + /// + /// Gets an . + /// + /// The element args. + public IControl GetElement(ElementFactoryGetArgs args); + + /// + /// Recycles an that was previously retrieved using + /// . + /// + /// The recycle args. + public void RecycleElement(ElementFactoryRecycleArgs args); + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs index 04d859c742..4b784375a9 100644 --- a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs @@ -7,13 +7,27 @@ using Avalonia.Controls.Templates; namespace Avalonia.Controls { - internal class ItemTemplateWrapper + internal class ItemTemplateWrapper : IElementFactory { private readonly IDataTemplate _dataTemplate; public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; - public IControl GetElement(IControl parent, object data) + public bool SupportsRecycling => false; + public IControl Build(object param) => GetElement(null, param); + public bool Match(object data) => _dataTemplate.Match(data); + + public IControl GetElement(ElementFactoryGetArgs args) + { + return GetElement(args.Parent, args.Data); + } + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + RecycleElement(args.Parent, args.Element); + } + + private IControl GetElement(IControl parent, object data) { var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); @@ -37,7 +51,7 @@ namespace Avalonia.Controls return element; } - public void RecycleElement(IControl parent, IControl element) + private void RecycleElement(IControl parent, IControl element) { var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 87f4760156..8bc356bdec 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls /// public ItemsSourceView ItemsSourceView { get; private set; } - internal ItemTemplateWrapper ItemTemplateShim { get; set; } + internal IElementFactory ItemTemplateShim { get; set; } internal Point LayoutOrigin { get; set; } internal object LayoutState { get; set; } internal IControl MadeAnchor => _viewportManager.MadeAnchor; @@ -664,7 +664,7 @@ namespace Avalonia.Controls } } - ItemTemplateShim = new ItemTemplateWrapper(newValue); + ItemTemplateShim = newValue as IElementFactory ?? new ItemTemplateWrapper(newValue); InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs index 4e5950bdc5..28f299043c 100644 --- a/src/Avalonia.Controls/Repeater/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -11,10 +11,13 @@ using Avalonia.Controls.Templates; namespace Avalonia.Controls { - internal class RecyclePool + public class RecyclePool { - public static readonly AttachedProperty OriginTemplateProperty = - AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool)); + internal static readonly AttachedProperty OriginTemplateProperty = + AvaloniaProperty.RegisterAttached("OriginTemplate"); + + internal static readonly AttachedProperty ReuseKeyProperty = + AvaloniaProperty.RegisterAttached("ReuseKey", string.Empty); private static ConditionalWeakTable s_pools = new ConditionalWeakTable(); private readonly Dictionary> _elements = new Dictionary>(); @@ -77,6 +80,9 @@ namespace Avalonia.Controls return null; } + internal string GetReuseKey(IControl element) => element.GetValue(ReuseKeyProperty); + internal void SetReuseKey(IControl element, string value) => element.SetValue(ReuseKeyProperty, value); + private IPanel EnsureOwnerIsPanelOrNull(IControl owner) { if (owner is IPanel panel) diff --git a/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs b/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs new file mode 100644 index 0000000000..9503239e34 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Templates; + +#nullable enable + +namespace Avalonia.Controls +{ + public class SelectTemplateEventArgs : EventArgs + { + public string? TemplateKey { get; set; } + public object? DataContext { get; internal set; } + public IControl? Owner { get; internal set; } + } + + public class RecyclingElementFactory : ElementFactory + { + private RecyclePool? _recyclePool; + private IDictionary? _templates; + private SelectTemplateEventArgs? _args; + + public RecyclingElementFactory() + { + Templates = new Dictionary(); + } + + public RecyclePool RecyclePool + { + get => _recyclePool ??= new RecyclePool(); + set => _recyclePool = value ?? throw new ArgumentNullException(nameof(value)); + } + + public IDictionary Templates + { + get => _templates ??= new Dictionary(); + set => _templates = value ?? throw new ArgumentNullException(nameof(value)); + } + + public event EventHandler? SelectTemplateKey; + + protected override IControl GetElementCore(ElementFactoryGetArgs args) + { + if (_templates == null || _templates.Count == 0) + { + throw new InvalidOperationException("Templates cannot be empty."); + } + + var templateKey = Templates.Count == 1 ? + Templates.First().Key : + OnSelectTemplateKeyCore(args.Data, args.Parent); + + if (string.IsNullOrEmpty(templateKey)) + { + // Note: We could allow null/whitespace, which would work as long as + // the recycle pool is not shared. in order to make this work in all cases + // currently we validate that a valid template key is provided. + throw new InvalidOperationException("Template key cannot be null or empty."); + } + + // Get an element from the Recycle Pool or create one + var element = RecyclePool.TryGetElement(templateKey, args.Parent); + + if (element is null) + { + // No need to call HasKey if there is only one template. + if (Templates.Count > 1 && !Templates.ContainsKey(templateKey)) + { + var message = $"No templates of key '{templateKey}' were found in the templates collection."; + throw new InvalidOperationException(message); + } + + var dataTemplate = Templates[templateKey]; + element = dataTemplate.Build(args.Data); + + // Associate ReuseKey with element + RecyclePool.SetReuseKey(element, templateKey); + } + + return element; + } + + protected override void RecycleElementCore(ElementFactoryRecycleArgs args) + { + var element = args.Element; + var key = RecyclePool.GetReuseKey(element); + RecyclePool.PutElement(element, key, args.Parent); + } + + protected virtual string OnSelectTemplateKeyCore(object dataContext, IControl owner) + { + if (SelectTemplateKey is object) + { + _args ??= new SelectTemplateEventArgs(); + _args.TemplateKey = null; + _args.DataContext = dataContext; + _args.Owner = owner; + + try + { + SelectTemplateKey(this, _args); + } + finally + { + _args.DataContext = null; + _args.Owner = null; + } + } + + if (string.IsNullOrEmpty(_args?.TemplateKey)) + { + throw new InvalidOperationException( + "Please provide a valid template identifier in the handler for the SelectTemplateKey event."); + } + + return _args!.TemplateKey!; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index eff51804b9..416b1e2824 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -6,11 +6,9 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.Logging; using Avalonia.VisualTree; @@ -26,6 +24,8 @@ namespace Avalonia.Controls private readonly UniqueIdElementPool _resetPool; private IControl _lastFocusedElement; private bool _isDataSourceStableResetPending; + private ElementFactoryGetArgs _elementFactoryGetArgs; + private ElementFactoryRecycleArgs _elementFactoryRecycleArgs; private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; private bool _eventsSubscribed; @@ -134,7 +134,14 @@ namespace Avalonia.Controls if (_owner.ItemTemplateShim != null) { - _owner.ItemTemplateShim.RecycleElement(_owner, element); + var context = _elementFactoryRecycleArgs ??= new ElementFactoryRecycleArgs(); + context.Element = element; + context.Parent = _owner; + + _owner.ItemTemplateShim.RecycleElement(context); + + context.Element = null; + context.Parent = null; } else { @@ -579,7 +586,7 @@ namespace Avalonia.Controls var data = _owner.ItemsSourceView.GetAt(index); var providedElementFactory = _owner.ItemTemplateShim; - ItemTemplateWrapper GetElementFactory() + IElementFactory GetElementFactory() { if (providedElementFactory == null) { @@ -602,7 +609,20 @@ namespace Avalonia.Controls } var elementFactory = GetElementFactory(); - return elementFactory.GetElement(_owner, data); + var args = _elementFactoryGetArgs ??= new ElementFactoryGetArgs(); + + try + { + args.Data = data; + args.Parent = _owner; + args.Index = index; + return elementFactory.GetElement(args); + } + finally + { + args.Data = null; + args.Parent = null; + } } var element = GetElement(); @@ -732,6 +752,7 @@ namespace Avalonia.Controls { _owner.GotFocus += OnFocusChanged; _owner.LostFocus += OnFocusChanged; + _eventsSubscribed = 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.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index 4b42c574cf..c32f2d8102 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -10,10 +10,32 @@ namespace Avalonia.Controls /// public class ToggleSwitch : ToggleButton { + private Panel _knobsPanel; + private Panel _switchKnob; + private bool _knobsPanelPressed = false; + private Point _switchStartPoint = new Point(); + private double _initLeft = -1; + private bool _isDragging = false; + static ToggleSwitch() { OffContentProperty.Changed.AddClassHandler((x, e) => x.OffContentChanged(e)); OnContentProperty.Changed.AddClassHandler((x, e) => x.OnContentChanged(e)); + IsCheckedProperty.Changed.AddClassHandler((x, e) => + { + if ((e.NewValue != null) && (e.NewValue is bool val)) + { + x.UpdateKnobPos(val); + } + }); + + BoundsProperty.Changed.AddClassHandler((x, e) => + { + if (x.IsChecked != null) + { + x.UpdateKnobPos(x.IsChecked.Value); + } + }); } /// @@ -131,6 +153,102 @@ namespace Avalonia.Controls return result; } + + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _switchKnob = e.NameScope.Find("SwitchKnob"); + _knobsPanel = e.NameScope.Find("MovingKnobs"); + + _knobsPanel.PointerPressed += KnobsPanel_PointerPressed; + _knobsPanel.PointerReleased += KnobsPanel_PointerReleased; + _knobsPanel.PointerMoved += KnobsPanel_PointerMoved; + + if (IsChecked.HasValue) + { + UpdateKnobPos(IsChecked.Value); + } + } + + private void KnobsPanel_PointerPressed(object sender, Input.PointerPressedEventArgs e) + { + _switchStartPoint = e.GetPosition(_switchKnob); + _initLeft = Canvas.GetLeft(_knobsPanel); + _isDragging = false; + _knobsPanelPressed = true; + } + + private void KnobsPanel_PointerReleased(object sender, Input.PointerReleasedEventArgs e) + { + if (_isDragging) + { + bool shouldBecomeChecked = Canvas.GetLeft(_knobsPanel) >= (_switchKnob.Bounds.Width / 2); + _knobsPanel.ClearValue(Canvas.LeftProperty); + + PseudoClasses.Set(":dragging", false); + + if (shouldBecomeChecked == IsChecked) + { + UpdateKnobPos(shouldBecomeChecked); + } + else + { + IsChecked = shouldBecomeChecked; + } + } + else + { + base.Toggle(); + } + + _isDragging = false; + + _knobsPanelPressed = false; + } + + private void KnobsPanel_PointerMoved(object sender, Input.PointerEventArgs e) + { + if (_knobsPanelPressed) + { + var difference = e.GetPosition(_switchKnob) - _switchStartPoint; + + if ((!_isDragging) && (System.Math.Abs(difference.X) > 3)) + { + _isDragging = true; + PseudoClasses.Set(":dragging", true); + } + + if (_isDragging) + { + Canvas.SetLeft(_knobsPanel, System.Math.Min(_switchKnob.Bounds.Width, System.Math.Max(0, (_initLeft + difference.X)))); + } + } + } + + protected override void Toggle() + { + if ((_switchKnob != null) && (!_switchKnob.IsPointerOver)) + { + base.Toggle(); + } + } + + protected void UpdateKnobPos(bool value) + { + if ((_switchKnob != null) && (_knobsPanel != null)) + { + if (value) + { + Canvas.SetLeft(_knobsPanel, _switchKnob.Bounds.Width); + } + else + { + Canvas.SetLeft(_knobsPanel, 0); + } + } + } } } 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.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index 893d64f505..e7f8fb1641 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -155,8 +155,7 @@ - - - @@ -263,10 +258,6 @@ - - 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/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index e247a4241a..af6d2674b6 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -48,9 +48,13 @@ namespace Avalonia.X11 var pwa = (IntPtr*)prop; var wa = new PixelRect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32()); - - foreach (var s in screens) + + foreach (var s in screens) + { s.WorkingArea = s.Bounds.Intersect(wa); + if (s.WorkingArea.Width <= 0 || s.WorkingArea.Height <= 0) + s.WorkingArea = s.Bounds; + } XFree(prop); return screens; @@ -134,8 +138,14 @@ namespace Avalonia.X11 settings.GlobalScaleFactor) }); } - - Screens = new[] {new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null, settings.GlobalScaleFactor)}; + else + { + Screens = new[] + { + new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null, + settings.GlobalScaleFactor) + }; + } } public X11Screen[] Screens { get; } 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/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 03832d3063..d574732e3d 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -86,11 +86,15 @@ namespace Avalonia.Shared.PlatformSupport #if DEBUG if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) { - lock(_lock) + lock (_lock) + { if (!IsDisposed) + { Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " - + Environment.StackTrace - + "\n\nBlob created by " + _backtrace); + + Environment.StackTrace + + "\n\nBlob created by " + _backtrace); + } + } } #endif DoDispose(); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a510763f64..d818e683c3 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -629,8 +629,8 @@ namespace Avalonia.Skia var tileTransform = tileBrush.TileMode != TileMode.None - ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) - : SKMatrix.MakeIdentity(); + ? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.CreateIdentity(); SKShaderTileMode tileX = tileBrush.TileMode == TileMode.None @@ -655,7 +655,7 @@ namespace Avalonia.Skia SKMatrix.Concat( ref paintTransform, tileTransform, - SKMatrix.MakeScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); using (var shader = image.ToShader(tileX, tileY, paintTransform)) { 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/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index ade659f5eb..d1f8d6a779 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -35,6 +35,7 @@ namespace Avalonia.Skia IsAntialias = true, LcdRenderText = true, SubpixelText = true, + IsLinearText = true, Typeface = glyphTypeface.Typeface, TextSize = (float)fontSize, TextAlign = textAlignment.ToSKTextAlign() 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/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 9b73174006..e0b7019672 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -82,10 +82,10 @@ namespace Avalonia.Skia var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, - new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); + new GRGlFramebufferInfo((uint)fb, SKColorType.Rgba8888.ToGlSizedFormat())); var surface = SKSurface.Create(_grContext, renderTarget, glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, - GRPixelConfig.Rgba8888.ToColorType()); + SKColorType.Rgba8888); success = true; return new GlGpuSession(_grContext, renderTarget, surface, glSession); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index de188f42bd..9278de2137 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -15,19 +15,18 @@ namespace Avalonia.Skia using (context.MakeCurrent()) { using (var iface = context.Version.Type == GlProfileType.OpenGL ? - GRGlInterface.AssembleGlInterface((_, proc) => context.GlInterface.GetProcAddress(proc)) : - GRGlInterface.AssembleGlesInterface((_, proc) => context.GlInterface.GetProcAddress(proc))) + GRGlInterface.CreateOpenGl(proc => context.GlInterface.GetProcAddress(proc)) : + GRGlInterface.CreateGles(proc => context.GlInterface.GetProcAddress(proc))) { - _grContext = GRContext.Create(GRBackend.OpenGL, iface); + _grContext = GRContext.CreateGl(iface); if (maxResourceBytes.HasValue) { - _grContext.GetResourceCacheLimits(out var maxResources, out _); - _grContext.SetResourceCacheLimits(maxResources, maxResourceBytes.Value); + _grContext.SetResourceCacheLimit(maxResourceBytes.Value); } } } } - + public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) { foreach (var surface in surfaces) diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 0bc5dd56ac..66b7fdea13 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -157,12 +157,12 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format); } - private static readonly SKPaint s_paint = new SKPaint + private static readonly SKFont s_font = new SKFont { - TextEncoding = SKTextEncoding.GlyphId, - IsAntialias = true, - IsStroke = false, - SubpixelText = true + Subpixel = true, + Edging = SKFontEdging.Antialias, + Hinting = SKFontHinting.Full, + LinearMetrics = true }; private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder(); @@ -176,8 +176,8 @@ namespace Avalonia.Skia var typeface = glyphTypeface.Typeface; - s_paint.TextSize = (float)glyphRun.FontRenderingEmSize; - s_paint.Typeface = typeface; + s_font.Size = (float)glyphRun.FontRenderingEmSize; + s_font.Typeface = typeface; SKTextBlob textBlob; @@ -190,7 +190,7 @@ namespace Avalonia.Skia { if (glyphTypeface.IsFixedPitch) { - s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); + s_textBlobBuilder.AddRun(glyphRun.GlyphIndices.Buffer.Span, s_font); textBlob = s_textBlobBuilder.Build(); @@ -198,7 +198,7 @@ namespace Avalonia.Skia } else { - var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0); + var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_font, count, 0); var positions = buffer.GetPositionSpan(); @@ -223,7 +223,7 @@ namespace Avalonia.Skia } else { - var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count); + var buffer = s_textBlobBuilder.AllocatePositionedRun(s_font, count); var glyphPositions = buffer.GetPositionSpan(); 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/src/Windows/Avalonia.Win32/FramebufferManager.cs b/src/Windows/Avalonia.Win32/FramebufferManager.cs index 87c5a1bb02..6969a49dad 100644 --- a/src/Windows/Avalonia.Win32/FramebufferManager.cs +++ b/src/Windows/Avalonia.Win32/FramebufferManager.cs @@ -5,7 +5,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class FramebufferManager : IFramebufferPlatformSurface + class FramebufferManager : IFramebufferPlatformSurface, IDisposable { private readonly IntPtr _hwnd; private WindowFramebuffer _fb; @@ -17,17 +17,25 @@ namespace Avalonia.Win32 public ILockedFramebuffer Lock() { - UnmanagedMethods.RECT rc; - UnmanagedMethods.GetClientRect(_hwnd, out rc); - var width = rc.right - rc.left; - var height = rc.bottom - rc.top; - if ((_fb == null || _fb.Size.Width != width || _fb.Size.Height != height) && width > 0 && height > 0) + UnmanagedMethods.GetClientRect(_hwnd, out var rc); + + var width = Math.Max(1, rc.right - rc.left); + var height = Math.Max(1, rc.bottom - rc.top); + + if ((_fb == null || _fb.Size.Width != width || _fb.Size.Height != height)) { _fb?.Deallocate(); _fb = null; _fb = new WindowFramebuffer(_hwnd, new PixelSize(width, height)); } + return _fb; } + + public void Dispose() + { + _fb?.Deallocate(); + _fb = null; + } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6e9cb81b11..0ee1342d27 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -389,6 +389,8 @@ namespace Avalonia.Win32 UnregisterClass(_className, GetModuleHandle(null)); _className = null; } + + _framebuffer.Dispose(); } public void Invalidate(Rect rect) 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); + } + } +}