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 9 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
2 changes: 1 addition & 1 deletion src/Consolonia.Core/Consolonia.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<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" />
</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
227 changes: 143 additions & 84 deletions src/Consolonia.Core/Drawing/DrawingContextImpl.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System;
using System.Diagnostics;
using System.Text;

namespace Consolonia.Core.Drawing.PixelBufferImplementation
{
/// <summary>
/// https://en.wikipedia.org/wiki/Box-drawing_character
/// </summary>
[DebuggerDisplay("DrawingBox {Text}")]
public struct DrawingBoxSymbol : ISymbol
{
// all 0bXXXX_0000 are special values
Expand All @@ -18,10 +21,14 @@ public DrawingBoxSymbol(byte upRightDownLeft)

private byte _upRightDownLeft;

public string Text => GetBoxSymbol().ToString();

public ushort Width { get; } = 1;

/// <summary>
/// https://en.wikipedia.org/wiki/Code_page_437
/// </summary>
char ISymbol.GetCharacter()
private char GetBoxSymbol()
{
//DOS linedraw characters are not ordered in any programmatic manner, and calculating a particular character shape needs to use a look-up table. from https://en.wikipedia.org/wiki/Box-drawing_character

Expand Down Expand Up @@ -49,46 +56,46 @@ char ISymbol.GetCharacter()
return horizontal ? '╪' : '╫';

default:
{
return _upRightDownLeft switch
{
EmptySymbol => char.MinValue,
BoldSymbol => '█',
0b0000_1001 => '┘',
0b1000_1001 => '╜',
0b0001_1001 => '╛',
0b1001_1001 => '╝',
0b0000_0011 => '┐',
0b0010_0011 => '╖',
0b0001_0011 => '╕',
0b0011_0011 => '╗',
0b0000_0110 => '┌',
0b0100_0110 => '╒',
0b0010_0110 => '╓',
0b0110_0110 => '╔',
0b0000_1100 => '└',
0b0100_1100 => '╘',
0b1000_1100 => '╙',
0b1100_1100 => '╚',
0b0000_1110 => '├',
0b1000_1110 or 0b0010_1110 or 0b1010_1110 => '╟',
0b0100_1110 => '╞',
0b1100_1110 or 0b0110_1110 or 0b1110_1110 => '╠',
0b0000_1011 => '┤',
0b1000_1011 or 0b0010_1011 or 0b1010_1011 => '╢',
0b0001_1011 => '╡',
0b1001_1011 or 0b0011_1011 or 0b1011_1011 => '╣',
0b0000_1101 => '┴',
0b1000_1101 => '╨',
0b0100_1101 or 0b0001_1101 or 0b0101_1101 => '╧',
0b1100_1101 or 0b1001_1101 or 0b1101_1101 => '╩',
0b0000_0111 => '┬',
0b0010_0111 => '╥',
0b0100_0111 or 0b0001_0111 or 0b0101_0111 => '╤',
0b0110_0111 or 0b0011_0111 or 0b0111_0111 => '╦',
_ => throw new InvalidOperationException()
};
}
return _upRightDownLeft switch
{
EmptySymbol => char.MinValue,
BoldSymbol => '█',
0b0000_1001 => '┘',
0b1000_1001 => '╜',
0b0001_1001 => '╛',
0b1001_1001 => '╝',
0b0000_0011 => '┐',
0b0010_0011 => '╖',
0b0001_0011 => '╕',
0b0011_0011 => '╗',
0b0000_0110 => '┌',
0b0100_0110 => '╒',
0b0010_0110 => '╓',
0b0110_0110 => '╔',
0b0000_1100 => '└',
0b0100_1100 => '╘',
0b1000_1100 => '╙',
0b1100_1100 => '╚',
0b0000_1110 => '├',
0b1000_1110 or 0b0010_1110 or 0b1010_1110 => '╟',
0b0100_1110 => '╞',
0b1100_1110 or 0b0110_1110 or 0b1110_1110 => '╠',
0b0000_1011 => '┤',
0b1000_1011 or 0b0010_1011 or 0b1010_1011 => '╢',
0b0001_1011 => '╡',
0b1001_1011 or 0b0011_1011 or 0b1011_1011 => '╣',
0b0000_1101 => '┴',
0b1000_1101 => '╨',
0b0100_1101 or 0b0001_1101 or 0b0101_1101 => '╧',
0b1100_1101 or 0b1001_1101 or 0b1101_1101 => '╩',
0b0000_0111 => '┬',
0b0010_0111 => '╥',
0b0100_0111 or 0b0001_0111 or 0b0101_0111 => '╤',
0b0110_0111 or 0b0011_0111 or 0b0111_0111 => '╦',
_ => throw new InvalidOperationException()
tomlm marked this conversation as resolved.
Show resolved Hide resolved
};
}
}
}

Expand All @@ -106,22 +113,21 @@ public ISymbol Blend(ref ISymbol symbolAbove)
_upRightDownLeft = BoldSymbol;
else
_upRightDownLeft |= drawingBoxSymbol._upRightDownLeft;

return this;
}

public static byte UpRightDownLeftFromPattern(byte pattern, LineStyle lineStyle)
public static DrawingBoxSymbol UpRightDownLeftFromPattern(byte pattern, LineStyle lineStyle)
{
if (pattern == EmptySymbol) return EmptySymbol;
if (pattern == EmptySymbol) return new DrawingBoxSymbol(EmptySymbol);
switch (lineStyle)
{
case LineStyle.SingleLine:
return pattern;
return new DrawingBoxSymbol(pattern);
case LineStyle.Bold:
return BoldSymbol;
return new DrawingBoxSymbol(BoldSymbol);
case LineStyle.DoubleLine:
byte leftPart = (byte)(pattern << 4);
return (byte)(leftPart | pattern);
return new DrawingBoxSymbol((byte)(leftPart | pattern));
default:
throw new ArgumentOutOfRangeException(nameof(lineStyle), lineStyle, null);
}
Expand Down
19 changes: 18 additions & 1 deletion src/Consolonia.Core/Drawing/PixelBufferImplementation/ISymbol.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
using System.Text;

namespace Consolonia.Core.Drawing.PixelBufferImplementation
{
public interface ISymbol
{
char GetCharacter();
/// <summary>
/// The text for the symbol (This can be single character or unicode encoding for Emoji's and the like)
/// </summary>
string Text { get; }

/// <summary>
/// The number of characters the symbol takes up
/// </summary>
ushort Width { get; }

bool IsWhiteSpace();

/// <summary>
/// Blend 2 symbols together
/// </summary>
/// <param name="symbolAbove"></param>
/// <returns></returns>
tomlm marked this conversation as resolved.
Show resolved Hide resolved
ISymbol Blend(ref ISymbol symbolAbove);
}
}
31 changes: 12 additions & 19 deletions src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Diagnostics;
using System.Text;
using Avalonia.Media;

// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedMember.Global

namespace Consolonia.Core.Drawing.PixelBufferImplementation
{
[DebuggerDisplay("'{Foreground.Symbol.Text}' [{Foreground.Color}, {Background.Color}]")]
public readonly struct Pixel
{
public PixelForeground Foreground { get; }
Expand All @@ -14,22 +17,6 @@ public readonly struct Pixel

public bool IsCaret { get; }

public Pixel(bool isCaret) : this(PixelBackgroundMode.Transparent)
{
IsCaret = isCaret;
}

public Pixel(char character, Color foregroundColor, FontStyle style = FontStyle.Normal,
FontWeight weight = FontWeight.Normal) :
this(new SimpleSymbol(character), foregroundColor, style, weight)
{
}

public Pixel(byte drawingBoxSymbol, Color foregroundColor) : this(
new DrawingBoxSymbol(drawingBoxSymbol), foregroundColor)
{
}

public Pixel(ISymbol symbol, Color foregroundColor, FontStyle style = FontStyle.Normal,
FontWeight weight = FontWeight.Normal, TextDecorationCollection textDecorations = null) : this(
new PixelForeground(symbol, weight, style, textDecorations, foregroundColor),
Expand Down Expand Up @@ -68,11 +55,17 @@ public Pixel Blend(Pixel pixelAbove)
case PixelBackgroundMode.Colored:
// merge pixelAbove into this pixel using alpha channel.
Color mergedColors = MergeColors(Background.Color, pixelAbove.Background.Color);
return new Pixel(pixelAbove.Foreground,
new PixelBackground(mergedColors));
newForeground = pixelAbove.Foreground;
newBackground = new PixelBackground(mergedColors);
break;

case PixelBackgroundMode.Transparent:
newForeground = Foreground.Blend(pixelAbove.Foreground);
// if the foreground is transparent, ignore pixelAbove foreground.
newForeground = pixelAbove.Foreground.Color != Colors.Transparent
? Foreground.Blend(pixelAbove.Foreground)
: Foreground;

// background is transparent, ignore pixelAbove background.
newBackground = Background;
break;
case PixelBackgroundMode.Shaded:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Diagnostics;
using Avalonia.Media;

namespace Consolonia.Core.Drawing.PixelBufferImplementation
{
[DebuggerDisplay("[{Color}, {Mode}]")]
public readonly struct PixelBackground
{
public PixelBackground(Color color)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ namespace Consolonia.Core.Drawing.PixelBufferImplementation
public class PixelBuffer : IEnumerable<Pixel>
{
private readonly Pixel[,] _buffer;
private PixelBufferCoordinate _caretPosition;
tomlm marked this conversation as resolved.
Show resolved Hide resolved

public PixelBuffer(ushort width, ushort height)
{
Width = width;
Height = height;
_buffer = new Pixel[width, height];
_caretPosition = new PixelBufferCoordinate(0, 0);
}

public ushort Width { get; }
Expand Down Expand Up @@ -68,6 +70,25 @@ public void Set<TUserObject>(PixelBufferCoordinate point, Func<Pixel, TUserObjec
this[point] = changeAction(this[point], userObject);
}

/// <summary>
/// Clears old pixel caret position and sets new caret position
/// </summary>
/// <param name="point"></param>
public void SetCaretPosition(PixelBufferCoordinate point)
{
// clear old caret position by merging in IsCaret = false
Pixel oldCaretPixel = _buffer[_caretPosition.X, _caretPosition.Y];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With "clearing old caret" approach, we do not detect whether more than one carets were requested. If an attempt happened to draw one more caret, then only second will be "painted". We should keep checking that only caret can be drawn at a moment

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I did this is there was a check and exception thrown on the second caret found. I assumed this meant your intent was for only a single caret.

I can simply remove the check... But what is the scenario for two carets? Can that happen in avalonia?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I tried not toggling and removing the check and what happens is that it completely broke cursor navigation on pages with multiple input controls.

I think way I have it is correct, or at least if there is a scenario where we need this it can be a future PR.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can simply remove the check... But what is the scenario for two carets? Can that happen in avalonia?

Opposite, I'm saying that we should throw an exception (may be optional tho) when two carets have been tried to show.
it should not be legit to try to show two cursors, we can not determine which one is supposed to be correct.
Any control is supposed to show the caret only when focused. Avalonia tracks the focus: any focus change starts with LostFocus in old place and only then GotFocus in new place.

Copy link
Owner

@jinek jinek Nov 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I tried not toggling and removing the check and what happens is that it completely broke cursor navigation on pages with multiple input controls.

How to reproduce it?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomlm we can move this discussion to a separate topic to merge this PR if you think would be better

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It confuses me only because I expected there are no attempts to show double cursor which we just can not do anyway. If this attempt happens, I would better investigate why and fix that instead of allowing this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, I will dig into why this is happening now. It must be something about my changes.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, I will dig into why this is happening now. It must be something about my changes.

If you push intermediate changes with the repro, I could try to investigate in parallel.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it, you are right.
I introduced a bug in Pixel.Blend(). I was merging IsCaret when merging regular foreground pixels by falling through to the the isCaret section instead of returning the blended pixel. I have fixed it, and the exception doesn't throw anymore.

Good catch!

_buffer[_caretPosition.X, _caretPosition.Y] =
new Pixel(oldCaretPixel.Foreground, oldCaretPixel.Background, false);

_caretPosition = point;

// set new caret position by merging in IsCaret = true
Pixel newCaretPixel = _buffer[_caretPosition.X, _caretPosition.Y];
_buffer[_caretPosition.X, _caretPosition.Y] =
new Pixel(newCaretPixel.Foreground, newCaretPixel.Background, true);
}

public void Foreach(Func<PixelBufferCoordinate, Pixel, Pixel> replaceAction)
{
ForeachReadonly((point, oldPixel) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Diagnostics;
using Avalonia.Media;

namespace Consolonia.Core.Drawing.PixelBufferImplementation
{
[DebuggerDisplay("'{Symbol.Text}' [{Color}]")]
public readonly struct PixelForeground
{
public PixelForeground(ISymbol symbol, FontWeight weight = FontWeight.Normal,
Expand Down Expand Up @@ -38,8 +40,6 @@ public PixelForeground Blend(PixelForeground pixelAboveForeground)
ISymbol symbolAbove = pixelAboveForeground.Symbol;
ArgumentNullException.ThrowIfNull(symbolAbove);

if (symbolAbove.IsWhiteSpace()) return this;

ISymbol newSymbol = Symbol.Blend(ref symbolAbove);

return new PixelForeground(newSymbol, pixelAboveForeground.Weight, pixelAboveForeground.Style,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;

tomlm marked this conversation as resolved.
Show resolved Hide resolved
namespace Consolonia.Core.Drawing.PixelBufferImplementation
{
[DebuggerDisplay("'{Text}'")]
public readonly struct SimpleSymbol : ISymbol
{
public SimpleSymbol(char character)
public SimpleSymbol()
{
_character = character;
Text = string.Empty;
Width = 1;
}

private readonly char _character;
public SimpleSymbol(char character)
{
Text = character.ToString();
Width = 1;
}

char ISymbol.GetCharacter()
public SimpleSymbol(string text, ushort width)
{
return _character;
Text = text;
Width = width;
}

public string Text { get; } = string.Empty;

public ushort Width { get; }

public bool IsWhiteSpace()
{
return _character == char.MinValue;
return string.IsNullOrWhiteSpace(Text);
}
tomlm marked this conversation as resolved.
Show resolved Hide resolved

public ISymbol Blend(ref ISymbol symbolAbove)
{
return symbolAbove;
return !String.IsNullOrEmpty(symbolAbove.Text) ? symbolAbove : this;
}
}
}
Loading
Loading