diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index cb79bf219a..f37e0f8701 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -90,7 +90,6 @@ namespace ControlCatalog.Pages private int _vertexBufferObject; private int _indexBufferObject; private int _vertexArrayObject; - private GlExtrasInterface _glExt; private string GetShader(bool fragment, string shader) { @@ -258,7 +257,6 @@ namespace ControlCatalog.Pages protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) { CheckError(GL); - _glExt = new GlExtrasInterface(GL); Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}"; @@ -298,8 +296,8 @@ namespace ControlCatalog.Pages GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata), GL_STATIC_DRAW); CheckError(GL); - _vertexArrayObject = _glExt.GenVertexArray(); - _glExt.BindVertexArray(_vertexArrayObject); + _vertexArrayObject = GL.GenVertexArray(); + GL.BindVertexArray(_vertexArrayObject); CheckError(GL); GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT, 0, vertexSize, IntPtr.Zero); @@ -316,12 +314,13 @@ namespace ControlCatalog.Pages // Unbind everything GL.BindBuffer(GL_ARRAY_BUFFER, 0); GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - _glExt.BindVertexArray(0); + GL.BindVertexArray(0); GL.UseProgram(0); // Delete all resources. - GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject }); - _glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject }); + GL.DeleteBuffer(_vertexBufferObject); + GL.DeleteBuffer(_indexBufferObject); + GL.DeleteVertexArray(_vertexArrayObject); GL.DeleteProgram(_shaderProgram); GL.DeleteShader(_fragmentShader); GL.DeleteShader(_vertexShader); @@ -338,7 +337,7 @@ namespace ControlCatalog.Pages GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject); - _glExt.BindVertexArray(_vertexArrayObject); + GL.BindVertexArray(_vertexArrayObject); GL.UseProgram(_shaderProgram); CheckError(GL); var projection = @@ -369,34 +368,5 @@ namespace ControlCatalog.Pages if (_disco > 0.01) Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); } - - class GlExtrasInterface : GlInterfaceBase - { - public GlExtrasInterface(GlInterface gl) : base(gl.GetProcAddress, gl.ContextInfo) - { - } - - public delegate void GlDeleteVertexArrays(int count, int[] buffers); - [GlMinVersionEntryPoint("glDeleteVertexArrays", 3,0)] - [GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")] - public GlDeleteVertexArrays DeleteVertexArrays { get; } - - public delegate void GlBindVertexArray(int array); - [GlMinVersionEntryPoint("glBindVertexArray", 3,0)] - [GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")] - public GlBindVertexArray BindVertexArray { get; } - public delegate void GlGenVertexArrays(int n, int[] rv); - - [GlMinVersionEntryPoint("glGenVertexArrays",3,0)] - [GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")] - public GlGenVertexArrays GenVertexArrays { get; } - - public int GenVertexArray() - { - var rv = new int[1]; - GenVertexArrays(1, rv); - return rv[0]; - } - } } } diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index cb49ba96c6..32914428ed 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -118,7 +118,7 @@ - + This is a TextBlock with several diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index ac87d521a5..2a7f3360ad 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -445,7 +445,7 @@ namespace Avalonia.Media /// public int FindGlyphIndex(int characterIndex) { - if (GlyphClusters == null) + if (GlyphClusters == null || GlyphClusters.Count == 0) { return characterIndex; } @@ -614,17 +614,29 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { + var firstCluster = 0; + var lastCluster = Characters.Length - 1; + + if (!IsLeftToRight) + { + var cluster = firstCluster; + firstCluster = lastCluster; + lastCluster = cluster; + } + if (GlyphClusters != null && GlyphClusters.Count > 0) { - var firstCluster = GlyphClusters[0]; + firstCluster = GlyphClusters[0]; + lastCluster = GlyphClusters[GlyphClusters.Count - 1]; _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster); } + var isReversed = firstCluster > lastCluster; var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; var widthIncludingTrailingWhitespace = 0d; - var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount); + var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); for (var index = 0; index < GlyphIndices.Count; index++) { @@ -635,16 +647,16 @@ namespace Avalonia.Media var width = widthIncludingTrailingWhitespace; - if (IsLeftToRight) + if (isReversed) { - for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) + for (var index = 0; index < glyphCount; index++) { width -= GetGlyphAdvance(index, out _); } } else { - for (var index = 0; index < glyphCount; index++) + for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) { width -= GetGlyphAdvance(index, out _); } @@ -654,16 +666,15 @@ namespace Avalonia.Media height); } - private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount) - { - glyphCount = 0; - newLineLength = 0; - - if (Characters.IsEmpty) + private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount) + { + if (isReversed) { - return 0; + return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); } + glyphCount = 0; + newLineLength = 0; var trailingWhitespaceLength = 0; if (GlyphClusters == null) @@ -732,6 +743,78 @@ namespace Avalonia.Media return trailingWhitespaceLength; } + private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) + { + glyphCount = 0; + newLineLength = 0; + var trailingWhitespaceLength = 0; + + if (GlyphClusters == null) + { + for (var i = 0; i < Characters.Length;) + { + var codepoint = Codepoint.ReadAt(_characters, i, out var count); + + if (!codepoint.IsWhiteSpace) + { + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength++; + } + + trailingWhitespaceLength++; + + i += count; + glyphCount++; + } + } + else + { + for (var i = 0; i < GlyphClusters.Count; i++) + { + var currentCluster = GlyphClusters[i]; + var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); + + if (!codepoint.IsWhiteSpace) + { + break; + } + + var clusterLength = 1; + + while (i - 1 >= 0) + { + var nextCluster = GlyphClusters[i - 1]; + + if (currentCluster == nextCluster) + { + clusterLength++; + i--; + + continue; + } + + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; + } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; + } + } + + return trailingWhitespaceLength; + } + private void Set(ref T field, T value) { _glyphRunImpl?.Dispose(); diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index 8eeb86c555..4c9764af96 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -209,7 +209,7 @@ namespace Avalonia.Media var pen = new Pen(Stroke ?? defaultBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); - drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Size.Width, 0)); + drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0)); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f3af240c58..f3e8b5969c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -63,7 +63,7 @@ namespace Avalonia.Media.TextFormatting MaxHeight = maxHeight; - MaxLines = maxLines; + MaxLines = maxLines; TextLines = CreateTextLines(); } @@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting /// The maximum number of text lines. public TextLayout( ITextSource textSource, - TextParagraphProperties paragraphProperties, + TextParagraphProperties paragraphProperties, TextTrimming? textTrimming = null, double maxWidth = double.PositiveInfinity, double maxHeight = double.PositiveInfinity, @@ -178,24 +178,18 @@ namespace Avalonia.Media.TextFormatting return new Rect(); } - if (textPosition < 0 || textPosition >= _textSourceLength) + if (textPosition < 0) { - var lastLine = TextLines[TextLines.Count - 1]; - - var lineX = lastLine.Width; - - var lineY = Bounds.Bottom - lastLine.Height; - - return new Rect(lineX, lineY, 0, lastLine.Height); + textPosition = _textSourceLength; } var currentY = 0.0; foreach (var textLine in TextLines) { - var end = textLine.FirstTextSourceIndex + textLine.Length - 1; + var end = textLine.FirstTextSourceIndex + textLine.Length; - if (end < textPosition) + if (end <= textPosition && end < _textSourceLength) { currentY += textLine.Height; @@ -224,7 +218,7 @@ namespace Avalonia.Media.TextFormatting } var result = new List(TextLines.Count); - + var currentY = 0d; foreach (var textLine in TextLines) @@ -239,7 +233,7 @@ namespace Avalonia.Media.TextFormatting var textBounds = textLine.GetTextBounds(start, length); - if(textBounds.Count > 0) + if (textBounds.Count > 0) { foreach (var bounds in textBounds) { @@ -262,7 +256,7 @@ namespace Avalonia.Media.TextFormatting } } - if(textLine.FirstTextSourceIndex + textLine.Length >= start + length) + if (textLine.FirstTextSourceIndex + textLine.Length >= start + length) { break; } @@ -305,7 +299,7 @@ namespace Avalonia.Media.TextFormatting return GetHitTestResult(currentLine, characterHit, point); } - + public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge) { if (charIndex < 0) @@ -327,7 +321,7 @@ namespace Avalonia.Media.TextFormatting continue; } - if (charIndex >= textLine.FirstTextSourceIndex && + if (charIndex >= textLine.FirstTextSourceIndex && charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1)) { return index; @@ -398,7 +392,7 @@ namespace Avalonia.Media.TextFormatting /// The current left. /// The current width. /// The current height. - private static void UpdateBounds(TextLine textLine,ref double left, ref double width, ref double height) + private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height) { var lineWidth = textLine.WidthIncludingTrailingWhitespace; @@ -421,7 +415,7 @@ namespace Avalonia.Media.TextFormatting { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); - Bounds = new Rect(0,0,0, textLine.Height); + Bounds = new Rect(0, 0, 0, textLine.Height); return new List { textLine }; } @@ -439,9 +433,9 @@ namespace Avalonia.Media.TextFormatting var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if(textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) { - if(previousLine != null && previousLine.NewLineLength > 0) + if (previousLine != null && previousLine.NewLineLength > 0) { var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties); @@ -454,7 +448,7 @@ namespace Avalonia.Media.TextFormatting } _textSourceLength += textLine.Length; - + //Fulfill max height constraint if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight) { @@ -485,12 +479,17 @@ namespace Avalonia.Media.TextFormatting //Fulfill max lines constraint if (MaxLines > 0 && textLines.Count >= MaxLines) { + if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null) + { + textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); + } + break; } } //Make sure the TextLayout always contains at least on empty line - if(textLines.Count == 0) + if (textLines.Count == 0) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); @@ -501,7 +500,7 @@ namespace Avalonia.Media.TextFormatting Bounds = new Rect(left, 0, width, height); - if(_paragraphProperties.TextAlignment == TextAlignment.Justify) + if (_paragraphProperties.TextAlignment == TextAlignment.Justify) { var whitespaceWidth = 0d; @@ -509,7 +508,7 @@ namespace Avalonia.Media.TextFormatting { var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; - if(lineWhitespaceWidth > whitespaceWidth) + if (lineWhitespaceWidth > whitespaceWidth) { whitespaceWidth = lineWhitespaceWidth; } @@ -517,7 +516,7 @@ namespace Avalonia.Media.TextFormatting var justificationWidth = width - whitespaceWidth; - if(justificationWidth > 0) + if (justificationWidth > 0) { var justificationProperties = new InterWordJustification(justificationWidth); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7c686358e2..f3c62f4994 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -166,58 +166,74 @@ namespace Avalonia.Media.TextFormatting if (distance <= 0) { - // hit happens before the line, return the first position var firstRun = _textRuns[0]; - if (firstRun is ShapedTextCharacters shapedTextCharacters) - { - return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _); - } + return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); + } - return _resolvedFlowDirection == FlowDirection.LeftToRight ? - new CharacterHit(FirstTextSourceIndex) : - new CharacterHit(FirstTextSourceIndex + Length); + if (distance > WidthIncludingTrailingWhitespace) + { + var lastRun = _textRuns[_textRuns.Count - 1]; + + return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width); } // process hit that happens within the line var characterHit = new CharacterHit(); var currentPosition = FirstTextSourceIndex; + var currentDistance = 0.0; foreach (var currentRun in _textRuns) { - switch (currentRun) + if (currentDistance + currentRun.Size.Width < distance) { - case ShapedTextCharacters shapedRun: - { - characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + currentDistance += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; - var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + continue; + } - characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - break; - } - default: + break; + } + + return characterHit; + } + + private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance) + { + CharacterHit characterHit; + + switch (run) + { + case ShapedTextCharacters shapedRun: + { + characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + + var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + + if (!shapedRun.GlyphRun.IsLeftToRight) { - if (distance < currentRun.Size.Width / 2) - { - characterHit = new CharacterHit(currentPosition); - } - else - { - characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength); - } - break; + offset = Math.Max(0, offset - shapedRun.Text.End); } - } - if (distance <= currentRun.Size.Width) - { - break; - } + characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); - distance -= currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; + break; + } + default: + { + if (distance < run.Size.Width / 2) + { + characterHit = new CharacterHit(currentPosition); + } + else + { + characterHit = new CharacterHit(currentPosition, run.TextSourceLength); + } + break; + } } return characterHit; @@ -226,136 +242,122 @@ namespace Avalonia.Media.TextFormatting /// public override double GetDistanceFromCharacterHit(CharacterHit characterHit) { - var isTrailingHit = characterHit.TrailingLength > 0; + var flowDirection = _paragraphProperties.FlowDirection; var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var currentDistance = Start; var currentPosition = FirstTextSourceIndex; var remainingLength = characterIndex - FirstTextSourceIndex; - GlyphRun? lastRun = null; + var currentDistance = Start; - for (var index = 0; index < _textRuns.Count; index++) + if (flowDirection == FlowDirection.LeftToRight) + { + for (var index = 0; index < _textRuns.Count; index++) + { + var currentRun = _textRuns[index]; + + if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, + flowDirection, out var distance, out _)) + { + return currentDistance + distance; + } + + //No hit hit found so we add the full width + currentDistance += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; + remainingLength -= currentRun.TextSourceLength; + } + } + else { - var textRun = _textRuns[index]; + currentDistance += WidthIncludingTrailingWhitespace; - switch (textRun) + for (var index = _textRuns.Count - 1; index >= 0; index--) { - case ShapedTextCharacters shapedTextCharacters: - { - var currentRun = shapedTextCharacters.GlyphRun; + var currentRun = _textRuns[index]; - if (lastRun != null) - { - if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight && - currentRun.Characters.Start == characterHit.FirstCharacterIndex && - characterHit.TrailingLength == 0) - { - return currentDistance; - } - } + if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, + flowDirection, out var distance, out var currentGlyphRun)) + { + if (currentGlyphRun != null) + { + distance = currentGlyphRun.Size.Width - distance; + } - //Look for a hit in within the current run - if (currentPosition + remainingLength <= currentPosition + textRun.Text.Length) - { - characterHit = new CharacterHit(textRun.Text.Start + remainingLength); + return currentDistance - distance; + } - var distance = currentRun.GetDistanceFromCharacterHit(characterHit); + //No hit hit found so we add the full width + currentDistance -= currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; + remainingLength -= currentRun.TextSourceLength; + } + } - return currentDistance + distance; - } + return currentDistance; + } - //Look at the left and right edge of the current run - if (currentRun.IsLeftToRight) - { - if (_resolvedFlowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) - { - if (characterIndex <= currentPosition) - { - return currentDistance; - } - } - else - { - if (characterIndex == currentPosition) - { - return currentDistance; - } - } + private static bool TryGetDistanceFromCharacterHit( + DrawableTextRun currentRun, + CharacterHit characterHit, + int currentPosition, + int remainingLength, + FlowDirection flowDirection, + out double distance, + out GlyphRun? currentGlyphRun) + { + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + var isTrailingHit = characterHit.TrailingLength > 0; - if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit) - { - return currentDistance + currentRun.Size.Width; - } - } - else - { - if (characterIndex == currentPosition) - { - return currentDistance + currentRun.Size.Width; - } + distance = 0; + currentGlyphRun = null; - var nextRun = index + 1 < _textRuns.Count ? - _textRuns[index + 1] as ShapedTextCharacters : - null; + switch (currentRun) + { + case ShapedTextCharacters shapedTextCharacters: + { + currentGlyphRun = shapedTextCharacters.GlyphRun; - if (nextRun != null) - { - if (nextRun.ShapedBuffer.IsLeftToRight) - { - if (characterIndex == currentPosition + textRun.Text.Length) - { - return currentDistance; - } - } - else - { - if (currentPosition + nextRun.Text.Length == characterIndex) - { - return currentDistance; - } - } - } - else - { - if (characterIndex > currentPosition + textRun.Text.Length) - { - return currentDistance; - } - } - } + if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length) + { + characterHit = new CharacterHit(currentRun.Text.Start + remainingLength); - lastRun = currentRun; + distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit); - break; + return true; } - default: + + if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit) { - if (characterIndex == currentPosition) + if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { - return currentDistance; + distance = currentGlyphRun.Size.Width; } - if (characterIndex == currentPosition + textRun.TextSourceLength) - { - return currentDistance + textRun.Size.Width; - } + return true; + } - break; + break; + } + default: + { + if (characterIndex == currentPosition) + { + return true; } - } - //No hit hit found so we add the full width - currentDistance += textRun.Size.Width; - currentPosition += textRun.TextSourceLength; - remainingLength -= textRun.TextSourceLength; + if (characterIndex == currentPosition + currentRun.TextSourceLength) + { + distance = currentRun.Size.Width; - if (remainingLength <= 0) - { - break; - } + return true; + + } + + break; + } } - return currentDistance; + return false; } /// @@ -460,20 +462,33 @@ namespace Avalonia.Media.TextFormatting var startIndex = currentRun.Text.Start + offset; - var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex + remainingLength) : - new CharacterHit(startIndex)); + double startOffset; + double endOffset; - endX += endOffset; + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex) : - new CharacterHit(startIndex + remainingLength)); + if (currentPosition < startIndex) + { + startOffset = endOffset; + } + else + { + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } + } startX += startOffset; + endX += endOffset; + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); @@ -504,47 +519,40 @@ namespace Avalonia.Media.TextFormatting } //Lines that only contain a linebreak need to be covered here - if(characterLength == 0) + if (characterLength == 0) { characterLength = NewLineLength; } - var runwidth = endX - startX; - var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runwidth, Height), currentPosition, characterLength, currentRun); + var runWidth = endX - startX; + var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) { - currentRect = currentRect.WithWidth(currentWidth + runwidth); + if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + { + currentRect = currentRect.WithWidth(currentWidth + runWidth); - 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 })); + } } - currentWidth += runwidth; + currentWidth += runWidth; currentPosition += characterLength; - if (currentDirection == FlowDirection.LeftToRight) - { - if (currentPosition > characterIndex) - { - break; - } - } - else + if (currentPosition > characterIndex) { - if (currentPosition <= firstTextSourceIndex) - { - break; - } + break; } startX = endX; @@ -571,7 +579,7 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; - var startX = Start + WidthIncludingTrailingWhitespace; + var startX = WidthIncludingTrailingWhitespace; double currentWidth = 0; var currentRect = Rect.Empty; @@ -582,7 +590,7 @@ namespace Avalonia.Media.TextFormatting continue; } - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex) { startX -= currentRun.Size.Width; @@ -601,20 +609,31 @@ namespace Avalonia.Media.TextFormatting currentPosition += offset; var startIndex = currentRun.Text.Start + offset; + double startOffset; + double endOffset; - var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex + remainingLength) : - new CharacterHit(startIndex)); + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + if (currentPosition < startIndex) + { + startOffset = endOffset = 0; + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - endX += endOffset - currentShapedRun.Size.Width; + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + } + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex) : - new CharacterHit(startIndex + remainingLength)); + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } - startX += startOffset - currentShapedRun.Size.Width; + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); @@ -652,41 +671,35 @@ namespace Avalonia.Media.TextFormatting } var runWidth = endX - startX; - var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + + if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0) { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) + { + currentRect = currentRect.WithWidth(currentWidth + runWidth); - 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 })); + } + } currentWidth += runWidth; currentPosition += characterLength; - if (currentDirection == FlowDirection.LeftToRight) - { - if (currentPosition > characterIndex) - { - break; - } - } - else + if (currentPosition > characterIndex) { - if (currentPosition <= firstTextSourceIndex) - { - break; - } + break; } lastDirection = currentDirection; @@ -698,6 +711,8 @@ namespace Avalonia.Media.TextFormatting } } + result.Reverse(); + return result; } @@ -1302,8 +1317,14 @@ namespace Avalonia.Media.TextFormatting switch (textAlignment) { case TextAlignment.Center: - return Math.Max(0, (_paragraphWidth - width) / 2); + var start = (_paragraphWidth - width) / 2; + + if(paragraphFlowDirection == FlowDirection.RightToLeft) + { + start -= (widthIncludingTrailingWhitespace - width); + } + return Math.Max(0, start); case TextAlignment.Right: return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index aeb81f7313..108a38d86b 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text; -using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -22,7 +21,13 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - textRuns.Add(new TextEndOfLine()); + var text = Environment.NewLine.AsMemory(); + + var textRunProperties = CreateTextRunProperties(); + + var textCharacters = new TextCharacters(text, textRunProperties); + + textRuns.Add(textCharacters); } internal override void AppendText(StringBuilder stringBuilder) diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 9b5afaef5d..0c8b1d125d 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -41,9 +41,6 @@ namespace Avalonia.Controls public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue); - public static readonly StyledProperty SelectionForegroundBrushProperty = - AvaloniaProperty.Register(nameof(SelectionForegroundBrush)); - /// /// Defines the property. /// @@ -63,12 +60,13 @@ namespace Avalonia.Controls private bool _canCopy; private int _selectionStart; private int _selectionEnd; + private int _wordSelectionStart = -1; static RichTextBlock() { FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true); - AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionForegroundBrushProperty, SelectionBrushProperty); + AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty); } public RichTextBlock() @@ -89,15 +87,6 @@ namespace Avalonia.Controls set => SetValue(SelectionBrushProperty, value); } - /// - /// Gets or sets a value that defines the brush used for selected text. - /// - public IBrush? SelectionForegroundBrush - { - get => GetValue(SelectionForegroundBrushProperty); - set => SetValue(SelectionForegroundBrushProperty, value); - } - /// /// Gets or sets a character index for the beginning of the current selection. /// @@ -200,7 +189,7 @@ namespace Avalonia.Controls } } - public override void Render(DrawingContext context) + protected override void RenderTextLayout(DrawingContext context, Point origin) { var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -215,13 +204,16 @@ namespace Avalonia.Controls var rects = TextLayout.HitTestTextRange(start, length); - foreach (var rect in rects) + using (context.PushPostTransform(Matrix.CreateTranslation(origin))) { - context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1)); + foreach (var rect in rects) + { + context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1)); + } } } - base.Render(context); + base.RenderTextLayout(context, origin); } /// @@ -297,8 +289,9 @@ namespace Avalonia.Controls /// A object. protected override TextLayout CreateTextLayout(string? text) { + var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var defaultProperties = new GenericTextRunProperties( - new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + typeface, FontSize, TextDecorations, Foreground); @@ -345,6 +338,8 @@ namespace Avalonia.Controls protected override void OnKeyDown(KeyEventArgs e) { + base.OnKeyDown(e); + var handled = false; var modifiers = e.KeyModifiers; var keymap = AvaloniaLocator.Current.GetRequiredService(); @@ -363,6 +358,8 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { + base.OnPointerPressed(e); + if (!IsTextSelectionEnabled) { return; @@ -373,7 +370,9 @@ namespace Avalonia.Controls if (text != null && clickInfo.Properties.IsLeftButtonPressed) { - var point = e.GetPosition(this); + var padding = Padding; + + var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); @@ -382,8 +381,6 @@ namespace Avalonia.Controls var hit = TextLayout.HitTestPoint(point); var index = hit.TextPosition; - SelectionStart = SelectionEnd = index; - #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) #pragma warning restore CS0618 // Type or member is obsolete @@ -391,12 +388,34 @@ namespace Avalonia.Controls case 1: if (clickToSelect) { - SelectionStart = Math.Min(oldIndex, index); - SelectionEnd = Math.Max(oldIndex, index); + if (_wordSelectionStart >= 0) + { + var previousWord = StringUtils.PreviousWord(text, index); + + if (index > _wordSelectionStart) + { + SelectionEnd = StringUtils.NextWord(text, index); + } + + if (index < _wordSelectionStart || previousWord == _wordSelectionStart) + { + SelectionStart = previousWord; + } + } + else + { + SelectionStart = Math.Min(oldIndex, index); + SelectionEnd = Math.Max(oldIndex, index); + } } else { - SelectionStart = SelectionEnd = index; + if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) + { + SelectionStart = SelectionEnd = index; + + _wordSelectionStart = -1; + } } break; @@ -406,9 +425,13 @@ namespace Avalonia.Controls SelectionStart = StringUtils.PreviousWord(text, index); } + _wordSelectionStart = SelectionStart; + SelectionEnd = StringUtils.NextWord(text, index); break; case 3: + _wordSelectionStart = -1; + SelectAll(); break; } @@ -420,6 +443,8 @@ namespace Avalonia.Controls protected override void OnPointerMoved(PointerEventArgs e) { + base.OnPointerMoved(e); + if (!IsTextSelectionEnabled) { return; @@ -428,20 +453,49 @@ namespace Avalonia.Controls // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - var point = e.GetPosition(this); + var text = Text; + var padding = Padding; + + var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); point = new Point( - MathUtilities.Clamp(point.X, 0, Math.Max(Bounds.Width - 1, 0)), - MathUtilities.Clamp(point.Y, 0, Math.Max(Bounds.Height - 1, 0))); + MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)), + MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0))); var hit = TextLayout.HitTestPoint(point); + var textPosition = hit.TextPosition; + + if (text != null && _wordSelectionStart >= 0) + { + var distance = textPosition - _wordSelectionStart; + + if (distance <= 0) + { + SelectionStart = StringUtils.PreviousWord(text, textPosition); + } + + if (distance >= 0) + { + if (SelectionStart != _wordSelectionStart) + { + SelectionStart = _wordSelectionStart; + } + + SelectionEnd = StringUtils.NextWord(text, textPosition); + } + } + else + { + SelectionEnd = textPosition; + } - SelectionEnd = hit.TextPosition; } } protected override void OnPointerReleased(PointerReleasedEventArgs e) { + base.OnPointerReleased(e); + if (!IsTextSelectionEnabled) { return; @@ -454,7 +508,9 @@ namespace Avalonia.Controls if (e.InitialPressMouseButton == MouseButton.Right) { - var point = e.GetPosition(this); + var padding = Padding; + + var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); var hit = TextLayout.HitTestPoint(point); @@ -487,11 +543,6 @@ namespace Avalonia.Controls InvalidateTextLayout(); break; } - case nameof(TextProperty): - { - InvalidateTextLayout(); - break; - } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 80edaf4f26..99c8068b3d 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -505,7 +505,12 @@ namespace Avalonia.Controls } } - TextLayout.Draw(context, new Point(padding.Left, top)); + RenderTextLayout(context, new Point(padding.Left, top)); + } + + protected virtual void RenderTextLayout(DrawingContext context, Point origin) + { + TextLayout.Draw(context, origin); } void IAddChild.AddChild(string text) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 9531f719b9..1b268db2f7 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); - + public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); @@ -80,7 +80,7 @@ namespace Avalonia.Controls public static readonly StyledProperty MaxLinesProperty = AvaloniaProperty.Register(nameof(MaxLines), defaultValue: 0); - + public static readonly DirectProperty TextProperty = TextBlock.TextProperty.AddOwnerWithDataValidation( o => o.Text, @@ -105,7 +105,7 @@ namespace Avalonia.Controls public static readonly StyledProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(); - + /// /// Defines see property. /// @@ -202,6 +202,7 @@ namespace Avalonia.Controls private string _newLine = Environment.NewLine; private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; + private int _wordSelectionStart = -1; private int _selectedTextChangesMadeSinceLastUndoSnapshot; private bool _hasDoneSnapshotOnce; private const int _maxCharsBeforeUndoSnapshot = 7; @@ -275,7 +276,7 @@ namespace Avalonia.Controls get => GetValue(IsReadOnlyProperty); set => SetValue(IsReadOnlyProperty, value); } - + public char PasswordChar { get => GetValue(PasswordCharProperty); @@ -307,7 +308,7 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value); - + if (changed) { UpdateCommandStates(); @@ -327,12 +328,12 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); - + if (changed) { UpdateCommandStates(); } - + if (SelectionStart == value && CaretIndex != value) { CaretIndex = value; @@ -351,7 +352,7 @@ namespace Avalonia.Controls get => GetValue(MaxLinesProperty); set => SetValue(MaxLinesProperty, value); } - + /// /// Gets or sets the line height. /// @@ -370,7 +371,7 @@ namespace Avalonia.Controls var caretIndex = CaretIndex; var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; - + CaretIndex = CoerceCaretIndex(caretIndex, value); SelectionStart = CoerceCaretIndex(selectionStart, value); SelectionEnd = CoerceCaretIndex(selectionEnd, value); @@ -567,7 +568,7 @@ namespace Avalonia.Controls _presenter = e.NameScope.Get("PART_TextPresenter"); _imClient.SetPresenter(_presenter, this); - + if (IsFocused) { _presenter?.ShowCaret(); @@ -577,7 +578,7 @@ namespace Avalonia.Controls protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - + if (IsFocused) { _presenter?.ShowCaret(); @@ -587,7 +588,7 @@ namespace Avalonia.Controls protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - + _imClient.SetPresenter(null, null); } @@ -637,7 +638,7 @@ namespace Avalonia.Controls } UpdateCommandStates(); - + _imClient.SetPresenter(_presenter, this); _presenter?.ShowCaret(); @@ -657,7 +658,7 @@ namespace Avalonia.Controls UpdateCommandStates(); _presenter?.HideCaret(); - + _imClient.SetPresenter(null, null); } @@ -700,14 +701,14 @@ namespace Avalonia.Controls if (grapheme.FirstCodepoint.IsBreakChar) { - if(lineCount + 1 > MaxLines) + if (lineCount + 1 > MaxLines) { break; } else { lineCount++; - } + } } length += grapheme.Text.Length; @@ -736,7 +737,7 @@ namespace Avalonia.Controls text = Text ?? string.Empty; SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); ClearSelection(); - + if (IsUndoEnabled) { _undoRedoHelper.DiscardRedo(); @@ -746,7 +747,7 @@ namespace Avalonia.Controls { RaisePropertyChanged(TextProperty, oldText, _text); } - + CaretIndex = caretIndex + input.Length; } } @@ -828,7 +829,7 @@ namespace Avalonia.Controls { return; } - + var text = Text ?? string.Empty; var caretIndex = CaretIndex; var movement = false; @@ -985,87 +986,87 @@ namespace Avalonia.Controls break; case Key.Up: - { - selection = DetectSelection(); - - _presenter.MoveCaretVertical(LogicalDirection.Backward); - - if (caretIndex != _presenter.CaretIndex) { - movement = true; - } + selection = DetectSelection(); - if (selection) - { - SelectionEnd = _presenter.CaretIndex; - } - else - { - CaretIndex = _presenter.CaretIndex; + _presenter.MoveCaretVertical(LogicalDirection.Backward); + + if (caretIndex != _presenter.CaretIndex) + { + movement = true; + } + + if (selection) + { + SelectionEnd = _presenter.CaretIndex; + } + else + { + CaretIndex = _presenter.CaretIndex; + } + + break; } - - break; - } case Key.Down: - { - selection = DetectSelection(); - - _presenter.MoveCaretVertical(); - - if (caretIndex != _presenter.CaretIndex) - { - movement = true; - } - - if (selection) - { - SelectionEnd = _presenter.CaretIndex; - } - else { - CaretIndex = _presenter.CaretIndex; + selection = DetectSelection(); + + _presenter.MoveCaretVertical(); + + if (caretIndex != _presenter.CaretIndex) + { + movement = true; + } + + if (selection) + { + SelectionEnd = _presenter.CaretIndex; + } + else + { + CaretIndex = _presenter.CaretIndex; + } + + break; } - - break; - } case Key.Back: - { - SnapshotUndoRedo(); - - if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { - SetSelectionForControlBackspace(); - } - - if (!DeleteSelection()) - { - var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward); + SnapshotUndoRedo(); - var backspacePosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) + { + SetSelectionForControlBackspace(); + } - if (caretIndex != backspacePosition) + if (!DeleteSelection()) { - var start = Math.Min(backspacePosition, caretIndex); - var end = Math.Max(backspacePosition, caretIndex); + var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward); - var length = end - start; + var backspacePosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); + if (caretIndex != backspacePosition) + { + var start = Math.Min(backspacePosition, caretIndex); + var end = Math.Max(backspacePosition, caretIndex); - SetTextInternal(editedText); + var length = end - start; - CaretIndex = start; - } - } - - SnapshotUndoRedo(); + var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); - handled = true; - break; - } + SetTextInternal(editedText); + + CaretIndex = start; + } + } + + SnapshotUndoRedo(); + + handled = true; + break; + } case Key.Delete: SnapshotUndoRedo(); - + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlDelete(); @@ -1077,7 +1078,7 @@ namespace Avalonia.Controls var nextPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - if(nextPosition != caretIndex) + if (nextPosition != caretIndex) { var start = Math.Min(nextPosition, caretIndex); var end = Math.Max(nextPosition, caretIndex); @@ -1144,7 +1145,7 @@ namespace Avalonia.Controls { return; } - + var text = Text; var clickInfo = e.GetCurrentPoint(this); @@ -1170,24 +1171,50 @@ namespace Avalonia.Controls case 1: if (clickToSelect) { - SelectionStart = Math.Min(oldIndex, index); - SelectionEnd = Math.Max(oldIndex, index); + if (_wordSelectionStart >= 0) + { + var previousWord = StringUtils.PreviousWord(text, index); + + if (index > _wordSelectionStart) + { + SelectionEnd = StringUtils.NextWord(text, index); + } + + if (index < _wordSelectionStart || previousWord == _wordSelectionStart) + { + SelectionStart = previousWord; + } + } + else + { + SelectionStart = Math.Min(oldIndex, index); + SelectionEnd = Math.Max(oldIndex, index); + } } else { - SelectionStart = SelectionEnd = index; + if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) + { + SelectionStart = SelectionEnd = index; + _wordSelectionStart = -1; + } } break; - case 2: + case 2: + if (!StringUtils.IsStartOfWord(text, index)) { SelectionStart = StringUtils.PreviousWord(text, index); } + _wordSelectionStart = SelectionStart; + SelectionEnd = StringUtils.NextWord(text, index); break; case 3: + _wordSelectionStart = -1; + SelectAll(); break; } @@ -1203,7 +1230,7 @@ namespace Avalonia.Controls { return; } - + // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { @@ -1215,7 +1242,32 @@ namespace Avalonia.Controls _presenter.MoveCaretToPoint(point); - SelectionEnd = _presenter.CaretIndex; + var caretIndex = _presenter.CaretIndex; + var text = Text; + + if (text != null && _wordSelectionStart >= 0) + { + var distance = caretIndex - _wordSelectionStart; + + if (distance <= 0) + { + SelectionStart = StringUtils.PreviousWord(text, caretIndex); + } + + if (distance >= 0) + { + if(SelectionStart != _wordSelectionStart) + { + SelectionStart = _wordSelectionStart; + } + + SelectionEnd = StringUtils.NextWord(text, caretIndex); + } + } + else + { + SelectionEnd = _presenter.CaretIndex; + } } } @@ -1234,9 +1286,9 @@ namespace Avalonia.Controls if (e.InitialPressMouseButton == MouseButton.Right) { var point = e.GetPosition(_presenter); - + _presenter.MoveCaretToPoint(point); - + var caretIndex = _presenter.CaretIndex; // see if mouse clicked inside current selection @@ -1250,7 +1302,7 @@ namespace Avalonia.Controls CaretIndex = SelectionEnd = SelectionStart = caretIndex; } } - + e.Pointer.Capture(null); } @@ -1309,7 +1361,7 @@ namespace Avalonia.Controls { return; } - + var text = Text ?? string.Empty; var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -1319,11 +1371,11 @@ namespace Avalonia.Controls if (isSelecting) { _presenter.MoveCaretToTextPosition(selectionEnd); - + _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward); - + SelectionEnd = _presenter.CaretIndex; } else @@ -1347,7 +1399,7 @@ namespace Avalonia.Controls else { int offset; - + if (direction > 0) { offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd; @@ -1356,7 +1408,7 @@ namespace Avalonia.Controls { offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; } - + SelectionEnd += offset; _presenter.MoveCaretToTextPosition(SelectionEnd); @@ -1378,7 +1430,7 @@ namespace Avalonia.Controls { return; } - + var caretIndex = CaretIndex; if (document) @@ -1401,7 +1453,7 @@ namespace Avalonia.Controls { return; } - + var text = Text ?? string.Empty; var caretIndex = CaretIndex; @@ -1432,8 +1484,9 @@ namespace Avalonia.Controls private bool DeleteSelection(bool raiseTextChanged = true) { - if (IsReadOnly) return true; - + if (IsReadOnly) + return true; + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -1444,40 +1497,40 @@ namespace Avalonia.Controls var text = Text!; SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); - + _presenter?.MoveCaretToTextPosition(start); - - CaretIndex= start; - + + CaretIndex = start; + ClearSelection(); - + return true; } - + CaretIndex = SelectionStart; - + return false; } private string GetSelection() { var text = Text; - + if (string.IsNullOrEmpty(text)) { return ""; } - + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); - + if (start == end || (Text?.Length ?? 0) < end) { return ""; } - + return text.Substring(start, end - start); } @@ -1496,7 +1549,7 @@ namespace Avalonia.Controls private void SetSelectionForControlBackspace() { var selectionStart = CaretIndex; - + MoveHorizontal(-1, true, false); SelectionStart = selectionStart; @@ -1508,9 +1561,9 @@ namespace Avalonia.Controls { return; } - + SelectionStart = CaretIndex; - + MoveHorizontal(1, true, true); if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ') diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs index 94e7728d68..3a8fdb8491 100644 --- a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs +++ b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs @@ -21,7 +21,7 @@ namespace Avalonia.OpenGL.Angle var display = IntPtr.Zero; AngleOptions.PlatformApi angleApi = default; { - if (_egl.GetPlatformDisplayEXT == null) + if (!_egl.IsGetPlatformDisplayExtAvailable) throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis @@ -37,7 +37,7 @@ namespace Avalonia.OpenGL.Angle else continue; - display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + display = _egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); if (display != IntPtr.Zero) { diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 76924d060f..bacb10c792 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -11,4 +11,5 @@ + diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index b3469c212b..68442c1fd3 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -66,11 +66,9 @@ namespace Avalonia.OpenGL.Controls return; gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); - if (_depthBuffer != 0) gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + if (_depthBuffer != 0) gl.DeleteRenderbuffer(_depthBuffer); - var oneArr = new int[1]; - gl.GenRenderbuffers(1, oneArr); - _depthBuffer = oneArr[0]; + _depthBuffer = gl.GenRenderbuffer(); gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); gl.RenderbufferStorage(GL_RENDERBUFFER, GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, @@ -88,9 +86,9 @@ namespace Avalonia.OpenGL.Controls var gl = _context.GlInterface; gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); - gl.DeleteFramebuffers(1, new[] { _fb }); + gl.DeleteFramebuffer(_fb); _fb = 0; - gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + gl.DeleteRenderbuffer(_depthBuffer); _depthBuffer = 0; _attachment?.Dispose(); _attachment = null; @@ -174,9 +172,7 @@ namespace Avalonia.OpenGL.Controls { _depthBufferSize = GetPixelSize(); var gl = _context.GlInterface; - var oneArr = new int[1]; - gl.GenFramebuffers(1, oneArr); - _fb = oneArr[0]; + _fb = gl.GenFramebuffer(); gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); EnsureDepthBufferAttachment(gl); diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index bc517c3e27..e7b5dc7c83 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -24,7 +24,7 @@ namespace Avalonia.OpenGL.Egl SampleCount = sampleCount; StencilSize = stencilSize; using (MakeCurrent()) - GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, b => _egl.GetProcAddress(b)); + GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, _egl.GetProcAddress); } public IntPtr Context { get; } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index 623364866b..d3d85e8c83 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -34,9 +34,9 @@ namespace Avalonia.OpenGL.Egl } else { - if (egl.GetPlatformDisplayEXT == null) + if (!egl.IsGetPlatformDisplayExtAvailable) throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); - display = egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); + display = egl.GetPlatformDisplayExt(platformType, platformDisplay, attrs); } if (display == IntPtr.Zero) diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index 6148e58440..4fec8e5356 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -2,31 +2,26 @@ using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Platform.Interop; +using Avalonia.SourceGenerator; namespace Avalonia.OpenGL.Egl { - public class EglInterface : GlInterfaceBase + public unsafe partial class EglInterface { - public EglInterface() : base(Load()) + public EglInterface(Func getProcAddress) { - - } - - public EglInterface(Func getProcAddress) : base(getProcAddress) - { - + Initialize(getProcAddress); } - public EglInterface(Func getProcAddress) : base(getProcAddress) + public EglInterface(string library) : this(Load(library)) { - } - - public EglInterface(string library) : base(Load(library)) + + public EglInterface() : this(Load()) { + } - static Func Load() { var os = AvaloniaLocator.Current.GetService().GetRuntimeInfo().OperatingSystem; @@ -46,119 +41,75 @@ namespace Avalonia.OpenGL.Egl } // ReSharper disable UnassignedGetOnlyAutoProperty - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate int EglGetError(); - [GlEntryPoint("eglGetError")] - public EglGetError GetError { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglGetDisplay(IntPtr nativeDisplay); - [GlEntryPoint("eglGetDisplay")] - public EglGetDisplay GetDisplay { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglGetPlatformDisplayEXT(int platform, IntPtr nativeDisplay, int[] attrs); - [GlEntryPoint("eglGetPlatformDisplayEXT")] - [GlOptionalEntryPoint] - public EglGetPlatformDisplayEXT GetPlatformDisplayEXT { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglInitialize(IntPtr display, out int major, out int minor); - [GlEntryPoint("eglInitialize")] - public EglInitialize Initialize { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglGetProcAddress(Utf8Buffer proc); - [GlEntryPoint("eglGetProcAddress")] - public EglGetProcAddress GetProcAddress { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglBindApi(int api); - [GlEntryPoint("eglBindAPI")] - public EglBindApi BindApi { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglChooseConfig(IntPtr display, int[] attribs, - out IntPtr surfaceConfig, int numConfigs, out int choosenConfig); - [GlEntryPoint("eglChooseConfig")] - public EglChooseConfig ChooseConfig { get; } + + [GetProcAddress("eglGetError")] + public partial int GetError(); + + [GetProcAddress("eglGetDisplay")] + public partial IntPtr GetDisplay(IntPtr nativeDisplay); + + [GetProcAddress("eglGetPlatformDisplayEXT", true)] + public partial IntPtr GetPlatformDisplayExt(int platform, IntPtr nativeDisplay, int[] attrs); + + [GetProcAddress("eglInitialize")] + public partial bool Initialize(IntPtr display, out int major, out int minor); + + [GetProcAddress("eglGetProcAddress")] + public partial IntPtr GetProcAddress(IntPtr proc); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglCreateContext(IntPtr display, IntPtr config, + [GetProcAddress("eglBindAPI")] + public partial bool BindApi(int api); + + [GetProcAddress("eglChooseConfig")] + public partial bool ChooseConfig(IntPtr display, int[] attribs, + out IntPtr surfaceConfig, int numConfigs, out int choosenConfig); + + [GetProcAddress("eglCreateContext")] + public partial IntPtr CreateContext(IntPtr display, IntPtr config, IntPtr share, int[] attrs); - [GlEntryPoint("eglCreateContext")] - public EglCreateContext CreateContext { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglDestroyContext(IntPtr display, IntPtr context); - [GlEntryPoint("eglDestroyContext")] - public EglDestroyContext DestroyContext { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglCreatePBufferSurface(IntPtr display, IntPtr config, int[] attrs); - [GlEntryPoint("eglCreatePbufferSurface")] - public EglCreatePBufferSurface CreatePBufferSurface { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglMakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context); - [GlEntryPoint("eglMakeCurrent")] - public EglMakeCurrent MakeCurrent { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglGetCurrentContext(); - [GlEntryPoint("eglGetCurrentContext")] - public EglGetCurrentContext GetCurrentContext { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglGetCurrentDisplay(); - [GlEntryPoint("eglGetCurrentDisplay")] - public EglGetCurrentContext GetCurrentDisplay { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglGetCurrentSurface(int readDraw); - [GlEntryPoint("eglGetCurrentSurface")] - public EglGetCurrentSurface GetCurrentSurface { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void EglDisplaySurfaceVoidDelegate(IntPtr display, IntPtr surface); - [GlEntryPoint("eglDestroySurface")] - public EglDisplaySurfaceVoidDelegate DestroySurface { get; } - - [GlEntryPoint("eglSwapBuffers")] - public EglDisplaySurfaceVoidDelegate SwapBuffers { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr - EglCreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[] attrs); - [GlEntryPoint("eglCreateWindowSurface")] - public EglCreateWindowSurface CreateWindowSurface { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglGetConfigAttrib(IntPtr display, IntPtr config, int attr, out int rv); - [GlEntryPoint("eglGetConfigAttrib")] - public EglGetConfigAttrib GetConfigAttrib { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglWaitGL(); - [GlEntryPoint("eglWaitGL")] - public EglWaitGL WaitGL { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglWaitClient(); - [GlEntryPoint("eglWaitClient")] - public EglWaitGL WaitClient { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglWaitNative(int engine); - [GlEntryPoint("eglWaitNative")] - public EglWaitNative WaitNative { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglQueryString(IntPtr display, int i); - - [GlEntryPoint("eglQueryString")] - public EglQueryString QueryStringNative { get; } + + [GetProcAddress("eglDestroyContext")] + public partial bool DestroyContext(IntPtr display, IntPtr context); + + [GetProcAddress("eglCreatePbufferSurface")] + public partial IntPtr CreatePBufferSurface(IntPtr display, IntPtr config, int[] attrs); + [GetProcAddress("eglMakeCurrent")] + public partial bool MakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context); + + [GetProcAddress("eglGetCurrentContext")] + public partial IntPtr GetCurrentContext(); + + [GetProcAddress("eglGetCurrentDisplay")] + public partial IntPtr GetCurrentDisplay(); + + [GetProcAddress("eglGetCurrentSurface")] + public partial IntPtr GetCurrentSurface(int readDraw); + + [GetProcAddress("eglDestroySurface")] + public partial void DestroySurface(IntPtr display, IntPtr surface); + + [GetProcAddress("eglSwapBuffers")] + public partial void SwapBuffers(IntPtr display, IntPtr surface); + + [GetProcAddress("eglCreateWindowSurface")] + public partial IntPtr CreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[] attrs); + + [GetProcAddress("eglGetConfigAttrib")] + public partial bool GetConfigAttrib(IntPtr display, IntPtr config, int attr, out int rv); + + [GetProcAddress("eglWaitGL")] + public partial bool WaitGL(); + + [GetProcAddress("eglWaitClient")] + public partial bool WaitClient(); + + [GetProcAddress("eglWaitNative")] + public partial bool WaitNative(int engine); + + [GetProcAddress("eglQueryString")] + public partial IntPtr QueryStringNative(IntPtr display, int i); + public string QueryString(IntPtr display, int i) { var rv = QueryStringNative(display, i); @@ -166,25 +117,15 @@ namespace Avalonia.OpenGL.Egl return null; return Marshal.PtrToStringAnsi(rv); } + + [GetProcAddress("eglCreatePbufferFromClientBuffer")] + public partial IntPtr CreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list); + + [GetProcAddress("eglQueryDisplayAttribEXT", true)] + public partial bool QueryDisplayAttribExt(IntPtr display, int attr, out IntPtr res); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr EglCreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list); - [GlEntryPoint("eglCreatePbufferFromClientBuffer")] - - public EglCreatePbufferFromClientBuffer CreatePbufferFromClientBuffer { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglQueryDisplayAttribEXT(IntPtr display, int attr, out IntPtr res); - - [GlEntryPoint("eglQueryDisplayAttribEXT"), GlOptionalEntryPoint] - public EglQueryDisplayAttribEXT QueryDisplayAttribExt { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate bool EglQueryDeviceAttribEXT(IntPtr display, int attr, out IntPtr res); - - [GlEntryPoint("eglQueryDeviceAttribEXT"), GlOptionalEntryPoint] - public EglQueryDisplayAttribEXT QueryDeviceAttribExt { get; } - - // ReSharper restore UnassignedGetOnlyAutoProperty + + [GetProcAddress("eglQueryDeviceAttribEXT", true)] + public partial bool QueryDeviceAttribExt(IntPtr display, int attr, out IntPtr res); } } diff --git a/src/Avalonia.OpenGL/GlBasicInfoInterface.cs b/src/Avalonia.OpenGL/GlBasicInfoInterface.cs index aaba2ec09c..7a7110f15b 100644 --- a/src/Avalonia.OpenGL/GlBasicInfoInterface.cs +++ b/src/Avalonia.OpenGL/GlBasicInfoInterface.cs @@ -3,46 +3,24 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Platform.Interop; +using Avalonia.SourceGenerator; namespace Avalonia.OpenGL { - public class GlBasicInfoInterface : GlBasicInfoInterface + public unsafe partial class GlBasicInfoInterface { - public GlBasicInfoInterface(Func getProcAddress) : base(getProcAddress, null) - { - } - - public GlBasicInfoInterface(Func nativeGetProcAddress) : base(nativeGetProcAddress, null) - { + public GlBasicInfoInterface(Func getProcAddress){ + Initialize(getProcAddress); } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGetIntegerv(int name, out int rv); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr GlGetString(int v); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr GlGetStringi(int v, int v1); - } - public class GlBasicInfoInterface : GlInterfaceBase - { - public GlBasicInfoInterface(Func getProcAddress, TContextInfo context) : base(getProcAddress, context) - { - } + [GetProcAddress("glGetIntegerv")] + public partial void GetIntegerv(int name, out int rv); - public GlBasicInfoInterface(Func nativeGetProcAddress, TContextInfo context) : base(nativeGetProcAddress, context) - { - } - - [GlEntryPoint("glGetIntegerv")] - public GlBasicInfoInterface.GlGetIntegerv GetIntegerv { get; } - - - [GlEntryPoint("glGetString")] - public GlBasicInfoInterface.GlGetString GetStringNative { get; } - - [GlEntryPoint("glGetStringi")] - public GlBasicInfoInterface.GlGetStringi GetStringiNative { get; } + [GetProcAddress("glGetString")] + public partial IntPtr GetStringNative(int v); + + [GetProcAddress("glGetStringi")] + public partial IntPtr GetStringiNative(int v, int v1); public string GetString(int v) { diff --git a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs index 2ffdaca8fe..3e31de6995 100644 --- a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs +++ b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs @@ -2,117 +2,54 @@ using System; namespace Avalonia.OpenGL { - public interface IGlEntryPointAttribute + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + class GlMinVersionEntryPoint : Attribute { - IntPtr GetProcAddress(Func getProcAddress); - } - - public interface IGlEntryPointAttribute - { - IntPtr GetProcAddress(TContext context, Func getProcAddress); - } - - [AttributeUsage(AttributeTargets.Property)] - public class GlOptionalEntryPoint : Attribute - { - - } - - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] - public class GlEntryPointAttribute : Attribute, IGlEntryPointAttribute - { - public string[] EntryPoints { get; } - - public GlEntryPointAttribute(string entryPoint) - { - EntryPoints = new []{entryPoint}; - } -/* - public GlEntryPointAttribute(params string[] entryPoints) + public GlMinVersionEntryPoint(string entry, int minVersionMajor, int minVersionMinor) { - EntryPoints = entryPoints; - } -*/ - public IntPtr GetProcAddress(Func getProcAddress) - { - foreach (var name in EntryPoints) - { - var rv = getProcAddress(name); - if (rv != IntPtr.Zero) - return rv; - } - return IntPtr.Zero; - } - } - - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] - public class GlMinVersionEntryPoint : Attribute, IGlEntryPointAttribute - { - private readonly string _entry; - private readonly GlProfileType? _profile; - private readonly int _minVersionMajor; - private readonly int _minVersionMinor; - - public GlMinVersionEntryPoint(string entry, GlProfileType profile, int minVersionMajor, - int minVersionMinor) - { - _entry = entry; - _profile = profile; - _minVersionMajor = minVersionMajor; - _minVersionMinor = minVersionMinor; } - public GlMinVersionEntryPoint(string entry, int minVersionMajor, - int minVersionMinor) + public GlMinVersionEntryPoint(string entry, int minVersionMajor, int minVersionMinor, GlProfileType profile) { - _entry = entry; - _minVersionMajor = minVersionMajor; - _minVersionMinor = minVersionMinor; } + - public IntPtr GetProcAddress(GlInterface.GlContextInfo context, Func getProcAddress) + public static IntPtr GetProcAddress(Func getProcAddress, GlInterface.GlContextInfo context, + string entry, int minVersionMajor, int minVersionMinor, GlProfileType? profile = null) { - if(_profile.HasValue && context.Version.Type != _profile) + if(profile.HasValue && context.Version.Type != profile) return IntPtr.Zero; - if(context.Version.Major<_minVersionMajor) + if(context.Version.Major + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + class GlExtensionEntryPoint : Attribute { - private readonly string _entry; - private readonly GlProfileType? _profile; - private readonly string _extension; - - public GlExtensionEntryPoint(string entry, GlProfileType profile, string extension) + public GlExtensionEntryPoint(string entry, string extension) { - _entry = entry; - _profile = profile; - _extension = extension; } - public GlExtensionEntryPoint(string entry, string extension) + public GlExtensionEntryPoint(string entry, string extension, GlProfileType profile) { - _entry = entry; - _extension = extension; } - public IntPtr GetProcAddress(GlInterface.GlContextInfo context, Func getProcAddress) + public static IntPtr GetProcAddress(Func getProcAddress, GlInterface.GlContextInfo context, + string entry, string extension, GlProfileType? profile = null) { // Ignore different profile type - if (_profile.HasValue && _profile != context.Version.Type) + if (profile.HasValue && profile != context.Version.Type) return IntPtr.Zero; // Check if extension is supported by the current context - if (!context.Extensions.Contains(_extension)) + if (!context.Extensions.Contains(extension)) return IntPtr.Zero; - return getProcAddress(_entry); + return getProcAddress(entry); } } } diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 18bebe4cb5..a9dd882e3b 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -3,14 +3,14 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using Avalonia.Platform.Interop; +using Avalonia.SourceGenerator; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.OpenGL { - public delegate IntPtr GlGetProcAddressDelegate(string procName); - - public unsafe class GlInterface : GlBasicInfoInterface + public unsafe partial class GlInterface : GlBasicInfoInterface { + private readonly Func _getProcAddress; public string Version { get; } public string Vendor { get; } public string Renderer { get; } @@ -35,12 +35,14 @@ namespace Avalonia.OpenGL } } - private GlInterface(GlContextInfo info, Func getProcAddress) : base(getProcAddress, info) + private GlInterface(GlContextInfo info, Func getProcAddress) : base(getProcAddress) { + _getProcAddress = getProcAddress; ContextInfo = info; Version = GetString(GlConsts.GL_VERSION); Renderer = GetString(GlConsts.GL_RENDERER); - Vendor = GetString(GlConsts.GL_VENDOR); + Vendor = GetString(GlConsts.GL_VENDOR); + Initialize(getProcAddress, ContextInfo); } public GlInterface(GlVersion version, Func getProcAddress) : this( @@ -48,92 +50,58 @@ namespace Avalonia.OpenGL { } - public GlInterface(GlVersion version, Func n) : this(version, ConvertNative(n)) + public IntPtr GetProcAddress(string proc) => _getProcAddress(proc); + + [GetProcAddress("glGetError")] + public partial int GetError(); + + [GetProcAddress("glClearStencil")] + public partial void ClearStencil(int s); + + [GetProcAddress("glClearColor")] + public partial void ClearColor(float r, float g, float b, float a); + + [GetProcAddress("glClear")] + public partial void Clear(int bits); + + [GetProcAddress("glViewport")] + public partial void Viewport(int x, int y, int width, int height); + + [GetProcAddress("glFlush")] + public partial void Flush(); + + [GetProcAddress("glFinish")] + public partial void Finish(); + + [GetProcAddress("glGetIntegerv")] + public partial void GetIntegerv(int name, out int rv); + + [GetProcAddress("glGenFramebuffers")] + public partial void GenFramebuffers(int count, int* res); + + public int GenFramebuffer() { - + int rv = 0; + GenFramebuffers(1, &rv); + return rv; } - public static GlInterface FromNativeUtf8GetProcAddress(GlVersion version, Func getProcAddress) => - new GlInterface(version, getProcAddress); - - - public T GetProcAddress(string proc) => Marshal.GetDelegateForFunctionPointer(GetProcAddress(proc)); - - // ReSharper disable UnassignedGetOnlyAutoProperty - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate int GlGetError(); - [GlEntryPoint("glGetError")] - public GlGetError GetError { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlClearStencil(int s); - [GlEntryPoint("glClearStencil")] - public GlClearStencil ClearStencil { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlClearColor(float r, float g, float b, float a); - [GlEntryPoint("glClearColor")] - public GlClearColor ClearColor { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlClear(int bits); - [GlEntryPoint("glClear")] - public GlClear Clear { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlViewport(int x, int y, int width, int height); - [GlEntryPoint("glViewport")] - public GlViewport Viewport { get; } - - [GlEntryPoint("glFlush")] - public UnmanagedAction Flush { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void UnmanagedAction(); - - [GlEntryPoint("glFinish")] - public UnmanagedAction Finish { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate IntPtr GlGetString(int v); - [GlEntryPoint("glGetString")] - public GlGetString GetStringNative { get; } - - public string GetString(int v) + [GetProcAddress("glDeleteFramebuffers")] + public partial void DeleteFramebuffers(int count, int* framebuffers); + + public void DeleteFramebuffer(int fb) { - var ptr = GetStringNative(v); - if (ptr != IntPtr.Zero) - return Marshal.PtrToStringAnsi(ptr); - return null; + DeleteFramebuffers(1, &fb); } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGetIntegerv(int name, out int rv); - [GlEntryPoint("glGetIntegerv")] - public GlGetIntegerv GetIntegerv { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGenFramebuffers(int count, int[] res); - [GlEntryPoint("glGenFramebuffers")] - public GlGenFramebuffers GenFramebuffers { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlDeleteFramebuffers(int count, int[] framebuffers); - [GlEntryPoint("glDeleteFramebuffers")] - public GlDeleteFramebuffers DeleteFramebuffers { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlBindFramebuffer(int target, int fb); - [GlEntryPoint("glBindFramebuffer")] - public GlBindFramebuffer BindFramebuffer { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate int GlCheckFramebufferStatus(int target); - [GlEntryPoint("glCheckFramebufferStatus")] - public GlCheckFramebufferStatus CheckFramebufferStatus { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlBlitFramebuffer(int srcX0, + [GetProcAddress("glBindFramebuffer")] + public partial void BindFramebuffer(int target, int fb); + + [GetProcAddress("glCheckFramebufferStatus")] + public partial int CheckFramebufferStatus(int target); + + [GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0), GetProcAddress(true)] + public partial void BlitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, @@ -143,89 +111,78 @@ namespace Avalonia.OpenGL int dstY1, int mask, int filter); - [GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0), GlOptionalEntryPoint] - public GlBlitFramebuffer BlitFramebuffer { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGenRenderbuffers(int count, int[] res); - [GlEntryPoint("glGenRenderbuffers")] - public GlGenRenderbuffers GenRenderbuffers { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlDeleteRenderbuffers(int count, int[] renderbuffers); - [GlEntryPoint("glDeleteRenderbuffers")] - public GlDeleteTextures DeleteRenderbuffers { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlBindRenderbuffer(int target, int fb); - [GlEntryPoint("glBindRenderbuffer")] - public GlBindRenderbuffer BindRenderbuffer { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlRenderbufferStorage(int target, int internalFormat, int width, int height); - [GlEntryPoint("glRenderbufferStorage")] - public GlRenderbufferStorage RenderbufferStorage { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlFramebufferRenderbuffer(int target, int attachment, + + + [GetProcAddress("glGenRenderbuffers")] + public partial void GenRenderbuffers(int count, int* res); + + public int GenRenderbuffer() + { + int rv = 0; + GenRenderbuffers(1, &rv); + return rv; + } + + [GetProcAddress("glDeleteRenderbuffers")] + public partial void DeleteRenderbuffers(int count, int* renderbuffers); + + public void DeleteRenderbuffer(int renderbuffer) + { + DeleteRenderbuffers(1, &renderbuffer); + } + + [GetProcAddress("glBindRenderbuffer")] + public partial void BindRenderbuffer(int target, int fb); + + [GetProcAddress("glRenderbufferStorage")] + public partial void RenderbufferStorage(int target, int internalFormat, int width, int height); + + [GetProcAddress("glFramebufferRenderbuffer")] + public partial void FramebufferRenderbuffer(int target, int attachment, int renderbufferTarget, int renderbuffer); - [GlEntryPoint("glFramebufferRenderbuffer")] - public GlFramebufferRenderbuffer FramebufferRenderbuffer { get; } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGenTextures(int count, int[] res); - [GlEntryPoint("glGenTextures")] - public GlGenTextures GenTextures { get; } + [GetProcAddress("glGenTextures")] + public partial void GenTextures(int count, int* res); + + public int GenTexture() + { + int rv = 0; + GenTextures(1, &rv); + return rv; + } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlBindTexture(int target, int fb); - [GlEntryPoint("glBindTexture")] - public GlBindTexture BindTexture { get; } + [GetProcAddress("glBindTexture")] + public partial void BindTexture(int target, int fb); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlActiveTexture(int texture); - [GlEntryPoint("glActiveTexture")] - public GlActiveTexture ActiveTexture { get; } + [GetProcAddress("glActiveTexture")] + public partial void ActiveTexture(int texture); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlDeleteTextures(int count, int[] textures); - [GlEntryPoint("glDeleteTextures")] - public GlDeleteTextures DeleteTextures { get; } + [GetProcAddress("glDeleteTextures")] + public partial void DeleteTextures(int count, int* textures); + public void DeleteTexture(int texture) => DeleteTextures(1, &texture); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlTexImage2D(int target, int level, int internalFormat, int width, int height, int border, + [GetProcAddress("glTexImage2D")] + public partial void TexImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, IntPtr data); - [GlEntryPoint("glTexImage2D")] - public GlTexImage2D TexImage2D { get; } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlCopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, + [GetProcAddress("glCopyTexSubImage2D")] + public partial void CopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, int width, int height); - - [GlEntryPoint("glCopyTexSubImage2D")] - public GlCopyTexSubImage2D CopyTexSubImage2D { get; } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlTexParameteri(int target, int name, int value); - [GlEntryPoint("glTexParameteri")] - public GlTexParameteri TexParameteri { get; } + [GetProcAddress("glTexParameteri")] + public partial void TexParameteri(int target, int name, int value); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlFramebufferTexture2D(int target, int attachment, + + [GetProcAddress("glFramebufferTexture2D")] + public partial void FramebufferTexture2D(int target, int attachment, int texTarget, int texture, int level); - [GlEntryPoint("glFramebufferTexture2D")] - public GlFramebufferTexture2D FramebufferTexture2D { get; } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate int GlCreateShader(int shaderType); - [GlEntryPoint("glCreateShader")] - public GlCreateShader CreateShader { get; } + [GetProcAddress("glCreateShader")] + public partial int CreateShader(int shaderType); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlShaderSource(int shader, int count, IntPtr strings, IntPtr lengths); - [GlEntryPoint("glShaderSource")] - public GlShaderSource ShaderSource { get; } + [GetProcAddress("glShaderSource")] + public partial void ShaderSource(int shader, int count, IntPtr strings, IntPtr lengths); public void ShaderSourceString(int shader, string source) { @@ -237,20 +194,14 @@ namespace Avalonia.OpenGL } } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlCompileShader(int shader); - [GlEntryPoint("glCompileShader")] - public GlCompileShader CompileShader { get; } + [GetProcAddress("glCompileShader")] + public partial void CompileShader(int shader); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGetShaderiv(int shader, int name, int* parameters); - [GlEntryPoint("glGetShaderiv")] - public GlGetShaderiv GetShaderiv { get; } + [GetProcAddress("glGetShaderiv")] + public partial void GetShaderiv(int shader, int name, int* parameters); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGetShaderInfoLog(int shader, int maxLength, out int length, void*infoLog); - [GlEntryPoint("glGetShaderInfoLog")] - public GlGetShaderInfoLog GetShaderInfoLog { get; } + [GetProcAddress("glGetShaderInfoLog")] + public partial void GetShaderInfoLog(int shader, int maxLength, out int length, void* infoLog); public unsafe string CompileShaderAndGetError(int shader, string source) { @@ -268,33 +219,24 @@ namespace Avalonia.OpenGL int len; fixed (void* ptr = logData) GetShaderInfoLog(shader, logLength, out len, ptr); - return Encoding.UTF8.GetString(logData,0, len); + return Encoding.UTF8.GetString(logData, 0, len); } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate int GlCreateProgram(); - [GlEntryPoint("glCreateProgram")] - public GlCreateProgram CreateProgram { get; } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlAttachShader(int program, int shader); - [GlEntryPoint("glAttachShader")] - public GlAttachShader AttachShader { get; } + [GetProcAddress("glCreateProgram")] + public partial int CreateProgram(); + + [GetProcAddress("glAttachShader")] + public partial void AttachShader(int program, int shader); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlLinkProgram(int program); - [GlEntryPoint("glLinkProgram")] - public GlLinkProgram LinkProgram { get; } + [GetProcAddress("glLinkProgram")] + public partial void LinkProgram(int program); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGetProgramiv(int program, int name, int* parameters); - [GlEntryPoint("glGetProgramiv")] - public GlGetProgramiv GetProgramiv { get; } + [GetProcAddress("glGetProgramiv")] + public partial void GetProgramiv(int program, int name, int* parameters); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGetProgramInfoLog(int program, int maxLength, out int len, void* infoLog); - [GlEntryPoint("glGetProgramInfoLog")] - public GlGetProgramInfoLog GetProgramInfoLog { get; } + [GetProcAddress("glGetProgramInfoLog")] + public partial void GetProgramInfoLog(int program, int maxLength, out int len, void* infoLog); public unsafe string LinkProgramAndGetError(int program) { @@ -309,13 +251,11 @@ namespace Avalonia.OpenGL int len; fixed (void* ptr = logData) GetProgramInfoLog(program, logLength, out len, ptr); - return Encoding.UTF8.GetString(logData,0, len); + return Encoding.UTF8.GetString(logData, 0, len); } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlBindAttribLocation(int program, int index, IntPtr name); - [GlEntryPoint("glBindAttribLocation")] - public GlBindAttribLocation BindAttribLocation { get; } + [GetProcAddress("glBindAttribLocation")] + public partial void BindAttribLocation(int program, int index, IntPtr name); public void BindAttribLocationString(int program, int index, string name) { @@ -323,32 +263,24 @@ namespace Avalonia.OpenGL BindAttribLocation(program, index, b.DangerousGetHandle()); } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlGenBuffers(int len, int[] rv); - [GlEntryPoint("glGenBuffers")] - public GlGenBuffers GenBuffers { get; } + [GetProcAddress("glGenBuffers")] + public partial void GenBuffers(int len, int* rv); public int GenBuffer() { - var rv = new int[1]; - GenBuffers(1, rv); - return rv[0]; + int rv; + GenBuffers(1, &rv); + return rv; } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlBindBuffer(int target, int buffer); - [GlEntryPoint("glBindBuffer")] - public GlBindBuffer BindBuffer { get; } + [GetProcAddress("glBindBuffer")] + public partial void BindBuffer(int target, int buffer); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlBufferData(int target, IntPtr size, IntPtr data, int usage); - [GlEntryPoint("glBufferData")] - public GlBufferData BufferData { get; } + [GetProcAddress("glBufferData")] + public partial void BufferData(int target, IntPtr size, IntPtr data, int usage); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate int GlGetAttribLocation(int program, IntPtr name); - [GlEntryPoint("glGetAttribLocation")] - public GlGetAttribLocation GetAttribLocation { get; } + [GetProcAddress("glGetAttribLocation")] + public partial int GetAttribLocation(int program, IntPtr name); public int GetAttribLocationString(int program, string name) { @@ -356,36 +288,24 @@ namespace Avalonia.OpenGL return GetAttribLocation(program, b.DangerousGetHandle()); } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlVertexAttribPointer(int index, int size, int type, + [GetProcAddress("glVertexAttribPointer")] + public partial void VertexAttribPointer(int index, int size, int type, int normalized, int stride, IntPtr pointer); - [GlEntryPoint("glVertexAttribPointer")] - public GlVertexAttribPointer VertexAttribPointer { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlEnableVertexAttribArray(int index); - [GlEntryPoint("glEnableVertexAttribArray")] - public GlEnableVertexAttribArray EnableVertexAttribArray { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlUseProgram(int program); - [GlEntryPoint("glUseProgram")] - public GlUseProgram UseProgram { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlDrawArrays(int mode, int first, IntPtr count); - [GlEntryPoint("glDrawArrays")] - public GlDrawArrays DrawArrays { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlDrawElements(int mode, int count, int type, IntPtr indices); - [GlEntryPoint("glDrawElements")] - public GlDrawElements DrawElements { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate int GlGetUniformLocation(int program, IntPtr name); - [GlEntryPoint("glGetUniformLocation")] - public GlGetUniformLocation GetUniformLocation { get; } + + [GetProcAddress("glEnableVertexAttribArray")] + public partial void EnableVertexAttribArray(int index); + + [GetProcAddress("glUseProgram")] + public partial void UseProgram(int program); + + [GetProcAddress("glDrawArrays")] + public partial void DrawArrays(int mode, int first, IntPtr count); + + [GetProcAddress("glDrawElements")] + public partial void DrawElements(int mode, int count, int type, IntPtr indices); + + [GetProcAddress("glGetUniformLocation")] + public partial int GetUniformLocation(int program, IntPtr name); public int GetUniformLocationString(int program, string name) { @@ -393,41 +313,65 @@ namespace Avalonia.OpenGL return GetUniformLocation(program, b.DangerousGetHandle()); } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlUniform1f(int location, float falue); - [GlEntryPoint("glUniform1f")] - public GlUniform1f Uniform1f { get; } - - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlUniformMatrix4fv(int location, int count, bool transpose, void* value); - [GlEntryPoint("glUniformMatrix4fv")] - public GlUniformMatrix4fv UniformMatrix4fv { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlEnable(int what); - [GlEntryPoint("glEnable")] - public GlEnable Enable { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlDeleteBuffers(int count, int[] buffers); - [GlEntryPoint("glDeleteBuffers")] - public GlDeleteBuffers DeleteBuffers { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlDeleteProgram(int program); - [GlEntryPoint("glDeleteProgram")] - public GlDeleteProgram DeleteProgram { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GlDeleteShader(int shader); - [GlEntryPoint("glDeleteShader")] - public GlDeleteShader DeleteShader { get; } - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - public delegate void GLGetRenderbufferParameteriv(int target, int name, int[] value); - [GlEntryPoint("glGetRenderbufferParameteriv")] - public GLGetRenderbufferParameteriv GetRenderbufferParameteriv { get; } + [GetProcAddress("glUniform1f")] + public partial void Uniform1f(int location, float falue); + + + [GetProcAddress("glUniformMatrix4fv")] + public partial void UniformMatrix4fv(int location, int count, bool transpose, void* value); + + [GetProcAddress("glEnable")] + public partial void Enable(int what); + + [GetProcAddress("glDeleteBuffers")] + public partial void DeleteBuffers(int count, int* buffers); + + public void DeleteBuffer(int buffer) => DeleteBuffers(1, &buffer); + + [GetProcAddress("glDeleteProgram")] + public partial void DeleteProgram(int program); + + [GetProcAddress("glDeleteShader")] + public partial void DeleteShader(int shader); + + [GetProcAddress("glGetRenderbufferParameteriv")] + public partial void GetRenderbufferParameteriv(int target, int name, out int value); // ReSharper restore UnassignedGetOnlyAutoProperty + + [GetProcAddress(true)] + [GlMinVersionEntryPoint("glDeleteVertexArrays", 3, 0)] + [GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")] + public partial void DeleteVertexArrays(int count, int* arrays); + + public void DeleteVertexArray(int array) => DeleteVertexArrays(1, &array); + + [GetProcAddress(true)] + [GlMinVersionEntryPoint("glBindVertexArray", 3, 0)] + [GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")] + public partial void BindVertexArray(int array); + + + [GetProcAddress(true)] + [GlMinVersionEntryPoint("glGenVertexArrays", 3, 0)] + [GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")] + public partial void GenVertexArrays(int n, int* rv); + + public int GenVertexArray() + { + int rv = 0; + GenVertexArrays(1, &rv); + return rv; + } + + public static GlInterface FromNativeUtf8GetProcAddress(GlVersion version, Func getProcAddress) + { + return new GlInterface(version, s => + { + var ptr = Marshal.StringToHGlobalAnsi(s); + var rv = getProcAddress(ptr); + Marshal.FreeHGlobal(ptr); + return rv; + }); + } } -} +} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/GlInterfaceBase.cs b/src/Avalonia.OpenGL/GlInterfaceBase.cs deleted file mode 100644 index e7dd440e36..0000000000 --- a/src/Avalonia.OpenGL/GlInterfaceBase.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using Avalonia.Platform.Interop; - -namespace Avalonia.OpenGL -{ - public class GlInterfaceBase : GlInterfaceBase - { - public GlInterfaceBase(Func getProcAddress) : base(getProcAddress, null) - { - } - - public GlInterfaceBase(Func nativeGetProcAddress) : base(nativeGetProcAddress, null) - { - } - } - - public class GlInterfaceBase - { - private readonly Func _getProcAddress; - public GlInterfaceBase(Func getProcAddress, TContext context) - { - _getProcAddress = getProcAddress; - foreach (var prop in this.GetType().GetProperties()) - { - var attrs = prop.GetCustomAttributes() - .Where(a => - a is IGlEntryPointAttribute || a is IGlEntryPointAttribute) - .ToList(); - if(attrs.Count == 0) - continue; - - var isOptional = prop.GetCustomAttribute() != null; - - var fieldName = $"<{prop.Name}>k__BackingField"; - var field = prop.DeclaringType.GetField(fieldName, - BindingFlags.Instance | BindingFlags.NonPublic); - if (field == null) - throw new InvalidProgramException($"Expected property {prop.Name} to have {fieldName}"); - - - IntPtr proc = IntPtr.Zero; - foreach (var attr in attrs) - { - if (attr is IGlEntryPointAttribute typed) - proc = typed.GetProcAddress(context, getProcAddress); - else if (attr is IGlEntryPointAttribute untyped) - proc = untyped.GetProcAddress(getProcAddress); - if (proc != IntPtr.Zero) - break; - } - - if (proc != IntPtr.Zero) - field.SetValue(this, Marshal.GetDelegateForFunctionPointer(proc, prop.PropertyType)); - else if (!isOptional) - throw new OpenGlException("Unable to find a suitable GL function for " + prop.Name); - } - } - - protected static Func ConvertNative(Func func) => - (proc) => - { - using (var u = new Utf8Buffer(proc)) - { - var rv = func(u); - return rv; - } - }; - - public GlInterfaceBase(Func nativeGetProcAddress, TContext context) : this(ConvertNative(nativeGetProcAddress), context) - { - - } - - public IntPtr GetProcAddress(string proc) => _getProcAddress(proc); - } -} diff --git a/src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml new file mode 100644 index 0000000000..d7bf6e5cf9 --- /dev/null +++ b/src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 468b723f5b..f266402aef 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -66,4 +66,5 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index bc2352d5d0..0499495239 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -68,6 +68,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml new file mode 100644 index 0000000000..75af2efcb1 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 45a76bc3d6..621e6fabd7 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -11,5 +11,5 @@ - + diff --git a/src/Avalonia.X11/Glx/Glx.cs b/src/Avalonia.X11/Glx/Glx.cs index 37ca7f5603..1a26d7d5f5 100644 --- a/src/Avalonia.X11/Glx/Glx.cs +++ b/src/Avalonia.X11/Glx/Glx.cs @@ -4,111 +4,97 @@ using System.Linq; using System.Runtime.InteropServices; using Avalonia.OpenGL; using Avalonia.Platform.Interop; +using Avalonia.SourceGenerator; + // ReSharper disable UnassignedGetOnlyAutoProperty namespace Avalonia.X11.Glx { - unsafe class GlxInterface : GlInterfaceBase + unsafe partial class GlxInterface { private const string libGL = "libGL.so.1"; - [GlEntryPointAttribute("glXMakeContextCurrent")] - public GlxMakeContextCurrent MakeContextCurrent { get; } - public delegate bool GlxMakeContextCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context); + [GetProcAddress("glXMakeContextCurrent")] + public partial bool MakeContextCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context); - [GlEntryPoint("glXGetCurrentContext")] - public GlxGetCurrentContext GetCurrentContext { get; } - public delegate IntPtr GlxGetCurrentContext(); + [GetProcAddress("glXGetCurrentContext")] + public partial IntPtr GetCurrentContext(); - [GlEntryPoint("glXGetCurrentDisplay")] - public GlxGetCurrentDisplay GetCurrentDisplay { get; } - public delegate IntPtr GlxGetCurrentDisplay(); + [GetProcAddress("glXGetCurrentDisplay")] + public partial IntPtr GetCurrentDisplay(); - [GlEntryPoint("glXGetCurrentDrawable")] - public GlxGetCurrentDrawable GetCurrentDrawable { get; } - public delegate IntPtr GlxGetCurrentDrawable(); + [GetProcAddress("glXGetCurrentDrawable")] + public partial IntPtr GetCurrentDrawable(); - [GlEntryPoint("glXGetCurrentReadDrawable")] - public GlxGetCurrentReadDrawable GetCurrentReadDrawable { get; } - public delegate IntPtr GlxGetCurrentReadDrawable(); + [GetProcAddress("glXGetCurrentReadDrawable")] + public partial IntPtr GetCurrentReadDrawable(); - [GlEntryPoint("glXCreatePbuffer")] - public GlxCreatePbuffer CreatePbuffer { get; } - public delegate IntPtr GlxCreatePbuffer(IntPtr dpy, IntPtr fbc, int[] attrib_list); + [GetProcAddress("glXCreatePbuffer")] + public partial IntPtr CreatePbuffer(IntPtr dpy, IntPtr fbc, int[] attrib_list); - [GlEntryPoint("glXDestroyPbuffer")] - public GlxDestroyPbuffer DestroyPbuffer { get; } - public delegate IntPtr GlxDestroyPbuffer(IntPtr dpy, IntPtr fb); + [GetProcAddress("glXDestroyPbuffer")] + public partial IntPtr DestroyPbuffer(IntPtr dpy, IntPtr fb); - [GlEntryPointAttribute("glXChooseVisual")] - public GlxChooseVisual ChooseVisual { get; } - public delegate XVisualInfo* GlxChooseVisual(IntPtr dpy, int screen, int[] attribList); + [GetProcAddress("glXChooseVisual")] + public partial XVisualInfo* ChooseVisual(IntPtr dpy, int screen, int[] attribList); - [GlEntryPointAttribute("glXCreateContext")] - public GlxCreateContext CreateContext { get; } - public delegate IntPtr GlxCreateContext(IntPtr dpy, XVisualInfo* vis, IntPtr shareList, bool direct); + [GetProcAddress("glXCreateContext")] + public partial IntPtr CreateContext(IntPtr dpy, XVisualInfo* vis, IntPtr shareList, bool direct); - [GlEntryPointAttribute("glXCreateContextAttribsARB")] - public GlxCreateContextAttribsARB CreateContextAttribsARB { get; } - public delegate IntPtr GlxCreateContextAttribsARB(IntPtr dpy, IntPtr fbconfig, IntPtr shareList, + [GetProcAddress("glXCreateContextAttribsARB")] + public partial IntPtr CreateContextAttribsARB(IntPtr dpy, IntPtr fbconfig, IntPtr shareList, bool direct, int[] attribs); [DllImport(libGL, EntryPoint = "glXGetProcAddress")] - public static extern IntPtr GlxGetProcAddress(Utf8Buffer buffer); + public static extern IntPtr GlxGetProcAddress(string buffer); - [GlEntryPointAttribute("glXDestroyContext")] - public GlxDestroyContext DestroyContext { get; } - public delegate void GlxDestroyContext(IntPtr dpy, IntPtr ctx); + [GetProcAddress("glXDestroyContext")] + public partial void DestroyContext(IntPtr dpy, IntPtr ctx); - [GlEntryPointAttribute("glXChooseFBConfig")] - public GlxChooseFBConfig ChooseFBConfig { get; } - public delegate IntPtr* GlxChooseFBConfig(IntPtr dpy, int screen, int[] attrib_list, out int nelements); + [GetProcAddress("glXChooseFBConfig")] + public partial IntPtr* ChooseFBConfig(IntPtr dpy, int screen, int[] attrib_list, out int nelements); - public IntPtr* GlxChooseFbConfig(IntPtr dpy, int screen, IEnumerable attribs, out int nelements) + public IntPtr* ChooseFbConfig(IntPtr dpy, int screen, IEnumerable attribs, out int nelements) { var arr = attribs.Concat(new[]{0}).ToArray(); return ChooseFBConfig(dpy, screen, arr, out nelements); } - [GlEntryPointAttribute("glXGetVisualFromFBConfig")] - public GlxGetVisualFromFBConfig GetVisualFromFBConfig { get; } - public delegate XVisualInfo * GlxGetVisualFromFBConfig(IntPtr dpy, IntPtr config); + [GetProcAddress("glXGetVisualFromFBConfig")] + public partial XVisualInfo * GetVisualFromFBConfig(IntPtr dpy, IntPtr config); - [GlEntryPointAttribute("glXGetFBConfigAttrib")] - public GlxGetFBConfigAttrib GetFBConfigAttrib { get; } - public delegate int GlxGetFBConfigAttrib(IntPtr dpy, IntPtr config, int attribute, out int value); + [GetProcAddress("glXGetFBConfigAttrib")] + public partial int GetFBConfigAttrib(IntPtr dpy, IntPtr config, int attribute, out int value); - [GlEntryPointAttribute("glXSwapBuffers")] - public GlxSwapBuffers SwapBuffers { get; } - public delegate void GlxSwapBuffers(IntPtr dpy, IntPtr drawable); + [GetProcAddress("glXSwapBuffers")] + public partial void SwapBuffers(IntPtr dpy, IntPtr drawable); - [GlEntryPointAttribute("glXWaitX")] - public GlxWaitX WaitX { get; } - public delegate void GlxWaitX(); + [GetProcAddress("glXWaitX")] + public partial void WaitX(); - [GlEntryPointAttribute("glXWaitGL")] - public GlxWaitGL WaitGL { get; } - public delegate void GlxWaitGL(); + [GetProcAddress("glXWaitGL")] + public partial void WaitGL(); - public delegate int GlGetError(); - [GlEntryPoint("glGetError")] - public GlGetError GetError { get; } - public delegate IntPtr GlxQueryExtensionsString(IntPtr display, int screen); - [GlEntryPoint("glXQueryExtensionsString")] - public GlxQueryExtensionsString QueryExtensionsString { get; } + [GetProcAddress("glGetError")] + public partial int GlGetError(); + + + [GetProcAddress("glXQueryExtensionsString")] + public partial IntPtr QueryExtensionsString(IntPtr display, int screen); - public GlxInterface() : base(SafeGetProcAddress) + public GlxInterface() { + Initialize(SafeGetProcAddress); } // Ignores egl functions. @@ -122,10 +108,9 @@ namespace Avalonia.X11.Glx return IntPtr.Zero; } - return GlxConverted(proc); + return GlxGetProcAddress(proc); } - private static readonly Func GlxConverted = ConvertNative(GlxGetProcAddress); public string[] GetExtensions(IntPtr display) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs index 64fafc65f3..de281245f1 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs @@ -164,7 +164,7 @@ namespace Avalonia.LinuxFramebuffer.Output else { Fd = open(path, 2, 0); - if(Fd != -1) throw new Win32Exception($"Couldn't open {path}"); + if(Fd == -1) throw new Win32Exception($"Couldn't open {path}"); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 46a985c0e8..99c4b62716 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -50,7 +50,7 @@ namespace Avalonia.LinuxFramebuffer.Output } [DllImport("libEGL.so.1")] - static extern IntPtr eglGetProcAddress(Utf8Buffer proc); + static extern IntPtr eglGetProcAddress(string proc); private GbmBoUserDataDestroyCallbackDelegate FbDestroyDelegate; private drmModeModeInfo _mode; diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs index fdb5977d23..ac7c82c469 100644 --- a/src/Shared/SourceGeneratorAttributes.cs +++ b/src/Shared/SourceGeneratorAttributes.cs @@ -14,4 +14,28 @@ namespace Avalonia.SourceGenerator public string Namespace { get; } public Type BaseType { get; } } + + + internal class GetProcAddressAttribute : Attribute + { + public GetProcAddressAttribute(string proc) + { + + } + + public GetProcAddressAttribute(string proc, bool optional = false) + { + + } + + public GetProcAddressAttribute(bool optional) + { + + } + + public GetProcAddressAttribute() + { + + } + } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index 8ab275df63..984c1785b7 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -27,16 +27,13 @@ namespace Avalonia.Skia gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderbuffer); gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); - var arr = new int[2]; - + // Generate FBO - gl.GenFramebuffers(1, arr); - _fbo = arr[0]; + _fbo = gl.GenFramebuffer(); gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); // Create a texture to render into - gl.GenTextures(1, arr); - _texture = arr[0]; + _texture = gl.GenTexture(); gl.BindTexture(GL_TEXTURE_2D, _texture); gl.TexImage2D(GL_TEXTURE_2D, 0, InternalFormat, pixelSize.Width, pixelSize.Height, @@ -48,8 +45,7 @@ namespace Avalonia.Skia var success = false; foreach (var useStencil8 in TrueFalse) { - gl.GenRenderbuffers(1, arr); - _depthStencil = arr[0]; + _depthStencil = gl.GenRenderbuffer(); gl.BindRenderbuffer(GL_RENDERBUFFER, _depthStencil); if (useStencil8) @@ -73,7 +69,7 @@ namespace Avalonia.Skia else { gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderbuffer); - gl.DeleteRenderbuffers(1, arr); + gl.DeleteRenderbuffer(_depthStencil); } } @@ -83,10 +79,8 @@ namespace Avalonia.Skia if (!success) { - arr[0] = _fbo; - gl.DeleteFramebuffers(1, arr); - arr[0] = _texture; - gl.DeleteTextures(1, arr); + gl.DeleteFramebuffer(_fbo); + gl.DeleteTexture(_texture); throw new OpenGlException("Unable to create FBO with stencil"); } @@ -94,7 +88,7 @@ namespace Avalonia.Skia new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); Surface = SKSurface.Create(_grContext, target, surfaceOrigin, SKColorType.Rgba8888); - CanBlit = gl.BlitFramebuffer != null; + CanBlit = gl.IsBlitFramebufferAvailable; } public void Dispose() @@ -106,9 +100,9 @@ namespace Avalonia.Skia var gl = _glContext.GlInterface; if (_fbo != 0) { - gl.DeleteFramebuffers(1, new[] { _fbo }); - gl.DeleteTextures(1, new[] { _texture }); - gl.DeleteRenderbuffers(1, new[] { _depthStencil }); + gl.DeleteFramebuffer(_fbo); + gl.DeleteTexture(_texture); + gl.DeleteRenderbuffer(_depthStencil); _fbo = _texture = _depthStencil = 0; } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index ec8a8436e1..08e0fbd808 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -54,7 +54,7 @@ namespace Avalonia.Skia return null; // Blit feature requires glBlitFramebuffer - if (_glContext.GlInterface.BlitFramebuffer == null) + if (!_glContext.GlInterface.IsBlitFramebufferAvailable) return null; size = new PixelSize(Math.Max(size.Width, 1), Math.Max(size.Height, 1)); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs index 2ebf7c680b..a4617bb4d5 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs @@ -101,7 +101,7 @@ namespace Avalonia.Skia private bool _disposed; private readonly DisposableLock _lock = new DisposableLock(); - public SharedOpenGlBitmapAttachment(GlOpenGlBitmapImpl bitmap, IGlContext context, Action presentCallback) + public unsafe SharedOpenGlBitmapAttachment(GlOpenGlBitmapImpl bitmap, IGlContext context, Action presentCallback) { _bitmap = bitmap; _context = context; @@ -119,7 +119,8 @@ namespace Avalonia.Skia var gl = _context.GlInterface; var textures = new int[2]; - gl.GenTextures(2, textures); + fixed (int* ptex = textures) + gl.GenTextures(2, ptex); _texture = textures[0]; _frontBuffer = textures[1]; @@ -178,7 +179,7 @@ namespace Avalonia.Skia _presentCallback(); } - public void Dispose() + public unsafe void Dispose() { var gl = _context.GlInterface; _bitmap.Present(null); @@ -191,7 +192,9 @@ namespace Avalonia.Skia if(_disposed) return; _disposed = true; - gl.DeleteTextures(2, new[] { _texture, _frontBuffer }); + var tex = new[] { _texture, _frontBuffer }; + fixed (int* ptex = tex) + gl.DeleteTextures(2, ptex); } } diff --git a/src/iOS/Avalonia.iOS/LayerFbo.cs b/src/iOS/Avalonia.iOS/LayerFbo.cs index 955aaef59f..9a93cdfb22 100644 --- a/src/iOS/Avalonia.iOS/LayerFbo.cs +++ b/src/iOS/Avalonia.iOS/LayerFbo.cs @@ -10,12 +10,12 @@ namespace Avalonia.iOS private readonly EAGLContext _context; private readonly GlInterface _gl; private readonly CAEAGLLayer _layer; - private int[] _framebuffer; - private int[] _renderbuffer; - private int[] _depthBuffer; + private int _framebuffer; + private int _renderbuffer; + private int _depthBuffer; private bool _disposed; - private LayerFbo(EAGLContext context, GlInterface gl, CAEAGLLayer layer, int[] framebuffer, int[] renderbuffer, int[] depthBuffer) + private LayerFbo(EAGLContext context, GlInterface gl, CAEAGLLayer layer, int framebuffer, int renderbuffer, int depthBuffer) { _context = context; _gl = gl; @@ -30,42 +30,36 @@ namespace Avalonia.iOS if (context != EAGLContext.CurrentContext) return null; - var fb = new int[2]; - var rb = new int[2]; - var db = new int[2]; - - gl.GenRenderbuffers(1, rb); - gl.BindRenderbuffer(GlConsts.GL_RENDERBUFFER, rb[0]); + var rb = gl.GenRenderbuffer(); + gl.BindRenderbuffer(GlConsts.GL_RENDERBUFFER, rb); context.RenderBufferStorage(GlConsts.GL_RENDERBUFFER, layer); - gl.GenFramebuffers(1, fb); - gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, fb[0]); - gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_COLOR_ATTACHMENT0, GlConsts.GL_RENDERBUFFER, rb[0]); - - int[] w = new int[1]; - int[] h = new int[1]; - gl.GetRenderbufferParameteriv(GlConsts.GL_RENDERBUFFER, GlConsts.GL_RENDERBUFFER_WIDTH, w); - gl.GetRenderbufferParameteriv(GlConsts.GL_RENDERBUFFER, GlConsts.GL_RENDERBUFFER_HEIGHT, h); + var fb = gl.GenFramebuffer(); + gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, fb); + gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_COLOR_ATTACHMENT0, GlConsts.GL_RENDERBUFFER, rb); - gl.GenRenderbuffers(1, db); + gl.GetRenderbufferParameteriv(GlConsts.GL_RENDERBUFFER, GlConsts.GL_RENDERBUFFER_WIDTH, out var w); + gl.GetRenderbufferParameteriv(GlConsts.GL_RENDERBUFFER, GlConsts.GL_RENDERBUFFER_HEIGHT, out var h); + + var db = gl.GenRenderbuffer(); //GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, depthBuffer); //GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferInternalFormat.DepthComponent16, w, h); - gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_DEPTH_ATTACHMENT, GlConsts.GL_RENDERBUFFER, db[0]); + gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_DEPTH_ATTACHMENT, GlConsts.GL_RENDERBUFFER, db); var frameBufferError = gl.CheckFramebufferStatus(GlConsts.GL_FRAMEBUFFER); if(frameBufferError != GlConsts.GL_FRAMEBUFFER_COMPLETE) { - gl.DeleteFramebuffers(1, fb); - gl.DeleteRenderbuffers(1, db); - gl.DeleteRenderbuffers(1, rb); + gl.DeleteFramebuffer(fb); + gl.DeleteRenderbuffer(db); + gl.DeleteRenderbuffer(rb); return null; } return new LayerFbo(context, gl, layer, fb, rb, db) { - Width = w[0], - Height = h[0] + Width = w, + Height = h }; } @@ -74,7 +68,7 @@ namespace Avalonia.iOS public void Bind() { - _gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, _framebuffer[0]); + _gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, _framebuffer); } public void Present() @@ -88,9 +82,9 @@ namespace Avalonia.iOS if(_disposed) return; _disposed = true; - _gl.DeleteFramebuffers(1, _framebuffer); - _gl.DeleteRenderbuffers(1, _depthBuffer); - _gl.DeleteRenderbuffers(1, _renderbuffer); + _gl.DeleteFramebuffer(_framebuffer); + _gl.DeleteRenderbuffer(_depthBuffer); + _gl.DeleteRenderbuffer(_renderbuffer); if (_context != EAGLContext.CurrentContext) throw new InvalidOperationException("Associated EAGLContext is not current"); } diff --git a/src/tools/DevGenerators/GetProcAddressInitialization.cs b/src/tools/DevGenerators/GetProcAddressInitialization.cs new file mode 100644 index 0000000000..64698d1f2e --- /dev/null +++ b/src/tools/DevGenerators/GetProcAddressInitialization.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Generator; + +[Generator(LanguageNames.CSharp)] +public class GetProcAddressInitializationGenerator : IIncrementalGenerator +{ + const string GetProcAddressFullName = "global::Avalonia.SourceGenerator.GetProcAddressAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var allMethodsWithAttributes = context.SyntaxProvider + .CreateSyntaxProvider( + static (s, _) => s is MethodDeclarationSyntax + { + AttributeLists.Count: > 0, + } md && md.Modifiers.Any(m=>m.IsKind(SyntaxKind.PartialKeyword)), + static (context, _) => + (IMethodSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!); + + var fieldsWithAttribute = allMethodsWithAttributes + .Where(s => s.HasAttributeWithFullyQualifiedName(GetProcAddressFullName)); + + var all = fieldsWithAttribute.Collect(); + context.RegisterSourceOutput(all, static (context, methods) => + { + foreach (var typeGroup in methods.GroupBy(f => f.ContainingType)) + { + var nextContext = 0; + var contexts = new Dictionary(); + + string GetContextNameFromIndex(int c) => "context" + (c == 0 ? "" : c); + string GetContextName(string type) + { + if (contexts.TryGetValue(type, out var idx)) + return GetContextNameFromIndex(idx); + if (nextContext != 0) + idx += nextContext; + nextContext++; + return GetContextNameFromIndex(contexts[type] = idx); + } + + var classBuilder = new StringBuilder(); + if (typeGroup.Key.ContainingNamespace != null) + classBuilder + .AppendLine("using System;") + .Append("namespace ") + .Append(typeGroup.Key.ContainingNamespace) + .AppendLine(";"); + classBuilder + .Append("unsafe partial class ") + .AppendLine(typeGroup.Key.Name) + .AppendLine("{"); + var initializeBody = new StringBuilder() + .Pad(2) + .AppendLine("var addr = IntPtr.Zero;"); + + foreach (var method in typeGroup) + { + var isOptional = false; + var first = true; + var fieldName = "_addr_" + method.Name; + var delegateType = BuildDelegateType(method); + + void AppendNextAddr() + { + if (first) + { + first = false; + initializeBody.Pad(2); + } + else + initializeBody + .Pad(2) + .Append("if(addr == IntPtr.Zero) "); + } + + initializeBody + .Pad(2).Append("// Initializing ").AppendLine(method.Name) + .Pad(2) + .AppendLine("addr = IntPtr.Zero;"); + foreach (var attr in method.GetAttributes()) + { + if (attr.AttributeClass?.HasFullyQualifiedName(GetProcAddressFullName) == true) + { + string? primaryName = null; + foreach (var arg in attr.ConstructorArguments) + { + if (arg.Value is string name) + primaryName = name; + if (arg.Value is bool opt) + isOptional = opt; + } + + if (primaryName != null) + { + AppendNextAddr(); + initializeBody + .Append("addr = getProcAddress(\"") + .Append(primaryName) + .AppendLine("\");"); + } + } + else + { + if (attr.AttributeClass != null + && attr.AttributeClass.MemberNames.Contains("GetProcAddress")) + { + var getProcMethod = attr.AttributeClass.GetMembers() + .FirstOrDefault(m => m.Name == "GetProcAddress") as IMethodSymbol; + if (getProcMethod == null || !getProcMethod.IsStatic || getProcMethod.Parameters.Length < 2) + continue; + var contextName = + GetContextName(getProcMethod + .Parameters[1].Type + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + AppendNextAddr(); + initializeBody + .Append("addr = ") + .Append(attr.AttributeClass.GetFullyQualifiedName()) + .Append(".GetProcAddress(") + .Append("getProcAddress, ") + .Append(contextName); + + var syntaxNode = (AttributeSyntax)attr.ApplicationSyntaxReference.GetSyntax(); + foreach (var arg in syntaxNode.ArgumentList.Arguments) + initializeBody.Append(", ").Append(arg.GetText()); + initializeBody.AppendLine(");"); + } + } + } + + if (!isOptional) + { + initializeBody + .Pad(2) + .Append("if (addr == IntPtr.Zero) throw new System.EntryPointNotFoundException(\"") + .Append(fieldName).AppendLine("\");"); + } + + initializeBody + .Pad(2) + .Append(fieldName) + .Append(" = (") + .Append(delegateType) + .AppendLine(")addr;"); + + classBuilder + .Pad(1) + .Append(delegateType); + classBuilder + .Append(fieldName) + .AppendLine(";"); + + classBuilder + .Pad(1) + .Append("public partial ") + .Append(method.ReturnType.GetFullyQualifiedName()) + .Append(" ") + .Append(method.Name) + .Append("("); + var firstArg = true; + foreach (var p in method.Parameters) + { + if (firstArg) + firstArg = false; + else + classBuilder.Append(", "); + AppendRefKind(classBuilder, p.RefKind); + classBuilder + .Append(p.Type.GetFullyQualifiedName()) + .Append(" @") + .Append(p.Name); + } + classBuilder + .AppendLine(")") + .Pad(1) + .AppendLine("{"); + if (isOptional) + classBuilder + .Pad(2) + .Append("if (") + .Append(fieldName) + .Append(" == null) throw new System.EntryPointNotFoundException(\"") + .Append(method.Name) + .AppendLine("\");"); + + foreach(var p in method.Parameters) + if (NeedsPin(p.Type)) + classBuilder.Pad(2) + .Append("fixed(") + .Append(MapToNative(p.Type)) + .Append(" @__p_") + .Append(p.Name) + .Append(" = ") + .Append(p.Name) + .AppendLine(")"); + + classBuilder.Pad(2); + if (!method.ReturnsVoid) + classBuilder.Append("return "); + + var invokeBuilder = new StringBuilder(); + + invokeBuilder + .Append(fieldName) + .Append("("); + firstArg = true; + foreach (var p in method.Parameters) + { + if (firstArg) + firstArg = false; + else + invokeBuilder.Append(", "); + AppendRefKind(invokeBuilder, p.RefKind); + invokeBuilder + .Append("@") + .Append(ConvertToNative(p.Name, p.Type)); + } + + invokeBuilder.Append(")"); + classBuilder.Append(ConvertToManaged(method.ReturnType, invokeBuilder.ToString())); + + classBuilder.AppendLine(";").Pad(1).AppendLine("}"); + if (isOptional) + classBuilder + .Pad(1) + .Append("public bool Is") + .Append(method.Name) + .Append("Available => ") + .Append(fieldName) + .AppendLine(" != null;"); + } + + classBuilder + .Pad(1) + .Append("void Initialize(Func getProcAddress"); + foreach (var kv in contexts.OrderBy(x => x.Value)) + { + classBuilder + .Append(", ") + .Append(kv.Key) + .Append(" ") + .Append(GetContextNameFromIndex(kv.Value)); + } + + classBuilder.AppendLine(")").Pad(1).AppendLine("{"); + classBuilder.Append(initializeBody.ToString()); + classBuilder.Append("}\n}"); + + + context.AddSource(typeGroup.Key.GetFullyQualifiedName().Replace(":", ""), classBuilder.ToString()); + } + }); + + + } + + static StringBuilder AppendRefKind(StringBuilder sb, RefKind kind) + { + if (kind == RefKind.Ref) + sb.Append("ref "); + if (kind == RefKind.Out) + sb.Append("out "); + return sb; + } + + static bool NeedsPin(ITypeSymbol type) + { + if (type.TypeKind == TypeKind.Array) + return true; + return false; + } + + static string ConvertToNative(string name, ITypeSymbol type) + { + if (NeedsPin(type)) + return "__p_" + name; + if (IsBool(type)) + return $"{name} ? 1 : 0"; + return name; + } + + static string ConvertToManaged(ITypeSymbol type, string expr) + { + if (IsBool(type)) + return expr + " != 0"; + return expr; + } + + static bool IsBool(ITypeSymbol type) => type.GetFullyQualifiedName() == "global::System.Boolean" || + type.GetFullyQualifiedName() == "bool"; + + static string MapToNative(ITypeSymbol type) + { + if (type.TypeKind == TypeKind.Array) + return ((IArrayTypeSymbol)type).ElementType.GetFullyQualifiedName() + "*"; + if (IsBool(type)) + return "int"; + return type.GetFullyQualifiedName(); + } + + static string BuildDelegateType(IMethodSymbol method) + { + StringBuilder name = new("delegate* unmanaged[Stdcall]<"); + var firstArg = true; + + void AppendArg(string a, RefKind kind) + { + if (firstArg) + firstArg = false; + else + name.Append(","); + AppendRefKind(name, kind); + name.Append(a); + } + + foreach (var p in method.Parameters) + { + AppendArg(MapToNative(p.Type), p.RefKind); + } + + AppendArg(MapToNative(method.ReturnType), RefKind.None); + name.Append(">"); + return name.ToString(); + } + +} \ No newline at end of file diff --git a/src/tools/DevGenerators/Helpers.cs b/src/tools/DevGenerators/Helpers.cs new file mode 100644 index 0000000000..3da89d2d0e --- /dev/null +++ b/src/tools/DevGenerators/Helpers.cs @@ -0,0 +1,31 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Generator; + +static class Helpers +{ + public static StringBuilder Pad(this StringBuilder sb, int count) => sb.Append(' ', count * 4); + + public static string GetFullyQualifiedName(this ISymbol symbol) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + public static bool HasFullyQualifiedName(this ISymbol symbol, string name) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name; + } + + public static bool HasAttributeWithFullyQualifiedName(this ISymbol symbol, string name) + { + ImmutableArray attributes = symbol.GetAttributes(); + + foreach (AttributeData attribute in attributes) + if (attribute.AttributeClass?.HasFullyQualifiedName(name) == true) + return true; + + return false; + } +} \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf new file mode 100644 index 0000000000..6d2ad86f94 Binary files /dev/null and b/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf differ diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 04b408a666..13cc14b03e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -15,6 +15,8 @@ namespace Avalonia.Skia.UnitTests.Media private readonly Typeface _defaultTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); + private readonly Typeface _arabicTypeface = + new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Kufi Arabic"); private readonly Typeface _italicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); private readonly Typeface _emojiTypeface = @@ -22,7 +24,7 @@ namespace Avalonia.Skia.UnitTests.Media public CustomFontManagerImpl() { - _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; + _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _defaultTypeface }; _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index df286d709e..649e1fbf3d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs @@ -33,15 +33,11 @@ namespace Avalonia.Skia.UnitTests.Media { var fontManager = new FontManagerImpl(); - //we need to have a valid font name different from the default one - string fontName = fontManager.GetInstalledFontFamilyNames().First(); - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily($"A, B, {fontName}"), weight: FontWeight.Bold)); + new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold)); var skTypeface = glyphTypeface.Typeface; - - Assert.Equal(fontName, skTypeface.FamilyName); + Assert.True(skTypeface.FontWeight >= 600); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 7e1103d624..7d33f094fa 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -154,7 +154,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { j += inner.Current.Text.Length; - if(j + i > text.Length) + if (j + i > text.Length) { break; } @@ -738,7 +738,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); - + var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); var rects = layout.HitTestTextRange(0, 7).ToArray(); @@ -746,7 +746,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, rects.Length); var expected = rects[0]; - + Assert.Equal(expected.Left, start); Assert.Equal(expected.Right, end); } @@ -818,11 +818,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); Assert.Equal(expected, actual); - } + } } } } - + [Fact] public void Should_Layout_Empty_String() { @@ -833,11 +833,128 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Typeface.Default, 12, Brushes.Black); - + Assert.True(layout.Bounds.Height > 0); } } + [Fact] + public void Should_HitTestPoint_RightToLeft() + { + using (Start()) + { + var text = "אאא AAA"; + + var layout = new TextLayout( + text, + Typeface.Default, + 12, + Brushes.Black, + flowDirection: FlowDirection.RightToLeft); + + var firstRun = layout.TextLines[0].TextRuns[0] as ShapedTextCharacters; + + var hit = layout.HitTestPoint(new Point()); + + Assert.Equal(4, hit.TextPosition); + + var currentX = 0.0; + + for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++) + { + var cluster = firstRun.GlyphRun.GlyphClusters[i]; + var advance = firstRun.GlyphRun.GlyphAdvances[i]; + + hit = layout.HitTestPoint(new Point(currentX, 0)); + + Assert.Equal(cluster, hit.TextPosition); + + var hitRange = layout.HitTestTextRange(hit.TextPosition, 1); + + var distance = hitRange.First().Left; + + Assert.Equal(currentX, distance); + + currentX += advance; + } + + var secondRun = layout.TextLines[0].TextRuns[1] as ShapedTextCharacters; + + hit = layout.HitTestPoint(new Point(firstRun.Size.Width, 0)); + + Assert.Equal(7, hit.TextPosition); + + hit = layout.HitTestPoint(new Point(layout.TextLines[0].WidthIncludingTrailingWhitespace, 0)); + + Assert.Equal(0, hit.TextPosition); + + currentX = firstRun.Size.Width + 0.5; + + for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++) + { + var cluster = secondRun.GlyphRun.GlyphClusters[i]; + var advance = secondRun.GlyphRun.GlyphAdvances[i]; + + hit = layout.HitTestPoint(new Point(currentX, 0)); + + Assert.Equal(cluster, hit.CharacterHit.FirstCharacterIndex); + + var hitRange = layout.HitTestTextRange(hit.CharacterHit.FirstCharacterIndex, hit.CharacterHit.TrailingLength); + + var distance = hitRange.First().Left + 0.5; + + Assert.Equal(currentX, distance); + + currentX += advance; + } + } + } + + [Fact] + public void Should_Get_CharacterHit_From_Distance_RTL() + { + using (Start()) + { + var text = "أَبْجَدِيَّة عَرَبِيَّة"; + + var layout = new TextLayout( + text, + Typeface.Default, + 12, + Brushes.Black); + + var textLine = layout.TextLines[0]; + + var firstRun = (ShapedTextCharacters)textLine.TextRuns[0]; + + var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; + + var characterHit = textLine.GetCharacterHitFromDistance(0); + + Assert.Equal(firstCluster, characterHit.FirstCharacterIndex); + + Assert.Equal(text.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + + var distance = textLine.GetDistanceFromCharacterHit(characterHit); + + Assert.Equal(0, distance); + + distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); + + var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0]; + + Assert.Equal(firstAdvance, distance, 5); + + var rect = layout.HitTestTextPosition(22); + + Assert.Equal(firstAdvance, rect.Left, 5); + + rect = layout.HitTestTextPosition(23); + + Assert.Equal(0, rect.Left, 5); + } + } + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 58cb84c4a4..d744ede87d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -867,28 +867,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, 4); - var firstRun = textLine.TextRuns[1] as ShapedTextCharacters; + var secondRun = textLine.TextRuns[1] as ShapedTextCharacters; Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); + Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(4, 3); - var secondRun = textLine.TextRuns[0] as ShapedTextCharacters; + var firstRun = textLine.TextRuns[0] as ShapedTextCharacters; Assert.Equal(1, textBounds.Count); Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length)); - Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); + Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 5); Assert.Equal(2, textBounds.Count); Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length))); - Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); - Assert.Equal(7.201171875, textBounds[1].Rectangle.Width); - Assert.Equal(textLine.Start + 7.201171875, textBounds[1].Rectangle.Right); + Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); + Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); + Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right); + Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left); textBounds = textLine.GetTextBounds(0, text.Length); @@ -896,7 +897,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } - } + } private class FixedRunsTextSource : ITextSource {