From 11a528766cff44469b7add688843d4edc4e889c5 Mon Sep 17 00:00:00 2001 From: JFronny <33260128+JFronny@users.noreply.github.com> Date: Fri, 22 May 2020 21:12:09 +0200 Subject: [PATCH] Simple component system for TUI apps, some basic components --- .../.idea/projectSettingsUpdater.xml | 2 +- CC-Functions.sln | 12 ++ CLITest/CLITest.csproj | 13 ++ CLITest/Program.cs | 79 +++++++++ Commandline/Commandline.csproj | 36 ++++ Commandline/TUI/Button.cs | 34 ++++ Commandline/TUI/CheckBox.cs | 61 +++++++ Commandline/TUI/Control.cs | 81 +++++++++ Commandline/TUI/DiffDraw.cs | 102 +++++++++++ Commandline/TUI/InputEventArgs.cs | 12 ++ Commandline/TUI/Label.cs | 31 ++++ Commandline/TUI/Panel.cs | 44 +++++ Commandline/TUI/Pixel.cs | 60 +++++++ Commandline/TUI/Screen.cs | 111 ++++++++++++ Commandline/TUI/Slider.cs | 94 +++++++++++ Commandline/TUI/TextBox.cs | 158 ++++++++++++++++++ Commandline/TableParser.cs | 107 ++++++++++++ Misc/ArrayFormatter.cs | 72 +++++++- Misc/SpecialChars.cs | 7 + 19 files changed, 1107 insertions(+), 9 deletions(-) create mode 100644 CLITest/CLITest.csproj create mode 100644 CLITest/Program.cs create mode 100644 Commandline/Commandline.csproj create mode 100644 Commandline/TUI/Button.cs create mode 100644 Commandline/TUI/CheckBox.cs create mode 100644 Commandline/TUI/Control.cs create mode 100644 Commandline/TUI/DiffDraw.cs create mode 100644 Commandline/TUI/InputEventArgs.cs create mode 100644 Commandline/TUI/Label.cs create mode 100644 Commandline/TUI/Panel.cs create mode 100644 Commandline/TUI/Pixel.cs create mode 100644 Commandline/TUI/Screen.cs create mode 100644 Commandline/TUI/Slider.cs create mode 100644 Commandline/TUI/TextBox.cs create mode 100644 Commandline/TableParser.cs create mode 100644 Misc/SpecialChars.cs diff --git a/.idea/.idea.CC-Functions/.idea/projectSettingsUpdater.xml b/.idea/.idea.CC-Functions/.idea/projectSettingsUpdater.xml index 7515e76..4bb9f4d 100644 --- a/.idea/.idea.CC-Functions/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.CC-Functions/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/CC-Functions.sln b/CC-Functions.sln index 283bc70..3b833f4 100644 --- a/CC-Functions.sln +++ b/CC-Functions.sln @@ -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 diff --git a/CLITest/CLITest.csproj b/CLITest/CLITest.csproj new file mode 100644 index 0000000..86a92c3 --- /dev/null +++ b/CLITest/CLITest.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp3.1 + 8 + + + + + + + diff --git a/CLITest/Program.cs b/CLITest/Program.cs new file mode 100644 index 0000000..456a92f --- /dev/null +++ b/CLITest/Program.cs @@ -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"); + } + } +} \ No newline at end of file diff --git a/Commandline/Commandline.csproj b/Commandline/Commandline.csproj new file mode 100644 index 0000000..1186f20 --- /dev/null +++ b/Commandline/Commandline.csproj @@ -0,0 +1,36 @@ + + + + netstandard2.1 + Library + CC_Functions.Commandline + CC_Functions.Commandline + 8 + false + CC-Functions.Commandline + CC-Functions.Commandline + CC24 + Random pieces of code used across my projects. CLI/TUI extensions + Copyright 2020 + https://github.com/JFronny/CC-Functions + https://github.com/JFronny/CC-Functions.git + git + 1.1.* + 1.0.0.0 + 0.0 + 1.1.$(VersionSuffix) + + + + bin\Debug\Commandline.xml + + + + bin\Release\Commandline.xml + + + + + + + diff --git a/Commandline/TUI/Button.cs b/Commandline/TUI/Button.cs new file mode 100644 index 0000000..d943b14 --- /dev/null +++ b/Commandline/TUI/Button.cs @@ -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; + } +} \ No newline at end of file diff --git a/Commandline/TUI/CheckBox.cs b/Commandline/TUI/CheckBox.cs new file mode 100644 index 0000000..3472213 --- /dev/null +++ b/Commandline/TUI/CheckBox.cs @@ -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; + } +} \ No newline at end of file diff --git a/Commandline/TUI/Control.cs b/Commandline/TUI/Control.cs new file mode 100644 index 0000000..7ff955c --- /dev/null +++ b/Commandline/TUI/Control.cs @@ -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; } + + /// + /// Called when [enter] is pressed while the control is selected + /// + /// An instance of the calling screen + /// Args + public delegate void OnClick(Screen screen, EventArgs e); + /// + /// Called when [enter] is pressed while the control is selected + /// + public event OnClick Click; + /// + /// Called when the control is selected and unknown input is given + /// + /// An instance of the calling screen + /// Args + public delegate void OnInput(Screen screen, InputEventArgs e); + /// + /// Called when the control is selected and unknown input is given + /// + public event OnInput Input; + /// + /// Whether the object is selected. Used internally and for drawing + /// + public bool Selected { get; internal set; } = false; + /// + /// Invokes click events + /// + /// The calling screen + internal void InvokeClick(Screen screen) + { + Click?.Invoke(screen, new EventArgs()); + } + /// + /// Invokes input events + /// + /// The calling screen + /// The input data + internal void InvokeInput(Screen screen, ConsoleKeyInfo info) + { + Input?.Invoke(screen, new InputEventArgs(info)); + } + + /// + /// Whether the control should be rendered + /// + public bool Visible = true; + } +} \ No newline at end of file diff --git a/Commandline/TUI/DiffDraw.cs b/Commandline/TUI/DiffDraw.cs new file mode 100644 index 0000000..65033f8 --- /dev/null +++ b/Commandline/TUI/DiffDraw.cs @@ -0,0 +1,102 @@ +using System; +using System.Drawing; +using CC_Functions.Misc; + +namespace CC_Functions.Commandline.TUI +{ + /// + /// Provides differential drawing of a char[,] Do not use in combination with System.Console + /// + 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); + } + } +} \ No newline at end of file diff --git a/Commandline/TUI/InputEventArgs.cs b/Commandline/TUI/InputEventArgs.cs new file mode 100644 index 0000000..f528784 --- /dev/null +++ b/Commandline/TUI/InputEventArgs.cs @@ -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; + } +} \ No newline at end of file diff --git a/Commandline/TUI/Label.cs b/Commandline/TUI/Label.cs new file mode 100644 index 0000000..27f3a67 --- /dev/null +++ b/Commandline/TUI/Label.cs @@ -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; + } +} \ No newline at end of file diff --git a/Commandline/TUI/Panel.cs b/Commandline/TUI/Panel.cs new file mode 100644 index 0000000..a47f244 --- /dev/null +++ b/Commandline/TUI/Panel.cs @@ -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 Controls = new List(); + + 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 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(); + } + } +} diff --git a/Commandline/TUI/Pixel.cs b/Commandline/TUI/Pixel.cs new file mode 100644 index 0000000..c76a9ba --- /dev/null +++ b/Commandline/TUI/Pixel.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace CC_Functions.Commandline.TUI +{ + public class Pixel + { + private sealed class ColorContentEqualityComparer : IEqualityComparer + { + 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 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(); + } +} \ No newline at end of file diff --git a/Commandline/TUI/Screen.cs b/Commandline/TUI/Screen.cs new file mode 100644 index 0000000..ea65a3c --- /dev/null +++ b/Commandline/TUI/Screen.cs @@ -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; + /// + /// Creates a screen object. Multiple can be instantiated but drawing one overrides others. Use panels for that + /// + /// The screens with + /// The screens height + /// Whether to output in color + public Screen(int width, int height, bool color = true) + { + Color = color; + Resize(width, height); + Tab(); + } + + /// + /// Resizes the screen. Make sure that this does not exceed the console size + /// + /// + /// + public void Resize(int width, int height) + { + Size = new Size(width, height); + DiffDraw.Clear(width, height); + } + + /// + /// Renders the screen, draws it to the console and outputs the new state + /// + /// The new state of the screen + 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; + } +} \ No newline at end of file diff --git a/Commandline/TUI/Slider.cs b/Commandline/TUI/Slider.cs new file mode 100644 index 0000000..f324246 --- /dev/null +++ b/Commandline/TUI/Slider.cs @@ -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; + } +} \ No newline at end of file diff --git a/Commandline/TUI/TextBox.cs b/Commandline/TUI/TextBox.cs new file mode 100644 index 0000000..dc2b77c --- /dev/null +++ b/Commandline/TUI/TextBox.cs @@ -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 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; + } +} \ No newline at end of file diff --git a/Commandline/TableParser.cs b/Commandline/TableParser.cs new file mode 100644 index 0000000..7fd2e93 --- /dev/null +++ b/Commandline/TableParser.cs @@ -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 +{ + /// + /// Provides functions for parsing enumerables to powershell-like tables + /// + public static class TableParser + { + public static string ToStringTable(this IEnumerable values, string[] columnHeaders, + params Func[] valueSelectors) => ToStringTable(values.ToArray(), columnHeaders, valueSelectors); + + public static string ToStringTable(this T[] values, string[] columnHeaders, + params Func[] 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(this IEnumerable values, + params Expression>[] valueSelectors) + { + string[] headers = valueSelectors.Select(func => GetProperty(func).Name).ToArray(); + Func[] selectors = valueSelectors.Select(exp => exp.Compile()).ToArray(); + return ToStringTable(values, headers, selectors); + } + + private static PropertyInfo GetProperty(Expression> 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; + } + } +} \ No newline at end of file diff --git a/Misc/ArrayFormatter.cs b/Misc/ArrayFormatter.cs index 59ea186..28c3aef 100644 --- a/Misc/ArrayFormatter.cs +++ b/Misc/ArrayFormatter.cs @@ -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(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(this T[] arr, T value) { + for ( int i = 0; i < arr.Length;i++ ) arr[i] = value; + } + + public static void Populate(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(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(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; } } } \ No newline at end of file diff --git a/Misc/SpecialChars.cs b/Misc/SpecialChars.cs new file mode 100644 index 0000000..03e7372 --- /dev/null +++ b/Misc/SpecialChars.cs @@ -0,0 +1,7 @@ +namespace CC_Functions.Misc +{ + public static class SpecialChars + { + public const char empty = ' '; + } +} \ No newline at end of file