Browse Source

Merge branch 'master' into fix/ProgressBarShouldListenToTrackSize

pull/8498/head
Max Katz 4 years ago
committed by GitHub
parent
commit
deb104a789
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 44
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  2. 2
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  3. 109
      src/Avalonia.Base/Media/GlyphRun.cs
  4. 2
      src/Avalonia.Base/Media/TextDecoration.cs
  5. 51
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  6. 433
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  7. 9
      src/Avalonia.Controls/Documents/LineBreak.cs
  8. 119
      src/Avalonia.Controls/RichTextBlock.cs
  9. 7
      src/Avalonia.Controls/TextBlock.cs
  10. 285
      src/Avalonia.Controls/TextBox.cs
  11. 4
      src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs
  12. 1
      src/Avalonia.OpenGL/Avalonia.OpenGL.csproj
  13. 14
      src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs
  14. 2
      src/Avalonia.OpenGL/Egl/EglContext.cs
  15. 4
      src/Avalonia.OpenGL/Egl/EglDisplay.cs
  16. 225
      src/Avalonia.OpenGL/Egl/EglInterface.cs
  17. 44
      src/Avalonia.OpenGL/GlBasicInfoInterface.cs
  18. 103
      src/Avalonia.OpenGL/GlEntryPointAttribute.cs
  19. 494
      src/Avalonia.OpenGL/GlInterface.cs
  20. 79
      src/Avalonia.OpenGL/GlInterfaceBase.cs
  21. 10
      src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml
  22. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  23. 1
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  24. 14
      src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
  25. 2
      src/Avalonia.X11/Avalonia.X11.csproj
  26. 111
      src/Avalonia.X11/Glx/Glx.cs
  27. 2
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
  28. 2
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
  29. 24
      src/Shared/SourceGeneratorAttributes.cs
  30. 28
      src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs
  31. 2
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs
  32. 11
      src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs
  33. 52
      src/iOS/Avalonia.iOS/LayerFbo.cs
  34. 338
      src/tools/DevGenerators/GetProcAddressInitialization.cs
  35. 31
      src/tools/DevGenerators/Helpers.cs
  36. BIN
      tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf
  37. 4
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  38. 8
      tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs
  39. 129
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  40. 17
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

44
samples/ControlCatalog/Pages/OpenGlPage.xaml.cs

@ -90,7 +90,6 @@ namespace ControlCatalog.Pages
private int _vertexBufferObject; private int _vertexBufferObject;
private int _indexBufferObject; private int _indexBufferObject;
private int _vertexArrayObject; private int _vertexArrayObject;
private GlExtrasInterface _glExt;
private string GetShader(bool fragment, string shader) private string GetShader(bool fragment, string shader)
{ {
@ -258,7 +257,6 @@ namespace ControlCatalog.Pages
protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) protected unsafe override void OnOpenGlInit(GlInterface GL, int fb)
{ {
CheckError(GL); CheckError(GL);
_glExt = new GlExtrasInterface(GL);
Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}"; 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.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata),
GL_STATIC_DRAW); GL_STATIC_DRAW);
CheckError(GL); CheckError(GL);
_vertexArrayObject = _glExt.GenVertexArray(); _vertexArrayObject = GL.GenVertexArray();
_glExt.BindVertexArray(_vertexArrayObject); GL.BindVertexArray(_vertexArrayObject);
CheckError(GL); CheckError(GL);
GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT, GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT,
0, vertexSize, IntPtr.Zero); 0, vertexSize, IntPtr.Zero);
@ -316,12 +314,13 @@ namespace ControlCatalog.Pages
// Unbind everything // Unbind everything
GL.BindBuffer(GL_ARRAY_BUFFER, 0); GL.BindBuffer(GL_ARRAY_BUFFER, 0);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
_glExt.BindVertexArray(0); GL.BindVertexArray(0);
GL.UseProgram(0); GL.UseProgram(0);
// Delete all resources. // Delete all resources.
GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject }); GL.DeleteBuffer(_vertexBufferObject);
_glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject }); GL.DeleteBuffer(_indexBufferObject);
GL.DeleteVertexArray(_vertexArrayObject);
GL.DeleteProgram(_shaderProgram); GL.DeleteProgram(_shaderProgram);
GL.DeleteShader(_fragmentShader); GL.DeleteShader(_fragmentShader);
GL.DeleteShader(_vertexShader); GL.DeleteShader(_vertexShader);
@ -338,7 +337,7 @@ namespace ControlCatalog.Pages
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject); GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject);
_glExt.BindVertexArray(_vertexArrayObject); GL.BindVertexArray(_vertexArrayObject);
GL.UseProgram(_shaderProgram); GL.UseProgram(_shaderProgram);
CheckError(GL); CheckError(GL);
var projection = var projection =
@ -369,34 +368,5 @@ namespace ControlCatalog.Pages
if (_disco > 0.01) if (_disco > 0.01)
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
} }
class GlExtrasInterface : GlInterfaceBase<GlInterface.GlContextInfo>
{
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];
}
}
} }
} }

2
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border> <Border>
<RichTextBlock Margin="10" TextWrapping="Wrap"> <RichTextBlock SelectionBrush="LightBlue" IsTextSelectionEnabled="True" Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span> <Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span> with <Span TextDecorations="Underline">several</Span>

109
src/Avalonia.Base/Media/GlyphRun.cs

@ -445,7 +445,7 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public int FindGlyphIndex(int characterIndex) public int FindGlyphIndex(int characterIndex)
{ {
if (GlyphClusters == null) if (GlyphClusters == null || GlyphClusters.Count == 0)
{ {
return characterIndex; return characterIndex;
} }
@ -614,17 +614,29 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics() 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) 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); _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
} }
var isReversed = firstCluster > lastCluster;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var widthIncludingTrailingWhitespace = 0d; 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++) for (var index = 0; index < GlyphIndices.Count; index++)
{ {
@ -635,16 +647,16 @@ namespace Avalonia.Media
var width = widthIncludingTrailingWhitespace; 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 _); width -= GetGlyphAdvance(index, out _);
} }
} }
else else
{ {
for (var index = 0; index < glyphCount; index++) for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
{ {
width -= GetGlyphAdvance(index, out _); width -= GetGlyphAdvance(index, out _);
} }
@ -654,16 +666,15 @@ namespace Avalonia.Media
height); height);
} }
private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount) private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
{ {
glyphCount = 0; if (isReversed)
newLineLength = 0;
if (Characters.IsEmpty)
{ {
return 0; return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
} }
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0; var trailingWhitespaceLength = 0;
if (GlyphClusters == null) if (GlyphClusters == null)
@ -732,6 +743,78 @@ namespace Avalonia.Media
return trailingWhitespaceLength; 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<T>(ref T field, T value) private void Set<T>(ref T field, T value)
{ {
_glyphRunImpl?.Dispose(); _glyphRunImpl?.Dispose();

2
src/Avalonia.Base/Media/TextDecoration.cs

@ -209,7 +209,7 @@ namespace Avalonia.Media
var pen = new Pen(Stroke ?? defaultBrush, thickness, var pen = new Pen(Stroke ?? defaultBrush, thickness,
new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); 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));
} }
} }
} }

51
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -63,7 +63,7 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight; MaxHeight = maxHeight;
MaxLines = maxLines; MaxLines = maxLines;
TextLines = CreateTextLines(); TextLines = CreateTextLines();
} }
@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxLines">The maximum number of text lines.</param> /// <param name="maxLines">The maximum number of text lines.</param>
public TextLayout( public TextLayout(
ITextSource textSource, ITextSource textSource,
TextParagraphProperties paragraphProperties, TextParagraphProperties paragraphProperties,
TextTrimming? textTrimming = null, TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity, double maxHeight = double.PositiveInfinity,
@ -178,24 +178,18 @@ namespace Avalonia.Media.TextFormatting
return new Rect(); return new Rect();
} }
if (textPosition < 0 || textPosition >= _textSourceLength) if (textPosition < 0)
{ {
var lastLine = TextLines[TextLines.Count - 1]; textPosition = _textSourceLength;
var lineX = lastLine.Width;
var lineY = Bounds.Bottom - lastLine.Height;
return new Rect(lineX, lineY, 0, lastLine.Height);
} }
var currentY = 0.0; var currentY = 0.0;
foreach (var textLine in TextLines) 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; currentY += textLine.Height;
@ -224,7 +218,7 @@ namespace Avalonia.Media.TextFormatting
} }
var result = new List<Rect>(TextLines.Count); var result = new List<Rect>(TextLines.Count);
var currentY = 0d; var currentY = 0d;
foreach (var textLine in TextLines) foreach (var textLine in TextLines)
@ -239,7 +233,7 @@ namespace Avalonia.Media.TextFormatting
var textBounds = textLine.GetTextBounds(start, length); var textBounds = textLine.GetTextBounds(start, length);
if(textBounds.Count > 0) if (textBounds.Count > 0)
{ {
foreach (var bounds in textBounds) 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; break;
} }
@ -305,7 +299,7 @@ namespace Avalonia.Media.TextFormatting
return GetHitTestResult(currentLine, characterHit, point); return GetHitTestResult(currentLine, characterHit, point);
} }
public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge) public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge)
{ {
if (charIndex < 0) if (charIndex < 0)
@ -327,7 +321,7 @@ namespace Avalonia.Media.TextFormatting
continue; continue;
} }
if (charIndex >= textLine.FirstTextSourceIndex && if (charIndex >= textLine.FirstTextSourceIndex &&
charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1)) charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
{ {
return index; return index;
@ -398,7 +392,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="left">The current left.</param> /// <param name="left">The current left.</param>
/// <param name="width">The current width.</param> /// <param name="width">The current width.</param>
/// <param name="height">The current height.</param> /// <param name="height">The current height.</param>
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; var lineWidth = textLine.WidthIncludingTrailingWhitespace;
@ -421,7 +415,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); 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> { textLine }; return new List<TextLine> { textLine };
} }
@ -439,9 +433,9 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak); _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); var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
@ -454,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
} }
_textSourceLength += textLine.Length; _textSourceLength += textLine.Length;
//Fulfill max height constraint //Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight) if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
{ {
@ -485,12 +479,17 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max lines constraint //Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines) if (MaxLines > 0 && textLines.Count >= MaxLines)
{ {
if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null)
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
}
break; break;
} }
} }
//Make sure the TextLayout always contains at least on empty line //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); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
@ -501,7 +500,7 @@ namespace Avalonia.Media.TextFormatting
Bounds = new Rect(left, 0, width, height); Bounds = new Rect(left, 0, width, height);
if(_paragraphProperties.TextAlignment == TextAlignment.Justify) if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{ {
var whitespaceWidth = 0d; var whitespaceWidth = 0d;
@ -509,7 +508,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if(lineWhitespaceWidth > whitespaceWidth) if (lineWhitespaceWidth > whitespaceWidth)
{ {
whitespaceWidth = lineWhitespaceWidth; whitespaceWidth = lineWhitespaceWidth;
} }
@ -517,7 +516,7 @@ namespace Avalonia.Media.TextFormatting
var justificationWidth = width - whitespaceWidth; var justificationWidth = width - whitespaceWidth;
if(justificationWidth > 0) if (justificationWidth > 0)
{ {
var justificationProperties = new InterWordJustification(justificationWidth); var justificationProperties = new InterWordJustification(justificationWidth);

433
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -166,58 +166,74 @@ namespace Avalonia.Media.TextFormatting
if (distance <= 0) if (distance <= 0)
{ {
// hit happens before the line, return the first position
var firstRun = _textRuns[0]; var firstRun = _textRuns[0];
if (firstRun is ShapedTextCharacters shapedTextCharacters) return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0);
{ }
return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _);
}
return _resolvedFlowDirection == FlowDirection.LeftToRight ? if (distance > WidthIncludingTrailingWhitespace)
new CharacterHit(FirstTextSourceIndex) : {
new CharacterHit(FirstTextSourceIndex + Length); var lastRun = _textRuns[_textRuns.Count - 1];
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width);
} }
// process hit that happens within the line // process hit that happens within the line
var characterHit = new CharacterHit(); var characterHit = new CharacterHit();
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0;
foreach (var currentRun in _textRuns) foreach (var currentRun in _textRuns)
{ {
switch (currentRun) if (currentDistance + currentRun.Size.Width < distance)
{ {
case ShapedTextCharacters shapedRun: currentDistance += currentRun.Size.Width;
{ currentPosition += currentRun.TextSourceLength;
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
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; break;
} }
default:
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) offset = Math.Max(0, offset - shapedRun.Text.End);
{
characterHit = new CharacterHit(currentPosition);
}
else
{
characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength);
}
break;
} }
}
if (distance <= currentRun.Size.Width) characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
{
break;
}
distance -= currentRun.Size.Width; break;
currentPosition += currentRun.TextSourceLength; }
default:
{
if (distance < run.Size.Width / 2)
{
characterHit = new CharacterHit(currentPosition);
}
else
{
characterHit = new CharacterHit(currentPosition, run.TextSourceLength);
}
break;
}
} }
return characterHit; return characterHit;
@ -226,136 +242,122 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override double GetDistanceFromCharacterHit(CharacterHit characterHit) public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{ {
var isTrailingHit = characterHit.TrailingLength > 0; var flowDirection = _paragraphProperties.FlowDirection;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var currentDistance = Start;
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var remainingLength = characterIndex - 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 = _textRuns[index];
{
var currentRun = shapedTextCharacters.GlyphRun;
if (lastRun != null) if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
{ flowDirection, out var distance, out var currentGlyphRun))
if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight && {
currentRun.Characters.Start == characterHit.FirstCharacterIndex && if (currentGlyphRun != null)
characterHit.TrailingLength == 0) {
{ distance = currentGlyphRun.Size.Width - distance;
return currentDistance; }
}
}
//Look for a hit in within the current run return currentDistance - distance;
if (currentPosition + remainingLength <= currentPosition + textRun.Text.Length) }
{
characterHit = new CharacterHit(textRun.Text.Start + remainingLength);
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 private static bool TryGetDistanceFromCharacterHit(
if (currentRun.IsLeftToRight) DrawableTextRun currentRun,
{ CharacterHit characterHit,
if (_resolvedFlowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) int currentPosition,
{ int remainingLength,
if (characterIndex <= currentPosition) FlowDirection flowDirection,
{ out double distance,
return currentDistance; out GlyphRun? currentGlyphRun)
} {
} var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
else var isTrailingHit = characterHit.TrailingLength > 0;
{
if (characterIndex == currentPosition)
{
return currentDistance;
}
}
if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit) distance = 0;
{ currentGlyphRun = null;
return currentDistance + currentRun.Size.Width;
}
}
else
{
if (characterIndex == currentPosition)
{
return currentDistance + currentRun.Size.Width;
}
var nextRun = index + 1 < _textRuns.Count ? switch (currentRun)
_textRuns[index + 1] as ShapedTextCharacters : {
null; case ShapedTextCharacters shapedTextCharacters:
{
currentGlyphRun = shapedTextCharacters.GlyphRun;
if (nextRun != null) if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length)
{ {
if (nextRun.ShapedBuffer.IsLeftToRight) characterHit = new CharacterHit(currentRun.Text.Start + remainingLength);
{
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;
}
}
}
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 true;
{ }
return currentDistance + textRun.Size.Width;
}
break; break;
}
default:
{
if (characterIndex == currentPosition)
{
return true;
} }
}
//No hit hit found so we add the full width if (characterIndex == currentPosition + currentRun.TextSourceLength)
currentDistance += textRun.Size.Width; {
currentPosition += textRun.TextSourceLength; distance = currentRun.Size.Width;
remainingLength -= textRun.TextSourceLength;
if (remainingLength <= 0) return true;
{
break; }
}
break;
}
} }
return currentDistance; return false;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -460,20 +462,33 @@ namespace Avalonia.Media.TextFormatting
var startIndex = currentRun.Text.Start + offset; var startIndex = currentRun.Text.Start + offset;
var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( double startOffset;
currentShapedRun.ShapedBuffer.IsLeftToRight ? double endOffset;
new CharacterHit(startIndex + remainingLength) :
new CharacterHit(startIndex));
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( if (currentPosition < startIndex)
currentShapedRun.ShapedBuffer.IsLeftToRight ? {
new CharacterHit(startIndex) : startOffset = endOffset;
new CharacterHit(startIndex + remainingLength)); }
else
{
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
}
startX += startOffset; startX += startOffset;
endX += endOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, 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 //Lines that only contain a linebreak need to be covered here
if(characterLength == 0) if (characterLength == 0)
{ {
characterLength = NewLineLength; characterLength = NewLineLength;
} }
var runwidth = endX - startX; var runWidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runwidth, Height), currentPosition, characterLength, currentRun); 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); textBounds.TextRunBounds.Add(currentRunBounds);
} }
else else
{ {
currentRect = currentRunBounds.Rectangle; currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds })); result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
} }
currentWidth += runwidth; currentWidth += runWidth;
currentPosition += characterLength; currentPosition += characterLength;
if (currentDirection == FlowDirection.LeftToRight) if (currentPosition > characterIndex)
{
if (currentPosition > characterIndex)
{
break;
}
}
else
{ {
if (currentPosition <= firstTextSourceIndex) break;
{
break;
}
} }
startX = endX; startX = endX;
@ -571,7 +579,7 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength; var remainingLength = textLength;
var startX = Start + WidthIncludingTrailingWhitespace; var startX = WidthIncludingTrailingWhitespace;
double currentWidth = 0; double currentWidth = 0;
var currentRect = Rect.Empty; var currentRect = Rect.Empty;
@ -582,7 +590,7 @@ namespace Avalonia.Media.TextFormatting
continue; continue;
} }
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex)
{ {
startX -= currentRun.Size.Width; startX -= currentRun.Size.Width;
@ -601,20 +609,31 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset; currentPosition += offset;
var startIndex = currentRun.Text.Start + offset; var startIndex = currentRun.Text.Start + offset;
double startOffset;
double endOffset;
var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( if (currentShapedRun.ShapedBuffer.IsLeftToRight)
currentShapedRun.ShapedBuffer.IsLeftToRight ? {
new CharacterHit(startIndex + remainingLength) : if (currentPosition < startIndex)
new CharacterHit(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( startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
currentShapedRun.ShapedBuffer.IsLeftToRight ? }
new CharacterHit(startIndex) :
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 endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
@ -652,41 +671,35 @@ namespace Avalonia.Media.TextFormatting
} }
var runWidth = endX - startX; 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); textBounds.TextRunBounds.Add(currentRunBounds);
} }
else else
{ {
currentRect = currentRunBounds.Rectangle; currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds })); result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
} }
}
currentWidth += runWidth; currentWidth += runWidth;
currentPosition += characterLength; currentPosition += characterLength;
if (currentDirection == FlowDirection.LeftToRight) if (currentPosition > characterIndex)
{
if (currentPosition > characterIndex)
{
break;
}
}
else
{ {
if (currentPosition <= firstTextSourceIndex) break;
{
break;
}
} }
lastDirection = currentDirection; lastDirection = currentDirection;
@ -698,6 +711,8 @@ namespace Avalonia.Media.TextFormatting
} }
} }
result.Reverse();
return result; return result;
} }
@ -1302,8 +1317,14 @@ namespace Avalonia.Media.TextFormatting
switch (textAlignment) switch (textAlignment)
{ {
case TextAlignment.Center: 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: case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);

9
src/Avalonia.Controls/Documents/LineBreak.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.LogicalTree;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -22,7 +21,13 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns) internal override void BuildTextRun(IList<TextRun> 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) internal override void AppendText(StringBuilder stringBuilder)

119
src/Avalonia.Controls/RichTextBlock.cs

@ -41,9 +41,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush?> SelectionBrushProperty = public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue); AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionForegroundBrush));
/// <summary> /// <summary>
/// Defines the <see cref="Inlines"/> property. /// Defines the <see cref="Inlines"/> property.
/// </summary> /// </summary>
@ -63,12 +60,13 @@ namespace Avalonia.Controls
private bool _canCopy; private bool _canCopy;
private int _selectionStart; private int _selectionStart;
private int _selectionEnd; private int _selectionEnd;
private int _wordSelectionStart = -1;
static RichTextBlock() static RichTextBlock()
{ {
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true); FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionForegroundBrushProperty, SelectionBrushProperty); AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
} }
public RichTextBlock() public RichTextBlock()
@ -89,15 +87,6 @@ namespace Avalonia.Controls
set => SetValue(SelectionBrushProperty, value); set => SetValue(SelectionBrushProperty, value);
} }
/// <summary>
/// Gets or sets a value that defines the brush used for selected text.
/// </summary>
public IBrush? SelectionForegroundBrush
{
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets a character index for the beginning of the current selection. /// Gets or sets a character index for the beginning of the current selection.
/// </summary> /// </summary>
@ -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 selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
@ -215,13 +204,16 @@ namespace Avalonia.Controls
var rects = TextLayout.HitTestTextRange(start, length); 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);
} }
/// <summary> /// <summary>
@ -297,8 +289,9 @@ namespace Avalonia.Controls
/// <returns>A <see cref="TextLayout"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout(string? text) protected override TextLayout CreateTextLayout(string? text)
{ {
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var defaultProperties = new GenericTextRunProperties( var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), typeface,
FontSize, FontSize,
TextDecorations, TextDecorations,
Foreground); Foreground);
@ -345,6 +338,8 @@ namespace Avalonia.Controls
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
{ {
base.OnKeyDown(e);
var handled = false; var handled = false;
var modifiers = e.KeyModifiers; var modifiers = e.KeyModifiers;
var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>(); var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>();
@ -363,6 +358,8 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
base.OnPointerPressed(e);
if (!IsTextSelectionEnabled) if (!IsTextSelectionEnabled)
{ {
return; return;
@ -373,7 +370,9 @@ namespace Avalonia.Controls
if (text != null && clickInfo.Properties.IsLeftButtonPressed) 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); var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
@ -382,8 +381,6 @@ namespace Avalonia.Controls
var hit = TextLayout.HitTestPoint(point); var hit = TextLayout.HitTestPoint(point);
var index = hit.TextPosition; var index = hit.TextPosition;
SelectionStart = SelectionEnd = index;
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount) switch (e.ClickCount)
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
@ -391,12 +388,34 @@ namespace Avalonia.Controls
case 1: case 1:
if (clickToSelect) if (clickToSelect)
{ {
SelectionStart = Math.Min(oldIndex, index); if (_wordSelectionStart >= 0)
SelectionEnd = Math.Max(oldIndex, index); {
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 else
{ {
SelectionStart = SelectionEnd = index; if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
{
SelectionStart = SelectionEnd = index;
_wordSelectionStart = -1;
}
} }
break; break;
@ -406,9 +425,13 @@ namespace Avalonia.Controls
SelectionStart = StringUtils.PreviousWord(text, index); SelectionStart = StringUtils.PreviousWord(text, index);
} }
_wordSelectionStart = SelectionStart;
SelectionEnd = StringUtils.NextWord(text, index); SelectionEnd = StringUtils.NextWord(text, index);
break; break;
case 3: case 3:
_wordSelectionStart = -1;
SelectAll(); SelectAll();
break; break;
} }
@ -420,6 +443,8 @@ namespace Avalonia.Controls
protected override void OnPointerMoved(PointerEventArgs e) protected override void OnPointerMoved(PointerEventArgs e)
{ {
base.OnPointerMoved(e);
if (!IsTextSelectionEnabled) if (!IsTextSelectionEnabled)
{ {
return; return;
@ -428,20 +453,49 @@ namespace Avalonia.Controls
// selection should not change during pointer move if the user right clicks // selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) 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( point = new Point(
MathUtilities.Clamp(point.X, 0, Math.Max(Bounds.Width - 1, 0)), MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(Bounds.Height - 1, 0))); MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0)));
var hit = TextLayout.HitTestPoint(point); 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) protected override void OnPointerReleased(PointerReleasedEventArgs e)
{ {
base.OnPointerReleased(e);
if (!IsTextSelectionEnabled) if (!IsTextSelectionEnabled)
{ {
return; return;
@ -454,7 +508,9 @@ namespace Avalonia.Controls
if (e.InitialPressMouseButton == MouseButton.Right) 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); var hit = TextLayout.HitTestPoint(point);
@ -487,11 +543,6 @@ namespace Avalonia.Controls
InvalidateTextLayout(); InvalidateTextLayout();
break; break;
} }
case nameof(TextProperty):
{
InvalidateTextLayout();
break;
}
} }
} }

7
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<string>.AddChild(string text) void IAddChild<string>.AddChild(string text)

285
src/Avalonia.Controls/TextBox.cs

@ -53,7 +53,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<char> PasswordCharProperty = public static readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar)); AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar));
public static readonly StyledProperty<IBrush?> SelectionBrushProperty = public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush?>(nameof(SelectionBrush)); AvaloniaProperty.Register<TextBox, IBrush?>(nameof(SelectionBrush));
@ -80,7 +80,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<int> MaxLinesProperty = public static readonly StyledProperty<int> MaxLinesProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines), defaultValue: 0); AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines), defaultValue: 0);
public static readonly DirectProperty<TextBox, string?> TextProperty = public static readonly DirectProperty<TextBox, string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>( TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>(
o => o.Text, o => o.Text,
@ -105,7 +105,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextWrapping> TextWrappingProperty = public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
TextBlock.TextWrappingProperty.AddOwner<TextBox>(); TextBlock.TextWrappingProperty.AddOwner<TextBox>();
/// <summary> /// <summary>
/// Defines see <see cref="TextPresenter.LineHeight"/> property. /// Defines see <see cref="TextPresenter.LineHeight"/> property.
/// </summary> /// </summary>
@ -202,6 +202,7 @@ namespace Avalonia.Controls
private string _newLine = Environment.NewLine; private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
private int _wordSelectionStart = -1;
private int _selectedTextChangesMadeSinceLastUndoSnapshot; private int _selectedTextChangesMadeSinceLastUndoSnapshot;
private bool _hasDoneSnapshotOnce; private bool _hasDoneSnapshotOnce;
private const int _maxCharsBeforeUndoSnapshot = 7; private const int _maxCharsBeforeUndoSnapshot = 7;
@ -275,7 +276,7 @@ namespace Avalonia.Controls
get => GetValue(IsReadOnlyProperty); get => GetValue(IsReadOnlyProperty);
set => SetValue(IsReadOnlyProperty, value); set => SetValue(IsReadOnlyProperty, value);
} }
public char PasswordChar public char PasswordChar
{ {
get => GetValue(PasswordCharProperty); get => GetValue(PasswordCharProperty);
@ -307,7 +308,7 @@ namespace Avalonia.Controls
{ {
value = CoerceCaretIndex(value); value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value); var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
if (changed) if (changed)
{ {
UpdateCommandStates(); UpdateCommandStates();
@ -327,12 +328,12 @@ namespace Avalonia.Controls
{ {
value = CoerceCaretIndex(value); value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
if (changed) if (changed)
{ {
UpdateCommandStates(); UpdateCommandStates();
} }
if (SelectionStart == value && CaretIndex != value) if (SelectionStart == value && CaretIndex != value)
{ {
CaretIndex = value; CaretIndex = value;
@ -351,7 +352,7 @@ namespace Avalonia.Controls
get => GetValue(MaxLinesProperty); get => GetValue(MaxLinesProperty);
set => SetValue(MaxLinesProperty, value); set => SetValue(MaxLinesProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the line height. /// Gets or sets the line height.
/// </summary> /// </summary>
@ -370,7 +371,7 @@ namespace Avalonia.Controls
var caretIndex = CaretIndex; var caretIndex = CaretIndex;
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
CaretIndex = CoerceCaretIndex(caretIndex, value); CaretIndex = CoerceCaretIndex(caretIndex, value);
SelectionStart = CoerceCaretIndex(selectionStart, value); SelectionStart = CoerceCaretIndex(selectionStart, value);
SelectionEnd = CoerceCaretIndex(selectionEnd, value); SelectionEnd = CoerceCaretIndex(selectionEnd, value);
@ -567,7 +568,7 @@ namespace Avalonia.Controls
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
_imClient.SetPresenter(_presenter, this); _imClient.SetPresenter(_presenter, this);
if (IsFocused) if (IsFocused)
{ {
_presenter?.ShowCaret(); _presenter?.ShowCaret();
@ -577,7 +578,7 @@ namespace Avalonia.Controls
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
if (IsFocused) if (IsFocused)
{ {
_presenter?.ShowCaret(); _presenter?.ShowCaret();
@ -587,7 +588,7 @@ namespace Avalonia.Controls
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnDetachedFromVisualTree(e); base.OnDetachedFromVisualTree(e);
_imClient.SetPresenter(null, null); _imClient.SetPresenter(null, null);
} }
@ -637,7 +638,7 @@ namespace Avalonia.Controls
} }
UpdateCommandStates(); UpdateCommandStates();
_imClient.SetPresenter(_presenter, this); _imClient.SetPresenter(_presenter, this);
_presenter?.ShowCaret(); _presenter?.ShowCaret();
@ -657,7 +658,7 @@ namespace Avalonia.Controls
UpdateCommandStates(); UpdateCommandStates();
_presenter?.HideCaret(); _presenter?.HideCaret();
_imClient.SetPresenter(null, null); _imClient.SetPresenter(null, null);
} }
@ -700,14 +701,14 @@ namespace Avalonia.Controls
if (grapheme.FirstCodepoint.IsBreakChar) if (grapheme.FirstCodepoint.IsBreakChar)
{ {
if(lineCount + 1 > MaxLines) if (lineCount + 1 > MaxLines)
{ {
break; break;
} }
else else
{ {
lineCount++; lineCount++;
} }
} }
length += grapheme.Text.Length; length += grapheme.Text.Length;
@ -736,7 +737,7 @@ namespace Avalonia.Controls
text = Text ?? string.Empty; text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
ClearSelection(); ClearSelection();
if (IsUndoEnabled) if (IsUndoEnabled)
{ {
_undoRedoHelper.DiscardRedo(); _undoRedoHelper.DiscardRedo();
@ -746,7 +747,7 @@ namespace Avalonia.Controls
{ {
RaisePropertyChanged(TextProperty, oldText, _text); RaisePropertyChanged(TextProperty, oldText, _text);
} }
CaretIndex = caretIndex + input.Length; CaretIndex = caretIndex + input.Length;
} }
} }
@ -828,7 +829,7 @@ namespace Avalonia.Controls
{ {
return; return;
} }
var text = Text ?? string.Empty; var text = Text ?? string.Empty;
var caretIndex = CaretIndex; var caretIndex = CaretIndex;
var movement = false; var movement = false;
@ -985,87 +986,87 @@ namespace Avalonia.Controls
break; break;
case Key.Up: case Key.Up:
{
selection = DetectSelection();
_presenter.MoveCaretVertical(LogicalDirection.Backward);
if (caretIndex != _presenter.CaretIndex)
{ {
movement = true; selection = DetectSelection();
}
if (selection) _presenter.MoveCaretVertical(LogicalDirection.Backward);
{
SelectionEnd = _presenter.CaretIndex; if (caretIndex != _presenter.CaretIndex)
} {
else movement = true;
{ }
CaretIndex = _presenter.CaretIndex;
if (selection)
{
SelectionEnd = _presenter.CaretIndex;
}
else
{
CaretIndex = _presenter.CaretIndex;
}
break;
} }
break;
}
case Key.Down: 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: case Key.Back:
{
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlBackspace(); SnapshotUndoRedo();
}
if (!DeleteSelection())
{
var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward);
var backspacePosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
}
if (caretIndex != backspacePosition) if (!DeleteSelection())
{ {
var start = Math.Min(backspacePosition, caretIndex); var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward);
var end = Math.Max(backspacePosition, caretIndex);
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; var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length));
}
}
SnapshotUndoRedo();
handled = true; SetTextInternal(editedText);
break;
} CaretIndex = start;
}
}
SnapshotUndoRedo();
handled = true;
break;
}
case Key.Delete: case Key.Delete:
SnapshotUndoRedo(); SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlDelete(); SetSelectionForControlDelete();
@ -1077,7 +1078,7 @@ namespace Avalonia.Controls
var nextPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var nextPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if(nextPosition != caretIndex) if (nextPosition != caretIndex)
{ {
var start = Math.Min(nextPosition, caretIndex); var start = Math.Min(nextPosition, caretIndex);
var end = Math.Max(nextPosition, caretIndex); var end = Math.Max(nextPosition, caretIndex);
@ -1144,7 +1145,7 @@ namespace Avalonia.Controls
{ {
return; return;
} }
var text = Text; var text = Text;
var clickInfo = e.GetCurrentPoint(this); var clickInfo = e.GetCurrentPoint(this);
@ -1170,24 +1171,50 @@ namespace Avalonia.Controls
case 1: case 1:
if (clickToSelect) if (clickToSelect)
{ {
SelectionStart = Math.Min(oldIndex, index); if (_wordSelectionStart >= 0)
SelectionEnd = Math.Max(oldIndex, index); {
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 else
{ {
SelectionStart = SelectionEnd = index; if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
{
SelectionStart = SelectionEnd = index;
_wordSelectionStart = -1;
}
} }
break; break;
case 2: case 2:
if (!StringUtils.IsStartOfWord(text, index)) if (!StringUtils.IsStartOfWord(text, index))
{ {
SelectionStart = StringUtils.PreviousWord(text, index); SelectionStart = StringUtils.PreviousWord(text, index);
} }
_wordSelectionStart = SelectionStart;
SelectionEnd = StringUtils.NextWord(text, index); SelectionEnd = StringUtils.NextWord(text, index);
break; break;
case 3: case 3:
_wordSelectionStart = -1;
SelectAll(); SelectAll();
break; break;
} }
@ -1203,7 +1230,7 @@ namespace Avalonia.Controls
{ {
return; return;
} }
// selection should not change during pointer move if the user right clicks // selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
@ -1215,7 +1242,32 @@ namespace Avalonia.Controls
_presenter.MoveCaretToPoint(point); _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) if (e.InitialPressMouseButton == MouseButton.Right)
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
_presenter.MoveCaretToPoint(point); _presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex; var caretIndex = _presenter.CaretIndex;
// see if mouse clicked inside current selection // see if mouse clicked inside current selection
@ -1250,7 +1302,7 @@ namespace Avalonia.Controls
CaretIndex = SelectionEnd = SelectionStart = caretIndex; CaretIndex = SelectionEnd = SelectionStart = caretIndex;
} }
} }
e.Pointer.Capture(null); e.Pointer.Capture(null);
} }
@ -1309,7 +1361,7 @@ namespace Avalonia.Controls
{ {
return; return;
} }
var text = Text ?? string.Empty; var text = Text ?? string.Empty;
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
@ -1319,11 +1371,11 @@ namespace Avalonia.Controls
if (isSelecting) if (isSelecting)
{ {
_presenter.MoveCaretToTextPosition(selectionEnd); _presenter.MoveCaretToTextPosition(selectionEnd);
_presenter.MoveCaretHorizontal(direction > 0 ? _presenter.MoveCaretHorizontal(direction > 0 ?
LogicalDirection.Forward : LogicalDirection.Forward :
LogicalDirection.Backward); LogicalDirection.Backward);
SelectionEnd = _presenter.CaretIndex; SelectionEnd = _presenter.CaretIndex;
} }
else else
@ -1347,7 +1399,7 @@ namespace Avalonia.Controls
else else
{ {
int offset; int offset;
if (direction > 0) if (direction > 0)
{ {
offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd; offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd;
@ -1356,7 +1408,7 @@ namespace Avalonia.Controls
{ {
offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd;
} }
SelectionEnd += offset; SelectionEnd += offset;
_presenter.MoveCaretToTextPosition(SelectionEnd); _presenter.MoveCaretToTextPosition(SelectionEnd);
@ -1378,7 +1430,7 @@ namespace Avalonia.Controls
{ {
return; return;
} }
var caretIndex = CaretIndex; var caretIndex = CaretIndex;
if (document) if (document)
@ -1401,7 +1453,7 @@ namespace Avalonia.Controls
{ {
return; return;
} }
var text = Text ?? string.Empty; var text = Text ?? string.Empty;
var caretIndex = CaretIndex; var caretIndex = CaretIndex;
@ -1432,8 +1484,9 @@ namespace Avalonia.Controls
private bool DeleteSelection(bool raiseTextChanged = true) private bool DeleteSelection(bool raiseTextChanged = true)
{ {
if (IsReadOnly) return true; if (IsReadOnly)
return true;
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
@ -1444,40 +1497,40 @@ namespace Avalonia.Controls
var text = Text!; var text = Text!;
SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged);
_presenter?.MoveCaretToTextPosition(start); _presenter?.MoveCaretToTextPosition(start);
CaretIndex= start; CaretIndex = start;
ClearSelection(); ClearSelection();
return true; return true;
} }
CaretIndex = SelectionStart; CaretIndex = SelectionStart;
return false; return false;
} }
private string GetSelection() private string GetSelection()
{ {
var text = Text; var text = Text;
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
{ {
return ""; return "";
} }
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd);
if (start == end || (Text?.Length ?? 0) < end) if (start == end || (Text?.Length ?? 0) < end)
{ {
return ""; return "";
} }
return text.Substring(start, end - start); return text.Substring(start, end - start);
} }
@ -1496,7 +1549,7 @@ namespace Avalonia.Controls
private void SetSelectionForControlBackspace() private void SetSelectionForControlBackspace()
{ {
var selectionStart = CaretIndex; var selectionStart = CaretIndex;
MoveHorizontal(-1, true, false); MoveHorizontal(-1, true, false);
SelectionStart = selectionStart; SelectionStart = selectionStart;
@ -1508,9 +1561,9 @@ namespace Avalonia.Controls
{ {
return; return;
} }
SelectionStart = CaretIndex; SelectionStart = CaretIndex;
MoveHorizontal(1, true, true); MoveHorizontal(1, true, true);
if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ') if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ')

4
src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs

@ -21,7 +21,7 @@ namespace Avalonia.OpenGL.Angle
var display = IntPtr.Zero; var display = IntPtr.Zero;
AngleOptions.PlatformApi angleApi = default; AngleOptions.PlatformApi angleApi = default;
{ {
if (_egl.GetPlatformDisplayEXT == null) if (!_egl.IsGetPlatformDisplayExtAvailable)
throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll");
var allowedApis = AvaloniaLocator.Current.GetService<AngleOptions>()?.AllowedPlatformApis var allowedApis = AvaloniaLocator.Current.GetService<AngleOptions>()?.AllowedPlatformApis
@ -37,7 +37,7 @@ namespace Avalonia.OpenGL.Angle
else else
continue; 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 }); new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE });
if (display != IntPtr.Zero) if (display != IntPtr.Zero)
{ {

1
src/Avalonia.OpenGL/Avalonia.OpenGL.csproj

@ -11,4 +11,5 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\SourceGenerators.props" />
</Project> </Project>

14
src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs

@ -66,11 +66,9 @@ namespace Avalonia.OpenGL.Controls
return; return;
gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); 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]; _depthBuffer = gl.GenRenderbuffer();
gl.GenRenderbuffers(1, oneArr);
_depthBuffer = oneArr[0];
gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer);
gl.RenderbufferStorage(GL_RENDERBUFFER, gl.RenderbufferStorage(GL_RENDERBUFFER,
GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT,
@ -88,9 +86,9 @@ namespace Avalonia.OpenGL.Controls
var gl = _context.GlInterface; var gl = _context.GlInterface;
gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindTexture(GL_TEXTURE_2D, 0);
gl.BindFramebuffer(GL_FRAMEBUFFER, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
gl.DeleteFramebuffers(1, new[] { _fb }); gl.DeleteFramebuffer(_fb);
_fb = 0; _fb = 0;
gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); gl.DeleteRenderbuffer(_depthBuffer);
_depthBuffer = 0; _depthBuffer = 0;
_attachment?.Dispose(); _attachment?.Dispose();
_attachment = null; _attachment = null;
@ -174,9 +172,7 @@ namespace Avalonia.OpenGL.Controls
{ {
_depthBufferSize = GetPixelSize(); _depthBufferSize = GetPixelSize();
var gl = _context.GlInterface; var gl = _context.GlInterface;
var oneArr = new int[1]; _fb = gl.GenFramebuffer();
gl.GenFramebuffers(1, oneArr);
_fb = oneArr[0];
gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); gl.BindFramebuffer(GL_FRAMEBUFFER, _fb);
EnsureDepthBufferAttachment(gl); EnsureDepthBufferAttachment(gl);

2
src/Avalonia.OpenGL/Egl/EglContext.cs

@ -24,7 +24,7 @@ namespace Avalonia.OpenGL.Egl
SampleCount = sampleCount; SampleCount = sampleCount;
StencilSize = stencilSize; StencilSize = stencilSize;
using (MakeCurrent()) using (MakeCurrent())
GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, b => _egl.GetProcAddress(b)); GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, _egl.GetProcAddress);
} }
public IntPtr Context { get; } public IntPtr Context { get; }

4
src/Avalonia.OpenGL/Egl/EglDisplay.cs

@ -34,9 +34,9 @@ namespace Avalonia.OpenGL.Egl
} }
else else
{ {
if (egl.GetPlatformDisplayEXT == null) if (!egl.IsGetPlatformDisplayExtAvailable)
throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); 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) if (display == IntPtr.Zero)

225
src/Avalonia.OpenGL/Egl/EglInterface.cs

@ -2,31 +2,26 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using Avalonia.SourceGenerator;
namespace Avalonia.OpenGL.Egl namespace Avalonia.OpenGL.Egl
{ {
public class EglInterface : GlInterfaceBase public unsafe partial class EglInterface
{ {
public EglInterface() : base(Load()) public EglInterface(Func<string, IntPtr> getProcAddress)
{ {
Initialize(getProcAddress);
}
public EglInterface(Func<Utf8Buffer,IntPtr> getProcAddress) : base(getProcAddress)
{
} }
public EglInterface(Func<string, IntPtr> getProcAddress) : base(getProcAddress) public EglInterface(string library) : this(Load(library))
{ {
} }
public EglInterface(string library) : base(Load(library)) public EglInterface() : this(Load())
{ {
} }
static Func<string, IntPtr> Load() static Func<string, IntPtr> Load()
{ {
var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem; var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem;
@ -46,119 +41,75 @@ namespace Avalonia.OpenGL.Egl
} }
// ReSharper disable UnassignedGetOnlyAutoProperty // ReSharper disable UnassignedGetOnlyAutoProperty
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate int EglGetError(); [GetProcAddress("eglGetError")]
[GlEntryPoint("eglGetError")] public partial int GetError();
public EglGetError GetError { get; }
[GetProcAddress("eglGetDisplay")]
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public partial IntPtr GetDisplay(IntPtr nativeDisplay);
public delegate IntPtr EglGetDisplay(IntPtr nativeDisplay);
[GlEntryPoint("eglGetDisplay")] [GetProcAddress("eglGetPlatformDisplayEXT", true)]
public EglGetDisplay GetDisplay { get; } public partial IntPtr GetPlatformDisplayExt(int platform, IntPtr nativeDisplay, int[] attrs);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("eglInitialize")]
public delegate IntPtr EglGetPlatformDisplayEXT(int platform, IntPtr nativeDisplay, int[] attrs); public partial bool Initialize(IntPtr display, out int major, out int minor);
[GlEntryPoint("eglGetPlatformDisplayEXT")]
[GlOptionalEntryPoint] [GetProcAddress("eglGetProcAddress")]
public EglGetPlatformDisplayEXT GetPlatformDisplayEXT { get; } public partial IntPtr GetProcAddress(IntPtr proc);
[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; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("eglBindAPI")]
public delegate IntPtr EglCreateContext(IntPtr display, IntPtr config, 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); IntPtr share, int[] attrs);
[GlEntryPoint("eglCreateContext")]
public EglCreateContext CreateContext { get; } [GetProcAddress("eglDestroyContext")]
public partial bool DestroyContext(IntPtr display, IntPtr context);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool EglDestroyContext(IntPtr display, IntPtr context); [GetProcAddress("eglCreatePbufferSurface")]
[GlEntryPoint("eglDestroyContext")] public partial IntPtr CreatePBufferSurface(IntPtr display, IntPtr config, int[] attrs);
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("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) public string QueryString(IntPtr display, int i)
{ {
var rv = QueryStringNative(display, i); var rv = QueryStringNative(display, i);
@ -166,25 +117,15 @@ namespace Avalonia.OpenGL.Egl
return null; return null;
return Marshal.PtrToStringAnsi(rv); 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); [GetProcAddress("eglQueryDeviceAttribEXT", true)]
[GlEntryPoint("eglCreatePbufferFromClientBuffer")] public partial bool QueryDeviceAttribExt(IntPtr display, int attr, out IntPtr res);
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
} }
} }

44
src/Avalonia.OpenGL/GlBasicInfoInterface.cs

@ -3,46 +3,24 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using Avalonia.SourceGenerator;
namespace Avalonia.OpenGL namespace Avalonia.OpenGL
{ {
public class GlBasicInfoInterface : GlBasicInfoInterface<object> public unsafe partial class GlBasicInfoInterface
{ {
public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress) : base(getProcAddress, null) public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress){
{ Initialize(getProcAddress);
}
public GlBasicInfoInterface(Func<Utf8Buffer, IntPtr> nativeGetProcAddress) : base(nativeGetProcAddress, null)
{
} }
[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<TContextInfo> : GlInterfaceBase<TContextInfo> [GetProcAddress("glGetIntegerv")]
{ public partial void GetIntegerv(int name, out int rv);
public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress, TContextInfo context) : base(getProcAddress, context)
{
}
public GlBasicInfoInterface(Func<Utf8Buffer, IntPtr> nativeGetProcAddress, TContextInfo context) : base(nativeGetProcAddress, context) [GetProcAddress("glGetString")]
{ public partial IntPtr GetStringNative(int v);
}
[GetProcAddress("glGetStringi")]
[GlEntryPoint("glGetIntegerv")] public partial IntPtr GetStringiNative(int v, int v1);
public GlBasicInfoInterface.GlGetIntegerv GetIntegerv { get; }
[GlEntryPoint("glGetString")]
public GlBasicInfoInterface.GlGetString GetStringNative { get; }
[GlEntryPoint("glGetStringi")]
public GlBasicInfoInterface.GlGetStringi GetStringiNative { get; }
public string GetString(int v) public string GetString(int v)
{ {

103
src/Avalonia.OpenGL/GlEntryPointAttribute.cs

@ -2,117 +2,54 @@ using System;
namespace Avalonia.OpenGL namespace Avalonia.OpenGL
{ {
public interface IGlEntryPointAttribute [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
class GlMinVersionEntryPoint : Attribute
{ {
IntPtr GetProcAddress(Func<string, IntPtr> getProcAddress); public GlMinVersionEntryPoint(string entry, int minVersionMajor, int minVersionMinor)
}
public interface IGlEntryPointAttribute<in TContext>
{
IntPtr GetProcAddress(TContext context, Func<string, IntPtr> 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)
{ {
EntryPoints = entryPoints;
}
*/
public IntPtr GetProcAddress(Func<string, IntPtr> 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<GlInterface.GlContextInfo>
{
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, public GlMinVersionEntryPoint(string entry, int minVersionMajor, int minVersionMinor, GlProfileType profile)
int minVersionMinor)
{ {
_entry = entry;
_minVersionMajor = minVersionMajor;
_minVersionMinor = minVersionMinor;
} }
public IntPtr GetProcAddress(GlInterface.GlContextInfo context, Func<string, IntPtr> getProcAddress) public static IntPtr GetProcAddress(Func<string, IntPtr> 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; return IntPtr.Zero;
if(context.Version.Major<_minVersionMajor) if(context.Version.Major<minVersionMajor)
return IntPtr.Zero; return IntPtr.Zero;
if (context.Version.Major == _minVersionMajor && context.Version.Minor < _minVersionMinor) if (context.Version.Major == minVersionMajor && context.Version.Minor < minVersionMinor)
return IntPtr.Zero; return IntPtr.Zero;
return getProcAddress(_entry); return getProcAddress(entry);
} }
} }
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class GlExtensionEntryPoint : Attribute, IGlEntryPointAttribute<GlInterface.GlContextInfo> class GlExtensionEntryPoint : Attribute
{ {
private readonly string _entry; public GlExtensionEntryPoint(string entry, string extension)
private readonly GlProfileType? _profile;
private readonly string _extension;
public GlExtensionEntryPoint(string entry, GlProfileType profile, 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<string, IntPtr> getProcAddress) public static IntPtr GetProcAddress(Func<string, IntPtr> getProcAddress, GlInterface.GlContextInfo context,
string entry, string extension, GlProfileType? profile = null)
{ {
// Ignore different profile type // Ignore different profile type
if (_profile.HasValue && _profile != context.Version.Type) if (profile.HasValue && profile != context.Version.Type)
return IntPtr.Zero; return IntPtr.Zero;
// Check if extension is supported by the current context // Check if extension is supported by the current context
if (!context.Extensions.Contains(_extension)) if (!context.Extensions.Contains(extension))
return IntPtr.Zero; return IntPtr.Zero;
return getProcAddress(_entry); return getProcAddress(entry);
} }
} }
} }

494
src/Avalonia.OpenGL/GlInterface.cs

@ -3,14 +3,14 @@ using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using Avalonia.SourceGenerator;
using static Avalonia.OpenGL.GlConsts; using static Avalonia.OpenGL.GlConsts;
namespace Avalonia.OpenGL namespace Avalonia.OpenGL
{ {
public delegate IntPtr GlGetProcAddressDelegate(string procName); public unsafe partial class GlInterface : GlBasicInfoInterface
public unsafe class GlInterface : GlBasicInfoInterface<GlInterface.GlContextInfo>
{ {
private readonly Func<string, IntPtr> _getProcAddress;
public string Version { get; } public string Version { get; }
public string Vendor { get; } public string Vendor { get; }
public string Renderer { get; } public string Renderer { get; }
@ -35,12 +35,14 @@ namespace Avalonia.OpenGL
} }
} }
private GlInterface(GlContextInfo info, Func<string, IntPtr> getProcAddress) : base(getProcAddress, info) private GlInterface(GlContextInfo info, Func<string, IntPtr> getProcAddress) : base(getProcAddress)
{ {
_getProcAddress = getProcAddress;
ContextInfo = info; ContextInfo = info;
Version = GetString(GlConsts.GL_VERSION); Version = GetString(GlConsts.GL_VERSION);
Renderer = GetString(GlConsts.GL_RENDERER); Renderer = GetString(GlConsts.GL_RENDERER);
Vendor = GetString(GlConsts.GL_VENDOR); Vendor = GetString(GlConsts.GL_VENDOR);
Initialize(getProcAddress, ContextInfo);
} }
public GlInterface(GlVersion version, Func<string, IntPtr> getProcAddress) : this( public GlInterface(GlVersion version, Func<string, IntPtr> getProcAddress) : this(
@ -48,92 +50,58 @@ namespace Avalonia.OpenGL
{ {
} }
public GlInterface(GlVersion version, Func<Utf8Buffer, IntPtr> 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<Utf8Buffer, IntPtr> getProcAddress) => [GetProcAddress("glDeleteFramebuffers")]
new GlInterface(version, getProcAddress); public partial void DeleteFramebuffers(int count, int* framebuffers);
public void DeleteFramebuffer(int fb)
public T GetProcAddress<T>(string proc) => Marshal.GetDelegateForFunctionPointer<T>(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)
{ {
var ptr = GetStringNative(v); DeleteFramebuffers(1, &fb);
if (ptr != IntPtr.Zero)
return Marshal.PtrToStringAnsi(ptr);
return null;
} }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glBindFramebuffer")]
public delegate void GlGetIntegerv(int name, out int rv); public partial void BindFramebuffer(int target, int fb);
[GlEntryPoint("glGetIntegerv")]
public GlGetIntegerv GetIntegerv { get; } [GetProcAddress("glCheckFramebufferStatus")]
public partial int CheckFramebufferStatus(int target);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void GlGenFramebuffers(int count, int[] res); [GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0), GetProcAddress(true)]
[GlEntryPoint("glGenFramebuffers")] public partial void BlitFramebuffer(int srcX0,
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,
int srcY0, int srcY0,
int srcX1, int srcX1,
int srcY1, int srcY1,
@ -143,89 +111,78 @@ namespace Avalonia.OpenGL
int dstY1, int dstY1,
int mask, int mask,
int filter); int filter);
[GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0), GlOptionalEntryPoint]
public GlBlitFramebuffer BlitFramebuffer { get; }
[GetProcAddress("glGenRenderbuffers")]
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public partial void GenRenderbuffers(int count, int* res);
public delegate void GlGenRenderbuffers(int count, int[] res);
[GlEntryPoint("glGenRenderbuffers")] public int GenRenderbuffer()
public GlGenRenderbuffers GenRenderbuffers { get; } {
int rv = 0;
[UnmanagedFunctionPointer(CallingConvention.StdCall)] GenRenderbuffers(1, &rv);
public delegate void GlDeleteRenderbuffers(int count, int[] renderbuffers); return rv;
[GlEntryPoint("glDeleteRenderbuffers")] }
public GlDeleteTextures DeleteRenderbuffers { get; }
[GetProcAddress("glDeleteRenderbuffers")]
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public partial void DeleteRenderbuffers(int count, int* renderbuffers);
public delegate void GlBindRenderbuffer(int target, int fb);
[GlEntryPoint("glBindRenderbuffer")] public void DeleteRenderbuffer(int renderbuffer)
public GlBindRenderbuffer BindRenderbuffer { get; } {
DeleteRenderbuffers(1, &renderbuffer);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] }
public delegate void GlRenderbufferStorage(int target, int internalFormat, int width, int height);
[GlEntryPoint("glRenderbufferStorage")] [GetProcAddress("glBindRenderbuffer")]
public GlRenderbufferStorage RenderbufferStorage { get; } public partial void BindRenderbuffer(int target, int fb);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glRenderbufferStorage")]
public delegate void GlFramebufferRenderbuffer(int target, int attachment, 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); int renderbufferTarget, int renderbuffer);
[GlEntryPoint("glFramebufferRenderbuffer")]
public GlFramebufferRenderbuffer FramebufferRenderbuffer { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGenTextures")]
public delegate void GlGenTextures(int count, int[] res); public partial void GenTextures(int count, int* res);
[GlEntryPoint("glGenTextures")]
public GlGenTextures GenTextures { get; } public int GenTexture()
{
int rv = 0;
GenTextures(1, &rv);
return rv;
}
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glBindTexture")]
public delegate void GlBindTexture(int target, int fb); public partial void BindTexture(int target, int fb);
[GlEntryPoint("glBindTexture")]
public GlBindTexture BindTexture { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glActiveTexture")]
public delegate void GlActiveTexture(int texture); public partial void ActiveTexture(int texture);
[GlEntryPoint("glActiveTexture")]
public GlActiveTexture ActiveTexture { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glDeleteTextures")]
public delegate void GlDeleteTextures(int count, int[] textures); public partial void DeleteTextures(int count, int* textures);
[GlEntryPoint("glDeleteTextures")]
public GlDeleteTextures DeleteTextures { get; }
public void DeleteTexture(int texture) => DeleteTextures(1, &texture);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glTexImage2D")]
public delegate void GlTexImage2D(int target, int level, int internalFormat, int width, int height, int border, public partial void TexImage2D(int target, int level, int internalFormat, int width, int height, int border,
int format, int type, IntPtr data); int format, int type, IntPtr data);
[GlEntryPoint("glTexImage2D")]
public GlTexImage2D TexImage2D { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glCopyTexSubImage2D")]
public delegate void GlCopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, public partial void CopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y,
int width, int height); int width, int height);
[GlEntryPoint("glCopyTexSubImage2D")]
public GlCopyTexSubImage2D CopyTexSubImage2D { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glTexParameteri")]
public delegate void GlTexParameteri(int target, int name, int value); public partial void TexParameteri(int target, int name, int value);
[GlEntryPoint("glTexParameteri")]
public GlTexParameteri TexParameteri { get; }
[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); int texTarget, int texture, int level);
[GlEntryPoint("glFramebufferTexture2D")]
public GlFramebufferTexture2D FramebufferTexture2D { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glCreateShader")]
public delegate int GlCreateShader(int shaderType); public partial int CreateShader(int shaderType);
[GlEntryPoint("glCreateShader")]
public GlCreateShader CreateShader { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glShaderSource")]
public delegate void GlShaderSource(int shader, int count, IntPtr strings, IntPtr lengths); public partial void ShaderSource(int shader, int count, IntPtr strings, IntPtr lengths);
[GlEntryPoint("glShaderSource")]
public GlShaderSource ShaderSource { get; }
public void ShaderSourceString(int shader, string source) public void ShaderSourceString(int shader, string source)
{ {
@ -237,20 +194,14 @@ namespace Avalonia.OpenGL
} }
} }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glCompileShader")]
public delegate void GlCompileShader(int shader); public partial void CompileShader(int shader);
[GlEntryPoint("glCompileShader")]
public GlCompileShader CompileShader { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGetShaderiv")]
public delegate void GlGetShaderiv(int shader, int name, int* parameters); public partial void GetShaderiv(int shader, int name, int* parameters);
[GlEntryPoint("glGetShaderiv")]
public GlGetShaderiv GetShaderiv { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGetShaderInfoLog")]
public delegate void GlGetShaderInfoLog(int shader, int maxLength, out int length, void*infoLog); public partial void GetShaderInfoLog(int shader, int maxLength, out int length, void* infoLog);
[GlEntryPoint("glGetShaderInfoLog")]
public GlGetShaderInfoLog GetShaderInfoLog { get; }
public unsafe string CompileShaderAndGetError(int shader, string source) public unsafe string CompileShaderAndGetError(int shader, string source)
{ {
@ -268,33 +219,24 @@ namespace Avalonia.OpenGL
int len; int len;
fixed (void* ptr = logData) fixed (void* ptr = logData)
GetShaderInfoLog(shader, logLength, out len, ptr); 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)] [GetProcAddress("glCreateProgram")]
public delegate void GlAttachShader(int program, int shader); public partial int CreateProgram();
[GlEntryPoint("glAttachShader")]
public GlAttachShader AttachShader { get; } [GetProcAddress("glAttachShader")]
public partial void AttachShader(int program, int shader);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glLinkProgram")]
public delegate void GlLinkProgram(int program); public partial void LinkProgram(int program);
[GlEntryPoint("glLinkProgram")]
public GlLinkProgram LinkProgram { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGetProgramiv")]
public delegate void GlGetProgramiv(int program, int name, int* parameters); public partial void GetProgramiv(int program, int name, int* parameters);
[GlEntryPoint("glGetProgramiv")]
public GlGetProgramiv GetProgramiv { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGetProgramInfoLog")]
public delegate void GlGetProgramInfoLog(int program, int maxLength, out int len, void* infoLog); public partial void GetProgramInfoLog(int program, int maxLength, out int len, void* infoLog);
[GlEntryPoint("glGetProgramInfoLog")]
public GlGetProgramInfoLog GetProgramInfoLog { get; }
public unsafe string LinkProgramAndGetError(int program) public unsafe string LinkProgramAndGetError(int program)
{ {
@ -309,13 +251,11 @@ namespace Avalonia.OpenGL
int len; int len;
fixed (void* ptr = logData) fixed (void* ptr = logData)
GetProgramInfoLog(program, logLength, out len, ptr); GetProgramInfoLog(program, logLength, out len, ptr);
return Encoding.UTF8.GetString(logData,0, len); return Encoding.UTF8.GetString(logData, 0, len);
} }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glBindAttribLocation")]
public delegate void GlBindAttribLocation(int program, int index, IntPtr name); public partial void BindAttribLocation(int program, int index, IntPtr name);
[GlEntryPoint("glBindAttribLocation")]
public GlBindAttribLocation BindAttribLocation { get; }
public void BindAttribLocationString(int program, int index, string name) public void BindAttribLocationString(int program, int index, string name)
{ {
@ -323,32 +263,24 @@ namespace Avalonia.OpenGL
BindAttribLocation(program, index, b.DangerousGetHandle()); BindAttribLocation(program, index, b.DangerousGetHandle());
} }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGenBuffers")]
public delegate void GlGenBuffers(int len, int[] rv); public partial void GenBuffers(int len, int* rv);
[GlEntryPoint("glGenBuffers")]
public GlGenBuffers GenBuffers { get; }
public int GenBuffer() public int GenBuffer()
{ {
var rv = new int[1]; int rv;
GenBuffers(1, rv); GenBuffers(1, &rv);
return rv[0]; return rv;
} }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glBindBuffer")]
public delegate void GlBindBuffer(int target, int buffer); public partial void BindBuffer(int target, int buffer);
[GlEntryPoint("glBindBuffer")]
public GlBindBuffer BindBuffer { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glBufferData")]
public delegate void GlBufferData(int target, IntPtr size, IntPtr data, int usage); public partial void BufferData(int target, IntPtr size, IntPtr data, int usage);
[GlEntryPoint("glBufferData")]
public GlBufferData BufferData { get; }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGetAttribLocation")]
public delegate int GlGetAttribLocation(int program, IntPtr name); public partial int GetAttribLocation(int program, IntPtr name);
[GlEntryPoint("glGetAttribLocation")]
public GlGetAttribLocation GetAttribLocation { get; }
public int GetAttribLocationString(int program, string name) public int GetAttribLocationString(int program, string name)
{ {
@ -356,36 +288,24 @@ namespace Avalonia.OpenGL
return GetAttribLocation(program, b.DangerousGetHandle()); return GetAttribLocation(program, b.DangerousGetHandle());
} }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glVertexAttribPointer")]
public delegate void GlVertexAttribPointer(int index, int size, int type, public partial void VertexAttribPointer(int index, int size, int type,
int normalized, int stride, IntPtr pointer); int normalized, int stride, IntPtr pointer);
[GlEntryPoint("glVertexAttribPointer")]
public GlVertexAttribPointer VertexAttribPointer { get; } [GetProcAddress("glEnableVertexAttribArray")]
public partial void EnableVertexAttribArray(int index);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void GlEnableVertexAttribArray(int index); [GetProcAddress("glUseProgram")]
[GlEntryPoint("glEnableVertexAttribArray")] public partial void UseProgram(int program);
public GlEnableVertexAttribArray EnableVertexAttribArray { get; }
[GetProcAddress("glDrawArrays")]
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public partial void DrawArrays(int mode, int first, IntPtr count);
public delegate void GlUseProgram(int program);
[GlEntryPoint("glUseProgram")] [GetProcAddress("glDrawElements")]
public GlUseProgram UseProgram { get; } public partial void DrawElements(int mode, int count, int type, IntPtr indices);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGetUniformLocation")]
public delegate void GlDrawArrays(int mode, int first, IntPtr count); public partial int GetUniformLocation(int program, IntPtr name);
[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; }
public int GetUniformLocationString(int program, string name) public int GetUniformLocationString(int program, string name)
{ {
@ -393,41 +313,65 @@ namespace Avalonia.OpenGL
return GetUniformLocation(program, b.DangerousGetHandle()); return GetUniformLocation(program, b.DangerousGetHandle());
} }
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glUniform1f")]
public delegate void GlUniform1f(int location, float falue); public partial void Uniform1f(int location, float falue);
[GlEntryPoint("glUniform1f")]
public GlUniform1f Uniform1f { get; }
[GetProcAddress("glUniformMatrix4fv")]
public partial void UniformMatrix4fv(int location, int count, bool transpose, void* value);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void GlUniformMatrix4fv(int location, int count, bool transpose, void* value); [GetProcAddress("glEnable")]
[GlEntryPoint("glUniformMatrix4fv")] public partial void Enable(int what);
public GlUniformMatrix4fv UniformMatrix4fv { get; }
[GetProcAddress("glDeleteBuffers")]
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public partial void DeleteBuffers(int count, int* buffers);
public delegate void GlEnable(int what);
[GlEntryPoint("glEnable")] public void DeleteBuffer(int buffer) => DeleteBuffers(1, &buffer);
public GlEnable Enable { get; }
[GetProcAddress("glDeleteProgram")]
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public partial void DeleteProgram(int program);
public delegate void GlDeleteBuffers(int count, int[] buffers);
[GlEntryPoint("glDeleteBuffers")] [GetProcAddress("glDeleteShader")]
public GlDeleteBuffers DeleteBuffers { get; } public partial void DeleteShader(int shader);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [GetProcAddress("glGetRenderbufferParameteriv")]
public delegate void GlDeleteProgram(int program); public partial void GetRenderbufferParameteriv(int target, int name, out int value);
[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; }
// ReSharper restore UnassignedGetOnlyAutoProperty // 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<IntPtr, IntPtr> getProcAddress)
{
return new GlInterface(version, s =>
{
var ptr = Marshal.StringToHGlobalAnsi(s);
var rv = getProcAddress(ptr);
Marshal.FreeHGlobal(ptr);
return rv;
});
}
} }
} }

79
src/Avalonia.OpenGL/GlInterfaceBase.cs

@ -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<object>
{
public GlInterfaceBase(Func<string, IntPtr> getProcAddress) : base(getProcAddress, null)
{
}
public GlInterfaceBase(Func<Utf8Buffer, IntPtr> nativeGetProcAddress) : base(nativeGetProcAddress, null)
{
}
}
public class GlInterfaceBase<TContext>
{
private readonly Func<string, IntPtr> _getProcAddress;
public GlInterfaceBase(Func<string, IntPtr> getProcAddress, TContext context)
{
_getProcAddress = getProcAddress;
foreach (var prop in this.GetType().GetProperties())
{
var attrs = prop.GetCustomAttributes()
.Where(a =>
a is IGlEntryPointAttribute || a is IGlEntryPointAttribute<TContext>)
.ToList();
if(attrs.Count == 0)
continue;
var isOptional = prop.GetCustomAttribute<GlOptionalEntryPoint>() != 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<TContext> 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<string, IntPtr> ConvertNative(Func<Utf8Buffer, IntPtr> func) =>
(proc) =>
{
using (var u = new Utf8Buffer(proc))
{
var rv = func(u);
return rv;
}
};
public GlInterfaceBase(Func<Utf8Buffer, IntPtr> nativeGetProcAddress, TContext context) : this(ConvertNative(nativeGetProcAddress), context)
{
}
public IntPtr GetProcAddress(string proc) => _getProcAddress(proc);
}
}

10
src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml

@ -0,0 +1,10 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<RichTextBlock IsTextSelectionEnabled="True" Text="Preview"/>
</Design.PreviewWith>
<Style Selector="RichTextBlock[IsTextSelectionEnabled=true]">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</Styles>

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -66,4 +66,5 @@
<StyleInclude Source="avares://Avalonia.Themes.Default/Controls/FlyoutPresenter.xaml"/> <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/FlyoutPresenter.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Controls/MenuFlyoutPresenter.xaml"/> <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/MenuFlyoutPresenter.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Controls/ManagedFileChooser.xaml"/> <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/ManagedFileChooser.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Controls/RichTextBlock.xaml"/>
</Styles> </Styles>

1
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@ -68,6 +68,7 @@
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Slider.xaml" /> <ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Slider.xaml" />
<!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all --> <!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all -->
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml" /> <ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Styles.Resources> </Styles.Resources>

14
src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml

@ -0,0 +1,14 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<RichTextBlock IsTextSelectionEnabled="True" Text="Preview"/>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type RichTextBlock}" TargetType="RichTextBlock">
<Style Selector="^[IsTextSelectionEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</ControlTheme>
</ResourceDictionary>

2
src/Avalonia.X11/Avalonia.X11.csproj

@ -11,5 +11,5 @@
<ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" /> <ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" />
<Compile Include="..\Shared\RawEventGrouping.cs" /> <Compile Include="..\Shared\RawEventGrouping.cs" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\SourceGenerators.props" />
</Project> </Project>

111
src/Avalonia.X11/Glx/Glx.cs

@ -4,111 +4,97 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.OpenGL; using Avalonia.OpenGL;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using Avalonia.SourceGenerator;
// ReSharper disable UnassignedGetOnlyAutoProperty // ReSharper disable UnassignedGetOnlyAutoProperty
namespace Avalonia.X11.Glx namespace Avalonia.X11.Glx
{ {
unsafe class GlxInterface : GlInterfaceBase unsafe partial class GlxInterface
{ {
private const string libGL = "libGL.so.1"; private const string libGL = "libGL.so.1";
[GlEntryPointAttribute("glXMakeContextCurrent")] [GetProcAddress("glXMakeContextCurrent")]
public GlxMakeContextCurrent MakeContextCurrent { get; } public partial bool MakeContextCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context);
public delegate bool GlxMakeContextCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context);
[GlEntryPoint("glXGetCurrentContext")] [GetProcAddress("glXGetCurrentContext")]
public GlxGetCurrentContext GetCurrentContext { get; } public partial IntPtr GetCurrentContext();
public delegate IntPtr GlxGetCurrentContext();
[GlEntryPoint("glXGetCurrentDisplay")] [GetProcAddress("glXGetCurrentDisplay")]
public GlxGetCurrentDisplay GetCurrentDisplay { get; } public partial IntPtr GetCurrentDisplay();
public delegate IntPtr GlxGetCurrentDisplay();
[GlEntryPoint("glXGetCurrentDrawable")] [GetProcAddress("glXGetCurrentDrawable")]
public GlxGetCurrentDrawable GetCurrentDrawable { get; } public partial IntPtr GetCurrentDrawable();
public delegate IntPtr GlxGetCurrentDrawable();
[GlEntryPoint("glXGetCurrentReadDrawable")] [GetProcAddress("glXGetCurrentReadDrawable")]
public GlxGetCurrentReadDrawable GetCurrentReadDrawable { get; } public partial IntPtr GetCurrentReadDrawable();
public delegate IntPtr GlxGetCurrentReadDrawable();
[GlEntryPoint("glXCreatePbuffer")] [GetProcAddress("glXCreatePbuffer")]
public GlxCreatePbuffer CreatePbuffer { get; } public partial IntPtr CreatePbuffer(IntPtr dpy, IntPtr fbc, int[] attrib_list);
public delegate IntPtr GlxCreatePbuffer(IntPtr dpy, IntPtr fbc, int[] attrib_list);
[GlEntryPoint("glXDestroyPbuffer")] [GetProcAddress("glXDestroyPbuffer")]
public GlxDestroyPbuffer DestroyPbuffer { get; } public partial IntPtr DestroyPbuffer(IntPtr dpy, IntPtr fb);
public delegate IntPtr GlxDestroyPbuffer(IntPtr dpy, IntPtr fb);
[GlEntryPointAttribute("glXChooseVisual")] [GetProcAddress("glXChooseVisual")]
public GlxChooseVisual ChooseVisual { get; } public partial XVisualInfo* ChooseVisual(IntPtr dpy, int screen, int[] attribList);
public delegate XVisualInfo* GlxChooseVisual(IntPtr dpy, int screen, int[] attribList);
[GlEntryPointAttribute("glXCreateContext")] [GetProcAddress("glXCreateContext")]
public GlxCreateContext CreateContext { get; } public partial IntPtr CreateContext(IntPtr dpy, XVisualInfo* vis, IntPtr shareList, bool direct);
public delegate IntPtr GlxCreateContext(IntPtr dpy, XVisualInfo* vis, IntPtr shareList, bool direct);
[GlEntryPointAttribute("glXCreateContextAttribsARB")] [GetProcAddress("glXCreateContextAttribsARB")]
public GlxCreateContextAttribsARB CreateContextAttribsARB { get; } public partial IntPtr CreateContextAttribsARB(IntPtr dpy, IntPtr fbconfig, IntPtr shareList,
public delegate IntPtr GlxCreateContextAttribsARB(IntPtr dpy, IntPtr fbconfig, IntPtr shareList,
bool direct, int[] attribs); bool direct, int[] attribs);
[DllImport(libGL, EntryPoint = "glXGetProcAddress")] [DllImport(libGL, EntryPoint = "glXGetProcAddress")]
public static extern IntPtr GlxGetProcAddress(Utf8Buffer buffer); public static extern IntPtr GlxGetProcAddress(string buffer);
[GlEntryPointAttribute("glXDestroyContext")] [GetProcAddress("glXDestroyContext")]
public GlxDestroyContext DestroyContext { get; } public partial void DestroyContext(IntPtr dpy, IntPtr ctx);
public delegate void GlxDestroyContext(IntPtr dpy, IntPtr ctx);
[GlEntryPointAttribute("glXChooseFBConfig")] [GetProcAddress("glXChooseFBConfig")]
public GlxChooseFBConfig ChooseFBConfig { get; } public partial IntPtr* ChooseFBConfig(IntPtr dpy, int screen, int[] attrib_list, out int nelements);
public delegate IntPtr* GlxChooseFBConfig(IntPtr dpy, int screen, int[] attrib_list, out int nelements);
public IntPtr* GlxChooseFbConfig(IntPtr dpy, int screen, IEnumerable<int> attribs, out int nelements) public IntPtr* ChooseFbConfig(IntPtr dpy, int screen, IEnumerable<int> attribs, out int nelements)
{ {
var arr = attribs.Concat(new[]{0}).ToArray(); var arr = attribs.Concat(new[]{0}).ToArray();
return ChooseFBConfig(dpy, screen, arr, out nelements); return ChooseFBConfig(dpy, screen, arr, out nelements);
} }
[GlEntryPointAttribute("glXGetVisualFromFBConfig")] [GetProcAddress("glXGetVisualFromFBConfig")]
public GlxGetVisualFromFBConfig GetVisualFromFBConfig { get; } public partial XVisualInfo * GetVisualFromFBConfig(IntPtr dpy, IntPtr config);
public delegate XVisualInfo * GlxGetVisualFromFBConfig(IntPtr dpy, IntPtr config);
[GlEntryPointAttribute("glXGetFBConfigAttrib")] [GetProcAddress("glXGetFBConfigAttrib")]
public GlxGetFBConfigAttrib GetFBConfigAttrib { get; } public partial int GetFBConfigAttrib(IntPtr dpy, IntPtr config, int attribute, out int value);
public delegate int GlxGetFBConfigAttrib(IntPtr dpy, IntPtr config, int attribute, out int value);
[GlEntryPointAttribute("glXSwapBuffers")] [GetProcAddress("glXSwapBuffers")]
public GlxSwapBuffers SwapBuffers { get; } public partial void SwapBuffers(IntPtr dpy, IntPtr drawable);
public delegate void GlxSwapBuffers(IntPtr dpy, IntPtr drawable);
[GlEntryPointAttribute("glXWaitX")] [GetProcAddress("glXWaitX")]
public GlxWaitX WaitX { get; } public partial void WaitX();
public delegate void GlxWaitX();
[GlEntryPointAttribute("glXWaitGL")] [GetProcAddress("glXWaitGL")]
public GlxWaitGL WaitGL { get; } public partial void WaitGL();
public delegate void GlxWaitGL();
public delegate int GlGetError();
[GlEntryPoint("glGetError")]
public GlGetError GetError { get; }
public delegate IntPtr GlxQueryExtensionsString(IntPtr display, int screen); [GetProcAddress("glGetError")]
[GlEntryPoint("glXQueryExtensionsString")] public partial int GlGetError();
public GlxQueryExtensionsString QueryExtensionsString { get; }
[GetProcAddress("glXQueryExtensionsString")]
public partial IntPtr QueryExtensionsString(IntPtr display, int screen);
public GlxInterface() : base(SafeGetProcAddress) public GlxInterface()
{ {
Initialize(SafeGetProcAddress);
} }
// Ignores egl functions. // Ignores egl functions.
@ -122,10 +108,9 @@ namespace Avalonia.X11.Glx
return IntPtr.Zero; return IntPtr.Zero;
} }
return GlxConverted(proc); return GlxGetProcAddress(proc);
} }
private static readonly Func<string, IntPtr> GlxConverted = ConvertNative(GlxGetProcAddress);
public string[] GetExtensions(IntPtr display) public string[] GetExtensions(IntPtr display)
{ {

2
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs

@ -164,7 +164,7 @@ namespace Avalonia.LinuxFramebuffer.Output
else else
{ {
Fd = open(path, 2, 0); 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}");
} }
} }

2
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs

@ -50,7 +50,7 @@ namespace Avalonia.LinuxFramebuffer.Output
} }
[DllImport("libEGL.so.1")] [DllImport("libEGL.so.1")]
static extern IntPtr eglGetProcAddress(Utf8Buffer proc); static extern IntPtr eglGetProcAddress(string proc);
private GbmBoUserDataDestroyCallbackDelegate FbDestroyDelegate; private GbmBoUserDataDestroyCallbackDelegate FbDestroyDelegate;
private drmModeModeInfo _mode; private drmModeModeInfo _mode;

24
src/Shared/SourceGeneratorAttributes.cs

@ -14,4 +14,28 @@ namespace Avalonia.SourceGenerator
public string Namespace { get; } public string Namespace { get; }
public Type BaseType { 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()
{
}
}
} }

28
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_RENDERBUFFER_BINDING, out var oldRenderbuffer);
gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture);
var arr = new int[2];
// Generate FBO // Generate FBO
gl.GenFramebuffers(1, arr); _fbo = gl.GenFramebuffer();
_fbo = arr[0];
gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo);
// Create a texture to render into // Create a texture to render into
gl.GenTextures(1, arr); _texture = gl.GenTexture();
_texture = arr[0];
gl.BindTexture(GL_TEXTURE_2D, _texture); gl.BindTexture(GL_TEXTURE_2D, _texture);
gl.TexImage2D(GL_TEXTURE_2D, 0, gl.TexImage2D(GL_TEXTURE_2D, 0,
InternalFormat, pixelSize.Width, pixelSize.Height, InternalFormat, pixelSize.Width, pixelSize.Height,
@ -48,8 +45,7 @@ namespace Avalonia.Skia
var success = false; var success = false;
foreach (var useStencil8 in TrueFalse) foreach (var useStencil8 in TrueFalse)
{ {
gl.GenRenderbuffers(1, arr); _depthStencil = gl.GenRenderbuffer();
_depthStencil = arr[0];
gl.BindRenderbuffer(GL_RENDERBUFFER, _depthStencil); gl.BindRenderbuffer(GL_RENDERBUFFER, _depthStencil);
if (useStencil8) if (useStencil8)
@ -73,7 +69,7 @@ namespace Avalonia.Skia
else else
{ {
gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderbuffer); gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderbuffer);
gl.DeleteRenderbuffers(1, arr); gl.DeleteRenderbuffer(_depthStencil);
} }
} }
@ -83,10 +79,8 @@ namespace Avalonia.Skia
if (!success) if (!success)
{ {
arr[0] = _fbo; gl.DeleteFramebuffer(_fbo);
gl.DeleteFramebuffers(1, arr); gl.DeleteTexture(_texture);
arr[0] = _texture;
gl.DeleteTextures(1, arr);
throw new OpenGlException("Unable to create FBO with stencil"); throw new OpenGlException("Unable to create FBO with stencil");
} }
@ -94,7 +88,7 @@ namespace Avalonia.Skia
new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat()));
Surface = SKSurface.Create(_grContext, target, Surface = SKSurface.Create(_grContext, target,
surfaceOrigin, SKColorType.Rgba8888); surfaceOrigin, SKColorType.Rgba8888);
CanBlit = gl.BlitFramebuffer != null; CanBlit = gl.IsBlitFramebufferAvailable;
} }
public void Dispose() public void Dispose()
@ -106,9 +100,9 @@ namespace Avalonia.Skia
var gl = _glContext.GlInterface; var gl = _glContext.GlInterface;
if (_fbo != 0) if (_fbo != 0)
{ {
gl.DeleteFramebuffers(1, new[] { _fbo }); gl.DeleteFramebuffer(_fbo);
gl.DeleteTextures(1, new[] { _texture }); gl.DeleteTexture(_texture);
gl.DeleteRenderbuffers(1, new[] { _depthStencil }); gl.DeleteRenderbuffer(_depthStencil);
_fbo = _texture = _depthStencil = 0; _fbo = _texture = _depthStencil = 0;
} }
} }

2
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs

@ -54,7 +54,7 @@ namespace Avalonia.Skia
return null; return null;
// Blit feature requires glBlitFramebuffer // Blit feature requires glBlitFramebuffer
if (_glContext.GlInterface.BlitFramebuffer == null) if (!_glContext.GlInterface.IsBlitFramebufferAvailable)
return null; return null;
size = new PixelSize(Math.Max(size.Width, 1), Math.Max(size.Height, 1)); size = new PixelSize(Math.Max(size.Width, 1), Math.Max(size.Height, 1));

11
src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs

@ -101,7 +101,7 @@ namespace Avalonia.Skia
private bool _disposed; private bool _disposed;
private readonly DisposableLock _lock = new DisposableLock(); 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; _bitmap = bitmap;
_context = context; _context = context;
@ -119,7 +119,8 @@ namespace Avalonia.Skia
var gl = _context.GlInterface; var gl = _context.GlInterface;
var textures = new int[2]; var textures = new int[2];
gl.GenTextures(2, textures); fixed (int* ptex = textures)
gl.GenTextures(2, ptex);
_texture = textures[0]; _texture = textures[0];
_frontBuffer = textures[1]; _frontBuffer = textures[1];
@ -178,7 +179,7 @@ namespace Avalonia.Skia
_presentCallback(); _presentCallback();
} }
public void Dispose() public unsafe void Dispose()
{ {
var gl = _context.GlInterface; var gl = _context.GlInterface;
_bitmap.Present(null); _bitmap.Present(null);
@ -191,7 +192,9 @@ namespace Avalonia.Skia
if(_disposed) if(_disposed)
return; return;
_disposed = true; _disposed = true;
gl.DeleteTextures(2, new[] { _texture, _frontBuffer }); var tex = new[] { _texture, _frontBuffer };
fixed (int* ptex = tex)
gl.DeleteTextures(2, ptex);
} }
} }

52
src/iOS/Avalonia.iOS/LayerFbo.cs

@ -10,12 +10,12 @@ namespace Avalonia.iOS
private readonly EAGLContext _context; private readonly EAGLContext _context;
private readonly GlInterface _gl; private readonly GlInterface _gl;
private readonly CAEAGLLayer _layer; private readonly CAEAGLLayer _layer;
private int[] _framebuffer; private int _framebuffer;
private int[] _renderbuffer; private int _renderbuffer;
private int[] _depthBuffer; private int _depthBuffer;
private bool _disposed; 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; _context = context;
_gl = gl; _gl = gl;
@ -30,42 +30,36 @@ namespace Avalonia.iOS
if (context != EAGLContext.CurrentContext) if (context != EAGLContext.CurrentContext)
return null; return null;
var fb = new int[2]; var rb = gl.GenRenderbuffer();
var rb = new int[2]; gl.BindRenderbuffer(GlConsts.GL_RENDERBUFFER, rb);
var db = new int[2];
gl.GenRenderbuffers(1, rb);
gl.BindRenderbuffer(GlConsts.GL_RENDERBUFFER, rb[0]);
context.RenderBufferStorage(GlConsts.GL_RENDERBUFFER, layer); context.RenderBufferStorage(GlConsts.GL_RENDERBUFFER, layer);
gl.GenFramebuffers(1, fb); var fb = gl.GenFramebuffer();
gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, fb[0]); gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, fb);
gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_COLOR_ATTACHMENT0, GlConsts.GL_RENDERBUFFER, rb[0]); gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_COLOR_ATTACHMENT0, GlConsts.GL_RENDERBUFFER, rb);
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);
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.BindRenderbuffer(RenderbufferTarget.Renderbuffer, depthBuffer);
//GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferInternalFormat.DepthComponent16, w, h); //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); var frameBufferError = gl.CheckFramebufferStatus(GlConsts.GL_FRAMEBUFFER);
if(frameBufferError != GlConsts.GL_FRAMEBUFFER_COMPLETE) if(frameBufferError != GlConsts.GL_FRAMEBUFFER_COMPLETE)
{ {
gl.DeleteFramebuffers(1, fb); gl.DeleteFramebuffer(fb);
gl.DeleteRenderbuffers(1, db); gl.DeleteRenderbuffer(db);
gl.DeleteRenderbuffers(1, rb); gl.DeleteRenderbuffer(rb);
return null; return null;
} }
return new LayerFbo(context, gl, layer, fb, rb, db) return new LayerFbo(context, gl, layer, fb, rb, db)
{ {
Width = w[0], Width = w,
Height = h[0] Height = h
}; };
} }
@ -74,7 +68,7 @@ namespace Avalonia.iOS
public void Bind() public void Bind()
{ {
_gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, _framebuffer[0]); _gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, _framebuffer);
} }
public void Present() public void Present()
@ -88,9 +82,9 @@ namespace Avalonia.iOS
if(_disposed) if(_disposed)
return; return;
_disposed = true; _disposed = true;
_gl.DeleteFramebuffers(1, _framebuffer); _gl.DeleteFramebuffer(_framebuffer);
_gl.DeleteRenderbuffers(1, _depthBuffer); _gl.DeleteRenderbuffer(_depthBuffer);
_gl.DeleteRenderbuffers(1, _renderbuffer); _gl.DeleteRenderbuffer(_renderbuffer);
if (_context != EAGLContext.CurrentContext) if (_context != EAGLContext.CurrentContext)
throw new InvalidOperationException("Associated EAGLContext is not current"); throw new InvalidOperationException("Associated EAGLContext is not current");
} }

338
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, int>();
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<string, IntPtr> 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();
}
}

31
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<AttributeData> attributes = symbol.GetAttributes();
foreach (AttributeData attribute in attributes)
if (attribute.AttributeClass?.HasFullyQualifiedName(name) == true)
return true;
return false;
}
}

BIN
tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf

Binary file not shown.

4
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@ -15,6 +15,8 @@ namespace Avalonia.Skia.UnitTests.Media
private readonly Typeface _defaultTypeface = private readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); 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 = private readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic);
private readonly Typeface _emojiTypeface = private readonly Typeface _emojiTypeface =
@ -22,7 +24,7 @@ namespace Avalonia.Skia.UnitTests.Media
public CustomFontManagerImpl() public CustomFontManagerImpl()
{ {
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _defaultTypeface };
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
} }

8
tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs

@ -33,15 +33,11 @@ namespace Avalonia.Skia.UnitTests.Media
{ {
var fontManager = new FontManagerImpl(); 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( 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; var skTypeface = glyphTypeface.Typeface;
Assert.Equal(fontName, skTypeface.FamilyName);
Assert.True(skTypeface.FontWeight >= 600); Assert.True(skTypeface.FontWeight >= 600);
} }

129
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -154,7 +154,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
j += inner.Current.Text.Length; j += inner.Current.Text.Length;
if(j + i > text.Length) if (j + i > text.Length)
{ {
break; break;
} }
@ -738,7 +738,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine = layout.TextLines[0]; var textLine = layout.TextLines[0];
var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1));
var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1));
var rects = layout.HitTestTextRange(0, 7).ToArray(); var rects = layout.HitTestTextRange(0, 7).ToArray();
@ -746,7 +746,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, rects.Length); Assert.Equal(1, rects.Length);
var expected = rects[0]; var expected = rects[0];
Assert.Equal(expected.Left, start); Assert.Equal(expected.Left, start);
Assert.Equal(expected.Right, end); Assert.Equal(expected.Right, end);
} }
@ -818,11 +818,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
} }
} }
} }
[Fact] [Fact]
public void Should_Layout_Empty_String() public void Should_Layout_Empty_String()
{ {
@ -833,11 +833,128 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Typeface.Default, Typeface.Default,
12, 12,
Brushes.Black); Brushes.Black);
Assert.True(layout.Bounds.Height > 0); 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() private static IDisposable Start()
{ {
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

17
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 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(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); 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(1, textBounds.Count);
Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length)); 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); textBounds = textLine.GetTextBounds(0, 5);
Assert.Equal(2, textBounds.Count); Assert.Equal(2, textBounds.Count);
Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width);
Assert.Equal(textLine.Start + 7.201171875, textBounds[1].Rectangle.Right); 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); 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(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
} }
} }
private class FixedRunsTextSource : ITextSource private class FixedRunsTextSource : ITextSource
{ {

Loading…
Cancel
Save