├── .gitattributes
├── .github
└── workflows
│ ├── build-linux.yml
│ ├── build-window.yml
│ └── tests.yml
├── .gitignore
├── ConsoleGUI.Example
├── Board.cs
├── ConsoleGUI.Example.csproj
├── InputController.cs
├── LogPanel.cs
├── Player.cs
├── Program.cs
├── SimpleDecorator.cs
└── TabPanel.cs
├── ConsoleGUI.MouseExample
├── App.config
├── ConsoleGUI.MouseExample.csproj
├── MouseHandler.cs
├── Program.cs
└── Properties
│ └── AssemblyInfo.cs
├── ConsoleGUI.Test
├── Assembly
│ └── Assembly.cs
├── Common
│ ├── ControlTest.cs
│ └── DrawingContextTest.cs
├── ConsoleGUI.Test.csproj
├── Controls
│ └── BorderTest.cs
└── Utils
│ └── ColorConverterTest.cs
├── ConsoleGUI.sln
├── ConsoleGUI
├── Api
│ ├── IConsole.cs
│ ├── SimplifiedConsole.cs
│ └── StandardConsole.cs
├── Assembly
│ └── Assembly.cs
├── Buffering
│ └── ConsoleBuffer.cs
├── Common
│ ├── Control.cs
│ └── DrawingContext.cs
├── ConsoleGUI.csproj
├── ConsoleManager.cs
├── Controls
│ ├── Background.cs
│ ├── Border.cs
│ ├── Boundary.cs
│ ├── Box.cs
│ ├── BreakPanel.cs
│ ├── Button.cs
│ ├── Canvas.cs
│ ├── CheckBox.cs
│ ├── DataGrid.cs
│ ├── Decorator.cs
│ ├── DockPanel.cs
│ ├── Grid.cs
│ ├── HorizontalAlignment.cs
│ ├── HorizontalSeparator.cs
│ ├── HorizontalStackPanel.cs
│ ├── Margin.cs
│ ├── MousePanel.cs
│ ├── Overlay.cs
│ ├── Style.cs
│ ├── TextBlock.cs
│ ├── TextBox.cs
│ ├── VerticalScrollPanel.cs
│ ├── VerticalSeparator.cs
│ ├── VerticalStackPanel.cs
│ └── WrapPanel.cs
├── Data
│ ├── BorderPlacement.cs
│ ├── BorderStyle.cs
│ ├── Cell.cs
│ ├── Character.cs
│ ├── Color.cs
│ ├── DataGridStyle.cs
│ ├── MouseContext.cs
│ └── TextAlignment.cs
├── IControl.cs
├── IDrawingContext.cs
├── Input
│ ├── IInputListener.cs
│ ├── IMouseListener.cs
│ └── InputEvent.cs
├── Space
│ ├── Offset.cs
│ ├── Position.cs
│ ├── Rect.cs
│ ├── Size.cs
│ └── Vector.cs
├── UserDefined
│ ├── DrawingContextWrapper.cs
│ └── SimpleControl.cs
└── Utils
│ ├── ColorConverter.cs
│ ├── DrawingSection.cs
│ ├── FreezeLock.cs
│ ├── SafeConsole.cs
│ ├── SafeConsoleException.cs
│ ├── Setter.cs
│ └── TextUtils.cs
├── LICENSE
├── README.md
└── Resources
├── Problems.png
├── example.png
└── input example.gif
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/build-linux.yml:
--------------------------------------------------------------------------------
1 | name: build linux
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Setup .NET Core
13 | uses: actions/setup-dotnet@v1
14 | with:
15 | dotnet-version: 3.0.100
16 | - name: Build with dotnet
17 | run: dotnet build ./ConsoleGUI.Example/ConsoleGUI.Example.csproj --configuration Release
18 |
--------------------------------------------------------------------------------
/.github/workflows/build-window.yml:
--------------------------------------------------------------------------------
1 | name: build windows
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: [windows-latest]
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Setup .NET Core
13 | uses: actions/setup-dotnet@v1
14 | with:
15 | dotnet-version: 3.0.100
16 | - name: Build with dotnet
17 | run: dotnet build --configuration Release
18 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Setup .NET Core
13 | uses: actions/setup-dotnet@v1
14 | with:
15 | dotnet-version: 2.1.802
16 | - name: Test with dotnet
17 | run: dotnet test
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/ConsoleGUI.Example/Board.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using ConsoleGUI.Controls;
6 | using ConsoleGUI.Data;
7 | using ConsoleGUI.Space;
8 | using ConsoleGUI.UserDefined;
9 |
10 | namespace ConsoleGUI.Example
11 | {
12 | internal class BoardCell : SimpleControl
13 | {
14 | private readonly IControl _cell;
15 |
16 | public BoardCell(char content, Color color)
17 | {
18 | _cell = new Background
19 | {
20 | Color = color,
21 | Content = new Box
22 | {
23 | HorizontalContentPlacement = Box.HorizontalPlacement.Center,
24 | VerticalContentPlacement = Box.VerticalPlacement.Center,
25 | Content = new TextBlock { Text = content.ToString() }
26 | }
27 | };
28 |
29 | Content = _cell;
30 | }
31 | }
32 |
33 | internal class Board : SimpleControl
34 | {
35 | private readonly Grid _board;
36 |
37 | public Board()
38 | {
39 | _board = new Grid
40 | {
41 | Rows = Enumerable.Repeat(new Grid.RowDefinition(3), 10).ToArray(),
42 | Columns = Enumerable.Repeat(new Grid.ColumnDefinition(5), 10).ToArray()
43 | };
44 |
45 | for (int i = 1; i < 9; i++)
46 | {
47 | var character = (char)('a' + (i - 1));
48 | var number = (char)('0' + (i - 1));
49 | var darkColor = new Color(50, 50, 50).Mix(Color.White, i % 2 == 1 ? 0f : 0.1f);
50 | var lightColor = new Color(50, 50, 50).Mix(Color.White, i % 2 == 0 ? 0f : 0.1f);
51 |
52 | _board.AddChild(i, 0, new BoardCell(character, darkColor));
53 | _board.AddChild(i, 9, new BoardCell(character, lightColor));
54 | _board.AddChild(0, i, new BoardCell(number, darkColor));
55 | _board.AddChild(9, i, new BoardCell(number, lightColor));
56 | }
57 |
58 | string[] pieces = new[] {
59 | "♜♞♝♛♚♝♞♜",
60 | "♟♟♟♟♟♟♟♟",
61 | " ",
62 | " ",
63 | " ",
64 | " ",
65 | "♙♙♙♙♙♙♙♙",
66 | "♖♘♗♕♔♗♘♖"
67 | };
68 |
69 | for (int i = 1; i < 9; i++)
70 | for (int j = 1; j < 9; j++)
71 | _board.AddChild(i, j, new BoardCell(pieces[j - 1][i - 1], new Color(139, 69, 19).Mix(Color.White, ((i + j) % 2) == 1 ? 0f : 0.4f)));
72 |
73 | Content = _board;
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/ConsoleGUI.Example/ConsoleGUI.Example.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp2.1
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/ConsoleGUI.Example/InputController.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Controls;
2 | using ConsoleGUI.Input;
3 | using System;
4 |
5 | namespace ConsoleGUI.Example
6 | {
7 | class InputController : IInputListener
8 | {
9 | private readonly TextBox _textBox;
10 | private readonly LogPanel _logPanel;
11 |
12 | public InputController(TextBox textBox, LogPanel logPanel)
13 | {
14 | _textBox = textBox;
15 | _logPanel = logPanel;
16 | }
17 |
18 | public void OnInput(InputEvent inputEvent)
19 | {
20 | if (inputEvent.Key.Key != ConsoleKey.Enter) return;
21 |
22 | _logPanel.Add(_textBox.Text);
23 |
24 | _textBox.Text = string.Empty;
25 | inputEvent.Handled = true;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ConsoleGUI.Example/LogPanel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using ConsoleGUI.Controls;
5 | using ConsoleGUI.Data;
6 | using ConsoleGUI.Space;
7 | using ConsoleGUI.UserDefined;
8 |
9 | namespace ConsoleGUI.Example
10 | {
11 | internal class LogPanel : SimpleControl
12 | {
13 | private readonly VerticalStackPanel _stackPanel;
14 |
15 | public LogPanel()
16 | {
17 | _stackPanel = new VerticalStackPanel();
18 |
19 | Content = _stackPanel;
20 | }
21 |
22 | public void Add(string message)
23 | {
24 | _stackPanel.Add(new WrapPanel
25 | {
26 | Content = new HorizontalStackPanel
27 | {
28 | Children = new[]
29 | {
30 | new TextBlock {Text = $"[{DateTime.Now.ToLongTimeString()}] ", Color = new Color(200, 20, 20)},
31 | new TextBlock {Text = message}
32 | }
33 | }
34 | });
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ConsoleGUI.Example/Player.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ConsoleGUI.Example
4 | {
5 | internal class Player
6 | {
7 | public string Name { get; }
8 | public string Surname { get; }
9 | public DateTime BirthDate { get; }
10 | public int Points { get; }
11 |
12 | public Player(string name, string surname, DateTime birthDate, int points)
13 | {
14 | Name = name;
15 | Surname = surname;
16 | BirthDate = birthDate;
17 | Points = points;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ConsoleGUI.Example/Program.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Controls;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Input;
4 | using ConsoleGUI.Space;
5 | using System;
6 | using System.Threading;
7 |
8 | namespace ConsoleGUI.Example
9 | {
10 | class Program
11 | {
12 | static void Main()
13 | {
14 | var clock = new TextBlock();
15 |
16 | var canvas = new Canvas();
17 | var textBox = new TextBox();
18 | var mainConsole = new LogPanel();
19 | var secondaryConsole = new LogPanel();
20 |
21 | var leaderboard = new DataGrid
22 | {
23 | Columns = new[]
24 | {
25 | new DataGrid.ColumnDefinition("Name", 10, p => p.Name, foreground: p => p.Name == "Tomasz" ? (Color?)new Color(100, 100, 220) : null, textAlignment: TextAlignment.Right),
26 | new DataGrid.ColumnDefinition("Surname", 10, p => p.Surname),
27 | new DataGrid.ColumnDefinition("Birth date", 15, p => p.BirthDate.ToShortDateString(), textAlignment: TextAlignment.Center),
28 | new DataGrid.ColumnDefinition("Points", 5, p => p.Points.ToString(), background: p => p.Points > 20 ? (Color?)new Color(0, 220, 0) : null, textAlignment: TextAlignment.Right)
29 | },
30 | Data = new[]
31 | {
32 | new Player("John", "Connor", new DateTime(1985, 2, 28), 10),
33 | new Player("Ellen", "Ripley", new DateTime(2092, 1, 1), 23),
34 | new Player("Jan", "Kowalski", new DateTime(1990, 4, 10), 50),
35 | new Player("Tomasz", "Rewak", new DateTime(1900, 1, 1), 0),
36 | },
37 | Style = DataGridStyle.AllBorders
38 | };
39 |
40 | var tabPanel = new TabPanel();
41 | tabPanel.AddTab("game", new Box
42 | {
43 | HorizontalContentPlacement = Box.HorizontalPlacement.Center,
44 | VerticalContentPlacement = Box.VerticalPlacement.Center,
45 | Content = new Board()
46 | });
47 |
48 | tabPanel.AddTab("leaderboard", new Box
49 | {
50 | HorizontalContentPlacement = Box.HorizontalPlacement.Center,
51 | VerticalContentPlacement = Box.VerticalPlacement.Center,
52 | Content = new Background
53 | {
54 | Color = new Color(45, 74, 85),
55 | Content = new Border
56 | {
57 | BorderStyle = BorderStyle.Single,
58 | Content = leaderboard
59 | }
60 | }
61 | });
62 |
63 | tabPanel.AddTab("about", new Box
64 | {
65 | HorizontalContentPlacement = Box.HorizontalPlacement.Center,
66 | VerticalContentPlacement = Box.VerticalPlacement.Center,
67 | Content = new Boundary
68 | {
69 | MaxWidth = 20,
70 | Content = new VerticalStackPanel
71 | {
72 | Children = new IControl[]
73 | {
74 | new WrapPanel
75 | {
76 | Content = new TextBlock { Text = "This is just a demo application that uses a library for creating a GUI in a console." }
77 | },
78 | new HorizontalSeparator(),
79 | new TextBlock {Text ="By Tomasz Rewak.", Color = new Color(200, 200, 200)}
80 | }
81 | }
82 | }
83 | });
84 |
85 | var dockPanel = new DockPanel
86 | {
87 | Placement = DockPanel.DockedControlPlacement.Top,
88 | DockedControl = new DockPanel
89 | {
90 | Placement = DockPanel.DockedControlPlacement.Right,
91 | DockedControl = new Background
92 | {
93 | Color = new Color(100, 100, 100),
94 | Content = new Boundary
95 | {
96 | MinWidth = 20,
97 | Content = new Box
98 | {
99 | Content = clock,
100 | HorizontalContentPlacement = Box.HorizontalPlacement.Center
101 | }
102 | }
103 | },
104 | FillingControl = new Background
105 | {
106 | Color = ConsoleColor.DarkRed,
107 | Content = new Box
108 | {
109 | Content = new TextBlock { Text = "Center" },
110 | HorizontalContentPlacement = Box.HorizontalPlacement.Center
111 | }
112 | }
113 | },
114 | FillingControl = new DockPanel
115 | {
116 | Placement = DockPanel.DockedControlPlacement.Bottom,
117 | DockedControl = new Boundary
118 | {
119 | MinHeight = 1,
120 | MaxHeight = 1,
121 | Content = new Background
122 | {
123 | Color = new Color(0, 100, 0),
124 | Content = new HorizontalStackPanel
125 | {
126 | Children = new IControl[] {
127 | new TextBlock { Text = " 10 ↑ " },
128 | new VerticalSeparator(),
129 | new TextBlock { Text = " 5 ↓ " }
130 | }
131 | }
132 | }
133 | },
134 | FillingControl = new Overlay
135 | {
136 | BottomContent = new Background
137 | {
138 | Color = new Color(25, 54, 65),
139 | Content = new DockPanel
140 | {
141 | Placement = DockPanel.DockedControlPlacement.Right,
142 | DockedControl = new Background
143 | {
144 | Color = new Color(30, 40, 50),
145 | Content = new Border
146 | {
147 | BorderPlacement = BorderPlacement.Left,
148 | BorderStyle = BorderStyle.Double.WithColor(new Color(50, 60, 70)),
149 | Content = new Boundary
150 | {
151 | MinWidth = 50,
152 | MaxWidth = 50,
153 | Content = new DockPanel
154 | {
155 | Placement = DockPanel.DockedControlPlacement.Bottom,
156 | DockedControl = new Boundary
157 | {
158 | MaxHeight = 1,
159 | Content = new HorizontalStackPanel
160 | {
161 | Children = new IControl[]
162 | {
163 | new Style
164 | {
165 | Foreground = new Color(150, 150, 200),
166 | Content = new TextBlock { Text = @"D:\Software\> " }
167 | },
168 | textBox
169 | }
170 | }
171 | },
172 | FillingControl = new Box
173 | {
174 | VerticalContentPlacement = Box.VerticalPlacement.Bottom,
175 | HorizontalContentPlacement = Box.HorizontalPlacement.Stretch,
176 | Content = mainConsole
177 | }
178 | }
179 | }
180 | }
181 | },
182 | FillingControl = new DockPanel
183 | {
184 | Placement = DockPanel.DockedControlPlacement.Right,
185 | DockedControl = new Background
186 | {
187 | Color = new Color(20, 30, 40),
188 | Content = new Border
189 | {
190 | BorderPlacement = BorderPlacement.Left,
191 | BorderStyle = BorderStyle.Double.WithColor(new Color(50, 60, 70)),
192 | Content = new Boundary
193 | {
194 | MinWidth = 30,
195 | MaxWidth = 30,
196 | Content = new Box
197 | {
198 | VerticalContentPlacement = Box.VerticalPlacement.Bottom,
199 | HorizontalContentPlacement = Box.HorizontalPlacement.Stretch,
200 | Content = secondaryConsole
201 | }
202 | }
203 | }
204 | },
205 | FillingControl = tabPanel
206 | }
207 | }
208 | },
209 | TopContent = new Box
210 | {
211 | HorizontalContentPlacement = Box.HorizontalPlacement.Center,
212 | VerticalContentPlacement = Box.VerticalPlacement.Center,
213 | Content = new Boundary
214 | {
215 | Width = 41,
216 | Height = 12,
217 | Content = canvas
218 | }
219 | }
220 | }
221 | }
222 | };
223 |
224 | var scrollPanel = new VerticalScrollPanel
225 | {
226 | Content = new SimpleDecorator
227 | {
228 | Content = new VerticalStackPanel
229 | {
230 | Children = new IControl[]
231 | {
232 | new WrapPanel { Content = new TextBlock{Text = "Here is a short example of text wrapping" } },
233 | new HorizontalSeparator(),
234 | new WrapPanel { Content = new TextBlock{Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." } },
235 | }
236 | }
237 | }
238 | };
239 |
240 | canvas.Add(new Background
241 | {
242 | Color = new Color(10, 10, 10),
243 | Content = new VerticalStackPanel
244 | {
245 | Children = new IControl[]
246 | {
247 | new Border
248 | {
249 | BorderPlacement = BorderPlacement.Left | BorderPlacement.Top | BorderPlacement.Right,
250 | BorderStyle = BorderStyle.Double.WithColor(new Color(80, 80, 120)),
251 | Content = new Box
252 | {
253 | HorizontalContentPlacement = Box.HorizontalPlacement.Center,
254 | Content = new TextBlock { Text = "Popup 1", Color = new Color(200, 200, 100) }
255 | }
256 | },
257 | new Border
258 | {
259 | BorderPlacement = BorderPlacement.All,
260 | BorderStyle = BorderStyle.Double.WithTopLeft(new Character('╠')).WithTopRight(new Character('╣')).WithColor(new Color(80, 80, 120)),
261 | Content = scrollPanel
262 | }
263 | }
264 | }
265 | }, new Rect(11, 0, 30, 10));
266 |
267 | canvas.Add(new Background
268 | {
269 | Color = new Color(10, 40, 10),
270 | Content = new Border
271 | {
272 | Content = new Box
273 | {
274 | HorizontalContentPlacement = Box.HorizontalPlacement.Center,
275 | VerticalContentPlacement = Box.VerticalPlacement.Center,
276 | Content = new TextBlock { Text = "Popup 2" }
277 | }
278 | }
279 | }, new Rect(0, 7, 17, 5));
280 |
281 | ConsoleManager.Setup();
282 | ConsoleManager.Resize(new Size(150, 40));
283 | ConsoleManager.Content = dockPanel;
284 |
285 | var input = new IInputListener[]
286 | {
287 | scrollPanel,
288 | tabPanel,
289 | new InputController(textBox, mainConsole),
290 | textBox
291 | };
292 |
293 | for (int i = 0; ; i++)
294 | {
295 | Thread.Sleep(10);
296 |
297 | clock.Text = DateTime.Now.ToLongTimeString();
298 | if (i % 200 == 0) secondaryConsole.Add($"Ping {i / 200 + 1}");
299 |
300 | ConsoleManager.ReadInput(input);
301 | ConsoleManager.AdjustBufferSize();
302 | }
303 | }
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/ConsoleGUI.Example/SimpleDecorator.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Controls;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text;
7 |
8 | namespace ConsoleGUI.Example
9 | {
10 | internal sealed class SimpleDecorator : Decorator
11 | {
12 | public override Cell this[Position position] =>
13 | Math.Abs(position.X - Size.Width / 2) > Size.Width / 2 - 3
14 | ? Content[position].WithForeground(new Color(255, 0, 0))
15 | : Content[position];
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ConsoleGUI.Example/TabPanel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using ConsoleGUI;
5 | using ConsoleGUI.Controls;
6 | using ConsoleGUI.Data;
7 | using ConsoleGUI.Input;
8 | using ConsoleGUI.Space;
9 | using ConsoleGUI.UserDefined;
10 |
11 | namespace ConsoleGUI.Example
12 | {
13 | internal class TabPanel : SimpleControl, IInputListener
14 | {
15 | private class Tab
16 | {
17 | private readonly Background hederBackground;
18 |
19 | public IControl Header { get; }
20 | public IControl Content { get; }
21 |
22 | public Tab(string name, IControl content)
23 | {
24 | hederBackground = new Background
25 | {
26 | Content = new Margin
27 | {
28 | Offset = new Offset(1, 0, 1, 0),
29 | Content = new TextBlock { Text = name }
30 | }
31 | };
32 |
33 | Header = new Margin
34 | {
35 | Offset = new Offset(0, 0, 1, 0),
36 | Content = hederBackground
37 | };
38 | Content = content;
39 |
40 | MarkAsInactive();
41 | }
42 |
43 | public void MarkAsActive() => hederBackground.Color = new Color(25, 54, 65);
44 | public void MarkAsInactive() => hederBackground.Color = new Color(65, 24, 25);
45 | }
46 |
47 | private readonly List tabs = new List();
48 | private readonly DockPanel wrapper;
49 | private readonly HorizontalStackPanel tabsPanel;
50 |
51 | private Tab currentTab;
52 |
53 | public TabPanel()
54 | {
55 | tabsPanel = new HorizontalStackPanel();
56 |
57 | wrapper = new DockPanel
58 | {
59 | Placement = DockPanel.DockedControlPlacement.Top,
60 | DockedControl = new Background
61 | {
62 | Color = new Color(25, 25, 52),
63 | Content = new Boundary
64 | {
65 | MinHeight = 1,
66 | MaxHeight = 1,
67 | Content = tabsPanel
68 | }
69 | }
70 | };
71 |
72 | Content = wrapper;
73 | }
74 |
75 | public void AddTab(string name, IControl content)
76 | {
77 | var newTab = new Tab(name, content);
78 | tabs.Add(newTab);
79 | tabsPanel.Add(newTab.Header);
80 | if (tabs.Count == 1)
81 | SelectTab(0);
82 | }
83 |
84 | public void SelectTab(int tab)
85 | {
86 | currentTab?.MarkAsInactive();
87 | currentTab = tabs[tab];
88 | currentTab.MarkAsActive();
89 | wrapper.FillingControl = currentTab.Content;
90 | }
91 |
92 | public void OnInput(InputEvent inputEvent)
93 | {
94 | if (inputEvent.Key.Key != ConsoleKey.Tab) return;
95 |
96 | SelectTab((tabs.IndexOf(currentTab) + 1) % tabs.Count);
97 | inputEvent.Handled = true;
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/ConsoleGUI.MouseExample/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ConsoleGUI.MouseExample/ConsoleGUI.MouseExample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {97B69CAE-3F74-4DEA-86FF-929FC45D3080}
8 | Exe
9 | ConsoleGUI.MouseExample
10 | ConsoleGUI.MouseExample
11 | v4.7.2
12 | 512
13 | true
14 | true
15 |
16 |
17 | AnyCPU
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | AnyCPU
28 | pdbonly
29 | true
30 | bin\Release\
31 | TRACE
32 | prompt
33 | 4
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {E3D8BC25-7EA1-4F81-9C31-BCF2DCBF77A9}
56 | ConsoleGUI
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/ConsoleGUI.MouseExample/MouseHandler.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Space;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace ConsoleGUI.MouseExample
10 | {
11 | public static class MouseHandler
12 | {
13 | private static IntPtr _inputHandle = IntPtr.Zero;
14 | private static InputRecord[] _inputBuffer;
15 |
16 | public static void Initialize()
17 | {
18 | _inputHandle = GetStdHandle(unchecked((uint)-10));
19 | _inputBuffer = new InputRecord[100];
20 | }
21 |
22 | public static void ReadMouseEvents()
23 | {
24 | if (_inputHandle == IntPtr.Zero)
25 | throw new InvalidOperationException("First call the Initialize method of the MouseHandler");
26 |
27 | if (!ReadConsoleInput(_inputHandle, _inputBuffer, (uint)_inputBuffer.Length, out var eventsRead)) return;
28 |
29 | for (int i = 0; i < eventsRead; i++)
30 | {
31 | var inputEvent = _inputBuffer[i];
32 |
33 | if ((inputEvent.EventType & 0x0002) != 0)
34 | ProcessMouseEvent(inputEvent.MouseEvent);
35 | else
36 | WriteConsoleInput(_inputHandle, new[] { inputEvent }, 1, out var eventsWritten);
37 | }
38 | }
39 |
40 | private static void ProcessMouseEvent(in MouseRecord mouseEvent)
41 | {
42 | ConsoleManager.MousePosition = new Position(mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y);
43 | ConsoleManager.MouseDown = (mouseEvent.ButtonState & 0x0001) != 0;
44 | }
45 |
46 | private struct COORD
47 | {
48 | public short X;
49 | public short Y;
50 | }
51 |
52 | private struct MouseRecord
53 | {
54 | public COORD MousePosition;
55 | public uint ButtonState;
56 | public uint ControlKeyState;
57 | public uint EventFlags;
58 | }
59 |
60 | private struct InputRecord
61 | {
62 | public ushort EventType;
63 | public MouseRecord MouseEvent;
64 | }
65 |
66 | [DllImport("kernel32.dll")]
67 | public static extern IntPtr GetStdHandle(uint nStdHandle);
68 |
69 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
70 | private static extern bool ReadConsoleInput(IntPtr hConsoleInput, [Out] InputRecord[] lpBuffer, uint nLength, out uint lpNumberOfEventsRead);
71 |
72 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
73 | private static extern bool WriteConsoleInput(IntPtr hConsoleInput, InputRecord[] lpBuffer, uint nLength, out uint lpNumberOfEventsWritten);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ConsoleGUI.MouseExample/Program.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Api;
2 | using ConsoleGUI.Controls;
3 | using ConsoleGUI.Data;
4 | using ConsoleGUI.Input;
5 | using ConsoleGUI.Space;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Text;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 |
13 | namespace ConsoleGUI.MouseExample
14 | {
15 | class InputController : IInputListener
16 | {
17 | private readonly TextBox _textBox1;
18 | private readonly TextBox _textBox2;
19 | private readonly Button _button;
20 |
21 | private TextBox _selectedTextBox;
22 |
23 | public InputController(TextBox textBox1, TextBox textBox2, Button button)
24 | {
25 | _textBox1 = textBox1;
26 | _textBox2 = textBox2;
27 | _button = button;
28 |
29 | _textBox1.ShowCaret = false;
30 | _textBox2.ShowCaret = false;
31 |
32 | _textBox1.Clicked += TextBoxClicked;
33 | _textBox2.Clicked += TextBoxClicked;
34 | _button.Clicked += ButtonClicked;
35 | }
36 |
37 | private void TextBoxClicked(object sender, EventArgs e)
38 | {
39 | Select(sender as TextBox);
40 | }
41 |
42 | private void ButtonClicked(object sender, EventArgs e)
43 | {
44 | _textBox1.Text = "";
45 | _textBox2.Text = "";
46 |
47 | Select(_textBox1);
48 | }
49 |
50 | private void Select(TextBox textBox)
51 | {
52 | if (_selectedTextBox != null) _selectedTextBox.ShowCaret = false;
53 | _selectedTextBox = textBox as TextBox;
54 | if (_selectedTextBox != null) _selectedTextBox.ShowCaret = true;
55 | }
56 |
57 | void IInputListener.OnInput(InputEvent inputEvent)
58 | {
59 | if (inputEvent.Key.Key == ConsoleKey.Tab)
60 | {
61 | Select(_selectedTextBox == _textBox1 ? _textBox2 : _textBox1);
62 | inputEvent.Handled = true;
63 | }
64 | else
65 | {
66 | (_selectedTextBox as IInputListener)?.OnInput(inputEvent);
67 | }
68 | }
69 | }
70 |
71 | class Program
72 | {
73 | static void Main()
74 | {
75 | MouseHandler.Initialize();
76 |
77 | ConsoleManager.Setup();
78 | ConsoleManager.Console = new SimplifiedConsole();
79 | ConsoleManager.Resize(new Size(80, 30));
80 |
81 | var textBox1 = new TextBox { Text = "Hello world" };
82 | var textBox2 = new TextBox { Text = "Test" };
83 | var textBlock = new TextBlock();
84 | var button = new Button { Content = new Margin { Offset = new Offset(4, 1, 4, 1), Content = new TextBlock { Text = "Button" } } };
85 |
86 | ConsoleManager.Content = new Background
87 | {
88 | Color = new Color(100, 0, 0),
89 | Content = new Margin
90 | {
91 | Offset = new Offset(5, 2, 5, 2),
92 | Content = new VerticalStackPanel
93 | {
94 | Children = new IControl[]
95 | {
96 | textBlock,
97 | new HorizontalSeparator(),
98 | new TextBlock { Text = "Simple text box" },
99 | new Background{
100 | Color = Color.Black,
101 | Content = textBox1
102 | },
103 | new HorizontalSeparator(),
104 | new TextBlock { Text = "Wrapped text box" },
105 | new Boundary
106 | {
107 | Width = 10,
108 | Content = new Background
109 | {
110 | Color = new Color(0, 100, 0),
111 | Content = new WrapPanel { Content = new Boundary{ MinWidth = 10, Content = textBox2 } }
112 | }
113 | },
114 | new HorizontalSeparator(),
115 | new Boundary
116 | {
117 | Height = 1,
118 | Content = new HorizontalStackPanel
119 | {
120 | Children = new IControl[]
121 | {
122 | new TextBlock {Text = "Check box: "},
123 | new CheckBox {
124 | TrueCharacter = new Character('Y', new Color(0, 255, 0)),
125 | FalseCharacter = new Character('N', new Color(255, 0, 0))
126 | }
127 | }
128 | }
129 | },
130 | new HorizontalSeparator(),
131 | new Box { Content = button }
132 | }
133 | }
134 | }
135 | };
136 |
137 | var input = new IInputListener[]
138 | {
139 | new InputController(textBox1, textBox2, button)
140 | };
141 |
142 | while (true)
143 | {
144 | ConsoleManager.AdjustBufferSize();
145 | ConsoleManager.ReadInput(input);
146 |
147 | MouseHandler.ReadMouseEvents();
148 |
149 | textBlock.Text = $"Mouse position: ({ConsoleManager.MousePosition?.X}, {ConsoleManager.MousePosition?.Y})";
150 |
151 | Thread.Sleep(50);
152 | }
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/ConsoleGUI.MouseExample/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("ConsoleGUI.MouseExample")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("ConsoleGUI.MouseExample")]
13 | [assembly: AssemblyCopyright("Copyright © 2019")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("97b69cae-3f74-4dea-86ff-929fc45d3080")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/ConsoleGUI.Test/Assembly/Assembly.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
4 |
--------------------------------------------------------------------------------
/ConsoleGUI.Test/Common/ControlTest.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Space;
3 | using Moq;
4 | using NUnit.Framework;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Test.Common
10 | {
11 | internal abstract class TestControl : Control
12 | {
13 | public new FreezeContext Freeze() => base.Freeze();
14 | public new void Update(in Rect rect) => base.Update(rect);
15 | public new void Redraw() => base.Redraw();
16 | public new void Resize(in Size size) => base.Resize(size);
17 | }
18 |
19 | [TestFixture]
20 | public class ControlTest
21 | {
22 | [Test]
23 | public void Control_DoesntUpdate_WhenFrozen()
24 | {
25 | var context = new Mock();
26 | context.SetupGet(c => c.MinSize).Returns(new Size(20, 20));
27 | context.SetupGet(c => c.MaxSize).Returns(new Size(20, 20));
28 |
29 | var control = new Mock();
30 | (control.Object as IControl).Context = context.Object;
31 |
32 | control.Object.Freeze();
33 | control.Object.Update(new Rect(2, 3, 10, 11));
34 |
35 | context.Verify(c => c.Update(control.Object, It.IsAny()), Times.Never);
36 | }
37 |
38 | [Test]
39 | public void Control_Updates_WhenUnfrozen()
40 | {
41 | var context = new Mock();
42 | context.SetupGet(c => c.MinSize).Returns(new Size(20, 20));
43 | context.SetupGet(c => c.MaxSize).Returns(new Size(20, 20));
44 |
45 | var control = new Mock();
46 | (control.Object as IControl).Context = context.Object;
47 |
48 | using (control.Object.Freeze())
49 | control.Object.Update(new Rect(2, 3, 10, 11));
50 |
51 | context.Verify(c => c.Update(control.Object, new Rect(2, 3, 10, 11)), Times.Once);
52 | }
53 |
54 | [Test]
55 | public void Control_Updates_OnlyOnceAfterUnfrozen()
56 | {
57 | var context = new Mock();
58 | context.SetupGet(c => c.MinSize).Returns(new Size(20, 20));
59 | context.SetupGet(c => c.MaxSize).Returns(new Size(20, 20));
60 |
61 | var control = new Mock();
62 | (control.Object as IControl).Context = context.Object;
63 | context.Reset();
64 |
65 | using (control.Object.Freeze())
66 | {
67 | control.Object.Update(new Rect(2, 3, 10, 11));
68 | control.Object.Update(new Rect(1, 1, 2, 4));
69 | }
70 |
71 | context.Verify(c => c.Update(control.Object, It.Ref.IsAny), Times.Once);
72 | }
73 |
74 | [Test]
75 | public void Control_Updates_AllUpdatedCellsAfterUnfrozen()
76 | {
77 | var context = new Mock();
78 | context.SetupGet(c => c.MinSize).Returns(new Size(20, 20));
79 | context.SetupGet(c => c.MaxSize).Returns(new Size(20, 20));
80 |
81 | var control = new Mock();
82 | (control.Object as IControl).Context = context.Object;
83 | context.Reset();
84 |
85 | using (control.Object.Freeze())
86 | {
87 | control.Object.Update(new Rect(2, 3, 10, 11));
88 | control.Object.Update(new Rect(1, 1, 2, 4));
89 | }
90 |
91 | context.Verify(c => c.Update(control.Object, new Rect(1, 1, 11, 13)), Times.Once);
92 | }
93 |
94 | [Test]
95 | public void Control_Redraws_IfAskedToRedraw()
96 | {
97 | var context = new Mock();
98 | context.SetupGet(c => c.MinSize).Returns(new Size(20, 20));
99 | context.SetupGet(c => c.MaxSize).Returns(new Size(20, 20));
100 |
101 | var control = new Mock();
102 | (control.Object as IControl).Context = context.Object;
103 | context.Reset();
104 |
105 | using (control.Object.Freeze())
106 | {
107 | control.Object.Update(new Rect(2, 3, 10, 11));
108 | control.Object.Redraw();
109 | control.Object.Update(new Rect(1, 1, 2, 4));
110 | }
111 |
112 | context.Verify(c => c.Redraw(control.Object));
113 | context.Verify(c => c.Update(control.Object, It.Ref.IsAny), Times.Never);
114 | }
115 |
116 | [Test]
117 | public void Control_Updates_RedrawsIfSizeChanged()
118 | {
119 | var context = new Mock();
120 | context.SetupGet(c => c.MinSize).Returns(new Size(20, 20));
121 | context.SetupGet(c => c.MaxSize).Returns(new Size(40, 40));
122 |
123 | var control = new Mock();
124 | (control.Object as IControl).Context = context.Object;
125 | control.Object.Resize(new Size(30, 30));
126 | context.Reset();
127 |
128 | control.Object.Resize(new Size(35, 35));
129 |
130 | context.Verify(c => c.Redraw(control.Object));
131 | context.Verify(c => c.Update(control.Object, It.Ref.IsAny), Times.Never);
132 | }
133 |
134 | [Test]
135 | public void Control_Updates_UpdatedIfSizeDidintChange()
136 | {
137 | var context = new Mock();
138 | context.SetupGet(c => c.MinSize).Returns(new Size(20, 20));
139 | context.SetupGet(c => c.MaxSize).Returns(new Size(40, 40));
140 |
141 | var control = new Mock();
142 | (control.Object as IControl).Context = context.Object;
143 | control.Object.Resize(new Size(30, 30));
144 | context.Reset();
145 |
146 | using (control.Object.Freeze())
147 | {
148 | control.Object.Resize(new Size(35, 35));
149 | control.Object.Resize(new Size(30, 30));
150 | }
151 |
152 | context.Verify(c => c.Redraw(control.Object), Times.Never);
153 | context.Verify(c => c.Update(control.Object, It.Ref.IsAny), Times.Once);
154 | context.Verify(c => c.Update(control.Object, new Rect(0, 0, 30, 30)), Times.Once);
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/ConsoleGUI.Test/Common/DrawingContextTest.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Space;
3 | using Moq;
4 | using NUnit.Framework;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Test.Common
10 | {
11 | [TestFixture]
12 | public class DrawingContextTest
13 | {
14 | [Test]
15 | public void DummyDrawingContext_HasEmptySize()
16 | {
17 | var drawingContext = DrawingContext.Dummy;
18 |
19 | Assert.AreEqual(Size.Empty, drawingContext.Size);
20 | }
21 |
22 | [Test]
23 | public void DrawingContext_PropagatesUpdates()
24 | {
25 | var listener = new Mock();
26 | var control = new Mock();
27 |
28 | var drawingContext = new DrawingContext(listener.Object, control.Object);
29 | drawingContext.SetLimits(new Size(10, 10), new Size(10, 10));
30 |
31 | listener.Reset();
32 | drawingContext.Update(control.Object, new Rect(1, 1, 5, 5));
33 |
34 | listener.Verify(l => l.OnUpdate(drawingContext, new Rect(1, 1, 5, 5)));
35 | }
36 |
37 | [Test]
38 | public void DrawingContext_PropagatesUpdates_WithOffset()
39 | {
40 | var listener = new Mock();
41 | var control = new Mock();
42 |
43 | var drawingContext = new DrawingContext(listener.Object, control.Object);
44 | drawingContext.SetLimits(new Size(10, 10), new Size(10, 10));
45 | drawingContext.SetOffset(new Vector(2, 2));
46 |
47 | listener.Reset();
48 | drawingContext.Update(control.Object, new Rect(1, 1, 5, 5));
49 |
50 | listener.Verify(l => l.OnUpdate(drawingContext, new Rect(3, 3, 5, 5)));
51 | }
52 |
53 | [Test]
54 | public void DrawingContext_UpdatesOldRect_AfterOffsetChage()
55 | {
56 | var listener = new Mock();
57 | var control = new Mock();
58 | control.SetupGet(c => c.Size).Returns(new Size(5, 5));
59 |
60 | var drawingContext = new DrawingContext(listener.Object, control.Object);
61 | drawingContext.SetLimits(new Size(10, 10), new Size(10, 10));
62 | drawingContext.SetOffset(new Vector(2, 2));
63 |
64 | listener.Reset();
65 | drawingContext.SetOffset(new Vector(4, 4));
66 |
67 | listener.Verify(l => l.OnUpdate(drawingContext, new Rect(2, 2, 5, 5)));
68 | }
69 |
70 | [Test]
71 | public void DrawingContext_PropagatesSizeLimits()
72 | {
73 | bool raised = false;
74 |
75 | var drawingContext = new DrawingContext(null, null);
76 | drawingContext.SizeLimitsChanged += c => raised |= c == drawingContext;
77 |
78 | drawingContext.SetLimits(new Size(10, 15), new Size(20, 25));
79 |
80 | Assert.IsTrue(raised);
81 | }
82 |
83 | [Test]
84 | public void DrawingContext_DoesntPropagateUpdates_WhenDisposed()
85 | {
86 | var listener = new Mock();
87 | var control = new Mock();
88 |
89 | var drawingContext = new DrawingContext(listener.Object, control.Object);
90 | drawingContext.Dispose();
91 |
92 | listener.Reset();
93 | drawingContext.Update(control.Object, new Rect(1, 1, 5, 5));
94 |
95 | listener.Verify(l => l.OnUpdate(It.IsAny(), It.IsAny()), Times.Never);
96 | }
97 |
98 | [Test]
99 | public void DrawingContext_ReturnsCorrentCharacter()
100 | {
101 | var listener = new Mock();
102 | var control = new Mock();
103 |
104 | var drawingContext = new DrawingContext(listener.Object, control.Object);
105 | drawingContext.SetOffset(new Vector(5, 10));
106 |
107 | var character = drawingContext[new Position(7, 13)];
108 |
109 | control.Verify(l => l[new Position(2, 3)]);
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/ConsoleGUI.Test/ConsoleGUI.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.1
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/ConsoleGUI.Test/Controls/BorderTest.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Controls;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using Moq;
5 | using NUnit.Framework;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Text;
9 |
10 | namespace ConsoleGUI.Test.Controls
11 | {
12 | [TestFixture]
13 | public class BorderTest
14 | {
15 | [Test]
16 | public void Border_AdjustsItsSize_ToTheContent()
17 | {
18 | var context = new Mock();
19 | context.SetupGet(c => c.MinSize).Returns(new Size(0, 0));
20 | context.SetupGet(c => c.MaxSize).Returns(new Size(100, 100));
21 |
22 | var content = new Mock();
23 | content.SetupGet(c => c.Size).Returns(new Size(20, 30));
24 |
25 | var border = new Border
26 | {
27 | Content = content.Object
28 | };
29 | (border as IControl).Context = context.Object;
30 |
31 | Assert.AreEqual(new Size(22, 32), border.Size);
32 | }
33 |
34 | [Test]
35 | public void EmptyBorder_AdjustsItsSize_ToTheMinSizeOfItsContainer()
36 | {
37 | var context = new Mock();
38 | context.SetupGet(c => c.MinSize).Returns(new Size(10, 20));
39 | context.SetupGet(c => c.MaxSize).Returns(new Size(40, 50));
40 |
41 | var border = new Border();
42 | (border as IControl).Context = context.Object;
43 |
44 | Assert.AreEqual(new Size(10, 20), border.Size);
45 | }
46 |
47 | [Test]
48 | public void Border_ReturnsEmptyCharacter_ForItsInterior()
49 | {
50 | var context = new Mock();
51 | context.SetupGet(c => c.MinSize).Returns(new Size(3, 3));
52 | context.SetupGet(c => c.MaxSize).Returns(new Size(3, 3));
53 |
54 | var border = new Border();
55 | (border as IControl).Context = context.Object;
56 |
57 | Assert.AreEqual(Character.Empty, border[new Position(1, 1)].Character);
58 | }
59 |
60 | [Test]
61 | public void Border_ReturnsEmptyCharacter_OutsideOfItsSize()
62 | {
63 | var context = new Mock();
64 | context.SetupGet(c => c.MinSize).Returns(new Size(3, 3));
65 | context.SetupGet(c => c.MaxSize).Returns(new Size(3, 3));
66 |
67 | var border = new Border();
68 | (border as IControl).Context = context.Object;
69 |
70 | Assert.AreEqual(Character.Empty, border[new Position(3, 1)].Character);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/ConsoleGUI.Test/Utils/ColorConverterTest.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Utils;
2 | using NUnit.Framework;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace ConsoleGUI.Test.Utils
8 | {
9 | [TestFixture]
10 | class ColorConverterTest
11 | {
12 | [TestCase(ConsoleColor.Black)]
13 | [TestCase(ConsoleColor.DarkBlue)]
14 | [TestCase(ConsoleColor.DarkGreen)]
15 | [TestCase(ConsoleColor.DarkCyan)]
16 | [TestCase(ConsoleColor.DarkRed)]
17 | [TestCase(ConsoleColor.DarkMagenta)]
18 | [TestCase(ConsoleColor.DarkYellow)]
19 | [TestCase(ConsoleColor.Gray)]
20 | [TestCase(ConsoleColor.DarkGray)]
21 | [TestCase(ConsoleColor.Blue)]
22 | [TestCase(ConsoleColor.Green)]
23 | [TestCase(ConsoleColor.Cyan)]
24 | [TestCase(ConsoleColor.Red)]
25 | [TestCase(ConsoleColor.Magenta)]
26 | [TestCase(ConsoleColor.Yellow)]
27 | [TestCase(ConsoleColor.White)]
28 | public void ConsoleColor_IsRestoredCorrectly(ConsoleColor initialColor)
29 | {
30 | var color = ColorConverter.GetColor(initialColor);
31 | var convertedColor = ColorConverter.GetNearestConsoleColor(color);
32 |
33 | Assert.AreEqual(initialColor, convertedColor);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ConsoleGUI.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.28922.388
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleGUI.Example", "ConsoleGUI.Example\ConsoleGUI.Example.csproj", "{E8F3510F-8D4A-46B4-8E34-8ABE157C6B7E}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleGUI", "ConsoleGUI\ConsoleGUI.csproj", "{E3D8BC25-7EA1-4F81-9C31-BCF2DCBF77A9}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleGUI.Test", "ConsoleGUI.Test\ConsoleGUI.Test.csproj", "{ABC28308-500E-48E9-AD7E-AA8977370D35}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleGUI.MouseExample", "ConsoleGUI.MouseExample\ConsoleGUI.MouseExample.csproj", "{97B69CAE-3F74-4DEA-86FF-929FC45D3080}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {E8F3510F-8D4A-46B4-8E34-8ABE157C6B7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {E8F3510F-8D4A-46B4-8E34-8ABE157C6B7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {E8F3510F-8D4A-46B4-8E34-8ABE157C6B7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {E8F3510F-8D4A-46B4-8E34-8ABE157C6B7E}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {E3D8BC25-7EA1-4F81-9C31-BCF2DCBF77A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {E3D8BC25-7EA1-4F81-9C31-BCF2DCBF77A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {E3D8BC25-7EA1-4F81-9C31-BCF2DCBF77A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {E3D8BC25-7EA1-4F81-9C31-BCF2DCBF77A9}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {ABC28308-500E-48E9-AD7E-AA8977370D35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {ABC28308-500E-48E9-AD7E-AA8977370D35}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {ABC28308-500E-48E9-AD7E-AA8977370D35}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {ABC28308-500E-48E9-AD7E-AA8977370D35}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {97B69CAE-3F74-4DEA-86FF-929FC45D3080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {97B69CAE-3F74-4DEA-86FF-929FC45D3080}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {97B69CAE-3F74-4DEA-86FF-929FC45D3080}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {97B69CAE-3F74-4DEA-86FF-929FC45D3080}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {79B9F117-2044-4C5C-96D3-DC719F9319BD}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/ConsoleGUI/Api/IConsole.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Data;
2 | using ConsoleGUI.Space;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace ConsoleGUI.Api
8 | {
9 | public interface IConsole
10 | {
11 | Size Size { get; set; }
12 | bool KeyAvailable { get; }
13 |
14 | void Initialize();
15 | void OnRefresh();
16 | void Write(Position position, in Character character);
17 | ConsoleKeyInfo ReadKey();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ConsoleGUI/Api/SimplifiedConsole.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using ConsoleGUI.Data;
5 | using ConsoleGUI.Space;
6 | using ConsoleGUI.Utils;
7 |
8 | namespace ConsoleGUI.Api
9 | {
10 | public class SimplifiedConsole : StandardConsole
11 | {
12 | private Position _lastPosition;
13 |
14 | public override void Write(Position position, in Character character)
15 | {
16 | if (position == _lastPosition) return;
17 |
18 | var content = character.Content ?? ' ';
19 | var foreground = character.Foreground ?? Color.White;
20 | var background = character.Background ?? Color.Black;
21 |
22 | if (content == '\n') content = ' ';
23 |
24 | SafeConsole.WriteOrThrow(
25 | position.X,
26 | position.Y,
27 | ColorConverter.GetNearestConsoleColor(background),
28 | ColorConverter.GetNearestConsoleColor(foreground),
29 | content);
30 | }
31 |
32 | public override void OnRefresh()
33 | {
34 | base.OnRefresh();
35 | _lastPosition = Size.AsRect().BottomRightCorner;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ConsoleGUI/Api/StandardConsole.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Data;
2 | using ConsoleGUI.Space;
3 | using ConsoleGUI.Utils;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text;
7 |
8 | namespace ConsoleGUI.Api
9 | {
10 | public class StandardConsole : IConsole
11 | {
12 | public Size Size
13 | {
14 | get => new Size(Console.WindowWidth, Console.WindowHeight);
15 | set
16 | {
17 | SafeConsole.SetCursorPosition(0, 0);
18 | SafeConsole.SetWindowPosition(0, 0);
19 | if (!(Size <= value)) SafeConsole.SetWindowSize(1, 1);
20 | SafeConsole.SetBufferSize(value.Width, value.Height);
21 | if (Size != value) SafeConsole.SetWindowSize(value.Width, value.Height);
22 | Initialize();
23 | }
24 | }
25 |
26 | public bool KeyAvailable => Console.KeyAvailable;
27 |
28 | public virtual void Initialize()
29 | {
30 | SafeConsole.SetUtf8();
31 | SafeConsole.HideCursor();
32 | SafeConsole.Clear();
33 | }
34 |
35 | public virtual void OnRefresh()
36 | {
37 | SafeConsole.HideCursor();
38 | }
39 |
40 | public virtual void Write(Position position, in Character character)
41 | {
42 | var content = character.Content ?? ' ';
43 | var foreground = character.Foreground ?? Color.White;
44 | var background = character.Background ?? Color.Black;
45 |
46 | if (content == '\n') content = ' ';
47 |
48 | SafeConsole.WriteOrThrow(position.X, position.Y, $"\x1b[38;2;{foreground.Red};{foreground.Green};{foreground.Blue}m\x1b[48;2;{background.Red};{background.Green};{background.Blue}m{content}");
49 | }
50 |
51 | public ConsoleKeyInfo ReadKey()
52 | {
53 | return Console.ReadKey(true);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/ConsoleGUI/Assembly/Assembly.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("ConsoleGUI.Test")]
4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
5 |
--------------------------------------------------------------------------------
/ConsoleGUI/Buffering/ConsoleBuffer.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Data;
2 | using ConsoleGUI.Space;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace ConsoleGUI.Buffering
8 | {
9 | internal class ConsoleBuffer
10 | {
11 | private Cell?[,] _buffer = new Cell?[0, 0];
12 |
13 | public Size Size => new Size(_buffer.GetLength(0), _buffer.GetLength(1));
14 |
15 | public void Initialize(in Size size)
16 | {
17 | _buffer = new Cell?[size.Width, size.Height];
18 | }
19 |
20 | public void Clear()
21 | {
22 | for (int i = 0; i < _buffer.GetLength(0); i++)
23 | for (int j = 0; j < _buffer.GetLength(1); j++)
24 | _buffer[i, j] = null;
25 | }
26 |
27 | public bool Update(in Position position, in Cell newCell)
28 | {
29 | ref var cell = ref _buffer[position.X, position.Y];
30 | bool characterChanged = cell?.Character != newCell.Character;
31 |
32 | cell = newCell;
33 |
34 | return characterChanged;
35 | }
36 |
37 | public MouseContext? GetMouseContext(Position position)
38 | {
39 | if (!Size.Contains(position)) return null;
40 |
41 | return _buffer[position.X, position.Y]?.MouseListener;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ConsoleGUI/Common/Control.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Data;
2 | using ConsoleGUI.Space;
3 | using ConsoleGUI.Utils;
4 | using System;
5 |
6 | namespace ConsoleGUI.Common
7 | {
8 | public abstract class Control : IControl
9 | {
10 | private FreezeLock _freezeLock;
11 | private Rect _updatedRect;
12 | private Size _previousSize;
13 |
14 | public abstract Cell this[Position position] { get; }
15 |
16 | protected abstract void Initialize();
17 |
18 | private IDrawingContext _context;
19 | IDrawingContext IControl.Context
20 | {
21 | get => _context;
22 | set => Setter
23 | .SetContext(ref _context, value, OnSizeLimitsChanged)
24 | .Then(UpdateSizeLimits);
25 | }
26 |
27 | public Size Size { get; private set; }
28 | protected Size MinSize { get; private set; }
29 | protected Size MaxSize { get; private set; }
30 |
31 | protected void Redraw()
32 | {
33 | Resize(Size);
34 | }
35 |
36 | protected void Resize(in Size newSize)
37 | {
38 | using (Freeze())
39 | {
40 | Size = Size.Clip(MinSize, newSize, MaxSize);
41 | _updatedRect = Rect.OfSize(Size);
42 | }
43 | }
44 |
45 | protected void Update(in Rect rect)
46 | {
47 | using (Freeze())
48 | _updatedRect = Rect.Surround(_updatedRect, rect);
49 | }
50 |
51 | protected internal FreezeContext Freeze()
52 | {
53 | return new FreezeContext(this);
54 | }
55 |
56 | private void OnSizeLimitsChanged(IDrawingContext context)
57 | {
58 | UpdateSizeLimits();
59 | }
60 |
61 | private void UpdateSizeLimits()
62 | {
63 | if (MinSize == _context?.MinSize && MaxSize == _context?.MaxSize) return;
64 |
65 | MinSize = _context?.MinSize ?? Size.Empty;
66 | MaxSize = _context?.MaxSize ?? Size.Empty;
67 |
68 | Initialize();
69 | }
70 |
71 | protected internal struct FreezeContext : IDisposable
72 | {
73 | private readonly Control _control;
74 |
75 | private bool RequiresRedraw => _control.Size != _control._previousSize;
76 | private bool RequiresUpdate => !_control._updatedRect.IsEmpty;
77 |
78 | public FreezeContext(Control control)
79 | {
80 | _control = control;
81 |
82 | if (_control._freezeLock.IsUnfrozen)
83 | {
84 | _control._previousSize = _control.Size;
85 | _control._updatedRect = Rect.Empty;
86 | }
87 |
88 | _control._freezeLock.Freeze();
89 | }
90 |
91 | public void Dispose()
92 | {
93 | _control._freezeLock.Unfreeze();
94 |
95 | if (_control._freezeLock.IsFrozen) return;
96 |
97 | if (RequiresRedraw)
98 | Redraw();
99 | else if (RequiresUpdate)
100 | Update();
101 | }
102 |
103 | private void Redraw() => _control._context?.Redraw(_control);
104 | private void Update() => _control._context?.Update(_control, _control._updatedRect);
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/ConsoleGUI/Common/DrawingContext.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Data;
2 | using ConsoleGUI.Space;
3 | using System;
4 | using System.Runtime.CompilerServices;
5 |
6 | namespace ConsoleGUI.Common
7 | {
8 | public interface IDrawingContextListener
9 | {
10 | void OnRedraw(DrawingContext drawingContext);
11 | void OnUpdate(DrawingContext drawingContext, Rect rect);
12 | }
13 |
14 | public sealed class DrawingContext : IDrawingContext, IDisposable
15 | {
16 | public IDrawingContextListener Parent { get; private set; }
17 | public IControl Child { get; private set; }
18 |
19 | public DrawingContext(IDrawingContextListener parent, IControl control)
20 | {
21 | if (control == null) return;
22 |
23 | Parent = parent;
24 |
25 | Child = control;
26 | Child.Context = this;
27 | }
28 |
29 | public static DrawingContext Dummy => new DrawingContext(null, null);
30 |
31 | public void Dispose()
32 | {
33 | Parent = null;
34 | Child = null;
35 | }
36 |
37 | public Size MinSize { get; private set; }
38 | public Size MaxSize { get; private set; }
39 | public Vector Offset { get; private set; }
40 |
41 | public Size Size => Child?.Size ?? Size.Empty;
42 |
43 | public Cell this[Position position]
44 | {
45 | get
46 | {
47 | return Child[position.Move(-Offset)];
48 | }
49 | }
50 |
51 | public void SetLimits(in Size minSize, in Size maxSize)
52 | {
53 | if (MinSize == minSize && MaxSize == maxSize) return;
54 |
55 | MinSize = minSize;
56 | MaxSize = maxSize;
57 |
58 | SizeLimitsChanged?.Invoke(this);
59 | }
60 |
61 | public void SetOffset(in Vector offset)
62 | {
63 | if (offset == Offset) return;
64 |
65 | Update(Child, Rect.OfSize(Size));
66 | Offset = offset;
67 | Update(Child, Rect.OfSize(Size));
68 | }
69 |
70 | public bool Contains(in Position position)
71 | {
72 | if (Child == null) return false;
73 |
74 | return Child.Size.Contains(position.Move(-Offset));
75 | }
76 |
77 | public void Redraw(IControl control)
78 | {
79 | if (control != Child) return;
80 |
81 | Parent?.OnRedraw(this);
82 | }
83 |
84 | public void Update(IControl control, in Rect rect)
85 | {
86 | if (control != Child) return;
87 |
88 | Parent?.OnUpdate(this, rect.Move(Offset));
89 | }
90 |
91 | public event SizeLimitsChangedHandler SizeLimitsChanged;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/ConsoleGUI/ConsoleGUI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | true
6 | Tomasz Rewak
7 |
8 | ConsoleGUI
9 | ConsoleGUI is a simple C# framework for creating console based GUI applications.
10 | Tomasz Rewak
11 | MIT
12 | https://github.com/TomaszRewak/C-sharp-console-gui-framework
13 | https://github.com/TomaszRewak/C-sharp-console-gui-framework
14 | console, gui, framework
15 | true
16 | 1.4.2
17 | 1.4.2 - Fixing the break panel drawing new line characters
18 | 1.4.1 - Fixing the refresh bug on swapping of the ConsoleManager.Content
19 | 1.4.0 - Customizable data grid style
20 | 1.3.0 - Making the ScrollUpKey and the ScrollDownKey configurable in the VerticalScrollPanel
21 | 1.2.2 - Fixing AdjustWindowSize exception when window has 0 height
22 | 1.2.1 - Fixing exceptions on window resizing
23 | 1.2.0 - Adding modifiable IConsole interface and fixing collection setters in HorizontalStackPanel and VerticalStackPanel
24 | 1.1.0 - Adding BreakPanel and Decorator
25 | 1.0.5 - Adding basic mouse support
26 | 1.0.4 - Adding support for window resizing
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/ConsoleGUI/ConsoleManager.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Api;
2 | using ConsoleGUI.Buffering;
3 | using ConsoleGUI.Common;
4 | using ConsoleGUI.Controls;
5 | using ConsoleGUI.Data;
6 | using ConsoleGUI.Input;
7 | using ConsoleGUI.Space;
8 | using ConsoleGUI.Utils;
9 | using System;
10 | using System.Collections.Generic;
11 | using System.Text;
12 | using System.Threading;
13 |
14 | namespace ConsoleGUI
15 | {
16 | public static class ConsoleManager
17 | {
18 | private class ConsoleManagerDrawingContextListener : IDrawingContextListener
19 | {
20 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
21 | {
22 | if (_freezeLock.IsFrozen) return;
23 | Redraw();
24 | }
25 |
26 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
27 | {
28 | if (_freezeLock.IsFrozen) return;
29 | Update(rect);
30 | }
31 | }
32 |
33 | private static readonly ConsoleBuffer _buffer = new ConsoleBuffer();
34 | private static FreezeLock _freezeLock;
35 |
36 | private static DrawingContext _contentContext = DrawingContext.Dummy;
37 | private static DrawingContext ContentContext
38 | {
39 | get => _contentContext;
40 | set => Setter
41 | .SetDisposable(ref _contentContext, value)
42 | .Then(Initialize);
43 | }
44 |
45 | private static IControl _content;
46 | public static IControl Content
47 | {
48 | get => _content;
49 | set => Setter
50 | .Set(ref _content, value)
51 | .Then(BindContent);
52 | }
53 |
54 | private static IConsole _console = new StandardConsole();
55 | public static IConsole Console
56 | {
57 | get => _console;
58 | set => Setter
59 | .Set(ref _console, value)
60 | .Then(Initialize);
61 | }
62 |
63 | private static Position? _mousePosition;
64 | public static Position? MousePosition
65 | {
66 | get => _mousePosition;
67 | set => Setter
68 | .Set(ref _mousePosition, value)
69 | .Then(UpdateMouseContext);
70 | }
71 |
72 | private static bool _mouseDown;
73 | public static bool MouseDown
74 | {
75 | get => _mouseDown;
76 | set
77 | {
78 | if (_mouseDown && !value)
79 | MouseContext?.MouseListener?.OnMouseUp(MouseContext.Value.RelativePosition);
80 | if (!_mouseDown && value)
81 | MouseContext?.MouseListener?.OnMouseDown(MouseContext.Value.RelativePosition);
82 |
83 | _mouseDown = value;
84 | }
85 | }
86 |
87 | private static MouseContext? _mouseContext;
88 | private static MouseContext? MouseContext
89 | {
90 | get => _mouseContext;
91 | set
92 | {
93 | if (value?.MouseListener != _mouseContext?.MouseListener)
94 | {
95 | _mouseContext?.MouseListener.OnMouseLeave();
96 | value?.MouseListener.OnMouseEnter();
97 | value?.MouseListener.OnMouseMove(value.Value.RelativePosition);
98 | }
99 | else if (value.HasValue && value.Value.RelativePosition != _mouseContext?.RelativePosition)
100 | {
101 | value.Value.MouseListener.OnMouseMove(value.Value.RelativePosition);
102 | }
103 |
104 | _mouseContext = value;
105 | }
106 | }
107 |
108 | public static Size WindowSize => Console.Size;
109 | public static Size BufferSize => _buffer.Size;
110 |
111 | private static void Initialize()
112 | {
113 | var consoleSize = BufferSize;
114 |
115 | Console.Initialize();
116 | _buffer.Clear();
117 |
118 | _freezeLock.Freeze();
119 | ContentContext.SetLimits(consoleSize, consoleSize);
120 | _freezeLock.Unfreeze();
121 |
122 | Redraw();
123 | }
124 |
125 | private static void Redraw()
126 | {
127 | Update(ContentContext.Size.AsRect());
128 | }
129 |
130 | private static void Update(Rect rect)
131 | {
132 | Console.OnRefresh();
133 |
134 | rect = Rect.Intersect(rect, Rect.OfSize(BufferSize));
135 | rect = Rect.Intersect(rect, Rect.OfSize(WindowSize));
136 |
137 | for (int y = rect.Top; y <= rect.Bottom; y++)
138 | {
139 | for (int x = rect.Left; x <= rect.Right; x++)
140 | {
141 | var position = new Position(x, y);
142 |
143 | var cell = ContentContext[position];
144 |
145 | if (!_buffer.Update(position, cell)) continue;
146 |
147 | try
148 | {
149 | Console.Write(position, cell.Character);
150 | }
151 | catch (SafeConsoleException)
152 | {
153 | rect = Rect.Intersect(rect, Rect.OfSize(WindowSize));
154 | }
155 | }
156 | }
157 | }
158 |
159 | public static void Setup()
160 | {
161 | Resize(WindowSize);
162 | }
163 |
164 | public static void Resize(in Size size)
165 | {
166 | Console.Size = size;
167 | _buffer.Initialize(size);
168 |
169 | Initialize();
170 | }
171 |
172 | public static void AdjustBufferSize()
173 | {
174 | if (WindowSize != BufferSize)
175 | Resize(WindowSize);
176 | }
177 |
178 | public static void AdjustWindowSize()
179 | {
180 | if (WindowSize != BufferSize)
181 | Resize(BufferSize);
182 | }
183 |
184 | public static void ReadInput(IReadOnlyCollection controls)
185 | {
186 | while (Console.KeyAvailable)
187 | {
188 | var key = Console.ReadKey();
189 | var inputEvent = new InputEvent(key);
190 |
191 | foreach (var control in controls)
192 | {
193 | control?.OnInput(inputEvent);
194 | if (inputEvent.Handled) break;
195 | }
196 | }
197 | }
198 |
199 | private static void BindContent()
200 | {
201 | ContentContext = new DrawingContext(new ConsoleManagerDrawingContextListener(), Content);
202 | }
203 |
204 | private static void UpdateMouseContext()
205 | {
206 | MouseContext = MousePosition.HasValue
207 | ? _buffer.GetMouseContext(MousePosition.Value)
208 | : null;
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Background.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class Background : Control, IDrawingContextListener
12 | {
13 | private DrawingContext _contentContext = DrawingContext.Dummy;
14 | private DrawingContext ContentContext
15 | {
16 | get => _contentContext;
17 | set => Setter
18 | .SetDisposable(ref _contentContext, value)
19 | .Then(Initialize);
20 | }
21 |
22 | private IControl _content;
23 | public IControl Content
24 | {
25 | get => _content;
26 | set => Setter
27 | .Set(ref _content, value)
28 | .Then(BindContent);
29 | }
30 |
31 | private Color _color;
32 | public Color Color
33 | {
34 | get => _color;
35 | set => Setter
36 | .Set(ref _color, value)
37 | .Then(Redraw);
38 | }
39 |
40 | public bool _important;
41 | public bool Important
42 | {
43 | get => _important;
44 | set => Setter
45 | .Set(ref _important, value)
46 | .Then(Redraw);
47 | }
48 |
49 | public override Cell this[Position position]
50 | {
51 | get
52 | {
53 | if (!ContentContext.Contains(position)) return new Character(Color);
54 |
55 | var cell = ContentContext[position];
56 |
57 | if (!cell.Background.HasValue || Important)
58 | cell = cell.WithBackground(Color);
59 |
60 | return cell;
61 | }
62 | }
63 |
64 | protected override void Initialize()
65 | {
66 | using (Freeze())
67 | {
68 | ContentContext.SetLimits(MinSize, MaxSize);
69 |
70 | Resize(ContentContext.Size);
71 | }
72 | }
73 |
74 | private void BindContent()
75 | {
76 | ContentContext = new DrawingContext(this, Content);
77 | }
78 |
79 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
80 | {
81 | Initialize();
82 | }
83 |
84 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
85 | {
86 | Update(rect);
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Border.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 |
6 | namespace ConsoleGUI.Controls
7 | {
8 | public sealed class Border : Control, IDrawingContextListener
9 | {
10 | private DrawingContext _contentContext = DrawingContext.Dummy;
11 | private DrawingContext ContentContext
12 | {
13 | get => _contentContext;
14 | set => Setter
15 | .SetDisposable(ref _contentContext, value)
16 | .Then(Initialize);
17 | }
18 |
19 | private IControl _content;
20 | public IControl Content
21 | {
22 | get => _content;
23 | set => Setter
24 | .Set(ref _content, value)
25 | .Then(BindContent);
26 | }
27 |
28 | private BorderPlacement _borderPlacement = BorderPlacement.All;
29 | public BorderPlacement BorderPlacement
30 | {
31 | get => _borderPlacement;
32 | set => Setter
33 | .Set(ref _borderPlacement, value)
34 | .Then(Initialize);
35 | }
36 |
37 | private BorderStyle _borderStyle = BorderStyle.Double;
38 | public BorderStyle BorderStyle
39 | {
40 | get => _borderStyle;
41 | set => Setter
42 | .Set(ref _borderStyle, value)
43 | .Then(Redraw);
44 | }
45 |
46 | public override Cell this[Position position]
47 | {
48 | get
49 | {
50 | if (ContentContext.Contains(position))
51 | return ContentContext[position];
52 |
53 | if (position.X == 0 && position.Y == 0 && BorderPlacement.HasBorder(BorderPlacement.Top | BorderPlacement.Left))
54 | return _borderStyle.TopLeft;
55 |
56 | if (position.X == Size.Width - 1 && position.Y == 0 && BorderPlacement.HasBorder(BorderPlacement.Top | BorderPlacement.Right))
57 | return _borderStyle.TopRight;
58 |
59 | if (position.X == 0 && position.Y == Size.Height - 1 && BorderPlacement.HasBorder(BorderPlacement.Bottom | BorderPlacement.Left))
60 | return _borderStyle.BottomLeft;
61 |
62 | if (position.X == Size.Width - 1 && position.Y == Size.Height - 1 && BorderPlacement.HasBorder(BorderPlacement.Bottom | BorderPlacement.Right))
63 | return _borderStyle.BottomRight;
64 |
65 | if (position.X == 0 && BorderPlacement.HasBorder(BorderPlacement.Left))
66 | return _borderStyle.Left;
67 |
68 | if (position.X == Size.Width - 1 && BorderPlacement.HasBorder(BorderPlacement.Right))
69 | return _borderStyle.Right;
70 |
71 | if (position.Y == 0 && BorderPlacement.HasBorder(BorderPlacement.Top))
72 | return _borderStyle.Top;
73 |
74 | if (position.Y == Size.Height - 1 && BorderPlacement.HasBorder(BorderPlacement.Bottom))
75 | return _borderStyle.Bottom;
76 |
77 | return Character.Empty;
78 | }
79 | }
80 |
81 | protected override void Initialize()
82 | {
83 | using (Freeze())
84 | {
85 | ContentContext?.SetOffset(BorderPlacement.AsVector());
86 | ContentContext?.SetLimits(
87 | MinSize.AsRect().Remove(BorderPlacement.AsOffset()).Size,
88 | MaxSize.AsRect().Remove(BorderPlacement.AsOffset()).Size);
89 |
90 | Resize(Content?.Size.AsRect().Add(BorderPlacement.AsOffset()).Size ?? Size.Empty);
91 | }
92 | }
93 |
94 | private void BindContent()
95 | {
96 | ContentContext = new DrawingContext(this, Content);
97 | }
98 |
99 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
100 | {
101 | Initialize();
102 | }
103 |
104 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
105 | {
106 | Update(rect);
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Boundary.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class Boundary : Control, IDrawingContextListener
12 | {
13 | private DrawingContext _contentContext = DrawingContext.Dummy;
14 | private DrawingContext ContentContext
15 | {
16 | get => _contentContext;
17 | set => Setter
18 | .SetDisposable(ref _contentContext, value)
19 | .Then(Initialize);
20 | }
21 |
22 | private IControl _content;
23 | public IControl Content
24 | {
25 | get => _content;
26 | set => Setter
27 | .Set(ref _content, value)
28 | .Then(BindContent);
29 | }
30 |
31 | private int? _minWidth;
32 | public int? MinWidth
33 | {
34 | get => _minWidth;
35 | set => Setter
36 | .Set(ref _minWidth, value)
37 | .Then(Initialize);
38 | }
39 |
40 | private int? _minHeight;
41 | public int? MinHeight
42 | {
43 | get => _minHeight;
44 | set => Setter
45 | .Set(ref _minHeight, value)
46 | .Then(Initialize);
47 | }
48 |
49 | private int? _maxWidth;
50 | public int? MaxWidth
51 | {
52 | get => _maxWidth;
53 | set => Setter
54 | .Set(ref _maxWidth, value)
55 | .Then(Initialize);
56 | }
57 |
58 | private int? _maxHeight;
59 | public int? MaxHeight
60 | {
61 | get => _maxHeight;
62 | set => Setter
63 | .Set(ref _maxHeight, value)
64 | .Then(Initialize);
65 | }
66 |
67 | public int? Width
68 | {
69 | set
70 | {
71 | using (Freeze())
72 | {
73 | MinWidth = value;
74 | MaxWidth = value;
75 | }
76 | }
77 | }
78 |
79 | public int? Height
80 | {
81 | set
82 | {
83 | using (Freeze())
84 | {
85 | MinHeight = value;
86 | MaxHeight = value;
87 | }
88 | }
89 | }
90 |
91 | public override Cell this[Position position]
92 | {
93 | get
94 | {
95 | if (ContentContext.Contains(position))
96 | return ContentContext[position];
97 |
98 | return Character.Empty;
99 | }
100 | }
101 |
102 | protected override void Initialize()
103 | {
104 | using (Freeze())
105 | {
106 | var minSize = new Size(
107 | Math.Min(MinWidth ?? MinSize.Width, MaxWidth ?? int.MaxValue),
108 | Math.Min(MinHeight ?? MinSize.Height, MaxHeight ?? int.MaxValue));
109 | var maxSize = new Size(
110 | Math.Max(MaxWidth ?? MaxSize.Width, MinWidth ?? 0),
111 | Math.Max(MaxHeight ?? MaxSize.Height, MinHeight ?? 0));
112 |
113 | ContentContext.SetLimits(minSize, maxSize);
114 |
115 | Resize(Size.Clip(minSize, ContentContext.Size, maxSize));
116 | }
117 | }
118 |
119 | private void BindContent()
120 | {
121 | ContentContext = new DrawingContext(this, Content);
122 | }
123 |
124 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
125 | {
126 | Initialize();
127 | }
128 |
129 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
130 | {
131 | Update(rect);
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Box.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class Box : Control, IDrawingContextListener
12 | {
13 | public enum VerticalPlacement
14 | {
15 | Top,
16 | Center,
17 | Bottom,
18 | Stretch
19 | }
20 |
21 | public enum HorizontalPlacement
22 | {
23 | Left,
24 | Center,
25 | Right,
26 | Stretch
27 | }
28 |
29 | private HorizontalPlacement _horizontalContentPlacement = HorizontalPlacement.Center;
30 | public HorizontalPlacement HorizontalContentPlacement
31 | {
32 | get => _horizontalContentPlacement;
33 | set => Setter
34 | .Set(ref _horizontalContentPlacement, value)
35 | .Then(Initialize);
36 | }
37 |
38 | private VerticalPlacement _verticalContentPlacement = VerticalPlacement.Center;
39 | public VerticalPlacement VerticalContentPlacement
40 | {
41 | get => _verticalContentPlacement;
42 | set => Setter
43 | .Set(ref _verticalContentPlacement, value)
44 | .Then(Initialize);
45 | }
46 |
47 | private IControl _content;
48 | public IControl Content
49 | {
50 | get => _content;
51 | set => Setter
52 | .Set(ref _content, value)
53 | .Then(BindContent);
54 | }
55 |
56 | private DrawingContext _contentContext = DrawingContext.Dummy;
57 | private DrawingContext ContentContext
58 | {
59 | get => _contentContext;
60 | set => Setter
61 | .SetDisposable(ref _contentContext, value)
62 | .Then(Initialize);
63 | }
64 |
65 | public override Cell this[Position position]
66 | {
67 | get
68 | {
69 | if (ContentContext.Contains(position))
70 | return ContentContext[position];
71 |
72 | return Character.Empty;
73 | }
74 | }
75 |
76 | protected override void Initialize()
77 | {
78 | using (Freeze())
79 | {
80 | var minSize = new Size(
81 | HorizontalContentPlacement == HorizontalPlacement.Stretch ? MinSize.Width : 0,
82 | VerticalContentPlacement == VerticalPlacement.Stretch ? MinSize.Height : 0);
83 |
84 | var maxSize = new Size(
85 | HorizontalContentPlacement == HorizontalPlacement.Stretch ? MaxSize.Width : Size.MaxLength,
86 | VerticalContentPlacement == VerticalPlacement.Stretch ? MaxSize.Height : Size.MaxLength);
87 |
88 | ContentContext.SetLimits(minSize, maxSize);
89 |
90 | Resize(ContentContext.Size);
91 |
92 | int left = 0;
93 | int top = 0;
94 |
95 | switch (VerticalContentPlacement)
96 | {
97 | case VerticalPlacement.Stretch:
98 | case VerticalPlacement.Top:
99 | top = 0;
100 | break;
101 | case VerticalPlacement.Center:
102 | top = (Size.Height - ContentContext.Size.Height) / 2;
103 | break;
104 | case VerticalPlacement.Bottom:
105 | top = Size.Height - ContentContext.Size.Height;
106 | break;
107 | }
108 |
109 | switch (HorizontalContentPlacement)
110 | {
111 | case HorizontalPlacement.Stretch:
112 | case HorizontalPlacement.Left:
113 | left = 0;
114 | break;
115 | case HorizontalPlacement.Center:
116 | left = (Size.Width - ContentContext.Size.Width) / 2;
117 | break;
118 | case HorizontalPlacement.Right:
119 | left = Size.Width - ContentContext.Size.Width;
120 | break;
121 | }
122 |
123 | ContentContext.SetOffset(new Vector(left, top));
124 | }
125 | }
126 |
127 | private void BindContent()
128 | {
129 | ContentContext = new DrawingContext(this, Content);
130 | }
131 |
132 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
133 | {
134 | Initialize();
135 | }
136 |
137 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
138 | {
139 | Update(rect);
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/BreakPanel.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Text;
9 |
10 | namespace ConsoleGUI.Controls
11 | {
12 | public class BreakPanel : Control, IDrawingContextListener
13 | {
14 | private readonly VerticalStackPanel _stackPanel;
15 | private readonly DrawingContext _stackPanelContext;
16 | private readonly List _wrapPanels;
17 |
18 | public BreakPanel()
19 | {
20 | _stackPanel = new VerticalStackPanel();
21 | _stackPanelContext = new DrawingContext(this, _stackPanel);
22 | _wrapPanels = new List();
23 | }
24 |
25 | private DrawingContext _contentContext = DrawingContext.Dummy;
26 | private DrawingContext ContentContext
27 | {
28 | get => _contentContext;
29 | set => Setter
30 | .SetDisposable(ref _contentContext, value)
31 | .Then(Initialize);
32 | }
33 |
34 | private IControl _content;
35 | public IControl Content
36 | {
37 | get => _content;
38 | set => Setter
39 | .Set(ref _content, value)
40 | .Then(BindContent);
41 | }
42 |
43 | public override Cell this[Position position]
44 | {
45 | get
46 | {
47 | return _stackPanel[position];
48 | }
49 | }
50 |
51 | protected override void Initialize()
52 | {
53 | using (Freeze())
54 | {
55 | ContentContext.SetLimits(
56 | new Size(0, 1),
57 | new Size(Math.Max(0, MaxSize.Width * MaxSize.Height), 1));
58 |
59 | _stackPanelContext.SetLimits(MinSize, MaxSize);
60 |
61 | int breaks = 0;
62 | int width = 0;
63 |
64 | for (int x = 0; x <= Content.Size.Width; x++)
65 | {
66 | if (Content[new Position(x, 0)].Character.Content == '\n' || x == Content.Size.Width)
67 | {
68 | if (_wrapPanels.Count <= breaks)
69 | {
70 | var newWrapPanel = new WrapPanel() { Content = new DrawingSection() };
71 | _stackPanel.Add(newWrapPanel);
72 | _wrapPanels.Add(newWrapPanel);
73 | }
74 |
75 | var drawingSection = _wrapPanels[breaks].Content as DrawingSection;
76 | drawingSection.Content = Content;
77 | drawingSection.Rect = new Rect(x - width, 0, width, 1);
78 |
79 | breaks++;
80 | width = 0;
81 | }
82 | else
83 | {
84 | width++;
85 | }
86 | }
87 |
88 | while (_wrapPanels.Count > breaks)
89 | {
90 | _stackPanel.Remove(_wrapPanels.Last());
91 | _wrapPanels.RemoveAt(_wrapPanels.Count - 1);
92 | }
93 |
94 | Resize(_stackPanel.Size);
95 | }
96 | }
97 |
98 | private void BindContent()
99 | {
100 | ContentContext = new DrawingContext(this, Content);
101 | }
102 |
103 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
104 | {
105 | if (drawingContext == ContentContext)
106 | Initialize();
107 | if (drawingContext == _stackPanelContext)
108 | Resize(_stackPanelContext.Size);
109 | }
110 |
111 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
112 | {
113 | if (drawingContext == ContentContext)
114 | {
115 | using (Freeze())
116 | {
117 | Initialize();
118 | foreach (var wrapPanel in _wrapPanels)
119 | (wrapPanel.Content as DrawingSection).Update(rect);
120 | }
121 | }
122 |
123 | if (drawingContext == _stackPanelContext)
124 | Update(rect);
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Button.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Input;
4 | using ConsoleGUI.Space;
5 | using ConsoleGUI.Utils;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Text;
9 |
10 | namespace ConsoleGUI.Controls
11 | {
12 | public sealed class Button : Control, IDrawingContextListener, IMouseListener
13 | {
14 | public event EventHandler Clicked;
15 |
16 | private DrawingContext _contentContext = DrawingContext.Dummy;
17 | private DrawingContext ContentContext
18 | {
19 | get => _contentContext;
20 | set => Setter
21 | .SetDisposable(ref _contentContext, value)
22 | .Then(Initialize);
23 | }
24 |
25 | private IControl _content;
26 | public IControl Content
27 | {
28 | get => _content;
29 | set => Setter
30 | .Set(ref _content, value)
31 | .Then(BindContent);
32 | }
33 |
34 | private Color _mouseOverColor = new Color(50, 50, 100);
35 | public Color MouseOverColor
36 | {
37 | get => _mouseOverColor;
38 | set => Setter
39 | .Set(ref _mouseOverColor, value)
40 | .Then(Redraw);
41 | }
42 |
43 | private Color _mouseDownColor = new Color(50, 50, 200);
44 | public Color MouseDownColor
45 | {
46 | get => _mouseDownColor;
47 | set => Setter
48 | .Set(ref _mouseDownColor, value)
49 | .Then(Redraw);
50 | }
51 |
52 | private bool _mouseOver;
53 | private bool MouseOver
54 | {
55 | get => _mouseOver;
56 | set => Setter
57 | .Set(ref _mouseOver, value)
58 | .Then(Redraw);
59 | }
60 |
61 | private bool _mouseDown;
62 | private bool MouseDown
63 | {
64 | get => _mouseDown;
65 | set => Setter
66 | .Set(ref _mouseDown, value)
67 | .Then(Redraw);
68 | }
69 |
70 | public override Cell this[Position position]
71 | {
72 | get
73 | {
74 | var character = ContentContext[position];
75 |
76 | if (MouseDown)
77 | character = character.WithBackground(character.Background?.Mix(MouseDownColor, 0.75f) ?? MouseDownColor);
78 | else if (MouseOver)
79 | character = character.WithBackground(character.Background?.Mix(MouseOverColor, 0.75f) ?? MouseOverColor);
80 |
81 | return character.WithMouseListener(this, position);
82 | }
83 | }
84 |
85 | protected override void Initialize()
86 | {
87 | using (Freeze())
88 | {
89 | ContentContext.SetLimits(MinSize, MaxSize);
90 | Resize(ContentContext.Size);
91 | }
92 | }
93 |
94 | private void BindContent()
95 | {
96 | ContentContext = new DrawingContext(this, _content);
97 | }
98 |
99 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
100 | {
101 | Initialize();
102 | }
103 |
104 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
105 | {
106 | Update(rect);
107 | }
108 |
109 | void IMouseListener.OnMouseEnter()
110 | {
111 | MouseOver = true;
112 | }
113 |
114 | void IMouseListener.OnMouseLeave()
115 | {
116 | MouseOver = false;
117 | MouseDown = false;
118 | }
119 |
120 | void IMouseListener.OnMouseUp(Position position)
121 | {
122 | if (MouseDown)
123 | {
124 | MouseDown = false;
125 | Clicked?.Invoke(this, EventArgs.Empty);
126 | }
127 |
128 | }
129 |
130 | void IMouseListener.OnMouseDown(Position position)
131 | {
132 | MouseDown = true;
133 | }
134 |
135 | void IMouseListener.OnMouseMove(Position position)
136 | { }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Canvas.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class Canvas : Control, IDrawingContextListener
12 | {
13 | private readonly List _children = new List();
14 |
15 | public void Add(IControl control, in Rect rect)
16 | {
17 | using (Freeze())
18 | {
19 | var newChild = new DrawingContext(this, control);
20 | newChild.SetOffset(rect.Offset);
21 | newChild.SetLimits(rect.Size, rect.Size);
22 |
23 | _children.Insert(0, newChild);
24 |
25 | Update(rect);
26 | }
27 | }
28 |
29 | public void Move(IControl control, in Rect rect)
30 | {
31 | using (Freeze())
32 | {
33 | foreach (var child in _children)
34 | {
35 | if (child.Child != control) continue;
36 |
37 | child.SetOffset(rect.Offset);
38 | child.SetLimits(rect.Size, rect.Size);
39 | }
40 | }
41 | }
42 |
43 | public void Remove(IControl control)
44 | {
45 | using (Freeze())
46 | {
47 | var child = _children.FirstOrDefault(context => context.Child == control);
48 |
49 | if (child == null) return;
50 |
51 | Update(new Rect(child.Offset, child.MaxSize));
52 |
53 | _children.Remove(child);
54 | }
55 | }
56 |
57 | public override Cell this[Position position]
58 | {
59 | get
60 | {
61 | if (!Size.Contains(position)) return Character.Empty;
62 |
63 | foreach (var child in _children)
64 | if (child.Contains(position))
65 | return child[position];
66 |
67 | return Character.Empty;
68 | }
69 | }
70 |
71 | protected override void Initialize()
72 | {
73 | Resize(MinSize);
74 | }
75 |
76 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
77 | {
78 | Update(drawingContext.MinSize.AsRect().Move(drawingContext.Offset));
79 | }
80 |
81 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
82 | {
83 | Update(rect);
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/CheckBox.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Input;
4 | using ConsoleGUI.Space;
5 | using ConsoleGUI.Utils;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Text;
9 |
10 | namespace ConsoleGUI.Controls
11 | {
12 | public class CheckBox : Control, IInputListener, IMouseListener
13 | {
14 | private bool _mouseDown;
15 |
16 | public event EventHandler ValueChanged;
17 |
18 | private bool _value;
19 | public bool Value
20 | {
21 | get => _value;
22 | set => Setter
23 | .Set(ref _value, value)
24 | .Then(Redraw);
25 | }
26 |
27 | private Character _trueCharacter = new Character('☑');
28 | public Character TrueCharacter
29 | {
30 | get => _trueCharacter;
31 | set => Setter
32 | .Set(ref _trueCharacter, value)
33 | .Then(Redraw);
34 | }
35 |
36 | private Character _falseCharacter = new Character('☐');
37 | public Character FalseCharacter
38 | {
39 | get => _falseCharacter;
40 | set => Setter
41 | .Set(ref _falseCharacter, value)
42 | .Then(Redraw);
43 | }
44 |
45 | public override Cell this[Position position]
46 | {
47 | get
48 | {
49 | if (position != Position.Begin) return Character.Empty;
50 |
51 | var character = Value
52 | ? TrueCharacter
53 | : FalseCharacter;
54 |
55 | return new Cell(character).WithMouseListener(this, position);
56 | }
57 | }
58 |
59 | protected override void Initialize()
60 | {
61 | Resize(new Size(1, 1));
62 | }
63 |
64 | void IMouseListener.OnMouseEnter()
65 | { }
66 |
67 | void IMouseListener.OnMouseLeave()
68 | {
69 | _mouseDown = false;
70 | }
71 |
72 | void IMouseListener.OnMouseUp(Position position)
73 | {
74 | if (_mouseDown)
75 | {
76 | _mouseDown = false;
77 | Value = !Value;
78 | ValueChanged?.Invoke(this, Value);
79 | }
80 | }
81 |
82 | void IMouseListener.OnMouseDown(Position position)
83 | {
84 | _mouseDown = true;
85 | }
86 |
87 | void IMouseListener.OnMouseMove(Position position)
88 | { }
89 |
90 | void IInputListener.OnInput(InputEvent inputEvent)
91 | {
92 | if (inputEvent.Key.Key == ConsoleKey.Spacebar)
93 | {
94 | Value = !Value;
95 | inputEvent.Handled = true;
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/DataGrid.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class DataGrid : Control
12 | {
13 | public class ColumnDefinition
14 | {
15 | private readonly Func _headerSelector;
16 | private readonly Func _valueSelector;
17 |
18 | public readonly int Width;
19 |
20 | public ColumnDefinition(string header, int width, Func selector)
21 | {
22 | Width = width;
23 |
24 | _headerSelector = i =>
25 | {
26 | var text = header;
27 | return i < text.Length ? new Character(text[i]) : Character.Empty;
28 | };
29 |
30 | _valueSelector = (v, i) =>
31 | {
32 | var text = selector(v);
33 | return i < text.Length ? new Character(text[i]) : Character.Empty;
34 | };
35 | }
36 |
37 | public ColumnDefinition(string header, int width, Func selector)
38 | {
39 | Width = width;
40 |
41 | _headerSelector = i =>
42 | {
43 | var text = header;
44 | return i < text.Length ? new Character(text[i]) : Character.Empty;
45 | };
46 |
47 | _valueSelector = selector;
48 | }
49 |
50 | public ColumnDefinition(int width, Func headerSelector, Func valueSelector)
51 | {
52 | Width = width;
53 |
54 | _headerSelector = headerSelector;
55 | _valueSelector = valueSelector;
56 | }
57 |
58 | public ColumnDefinition(
59 | string header,
60 | int width,
61 | Func selector,
62 | Func foreground = null,
63 | Func background = null,
64 | TextAlignment textAlignment = TextAlignment.Left)
65 | {
66 | Width = width;
67 |
68 | _headerSelector = i =>
69 | {
70 | var text = header;
71 | return i < text.Length ? new Character(text[i]) : Character.Empty;
72 | };
73 |
74 | _valueSelector = (v, i) =>
75 | {
76 | var text = selector(v);
77 |
78 | if (textAlignment == TextAlignment.Center)
79 | i -= (Width - text.Length) / 2;
80 | if (textAlignment == TextAlignment.Right)
81 | i -= Width - text.Length;
82 |
83 | return new Character(
84 | i >= 0 && i < text.Length ? (char?)text[i] : null,
85 | foreground?.Invoke(v),
86 | background?.Invoke(v));
87 | };
88 | }
89 |
90 | internal Character GetHeader(int xOffset) => _headerSelector(xOffset);
91 | internal Character GetValue(T value, int xOffset) => _valueSelector(value, xOffset);
92 | }
93 |
94 | private ColumnDefinition[] _columns = new ColumnDefinition[0];
95 | public ColumnDefinition[] Columns
96 | {
97 | get => _columns;
98 | set => Setter
99 | .Set(ref _columns, value.ToArray())
100 | .Then(Initialize);
101 | }
102 |
103 | private IReadOnlyCollection _data = new T[0];
104 | public IReadOnlyCollection Data
105 | {
106 | get => _data;
107 | set => Setter
108 | .Set(ref _data, value)
109 | .Then(Initialize);
110 | }
111 |
112 | private DataGridStyle _style = DataGridStyle.AllBorders;
113 | public DataGridStyle Style
114 | {
115 | get => _style;
116 | set => Setter
117 | .Set(ref _style, value)
118 | .Then(Initialize);
119 | }
120 |
121 | private bool _showHeader = true;
122 | public bool ShowHeader
123 | {
124 | get => _showHeader;
125 | set => Setter
126 | .Set(ref _showHeader, value)
127 | .Then(Initialize);
128 | }
129 |
130 | public void Update()
131 | {
132 | Redraw();
133 | }
134 |
135 | public void Update(int row)
136 | {
137 | Update(new Rect(0, (row + 1) * 2, Size.Width, 1));
138 | }
139 |
140 | public void Update(int row, int column)
141 | {
142 | if (column >= Columns.Length) return;
143 |
144 | int xOffset = 0;
145 | for (int c = 0; c < column; c++) xOffset += Columns[c].Width + 1;
146 |
147 | Update(new Rect(xOffset, (row + 1) * 2, Columns[column].Width, 1));
148 | }
149 |
150 | public override Cell this[Position position]
151 | {
152 | get
153 | {
154 | if (position.Y < 0) return Character.Empty;
155 | if (position.X < 0) return Character.Empty;
156 | if (position.Y >= CalculateDesiredHeight()) return Character.Empty;
157 | if (position.X >= CalculateDesiredWidth()) return Character.Empty;
158 |
159 | int column = 0;
160 | int xOffset = position.X;
161 | bool isVerticalBorder = false;
162 |
163 | while (xOffset >= Columns[column].Width)
164 | {
165 | if (Style.HasVertivalBorders && xOffset == Columns[column].Width)
166 | {
167 | isVerticalBorder = true;
168 | break;
169 | }
170 |
171 | xOffset -= Columns[column].Width;
172 | xOffset -= Style.HasVertivalBorders ? 1 : 0;
173 | column += 1;
174 | }
175 |
176 | if (ShowHeader && position.Y == 0 && isVerticalBorder) return Style.HeaderVerticalBorder.Value;
177 | if (ShowHeader && position.Y == 1 && isVerticalBorder && Style.HeaderIntersectionBorder.HasValue) return Style.HeaderIntersectionBorder.Value;
178 | if (ShowHeader && position.Y == 1 && Style.HeaderHorizontalBorder.HasValue) return Style.HeaderHorizontalBorder.Value;
179 | if (ShowHeader && position.Y == 0) return Columns[column].GetHeader(xOffset);
180 |
181 | int yOffset = position.Y;
182 |
183 | if (ShowHeader) yOffset--;
184 | if (ShowHeader && Style.HeaderHorizontalBorder.HasValue) yOffset--;
185 |
186 | int row = Style.CellHorizontalBorder.HasValue
187 | ? yOffset / 2
188 | : yOffset;
189 | bool isHorizontalBorder = Style.CellHorizontalBorder.HasValue
190 | ? yOffset % 2 == 1
191 | : false;
192 |
193 | if (isHorizontalBorder && isVerticalBorder) return Style.CellIntersectionBorder.Value;
194 | if (isHorizontalBorder) return Style.CellHorizontalBorder.Value;
195 | if (isVerticalBorder) return Style.CellVerticalBorder.Value;
196 |
197 | return Columns[column].GetValue(Data.ElementAt(row), xOffset);
198 | }
199 | }
200 |
201 | protected override void Initialize()
202 | {
203 | Resize(new Size(CalculateDesiredWidth(), CalculateDesiredHeight()));
204 | }
205 |
206 | private int CalculateDesiredHeight()
207 | {
208 | int height = Data.Count;
209 |
210 | if (ShowHeader)
211 | height += 1;
212 | if (ShowHeader && Style.HeaderHorizontalBorder.HasValue)
213 | height += 1;
214 | if (Data.Count > 0 && Style.CellHorizontalBorder.HasValue)
215 | height += Data.Count - 1;
216 |
217 | return height;
218 | }
219 |
220 | private int CalculateDesiredWidth()
221 | {
222 | int width = Columns.Sum(c => c.Width);
223 |
224 | if (Columns.Length > 0 && Style.HasVertivalBorders)
225 | width += Columns.Length - 1;
226 |
227 | return width;
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Decorator.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public abstract class Decorator : Control, IDrawingContextListener
12 | {
13 | private DrawingContext _contentContext = DrawingContext.Dummy;
14 | private DrawingContext ContentContext
15 | {
16 | get => _contentContext;
17 | set => Setter
18 | .SetDisposable(ref _contentContext, value)
19 | .Then(Initialize);
20 | }
21 |
22 | private IControl _content;
23 | public IControl Content
24 | {
25 | get => _content;
26 | set => Setter
27 | .Set(ref _content, value)
28 | .Then(BindContent);
29 | }
30 |
31 | protected override void Initialize()
32 | {
33 | using (Freeze())
34 | {
35 | ContentContext.SetLimits(MinSize, MaxSize);
36 |
37 | Resize(Size.Clip(MinSize, ContentContext.Size, MaxSize));
38 | }
39 | }
40 |
41 | private void BindContent()
42 | {
43 | ContentContext = new DrawingContext(this, Content);
44 | }
45 |
46 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
47 | {
48 | Initialize();
49 | }
50 |
51 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
52 | {
53 | Update(rect);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/DockPanel.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class DockPanel : Control, IDrawingContextListener
12 | {
13 | public enum DockedControlPlacement
14 | {
15 | Top,
16 | Right,
17 | Bottom,
18 | Left
19 | }
20 |
21 | private DockedControlPlacement placement = DockedControlPlacement.Top;
22 | public DockedControlPlacement Placement
23 | {
24 | get => placement;
25 | set => Setter
26 | .Set(ref placement, value)
27 | .Then(Initialize);
28 | }
29 |
30 | private IControl dockedControl;
31 | public IControl DockedControl
32 | {
33 | get => dockedControl;
34 | set => Setter
35 | .Set(ref dockedControl, value)
36 | .Then(BindDockedControl);
37 | }
38 |
39 | private IControl fillingControl;
40 | public IControl FillingControl
41 | {
42 | get => fillingControl;
43 | set => Setter
44 | .Set(ref fillingControl, value)
45 | .Then(BindFillingControl);
46 | }
47 |
48 | private DrawingContext dockedDrawingContext = DrawingContext.Dummy;
49 | private DrawingContext DockedDrawingContext
50 | {
51 | get => dockedDrawingContext;
52 | set => Setter
53 | .SetDisposable(ref dockedDrawingContext, value)
54 | .Then(Initialize);
55 | }
56 |
57 | private DrawingContext fillingDrawingContext = DrawingContext.Dummy;
58 | private DrawingContext FillingDrawingContext
59 | {
60 | get => fillingDrawingContext;
61 | set => Setter
62 | .SetDisposable(ref fillingDrawingContext, value)
63 | .Then(Initialize);
64 | }
65 |
66 | public override Cell this[Position position]
67 | {
68 | get
69 | {
70 | if (DockedDrawingContext.Contains(position))
71 | return DockedDrawingContext[position];
72 |
73 | if (FillingDrawingContext.Contains(position))
74 | return FillingDrawingContext[position];
75 |
76 | return Character.Empty;
77 | }
78 | }
79 |
80 | protected override void Initialize()
81 | {
82 | using (Freeze())
83 | {
84 | switch (Placement)
85 | {
86 | case DockedControlPlacement.Top:
87 | case DockedControlPlacement.Bottom:
88 | DockedDrawingContext.SetLimits(MinSize.WithHeight(0), MaxSize);
89 | FillingDrawingContext.SetLimits(MinSize.Shrink(0, DockedDrawingContext.Size.Height), MaxSize.Shrink(0, DockedDrawingContext.Size.Height));
90 | Resize(new Size(Math.Max(DockedDrawingContext.Size.Width, FillingDrawingContext.Size.Width), DockedDrawingContext.Size.Height + FillingDrawingContext.Size.Height));
91 | break;
92 | case DockedControlPlacement.Left:
93 | case DockedControlPlacement.Right:
94 | DockedDrawingContext.SetLimits(MinSize.WithWidth(0), MaxSize);
95 | FillingDrawingContext.SetLimits(MinSize.Shrink(DockedDrawingContext.Size.Width, 0), MaxSize.Shrink(DockedDrawingContext.Size.Width, 0));
96 | Resize(new Size(DockedDrawingContext.Size.Width + FillingDrawingContext.Size.Width, Math.Max(DockedDrawingContext.Size.Height, FillingDrawingContext.Size.Height)));
97 | break;
98 | }
99 |
100 | switch (Placement)
101 | {
102 | case DockedControlPlacement.Top:
103 | DockedDrawingContext.SetOffset(new Vector(0, 0));
104 | FillingDrawingContext.SetOffset(new Vector(0, DockedDrawingContext.Size.Height));
105 | break;
106 | case DockedControlPlacement.Bottom:
107 | DockedDrawingContext.SetOffset(new Vector(0, Size.Height - DockedDrawingContext.Size.Height));
108 | FillingDrawingContext.SetOffset(new Vector(0, 0));
109 | break;
110 | case DockedControlPlacement.Left:
111 | DockedDrawingContext.SetOffset(new Vector(0, 0));
112 | FillingDrawingContext.SetOffset(new Vector(DockedDrawingContext.Size.Width, 0));
113 | break;
114 | case DockedControlPlacement.Right:
115 | DockedDrawingContext.SetOffset(new Vector(Size.Width - DockedDrawingContext.Size.Width, 0));
116 | FillingDrawingContext.SetOffset(new Vector(0, 0));
117 | break;
118 | }
119 | }
120 | }
121 |
122 | private void BindDockedControl()
123 | {
124 | DockedDrawingContext = new DrawingContext(this, DockedControl);
125 | }
126 |
127 | private void BindFillingControl()
128 | {
129 | FillingDrawingContext = new DrawingContext(this, FillingControl);
130 | }
131 |
132 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
133 | {
134 | Initialize();
135 | }
136 |
137 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
138 | {
139 | Update(rect);
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Grid.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Text;
9 |
10 | namespace ConsoleGUI.Controls
11 | {
12 | public class Grid : Control, IDrawingContextListener
13 | {
14 | public readonly struct ColumnDefinition
15 | {
16 | public readonly int Width;
17 |
18 | public ColumnDefinition(int width)
19 | {
20 | Width = width;
21 | }
22 | }
23 |
24 | public readonly struct RowDefinition
25 | {
26 | public readonly int Height;
27 |
28 | public RowDefinition(int height)
29 | {
30 | Height = height;
31 | }
32 | }
33 |
34 | private DrawingContext[,] _children = new DrawingContext[0, 0];
35 | private DrawingContext[,] Children
36 | {
37 | get => _children;
38 | set => Setter
39 | .Set(ref _children, value);
40 | }
41 |
42 | private ColumnDefinition[] _columns = new ColumnDefinition[0];
43 | public ColumnDefinition[] Columns
44 | {
45 | get => _columns;
46 | set => Setter
47 | .Set(ref _columns, value.ToArray())
48 | .Then(ResizeBuffer)
49 | .Then(Initialize);
50 | }
51 |
52 | private RowDefinition[] _rows = new RowDefinition[0];
53 | public RowDefinition[] Rows
54 | {
55 | get => _rows;
56 | set => Setter
57 | .Set(ref _rows, value.ToArray())
58 | .Then(ResizeBuffer)
59 | .Then(Initialize);
60 | }
61 |
62 | public void AddChild(int x, int y, IControl control)
63 | {
64 | using (Freeze())
65 | {
66 | ref var child = ref Children[x, y];
67 | var rect = GetRect(x, y);
68 |
69 | child?.Dispose();
70 | child = new DrawingContext(this, control);
71 | child.SetOffset(rect.Offset);
72 | child.SetLimits(rect.Size, rect.Size);
73 |
74 | Update(rect);
75 | }
76 | }
77 |
78 | public bool HasChild(int x, int y)
79 | {
80 | return Children[x, y] != null;
81 | }
82 |
83 | public IControl GetChild(int x, int y)
84 | {
85 | return Children[x, y].Child;
86 | }
87 |
88 | public void RemoveChild(int x, int y)
89 | {
90 | Children[x, y]?.Dispose();
91 | Children[x, y] = null;
92 |
93 | Update(GetRect(x, y));
94 | }
95 |
96 | public override Cell this[Position position]
97 | {
98 | get
99 | {
100 | int x = 0;
101 | int y = 0;
102 |
103 | for (int xOffset = 0; x < Columns.Length && position.X >= xOffset + Columns[x].Width; xOffset += Columns[x++].Width) ;
104 | for (int yOffset = 0; y < Rows.Length && position.Y >= yOffset + Rows[y].Height; yOffset += Rows[y++].Height) ;
105 |
106 | if (x >= Columns.Length || y >= Rows.Length || Children[x, y] == null) return Character.Empty;
107 |
108 | return Children[x, y][position];
109 | }
110 | }
111 |
112 | protected override void Initialize()
113 | {
114 | using (Freeze())
115 | {
116 | for (int x = 0, xOffset = 0; x < Columns.Length; xOffset += Columns[x++].Width)
117 | {
118 | for (int y = 0, yOffset = 0; y < Rows.Length; yOffset += Rows[y++].Height)
119 | {
120 | var child = Children[x, y];
121 | var size = new Size(Columns[x].Width, Rows[y].Height);
122 |
123 | child?.SetOffset(new Vector(xOffset, yOffset));
124 | child?.SetLimits(size, size);
125 | }
126 | }
127 |
128 | int width = 0,
129 | height = 0;
130 |
131 | for (int x = 0; x < Columns.Length; x++) width += Columns[x].Width;
132 | for (int y = 0; y < Rows.Length; y++) height += Rows[y].Height;
133 |
134 | Resize(new Size(width, height));
135 | }
136 | }
137 |
138 | private void ResizeBuffer()
139 | {
140 | var newBuffer = new DrawingContext[Columns.Length, Rows.Length];
141 |
142 | for (int x = 0; x < Children.GetLength(0); x++)
143 | {
144 | for (int y = 0; y < Children.GetLength(1); y++)
145 | {
146 | if (x < Columns.Length && y < Columns.Length)
147 | newBuffer[x, y] = Children[x, y];
148 | else
149 | Children[x, y]?.Dispose();
150 | }
151 | }
152 |
153 | Children = newBuffer;
154 | }
155 |
156 | private Rect GetRect(int x, int y)
157 | {
158 | var size = new Size(Columns[x].Width, Rows[y].Height);
159 |
160 | int xOffset = 0;
161 | int yOffset = 0;
162 |
163 | while (--x >= 0) xOffset += Columns[x].Width;
164 | while (--y >= 0) yOffset -= Rows[y].Height;
165 |
166 | return new Rect(new Vector(xOffset, yOffset), size);
167 | }
168 |
169 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
170 | {
171 | Update(new Rect(drawingContext.Offset, drawingContext.MaxSize));
172 | }
173 |
174 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
175 | {
176 | Update(rect);
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/HorizontalAlignment.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class HorizontalAlignment : Control, IDrawingContextListener
12 | {
13 | private DrawingContext _contentContext;
14 | private DrawingContext ContentContext
15 | {
16 | get => _contentContext;
17 | set => Setter
18 | .SetDisposable(ref _contentContext, value)
19 | .Then(Initialize);
20 | }
21 |
22 | private IControl _content;
23 | public IControl Content
24 | {
25 | get => _content;
26 | set => Setter
27 | .Set(ref _content, value)
28 | .Then(BindContent);
29 | }
30 |
31 | private int ContentOffset => (Size.Width - Content?.Size.Width ?? 0) / 2;
32 |
33 | public override Cell this[Position position]
34 | {
35 | get
36 | {
37 | var contentPosition = position.Move(-ContentOffset, 0);
38 |
39 | if (Content.Size.Contains(contentPosition))
40 | return Content[contentPosition];
41 |
42 | return Character.Empty;
43 | }
44 | }
45 |
46 | protected override void Initialize()
47 | {
48 | using (Freeze())
49 | {
50 | ContentContext?.SetLimits(
51 | new Size(0, MinSize.Height),
52 | MaxSize);
53 |
54 | var newSize = Size.Clip(MinSize, Content?.Size ?? Size.Empty, MaxSize);
55 |
56 | ContentContext?.SetOffset(new Vector((Size.Width - Content?.Size.Width ?? 0) / 2, 0));
57 |
58 | Resize(newSize);
59 | }
60 | }
61 |
62 | private void BindContent()
63 | {
64 | ContentContext = new DrawingContext(this, Content);
65 | }
66 |
67 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
68 | {
69 | Initialize();
70 | }
71 |
72 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
73 | {
74 | Update(rect);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/HorizontalSeparator.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public sealed class HorizontalSeparator : Control
12 | {
13 | private Character character = new Character('─');
14 | public Character Character
15 | {
16 | get => character;
17 | set => Setter
18 | .Set(ref character, value)
19 | .Then(Initialize);
20 | }
21 |
22 | public override Cell this[Position position] => Character;
23 |
24 | protected override void Initialize()
25 | {
26 | Resize(new Size(MinSize.Width, 1));
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/HorizontalStackPanel.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 |
8 | namespace ConsoleGUI.Controls
9 | {
10 | public class HorizontalStackPanel : Control, IDrawingContextListener
11 | {
12 | private readonly List _children = new List();
13 | public IEnumerable Children
14 | {
15 | get => _children.Select(c => c.Child);
16 | set
17 | {
18 | foreach (var child in _children) child.Dispose();
19 |
20 | _children.Clear();
21 |
22 | foreach (var child in value) _children.Add(new DrawingContext(this, child));
23 |
24 | Initialize();
25 | }
26 | }
27 |
28 | public void Add(IControl control)
29 | {
30 | using (Freeze())
31 | {
32 | _children.Add(new DrawingContext(this, control));
33 |
34 | Initialize();
35 | }
36 | }
37 |
38 | public void Remove(IControl control)
39 | {
40 |
41 | using (Freeze())
42 | {
43 | var child = _children.FirstOrDefault(c => c.Child == control);
44 |
45 | if (child == null) return;
46 |
47 | child.Dispose();
48 | _children.Remove(child);
49 |
50 | Initialize();
51 | }
52 | }
53 |
54 | public override Cell this[Position position]
55 | {
56 | get
57 | {
58 | foreach (var child in _children)
59 | if (child.Contains(position))
60 | return child[position];
61 |
62 | return Character.Empty;
63 | }
64 | }
65 |
66 | protected override void Initialize()
67 | {
68 | using (Freeze())
69 | {
70 | int left = 0;
71 | foreach (var child in _children)
72 | {
73 | child.SetOffset(new Vector(left, 0));
74 | child.SetLimits(
75 | new Size(0, MaxSize.Height),
76 | new Size(Math.Max(0, MaxSize.Width - left), MaxSize.Height));
77 |
78 | left += child.Child.Size.Width;
79 | }
80 |
81 | Resize(new Size(left, MaxSize.Height));
82 | }
83 | }
84 |
85 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
86 | {
87 | Initialize();
88 | }
89 |
90 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
91 | {
92 | Update(rect);
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Margin.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public sealed class Margin : Control, IDrawingContextListener
12 | {
13 | private DrawingContext _contentContext = DrawingContext.Dummy;
14 | private DrawingContext ContentContext
15 | {
16 | get => _contentContext;
17 | set => Setter
18 | .SetDisposable(ref _contentContext, value)
19 | .Then(Initialize);
20 | }
21 |
22 | private IControl _content;
23 | public IControl Content
24 | {
25 | get => _content;
26 | set => Setter
27 | .Set(ref _content, value)
28 | .Then(BindContent);
29 | }
30 |
31 | private Offset offset;
32 | public Offset Offset
33 | {
34 | get => offset;
35 | set => Setter
36 | .Set(ref offset, value)
37 | .Then(Initialize);
38 | }
39 |
40 | public override Cell this[Position position]
41 | {
42 | get
43 | {
44 | if (ContentContext.Contains(position))
45 | return ContentContext[position];
46 |
47 | return Character.Empty;
48 | }
49 | }
50 |
51 | protected override void Initialize()
52 | {
53 | using (Freeze())
54 | {
55 | ContentContext?.SetOffset(new Vector(Offset.Left, Offset.Top));
56 | ContentContext?.SetLimits(
57 | MinSize.AsRect().Remove(Offset).Size,
58 | MaxSize.AsRect().Remove(Offset).Size);
59 |
60 | Resize(Content?.Size.AsRect().Add(Offset).Size ?? Size.Empty);
61 | }
62 | }
63 |
64 | private void BindContent()
65 | {
66 | ContentContext = new DrawingContext(this, Content);
67 | }
68 |
69 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
70 | {
71 | Initialize();
72 | }
73 |
74 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
75 | {
76 | Update(rect);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/MousePanel.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Input;
4 | using ConsoleGUI.Space;
5 | using ConsoleGUI.Utils;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Text;
9 |
10 | namespace ConsoleGUI.Controls
11 | {
12 | public sealed class MousePanel : Control, IDrawingContextListener, IMouseListener
13 | {
14 | public event EventHandler MouseMove;
15 | public event EventHandler MouseUp;
16 | public event EventHandler MouseDown;
17 | public event EventHandler MouseEnter;
18 | public event EventHandler MouseLeave;
19 |
20 | private IControl _content;
21 | public IControl Content
22 | {
23 | get => _content;
24 | set => Setter
25 | .Set(ref _content, value)
26 | .Then(BindContent);
27 | }
28 |
29 | private DrawingContext _contentContext = DrawingContext.Dummy;
30 | public DrawingContext ContentContext
31 | {
32 | get => _contentContext;
33 | set => Setter
34 | .SetDisposable(ref _contentContext, value)
35 | .Then(Initialize);
36 | }
37 |
38 | private bool _interceptChildEvents;
39 | public bool InterceptChildEvents
40 | {
41 | get => _interceptChildEvents;
42 | set => Setter
43 | .Set(ref _interceptChildEvents, value)
44 | .Then(Redraw);
45 | }
46 |
47 | private Position? _mousePosition;
48 | public Position? MousePosition
49 | {
50 | get => _mousePosition;
51 | private set => Setter
52 | .Set(ref _mousePosition, value);
53 | }
54 |
55 | private bool _isMouseDown;
56 | public bool IsMouseDown
57 | {
58 | get => _isMouseDown;
59 | private set => Setter
60 | .Set(ref _isMouseDown, value);
61 | }
62 |
63 | public bool IsMouseOver => MousePosition.HasValue;
64 |
65 | public override Cell this[Position position]
66 | {
67 | get
68 | {
69 | var cell = ContentContext[position];
70 |
71 | if (InterceptChildEvents || !cell.MouseListener.HasValue)
72 | cell = cell.WithMouseListener(this, position);
73 |
74 | return cell;
75 | }
76 | }
77 |
78 | protected override void Initialize()
79 | {
80 | using (Freeze())
81 | {
82 | ContentContext.SetLimits(MinSize, MaxSize);
83 | Resize(ContentContext.Size);
84 | }
85 | }
86 |
87 | private void BindContent()
88 | {
89 | this.ContentContext = new DrawingContext(this, Content);
90 | }
91 |
92 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
93 | {
94 | Initialize();
95 | }
96 |
97 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
98 | {
99 | Update(rect);
100 | }
101 |
102 | public void OnMouseDown(Position position)
103 | {
104 | IsMouseDown = true;
105 | MouseDown?.Invoke(this, position);
106 | }
107 |
108 | void IMouseListener.OnMouseEnter()
109 | {
110 | MouseEnter?.Invoke(this, EventArgs.Empty);
111 | }
112 |
113 | void IMouseListener.OnMouseLeave()
114 | {
115 | IsMouseDown = false;
116 | MousePosition = null;
117 | MouseLeave?.Invoke(this, EventArgs.Empty);
118 | }
119 |
120 | void IMouseListener.OnMouseMove(Position position)
121 | {
122 | MousePosition = position;
123 | MouseMove?.Invoke(this, position);
124 | }
125 |
126 | void IMouseListener.OnMouseUp(Position position)
127 | {
128 | IsMouseDown = false;
129 | MouseUp?.Invoke(this, position);
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Overlay.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class Overlay : Control, IDrawingContextListener
12 | {
13 | private DrawingContext _topContentContext = DrawingContext.Dummy;
14 | private DrawingContext TopContentContext
15 | {
16 | get => _topContentContext;
17 | set => Setter
18 | .SetDisposable(ref _topContentContext, value)
19 | .Then(Initialize);
20 | }
21 |
22 | private DrawingContext _bottomContentContext = DrawingContext.Dummy;
23 | private DrawingContext BottomContentContext
24 | {
25 | get => _bottomContentContext;
26 | set => Setter
27 | .SetDisposable(ref _bottomContentContext, value)
28 | .Then(Initialize);
29 | }
30 |
31 | private IControl _topContent;
32 | public IControl TopContent
33 | {
34 | get => _topContent;
35 | set => Setter
36 | .Set(ref _topContent, value)
37 | .Then(BindTopContent);
38 | }
39 |
40 | private IControl _bottomContent;
41 | public IControl BottomContent
42 | {
43 | get => _bottomContent;
44 | set => Setter
45 | .Set(ref _bottomContent, value)
46 | .Then(BindBottomContent);
47 | }
48 |
49 | public override Cell this[Position position]
50 | {
51 | get
52 | {
53 | if (TopContentContext.Contains(position))
54 | {
55 | var cell = TopContentContext[position];
56 | if (cell.Character != Character.Empty) return cell;
57 | }
58 |
59 | if (BottomContentContext.Contains(position))
60 | {
61 | var cell = BottomContentContext[position];
62 | if (cell.Character != Character.Empty) return cell;
63 | }
64 |
65 | return Character.Empty;
66 | }
67 | }
68 |
69 | protected override void Initialize()
70 | {
71 | using (Freeze())
72 | {
73 | TopContentContext.SetLimits(MinSize, MaxSize);
74 | BottomContentContext.SetLimits(MinSize, MaxSize);
75 |
76 | Resize(Size.Max(TopContentContext.Size, BottomContentContext.Size));
77 | }
78 | }
79 |
80 | private void BindTopContent()
81 | {
82 | TopContentContext = new DrawingContext(this, TopContent);
83 | }
84 |
85 | private void BindBottomContent()
86 | {
87 | BottomContentContext = new DrawingContext(this, BottomContent);
88 | }
89 |
90 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
91 | {
92 | Initialize();
93 | }
94 |
95 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
96 | {
97 | Update(rect);
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/Style.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class Style : Control, IDrawingContextListener
12 | {
13 | private DrawingContext _contentContext = DrawingContext.Dummy;
14 | private DrawingContext ContentContext
15 | {
16 | get => _contentContext;
17 | set => Setter
18 | .SetDisposable(ref _contentContext, value)
19 | .Then(Initialize);
20 | }
21 |
22 | private IControl _content;
23 | public IControl Content
24 | {
25 | get => _content;
26 | set => Setter
27 | .Set(ref _content, value)
28 | .Then(BindContent);
29 | }
30 |
31 | private Color? _background;
32 | public Color? Background
33 | {
34 | get => _background;
35 | set => Setter
36 | .Set(ref _background, value)
37 | .Then(Redraw);
38 | }
39 |
40 | private Color? _foreground;
41 | public Color? Foreground
42 | {
43 | get => _foreground;
44 | set => Setter
45 | .Set(ref _foreground, value)
46 | .Then(Redraw);
47 | }
48 |
49 | public override Cell this[Position position]
50 | {
51 | get
52 | {
53 | if (!ContentContext.Contains(position)) return Character.Empty;
54 |
55 | var cell = ContentContext[position];
56 |
57 | if (!cell.Content.HasValue) return Character.Empty;
58 |
59 | return cell
60 | .WithForeground(Foreground ?? cell.Foreground)
61 | .WithBackground(Background ?? cell.Background);
62 | }
63 | }
64 |
65 | protected override void Initialize()
66 | {
67 | using (Freeze())
68 | {
69 | ContentContext.SetLimits(MinSize, MaxSize);
70 |
71 | Resize(Size.Clip(MinSize, ContentContext.Size, MaxSize));
72 | }
73 | }
74 |
75 | private void BindContent()
76 | {
77 | ContentContext = new DrawingContext(this, Content);
78 | }
79 |
80 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
81 | {
82 | Initialize();
83 | }
84 |
85 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
86 | {
87 | Update(rect);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/TextBlock.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public class TextBlock : Control
12 | {
13 | private string _text = "";
14 | public string Text
15 | {
16 | get => _text;
17 | set => Setter
18 | .Set(ref _text, value)
19 | .Then(Initialize);
20 | }
21 |
22 | private Color? _color;
23 | public Color? Color
24 | {
25 | get => _color;
26 | set => Setter
27 | .Set(ref _color, value)
28 | .Then(Redraw);
29 | }
30 |
31 | public override Cell this[Position position]
32 | {
33 | get
34 | {
35 | if (Text == null) return Character.Empty;
36 | if (position.X >= Text.Length) return Character.Empty;
37 | if (position.Y >= 1) return Character.Empty;
38 | return new Character(Text[position.X], foreground: Color);
39 | }
40 | }
41 |
42 | protected override void Initialize()
43 | {
44 | Resize(new Size(Text.Length, 1));
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/TextBox.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Input;
4 | using ConsoleGUI.Space;
5 | using ConsoleGUI.Utils;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Text;
9 |
10 | namespace ConsoleGUI.Controls
11 | {
12 | public class TextBox : Control, IInputListener, IMouseListener
13 | {
14 | public event EventHandler Clicked;
15 |
16 | private string _text = string.Empty;
17 | public string Text
18 | {
19 | get => _text ?? string.Empty;
20 | set => Setter
21 | .Set(ref _text, value)
22 | .Then(Initialize);
23 | }
24 |
25 | private int _caretStart;
26 | public int CaretStart
27 | {
28 | get => Math.Min(Math.Max(_caretStart, 0), TextLength);
29 | set => Setter
30 | .Set(ref _caretStart, value)
31 | .Then(Redraw);
32 | }
33 |
34 | private int _caretEnd;
35 | public int CaretEnd
36 | {
37 | get => Math.Min(Math.Max(_caretEnd, CaretStart), TextLength);
38 | set => Setter
39 | .Set(ref _caretEnd, value)
40 | .Then(Redraw);
41 | }
42 |
43 | public int Caret
44 | {
45 | set
46 | {
47 | using(Freeze())
48 | {
49 | CaretStart = value;
50 | CaretEnd = value;
51 | }
52 | }
53 | }
54 |
55 | private bool _showCaret = true;
56 | public bool ShowCaret
57 | {
58 | get => _showCaret;
59 | set => Setter
60 | .Set(ref _showCaret, value)
61 | .Then(Redraw);
62 | }
63 |
64 | private int? _mouseDownPosition;
65 | private int? MouseDownPosition
66 | {
67 | get => _mouseDownPosition;
68 | set => Setter
69 | .Set(ref _mouseDownPosition, value)
70 | .Then(UpdateSelection);
71 | }
72 |
73 | private int? _mousePosition;
74 | private int? MousePosition
75 | {
76 | get => _mousePosition;
77 | set => Setter
78 | .Set(ref _mousePosition, value)
79 | .Then(UpdateSelection);
80 | }
81 |
82 | private int TextLength => Text?.Length ?? 0;
83 | private Size TextSize => new Size(TextLength, 1);
84 | private Size EditorSize => TextSize.Expand(1, 0);
85 |
86 | public override Cell this[Position position]
87 | {
88 | get
89 | {
90 | if (CaretEnd + 1 > Size.Width)
91 | position = position.Move(CaretEnd - Size.Width + 1, 0);
92 |
93 | var content = EditorSize.Contains(position) && position.X < TextLength
94 | ? Text[position.X]
95 | : (char?)null;
96 |
97 | var cell = new Cell(content).WithMouseListener(this, position);
98 |
99 | if (ShowCaret && position.X == CaretStart && position.X == CaretEnd)
100 | cell = cell.WithBackground(new Color(70, 70, 70));
101 | if (ShowCaret && position.X >= CaretStart && position.X < CaretEnd)
102 | cell = cell.WithBackground(Color.White).WithForeground(Color.Black);
103 |
104 | return cell;
105 | }
106 | }
107 |
108 | void IInputListener.OnInput(InputEvent inputEvent)
109 | {
110 | using (Freeze())
111 | {
112 | string newText = null;
113 |
114 | switch (inputEvent.Key.Key)
115 | {
116 | case ConsoleKey.LeftArrow when inputEvent.Key.Modifiers.HasFlag(ConsoleModifiers.Control):
117 | CaretStart = Math.Max(0, CaretStart - 1);
118 | break;
119 | case ConsoleKey.LeftArrow:
120 | CaretStart = CaretEnd = Math.Max(0, CaretStart - 1);
121 | break;
122 | case ConsoleKey.RightArrow when inputEvent.Key.Modifiers.HasFlag(ConsoleModifiers.Control):
123 | CaretEnd = Math.Min(Text.Length, CaretEnd + 1);
124 | break;
125 | case ConsoleKey.RightArrow:
126 | CaretStart = CaretEnd = Math.Min(Text.Length, CaretEnd + 1);
127 | break;
128 | case ConsoleKey.UpArrow:
129 | CaretStart = CaretEnd = TextUtils.PreviousLine(Text, CaretStart);
130 | break;
131 | case ConsoleKey.DownArrow:
132 | CaretStart = CaretEnd = TextUtils.NextLine(Text, CaretEnd);
133 | break;
134 | case ConsoleKey.Delete when CaretStart != CaretEnd:
135 | case ConsoleKey.Backspace when CaretStart != CaretEnd:
136 | newText = $"{Text.Substring(0, CaretStart)}{Text.Substring(CaretEnd)}";
137 | CaretEnd = CaretStart;
138 | break;
139 | case ConsoleKey.Backspace when CaretStart > 0:
140 | newText = $"{Text.Substring(0, CaretStart - 1)}{Text.Substring(CaretStart)}";
141 | CaretStart = CaretEnd = CaretStart - 1;
142 | break;
143 | case ConsoleKey.Delete when CaretStart < TextLength:
144 | newText = $"{Text.Substring(0, CaretStart)}{Text.Substring(CaretStart + 1)}";
145 | break;
146 | case ConsoleKey key when char.IsControl(inputEvent.Key.KeyChar) && inputEvent.Key.Key != ConsoleKey.Enter:
147 | return;
148 | default:
149 | var character = inputEvent.Key.Key == ConsoleKey.Enter
150 | ? '\n'
151 | : inputEvent.Key.KeyChar;
152 | newText = $"{Text.Substring(0, CaretStart)}{character}{Text.Substring(CaretEnd)}";
153 | CaretStart = CaretEnd = CaretStart + 1;
154 | break;
155 | }
156 |
157 | if (newText != null)
158 | Text = newText;
159 |
160 | inputEvent.Handled = true;
161 | }
162 | }
163 |
164 | protected override void Initialize()
165 | {
166 | using (Freeze())
167 | {
168 | Resize(EditorSize);
169 | }
170 | }
171 |
172 | private void UpdateSelection()
173 | {
174 | if (!MouseDownPosition.HasValue) return;
175 | if (!MousePosition.HasValue) return;
176 |
177 | CaretStart = Math.Min(MouseDownPosition.Value, MousePosition.Value);
178 | CaretEnd = Math.Max(MouseDownPosition.Value, MousePosition.Value);
179 | }
180 |
181 | void IMouseListener.OnMouseEnter()
182 | { }
183 |
184 | void IMouseListener.OnMouseLeave()
185 | {
186 | MouseDownPosition = null;
187 | MousePosition = null;
188 | }
189 |
190 | void IMouseListener.OnMouseUp(Position position)
191 | {
192 | MousePosition = position.X;
193 | MouseDownPosition = null;
194 | }
195 |
196 | void IMouseListener.OnMouseDown(Position position)
197 | {
198 | MouseDownPosition = position.X;
199 | Clicked?.Invoke(this, EventArgs.Empty);
200 | }
201 |
202 | void IMouseListener.OnMouseMove(Position position)
203 | {
204 | MousePosition = position.X;
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/VerticalScrollPanel.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Input;
4 | using ConsoleGUI.Space;
5 | using ConsoleGUI.Utils;
6 | using System;
7 |
8 | namespace ConsoleGUI.Controls
9 | {
10 | public class VerticalScrollPanel : Control, IDrawingContextListener, IInputListener
11 | {
12 | private DrawingContext _contentContext = DrawingContext.Dummy;
13 | private DrawingContext ContentContext
14 | {
15 | get => _contentContext;
16 | set => Setter
17 | .SetDisposable(ref _contentContext, value)
18 | .Then(Initialize);
19 | }
20 |
21 | private IControl _content;
22 | public IControl Content
23 | {
24 | get => _content;
25 | set => Setter
26 | .Set(ref _content, value)
27 | .Then(BindContent);
28 | }
29 |
30 | private int _top;
31 | public int Top
32 | {
33 | get => _top;
34 | set => Setter
35 | .Set(ref _top, Math.Min(ContentContext.Size.Height - Size.Height, Math.Max(0, value)))
36 | .Then(Initialize);
37 | }
38 |
39 | private Character _scrollBarForeground = new Character('▀', foreground: new Color(100, 100, 255));
40 | public Character ScrollBarForeground
41 | {
42 | get => _scrollBarForeground;
43 | set => Setter
44 | .Set(ref _scrollBarForeground, value)
45 | .Then(RedrawScrollBar);
46 | }
47 |
48 | private Character _scrollBarBackground = new Character('║', foreground: new Color(100, 100, 100));
49 | public Character ScrollBarBackground
50 | {
51 | get => _scrollBarBackground;
52 | set => Setter
53 | .Set(ref _scrollBarBackground, value)
54 | .Then(RedrawScrollBar);
55 | }
56 |
57 | public ConsoleKey ScrollUpKey { get; set; } = ConsoleKey.UpArrow;
58 | public ConsoleKey ScrollDownKey { get; set; } = ConsoleKey.DownArrow;
59 |
60 | public override Cell this[Position position]
61 | {
62 | get
63 | {
64 | if (position.X != Size.Width - 1)
65 | return ContentContext[position];
66 |
67 | if (Content == null) return ScrollBarForeground;
68 | if (Content.Size.Height <= Size.Height) return ScrollBarForeground;
69 | if (position.Y * Content.Size.Height < Top * Size.Height) return ScrollBarBackground;
70 | if (position.Y * Content.Size.Height > (Top + Size.Height) * Size.Height) return ScrollBarBackground;
71 |
72 | return ScrollBarForeground;
73 | }
74 | }
75 |
76 | protected override void Initialize()
77 | {
78 | using (Freeze())
79 | {
80 | ContentContext.SetLimits(MaxSize.Shrink(1, 0), MaxSize.Shrink(1, 0).WithInfitineHeight());
81 | ContentContext.SetOffset(new Vector(0, -Top));
82 |
83 | Resize(Size.Clip(MinSize, ContentContext.Size.Expand(1, 0), MaxSize));
84 | }
85 | }
86 |
87 | private void BindContent()
88 | {
89 | ContentContext = new DrawingContext(this, Content);
90 | }
91 |
92 | private void RedrawScrollBar()
93 | {
94 | Update(Size.WithWidth(1).AsRect().Move(Size.Width - 1, 0));
95 | }
96 |
97 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
98 | {
99 | Initialize();
100 | }
101 |
102 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
103 | {
104 | Update(rect);
105 | }
106 |
107 | void IInputListener.OnInput(InputEvent inputEvent)
108 | {
109 | if (inputEvent.Key.Key == ScrollUpKey)
110 | {
111 | Top -= 1;
112 | inputEvent.Handled = true;
113 | }
114 | else if (inputEvent.Key.Key == ScrollDownKey)
115 | {
116 | Top += 1;
117 | inputEvent.Handled = true;
118 | }
119 | }
120 | }
121 | }
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/VerticalSeparator.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 |
9 | namespace ConsoleGUI.Controls
10 | {
11 | public sealed class VerticalSeparator : Control
12 | {
13 | private Character character = new Character('│');
14 | public Character Character
15 | {
16 | get => character;
17 | set => Setter
18 | .Set(ref character, value)
19 | .Then(Initialize);
20 | }
21 |
22 | public override Cell this[Position position] => Character;
23 |
24 | protected override void Initialize()
25 | {
26 | Resize(new Size(1, MinSize.Height));
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/VerticalStackPanel.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 |
8 | namespace ConsoleGUI.Controls
9 | {
10 | public class VerticalStackPanel : Control, IDrawingContextListener
11 | {
12 | private readonly List _children = new List();
13 | public IEnumerable Children
14 | {
15 | get => _children.Select(c => c.Child);
16 | set
17 | {
18 | foreach (var child in _children) child.Dispose();
19 |
20 | _children.Clear();
21 |
22 | foreach (var child in value) _children.Add(new DrawingContext(this, child));
23 |
24 | Initialize();
25 | }
26 | }
27 |
28 | public void Add(IControl control)
29 | {
30 | using (Freeze())
31 | {
32 | _children.Add(new DrawingContext(this, control));
33 |
34 | Initialize();
35 | }
36 | }
37 |
38 | public void Remove(IControl control)
39 | {
40 |
41 | using (Freeze())
42 | {
43 | var child = _children.FirstOrDefault(c => c.Child == control);
44 |
45 | if (child == null) return;
46 |
47 | child.Dispose();
48 | _children.Remove(child);
49 |
50 | Initialize();
51 | }
52 | }
53 |
54 | public override Cell this[Position position]
55 | {
56 | get
57 | {
58 | foreach (var child in _children)
59 | if (child.Contains(position))
60 | return child[position];
61 |
62 | return Character.Empty;
63 | }
64 | }
65 |
66 | protected override void Initialize()
67 | {
68 | using (Freeze())
69 | {
70 | int top = 0;
71 | foreach (var child in _children)
72 | {
73 | child.SetOffset(new Vector(0, top));
74 | child.SetLimits(
75 | new Size(MaxSize.Width, 0),
76 | new Size(MaxSize.Width, Math.Max(0, MaxSize.Height - top)));
77 |
78 | top += child.Child.Size.Height;
79 | }
80 |
81 | Resize(new Size(MaxSize.Width, top));
82 | }
83 | }
84 |
85 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
86 | {
87 | Initialize();
88 | }
89 |
90 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
91 | {
92 | Update(rect);
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/ConsoleGUI/Controls/WrapPanel.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Common;
2 | using ConsoleGUI.Data;
3 | using ConsoleGUI.Space;
4 | using ConsoleGUI.Utils;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Text;
9 |
10 | namespace ConsoleGUI.Controls
11 | {
12 | public class WrapPanel : Control, IDrawingContextListener
13 | {
14 | private DrawingContext _contentContext = DrawingContext.Dummy;
15 | private DrawingContext ContentContext
16 | {
17 | get => _contentContext;
18 | set => Setter
19 | .SetDisposable(ref _contentContext, value)
20 | .Then(Initialize);
21 | }
22 |
23 | private IControl _content;
24 | public IControl Content
25 | {
26 | get => _content;
27 | set => Setter
28 | .Set(ref _content, value)
29 | .Then(BindContent);
30 | }
31 |
32 | public override Cell this[Position position]
33 | {
34 | get
35 | {
36 | var localPosition = position.UnWrap(Size.Width);
37 |
38 | if (ContentContext.Contains(localPosition))
39 | return ContentContext[localPosition];
40 |
41 | return Character.Empty;
42 | }
43 | }
44 |
45 | protected override void Initialize()
46 | {
47 | if (MaxSize.Width == 0)
48 | {
49 | Redraw();
50 | return;
51 | }
52 |
53 | using (Freeze())
54 | {
55 | ContentContext.SetLimits(
56 | new Size(0, 1),
57 | new Size(Math.Max(0, MaxSize.Width * MaxSize.Height), 1));
58 |
59 | Resize(new Size(Math.Min(ContentContext.Size.Width, MaxSize.Width), (ContentContext.Size.Width - 1) / MaxSize.Width + 1));
60 | }
61 | }
62 |
63 | private void BindContent()
64 | {
65 | ContentContext = new DrawingContext(this, Content);
66 | }
67 |
68 | void IDrawingContextListener.OnRedraw(DrawingContext drawingContext)
69 | {
70 | Initialize();
71 | }
72 |
73 | void IDrawingContextListener.OnUpdate(DrawingContext drawingContext, Rect rect)
74 | {
75 | if (Size.Width == 0)
76 | {
77 | Redraw();
78 | return;
79 | }
80 |
81 | var begin = rect.TopLeftCorner.Wrap(Size.Width);
82 | var end = rect.BottomRightCorner.Wrap(Size.Width);
83 |
84 | Update(new Rect(0, begin.Y, Size.Width, end.Y - begin.Y + 1));
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/ConsoleGUI/Data/BorderPlacement.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Space;
2 | using System;
3 |
4 | namespace ConsoleGUI.Data
5 | {
6 | [Flags]
7 | public enum BorderPlacement
8 | {
9 | None = 0b0000,
10 | Left = 0b0001,
11 | Top = 0b0010,
12 | Right = 0b0100,
13 | Bottom = 0b1000,
14 | All = 0b1111
15 | }
16 |
17 | public static class BorderPalcementExtension
18 | {
19 | public static bool HasBorder(this BorderPlacement self, BorderPlacement border) => (self & border) == border;
20 |
21 | public static int Offset(this BorderPlacement self, BorderPlacement border) => self.HasBorder(border) ? 1 : 0;
22 |
23 | public static Offset AsOffset(this BorderPlacement self)
24 | {
25 | return new Offset(
26 | self.Offset(BorderPlacement.Left),
27 | self.Offset(BorderPlacement.Top),
28 | self.Offset(BorderPlacement.Right),
29 | self.Offset(BorderPlacement.Bottom));
30 | }
31 |
32 | public static Vector AsVector(this BorderPlacement self)
33 | {
34 | return new Vector(
35 | self.Offset(BorderPlacement.Left),
36 | self.Offset(BorderPlacement.Top));
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ConsoleGUI/Data/BorderStyle.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Data
6 | {
7 | public readonly struct BorderStyle
8 | {
9 | public readonly Character Top;
10 | public readonly Character TopRight;
11 | public readonly Character Right;
12 | public readonly Character BottomRight;
13 | public readonly Character Bottom;
14 | public readonly Character BottomLeft;
15 | public readonly Character Left;
16 | public readonly Character TopLeft;
17 |
18 | public BorderStyle(
19 | in Character top,
20 | in Character topRight,
21 | in Character right,
22 | in Character bottomRight,
23 | in Character bottom,
24 | in Character bottomLeft,
25 | in Character left,
26 | in Character topLeft)
27 | {
28 | Top = top;
29 | TopRight = topRight;
30 | Right = right;
31 | BottomRight = bottomRight;
32 | Bottom = bottom;
33 | BottomLeft = bottomLeft;
34 | Left = left;
35 | TopLeft = topLeft;
36 | }
37 |
38 | public static BorderStyle Double => new BorderStyle(
39 | new Character('═'),
40 | new Character('╗'),
41 | new Character('║'),
42 | new Character('╝'),
43 | new Character('═'),
44 | new Character('╚'),
45 | new Character('║'),
46 | new Character('╔'));
47 |
48 | public static BorderStyle Single => new BorderStyle(
49 | new Character('─'),
50 | new Character('┐'),
51 | new Character('│'),
52 | new Character('┘'),
53 | new Character('─'),
54 | new Character('└'),
55 | new Character('│'),
56 | new Character('┌'));
57 |
58 | public BorderStyle WithColor(in Color foreground) => new BorderStyle(
59 | Top.WithForeground(foreground),
60 | TopRight.WithForeground(foreground),
61 | Right.WithForeground(foreground),
62 | BottomRight.WithForeground(foreground),
63 | Bottom.WithForeground(foreground),
64 | BottomLeft.WithForeground(foreground),
65 | Left.WithForeground(foreground),
66 | TopLeft.WithForeground(foreground));
67 |
68 | public BorderStyle WithTopLeft(in Character topLeft) => new BorderStyle(
69 | Top,
70 | TopRight,
71 | Right,
72 | BottomRight,
73 | Bottom,
74 | BottomLeft,
75 | Left,
76 | topLeft);
77 |
78 | public BorderStyle WithTopRight(in Character topRight) => new BorderStyle(
79 | Top,
80 | topRight,
81 | Right,
82 | BottomRight,
83 | Bottom,
84 | BottomLeft,
85 | Left,
86 | TopLeft);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/ConsoleGUI/Data/Cell.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Input;
2 | using ConsoleGUI.Space;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace ConsoleGUI.Data
8 | {
9 | public readonly struct Cell
10 | {
11 | public readonly Character Character;
12 | public readonly MouseContext? MouseListener;
13 |
14 | public Cell(char? content)
15 | {
16 | Character = new Character(content);
17 | MouseListener = null;
18 | }
19 |
20 | public Cell(in Character character)
21 | {
22 | Character = character;
23 | MouseListener = null;
24 | }
25 |
26 | public Cell(in Character character, in MouseContext? mouseListener)
27 | {
28 | Character = character;
29 | MouseListener = mouseListener;
30 | }
31 |
32 | public char? Content => Character.Content;
33 | public Color? Foreground => Character.Foreground;
34 | public Color? Background => Character.Background;
35 |
36 | public Cell WithContent(char? content) => new Cell(Character.WithContent(content), MouseListener);
37 | public Cell WithForeground(in Color? foreground) => new Cell(Character.WithForeground(foreground), MouseListener);
38 | public Cell WithBackground(in Color? background) => new Cell(Character.WithBackground(background), MouseListener);
39 | public Cell WithMouseListener(IMouseListener mouseListener, in Position position) => new Cell(Character, new MouseContext(mouseListener, position));
40 |
41 | public static implicit operator Cell(in Character character) => new Cell(character);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ConsoleGUI/Data/Character.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Data
6 | {
7 | public readonly struct Character
8 | {
9 | public readonly char? Content;
10 |
11 | public readonly Color? Foreground;
12 | public readonly Color? Background;
13 |
14 | public Character(char? content, Color? foreground = null, Color? background = null)
15 | {
16 | Content = content;
17 | Foreground = foreground;
18 | Background = background;
19 | }
20 |
21 | public Character(in Color background)
22 | {
23 | Content = null;
24 | Foreground = null;
25 | Background = background;
26 | }
27 |
28 | public Character WithContent(char? content) => new Character(content, Foreground, Background);
29 | public Character WithForeground(in Color? foreground) => new Character(Content, foreground, Background);
30 | public Character WithBackground(in Color? background) => new Character(Content, Foreground, background);
31 |
32 | public static Character Empty => new Character();
33 |
34 | public static bool operator==(in Character lhs, in Character rhs)
35 | {
36 | return lhs.Content == rhs.Content &&
37 | lhs.Foreground == rhs.Foreground &&
38 | lhs.Background == rhs.Background;
39 | }
40 |
41 | public static bool operator !=(in Character lhs, in Character rhs) => !(lhs == rhs);
42 |
43 | public override bool Equals(object obj)
44 | {
45 | return obj is Character character && this == character;
46 | }
47 |
48 | public override int GetHashCode()
49 | {
50 | var hashCode = -1661473088;
51 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Content);
52 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Foreground);
53 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Background);
54 | return hashCode;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/ConsoleGUI/Data/Color.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Utils;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace ConsoleGUI.Data
7 | {
8 | public readonly struct Color
9 | {
10 | public readonly byte Red;
11 | public readonly byte Green;
12 | public readonly byte Blue;
13 |
14 | public Color(byte red, byte green, byte blue)
15 | {
16 | Red = red;
17 | Green = green;
18 | Blue = blue;
19 | }
20 |
21 | public static implicit operator Color(ConsoleColor color) => ColorConverter.GetColor(color);
22 |
23 | public Color Mix(in Color color, float factor) => this * (1 - factor) + color * factor;
24 |
25 | public static Color White => new Color(255, 255, 255);
26 | public static Color Black => new Color(0, 0, 0);
27 |
28 | public static bool operator ==(in Color lhs, in Color rhs)
29 | {
30 | return
31 | lhs.Red == rhs.Red &&
32 | lhs.Green == rhs.Green &&
33 | lhs.Blue == rhs.Blue;
34 | }
35 |
36 | public static bool operator !=(in Color lhs, in Color rhs) => !(lhs == rhs);
37 |
38 | public static Color operator *(in Color color, float factor) => new Color((byte)(color.Red * factor), (byte)(color.Green * factor), (byte)(color.Blue * factor));
39 | public static Color operator +(in Color lhs, in Color rhs) => new Color(
40 | (byte)Math.Min(byte.MaxValue, lhs.Red + rhs.Red),
41 | (byte)Math.Min(byte.MaxValue, lhs.Green + rhs.Green),
42 | (byte)Math.Min(byte.MaxValue, lhs.Blue + rhs.Blue));
43 |
44 | public override bool Equals(object obj)
45 | {
46 | return obj is Color color && this == color;
47 | }
48 |
49 | public override int GetHashCode()
50 | {
51 | var hashCode = -1058441243;
52 | hashCode = hashCode * -1521134295 + Red.GetHashCode();
53 | hashCode = hashCode * -1521134295 + Green.GetHashCode();
54 | hashCode = hashCode * -1521134295 + Blue.GetHashCode();
55 | return hashCode;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/ConsoleGUI/Data/DataGridStyle.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ConsoleGUI.Data
4 | {
5 | public readonly struct DataGridStyle
6 | {
7 | public readonly Character? HeaderHorizontalBorder;
8 | public readonly Character? HeaderVerticalBorder;
9 | public readonly Character? HeaderIntersectionBorder;
10 | public readonly Character? CellHorizontalBorder;
11 | public readonly Character? CellVerticalBorder;
12 | public readonly Character? CellIntersectionBorder;
13 |
14 | public DataGridStyle(Character? headerHorizontalBorder, Character? headerVerticalBorder, Character? headerIntersectionBorder, Character? cellHorizontalBorder, Character? cellVerticalBorder, Character? cellIntersectionBorder)
15 | {
16 | if (headerVerticalBorder.HasValue ^ cellVerticalBorder.HasValue)
17 | throw new InvalidOperationException($"The {nameof(HeaderVerticalBorder)} and the {nameof(CellVerticalBorder)} have to either both be set or both be left empty.");
18 | if ((headerHorizontalBorder.HasValue && headerVerticalBorder.HasValue) ^ headerIntersectionBorder.HasValue)
19 | throw new InvalidOperationException($"The {nameof(HeaderIntersectionBorder)} needs to be set when and only when the {nameof(HeaderHorizontalBorder)} and the {nameof(HeaderVerticalBorder)} are both set");
20 | if ((cellHorizontalBorder.HasValue && cellVerticalBorder.HasValue) ^ cellIntersectionBorder.HasValue)
21 | throw new InvalidOperationException($"The {nameof(CellIntersectionBorder)} needs to be set when and only when the {nameof(CellHorizontalBorder)} and the {nameof(CellVerticalBorder)} are both set");
22 |
23 | HeaderHorizontalBorder = headerHorizontalBorder;
24 | HeaderVerticalBorder = headerVerticalBorder;
25 | HeaderIntersectionBorder = headerIntersectionBorder;
26 | CellHorizontalBorder = cellHorizontalBorder;
27 | CellVerticalBorder = cellVerticalBorder;
28 | CellIntersectionBorder = cellIntersectionBorder;
29 | }
30 |
31 | internal bool HasVertivalBorders => HeaderVerticalBorder.HasValue;
32 |
33 | public static DataGridStyle AllBorders => new DataGridStyle(new Character('═'), new Character('│'), new Character('╪'), new Character('─'), new Character('│'), new Character('┼'));
34 | public static DataGridStyle NoHorizontalCellBorders => new DataGridStyle(new Character('═'), new Character('│'), new Character('╪'), null, new Character('│'), null);
35 | public static DataGridStyle OnlyHorizontalHeaderBorder => new DataGridStyle(new Character('═'), null, null, null, null, null);
36 | public static DataGridStyle NoBorders => new DataGridStyle(null, null, null, null, null, null);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ConsoleGUI/Data/MouseContext.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Input;
2 | using ConsoleGUI.Space;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace ConsoleGUI.Data
8 | {
9 | public readonly struct MouseContext
10 | {
11 | public readonly IMouseListener MouseListener;
12 | public readonly Position RelativePosition;
13 |
14 | public MouseContext(IMouseListener mouseListener, in Position relativePosition)
15 | {
16 | MouseListener = mouseListener;
17 | RelativePosition = relativePosition;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ConsoleGUI/Data/TextAlignment.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Data
6 | {
7 | public enum TextAlignment
8 | {
9 | Left,
10 | Center,
11 | Right
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ConsoleGUI/IControl.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Data;
2 | using ConsoleGUI.Space;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace ConsoleGUI
8 | {
9 | public delegate void SizeChangedHandler(IControl control);
10 | public delegate void CharacterChangedHandler(IControl control, Position position);
11 |
12 | public interface IControl
13 | {
14 | Cell this[Position position] { get; }
15 |
16 | Size Size { get; }
17 |
18 | IDrawingContext Context { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ConsoleGUI/IDrawingContext.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Space;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace ConsoleGUI
7 | {
8 | public delegate void SizeLimitsChangedHandler(IDrawingContext drawingContext);
9 |
10 | public interface IDrawingContext
11 | {
12 | Size MinSize { get; }
13 | Size MaxSize { get; }
14 |
15 | void Redraw(IControl control);
16 | void Update(IControl control, in Rect rect);
17 |
18 | event SizeLimitsChangedHandler SizeLimitsChanged;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ConsoleGUI/Input/IInputListener.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Input
6 | {
7 | public interface IInputListener
8 | {
9 | void OnInput(InputEvent inputEvent);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ConsoleGUI/Input/IMouseListener.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Space;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace ConsoleGUI.Input
7 | {
8 | public interface IMouseListener
9 | {
10 | void OnMouseEnter();
11 | void OnMouseLeave();
12 | void OnMouseUp(Position position);
13 | void OnMouseDown(Position position);
14 | void OnMouseMove(Position position);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ConsoleGUI/Input/InputEvent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Input
6 | {
7 | public class InputEvent
8 | {
9 | public ConsoleKeyInfo Key { get; }
10 | public bool Handled { get; set; }
11 |
12 | public InputEvent(ConsoleKeyInfo key)
13 | {
14 | Key = key;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ConsoleGUI/Space/Offset.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Space
6 | {
7 | public readonly struct Offset
8 | {
9 | public int Left { get; }
10 | public int Top { get; }
11 | public int Right { get; }
12 | public int Bottom { get; }
13 |
14 | public Offset(int left, int top, int right, int bottom)
15 | {
16 | Left = left;
17 | Top = top;
18 | Right = right;
19 | Bottom = bottom;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ConsoleGUI/Space/Position.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Space
6 | {
7 | public readonly struct Position
8 | {
9 | public int X { get; }
10 | public int Y { get; }
11 |
12 | public Position(int x, int y)
13 | {
14 | X = x;
15 | Y = y;
16 | }
17 |
18 | public static Position Begin => new Position(0, 0);
19 | public static Position At(int x, int y) => new Position(x, y);
20 |
21 | public Position Next => new Position(X + 1, Y);
22 | public Position NextLine => new Position(0, Y + 1);
23 | public Position Move(int x, int y) => new Position(X + x, Y + y);
24 | public Position Move(Vector vector) => new Position(X + vector.X, Y + vector.Y);
25 | public Position Wrap(int width) => new Position(X % width, X / width);
26 | public Position UnWrap(int width) => new Position(X + Y * width, 0);
27 | public Vector AsVector() => new Vector(X, Y);
28 |
29 | public static bool operator ==(in Position lhs, in Position rhs) => lhs.X == rhs.X && lhs.Y == rhs.Y;
30 | public static bool operator !=(in Position lhs, in Position rhs) => !(lhs == rhs);
31 |
32 | public override bool Equals(object obj) => obj is Position position && this == position;
33 |
34 | public override int GetHashCode()
35 | {
36 | var hashCode = -695327075;
37 | hashCode = hashCode * -1521134295 + X.GetHashCode();
38 | hashCode = hashCode * -1521134295 + Y.GetHashCode();
39 | return hashCode;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ConsoleGUI/Space/Rect.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Space
6 | {
7 | public readonly struct Rect
8 | {
9 | public int Left { get; }
10 | public int Top { get; }
11 | public int Width { get; }
12 | public int Height { get; }
13 |
14 | public bool IsEmpty => Width == 0 && Height == 0;
15 | public int Right => Left + Width - 1;
16 | public int Bottom => Top + Height - 1;
17 | public Position TopLeftCorner => new Position(Left, Top);
18 | public Position BottomRightCorner => new Position(Right, Bottom);
19 | public Size Size => new Size(Width, Height);
20 | public Vector Offset => new Vector(Left, Top);
21 |
22 | public Rect(int left, int top, int width, int height)
23 | {
24 | Left = left;
25 | Top = top;
26 | Width = width;
27 | Height = height;
28 | }
29 |
30 | public Rect(in Vector offset, in Size size) : this(offset.X, offset.Y, size.Width, size.Height)
31 | { }
32 |
33 | public static Rect Empty => new Rect(0, 0, 0, 0);
34 |
35 | public static Rect Containing(in Position position) => new Rect(position.X, position.Y, 1, 1);
36 | public static Rect OfSize(in Size size) => new Rect(0, 0, size.Width, size.Height);
37 |
38 | public static Rect Surround(in Rect lhs, in Rect rhs)
39 | {
40 | if (rhs.IsEmpty) return lhs;
41 | return lhs.ExtendBy(rhs.TopLeftCorner).ExtendBy(rhs.BottomRightCorner);
42 | }
43 |
44 | public static Rect Intersect(in Rect lhs, in Rect rhs)
45 | {
46 | if (lhs.IsEmpty || rhs.IsEmpty) return Empty;
47 |
48 | var left = Math.Max(lhs.Left, rhs.Left);
49 | var top = Math.Max(lhs.Top, rhs.Top);
50 | var width = Math.Min(lhs.Right, rhs.Right) - left + 1;
51 | var height = Math.Min(lhs.Bottom, rhs.Bottom) - top + 1;
52 |
53 | return new Rect(left, top, width, height);
54 | }
55 |
56 | public Rect ExtendBy(in Position position)
57 | {
58 | if (IsEmpty) return Containing(position);
59 |
60 | var left = Math.Min(Left, position.X);
61 | var top = Math.Min(Top, position.Y);
62 | var width = Math.Max(Right, position.X) - left + 1;
63 | var height = Math.Max(Bottom, position.Y) - top + 1;
64 |
65 | return new Rect(left, top, width, height);
66 | }
67 |
68 | public Rect Remove(in Offset offset)
69 | {
70 | return new Rect(
71 | Left + offset.Left,
72 | Top + offset.Top,
73 | Math.Max(0, Width - offset.Left - offset.Right),
74 | Math.Max(0, Height - offset.Top - offset.Bottom));
75 | }
76 |
77 | public Rect Add(in Offset offset)
78 | {
79 | return new Rect(
80 | Left + offset.Left,
81 | Top + offset.Top,
82 | Math.Max(0, Width + offset.Left + offset.Right),
83 | Math.Max(0, Height + offset.Top + offset.Bottom));
84 | }
85 |
86 | public Rect Move(int x, int y)
87 | {
88 | return new Rect(
89 | Left + x,
90 | Top + y,
91 | Width,
92 | Height);
93 | }
94 |
95 | public Rect Move(in Vector vector) => Move(vector.X, vector.Y);
96 |
97 | public bool Contains(in Position position)
98 | {
99 | return
100 | position.X >= Left &&
101 | position.X <= Right &&
102 | position.Y >= Top &&
103 | position.Y <= Bottom;
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/ConsoleGUI/Space/Size.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Space
6 | {
7 | public readonly struct Size
8 | {
9 | public int Width { get; }
10 | public int Height { get; }
11 |
12 | public Size(int width, int height)
13 | {
14 | Width = width;
15 | Height = height;
16 | }
17 |
18 | public static int MaxLength => 1_000_000;
19 | public static Size Empty => new Size(0, 0);
20 | public static Size Infinite => new Size(MaxLength, MaxLength);
21 | public static Size Containing(in Position position) => new Size(position.X + 1, position.Y + 1);
22 | public static Size Max(in Size lhs, in Size rhs) => new Size(Math.Max(lhs.Width, rhs.Width), Math.Max(lhs.Height, rhs.Height));
23 | public static Size Min(in Size lhs, in Size rhs) => new Size(Math.Min(lhs.Width, rhs.Width), Math.Min(lhs.Height, rhs.Height));
24 | public static Size Clip(in Size min, in Size value, in Size max) => Max(min, Min(max, value));
25 | public static Size Of(Array array) => new Size(array.GetLength(0), array.GetLength(1));
26 |
27 | public bool Contains(in Size size)
28 | {
29 | return
30 | size.Width <= Width &&
31 | size.Height <= Height;
32 | }
33 |
34 | public bool Contains(in Position position)
35 | {
36 | return
37 | position.X >= 0 &&
38 | position.Y >= 0 &&
39 | position.X < Width &&
40 | position.Y < Height;
41 | }
42 |
43 | public Rect AsRect() => new Rect(0, 0, Width, Height);
44 |
45 | public Size Expand(int width, int height) => new Size(Width + width, Height + height);
46 | public Size Shrink(int width, int height) => new Size(Width - width, Height - height);
47 | public Size WithHeight(int height) => new Size(Width, height);
48 | public Size WithWidth(int width) => new Size(width, Height);
49 | public Size WithInfitineHeight() => new Size(Width, MaxLength);
50 | public Size WithInfitineWidth() => new Size(MaxLength, Height);
51 |
52 | public IEnumerator GetEnumerator()
53 | {
54 | for (int x = 0; x < Width; x++)
55 | for (int y = 0; y < Height; y++)
56 | yield return new Position(x, y);
57 | }
58 |
59 | public static bool operator ==(in Size lhs, in Size rhs) => lhs.Width == rhs.Width && lhs.Height == rhs.Height;
60 | public static bool operator !=(in Size lhs, in Size rhs) => !(lhs == rhs);
61 | public static bool operator <=(in Size lhs, in Size rhs) => lhs.Width <= rhs.Width && lhs.Height <= rhs.Height;
62 | public static bool operator >=(in Size lhs, in Size rhs) => lhs.Width >= rhs.Width && lhs.Height >= rhs.Height;
63 |
64 | public override string ToString()
65 | {
66 | return $"({Width}, {Height})";
67 | }
68 |
69 | public override bool Equals(object obj) => obj is Size size && this == size;
70 |
71 | public override int GetHashCode()
72 | {
73 | var hashCode = 859600377;
74 | hashCode = hashCode * -1521134295 + Width.GetHashCode();
75 | hashCode = hashCode * -1521134295 + Height.GetHashCode();
76 | return hashCode;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ConsoleGUI/Space/Vector.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Space
6 | {
7 | public readonly struct Vector
8 | {
9 | public int X { get; }
10 | public int Y { get; }
11 |
12 | public Vector(int x, int y)
13 | {
14 | X = x;
15 | Y = y;
16 | }
17 |
18 | public static Vector operator -(in Vector vector) => new Vector(-vector.X, -vector.Y);
19 |
20 | public static bool operator ==(in Vector lhs, in Vector rhs) => lhs.X == rhs.X && lhs.Y == rhs.Y;
21 | public static bool operator !=(in Vector lhs, in Vector rhs) => !(lhs == rhs);
22 |
23 | public override bool Equals(object obj) => obj is Vector vector && this == vector;
24 |
25 | public override int GetHashCode()
26 | {
27 | var hashCode = 1861411795;
28 | hashCode = hashCode * -1521134295 + X.GetHashCode();
29 | hashCode = hashCode * -1521134295 + Y.GetHashCode();
30 | return hashCode;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ConsoleGUI/UserDefined/DrawingContextWrapper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using ConsoleGUI.Space;
5 |
6 | namespace ConsoleGUI.UserDefined
7 | {
8 | internal class DrawingContextWrapper : IDrawingContext, IDisposable
9 | {
10 | public IControl Parent { get; }
11 | public IControl Child { get; }
12 | public IDrawingContext Context { get; }
13 |
14 | public DrawingContextWrapper(IControl parent, IControl child, IDrawingContext context)
15 | {
16 | Parent = parent;
17 | Child = child;
18 | Context = context;
19 |
20 | if (Context != null)
21 | Context.SizeLimitsChanged += OnSizeLimitsChanged;
22 | }
23 |
24 | public void Dispose()
25 | {
26 | if (Context != null)
27 | Context.SizeLimitsChanged -= OnSizeLimitsChanged;
28 | }
29 |
30 | private void OnSizeLimitsChanged(IDrawingContext drawingContext)
31 | {
32 | SizeLimitsChanged?.Invoke(this);
33 | }
34 |
35 | public Size MinSize => Context?.MinSize ?? Size.Empty;
36 | public Size MaxSize => Context?.MaxSize ?? Size.Empty;
37 |
38 | public void Redraw(IControl control)
39 | {
40 | if (control != Child) return;
41 |
42 | Context?.Redraw(Parent);
43 | }
44 |
45 | public void Update(IControl control, in Rect rect)
46 | {
47 | if (control != Child) return;
48 |
49 | Context?.Update(Parent, rect);
50 | }
51 |
52 | public event SizeLimitsChangedHandler SizeLimitsChanged;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/ConsoleGUI/UserDefined/SimpleControl.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using ConsoleGUI.Data;
5 | using ConsoleGUI.Space;
6 | using ConsoleGUI.Utils;
7 |
8 | namespace ConsoleGUI.UserDefined
9 | {
10 | public class SimpleControl : IControl
11 | {
12 | private IControl _content;
13 | protected IControl Content
14 | {
15 | get => _content;
16 | set => Setter
17 | .Set(ref _content, value)
18 | .Then(BindContent);
19 | }
20 |
21 | private IDrawingContext _context;
22 | public IDrawingContext Context
23 | {
24 | get => _context;
25 | set => Setter
26 | .Set(ref _context, value)
27 | .Then(BindContent);
28 | }
29 |
30 | private DrawingContextWrapper _contextWrapper;
31 | private DrawingContextWrapper ContextWrapper
32 | {
33 | get => _contextWrapper;
34 | set => Setter
35 | .SetDisposable(ref _contextWrapper, value);
36 | }
37 |
38 | public Cell this[Position position] => _content[position];
39 | public Size Size => _content.Size;
40 |
41 | private void BindContent()
42 | {
43 | ContextWrapper = new DrawingContextWrapper(this, Content, Context);
44 | Content.Context = ContextWrapper;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ConsoleGUI/Utils/ColorConverter.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Data;
2 | using System;
3 |
4 | namespace ConsoleGUI.Utils
5 | {
6 | internal static class ColorConverter
7 | {
8 | public static ConsoleColor GetNearestConsoleColor(Color color)
9 | {
10 | if (Math.Max(Math.Max(color.Red, color.Green), color.Blue) - Math.Min(Math.Min(color.Red, color.Green), color.Blue) < 32)
11 | {
12 | int brightness = ((int)color.Red + (int)color.Green + (int)color.Blue) / 3;
13 | if (brightness < 64) return ConsoleColor.Black;
14 | if (brightness < 160) return ConsoleColor.DarkGray;
15 | if (brightness < 224) return ConsoleColor.Gray;
16 | return ConsoleColor.White;
17 | }
18 | int index = (color.Red > 128 | color.Green > 128 | color.Blue > 128) ? 8 : 0;
19 | index |= (color.Red > 64) ? 4 : 0;
20 | index |= (color.Green > 64) ? 2 : 0;
21 | index |= (color.Blue > 64) ? 1 : 0;
22 | return (ConsoleColor)index;
23 | }
24 |
25 | public static Color GetColor(ConsoleColor color)
26 | {
27 | if (color == ConsoleColor.DarkGray) return new Color(128, 128, 128);
28 | if (color == ConsoleColor.Gray) return new Color(192, 192, 192);
29 | int index = (int)color;
30 | byte d = ((index & 8) != 0) ? (byte)255 : (byte)128;
31 | return new Color(
32 | ((index & 4) != 0) ? d : (byte)0,
33 | ((index & 2) != 0) ? d : (byte)0,
34 | ((index & 1) != 0) ? d : (byte)0);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ConsoleGUI/Utils/DrawingSection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using ConsoleGUI.Data;
5 | using ConsoleGUI.Space;
6 |
7 | namespace ConsoleGUI.Utils
8 | {
9 | internal class DrawingSection : IControl
10 | {
11 | public IDrawingContext Context { get; set; }
12 |
13 | private IControl _content;
14 | public IControl Content
15 | {
16 | get => _content;
17 | set => Setter
18 | .Set(ref _content, value)
19 | .Then(Redraw);
20 | }
21 |
22 | private Rect _rect;
23 | public Rect Rect
24 | {
25 | get => _rect;
26 | set => Setter
27 | .Set(ref _rect, value)
28 | .Then(Redraw);
29 | }
30 |
31 | public Cell this[Position position]
32 | {
33 | get
34 | {
35 | if (Content == null) return Character.Empty;
36 |
37 | position = position.Move(Rect.TopLeftCorner.AsVector());
38 |
39 | if (!Rect.Contains(position)) return Character.Empty;
40 | if (!Content.Size.Contains(position)) return Character.Empty;
41 |
42 | return Content[position];
43 | }
44 | }
45 |
46 | public void Update(in Rect rect)
47 | {
48 | var intersection = Rect.Intersect(Rect, rect);
49 |
50 | if (intersection.IsEmpty) return;
51 |
52 | Context?.Update(this, intersection.Move(-Rect.TopLeftCorner.AsVector()));
53 | }
54 |
55 | public Size Size => Rect.Size;
56 |
57 | private void Redraw() => Context?.Redraw(this);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ConsoleGUI/Utils/FreezeLock.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Utils
6 | {
7 | internal struct FreezeLock
8 | {
9 | private int _freezeCount;
10 |
11 | public void Freeze()
12 | {
13 | _freezeCount++;
14 | }
15 |
16 | public void Unfreeze()
17 | {
18 | _freezeCount--;
19 | }
20 |
21 | public bool IsFrozen => _freezeCount > 0;
22 | public bool IsUnfrozen => !IsFrozen;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ConsoleGUI/Utils/SafeConsole.cs:
--------------------------------------------------------------------------------
1 | using ConsoleGUI.Data;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace ConsoleGUI.Utils
7 | {
8 | internal static class SafeConsole
9 | {
10 | public static void SetCursorPosition(int left, int top)
11 | {
12 | try
13 | {
14 | Console.SetCursorPosition(left, top);
15 | }
16 | catch (Exception)
17 | { }
18 | }
19 |
20 | public static void SetWindowPosition(int left, int top)
21 | {
22 | try
23 | {
24 | Console.SetWindowPosition(left, top);
25 | }
26 | catch (Exception)
27 | { }
28 | }
29 |
30 | public static void SetWindowSize(int width, int height)
31 | {
32 | try
33 | {
34 | Console.SetWindowSize(width, height);
35 | }
36 | catch (Exception)
37 | { }
38 | }
39 |
40 | public static void SetBufferSize(int width, int height)
41 | {
42 | try
43 | {
44 | Console.SetBufferSize(width, height);
45 | }
46 | catch (Exception)
47 | { }
48 | }
49 |
50 | public static void WriteOrThrow(int left, int top, string content)
51 | {
52 | try
53 | {
54 | Console.SetCursorPosition(left, top);
55 | Console.Write(content);
56 | }
57 | catch (Exception)
58 | {
59 | throw new SafeConsoleException();
60 | }
61 | }
62 |
63 | public static void WriteOrThrow(int left, int top, ConsoleColor background, ConsoleColor foreground, char content)
64 | {
65 | try
66 | {
67 | Console.SetCursorPosition(left, top);
68 | Console.BackgroundColor = background;
69 | Console.ForegroundColor = foreground;
70 | Console.Write(content);
71 | }
72 | catch (Exception)
73 | {
74 | throw new SafeConsoleException();
75 | }
76 | }
77 |
78 | public static void SetUtf8()
79 | {
80 | try
81 | {
82 | Console.OutputEncoding = Encoding.UTF8;
83 | }
84 | catch (Exception)
85 | { }
86 | }
87 |
88 | public static void HideCursor()
89 | {
90 | try
91 | {
92 | Console.CursorVisible = false;
93 | }
94 | catch (Exception)
95 | { }
96 | }
97 |
98 | public static void Clear()
99 | {
100 | try
101 | {
102 | Console.Clear();
103 | }
104 | catch (Exception)
105 | { }
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/ConsoleGUI/Utils/SafeConsoleException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Utils
6 | {
7 | internal sealed class SafeConsoleException : Exception
8 | { }
9 | }
10 |
--------------------------------------------------------------------------------
/ConsoleGUI/Utils/Setter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Utils
6 | {
7 | internal class Setter
8 | {
9 | public static SetterContext Set(ref T field, T value)
10 | {
11 | var changed = !Equals(field, value);
12 |
13 | if (changed)
14 | field = value;
15 |
16 | return new SetterContext(changed);
17 | }
18 |
19 | public static SetterContext SetContext(ref IDrawingContext field, IDrawingContext value, SizeLimitsChangedHandler onSizeLimitsChanged)
20 | {
21 | var changed = !Equals(field, value);
22 |
23 | if (changed)
24 | {
25 | if (field != null) field.SizeLimitsChanged -= onSizeLimitsChanged;
26 | if (value != null) value.SizeLimitsChanged += onSizeLimitsChanged;
27 |
28 | field = value;
29 | }
30 |
31 | return new SetterContext(changed);
32 | }
33 |
34 | public static SetterContext SetDisposable(ref T field, T value) where T : IDisposable
35 | {
36 | var changed = !Equals(field, value);
37 |
38 | if (changed)
39 | {
40 | field?.Dispose();
41 |
42 | field = value;
43 | }
44 |
45 | return new SetterContext(changed);
46 | }
47 | }
48 |
49 | internal struct SetterContext
50 | {
51 | public bool Changed { get; private set; }
52 |
53 | public SetterContext(bool changed)
54 | {
55 | Changed = changed;
56 | }
57 |
58 | public SetterContext Then(Action action)
59 | {
60 | if (Changed)
61 | action();
62 |
63 | return this;
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/ConsoleGUI/Utils/TextUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace ConsoleGUI.Utils
6 | {
7 | internal static class TextUtils
8 | {
9 | public static int PreviousLine(string text, int position)
10 | {
11 | var frontOfThisLine = Front(text, position);
12 | if (frontOfThisLine == 0) return 0;
13 |
14 | var previousLine = Front(text, frontOfThisLine - 1);
15 | var leftOffset = LeftOffset(text, position);
16 | if (previousLine + leftOffset >= frontOfThisLine) return frontOfThisLine - 1;
17 |
18 | return previousLine + leftOffset;
19 | }
20 |
21 | public static int NextLine(string text, int position)
22 | {
23 | var backOfThisLine = Back(text, position);
24 | if (backOfThisLine == text.Length - 1) return backOfThisLine;
25 |
26 | var newLine = backOfThisLine + 1;
27 | var leftOffset = LeftOffset(text, position);
28 | var backOfNextLine = Back(text, newLine);
29 | if (newLine + leftOffset > backOfNextLine) return backOfNextLine;
30 |
31 | return newLine + leftOffset;
32 | }
33 |
34 | private static int LeftOffset(string text, int position)
35 | {
36 | return position - Front(text, position);
37 | }
38 |
39 | private static int Front(string text, int position)
40 | {
41 | while (position > 0 && text[position - 1] != '\n') position--;
42 | return position;
43 | }
44 |
45 | private static int Back(string text, int position)
46 | {
47 | while (position < text.Length - 1 && text[position] != '\n') position++;
48 | return position;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Tomasz Rewak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | 
4 |
5 | # ConsoleGUI
6 |
7 | ConsoleGUI is a simple layout-driven .NET framework for creating console-based GUI applications.
8 |
9 | It provides most essential layout management utilities as well as a set of basic controls.
10 |
11 |
12 |
13 |
14 |
15 | *The example above is not really a playable chess game. The board on the left is simply just a grid with some text in it - it's here for display purposes only. But of course, with a little bit of code behind, it could be made interactive.*
16 |
17 | #### Supported platforms
18 |
19 | This framework is platform agnostic and dependency free. The library targets .NET standard 2.0 and should run fine on both Windows and Linux machines.
20 |
21 | #### Motivation
22 |
23 | What sets this library apart from other projects that provide similar functionalities, is the fact that the ConsoleGUI framework is fully layout-driven. In this regard it’s more like WPF or HTML, than for example Windows Forms. You don’t specify exact coordinates at which a given control should reside, but rather let stack panels, dock panels and other layout managers do their work. I don’t claim it’s THE right way of doing things, it’s just what my background is.
24 |
25 | More details about this project (as well as a glimpse at the working example) can be found in this video: https://youtu.be/YIrmjENTaaU
26 |
27 | ## Setup
28 |
29 | First install the NuGet package:
30 |
31 | ```powershell
32 | dotnet add package ConsoleGUI
33 | ```
34 |
35 | then include required namespaces in your code:
36 |
37 | ```csharp
38 | using ConsoleGUI;
39 | using ConsoleGUI.Controls;
40 | using ConsoleGUI.Space;
41 | ```
42 |
43 | and finally setup the `ConsoleManager`:
44 |
45 | ```csharp
46 | // optional: adjusts the buffer size and sets the output encoding to the UTF8
47 | ConsoleManager.Setup();
48 |
49 | // optional: resizes the console window (the size is set in a number of characters, not pixels)
50 | ConsoleManager.Resize(new Size(150, 40));
51 |
52 | // sets the main layout element and prints it on the screen
53 | ConsoleManager.Content = new TextBlock { Text = "Hello world" };
54 | ```
55 |
56 | And that's it. As you can see most of those steps are optional, depending on how you want to configure your window.
57 |
58 | After that, whenever you make a change to any of the controls within the UI tree, the updates will be propagated and displayed automatically. No manual `Redraw()` calls are required.
59 |
60 | #### Threading
61 |
62 | The ConsoleGUI (as many other UI frameworks) is not thread-safe. All UI changes should be performed from the same thread.
63 |
64 | #### Compatibility mode
65 |
66 | By default, the ConsoleGUI uses the true color formatting to provide the best possible user experience by supporting 16777216 foreground and background colors. Unfortunately this way of formatting is not supported by all terminals. Some of them (depending on the platform and software) support only 16bit colors, or just the 4bit colors defined in the `ConsoleColor` enum.
67 |
68 | Terminals that DO NOT support the true color formatting are (for example): powershell.exe and cmd.exe.
69 |
70 | Terminals that DO support the true color formatting are (for example): the new Windows Terminal and the terminal that is built in into the VS.
71 |
72 |
73 |
74 |
75 |
76 | If after starting the application you see the same output as the one on the left, you have to enable the compatibility mode by changing the `Console` interface used by the framework:
77 |
78 | ```csharp
79 | ConsoleManager.Console = new SimplifiedConsole();
80 | ```
81 |
82 | The `SimplifiedConsole` translates all of the RGB colors into 4bit values of the ConsoleColor enum. It also prevents the bottom-right character from being printed, to avoid the bug visible on the right image (in some terminals printing the last character can cause the buffer to scroll).
83 |
84 | If the output is still being printed incorrectly (or if you want to print it on a non-standard console) you can implement the `IConsole` interface yourself and set the `Console` property of the `ConsoleManager` with the instance of your custom class. Alternatively you can also derive from the `StandardConsole` class (which is being used by default) and only override its virtual `void Write(Position, in Character)` method.
85 |
86 | #### Responsiveness
87 |
88 | If the window size is not set explicitly, the layout will be adjusted to the initial size of that window. It's important to note that this framework doesn't detect terminal size changes automatically (as there is no standard way of listening to such events). If the user resizes the window manually, the layout will become broken.
89 |
90 | To adjust the layout to the updated size of the window, remember to call the `AdjustBufferSize` method of the `ConsoleManager` on every frame (on every iteration of your program's main loop). It will compare the current window size with its previous value and, if necessary, redraw the entire screen.
91 |
92 | ## Basic controls
93 |
94 | This is a list of all available controls:
95 |
96 | ##### Background
97 |
98 | Sets the background color of the `Content` control. If the `Important` property is set, the background color will be updated even if the stored control already sets its own background color.
99 |
100 | ##### Border
101 |
102 | Draws a border around the `Content` control. The `BorderPlacement` and the `BorderStyle` can be adjusted to change the look of the generated outline.
103 |
104 | ##### Boundary
105 |
106 | Allows the user to modify the `MinWidth`, `MinHeight`, `MaxWidth` and `MaxHeight` of the `Content` control in relation to its parent control.
107 |
108 | Especially useful to limit the space taken by controls that would otherwise stretch to fill all of the available space (like when storing a `HorizontalStackPanel` within a horizontal `DockPanel`)
109 |
110 | ##### Box
111 |
112 | Aligns the `Content` control vertically (`Top`/`Center`/`Bottom`/`Stretch`) and horizontally (`Left`/`Center`/`Right`/`Stretch`).
113 |
114 | ##### BreakPanel
115 |
116 | Breaks a single line of text into multiple lines based on the available vertical space and new line characters. It can be used with any type of control (`TextBox`, `TextBlock` but also `HorizontalStackPanel` and any other).
117 |
118 | ##### Canvas
119 |
120 | Can host multiple child controls, each displayed within a specified rectangle. Allows content overlapping.
121 |
122 | ##### DataGrid
123 |
124 | Displays `Data` in a grid based on provided column definitions.
125 |
126 | The `ColumnDefinition` defines the column header, its width and the data selector. The selector can be used to extract text from a data row, specify that cell's color, or even define a custom content generator.
127 |
128 | ##### Decorator
129 |
130 | `Decorator` is an abstract class that allows the user to define custom formatting rules (like applying foreground and background colors based on the content and position of a cell), while preserving the layout of the `Content` control. See the `SimpleDecorator` class in the `ConsoleGUI.Example` project for an example.
131 |
132 | ##### DockPanel
133 |
134 | `DockPanel` consists of two parts: `DockedControl` and `FillingControl`. The `DockedControl` is placed within the available space according to the `Placement` property value (`Top`/`Right`/`Bottom`/`Left`). The `FillingControl` takes up all of the remaining space.
135 |
136 | ##### Grid
137 |
138 | Splits the available space into smaller pieces according to the provided `Columns` and `Rows` definitions. Each cell can store up to one child control.
139 |
140 | ##### HorizontalSeparator
141 |
142 | Draws a horizontal line.
143 |
144 | ##### HorizontalStackPanel
145 |
146 | Stacks multiple controls horizontally.
147 |
148 | ##### Margin
149 |
150 | Adds the `Offset` around the `Content` control when displaying it. It affects both the `MinSize` and the `MaxSize` of the `IDrawingContext`.
151 |
152 | ##### Overlay
153 |
154 | Allows two controls to be displayed on top of each other. Unlike the `Canvas`, it uses its own size when specifying size limits for child controls.
155 |
156 | ##### Style
157 |
158 | Modifies the `Background` and the `Foreground` colors of its `Content`.
159 |
160 | ##### TextBlock
161 |
162 | Displays a single line of text.
163 |
164 | ##### TextBox
165 |
166 | An input control. Allows the user to insert a single line of text.
167 |
168 | ##### VerticalScrollPanel
169 |
170 | Allows its `Content` to expand infinitely in the vertical dimension and displays only the part of it that is currently in view. The `ScrollBarForeground` and the `ScrollBarBackground` can be modified to adjust the look of the scroll bar.
171 |
172 | ##### VerticalSeparator
173 |
174 | Draws a vertical line.
175 |
176 | ##### VerticalStackPanel
177 |
178 | Stacks multiple controls vertically.
179 |
180 | ##### WrapPanel
181 |
182 | Breaks a single line of text into multiple lines based on the available vertical space. It can be used with any type of control (`TextBox`, `TextBlock` but also `HorizontalStackPanel` and any other).
183 |
184 | ## Creating custom controls
185 |
186 | The set of predefined control is relatively small, but it's very easy to create custom ones. There are two main ways to do it.
187 |
188 | #### Inheriting the `SimpleControl` class
189 |
190 | If you want to define a control that is simply composed of other controls (like a text box with a specific background and border), inheriting from the `SimpleControl` class is the way to go.
191 |
192 | All you have to do is to set the `protected` `Content` property with a content that you want to display.
193 |
194 | ```csharp
195 | internal sealed class MyControl : SimpleControl
196 | {
197 | private readonly TextBlock _textBlock;
198 |
199 | public MyControl()
200 | {
201 | _textBlock = new TextBlock();
202 |
203 | Content = new Background
204 | {
205 | Color = new Color(200, 200, 100),
206 | Content = new Border
207 | {
208 | Content = _textBlock
209 | }
210 | };
211 | }
212 |
213 | public string Text
214 | {
215 | get => _textBlock.Text;
216 | set => _textBlock.Text = value;
217 | }
218 | }
219 | ```
220 |
221 | #### Implementing the `IControl` interface or inheriting the `Control` class
222 |
223 | This approach can be used to define fully custom controls. All of the basic controls within this library are implemented this way.
224 |
225 | The `IControl` interface requires providing 3 members:
226 |
227 | ```csharp
228 | public interface IControl
229 | {
230 | Character this[Position position] { get; }
231 | Size Size { get; }
232 | IDrawingContext Context { get; set; }
233 | }
234 | ```
235 |
236 | The `[]` operator must return a character that is to be displayed on the specific position. The position is defined relative to this control's space and not to the screen.
237 |
238 | The control can also notify its parent about its internal changes using the provided `Context`. The `IDrawingContext` interface is defined as follows:
239 |
240 | ```csharp
241 | public interface IDrawingContext
242 | {
243 | Size MinSize { get; }
244 | Size MaxSize { get; }
245 |
246 | void Redraw(IControl control);
247 | void Update(IControl control, in Rect rect);
248 |
249 | event SizeLimitsChangedHandler SizeLimitsChanged;
250 | }
251 | ```
252 |
253 | If only a part of the control has changed, it should call the `Update` method, providing a reference to itself and the rect (once again - in its local space) that has to be redrawn. If the `Size` of the control has changed or the entire control requires redrawing, the control should call the `Redraw` method of its current `Context`.
254 |
255 | The `Context` is also used to notify the child control about changes in size limits imposed on it by its parent. The child control should listen to the `SizeLimitsChanged` event and update its layout according to the `MinSize` and `MaxSize` values of the current `Context`.
256 |
257 | When defining a custom control that can host other controls, you might have to implement a custom `IDrawingContext` class.
258 |
259 | Instead of implementing the `IControl` and `IDrawingContext` directly, you can also use the `Control` and `DrawingContext` base classes. They allow for a similar level of flexibility, at the same time providing more advanced functionalities.
260 |
261 | The `DrawingContext` is an `IDisposable` non-abstract class that translates the parent's space into the child's space based on the provided size limits and offset. It also ensures that propagated notifications actually come from the hosted control and not from controls that were previously assigned to a given parent.
262 |
263 | The `Control` class not only trims all of the incoming and outgoing messages to the current size limits but also allows to temporarily freeze the control so that only a single update message is generated after multiple related changes are performed.
264 |
265 | For more information on how to define custom controls using the `IControl`/`IDrawingContext` interfaces or the `Control`/`DrawingContext` classes, please see the source code of one of the controls defined within this library.
266 |
267 | ## Input
268 |
269 | As the standard `Console` class doesn't provide any event-based interface for detecting incoming characters, the availability of input messages has to be checked periodically within the main loop of your application. Of course, it's not required if your layout doesn't contain any interactive components.
270 |
271 | To handle pending input messages, call the `ReadInput` method of the `ConsoleManager` class. It accepts a single argument being a collection of `IInputListener` objects. You can define this collection just once and reuse it - it specifies the list of input elements that are currently active and should be listening to keystrokes. The order of those elements is important, because if one control sets the `Handled` property of the provided `InputEvent`, the propagation will be terminated.
272 |
273 | ```csharp
274 | var input = new IInputListener[]
275 | {
276 | scrollPanel,
277 | tabPanel,
278 | textBox
279 | };
280 |
281 | for (int i = 0; ; i++)
282 | {
283 | Thread.Sleep(10);
284 | ConsoleManager.ReadInput(input);
285 | }
286 | ```
287 |
288 | The `IInputListener` interface is not restricted only for classes that implement the `IControl` interface, but can also be used to define any custom (user defined) controllers that manage application behavior.
289 |
290 | #### Forms
291 |
292 | As you might have noticed, there is no general purpose `Form` control available in this framework. That’s because it’s very hard to come up with a design that would fit all needs. Of course such an obstacle is not a good reason on its own, but at the same time it’s extremely easy to implement a tailor made form controller within the target application itself. Here is an example:
293 |
294 | ```csharp
295 | class FromController : IInputListener
296 | {
297 | IInputListener _currentInput;
298 |
299 | // ...
300 |
301 | public void OnInput(InputEvent inputEvent)
302 | {
303 | if (inputEvent.Key.Key == ConsoleKey.Tab)
304 | {
305 | _currentInput = // nextInput...
306 | inputEvent.Handled = true;
307 | }
308 | else
309 | {
310 | _currentInput.OnInput(inputEvent)
311 | }
312 | }
313 | }
314 | ```
315 |
316 | After implementing it, all you have to do is to initialize an instance of this class with a list of your inputs and call the ` ConsoleManager.ReadInput(fromControllers)` on each frame.
317 |
318 | The biggest strength of this approach is that you decide what is the order of controls within the form, you can do special validation after leaving each input, create a custom layout of the form itself, highlight currently active input, and much, much more. I believe it’s a good tradeoff.
319 |
320 | ## Mouse
321 |
322 | The ConsoleGUI framework does support mouse input, but it doesn’t create mouse event bindings automatically. That's because intercepting and translating mouse events is a very platform-specific operation that might vary based on the operating system and the terminal you are using.
323 |
324 | An example code that properly handles mouse events in the Powershell.exe and cmd.exe terminals can be found in the `ConsoleGUI.MouseExample/MouseHandler.cs` source file. (To use this example you will have to disable the QuickEdit option of your console window).
325 |
326 | When creating your own bindings, all you have to do from the framework perspective, is to set the `MousePosition` and `MouseDown` properties of the `ConsoleManager` whenever a user interaction is detected. For example:
327 |
328 | ```csharp
329 | private static void ProcessMouseEvent(in MouseRecord mouseEvent)
330 | {
331 | ConsoleManager.MousePosition = new Position(mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y);
332 | ConsoleManager.MouseDown = (mouseEvent.ButtonState & 0x0001) != 0;
333 | }
334 | ```
335 |
336 | The `ConsoleManager` will take care of the rest. It will find a control that the cursor is currently hovering over and raise a proper method as described in the `IMouseListener` interface.
337 |
338 |
339 |
340 |
341 |
342 | ## Performance
343 |
344 | This library is designed with high performance applications in mind. It means that if a control requests an `Update`, only the specified screen rectangle will be recalculated, and only if all of its parent controls agree that this part of the content is actually visible.
345 |
346 | As the most expensive operation of the whole process is printing characters on the screen, the `ConsoleManager` defines its own, additional buffer. If the requested pixel (character) didn't change, it's not repainted.
347 |
348 | ## Contributions
349 |
350 | I'm open to all sorts of contributions and feedback.
351 |
352 | Also, please feel free to request new controls/features through github issues.
353 |
--------------------------------------------------------------------------------
/Resources/Problems.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomaszRewak/C-sharp-console-gui-framework/474c8c4601c2050a8aac2a4264be74277f227570/Resources/Problems.png
--------------------------------------------------------------------------------
/Resources/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomaszRewak/C-sharp-console-gui-framework/474c8c4601c2050a8aac2a4264be74277f227570/Resources/example.png
--------------------------------------------------------------------------------
/Resources/input example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomaszRewak/C-sharp-console-gui-framework/474c8c4601c2050a8aac2a4264be74277f227570/Resources/input example.gif
--------------------------------------------------------------------------------