mirror of
https://github.com/spectreconsole/spectre.console.git
synced 2026-02-10 04:13:32 +01:00
Add half-block support to Canvas
This commit is contained in:
committed by
Patrik Svensson
parent
0608bac360
commit
fa071a9368
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
||||||
@@ -33,6 +34,7 @@ public sealed class CanvasImage : Renderable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the render width of the canvas.
|
/// Gets or sets the render width of the canvas.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Not used anymore. Will be removed in future update.")]
|
||||||
public int PixelWidth { get; set; } = 2;
|
public int PixelWidth { get; set; } = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -73,27 +75,23 @@ public sealed class CanvasImage : Renderable
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override Measurement Measure(RenderOptions options, int maxWidth)
|
protected override Measurement Measure(RenderOptions options, int maxWidth)
|
||||||
{
|
{
|
||||||
if (PixelWidth < 0)
|
var pixelWidth = options.Unicode ? 1 : 2;
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Pixel width must be greater than zero.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var width = MaxWidth ?? Width;
|
var width = MaxWidth ?? Width;
|
||||||
if (maxWidth < width * PixelWidth)
|
if (maxWidth < width * pixelWidth)
|
||||||
{
|
{
|
||||||
return new Measurement(maxWidth, maxWidth);
|
return new Measurement(maxWidth, maxWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Measurement(width * PixelWidth, width * PixelWidth);
|
return new Measurement(width * pixelWidth, width * pixelWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
|
protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
|
||||||
{
|
{
|
||||||
var image = Image;
|
var image = Image;
|
||||||
|
|
||||||
var width = Width;
|
var width = Width;
|
||||||
var height = Height;
|
var height = Height;
|
||||||
|
var pixelWidth = options.Unicode ? 1 : 2;
|
||||||
|
|
||||||
// Got a max width?
|
// Got a max width?
|
||||||
if (MaxWidth != null)
|
if (MaxWidth != null)
|
||||||
@@ -103,10 +101,10 @@ public sealed class CanvasImage : Renderable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exceed the max width when we take pixel width into account?
|
// Exceed the max width when we take pixel width into account?
|
||||||
if (width * PixelWidth > maxWidth)
|
if (width * pixelWidth > maxWidth)
|
||||||
{
|
{
|
||||||
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
|
height = (int)(height * (maxWidth / (float)(width * pixelWidth)));
|
||||||
width = maxWidth / PixelWidth;
|
width = maxWidth / pixelWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to rescale the pixel buffer?
|
// Need to rescale the pixel buffer?
|
||||||
@@ -120,7 +118,6 @@ public sealed class CanvasImage : Renderable
|
|||||||
var canvas = new Canvas(width, height)
|
var canvas = new Canvas(width, height)
|
||||||
{
|
{
|
||||||
MaxWidth = MaxWidth,
|
MaxWidth = MaxWidth,
|
||||||
PixelWidth = PixelWidth,
|
|
||||||
Scale = false,
|
Scale = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public static class CanvasImageExtensions
|
|||||||
/// <param name="image">The canvas image.</param>
|
/// <param name="image">The canvas image.</param>
|
||||||
/// <param name="width">The pixel width.</param>
|
/// <param name="width">The pixel width.</param>
|
||||||
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
[Obsolete("Not used anymore. Will be removed in future update.")]
|
||||||
public static CanvasImage PixelWidth(this CanvasImage image, int width)
|
public static CanvasImage PixelWidth(this CanvasImage image, int width)
|
||||||
{
|
{
|
||||||
if (image is null)
|
if (image is null)
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ public static partial class TestConsoleExtensions
|
|||||||
return console;
|
return console;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets whether or not Unicode is supported.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="console">The console.</param>
|
||||||
|
/// <param name="enable">Whether or not Unicode is supported.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static TestConsole SupportsUnicode(this TestConsole console, bool enable)
|
||||||
|
{
|
||||||
|
console.Profile.Capabilities.Unicode = enable;
|
||||||
|
return console;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Makes the console interactive.
|
/// Makes the console interactive.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[91m▀[0m [32m▀[0m
|
||||||
|
|
||||||
|
[94m▀[0m [93m▀[0m
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[96m▀[0m
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[96m▀[0m
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
┌────┐
|
||||||
|
│ [96m▀[0m[90m▄[0m │
|
||||||
|
└────┘
|
||||||
@@ -28,13 +28,16 @@ public class CanvasTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
[Expectation("Render")]
|
[Expectation("Render")]
|
||||||
public async Task Should_Render_Canvas_Correctly()
|
public async Task Should_Render_Canvas_Correctly(bool supportsUnicode)
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new TestConsole()
|
var console = new TestConsole()
|
||||||
.Colors(ColorSystem.Standard)
|
.Colors(ColorSystem.Standard)
|
||||||
|
.SupportsUnicode(supportsUnicode)
|
||||||
.EmitAnsiSequences();
|
.EmitAnsiSequences();
|
||||||
|
|
||||||
var canvas = new Canvas(width: 5, height: 5);
|
var canvas = new Canvas(width: 5, height: 5);
|
||||||
@@ -47,16 +50,21 @@ public class CanvasTests
|
|||||||
console.Write(canvas);
|
console.Write(canvas);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
await Verifier.Verify(console.Output);
|
await Verifier
|
||||||
|
.Verify(console.Output)
|
||||||
|
.UseMethodName(supportsUnicode ? "Unicode" : "NonUnicode");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
[Expectation("Render_Nested")]
|
[Expectation("Render_Nested")]
|
||||||
public async Task Simple_Measure()
|
public async Task Simple_Measure(bool supportsUnicode)
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new TestConsole()
|
var console = new TestConsole()
|
||||||
.Colors(ColorSystem.Standard)
|
.Colors(ColorSystem.Standard)
|
||||||
|
.SupportsUnicode(supportsUnicode)
|
||||||
.EmitAnsiSequences();
|
.EmitAnsiSequences();
|
||||||
|
|
||||||
var panel = new Panel(new Canvas(width: 2, height: 2)
|
var panel = new Panel(new Canvas(width: 2, height: 2)
|
||||||
@@ -67,17 +75,22 @@ public class CanvasTests
|
|||||||
console.Write(panel);
|
console.Write(panel);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
await Verifier.Verify(console.Output);
|
await Verifier
|
||||||
|
.Verify(console.Output)
|
||||||
|
.UseMethodName(supportsUnicode ? "Unicode" : "NonUnicode");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
[Expectation("Render_NarrowTerminal")]
|
[Expectation("Render_NarrowTerminal")]
|
||||||
public async Task Should_Scale_Down_Canvas_Is_Bigger_Than_Terminal()
|
public async Task Should_Scale_Down_Canvas_Is_Bigger_Than_Terminal(bool supportsUnicode)
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new TestConsole()
|
var console = new TestConsole()
|
||||||
.Width(10)
|
.Width(10)
|
||||||
.Colors(ColorSystem.Standard)
|
.Colors(ColorSystem.Standard)
|
||||||
|
.SupportsUnicode(supportsUnicode)
|
||||||
.EmitAnsiSequences();
|
.EmitAnsiSequences();
|
||||||
|
|
||||||
var canvas = new Canvas(width: 20, height: 10);
|
var canvas = new Canvas(width: 20, height: 10);
|
||||||
@@ -88,19 +101,27 @@ public class CanvasTests
|
|||||||
console.Write(canvas);
|
console.Write(canvas);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
await Verifier.Verify(console.Output);
|
await Verifier
|
||||||
|
.Verify(console.Output)
|
||||||
|
.UseMethodName(supportsUnicode ? "Unicode" : "NonUnicode");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
[Expectation("Render_MaxWidth")]
|
[Expectation("Render_MaxWidth")]
|
||||||
public async Task Should_Scale_Down_Canvas_If_MaxWidth_Is_Set()
|
public async Task Should_Scale_Down_Canvas_If_MaxWidth_Is_Set(bool supportsUnicode)
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new TestConsole()
|
var console = new TestConsole()
|
||||||
.Colors(ColorSystem.Standard)
|
.Colors(ColorSystem.Standard)
|
||||||
|
.SupportsUnicode(supportsUnicode)
|
||||||
.EmitAnsiSequences();
|
.EmitAnsiSequences();
|
||||||
|
|
||||||
var canvas = new Canvas(width: 20, height: 10) { MaxWidth = 10 };
|
var canvas = new Canvas(width: 20, height: 10)
|
||||||
|
{
|
||||||
|
MaxWidth = 10
|
||||||
|
};
|
||||||
canvas.SetPixel(0, 0, Color.Aqua);
|
canvas.SetPixel(0, 0, Color.Aqua);
|
||||||
canvas.SetPixel(19, 9, Color.Aqua);
|
canvas.SetPixel(19, 9, Color.Aqua);
|
||||||
|
|
||||||
@@ -108,16 +129,22 @@ public class CanvasTests
|
|||||||
console.Write(canvas);
|
console.Write(canvas);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
await Verifier.Verify(console.Output);
|
await Verifier
|
||||||
|
.Verify(console.Output)
|
||||||
|
.UseMethodName(supportsUnicode ? "Unicode" : "NonUnicode");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void Should_Not_Render_Canvas_If_Canvas_Cannot_Be_Scaled_Down()
|
[InlineData(true, 5)]
|
||||||
|
[InlineData(false, 10)]
|
||||||
|
public void Should_Not_Render_Canvas_If_Canvas_Cannot_Be_Scaled_Down(
|
||||||
|
bool supportsUnicode, int consoleWidth)
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new TestConsole()
|
var console = new TestConsole()
|
||||||
.Width(10)
|
.Width(consoleWidth)
|
||||||
.Colors(ColorSystem.Standard)
|
.Colors(ColorSystem.Standard)
|
||||||
|
.SupportsUnicode(supportsUnicode)
|
||||||
.EmitAnsiSequences();
|
.EmitAnsiSequences();
|
||||||
|
|
||||||
var canvas = new Canvas(width: 20, height: 2);
|
var canvas = new Canvas(width: 20, height: 2);
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ public sealed class Canvas : Renderable
|
|||||||
{
|
{
|
||||||
private readonly Color?[,] _pixels;
|
private readonly Color?[,] _pixels;
|
||||||
|
|
||||||
|
private const string Transparent = " ";
|
||||||
|
private const string DoubleTransparent = " ";
|
||||||
|
private const string UpperHalfBlock = "▀";
|
||||||
|
private const string LowerHalfBlock = "▄";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the width of the canvas.
|
/// Gets the width of the canvas.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -31,6 +36,7 @@ public sealed class Canvas : Renderable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the pixel width.
|
/// Gets or sets the pixel width.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Not used anymore. Will be removed in future update.")]
|
||||||
public int PixelWidth { get; set; } = 2;
|
public int PixelWidth { get; set; } = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -72,31 +78,25 @@ public sealed class Canvas : Renderable
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override Measurement Measure(RenderOptions options, int maxWidth)
|
protected override Measurement Measure(RenderOptions options, int maxWidth)
|
||||||
{
|
{
|
||||||
if (PixelWidth < 0)
|
var pixelWidth = options.Unicode ? 1 : 2;
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Pixel width must be greater than zero.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var width = MaxWidth ?? Width;
|
var width = MaxWidth ?? Width;
|
||||||
|
|
||||||
if (maxWidth < width * PixelWidth)
|
return maxWidth < width * pixelWidth
|
||||||
{
|
? new Measurement(maxWidth, maxWidth)
|
||||||
return new Measurement(maxWidth, maxWidth);
|
: new Measurement(width * pixelWidth, width * pixelWidth);
|
||||||
}
|
|
||||||
|
|
||||||
return new Measurement(width * PixelWidth, width * PixelWidth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
|
protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
|
||||||
{
|
{
|
||||||
if (PixelWidth < 0)
|
return options.Unicode
|
||||||
{
|
? RenderUnicode(maxWidth)
|
||||||
throw new InvalidOperationException("Pixel width must be greater than zero.");
|
: RenderNonUnicode(maxWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Segment> RenderUnicode(int maxWidth)
|
||||||
|
{
|
||||||
var pixels = _pixels;
|
var pixels = _pixels;
|
||||||
var pixel = new string(' ', PixelWidth);
|
|
||||||
var width = Width;
|
var width = Width;
|
||||||
var height = Height;
|
var height = Height;
|
||||||
|
|
||||||
@@ -108,14 +108,86 @@ public sealed class Canvas : Renderable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exceed the max width when we take pixel width into account?
|
// Exceed the max width when we take pixel width into account?
|
||||||
if (width * PixelWidth > maxWidth)
|
if (width > maxWidth)
|
||||||
{
|
{
|
||||||
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
|
height = (int)(height * (maxWidth / (float)(width)));
|
||||||
width = maxWidth / PixelWidth;
|
width = maxWidth;
|
||||||
|
|
||||||
// If it's not possible to scale the canvas sufficiently, it's too small to render.
|
|
||||||
if (height == 0)
|
if (height == 0)
|
||||||
{
|
{
|
||||||
|
// If it's not possible to scale the canvas sufficiently, it's too small to render.
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to rescale the pixel buffer?
|
||||||
|
if (Scale && (width != Width || height != Height))
|
||||||
|
{
|
||||||
|
pixels = ScaleDown(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var y = 0; y < height; y += 2)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
var upper = pixels[x, y];
|
||||||
|
var lower = y < height - 1 ? pixels[x, y + 1] : null;
|
||||||
|
|
||||||
|
if (upper == null && lower == null)
|
||||||
|
{
|
||||||
|
// None visible
|
||||||
|
yield return new Segment(Transparent);
|
||||||
|
}
|
||||||
|
else if (upper != null && lower != null)
|
||||||
|
{
|
||||||
|
// Both pixels visible
|
||||||
|
yield return new Segment(
|
||||||
|
UpperHalfBlock, new Style(
|
||||||
|
foreground: upper,
|
||||||
|
background: lower));
|
||||||
|
}
|
||||||
|
else if (upper != null)
|
||||||
|
{
|
||||||
|
// Upper visible
|
||||||
|
yield return new Segment(
|
||||||
|
UpperHalfBlock, new Style(
|
||||||
|
foreground: upper));
|
||||||
|
}
|
||||||
|
else if (lower != null)
|
||||||
|
{
|
||||||
|
// Lower visible
|
||||||
|
yield return new Segment(
|
||||||
|
LowerHalfBlock, new Style(
|
||||||
|
foreground: lower));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return Segment.LineBreak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Segment> RenderNonUnicode(int maxWidth)
|
||||||
|
{
|
||||||
|
var pixels = _pixels;
|
||||||
|
var width = Width;
|
||||||
|
var height = Height;
|
||||||
|
|
||||||
|
// Got a max width?
|
||||||
|
if (MaxWidth != null)
|
||||||
|
{
|
||||||
|
height = (int)(height * ((float)MaxWidth.Value) / Width);
|
||||||
|
width = MaxWidth.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exceed the max width when we take pixel width into account?
|
||||||
|
if (width * 2 > maxWidth)
|
||||||
|
{
|
||||||
|
height = (int)(height * (maxWidth / (float)(width * 2)));
|
||||||
|
width = maxWidth / 2;
|
||||||
|
|
||||||
|
if (height == 0)
|
||||||
|
{
|
||||||
|
// If it's not possible to scale the canvas sufficiently, it's too small to render.
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,11 +205,11 @@ public sealed class Canvas : Renderable
|
|||||||
var color = pixels[x, y];
|
var color = pixels[x, y];
|
||||||
if (color != null)
|
if (color != null)
|
||||||
{
|
{
|
||||||
yield return new Segment(pixel, new Style(background: color));
|
yield return new Segment(DoubleTransparent, new Style(background: color));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
yield return new Segment(pixel);
|
yield return new Segment(DoubleTransparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user