Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Surrogate Unicode sequences #139

Merged
merged 39 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f087bf5
Add support for Surragate sequences
tomlm Nov 6, 2024
9446c9b
fix release build warning.
tomlm Nov 6, 2024
637747c
* fix unit tests
tomlm Nov 6, 2024
559fe7b
Automated JetBrains cleanup
github-actions[bot] Nov 6, 2024
ef299e5
use Text...everything works except for double char emojiis
tomlm Nov 7, 2024
ebc751d
formatting merge
tomlm Nov 7, 2024
e0a2a52
use TextShaper.Text for render text instead of trying to render from …
tomlm Nov 7, 2024
352bb67
FINALLY I have emoticons rendering correctly
tomlm Nov 8, 2024
adeb5ea
added scroll bars to textlbock gallery page
tomlm Nov 8, 2024
c174f9b
update sample to show that CJK wide chars work
tomlm Nov 8, 2024
479460b
removed unused using statements
tomlm Nov 8, 2024
cc417f8
update unit tests for textblock
tomlm Nov 8, 2024
7e36247
lint feedback, adopt WcWidth for accurate calculation of char width
tomlm Nov 8, 2024
88ebb08
properly calculate GlyphInfos in textshaper
tomlm Nov 8, 2024
5891381
fix bugs found by coderabbit. Nice job code rabbit!
tomlm Nov 8, 2024
e4e247b
remove using
tomlm Nov 8, 2024
532ac52
add detection of complex emojii
tomlm Nov 8, 2024
b7c1e35
fix calc of glyph
tomlm Nov 8, 2024
8cab697
update unitestconsole
tomlm Nov 8, 2024
78710e5
add comment
tomlm Nov 8, 2024
2be4221
fix
tomlm Nov 8, 2024
265eb64
ah, finally figured out the using issue. Visual studio didn't conside…
tomlm Nov 8, 2024
8c76411
argh. Lint is killing me!
tomlm Nov 8, 2024
0b478fc
Automated JetBrains cleanup
github-actions[bot] Nov 8, 2024
9791ab4
Merge branch 'main' into tomlm/rune
tomlm Nov 9, 2024
6c008eb
Merge branch 'main' into tomlm/rune
tomlm Nov 9, 2024
272b00f
merge
tomlm Nov 9, 2024
d4fc7f1
make list readonly
tomlm Nov 9, 2024
019a80f
Automated JetBrains cleanup
github-actions[bot] Nov 9, 2024
a823630
cleanup
tomlm Nov 9, 2024
3a5efde
add comments
tomlm Nov 9, 2024
61b9697
Merge branch 'tomlm/rune' of https://github.com/jinek/Consolonia into…
tomlm Nov 9, 2024
40dedcc
removed accidental comment of /// instead of //
tomlm Nov 9, 2024
379870e
Automated JetBrains cleanup
github-actions[bot] Nov 9, 2024
a829699
fix caret drawing bug in Pixel.Blend() I was merging IsCaret when mer…
tomlm Nov 10, 2024
51b30fe
huh, I thought I removed this.
tomlm Nov 10, 2024
7560a6c
Add PlaceholderGlyph
tomlm Nov 10, 2024
b97e7bc
sigh. Linter
tomlm Nov 10, 2024
c356590
Automated JetBrains cleanup
github-actions[bot] Nov 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Consolonia.Core/Consolonia.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
<PackageReference Include="Avalonia.FreeDesktop" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Skia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
<PackageReference Include="NullLib.ConsoleEx" Version="1.0.4.4" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.8" />
<PackageReference Include="Unicode.net" Version="2.0.0" />
<PackageReference Include="Wcwidth" Version="2.0.0" />
</ItemGroup>

</Project>
Expand Down
2 changes: 2 additions & 0 deletions src/Consolonia.Core/Drawing/ConsoleBrush.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Diagnostics;
using Avalonia;
using Avalonia.Media;
using Consolonia.Core.Drawing.PixelBufferImplementation;
using Consolonia.Core.Infrastructure;

namespace Consolonia.Core.Drawing
{
[DebuggerDisplay("{Color} [{Mode}]")]
public class ConsoleBrush : AvaloniaObject, IImmutableBrush
{
public static readonly StyledProperty<Color> ColorProperty =
Expand Down Expand Up @@ -183,7 +185,7 @@
break;
}

if (before == null && after == null)

Check notice on line 188 in src/Consolonia.Core/Drawing/ConsoleBrush.cs

View workflow job for this annotation

GitHub Actions / build

"[ConvertIfStatementToSwitchStatement] Convert 'if' statement into 'switch' statement" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/ConsoleBrush.cs(188,13)
throw new ArgumentException("no gradientstops defined");

if (before == null) return after.Color;
Expand Down
208 changes: 124 additions & 84 deletions src/Consolonia.Core/Drawing/DrawingContextImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Consolonia.Core.Drawing.PixelBufferImplementation;
using Consolonia.Core.Infrastructure;
using Consolonia.Core.InternalHelpers;
using Consolonia.Core.Text;
using NeoSmart.Unicode;
using SkiaSharp;

namespace Consolonia.Core.Drawing
Expand All @@ -24,7 +26,8 @@ internal class DrawingContextImpl : IDrawingContextImpl
private const byte HorizontalEndPattern = 0b0001;

public const int UnderlineThickness = 10;
public const int StrikthroughThickness = 11;
public const int StrikethroughThickness = 11;

private readonly Stack<Rect> _clipStack = new(100);
private readonly ConsoleWindow _consoleWindow;
private readonly PixelBuffer _pixelBuffer;
Expand Down Expand Up @@ -70,36 +73,37 @@ public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect
new SKPaint { FilterQuality = SKFilterQuality.Medium });

for (int y = 0; y < bitmap.Info.Height; y += 2)
for (int x = 0; x < bitmap.Info.Width; x += 2)
{
// NOTE: we divide by 2 because we are working with quad pixels,
// // the bitmap has twice the horizontal and twice the vertical of the target rect.
int px = (int)targetRect.TopLeft.X + x / 2;
int py = (int)targetRect.TopLeft.Y + y / 2;

// get the quad pixel the bitmap
var quadColors = new[]
for (int x = 0; x < bitmap.Info.Width; x += 2)
{
// NOTE: we divide by 2 because we are working with quad pixels,
// // the bitmap has twice the horizontal and twice the vertical of the target rect.
int px = (int)targetRect.TopLeft.X + x / 2;
int py = (int)targetRect.TopLeft.Y + y / 2;

// get the quad pixel the bitmap
var quadColors = new[]
{
bitmap.GetPixel(x, y), bitmap.GetPixel(x + 1, y),
bitmap.GetPixel(x, y + 1), bitmap.GetPixel(x + 1, y + 1)
};

// map it to a single char to represet the 4 pixels
char quadPixel = GetQuadPixelCharacter(quadColors);
// map it to a single char to represet the 4 pixels
char quadPixel = GetQuadPixelCharacter(quadColors);

// get the combined colors for the quad pixel
Color foreground = GetForegroundColorForQuadPixel(quadColors, quadPixel);
Color background = GetBackgroundColorForQuadPixel(quadColors, quadPixel);
// get the combined colors for the quad pixel
Color foreground = GetForegroundColorForQuadPixel(quadColors, quadPixel);
Color background = GetBackgroundColorForQuadPixel(quadColors, quadPixel);

var imagePixel = new Pixel(new PixelForeground(new SimpleSymbol(quadPixel), color: foreground),
new PixelBackground(background));
CurrentClip.ExecuteWithClipping(new Point(px, py),
() =>
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(existingPixel, _) => existingPixel.Blend(imagePixel), imagePixel.Background.Color);
});
}
var imagePixel = new Pixel(
new PixelForeground(new SimpleSymbol(quadPixel), color: foreground),
new PixelBackground(background));
CurrentClip.ExecuteWithClipping(new Point(px, py),
() =>
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(existingPixel, _) => existingPixel.Blend(imagePixel), imagePixel.Background.Color);
});
}
}

public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
Expand Down Expand Up @@ -150,31 +154,31 @@ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
case VisualBrush:
throw new NotImplementedException();
case ISceneBrush sceneBrush:
{
ISceneBrushContent sceneBrushContent = sceneBrush.CreateContent();
if (sceneBrushContent != null) sceneBrushContent.Render(this, Matrix.Identity);
return;
}
{
ISceneBrushContent sceneBrushContent = sceneBrush.CreateContent();
if (sceneBrushContent != null) sceneBrushContent.Render(this, Matrix.Identity);
return;
}
}

Rect r2 = r.TransformToAABB(Transform);

double width = r2.Width + (pen?.Thickness ?? 0);
double height = r2.Height + (pen?.Thickness ?? 0);
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
int px = (int)(r2.TopLeft.X + x);
int py = (int)(r2.TopLeft.Y + y);

ConsoleBrush backgroundBrush = ConsoleBrush.FromPosition(brush, x, y, (int)width, (int)height);
CurrentClip.ExecuteWithClipping(new Point(px, py), () =>
for (int y = 0; y < height; y++)
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(pixel, bb) => pixel.Blend(new Pixel(new PixelBackground(bb.Mode, bb.Color))),
backgroundBrush);
});
}
int px = (int)(r2.TopLeft.X + x);
int py = (int)(r2.TopLeft.Y + y);

ConsoleBrush backgroundBrush = ConsoleBrush.FromPosition(brush, x, y, (int)width, (int)height);
CurrentClip.ExecuteWithClipping(new Point(px, py), () =>
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(pixel, bb) => pixel.Blend(new Pixel(new PixelBackground(bb.Mode, bb.Color))),
backgroundBrush);
});
}
}

if (pen is null or { Thickness: 0 }
Expand Down Expand Up @@ -206,9 +210,9 @@ public void DrawGlyphRun(IBrush foreground, IGlyphRunImpl glyphRun)
return;
}

string charactersDoDraw =
string.Concat(glyphRunImpl.GlyphIndices.Select(us => (char)us).ToArray());
DrawStringInternal(foreground, charactersDoDraw, glyphRun.GlyphTypeface);
var shapedBuffer = (ShapedBuffer)glyphRunImpl.GlyphInfos;
var text = shapedBuffer.Text.ToString();
DrawStringInternal(foreground, text, glyphRun.GlyphTypeface);
}

public IDrawingContextLayerImpl CreateLayer(Size size)
Expand Down Expand Up @@ -328,7 +332,7 @@ private void ApplyTextDecorationLineInternal(ref Point head, IPen pen, Line line
TextDecorationCollection textDecoration = pen.Thickness switch
{
UnderlineThickness => TextDecorations.Underline,
StrikthroughThickness => TextDecorations.Strikethrough,
StrikethroughThickness => TextDecorations.Strikethrough,
_ => throw new ArgumentOutOfRangeException($"Unsupported thickness {pen.Thickness}")
};

Expand Down Expand Up @@ -409,8 +413,7 @@ private bool IfMoveConsoleCaretMove(IPen pen, Point head)
if (pen.Brush is not MoveConsoleCaretToPositionBrush)
return false;

CurrentClip.ExecuteWithClipping(head,
() => { _pixelBuffer.Set((PixelBufferCoordinate)head, pixel => pixel.Blend(new Pixel(true))); });
_pixelBuffer.SetCaretPosition((PixelBufferCoordinate)head);
return true;
}

Expand Down Expand Up @@ -486,7 +489,7 @@ private void DrawPixelAndMoveHead(ref Point head, Line line, LineStyle? lineStyl
}
}

private void DrawStringInternal(IBrush foreground, string str, IGlyphTypeface typeface, Point origin = new())
private void DrawStringInternal(IBrush foreground, string text, IGlyphTypeface typeface, Point origin = new())
{
foreground = ConsoleBrush.FromBrush(foreground);
if (foreground is not ConsoleBrush { Mode: PixelBackgroundMode.Colored } consoleBrush)
Expand All @@ -495,57 +498,94 @@ private void DrawPixelAndMoveHead(ref Point head, Line line, LineStyle? lineStyl
return;
}

//if (!Transform.IsTranslateOnly()) ConsoloniaPlatform.RaiseNotSupported(15);
// if (!Transform.IsTranslateOnly()) ConsoloniaPlatform.RaiseNotSupported(15);

Point whereToDraw = origin.Transform(Transform);
int currentXPosition = 0;
int currentYPosition = 0;

// Process text into collection of glyphs where
// a glyph is either text or a combination of chars which make up an emoji.
List<string> glyphs = new List<string>();
StringBuilder emoji = new StringBuilder();
var runes = text.EnumerateRunes();
Rune lastRune = new Rune();

while (runes.MoveNext())
{
if (lastRune.Value == Codepoints.ZWJ ||
lastRune.Value == Codepoints.ORC ||
Emoji.IsEmoji(runes.Current.ToString()))
{
emoji.Append(runes.Current);
}
else if (runes.Current.Value == Emoji.ZeroWidthJoiner ||
runes.Current.Value == Emoji.ObjectReplacementCharacter ||
runes.Current.Value == Codepoints.VariationSelectors.EmojiSymbol ||
runes.Current.Value == Codepoints.VariationSelectors.TextSymbol)
{
emoji.Append(runes.Current);
}
else
{
if (emoji.Length > 0)
{
glyphs.Add(emoji.ToString());
emoji.Clear();
}
glyphs.Add(runes.Current.ToString());
}
lastRune = runes.Current;
}

//todo: support surrogates
foreach (char c in str)
// Each glyph maps to a pixel as a starting point.
// Emoji's and Ligatures are complex strings, so they start at a point and then overlap following pixels
// the x and y are adjusted accodingly.
foreach (var glyph in glyphs)
{
Point characterPoint = whereToDraw.Transform(Matrix.CreateTranslation(currentXPosition++, 0));
Point characterPoint = whereToDraw.Transform(Matrix.CreateTranslation(currentXPosition, currentYPosition));
Color foregroundColor = consoleBrush.Color;

switch (c)
switch (glyph)
{
case '\t':
{
const int tabSize = 8;
var consolePixel = new Pixel(' ', foregroundColor);
for (int j = 0; j < tabSize; j++)
case "\t":
{
Point newCharacterPoint = characterPoint.WithX(characterPoint.X + j);
CurrentClip.ExecuteWithClipping(newCharacterPoint, () =>
const int tabSize = 8;
var consolePixel = new Pixel(new SimpleSymbol(' '), foregroundColor);
for (int j = 0; j < tabSize; j++)
{
_pixelBuffer.Set((PixelBufferCoordinate)newCharacterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});
Point newCharacterPoint = characterPoint.WithX(characterPoint.X + j);
CurrentClip.ExecuteWithClipping(newCharacterPoint, () =>
{
_pixelBuffer.Set((PixelBufferCoordinate)newCharacterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});
}

currentXPosition += tabSize - 1;
}

currentXPosition += tabSize - 1;
}
break;
case '\n':
{
/* it's not clear if we need to draw anything. Cursor can be placed at the end of the line
var consolePixel = new Pixel(' ', foregroundColor);

_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);*/
}
break;
case '\u200B':
currentXPosition--;
case "\r":
case "\f":
case "\n":
currentXPosition = 0;
currentYPosition++;
break;
default:
{
var consolePixel = new Pixel(c, foregroundColor, typeface.Style, typeface.Weight);
CurrentClip.ExecuteWithClipping(characterPoint, () =>
{
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});
}
var symbol = new SimpleSymbol(glyph);
var consolePixel = new Pixel(symbol, foregroundColor, typeface.Style, typeface.Weight);
CurrentClip.ExecuteWithClipping(characterPoint, () =>
{
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});

if (symbol.Width > 1)
currentXPosition += symbol.Width;
else
currentXPosition++;
}
break;
}
}
Expand Down
Loading
Loading