From e8719e018db82a6998900e61bea3f11470eade9f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 4 Aug 2022 14:24:53 +0200 Subject: [PATCH 01/17] Fix GlyphRun.GetTralingWhitespaceLengthRightToLeft --- src/Avalonia.Base/Media/GlyphRun.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index cae7a8fe75..f207b3c636 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -786,14 +786,15 @@ namespace Avalonia.Media var clusterLength = 1; - while (i - 1 >= 0) + var j = i; + + while (j - 1 >= 0) { - var nextCluster = GlyphClusters[i - 1]; + var nextCluster = GlyphClusters[--j]; if (currentCluster == nextCluster) { - clusterLength++; - i--; + clusterLength++; continue; } @@ -808,7 +809,7 @@ namespace Avalonia.Media trailingWhitespaceLength += clusterLength; - glyphCount++; + glyphCount += clusterLength; } } From 0856cfaff985c15614b3bd647cf827b021a0444a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 4 Aug 2022 14:25:12 +0200 Subject: [PATCH 02/17] Remove redundant comment --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index fa1ab6fd29..7495956cd2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -266,10 +266,6 @@ namespace Avalonia.Media.TextFormatting { offset = Math.Max(0, currentPosition - shapedRun.Text.Start); } - //else - //{ - // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); - //} characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); From 678620422df2e6bea1b9c7e3832a998559af2083 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 19 Aug 2022 15:59:51 +0200 Subject: [PATCH 03/17] More RTL hit testing fixes --- .../Media/TextFormatting/TextLineImpl.cs | 249 ++++++++---------- .../Media/TextFormatting/TextRunBounds.cs | 2 +- .../Documents/InlineCollection.cs | 14 +- .../Media/TextFormatting/TextLineTests.cs | 83 +++++- 4 files changed, 202 insertions(+), 146 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7495956cd2..aba8008fb9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting var collapsingProperties = collapsingPropertiesList[0]; - if(collapsingProperties is null) + if (collapsingProperties is null) { return this; } @@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[i]; - if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; currentPosition += currentRun.TextSourceLength; @@ -213,14 +213,14 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if(j > _textRuns.Count - 1) + if (j > _textRuns.Count - 1) { break; } currentRun = _textRuns[j]; - if(currentDistance + currentRun.Size.Width <= distance) + if (currentDistance + currentRun.Size.Width <= distance) { currentDistance += currentRun.Size.Width; currentPosition -= currentRun.TextSourceLength; @@ -322,11 +322,11 @@ namespace Avalonia.Media.TextFormatting continue; } - + break; } - if(i > index) + if (i > index) { while (i >= index) { @@ -350,7 +350,7 @@ namespace Avalonia.Media.TextFormatting } } - if (currentPosition + currentRun.TextSourceLength >= characterIndex && + if (currentPosition + currentRun.TextSourceLength >= characterIndex && TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { return Math.Max(0, currentDistance + distance); @@ -530,6 +530,8 @@ namespace Avalonia.Media.TextFormatting double currentWidth = 0; var currentRect = Rect.Empty; + TextRunBounds lastRunBounds = default; + for (var index = 0; index < TextRuns.Count; index++) { if (TextRuns[index] is not DrawableTextRun currentRun) @@ -539,53 +541,93 @@ namespace Avalonia.Media.TextFormatting var characterLength = 0; var endX = startX; - var runWidth = 0.0; - TextRunBounds? currentRunBounds = null; var currentShapedRun = currentRun as ShapedTextCharacters; + TextRunBounds currentRunBounds; + + double combinedWidth; + + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = index; - startX += currentShapedRun.Size.Width; + var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) { - var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { break; } - startX += nextShapedRun.Size.Width; - rightToLeftIndex++; + + rightToLeftWidth += nextShapedRun.Size.Width; + + if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength) + { + break; + } + + currentShapedRun = nextShapedRun; } - if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds)) + startX = startX + rightToLeftWidth; + + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + + remainingLength -= currentRunBounds.Length; + currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; + endX = currentRunBounds.Rectangle.Right; + startX = currentRunBounds.Rectangle.Left; + + var rightToLeftRunBounds = new List { currentRunBounds }; + + for (int i = rightToLeftIndex - 1; i >= index; i--) { - startX = currentRunBounds!.Rectangle.Left; - endX = currentRunBounds.Rectangle.Right; + currentShapedRun = TextRuns[i] as ShapedTextCharacters; + + if(currentShapedRun == null) + { + continue; + } - runWidth = currentRunBounds.Rectangle.Width; + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + + rightToLeftRunBounds.Insert(0, currentRunBounds); + + remainingLength -= currentRunBounds.Length; + startX = currentRunBounds.Rectangle.Left; + + currentPosition += currentRunBounds.Length; } + combinedWidth = endX - startX; + + currentRect = new Rect(startX, 0, combinedWidth, Height); + currentDirection = FlowDirection.RightToLeft; + + if (!MathUtilities.IsZero(combinedWidth)) + { + result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); + } + + startX = endX; } else { if (currentShapedRun != null) { - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - - currentPosition += currentRun.TextSourceLength; - - continue; - } - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); currentPosition += offset; @@ -661,43 +703,46 @@ namespace Avalonia.Media.TextFormatting characterLength = NewLineLength; } - runWidth = endX - startX; - currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + combinedWidth = endX - startX; + + currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); currentPosition += characterLength; remainingLength -= characterLength; - } - if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0) - { - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + startX = endX; + + if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + { + currentRect = currentRect.WithWidth(currentWidth + combinedWidth); - var textBounds = result[result.Count - 1]; + var textBounds = result[result.Count - 1]; - textBounds.Rectangle = currentRect; + textBounds.Rectangle = currentRect; - textBounds.TextRunBounds.Add(currentRunBounds!); - } - else - { - currentRect = currentRunBounds!.Rectangle; + textBounds.TextRunBounds.Add(currentRunBounds); + } + else + { + currentRect = currentRunBounds.Rectangle; - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + } } + + lastRunBounds = currentRunBounds; } - currentWidth += runWidth; - + currentWidth += combinedWidth; if (remainingLength <= 0 || currentPosition >= characterIndex) { break; } - startX = endX; lastDirection = currentDirection; } @@ -852,105 +897,45 @@ namespace Avalonia.Media.TextFormatting return result; } - private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds) + private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) { - textRunBounds = null; + var startX = endX; - for (var index = runIndex; index >= 0; index--) - { - if (TextRuns[index] is not DrawableTextRun currentRun) - { - continue; - } + var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + currentPosition += offset; - currentPosition += currentRun.TextSourceLength; + var startIndex = currentRun.Text.Start + offset; - continue; - } + double startOffset; + double endOffset; - var characterLength = 0; - var endX = startX; - - if (currentRun is ShapedTextCharacters currentShapedRun) - { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - - currentPosition += offset; - - var startIndex = currentRun.Text.Start + offset; - double startOffset; - double endOffset; - - if (currentShapedRun.ShapedBuffer.IsLeftToRight) - { - if (currentPosition < startIndex) - { - startOffset = endOffset = 0; - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - } - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - } - - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; - - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - } - else - { - if (currentPosition + currentRun.TextSourceLength <= characterIndex) - { - endX -= currentRun.Size.Width; - } + startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - if (currentPosition < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; - characterLength = currentRun.TextSourceLength; - } - } + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } - - var runWidth = endX - startX; - - remainingLength -= characterLength; - - currentPosition += characterLength; - - textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + if (endX < startX) + { + (endX, startX) = (startX, endX); + } - return true; + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; } - return false; + var runWidth = endX - startX; + + return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) @@ -1532,7 +1517,7 @@ namespace Avalonia.Media.TextFormatting var textAlignment = _paragraphProperties.TextAlignment; var paragraphFlowDirection = _paragraphProperties.FlowDirection; - if(textAlignment == TextAlignment.Justify) + if (textAlignment == TextAlignment.Justify) { textAlignment = TextAlignment.Start; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs index 91150160ed..bdc7a1ca89 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs @@ -3,7 +3,7 @@ /// /// The bounding rectangle of text run /// - public sealed class TextRunBounds + public readonly struct TextRunBounds { /// /// Constructing TextRunBounds diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 565ed75ad9..15b4688809 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls.Documents private void AddText(string text) { - if(Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) + if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) { textBlock._text += text; } @@ -156,7 +156,17 @@ namespace Avalonia.Controls.Documents { foreach (var child in this) { - ((ISetLogicalParent)child).SetParent(parent); + var oldParent = child.Parent; + + if (oldParent != parent) + { + if (oldParent != null) + { + ((ISetLogicalParent)child).SetParent(null); + } + + ((ISetLogicalParent)child).SetParent(parent); + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index d744ede87d..251c850fc8 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -597,21 +597,82 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 20); - Assert.Equal(1, textBounds.Count); + Assert.Equal(2, textBounds.Count); - Assert.Equal(144.0234375, textBounds[0].Rectangle.Width); + Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 30); - Assert.Equal(1, textBounds.Count); + Assert.Equal(3, textBounds.Count); - Assert.Equal(216.03515625, textBounds[0].Rectangle.Width); + Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 40); - Assert.Equal(1, textBounds.Count); + Assert.Equal(4, textBounds.Count); + + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + } + } + + [Fact] + public void Should_GetTextRange() + { + var text = "שדגככעיחדגכAישדגשדגחייטYDASYWIWחיחלדשSAטויליHUHIUHUIDWKLאא'ק'קחליק/'וקןגגגלךשף'/קפוכדגכשדגשיח'/קטאגשד"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textRuns = textLine.TextRuns.Cast().ToList(); - Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds[0].Rectangle.Width); + var lineWidth = textLine.WidthIncludingTrailingWhitespace; + + var textBounds = textLine.GetTextBounds(0, text.Length); + + TextBounds lastBounds = null; + + var runBounds = textBounds.SelectMany(x => x.TextRunBounds).ToList(); + + Assert.Equal(textRuns.Count, runBounds.Count); + + for (var i = 0; i < textRuns.Count; i++) + { + var run = textRuns[i]; + var bounds = runBounds[i]; + + Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex); + Assert.Equal(run, bounds.TextRun); + Assert.Equal(run.Size.Width, bounds.Rectangle.Width); + } + + for (var i = 0; i < textBounds.Count; i++) + { + var currentBounds = textBounds[i]; + + if (lastBounds != null) + { + Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left); + } + + var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); + + Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width); + + lastBounds = currentBounds; + } + + var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); + + Assert.Equal(lineWidth, sumOfBoundsWidth); } } @@ -779,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); - Assert.Equal(1, textBounds.Count); + Assert.Equal(6, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 1); @@ -789,8 +850,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); - Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + Assert.Equal(2, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); @@ -799,8 +860,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); - Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + Assert.Equal(2, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); } } From 1f05ee41bbccbf17de73e5da68d32054b1b61e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 04:42:58 +0100 Subject: [PATCH 04/17] fallback from CenterOwner to CenterScreen when owner is null --- src/Avalonia.Controls/Window.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 03c66aff2b..db161968b0 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -830,10 +830,11 @@ namespace Avalonia.Controls var startupLocation = WindowStartupLocation; if (startupLocation == WindowStartupLocation.CenterOwner && - Owner is Window ownerWindow && - ownerWindow.WindowState == WindowState.Minimized) + (Owner is null || + (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) + ) { - // If startup location is CenterOwner, but owner is minimized then fall back + // If startup location is CenterOwner, but owner is null or minimized then fall back // to CenterScreen. This behavior is consistent with WPF. startupLocation = WindowStartupLocation.CenterScreen; } From 541b2e74dae3bf2d653be0b0ff45ad2f4eb5a9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 05:03:32 +0100 Subject: [PATCH 05/17] Use owner argument instead of Owner property --- src/Avalonia.Controls/Window.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index db161968b0..674b47f0d8 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -830,8 +830,8 @@ namespace Avalonia.Controls var startupLocation = WindowStartupLocation; if (startupLocation == WindowStartupLocation.CenterOwner && - (Owner is null || - (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) + (owner is null || + (owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) ) { // If startup location is CenterOwner, but owner is null or minimized then fall back From fa78c1c886be76f52adfd06f3fecc1ab7099c3c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 05:07:01 +0100 Subject: [PATCH 06/17] Revert original check to the Owner property --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 674b47f0d8..a7f82fc26f 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -831,7 +831,7 @@ namespace Avalonia.Controls if (startupLocation == WindowStartupLocation.CenterOwner && (owner is null || - (owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) + (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) ) { // If startup location is CenterOwner, but owner is null or minimized then fall back From e3238ed20d47b3cc5d1efd7c38b0f73d7fa419b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 05:09:19 +0100 Subject: [PATCH 07/17] Remove useless check, it's always true --- src/Avalonia.Controls/Window.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a7f82fc26f..f0369c416c 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -869,14 +869,11 @@ namespace Avalonia.Controls } else if (startupLocation == WindowStartupLocation.CenterOwner) { - if (owner != null) - { - var ownerSize = owner.FrameSize ?? owner.ClientSize; - var ownerRect = new PixelRect( - owner.Position, - PixelSize.FromSize(ownerSize, scaling)); - Position = ownerRect.CenterRect(rect).Position; - } + var ownerSize = owner.FrameSize ?? owner.ClientSize; + var ownerRect = new PixelRect( + owner.Position, + PixelSize.FromSize(ownerSize, scaling)); + Position = ownerRect.CenterRect(rect).Position; } } From 80edb2d1a4f5e5dc9357d31acb7d9120eff96549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 05:12:29 +0100 Subject: [PATCH 08/17] Minor improvement to reuse parent and spare a property get --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f0369c416c..b1d50cf430 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -668,7 +668,7 @@ namespace Avalonia.Controls Owner = parent; parent?.AddChild(this, false); - SetWindowStartupLocation(Owner?.PlatformImpl); + SetWindowStartupLocation(parent?.PlatformImpl); PlatformImpl?.Show(ShowActivated, false); Renderer?.Start(); From 8351932bf6635b8075c8a9be69de8f3da6d439c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sat, 24 Sep 2022 14:39:44 +0200 Subject: [PATCH 09/17] Set control adorner from xaml --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/AdornerLayerPage.xaml | 30 ++++++++ .../Pages/AdornerLayerPage.xaml.cs | 19 +++++ .../Primitives/AdornerLayer.cs | 72 +++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 samples/ControlCatalog/Pages/AdornerLayerPage.xaml create mode 100644 samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 7f5a191519..ec198c6bba 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -19,6 +19,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml new file mode 100644 index 0000000000..853bae4695 --- /dev/null +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -0,0 +1,30 @@ + + + + + diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs new file mode 100644 index 0000000000..2a9e5bf9ba --- /dev/null +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class AdornerLayerPage : UserControl + { + public AdornerLayerPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 57fb7226e8..51975ccd1a 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -27,6 +27,12 @@ namespace Avalonia.Controls.Primitives public static readonly AttachedProperty IsClipEnabledProperty = AvaloniaProperty.RegisterAttached("IsClipEnabled", true); + /// + /// Allows for getting and setting of the adorner for control. + /// + public static readonly AttachedProperty AdornerProperty = + AvaloniaProperty.RegisterAttached("Adorner"); + private static readonly AttachedProperty s_adornedElementInfoProperty = AvaloniaProperty.RegisterAttached("AdornedElementInfo"); @@ -65,6 +71,72 @@ namespace Avalonia.Controls.Primitives adorner.SetValue(IsClipEnabledProperty, isClipEnabled); } + public static Control? GetAdorner(Visual visual) + { + return visual.GetValue(AdornerProperty); + } + + public static void SetAdorner(Visual visual, Control? adorner) + { + visual.SetValue(AdornerProperty, adorner); + + SetVisualAdorner(visual, adorner); + } + + private static void SetVisualAdorner(Visual visual, Control? adorner) + { + var layer = default(AdornerLayer); + + visual.AttachedToVisualTree += (_, _) => + { + layer = AddVisualAdorner(visual, adorner); + }; + + visual.DetachedFromVisualTree += (_, _) => + { + RemoveVisualAdorner(visual, adorner, layer); + }; + } + + private static AdornerLayer? AddVisualAdorner(Visual visual, Control? adorner) + { + if (adorner is null) + { + return null; + } + + var layer = AdornerLayer.GetAdornerLayer(visual); + if (layer == null || layer.Children.Contains(adorner)) + { + return layer; + } + + AdornerLayer.SetAdornedElement(adorner, visual); + AdornerLayer.SetIsClipEnabled(adorner, false); + + ((ISetLogicalParent) adorner).SetParent(visual); + layer.Children.Add(adorner); + + return layer; + } + + private static void RemoveVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer) + { + if (adorner is null) + { + return; + } + + // var layer = AdornerLayer.GetAdornerLayer(visual); + if (layer is null || !layer.Children.Contains(adorner)) + { + return; + } + + layer.Children.Remove(adorner); + ((ISetLogicalParent) adorner).SetParent(null); + } + protected override Size MeasureOverride(Size availableSize) { foreach (var child in Children) From db35037dfb0ce9fe46b311b97635186ea7bc3601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sat, 24 Sep 2022 14:48:23 +0200 Subject: [PATCH 10/17] Add LayoutTransformControl --- .../Pages/AdornerLayerPage.xaml | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml index 853bae4695..8bf948c58d 100644 --- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -6,25 +6,49 @@ d:DesignHeight="800" d:DesignWidth="400" mc:Ignorable="d"> - - - + + + + Rotation + + + + + + + + + + + + + + + + + + + + From 82aebeeb34846ddf1f3df578b1b208b0992a2de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sun, 25 Sep 2022 13:09:04 +0200 Subject: [PATCH 11/17] Handler Adorner property changes --- .../Primitives/AdornerLayer.cs | 89 +++++++++++++------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 51975ccd1a..d557424fbb 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -36,9 +36,13 @@ namespace Avalonia.Controls.Primitives private static readonly AttachedProperty s_adornedElementInfoProperty = AvaloniaProperty.RegisterAttached("AdornedElementInfo"); + private static readonly AttachedProperty s_savedAdornerLayerProperty = + AvaloniaProperty.RegisterAttached("SavedAdornerLayer"); + static AdornerLayer() { AdornedElementProperty.Changed.Subscribe(AdornedElementChanged); + AdornerProperty.Changed.Subscribe(AdornerChanged); } public AdornerLayer() @@ -79,36 +83,79 @@ namespace Avalonia.Controls.Primitives public static void SetAdorner(Visual visual, Control? adorner) { visual.SetValue(AdornerProperty, adorner); - - SetVisualAdorner(visual, adorner); } - private static void SetVisualAdorner(Visual visual, Control? adorner) + private static void AdornerChanged(AvaloniaPropertyChangedEventArgs e) { - var layer = default(AdornerLayer); - - visual.AttachedToVisualTree += (_, _) => + if (e.Sender is Visual visual) { - layer = AddVisualAdorner(visual, adorner); - }; + var oldAdorner = e.OldValue.GetValueOrDefault(); + var newAdorner = e.NewValue.GetValueOrDefault(); + + if (Equals(oldAdorner, newAdorner)) + { + return; + } + + if (oldAdorner is { }) + { + visual.AttachedToVisualTree -= VisualOnAttachedToVisualTree; + visual.DetachedFromVisualTree -= VisualOnDetachedFromVisualTree; + Detach(visual, oldAdorner); + } - visual.DetachedFromVisualTree += (_, _) => + if (newAdorner is { }) + { + visual.AttachedToVisualTree += VisualOnAttachedToVisualTree; + visual.DetachedFromVisualTree += VisualOnDetachedFromVisualTree; + Attach(visual, newAdorner); + } + } + } + + private static void VisualOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + if (sender is Visual visual) { - RemoveVisualAdorner(visual, adorner, layer); - }; + var adorner = GetAdorner(visual); + if (adorner is { }) + { + Attach(visual, adorner); + } + } } - private static AdornerLayer? AddVisualAdorner(Visual visual, Control? adorner) + private static void VisualOnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { - if (adorner is null) + if (sender is Visual visual) { - return null; + var adorner = GetAdorner(visual); + if (adorner is { }) + { + Detach(visual, adorner); + } } + } + private static void Attach(Visual visual, Control adorner) + { var layer = AdornerLayer.GetAdornerLayer(visual); - if (layer == null || layer.Children.Contains(adorner)) + AddVisualAdorner(visual, adorner, layer); + visual.SetValue(s_savedAdornerLayerProperty, layer); + } + + private static void Detach(Visual visual, Control adorner) + { + var layer = visual.GetValue(s_savedAdornerLayerProperty); + RemoveVisualAdorner(visual, adorner, layer); + visual.ClearValue(s_savedAdornerLayerProperty); + } + + private static void AddVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer) + { + if (adorner is null || layer == null || layer.Children.Contains(adorner)) { - return layer; + return; } AdornerLayer.SetAdornedElement(adorner, visual); @@ -116,19 +163,11 @@ namespace Avalonia.Controls.Primitives ((ISetLogicalParent) adorner).SetParent(visual); layer.Children.Add(adorner); - - return layer; } private static void RemoveVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer) { - if (adorner is null) - { - return; - } - - // var layer = AdornerLayer.GetAdornerLayer(visual); - if (layer is null || !layer.Children.Contains(adorner)) + if (adorner is null || layer is null || !layer.Children.Contains(adorner)) { return; } From 272775a3d908e7740f9938985b62d253a4faf89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sun, 25 Sep 2022 13:09:29 +0200 Subject: [PATCH 12/17] Format xaml --- .../Pages/AdornerLayerPage.xaml | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml index 8bf948c58d..7ddbbbfcf1 100644 --- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -27,25 +27,25 @@ - + From 4b31690b12c55368181c1726d2975386936f29d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sun, 25 Sep 2022 13:30:16 +0200 Subject: [PATCH 13/17] Add add / remove adorner buttons for testing --- .../Pages/AdornerLayerPage.xaml | 14 +++++++- .../Pages/AdornerLayerPage.xaml.cs | 33 +++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml index 7ddbbbfcf1..598844d695 100644 --- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -13,6 +13,17 @@ + +