Skip to content

Commit

Permalink
Add support for full RGB colors and TextDecorations (#126)
Browse files Browse the repository at this point in the history
* Avalonia 11.0.9

* Refactor multiple code files and update code comments

Overhauled various files through code refactoring, removing unnecessary attributes, updating type definitions, cleaning up method signatures. Significant changes were made to the Core/Infrastructure platform and several TurboVision themes and templates.

* Update helper method syntax and adjust bindings

This commit transitions helper methods to use a new syntax, and adjusts various bindings in grid items and data templates. In several instances, template-related properties such as design width and height were removed. Changes were also made to focus management, specifically transitioning from using the deprecated FocusManager.Instance to using AvaloniaLocator for service retrieval. The GlyphRun creation was refactored in SymbolsControl.cs.

* more fixes

* Add Avalonia threading and improve text shaping

Introduced Avalonia threading in ConsoloniaPlatform, and bound to a new ManagedDispatcherImpl. Moreover, text shaping has been improved in SymbolsControl by replacing the default measure with an enumerable list of GlyphInfo. Some code has been commented out in ApplicationStartup for later consideration.

* Refactor drawing and rendering methods

Removed dependency on IPlatformRenderInterface in several methods of ConsoloniaRenderInterface and adjusted corresponding logic. Improved DrawGlyphRun method's implementation in DrawingContextImpl, modifying glyphRun checks and string drawing. Made small adjustments to classes MoveConsoleCaretToPositionBrush and FourBitColorBrush for better alignment with IImmutableBrush.

* application is now being run, but does not render

* Welcome page is rendered

* Window is painted when resized

* Clickable button

* - unused code

* - unused button

* Button shadow drawing

* Button drawing/ SymbolsControl.cs drawing

* text trimming is fixed

* Wild text rendering

* Initialize transform with Matrix Identity.

The transform in the DrawingContextImpl.cs was updated to be initialized with Matrix identity. Removed the unused methods related to geometry and rendering in ConsoloniaRenderInterface.cs. Made a small adjustment to exception message in RenderTarget.cs for clarity.

* Platform settings for investigation

* LineBrush is rendered

* ScrollViewer now works

* Null reference check brought back

* hit test for GlyphRun

* +1, similar to avalonia

* Textbox working

* more textbox

* keyboardnavigationhandler is back

* combobox fix 1

* ComboBox drop-down works fine

* array in the combobox items

* Single test run. Buttons and TextBlock tests success separately.

* Multiple tests now can run

* ProgressBar.axaml adjusted

* Separator

* Dialog fixed

* Entire list item is clickable

* Entire row in datagrid is clickable

* Custom textbox Caret

* Comment removed

* datagrid lines

* Calendar fixed

* build fixes

* more quality fixes

* TextBoxTests.cs

* ComboBoxTests.cs

* FlyoutTests.cs

* PR fixes

* build fixes

* refactoring

* formatting and small fixes

* Limits to run jobs

* newer analysis version and null referene fix

* Delegate error

* other errors

* PR fixes

* last warning

* Automated JetBrains cleanup

Co-authored-by:  <[email protected]>

* newer jb version

* Revert "Automated JetBrains cleanup"

This reverts commit e9c3497.

* fix click scroll bug and wheel scroll bug.

* Added full color support
* renamed FourBitColorBrush with ConsoleColorBrush
* Changed to use Color instead of ConsoleColor giving full spectrum
* You can now directly set color properties and they will render correctly Example Background="Aquamarine"
* Plumbed FontStyle/Weight through to render engine so it can render Bold text
* added Colors example tab
* Modified flushing buffer to flush on change of color weight or style

* cleanup brush detection code

* add relative Shade()/Brighten() methods
add pixel tests

* add support for underline

* merge with master

* clean up sample#

* cleanup sample again

* cleanup switch statement

* cleanup sample

* Consolidated code for converting IBrush in ConsoleBrush.FromBrush() method.
Fixed warning errors
Added more robust unit tests around boundaries of shade()/brighten()

* add suport for linearGradientBrush on rectangles.

* Update src/Tests/Consolonia.TestsCore/Consolonia.TestsCore.csproj

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/Consolonia.Gallery/Gallery/GalleryViews/GalleryColors.axaml.cs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/Tests/Consolonia.TestsCore/UnitTestConsole.cs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* plumb textdecorations through instead of using Oblique

* code review

* fix bad auto-merge <sigh>

* update packages

* cleanup based on code review

* Update src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* code review cleanup

* final round of code reviews

* more cleanup

* more cleanup

* removed consolebrush converter

* Consolidate Special character logic in Symbol.GetCharacer()

* add support for Radial and Conic brushs

* fix unit tests

* fix bug in gradient interpolation

* remove -1, it was not necessary

* remove testsCore from packable list

* lint fixes

* code rabbit changes

* more lint

* sigh. try again.

* Automated JetBrains cleanup

Co-authored-by:  <[email protected]>

* fix const

---------

Co-authored-by: Evgeny Gorbovoy <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Oct 31, 2024
1 parent 36f462a commit 55c98a1
Show file tree
Hide file tree
Showing 48 changed files with 1,108 additions and 379 deletions.
1 change: 1 addition & 0 deletions src/Consolonia.Core/Consolonia.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.FreeDesktop" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
<PackageReference Include="Crayon" Version="2.0.69" />
<PackageReference Include="NullLib.ConsoleEx" Version="1.0.4.4" />
</ItemGroup>

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

namespace Consolonia.Core.Drawing
{
public class ConsoleBrush : AvaloniaObject, IImmutableBrush
{
public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<ConsoleBrush, Color>(nameof(Color));

public static readonly StyledProperty<PixelBackgroundMode> ModeProperty =
AvaloniaProperty.Register<ConsoleBrush, PixelBackgroundMode>(nameof(Mode));

// ReSharper disable once UnusedMember.Global
public ConsoleBrush(Color color, PixelBackgroundMode mode) : this(color)
{
Mode = mode;
}

public ConsoleBrush(Color color)
{
Color = color;
}

public ConsoleBrush()
{
}

public PixelBackgroundMode Mode
{
get => GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}

public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}

public double Opacity => 1;
public ITransform Transform => null;
public RelativePoint TransformOrigin => RelativePoint.TopLeft;

/// <summary>
/// Convert a IBrush to a Brush.
/// </summary>
/// <remarks>
/// NOTE: If it's a ConsoleBrush it will be passed through unchanged, unless mode is set then it will convert
/// consolebrush to mode
/// </remarks>
/// <param name="brush"></param>
/// <param name="mode">Default is Colored.</param>
/// <returns></returns>
public static ConsoleBrush FromBrush(IBrush brush, PixelBackgroundMode? mode = null)
{
ArgumentNullException.ThrowIfNull(brush, nameof(brush));

switch (brush)
{
case ConsoleBrush consoleBrush:
if (mode != null && consoleBrush.Mode != mode)
return new ConsoleBrush(consoleBrush.Color, mode.Value);
return consoleBrush;

case LineBrush lineBrush:
switch (lineBrush.Brush)
{
case ConsoleBrush consoleBrush:
return consoleBrush;
case ISolidColorBrush br:
return new ConsoleBrush(br.Color, mode ?? PixelBackgroundMode.Colored);
default:
ConsoloniaPlatform.RaiseNotSupported(6);
return null;
}

case ISolidColorBrush solidBrush:
return new ConsoleBrush(solidBrush.Color, mode ?? PixelBackgroundMode.Colored);

default:
ConsoloniaPlatform.RaiseNotSupported(6);
return null;
}
}

public static ConsoleBrush FromPosition(IBrush brush, int x, int y, int width, int height)
{
ArgumentNullException.ThrowIfNull(brush);
if (x < 0 || x > width)
throw new ArgumentOutOfRangeException(nameof(x), "x is out bounds");
if (y < 0 || y > height)
throw new ArgumentOutOfRangeException(nameof(y), "y is out bounds");
if (width <= 0)
throw new ArgumentOutOfRangeException(nameof(width), "Width must be positive");
if (height <= 0)
throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive");

switch (brush)
{
case ILinearGradientBrush gradientBrush:
{
// Calculate the relative position within the gradient
double horizontalRelativePosition = (double)x / width;
double verticalRelativePosition = (double)y / height;

// Interpolate horizontal and vertical colors
Color horizontalColor = InterpolateColor(gradientBrush, horizontalRelativePosition);
Color verticalColor = InterpolateColor(gradientBrush, verticalRelativePosition);

// Average the two colors to get the final color
Color color = BlendColors(horizontalColor, verticalColor);
return new ConsoleBrush(color);
}
case IRadialGradientBrush radialBrush:
{
// Calculate the normalized center coordinates
double centerX = radialBrush.Center.Point.X * width;
double centerY = radialBrush.Center.Point.Y * height;

// Calculate the distance from the center
double dx = x - centerX;
double dy = y - centerY;
double distance = Math.Sqrt(dx * dx + dy * dy);

// Normalize the distance based on the brush radius
double normalizedDistance = distance / (Math.Min(width, height) * radialBrush.Radius);

// Clamp the normalized distance to [0, 1]
normalizedDistance = Math.Min(Math.Max(normalizedDistance, 0), 1);

// Interpolate the color based on the normalized distance
Color color = InterpolateColor(radialBrush, normalizedDistance);
return new ConsoleBrush(color);
}
case IConicGradientBrush conicBrush:
{
// Calculate the relative position within the gradient
double horizontalRelativePosition = (double)x / width;
double verticalRelativePosition = (double)y / height;

// Interpolate horizontal and vertical colors
Color horizontalColor = InterpolateColor(conicBrush, horizontalRelativePosition);
Color verticalColor = InterpolateColor(conicBrush, verticalRelativePosition);

// Average the two colors to get the final color
Color color = BlendColors(horizontalColor, verticalColor);
return new ConsoleBrush(color);
}

default:
return FromBrush(brush);
}
}

// ReSharper disable once UnusedMember.Global used by Avalonia
// ReSharper disable once UnusedParameter.Global
public IBrush ProvideValue(IServiceProvider _)
{
return this;
}

private static Color InterpolateColor(IGradientBrush brush, double relativePosition)
{
IGradientStop before = null;
IGradientStop after = null;

foreach (IGradientStop stop in brush.GradientStops)
if (stop.Offset <= relativePosition)
{
before = stop;
}
else if (stop.Offset >= relativePosition)
{
after = stop;
break;
}

if (before == null && after == null)
throw new ArgumentException("no gradientstops defined");

if (before == null) return after.Color;
if (after == null) return before.Color;

double ratio = (relativePosition - before.Offset) / (after.Offset - before.Offset);
byte r = (byte)(before.Color.R + ratio * (after.Color.R - before.Color.R));
byte g = (byte)(before.Color.G + ratio * (after.Color.G - before.Color.G));
byte b = (byte)(before.Color.B + ratio * (after.Color.B - before.Color.B));
byte a = (byte)(before.Color.A + ratio * (after.Color.A - before.Color.A));

return Color.FromArgb(a, r, g, b);
}

private static Color BlendColors(Color color1, Color color2)
{
int r = (color1.R + color2.R) / 2;
int g = (color1.G + color2.G) / 2;
int b = (color1.B + color2.B) / 2;
int a = (color1.A + color2.A) / 2;
return Color.FromArgb((byte)a, (byte)r, (byte)g, (byte)b);
}
}
}
71 changes: 33 additions & 38 deletions src/Consolonia.Core/Drawing/DrawingContextImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Consolonia.Core.Drawing.PixelBufferImplementation;
using Consolonia.Core.Infrastructure;
Expand Down Expand Up @@ -119,38 +120,33 @@ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
case ISceneBrush sceneBrush:
{
ISceneBrushContent sceneBrushContent = sceneBrush.CreateContent();
sceneBrushContent!.Render(this, Matrix.Identity);
if (sceneBrushContent != null) sceneBrushContent.Render(this, Matrix.Identity);
return;
}
}

if (brush is not FourBitColorBrush backgroundBrush)
{
ConsoloniaPlatform.RaiseNotSupported(9, brush, pen, rect, boxShadows);
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++)
{
Rect r2 = r.TransformToAABB(Transform);
int px = (int)(r2.TopLeft.X + x);
int py = (int)(r2.TopLeft.Y + y);

(double x, double y) = r2.TopLeft;
for (int i = 0; i < r2.Width + (pen?.Thickness ?? 0); i++)
for (int j = 0; j < r2.Height + (pen?.Thickness ?? 0); j++)
ConsoleBrush backgroundBrush = ConsoleBrush.FromPosition(brush, x, y, (int)width, (int)height);
CurrentClip.ExecuteWithClipping(new Point(px, py), () =>
{
int px = (int)(x + i);
int py = (int)(y + j);
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);
});
}
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(pixel, bb) => { return pixel.Blend(new Pixel(new PixelBackground(bb.Mode, bb.Color))); },
backgroundBrush);
});
}
}

if (pen is null or { Thickness: 0 } or { Brush: null }) return;
if (pen is null or { Thickness: 0 }
or { Brush: null }) return;

DrawLineInternal(pen, new Line(r.TopLeft, false, (int)r.Width));
DrawLineInternal(pen, new Line(r.BottomLeft, false, (int)r.Width));
Expand Down Expand Up @@ -180,7 +176,7 @@ public void DrawGlyphRun(IBrush foreground, IGlyphRunImpl glyphRun)

string charactersDoDraw =
string.Concat(glyphRunImpl.GlyphIndices.Select(us => (char)us).ToArray());
DrawStringInternal(foreground, charactersDoDraw);
DrawStringInternal(foreground, charactersDoDraw, glyphRun.GlyphTypeface);
}

public IDrawingContextLayerImpl CreateLayer(Size size)
Expand Down Expand Up @@ -262,14 +258,14 @@ public Matrix Transform
set => _transform = value * _postTransform;
}

private static ConsoleColor? ExtractConsoleColorOrNullWithPlatformCheck(IPen pen, out LineStyle? lineStyle)
private static Color? ExtractColorOrNullWithPlatformCheck(IPen pen, out LineStyle? lineStyle)
{
lineStyle = null;
if (pen is not
{
Brush: FourBitColorBrush or LineBrush { Brush: FourBitColorBrush },
Brush: ConsoleBrush or LineBrush or ImmutableSolidColorBrush,
Thickness: 1,
DashStyle: null or { Dashes.Count: 0 },
DashStyle: null or { Dashes: { Count: 0 } },
LineCap: PenLineCap.Flat,
LineJoin: PenLineJoin.Miter
})
Expand All @@ -278,12 +274,10 @@ public Matrix Transform
return null;
}

if (pen.Brush is not FourBitColorBrush consoleColorBrush)
{
var lineBrush = (LineBrush)pen.Brush;
consoleColorBrush = (FourBitColorBrush)lineBrush.Brush;
if (pen.Brush is LineBrush lineBrush)
lineStyle = lineBrush.LineStyle;
}

ConsoleBrush consoleColorBrush = ConsoleBrush.FromBrush(pen.Brush);

switch (consoleColorBrush.Mode)
{
Expand Down Expand Up @@ -317,12 +311,12 @@ private void DrawLineInternal(IPen pen, Line line)
return;
}

var extractConsoleColorCheckPlatformSupported =
ExtractConsoleColorOrNullWithPlatformCheck(pen, out var lineStyle);
if (extractConsoleColorCheckPlatformSupported == null)
var extractColorCheckPlatformSupported =
ExtractColorOrNullWithPlatformCheck(pen, out var lineStyle);
if (extractColorCheckPlatformSupported == null)
return;

var consoleColor = (ConsoleColor)extractConsoleColorCheckPlatformSupported;
var consoleColor = (Color)extractColorCheckPlatformSupported;

byte pattern = (byte)(line.Vertical ? 0b0010 : 0b0100);
DrawPixelAndMoveHead(1); //beginning
Expand Down Expand Up @@ -354,9 +348,10 @@ void DrawPixelAndMoveHead(int count)
}
}

private void DrawStringInternal(IBrush foreground, string str, Point origin = new())
private void DrawStringInternal(IBrush foreground, string str, IGlyphTypeface typeface, Point origin = new())
{
if (foreground is not FourBitColorBrush { Mode: PixelBackgroundMode.Colored } consoleColorBrush)
foreground = ConsoleBrush.FromBrush(foreground);
if (foreground is not ConsoleBrush { Mode: PixelBackgroundMode.Colored } consoleColorBrush)
{
ConsoloniaPlatform.RaiseNotSupported(4);
return;
Expand All @@ -371,7 +366,7 @@ void DrawPixelAndMoveHead(int count)
foreach (char c in str)
{
Point characterPoint = whereToDraw.Transform(Matrix.CreateTranslation(currentXPosition++, 0));
ConsoleColor foregroundColor = consoleColorBrush.Color;
Color foregroundColor = consoleColorBrush.Color;

switch (c)
{
Expand Down Expand Up @@ -406,7 +401,7 @@ void DrawPixelAndMoveHead(int count)
break;
default:
{
var consolePixel = new Pixel(c, foregroundColor);
var consolePixel = new Pixel(c, foregroundColor, typeface.Style, typeface.Weight);
CurrentClip.ExecuteWithClipping(characterPoint, () =>
{
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,
Expand Down
Loading

0 comments on commit 55c98a1

Please sign in to comment.