├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src ├── app.rs ├── events ├── ant_events.rs ├── elementary_events.rs ├── game_of_life_events.rs ├── main_events.rs └── mod.rs ├── main.rs ├── simulations ├── ant.rs ├── elementary.rs ├── game_of_life.rs └── mod.rs └── ui ├── ant_ui.rs ├── elementary_ui.rs ├── game_of_life_ui.rs ├── main_ui.rs └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "1.1.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "allocator-api2" 28 | version = "0.2.18" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 31 | 32 | [[package]] 33 | name = "autocfg" 34 | version = "1.3.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 37 | 38 | [[package]] 39 | name = "bitflags" 40 | version = "2.6.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 43 | 44 | [[package]] 45 | name = "byteorder" 46 | version = "1.5.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 49 | 50 | [[package]] 51 | name = "cassowary" 52 | version = "0.3.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 55 | 56 | [[package]] 57 | name = "castaway" 58 | version = "0.2.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 61 | dependencies = [ 62 | "rustversion", 63 | ] 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 70 | 71 | [[package]] 72 | name = "compact_str" 73 | version = "0.8.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" 76 | dependencies = [ 77 | "castaway", 78 | "cfg-if", 79 | "itoa", 80 | "rustversion", 81 | "ryu", 82 | "static_assertions", 83 | ] 84 | 85 | [[package]] 86 | name = "crossbeam-deque" 87 | version = "0.8.5" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 90 | dependencies = [ 91 | "crossbeam-epoch", 92 | "crossbeam-utils", 93 | ] 94 | 95 | [[package]] 96 | name = "crossbeam-epoch" 97 | version = "0.9.18" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 100 | dependencies = [ 101 | "crossbeam-utils", 102 | ] 103 | 104 | [[package]] 105 | name = "crossbeam-utils" 106 | version = "0.8.20" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 109 | 110 | [[package]] 111 | name = "crossterm" 112 | version = "0.28.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 115 | dependencies = [ 116 | "bitflags", 117 | "crossterm_winapi", 118 | "mio", 119 | "parking_lot", 120 | "rustix", 121 | "signal-hook", 122 | "signal-hook-mio", 123 | "winapi", 124 | ] 125 | 126 | [[package]] 127 | name = "crossterm_winapi" 128 | version = "0.9.1" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 131 | dependencies = [ 132 | "winapi", 133 | ] 134 | 135 | [[package]] 136 | name = "darling" 137 | version = "0.20.10" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 140 | dependencies = [ 141 | "darling_core", 142 | "darling_macro", 143 | ] 144 | 145 | [[package]] 146 | name = "darling_core" 147 | version = "0.20.10" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 150 | dependencies = [ 151 | "fnv", 152 | "ident_case", 153 | "proc-macro2", 154 | "quote", 155 | "strsim", 156 | "syn", 157 | ] 158 | 159 | [[package]] 160 | name = "darling_macro" 161 | version = "0.20.10" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 164 | dependencies = [ 165 | "darling_core", 166 | "quote", 167 | "syn", 168 | ] 169 | 170 | [[package]] 171 | name = "derive_builder" 172 | version = "0.20.2" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 175 | dependencies = [ 176 | "derive_builder_macro", 177 | ] 178 | 179 | [[package]] 180 | name = "derive_builder_core" 181 | version = "0.20.2" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 184 | dependencies = [ 185 | "darling", 186 | "proc-macro2", 187 | "quote", 188 | "syn", 189 | ] 190 | 191 | [[package]] 192 | name = "derive_builder_macro" 193 | version = "0.20.2" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 196 | dependencies = [ 197 | "derive_builder_core", 198 | "syn", 199 | ] 200 | 201 | [[package]] 202 | name = "either" 203 | version = "1.13.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 206 | 207 | [[package]] 208 | name = "equivalent" 209 | version = "1.0.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 212 | 213 | [[package]] 214 | name = "errno" 215 | version = "0.3.9" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 218 | dependencies = [ 219 | "libc", 220 | "windows-sys", 221 | ] 222 | 223 | [[package]] 224 | name = "fnv" 225 | version = "1.0.7" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 228 | 229 | [[package]] 230 | name = "font8x8" 231 | version = "0.3.1" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" 234 | 235 | [[package]] 236 | name = "futures" 237 | version = "0.3.31" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 240 | dependencies = [ 241 | "futures-channel", 242 | "futures-core", 243 | "futures-executor", 244 | "futures-io", 245 | "futures-sink", 246 | "futures-task", 247 | "futures-util", 248 | ] 249 | 250 | [[package]] 251 | name = "futures-channel" 252 | version = "0.3.31" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 255 | dependencies = [ 256 | "futures-core", 257 | "futures-sink", 258 | ] 259 | 260 | [[package]] 261 | name = "futures-core" 262 | version = "0.3.31" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 265 | 266 | [[package]] 267 | name = "futures-executor" 268 | version = "0.3.31" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 271 | dependencies = [ 272 | "futures-core", 273 | "futures-task", 274 | "futures-util", 275 | ] 276 | 277 | [[package]] 278 | name = "futures-io" 279 | version = "0.3.31" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 282 | 283 | [[package]] 284 | name = "futures-macro" 285 | version = "0.3.31" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 288 | dependencies = [ 289 | "proc-macro2", 290 | "quote", 291 | "syn", 292 | ] 293 | 294 | [[package]] 295 | name = "futures-sink" 296 | version = "0.3.31" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 299 | 300 | [[package]] 301 | name = "futures-task" 302 | version = "0.3.31" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 305 | 306 | [[package]] 307 | name = "futures-timer" 308 | version = "3.0.3" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 311 | 312 | [[package]] 313 | name = "futures-util" 314 | version = "0.3.31" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 317 | dependencies = [ 318 | "futures-channel", 319 | "futures-core", 320 | "futures-io", 321 | "futures-macro", 322 | "futures-sink", 323 | "futures-task", 324 | "memchr", 325 | "pin-project-lite", 326 | "pin-utils", 327 | "slab", 328 | ] 329 | 330 | [[package]] 331 | name = "getrandom" 332 | version = "0.2.15" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 335 | dependencies = [ 336 | "cfg-if", 337 | "libc", 338 | "wasi", 339 | ] 340 | 341 | [[package]] 342 | name = "glob" 343 | version = "0.3.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 346 | 347 | [[package]] 348 | name = "hashbrown" 349 | version = "0.14.5" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 352 | dependencies = [ 353 | "ahash", 354 | "allocator-api2", 355 | ] 356 | 357 | [[package]] 358 | name = "hashbrown" 359 | version = "0.15.0" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 362 | 363 | [[package]] 364 | name = "heck" 365 | version = "0.5.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 368 | 369 | [[package]] 370 | name = "hermit-abi" 371 | version = "0.3.9" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 374 | 375 | [[package]] 376 | name = "ident_case" 377 | version = "1.0.1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 380 | 381 | [[package]] 382 | name = "indexmap" 383 | version = "2.6.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 386 | dependencies = [ 387 | "equivalent", 388 | "hashbrown 0.15.0", 389 | ] 390 | 391 | [[package]] 392 | name = "indoc" 393 | version = "2.0.5" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 396 | 397 | [[package]] 398 | name = "instability" 399 | version = "0.3.2" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" 402 | dependencies = [ 403 | "quote", 404 | "syn", 405 | ] 406 | 407 | [[package]] 408 | name = "itertools" 409 | version = "0.13.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 412 | dependencies = [ 413 | "either", 414 | ] 415 | 416 | [[package]] 417 | name = "itoa" 418 | version = "1.0.11" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 421 | 422 | [[package]] 423 | name = "libc" 424 | version = "0.2.158" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 427 | 428 | [[package]] 429 | name = "linux-raw-sys" 430 | version = "0.4.14" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 433 | 434 | [[package]] 435 | name = "lock_api" 436 | version = "0.4.12" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 439 | dependencies = [ 440 | "autocfg", 441 | "scopeguard", 442 | ] 443 | 444 | [[package]] 445 | name = "log" 446 | version = "0.4.22" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 449 | 450 | [[package]] 451 | name = "lru" 452 | version = "0.12.4" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" 455 | dependencies = [ 456 | "hashbrown 0.14.5", 457 | ] 458 | 459 | [[package]] 460 | name = "memchr" 461 | version = "2.7.4" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 464 | 465 | [[package]] 466 | name = "mio" 467 | version = "1.0.2" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 470 | dependencies = [ 471 | "hermit-abi", 472 | "libc", 473 | "log", 474 | "wasi", 475 | "windows-sys", 476 | ] 477 | 478 | [[package]] 479 | name = "once_cell" 480 | version = "1.19.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 483 | 484 | [[package]] 485 | name = "parking_lot" 486 | version = "0.12.3" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 489 | dependencies = [ 490 | "lock_api", 491 | "parking_lot_core", 492 | ] 493 | 494 | [[package]] 495 | name = "parking_lot_core" 496 | version = "0.9.10" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 499 | dependencies = [ 500 | "cfg-if", 501 | "libc", 502 | "redox_syscall", 503 | "smallvec", 504 | "windows-targets", 505 | ] 506 | 507 | [[package]] 508 | name = "paste" 509 | version = "1.0.15" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 512 | 513 | [[package]] 514 | name = "pin-project-lite" 515 | version = "0.2.14" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 518 | 519 | [[package]] 520 | name = "pin-utils" 521 | version = "0.1.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 524 | 525 | [[package]] 526 | name = "ppv-lite86" 527 | version = "0.2.20" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 530 | dependencies = [ 531 | "zerocopy", 532 | ] 533 | 534 | [[package]] 535 | name = "proc-macro-crate" 536 | version = "3.2.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" 539 | dependencies = [ 540 | "toml_edit", 541 | ] 542 | 543 | [[package]] 544 | name = "proc-macro2" 545 | version = "1.0.86" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 548 | dependencies = [ 549 | "unicode-ident", 550 | ] 551 | 552 | [[package]] 553 | name = "quote" 554 | version = "1.0.37" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 557 | dependencies = [ 558 | "proc-macro2", 559 | ] 560 | 561 | [[package]] 562 | name = "rand" 563 | version = "0.8.5" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 566 | dependencies = [ 567 | "libc", 568 | "rand_chacha", 569 | "rand_core", 570 | ] 571 | 572 | [[package]] 573 | name = "rand_chacha" 574 | version = "0.3.1" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 577 | dependencies = [ 578 | "ppv-lite86", 579 | "rand_core", 580 | ] 581 | 582 | [[package]] 583 | name = "rand_core" 584 | version = "0.6.4" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 587 | dependencies = [ 588 | "getrandom", 589 | ] 590 | 591 | [[package]] 592 | name = "ratatui" 593 | version = "0.28.1" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" 596 | dependencies = [ 597 | "bitflags", 598 | "cassowary", 599 | "compact_str", 600 | "crossterm", 601 | "instability", 602 | "itertools", 603 | "lru", 604 | "paste", 605 | "strum", 606 | "strum_macros", 607 | "unicode-segmentation", 608 | "unicode-truncate", 609 | "unicode-width 0.1.13", 610 | ] 611 | 612 | [[package]] 613 | name = "ratatui" 614 | version = "0.29.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 617 | dependencies = [ 618 | "bitflags", 619 | "cassowary", 620 | "compact_str", 621 | "crossterm", 622 | "indoc", 623 | "instability", 624 | "itertools", 625 | "lru", 626 | "paste", 627 | "strum", 628 | "unicode-segmentation", 629 | "unicode-truncate", 630 | "unicode-width 0.2.0", 631 | ] 632 | 633 | [[package]] 634 | name = "rayon" 635 | version = "1.10.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 638 | dependencies = [ 639 | "either", 640 | "rayon-core", 641 | ] 642 | 643 | [[package]] 644 | name = "rayon-core" 645 | version = "1.12.1" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 648 | dependencies = [ 649 | "crossbeam-deque", 650 | "crossbeam-utils", 651 | ] 652 | 653 | [[package]] 654 | name = "redox_syscall" 655 | version = "0.5.4" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" 658 | dependencies = [ 659 | "bitflags", 660 | ] 661 | 662 | [[package]] 663 | name = "regex" 664 | version = "1.11.0" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" 667 | dependencies = [ 668 | "aho-corasick", 669 | "memchr", 670 | "regex-automata", 671 | "regex-syntax", 672 | ] 673 | 674 | [[package]] 675 | name = "regex-automata" 676 | version = "0.4.8" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 679 | dependencies = [ 680 | "aho-corasick", 681 | "memchr", 682 | "regex-syntax", 683 | ] 684 | 685 | [[package]] 686 | name = "regex-syntax" 687 | version = "0.8.5" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 690 | 691 | [[package]] 692 | name = "relative-path" 693 | version = "1.9.3" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" 696 | 697 | [[package]] 698 | name = "rstest" 699 | version = "0.23.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" 702 | dependencies = [ 703 | "futures", 704 | "futures-timer", 705 | "rstest_macros", 706 | "rustc_version", 707 | ] 708 | 709 | [[package]] 710 | name = "rstest_macros" 711 | version = "0.23.0" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" 714 | dependencies = [ 715 | "cfg-if", 716 | "glob", 717 | "proc-macro-crate", 718 | "proc-macro2", 719 | "quote", 720 | "regex", 721 | "relative-path", 722 | "rustc_version", 723 | "syn", 724 | "unicode-ident", 725 | ] 726 | 727 | [[package]] 728 | name = "rustc_version" 729 | version = "0.4.1" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 732 | dependencies = [ 733 | "semver", 734 | ] 735 | 736 | [[package]] 737 | name = "rustix" 738 | version = "0.38.37" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 741 | dependencies = [ 742 | "bitflags", 743 | "errno", 744 | "libc", 745 | "linux-raw-sys", 746 | "windows-sys", 747 | ] 748 | 749 | [[package]] 750 | name = "rustversion" 751 | version = "1.0.17" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 754 | 755 | [[package]] 756 | name = "ryu" 757 | version = "1.0.18" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 760 | 761 | [[package]] 762 | name = "scopeguard" 763 | version = "1.2.0" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 766 | 767 | [[package]] 768 | name = "semver" 769 | version = "1.0.23" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 772 | 773 | [[package]] 774 | name = "signal-hook" 775 | version = "0.3.17" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 778 | dependencies = [ 779 | "libc", 780 | "signal-hook-registry", 781 | ] 782 | 783 | [[package]] 784 | name = "signal-hook-mio" 785 | version = "0.2.4" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 788 | dependencies = [ 789 | "libc", 790 | "mio", 791 | "signal-hook", 792 | ] 793 | 794 | [[package]] 795 | name = "signal-hook-registry" 796 | version = "1.4.2" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 799 | dependencies = [ 800 | "libc", 801 | ] 802 | 803 | [[package]] 804 | name = "slab" 805 | version = "0.4.9" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 808 | dependencies = [ 809 | "autocfg", 810 | ] 811 | 812 | [[package]] 813 | name = "smallvec" 814 | version = "1.13.2" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 817 | 818 | [[package]] 819 | name = "static_assertions" 820 | version = "1.1.0" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 823 | 824 | [[package]] 825 | name = "strsim" 826 | version = "0.11.1" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 829 | 830 | [[package]] 831 | name = "strum" 832 | version = "0.26.3" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 835 | dependencies = [ 836 | "strum_macros", 837 | ] 838 | 839 | [[package]] 840 | name = "strum_macros" 841 | version = "0.26.4" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 844 | dependencies = [ 845 | "heck", 846 | "proc-macro2", 847 | "quote", 848 | "rustversion", 849 | "syn", 850 | ] 851 | 852 | [[package]] 853 | name = "syn" 854 | version = "2.0.85" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" 857 | dependencies = [ 858 | "proc-macro2", 859 | "quote", 860 | "unicode-ident", 861 | ] 862 | 863 | [[package]] 864 | name = "termCA" 865 | version = "0.1.1" 866 | dependencies = [ 867 | "crossterm", 868 | "rand", 869 | "ratatui 0.29.0", 870 | "rayon", 871 | "tui-big-text", 872 | "tui-input", 873 | "tui-scrollview", 874 | "tui-widget-list", 875 | ] 876 | 877 | [[package]] 878 | name = "toml_datetime" 879 | version = "0.6.8" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 882 | 883 | [[package]] 884 | name = "toml_edit" 885 | version = "0.22.22" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 888 | dependencies = [ 889 | "indexmap", 890 | "toml_datetime", 891 | "winnow", 892 | ] 893 | 894 | [[package]] 895 | name = "tui-big-text" 896 | version = "0.7.0" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "8b046cf880cb40db75567b60ca22dc38a4165ef490be6bfad5718b22bcb6aabf" 899 | dependencies = [ 900 | "derive_builder", 901 | "font8x8", 902 | "itertools", 903 | "ratatui 0.29.0", 904 | ] 905 | 906 | [[package]] 907 | name = "tui-input" 908 | version = "0.10.1" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "bd137780d743c103a391e06fe952487f914b299a4fe2c3626677f6a6339a7c6b" 911 | dependencies = [ 912 | "ratatui 0.28.1", 913 | "unicode-width 0.1.13", 914 | ] 915 | 916 | [[package]] 917 | name = "tui-scrollview" 918 | version = "0.5.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "235ee99b9af57bc0bc8cf42604f5868641690ed652001120d63ca2784a41c3d3" 921 | dependencies = [ 922 | "indoc", 923 | "ratatui 0.29.0", 924 | "rstest", 925 | ] 926 | 927 | [[package]] 928 | name = "tui-widget-list" 929 | version = "0.13.0" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "dd80265ccaaad37d8e315c2ca1ba40dd30bced081ba4fbe22fa0584fd9c1ca3f" 932 | dependencies = [ 933 | "ratatui 0.29.0", 934 | ] 935 | 936 | [[package]] 937 | name = "unicode-ident" 938 | version = "1.0.13" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 941 | 942 | [[package]] 943 | name = "unicode-segmentation" 944 | version = "1.12.0" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 947 | 948 | [[package]] 949 | name = "unicode-truncate" 950 | version = "1.1.0" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 953 | dependencies = [ 954 | "itertools", 955 | "unicode-segmentation", 956 | "unicode-width 0.1.13", 957 | ] 958 | 959 | [[package]] 960 | name = "unicode-width" 961 | version = "0.1.13" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 964 | 965 | [[package]] 966 | name = "unicode-width" 967 | version = "0.2.0" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 970 | 971 | [[package]] 972 | name = "version_check" 973 | version = "0.9.5" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 976 | 977 | [[package]] 978 | name = "wasi" 979 | version = "0.11.0+wasi-snapshot-preview1" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 982 | 983 | [[package]] 984 | name = "winapi" 985 | version = "0.3.9" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 988 | dependencies = [ 989 | "winapi-i686-pc-windows-gnu", 990 | "winapi-x86_64-pc-windows-gnu", 991 | ] 992 | 993 | [[package]] 994 | name = "winapi-i686-pc-windows-gnu" 995 | version = "0.4.0" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 998 | 999 | [[package]] 1000 | name = "winapi-x86_64-pc-windows-gnu" 1001 | version = "0.4.0" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1004 | 1005 | [[package]] 1006 | name = "windows-sys" 1007 | version = "0.52.0" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1010 | dependencies = [ 1011 | "windows-targets", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "windows-targets" 1016 | version = "0.52.6" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1019 | dependencies = [ 1020 | "windows_aarch64_gnullvm", 1021 | "windows_aarch64_msvc", 1022 | "windows_i686_gnu", 1023 | "windows_i686_gnullvm", 1024 | "windows_i686_msvc", 1025 | "windows_x86_64_gnu", 1026 | "windows_x86_64_gnullvm", 1027 | "windows_x86_64_msvc", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "windows_aarch64_gnullvm" 1032 | version = "0.52.6" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1035 | 1036 | [[package]] 1037 | name = "windows_aarch64_msvc" 1038 | version = "0.52.6" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1041 | 1042 | [[package]] 1043 | name = "windows_i686_gnu" 1044 | version = "0.52.6" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1047 | 1048 | [[package]] 1049 | name = "windows_i686_gnullvm" 1050 | version = "0.52.6" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1053 | 1054 | [[package]] 1055 | name = "windows_i686_msvc" 1056 | version = "0.52.6" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1059 | 1060 | [[package]] 1061 | name = "windows_x86_64_gnu" 1062 | version = "0.52.6" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1065 | 1066 | [[package]] 1067 | name = "windows_x86_64_gnullvm" 1068 | version = "0.52.6" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1071 | 1072 | [[package]] 1073 | name = "windows_x86_64_msvc" 1074 | version = "0.52.6" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1077 | 1078 | [[package]] 1079 | name = "winnow" 1080 | version = "0.6.20" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 1083 | dependencies = [ 1084 | "memchr", 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "zerocopy" 1089 | version = "0.7.35" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1092 | dependencies = [ 1093 | "byteorder", 1094 | "zerocopy-derive", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "zerocopy-derive" 1099 | version = "0.7.35" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1102 | dependencies = [ 1103 | "proc-macro2", 1104 | "quote", 1105 | "syn", 1106 | ] 1107 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "termCA" 3 | version = "0.1.1" 4 | edition = "2021" 5 | description = "Interactive TUI Cellular Automata simulator" 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/fabiooo4/termCA" 9 | repository = "https://github.com/fabiooo4/termCA" 10 | keywords = ["tui", "cellular", "automata", "cli", "simulation"] 11 | categories = ["games", "simulation"] 12 | 13 | [dependencies] 14 | crossterm = "0.28.1" 15 | rand = "0.8.5" 16 | ratatui = "0.29.0" 17 | rayon = "1.10.0" 18 | tui-big-text = "0.7.0" 19 | tui-input = "0.10.1" 20 | tui-scrollview = "0.5.0" 21 | tui-widget-list = "0.13.0" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://github.com/user-attachments/assets/7d191463-03d3-4047-82db-623dbb0132bd) 2 | # TermCA 3 | TermCA is a TUI simulator for cellular automata written in Rust. It displays the 4 | automata in the terminal and allows you to interact with the simulation in real 5 | time. 6 | 7 | ## Simulations 8 | The latest version supports the following simulations: 9 | - [Elementary Cellular Automata](https://en.wikipedia.org/wiki/Elementary_cellular_automaton) 10 | - [Langton's Ant](https://en.wikipedia.org/wiki/Langton%27s_ant) 11 | - [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) 12 | 13 | ## Installation 14 | ```bash 15 | cargo install termCA 16 | ``` 17 | 18 | ## Usage 19 | When you run the program, you will be presented with a menu where you can select 20 | the simulation you want to run. Every screen and popup has its own set of controls that 21 | you can see by pressing `?`. 22 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::simulations::{ant::AntSim, elementary::ElementarySim, game_of_life::GolSim}; 2 | use ratatui:: 3 | widgets::{ScrollbarState, TableState} 4 | ; 5 | use std::time::Duration; 6 | 7 | /// All the possible screens in the application 8 | #[derive(Clone, Copy)] 9 | pub enum Screen { 10 | Main, 11 | Exit, 12 | Ant, 13 | AntEdit(usize), // Screen for editing the ants' position and direction 14 | Elementary, 15 | GameOfLife, 16 | GolEdit, // Screen for editing the game of life starting grid 17 | } 18 | 19 | pub enum InputMode { 20 | Normal, 21 | Editing, 22 | } 23 | 24 | pub struct SimulationItem { 25 | pub label: String, 26 | pub screen: Screen, 27 | } 28 | 29 | #[derive(PartialEq, Eq, Default)] 30 | pub enum EditTab { 31 | #[default] 32 | Setting, 33 | Content, 34 | } 35 | 36 | impl EditTab { 37 | pub fn next(&mut self) { 38 | match self { 39 | Self::Setting => *self = EditTab::Content, 40 | Self::Content => *self = EditTab::Setting, 41 | } 42 | } 43 | } 44 | 45 | /// Struct that holds the application data 46 | pub struct App { 47 | pub current_screen: Screen, 48 | pub editing: Option, 49 | pub selected_edit_tab: Option, 50 | pub help_screen: bool, 51 | pub is_running: bool, // Pause/Resume 52 | pub speed: Duration, // Delay between each generation 53 | pub speed_multiplier: usize, // Number of generations per frame 54 | 55 | pub list_items: Vec, 56 | pub list_state: TableState, // State of the list 57 | pub scroll_state: ScrollbarState, 58 | 59 | /// Simulation data (optional because it's only used in one screen) 60 | pub ant_sim: Option, 61 | pub elementary_sim: Option, 62 | pub gol_sim: Option, 63 | } 64 | 65 | impl App { 66 | /// Constructs a new `App` with default values 67 | pub fn new() -> Self { 68 | let simulations_list = vec![ 69 | SimulationItem { 70 | label: String::from("Elementary CA\n "), 71 | screen: Screen::Elementary, 72 | }, 73 | SimulationItem { 74 | label: String::from("Langton's Ant\n "), 75 | screen: Screen::Ant, 76 | }, 77 | SimulationItem { 78 | label: String::from("Game of Life\n "), 79 | screen: Screen::GameOfLife, 80 | }, 81 | SimulationItem { 82 | label: String::from("Exit\n "), 83 | screen: Screen::Exit, 84 | }, 85 | ]; 86 | 87 | App { 88 | help_screen: false, 89 | current_screen: Screen::Main, 90 | editing: None, 91 | selected_edit_tab: None, 92 | is_running: false, 93 | speed: Duration::from_millis(80), 94 | speed_multiplier: 1, 95 | list_items: simulations_list, 96 | list_state: TableState::new().with_selected_cell((0, 0)), 97 | scroll_state: ScrollbarState::default(), 98 | 99 | ant_sim: None, 100 | elementary_sim: None, 101 | gol_sim: None, 102 | } 103 | } 104 | 105 | /// Increases simulation speed 106 | pub fn speed_increase(&mut self) { 107 | if self.speed > Duration::from_millis(100) { 108 | self.speed = self.speed.saturating_sub(Duration::from_millis(100)); 109 | } else if self.speed > Duration::from_millis(10) { 110 | self.speed = self.speed.saturating_sub(Duration::from_millis(10)); 111 | } else if self.speed > Duration::from_millis(0) { 112 | self.speed = self.speed.saturating_sub(Duration::from_millis(1)); 113 | } else if self.speed_multiplier < 10 { 114 | self.speed_multiplier = self.speed_multiplier.saturating_add(1); 115 | } else if self.speed_multiplier < 100 { 116 | self.speed_multiplier = self.speed_multiplier.saturating_add(10); 117 | } else if self.speed_multiplier < 1000 { 118 | self.speed_multiplier = self.speed_multiplier.saturating_add(100); 119 | } else { 120 | self.speed_multiplier = self.speed_multiplier.saturating_add(1000); 121 | } 122 | } 123 | 124 | /// Decreases simulation speed 125 | pub fn speed_decrease(&mut self) { 126 | if self.speed_multiplier > 1 { 127 | if self.speed_multiplier > 1000 { 128 | self.speed_multiplier = self.speed_multiplier.saturating_sub(1000); 129 | } else if self.speed_multiplier > 100 { 130 | self.speed_multiplier = self.speed_multiplier.saturating_sub(100); 131 | } else if self.speed_multiplier > 10 { 132 | self.speed_multiplier = self.speed_multiplier.saturating_sub(10); 133 | } else { 134 | self.speed_multiplier = self.speed_multiplier.saturating_sub(1); 135 | } 136 | } else if self.speed < Duration::from_millis(10) { 137 | self.speed = self.speed.saturating_add(Duration::from_millis(1)); 138 | } else if self.speed < Duration::from_millis(100) { 139 | self.speed = self.speed.saturating_add(Duration::from_millis(10)); 140 | } else { 141 | self.speed = self.speed.saturating_add(Duration::from_millis(100)); 142 | } 143 | } 144 | 145 | /// Applies the currently selected item of the list: 146 | /// - If a simulation is selected the screen changes to that simulation 147 | /// - If edit is selected enters edit mode on the selected simulation 148 | pub fn apply_selected(&mut self) { 149 | // If a simulation is selected from the list, 150 | // change the screen to that simulation 151 | if let Some(0) = self.list_state.selected_column() { 152 | if let Some(i) = self.list_state.selected() { 153 | self.current_screen = self.list_items[i].screen 154 | } 155 | } else { 156 | // If edit is selected, enter edit mode on the selected simulation 157 | if let Some(i) = self.list_state.selected() { 158 | match self.list_items[i].screen { 159 | Screen::Ant => { 160 | // Create a default simulation to be able to edit it 161 | self.start_ant_default(); 162 | self.editing = Some(self.list_items[i].screen); 163 | self.selected_edit_tab = Some(EditTab::Setting); 164 | } 165 | Screen::Elementary => { 166 | self.start_elementary_default(); 167 | self.editing = Some(self.list_items[i].screen); 168 | self.selected_edit_tab = Some(EditTab::Setting); 169 | } 170 | Screen::GameOfLife => { 171 | self.start_gol_default(); 172 | self.editing = Some(self.list_items[i].screen); 173 | self.selected_edit_tab = Some(EditTab::Setting); 174 | } 175 | _ => {} 176 | } 177 | } 178 | } 179 | } 180 | 181 | /// Stops all simulations 182 | pub fn stop_all(&mut self) { 183 | self.ant_sim = None; 184 | self.elementary_sim = None; 185 | self.gol_sim = None; 186 | 187 | self.is_running = false; 188 | self.editing = None; 189 | self.speed = Duration::from_millis(80); 190 | self.speed_multiplier = 1; 191 | } 192 | 193 | /// Starts the Langton's Ant simulation with default values 194 | pub fn start_ant_default(&mut self) { 195 | self.stop_all(); 196 | self.ant_sim = Some(AntSim::default()); 197 | } 198 | 199 | /// Starts the Elementary CA simulation with default values 200 | pub fn start_elementary_default(&mut self) { 201 | self.stop_all(); 202 | self.elementary_sim = Some(ElementarySim::default()); 203 | } 204 | 205 | pub fn start_gol_default(&mut self) { 206 | self.stop_all(); 207 | self.gol_sim = Some(GolSim::default()); 208 | } 209 | 210 | // List handling 211 | pub fn select_first(&mut self) { 212 | if let Some(selected_column) = self.list_state.selected_column() { 213 | self.list_state.select_cell(Some((0, selected_column))); 214 | } else { 215 | self.list_state.select_first(); 216 | } 217 | 218 | self.scroll_state = self.scroll_state.position(0); 219 | } 220 | 221 | pub fn select_last(&mut self) { 222 | self.list_state.select_first_column(); 223 | self.list_state.select_last(); 224 | 225 | self.scroll_state = self.scroll_state.position(100000); 226 | } 227 | 228 | pub fn select_next(&mut self) { 229 | if self.list_state.selected_column() == Some(1) 230 | && self.list_state.selected() == Some(self.list_items.len() - 2) 231 | { 232 | self.select_last() 233 | } else if self.list_state.selected().is_some() 234 | && self.list_state.selected() != Some(self.list_items.len() - 1) 235 | { 236 | self.list_state.select_next(); 237 | } 238 | 239 | self.scroll_state = self.scroll_state.position(self.list_state.offset()); 240 | } 241 | 242 | pub fn select_previous(&mut self) { 243 | if self.list_state.selected().is_some() && self.list_state.selected() != Some(0) { 244 | self.list_state.select_previous(); 245 | } 246 | 247 | self.scroll_state = self.scroll_state.position(self.list_state.offset()); 248 | } 249 | 250 | pub fn select_left(&mut self) { 251 | if self.list_state.selected().is_some() { 252 | self.list_state.select_previous_column(); 253 | } 254 | } 255 | 256 | pub fn select_right(&mut self) { 257 | if self.list_state.selected().is_some() 258 | && self.list_state.selected() != Some(self.list_items.len() - 1) 259 | { 260 | self.list_state.select_next_column(); 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/events/ant_events.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{Event, KeyCode, KeyEvent}; 2 | use ratatui::style::Color; 3 | use tui_input::backend::crossterm::EventHandler; 4 | 5 | use crate::{ 6 | app::{App, EditTab, InputMode, Screen}, 7 | simulations::{ 8 | ant::{Ant, AntPresets, AntSettings, AntSim}, 9 | Direction, 10 | }, 11 | }; 12 | 13 | pub fn main(key: KeyEvent, app: &mut App) { 14 | match key.code { 15 | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => { 16 | app.current_screen = Screen::Main; 17 | app.stop_all(); 18 | } 19 | KeyCode::Char(' ') => app.is_running = !app.is_running, 20 | KeyCode::Char('?') => app.help_screen = !app.help_screen, 21 | // Run the simulation one step at a time 22 | KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => { 23 | app.ant_sim.as_mut().unwrap().run(app.speed_multiplier) 24 | } 25 | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => app.speed_increase(), 26 | KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => app.speed_decrease(), 27 | _ => {} 28 | } 29 | } 30 | 31 | pub fn edit(key: KeyEvent, app: &mut App) { 32 | let sim = app.ant_sim.as_mut().unwrap(); 33 | match app.selected_edit_tab.as_ref().unwrap() { 34 | EditTab::Setting => match key.code { 35 | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => { 36 | app.editing = None; 37 | app.selected_edit_tab = None; 38 | app.ant_sim = None; 39 | } 40 | 41 | KeyCode::Char(' ') => { 42 | // Change the screen 43 | app.editing = None; 44 | app.selected_edit_tab = None; 45 | app.current_screen = Screen::Ant; 46 | } 47 | 48 | KeyCode::Char('?') => { 49 | app.help_screen = !app.help_screen; 50 | } 51 | 52 | KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => sim.settings_state.next(), 53 | 54 | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => sim.settings_state.previous(), 55 | 56 | KeyCode::Enter | KeyCode::Char('l') | KeyCode::Char('L') | KeyCode::Right => { 57 | match AntSettings::from_index(sim.settings_state.selected.unwrap_or(0)) { 58 | AntSettings::Presets => { 59 | app.selected_edit_tab.as_mut().unwrap().next(); 60 | } 61 | AntSettings::Ruleset => { 62 | sim.rules_input_mode = InputMode::Editing; 63 | app.selected_edit_tab.as_mut().unwrap().next(); 64 | } 65 | 66 | AntSettings::Ants => { 67 | app.selected_edit_tab.as_mut().unwrap().next(); 68 | } 69 | 70 | AntSettings::Start => { 71 | // Change the screen 72 | app.editing = None; 73 | app.selected_edit_tab = None; 74 | app.current_screen = Screen::Ant; 75 | } 76 | } 77 | } 78 | 79 | _ => {} 80 | }, 81 | 82 | EditTab::Content => { 83 | match AntSettings::from_index(sim.settings_state.selected.unwrap_or(0)) { 84 | AntSettings::Presets => match key.code { 85 | KeyCode::Char('?') => { 86 | app.help_screen = !app.help_screen; 87 | } 88 | 89 | KeyCode::Esc 90 | | KeyCode::Char('q') 91 | | KeyCode::Char('h') 92 | | KeyCode::Char('H') 93 | | KeyCode::Left => { 94 | app.selected_edit_tab.as_mut().unwrap().next(); 95 | } 96 | 97 | KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => { 98 | sim.preset_state.next(); 99 | } 100 | 101 | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => { 102 | sim.preset_state.previous(); 103 | } 104 | 105 | KeyCode::Enter => { 106 | if let Some(selected) = sim.preset_state.selected { 107 | // Assign to the simulation the selected preset 108 | *sim = AntSim::get_preset(AntPresets::from_index(selected)); 109 | sim.preset_state.selected = Some(selected); 110 | app.selected_edit_tab.as_mut().unwrap().next(); 111 | } 112 | } 113 | _ => {} 114 | }, 115 | AntSettings::Ruleset => { 116 | match key.code { 117 | KeyCode::Char('?') => { 118 | app.help_screen = !app.help_screen; 119 | } 120 | 121 | KeyCode::Esc 122 | | KeyCode::Enter 123 | | KeyCode::Char('q') 124 | | KeyCode::Char('h') 125 | | KeyCode::Char('H') => { 126 | let sim = app.ant_sim.as_mut().unwrap(); 127 | 128 | sim.rules_input_mode = InputMode::Normal; 129 | app.selected_edit_tab.as_mut().unwrap().next(); 130 | 131 | // Parse the user inserted rules 132 | sim.rules = AntSim::parse_ant_ruleset(sim.rules_input.value()); 133 | 134 | // Add states for every rule 135 | let rules_len = sim.rules.len(); 136 | let states_len = sim.states.len(); 137 | if rules_len > states_len { 138 | for i in (states_len + 1)..=rules_len { 139 | sim.states.push(Color::Indexed(i as u8)); 140 | } 141 | } 142 | } 143 | _ => { 144 | let sim = app.ant_sim.as_mut().unwrap(); 145 | let allowed_chars = "rlfbRLFB"; 146 | 147 | // Only handle allowed characters 148 | sim.rules_input.handle_event(&Event::Key(match key.code { 149 | KeyCode::Char(c) => { 150 | if allowed_chars.contains(c) { 151 | KeyEvent::from(KeyCode::Char(c.to_ascii_uppercase())) 152 | } else { 153 | KeyEvent::from(KeyCode::Null) 154 | } 155 | } 156 | _ => key, 157 | })); 158 | } 159 | } 160 | } 161 | 162 | AntSettings::Ants => match key.code { 163 | KeyCode::Char('?') => { 164 | app.help_screen = !app.help_screen; 165 | } 166 | 167 | KeyCode::Esc 168 | | KeyCode::Char('q') 169 | | KeyCode::Char('h') 170 | | KeyCode::Char('H') 171 | | KeyCode::Left => { 172 | app.selected_edit_tab.as_mut().unwrap().next(); 173 | } 174 | 175 | KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => { 176 | sim.ants_list_state.next() 177 | } 178 | 179 | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => { 180 | sim.ants_list_state.previous() 181 | } 182 | 183 | KeyCode::Backspace => { 184 | if sim 185 | .ants_list_state 186 | .selected 187 | .is_some_and(|selected| selected > 0) 188 | { 189 | let selected = sim.ants_list_state.selected.unwrap(); 190 | 191 | if selected == sim.ants.len() { 192 | sim.ants_list_state.previous(); 193 | } 194 | 195 | sim.ants.remove(selected.saturating_sub(1)); 196 | } 197 | } 198 | 199 | KeyCode::Enter 200 | if sim 201 | .ants_list_state 202 | .selected 203 | .is_some_and(|selected| selected == 0) => 204 | { 205 | sim.ants.push(Ant::default()); 206 | } 207 | 208 | KeyCode::Enter => { 209 | app.editing = Some(Screen::AntEdit( 210 | sim.ants_list_state.selected.unwrap().saturating_sub(1), 211 | )); 212 | } 213 | _ => {} 214 | }, 215 | AntSettings::Start => {} 216 | } 217 | } 218 | } 219 | } 220 | 221 | pub fn edit_ant(key: KeyEvent, app: &mut App, ant_idx: usize) { 222 | let ant_sim = app.ant_sim.as_mut().unwrap(); 223 | let speed_toggle = 2; 224 | 225 | match key.code { 226 | KeyCode::Char('?') => app.help_screen = !app.help_screen, 227 | 228 | KeyCode::Enter | KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { 229 | app.editing = Some(Screen::Ant) 230 | } 231 | 232 | KeyCode::Up | KeyCode::Char('k') => { 233 | ant_sim.ants[ant_idx].change_position(Direction::Up, &ant_sim.grid) 234 | } 235 | KeyCode::Char('K') => { 236 | for _ in 0..speed_toggle { 237 | ant_sim.ants[ant_idx].change_position(Direction::Up, &ant_sim.grid) 238 | } 239 | } 240 | 241 | KeyCode::Down | KeyCode::Char('j') => { 242 | ant_sim.ants[ant_idx].change_position(Direction::Down, &ant_sim.grid) 243 | } 244 | KeyCode::Char('J') => { 245 | for _ in 0..speed_toggle { 246 | ant_sim.ants[ant_idx].change_position(Direction::Down, &ant_sim.grid) 247 | } 248 | } 249 | 250 | KeyCode::Right | KeyCode::Char('l') => { 251 | ant_sim.ants[ant_idx].change_position(Direction::Right, &ant_sim.grid) 252 | } 253 | KeyCode::Char('L') => { 254 | for _ in 0..speed_toggle { 255 | ant_sim.ants[ant_idx].change_position(Direction::Right, &ant_sim.grid) 256 | } 257 | } 258 | 259 | KeyCode::Left | KeyCode::Char('h') => { 260 | ant_sim.ants[ant_idx].change_position(Direction::Left, &ant_sim.grid) 261 | } 262 | KeyCode::Char('H') => { 263 | for _ in 0..speed_toggle { 264 | ant_sim.ants[ant_idx].change_position(Direction::Left, &ant_sim.grid) 265 | } 266 | } 267 | 268 | KeyCode::Char('r') => { 269 | ant_sim.ants[ant_idx].direction = ant_sim.ants[ant_idx].direction.turn_right() 270 | } 271 | 272 | KeyCode::Char('R') => { 273 | ant_sim.ants[ant_idx].direction = ant_sim.ants[ant_idx].direction.turn_left() 274 | } 275 | _ => {} 276 | } 277 | } 278 | 279 | pub fn resize(new_width: u16, new_height: u16, app: &mut App) { 280 | let new_width: usize = new_width as usize - 2; 281 | let new_height: usize = (new_height as usize - 2) * 2; 282 | 283 | // Reposition the ant in the view if it is out of bounds 284 | for ant in app.ant_sim.as_mut().unwrap().ants.iter_mut() { 285 | if ant.x >= new_width { 286 | ant.x = new_width - 1; 287 | } 288 | 289 | if ant.y >= new_height { 290 | ant.y = new_height - 1; 291 | } 292 | } 293 | 294 | // Resize the grid 295 | app.ant_sim 296 | .as_mut() 297 | .unwrap() 298 | .grid 299 | .resize(new_width, new_height, Color::Reset); 300 | } 301 | -------------------------------------------------------------------------------- /src/events/elementary_events.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{Event, KeyCode, KeyEvent}; 2 | use ratatui::style::Color; 3 | use tui_input::backend::crossterm::EventHandler; 4 | 5 | use crate::{ 6 | app::{App, EditTab, InputMode, Screen}, 7 | simulations::elementary::ElementarySettings, 8 | }; 9 | 10 | pub fn main(key: KeyEvent, app: &mut App) { 11 | match key.code { 12 | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => { 13 | app.current_screen = Screen::Main; 14 | app.stop_all(); 15 | } 16 | KeyCode::Char(' ') => app.is_running = !app.is_running, 17 | KeyCode::Char('?') => { 18 | app.help_screen = !app.help_screen; 19 | } 20 | KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => { 21 | // Run simulation once 22 | app.elementary_sim 23 | .as_mut() 24 | .unwrap() 25 | .run(app.speed_multiplier); 26 | } 27 | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => { 28 | // Increase simulation speed 29 | app.speed_increase(); 30 | } 31 | KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => { 32 | // Decrease simulation speed 33 | app.speed_decrease(); 34 | } 35 | _ => {} 36 | } 37 | } 38 | 39 | pub fn edit(key: KeyEvent, app: &mut App) { 40 | let sim = app.elementary_sim.as_mut().unwrap(); 41 | match app.selected_edit_tab.as_ref().unwrap() { 42 | EditTab::Setting => match key.code { 43 | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => { 44 | app.editing = None; 45 | app.selected_edit_tab = None; 46 | app.elementary_sim = None; 47 | } 48 | 49 | KeyCode::Char(' ') => { 50 | // Change the screen 51 | app.editing = None; 52 | app.selected_edit_tab = None; 53 | app.current_screen = Screen::Elementary; 54 | } 55 | 56 | KeyCode::Char('?') => { 57 | app.help_screen = !app.help_screen; 58 | } 59 | 60 | KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => sim.settings_state.next(), 61 | 62 | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => sim.settings_state.previous(), 63 | 64 | KeyCode::Enter | KeyCode::Char('l') | KeyCode::Char('L') | KeyCode::Right => { 65 | match ElementarySettings::from_index(sim.settings_state.selected.unwrap_or(0)) { 66 | ElementarySettings::Rule => { 67 | sim.rule_input_mode = InputMode::Editing; 68 | app.selected_edit_tab.as_mut().unwrap().next(); 69 | } 70 | ElementarySettings::Start => { 71 | // Change the screen 72 | app.editing = None; 73 | app.selected_edit_tab = None; 74 | app.current_screen = Screen::Elementary; 75 | } 76 | } 77 | } 78 | 79 | _ => {} 80 | }, 81 | 82 | EditTab::Content => { 83 | if let ElementarySettings::Rule = 84 | ElementarySettings::from_index(sim.settings_state.selected.unwrap_or(0)) 85 | { 86 | match key.code { 87 | KeyCode::Char('?') => { 88 | app.help_screen = !app.help_screen; 89 | } 90 | 91 | KeyCode::Esc 92 | | KeyCode::Enter 93 | | KeyCode::Char('q') 94 | | KeyCode::Char('h') 95 | | KeyCode::Char('H') => { 96 | app.elementary_sim.as_mut().unwrap().rule_input_mode = InputMode::Normal; 97 | app.selected_edit_tab.as_mut().unwrap().next(); 98 | } 99 | _ => { 100 | let sim = app.elementary_sim.as_mut().unwrap(); 101 | let allowed_chars = "0123456789"; 102 | 103 | // Only handle allowed characters 104 | sim.rule_input.handle_event(&Event::Key(match key.code { 105 | KeyCode::Char(c) => { 106 | if allowed_chars.contains(c) { 107 | KeyEvent::from(KeyCode::Char(c)) 108 | } else { 109 | KeyEvent::from(KeyCode::Null) 110 | } 111 | } 112 | _ => key, 113 | })); 114 | 115 | sim.parse_input(); 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | pub fn resize(new_width: u16, new_height: u16, app: &mut App) { 124 | let new_width: usize = new_width as usize; 125 | let new_height: usize = (new_height as usize - 2) * 2; 126 | 127 | let sim = app.elementary_sim.as_mut().unwrap(); 128 | 129 | // Resize the grid 130 | sim.grid.resize(new_width, new_height, Color::Reset); 131 | 132 | // Resize the line 133 | sim.current_line.resize(new_width, false); 134 | } 135 | -------------------------------------------------------------------------------- /src/events/game_of_life_events.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyEvent}; 2 | use ratatui::style::Color; 3 | 4 | use crate::{ 5 | app::{App, EditTab, Screen}, 6 | simulations::{game_of_life::GolSettings, Direction}, 7 | }; 8 | 9 | pub fn main(key: KeyEvent, app: &mut App) { 10 | match key.code { 11 | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => { 12 | app.current_screen = Screen::Main; 13 | app.stop_all(); 14 | } 15 | KeyCode::Char(' ') => app.is_running = !app.is_running, 16 | KeyCode::Char('?') => app.help_screen = !app.help_screen, 17 | // Run the simulation one step at a time 18 | KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => { 19 | app.gol_sim.as_mut().unwrap().run(app.speed_multiplier); 20 | } 21 | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => app.speed_increase(), 22 | KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => app.speed_decrease(), 23 | _ => {} 24 | } 25 | } 26 | 27 | pub fn edit(key: KeyEvent, app: &mut App) { 28 | let sim = app.gol_sim.as_mut().unwrap(); 29 | if app.selected_edit_tab.as_ref().unwrap() == &EditTab::Setting { 30 | match key.code { 31 | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => { 32 | app.editing = None; 33 | app.selected_edit_tab = None; 34 | app.ant_sim = None; 35 | } 36 | 37 | KeyCode::Char(' ') => { 38 | // Change the screen 39 | app.editing = None; 40 | app.selected_edit_tab = None; 41 | app.current_screen = Screen::GameOfLife; 42 | } 43 | 44 | KeyCode::Char('?') => { 45 | app.help_screen = !app.help_screen; 46 | } 47 | 48 | KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => sim.settings_state.next(), 49 | 50 | KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => sim.settings_state.previous(), 51 | 52 | KeyCode::Enter | KeyCode::Char('l') | KeyCode::Char('L') | KeyCode::Right => { 53 | match GolSettings::from_index(sim.settings_state.selected.unwrap_or(0)) { 54 | GolSettings::EditGrid => { 55 | app.editing = Some(Screen::GolEdit); 56 | } 57 | 58 | GolSettings::Start => { 59 | // Change the screen 60 | app.editing = None; 61 | app.selected_edit_tab = None; 62 | app.current_screen = Screen::GameOfLife; 63 | } 64 | } 65 | } 66 | 67 | _ => {} 68 | } 69 | } 70 | } 71 | 72 | pub fn edit_gol(key: KeyEvent, app: &mut App) { 73 | let sim = app.gol_sim.as_mut().unwrap(); 74 | let speed_toggle = 2; 75 | 76 | match key.code { 77 | KeyCode::Char('?') => app.help_screen = !app.help_screen, 78 | KeyCode::Enter => { 79 | app.current_screen = Screen::GameOfLife; 80 | app.editing = None; 81 | app.is_running = !app.is_running; 82 | } 83 | 84 | KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { 85 | app.editing = Some(Screen::GameOfLife) 86 | } 87 | 88 | KeyCode::Up | KeyCode::Char('k') => { 89 | sim.edit_cursor.change_position(Direction::Up, &sim.grid) 90 | } 91 | KeyCode::Char('K') => { 92 | for _ in 0..speed_toggle { 93 | sim.edit_cursor.change_position(Direction::Up, &sim.grid) 94 | } 95 | } 96 | 97 | KeyCode::Down | KeyCode::Char('j') => { 98 | sim.edit_cursor.change_position(Direction::Down, &sim.grid) 99 | } 100 | KeyCode::Char('J') => { 101 | for _ in 0..speed_toggle { 102 | sim.edit_cursor.change_position(Direction::Down, &sim.grid) 103 | } 104 | } 105 | 106 | KeyCode::Right | KeyCode::Char('l') => { 107 | sim.edit_cursor.change_position(Direction::Right, &sim.grid) 108 | } 109 | KeyCode::Char('L') => { 110 | for _ in 0..speed_toggle { 111 | sim.edit_cursor.change_position(Direction::Right, &sim.grid) 112 | } 113 | } 114 | 115 | KeyCode::Left | KeyCode::Char('h') => { 116 | sim.edit_cursor.change_position(Direction::Left, &sim.grid) 117 | } 118 | KeyCode::Char('H') => { 119 | for _ in 0..speed_toggle { 120 | sim.edit_cursor.change_position(Direction::Left, &sim.grid) 121 | } 122 | } 123 | 124 | KeyCode::Char(' ') => { 125 | sim.toggle_cell(sim.edit_cursor.x, sim.edit_cursor.y); 126 | } 127 | _ => {} 128 | } 129 | } 130 | 131 | pub fn resize(new_width: u16, new_height: u16, app: &mut App) { 132 | let new_width: usize = new_width as usize - 2; 133 | let new_height: usize = (new_height as usize - 2) * 2; 134 | 135 | // Resize the grid 136 | app.gol_sim 137 | .as_mut() 138 | .unwrap() 139 | .grid 140 | .resize(new_width, new_height, Color::Reset); 141 | } 142 | -------------------------------------------------------------------------------- /src/events/main_events.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyEvent}; 2 | 3 | use crate::app::{App, Screen}; 4 | 5 | pub fn main(key: KeyEvent, app: &mut App) { 6 | match key.code { 7 | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => app.current_screen = Screen::Exit, 8 | KeyCode::Char('?') => app.help_screen = !app.help_screen, 9 | KeyCode::Char('h') | KeyCode::Char('H') | KeyCode::Left => app.select_left(), 10 | KeyCode::Char('l') | KeyCode::Char('L') | KeyCode::Right => app.select_right(), 11 | KeyCode::Char('j') | KeyCode::Char('J') | KeyCode::Down => app.select_next(), 12 | KeyCode::Char('k') | KeyCode::Char('K') | KeyCode::Up => app.select_previous(), 13 | KeyCode::Char('g') | KeyCode::Home => app.select_first(), 14 | KeyCode::Char('G') | KeyCode::End => app.select_last(), 15 | KeyCode::Enter => app.apply_selected(), 16 | _ => {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod main_events; 2 | pub mod ant_events; 3 | pub mod elementary_events; 4 | pub mod game_of_life_events; 5 | 6 | use crossterm::event::poll; 7 | use std::{io, time::Duration}; 8 | 9 | pub fn is_event_available(speed: Duration) -> io::Result { 10 | poll(speed) 11 | } 12 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod events; 3 | mod simulations; 4 | mod ui; 5 | 6 | use crate::app::App; 7 | use app::Screen; 8 | use crossterm::event::{self, Event}; 9 | use events::{ant_events, elementary_events, game_of_life_events, is_event_available, main_events}; 10 | use ratatui::DefaultTerminal; 11 | use std::io::{self}; 12 | use ui::{ant_ui, elementary_ui, game_of_life_ui, main_ui}; 13 | 14 | fn main() -> io::Result<()> { 15 | let mut terminal = ratatui::init(); 16 | 17 | let mut app = App::new(); 18 | let app_result = run_app(&mut terminal, &mut app); 19 | 20 | ratatui::restore(); 21 | 22 | app_result 23 | } 24 | 25 | fn run_app(terminal: &mut DefaultTerminal, app: &mut App) -> io::Result<()> { 26 | loop { 27 | // Render 28 | terminal.draw(|f| match app.current_screen { 29 | Screen::Main => main_ui::main_screen(f, app), 30 | Screen::Ant => ant_ui::ant_screen(f, app), 31 | Screen::Elementary => elementary_ui::elementary_screen(f, app), 32 | Screen::GameOfLife => game_of_life_ui::gol_screen(f, app), 33 | _ => {} 34 | })?; 35 | 36 | // Event handling 37 | // Always running 38 | match app.current_screen { 39 | Screen::Exit => break Ok(()), 40 | Screen::Ant => { 41 | if app.is_running && !app.help_screen { 42 | app.ant_sim.as_mut().unwrap().run(app.speed_multiplier); 43 | } 44 | } 45 | Screen::Elementary => { 46 | if app.is_running && !app.help_screen { 47 | app.elementary_sim 48 | .as_mut() 49 | .unwrap() 50 | .run(app.speed_multiplier); 51 | } 52 | } 53 | Screen::GameOfLife => { 54 | if app.is_running && !app.help_screen { 55 | app.gol_sim.as_mut().unwrap().run(app.speed_multiplier); 56 | } 57 | } 58 | _ => {} 59 | } 60 | 61 | // Only run when an event is available 62 | if !is_event_available(app.speed)? { 63 | continue; 64 | } 65 | 66 | match event::read()? { 67 | Event::Resize(new_width, new_height) => match app.current_screen { 68 | Screen::Ant => ant_events::resize(new_width, new_height, app), 69 | Screen::Elementary => elementary_events::resize(new_width, new_height, app), 70 | Screen::GameOfLife => game_of_life_events::resize(new_width, new_height, app), 71 | _ => (), 72 | }, 73 | 74 | Event::Key(key) => { 75 | if key.kind != event::KeyEventKind::Press { 76 | // Skip events that are not KeyEventKind::Press 77 | continue; 78 | } 79 | 80 | if app.help_screen { 81 | // Prevent any action when the help screen is displayed 82 | app.help_screen = false; 83 | continue; 84 | } 85 | 86 | if let Some(edit_sim) = app.editing { 87 | match edit_sim { 88 | Screen::Ant => ant_events::edit(key, app), 89 | Screen::AntEdit(ant_idx) => ant_events::edit_ant(key, app, ant_idx), 90 | Screen::Elementary => elementary_events::edit(key, app), 91 | Screen::GameOfLife => game_of_life_events::edit(key, app), 92 | Screen::GolEdit => game_of_life_events::edit_gol(key, app), 93 | _ => {} 94 | } 95 | } else { 96 | match app.current_screen { 97 | Screen::Main => main_events::main(key, app), 98 | Screen::Ant => ant_events::main(key, app), 99 | Screen::Elementary => elementary_events::main(key, app), 100 | Screen::GameOfLife => game_of_life_events::main(key, app), 101 | _ => {} 102 | } 103 | } 104 | } 105 | _ => {} 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/simulations/ant.rs: -------------------------------------------------------------------------------- 1 | use crate::app::InputMode; 2 | use ratatui::{ 3 | style::Color, 4 | symbols::Marker, widgets::ScrollbarState, 5 | }; 6 | use tui_input::Input; 7 | use tui_widget_list::ListState; 8 | 9 | use super::{Direction, Grid}; 10 | 11 | /// Struct that holds the ant simulation data 12 | pub struct AntSim { 13 | pub marker: Marker, // Character to draw the cells 14 | pub ants: Vec, // Vector that holds the ants 15 | pub grid: Grid, // Grid of cells 16 | pub states: Vec, // Possible states of the cells 17 | pub rules: Vec, // Rules for the ant 18 | pub generation: usize, // Number of generations 19 | 20 | // Edit state 21 | pub settings_state: ListState, 22 | pub preset_state: ListState, 23 | pub ants_list_state: ListState, 24 | 25 | pub preset_scroll_state: ScrollbarState, 26 | 27 | pub rules_input: Input, // Input widget 28 | pub rules_input_mode: InputMode, // Input mode 29 | } 30 | 31 | impl Default for AntSim { 32 | fn default() -> Self { 33 | let mut list_state = ListState::default(); 34 | list_state.selected = Some(0); 35 | AntSim { 36 | ants: vec![Ant::default()], 37 | grid: Grid::new(), 38 | states: vec![ 39 | Color::Reset, 40 | Color::Indexed(3), 41 | Color::Indexed(1), 42 | Color::Indexed(2), 43 | Color::Indexed(4), 44 | Color::Indexed(5), 45 | Color::Indexed(6), 46 | Color::Indexed(9), 47 | Color::Indexed(10), 48 | Color::Indexed(11), 49 | Color::Indexed(12), 50 | Color::Indexed(13), 51 | Color::Indexed(14), 52 | Color::Indexed(7), 53 | Color::Indexed(8), 54 | Color::Indexed(15), 55 | Color::Indexed(17), 56 | ], 57 | rules: vec![Direction::Right, Direction::Left], 58 | generation: 0, 59 | marker: Marker::HalfBlock, 60 | 61 | settings_state: list_state.clone(), 62 | preset_state: list_state.clone(), 63 | ants_list_state: list_state, 64 | 65 | preset_scroll_state: ScrollbarState::default(), 66 | 67 | rules_input: Input::from(String::from("RL")), 68 | rules_input_mode: InputMode::Normal, 69 | } 70 | } 71 | } 72 | 73 | /// Struct that holds the ant data 74 | #[derive(Clone, Copy)] 75 | pub struct Ant { 76 | pub x: usize, 77 | pub y: usize, 78 | pub color: Color, 79 | pub direction: Direction, 80 | } 81 | 82 | impl Default for Ant { 83 | /// Constructs a new empty `Ant` 84 | fn default() -> Self { 85 | Ant { 86 | // Set to invalid position to reposition in the center of the screen when the 87 | // frame is available 88 | x: usize::MAX, 89 | y: usize::MAX, 90 | color: Color::Indexed(16), 91 | direction: Direction::Up, 92 | } 93 | } 94 | } 95 | 96 | impl Ant { 97 | /// Move the ant in the specified direction with grid wrapping 98 | pub fn change_position(&mut self, direction: Direction, grid: &Grid) { 99 | match direction { 100 | Direction::Left => { 101 | self.x = if self.x > 0 { 102 | self.x - 1 103 | } else { 104 | grid.width() - 1 105 | }; 106 | } 107 | Direction::Right => { 108 | self.x = if self.x < (grid.width() - 1) { 109 | self.x + 1 110 | } else { 111 | 0 112 | }; 113 | } 114 | Direction::Up => { 115 | self.y = if self.y < (grid.height() - 1) { 116 | self.y + 1 117 | } else { 118 | 0 119 | }; 120 | } 121 | Direction::Down => { 122 | self.y = if self.y > 0 { 123 | self.y - 1 124 | } else { 125 | grid.height() - 1 126 | }; 127 | } 128 | } 129 | } 130 | } 131 | 132 | impl AntSim { 133 | /// Parses the ant ruleset from a string 134 | /// - `L` -> turn left 135 | /// - `R` -> turn right 136 | /// - `F` -> continue in the same direction (Forward) 137 | /// - `B` -> turn opposite (Backward) 138 | /// 139 | /// # Example 140 | /// ``` 141 | /// assert_eq!(parse_ant_ruleset("LRFB"), vec![ 142 | /// Direction::Left, 143 | /// Direction::Right, 144 | /// Direction::Up, 145 | /// Direction::Down, 146 | /// ]); 147 | /// ``` 148 | pub fn parse_ant_ruleset(rules: &str) -> Vec { 149 | let mut ruleset = Vec::new(); 150 | for c in rules.to_uppercase().chars() { 151 | match c { 152 | 'L' => ruleset.push(Direction::Left), 153 | 'R' => ruleset.push(Direction::Right), 154 | 'F' => ruleset.push(Direction::Up), 155 | 'B' => ruleset.push(Direction::Down), 156 | _ => {} 157 | } 158 | } 159 | 160 | ruleset 161 | } 162 | 163 | /// Standard Langton's Ant simulation 164 | pub fn run(&mut self, speed_multiplier: usize) { 165 | for _ in 0..speed_multiplier { 166 | for ant in self.ants.iter_mut() { 167 | Self::ant_turn(ant, &self.grid, &self.states, &self.rules); 168 | Self::ant_flip(ant, &mut self.grid, &self.states, &self.rules); 169 | Self::ant_forward(ant, &self.grid); 170 | } 171 | } 172 | self.generation = self.generation.saturating_add(speed_multiplier); 173 | } 174 | 175 | /// Moves the ant forward based on its direction with grid wrapping 176 | pub fn ant_forward(ant: &mut Ant, grid: &Grid) { 177 | ant.change_position(ant.direction, grid); 178 | } 179 | 180 | /// Turns the ant based on the current cell state and rule 181 | pub fn ant_turn(ant: &mut Ant, grid: &Grid, states: &[Color], rules: &[Direction]) { 182 | for (state, rule) in states.iter().zip(rules.iter()) { 183 | if grid.cells[ant.y][ant.x] == *state { 184 | ant.direction = ant.direction.turn(rule); 185 | break; 186 | } 187 | } 188 | } 189 | 190 | /// Flips the current cell state based on the rule 191 | pub fn ant_flip(ant: &Ant, grid: &mut Grid, states: &[Color], rules: &[Direction]) { 192 | let rules_len = rules.len(); 193 | let mut states = states[0..rules_len].iter().cycle(); 194 | 195 | // Assign the next state to the current cell 196 | while let Some(state) = states.next() { 197 | if grid.cells[ant.y][ant.x] == *state { 198 | grid.cells[ant.y][ant.x] = *states.next().unwrap(); 199 | break; 200 | } 201 | } 202 | } 203 | } 204 | 205 | pub enum AntSettings { 206 | Presets, 207 | Ruleset, 208 | Ants, 209 | Start, 210 | } 211 | 212 | impl AntSettings { 213 | pub const COUNT: usize = 4; 214 | pub fn from_index(index: usize) -> Self { 215 | match index { 216 | 0 => AntSettings::Presets, 217 | 1 => AntSettings::Ruleset, 218 | 2 => AntSettings::Ants, 219 | 3 => AntSettings::Start, 220 | _ => AntSettings::Ruleset, 221 | } 222 | } 223 | } 224 | 225 | impl std::fmt::Display for AntSettings { 226 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 227 | match self { 228 | AntSettings::Presets => write!(f, "Presets"), 229 | AntSettings::Ruleset => write!(f, "Ruleset"), 230 | AntSettings::Ants => write!(f, "Ants"), 231 | AntSettings::Start => write!(f, "Start"), 232 | } 233 | } 234 | } 235 | 236 | pub enum AntPresets { 237 | Default, 238 | 239 | FractalLabyrinth, 240 | CosmicBloom, 241 | Starburst, 242 | InfinitePrism, 243 | IntersectingTriangles, 244 | SquareEscape, 245 | SoaringSerpent, 246 | ConvergingChaos, 247 | DiamondDance, 248 | ZigzaggingZones, 249 | JaggedJourney, 250 | TiltedTiles, 251 | GalaxyNeedle, 252 | Highway, 253 | PatternedHighway, 254 | SquaresHighway, 255 | TriangleHighway, 256 | TilesHighway, 257 | StripedHighway, 258 | BoxHighway, 259 | ChaosHighway, 260 | OrderedHighways, 261 | MosaicMaze, 262 | TessellationTrail, 263 | Spiral, 264 | ChaoticSpiral, 265 | RandomSpirals, 266 | SpiralHighway, 267 | SymmetricBloom, 268 | ExpandingTriangle, 269 | SwirlingBlades, 270 | Island, 271 | Cross, 272 | InfiniteSaw, 273 | DomainExpansion, 274 | TriangleDunes, 275 | Butterfly, 276 | Moth, 277 | Mountains, 278 | Laser, 279 | Stairs, 280 | Pyramid, 281 | DoubleCircle, 282 | TriangleFractal, 283 | TriangleFractal2, 284 | Spikes, 285 | Sword, 286 | Key, 287 | SpiralingBlocks, 288 | ChaosTriangles, 289 | Shuriken, 290 | ExpandingBoxes, 291 | PyramidBox, 292 | RandomSquarePaths, 293 | BouncingPath, 294 | FillingBoxes, 295 | RandomAreas, 296 | InfiniteTriangle, 297 | TriangleSpiral, 298 | ExplodingStar, 299 | PyramidExplosion, 300 | Spider, 301 | Towers, 302 | DitheringFill, 303 | SwirlingPath, 304 | SpiralFilling, 305 | Thorns, 306 | Nest, 307 | ZigzagPath, 308 | ChaosPrison, 309 | ExpandingCage, 310 | Caterpillar, 311 | } 312 | 313 | impl AntPresets { 314 | pub const COUNT: usize = 73; 315 | pub fn from_index(index: usize) -> Self { 316 | match index { 317 | 0 => AntPresets::Default, 318 | 1 => AntPresets::FractalLabyrinth, 319 | 2 => AntPresets::CosmicBloom, 320 | 3 => AntPresets::Starburst, 321 | 4 => AntPresets::InfinitePrism, 322 | 5 => AntPresets::IntersectingTriangles, 323 | 6 => AntPresets::SquareEscape, 324 | 7 => AntPresets::SoaringSerpent, 325 | 8 => AntPresets::ConvergingChaos, 326 | 9 => AntPresets::DiamondDance, 327 | 10 => AntPresets::ZigzaggingZones, 328 | 11 => AntPresets::JaggedJourney, 329 | 12 => AntPresets::TiltedTiles, 330 | 13 => AntPresets::GalaxyNeedle, 331 | 14 => AntPresets::Highway, 332 | 15 => AntPresets::PatternedHighway, 333 | 16 => AntPresets::SquaresHighway, 334 | 17 => AntPresets::TriangleHighway, 335 | 18 => AntPresets::TilesHighway, 336 | 19 => AntPresets::StripedHighway, 337 | 20 => AntPresets::BoxHighway, 338 | 21 => AntPresets::ChaosHighway, 339 | 22 => AntPresets::OrderedHighways, 340 | 23 => AntPresets::MosaicMaze, 341 | 24 => AntPresets::TessellationTrail, 342 | 25 => AntPresets::Spiral, 343 | 26 => AntPresets::ChaoticSpiral, 344 | 27 => AntPresets::RandomSpirals, 345 | 28 => AntPresets::SpiralHighway, 346 | 29 => AntPresets::SymmetricBloom, 347 | 30 => AntPresets::ExpandingTriangle, 348 | 31 => AntPresets::SwirlingBlades, 349 | 32 => AntPresets::Island, 350 | 33 => AntPresets::Cross, 351 | 34 => AntPresets::InfiniteSaw, 352 | 35 => AntPresets::DomainExpansion, 353 | 36 => AntPresets::TriangleDunes, 354 | 37 => AntPresets::Butterfly, 355 | 38 => AntPresets::Moth, 356 | 39 => AntPresets::Mountains, 357 | 40 => AntPresets::Laser, 358 | 41 => AntPresets::Stairs, 359 | 42 => AntPresets::Pyramid, 360 | 43 => AntPresets::DoubleCircle, 361 | 44 => AntPresets::TriangleFractal, 362 | 45 => AntPresets::TriangleFractal2, 363 | 46 => AntPresets::Spikes, 364 | 47 => AntPresets::Sword, 365 | 48 => AntPresets::Key, 366 | 49 => AntPresets::SpiralingBlocks, 367 | 50 => AntPresets::ChaosTriangles, 368 | 51 => AntPresets::Shuriken, 369 | 52 => AntPresets::ExpandingBoxes, 370 | 53 => AntPresets::PyramidBox, 371 | 54 => AntPresets::RandomSquarePaths, 372 | 55 => AntPresets::BouncingPath, 373 | 56 => AntPresets::FillingBoxes, 374 | 57 => AntPresets::RandomAreas, 375 | 58 => AntPresets::InfiniteTriangle, 376 | 59 => AntPresets::TriangleSpiral, 377 | 60 => AntPresets::ExplodingStar, 378 | 61 => AntPresets::PyramidExplosion, 379 | 62 => AntPresets::Spider, 380 | 63 => AntPresets::Towers, 381 | 64 => AntPresets::DitheringFill, 382 | 65 => AntPresets::SwirlingPath, 383 | 66 => AntPresets::SpiralFilling, 384 | 67 => AntPresets::Thorns, 385 | 68 => AntPresets::Nest, 386 | 69 => AntPresets::ZigzagPath, 387 | 70 => AntPresets::ChaosPrison, 388 | 71 => AntPresets::ExpandingCage, 389 | 72 => AntPresets::Caterpillar, 390 | _ => AntPresets::Default, 391 | } 392 | } 393 | } 394 | 395 | impl std::fmt::Display for AntPresets { 396 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 397 | match self { 398 | AntPresets::Default => write!(f, "Default"), 399 | AntPresets::FractalLabyrinth => write!(f, "Fractal Labyrinth"), 400 | AntPresets::CosmicBloom => write!(f, "Cosmic Bloom"), 401 | AntPresets::Starburst => write!(f, "Starburst"), 402 | AntPresets::InfinitePrism => write!(f, "Infinite Prism"), 403 | AntPresets::IntersectingTriangles => write!(f, "Intersecting Triangles"), 404 | AntPresets::SquareEscape => write!(f, "Square Escape"), 405 | AntPresets::SoaringSerpent => write!(f, "Soaring Serpent"), 406 | AntPresets::ConvergingChaos => write!(f, "Converging Chaos"), 407 | AntPresets::DiamondDance => write!(f, "Diamond Dance"), 408 | AntPresets::ZigzaggingZones => write!(f, "Zigzagging Zones"), 409 | AntPresets::JaggedJourney => write!(f, "Jagged Journey"), 410 | AntPresets::TiltedTiles => write!(f, "Tilted Tiles"), 411 | AntPresets::GalaxyNeedle => write!(f, "Galaxy Needle"), 412 | AntPresets::Highway => write!(f, "Highway"), 413 | AntPresets::PatternedHighway => write!(f, "Patterned Highway"), 414 | AntPresets::SquaresHighway => write!(f, "Squares Highway"), 415 | AntPresets::TriangleHighway => write!(f, "Triangle Highway"), 416 | AntPresets::TilesHighway => write!(f, "Tiles Highway"), 417 | AntPresets::StripedHighway => write!(f, "Striped Highway"), 418 | AntPresets::BoxHighway => write!(f, "Box Highway"), 419 | AntPresets::ChaosHighway => write!(f, "Chaos Highway"), 420 | AntPresets::OrderedHighways => write!(f, "Ordered Highways"), 421 | AntPresets::MosaicMaze => write!(f, "Mosaic Maze"), 422 | AntPresets::TessellationTrail => write!(f, "Tessellation Trail"), 423 | AntPresets::Spiral => write!(f, "Spiral"), 424 | AntPresets::ChaoticSpiral => write!(f, "Chaotic Spiral"), 425 | AntPresets::RandomSpirals => write!(f, "Random Spirals"), 426 | AntPresets::SpiralHighway => write!(f, "Spiral Highway"), 427 | AntPresets::SymmetricBloom => write!(f, "Symmetric Bloom"), 428 | AntPresets::ExpandingTriangle => write!(f, "Expanding Triangle"), 429 | AntPresets::SwirlingBlades => write!(f, "Swirling Blades"), 430 | AntPresets::Island => write!(f, "Island"), 431 | AntPresets::Cross => write!(f, "Cross"), 432 | AntPresets::InfiniteSaw => write!(f, "Infinite Saw"), 433 | AntPresets::DomainExpansion => write!(f, "Domain Expansion"), 434 | AntPresets::TriangleDunes => write!(f, "Triangle Dunes"), 435 | AntPresets::Butterfly => write!(f, "Butterfly"), 436 | AntPresets::Moth => write!(f, "Moth"), 437 | AntPresets::Mountains => write!(f, "Mountains"), 438 | AntPresets::Laser => write!(f, "Laser"), 439 | AntPresets::Stairs => write!(f, "Stairs"), 440 | AntPresets::Pyramid => write!(f, "Pyramid"), 441 | AntPresets::DoubleCircle => write!(f, "Double Circle"), 442 | AntPresets::TriangleFractal => write!(f, "Triangle Fractal"), 443 | AntPresets::TriangleFractal2 => write!(f, "Triangle Fractal 2"), 444 | AntPresets::Spikes => write!(f, "Spikes"), 445 | AntPresets::Sword => write!(f, "Sword"), 446 | AntPresets::Key => write!(f, "Key"), 447 | AntPresets::SpiralingBlocks => write!(f, "Spiraling Blocks"), 448 | AntPresets::ChaosTriangles => write!(f, "Chaos Triangles"), 449 | AntPresets::Shuriken => write!(f, "Shuriken"), 450 | AntPresets::ExpandingBoxes => write!(f, "Expanding Boxes"), 451 | AntPresets::PyramidBox => write!(f, "Pyramid in a Box"), 452 | AntPresets::RandomSquarePaths => write!(f, "Random Square Paths"), 453 | AntPresets::BouncingPath => write!(f, "Bouncing Path"), 454 | AntPresets::FillingBoxes => write!(f, "Filling Boxes"), 455 | AntPresets::RandomAreas => write!(f, "Random Areas"), 456 | AntPresets::InfiniteTriangle => write!(f, "Infinite Triangle"), 457 | AntPresets::TriangleSpiral => write!(f, "Triangle Spiral"), 458 | AntPresets::ExplodingStar => write!(f, "Exploding Star"), 459 | AntPresets::PyramidExplosion => write!(f, "Pyramid Explosion"), 460 | AntPresets::Spider => write!(f, "Spider"), 461 | AntPresets::Towers => write!(f, "Towers"), 462 | AntPresets::DitheringFill => write!(f, "Dithering Fill"), 463 | AntPresets::SwirlingPath => write!(f, "Swirling Path"), 464 | AntPresets::SpiralFilling => write!(f, "Spiral Filling"), 465 | AntPresets::Thorns => write!(f, "Thorns"), 466 | AntPresets::Nest => write!(f, "Nest"), 467 | AntPresets::ZigzagPath => write!(f, "Zigzag Path"), 468 | AntPresets::ChaosPrison => write!(f, "Chaos Prison"), 469 | AntPresets::ExpandingCage => write!(f, "Expanding Cage"), 470 | AntPresets::Caterpillar => write!(f, "Caterpillar"), 471 | } 472 | } 473 | } 474 | 475 | impl AntSim { 476 | pub fn get_preset(preset: AntPresets) -> Self { 477 | let ruleset = match preset { 478 | AntPresets::Default => "RL", 479 | AntPresets::FractalLabyrinth => "LRRRRLLLRLLLLLLL", 480 | AntPresets::CosmicBloom => "LRRLLLLRLLRRLLLL", 481 | AntPresets::Starburst => "LRLLLRRRRRLRLL", 482 | AntPresets::InfinitePrism => "LLLRLLRRRLLLLL", 483 | AntPresets::IntersectingTriangles => "LLRLLLRRRRRLLRLL", 484 | AntPresets::SquareEscape => "LRRRLLLLLLRRLL", 485 | AntPresets::SoaringSerpent => "LLLLLLLRRLLRLLL", 486 | AntPresets::ConvergingChaos => "LLLLRLLLRRLRLLL", 487 | AntPresets::DiamondDance => "LRRLLLRRRLLLL", 488 | AntPresets::ZigzaggingZones => "LRRLLLRRRLLLLL", 489 | AntPresets::JaggedJourney => "LRRLLLRRRLLLLLL", 490 | AntPresets::TiltedTiles => "LRRLLLRRRLLLLLLL", 491 | AntPresets::GalaxyNeedle => "LRRLRLLLLLRRLL", 492 | AntPresets::Highway => "LLRLRLLLRLLLLL", 493 | AntPresets::PatternedHighway => "RRLRLLRRRRLL", 494 | AntPresets::SquaresHighway => "LRRRRRLRRRRLLL", 495 | AntPresets::TriangleHighway => "LLRLRRLRLLRRLLL", 496 | AntPresets::TilesHighway => "LRRRRRLRRRRLLLL", 497 | AntPresets::StripedHighway => "LLRLRRLRLRRLLLLL", 498 | AntPresets::BoxHighway => "RRRLRRLLRLRRRLL", 499 | AntPresets::ChaosHighway => "LLRLRRLLRLLLLLL", 500 | AntPresets::OrderedHighways => "RRLLRLLLLLLLRRL", 501 | AntPresets::MosaicMaze => "LRRRRRLRRRRLLLLL", 502 | AntPresets::TessellationTrail => "LRRRRRLRRRRLLLLLL", 503 | AntPresets::Spiral => "RLLLLRRRLLL", 504 | AntPresets::ChaoticSpiral => "RRLLLLRLRLRRLLL", 505 | AntPresets::RandomSpirals => "RLLLLLRRRLLLLRRL", 506 | AntPresets::SpiralHighway => "LRLRRLLLLLLLRL", 507 | AntPresets::SymmetricBloom => "LLRRLLRRRRLLRRLL", 508 | AntPresets::ExpandingTriangle => "LLRRRLRRRLLLLRRL", 509 | AntPresets::SwirlingBlades => "LLRRRLLLRRRLLLLL", 510 | AntPresets::Island => "LLRLRRLRLRRRRRRL", 511 | AntPresets::Cross => "LRLRLLRLLLRRLLLL", 512 | AntPresets::InfiniteSaw => "LLRRRLRRRLRLLLL", 513 | AntPresets::DomainExpansion => "LRRLLLRLRRRRRRL", 514 | AntPresets::TriangleDunes => "LLRLRRLLLLLRLRL", 515 | AntPresets::Butterfly => "LLLRRRRRRLLL", 516 | AntPresets::Moth => "LRRLLRRL", 517 | AntPresets::Mountains => "RRLRLLLLRLLLLLLL", 518 | AntPresets::Laser => "RRLLLRRLRRRRLLLL", 519 | AntPresets::Stairs => "RRLRLLLLRLLRLLL", 520 | AntPresets::Pyramid => "RRRLRRRRLLRRRRLL", 521 | AntPresets::DoubleCircle => "RRRRRRRRLLLLLLLL", 522 | AntPresets::TriangleFractal => "LLLLLRLLLRRLLLLL", 523 | AntPresets::TriangleFractal2 => "LRRRLLLRLLLLLLL", 524 | AntPresets::Spikes => "LLRLRLRRRRRRL", 525 | AntPresets::Sword => "LLRLRLLLLRLRLRRL", 526 | AntPresets::Key => "RRLRLLLRLLRRRRLL", 527 | AntPresets::SpiralingBlocks => "LRRLLLRLRLRRLLL", 528 | AntPresets::ChaosTriangles => "LRRRRLRRLLRRRRRL", 529 | AntPresets::Shuriken => "LLLLRRRRRLLRRRRL", 530 | AntPresets::ExpandingBoxes => "RRLRLLLRRRRRLL", 531 | AntPresets::PyramidBox => "RRLLLLRRLRLLLLLL", 532 | AntPresets::RandomSquarePaths => "RLLLLLLLRRL", 533 | AntPresets::BouncingPath => "RRLLLLRRLRLRLLL", 534 | AntPresets::FillingBoxes => "LRRLLRRLLLRRRLLL", 535 | AntPresets::RandomAreas => "RRLLLLRLLLLRRLLL", 536 | AntPresets::InfiniteTriangle => "RRLLLRRRRRLRLLL", 537 | AntPresets::TriangleSpiral => "RRLLLLRRRLLLLLLL", 538 | AntPresets::ExplodingStar => "RRRRLRRRLLRRRLL", 539 | AntPresets::PyramidExplosion => "LLRLRRRLRRLLLL", 540 | AntPresets::Spider => "LRRLLLLRLLRLL", 541 | AntPresets::Towers => "RLLLRRRLLLL", 542 | AntPresets::DitheringFill => "RLLLRLRRRRRLRL", 543 | AntPresets::SwirlingPath => "RLLLLLLLLLRRL", 544 | AntPresets::SpiralFilling => "RLLLRLLLLRRLLLLL", 545 | AntPresets::Thorns => "LLLLLRRLLLLRLLL", 546 | AntPresets::Nest => "LRRLLLLRRLLRLLLL", 547 | AntPresets::ZigzagPath => "LRRLLLLRRRLLLRRL", 548 | AntPresets::ChaosPrison => "LRRLLLRRRRRRRL", 549 | AntPresets::ExpandingCage => "LRRRLLLLLLRRLLL", 550 | AntPresets::Caterpillar => "LRRRRLLLRRRLRL", 551 | }; 552 | 553 | AntSim { 554 | rules: AntSim::parse_ant_ruleset(ruleset), 555 | rules_input: Input::default().with_value(ruleset.to_string()), 556 | ..Default::default() 557 | } 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /src/simulations/elementary.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{style::Color, symbols::Marker}; 2 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 3 | use tui_input::Input; 4 | use tui_widget_list::ListState; 5 | 6 | use crate::app::InputMode; 7 | 8 | use super::Grid; 9 | 10 | pub struct ElementarySim { 11 | pub marker: Marker, // Character to draw the cells 12 | pub grid: Grid, // Grid of cells 13 | pub current_line: Vec, // Grid of cells 14 | pub neighbours: usize, 15 | pub alive_state: Color, 16 | pub dead_state: Color, 17 | pub generation: usize, // Number of generations 18 | pub rule: u8, // Rules for the ant 19 | 20 | pub settings_state: ListState, 21 | 22 | pub rule_input: Input, // Rules for the ant 23 | pub rule_input_mode: InputMode, // Input mode 24 | } 25 | 26 | impl Default for ElementarySim { 27 | fn default() -> Self { 28 | let mut list_state = ListState::default(); 29 | list_state.selected = Some(0); 30 | Self { 31 | marker: Marker::HalfBlock, 32 | grid: Grid::new(), 33 | current_line: Vec::new(), 34 | neighbours: 3, 35 | alive_state: Color::Yellow, 36 | dead_state: Color::Reset, 37 | generation: 0, 38 | rule: 22, 39 | 40 | settings_state: list_state, 41 | 42 | rule_input: Input::from(String::from("22")), 43 | rule_input_mode: InputMode::Normal, 44 | } 45 | } 46 | } 47 | 48 | impl ElementarySim { 49 | pub fn run(&mut self, speed_multiplier: usize) { 50 | for _ in 0..speed_multiplier { 51 | if self.generation == 0 { 52 | self.grid.cells[0] = self 53 | .current_line 54 | .par_iter() 55 | .map(|&b| if b { self.alive_state } else { self.dead_state }) 56 | .collect(); 57 | } else { 58 | // Scroll the grid upwards 59 | self.grid.cells.pop(); 60 | self.grid 61 | .cells 62 | .insert(0, vec![self.dead_state; self.grid.width()]); 63 | 64 | // Iterate over every window of neighbours 65 | for (center_idx, neighbours) in 66 | self.current_line.windows(self.neighbours).enumerate() 67 | { 68 | // Get the index of the rule corresponding to the slice of bools 69 | let rule_idx = bin_to_idx(neighbours); 70 | 71 | let center_idx = center_idx + self.neighbours / 2; 72 | 73 | // Get the nth bit of the rule 74 | let rule = get_bit(self.rule as u32, rule_idx); 75 | 76 | match rule { 77 | true => self.grid.cells[0][center_idx] = self.alive_state, 78 | false => self.grid.cells[0][center_idx] = self.dead_state, 79 | } 80 | } 81 | } 82 | 83 | // Update the line with the next generation 84 | for (i, c) in self.grid.cells[0].iter().enumerate() { 85 | self.current_line[i] = *c == self.alive_state 86 | } 87 | 88 | self.generation = self.generation.saturating_add(1); 89 | } 90 | } 91 | 92 | pub fn parse_input(&mut self) { 93 | let input: Result = match !self.rule_input.value().is_empty() { 94 | true => self.rule_input.value().parse(), 95 | false => Ok(0), 96 | }; 97 | 98 | match input { 99 | Ok(n) => { 100 | self.rule = n; 101 | } 102 | Err(_) => { 103 | self.rule_input = self.rule_input.clone().with_value(255.to_string()); 104 | self.rule = 255; 105 | } 106 | } 107 | } 108 | } 109 | 110 | pub enum ElementarySettings { 111 | Rule, 112 | Start, 113 | } 114 | 115 | impl ElementarySettings { 116 | pub const COUNT: usize = 2; 117 | pub fn from_index(index: usize) -> Self { 118 | match index { 119 | 0 => ElementarySettings::Rule, 120 | 1 => ElementarySettings::Start, 121 | _ => ElementarySettings::Rule, 122 | } 123 | } 124 | } 125 | 126 | impl std::fmt::Display for ElementarySettings { 127 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 128 | match self { 129 | ElementarySettings::Rule => write!(f, "Rule"), 130 | ElementarySettings::Start => write!(f, "Start"), 131 | } 132 | } 133 | } 134 | 135 | /// Converts a slice of booleans into integer 136 | /// # Example 137 | /// ```rust 138 | /// let bits: [bool; 3] = [true, false, true]; 139 | /// assert_eq!(bin_to_idx(&bits), 5); 140 | /// ``` 141 | pub fn bin_to_idx(slice: &[bool]) -> usize { 142 | let mut rule_idx: usize = 0; 143 | slice.iter().rev().enumerate().for_each(|(i, b)| { 144 | rule_idx += (*b as usize) << (i * *b as usize); 145 | }); 146 | rule_idx 147 | } 148 | 149 | /// Gets the nth bit of a positive integer 150 | pub fn get_bit(num: u32, idx: usize) -> bool { 151 | ((num >> idx) & 1) != 0 152 | } 153 | 154 | #[cfg(test)] 155 | #[test] 156 | fn idx_seven() { 157 | let bits: [bool; 3] = [true, true, true]; 158 | assert_eq!(bin_to_idx(&bits), 7); 159 | 160 | // Check if the third bit of 7 is 1 161 | assert!(get_bit(7_u32, 2)); 162 | } 163 | 164 | #[test] 165 | fn idx_zero() { 166 | let bits: [bool; 3] = [false, false, false]; 167 | assert_eq!(bin_to_idx(&bits), 0); 168 | 169 | assert!(!get_bit(0_u32, 2)); 170 | } 171 | 172 | #[test] 173 | fn idx_one() { 174 | let bits: [bool; 3] = [false, false, true]; 175 | assert_eq!(bin_to_idx(&bits), 1); 176 | 177 | assert!(get_bit(1_u32, 0)); 178 | } 179 | 180 | #[test] 181 | fn idx_four() { 182 | let bits: [bool; 3] = [true, false, false]; 183 | assert_eq!(bin_to_idx(&bits), 4); 184 | 185 | assert!(get_bit(4_u32, 2)); 186 | } 187 | 188 | #[test] 189 | fn idx_two() { 190 | let bits: [bool; 3] = [false, true, false]; 191 | assert_eq!(bin_to_idx(&bits), 2); 192 | 193 | assert!(get_bit(2_u32, 1)); 194 | } 195 | 196 | #[test] 197 | fn idx_five() { 198 | let bits: [bool; 3] = [true, false, true]; 199 | assert_eq!(bin_to_idx(&bits), 5); 200 | 201 | assert!(!get_bit(5_u32, 1)); 202 | } 203 | -------------------------------------------------------------------------------- /src/simulations/game_of_life.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{style::Color, symbols::Marker}; 2 | use tui_widget_list::ListState; 3 | 4 | use super::{ant::Ant, Direction, Grid}; 5 | 6 | pub struct GolSim { 7 | pub marker: Marker, // Character to draw the cells 8 | pub grid: Grid, // Grid of cells 9 | pub alive_state: Color, 10 | pub dead_state: Color, 11 | pub generation: usize, // Number of generations 12 | 13 | pub settings_state: ListState, 14 | pub edit_cursor: Ant, // Reuse of the ant as a cursor 15 | } 16 | 17 | impl Default for GolSim { 18 | fn default() -> Self { 19 | let mut list_state = ListState::default(); 20 | list_state.selected = Some(0); 21 | Self { 22 | marker: Marker::HalfBlock, 23 | grid: Grid::new(), 24 | alive_state: Color::Yellow, 25 | dead_state: Color::Reset, 26 | generation: Default::default(), 27 | 28 | settings_state: list_state, 29 | edit_cursor: Ant { 30 | x: usize::MAX, 31 | y: usize::MAX, 32 | color: Color::Red, 33 | direction: Direction::Up, 34 | }, 35 | } 36 | } 37 | } 38 | 39 | impl GolSim { 40 | pub fn run(&mut self, speed_multiplier: usize) { 41 | for _ in 0..speed_multiplier { 42 | let grid = self.grid.clone(); 43 | for y in 0..grid.height() { 44 | for x in 0..grid.width() { 45 | let alive_count = count_alive_neighbours(&grid, x, y, self.alive_state); 46 | let cell = &mut self.grid.cells[y][x]; 47 | 48 | // Game of life rules 49 | if *cell == self.alive_state { 50 | if alive_count < 2 { 51 | *cell = self.dead_state; 52 | } else if alive_count <= 3 { 53 | *cell = self.alive_state; 54 | } else if alive_count > 3 { 55 | *cell = self.dead_state; 56 | } 57 | } else if alive_count == 3 { 58 | *cell = self.alive_state; 59 | } 60 | } 61 | } 62 | } 63 | self.generation = self.generation.saturating_add(speed_multiplier); 64 | } 65 | 66 | pub fn toggle_cell(&mut self, x: usize, y: usize) { 67 | let cell = &mut self.grid.cells[y][x]; 68 | if *cell == self.alive_state { 69 | *cell = self.dead_state; 70 | } else { 71 | *cell = self.alive_state; 72 | } 73 | } 74 | } 75 | 76 | /// Counts the alive neighbours of a cell based on its position and the alive state 77 | fn count_alive_neighbours(grid: &Grid, x: usize, y: usize, alive_state: Color) -> usize { 78 | let left = grid.cells[y][match x as i32 - 1 < 0 { 79 | true => grid.width() - 1, 80 | false => x - 1, 81 | }]; 82 | let right = grid.cells[y][match x + 1 >= grid.width() { 83 | true => 0, 84 | false => x + 1, 85 | }]; 86 | 87 | let bottom = grid.cells[match y as i32 - 1 < 0 { 88 | true => grid.height() - 1, 89 | false => y - 1, 90 | }][x]; 91 | let top = grid.cells[match y + 1 >= grid.height() { 92 | true => 0, 93 | false => y + 1, 94 | }][x]; 95 | 96 | let top_right = grid.cells[match y + 1 >= grid.height() { 97 | true => 0, 98 | false => y + 1, 99 | }][match x + 1 >= grid.width() { 100 | true => 0, 101 | false => x + 1, 102 | }]; 103 | let top_left = grid.cells[match y + 1 >= grid.height() { 104 | true => 0, 105 | false => y + 1, 106 | }][match x as i32 - 1 < 0 { 107 | true => grid.width() - 1, 108 | false => x - 1, 109 | }]; 110 | 111 | let bottom_right = grid.cells[match y as i32 - 1 < 0 { 112 | true => grid.height() - 1, 113 | false => y - 1, 114 | }][match x + 1 >= grid.width() { 115 | true => 0, 116 | false => x + 1, 117 | }]; 118 | let bottom_left = grid.cells[match y as i32 - 1 < 0 { 119 | true => grid.height() - 1, 120 | false => y - 1, 121 | }][match x as i32 - 1 < 0 { 122 | true => grid.width() - 1, 123 | false => x - 1, 124 | }]; 125 | 126 | let neighbours = [ 127 | top_left, 128 | top, 129 | top_right, 130 | left, 131 | right, 132 | bottom_left, 133 | bottom, 134 | bottom_right, 135 | ]; 136 | 137 | let mut count = 0; 138 | for cell in neighbours { 139 | if cell == alive_state { 140 | count += 1; 141 | } 142 | } 143 | 144 | count 145 | } 146 | 147 | pub enum GolSettings { 148 | EditGrid, 149 | Start, 150 | } 151 | 152 | impl GolSettings { 153 | pub const COUNT: usize = 2; 154 | pub fn from_index(index: usize) -> Self { 155 | match index { 156 | 0 => GolSettings::EditGrid, 157 | 1 => GolSettings::Start, 158 | _ => GolSettings::EditGrid, 159 | } 160 | } 161 | } 162 | 163 | impl std::fmt::Display for GolSettings { 164 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 165 | match self { 166 | GolSettings::EditGrid => write!(f, "Edit grid"), 167 | GolSettings::Start => write!(f, "Start"), 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/simulations/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ant; 2 | pub mod elementary; 3 | pub mod game_of_life; 4 | 5 | 6 | use ratatui::style::Color; 7 | use std::fmt; 8 | use std::fmt::Display; 9 | use std::fmt::Formatter; 10 | 11 | 12 | /// Enum that represents the 2D directions 13 | /// ```plain 14 | /// U 15 | /// | 16 | /// L --|-- R 17 | /// | 18 | /// D 19 | /// ``` 20 | #[derive(Clone, Copy)] 21 | pub enum Direction { 22 | Left, 23 | Right, 24 | Up, 25 | Down, 26 | } 27 | 28 | impl Direction { 29 | /// Returns the direction to the left of the current direction 30 | pub fn turn_left(&self) -> Self { 31 | match self { 32 | Direction::Left => Direction::Down, 33 | Direction::Right => Direction::Up, 34 | Direction::Up => Direction::Left, 35 | Direction::Down => Direction::Right, 36 | } 37 | } 38 | 39 | /// Returns the direction to the right of the current direction 40 | pub fn turn_right(&self) -> Self { 41 | match self { 42 | Direction::Left => Direction::Up, 43 | Direction::Right => Direction::Down, 44 | Direction::Up => Direction::Right, 45 | Direction::Down => Direction::Left, 46 | } 47 | } 48 | 49 | /// Returns the opposite direction of the current direction 50 | pub fn turn_opposite(&self) -> Self { 51 | match self { 52 | Direction::Left => Direction::Right, 53 | Direction::Right => Direction::Left, 54 | Direction::Up => Direction::Down, 55 | Direction::Down => Direction::Up, 56 | } 57 | } 58 | 59 | /// Returns the direction relative to the current direction 60 | /// - `Left` -> turn left 61 | /// - `Right` -> turn right 62 | /// - `Up` -> continue in the same direction 63 | /// - `Down` -> turn opposite 64 | pub fn turn(&self, direction: &Direction) -> Self { 65 | match direction { 66 | Direction::Left => self.turn_left(), 67 | Direction::Right => self.turn_right(), 68 | Direction::Up => *self, 69 | Direction::Down => self.turn_opposite(), 70 | } 71 | } 72 | } 73 | 74 | impl Display for Direction { 75 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 76 | match self { 77 | Direction::Left => write!(f, "←"), 78 | Direction::Right => write!(f, "→"), 79 | Direction::Up => write!(f, "↑"), 80 | Direction::Down => write!(f, "↓"), 81 | } 82 | } 83 | } 84 | 85 | /// Struct that represents a grid of cells 86 | #[derive(Clone)] 87 | pub struct Grid { 88 | pub cells: Vec>, 89 | } 90 | 91 | impl Grid { 92 | /// Constructs a new empty `Grid` 93 | pub fn new() -> Self { 94 | Grid { cells: Vec::new() } 95 | } 96 | 97 | /// Resizes the grid in-place by providing the new dimensions and a new state 98 | pub fn resize(&mut self, new_width: usize, new_height: usize, new_state: Color) { 99 | for row in self.cells.iter_mut() { 100 | row.resize(new_width, new_state); 101 | } 102 | 103 | self.cells 104 | .resize(new_height, vec![new_state; new_width]); 105 | } 106 | 107 | /// Returns the width of the grid 108 | pub fn width(&self) -> usize { 109 | if self.cells.is_empty() { 110 | 0 111 | } else { 112 | self.cells[0].len() 113 | } 114 | } 115 | 116 | /// Returns the height of the grid 117 | pub fn height(&self) -> usize { 118 | if self.cells.is_empty() { 119 | 0 120 | } else { 121 | self.cells.len() 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/ui/elementary_ui.rs: -------------------------------------------------------------------------------- 1 | use ratatui::layout::{Constraint, Layout}; 2 | use ratatui::widgets::block::Position; 3 | use ratatui::widgets::{Clear, Padding, Wrap}; 4 | use ratatui::{layout::Rect, Frame}; 5 | 6 | use ratatui::{ 7 | layout::Alignment, 8 | style::{Style, Stylize}, 9 | text::Line, 10 | widgets::{ 11 | canvas::{Canvas, Points}, 12 | Block, BorderType, Borders, Paragraph, 13 | }, 14 | }; 15 | use tui_widget_list::{ListBuilder, ListView}; 16 | 17 | use crate::app::{App, EditTab, InputMode}; 18 | use crate::simulations::elementary::ElementarySettings; 19 | 20 | use super::{centered_rect_length, render_help, settings_help, start_content, ListItemContainer}; 21 | 22 | pub fn elementary_screen(frame: &mut Frame, app: &mut App) { 23 | if frame 24 | .area() 25 | .width 26 | .checked_mul(frame.area().height) 27 | .is_none() 28 | { 29 | let error_paragraph = Paragraph::new( 30 | "EEEEEEEEEEEEEEEEEEEEEE tttt hhhhhhh hhhhhhh tttt iiii iiii tttt lllllll lllllll 31 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E ttt⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h ttt⣿⣿⣿t i⣿⣿⣿⣿i i⣿⣿⣿⣿i ttt⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 32 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h t⣿⣿⣿⣿⣿t iiii iiii t⣿⣿⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 33 | EE⣿⣿⣿⣿⣿⣿EEEEEEEEE⣿⣿⣿⣿E t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h t⣿⣿⣿⣿⣿t t⣿⣿⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 34 | E⣿⣿⣿⣿⣿E EEEEEErrrrr rrrrrrrrr rrrrr rrrrrrrrr ooooooooooo rrrrr rrrrrrrrr ttttttt⣿⣿⣿⣿⣿ttttttt h⣿⣿⣿⣿h hhhhh eeeeeeeeeeee cccccccccccccccch⣿⣿⣿⣿h hhhhh aaaaaaaaaaaaa rrrrr rrrrrrrrr aaaaaaaaaaaaa ccccccccccccccccttttttt⣿⣿⣿⣿⣿ttttttt eeeeeeeeeeee rrrrr rrrrrrrrr ssssssssss iiiiiii zzzzzzzzzzzzzzzzz eeeeeeeeeeee iiiiiii ssssssssss ttttttt⣿⣿⣿⣿⣿ttttttt ooooooooooo ooooooooooo ssssssssss mmmmmmm mmmmmmm aaaaaaaaaaaaa l⣿⣿⣿⣿l l⣿⣿⣿⣿l 35 | E⣿⣿⣿⣿⣿E r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t h⣿⣿⣿⣿hh⣿⣿⣿⣿⣿hhh ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿hh⣿⣿⣿⣿⣿hhh a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ct⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee i⣿⣿⣿⣿⣿i ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s mm⣿⣿⣿⣿⣿⣿⣿m m⣿⣿⣿⣿⣿⣿⣿mm a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 36 | E⣿⣿⣿⣿⣿⣿EEEEEEEEEE r⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿or⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿hh e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿ee c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿hh aaaaaaaaa⣿⣿⣿⣿⣿ar⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r aaaaaaaaa⣿⣿⣿⣿⣿a c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ct⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿eer⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿ee i⣿⣿⣿⣿i ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿mm⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿m aaaaaaaaa⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 37 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E rr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿ro⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿orr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ tttttt⣿⣿⣿⣿⣿⣿⣿tttttt h⣿⣿⣿⣿⣿⣿⣿hhh⣿⣿⣿⣿⣿⣿h e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿⣿⣿hhh⣿⣿⣿⣿⣿⣿h a⣿⣿⣿⣿arr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r a⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ctttttt⣿⣿⣿⣿⣿⣿⣿tttttt e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿err⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i zzzzzzzz⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿s tttttt⣿⣿⣿⣿⣿⣿⣿tttttt o⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿sm⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿m a⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 38 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿ro⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿c ccccccch⣿⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿⣿h aaaaaaa⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r aaaaaaa⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿c ccccccc t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿s ssssss i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿s ssssss t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿s ssssss m⣿⣿⣿⣿⣿mmm⣿⣿⣿⣿⣿⣿mmm⣿⣿⣿⣿⣿m aaaaaaa⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 39 | E⣿⣿⣿⣿⣿⣿EEEEEEEEEE r⣿⣿⣿⣿⣿r rrrrrrr r⣿⣿⣿⣿⣿r rrrrrrro⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r rrrrrrr t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿c h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h aa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r rrrrrrraa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r rrrrrrr s⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m aa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 40 | E⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿eeeeeeeeeee c⣿⣿⣿⣿⣿c h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿eeeeeeeeeee r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿eeeeeeeeeee i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 41 | E⣿⣿⣿⣿⣿E EEEEEE r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿t tttttth⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿c ccccccch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿a a⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿c ccccccc t⣿⣿⣿⣿⣿t tttttte⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r ssssss s⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i ssssss s⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t tttttto⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o ssssss s⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 42 | EE⣿⣿⣿⣿⣿⣿EEEEEEEE⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿th⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿a a⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿te⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿si⣿⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿zzzzzzzze⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿⣿⣿is⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿to⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿sm⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a l⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 43 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿th⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿c tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee i⣿⣿⣿⣿⣿⣿is⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿to⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 44 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo r⣿⣿⣿⣿⣿r tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tth⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿ar⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿a cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿c tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tt ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss i⣿⣿⣿⣿⣿⣿iz⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tt oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿al⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 45 | EEEEEEEEEEEEEEEEEEEEEE rrrrrrr rrrrrrr ooooooooooo rrrrrrr ttttttttttt hhhhhhh hhhhhhh eeeeeeeeeeeeee cccccccccccccccchhhhhhh hhhhhhh aaaaaaaaaa aaaarrrrrrr aaaaaaaaaa aaaa cccccccccccccccc ttttttttttt eeeeeeeeeeeeee rrrrrrr sssssssssss iiiiiiiizzzzzzzzzzzzzzzzz eeeeeeeeeeeeee iiiiiiii sssssssssss ttttttttttt ooooooooooo ooooooooooo sssssssssss mmmmmm mmmmmm mmmmmm aaaaaaaaaa aaaallllllllllllllll" 46 | ).alignment(Alignment::Center); 47 | frame.render_widget( 48 | error_paragraph, 49 | Rect { 50 | x: 0, 51 | y: 0, 52 | width: 592, 53 | height: 16, 54 | }, 55 | ); 56 | return; 57 | } 58 | 59 | // Initialize the simulation if it's not already 60 | let width = f64::from(frame.area().width - 1); 61 | let height = f64::from((frame.area().height - 2) * 2); 62 | 63 | if app.elementary_sim.is_none() { 64 | app.start_elementary_default(); 65 | 66 | let sim = app.elementary_sim.as_mut().unwrap(); 67 | 68 | // Initialize the grid with the same size as the canvas 69 | sim.grid 70 | .resize(width as usize, height as usize, sim.dead_state); 71 | sim.current_line.resize(width as usize, false); 72 | 73 | // Set the middle element of the first generation to true 74 | sim.current_line.insert(width as usize / 2, true); 75 | 76 | // Show the generation 0 77 | sim.run(app.speed_multiplier); 78 | } else if app.elementary_sim.as_ref().unwrap().generation == 0 { 79 | // If the simulation is already set, the grid still needs to be initialized with the 80 | // screen size 81 | let sim = app.elementary_sim.as_mut().unwrap(); 82 | 83 | // Initialize the grid with the same size as the canvas 84 | sim.grid 85 | .resize(width as usize, height as usize, sim.dead_state); 86 | sim.current_line.resize(width as usize, false); 87 | 88 | // Set the middle element of the first generation to true 89 | sim.current_line.insert(width as usize / 2, true); 90 | 91 | // Show the generation 0 92 | sim.run(app.speed_multiplier); 93 | } 94 | 95 | // From here `app.elementary_sim` is `Some` 96 | let sim = app.elementary_sim.as_mut().unwrap(); 97 | 98 | ///////////////////////////// 99 | // Border content 100 | ///////////////////////////// 101 | 102 | let top_title = Line::from(vec![" Elementary CA ".yellow()]); 103 | 104 | let bottom_left_title = Line::from(vec![ 105 | " Iteration: ".into(), 106 | sim.generation.to_string().yellow(), 107 | " ".into(), 108 | ]); 109 | 110 | let key_help = Line::from(vec![" '?' ".yellow(), "Help ".into()]); 111 | 112 | let bottom_right_title = Line::from(vec![ 113 | " Speed: ".into(), 114 | if app.speed.as_millis() == 0 { 115 | format!("{}x ", app.speed_multiplier).yellow() 116 | } else { 117 | format!("{}ms ", app.speed.as_millis()).yellow() 118 | }, 119 | ]); 120 | 121 | ///////////////////////////// 122 | // Simulation canvas 123 | ///////////////////////////// 124 | 125 | let canvas = Canvas::default() 126 | .block( 127 | Block::default() 128 | .border_type(BorderType::Double) 129 | .borders(Borders::ALL) 130 | .title_top(top_title.centered()) 131 | .title_bottom(bottom_left_title.left_aligned()) 132 | .title_bottom(bottom_right_title.right_aligned()) 133 | .title_bottom(key_help.centered()) 134 | .title_style(Style::default().bold()), 135 | ) 136 | .marker(sim.marker) 137 | .paint(|ctx| { 138 | // Draw grid 139 | for (y, row) in sim.grid.cells.iter().enumerate() { 140 | for (x, cell) in row.iter().enumerate() { 141 | ctx.draw(&Points { 142 | coords: &[(x as f64 - 1., y as f64)], 143 | color: *cell, 144 | }); 145 | } 146 | } 147 | }) 148 | .x_bounds([0., f64::from((frame.area().width - 2) - 1)]) 149 | .y_bounds([0., f64::from(((frame.area().height - 2) * 2) - 1)]); 150 | 151 | frame.render_widget(canvas, frame.area()); 152 | 153 | ///////////////////////////// 154 | // Help screen 155 | ///////////////////////////// 156 | 157 | let help_entries: Vec<(Line, Line)> = vec![ 158 | (Line::from("?".yellow()), Line::from("Help")), 159 | (Line::from("Q / Esc".yellow()), Line::from("Quit")), 160 | (Line::from("Space".yellow()), Line::from("Start/Pause")), 161 | (Line::from("K / ↑".yellow()), Line::from("Speed Up")), 162 | (Line::from("J / ↓".yellow()), Line::from("Speed Down")), 163 | (Line::from("L / →".yellow()), Line::from("Next Generation")), 164 | ]; 165 | 166 | if app.help_screen { 167 | render_help(frame, help_entries); 168 | } 169 | } 170 | 171 | pub fn edit(frame: &mut Frame, app: &mut App) { 172 | let sim = app.elementary_sim.as_mut().unwrap(); 173 | 174 | ///////////////////////////// 175 | // Centered popup 176 | ///////////////////////////// 177 | let settings = [ElementarySettings::Rule.to_string(), ElementarySettings::Start.to_string()]; 178 | let longest_setting: u16 = settings.iter().map(|k| k.to_string().len() as u16).max().unwrap_or(1); 179 | 180 | let edit_area = 181 | centered_rect_length(longest_setting + 32, ElementarySettings::COUNT as u16 * 2 + 5, frame.area()); 182 | 183 | let edit_block = Block::default() 184 | .title(" Editing Elementary CA ".bold()) 185 | .title_alignment(Alignment::Center) 186 | .title_position(Position::Bottom); 187 | 188 | frame.render_widget(Clear, edit_area); 189 | 190 | ///////////////////////////// 191 | // Layouts 192 | ///////////////////////////// 193 | 194 | let [top, bottom] = 195 | Layout::vertical([Constraint::Length(2), Constraint::Min(0)]).areas(edit_area); 196 | 197 | let [left, right] = 198 | Layout::horizontal([Constraint::Length(longest_setting + 4), Constraint::Min(0)]).areas(bottom); 199 | 200 | frame.render_widget(edit_block, top); 201 | 202 | ///////////////////////////// 203 | // Block styles 204 | ///////////////////////////// 205 | 206 | let selected_block = ratatui::widgets::Block::default() 207 | .borders(Borders::ALL) 208 | .border_type(BorderType::Thick) 209 | .border_style(Style::default().yellow().bold()) 210 | .padding(Padding::horizontal(1)) 211 | .style(Style::default().not_dim()); 212 | 213 | let disabled_block = ratatui::widgets::Block::default() 214 | .borders(Borders::ALL) 215 | .border_type(BorderType::Thick) 216 | .padding(Padding::horizontal(1)) 217 | .style(Style::default().dim()); 218 | 219 | ///////////////////////////// 220 | // Settings list 221 | ///////////////////////////// 222 | 223 | let block = match app.selected_edit_tab.as_ref().unwrap() { 224 | EditTab::Setting => selected_block.clone(), 225 | _ => disabled_block.clone(), 226 | }; 227 | 228 | frame.render_stateful_widget( 229 | ElementarySettingsList::build_list().block(block.clone()), 230 | left, 231 | &mut sim.settings_state, 232 | ); 233 | 234 | ///////////////////////////// 235 | // Content 236 | ///////////////////////////// 237 | 238 | let block = match app.selected_edit_tab.as_ref().unwrap() { 239 | EditTab::Content => selected_block.clone(), 240 | _ => disabled_block.clone(), 241 | }; 242 | 243 | frame.render_widget(block, right); 244 | 245 | let [right] = Layout::vertical([Constraint::Fill(1)]) 246 | .vertical_margin(1) 247 | .horizontal_margin(2) 248 | .areas(right); 249 | 250 | match ElementarySettings::from_index(sim.settings_state.selected.unwrap_or(0)) { 251 | ElementarySettings::Rule => { 252 | rule_content(sim, right, app.help_screen, frame); 253 | } 254 | ElementarySettings::Start => { 255 | start_content(frame, right); 256 | } 257 | } 258 | 259 | ///////////////////////////// 260 | // Help screen 261 | ///////////////////////////// 262 | 263 | let help_entries: Vec<(Line, Line)> = match app.selected_edit_tab.as_ref().unwrap() { 264 | EditTab::Setting => settings_help(), 265 | EditTab::Content => { 266 | match ElementarySettings::from_index(sim.settings_state.selected.unwrap_or(0)) { 267 | ElementarySettings::Rule => rule_help(), 268 | _ => vec![], 269 | } 270 | } 271 | }; 272 | 273 | if app.help_screen { 274 | render_help(frame, help_entries); 275 | } 276 | } 277 | 278 | 279 | fn rule_help<'a>() -> Vec<(Line<'a>, Line<'a>)> { 280 | vec![ 281 | (Line::from("?".yellow()), Line::from("Help")), 282 | ( 283 | Line::from("Q / Esc / Enter / H".yellow()), 284 | Line::from("Stop typing"), 285 | ), 286 | ] 287 | } 288 | 289 | fn rule_content( 290 | sim: &mut crate::simulations::elementary::ElementarySim, 291 | buf: Rect, 292 | help_screen: bool, 293 | frame: &mut Frame<'_>, 294 | ) { 295 | let info_text = format!( 296 | "Insert a number between 0 and {}:", 297 | 2_u32.pow(2_u32.pow(sim.neighbours as u32)) - 1 298 | ); 299 | let info = Paragraph::new(info_text.dim()).wrap(Wrap { trim: true }); 300 | 301 | let [info_chunk, input_chunk] = 302 | Layout::vertical([Constraint::Min(1), Constraint::Max(3)]).areas(buf); 303 | 304 | let scroll = sim 305 | .rule_input 306 | .visual_scroll(buf.width.saturating_sub(3) as usize); 307 | 308 | let input = Paragraph::new(sim.rule_input.value()) 309 | .scroll((0, scroll as u16)) 310 | .block( 311 | Block::default() 312 | .borders(Borders::ALL) 313 | .border_type(BorderType::Rounded) 314 | .border_style(match sim.rule_input_mode { 315 | InputMode::Normal => Style::default().dim(), 316 | InputMode::Editing => Style::default().bold().not_dim(), 317 | }), 318 | ); 319 | 320 | match sim.rule_input_mode { 321 | InputMode::Normal => 322 | // Hide the cursor. `Frame` does this by default, so we don't need to do anything here 323 | {} 324 | 325 | InputMode::Editing => { 326 | if !help_screen { 327 | { 328 | frame.set_cursor_position(( 329 | // Put cursor past the end of the input text 330 | buf.x 331 | + ((sim.rule_input.visual_cursor()).saturating_sub(scroll)) as u16 332 | + 1, 333 | // Move one line down, from the border to the input line 334 | buf.y + 3, 335 | )) 336 | } 337 | } 338 | } 339 | } 340 | 341 | frame.render_widget(info, info_chunk); 342 | frame.render_widget(input, input_chunk); 343 | } 344 | 345 | pub struct ElementarySettingsList; 346 | impl ElementarySettingsList { 347 | pub fn build_list<'a>() -> ListView<'a, ListItemContainer<'a, Line<'a>>> { 348 | let builder = ListBuilder::new(move |context| { 349 | let config = ElementarySettings::from_index(context.index); 350 | let line = Line::from(format!("{config}")).alignment(Alignment::Center); 351 | let mut item = ListItemContainer::new(line, Padding::ZERO); 352 | 353 | if context.is_selected { 354 | item = item.yellow().bold(); 355 | }; 356 | 357 | (item, 2) 358 | }); 359 | 360 | return ListView::new(builder, ElementarySettings::COUNT); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/ui/game_of_life_ui.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use ratatui::layout::{Constraint, Layout}; 3 | use ratatui::style::Color; 4 | use ratatui::widgets::block::Position; 5 | use ratatui::widgets::{Padding, Wrap}; 6 | use ratatui::{layout::Rect, Frame}; 7 | 8 | use ratatui::{ 9 | layout::Alignment, 10 | style::{Style, Stylize}, 11 | text::Line, 12 | widgets::{ 13 | canvas::{Canvas, Points}, 14 | Block, BorderType, Borders, Clear, Paragraph, 15 | }, 16 | }; 17 | use tui_widget_list::{ListBuilder, ListView}; 18 | 19 | use crate::app::{App, EditTab}; 20 | use crate::simulations::game_of_life::GolSettings; 21 | 22 | use super::{centered_rect_length, render_help, settings_help, start_content, ListItemContainer}; 23 | 24 | pub fn gol_screen(frame: &mut Frame, app: &mut App) { 25 | if frame 26 | .area() 27 | .width 28 | .checked_mul(frame.area().height) 29 | .is_none() 30 | { 31 | let error_paragraph = Paragraph::new( 32 | "EEEEEEEEEEEEEEEEEEEEEE tttt hhhhhhh hhhhhhh tttt iiii iiii tttt lllllll lllllll 33 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E ttt⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h ttt⣿⣿⣿t i⣿⣿⣿⣿i i⣿⣿⣿⣿i ttt⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 34 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h t⣿⣿⣿⣿⣿t iiii iiii t⣿⣿⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 35 | EE⣿⣿⣿⣿⣿⣿EEEEEEEEE⣿⣿⣿⣿E t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h t⣿⣿⣿⣿⣿t t⣿⣿⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 36 | E⣿⣿⣿⣿⣿E EEEEEErrrrr rrrrrrrrr rrrrr rrrrrrrrr ooooooooooo rrrrr rrrrrrrrr ttttttt⣿⣿⣿⣿⣿ttttttt h⣿⣿⣿⣿h hhhhh eeeeeeeeeeee cccccccccccccccch⣿⣿⣿⣿h hhhhh aaaaaaaaaaaaa rrrrr rrrrrrrrr aaaaaaaaaaaaa ccccccccccccccccttttttt⣿⣿⣿⣿⣿ttttttt eeeeeeeeeeee rrrrr rrrrrrrrr ssssssssss iiiiiii zzzzzzzzzzzzzzzzz eeeeeeeeeeee iiiiiii ssssssssss ttttttt⣿⣿⣿⣿⣿ttttttt ooooooooooo ooooooooooo ssssssssss mmmmmmm mmmmmmm aaaaaaaaaaaaa l⣿⣿⣿⣿l l⣿⣿⣿⣿l 37 | E⣿⣿⣿⣿⣿E r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t h⣿⣿⣿⣿hh⣿⣿⣿⣿⣿hhh ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿hh⣿⣿⣿⣿⣿hhh a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ct⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee i⣿⣿⣿⣿⣿i ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s mm⣿⣿⣿⣿⣿⣿⣿m m⣿⣿⣿⣿⣿⣿⣿mm a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 38 | E⣿⣿⣿⣿⣿⣿EEEEEEEEEE r⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿or⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿hh e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿ee c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿hh aaaaaaaaa⣿⣿⣿⣿⣿ar⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r aaaaaaaaa⣿⣿⣿⣿⣿a c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ct⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿eer⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿ee i⣿⣿⣿⣿i ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿mm⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿m aaaaaaaaa⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 39 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E rr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿ro⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿orr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ tttttt⣿⣿⣿⣿⣿⣿⣿tttttt h⣿⣿⣿⣿⣿⣿⣿hhh⣿⣿⣿⣿⣿⣿h e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿⣿⣿hhh⣿⣿⣿⣿⣿⣿h a⣿⣿⣿⣿arr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r a⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ctttttt⣿⣿⣿⣿⣿⣿⣿tttttt e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿err⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i zzzzzzzz⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿s tttttt⣿⣿⣿⣿⣿⣿⣿tttttt o⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿sm⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿m a⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 40 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿ro⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿c ccccccch⣿⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿⣿h aaaaaaa⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r aaaaaaa⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿c ccccccc t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿s ssssss i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿s ssssss t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿s ssssss m⣿⣿⣿⣿⣿mmm⣿⣿⣿⣿⣿⣿mmm⣿⣿⣿⣿⣿m aaaaaaa⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 41 | E⣿⣿⣿⣿⣿⣿EEEEEEEEEE r⣿⣿⣿⣿⣿r rrrrrrr r⣿⣿⣿⣿⣿r rrrrrrro⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r rrrrrrr t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿c h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h aa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r rrrrrrraa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r rrrrrrr s⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m aa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 42 | E⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿eeeeeeeeeee c⣿⣿⣿⣿⣿c h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿eeeeeeeeeee r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿eeeeeeeeeee i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 43 | E⣿⣿⣿⣿⣿E EEEEEE r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿t tttttth⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿c ccccccch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿a a⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿c ccccccc t⣿⣿⣿⣿⣿t tttttte⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r ssssss s⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i ssssss s⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t tttttto⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o ssssss s⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 44 | EE⣿⣿⣿⣿⣿⣿EEEEEEEE⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿th⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿a a⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿te⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿si⣿⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿zzzzzzzze⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿⣿⣿is⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿to⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿sm⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a l⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 45 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿th⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿c tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee i⣿⣿⣿⣿⣿⣿is⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿to⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 46 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo r⣿⣿⣿⣿⣿r tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tth⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿ar⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿a cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿c tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tt ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss i⣿⣿⣿⣿⣿⣿iz⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tt oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿al⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 47 | EEEEEEEEEEEEEEEEEEEEEE rrrrrrr rrrrrrr ooooooooooo rrrrrrr ttttttttttt hhhhhhh hhhhhhh eeeeeeeeeeeeee cccccccccccccccchhhhhhh hhhhhhh aaaaaaaaaa aaaarrrrrrr aaaaaaaaaa aaaa cccccccccccccccc ttttttttttt eeeeeeeeeeeeee rrrrrrr sssssssssss iiiiiiiizzzzzzzzzzzzzzzzz eeeeeeeeeeeeee iiiiiiii sssssssssss ttttttttttt ooooooooooo ooooooooooo sssssssssss mmmmmm mmmmmm mmmmmm aaaaaaaaaa aaaallllllllllllllll" 48 | ).alignment(Alignment::Center); 49 | frame.render_widget( 50 | error_paragraph, 51 | Rect { 52 | x: 0, 53 | y: 0, 54 | width: 592, 55 | height: 16, 56 | }, 57 | ); 58 | return; 59 | } 60 | 61 | // Initialize the simulation if it's not already 62 | let width = f64::from(frame.area().width - 2); 63 | let height = f64::from((frame.area().height - 2) * 2); 64 | 65 | if app.gol_sim.is_none() { 66 | app.start_gol_default(); 67 | 68 | let sim = app.gol_sim.as_mut().unwrap(); 69 | 70 | // Initialize the grid with the same size as the canvas 71 | sim.grid 72 | .resize(width as usize, height as usize, sim.dead_state); 73 | 74 | // Set random cells on 75 | let mut rng = rand::thread_rng(); 76 | let num_cells = width as usize * height as usize/2; 77 | 78 | for _ in 0..num_cells { 79 | let x = rng.gen_range((width * 0.1) as usize..(width - width * 0.1) as usize) as usize; 80 | let y = 81 | rng.gen_range((height * 0.1) as usize..(height - height * 0.1) as usize) as usize; 82 | sim.grid.cells[y][x] = sim.alive_state; 83 | } 84 | } else if app.gol_sim.as_ref().unwrap().generation == 0 { 85 | // If the simulation is already set, the grid still needs to be initialized with the 86 | // screen size 87 | let sim = app.gol_sim.as_mut().unwrap(); 88 | 89 | // Initialize the grid with the same size as the canvas 90 | sim.grid 91 | .resize(width as usize, height as usize, sim.dead_state); 92 | } 93 | 94 | // From here `app.gol_sim` is `Some` 95 | let sim = app.gol_sim.as_ref().unwrap(); 96 | 97 | ///////////////////////////// 98 | // Border content 99 | ///////////////////////////// 100 | 101 | let top_title = Line::from(vec![" Conway's Game of Life ".yellow()]); 102 | 103 | let bottom_left_title = Line::from(vec![ 104 | " Iteration: ".into(), 105 | sim.generation.to_string().yellow(), 106 | " ".into(), 107 | ]); 108 | 109 | let key_help = Line::from(vec![" '?' ".yellow(), "Help ".into()]); 110 | 111 | let bottom_right_title = Line::from(vec![ 112 | " Speed: ".into(), 113 | if app.speed.as_millis() == 0 { 114 | format!("{}x ", app.speed_multiplier).yellow() 115 | } else { 116 | format!("{}ms ", app.speed.as_millis()).yellow() 117 | }, 118 | ]); 119 | 120 | ///////////////////////////// 121 | // Simulation canvas 122 | ///////////////////////////// 123 | 124 | let canvas = Canvas::default() 125 | .block( 126 | Block::default() 127 | .border_type(BorderType::Double) 128 | .borders(Borders::ALL) 129 | .title_top(top_title.centered()) 130 | .title_bottom(bottom_left_title.left_aligned()) 131 | .title_bottom(bottom_right_title.right_aligned()) 132 | .title_bottom(key_help.centered()) 133 | .title_style(Style::default().bold()), 134 | ) 135 | .marker(sim.marker) 136 | .paint(|ctx| { 137 | // Draw grid 138 | for (y, row) in sim.grid.cells.iter().enumerate() { 139 | for (x, cell) in row.iter().enumerate() { 140 | ctx.draw(&Points { 141 | coords: &[(x as f64, y as f64)], 142 | color: *cell, 143 | }); 144 | } 145 | } 146 | }) 147 | .x_bounds([0., f64::from((frame.area().width - 2) - 1)]) 148 | .y_bounds([0., f64::from(((frame.area().height - 2) * 2) - 1)]); 149 | 150 | frame.render_widget(canvas, frame.area()); 151 | 152 | ///////////////////////////// 153 | // Help screen 154 | ///////////////////////////// 155 | 156 | let help_entries: Vec<(Line, Line)> = vec![ 157 | (Line::from("?".yellow()), Line::from("Help")), 158 | (Line::from("Q / Esc".yellow()), Line::from("Quit")), 159 | (Line::from("Space".yellow()), Line::from("Start/Pause")), 160 | (Line::from("K / ↑".yellow()), Line::from("Speed Up")), 161 | (Line::from("J / ↓".yellow()), Line::from("Speed Down")), 162 | (Line::from("L / →".yellow()), Line::from("Next Generation")), 163 | ]; 164 | 165 | if app.help_screen { 166 | render_help(frame, help_entries); 167 | } 168 | } 169 | 170 | pub fn edit(frame: &mut Frame, app: &mut App) { 171 | let sim = app.gol_sim.as_mut().unwrap(); 172 | 173 | ///////////////////////////// 174 | // Centered popup 175 | ///////////////////////////// 176 | let settings = [ 177 | GolSettings::EditGrid.to_string(), 178 | GolSettings::Start.to_string(), 179 | ]; 180 | let longest_setting: u16 = settings 181 | .iter() 182 | .map(|k| k.to_string().len() as u16) 183 | .max() 184 | .unwrap_or(1); 185 | 186 | let edit_area = centered_rect_length( 187 | longest_setting + 43, 188 | GolSettings::COUNT as u16 * 2 + 5, 189 | frame.area(), 190 | ); 191 | 192 | let edit_block = Block::default() 193 | .title(" Editing Conway's Game of Life ".bold()) 194 | .title_alignment(Alignment::Center) 195 | .title_position(Position::Bottom); 196 | 197 | frame.render_widget(Clear, edit_area); 198 | 199 | ///////////////////////////// 200 | // Layouts 201 | ///////////////////////////// 202 | 203 | let [top, bottom] = 204 | Layout::vertical([Constraint::Length(2), Constraint::Min(0)]).areas(edit_area); 205 | 206 | let [left, right] = 207 | Layout::horizontal([Constraint::Length(longest_setting + 4), Constraint::Min(0)]) 208 | .areas(bottom); 209 | 210 | frame.render_widget(edit_block, top); 211 | 212 | ///////////////////////////// 213 | // Block styles 214 | ///////////////////////////// 215 | 216 | let selected_block = ratatui::widgets::Block::default() 217 | .borders(Borders::ALL) 218 | .border_type(BorderType::Thick) 219 | .border_style(Style::default().yellow().bold()) 220 | .padding(Padding::horizontal(1)) 221 | .style(Style::default().not_dim()); 222 | 223 | let disabled_block = ratatui::widgets::Block::default() 224 | .borders(Borders::ALL) 225 | .border_type(BorderType::Thick) 226 | .padding(Padding::horizontal(1)) 227 | .style(Style::default().dim()); 228 | 229 | ///////////////////////////// 230 | // Settings list 231 | ///////////////////////////// 232 | 233 | let block = match app.selected_edit_tab.as_ref().unwrap() { 234 | EditTab::Setting => selected_block.clone(), 235 | _ => disabled_block.clone(), 236 | }; 237 | 238 | frame.render_stateful_widget( 239 | GolSettingsList::build_list().block(block.clone()), 240 | left, 241 | &mut sim.settings_state, 242 | ); 243 | 244 | ///////////////////////////// 245 | // Content 246 | ///////////////////////////// 247 | 248 | let block = match app.selected_edit_tab.as_ref().unwrap() { 249 | EditTab::Content => selected_block.clone(), 250 | _ => disabled_block.clone(), 251 | }; 252 | 253 | frame.render_widget(block, right); 254 | 255 | let [right] = Layout::vertical([Constraint::Fill(1)]) 256 | .vertical_margin(1) 257 | .horizontal_margin(2) 258 | .areas(right); 259 | 260 | match GolSettings::from_index(sim.settings_state.selected.unwrap_or(0)) { 261 | GolSettings::EditGrid => { 262 | let info_text = "Edit the cells of the starting grid"; 263 | let info = Paragraph::new(info_text.dim()).wrap(Wrap { trim: true }); 264 | let [_, info_chunk, _] = 265 | Layout::vertical([Constraint::Min(0), Constraint::Min(1), Constraint::Min(0)]) 266 | .areas(right); 267 | frame.render_widget(info, info_chunk); 268 | } 269 | GolSettings::Start => { 270 | start_content(frame, right); 271 | } 272 | } 273 | 274 | ///////////////////////////// 275 | // Help screen 276 | ///////////////////////////// 277 | 278 | let help_entries: Vec<(Line, Line)> = match app.selected_edit_tab.as_ref().unwrap() { 279 | EditTab::Setting => settings_help(), 280 | EditTab::Content => { 281 | GolSettings::from_index(sim.settings_state.selected.unwrap_or(0)); 282 | vec![] 283 | } 284 | }; 285 | 286 | if app.help_screen { 287 | render_help(frame, help_entries); 288 | } 289 | } 290 | 291 | pub fn edit_gol(frame: &mut Frame, app: &mut App) { 292 | frame.render_widget(Clear, frame.area()); 293 | if frame 294 | .area() 295 | .width 296 | .checked_mul(frame.area().height) 297 | .is_none() 298 | { 299 | let error_paragraph = Paragraph::new( 300 | "EEEEEEEEEEEEEEEEEEEEEE tttt hhhhhhh hhhhhhh tttt iiii iiii tttt lllllll lllllll 301 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E ttt⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h ttt⣿⣿⣿t i⣿⣿⣿⣿i i⣿⣿⣿⣿i ttt⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 302 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h t⣿⣿⣿⣿⣿t iiii iiii t⣿⣿⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 303 | EE⣿⣿⣿⣿⣿⣿EEEEEEEEE⣿⣿⣿⣿E t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h t⣿⣿⣿⣿⣿t t⣿⣿⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 304 | E⣿⣿⣿⣿⣿E EEEEEErrrrr rrrrrrrrr rrrrr rrrrrrrrr ooooooooooo rrrrr rrrrrrrrr ttttttt⣿⣿⣿⣿⣿ttttttt h⣿⣿⣿⣿h hhhhh eeeeeeeeeeee cccccccccccccccch⣿⣿⣿⣿h hhhhh aaaaaaaaaaaaa rrrrr rrrrrrrrr aaaaaaaaaaaaa ccccccccccccccccttttttt⣿⣿⣿⣿⣿ttttttt eeeeeeeeeeee rrrrr rrrrrrrrr ssssssssss iiiiiii zzzzzzzzzzzzzzzzz eeeeeeeeeeee iiiiiii ssssssssss ttttttt⣿⣿⣿⣿⣿ttttttt ooooooooooo ooooooooooo ssssssssss mmmmmmm mmmmmmm aaaaaaaaaaaaa l⣿⣿⣿⣿l l⣿⣿⣿⣿l 305 | E⣿⣿⣿⣿⣿E r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t h⣿⣿⣿⣿hh⣿⣿⣿⣿⣿hhh ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿hh⣿⣿⣿⣿⣿hhh a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ct⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee i⣿⣿⣿⣿⣿i ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s mm⣿⣿⣿⣿⣿⣿⣿m m⣿⣿⣿⣿⣿⣿⣿mm a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 306 | E⣿⣿⣿⣿⣿⣿EEEEEEEEEE r⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿or⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿hh e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿ee c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿hh aaaaaaaaa⣿⣿⣿⣿⣿ar⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r aaaaaaaaa⣿⣿⣿⣿⣿a c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ct⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿eer⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿ee i⣿⣿⣿⣿i ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿mm⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿m aaaaaaaaa⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 307 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E rr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿ro⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿orr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ tttttt⣿⣿⣿⣿⣿⣿⣿tttttt h⣿⣿⣿⣿⣿⣿⣿hhh⣿⣿⣿⣿⣿⣿h e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿⣿⣿hhh⣿⣿⣿⣿⣿⣿h a⣿⣿⣿⣿arr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r a⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ctttttt⣿⣿⣿⣿⣿⣿⣿tttttt e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿err⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i zzzzzzzz⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿s tttttt⣿⣿⣿⣿⣿⣿⣿tttttt o⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿sm⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿m a⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 308 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿ro⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿c ccccccch⣿⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿⣿h aaaaaaa⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r aaaaaaa⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿c ccccccc t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿s ssssss i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿s ssssss t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿s ssssss m⣿⣿⣿⣿⣿mmm⣿⣿⣿⣿⣿⣿mmm⣿⣿⣿⣿⣿m aaaaaaa⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 309 | E⣿⣿⣿⣿⣿⣿EEEEEEEEEE r⣿⣿⣿⣿⣿r rrrrrrr r⣿⣿⣿⣿⣿r rrrrrrro⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r rrrrrrr t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿c h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h aa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r rrrrrrraa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r rrrrrrr s⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m aa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 310 | E⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿eeeeeeeeeee c⣿⣿⣿⣿⣿c h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿eeeeeeeeeee r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿eeeeeeeeeee i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 311 | E⣿⣿⣿⣿⣿E EEEEEE r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿t tttttth⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿c ccccccch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿a a⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿c ccccccc t⣿⣿⣿⣿⣿t tttttte⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r ssssss s⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i ssssss s⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t tttttto⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o ssssss s⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 312 | EE⣿⣿⣿⣿⣿⣿EEEEEEEE⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿th⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿a a⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿te⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿si⣿⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿zzzzzzzze⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿⣿⣿is⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿to⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿sm⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a l⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 313 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿th⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿c tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee i⣿⣿⣿⣿⣿⣿is⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿to⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 314 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo r⣿⣿⣿⣿⣿r tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tth⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿ar⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿a cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿c tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tt ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss i⣿⣿⣿⣿⣿⣿iz⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tt oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿al⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 315 | EEEEEEEEEEEEEEEEEEEEEE rrrrrrr rrrrrrr ooooooooooo rrrrrrr ttttttttttt hhhhhhh hhhhhhh eeeeeeeeeeeeee cccccccccccccccchhhhhhh hhhhhhh aaaaaaaaaa aaaarrrrrrr aaaaaaaaaa aaaa cccccccccccccccc ttttttttttt eeeeeeeeeeeeee rrrrrrr sssssssssss iiiiiiiizzzzzzzzzzzzzzzzz eeeeeeeeeeeeee iiiiiiii sssssssssss ttttttttttt ooooooooooo ooooooooooo sssssssssss mmmmmm mmmmmm mmmmmm aaaaaaaaaa aaaallllllllllllllll" 316 | ).alignment(Alignment::Center); 317 | frame.render_widget( 318 | error_paragraph, 319 | Rect { 320 | x: 0, 321 | y: 0, 322 | width: 592, 323 | height: 16, 324 | }, 325 | ); 326 | return; 327 | } 328 | 329 | let width = f64::from(frame.area().width - 2); 330 | let height = f64::from((frame.area().height - 2) * 2); 331 | 332 | // If the ant simulation is already set, the grid still needs to be initialized with the 333 | // screen size 334 | let sim = app.gol_sim.as_mut().unwrap(); 335 | 336 | // Initialize the grid with the same size as the canvas 337 | sim.grid 338 | .resize(width as usize, height as usize, sim.dead_state); 339 | 340 | // Set position to center if starting position is greater than the grid 341 | if sim.edit_cursor.x > width as usize { 342 | sim.edit_cursor.x = width as usize / 2; 343 | } 344 | 345 | if sim.edit_cursor.y > height as usize { 346 | sim.edit_cursor.y = height as usize / 2; 347 | } 348 | 349 | let sim = app.gol_sim.as_ref().unwrap(); 350 | 351 | ///////////////////////////// 352 | // Border content 353 | ///////////////////////////// 354 | 355 | let top_title = Line::from(" Editing Conway's Game of Life ".yellow()); 356 | 357 | let bottom_left_title = Line::from(vec![ 358 | " Generation: ".into(), 359 | sim.generation.to_string().yellow().bold(), 360 | " ".into(), 361 | ]); 362 | 363 | let help_label = Line::from(vec![" '?' ".yellow(), "Help ".into()]); 364 | 365 | let bottom_right_title = Line::from(vec![ 366 | " Position: ".into(), 367 | format!("(x: {}, y: {}) ", sim.edit_cursor.x, sim.edit_cursor.y).into(), 368 | ]); 369 | 370 | ///////////////////////////// 371 | // Simulation canvas 372 | ///////////////////////////// 373 | 374 | let canvas = Canvas::default() 375 | .block( 376 | Block::default() 377 | .border_type(BorderType::Double) 378 | .borders(Borders::ALL) 379 | .title_top(top_title.centered()) 380 | .title_bottom(bottom_left_title.left_aligned()) 381 | .title_bottom(bottom_right_title.right_aligned()) 382 | .title_bottom(help_label.centered()) 383 | .title_style(Style::default().bold()), 384 | ) 385 | .marker(sim.marker) 386 | .paint(|ctx| { 387 | // Draw grid 388 | for (y, row) in sim.grid.cells.iter().enumerate() { 389 | for (x, cell) in row.iter().enumerate() { 390 | ctx.draw(&Points { 391 | coords: &[(x as f64, y as f64)], 392 | color: *cell, 393 | }); 394 | } 395 | } 396 | 397 | // Draw cursor 398 | ctx.draw(&Points { 399 | coords: &[(sim.edit_cursor.x as f64, sim.edit_cursor.y as f64)], 400 | color: if sim.grid.cells[sim.edit_cursor.y][sim.edit_cursor.x] == sim.alive_state { 401 | Color::LightRed 402 | } else { 403 | sim.edit_cursor.color 404 | }, 405 | }); 406 | }) 407 | .x_bounds([0., f64::from((frame.area().width - 2) - 1)]) 408 | .y_bounds([0., f64::from(((frame.area().height - 2) * 2) - 1)]); 409 | 410 | frame.render_widget(canvas, frame.area()); 411 | 412 | ///////////////////////////// 413 | // Help screen 414 | ///////////////////////////// 415 | 416 | let help_entries: Vec<(Line, Line)> = vec![ 417 | (Line::from("?".yellow()), Line::from("Help")), 418 | ( 419 | Line::from("Q / Esc".yellow()), 420 | Line::from("Stop editing the grid"), 421 | ), 422 | (Line::from("Space".yellow()), Line::from("Toggle cell")), 423 | (Line::from("K / ↑".yellow()), Line::from("Move up")), 424 | (Line::from("J / ↓".yellow()), Line::from("Move down")), 425 | (Line::from("L / →".yellow()), Line::from("Move right")), 426 | (Line::from("H / ←".yellow()), Line::from("Move left")), 427 | (Line::from("Enter".yellow()), Line::from("Start simulation")), 428 | ]; 429 | 430 | if app.help_screen { 431 | render_help(frame, help_entries); 432 | } 433 | } 434 | 435 | pub struct GolSettingsList; 436 | impl GolSettingsList { 437 | pub fn build_list<'a>() -> ListView<'a, ListItemContainer<'a, Line<'a>>> { 438 | let builder = ListBuilder::new(move |context| { 439 | let config = GolSettings::from_index(context.index); 440 | let line = Line::from(format!("{config}")).centered(); 441 | let mut item = ListItemContainer::new(line, Padding::ZERO); 442 | 443 | if context.is_selected { 444 | item = item.yellow().bold(); 445 | }; 446 | 447 | (item, 2) 448 | }); 449 | 450 | return ListView::new(builder, GolSettings::COUNT); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/ui/main_ui.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Margin, Rect}, 3 | text::{Line, Text}, 4 | widgets::{Row, Scrollbar, ScrollbarOrientation, Table}, 5 | Frame, 6 | }; 7 | 8 | use ratatui::{ 9 | layout::{Alignment, Constraint, Direction, Layout}, 10 | style::{Style, Stylize}, 11 | widgets::{Block, BorderType, Borders, Paragraph}, 12 | }; 13 | use tui_big_text::{BigText, PixelSize}; 14 | 15 | use crate::app::{App, Screen}; 16 | 17 | use super::{ant_ui, elementary_ui, game_of_life_ui, render_help}; 18 | 19 | pub fn main_screen(frame: &mut Frame, app: &mut App) { 20 | if frame 21 | .area() 22 | .width 23 | .checked_mul(frame.area().height) 24 | .is_none() 25 | { 26 | let error_paragraph = Paragraph::new( 27 | "EEEEEEEEEEEEEEEEEEEEEE tttt hhhhhhh hhhhhhh tttt iiii iiii tttt lllllll lllllll 28 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E ttt⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h ttt⣿⣿⣿t i⣿⣿⣿⣿i i⣿⣿⣿⣿i ttt⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 29 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h t⣿⣿⣿⣿⣿t iiii iiii t⣿⣿⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 30 | EE⣿⣿⣿⣿⣿⣿EEEEEEEEE⣿⣿⣿⣿E t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h t⣿⣿⣿⣿⣿t t⣿⣿⣿⣿⣿t l⣿⣿⣿⣿⣿l l⣿⣿⣿⣿⣿l 31 | E⣿⣿⣿⣿⣿E EEEEEErrrrr rrrrrrrrr rrrrr rrrrrrrrr ooooooooooo rrrrr rrrrrrrrr ttttttt⣿⣿⣿⣿⣿ttttttt h⣿⣿⣿⣿h hhhhh eeeeeeeeeeee cccccccccccccccch⣿⣿⣿⣿h hhhhh aaaaaaaaaaaaa rrrrr rrrrrrrrr aaaaaaaaaaaaa ccccccccccccccccttttttt⣿⣿⣿⣿⣿ttttttt eeeeeeeeeeee rrrrr rrrrrrrrr ssssssssss iiiiiii zzzzzzzzzzzzzzzzz eeeeeeeeeeee iiiiiii ssssssssss ttttttt⣿⣿⣿⣿⣿ttttttt ooooooooooo ooooooooooo ssssssssss mmmmmmm mmmmmmm aaaaaaaaaaaaa l⣿⣿⣿⣿l l⣿⣿⣿⣿l 32 | E⣿⣿⣿⣿⣿E r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t h⣿⣿⣿⣿hh⣿⣿⣿⣿⣿hhh ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿hh⣿⣿⣿⣿⣿hhh a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ct⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee r⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿⣿⣿⣿r ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ee i⣿⣿⣿⣿⣿i ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s mm⣿⣿⣿⣿⣿⣿⣿m m⣿⣿⣿⣿⣿⣿⣿mm a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 33 | E⣿⣿⣿⣿⣿⣿EEEEEEEEEE r⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿or⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿hh e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿ee c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿hh aaaaaaaaa⣿⣿⣿⣿⣿ar⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r aaaaaaaaa⣿⣿⣿⣿⣿a c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ct⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿eer⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿r ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿ee i⣿⣿⣿⣿i ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o ss⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿mm⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿m aaaaaaaaa⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 34 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E rr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿rrr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿ro⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿orr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ tttttt⣿⣿⣿⣿⣿⣿⣿tttttt h⣿⣿⣿⣿⣿⣿⣿hhh⣿⣿⣿⣿⣿⣿h e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿⣿⣿hhh⣿⣿⣿⣿⣿⣿h a⣿⣿⣿⣿arr⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r a⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ctttttt⣿⣿⣿⣿⣿⣿⣿tttttt e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿err⣿⣿⣿⣿⣿⣿rrrrr⣿⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i zzzzzzzz⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿e e⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿s tttttt⣿⣿⣿⣿⣿⣿⣿tttttt o⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿sm⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿m a⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 35 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿ro⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿c ccccccch⣿⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿⣿h aaaaaaa⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r aaaaaaa⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿c ccccccc t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿s ssssss i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿eeeee⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿s ssssss t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿s ssssss m⣿⣿⣿⣿⣿mmm⣿⣿⣿⣿⣿⣿mmm⣿⣿⣿⣿⣿m aaaaaaa⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 36 | E⣿⣿⣿⣿⣿⣿EEEEEEEEEE r⣿⣿⣿⣿⣿r rrrrrrr r⣿⣿⣿⣿⣿r rrrrrrro⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r rrrrrrr t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿c h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h aa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r rrrrrrraa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r rrrrrrr s⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m aa⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 37 | E⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r t⣿⣿⣿⣿⣿t h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿eeeeeeeeeee c⣿⣿⣿⣿⣿c h⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿eeeeeeeeeee r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿eeeeeeeeeee i⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t o⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m a⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 38 | E⣿⣿⣿⣿⣿E EEEEEE r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿o o⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿t tttttth⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿c ccccccch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿a a⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿c ccccccc t⣿⣿⣿⣿⣿t tttttte⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r ssssss s⣿⣿⣿⣿⣿s i⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿i ssssss s⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿t tttttto⣿⣿⣿⣿o o⣿⣿⣿⣿oo⣿⣿⣿⣿o o⣿⣿⣿⣿o ssssss s⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a l⣿⣿⣿⣿l l⣿⣿⣿⣿l 39 | EE⣿⣿⣿⣿⣿⣿EEEEEEEE⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿th⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿he⣿⣿⣿⣿⣿⣿⣿⣿e c⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿a a⣿⣿⣿⣿⣿ac⣿⣿⣿⣿⣿⣿⣿cccccc⣿⣿⣿⣿⣿c t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿te⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿si⣿⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿zzzzzzzze⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿⣿⣿is⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿s t⣿⣿⣿⣿⣿⣿tttt⣿⣿⣿⣿⣿to⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿ooooo⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿ssss⣿⣿⣿⣿⣿⣿sm⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿a a⣿⣿⣿⣿⣿a l⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 40 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r o⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o r⣿⣿⣿⣿⣿r ⣿⣿⣿⣿⣿⣿ tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿th⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿ha⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a r⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a c⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿c tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿t e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s i⣿⣿⣿⣿⣿⣿i z⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e⣿⣿⣿⣿⣿⣿⣿⣿eeeeeeee i⣿⣿⣿⣿⣿⣿is⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿to⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿o s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿ma⣿⣿⣿⣿⣿aaaa⣿⣿⣿⣿⣿⣿a l⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 41 | E⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿E r⣿⣿⣿⣿⣿r r⣿⣿⣿⣿⣿r oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo r⣿⣿⣿⣿⣿r tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tth⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ch⣿⣿⣿⣿⣿h h⣿⣿⣿⣿⣿h a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿ar⣿⣿⣿⣿⣿r a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿a cc⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿c tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tt ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e r⣿⣿⣿⣿⣿r s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss i⣿⣿⣿⣿⣿⣿iz⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z ee⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿e i⣿⣿⣿⣿⣿⣿i s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss tt⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿tt oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo oo⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿oo s⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ss m⣿⣿⣿⣿m m⣿⣿⣿⣿m m⣿⣿⣿⣿m a⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿aa⣿⣿⣿al⣿⣿⣿⣿⣿⣿ll⣿⣿⣿⣿⣿⣿l 42 | EEEEEEEEEEEEEEEEEEEEEE rrrrrrr rrrrrrr ooooooooooo rrrrrrr ttttttttttt hhhhhhh hhhhhhh eeeeeeeeeeeeee cccccccccccccccchhhhhhh hhhhhhh aaaaaaaaaa aaaarrrrrrr aaaaaaaaaa aaaa cccccccccccccccc ttttttttttt eeeeeeeeeeeeee rrrrrrr sssssssssss iiiiiiiizzzzzzzzzzzzzzzzz eeeeeeeeeeeeee iiiiiiii sssssssssss ttttttttttt ooooooooooo ooooooooooo sssssssssss mmmmmm mmmmmm mmmmmm aaaaaaaaaa aaaallllllllllllllll" 43 | ).alignment(Alignment::Center); 44 | frame.render_widget( 45 | error_paragraph, 46 | Rect { 47 | x: 0, 48 | y: 0, 49 | width: 592, 50 | height: 16, 51 | }, 52 | ); 53 | return; 54 | } 55 | 56 | ///////////////////////////// 57 | // Border 58 | ///////////////////////////// 59 | 60 | let key_help = Line::from(vec![" '?' ".yellow(), "Help ".into()]); 61 | let border = Block::default() 62 | .title_bottom(key_help.centered()) 63 | .borders(Borders::ALL) 64 | .border_type(BorderType::Double); 65 | 66 | frame.render_widget(border, frame.area()); 67 | 68 | ///////////////////////////// 69 | // Layout 70 | ///////////////////////////// 71 | let vertical_layout = Layout::default() 72 | .direction(Direction::Vertical) 73 | .margin(3) 74 | .constraints([ 75 | Constraint::Min(0), // Spacing 76 | Constraint::Length(4), // Title 77 | Constraint::Length(1), // Subtitle 78 | Constraint::Max(3), // Spacing 79 | Constraint::Max(10), // List block 80 | Constraint::Min(0), // Spacing 81 | ]) 82 | .split(frame.area()); 83 | 84 | let longest_sim = app 85 | .list_items 86 | .iter() 87 | .map(|i| i.label.len()) 88 | .max() 89 | .unwrap_or(1); 90 | let longest_edit = 4; 91 | 92 | let horizontal_layout = Layout::default() 93 | .direction(Direction::Horizontal) 94 | .constraints([ 95 | Constraint::Min(1), 96 | Constraint::Length(longest_sim as u16 + longest_edit as u16 + 6), 97 | Constraint::Min(1), 98 | ]) 99 | .split(vertical_layout[4]); 100 | 101 | let list_layout = Layout::default() 102 | .direction(Direction::Vertical) 103 | .horizontal_margin(2) 104 | .vertical_margin(1) 105 | .constraints([Constraint::Min(1)]) 106 | .split(horizontal_layout[1]); 107 | 108 | ///////////////////////////// 109 | // Titles 110 | ///////////////////////////// 111 | let title = BigText::builder() 112 | .pixel_size(PixelSize::HalfHeight) 113 | .style(Style::new().yellow().bold()) 114 | .lines(vec!["TermCA".into()]) 115 | .alignment(Alignment::Center) 116 | .build(); 117 | 118 | let subtitle = Paragraph::new("Cellular Automata".to_string()) 119 | .alignment(Alignment::Center) 120 | .white() 121 | .bold(); 122 | 123 | frame.render_widget(title, vertical_layout[1]); 124 | frame.render_widget(subtitle, vertical_layout[2]); 125 | 126 | ///////////////////////////// 127 | // List 128 | ///////////////////////////// 129 | let list_border = Block::bordered() 130 | .border_type(BorderType::Rounded) 131 | .style(Style::new().yellow()); 132 | 133 | frame.render_widget(list_border, horizontal_layout[1]); 134 | 135 | let list_items: Vec = app 136 | .list_items 137 | .iter() 138 | .enumerate() 139 | .map(|(i, item)| { 140 | Row::new(vec![ 141 | Text::from(format!("{}\n", item.label.clone())), 142 | if i != app.list_items.len() - 1 { 143 | Text::from("Edit\n".to_string()) 144 | } else { 145 | Text::from("\n".to_string()) 146 | }, 147 | ]) 148 | .style(Style::default().white().dim()) 149 | .height(2) 150 | }) 151 | .collect(); 152 | 153 | // Highlight added to the selected row, but on the column that is not selected 154 | let selected_row_style = Style::default().white().bold().not_dim(); 155 | let selected_cell_style = Style::default().yellow().bold().not_dim(); 156 | 157 | let list = Table::new( 158 | list_items, 159 | [ 160 | Constraint::Length(longest_sim as u16 + 1), 161 | Constraint::Min(longest_edit as u16 + 1), 162 | ], 163 | ) 164 | .row_highlight_style(selected_row_style) 165 | .cell_highlight_style(selected_cell_style); 166 | 167 | frame.render_stateful_widget(list, list_layout[0], &mut app.list_state); 168 | 169 | let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight); 170 | let offsets_number: usize = ((app.list_items.len() * 2) as f32 171 | / (horizontal_layout[1].height.saturating_sub(2)) as f32) 172 | .ceil() as usize; 173 | 174 | app.scroll_state = app.scroll_state.content_length(offsets_number); 175 | 176 | app.scroll_state = app.scroll_state.position(app.list_state.offset()); 177 | 178 | if horizontal_layout[1].height.saturating_sub(2) < app.list_items.len() as u16 * 2 { 179 | frame.render_stateful_widget( 180 | scrollbar, 181 | horizontal_layout[1].inner(Margin { 182 | vertical: 1, 183 | horizontal: 0, 184 | }), 185 | &mut app.scroll_state, 186 | ); 187 | } 188 | 189 | ///////////////////////////// 190 | // Help screen 191 | ///////////////////////////// 192 | 193 | let help_entries: Vec<(Line, Line)> = vec![ 194 | (Line::from("?".yellow()), Line::from("Help")), 195 | (Line::from("Q / Esc".yellow()), Line::from("Quit")), 196 | (Line::from("Enter".yellow()), Line::from("Select")), 197 | (Line::from("K / ↑".yellow()), Line::from("Scroll Up")), 198 | (Line::from("J / ↓".yellow()), Line::from("Scroll Down")), 199 | (Line::from("L / →".yellow()), Line::from("Scroll Right")), 200 | (Line::from("H / ←".yellow()), Line::from("Scroll Left")), 201 | (Line::from("g".yellow()), Line::from("Scroll to Bottom")), 202 | (Line::from("G".yellow()), Line::from("Scroll to Top")), 203 | ]; 204 | 205 | if app.help_screen && app.editing.is_none() { 206 | render_help(frame, help_entries); 207 | } 208 | 209 | ///////////////////////////// 210 | // Edit screen 211 | ///////////////////////////// 212 | if let Some(edit_sim) = app.editing { 213 | match edit_sim { 214 | Screen::Ant => ant_ui::edit(frame, app), 215 | Screen::AntEdit(ant_idx) => ant_ui::edit_ant(frame, app, ant_idx), 216 | Screen::Elementary => elementary_ui::edit(frame, app), 217 | Screen::GameOfLife => game_of_life_ui::edit(frame, app), 218 | Screen::GolEdit => game_of_life_ui::edit_gol(frame, app), 219 | _ => {} 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ant_ui; 2 | pub mod elementary_ui; 3 | pub mod game_of_life_ui; 4 | pub mod main_ui; 5 | 6 | use ratatui::{ 7 | buffer::Buffer, 8 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 9 | style::{Style, Styled, Stylize}, 10 | text::Line, 11 | widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, Widget}, 12 | Frame, 13 | }; 14 | 15 | /// helper function to create a centered rect using up certain percentage of the available rect `r` 16 | pub fn _centered_rect_percent(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 17 | // Cut the given rectangle into three vertical pieces 18 | let popup_layout = Layout::default() 19 | .direction(Direction::Vertical) 20 | .constraints([ 21 | Constraint::Percentage((100 - percent_y) / 2), 22 | Constraint::Percentage(percent_y), 23 | Constraint::Percentage((100 - percent_y) / 2), 24 | ]) 25 | .split(r); 26 | 27 | // Then cut the middle vertical piece into three width-wise pieces 28 | Layout::default() 29 | .direction(Direction::Horizontal) 30 | .constraints([ 31 | Constraint::Percentage((100 - percent_x) / 2), 32 | Constraint::Percentage(percent_x), 33 | Constraint::Percentage((100 - percent_x) / 2), 34 | ]) 35 | .split(popup_layout[1])[1] // Return the middle chunk 36 | } 37 | 38 | pub fn centered_rect_length(cols: u16, rows: u16, r: Rect) -> Rect { 39 | // Cut the given rectangle into three vertical pieces 40 | let popup_layout = Layout::default() 41 | .direction(Direction::Vertical) 42 | .constraints([ 43 | { 44 | if r.height > rows { 45 | Constraint::Length((r.height - rows) / 2) 46 | } else { 47 | Constraint::Min(1) 48 | } 49 | }, 50 | Constraint::Length(rows), 51 | { 52 | if r.height > rows { 53 | Constraint::Length((r.height - rows) / 2) 54 | } else { 55 | Constraint::Min(1) 56 | } 57 | }, 58 | ]) 59 | .split(r); 60 | 61 | // Then cut the middle vertical piece into three width-wise pieces 62 | Layout::default() 63 | .direction(Direction::Horizontal) 64 | .constraints([ 65 | Constraint::Length((r.width - cols) / 2), 66 | Constraint::Length(cols), 67 | Constraint::Length((r.width - cols) / 2), 68 | ]) 69 | .split(popup_layout[1])[1] // Return the middle chunk 70 | } 71 | 72 | pub fn render_help(frame: &mut Frame, entries: Vec<(Line, Line)>) { 73 | let (keys, labels): (Vec, Vec) = entries.into_iter().unzip(); 74 | 75 | let longest_key = keys.iter().map(|k| k.to_string().len()).max().unwrap_or(1); 76 | 77 | let longest_label = labels 78 | .iter() 79 | .map(|l| l.to_string().len()) 80 | .max() 81 | .unwrap_or(1); 82 | 83 | let help_area = centered_rect_length( 84 | (longest_key + longest_label + 7) as u16, 85 | (keys.len() + 4) as u16, 86 | frame.area(), 87 | ); 88 | let help_block = Block::default() 89 | .title(" Help ".yellow().bold()) 90 | .title_alignment(Alignment::Center) 91 | .borders(Borders::ALL) 92 | .border_type(BorderType::Rounded) 93 | .style(Style::default()); 94 | 95 | let help_layout = Layout::default() 96 | .direction(Direction::Vertical) 97 | .margin(2) 98 | .constraints([ 99 | Constraint::Length(2), 100 | Constraint::Min(keys.len() as u16), 101 | Constraint::Length(2), 102 | ]) 103 | .split(help_area); 104 | 105 | let help_center = Layout::default() 106 | .direction(Direction::Horizontal) 107 | .constraints([ 108 | Constraint::Length(1), 109 | Constraint::Max(longest_key as u16), 110 | Constraint::Length(1), 111 | Constraint::Max(longest_label as u16), 112 | Constraint::Length(1), 113 | ]) 114 | .split(help_layout[1]); 115 | 116 | let help_keys = Paragraph::new(keys).alignment(Alignment::Right); 117 | let help_labels = Paragraph::new(labels).alignment(Alignment::Left); 118 | 119 | frame.render_widget(Clear, help_area); 120 | frame.render_widget(help_block, help_area); 121 | frame.render_widget(help_keys, help_center[1]); 122 | frame.render_widget(help_labels, help_center[3]); 123 | } 124 | 125 | pub struct ListItemContainer<'a, W> { 126 | child: W, 127 | block: ratatui::widgets::Block<'a>, 128 | style: Style, 129 | } 130 | 131 | impl<'a, W> ListItemContainer<'a, W> { 132 | pub fn new(child: W, padding: Padding) -> Self { 133 | Self { 134 | child, 135 | block: ratatui::widgets::Block::default().padding(padding), 136 | style: Style::default(), 137 | } 138 | } 139 | } 140 | 141 | impl Styled for ListItemContainer<'_, T> { 142 | type Item = Self; 143 | 144 | fn style(&self) -> Style { 145 | self.style 146 | } 147 | 148 | fn set_style>(mut self, style: S) -> Self::Item { 149 | self.style = style.into(); 150 | self 151 | } 152 | } 153 | 154 | impl Widget for ListItemContainer<'_, W> { 155 | fn render(self, area: Rect, buf: &mut Buffer) { 156 | let inner_area = self.block.inner(area); 157 | buf.set_style(area, self.style); 158 | self.block.render(area, buf); 159 | self.child.render(inner_area, buf); 160 | } 161 | } 162 | 163 | pub fn settings_help<'a>() -> Vec<(Line<'a>, Line<'a>)> { 164 | vec![ 165 | (Line::from("?".yellow()), Line::from("Help")), 166 | (Line::from("Q / Esc".yellow()), Line::from("Quit edit mode")), 167 | ( 168 | Line::from("Enter / L / →".yellow()), 169 | Line::from("Select setting"), 170 | ), 171 | (Line::from("K / ↑".yellow()), Line::from("Previous setting")), 172 | (Line::from("J / ↓".yellow()), Line::from("Next setting")), 173 | (Line::from("Space".yellow()), Line::from("Start simulation")), 174 | ] 175 | } 176 | 177 | fn start_content(frame: &mut Frame<'_>, buf: Rect) { 178 | let info = Paragraph::new("Start the simulation").centered(); 179 | 180 | let [_, middle, _] = Layout::vertical([ 181 | Constraint::Min(1), 182 | Constraint::Length(1), 183 | Constraint::Min(1), 184 | ]) 185 | .areas(buf); 186 | frame.render_widget(info, middle); 187 | } 188 | --------------------------------------------------------------------------------