From 55c98a19a97c9d373970de848ad4e5f2840ce83f Mon Sep 17 00:00:00 2001 From: Tom Laird-McConnell Date: Thu, 31 Oct 2024 09:36:43 -0700 Subject: [PATCH] Add support for full RGB colors and TextDecorations (#126) * 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: <+@users.noreply.github.com> * newer jb version * Revert "Automated JetBrains cleanup" This reverts commit e9c349728cd5c08e98fe9affc75be44fd2ae0227. * 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 * 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: <+@users.noreply.github.com> * fix const --------- Co-authored-by: Evgeny Gorbovoy 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> --- src/Consolonia.Core/Consolonia.Core.csproj | 1 + src/Consolonia.Core/Drawing/ConsoleBrush.cs | 206 ++++++++++++++++++ .../Drawing/DrawingContextImpl.cs | 71 +++--- .../Drawing/FourBitColorBrush.cs | 76 ------- .../PixelBufferImplementation/Pixel.cs | 29 ++- .../PixelBackground.cs | 11 +- .../PixelForeground.cs | 29 ++- .../PixelOperations.cs | 83 +++++-- src/Consolonia.Core/Drawing/RenderTarget.cs | 87 ++++---- .../Infrastructure/IConsole.cs | 5 +- .../InputLessDefaultNetConsole.cs | 30 ++- src/Consolonia.Core/Text/FontManagerImpl.cs | 6 +- src/Consolonia.Core/Text/GlyphRunImpl.cs | 3 +- src/Consolonia.Core/Text/GlyphTypeface.cs | 9 +- src/Consolonia.Core/Text/TextShaper.cs | 2 +- .../Consolonia.Gallery.csproj | 13 ++ .../GalleryViews/GalleryAnimatedLines.axaml | 16 +- .../Gallery/GalleryViews/GalleryButton.axaml | 4 +- .../Gallery/GalleryViews/GalleryColors.axaml | 176 +++++++++++++++ .../GalleryViews/GalleryColors.axaml.cs | 91 ++++++++ .../GalleryViews/GalleryComboBox.axaml | 4 +- .../GalleryViews/GalleryDataGrid.axaml.cs | 7 +- .../Gallery/GalleryViews/GalleryDialog.axaml | 4 +- .../GalleryViews/GalleryGradientBrush.axaml | 24 ++ .../GalleryViews/GalleryGradientBrush.xaml.cs | 73 +++++++ .../Gallery/GalleryViews/GalleryListBox.axaml | 4 +- .../GalleryViews/GalleryTextBlock.axaml | 52 ++--- .../Gallery/GalleryViews/GalleryTextBox.axaml | 4 +- .../Gallery/GalleryViews/GalleryWelcome.axaml | 4 +- .../GalleryViews/SomeDialogWindow.axaml | 2 +- .../View/ControlsListView.axaml | 2 +- .../WindowsConsole.cs | 12 +- .../Templates/Controls/CalendarItem.axaml | 6 +- .../Templates/Controls/CaretControl.axaml | 2 +- .../Templates/Controls/DataGrid.axaml | 2 +- .../Templates/Controls/DialogWindow.axaml | 4 +- .../Templates/Controls/DialogWrap.axaml | 2 +- .../Helpers/ConsoloniaTextPresenter.cs | 14 +- .../Templates/Controls/Popup.axaml | 4 +- .../Templates/Controls/TextBlock.axaml | 2 +- .../Templates/TurboVisionDefaultColors.axaml | 52 ++--- .../TurboVisionBlackColors.axaml | 48 ++-- .../TurboVisionDarkColors.axaml | 48 ++-- src/Example/Views/DataGridTestWindow.axaml | 7 +- .../Consolonia.GalleryTests/TextBlockTests.cs | 2 +- .../Consolonia.TestsCore.csproj | 8 +- src/Tests/Consolonia.TestsCore/PixelTests.cs | 134 ++++++++++++ .../Consolonia.TestsCore/UnitTestConsole.cs | 12 +- 48 files changed, 1108 insertions(+), 379 deletions(-) create mode 100644 src/Consolonia.Core/Drawing/ConsoleBrush.cs delete mode 100644 src/Consolonia.Core/Drawing/FourBitColorBrush.cs create mode 100644 src/Consolonia.Gallery/Gallery/GalleryViews/GalleryColors.axaml create mode 100644 src/Consolonia.Gallery/Gallery/GalleryViews/GalleryColors.axaml.cs create mode 100644 src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.axaml create mode 100644 src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.xaml.cs create mode 100644 src/Tests/Consolonia.TestsCore/PixelTests.cs diff --git a/src/Consolonia.Core/Consolonia.Core.csproj b/src/Consolonia.Core/Consolonia.Core.csproj index 695f2d90..ead17fc1 100644 --- a/src/Consolonia.Core/Consolonia.Core.csproj +++ b/src/Consolonia.Core/Consolonia.Core.csproj @@ -5,6 +5,7 @@ + diff --git a/src/Consolonia.Core/Drawing/ConsoleBrush.cs b/src/Consolonia.Core/Drawing/ConsoleBrush.cs new file mode 100644 index 00000000..f91bf5aa --- /dev/null +++ b/src/Consolonia.Core/Drawing/ConsoleBrush.cs @@ -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 ColorProperty = + AvaloniaProperty.Register(nameof(Color)); + + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(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; + + /// + /// Convert a IBrush to a Brush. + /// + /// + /// NOTE: If it's a ConsoleBrush it will be passed through unchanged, unless mode is set then it will convert + /// consolebrush to mode + /// + /// + /// Default is Colored. + /// + 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); + } + } +} \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs index ba90be07..2b5d9f4a 100644 --- a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs +++ b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs @@ -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; @@ -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)); @@ -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) @@ -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 }) @@ -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) { @@ -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 @@ -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; @@ -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) { @@ -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, diff --git a/src/Consolonia.Core/Drawing/FourBitColorBrush.cs b/src/Consolonia.Core/Drawing/FourBitColorBrush.cs deleted file mode 100644 index bd523b82..00000000 --- a/src/Consolonia.Core/Drawing/FourBitColorBrush.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using Avalonia; -using Avalonia.Media; -using Consolonia.Core.Drawing.PixelBufferImplementation; - -namespace Consolonia.Core.Drawing -{ - [TypeConverter(typeof(FourBitBrushConverter))] - public class FourBitColorBrush : AvaloniaObject, IImmutableBrush - { - public static readonly StyledProperty ColorProperty = - AvaloniaProperty.Register(nameof(Color)); - - public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode)); - - // ReSharper disable once UnusedMember.Global - public FourBitColorBrush(ConsoleColor consoleColor, PixelBackgroundMode mode) : this(consoleColor) - { - Mode = mode; - } - - public FourBitColorBrush(ConsoleColor consoleColor) - { - Color = consoleColor; - } - - public FourBitColorBrush() - { - } - - public PixelBackgroundMode Mode - { - get => GetValue(ModeProperty); - set => SetValue(ModeProperty, value); - } - - public ConsoleColor Color - { - get => GetValue(ColorProperty); - set => SetValue(ColorProperty, value); - } - - public double Opacity => 1; - public ITransform Transform => null; - public RelativePoint TransformOrigin => RelativePoint.TopLeft; - - // ReSharper disable once UnusedMember.Global used by Avalonia - // ReSharper disable once UnusedParameter.Global - public IBrush ProvideValue(IServiceProvider _) - { - return this; - } - } - - public class FourBitBrushConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom( - ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value is string s) - return Enum.TryParse(s, out ConsoleColor result) - ? result - : null; - - return null; - } - } -} \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs index 99748959..25d289e0 100644 --- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs +++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Global @@ -8,7 +9,9 @@ namespace Consolonia.Core.Drawing.PixelBufferImplementation public readonly struct Pixel { public PixelForeground Foreground { get; } + public PixelBackground Background { get; } + public bool IsCaret { get; } public Pixel(bool isCaret) : this(PixelBackgroundMode.Transparent) @@ -16,23 +19,25 @@ public Pixel(bool isCaret) : this(PixelBackgroundMode.Transparent) IsCaret = isCaret; } - public Pixel(char character, ConsoleColor foregroundColor) : this(new SimpleSymbol(character), - foregroundColor) + 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, ConsoleColor foregroundColor) : this( + public Pixel(byte drawingBoxSymbol, Color foregroundColor) : this( new DrawingBoxSymbol(drawingBoxSymbol), foregroundColor) { } - public Pixel(ISymbol symbol, ConsoleColor foregroundColor) : this( - new PixelForeground(symbol, 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), new PixelBackground(PixelBackgroundMode.Transparent)) { } - public Pixel(ConsoleColor backgroundColor) : this( + public Pixel(Color backgroundColor) : this( new PixelBackground(PixelBackgroundMode.Colored, backgroundColor)) { } @@ -63,7 +68,17 @@ public Pixel Blend(Pixel pixelAbove) case PixelBackgroundMode.Colored: return pixelAbove; case PixelBackgroundMode.Transparent: - newForeground = Foreground.Blend(pixelAbove.Foreground); + // when a textdecoration of underline happens a DrawLine() is called over the top of the a pixel with non-zero symbol. + // this detects this situation and eats the draw line, turning it into a textdecoration + if (pixelAbove.Foreground.Symbol is DrawingBoxSymbol && + Foreground.Symbol is SimpleSymbol simpleSymbol && + ((ISymbol)simpleSymbol).GetCharacter() != (char)0) + // this is a line being draw through text. add TextDecoration for underline. + newForeground = new PixelForeground(Foreground.Symbol, Foreground.Weight, Foreground.Style, + TextDecorations.Underline, Foreground.Color); + else + // do normal blend. + newForeground = Foreground.Blend(pixelAbove.Foreground); newBackground = Background; break; case PixelBackgroundMode.Shaded: diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs index fa38b802..ddefd3af 100644 --- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs +++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs @@ -1,21 +1,22 @@ using System; +using Avalonia.Media; namespace Consolonia.Core.Drawing.PixelBufferImplementation { public readonly struct PixelBackground { - public PixelBackground(PixelBackgroundMode mode, ConsoleColor color = ConsoleColor.Black) + public PixelBackground(PixelBackgroundMode mode, Color? color = null) { - Color = color; + Color = color ?? Colors.Black; Mode = mode; } - public ConsoleColor Color { get; } + public Color Color { get; } public PixelBackgroundMode Mode { get; } public PixelBackground Shade() { - ConsoleColor newColor = Color; + Color newColor = Color; PixelBackgroundMode newMode = Mode; switch (Mode) { @@ -27,7 +28,7 @@ public PixelBackground Shade() break; case PixelBackgroundMode.Shaded: newMode = PixelBackgroundMode.Colored; - newColor = ConsoleColor.Black; + newColor = Colors.Black; break; default: throw new ArgumentOutOfRangeException(); diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelForeground.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelForeground.cs index 1f80191a..36091826 100644 --- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelForeground.cs +++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelForeground.cs @@ -1,34 +1,49 @@ using System; +using Avalonia.Media; namespace Consolonia.Core.Drawing.PixelBufferImplementation { public readonly struct PixelForeground { - public PixelForeground(ISymbol symbol, ConsoleColor color = ConsoleColor.White) + public PixelForeground(ISymbol symbol, FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal, TextDecorationCollection textDecorations = null, Color? color = null) { + ArgumentNullException.ThrowIfNull(symbol); Symbol = symbol; - Color = color; + Color = color ?? Colors.White; + Weight = weight; + Style = style; + TextDecorations = textDecorations; } public ISymbol Symbol { get; } //now working with 16 bit unicode only for simplicity //todo: reference here - public ConsoleColor Color { get; } + + public Color Color { get; } + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public TextDecorationCollection TextDecorations { get; } public PixelForeground Shade() { - ConsoleColor newColor = Color.Shade(); - return new PixelForeground(Symbol, newColor); + Color newColor = Color.Shade(); + return new PixelForeground(Symbol, Weight, Style, TextDecorations, newColor); } public PixelForeground Blend(PixelForeground pixelAboveForeground) { //todo: check default(char) is there ISymbol symbolAbove = pixelAboveForeground.Symbol; + ArgumentNullException.ThrowIfNull(symbolAbove); if (symbolAbove.IsWhiteSpace()) return this; - ConsoleColor newColor = pixelAboveForeground.Color; ISymbol newSymbol = Symbol.Blend(ref symbolAbove); - return new PixelForeground(newSymbol, newColor); + + return new PixelForeground(newSymbol, pixelAboveForeground.Weight, pixelAboveForeground.Style, + pixelAboveForeground.TextDecorations, pixelAboveForeground.Color); } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelOperations.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelOperations.cs index 6f3f6460..8cfda27d 100644 --- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelOperations.cs +++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelOperations.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; // ReSharper disable UnusedMember.Global @@ -6,25 +7,77 @@ namespace Consolonia.Core.Drawing.PixelBufferImplementation { internal static class PixelOperations { - public static ConsoleColor Shade(this ConsoleColor color) + private const int ColorAdjustment = 32; + + /// + /// Make color darker + /// + /// + /// + public static Color Shade(this Color color) + { + return color.Shade(Colors.Black); + } + + /// + /// Make color brighter + /// + /// + /// + public static Color Brighten(this Color color) + { + return color.Brighten(Colors.Black); + } + + /// + /// Make color less contrast with background color + /// + /// + /// + /// + public static Color Shade(this Color foreground, Color background) + { + int foregroundBrightness = GetPerceivedBrightness(foreground); + int backgroundBrightness = GetPerceivedBrightness(background); + if (foregroundBrightness > backgroundBrightness || + foregroundBrightness == backgroundBrightness && foregroundBrightness > 0) + // if color is lighter than background shading should make it more dark (less contrast) + return Color.FromRgb((byte)Math.Max(0, foreground.R - ColorAdjustment), + (byte)Math.Max(0, foreground.G - ColorAdjustment), + (byte)Math.Max(0, foreground.B - ColorAdjustment)); + // if color is darker than background shading should make it more bright (less contrast) + return Color.FromRgb((byte)Math.Min(byte.MaxValue, foreground.R + ColorAdjustment), + (byte)Math.Min(byte.MaxValue, foreground.G + ColorAdjustment), + (byte)Math.Min(byte.MaxValue, foreground.B + ColorAdjustment)); + } + + /// + /// Make color more contrast with background color + /// + /// + /// + /// + public static Color Brighten(this Color foreground, Color background) { - // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - switch (color) - { - case ConsoleColor.Black: return color; - case ConsoleColor.White: return ConsoleColor.Gray; - default: - string name = Enum.GetName(color) ?? throw new NotImplementedException(); - const string dark = "Dark"; - return !name.Contains(dark, StringComparison.Ordinal) - ? Enum.Parse(dark + name) - : ConsoleColor.Black; - } + int foregroundBrightness = GetPerceivedBrightness(foreground); + int backgroundBrightness = GetPerceivedBrightness(background); + if (foregroundBrightness > backgroundBrightness || foregroundBrightness == backgroundBrightness && + foregroundBrightness < byte.MaxValue) + // if color is lighter than background brighten should make it more bright (more contrast) + return Color.FromRgb((byte)Math.Min(byte.MaxValue, foreground.R + ColorAdjustment), + (byte)Math.Min(byte.MaxValue, foreground.G + ColorAdjustment), + (byte)Math.Min(byte.MaxValue, foreground.B + ColorAdjustment)); + // if color is darker than background brighten should make it more dark (more contrast) + return Color.FromRgb((byte)Math.Max(0, foreground.R - ColorAdjustment), + (byte)Math.Max(0, foreground.G - ColorAdjustment), + (byte)Math.Max(0, foreground.B - ColorAdjustment)); } - public static (int integerFraction, int remainder) Divide(int a, int b) + private static int GetPerceivedBrightness(Color color) { - return (a / b, a % b); + return (int)(0.299 * color.R + + 0.587 * color.G + + 0.114 * color.B); } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/RenderTarget.cs b/src/Consolonia.Core/Drawing/RenderTarget.cs index d4cacc4e..631f5b88 100644 --- a/src/Consolonia.Core/Drawing/RenderTarget.cs +++ b/src/Consolonia.Core/Drawing/RenderTarget.cs @@ -5,6 +5,7 @@ using System.Text; using Avalonia; using Avalonia.Controls; +using Avalonia.Media; using Avalonia.Platform; using Consolonia.Core.Drawing.PixelBufferImplementation; using Consolonia.Core.Infrastructure; @@ -19,7 +20,8 @@ internal class RenderTarget : IDrawingContextLayerImpl private PixelBuffer _bufferBuffer; - private (ConsoleColor background, ConsoleColor foreground, char character)?[,] _cache; + private (Color background, Color foreground, FontWeight weight, FontStyle style, TextDecorationCollection + textDecorations, char character)?[,] _cache; internal RenderTarget(ConsoleWindow consoleWindow) { @@ -94,7 +96,9 @@ private void InitializeBuffer(Size size) private void InitializeCache(ushort width, ushort height) { - _cache = new (ConsoleColor background, ConsoleColor foreground, char character)?[width, height]; + _cache = + new (Color background, Color foreground, FontWeight weight, FontStyle style, TextDecorationCollection + textDecorations, char character)?[width, height]; } private void RenderToDevice() @@ -125,45 +129,22 @@ private void RenderToDevice() throw new InvalidOperationException( "All pixels in the buffer must have exact console color before rendering"); - if (x == pixelBuffer.Width - 1 && y == pixelBuffer.Height - 1) - break; - - char character; - try - { - character = pixel.Foreground.Symbol.GetCharacter(); - } - catch (NullReferenceException) - { - //todo: need to break current drawing - if (pixel.Foreground.Symbol is null) // not using 'when' as it swallows the exceptions - { - // buffer re-initialized after resizing - character = '░'; - pixel = new Pixel(new PixelForeground(), new PixelBackground(PixelBackgroundMode.Colored)); - } - else - { - throw; - } - } - - if (char.IsControl(character) /*|| character is '保' or '哥'*/) - character = ' '; // some terminals do not print \0 - - ConsoleColor backgroundColor = pixel.Background.Color; - ConsoleColor foregroundColor = pixel.Foreground.Color; + if (pixel.Foreground.Symbol is null) // not using 'when' as it swallows the exceptions + // buffer re-initialized after resizing + pixel = new Pixel(new PixelForeground(new SimpleSymbol('░')), + new PixelBackground(PixelBackgroundMode.Colored)); + (Color, Color, FontWeight Weight, FontStyle Style, TextDecorationCollection TextDecorations, char) + pixelSpread = (pixel.Background.Color, pixel.Foreground.Color, pixel.Foreground.Weight, + pixel.Foreground.Style, pixel.Foreground.TextDecorations, + pixel.Foreground.Symbol.GetCharacter()); //todo: indexOutOfRange during resize - if (_cache[x, y] == (backgroundColor, foregroundColor, character)) + if (_cache[x, y] == pixelSpread) continue; - _cache[x, y] = (backgroundColor, foregroundColor, character); + _cache[x, y] = pixelSpread; - flushingBuffer.WriteCharacter(new PixelBufferCoordinate(x, y), - backgroundColor, - foregroundColor, - character); + flushingBuffer.WritePixel(new PixelBufferCoordinate(x, y), pixel); } flushingBuffer.Flush(); @@ -184,8 +165,11 @@ private struct FlushingBuffer //todo: move class out private readonly IConsole _console; private readonly StringBuilder _stringBuilder; - private ConsoleColor _lastBackgroundColor; - private ConsoleColor _lastForegroundColor; + private Color _lastBackgroundColor; + private Color _lastForegroundColor; + private FontStyle _lastStyle = FontStyle.Normal; + private FontWeight _lastWeight = FontWeight.Normal; + private TextDecorationCollection _lastTextDecorations = new(); private PixelBufferCoordinate _currentBufferPoint; private PixelBufferCoordinate _lastBufferPointStart; @@ -196,24 +180,31 @@ public FlushingBuffer(IConsole console) _stringBuilder = new StringBuilder(); } - public void WriteCharacter( + public void WritePixel( PixelBufferCoordinate bufferPoint, - ConsoleColor backgroundColor, - ConsoleColor foregroundColor, - char character) + Pixel pixel) { if (!bufferPoint.Equals(_currentBufferPoint) /*todo: performance*/ || - _lastForegroundColor != foregroundColor || - _lastBackgroundColor != backgroundColor) + _lastForegroundColor != pixel.Foreground.Color || + _lastBackgroundColor != pixel.Background.Color || + _lastWeight != pixel.Foreground.Weight || + _lastStyle != pixel.Foreground.Style || + _lastTextDecorations != pixel.Foreground.TextDecorations) Flush(); if (_stringBuilder.Length == 0) { - _lastBackgroundColor = backgroundColor; - _lastForegroundColor = foregroundColor; + _lastBackgroundColor = pixel.Background.Color; + _lastForegroundColor = pixel.Foreground.Color; + _lastStyle = pixel.Foreground.Style; + _lastWeight = pixel.Foreground.Weight; + _lastTextDecorations = pixel.Foreground.TextDecorations; _lastBufferPointStart = _currentBufferPoint = bufferPoint; } + char character = pixel.Foreground.Symbol.GetCharacter(); + if (char.IsControl(character) /*|| character is '保' or '哥'*/) + character = ' '; // some terminals do not print \0 _stringBuilder.Append(character); _currentBufferPoint = _currentBufferPoint.WithXpp(); } @@ -223,8 +214,8 @@ public void Flush() if (_stringBuilder.Length == 0) return; - _console.Print(_lastBufferPointStart, _lastBackgroundColor, _lastForegroundColor, - _stringBuilder.ToString()); + _console.Print(_lastBufferPointStart, _lastBackgroundColor, _lastForegroundColor, _lastStyle, + _lastWeight, _lastTextDecorations, _stringBuilder.ToString()); _stringBuilder.Clear(); } } diff --git a/src/Consolonia.Core/Infrastructure/IConsole.cs b/src/Consolonia.Core/Infrastructure/IConsole.cs index 48a477e5..a3a89346 100644 --- a/src/Consolonia.Core/Infrastructure/IConsole.cs +++ b/src/Consolonia.Core/Infrastructure/IConsole.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Media; using Consolonia.Core.Drawing.PixelBufferImplementation; // ReSharper disable UnusedMember.Global @@ -19,8 +20,8 @@ public interface IConsole : IDisposable void SetCaretPosition(PixelBufferCoordinate bufferPoint); PixelBufferCoordinate GetCaretPosition(); - void Print(PixelBufferCoordinate bufferPoint, ConsoleColor backgroundColor, ConsoleColor foregroundColor, - string str); + void Print(PixelBufferCoordinate bufferPoint, Color background, Color foreground, FontStyle style, + FontWeight weight, TextDecorationCollection textDecorations, string str); event Action Resized; diff --git a/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs b/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs index 02f59ca8..de9891fa 100644 --- a/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs +++ b/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs @@ -1,11 +1,13 @@ -using System; +using System; using System.Linq; using System.Text; using System.Threading.Tasks; using Avalonia; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Media; using Consolonia.Core.Drawing.PixelBufferImplementation; +using Crayon; using NullLib.ConsoleEx; namespace Consolonia.Core.Infrastructure @@ -13,9 +15,7 @@ namespace Consolonia.Core.Infrastructure public class InputLessDefaultNetConsole : IConsole { private bool _caretVisible; - private ConsoleColor _headBackground; private PixelBufferCoordinate _headBufferPoint; - private ConsoleColor _headForeground; protected InputLessDefaultNetConsole() { @@ -66,17 +66,12 @@ public PixelBufferCoordinate GetCaretPosition() return _headBufferPoint; } - public void Print(PixelBufferCoordinate bufferPoint, ConsoleColor backgroundColor, ConsoleColor foregroundColor, - string str) + public void Print(PixelBufferCoordinate bufferPoint, Color background, Color foreground, FontStyle style, + FontWeight weight, TextDecorationCollection textDecorations, string str) { PauseTask?.Wait(); SetCaretPosition(bufferPoint); - if (_headBackground != backgroundColor) - _headBackground = Console.BackgroundColor = backgroundColor; - if (_headForeground != foregroundColor) - _headForeground = Console.ForegroundColor = foregroundColor; - if (!str.IsNormalized(NormalizationForm.FormKC)) throw new NotSupportedException("Is not supposed to be rendered"); @@ -85,7 +80,20 @@ public void Print(PixelBufferCoordinate bufferPoint, ConsoleColor backgroundColo char.IsLetterOrDigit(c) /*todo: https://github.com/SlimeNull/NullLib.ConsoleEx/issues/2*/)) throw new NotSupportedException("Is not supposed to be rendered"); - Console.Write(str); + if (textDecorations != null && textDecorations.Any(td => td.Location == TextDecorationLocation.Underline)) + str = Output.Underline(str); + + if (weight == FontWeight.Normal) + foreground = foreground.Shade(background); + else if (weight == FontWeight.Thin || weight == FontWeight.ExtraLight || weight == FontWeight.Light) + foreground = foreground.Shade(background).Shade(background); + else if (weight == FontWeight.Medium || weight == FontWeight.SemiBold || weight == FontWeight.Bold || + weight == FontWeight.ExtraBold || weight == FontWeight.Black || weight == FontWeight.ExtraBlack) + foreground = foreground.Brighten(background); + Console.Write(Output.Rgb(foreground.R, foreground.G, foreground.B) + .Background.Rgb(background.R, background.G, background.B) + .Text(str)); + if (_headBufferPoint.X < Size.Width - str.Length) _headBufferPoint = diff --git a/src/Consolonia.Core/Text/FontManagerImpl.cs b/src/Consolonia.Core/Text/FontManagerImpl.cs index c3c53874..fbf44abd 100644 --- a/src/Consolonia.Core/Text/FontManagerImpl.cs +++ b/src/Consolonia.Core/Text/FontManagerImpl.cs @@ -32,7 +32,11 @@ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeigh out IGlyphTypeface glyphTypeface) { //todo: check font is ours the only - glyphTypeface = new GlyphTypeface(); + glyphTypeface = new GlyphTypeface + { + Weight = weight, + Style = style + }; return true; } diff --git a/src/Consolonia.Core/Text/GlyphRunImpl.cs b/src/Consolonia.Core/Text/GlyphRunImpl.cs index 7c141569..7fbcbb50 100644 --- a/src/Consolonia.Core/Text/GlyphRunImpl.cs +++ b/src/Consolonia.Core/Text/GlyphRunImpl.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Avalonia; @@ -28,7 +27,7 @@ public void Dispose() public IReadOnlyList GetIntersections(float lowerLimit, float upperLimit) { - throw new NotImplementedException(); + return new List(); } public IGlyphTypeface GlyphTypeface { get; } diff --git a/src/Consolonia.Core/Text/GlyphTypeface.cs b/src/Consolonia.Core/Text/GlyphTypeface.cs index c8d28083..ad4a7c2d 100644 --- a/src/Consolonia.Core/Text/GlyphTypeface.cs +++ b/src/Consolonia.Core/Text/GlyphTypeface.cs @@ -61,8 +61,8 @@ public bool TryGetTable(uint tag, out byte[] table) } public string FamilyName { get; } = FontManagerImpl.GetTheOnlyFontFamilyName(); - public FontWeight Weight => FontWeight.Normal; - public FontStyle Style => FontStyle.Normal; + public FontWeight Weight { get; set; } = FontWeight.Normal; + public FontStyle Style { get; set; } = FontStyle.Normal; public FontStretch Stretch => FontStretch.Normal; public int GlyphCount => char.MaxValue; @@ -73,8 +73,9 @@ public bool TryGetTable(uint tag, out byte[] table) Ascent = -1, //var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; //todo: need to consult Avalonia guys Descent = 0, LineGap = 0, - UnderlinePosition = 0, - UnderlineThickness = 0, + UnderlinePosition = + -1, // this turns on TextDecorations="Underline", -1 is so it draws over the top of the text. + UnderlineThickness = 1, // this is the thickness of the underline, aka 1 char thick. StrikethroughPosition = 0, StrikethroughThickness = 0, IsFixedPitch = true diff --git a/src/Consolonia.Core/Text/TextShaper.cs b/src/Consolonia.Core/Text/TextShaper.cs index 84a26c9d..5331aeab 100644 --- a/src/Consolonia.Core/Text/TextShaper.cs +++ b/src/Consolonia.Core/Text/TextShaper.cs @@ -14,7 +14,7 @@ public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions optio var glyphInfos = Convert(text.Span.ToString()); var shapedBuffer = new ShapedBuffer(text, glyphInfos.Length, - new GlyphTypeface(), 1, 0 /*todo: must be 1 for right to left?*/); + options.Typeface, 1, 0 /*todo: must be 1 for right to left?*/); for (int i = 0; i < shapedBuffer.Length; i++) shapedBuffer[i] = glyphInfos[i]; return shapedBuffer; diff --git a/src/Consolonia.Gallery/Consolonia.Gallery.csproj b/src/Consolonia.Gallery/Consolonia.Gallery.csproj index 2b6a024d..a964102e 100644 --- a/src/Consolonia.Gallery/Consolonia.Gallery.csproj +++ b/src/Consolonia.Gallery/Consolonia.Gallery.csproj @@ -17,4 +17,17 @@ + + + MSBuild:Compile + + + + + + Code + GalleryGradientBrush.axaml + + + diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryAnimatedLines.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryAnimatedLines.axaml index 208dac67..6d15341c 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryAnimatedLines.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryAnimatedLines.axaml @@ -4,9 +4,8 @@ x:Class="Consolonia.Gallery.Gallery.GalleryViews.GalleryAnimatedLines"> - + @@ -46,10 +45,7 @@ VerticalAlignment="Center" HorizontalAlignment="Right"> - - - - + @@ -89,11 +85,7 @@ VerticalAlignment="Bottom" HorizontalAlignment="Right"> - - - - - + - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.xaml.cs b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.xaml.cs new file mode 100644 index 00000000..977b9a25 --- /dev/null +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.xaml.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Consolonia.Core.Drawing; + +namespace Consolonia.Gallery.Gallery.GalleryViews +{ + [GalleryOrder(1000)] + public partial class GalleryGradientBrush : UserControl + { + public GalleryGradientBrush() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Linear_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + { + this.FindControl("MyGrid").Background = new LinearGradientBrush + { + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative), + GradientStops = new GradientStops + { + new GradientStop { Color = Colors.White, Offset = 0 }, + new GradientStop { Color = Colors.Red, Offset = 1 } + } + }; + } + + private void Radial_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + { + this.FindControl("MyGrid").Background = new RadialGradientBrush + { + Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), + GradientOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), + Radius = 0.5, + GradientStops = new GradientStops + { + new GradientStop { Color = Colors.Black, Offset = 0 }, + new GradientStop { Color = Colors.Red, Offset = 1 } + } + }; + } + private void Conic_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + { + + this.FindControl("MyGrid").Background = new ConicGradientBrush + { + Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), + Angle = 0, + GradientStops = new GradientStops + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Yellow, Offset = 0.25 }, + new GradientStop { Color = Colors.Green, Offset = 0.5 }, + new GradientStop { Color = Colors.Cyan, Offset = 0.75 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + } + }; + + } + } + +} \ No newline at end of file diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryListBox.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryListBox.axaml index a4edd6b9..44b14827 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryListBox.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryListBox.axaml @@ -6,11 +6,11 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBox.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBox.axaml index 2cbbb006..e67ec4a9 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBox.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBox.axaml @@ -57,8 +57,8 @@ + SelectionBrush="Magenta" + SelectionForegroundBrush="Yellow" /> - Welcome to Avalonia! + + Welcome to Consolonia! + \ No newline at end of file diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/SomeDialogWindow.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/SomeDialogWindow.axaml index 9b5aa0e6..06c80b17 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/SomeDialogWindow.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/SomeDialogWindow.axaml @@ -10,7 +10,7 @@ . + Foreground="DarkRed">. /; ;\ __ \\____// /{_\_/ `'\____ diff --git a/src/Consolonia.Gallery/View/ControlsListView.axaml b/src/Consolonia.Gallery/View/ControlsListView.axaml index 94cc117d..0c8a46d8 100644 --- a/src/Consolonia.Gallery/View/ControlsListView.axaml +++ b/src/Consolonia.Gallery/View/ControlsListView.axaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:gallery="clr-namespace:Consolonia.Gallery.Gallery" x:Class="Consolonia.Gallery.View.ControlsListView" - Title="ControlsListView"> + Title="ControlsListView" Background="LightGray"> diff --git a/src/Consolonia.PlatformSupport/WindowsConsole.cs b/src/Consolonia.PlatformSupport/WindowsConsole.cs index 34a2eef9..d88d7d57 100644 --- a/src/Consolonia.PlatformSupport/WindowsConsole.cs +++ b/src/Consolonia.PlatformSupport/WindowsConsole.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Diagnostics; using System.Linq; @@ -163,9 +163,7 @@ private bool HandleMouseInput(WindowsConsole.MouseEventRecord mouseEvent) repeat = 0; break; case WindowsConsole.EventFlags.MouseWheeled: - double velocity = 1 / 12D; - if (incomeMouseState == -7864320) - velocity *= -1; + double velocity = incomeMouseState < 0 ? -1 : 1; wheelDelta = new Vector(0, velocity); eventType = RawPointerEventType.Wheel; break; @@ -174,6 +172,10 @@ private bool HandleMouseInput(WindowsConsole.MouseEventRecord mouseEvent) case WindowsConsole.EventFlags.MouseMoved: eventType = RawPointerEventType.Move; break; + case WindowsConsole.EventFlags.MouseMoved | WindowsConsole.EventFlags.DoubleClick: + RaiseMouseEvent(RawPointerEventType.LeftButtonDown, point, null, inputModifiers); + RaiseMouseEvent(RawPointerEventType.Move, point, null, inputModifiers); + return false; default: throw new InvalidOperationException(mouseEvent.EventFlags.ToString()); } @@ -257,7 +259,7 @@ private static extern bool WriteConsoleInput( // Resharper restore MemberCanBePrivate.Local // Resharper restore FieldCanBeMadeReadOnly.Global // Resharper restore FieldCanBeMadeReadOnly.Local -// ReSharper restore InconsistentNaming + // ReSharper restore InconsistentNaming #endregion } diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/CalendarItem.axaml b/src/Consolonia.Themes.TurboVision/Templates/Controls/CalendarItem.axaml index 5925a239..c0cb0240 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/CalendarItem.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/CalendarItem.axaml @@ -21,7 +21,7 @@ --> diff --git a/src/Consolonia.Themes.TurboVision/Templates/TurboVisionDefaultColors.axaml b/src/Consolonia.Themes.TurboVision/Templates/TurboVisionDefaultColors.axaml index 60d965c7..adec0823 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/TurboVisionDefaultColors.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/TurboVisionDefaultColors.axaml @@ -4,30 +4,30 @@ xmlns:drawing="clr-namespace:Consolonia.Core.Drawing;assembly=Consolonia.Core"> 1 - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Themes/TurboVisionBlack/TurboVisionBlackColors.axaml b/src/Consolonia.Themes.TurboVision/Themes/TurboVisionBlack/TurboVisionBlackColors.axaml index 549c2102..a3483c34 100644 --- a/src/Consolonia.Themes.TurboVision/Themes/TurboVisionBlack/TurboVisionBlackColors.axaml +++ b/src/Consolonia.Themes.TurboVision/Themes/TurboVisionBlack/TurboVisionBlackColors.axaml @@ -2,28 +2,28 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:drawing="clr-namespace:Consolonia.Core.Drawing;assembly=Consolonia.Core"> - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Themes/TurboVisionDark/TurboVisionDarkColors.axaml b/src/Consolonia.Themes.TurboVision/Themes/TurboVisionDark/TurboVisionDarkColors.axaml index ebf27518..5e85f1da 100644 --- a/src/Consolonia.Themes.TurboVision/Themes/TurboVisionDark/TurboVisionDarkColors.axaml +++ b/src/Consolonia.Themes.TurboVision/Themes/TurboVisionDark/TurboVisionDarkColors.axaml @@ -2,28 +2,28 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:drawing="clr-namespace:Consolonia.Core.Drawing;assembly=Consolonia.Core"> - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Example/Views/DataGridTestWindow.axaml b/src/Example/Views/DataGridTestWindow.axaml index 6fafbea7..bcc538d1 100644 --- a/src/Example/Views/DataGridTestWindow.axaml +++ b/src/Example/Views/DataGridTestWindow.axaml @@ -28,11 +28,8 @@ Margin="4,0,0,0" /> - - - - @@ -48,7 +45,7 @@ diff --git a/src/Tests/Consolonia.GalleryTests/TextBlockTests.cs b/src/Tests/Consolonia.GalleryTests/TextBlockTests.cs index 3d99584c..d1beb95e 100644 --- a/src/Tests/Consolonia.GalleryTests/TextBlockTests.cs +++ b/src/Tests/Consolonia.GalleryTests/TextBlockTests.cs @@ -15,7 +15,7 @@ protected override Task PerformSingleTest() "Text trimming with word...", "│Left aligned text ", " Center aligned text ", - "│ Right aligned text │", + "Right aligned text│", // multiline "│Lorem ipsum dolor sit amet, consectetur adipiscing elit.│", diff --git a/src/Tests/Consolonia.TestsCore/Consolonia.TestsCore.csproj b/src/Tests/Consolonia.TestsCore/Consolonia.TestsCore.csproj index aa551cab..2f3ae527 100644 --- a/src/Tests/Consolonia.TestsCore/Consolonia.TestsCore.csproj +++ b/src/Tests/Consolonia.TestsCore/Consolonia.TestsCore.csproj @@ -1,9 +1,15 @@ - + + + false + + + + diff --git a/src/Tests/Consolonia.TestsCore/PixelTests.cs b/src/Tests/Consolonia.TestsCore/PixelTests.cs new file mode 100644 index 00000000..5ffc2681 --- /dev/null +++ b/src/Tests/Consolonia.TestsCore/PixelTests.cs @@ -0,0 +1,134 @@ +using Avalonia.Media; +using Consolonia.Core.Drawing.PixelBufferImplementation; +using NUnit.Framework; + +namespace Consolonia.TestsCore +{ + [TestFixture] + public class PixelTests + { + [Test] + public void TestShade() + { + Color foreground = Colors.Gray; + Color newColor = foreground.Shade(); + Assert.That(newColor.R < foreground.R); + Assert.That(newColor.G < foreground.G); + Assert.That(newColor.B < foreground.B); + } + + [Test] + public void TestBrighten() + { + Color foreground = Colors.Gray; + Color newColor = foreground.Brighten(); + Assert.That(newColor.R > foreground.R); + Assert.That(newColor.G > foreground.G); + Assert.That(newColor.B > foreground.B); + } + + [Test] + public void TestRelativeShade() + { + Color foreground = Colors.LightGray; + Color background = Colors.DarkGray; + Color newColor = foreground.Shade(background); + Assert.That(newColor.R < foreground.R); + Assert.That(newColor.G < foreground.G); + Assert.That(newColor.B < foreground.B); + + Color foreground2 = Colors.DarkGray; + Color background2 = Colors.LightGray; + newColor = foreground2.Shade(background2); + Assert.That(newColor.R > foreground2.R); + Assert.That(newColor.G > foreground2.G); + Assert.That(newColor.B > foreground2.B); + } + + [Test] + public void TestRelativeBrighten() + { + Color foreground = Colors.LightGray; + Color background = Colors.DarkGray; + Color newColor = foreground.Brighten(background); + Assert.That(newColor.R > foreground.R); + Assert.That(newColor.G > foreground.G); + Assert.That(newColor.B > foreground.B); + + Color foreground2 = Colors.DarkGray; + Color background2 = Colors.LightGray; + newColor = foreground2.Brighten(background2); + Assert.That(newColor.R < foreground2.R); + Assert.That(newColor.G < foreground2.G); + Assert.That(newColor.B < foreground2.B); + } + + [Test] + public void TestShadeBoundaries() + { + Color foreground = Colors.White; + Color background = Colors.Black; + Color newColor = foreground; + for (int i = 0; i < byte.MaxValue; i++) + { + newColor = newColor.Shade(background); + Assert.That(newColor.R < foreground.R); + Assert.That(newColor.G < foreground.G); + Assert.That(newColor.B < foreground.B); + if (newColor == Colors.Black) + break; + } + + Assert.That(newColor == Colors.Black); + + Color foreground2 = Colors.Black; + Color background2 = Colors.White; + newColor = foreground2; + for (int i = 0; i < byte.MaxValue; i++) + { + newColor = newColor.Shade(background2); + Assert.That(newColor.R > foreground2.R); + Assert.That(newColor.G > foreground2.G); + Assert.That(newColor.B > foreground2.B); + if (newColor == Colors.White) + break; + } + + Assert.That(newColor == Colors.White); + } + + [Test] + public void TestBrightenBoundaries() + { + Color foreground = Colors.Black; + Color background = Colors.Black; + Color newColor = foreground; + for (int i = 0; i < byte.MaxValue; i++) + { + newColor = newColor.Brighten(background); + Assert.That(newColor.R > foreground.R); + Assert.That(newColor.G > foreground.G); + Assert.That(newColor.B > foreground.B); + if (newColor == Colors.White) + break; + } + + Assert.That(newColor == Colors.White); + + Color foreground2 = Colors.White; + Color background2 = Colors.White; + newColor = foreground2; + for (int i = 0; i < byte.MaxValue; i++) + { + newColor = newColor.Brighten(background2); + Assert.That(newColor.R < foreground2.R); + Assert.That(newColor.G < foreground2.G); + Assert.That(newColor.B < foreground2.B); + if (newColor == Colors.Black) + break; + } + + Assert.That(newColor == Colors.Black); + } + } +} \ No newline at end of file diff --git a/src/Tests/Consolonia.TestsCore/UnitTestConsole.cs b/src/Tests/Consolonia.TestsCore/UnitTestConsole.cs index 71ac5ca0..964e67a6 100644 --- a/src/Tests/Consolonia.TestsCore/UnitTestConsole.cs +++ b/src/Tests/Consolonia.TestsCore/UnitTestConsole.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Media; using Avalonia.Threading; using Consolonia.Core.Drawing.PixelBufferImplementation; using Consolonia.Core.Infrastructure; @@ -52,17 +53,18 @@ PixelBufferCoordinate IConsole.GetCaretPosition() return _fakeCaretPosition; } - void IConsole.Print(PixelBufferCoordinate bufferPoint, ConsoleColor backgroundColor, - ConsoleColor foregroundColor, - string str) + void IConsole.Print(PixelBufferCoordinate bufferPoint, Color background, Color foreground, FontStyle style, + FontWeight weight, TextDecorationCollection textDecorations, string str) { (ushort x, ushort y) = bufferPoint; for (int i = 0; i < str.Length; i++) PixelBuffer.Set(new PixelBufferCoordinate((ushort)(x + i), y), _ => // ReSharper disable once AccessToModifiedClosure we are sure about inline execution - new Pixel(new PixelForeground(new SimpleSymbol(str[i]), foregroundColor), - new PixelBackground(PixelBackgroundMode.Colored, backgroundColor))); + new Pixel( + new PixelForeground(new SimpleSymbol(str[i]), color: foreground, style: style, weight: weight, + textDecorations: textDecorations), + new PixelBackground(PixelBackgroundMode.Colored, background))); } public void PauseIO(Task task)