├── .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 | ![](https://github.com/TomaszRewak/C-sharp-console-gui-framework/workflows/build%20windows/badge.svg) 2 | ![](https://github.com/TomaszRewak/C-sharp-console-gui-framework/workflows/build%20linux/badge.svg) 3 | ![](https://github.com/TomaszRewak/C-sharp-console-gui-framework/workflows/tests/badge.svg) 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 --------------------------------------------------------------------------------