Skip to content

Commit

Permalink
FINALLY found the multi-char disappearing char bug!
Browse files Browse the repository at this point in the history
I need to actually treat Symbol Empty and Symbol space different, as Symbol space is for drawing background, and Symbol.Empty represents pixel that is invisible because
of prior double-wide char overlapping with it.
  • Loading branch information
tomlm committed Nov 17, 2024
1 parent 8253d3f commit c26b1ef
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 34 deletions.
17 changes: 15 additions & 2 deletions src/Consolonia.Core/Drawing/DrawingContextImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
CurrentClip.ExecuteWithClipping(new Point(px, py), () =>
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(pixel, bb) => (pixel ?? new Pixel()).Blend(new Pixel(new PixelBackground(bb.Mode, bb.Color))),
(pixel, bb) => pixel.Blend(new Pixel(new PixelBackground(bb.Mode, bb.Color))),
backgroundBrush);
});
}
Expand Down Expand Up @@ -539,8 +539,21 @@ private void DrawPixelAndMoveHead(ref Point head, Line line, LineStyle? lineStyl
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,

Check warning on line 539 in src/Consolonia.Core/Drawing/DrawingContextImpl.cs

View workflow job for this annotation

GitHub Actions / build

"[AccessToModifiedClosure] Captured variable is modified in the outer scope" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/DrawingContextImpl.cs(539,73)
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});

currentXPosition++;
if (symbol.Width > 1)
{
for(int i=1;i<symbol.Width;i++)
{
characterPoint =
whereToDraw.Transform(Matrix.CreateTranslation(currentXPosition, currentYPosition));
CurrentClip.ExecuteWithClipping(characterPoint, () =>
{
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,

Check warning on line 551 in src/Consolonia.Core/Drawing/DrawingContextImpl.cs

View workflow job for this annotation

GitHub Actions / build

"[AccessToModifiedClosure] Captured variable is modified in the outer scope" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/DrawingContextImpl.cs(551,81)
(oldPixel, cp) => Pixel.Empty, consolePixel);
});
currentXPosition++;
}
}
}
break;
}
Expand Down
11 changes: 8 additions & 3 deletions src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,26 @@ namespace Consolonia.Core.Drawing.PixelBufferImplementation
[DebuggerDisplay("'{Foreground.Symbol.Text}' [{Foreground.Color}, {Background.Color}]")]
public class Pixel : IEquatable<Pixel>
{
// Pixel empty is a non-pixel. It has no symbol, no color, no weight, no style, no text decoration, and no background.
// it is used only when a multichar sequence overlaps a pixel making it a non-entity.
public static Pixel Empty => new Pixel();

// pixel space is a pixel with a space symbol, but could have color blended into. it is used to advance the cursor
// and set the background color
public static Pixel Space => new Pixel(new PixelForeground(new SimpleSymbol(' '), Colors.Transparent), new PixelBackground(Colors.Transparent));

public PixelForeground Foreground { get; set; }

public PixelBackground Background { get; set; }

public bool IsCaret { get; set; }

public Pixel()
protected Pixel()
{
Foreground = new PixelForeground();
Background = new PixelBackground();
IsCaret = false;
}

public Pixel(bool isCaret)
{
Foreground = new PixelForeground();
Expand Down Expand Up @@ -56,7 +61,7 @@ public Pixel(ISymbol symbol,
/// </summary>
/// <param name="background"></param>
public Pixel(PixelBackground background) :
this(new PixelForeground(new SimpleSymbol(), Colors.Transparent),
this(new PixelForeground(new SimpleSymbol(' '), Colors.Transparent),
background)
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ public PixelBuffer(ushort width, ushort height)
Width = width;
Height = height;
_buffer = new Pixel[width, height];

// initialize the buffer with space so it draws any background color
// blended into it.
for (ushort y = 0; y < height; y++)
for (ushort x = 0; x < width; x++)
_buffer[x, y] = new Pixel();
_buffer[x, y] = Pixel.Space;
}

public ushort Width { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public class SimpleSymbol : ISymbol, IEquatable<SimpleSymbol>
{
public SimpleSymbol()
{
// we use String.Empty to represent an empty symbol. It still takes up space, but it's invisible
// we use String.Empty to represent an empty symbol
Text = string.Empty;
Width = 1;
Width = 0;
}

public SimpleSymbol(char character)
Expand Down
11 changes: 7 additions & 4 deletions src/Consolonia.Core/Drawing/RenderTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ private void OnResized(Size size, WindowResizeReason reason)
private static Pixel?[,] InitializeCache(ushort width, ushort height)
{
var cache = new Pixel?[width, height];

// initalize the cache with Pixel.Empty as it literally means nothing
for (ushort y = 0; y < height; y++)
for (ushort x = 0; x < width; x++)
cache[x, y] = new Pixel();
cache[x, y] = Pixel.Empty;
return cache;
}

Expand Down Expand Up @@ -192,9 +194,10 @@ public void WritePixel(
_lastBufferPointStart = _currentBufferPoint = bufferPoint;
}

if ((pixel.Foreground.Symbol?.Text.Length ?? 0) == 0)
_stringBuilder.Append(' ');
else
// the only pixels without width are Empty pixels, which we don't
// want to output as they are already invisible and represented
// by the complex glyph coming before it (aka double-wide chars)
if (pixel.Foreground.Symbol.Width > 0)
_stringBuilder.Append(pixel.Foreground.Symbol!.Text);

_currentBufferPoint = _currentBufferPoint.WithXpp();
Expand Down
2 changes: 1 addition & 1 deletion src/Consolonia.Core/Infrastructure/ConsoleWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ private void OnConsoleOnResized()
{
// clear screen so we don't see crazy while resizing.
#pragma warning disable CA1303 // Do not pass literals as localized parameters
System.Console.Write(ConsoleUtils.ClearScreen);
System.Console.Write(ESC.ClearScreen);
#pragma warning restore CA1303 // Do not pass literals as localized parameters

// Cancel previous task if there is one and start a new one
Expand Down
28 changes: 14 additions & 14 deletions src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ private void PrepareConsole()
{
#pragma warning disable CA1303 // Do not pass literals as localized parameters
// enable alternate screen so original console screen is not affected by the app
WriteText(ConsoleUtils.EnableAlternateBuffer);
WriteText(ConsoleUtils.HideCursor);
WriteText(ConsoleUtils.ClearScreen);
WriteText(ESC.EnableAlternateBuffer);
WriteText(ESC.HideCursor);
WriteText(ESC.ClearScreen);
#pragma warning restore CA1303 // Do not pass literals as localized parameters
}

Expand All @@ -47,7 +47,7 @@ public bool CaretVisible
set
{
if (_caretVisible == value) return;
WriteText(value ? ConsoleUtils.ShowCursor : ConsoleUtils.HideCursor);
WriteText(value ? ESC.ShowCursor : ESC.HideCursor);
_caretVisible = value;
}
#pragma warning restore CA1303 // Do not pass literals as localized parameters
Expand All @@ -74,7 +74,7 @@ public bool SupportsComplexEmoji
// TODO, escape sequence
var (left2, _) = Console.GetCursorPosition();
_supportEmoji = left2 - left == 2;
WriteText(ConsoleUtils.SetCursorPosition(left, top));
WriteText(ESC.SetCursorPosition(left, top));
}
catch (Exception)
{
Expand Down Expand Up @@ -112,7 +112,7 @@ public void SetCaretPosition(PixelBufferCoordinate bufferPoint)

try
{
WriteText(ConsoleUtils.SetCursorPosition(bufferPoint.X, bufferPoint.Y));
WriteText(ESC.SetCursorPosition(bufferPoint.X, bufferPoint.Y));
}
catch (ArgumentOutOfRangeException argumentOutOfRangeException)
{
Expand All @@ -134,17 +134,17 @@ public void Print(PixelBufferCoordinate bufferPoint, Color background, Color for

var sb = new StringBuilder();
if (textDecoration == TextDecorationLocation.Underline)
sb.Append(ConsoleUtils.Underline);
sb.Append(ESC.Underline);

if (textDecoration == TextDecorationLocation.Strikethrough)
sb.Append(ConsoleUtils.Strikethrough);
sb.Append(ESC.Strikethrough);

if (style == FontStyle.Italic)
sb.Append(ConsoleUtils.Italic);
sb.Append(ESC.Italic);

sb.Append(ConsoleUtils.Background(background));
sb.Append(ESC.Background(background));

sb.Append(ConsoleUtils.Foreground(weight switch
sb.Append(ESC.Foreground(weight switch
{
FontWeight.Medium or FontWeight.SemiBold or FontWeight.Bold or FontWeight.ExtraBold or FontWeight.Black
or FontWeight.ExtraBlack
Expand All @@ -155,7 +155,7 @@ FontWeight.Thin or FontWeight.ExtraLight or FontWeight.Light
}));

sb.Append(str);
sb.Append(ConsoleUtils.Reset);
sb.Append(ESC.Reset);

WriteText(sb.ToString());

Expand Down Expand Up @@ -190,8 +190,8 @@ public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
WriteText(ConsoleUtils.DisableAlternateBuffer);
WriteText(ConsoleUtils.ShowCursor);
WriteText(ESC.DisableAlternateBuffer);
WriteText(ESC.ShowCursor);
}
#pragma warning restore CA1063 // Implement IDisposable Correctly
#pragma warning restore CA1303 // Do not pass literals as localized parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Consolonia.Core.Text
/// <summary>
/// ANSI escape definitions and utility methods.
/// </summary>
internal static class ConsoleUtils
internal static class ESC
{
// Control
public const string EndOfText = "\u0003";
Expand All @@ -31,6 +31,14 @@ internal static class ConsoleUtils
public const string HideCursor = "\u001b[?25l";
public const string ShowCursor = "\u001b[?25h";

// move cursor
public static string MoveCursorUp(int n) => $"\u001b[{n}A";

public static string MoveCursorDown(int n) => $"\u001b[{n}B";

public static string MoveCursorRight(int n) => $"\u001b[{n}C";

public static string MoveCursorLeft(int n) => $"\u001b[{n}D";

public static string SetCursorPosition(int x, int y)
=> $"\u001b[{y+1};{x+1}f";
Expand Down
30 changes: 26 additions & 4 deletions src/Consolonia.Gallery/Gallery/GalleryViews/GalleryWelcome.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,31 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
mc:Ignorable="d"
x:Class="Consolonia.Gallery.Gallery.GalleryViews.GalleryWelcome">
<TextBlock FontWeight="Bold" TextDecorations="Underline" HorizontalAlignment="Center" >
Welcome to Consolonia!
</TextBlock>
<StackPanel>
<!--This binds to the observable collection in the ViewModel-->
<ItemsControl ItemsSource="{Binding Glyphs}">

<!--Use a StackPanel to display all the modules-->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

<!--This defines the layout for each item (i.e. each module)-->
<ItemsControl.ItemTemplate>
<DataTemplate>
<!--The controls to display each item; bind them to properties on the HardwareModule class-->
<TextBlock Text="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button Content="Add" Click="OnAdd"/>
<Button Content="Remove" Click="OnRemove"/>
</StackPanel>
</StackPanel>

</UserControl>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using CommunityToolkit.Mvvm.ComponentModel;

namespace Consolonia.Gallery.Gallery.GalleryViews
{
Expand All @@ -8,12 +10,34 @@ public partial class GalleryWelcome : UserControl
{
public GalleryWelcome()
{
this.DataContext = new TestModel();
InitializeComponent();
}

private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}

private void OnAdd(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var model = (TestModel)this.DataContext;
model.Glyphs.Insert(0, "X");
}

private void OnRemove(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var model = (TestModel)this.DataContext;
model.Glyphs.RemoveAt(0);
}
}

public partial class TestModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<string> glyphs = new ObservableCollection<string>()
{
"中"
};
}
}
2 changes: 1 addition & 1 deletion src/Tests/Consolonia.Core.Tests/PixelBufferTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private static PixelBuffer CreateBuffer()
if (x % 3 == 0)
buffer[x, y] = new Pixel(new SimpleSymbol($"{x},{y}"), Colors.Blue);
else if (x % 3 == 1)
buffer[x, y] = new Pixel();
buffer[x, y] = Pixel.Empty;
else
buffer[x, y] = new Pixel(new SimpleSymbol($"{x},{y}"), Colors.White, FontStyle.Italic, FontWeight.Bold, TextDecorationLocation.Underline);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"Consolonia.PreviewHost": {
"commandName": "Project",
"commandLineArgs": "GalleryTextBlock.axaml",
"commandLineArgs": "GalleryWelcome.axaml",
"workingDirectory": "S:\\github\\Consolonia\\src\\Consolonia.Gallery"
}
}
Expand Down

0 comments on commit c26b1ef

Please sign in to comment.