From 4b97ba49f075ae021f47c748d27a1c896e8db3d5 Mon Sep 17 00:00:00 2001 From: Tom Laird-McConnell Date: Tue, 17 Dec 2024 08:19:15 -0800 Subject: [PATCH] added ability to pick the cursor style when using cursor brush. --- src/Consolonia.Core/Drawing/CursorStyle.cs | 12 + .../Drawing/DrawingContextImpl.cs | 354 +++++++++--------- .../MoveConsoleCaretToPositionBrush.cs | 5 + .../PixelBufferImplementation/PixelBuffer.cs | 2 + src/Consolonia.Core/Drawing/RenderTarget.cs | 14 + .../InputLessDefaultNetConsole.cs | 1 - src/Consolonia.Core/Text/Esc.cs | 8 + 7 files changed, 219 insertions(+), 177 deletions(-) create mode 100644 src/Consolonia.Core/Drawing/CursorStyle.cs diff --git a/src/Consolonia.Core/Drawing/CursorStyle.cs b/src/Consolonia.Core/Drawing/CursorStyle.cs new file mode 100644 index 00000000..7e495edd --- /dev/null +++ b/src/Consolonia.Core/Drawing/CursorStyle.cs @@ -0,0 +1,12 @@ +namespace Consolonia.Core.Drawing +{ + public enum CursorStyle + { + BlinkingBar, + SteadyBar, + BlinkingBlock, + SteadyBlock, + BlinkingUnderline, + SteadyUnderline, + } +} \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs index 5714df4c..a30f26f6 100644 --- a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs +++ b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs @@ -96,37 +96,37 @@ public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect new SKPaint { FilterQuality = SKFilterQuality.Medium }); for (int y = 0; y < bitmap.Info.Height; y += 2) - for (int x = 0; x < bitmap.Info.Width; x += 2) - { - // NOTE: we divide by 2 because we are working with quad pixels, - // // the bitmap has twice the horizontal and twice the vertical of the target rect. - int px = (int)targetRect.TopLeft.X + x / 2; - int py = (int)targetRect.TopLeft.Y + y / 2; - - // get the quad pixel the bitmap - var quadColors = new[] + for (int x = 0; x < bitmap.Info.Width; x += 2) { + // NOTE: we divide by 2 because we are working with quad pixels, + // // the bitmap has twice the horizontal and twice the vertical of the target rect. + int px = (int)targetRect.TopLeft.X + x / 2; + int py = (int)targetRect.TopLeft.Y + y / 2; + + // get the quad pixel the bitmap + var quadColors = new[] + { bitmap.GetPixel(x, y), bitmap.GetPixel(x + 1, y), bitmap.GetPixel(x, y + 1), bitmap.GetPixel(x + 1, y + 1) }; - // map it to a single char to represent the 4 pixels - char quadPixel = GetQuadPixelCharacter(quadColors); + // map it to a single char to represent the 4 pixels + char quadPixel = GetQuadPixelCharacter(quadColors); - // get the combined colors for the quad pixel - Color foreground = GetForegroundColorForQuadPixel(quadColors, quadPixel); - Color background = GetBackgroundColorForQuadPixel(quadColors, quadPixel); + // get the combined colors for the quad pixel + Color foreground = GetForegroundColorForQuadPixel(quadColors, quadPixel); + Color background = GetBackgroundColorForQuadPixel(quadColors, quadPixel); - var imagePixel = new Pixel( - new PixelForeground(new SimpleSymbol(quadPixel), foreground), - new PixelBackground(background)); - CurrentClip.ExecuteWithClipping(new Point(px, py), - () => - { - _pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py), - existingPixel => existingPixel.Blend(imagePixel)); - }); - } + var imagePixel = new Pixel( + new PixelForeground(new SimpleSymbol(quadPixel), foreground), + new PixelBackground(background)); + CurrentClip.ExecuteWithClipping(new Point(px, py), + () => + { + _pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py), + existingPixel => existingPixel.Blend(imagePixel)); + }); + } } public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) @@ -150,83 +150,83 @@ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) DrawLineInternal(pen, myLine); break; case StreamGeometryImpl streamGeometry: - { - pen = pen ?? new Pen(brush); + { + pen = pen ?? new Pen(brush); - var extractColorCheckPlatformSupported = - ExtractColorOrNullWithPlatformCheck(pen, out var lineStyle); - if (extractColorCheckPlatformSupported == null) - return; + var extractColorCheckPlatformSupported = + ExtractColorOrNullWithPlatformCheck(pen, out var lineStyle); + if (extractColorCheckPlatformSupported == null) + return; - var color = (Color)extractColorCheckPlatformSupported; + var color = (Color)extractColorCheckPlatformSupported; - lineStyle ??= LineStyle.SingleLine; + lineStyle ??= LineStyle.SingleLine; - var strokePositions = InferStrokePositions(streamGeometry); + var strokePositions = InferStrokePositions(streamGeometry); - bool hasTop = strokePositions.Contains(RectangleLinePosition.Top); - bool hasRight = strokePositions.Contains(RectangleLinePosition.Right); - bool hasBottom = strokePositions.Contains(RectangleLinePosition.Bottom); - bool hasLeft = strokePositions.Contains(RectangleLinePosition.Left); + bool hasTop = strokePositions.Contains(RectangleLinePosition.Top); + bool hasRight = strokePositions.Contains(RectangleLinePosition.Right); + bool hasBottom = strokePositions.Contains(RectangleLinePosition.Bottom); + bool hasLeft = strokePositions.Contains(RectangleLinePosition.Left); - if (lineStyle == LineStyle.Edge || lineStyle == LineStyle.EdgeWide) - { - for (int iStroke = 0; iStroke < streamGeometry.Strokes.Count; iStroke++) + if (lineStyle == LineStyle.Edge || lineStyle == LineStyle.EdgeWide) { - Line stroke = TransformLineInternal(streamGeometry.Strokes[iStroke]); - - if (stroke.Bounds.Width > 0 || stroke.Bounds.Height > 0) + for (int iStroke = 0; iStroke < streamGeometry.Strokes.Count; iStroke++) { - if (stroke.Vertical) - DrawEdgeLine(stroke, strokePositions[iStroke], lineStyle.Value, color, hasTop, - hasBottom); - else - DrawEdgeLine(stroke, strokePositions[iStroke], lineStyle.Value, color, hasLeft, - hasRight); + Line stroke = TransformLineInternal(streamGeometry.Strokes[iStroke]); + + if (stroke.Bounds.Width > 0 || stroke.Bounds.Height > 0) + { + if (stroke.Vertical) + DrawEdgeLine(stroke, strokePositions[iStroke], lineStyle.Value, color, hasTop, + hasBottom); + else + DrawEdgeLine(stroke, strokePositions[iStroke], lineStyle.Value, color, hasLeft, + hasRight); + } } } - } - else - { - Line strokeTop = null; - Line strokeLeft = null; - Line strokeRight = null; - Line strokeBottom = null; - for (int iStroke = 0; iStroke < streamGeometry.Strokes.Count; iStroke++) + else { - Line stroke = streamGeometry.Strokes[iStroke]; - RectangleLinePosition strokePosition = strokePositions[iStroke]; - if (strokePosition == RectangleLinePosition.Left) - strokeLeft = stroke; - else if (strokePosition == RectangleLinePosition.Right) - strokeRight = stroke; - else if (strokePosition == RectangleLinePosition.Top) - strokeTop = stroke; - else if (strokePosition == RectangleLinePosition.Bottom) - strokeBottom = stroke; - } + Line strokeTop = null; + Line strokeLeft = null; + Line strokeRight = null; + Line strokeBottom = null; + for (int iStroke = 0; iStroke < streamGeometry.Strokes.Count; iStroke++) + { + Line stroke = streamGeometry.Strokes[iStroke]; + RectangleLinePosition strokePosition = strokePositions[iStroke]; + if (strokePosition == RectangleLinePosition.Left) + strokeLeft = stroke; + else if (strokePosition == RectangleLinePosition.Right) + strokeRight = stroke; + else if (strokePosition == RectangleLinePosition.Top) + strokeTop = stroke; + else if (strokePosition == RectangleLinePosition.Bottom) + strokeBottom = stroke; + } - if (strokeLeft != null) - //if (strokeBottom != null) - // strokeLeft = new Line(strokeLeft.PStart, strokeBottom.PStart, strokeLeft.SourceGeometry, strokeLeft.Transform); - DrawBoxLineInternal(pen, strokeLeft, RectangleLinePosition.Left); - - if (strokeTop != null) - //if (strokeRight != null) - // strokeTop = new Line(strokeTop.PStart, strokeRight.PStart, strokeTop.SourceGeometry, strokeTop.Transform); - DrawBoxLineInternal(pen, strokeTop, RectangleLinePosition.Top); - - if (strokeRight != null) - //if (strokeBottom != null) - // strokeRight = new Line(strokeRight.PStart, strokeBottom.PEnd, strokeRight.SourceGeometry, strokeRight.Transform); - DrawBoxLineInternal(pen, strokeRight, RectangleLinePosition.Right); - - if (strokeBottom != null) - //if (strokeLeft != null) - // strokeBottom = new Line(strokeLeft.PEnd, strokeBottom.PEnd, strokeBottom.SourceGeometry, strokeBottom.Transform); - DrawBoxLineInternal(pen, strokeBottom, RectangleLinePosition.Bottom); + if (strokeLeft != null) + //if (strokeBottom != null) + // strokeLeft = new Line(strokeLeft.PStart, strokeBottom.PStart, strokeLeft.SourceGeometry, strokeLeft.Transform); + DrawBoxLineInternal(pen, strokeLeft, RectangleLinePosition.Left); + + if (strokeTop != null) + //if (strokeRight != null) + // strokeTop = new Line(strokeTop.PStart, strokeRight.PStart, strokeTop.SourceGeometry, strokeTop.Transform); + DrawBoxLineInternal(pen, strokeTop, RectangleLinePosition.Top); + + if (strokeRight != null) + //if (strokeBottom != null) + // strokeRight = new Line(strokeRight.PStart, strokeBottom.PEnd, strokeRight.SourceGeometry, strokeRight.Transform); + DrawBoxLineInternal(pen, strokeRight, RectangleLinePosition.Right); + + if (strokeBottom != null) + //if (strokeLeft != null) + // strokeBottom = new Line(strokeLeft.PEnd, strokeBottom.PEnd, strokeBottom.SourceGeometry, strokeBottom.Transform); + DrawBoxLineInternal(pen, strokeBottom, RectangleLinePosition.Bottom); + } } - } break; default: ConsoloniaPlatform.RaiseNotSupported(5); @@ -256,21 +256,22 @@ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) case VisualBrush: throw new NotImplementedException(); case ISceneBrush sceneBrush: - { - ISceneBrushContent sceneBrushContent = sceneBrush.CreateContent(); - if (sceneBrushContent != null) sceneBrushContent.Render(this, Matrix.Identity); - return; - } - case MoveConsoleCaretToPositionBrush: - { - Point head = r.TopLeft.Transform(Transform); - CurrentClip.ExecuteWithClipping(head, - () => - { - _pixelBuffer.Set((PixelBufferCoordinate)head, pixel => pixel.Blend(new Pixel(true))); - }); - return; - } + { + ISceneBrushContent sceneBrushContent = sceneBrush.CreateContent(); + if (sceneBrushContent != null) sceneBrushContent.Render(this, Matrix.Identity); + return; + } + case MoveConsoleCaretToPositionBrush moveBrush: + { + Point head = r.TopLeft.Transform(Transform); + _pixelBuffer.CursorStyle = moveBrush.Style; + CurrentClip.ExecuteWithClipping(head, + () => + { + _pixelBuffer.Set((PixelBufferCoordinate)head, pixel => pixel.Blend(new Pixel(true))); + }); + return; + } } FillRectangleWithBrush(brush, pen, r); @@ -468,8 +469,9 @@ private void DrawLineInternal(IPen pen, Line line) return; } - if (pen.Brush is MoveConsoleCaretToPositionBrush) + if (pen.Brush is MoveConsoleCaretToPositionBrush moveBrush) { + _pixelBuffer.CursorStyle = moveBrush.Style; Point head = line.PStart.Transform(Transform); CurrentClip.ExecuteWithClipping(head, () => { _pixelBuffer.Set((PixelBufferCoordinate)head, pixel => pixel.Blend(new Pixel(true))); }); @@ -521,19 +523,19 @@ private void FillRectangleWithBrush(IBrush brush, IPen pen, Rect r) double width = r2.Width + (pen?.Thickness ?? 0); double height = r2.Height + (pen?.Thickness ?? 0); for (int x = 0; x < width; x++) - for (int y = 0; y < height; y++) - { - int px = (int)(r2.TopLeft.X + x); - int py = (int)(r2.TopLeft.Y + y); - - ConsoleBrush backgroundBrush = ConsoleBrush.FromPosition(brush, x, y, (int)width, (int)height); - CurrentClip.ExecuteWithClipping(new Point(px, py), () => + for (int y = 0; y < height; y++) { - _pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py), - pixel => pixel.Blend(new Pixel(new PixelBackground(backgroundBrush.Mode, - backgroundBrush.Color)))); - }); - } + int px = (int)(r2.TopLeft.X + x); + int py = (int)(r2.TopLeft.Y + y); + + ConsoleBrush backgroundBrush = ConsoleBrush.FromPosition(brush, x, y, (int)width, (int)height); + CurrentClip.ExecuteWithClipping(new Point(px, py), () => + { + _pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py), + pixel => pixel.Blend(new Pixel(new PixelBackground(backgroundBrush.Mode, + backgroundBrush.Color)))); + }); + } } /// @@ -754,21 +756,21 @@ private void DrawLineSymbolAndMoveHead(ref Point head, bool isVertical, ISymbol switch (glyph) { case "\t": - { - const int tabSize = 8; - var consolePixel = new Pixel(new SimpleSymbol(' '), foregroundColor); - for (int j = 0; j < tabSize; j++) { - Point newCharacterPoint = characterPoint.WithX(characterPoint.X + j); - CurrentClip.ExecuteWithClipping(newCharacterPoint, () => + const int tabSize = 8; + var consolePixel = new Pixel(new SimpleSymbol(' '), foregroundColor); + for (int j = 0; j < tabSize; j++) { - _pixelBuffer.Set((PixelBufferCoordinate)newCharacterPoint, - oldPixel => oldPixel.Blend(consolePixel)); - }); - } + Point newCharacterPoint = characterPoint.WithX(characterPoint.X + j); + CurrentClip.ExecuteWithClipping(newCharacterPoint, () => + { + _pixelBuffer.Set((PixelBufferCoordinate)newCharacterPoint, + oldPixel => oldPixel.Blend(consolePixel)); + }); + } - currentXPosition += tabSize - 1; - } + currentXPosition += tabSize - 1; + } break; case "\r": case "\f": @@ -777,66 +779,66 @@ private void DrawLineSymbolAndMoveHead(ref Point head, bool isVertical, ISymbol currentYPosition++; break; default: - { - var symbol = new SimpleSymbol(glyph); - // if we are attempting to draw a wide glyph we need to make sure that the clipping point - // is for the last physical char. Aka a double char should be clipped if it's second rendered - // char would break the boundary of the clip. - // var clippingPoint = new Point(characterPoint.X + symbol.Width - 1, characterPoint.Y); - var newPixel = new Pixel(symbol, foregroundColor, typeface.Style, typeface.Weight); - CurrentClip.ExecuteWithClipping(characterPoint, () => { - _pixelBuffer.Set((PixelBufferCoordinate)characterPoint, - oldPixel => - { - if (oldPixel.Width == 0) - { - // if the oldPixel was empty, we need to set the previous pixel to space - double targetX = characterPoint.X - 1; - if (targetX >= 0) - _pixelBuffer.Set( - (PixelBufferCoordinate)new Point(targetX, characterPoint.Y), - oldPixel2 => - new Pixel( - new PixelForeground(new SimpleSymbol(' '), Colors.Transparent), - oldPixel2.Background)); - } - else if (oldPixel.Width > 1) + var symbol = new SimpleSymbol(glyph); + // if we are attempting to draw a wide glyph we need to make sure that the clipping point + // is for the last physical char. Aka a double char should be clipped if it's second rendered + // char would break the boundary of the clip. + // var clippingPoint = new Point(characterPoint.X + symbol.Width - 1, characterPoint.Y); + var newPixel = new Pixel(symbol, foregroundColor, typeface.Style, typeface.Weight); + CurrentClip.ExecuteWithClipping(characterPoint, () => + { + _pixelBuffer.Set((PixelBufferCoordinate)characterPoint, + oldPixel => { - // if oldPixel was wide we need to reset overlapped symbols from empty to space - for (ushort i = 1; i < oldPixel.Width; i++) + if (oldPixel.Width == 0) { - double targetX = characterPoint.X + i; - if (targetX < _pixelBuffer.Size.Width) + // if the oldPixel was empty, we need to set the previous pixel to space + double targetX = characterPoint.X - 1; + if (targetX >= 0) _pixelBuffer.Set( (PixelBufferCoordinate)new Point(targetX, characterPoint.Y), oldPixel2 => new Pixel( - new PixelForeground(new SimpleSymbol(' '), - Colors.Transparent), oldPixel2.Background)); + new PixelForeground(new SimpleSymbol(' '), Colors.Transparent), + oldPixel2.Background)); } - } - - // if the pixel was a wide character, we need to set the overlapped pixels to empty pixels. - if (newPixel.Width > 1) - for (int i = 1; i < symbol.Width; i++) + else if (oldPixel.Width > 1) { - double targetX = characterPoint.X + i; - if (targetX < _pixelBuffer.Size.Width) - _pixelBuffer.Set( - (PixelBufferCoordinate)new Point(targetX, characterPoint.Y), - oldPixel2 => - new Pixel( - new PixelForeground(new SimpleSymbol(), Colors.Transparent), - oldPixel2.Background)); + // if oldPixel was wide we need to reset overlapped symbols from empty to space + for (ushort i = 1; i < oldPixel.Width; i++) + { + double targetX = characterPoint.X + i; + if (targetX < _pixelBuffer.Size.Width) + _pixelBuffer.Set( + (PixelBufferCoordinate)new Point(targetX, characterPoint.Y), + oldPixel2 => + new Pixel( + new PixelForeground(new SimpleSymbol(' '), + Colors.Transparent), oldPixel2.Background)); + } } - return oldPixel.Blend(newPixel); - }); - }); + // if the pixel was a wide character, we need to set the overlapped pixels to empty pixels. + if (newPixel.Width > 1) + for (int i = 1; i < symbol.Width; i++) + { + double targetX = characterPoint.X + i; + if (targetX < _pixelBuffer.Size.Width) + _pixelBuffer.Set( + (PixelBufferCoordinate)new Point(targetX, characterPoint.Y), + oldPixel2 => + new Pixel( + new PixelForeground(new SimpleSymbol(), Colors.Transparent), + oldPixel2.Background)); + } + + return oldPixel.Blend(newPixel); + }); + }); - currentXPosition += symbol.Width; - } + currentXPosition += symbol.Width; + } break; } } diff --git a/src/Consolonia.Core/Drawing/MoveConsoleCaretToPositionBrush.cs b/src/Consolonia.Core/Drawing/MoveConsoleCaretToPositionBrush.cs index 0f6f89e7..375fc360 100644 --- a/src/Consolonia.Core/Drawing/MoveConsoleCaretToPositionBrush.cs +++ b/src/Consolonia.Core/Drawing/MoveConsoleCaretToPositionBrush.cs @@ -9,5 +9,10 @@ public class MoveConsoleCaretToPositionBrush : IImmutableBrush public double Opacity => 1; public ITransform Transform => null; public RelativePoint TransformOrigin => RelativePoint.TopLeft; + + /// + /// style of curosr + /// + public CursorStyle Style { get; set; } = CursorStyle.BlinkingBar; } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs index 8ffb2aaa..418dc2ec 100644 --- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs +++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs @@ -32,6 +32,8 @@ public PixelBuffer(ushort width, ushort height) public ushort Width { get; } public ushort Height { get; } + public CursorStyle CursorStyle { get; set; } = CursorStyle.BlinkingBar; + // ReSharper disable once UnusedMember.Global [JsonIgnore] public Pixel this[int i] diff --git a/src/Consolonia.Core/Drawing/RenderTarget.cs b/src/Consolonia.Core/Drawing/RenderTarget.cs index 35665b86..02e016e6 100644 --- a/src/Consolonia.Core/Drawing/RenderTarget.cs +++ b/src/Consolonia.Core/Drawing/RenderTarget.cs @@ -10,6 +10,8 @@ using Avalonia.Platform; using Consolonia.Core.Drawing.PixelBufferImplementation; using Consolonia.Core.Infrastructure; +using Consolonia.Core.Text; +using Newtonsoft.Json.Linq; namespace Consolonia.Core.Drawing { @@ -141,11 +143,23 @@ private void RenderToDevice() if (caretPosition != null) { _console.SetCaretPosition((PixelBufferCoordinate)caretPosition); + _console.WriteText(pixelBuffer.CursorStyle switch + { + CursorStyle.BlinkingBar => Esc.BlinkingBarCursor, + CursorStyle.SteadyBar => Esc.SteadyBarCursor, + CursorStyle.BlinkingBlock => Esc.BlinkingBlockCursor, + CursorStyle.SteadyBlock => Esc.SteadyBlockCursor, + CursorStyle.BlinkingUnderline => Esc.BlinkingUnderlineCursor, + CursorStyle.SteadyUnderline => Esc.SteadyUnderlineCursor, + _ => throw new ArgumentOutOfRangeException() + }); + _console.WriteText(Esc.ShowCursor); _console.CaretVisible = true; } else { _console.CaretVisible = false; + _console.WriteText(Esc.HideCursor); } } diff --git a/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs b/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs index fb59bfab..3415ba94 100644 --- a/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs +++ b/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs @@ -39,7 +39,6 @@ public bool CaretVisible set { if (_caretVisible == value) return; - WriteText(value ? Esc.ShowCursor : Esc.HideCursor); _caretVisible = value; } #pragma warning restore CA1303 // Do not pass literals as localized parameters diff --git a/src/Consolonia.Core/Text/Esc.cs b/src/Consolonia.Core/Text/Esc.cs index 9eb86541..4d321f0c 100644 --- a/src/Consolonia.Core/Text/Esc.cs +++ b/src/Consolonia.Core/Text/Esc.cs @@ -31,6 +31,14 @@ internal static class Esc public const string HideCursor = "\u001b[?25l"; public const string ShowCursor = "\u001b[?25h"; + // cursor shape + public const string BlinkingBlockCursor = "\u001b[1 q"; + public const string SteadyBlockCursor = "\u001b[2 q"; + public const string BlinkingUnderlineCursor = "\u001b[3 q"; + public const string SteadyUnderlineCursor = "\u001b[4 q"; + public const string BlinkingBarCursor = "\u001b[5 q"; + public const string SteadyBarCursor = "\u001b[6 q"; + // move cursor public static string MoveCursorUp(int n) {