From d0a74bdf42ee5eaa51f22bad10c710ddeb4f69ba Mon Sep 17 00:00:00 2001 From: gabornemeth Date: Wed, 3 Nov 2021 20:55:04 +0100 Subject: [PATCH 01/12] type of the TreeView's items can be customized --- src/Avalonia.Controls/TreeView.cs | 14 ++++-- src/Avalonia.Controls/TreeViewItem.cs | 8 +++- .../TreeViewTests.cs | 46 +++++++++++++++++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 78cd22ae32..03f6fd3aaa 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -392,14 +392,22 @@ namespace Avalonia.Controls /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - var result = new TreeItemContainerGenerator( + var result = CreateTreeItemContainerGenerator(); + result.Index.Materialized += ContainerMaterialized; + return result; + } + + protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() => + CreateTreeItemContainerGenerator(); + + protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() + { + return new TreeItemContainerGenerator( this, TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, TreeViewItem.ItemsProperty, TreeViewItem.IsExpandedProperty); - result.Index.Materialized += ContainerMaterialized; - return result; } /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 8ce258b546..88fb3cd9f1 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -92,9 +92,13 @@ namespace Avalonia.Controls (ITreeItemContainerGenerator)base.ItemContainerGenerator; /// - protected override IItemContainerGenerator CreateItemContainerGenerator() + protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator(); + + /// + protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() + where TVItem: TreeViewItem, new() { - return new TreeItemContainerGenerator( + return new TreeItemContainerGenerator( this, TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 72ba3ab273..2169b15cad 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Collections; +using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -24,7 +25,7 @@ namespace Avalonia.Controls.UnitTests public class TreeViewTests { MouseTestHelper _mouse = new MouseTestHelper(); - + [Fact] public void Items_Should_Be_Created() { @@ -675,7 +676,7 @@ namespace Avalonia.Controls.UnitTests Assert.Same(node, focus.Current); } } - + [Fact] public void Keyboard_Navigation_Should_Not_Crash_If_Selected_Item_Is_not_In_Tree() { @@ -1166,6 +1167,34 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(target.ItemContainerGenerator.Index.Containers); } + [Fact] + public void Can_Use_Derived_TreeViewItem() + { + var tree = CreateTestTreeData(); + var target = new DerivedTreeViewWithDerivedTreeViewItems + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + ApplyTemplates(target); + + // Verify that all items are DerivedTreeViewItem + VerifyItemType(target.ItemContainerGenerator); + + void VerifyItemType(ITreeItemContainerGenerator containerGenerator) + { + foreach (var container in containerGenerator.Index.Containers) + { + var item = Assert.IsType(container); + if (item.ItemCount > 0) + { + VerifyItemType(item.ItemContainerGenerator); + } + } + } + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); @@ -1376,6 +1405,17 @@ namespace Avalonia.Controls.UnitTests { } + private class DerivedTreeViewWithDerivedTreeViewItems : TreeView + { + protected override ITreeItemContainerGenerator CreateTreeItemContainerGenerator() => + CreateTreeItemContainerGenerator(); + } + + private class DerivedTreeViewItem : TreeViewItem + { + protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator(); + } + private class TestDataContext : INotifyPropertyChanged { private string _selectedItem; @@ -1398,7 +1438,7 @@ namespace Avalonia.Controls.UnitTests } public event PropertyChangedEventHandler PropertyChanged; - + } } } From 517d7eba712dc4aed2d57c7ff402325ccd8d2d89 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 30 Nov 2021 14:54:16 +0100 Subject: [PATCH 02/12] Handle TabStopps --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 160 +++++++++++++++----- 1 file changed, 122 insertions(+), 38 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5f4980e461..60f8311441 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; +using System.Threading; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; using SkiaSharp; namespace Avalonia.Skia @@ -13,6 +14,25 @@ namespace Avalonia.Skia /// internal class FormattedTextImpl : IFormattedTextImpl { + private static readonly ThreadLocal t_builder = new ThreadLocal(() => new SKTextBlobBuilder()); + + private const float MAX_LINE_WIDTH = 10000; + + private readonly List> _foregroundBrushes = + new List>(); + private readonly List _lines = new List(); + private readonly SKPaint _paint; + private readonly List _rects = new List(); + public string Text { get; } + private readonly TextWrapping _wrapping; + private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); + private float _lineHeight = 0; + private float _lineOffset = 0; + private Rect _bounds; + private List _skiaLines; + private ReadOnlySlice _glyphs; + private ReadOnlySlice _advances; + public FormattedTextImpl( string text, Typeface typeface, @@ -23,12 +43,9 @@ namespace Avalonia.Skia IReadOnlyList spans) { Text = text ?? string.Empty; - - // Replace 0 characters with zero-width spaces (200B) - Text = Text.Replace((char)0, (char)0x200B); - - var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl; + UpdateGlyphInfo(Text, typeface.GlyphTypeface, (float)fontSize); + _paint = new SKPaint { TextEncoding = SKTextEncoding.Utf16, @@ -37,7 +54,7 @@ namespace Avalonia.Skia LcdRenderText = true, SubpixelText = true, IsLinearText = true, - Typeface = glyphTypeface.Typeface, + Typeface = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).Typeface, TextSize = (float)fontSize, TextAlign = textAlignment.ToSKTextAlign() }; @@ -195,6 +212,32 @@ namespace Avalonia.Skia return Text; } + private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint) + { + var glyphs = _glyphs.Buffer.Span.Slice(start, length); + var advances = _advances.Buffer.Span.Slice(start, length); + var builder = t_builder.Value; + + var buffer = builder.AllocateHorizontalRun(_paint.ToFont(), length, 0); + + buffer.SetGlyphs(glyphs); + + var positions = buffer.GetPositionSpan(); + + var pos = 0f; + + for (int i = 0; i < advances.Length; i++) + { + positions[i] = pos; + + pos += advances[i]; + } + + var blob = builder.Build(); + + canvas.DrawText(blob, x, y, paint); + } + internal void Draw(DrawingContextImpl context, SKCanvas canvas, SKPoint origin, @@ -244,16 +287,15 @@ namespace Avalonia.Skia if (!hasCusomFGBrushes) { - var subString = Text.Substring(line.Start, line.Length); - canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint); + DrawTextBlob(line.Start, line.Length, x, origin.Y + line.Top + _lineOffset, canvas, paint); } else { float currX = x; - string subStr; float measure; int len; float factor; + switch (paint.TextAlign) { case SKTextAlign.Left: @@ -270,7 +312,7 @@ namespace Avalonia.Skia } var textLine = Text.Substring(line.Start, line.Length); - currX -= textLine.Length == 0 ? 0 : paint.MeasureText(textLine) * factor; + currX -= textLine.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor; for (int i = line.Start; i < line.Start + line.Length;) { @@ -288,13 +330,12 @@ namespace Avalonia.Skia currentWrapper = foreground; } - subStr = Text.Substring(i, len); - measure = paint.MeasureText(subStr); + measure = MeasureText(i, len); currX += measure * factor; - ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); + ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); - canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); + DrawTextBlob(i, len, currX, origin.Y + line.Top + _lineOffset, canvas, paint); i += len; currX += measure * (1 - factor); @@ -310,21 +351,6 @@ namespace Avalonia.Skia } } - private const float MAX_LINE_WIDTH = 10000; - - private readonly List> _foregroundBrushes = - new List>(); - private readonly List _lines = new List(); - private readonly SKPaint _paint; - private readonly List _rects = new List(); - public string Text { get; } - private readonly TextWrapping _wrapping; - private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); - private float _lineHeight = 0; - private float _lineOffset = 0; - private Rect _bounds; - private List _skiaLines; - private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper, ref IDisposable curr, SKPaint paint, bool canUseLcdRendering) { @@ -352,9 +378,8 @@ namespace Avalonia.Skia } else { - float measuredWidth; string subText = textInput.Substring(textIndex, stop - textIndex); - lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth); + lengthBreak = (int)paint.BreakText(subText, maxWidth, out _); } //Check for white space or line breakers before the lengthBreak @@ -468,8 +493,7 @@ namespace Avalonia.Skia for (int i = line.Start; i < line.Start + line.TextLength; i++) { - var c = Text[i]; - var w = line.IsEmptyTrailingLine ? 0 :_paint.MeasureText(Text[i].ToString()); + var w = line.IsEmptyTrailingLine ? 0 : _advances[i]; _rects.Add(new Rect( prevRight, @@ -554,7 +578,7 @@ namespace Avalonia.Skia // This seems like the best measure of full vertical extent // matches Direct2D line height - _lineHeight = mDescent - mAscent; + _lineHeight = mDescent - mAscent + metrics.Leading; // Rendering is relative to baseline _lineOffset = (-metrics.Ascent); @@ -585,7 +609,7 @@ namespace Avalonia.Skia line.Start = curOff; line.TextLength = measured; subString = Text.Substring(line.Start, line.TextLength); - lineWidth = _paint.MeasureText(subString); + lineWidth = MeasureText(line.Start, line.TextLength); line.Length = measured - trailingnumber; line.Width = lineWidth; line.Height = _lineHeight; @@ -608,8 +632,7 @@ namespace Avalonia.Skia AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine(); lastLine.TextLength = lengthDiff; lastLine.Start = curOff - lengthDiff; - var lastLineSubString = Text.Substring(line.Start, line.TextLength); - var lastLineWidth = _paint.MeasureText(lastLineSubString); + var lastLineWidth = MeasureText(line.Start, line.TextLength); lastLine.Length = 0; lastLine.Width = lastLineWidth; lastLine.Height = _lineHeight; @@ -668,6 +691,67 @@ namespace Avalonia.Skia } } + private float MeasureText(int start, int length) + { + var width = 0f; + + for (int i = start; i < length; i++) + { + var advance = _advances[i]; + + width += advance; + } + + return width; + } + + private void UpdateGlyphInfo(string text, GlyphTypeface glyphTypeface, float fontSize) + { + var glyphs = new ushort[text.Length]; + var advances = new float[text.Length]; + + var scale = fontSize / glyphTypeface.DesignEmHeight; + var width = 0f; + var characters = text.AsSpan(); + + for (int i = 0; i < characters.Length; i++) + { + var c = characters[i]; + float advance; + ushort glyph; + + switch (c) + { + case (char)0: + { + glyph = glyphTypeface.GetGlyph(0x200B); + advance = 0; + break; + } + case '\t': + { + glyph = glyphTypeface.GetGlyph(' '); + advance = glyphTypeface.GetGlyphAdvance(glyph) * scale * 4; + break; + } + default: + { + glyph = glyphTypeface.GetGlyph(c); + advance = glyphTypeface.GetGlyphAdvance(glyph) * scale; + break; + } + } + + glyphs[i] = glyph; + advances[i] = advance; + + width += advance; + } + + _glyphs = new ReadOnlySlice(glyphs); + _advances = new ReadOnlySlice(advances); + } + private float TransformX(float originX, float lineWidth, SKTextAlign align) { float x = 0; From 03396dfa680d3b62ce1e6b17dabb342a14de6c47 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Dec 2021 12:43:40 +0100 Subject: [PATCH 03/12] Remove redundant SubString usage --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 60f8311441..a632376637 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -311,8 +311,7 @@ namespace Avalonia.Skia throw new ArgumentOutOfRangeException(); } - var textLine = Text.Substring(line.Start, line.Length); - currX -= textLine.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor; + currX -= line.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor; for (int i = line.Start; i < line.Start + line.Length;) { From 2cfd084db32d672f9776c3e35aa514eed35131bc Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Dec 2021 19:43:53 +0100 Subject: [PATCH 04/12] Fix empty text render --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index a632376637..2be8caaa29 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -214,6 +214,11 @@ namespace Avalonia.Skia private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint) { + if(length == 0) + { + return; + } + var glyphs = _glyphs.Buffer.Span.Slice(start, length); var advances = _advances.Buffer.Span.Slice(start, length); var builder = t_builder.Value; @@ -235,7 +240,10 @@ namespace Avalonia.Skia var blob = builder.Build(); - canvas.DrawText(blob, x, y, paint); + if(blob != null) + { + canvas.DrawText(blob, x, y, paint); + } } internal void Draw(DrawingContextImpl context, From 58e744050cf7ed1e90527df38a5915e015dc487c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Dec 2021 20:06:23 +0100 Subject: [PATCH 05/12] Fix MeasureText --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 2be8caaa29..81a79201d0 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -702,7 +702,7 @@ namespace Avalonia.Skia { var width = 0f; - for (int i = start; i < length; i++) + for (int i = start; i < start + length; i++) { var advance = _advances[i]; From b9d97c1127bb4a893ec66b90673edba0bf9deb1d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 6 Dec 2021 17:50:12 +0000 Subject: [PATCH 06/12] Update readme.md --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index a1cdb6fe9d..460005cc48 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,4 @@ +[!Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) From 392c4a1a63cacf45fdf62bfe03f172274ffb6dcc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 6 Dec 2021 17:50:24 +0000 Subject: [PATCH 07/12] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 460005cc48..a8a6399f2f 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -[!Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) +[![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) From d6a16903f3aabb09a16b2a4f8390053214731dcc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 6 Dec 2021 18:30:56 +0000 Subject: [PATCH 08/12] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 687355d825..9e28f532be 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,6 @@ contact_links: - name: Questions, Discussions, Ideas url: https://github.com/AvaloniaUI/Avalonia/discussions/new about: Please ask and answer questions here. - - name: Avalonia Community Support on Gitter - url: https://gitter.im/AvaloniaUI/Avalonia + - name: Avalonia Community Support on Telegram + url: https://t.me/Avalonia about: Please ask and answer questions here. From c9df7ae46eb2833670fed13c6482efdda7608591 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 6 Dec 2021 18:31:49 +0000 Subject: [PATCH 09/12] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcf95ce33c..cb5a7d7897 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Before You Start -Drop into our [gitter chat room](https://gitter.im/AvaloniaUI/Avalonia) and let us know what you're thinking of doing. We might be able to give you guidance or let you know if someone else is already working on the feature. +Drop into our [telegram group](https://t.me/Avalonia) or [gitter chat room](https://gitter.im/AvaloniaUI/Avalonia) and let us know what you're thinking of doing. We might be able to give you guidance or let you know if someone else is already working on the feature. ## Style From 621bb5688de8147610c206aae0369a6aa9f8d5a3 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Tue, 7 Dec 2021 01:32:20 +0300 Subject: [PATCH 10/12] Merge pull request #6836 from RomanSoloweow/master Border StrokeDashArray --- src/Avalonia.Controls/Border.cs | 74 ++++++++++++++++++- .../Utils/BorderRenderHelper.cs | 52 ++++++++++--- 2 files changed, 112 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 0c6949465b..ee67f303f3 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,3 +1,5 @@ +using Avalonia.Collections; +using Avalonia.Controls.Shapes; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Media; @@ -41,7 +43,31 @@ namespace Avalonia.Controls /// public static readonly StyledProperty BoxShadowProperty = AvaloniaProperty.Register(nameof(BoxShadow)); - + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderDashOffsetProperty = + AvaloniaProperty.Register(nameof(BorderDashOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty?> BorderDashArrayProperty = + AvaloniaProperty.Register?>(nameof(BorderDashArray)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderLineCapProperty = + AvaloniaProperty.Register(nameof(BorderLineCap), PenLineCap.Flat); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderLineJoinProperty = + AvaloniaProperty.Register(nameof(BorderLineJoin), PenLineJoin.Miter); + private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); /// @@ -54,6 +80,10 @@ namespace Avalonia.Controls BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty, + BorderDashArrayProperty, + BorderLineCapProperty, + BorderLineJoinProperty, + BorderDashOffsetProperty, BoxShadowProperty); AffectsMeasure(BorderThicknessProperty); } @@ -76,6 +106,15 @@ namespace Avalonia.Controls set { SetValue(BorderBrushProperty, value); } } + /// + /// Gets or sets a collection of values that indicate the pattern of dashes and gaps that is used to outline shapes. + /// + public AvaloniaList? BorderDashArray + { + get { return GetValue(BorderDashArrayProperty); } + set { SetValue(BorderDashArrayProperty, value); } + } + /// /// Gets or sets the thickness of the border. /// @@ -85,6 +124,33 @@ namespace Avalonia.Controls set { SetValue(BorderThicknessProperty, value); } } + /// + /// Gets or sets a value that specifies the distance within the dash pattern where a dash begins. + /// + public double BorderDashOffset + { + get { return GetValue(BorderDashOffsetProperty); } + set { SetValue(BorderDashOffsetProperty, value); } + } + + /// + /// Gets or sets a enumeration value that describes the shape at the ends of a line. + /// + public PenLineCap BorderLineCap + { + get { return GetValue(BorderLineCapProperty); } + set { SetValue(BorderLineCapProperty, value); } + } + + /// + /// Gets or sets a enumeration value that specifies the type of join that is used at the vertices of a Shape. + /// + public PenLineJoin BorderLineJoin + { + get { return GetValue(BorderLineJoinProperty); } + set { SetValue(BorderLineJoinProperty, value); } + } + /// /// Gets or sets the radius of the border rounded corners. /// @@ -93,7 +159,7 @@ namespace Avalonia.Controls get { return GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } } - + /// /// Gets or sets the box shadow effect parameters /// @@ -102,7 +168,7 @@ namespace Avalonia.Controls get => GetValue(BoxShadowProperty); set => SetValue(BoxShadowProperty, value); } - + /// /// Renders the control. /// @@ -110,7 +176,7 @@ namespace Avalonia.Controls public override void Render(DrawingContext context) { _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush, - BoxShadow); + BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray); } /// diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index f8ab58d46e..bae787f0ed 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -1,8 +1,10 @@ using System; +using Avalonia.Collections; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.Utilities; +using JetBrains.Annotations; namespace Avalonia.Controls.Utils { @@ -15,8 +17,13 @@ namespace Avalonia.Controls.Utils private Size _size; private Thickness _borderThickness; private CornerRadius _cornerRadius; + private AvaloniaList _borderDashArray; + private double _borderDashOffset; + private PenLineCap _borderLineCap; + private PenLineJoin _borderJoin; private bool _initialized; + void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius) { _backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetService() @@ -60,7 +67,8 @@ namespace Avalonia.Controls.Utils if (boundRect.Width != 0 && innerRect.Height != 0) { - var borderGeometryKeypoints = new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false); + var borderGeometryKeypoints = + new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false); var borderGeometry = new StreamGeometry(); using (var ctx = borderGeometry.Open()) @@ -84,17 +92,22 @@ namespace Avalonia.Controls.Utils public void Render(DrawingContext context, Size finalSize, Thickness borderThickness, CornerRadius cornerRadius, - IBrush background, IBrush borderBrush, BoxShadows boxShadows) + IBrush background, IBrush borderBrush, BoxShadows boxShadows, double borderDashOffset = 0, + PenLineCap borderLineCap = PenLineCap.Flat, PenLineJoin borderLineJoin = PenLineJoin.Miter, + AvaloniaList borderDashArray = null) { if (_size != finalSize || _borderThickness != borderThickness || _cornerRadius != cornerRadius || !_initialized) Update(finalSize, borderThickness, cornerRadius); - RenderCore(context, background, borderBrush, boxShadows); + RenderCore(context, background, borderBrush, boxShadows, borderDashOffset, borderLineCap, borderLineJoin, + borderDashArray); } - void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows) + void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows, + double borderDashOffset, PenLineCap borderLineCap, PenLineJoin borderLineJoin, + AvaloniaList borderDashArray) { if (_useComplexRendering) { @@ -115,11 +128,25 @@ namespace Avalonia.Controls.Utils var borderThickness = _borderThickness.Top; IPen pen = null; + + ImmutableDashStyle? dashStyle = null; + + if (borderDashArray != null && borderDashArray.Count > 0) + { + dashStyle = new ImmutableDashStyle(borderDashArray, borderDashOffset); + } + if (borderBrush != null && borderThickness > 0) { - pen = new ImmutablePen(borderBrush.ToImmutable(), borderThickness); + pen = new ImmutablePen( + borderBrush.ToImmutable(), + borderThickness, + dashStyle, + borderLineCap, + borderLineJoin); } + var rect = new Rect(_size); if (!MathUtilities.IsZero(borderThickness)) rect = rect.Deflate(borderThickness * 0.5); @@ -130,7 +157,8 @@ namespace Avalonia.Controls.Utils } } - private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderGeometryKeypoints keypoints) + private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, + BorderGeometryKeypoints keypoints) { context.BeginFigure(keypoints.TopLeft, true); @@ -184,7 +212,8 @@ namespace Avalonia.Controls.Utils private class BorderGeometryKeypoints { - internal BorderGeometryKeypoints(Rect boundRect, Thickness borderThickness, CornerRadius cornerRadius, bool inner) + internal BorderGeometryKeypoints(Rect boundRect, Thickness borderThickness, CornerRadius cornerRadius, + bool inner) { var left = 0.5 * borderThickness.Left; var top = 0.5 * borderThickness.Top; @@ -206,10 +235,13 @@ namespace Avalonia.Controls.Utils topLeftX = Math.Max(0, cornerRadius.TopLeft - left) + boundRect.TopLeft.X; topRightX = boundRect.Width - Math.Max(0, cornerRadius.TopRight - top) + boundRect.TopLeft.X; rightTopY = Math.Max(0, cornerRadius.TopRight - right) + boundRect.TopLeft.Y; - rightBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomRight - bottom) + boundRect.TopLeft.Y; - bottomRightX = boundRect.Width - Math.Max(0, cornerRadius.BottomRight - right) + boundRect.TopLeft.X; + rightBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomRight - bottom) + + boundRect.TopLeft.Y; + bottomRightX = boundRect.Width - Math.Max(0, cornerRadius.BottomRight - right) + + boundRect.TopLeft.X; bottomLeftX = Math.Max(0, cornerRadius.BottomLeft - left) + boundRect.TopLeft.X; - leftBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomLeft - bottom) + boundRect.TopLeft.Y; + leftBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomLeft - bottom) + + boundRect.TopLeft.Y; } else { From 05fdbd88ef94ecf90edc7daad9cc3fb354fa2244 Mon Sep 17 00:00:00 2001 From: usUyGBx <64971385+usUyGBx@users.noreply.github.com> Date: Tue, 7 Dec 2021 02:33:06 +0300 Subject: [PATCH 11/12] Send one notifi per seq range in RemoveAll method of AvaloniaList (optimized) (#5754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Send one notification per sequential range in RemoveAll method of AvaloniaList (optimized) * Simplified * Make unit tests a bit more understandable. Co-authored-by: Gitea Co-authored-by: Dariusz Komosiński Co-authored-by: Steven Kirk Co-authored-by: Steven Kirk --- src/Avalonia.Base/Collections/AvaloniaList.cs | 19 ++- .../Collections/AvaloniaListTests.cs | 152 ++++++++++++++++++ 2 files changed, 168 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 2f1cb2888e..5782159813 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -503,11 +503,24 @@ namespace Avalonia.Collections { Contract.Requires(items != null); - foreach (var i in items) + var hItems = new HashSet(items); + + int counter = 0; + for (int i = _inner.Count - 1; i >= 0; --i) { - // TODO: Optimize to only send as many notifications as necessary. - Remove(i); + if (hItems.Contains(_inner[i])) + { + counter += 1; + } + else if(counter > 0) + { + RemoveRange(i + 1, counter); + counter = 0; + } } + + if (counter > 0) + RemoveRange(0, counter); } /// diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index d5ac01a092..f5ae4cc1e0 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -357,5 +357,157 @@ namespace Avalonia.Base.UnitTests.Collections Assert.Equal(target, result); } + + [Fact] + public void RemoveAll_Should_Send_Single_Notification_For_Sequential_Range() + { + var target = new AvaloniaList(Enumerable.Range(0, 10).Select(x => $"Item {x}")); + var toRemove = new[] { "Item 5", "Item 6", "Item 7" }; + var raised = 0; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action); + Assert.Equal(5, e.OldStartingIndex); + Assert.Equal(toRemove, e.OldItems); + ++raised; + }; + + target.RemoveAll(toRemove); + + Assert.Equal(1, raised); + } + + [Fact] + public void RemoveAll_Should_Send_Single_Notification_For_Sequential_Range_With_Duplicate_Source_Items() + { + var items = Enumerable.Range(0, 20).Select(x => $"Item {x / 2}"); + var target = new AvaloniaList(items); + var toRemove = new[] { "Item 5", "Item 6", "Item 7" }; + var raised = 0; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action); + Assert.Equal(10, e.OldStartingIndex); + + Assert.Equal(new[] + { + "Item 5", + "Item 5", + "Item 6", + "Item 6", + "Item 7", + "Item 7", + }, e.OldItems); + ++raised; + }; + + target.RemoveAll(toRemove); + + Assert.Equal(1, raised); + } + + [Fact] + public void RemoveAll_Should_Send_Multiple_Notifications_For_Non_Sequential_Range() + { + var target = new AvaloniaList(Enumerable.Range(0, 10).Select(x => $"Item {x}")); + var raised = 0; + var toRemove = new[] + { + new[] { "Item 2", "Item 3" }, + new[] { "Item 5", "Item 6" } + }; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action); + + if (raised == 0) + { + Assert.Equal(5, e.OldStartingIndex); + Assert.Equal(toRemove[1], e.OldItems); + } + else + { + Assert.Equal(2, e.OldStartingIndex); + Assert.Equal(toRemove[0], e.OldItems); + } + + ++raised; + }; + + target.RemoveAll(toRemove[0].Concat(toRemove[1])); + + Assert.Equal(2, raised); + } + + [Fact] + public void RemoveAll_Should_Send_Multiple_Notifications_For_Sequential_Range_With_Nonsequential_Duplicate_Source_Items() + { + var items = Enumerable.Range(0, 10).Select(x => $"Item {x}"); + var target = new AvaloniaList(items.Concat(items)); + var raised = 0; + var toRemove = new[] { "Item 5", "Item 6", "Item 7" }; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action); + + if (raised == 0) + { + Assert.Equal(15, e.OldStartingIndex); + Assert.Equal(toRemove, e.OldItems); + } + else + { + Assert.Equal(5, e.OldStartingIndex); + Assert.Equal(toRemove, e.OldItems); + } + + ++raised; + }; + + target.RemoveAll(toRemove); + + Assert.Equal(2, raised); + } + + [Fact] + public void RemoveAll_Should_Not_Send_Notification_For_Items_Not_Present() + { + var target = new AvaloniaList(Enumerable.Range(0, 10).Select(x => $"Item {x}")); + var toRemove = new[] { "Item 5", "Item 6", "Item 7", "Not present" }; + var raised = 0; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action); + Assert.Equal(5, e.OldStartingIndex); + Assert.Equal(toRemove.Take(3).ToArray(), e.OldItems); + ++raised; + }; + + target.RemoveAll(toRemove); + + Assert.Equal(1, raised); + } + + [Fact] + public void RemoveAll_Should_Handle_Empty_List() + { + var target = new AvaloniaList(); + var toRemove = new[] { "Item 5", "Item 6", "Item 7" }; + var raised = 0; + + target.CollectionChanged += (s, e) => + { + ++raised; + }; + + target.RemoveAll(toRemove); + + Assert.Equal(0, raised); + } } } From 931a79dfb7fd6bc813ae14bc96ae4c8575404146 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Dec 2021 00:57:24 +0100 Subject: [PATCH 12/12] Send a single Text change nofication when TextBlock.SelectedText is changed. (#5508) * Added failing tests for #838. * Fix Text events when setting SelectedText. Only raise a single property changed event on `Text` when setting `SelectedText`. It's a bit ugly but the whole of `TextBox` needs a refactor... Fixes #838. --- src/Avalonia.Controls/TextBox.cs | 55 +++++++++++++------ .../TextBoxTests.cs | 48 ++++++++++++++++ 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 0a29db555c..00fc6002d1 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -642,15 +642,31 @@ namespace Avalonia.Controls if (!string.IsNullOrEmpty(input)) { - DeleteSelection(); - caretIndex = CaretIndex; - text = Text ?? string.Empty; - SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); - CaretIndex += input.Length; - ClearSelection(); - if (IsUndoEnabled) + var oldText = _text; + + _ignoreTextChanges = true; + + try { - _undoRedoHelper.DiscardRedo(); + DeleteSelection(false); + caretIndex = CaretIndex; + text = Text ?? string.Empty; + SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); + CaretIndex += input.Length; + ClearSelection(); + if (IsUndoEnabled) + { + _undoRedoHelper.DiscardRedo(); + } + + if (_text != oldText) + { + RaisePropertyChanged(TextProperty, oldText, _text); + } + } + finally + { + _ignoreTextChanges = false; } } } @@ -1285,7 +1301,7 @@ namespace Avalonia.Controls CaretIndex = SelectionEnd; } - private bool DeleteSelection() + private bool DeleteSelection(bool raiseTextChanged = true) { if (!IsReadOnly) { @@ -1297,7 +1313,7 @@ namespace Avalonia.Controls var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); var text = Text; - SetTextInternal(text.Substring(0, start) + text.Substring(end)); + SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); CaretIndex = start; ClearSelection(); return true; @@ -1348,16 +1364,23 @@ namespace Avalonia.Controls return i; } - private void SetTextInternal(string value) + private void SetTextInternal(string value, bool raiseTextChanged = true) { - try + if (raiseTextChanged) { - _ignoreTextChanges = true; - SetAndRaise(TextProperty, ref _text, value); + try + { + _ignoreTextChanges = true; + SetAndRaise(TextProperty, ref _text, value); + } + finally + { + _ignoreTextChanges = false; + } } - finally + else { - _ignoreTextChanges = false; + _text = value; } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index cb20071860..23cae8fd0d 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -784,6 +784,54 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setting_SelectedText_Should_Fire_Single_Text_Changed_Notification() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123", + AcceptsReturn = true, + AcceptsTab = true, + SelectionStart = 1, + SelectionEnd = 3, + }; + + var values = new List(); + target.GetObservable(TextBox.TextProperty).Subscribe(x => values.Add(x)); + + target.SelectedText = "A"; + + Assert.Equal(new[] { "0123", "0A3" }, values); + } + } + + [Fact] + public void Entering_Text_With_SelectedText_Should_Fire_Single_Text_Changed_Notification() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123", + AcceptsReturn = true, + AcceptsTab = true, + SelectionStart = 1, + SelectionEnd = 3, + }; + + var values = new List(); + target.GetObservable(TextBox.TextProperty).Subscribe(x => values.Add(x)); + + RaiseTextEvent(target, "A"); + + Assert.Equal(new[] { "0123", "0A3" }, values); + } + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(),