Simple component system for TUI apps, some basic components
This commit is contained in:
parent
b73bcce5d5
commit
11a528766c
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="vcsConfiguration" value="1" />
|
||||
<option name="vcsConfiguration" value="2" />
|
||||
</component>
|
||||
</project>
|
@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Misc", "Misc\Misc.csproj",
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "W32.Test", "W32.Test\W32.Test.csproj", "{6121A6D3-7C73-4EDE-A5A9-09DC52150824}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Commandline", "Commandline\Commandline.csproj", "{08C16A0B-A69D-4229-BC6D-F6949CA7BE76}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLITest", "CLITest\CLITest.csproj", "{3FAB0713-3021-4C6A-BF4A-ABBD542634A6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -27,6 +31,14 @@ Global
|
||||
{6121A6D3-7C73-4EDE-A5A9-09DC52150824}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6121A6D3-7C73-4EDE-A5A9-09DC52150824}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6121A6D3-7C73-4EDE-A5A9-09DC52150824}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08C16A0B-A69D-4229-BC6D-F6949CA7BE76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08C16A0B-A69D-4229-BC6D-F6949CA7BE76}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08C16A0B-A69D-4229-BC6D-F6949CA7BE76}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08C16A0B-A69D-4229-BC6D-F6949CA7BE76}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3FAB0713-3021-4C6A-BF4A-ABBD542634A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3FAB0713-3021-4C6A-BF4A-ABBD542634A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3FAB0713-3021-4C6A-BF4A-ABBD542634A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3FAB0713-3021-4C6A-BF4A-ABBD542634A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
13
CLITest/CLITest.csproj
Normal file
13
CLITest/CLITest.csproj
Normal file
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<LangVersion>8</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Commandline\Commandline.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
79
CLITest/Program.cs
Normal file
79
CLITest/Program.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using CC_Functions.Commandline.TUI;
|
||||
|
||||
namespace CLITest
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.BackgroundColor = ConsoleColor.Black;
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.Clear();
|
||||
Screen screen = new Screen(40, 20);
|
||||
Button btn1 = new Button("Test")
|
||||
{
|
||||
Point = new Point(2, 0)
|
||||
};
|
||||
screen.Controls.Add(btn1);
|
||||
btn1.Click += (screen1, eventArgs) =>
|
||||
{
|
||||
DiffDraw.FullDraw(true);
|
||||
};
|
||||
Label lab1 = new Label("Meem")
|
||||
{
|
||||
Point = new Point(2, 1)
|
||||
};
|
||||
screen.Controls.Add(lab1);
|
||||
screen.Controls.Add(new Label("Saas\nSoos")
|
||||
{
|
||||
Point = new Point(2, 2)
|
||||
});
|
||||
Button btn2 = new Button("X");
|
||||
screen.Controls.Add(btn2);
|
||||
|
||||
CheckBox box = new CheckBox("Are u gae?")
|
||||
{
|
||||
Point = new Point(2, 3)
|
||||
};
|
||||
screen.Controls.Add(box);
|
||||
box.CheckedChanged += (screen1, eventArgs) =>
|
||||
{
|
||||
lab1.Content = box.Checked ? "Sas" : "Meem";
|
||||
};
|
||||
|
||||
TextBox tbox = new TextBox("Hello\nWorld1\n\nHow are u?")
|
||||
{
|
||||
Size = new Size(20, 10),
|
||||
Point = new Point(0, 6)
|
||||
};
|
||||
screen.Controls.Add(tbox);
|
||||
|
||||
Slider slider = new Slider
|
||||
{
|
||||
Point = new Point(2, 4),
|
||||
Size = new Size(8, 2),
|
||||
MaxValue = 75,
|
||||
StepSize = 14,
|
||||
MinValue = -3,
|
||||
Value = 7
|
||||
};
|
||||
screen.Controls.Add(slider);
|
||||
|
||||
bool visible = true;
|
||||
btn2.Click += (screen1, eventArgs) => visible = false;
|
||||
screen.Close += (screen1, eventArgs) => visible = false;
|
||||
screen.Render();
|
||||
while (visible)
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
screen.ReadInput();
|
||||
}
|
||||
Console.ResetColor();
|
||||
Console.Clear();
|
||||
Console.WriteLine("Bye");
|
||||
}
|
||||
}
|
||||
}
|
36
Commandline/Commandline.csproj
Normal file
36
Commandline/Commandline.csproj
Normal file
@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>CC_Functions.Commandline</RootNamespace>
|
||||
<AssemblyName>CC_Functions.Commandline</AssemblyName>
|
||||
<LangVersion>8</LangVersion>
|
||||
<Deterministic>false</Deterministic>
|
||||
<PackageId>CC-Functions.Commandline</PackageId>
|
||||
<Title>CC-Functions.Commandline</Title>
|
||||
<Authors>CC24</Authors>
|
||||
<Description>Random pieces of code used across my projects. CLI/TUI extensions</Description>
|
||||
<Copyright>Copyright 2020</Copyright>
|
||||
<PackageProjectUrl>https://github.com/JFronny/CC-Functions</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/JFronny/CC-Functions.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<AssemblyVersion>1.1.*</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<VersionSuffix>0.0</VersionSuffix>
|
||||
<PackageVersion>1.1.$(VersionSuffix)</PackageVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DocumentationFile>bin\Debug\Commandline.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\Release\Commandline.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Misc\Misc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
34
Commandline/TUI/Button.cs
Normal file
34
Commandline/TUI/Button.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Drawing;
|
||||
using CC_Functions.Misc;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class Button : Control
|
||||
{
|
||||
public string Content;
|
||||
public Button(string content)
|
||||
{
|
||||
Content = content;
|
||||
char[,] tmp = Content.ToNDArray2D();
|
||||
Size = new Size(tmp.GetLength(0), tmp.GetLength(1));
|
||||
}
|
||||
|
||||
public override Pixel[,] Render()
|
||||
{
|
||||
char[,] inp = Content.ToNDArray2D();
|
||||
inp = inp.Resize(Size.Width, Size.Height);
|
||||
Pixel[,] output = new Pixel[Size.Width, Size.Height];
|
||||
for (int x = 0; x < Size.Width; x++)
|
||||
for (int y = 0; y < Size.Height; y++)
|
||||
output[x, y] = new Pixel(Selected ? ForeColor : BackColor, Selected ? BackColor : ForeColor, inp[x, y]);
|
||||
return output;
|
||||
}
|
||||
|
||||
protected override void Resize(int width, int height)
|
||||
{
|
||||
//ignored for [Render]s sake, do not use
|
||||
}
|
||||
|
||||
public override bool Selectable { get; } = true;
|
||||
}
|
||||
}
|
61
Commandline/TUI/CheckBox.cs
Normal file
61
Commandline/TUI/CheckBox.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using CC_Functions.Misc;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class CheckBox : Control
|
||||
{
|
||||
public string Content;
|
||||
public bool Checked = false;
|
||||
public CheckBox(string content)
|
||||
{
|
||||
Content = content;
|
||||
Input += (screen, args) =>
|
||||
{
|
||||
switch (args.Info.Key)
|
||||
{
|
||||
case ConsoleKey.LeftArrow:
|
||||
case ConsoleKey.RightArrow:
|
||||
case ConsoleKey.Spacebar:
|
||||
Checked = !Checked;
|
||||
CheckedChanged?.Invoke(screen, args);
|
||||
break;
|
||||
}
|
||||
};
|
||||
Click += (screen, args) =>
|
||||
{
|
||||
Checked = !Checked;
|
||||
CheckedChanged?.Invoke(screen, args);
|
||||
};
|
||||
}
|
||||
|
||||
public override Pixel[,] Render()
|
||||
{
|
||||
char[,] inp1 = Content.ToNDArray2D();
|
||||
char[,] inp = new char[inp1.GetLength(0), inp1.GetLength(1) + 4];
|
||||
inp.Populate(' ');
|
||||
inp1.CopyTo(inp, new Point(4, 0));
|
||||
inp[0, 0] = '[';
|
||||
inp[0, 1] = Checked ? 'X' : SpecialChars.empty;
|
||||
inp[0, 2] = ']';
|
||||
int w = inp.GetLength(0);
|
||||
int h = inp.GetLength(1);
|
||||
Pixel[,] output = new Pixel[w, h];
|
||||
for (int x = 0; x < w; x++)
|
||||
for (int y = 0; y < h; y++)
|
||||
output[x, y] = new Pixel(Selected ? ForeColor : BackColor, Selected ? BackColor : ForeColor, inp[x, y]);
|
||||
Size = new Size(w, h);
|
||||
return output;
|
||||
}
|
||||
|
||||
protected override void Resize(int width, int height)
|
||||
{
|
||||
//ignored for [Render]s sake, do not use
|
||||
}
|
||||
|
||||
public override bool Selectable { get; } = true;
|
||||
public delegate void OnCheckedChanged(Screen screen, EventArgs e);
|
||||
public event OnClick CheckedChanged;
|
||||
}
|
||||
}
|
81
Commandline/TUI/Control.cs
Normal file
81
Commandline/TUI/Control.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public abstract class Control
|
||||
{
|
||||
private Point _point;
|
||||
private Size _size;
|
||||
public abstract Pixel[,] Render();
|
||||
protected abstract void Resize(int width, int height);
|
||||
private void Resize(Size size) => Resize(size.Width, size.Height);
|
||||
|
||||
public Size Size
|
||||
{
|
||||
set
|
||||
{
|
||||
_size = value;
|
||||
Resize(value);
|
||||
}
|
||||
get => _size;
|
||||
}
|
||||
|
||||
public Point Point
|
||||
{
|
||||
get => _point;
|
||||
set => _point = value;
|
||||
}
|
||||
|
||||
public ConsoleColor ForeColor { get; set; } = Console.ForegroundColor;
|
||||
public ConsoleColor BackColor { get; set; } = Console.BackgroundColor;
|
||||
public abstract bool Selectable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when [enter] is pressed while the control is selected
|
||||
/// </summary>
|
||||
/// <param name="screen">An instance of the calling screen</param>
|
||||
/// <param name="e">Args</param>
|
||||
public delegate void OnClick(Screen screen, EventArgs e);
|
||||
/// <summary>
|
||||
/// Called when [enter] is pressed while the control is selected
|
||||
/// </summary>
|
||||
public event OnClick Click;
|
||||
/// <summary>
|
||||
/// Called when the control is selected and unknown input is given
|
||||
/// </summary>
|
||||
/// <param name="screen">An instance of the calling screen</param>
|
||||
/// <param name="e">Args</param>
|
||||
public delegate void OnInput(Screen screen, InputEventArgs e);
|
||||
/// <summary>
|
||||
/// Called when the control is selected and unknown input is given
|
||||
/// </summary>
|
||||
public event OnInput Input;
|
||||
/// <summary>
|
||||
/// Whether the object is selected. Used internally and for drawing
|
||||
/// </summary>
|
||||
public bool Selected { get; internal set; } = false;
|
||||
/// <summary>
|
||||
/// Invokes click events
|
||||
/// </summary>
|
||||
/// <param name="screen">The calling screen</param>
|
||||
internal void InvokeClick(Screen screen)
|
||||
{
|
||||
Click?.Invoke(screen, new EventArgs());
|
||||
}
|
||||
/// <summary>
|
||||
/// Invokes input events
|
||||
/// </summary>
|
||||
/// <param name="screen">The calling screen</param>
|
||||
/// <param name="info">The input data</param>
|
||||
internal void InvokeInput(Screen screen, ConsoleKeyInfo info)
|
||||
{
|
||||
Input?.Invoke(screen, new InputEventArgs(info));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the control should be rendered
|
||||
/// </summary>
|
||||
public bool Visible = true;
|
||||
}
|
||||
}
|
102
Commandline/TUI/DiffDraw.cs
Normal file
102
Commandline/TUI/DiffDraw.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using CC_Functions.Misc;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides differential drawing of a char[,] Do not use in combination with System.Console
|
||||
/// </summary>
|
||||
public static class DiffDraw
|
||||
{
|
||||
private static Pixel[,] Screen { get; set; } = new Pixel[0, 0];
|
||||
private static Pixel[,] _last = new Pixel[0,0];
|
||||
public static int Width => Screen.GetLength(1);
|
||||
public static int Height => Screen.GetLength(0);
|
||||
|
||||
public static void Draw(bool color)
|
||||
{
|
||||
Console.CursorTop = 0;
|
||||
Console.CursorLeft = 0;
|
||||
ConsoleColor fcol = Console.ForegroundColor;
|
||||
ConsoleColor bcol = Console.BackgroundColor;
|
||||
int width = Width;
|
||||
int height = Height;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
Pixel tmp1 = Screen[y, x];
|
||||
if (tmp1 == _last[y, x]) continue;
|
||||
if (color)
|
||||
{
|
||||
Console.ForegroundColor = tmp1.ForeColor;
|
||||
Console.BackgroundColor = tmp1.BackColor;
|
||||
}
|
||||
Console.CursorLeft = x;
|
||||
Console.Write(tmp1);
|
||||
}
|
||||
Console.WriteLine();
|
||||
Console.CursorLeft = 0;
|
||||
}
|
||||
Console.ForegroundColor = fcol;
|
||||
Console.BackgroundColor = bcol;
|
||||
_last = Screen;
|
||||
}
|
||||
|
||||
public static void FullDraw(bool color)
|
||||
{
|
||||
Console.CursorTop = 0;
|
||||
Console.CursorLeft = 0;
|
||||
ConsoleColor fcol = Console.ForegroundColor;
|
||||
ConsoleColor bcol = Console.BackgroundColor;
|
||||
Console.Clear();
|
||||
int width = Width;
|
||||
int height = Height;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
Pixel tmp1 = Screen[y, x];
|
||||
if (color)
|
||||
{
|
||||
Console.ForegroundColor = tmp1.ForeColor;
|
||||
Console.BackgroundColor = tmp1.BackColor;
|
||||
}
|
||||
Console.CursorLeft = x;
|
||||
Console.Write(tmp1);
|
||||
}
|
||||
Console.WriteLine();
|
||||
Console.CursorLeft = 0;
|
||||
}
|
||||
Console.ForegroundColor = fcol;
|
||||
Console.BackgroundColor = bcol;
|
||||
_last = Screen;
|
||||
}
|
||||
|
||||
public static char Get(Point p) => Get(p.X, p.Y);
|
||||
public static char Get(int x, int y) => Screen[y, x].Content;
|
||||
public static ConsoleColor GetForeColor(Point p) => GetForeColor(p.X, p.Y);
|
||||
public static ConsoleColor GetForeColor(int x, int y) => Screen[y, x].ForeColor;
|
||||
public static ConsoleColor GetBackColor(Point p) => GetBackColor(p.X, p.Y);
|
||||
public static ConsoleColor GetBackColor(int x, int y) => Screen[y, x].BackColor;
|
||||
|
||||
public static void Set(Point p, Pixel c) => Set(p.X, p.Y, c);
|
||||
|
||||
public static void Set(int x, int y, Pixel c) => Screen[y, x] = c;
|
||||
|
||||
public static void Clear() => Clear(Width, Height);
|
||||
|
||||
public static void Clear(int width, int height)
|
||||
{
|
||||
Screen = new Pixel[height, width];
|
||||
_last = _last.Resize(height, width);
|
||||
}
|
||||
|
||||
public static void Clear(Pixel[,] content)
|
||||
{
|
||||
Screen = content;
|
||||
_last = _last.Resize(Height, Width);
|
||||
}
|
||||
}
|
||||
}
|
12
Commandline/TUI/InputEventArgs.cs
Normal file
12
Commandline/TUI/InputEventArgs.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class InputEventArgs : EventArgs
|
||||
{
|
||||
private readonly ConsoleKeyInfo _info;
|
||||
public ConsoleKeyInfo Info => _info;
|
||||
|
||||
public InputEventArgs(ConsoleKeyInfo info) => _info = info;
|
||||
}
|
||||
}
|
31
Commandline/TUI/Label.cs
Normal file
31
Commandline/TUI/Label.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Drawing;
|
||||
using CC_Functions.Misc;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class Label : Control
|
||||
{
|
||||
public string Content;
|
||||
public Label(string content) => Content = content;
|
||||
|
||||
public override Pixel[,] Render()
|
||||
{
|
||||
char[,] inp = Content.ToNDArray2D();
|
||||
int w = inp.GetLength(0);
|
||||
int h = inp.GetLength(1);
|
||||
Pixel[,] output = new Pixel[w, h];
|
||||
for (int x = 0; x < w; x++)
|
||||
for (int y = 0; y < h; y++)
|
||||
output[x, y] = new Pixel(BackColor, ForeColor, inp[x, y]);
|
||||
Size = new Size(w, h);
|
||||
return output;
|
||||
}
|
||||
|
||||
protected override void Resize(int width, int height)
|
||||
{
|
||||
//ignored for [Render]s sake, do not use
|
||||
}
|
||||
|
||||
public override bool Selectable { get; } = false;
|
||||
}
|
||||
}
|
44
Commandline/TUI/Panel.cs
Normal file
44
Commandline/TUI/Panel.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CC_Functions.Misc;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class Panel : Control
|
||||
{
|
||||
public List<Control> Controls = new List<Control>();
|
||||
|
||||
public override Pixel[,] Render()
|
||||
{
|
||||
Pixel[,] tmp = new Pixel[Size.Height, Size.Width];
|
||||
tmp.Populate(new Pixel(BackColor, ForeColor, SpecialChars.empty));
|
||||
foreach (Control control in Controls)
|
||||
{
|
||||
if (control.Visible)
|
||||
{
|
||||
Pixel[,] render = control.Render();
|
||||
render.CopyTo(tmp, control.Point);
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
protected override void Resize(int width, int height)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Selectable { get; } = false;
|
||||
|
||||
public Control[] EnumerateRecursive()
|
||||
{
|
||||
List<Control> output = Controls.ToList();
|
||||
int i = 0;
|
||||
while (i < output.Count)
|
||||
{
|
||||
if (output[i] is Panel p) output.AddRange(p.EnumerateRecursive().Where(s => !output.Contains(s)));
|
||||
i++;
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
60
Commandline/TUI/Pixel.cs
Normal file
60
Commandline/TUI/Pixel.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class Pixel
|
||||
{
|
||||
private sealed class ColorContentEqualityComparer : IEqualityComparer<Pixel>
|
||||
{
|
||||
public bool Equals(Pixel x, Pixel y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return true;
|
||||
if (ReferenceEquals(x, null)) return false;
|
||||
if (ReferenceEquals(y, null)) return false;
|
||||
if (x.GetType() != y.GetType()) return false;
|
||||
return x.GetHashCode().Equals(y.GetHashCode());
|
||||
}
|
||||
|
||||
public int GetHashCode(Pixel obj) => obj.GetHashCode();
|
||||
}
|
||||
|
||||
public static IEqualityComparer<Pixel> ColorContentComparer { get; } = new ColorContentEqualityComparer();
|
||||
|
||||
protected bool Equals(Pixel other) => ColorContentComparer.Equals(this, other);
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj)) return false;
|
||||
if (ReferenceEquals(this, obj)) return true;
|
||||
if (obj.GetType() != GetType()) return false;
|
||||
return Equals((Pixel) obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine((int) BackColor, (int) ForeColor, Content);
|
||||
|
||||
public ConsoleColor BackColor;
|
||||
public ConsoleColor ForeColor;
|
||||
public char Content;
|
||||
|
||||
public Pixel(ConsoleColor backColor, ConsoleColor foreColor, char content)
|
||||
{
|
||||
BackColor = backColor;
|
||||
ForeColor = foreColor;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public Pixel(char content) : this(Console.BackgroundColor, Console.ForegroundColor, content)
|
||||
{
|
||||
}
|
||||
|
||||
public Pixel() : this(' ')
|
||||
{
|
||||
}
|
||||
|
||||
public static bool operator ==(Pixel a, Pixel b) => a.Equals(b);
|
||||
public static bool operator !=(Pixel a, Pixel b) => !a.Equals(b);
|
||||
|
||||
public override string ToString() => Content.ToString();
|
||||
}
|
||||
}
|
111
Commandline/TUI/Screen.cs
Normal file
111
Commandline/TUI/Screen.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class Screen : Panel
|
||||
{
|
||||
public readonly bool Color;
|
||||
public int TabPoint = 0;
|
||||
/// <summary>
|
||||
/// Creates a screen object. Multiple can be instantiated but drawing one overrides others. Use panels for that
|
||||
/// </summary>
|
||||
/// <param name="width">The screens with</param>
|
||||
/// <param name="height">The screens height</param>
|
||||
/// <param name="color">Whether to output in color</param>
|
||||
public Screen(int width, int height, bool color = true)
|
||||
{
|
||||
Color = color;
|
||||
Resize(width, height);
|
||||
Tab();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes the screen. Make sure that this does not exceed the console size
|
||||
/// </summary>
|
||||
/// <param name="width"></param>
|
||||
/// <param name="height"></param>
|
||||
public void Resize(int width, int height)
|
||||
{
|
||||
Size = new Size(width, height);
|
||||
DiffDraw.Clear(width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the screen, draws it to the console and outputs the new state
|
||||
/// </summary>
|
||||
/// <returns>The new state of the screen</returns>
|
||||
public Pixel[,] Render()
|
||||
{
|
||||
Pixel[,] tmp = base.Render();
|
||||
DiffDraw.Clear(tmp);
|
||||
DiffDraw.Draw(Color);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
public void ReadInput()
|
||||
{
|
||||
while (Console.KeyAvailable)
|
||||
{
|
||||
Control[] controls = EnumerateRecursive();
|
||||
Control[] selectable = controls.Where(s => s.Selectable).ToArray();
|
||||
ConsoleKeyInfo input = Console.ReadKey();
|
||||
switch (input.Key)
|
||||
{
|
||||
case ConsoleKey.Tab:
|
||||
Tab(selectable, (input.Modifiers & ConsoleModifiers.Shift) != 0);
|
||||
break;
|
||||
case ConsoleKey.Enter:
|
||||
if (selectable.Any() && selectable.Length >= TabPoint)
|
||||
{
|
||||
selectable[TabPoint].InvokeClick(this);
|
||||
Render();
|
||||
}
|
||||
break;
|
||||
case ConsoleKey.Escape:
|
||||
Close?.Invoke(this, new EventArgs());
|
||||
break;
|
||||
default:
|
||||
if (selectable.Any() && selectable.Length >= TabPoint)
|
||||
{
|
||||
selectable[TabPoint].InvokeInput(this, input);
|
||||
Render();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Tab(bool positive = true)
|
||||
{
|
||||
Control[] controls = EnumerateRecursive();
|
||||
Control[] selectable = controls.Where(s => s.Selectable).ToArray();
|
||||
Tab(selectable, positive);
|
||||
}
|
||||
|
||||
public void Tab(Control[] selectable, bool positive)
|
||||
{
|
||||
if (selectable.Any())
|
||||
{
|
||||
if (positive)
|
||||
{
|
||||
TabPoint++;
|
||||
if (TabPoint >= selectable.Length) TabPoint = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
TabPoint--;
|
||||
if (TabPoint < 0) TabPoint = selectable.Length - 1;
|
||||
}
|
||||
foreach (Control control in selectable) control.Selected = false;
|
||||
selectable[TabPoint].Selected = true;
|
||||
Render();
|
||||
}
|
||||
}
|
||||
|
||||
public delegate void OnClose(Screen screen, EventArgs e);
|
||||
|
||||
public event OnClick Close;
|
||||
}
|
||||
}
|
94
Commandline/TUI/Slider.cs
Normal file
94
Commandline/TUI/Slider.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using CC_Functions.Misc;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class Slider : Control
|
||||
{
|
||||
public Slider()
|
||||
{
|
||||
Input += (screen, args) =>
|
||||
{
|
||||
switch (args.Info.Key)
|
||||
{
|
||||
case ConsoleKey.LeftArrow:
|
||||
_value -= StepSize;
|
||||
if (_value < MinValue)
|
||||
_value = MinValue;
|
||||
Value = _value;
|
||||
break;
|
||||
case ConsoleKey.RightArrow:
|
||||
_value += StepSize;
|
||||
if (_value > MaxValue)
|
||||
_value = MaxValue;
|
||||
Value = _value;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public int MaxValue
|
||||
{
|
||||
get => _maxValue;
|
||||
set
|
||||
{
|
||||
if (value > MinValue && value >= Value)
|
||||
_maxValue = value;
|
||||
else
|
||||
throw new ArgumentOutOfRangeException("MaxValue must be larger than MinValue and equal to or larger than Value");
|
||||
}
|
||||
}
|
||||
|
||||
public int MinValue
|
||||
{
|
||||
get => _minValue;
|
||||
set
|
||||
{
|
||||
if (value < MaxValue && value <= Value)
|
||||
_minValue = value;
|
||||
else
|
||||
throw new ArgumentOutOfRangeException("MaxValue must be larger than MinValue and equal to or smaller than Value");
|
||||
}
|
||||
}
|
||||
|
||||
public int Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (value <= MaxValue && value >= MinValue)
|
||||
_value = value;
|
||||
else
|
||||
throw new ArgumentOutOfRangeException("Value must be between MinValue and MaxValue");
|
||||
}
|
||||
}
|
||||
|
||||
public int StepSize = 1;
|
||||
private int _value = 5;
|
||||
private int _maxValue = 10;
|
||||
private int _minValue = 0;
|
||||
|
||||
public override Pixel[,] Render()
|
||||
{
|
||||
int delta = MaxValue - MinValue;
|
||||
int litValLen = Math.Max(MaxValue.ToString().Length, MinValue.ToString().Length);
|
||||
int prevpts = Math.Max((Value - MinValue) * Size.Width / delta - litValLen - 2, 0);
|
||||
int postpts = Math.Max(Size.Width - prevpts - litValLen - 2, 0);
|
||||
char[,] rend = $"{new string('=', prevpts)}[{Value.ToString($"D{litValLen}")}]{new string('=', postpts)}".ToNDArray2D();
|
||||
int f1 = rend.GetLength(0);
|
||||
int f2 = rend.GetLength(1);
|
||||
Pixel[,] output = new Pixel[f1, f2];
|
||||
output.Populate(new Pixel());
|
||||
for (int i = 0; i < f1; i++)
|
||||
for (int j = 0; j < f2; j++) output[i, j] = new Pixel(Selected ? ForeColor : BackColor, Selected ? BackColor : ForeColor, rend[i, j]);
|
||||
return output;
|
||||
}
|
||||
|
||||
protected override void Resize(int width, int height)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override bool Selectable { get; } = true;
|
||||
}
|
||||
}
|
158
Commandline/TUI/TextBox.cs
Normal file
158
Commandline/TUI/TextBox.cs
Normal file
@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using CC_Functions.Misc;
|
||||
|
||||
namespace CC_Functions.Commandline.TUI
|
||||
{
|
||||
public class TextBox : Control
|
||||
{
|
||||
public string Content;
|
||||
private string[] Lines
|
||||
{
|
||||
get => Content.Split('\n');
|
||||
set => Content = string.Join('\n', value);
|
||||
}
|
||||
|
||||
public Point Cursor = new Point(0, 0);
|
||||
public TextBox(string content)
|
||||
{
|
||||
Content = content;
|
||||
Input += (screen, args) =>
|
||||
{
|
||||
ProcessInput(args.Info.Key, args.Info);
|
||||
};
|
||||
Click += (screen, args) => ProcessInput(ConsoleKey.Enter, new ConsoleKeyInfo());
|
||||
}
|
||||
|
||||
private void ProcessInput(ConsoleKey key, ConsoleKeyInfo info)
|
||||
{
|
||||
string[] lines = Lines;
|
||||
List<string> tmp;
|
||||
int tmplen;
|
||||
switch (key)
|
||||
{
|
||||
case ConsoleKey.LeftArrow:
|
||||
Cursor.X--;
|
||||
if (Cursor.X < 0)
|
||||
{
|
||||
if (Cursor.Y > 0) Cursor.Y--;
|
||||
ProcessInput(ConsoleKey.End, info);
|
||||
}
|
||||
break;
|
||||
case ConsoleKey.RightArrow:
|
||||
Cursor.X++;
|
||||
if (Cursor.X >= Lines[Cursor.Y].Length)
|
||||
{
|
||||
Cursor.Y++;
|
||||
Cursor.X = 0;
|
||||
if (Cursor.Y >= Lines.Length)
|
||||
{
|
||||
Cursor.Y--;
|
||||
ProcessInput(ConsoleKey.End, info);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ConsoleKey.UpArrow:
|
||||
if (Cursor.Y > 0)
|
||||
{
|
||||
Cursor.Y--;
|
||||
if (Cursor.X >= Lines[Cursor.Y].Length) ProcessInput(ConsoleKey.End, info);
|
||||
}
|
||||
else
|
||||
Cursor.X = 0;
|
||||
break;
|
||||
case ConsoleKey.DownArrow:
|
||||
if (Cursor.Y < Lines.Length - 1)
|
||||
{
|
||||
Cursor.Y++;
|
||||
if (Cursor.X >= Lines[Cursor.Y].Length) ProcessInput(ConsoleKey.End, info);
|
||||
}
|
||||
else
|
||||
ProcessInput(ConsoleKey.End, info);
|
||||
break;
|
||||
case ConsoleKey.Home:
|
||||
Cursor.X = 0;
|
||||
break;
|
||||
case ConsoleKey.End:
|
||||
Cursor.X = Math.Max(Lines[Cursor.Y].Length - 1, 0);
|
||||
break;
|
||||
case ConsoleKey.PageUp:
|
||||
for (int i = 0; i < 5; i++)
|
||||
ProcessInput(ConsoleKey.UpArrow, info);
|
||||
break;
|
||||
case ConsoleKey.PageDown:
|
||||
for (int i = 0; i < 5; i++)
|
||||
ProcessInput(ConsoleKey.DownArrow, info);
|
||||
break;
|
||||
case ConsoleKey.Delete:
|
||||
if (lines[Cursor.Y].Length > Cursor.X)
|
||||
lines[Cursor.Y] = lines[Cursor.Y].Remove(Cursor.X, 1);
|
||||
Lines = lines;
|
||||
break;
|
||||
case ConsoleKey.Backspace:
|
||||
if (Cursor.X > 0 && lines[Cursor.Y].Length > 0)
|
||||
{
|
||||
lines[Cursor.Y] = lines[Cursor.Y].Remove(Cursor.X - 1, 1);
|
||||
ProcessInput(ConsoleKey.LeftArrow, info);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Cursor.Y > 0)
|
||||
{
|
||||
tmp = lines.ToList();
|
||||
tmplen = tmp[Cursor.Y - 1].Length;
|
||||
tmp[Cursor.Y - 1] += tmp[Cursor.Y];
|
||||
tmp.RemoveAt(Cursor.Y);
|
||||
lines = tmp.ToArray();
|
||||
Cursor.Y--;
|
||||
Cursor.X = tmplen - 1;
|
||||
}
|
||||
}
|
||||
Lines = lines;
|
||||
break;
|
||||
case ConsoleKey.Enter:
|
||||
tmp = lines.ToList();
|
||||
lines[Cursor.Y] = lines[Cursor.Y].Insert(Cursor.X, "\n");
|
||||
Cursor.Y++;
|
||||
Cursor.X = 0;
|
||||
Lines = lines;
|
||||
break;
|
||||
default:
|
||||
lines[Cursor.Y] = lines[Cursor.Y].Insert(Cursor.X, info.KeyChar.ToString());
|
||||
Lines = lines;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override Pixel[,] Render()
|
||||
{
|
||||
char[,] inp1 = Content.ToNDArray2D();
|
||||
inp1 = inp1.Resize(Size.Height, Size.Width - 2);
|
||||
char[,] inp = new char[Size.Width, Size.Height];
|
||||
inp.Populate(SpecialChars.empty);
|
||||
for (int i = 0; i < Size.Height; i++)
|
||||
{
|
||||
inp[0, i] = '[';
|
||||
inp[Size.Width - 1, i] = ']';
|
||||
}
|
||||
for (int i = 0; i < Size.Width; i++) inp[i, Size.Height - 1] = '.';
|
||||
inp1.Rotate().CopyTo(inp, new Point(0, 1));
|
||||
if (Selected)
|
||||
inp[Cursor.X + 1, Cursor.Y] = '▒';
|
||||
Pixel[,] output = new Pixel[Size.Height, Size.Width];
|
||||
for (int x = 0; x < Size.Width; x++)
|
||||
for (int y = 0; y < Size.Height; y++)
|
||||
output[y, x] = new Pixel(Selected ? ForeColor : BackColor, Selected ? BackColor : ForeColor, inp[x, y]);
|
||||
return output;
|
||||
}
|
||||
|
||||
protected override void Resize(int width, int height)
|
||||
{
|
||||
//ignored for [Render]s sake, do not use
|
||||
}
|
||||
|
||||
public override bool Selectable { get; } = true;
|
||||
}
|
||||
}
|
107
Commandline/TableParser.cs
Normal file
107
Commandline/TableParser.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace CC_Functions.Commandline
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides functions for parsing enumerables to powershell-like tables
|
||||
/// </summary>
|
||||
public static class TableParser
|
||||
{
|
||||
public static string ToStringTable<T>(this IEnumerable<T> values, string[] columnHeaders,
|
||||
params Func<T, object>[] valueSelectors) => ToStringTable(values.ToArray(), columnHeaders, valueSelectors);
|
||||
|
||||
public static string ToStringTable<T>(this T[] values, string[] columnHeaders,
|
||||
params Func<T, object>[] valueSelectors)
|
||||
{
|
||||
Debug.Assert(columnHeaders.Length == valueSelectors.Length);
|
||||
|
||||
string[,] arrValues = new string[values.Length + 1, valueSelectors.Length];
|
||||
|
||||
// Fill headers
|
||||
for (int colIndex = 0; colIndex < arrValues.GetLength(1); colIndex++)
|
||||
arrValues[0, colIndex] = columnHeaders[colIndex];
|
||||
|
||||
// Fill table rows
|
||||
for (int rowIndex = 1; rowIndex < arrValues.GetLength(0); rowIndex++)
|
||||
for (int colIndex = 0; colIndex < arrValues.GetLength(1); colIndex++)
|
||||
{
|
||||
object value = valueSelectors[colIndex].Invoke(values[rowIndex - 1]);
|
||||
|
||||
arrValues[rowIndex, colIndex] = value != null ? value.ToString() : "null";
|
||||
}
|
||||
|
||||
return ToStringTable(arrValues);
|
||||
}
|
||||
|
||||
public static string ToStringTable(this string[,] arrValues)
|
||||
{
|
||||
int[] maxColumnsWidth = GetMaxColumnsWidth(arrValues);
|
||||
string headerSpliter = new string('-', maxColumnsWidth.Sum(i => i + 3) - 1);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int rowIndex = 0; rowIndex < arrValues.GetLength(0); rowIndex++)
|
||||
{
|
||||
for (int colIndex = 0; colIndex < arrValues.GetLength(1); colIndex++)
|
||||
{
|
||||
// Print cell
|
||||
string cell = arrValues[rowIndex, colIndex];
|
||||
cell = cell.PadRight(maxColumnsWidth[colIndex]);
|
||||
sb.Append(" | ");
|
||||
sb.Append(cell);
|
||||
}
|
||||
|
||||
// Print end of line
|
||||
sb.Append(" | ");
|
||||
sb.AppendLine();
|
||||
|
||||
// Print splitter
|
||||
if (rowIndex == 0)
|
||||
{
|
||||
sb.AppendFormat(" |{0}| ", headerSpliter);
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static int[] GetMaxColumnsWidth(string[,] arrValues)
|
||||
{
|
||||
int[] maxColumnsWidth = new int[arrValues.GetLength(1)];
|
||||
for (int colIndex = 0; colIndex < arrValues.GetLength(1); colIndex++)
|
||||
for (int rowIndex = 0; rowIndex < arrValues.GetLength(0); rowIndex++)
|
||||
{
|
||||
int newLength = arrValues[rowIndex, colIndex].Length;
|
||||
int oldLength = maxColumnsWidth[colIndex];
|
||||
|
||||
if (newLength > oldLength) maxColumnsWidth[colIndex] = newLength;
|
||||
}
|
||||
|
||||
return maxColumnsWidth;
|
||||
}
|
||||
|
||||
public static string ToStringTable<T>(this IEnumerable<T> values,
|
||||
params Expression<Func<T, object>>[] valueSelectors)
|
||||
{
|
||||
string[] headers = valueSelectors.Select(func => GetProperty(func).Name).ToArray();
|
||||
Func<T, object>[] selectors = valueSelectors.Select(exp => exp.Compile()).ToArray();
|
||||
return ToStringTable(values, headers, selectors);
|
||||
}
|
||||
|
||||
private static PropertyInfo GetProperty<T>(Expression<Func<T, object>> expresstion)
|
||||
{
|
||||
if (expresstion.Body is UnaryExpression expression)
|
||||
if (expression.Operand is MemberExpression memberExpression)
|
||||
return memberExpression.Member as PropertyInfo;
|
||||
|
||||
if (expresstion.Body is MemberExpression body) return body.Member as PropertyInfo;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,71 @@
|
||||
namespace CC_Functions.Misc
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
|
||||
namespace CC_Functions.Misc
|
||||
{
|
||||
public static class ArrayFormatter
|
||||
{
|
||||
public static string ElementsToString(this object[] input, string separator = "\r\n")
|
||||
public static T[,] Resize<T>(this T[,] original, int rows, int cols)
|
||||
{
|
||||
string a = "";
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
a += input[i] + separator;
|
||||
if (separator.Length > 0)
|
||||
a = a.Remove(a.Length - separator.Length);
|
||||
return a;
|
||||
T[,] newArray = new T[rows, cols];
|
||||
int minRows = Math.Min(rows, original.GetLength(0));
|
||||
int minCols = Math.Min(cols, original.GetLength(1));
|
||||
for (int i = 0; i < minRows; i++)
|
||||
for (int j = 0; j < minCols; j++)
|
||||
newArray[i, j] = original[i, j];
|
||||
return newArray;
|
||||
}
|
||||
|
||||
public static char[,] ToNDArray2D(this string source, char defaultEl = SpecialChars.empty)
|
||||
{
|
||||
string[] sourceArr = source.Split('\n');
|
||||
int width = sourceArr.Select(s => s.Length).OrderBy(s => s).Last();
|
||||
int height = sourceArr.Length;
|
||||
char[,] output = new char[height, width];
|
||||
output.Populate(defaultEl);
|
||||
for (int i = 0; i < sourceArr.Length; i++)
|
||||
{
|
||||
string s = sourceArr[i];
|
||||
for (int j = 0; j < s.Length; j++)
|
||||
output[i, j] = s[j];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public static void Populate<T>(this T[] arr, T value) {
|
||||
for ( int i = 0; i < arr.Length;i++ ) arr[i] = value;
|
||||
}
|
||||
|
||||
public static void Populate<T>(this T[,] arr, T value)
|
||||
{
|
||||
int w = arr.GetLength(0);
|
||||
int h = arr.GetLength(1);
|
||||
for (int i = 0; i < w; i++)
|
||||
for (int j = 0; j < h; j++) arr[i, j] = value;
|
||||
}
|
||||
|
||||
public static void CopyTo<T>(this T[,] arr, T[,] target, Point offset)
|
||||
{
|
||||
int w = arr.GetLength(1);
|
||||
int h = arr.GetLength(0);
|
||||
int mw = target.GetLength(1);
|
||||
int mh = target.GetLength(0);
|
||||
int ow = offset.X;
|
||||
int oh = offset.Y;
|
||||
for (int x = ow; x < Math.Min(mw, w + ow); x++)
|
||||
for (int y = oh; y < Math.Min(mh, h + oh); y++)
|
||||
target[y, x] = arr[y - oh, x - ow];
|
||||
}
|
||||
|
||||
public static T[,] Rotate<T>(this T[,] arr)
|
||||
{
|
||||
int w = arr.GetLength(0);
|
||||
int h = arr.GetLength(1);
|
||||
T[,] target = new T[h, w];
|
||||
for (int x = 0; x < w; x++)
|
||||
for (int y = 0; y < h; y++) target[y, x] = arr[x, y];
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
7
Misc/SpecialChars.cs
Normal file
7
Misc/SpecialChars.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace CC_Functions.Misc
|
||||
{
|
||||
public static class SpecialChars
|
||||
{
|
||||
public const char empty = ' ';
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user