├── .github └── workflows │ └── mdbook.yml ├── .gitignore ├── README.md ├── book.toml ├── examples └── 02-Todo │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ ├── app.rs │ ├── background.rs │ ├── main.rs │ ├── utils.rs │ └── widgets.rs └── src ├── 1x00-intro.md ├── 1x01-about-gtk.md ├── 1x02-getting-started.md ├── 1x03-glib-runtime.md ├── 1x04-event-driven.md ├── 1x05-window.md ├── 1x06-gtk-widgets.md ├── 2x00-intro.md ├── 2x01-gtk-application.md ├── 2x02-events.md ├── 2x03-background-events.md ├── 2x04-task-widget.md ├── 2x05-app.md ├── 2x06-tasks.md ├── 2x07-save.md ├── 2x08-load.md ├── 2x09-check-buttons.md ├── 2x10-multi-list.md └── SUMMARY.md /.github/workflows/mdbook.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup mdBook 15 | uses: peaceiris/actions-mdbook@v1 16 | with: 17 | mdbook-version: 'latest' 18 | 19 | - run: mdbook build 20 | 21 | - name: Deploy 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | publish_dir: ./book 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Event-Driven GTK by Example — 2021 Edition 2 | 3 | > This is currently a work in progress 4 | 5 | Several years ago, before I joined System76, I wrote a tutorial for GTK-rs that was based on my early experiences as I was learning Rust and GTK at the same time. Eventually, I took the tutorial down because it was severely out of date; and no longer representative of best practices, current APIs, or even the state of Rust itself. 6 | 7 | To this day, I'm still receiving emails from people who have found references to my tutorial and are wondering if I'm ever going to bring it back. So, given that I have gained additional years of experience with Rust and GTK at System76, and there is interest in a renewal of this tutorial, I am rebooting the project. If it can help at least one person establish a career in writing GTK applications, then it's worth the effort. 8 | 9 | However, since I have a full time job, this will only be worked on in my free time. The editing may not be as extensive as my former tutorial, so I'll keep the explanations concise, and pack it with real code that you can work alongside. 10 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Michael Aaron Murphy"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Event-Driven GTK by Example — 2021 Edition" 7 | -------------------------------------------------------------------------------- /examples/02-Todo/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/02-Todo/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.40" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" 10 | 11 | [[package]] 12 | name = "async-channel" 13 | version = "1.6.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" 16 | dependencies = [ 17 | "concurrent-queue", 18 | "event-listener", 19 | "futures-core", 20 | ] 21 | 22 | [[package]] 23 | name = "atk" 24 | version = "0.9.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "812b4911e210bd51b24596244523c856ca749e6223c50a7fbbba3f89ee37c426" 27 | dependencies = [ 28 | "atk-sys", 29 | "bitflags", 30 | "glib", 31 | "glib-sys", 32 | "gobject-sys", 33 | "libc", 34 | ] 35 | 36 | [[package]] 37 | name = "atk-sys" 38 | version = "0.10.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "f530e4af131d94cc4fa15c5c9d0348f0ef28bac64ba660b6b2a1cf2605dedfce" 41 | dependencies = [ 42 | "glib-sys", 43 | "gobject-sys", 44 | "libc", 45 | "system-deps", 46 | ] 47 | 48 | [[package]] 49 | name = "autocfg" 50 | version = "1.0.1" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 53 | 54 | [[package]] 55 | name = "bitflags" 56 | version = "1.2.1" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 59 | 60 | [[package]] 61 | name = "cache-padded" 62 | version = "1.1.1" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" 65 | 66 | [[package]] 67 | name = "cairo-rs" 68 | version = "0.9.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "c5c0f2e047e8ca53d0ff249c54ae047931d7a6ebe05d00af73e0ffeb6e34bdb8" 71 | dependencies = [ 72 | "bitflags", 73 | "cairo-sys-rs", 74 | "glib", 75 | "glib-sys", 76 | "gobject-sys", 77 | "libc", 78 | "thiserror", 79 | ] 80 | 81 | [[package]] 82 | name = "cairo-sys-rs" 83 | version = "0.10.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "2ed2639b9ad5f1d6efa76de95558e11339e7318426d84ac4890b86c03e828ca7" 86 | dependencies = [ 87 | "glib-sys", 88 | "libc", 89 | "system-deps", 90 | ] 91 | 92 | [[package]] 93 | name = "cascade" 94 | version = "1.0.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "f18c6a921baae2d947e4cf96f6ef1b5774b3056ae8edbdf5c5cfce4f33260921" 97 | 98 | [[package]] 99 | name = "cc" 100 | version = "1.0.68" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" 103 | 104 | [[package]] 105 | name = "concurrent-queue" 106 | version = "1.2.2" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" 109 | dependencies = [ 110 | "cache-padded", 111 | ] 112 | 113 | [[package]] 114 | name = "either" 115 | version = "1.6.1" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 118 | 119 | [[package]] 120 | name = "event-listener" 121 | version = "2.5.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" 124 | 125 | [[package]] 126 | name = "fomat-macros" 127 | version = "0.3.1" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "fe56556a8c9f9f556150eb6b390bc1a8b3715fd2ddbb4585f36b6a5672c6a833" 130 | 131 | [[package]] 132 | name = "futures" 133 | version = "0.3.15" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" 136 | dependencies = [ 137 | "futures-channel", 138 | "futures-core", 139 | "futures-executor", 140 | "futures-io", 141 | "futures-sink", 142 | "futures-task", 143 | "futures-util", 144 | ] 145 | 146 | [[package]] 147 | name = "futures-channel" 148 | version = "0.3.15" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" 151 | dependencies = [ 152 | "futures-core", 153 | "futures-sink", 154 | ] 155 | 156 | [[package]] 157 | name = "futures-core" 158 | version = "0.3.15" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" 161 | 162 | [[package]] 163 | name = "futures-executor" 164 | version = "0.3.15" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" 167 | dependencies = [ 168 | "futures-core", 169 | "futures-task", 170 | "futures-util", 171 | ] 172 | 173 | [[package]] 174 | name = "futures-io" 175 | version = "0.3.15" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" 178 | 179 | [[package]] 180 | name = "futures-macro" 181 | version = "0.3.15" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" 184 | dependencies = [ 185 | "autocfg", 186 | "proc-macro-hack", 187 | "proc-macro2", 188 | "quote", 189 | "syn", 190 | ] 191 | 192 | [[package]] 193 | name = "futures-sink" 194 | version = "0.3.15" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" 197 | 198 | [[package]] 199 | name = "futures-task" 200 | version = "0.3.15" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" 203 | 204 | [[package]] 205 | name = "futures-util" 206 | version = "0.3.15" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" 209 | dependencies = [ 210 | "autocfg", 211 | "futures-channel", 212 | "futures-core", 213 | "futures-io", 214 | "futures-macro", 215 | "futures-sink", 216 | "futures-task", 217 | "memchr", 218 | "pin-project-lite", 219 | "pin-utils", 220 | "proc-macro-hack", 221 | "proc-macro-nested", 222 | "slab", 223 | ] 224 | 225 | [[package]] 226 | name = "gdk" 227 | version = "0.13.2" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "db00839b2a68a7a10af3fa28dfb3febaba3a20c3a9ac2425a33b7df1f84a6b7d" 230 | dependencies = [ 231 | "bitflags", 232 | "cairo-rs", 233 | "cairo-sys-rs", 234 | "gdk-pixbuf", 235 | "gdk-sys", 236 | "gio", 237 | "gio-sys", 238 | "glib", 239 | "glib-sys", 240 | "gobject-sys", 241 | "libc", 242 | "pango", 243 | ] 244 | 245 | [[package]] 246 | name = "gdk-pixbuf" 247 | version = "0.9.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "8f6dae3cb99dd49b758b88f0132f8d401108e63ae8edd45f432d42cdff99998a" 250 | dependencies = [ 251 | "gdk-pixbuf-sys", 252 | "gio", 253 | "gio-sys", 254 | "glib", 255 | "glib-sys", 256 | "gobject-sys", 257 | "libc", 258 | ] 259 | 260 | [[package]] 261 | name = "gdk-pixbuf-sys" 262 | version = "0.10.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "3bfe468a7f43e97b8d193a762b6c5cf67a7d36cacbc0b9291dbcae24bfea1e8f" 265 | dependencies = [ 266 | "gio-sys", 267 | "glib-sys", 268 | "gobject-sys", 269 | "libc", 270 | "system-deps", 271 | ] 272 | 273 | [[package]] 274 | name = "gdk-sys" 275 | version = "0.10.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "0a9653cfc500fd268015b1ac055ddbc3df7a5c9ea3f4ccef147b3957bd140d69" 278 | dependencies = [ 279 | "cairo-sys-rs", 280 | "gdk-pixbuf-sys", 281 | "gio-sys", 282 | "glib-sys", 283 | "gobject-sys", 284 | "libc", 285 | "pango-sys", 286 | "pkg-config", 287 | "system-deps", 288 | ] 289 | 290 | [[package]] 291 | name = "gio" 292 | version = "0.9.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "1fb60242bfff700772dae5d9e3a1f7aa2e4ebccf18b89662a16acb2822568561" 295 | dependencies = [ 296 | "bitflags", 297 | "futures", 298 | "futures-channel", 299 | "futures-core", 300 | "futures-io", 301 | "futures-util", 302 | "gio-sys", 303 | "glib", 304 | "glib-sys", 305 | "gobject-sys", 306 | "libc", 307 | "once_cell", 308 | "thiserror", 309 | ] 310 | 311 | [[package]] 312 | name = "gio-sys" 313 | version = "0.10.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "5e24fb752f8f5d2cf6bbc2c606fd2bc989c81c5e2fe321ab974d54f8b6344eac" 316 | dependencies = [ 317 | "glib-sys", 318 | "gobject-sys", 319 | "libc", 320 | "system-deps", 321 | "winapi", 322 | ] 323 | 324 | [[package]] 325 | name = "glib" 326 | version = "0.10.3" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" 329 | dependencies = [ 330 | "bitflags", 331 | "futures-channel", 332 | "futures-core", 333 | "futures-executor", 334 | "futures-task", 335 | "futures-util", 336 | "glib-macros", 337 | "glib-sys", 338 | "gobject-sys", 339 | "libc", 340 | "once_cell", 341 | ] 342 | 343 | [[package]] 344 | name = "glib-macros" 345 | version = "0.10.1" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" 348 | dependencies = [ 349 | "anyhow", 350 | "heck", 351 | "itertools", 352 | "proc-macro-crate", 353 | "proc-macro-error", 354 | "proc-macro2", 355 | "quote", 356 | "syn", 357 | ] 358 | 359 | [[package]] 360 | name = "glib-sys" 361 | version = "0.10.1" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" 364 | dependencies = [ 365 | "libc", 366 | "system-deps", 367 | ] 368 | 369 | [[package]] 370 | name = "gobject-sys" 371 | version = "0.10.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" 374 | dependencies = [ 375 | "glib-sys", 376 | "libc", 377 | "system-deps", 378 | ] 379 | 380 | [[package]] 381 | name = "gtk" 382 | version = "0.9.2" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "2f022f2054072b3af07666341984562c8e626a79daa8be27b955d12d06a5ad6a" 385 | dependencies = [ 386 | "atk", 387 | "bitflags", 388 | "cairo-rs", 389 | "cairo-sys-rs", 390 | "cc", 391 | "gdk", 392 | "gdk-pixbuf", 393 | "gdk-pixbuf-sys", 394 | "gdk-sys", 395 | "gio", 396 | "gio-sys", 397 | "glib", 398 | "glib-sys", 399 | "gobject-sys", 400 | "gtk-sys", 401 | "libc", 402 | "once_cell", 403 | "pango", 404 | "pango-sys", 405 | "pkg-config", 406 | ] 407 | 408 | [[package]] 409 | name = "gtk-sys" 410 | version = "0.10.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "89acda6f084863307d948ba64a4b1ef674e8527dddab147ee4cdcc194c880457" 413 | dependencies = [ 414 | "atk-sys", 415 | "cairo-sys-rs", 416 | "gdk-pixbuf-sys", 417 | "gdk-sys", 418 | "gio-sys", 419 | "glib-sys", 420 | "gobject-sys", 421 | "libc", 422 | "pango-sys", 423 | "system-deps", 424 | ] 425 | 426 | [[package]] 427 | name = "gtk-todo" 428 | version = "0.1.0" 429 | dependencies = [ 430 | "async-channel", 431 | "cascade", 432 | "fomat-macros", 433 | "gio", 434 | "glib", 435 | "gtk", 436 | "slotmap", 437 | "xdg", 438 | ] 439 | 440 | [[package]] 441 | name = "heck" 442 | version = "0.3.2" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" 445 | dependencies = [ 446 | "unicode-segmentation", 447 | ] 448 | 449 | [[package]] 450 | name = "itertools" 451 | version = "0.9.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" 454 | dependencies = [ 455 | "either", 456 | ] 457 | 458 | [[package]] 459 | name = "libc" 460 | version = "0.2.95" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" 463 | 464 | [[package]] 465 | name = "memchr" 466 | version = "2.4.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" 469 | 470 | [[package]] 471 | name = "once_cell" 472 | version = "1.7.2" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 475 | 476 | [[package]] 477 | name = "pango" 478 | version = "0.9.1" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "9937068580bebd8ced19975938573803273ccbcbd598c58d4906efd4ac87c438" 481 | dependencies = [ 482 | "bitflags", 483 | "glib", 484 | "glib-sys", 485 | "gobject-sys", 486 | "libc", 487 | "once_cell", 488 | "pango-sys", 489 | ] 490 | 491 | [[package]] 492 | name = "pango-sys" 493 | version = "0.10.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "24d2650c8b62d116c020abd0cea26a4ed96526afda89b1c4ea567131fdefc890" 496 | dependencies = [ 497 | "glib-sys", 498 | "gobject-sys", 499 | "libc", 500 | "system-deps", 501 | ] 502 | 503 | [[package]] 504 | name = "pin-project-lite" 505 | version = "0.2.6" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" 508 | 509 | [[package]] 510 | name = "pin-utils" 511 | version = "0.1.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 514 | 515 | [[package]] 516 | name = "pkg-config" 517 | version = "0.3.19" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 520 | 521 | [[package]] 522 | name = "proc-macro-crate" 523 | version = "0.1.5" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" 526 | dependencies = [ 527 | "toml", 528 | ] 529 | 530 | [[package]] 531 | name = "proc-macro-error" 532 | version = "1.0.4" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 535 | dependencies = [ 536 | "proc-macro-error-attr", 537 | "proc-macro2", 538 | "quote", 539 | "syn", 540 | "version_check", 541 | ] 542 | 543 | [[package]] 544 | name = "proc-macro-error-attr" 545 | version = "1.0.4" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 548 | dependencies = [ 549 | "proc-macro2", 550 | "quote", 551 | "version_check", 552 | ] 553 | 554 | [[package]] 555 | name = "proc-macro-hack" 556 | version = "0.5.19" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 559 | 560 | [[package]] 561 | name = "proc-macro-nested" 562 | version = "0.1.7" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" 565 | 566 | [[package]] 567 | name = "proc-macro2" 568 | version = "1.0.27" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 571 | dependencies = [ 572 | "unicode-xid", 573 | ] 574 | 575 | [[package]] 576 | name = "quote" 577 | version = "1.0.9" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 580 | dependencies = [ 581 | "proc-macro2", 582 | ] 583 | 584 | [[package]] 585 | name = "serde" 586 | version = "1.0.126" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" 589 | 590 | [[package]] 591 | name = "slab" 592 | version = "0.4.3" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" 595 | 596 | [[package]] 597 | name = "slotmap" 598 | version = "1.0.3" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "585cd5dffe4e9e06f6dfdf66708b70aca3f781bed561f4f667b2d9c0d4559e36" 601 | dependencies = [ 602 | "version_check", 603 | ] 604 | 605 | [[package]] 606 | name = "strum" 607 | version = "0.18.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" 610 | 611 | [[package]] 612 | name = "strum_macros" 613 | version = "0.18.0" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" 616 | dependencies = [ 617 | "heck", 618 | "proc-macro2", 619 | "quote", 620 | "syn", 621 | ] 622 | 623 | [[package]] 624 | name = "syn" 625 | version = "1.0.72" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" 628 | dependencies = [ 629 | "proc-macro2", 630 | "quote", 631 | "unicode-xid", 632 | ] 633 | 634 | [[package]] 635 | name = "system-deps" 636 | version = "1.3.2" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" 639 | dependencies = [ 640 | "heck", 641 | "pkg-config", 642 | "strum", 643 | "strum_macros", 644 | "thiserror", 645 | "toml", 646 | "version-compare", 647 | ] 648 | 649 | [[package]] 650 | name = "thiserror" 651 | version = "1.0.25" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" 654 | dependencies = [ 655 | "thiserror-impl", 656 | ] 657 | 658 | [[package]] 659 | name = "thiserror-impl" 660 | version = "1.0.25" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" 663 | dependencies = [ 664 | "proc-macro2", 665 | "quote", 666 | "syn", 667 | ] 668 | 669 | [[package]] 670 | name = "toml" 671 | version = "0.5.8" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 674 | dependencies = [ 675 | "serde", 676 | ] 677 | 678 | [[package]] 679 | name = "unicode-segmentation" 680 | version = "1.7.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 683 | 684 | [[package]] 685 | name = "unicode-xid" 686 | version = "0.2.2" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 689 | 690 | [[package]] 691 | name = "version-compare" 692 | version = "0.0.10" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" 695 | 696 | [[package]] 697 | name = "version_check" 698 | version = "0.9.3" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 701 | 702 | [[package]] 703 | name = "winapi" 704 | version = "0.3.9" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 707 | dependencies = [ 708 | "winapi-i686-pc-windows-gnu", 709 | "winapi-x86_64-pc-windows-gnu", 710 | ] 711 | 712 | [[package]] 713 | name = "winapi-i686-pc-windows-gnu" 714 | version = "0.4.0" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 717 | 718 | [[package]] 719 | name = "winapi-x86_64-pc-windows-gnu" 720 | version = "0.4.0" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 723 | 724 | [[package]] 725 | name = "xdg" 726 | version = "2.2.0" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" 729 | -------------------------------------------------------------------------------- /examples/02-Todo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gtk-todo" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | async-channel = "1.6" 8 | cascade = "1.0" 9 | fomat-macros = "0.3" 10 | gio = "0.9" 11 | glib = "0.10" 12 | gtk = { version = "0.9", features = ["v3_22"] } 13 | slotmap = "1.0" 14 | xdg = "2.2" 15 | -------------------------------------------------------------------------------- /examples/02-Todo/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::{Event, BgEvent, TaskEntity}; 2 | use crate::widgets::Task; 3 | use crate::utils::spawn; 4 | 5 | use async_channel::Sender; 6 | use glib::clone; 7 | use glib::SourceId; 8 | use gtk::prelude::*; 9 | use slotmap::SlotMap; 10 | use std::path::PathBuf; 11 | 12 | pub struct App { 13 | pub container: gtk::Grid, 14 | pub delete_button: gtk::Button, 15 | pub headerbar: gtk::HeaderBar, 16 | pub tasks: SlotMap, 17 | pub scheduled_write: Option, 18 | pub tx: Sender, 19 | pub btx: Sender, 20 | pub checks_active: u32, 21 | pub last_saved: PathBuf, 22 | } 23 | 24 | impl App { 25 | pub fn new( 26 | app: >k::Application, 27 | tx: Sender, 28 | btx: Sender, 29 | ) -> Self { 30 | let container = cascade! { 31 | gtk::Grid::new(); 32 | ..set_column_spacing(4); 33 | ..set_row_spacing(4); 34 | ..set_border_width(4); 35 | ..show(); 36 | }; 37 | 38 | let scrolled = gtk::ScrolledWindowBuilder::new() 39 | .hscrollbar_policy(gtk::PolicyType::Never) 40 | .build(); 41 | 42 | scrolled.add(&container); 43 | 44 | let delete_button = cascade! { 45 | gtk::Button::from_icon_name(Some("edit-delete-symbolic"), gtk::IconSize::Button); 46 | ..set_label("Delete"); 47 | // Show the icon alongside the label 48 | ..set_always_show_image(true); 49 | // Don't show this when the window calls `.show_all()` 50 | ..set_no_show_all(true); 51 | // Give this a destructive styling to signal that the action is destructive 52 | ..get_style_context().add_class(>k::STYLE_CLASS_DESTRUCTIVE_ACTION); 53 | // Send the `Delete` event on click 54 | ..connect_clicked(clone!(@strong tx => move |_| { 55 | let tx = tx.clone(); 56 | spawn(async move { 57 | let _ = tx.send(Event::Delete).await; 58 | }); 59 | })); 60 | }; 61 | 62 | let data_dir = xdg::BaseDirectories::with_prefix(crate::APP_ID) 63 | .unwrap() 64 | .get_data_home(); 65 | 66 | let last_saved = data_dir.join("Default"); 67 | 68 | let open_button = cascade! { 69 | gtk::Button::from_icon_name(Some(""), gtk::IconSize::Button); 70 | ..set_label("Open"); 71 | ..set_always_show_image(true); 72 | ..connect_clicked(clone!(@strong btx => move |_| { 73 | let dialog = gtk::FileChooserNative::new( 74 | Some("Choose a Note"), 75 | Some(>k::Window::new(gtk::WindowType::Popup)), 76 | gtk::FileChooserAction::Save, 77 | Some("_Open"), 78 | Some("_Cancel") 79 | ); 80 | 81 | dialog.set_current_folder(&data_dir); 82 | 83 | if let gtk::ResponseType::Accept = dialog.run() { 84 | if let Some(path) = dialog.get_filename() { 85 | let btx = btx.clone(); 86 | spawn(async move { 87 | let _ = btx.send(BgEvent::Load(path)).await; 88 | }); 89 | } 90 | } 91 | 92 | dialog.destroy(); 93 | })); 94 | }; 95 | 96 | let headerbar = cascade! { 97 | gtk::HeaderBar::new(); 98 | ..pack_end(&open_button); 99 | ..pack_end(&delete_button); 100 | ..set_title(Some("ToDo")); 101 | ..set_subtitle(Some("Default")); 102 | ..set_show_close_button(true); 103 | }; 104 | 105 | let _window = cascade! { 106 | gtk::ApplicationWindow::new(app); 107 | ..set_titlebar(Some(&headerbar)); 108 | ..set_default_size(400, 600); 109 | ..add(&scrolled); 110 | ..connect_delete_event(clone!(@strong tx, @strong scrolled => move |win, _| { 111 | // Detach to preserve widgets after destruction of window 112 | win.remove(&scrolled); 113 | 114 | let tx = tx.clone(); 115 | spawn(async move { 116 | let _ = tx.send(Event::Closed).await; 117 | }); 118 | gtk::Inhibit(false) 119 | })); 120 | ..show_all(); 121 | }; 122 | 123 | gtk::Window::set_default_icon_name("icon-name-here"); 124 | 125 | let mut app = Self { 126 | container, 127 | delete_button, 128 | tasks: SlotMap::with_key(), 129 | scheduled_write: None, 130 | tx, 131 | btx, 132 | checks_active: 0, 133 | last_saved, 134 | headerbar 135 | }; 136 | 137 | app.insert_row(0); 138 | 139 | app 140 | } 141 | 142 | pub fn clear(&mut self) { 143 | while let Some(entity) = self.tasks.keys().next() { 144 | self.remove_(entity); 145 | } 146 | } 147 | 148 | pub fn delete(&mut self) { 149 | let remove_list = self.tasks.iter() 150 | .filter(|(_, task)| task.check.get_active()) 151 | .map(|(id, _)| id) 152 | .collect::>(); 153 | 154 | for id in remove_list { 155 | self.remove(id); 156 | } 157 | 158 | self.checks_active = 0; 159 | self.delete_button.set_visible(false); 160 | } 161 | 162 | pub fn toggled(&mut self, active: bool) { 163 | if active { 164 | self.checks_active += 1; 165 | } else { 166 | self.checks_active -= 1; 167 | } 168 | 169 | self.delete_button.set_visible(self.checks_active != 0); 170 | } 171 | 172 | pub fn insert(&mut self, entity: TaskEntity) { 173 | let mut insert_at = 0; 174 | 175 | if let Some(task) = self.tasks.get(entity) { 176 | insert_at = task.row + 1; 177 | } 178 | 179 | self.insert_row(insert_at); 180 | } 181 | 182 | fn insert_row(&mut self, row: i32) -> TaskEntity { 183 | // Increment the row value of each Task is below the new row 184 | for task in self.tasks.values_mut() { 185 | if task.row >= row { 186 | task.row += 1; 187 | } 188 | } 189 | 190 | self.container.insert_row(row); 191 | let task = Task::new(row); 192 | 193 | self.container.attach(&task.check, 0, row, 1, 1); 194 | self.container.attach(&task.entry, 1, row, 1, 1); 195 | self.container.attach(&task.insert, 2, row, 1, 1); 196 | 197 | task.entry.grab_focus(); 198 | 199 | let entity = self.tasks.insert(task); 200 | self.tasks[entity].connect(self.tx.clone(), entity); 201 | return entity; 202 | } 203 | 204 | pub fn load(&mut self, path: PathBuf, data: String) { 205 | self.clear(); 206 | 207 | for (row, line) in data.lines().enumerate() { 208 | let entity = self.insert_row(row as i32); 209 | self.tasks[entity].set_text(line); 210 | } 211 | 212 | use std::ffi::OsStr; 213 | self.headerbar.set_subtitle(path.file_name().and_then(OsStr::to_str)); 214 | self.last_saved = path; 215 | } 216 | 217 | pub fn modified(&mut self) { 218 | if let Some(id) = self.scheduled_write.take() { 219 | glib::source_remove(id); 220 | } 221 | 222 | let tx = self.tx.clone(); 223 | self.scheduled_write = Some(glib::timeout_add_local(5000, move || { 224 | let tx = tx.clone(); 225 | spawn(async move { 226 | let _ = tx.send(Event::SyncToDisk).await; 227 | }); 228 | 229 | glib::Continue(false) 230 | })); 231 | } 232 | 233 | pub fn remove(&mut self, entity: TaskEntity) { 234 | if self.tasks.len() == 1 { 235 | return; 236 | } 237 | self.remove_(entity); 238 | } 239 | 240 | fn remove_(&mut self, entity: TaskEntity) { 241 | if let Some(removed) = self.tasks.remove(entity) { 242 | self.container.remove_row(removed.row); 243 | 244 | // Decrement the row value of the tasks that were below the removed row 245 | for task in self.tasks.values_mut() { 246 | if task.row > removed.row { 247 | task.row -= 1; 248 | } 249 | } 250 | } 251 | } 252 | 253 | pub async fn closed(&mut self) { 254 | self.sync_to_disk().await; 255 | let _ = self.btx.send(BgEvent::Quit).await; 256 | } 257 | 258 | pub async fn sync_to_disk(&mut self) { 259 | self.scheduled_write = None; 260 | 261 | let contents = fomat_macros::fomat!( 262 | for node in self.tasks.values() { 263 | if node.entry.get_text_length() != 0 { 264 | (node.entry.get_text()) "\n" 265 | } 266 | } 267 | ); 268 | 269 | let _ = self.btx.send(BgEvent::Save(self.last_saved.clone(), contents)).await; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /examples/02-Todo/src/background.rs: -------------------------------------------------------------------------------- 1 | use crate::Event; 2 | use async_channel::{Receiver, Sender}; 3 | use std::path::{Path, PathBuf}; 4 | use std::time::SystemTime; 5 | use std::{fs, io}; 6 | 7 | pub enum BgEvent { 8 | Load(PathBuf), 9 | 10 | // Save tasks to a file 11 | Save(PathBuf, String), 12 | 13 | // Exit the from the event loop 14 | Quit 15 | } 16 | 17 | pub async fn run(tx: Sender, rx: Receiver) { 18 | let xdg_dirs = xdg::BaseDirectories::with_prefix(crate::APP_ID).unwrap(); 19 | 20 | let data_home = xdg_dirs.get_data_home(); 21 | 22 | let _ = fs::create_dir_all(&data_home); 23 | 24 | if let Some(path) = most_recent_file(&data_home).unwrap() { 25 | if let Ok(data) = std::fs::read_to_string(&path) { 26 | let _ = tx.send(Event::Load(path, data)).await; 27 | } 28 | } 29 | 30 | while let Ok(event) = rx.recv().await { 31 | match event { 32 | BgEvent::Load(path) => { 33 | println!("loading {:?}", path); 34 | if let Ok(data) = std::fs::read_to_string(&path) { 35 | let _ = tx.send(Event::Load(path, data)).await; 36 | } 37 | }, 38 | 39 | BgEvent::Save(path, data) => { 40 | let path = xdg_dirs.place_data_file(path).unwrap(); 41 | std::fs::write(&path, data.as_bytes()).unwrap(); 42 | }, 43 | 44 | BgEvent::Quit => break 45 | } 46 | } 47 | 48 | let _ = tx.send(Event::Quit).await; 49 | } 50 | 51 | fn most_recent_file(path: &Path) -> io::Result> { 52 | let mut most_recent = SystemTime::UNIX_EPOCH; 53 | let mut target = None; 54 | 55 | for entry in fs::read_dir(path)?.filter_map(Result::ok) { 56 | if entry.file_type().map_or(false, |kind| kind.is_file()) { 57 | if let Ok(modified) = entry.metadata().and_then(|m| m.modified()) { 58 | if modified > most_recent { 59 | target = Some(entry.path()); 60 | most_recent = modified; 61 | } 62 | } 63 | } 64 | } 65 | 66 | Ok(target) 67 | } 68 | -------------------------------------------------------------------------------- /examples/02-Todo/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate cascade; 3 | 4 | mod app; 5 | mod background; 6 | mod widgets; 7 | mod utils; 8 | 9 | use std::path::PathBuf; 10 | use self::app::App; 11 | use self::background::BgEvent; 12 | use gio::prelude::*; 13 | 14 | pub const APP_ID: &str = "io.github.mmstick.ToDo"; 15 | 16 | slotmap::new_key_type!{ 17 | pub struct TaskEntity; 18 | } 19 | 20 | pub enum Event { 21 | Delete, 22 | Insert(TaskEntity), 23 | Load(PathBuf, String), 24 | Modified, 25 | Remove(TaskEntity), 26 | SyncToDisk, 27 | Toggled(bool), 28 | Closed, 29 | Quit, 30 | } 31 | 32 | fn main() { 33 | let app_name = "Todo"; 34 | 35 | glib::set_program_name(Some(app_name)); 36 | glib::set_application_name(app_name); 37 | 38 | let app = gtk::Application::new(Some(APP_ID), Default::default()) 39 | .expect("failed to init application"); 40 | 41 | app.connect_activate(|app| { 42 | let (tx, rx) = async_channel::unbounded(); 43 | let (btx, brx) = async_channel::unbounded(); 44 | 45 | std::thread::spawn(glib::clone!(@strong tx => move || { 46 | utils::thread_context().block_on(background::run(tx, brx)); 47 | })); 48 | 49 | let mut app = App::new(app, tx, btx); 50 | 51 | let event_handler = async move { 52 | while let Ok(event) = rx.recv().await { 53 | match event { 54 | Event::Modified => app.modified(), 55 | Event::Insert(entity) => app.insert(entity), 56 | Event::Remove(entity) => app.remove(entity), 57 | Event::SyncToDisk => app.sync_to_disk().await, 58 | Event::Toggled(active) => app.toggled(active), 59 | Event::Delete => app.delete(), 60 | Event::Load(path, data) => app.load(path, data), 61 | Event::Closed => app.closed().await, 62 | Event::Quit => gtk::main_quit(), 63 | } 64 | } 65 | }; 66 | 67 | utils::spawn(event_handler); 68 | }); 69 | 70 | app.run(&[]); 71 | } 72 | -------------------------------------------------------------------------------- /examples/02-Todo/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | pub fn thread_context() -> glib::MainContext { 4 | glib::MainContext::thread_default().unwrap_or_else(|| { 5 | let ctx = glib::MainContext::new(); 6 | ctx.push_thread_default(); 7 | ctx 8 | }) 9 | } 10 | 11 | pub fn spawn(future: F) 12 | where 13 | F: Future + 'static, 14 | { 15 | glib::MainContext::default().spawn_local(future); 16 | } -------------------------------------------------------------------------------- /examples/02-Todo/src/widgets.rs: -------------------------------------------------------------------------------- 1 | use crate::{utils::spawn, Event, TaskEntity}; 2 | use async_channel::Sender; 3 | use glib::{clone, SignalHandlerId}; 4 | use gtk::prelude::*; 5 | 6 | pub struct Task { 7 | pub entry: gtk::Entry, 8 | pub insert: gtk::Button, 9 | pub remove: gtk::Button, 10 | pub check: gtk::CheckButton, 11 | 12 | entry_signal: Option, 13 | 14 | // Tracks our position in the list 15 | pub row: i32, 16 | } 17 | 18 | impl Task { 19 | pub fn new(row: i32) -> Self { 20 | Self { 21 | check: cascade! { 22 | gtk::CheckButton::new(); 23 | ..show(); 24 | }, 25 | 26 | insert: cascade! { 27 | gtk::Button::from_icon_name(Some("list-add-symbolic"), gtk::IconSize::Button); 28 | ..show(); 29 | }, 30 | 31 | remove: cascade! { 32 | gtk::Button::from_icon_name(Some("list-remove-symbolic"), gtk::IconSize::Button); 33 | ..show(); 34 | }, 35 | 36 | entry: cascade! { 37 | gtk::Entry::new(); 38 | ..set_hexpand(true); 39 | ..show(); 40 | }, 41 | 42 | entry_signal: None, 43 | row, 44 | } 45 | } 46 | 47 | pub fn connect(&mut self, tx: Sender, entity: TaskEntity) { 48 | let signal = self.entry.connect_changed(clone!(@strong tx => move |_| { 49 | let tx = tx.clone(); 50 | spawn(async move { 51 | let _ = tx.send(Event::Modified).await; 52 | }); 53 | })); 54 | 55 | self.entry_signal = Some(signal); 56 | 57 | self.check.connect_toggled(clone!(@strong tx => move |check| { 58 | let tx = tx.clone(); 59 | let check = check.clone(); 60 | spawn(async move { 61 | let _ = tx.send(Event::Toggled(check.get_active())).await; 62 | }) 63 | })); 64 | 65 | self.insert 66 | .connect_clicked(clone!(@strong tx, @weak self.entry as entry => move |_| { 67 | if entry.get_text_length() == 0 { 68 | return; 69 | } 70 | 71 | let tx = tx.clone(); 72 | spawn(async move { 73 | let _ = tx.send(Event::Insert(entity)).await; 74 | }); 75 | })); 76 | 77 | self.remove.connect_clicked(clone!(@strong tx => move |_| { 78 | let tx = tx.clone(); 79 | spawn(async move { 80 | let _ = tx.send(Event::Remove(entity)).await; 81 | }); 82 | })); 83 | 84 | self.entry 85 | .connect_activate(clone!(@weak self.entry as entry => move |_| { 86 | if entry.get_text_length() == 0 { 87 | return; 88 | } 89 | 90 | let tx = tx.clone(); 91 | spawn(async move { 92 | let _ = tx.send(Event::Insert(entity)).await; 93 | }); 94 | })); 95 | } 96 | 97 | pub fn set_text(&mut self, text: &str) { 98 | let signal = self.entry_signal.as_ref().unwrap(); 99 | self.entry.block_signal(signal); 100 | self.entry.set_text(text); 101 | self.entry.unblock_signal(signal); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/1x00-intro.md: -------------------------------------------------------------------------------- 1 | # Basics 2 | 3 | > This tutorial series is currently a work in progress. [GitHub](https://github.com/mmstick/gtkrs-tutorials) 4 | 5 | The purpose of this tutorial is to demonstrate GTK application development in Rust from an event-drive perspective. After gaining a lot of experience, I have come to the conclusion that this is the best way to develop GTK applications, and through this tutorial I will share what I consider to be best practices. 6 | 7 | Besides the first chapter, each chapter will contain a useful application that you will develop alongside the tutorial. Through this, you will gain some insight into how these applications are developed, and gain experience with a variety of aspects that GTK and Rust have to offer to an application developer. 8 | 9 | You will learn the following: 10 | 11 | - First and foremost, GLib and GTK 12 | - How to navigate the GTK-rs API documentation 13 | - Using GLib's global and thread-local async executors 14 | - Creating and configuring widgets with cascade macro 15 | - APP IDs and preventing applications from launching multiple instances 16 | - Adding an event handler in a GTK application 17 | - Utilizing an entity-component approach to widget management 18 | - Adhering to XDG standards 19 | - Embedding resources into a GTK applications 20 | - Translating applications with Fluent 21 | - Packaging for Debian platforms 22 | - GNOME Human Interface Guidelines 23 | -------------------------------------------------------------------------------- /src/1x01-about-gtk.md: -------------------------------------------------------------------------------- 1 | # About GTK 2 | 3 | > **WARNING**: This tutorial assumes familiarity with Rust. 4 | 5 | Before we begin, it is important to know a few things about GTK itself. The architecture that GTK is built upon strongly influences the way that we will interact with it. Yet I won't dive too deeply into the details, because we only need cover what's most important for us as a consumer of the API in Rust. 6 | 7 | ## GTK is built around GLib 8 | 9 | GTK is a GUI toolkit built strongly around GLib and it's GLib Object System — which we'll simply call GObject. GLib is essentially a high level cross-platform standard library for C. The GObject portion of GLib enables programming with an object-oriented paradigm. 10 | 11 | GTK uses GLib both for it's object system and asynchronous runtime. Every widget type is a GObject class, and most widget classes inherit multiple layers of GTK widget classes. Widgets schedule tasks for concurrent execution on the default async executor (`gtk::MainContext::default()`), and register signals that react to various property and state changes. 12 | 13 | Luckily for us, the behaviors of every class implemented in GTK can be conveniently represented as a trait in Rust. Even better, GTK fully supports a feature known as "GObject Introspection", which is a convenient way of automatically generating quality bindings in other programming languages. This allowed GTK to have first class bindings in a short amount of time. 14 | 15 | ## Initialized widgets are objects 16 | 17 | As for what that means to us, `GObjects` are heap-allocated with interior mutability. Behaviorally, they operate very similarly to how you would expect to work with a `Rc>` type in Rust. You'll never be required to have unique ownership or a mutable reference to modify a widget, as a result. 18 | 19 | When you clone a GObject, you are creating a new strong reference to the object. When all strong references have been dropped, the destructor for that object is run. However, cyclic references are possible with strong references which can prevent the strong count from ever reaching 0, so there's an option to downgrade them into a weak reference. We'll be designing our software in a way that mitigates the need for this though. 20 | 21 | ## Widgets have inheritance 22 | 23 | Being built in an object-oriented fashion, widgets are built by inheriting other widgets. So a `gtk::Box` is a subclass of `gtk::Container`, which is also a subclass of `gtk::Widget`; and therefore we can downgrade a `gtk::Box` into a `gtk::Container`, and we can further downgrade that into a `gtk::Widget`. 24 | 25 | There may be times when an API hands you a `gtk::Widget`, and you'll need to upgrade that widget into a more specific type of widget if you want to access methods from that widget's class. 26 | 27 | Or there may also be times when you just want to simplify your code and downgrade a widget into a `gtk::Widget` because you're passing it onto something that takes any kind of widget as an input. 28 | 29 | ## Widget classes have traits 30 | 31 | In the GTK-rs implementation, methods from classes are conveniently stored in traits. The `gtk::Widget`, `gtk::Container`, and `gtk::Box` classes have their methods stored in their respective `gtk::WidgetExt`, `gtk::ContainerExt`, and `gtk::BoxExt` traits. This will allow you to conveniently handle your widgets in a generic fashion. Maybe you have a function that can be perfectly written as `fn function(widget: &W) {}`, 32 | 33 | ## GTK is not thread-safe 34 | 35 | Finally, although GObjects can be thread-safe, GTK widgets are most definitely not. You should not attempt to send GTK widgets across thread boundaries, which thankfully the Rust type system will not permit. GTK widgets must be created and interacted with exclusively on the thread that GTK was initialized on. 36 | 37 | There are crates such as `fragile` that would allow you to wrap your widgets into a `Fragile` and send them across threads, but this is most certainly an anti-pattern. The way a `Fragile` works is that it prevents you from accessing the `T` inside the wrapper unless you are unwrapping it from the same thread that it was created on. If you design your software correctly, you won't have to resort to this kind of arcane magic. Turn back before it is too late. -------------------------------------------------------------------------------- /src/1x02-getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Dependencies 4 | 5 | Before we begin, ensure that you have the necessary development files installed for GTK. On Debian platforms, you will need: 6 | 7 | - `libgtk-3-dev` for GTK3 8 | - `libgtk-4-dev` for GTK4 9 | - `libwebkit2gtk-4.0-dev` if embedding a GTK WebKit View 10 | - `libgtksourceview-4-dev` if embedding a GTK Source View 11 | 12 | On the Rust side of things, you should install: 13 | 14 | - `cargo-edit` with `cargo install cargo-edit`, because that'll make adding dependencies to your project easier. 15 | - `rust-analyzer` in your IDE so that you'll have instant feedback about warnings and errors as you type 16 | 17 | 18 | ## API Documentation 19 | 20 | The API documentation generated on [docs.rs](https://docs.rs) lacks descriptions of the APIs. If you want the most complete API documentation, you will need to reference the documentation generated on the gtk-rs website [here](https://gtk-rs.org/docs-src/). 21 | 22 | To navigate this API, every widget has its own type, but those types only contain methods for constructing the widget. Methods specific to each widget can be found in the `Ext` trait for that widget, such as `ButtonExt`. You may reference the widget type to see what behaviors it implements, such as `ContainerExt` or `WidgetExt`. 23 | 24 | Finally, each widget also has a `Builder` type, such as `ButtonBuilder`. In some cases, the builder type will be the only way to achieve a certain desired effect, such creating a dialog with a `gtk::HeaderBar`. This is because this method will define each property before the widget is initialized. 25 | 26 | ## Cascade Macro 27 | 28 | This macro is an alternative to the builder pattern that I find more useful in general, as the builder type only works up to creation of the widget, rather than calling methods on created widget itself — such as adding widgets to a container. 29 | 30 | ```rust 31 | let container = cascade! { 32 | gtk::Box::new(gtk::Orientation::Vertical, 0); 33 | ..add(&widget1); 34 | ..add(&widget2); 35 | }; 36 | ``` 37 | 38 | Essentially, the first statement creates your widget, and following lines that start with `.` will allow you to invoke a method on that widget, before finally returning the widget itself. 39 | 40 | ## Creating Your Project 41 | 42 | Now we're going to start the process of actually building our first GTK application. Create your project, and add the following dependencies that we need to get started: 43 | 44 | ``` 45 | cargo new first-gtk-app 46 | cd first-gtk-app 47 | cargo add gtk glib async-channel cascade 48 | ``` 49 | 50 | ### Initializing GTK 51 | 52 | Now we're ready to start with code. Lets start by setting your application's name, and initializing GTK. 53 | 54 | ```rust 55 | #[macro_use] 56 | extern crate cascade; 57 | 58 | use gtk::prelude::*; 59 | use std::process; 60 | 61 | fn main() { 62 | glib::set_program_name("First GTK App".into()); 63 | glib::set_application_name("First GTK App"); 64 | 65 | if gtk::init().is_err() { 66 | eprintln!("failed to initialize GTK Application"); 67 | process::exit(1); 68 | } 69 | 70 | // Thread will block here until the application is quit 71 | gtk::main(); 72 | } 73 | ``` -------------------------------------------------------------------------------- /src/1x03-glib-runtime.md: -------------------------------------------------------------------------------- 1 | # Using GLib as an Async Runtime 2 | 3 | Before moving further, we should know that we can leverage the same async runtime that GTK uses for spawning and scheduling its tasks. 4 | 5 | ## Spawning tasks on the default executor 6 | 7 | This will schedule our futures to execute on the main thread, alongside all of the futures scheduled by GTK itself. 8 | 9 | ```rust 10 | use std::future::Future; 11 | 12 | /// Spawns a task on the default executor and waits to receive its output 13 | pub fn block_on(future: F) -> F::Output where F: Future { 14 | glib::MainContext::default().block_on(future) 15 | } 16 | 17 | /// Spawns a task in the background on the default executor 18 | pub fn spawn(future: F) where F: Future + 'static { 19 | glib::MainContext::default().spawn_local(future); 20 | } 21 | ``` 22 | 23 | ## Spawning tasks on the current thread's executor 24 | 25 | Using this approach, you can spawn futures onto the executor that is registered to the thread you are blocking from. By default, there is no executor initialized for newly-spawned threads, so you'll have to create and assign them to the current thread when `glib::MainContext::thread_default()` returns `None`. 26 | 27 | ```rust 28 | use std::future::Future; 29 | 30 | pub fn thread_context() -> glib::MainContext { 31 | glib::MainContext::thread_default() 32 | .unwrap_or_else(|| { 33 | let ctx = glib::MainContext::new(); 34 | ctx.push_thread_default(); 35 | ctx 36 | }) 37 | } 38 | 39 | pub fn block_on(future: F) -> F::Output where F: Future { 40 | thread_context().block_on(future) 41 | } 42 | 43 | pub fn spawn(future: F) where F: Future + 'static { 44 | thread_context().spawn_local(future); 45 | } 46 | ``` 47 | 48 | ## Spawning tasks on a background thread 49 | 50 | This is useful if you spawn a background thread and want to execute your futures using a GLib executor on that thread. 51 | 52 | ```rust 53 | use std::future::Future; 54 | 55 | pub fn thread_context() -> glib::MainContext { 56 | glib::MainContext::thread_default() 57 | .unwrap_or_else(|| { 58 | let ctx = glib::MainContext::new(); 59 | ctx.push_thread_default(); 60 | ctx 61 | }) 62 | } 63 | 64 | pub fn block_on(future: F) -> F::Output where F: Future { 65 | thread_context().block_on(future) 66 | } 67 | 68 | enum BackgroundEvent { DoThis } 69 | 70 | fn main() { 71 | let (bg_tx, bg_rx) = async_channel::unbounded(); 72 | 73 | std::thread::spawn(|| { 74 | block_on(async move { 75 | while let Ok(request) = bg_rx.recv().await { 76 | match request { 77 | BackgroundEvent::DoThis => do_this().await, 78 | 79 | } 80 | } 81 | }); 82 | }); 83 | } 84 | ``` 85 | 86 | ## Spawning tasks on a thread pool 87 | 88 | There is also an option of using `glib::ThreadPool`, which gives you exactly that. It defaults to the number of virtual CPU cores in the system, and by default parks threads that have been idle for more than 15 seconds. The pool does not have a requirement on mutability for spawning blocking tasks, so you can initialize this as a global variable using `once_cell::sync::Lazy` to use around your application, if you prefer this over a Rust-native thread pool like `rayon`. Perhaps to leverage system libraries. 89 | 90 | ```rust 91 | use std::time::Duration; 92 | use std::thread::sleep; 93 | 94 | fn main() { 95 | let pool = glib::ThreadPool::new_shared(None) 96 | .expect("failed to spawn thread pool"); 97 | 98 | let _ = pool.push(|| { 99 | sleep(Duration::from_secs(1)); 100 | println!("First Task"); 101 | }); 102 | 103 | let _ = pool.push(|| { 104 | sleep(Duration::from_secs(2)); 105 | println!("Second Task"); 106 | }); 107 | 108 | let _ = pool.push(|| { 109 | sleep(Duration::from_secs(3)); 110 | println!("Third Task"); 111 | }); 112 | 113 | // Wait for tasks to complete 114 | while pool.get_unprocessed() > 0 { 115 | sleep(Duration::from_secs(1)); 116 | } 117 | } 118 | ``` 119 | 120 | ## Spawning futures on a glib::ThreadPool 121 | 122 | However, this thread pool takes closures as inputs, rather than futures. So if you want to use for futures, you can combine it with the thread default executor above. 123 | 124 | ```rust 125 | use async_io::Timer; 126 | use std::time::Duration; 127 | use std::thread::sleep; 128 | use std::future::Future; 129 | 130 | fn thread_context() -> glib::MainContext { 131 | glib::MainContext::thread_default() 132 | .unwrap_or_else(|| { 133 | let ctx = glib::MainContext::new(); 134 | ctx.push_thread_default(); 135 | ctx 136 | }) 137 | } 138 | 139 | fn block_on(future: F) -> F::Output where F: Future { 140 | thread_context().block_on(future) 141 | } 142 | 143 | fn main() { 144 | let pool = glib::ThreadPool::new_shared(None) 145 | .expect("failed to spawn thread pool"); 146 | 147 | let _ = pool.push(|| { 148 | block_on(async { 149 | Timer::after(Duration::from_secs(1)).await; 150 | println!("First Task"); 151 | }) 152 | }); 153 | 154 | let _ = pool.push(|| { 155 | block_on(async { 156 | Timer::after(Duration::from_secs(2)).await; 157 | println!("Second Task"); 158 | }) 159 | }); 160 | 161 | let _ = pool.push(|| { 162 | block_on(async { 163 | Timer::after(Duration::from_secs(3)).await; 164 | println!("Third Task"); 165 | }) 166 | }); 167 | 168 | // Wait for tasks to complete 169 | block_on(async { 170 | while pool.get_unprocessed() > 0 { 171 | Timer::after(Duration::from_secs(1)).await; 172 | } 173 | }) 174 | } 175 | ``` -------------------------------------------------------------------------------- /src/1x04-event-driven.md: -------------------------------------------------------------------------------- 1 | # Event-Driven Approach 2 | 3 | In the event-driven approach, event handlers will capture and control access to application state. Widgets will have their signals connected to send events through a channel to these event handlers, without access to the global application state themselves. States can be moved and exclusively owned by their respective event handlers; thereby eliminating the need for reference counters, or the need to share your application states with every widget's signal. 4 | 5 | ## Setting it up 6 | 7 | To achieve this, we need an async channel that we can get from the `async-channel` crate: 8 | 9 | ```rust 10 | let (tx, rx) = async_channel::unbounded(); 11 | ``` 12 | 13 | Now we need some event variants that our channel will emit: 14 | 15 | ```rust 16 | enum Event { 17 | Clicked 18 | } 19 | ``` 20 | 21 | Then we will attach the receiver to a future which merely loops on our receiver forever: 22 | 23 | ```rust 24 | let event_handler = async move { 25 | while let Ok(event) = rx.recv().await { 26 | match event { 27 | Event::Clicked => { 28 | 29 | } 30 | } 31 | } 32 | }; 33 | ``` 34 | 35 | And spawn this event handler on the default executor: 36 | 37 | ```rust 38 | // GLib has an executor in the background that will 39 | // asynchronously handle our events on this thread 40 | glib::MainContext::default().spawn_local(event_handler); 41 | ``` 42 | 43 | Your source code should now look like so, and you are now ready to continue to setting up a window with a clickable button. 44 | 45 | ```rust 46 | #[macro_use] 47 | extern crate cascade; 48 | 49 | use gtk::prelude::*; 50 | use std::process; 51 | 52 | enum Event { 53 | Clicked 54 | } 55 | 56 | fn main() { 57 | glib::set_program_name("First GTK App".into()); 58 | glib::set_application_name("First GTK App"); 59 | 60 | // Initialize GTK before proceeding. 61 | if gtk::init().is_err() { 62 | eprintln!("failed to initialize GTK Application"); 63 | process::exit(1); 64 | } 65 | 66 | // Attach `tx` to our widgets, and `rx` to our event handler 67 | let (tx, rx) = async_channel::unbounded(); 68 | 69 | // Processes all application events received from signals 70 | let event_handler = async move { 71 | while let Ok(event) = rx.recv().await { 72 | match event { 73 | Event::Clicked => { 74 | 75 | } 76 | } 77 | } 78 | }; 79 | 80 | // GLib has an executor in the background that will 81 | // asynchronously handle our events on this thread 82 | glib::MainContext::default().spawn_local(event_handler); 83 | 84 | // Thread will block here until the application is quit 85 | gtk::main(); 86 | } 87 | ``` 88 | 89 | ## Avoid blocking the default executor 90 | 91 | Take careful note that because this async task has been spawned on the same executor as all of GTK's own tasks, you must be careful to avoid doing anything that would block your event handler. If your event handler were to block, the default `MainContext` would block with it, thereby freezing the GUI. 92 | 93 | Tasks that require a lot of CPU and/or I/O should be performed in a background thread. Generally, the only code that should be executed on the default context is code that interacts directly with the widgets — fetching information from widgets, creating new widgets, and updating existing ones. -------------------------------------------------------------------------------- /src/1x05-window.md: -------------------------------------------------------------------------------- 1 | # Creating a Window with a Button 2 | 3 | Let's start by setting up a convenience function for spawning futures on the default executor. This will be necessary to send messages through the async channel. 4 | 5 | ```rust 6 | use std::future::Future; 7 | 8 | /// Spawns a task on the default executor, without waiting for it to complete 9 | pub fn spawn(future: F) where F: Future + 'static { 10 | glib::MainContext::default().spawn_local(future); 11 | } 12 | ``` 13 | 14 | ## Creating the App struct 15 | 16 | I typically have a single `App` struct where all application state and GTK widgets that are regularly interacted with are stored. We're going to start with a struct that contains a `gtk::Button` and a `u32` "clicked" variable. 17 | 18 | ```rust 19 | use async_channel::Sender; 20 | 21 | struct App { 22 | pub button: gtk::Button, 23 | pub clicked: u32, 24 | } 25 | 26 | impl App { 27 | pub fn new(tx: Sender) -> Self {} 28 | } 29 | ``` 30 | 31 | When creating the application, we will take ownership of the `Sender` that we created earlier, and pass this into every `.connect_signal()` method that is called on a widget. The `.connect_signal()` methods will create a future on the main context that idles until the condition for that future has been emitted. A `gtk::Button`, for example, has a `.connect_clicked()` method which will have its callbacks invoked when `clicked` is emitted — which happens on a click of the button. 32 | 33 | Note that you may connect multiple callbacks onto the same signal. If you wish to remove one, you should be careful to store the `SignalHandlerId` that is returned from the `.connect_signal()` method. Then call `widget.disconnect(id)` to remove the signal registered to that widget. If you only wish to temporarily block a signal, you can call `widget.block_signal(id)` and `widget.unblock_signal(id)` respectively. 34 | 35 | ## Creating widgets for our app 36 | 37 | First, we will create the button that we will have the user click. The button will have a label which reads, "Click Me". The border will be set to 4 so that the button isn't hugging the edges of the container it is attached to. And then will program it to send an event when it is clicked. 38 | 39 | ```rust 40 | let button = cascade! { 41 | gtk::Button::with_label("Click Me"); 42 | ..set_border_width(4); 43 | ..connect_clicked(move |_| { 44 | let tx = tx.clone(); 45 | spawn(async move { 46 | let _ = tx.send(Event::Clicked).await; 47 | }); 48 | }); 49 | }; 50 | ``` 51 | 52 | Note that since we are using an async channel, the sender has to be awaited when it is sending a value. We can use GLib's default executor to await our send. 53 | 54 | > If the sender happens to block, it could block the default executor and thereby freeze the application. If you're using an unbounded receiver, it will never block on a send, so you will not have to worry about this. 55 | > 56 | > When using a bounded receiver, you should ensure that the tasks is spawned on the executor so that at least the sender can safely wait for its turn to send without blocking our application. However, there is no reason to use a bounded receiver for receiving events, because you'll simply cause the executor to fill up with unresolved tasks. 57 | 58 | Next is creating a container widget to hold our button. This container will also invoke `.show_all()` to make the container visible, and all of the widgets inside the container. 59 | 60 | ```rust 61 | let container = cascade! { 62 | gtk::Box::new(gtk::Orientation::Vertical, 0); 63 | ..add(&button); 64 | ..show_all(); 65 | }; 66 | ``` 67 | 68 | ## Creating the window 69 | 70 | Next we we will create the `Toplevel` window for this application, and attach our container to the window. We will set a title, connect the event to be called when window is deleted, and also set the default icon for our application. The `Toplevel` window is the main window of your application. A window can only have one widget attached to it, which we will assign with the `.add()` method. The `.set_title()` method will set the title of your application. The `.connect_delete_event` method is invoked whenever the window is destroyed, and we will program this to call `gtk::main_quit()` to stop the mainloop, thereby having `gtk::main()` return, which has our application quit. 71 | 72 | 73 | ```rust 74 | let _window = cascade! { 75 | gtk::Window::new(gtk::WindowType::Toplevel); 76 | ..add(&container); 77 | ..set_title("First GTK App"); 78 | ..set_default_size(300, 400); 79 | ..connect_delete_event(move |_, _| { 80 | gtk::main_quit(); 81 | gtk::Inhibit(false) 82 | }); 83 | ..show_all(); 84 | }; 85 | ``` 86 | 87 | One last thing that should be done for window managers is to set a default icon for the application: 88 | 89 | ```rust 90 | gtk::Window::set_default_icon_name("icon-name-here"); 91 | ``` 92 | 93 | Now we can finally return our `App` struct, which should look like so: 94 | 95 | 96 | ```rust 97 | impl App { 98 | pub fn new(tx: Sender) -> Self { 99 | let button = cascade! { 100 | gtk::Button::with_label("Click Me"); 101 | ..set_border_width(4); 102 | ..connect_clicked(move |_| { 103 | let tx = tx.clone(); 104 | spawn(async move { 105 | let _ = tx.send(Event::Clicked).await; 106 | }); 107 | }); 108 | }; 109 | 110 | let container = cascade! { 111 | gtk::Box::new(gtk::Orientation::Vertical, 0); 112 | ..add(&button); 113 | ..show_all(); 114 | }; 115 | 116 | let _window = cascade! { 117 | gtk::Window::new(gtk::WindowType::Toplevel); 118 | ..set_title("First GTK App"); 119 | ..add(&container); 120 | ..connect_delete_event(move |_, _| { 121 | gtk::main_quit(); 122 | gtk::Inhibit(false) 123 | }); 124 | ..show_all(); 125 | }; 126 | 127 | gtk::Window::set_default_icon_name("icon-name-here"); 128 | 129 | Self { button, clicked: 0 } 130 | } 131 | } 132 | ``` 133 | 134 | ## Responding to the clicked event 135 | 136 | In the example below, you can see that we have passed ownership of the `App` into the event handler. The `clicked` property is incremented whenever we receive `Event::Clicked`. The button's label is updated to show how many times it has been clicked. 137 | 138 | ```rust 139 | fn main() { 140 | glib::set_program_name("First GTK App".into()); 141 | glib::set_application_name("First GTK App"); 142 | 143 | // Initialize GTK before proceeding. 144 | if gtk::init().is_err() { 145 | eprintln!("failed to initialize GTK Application"); 146 | process::exit(1); 147 | } 148 | 149 | // Attach `tx` to our widgets, and `rx` to our event handler 150 | let (tx, rx) = async_channel::unbounded(); 151 | 152 | let mut app = App::new(tx); 153 | 154 | // Processes all application events received from signals 155 | let event_handler = async move { 156 | while let Ok(event) = rx.recv().await { 157 | match event { 158 | Event::Clicked => { 159 | app.clicked += 1; 160 | app.button.set_label(&format!("I have been clicked {} times", app.clicked)); 161 | } 162 | } 163 | } 164 | }; 165 | 166 | // GLib has an executor in the background that will 167 | // asynchronously handle our events on this thread 168 | glib::MainContext::default().spawn_local(event_handler); 169 | 170 | // Thread will block here until the application is quit 171 | gtk::main(); 172 | } 173 | ``` 174 | 175 | You may run the application with `cargo run` and try it out. -------------------------------------------------------------------------------- /src/1x06-gtk-widgets.md: -------------------------------------------------------------------------------- 1 | # GTK Widget Reference 2 | 3 | List of roughly every widget included in GTK 4 | 5 | ## Containers 6 | 7 | Containers control the behavior and layout of the widgets within them. 8 | 9 | - [AspectFrame](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.AspectFrame.html): Ensures that the widget retains the same aspect when resized 10 | - [Box](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Box.html): Lays widgets in vertical or horizontal layouts 11 | - [ButtonBox](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ButtonBox.html): Arranges buttons within the container, and themes may style buttons packed this way in a nicer way 12 | - [Expander](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Expander.html): Shows/hides a widget with a button that expands to reveal a hidden widget 13 | - [FlowBox](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.FlowBox.html): Lays widgets horizontally, and dynamically shifts them to new rows as the parent container shrinks 14 | - [Frame](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Frame.html): Displays a frame around a widget 15 | - [Grid](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Grid.html): Lays widgets within a grid of rows and columns, with each widget occupying a X,Y position with a defined width and height 16 | - [HeaderBar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.HeaderBar.html): Replaces the title bar, where widgets can be packed from the start, the center, or the end of the bar 17 | - [Notebook](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Notebook.html): Identical to a stack, but has tabs for switching between widgets. Essentially a Stack + StackSwitcher with a set style 18 | - [Paned](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Paned.html): Containers two widgets side-by-side with a boundary between them that allows the user to resize between them 19 | - [Revealer](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Revealer.html): Conceals and reveals a widget with an animation 20 | - [ScrolledWindow](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ScrolledWindow.html): Makes the contained widget scrollable 21 | - [Stack](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Stack.html): Stores multiple widgets, but only one widget is shown at a time. May be combined with a StackSwitcher to have tabs 22 | - [Toolbar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Toolbar.html): Bar at the top of the window for containing tool items 23 | 24 | ## Lists 25 | 26 | Containers with selectable widgets 27 | 28 | - [ComboBox](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ComboBox.html): Used in conjuction with a tree model to show a list of options to select from 29 | - [ComboBoxText](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ComboBoxText.html): Streamlined variant of a ComboBox to choose from a list of text options 30 | - [IconView](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.IconView.html): Think of a file browser with mouse drag selections. Essentially a FlowBox-like container with a grid of icons with text 31 | - [ListBox](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ListBox.html): Each widget is an interactive row in a list, which may be activated or clicked, and may support multiple selections 32 | - [TreeView](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.TreeView.html): Used to present tabular data, with each row being an object in the list, and each column a field of that object 33 | 34 | ## Text 35 | 36 | Containers which display or receive text 37 | 38 | - [Label](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Label.html): Displays text without any ability to copy or edit the text 39 | - [Entry](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Entry.html): Text box for a single line of text 40 | - [TextView](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.TextView.html): Multi-line text box 41 | - [SearchEntry](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.SearchEntry.html): Entry designed for use for searches 42 | - [SearchBar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.SearchBar.html): Toolbar that reveals a search entry when the user starts typing 43 | 44 | ## Buttons 45 | 46 | Widgets that can be clicked or activated by keyboard 47 | 48 | - [AppChooserButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.AppChooserButton.html): Button that shows an app chooser dialog 49 | - [Button](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Button.html): Interactive widget that may contain text, an image, or other widgets 50 | - [CheckButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.CheckButton.html): Check mark with a label that can be toggled on/off 51 | - [ColorButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ColorButton.html): Displays a color and shows a color chooser dialog to select a different one 52 | - [FileChooserButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.FileChooserButton.html): Shows a file chooser dialog to select file(s) or folder(s) 53 | - [FontButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.FontButton.html): Displays a font and shows a font chooser dialog ot select a different one 54 | - [LinkButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.LinkButton.html): Hyperlink text button for linking to a URI 55 | - [LockButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.LockButton.html): Button with a lock icon for unlocking / locking privileged options 56 | - [MenuButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.MenuButton.html): Button for showing a popover menu on click 57 | - [RadioButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.RadioButton.html): When grouped with other radio buttons, only one button may be activate 58 | - [ScaleButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ScaleButton.html): Button that pops up a scale 59 | - [SpinButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.SpinButton.html): Number entry with buttons for incrementing and decrementing 60 | - [StackSidebar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.StackSidebar.html): Vertical tabs for a stack 61 | - [StackSwitcher](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.StackSwitcher.html): Horizontal tabs for a stack 62 | - [Switch](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Switch.html): Toggle button represented as an off/on switch 63 | - [ToggleButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ToggleButton.html): Button that toggles between being pressed in and unpressed 64 | - [VolumeButton](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.VolumeButton.html): Button that pops up a volume scale 65 | 66 | ## Display 67 | 68 | Widgets that display things 69 | 70 | - [DrawingArea](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.DrawingArea.html): Provides a canvas for drawing images onto 71 | - [EventBox](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.EventBox.html): Makes it possible for a widget to receive button / mouse events 72 | - [GLArea](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.GLArea.html): Context for rendering OpenGL onto 73 | - [Image](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Image.html): Displays a picture 74 | - [InfoBar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.InfoBar.html): Hidden bar that is revealed when info or an error is to be shown 75 | - [LevelBar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.LevelBar.html): Shows a level of a scale 76 | - [ProgressBar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ProgressBar.html): Shows a progress bar 77 | - [Separator](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Separator.html): Shows a horizontal or vertical separator 78 | - [ShortcutLabel](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.ShortcutLabel.html): Keyboard shortcut label 79 | - [Spinner](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Spinner.html): Shows a spinning animation 80 | - [Statusbar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Statusbar.html): Displays information at the bottom of the window 81 | 82 | ## Misc 83 | 84 | Everything else 85 | 86 | - [PlacesSidebar](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.PlacesSidebar.html): Displays frequently visited places in the file system 87 | - [Plug](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Plug.html) / [Socket](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/struct.Socket.html): Allows sharing widgets across windows 88 | 89 | > An exhaustive list of the gtk widgets can be found in the [widget gallery](https://docs.gtk.org/gtk4/visual_index.html), but the API version is GTK 4 and above, so crosscheck with the GTK3 docs to get the correct syntax for a widget 90 | -------------------------------------------------------------------------------- /src/2x00-intro.md: -------------------------------------------------------------------------------- 1 | # ToDo 2 | 3 | > Full source code for this chapter can be found in the [GitHub Repository](https://github.com/mmstick/gtkrs-tutorials) under `examples/02-Todo`. 4 | 5 | The first application that we will create in this tutorial series is a ToDo list. There are a myriad of ways to create them, but we are going to opt for the event-driven approach with a [slotmap](https://docs.rs/slotmap). Each task in the ToDo list will be stored in the SlotMap and referenced by its key. On launch of the application, we will load the most-recently modified note. On close, we will write any changes that have yet to be saved before quitting the application. We will also save any changes made every 5 seconds after the last modification. 6 | 7 | ## What is a SlotMap? 8 | 9 | SlotMap is described as a "container with persistent unique keys to access stored values." Essentially, it is an arena allocator with generational indices. Imagine a vector where each slot contains a version number. Odd-numbered versions indicate to the allocator that the slot is empty and ready to be filled. On insertion of a new value into a slot, the version is incremented back to an even number, and a key is returned which contains the "generation" version, and the indice of the slot that was used. This gives SlotMap roughly the same performance as accessing an element of an array by its indice, but with an additional version check to determine if key is still valid. 10 | 11 | ## Initialize the project 12 | 13 | To get started, create a new project: 14 | 15 | ``` 16 | cargo new todo 17 | cd todo 18 | cargo add async-channel cascade fomat-macros gio glib gtk slotmap xdg 19 | ``` 20 | 21 | Then inside the src folder, structure your project like so: 22 | 23 | ``` 24 | src/ 25 | app.rs 26 | background.rs 27 | main.rs 28 | utils.rs 29 | widgets.rs 30 | ``` 31 | -------------------------------------------------------------------------------- /src/2x01-gtk-application.md: -------------------------------------------------------------------------------- 1 | # Using gtk::Application 2 | 3 | ## utils.rs 4 | 5 | Before we begin, we need to add some utility functions that we'll be using throughout the application. We will be spawning a background thread and executing tasks on it, so we'll need a convenience function for fetching the thread-default context. Likewise, we're going to spawning tasks on the global default context, so we'll need that here as well. 6 | 7 | ```rust 8 | use std::future::Future; 9 | 10 | pub fn thread_context() -> glib::MainContext { 11 | glib::MainContext::thread_default() 12 | .unwrap_or_else(|| { 13 | let ctx = glib::MainContext::new(); 14 | ctx.push_thread_default(); 15 | ctx 16 | }) 17 | } 18 | 19 | pub fn spawn(future: F) where F: Future + 'static { 20 | glib::MainContext::default().spawn_local(future); 21 | } 22 | ``` 23 | 24 | ## main.rs 25 | 26 | This time we will create a GTK application using the proper `gtk::Application` setup process. This will take care of initializing GTK for you, and registers your application with an application ID so that you can prevent your application from spawning multiple instances. 27 | 28 | ```rust 29 | #[macro_use] 30 | extern crate cascade; 31 | 32 | mod app; 33 | mod background; 34 | mod widgets; 35 | mod utils; 36 | 37 | use self::app::App; 38 | use gio::prelude::*; 39 | 40 | /// The name that we will register to the system to identify our application 41 | pub const APP_ID: &str = "io.github.mmstick.ToDo"; 42 | 43 | fn main() { 44 | let app_name = "Todo"; 45 | 46 | glib::set_program_name(Some(app_name)); 47 | glib::set_application_name(app_name); 48 | 49 | // Initializes GTK and registers our application. gtk::Application helps us 50 | // set up an application with less work 51 | let app = gtk::Application::new( 52 | Some(APP_ID), 53 | Default::default() 54 | ).expect("failed to init application"); 55 | 56 | // After the application has been registered, it will trigger an activate 57 | // signal, which will give us the okay to construct our application and set 58 | // up our application logic. We're going to use `app` to create the 59 | // application window in the future. 60 | app.connect_activate(|app| { 61 | let (tx, rx) = async_channel::unbounded(); 62 | 63 | let mut app = App::new(app, tx); 64 | 65 | let event_handler = async move { 66 | while let Ok(event) = rx.recv().await { 67 | match event { 68 | 69 | } 70 | } 71 | }; 72 | 73 | utils::spawn(event_handler); 74 | }); 75 | 76 | // This last step performs the same duty as gtk::main() 77 | app.run(&[]); 78 | } 79 | ``` 80 | 81 | Calling `gtk::Application::new()` will run `gtk::init()` and register your application by the `APP_ID` that we defined. The general practice for application IDs is to use [Reverse domain name notation (RDNN)](https://en.wikipedia.org/wiki/Reverse_domain_name_notation). `gtk::Application::connect_activate()` signals that GTK is ready for us to construct our application window and set up all of our application logic. This method receives a reference to the `gtk::Application` itself, which we will later use to create the `gtk::ApplicationWindow`, which is our top level `gtk::Window` for our application. `gtk::Application::run()` will then invoke `gtk::main()` to set the whole process in motion. -------------------------------------------------------------------------------- /src/2x02-events.md: -------------------------------------------------------------------------------- 1 | # Modeling Our Events 2 | 3 | Before going to the next step, we need to think about how we will design our application, and what events our application is going to handle. 4 | 5 | A ToDo application will have the following behaviors: 6 | 7 | - Insert a task 8 | - Remove a task 9 | 10 | Each task will be represented in our UI as a row containing the following widgets: A `gtk::Entry` for writing our task notes; with two `gtk::Button`s for inserting a new task below, or removing the task in that row. Our tasks will be stored in a `SlotMap`, where each task is referenced by their custom key: `TaskEntity`. 11 | 12 | - Load tasks from a file 13 | 14 | When we load our notes from a file, it will be in the form of a `String`, and each task will be a separate line in that string. The application will automatically create a new task row for each line in that string. 15 | 16 | - Notify when a task is modified 17 | - Save tasks to a file 18 | 19 | Every 5 seconds after the last modification, we will fetch the contents of each `gtk::Entry` and save them to a file. We will also save the contents of each widget when the application has been closed. 20 | 21 | - Notify that the application has been closed 22 | - Notify that we are ready to quit 23 | 24 | The last two events are an important distinction. When the GTK application has been closed, we will get notified that it has been destroyed. During that time, we will schedule to have our notes saved, and quit the application once they've been saved to the disk. 25 | 26 | ## main.rs 27 | 28 | ```rust 29 | // Create a key type to identify the keys that we'll use for the Task SlotMap. 30 | slotmap::new_key_type! { 31 | pub struct TaskEntity; 32 | } 33 | 34 | pub enum Event { 35 | // Insert a task below the given task, identified by its key 36 | Insert(TaskEntity), 37 | 38 | // A previous task list has been fetched from a file from the background 39 | // thread, and it is now our job to display it in our UI. 40 | Load(String), 41 | 42 | // Signals that an entry was modified, and at some point we should save it 43 | Modified, 44 | 45 | // Removes the task identified by this entity 46 | Remove(TaskEntity), 47 | 48 | // Signals that we should collect up the text from each task and pass it 49 | // to a background thread to save it to a file. 50 | SyncToDisk, 51 | 52 | // Signals that the window has been closed, so we should clean up and quit 53 | Closed, 54 | 55 | // Signals that the process has saved to disk and it is safe to exit 56 | Quit, 57 | } 58 | ``` 59 | 60 | Then modify our event handler like so: 61 | 62 | ```rust 63 | let event_handler = async move { 64 | while let Ok(event) = rx.recv().await { 65 | match event { 66 | Event::Modified => app.modified(), 67 | Event::Insert(entity) => app.insert(entity), 68 | Event::Remove(entity) => app.remove(entity), 69 | Event::SyncToDisk => app.sync_to_disk().await, 70 | Event::Load(data) => app.load(data), 71 | Event::Closed => app.closed().await, 72 | Event::Quit => gtk::main_quit(), 73 | } 74 | } 75 | }; 76 | ``` 77 | 78 | Events are listed in the order that they are most-likely to be called in, with the most-called events first. -------------------------------------------------------------------------------- /src/2x03-background-events.md: -------------------------------------------------------------------------------- 1 | # Loading and Saving in the Background 2 | 3 | ## main.rs 4 | 5 | To handle events in the background, and within the app itself, we will need two separate channels. One receiver will listen for application events in the main thread which manages the UI. The other receiver will listen for events from the application in a background thread. 6 | 7 | ```rust 8 | // Channel for UI events in the main thread 9 | let (tx, rx) = async_channel::unbounded(); 10 | 11 | // Channel for background events to the background thread 12 | let (btx, brx) = async_channel::unbounded(); 13 | ``` 14 | 15 | Reading and writing data to a file is a blocking operation that has risk of freezing the application when these operations are occurring on the same thread as the UI. We can therefore avoid hanging the UI simply by passing these tasks off to a background thread. 16 | 17 | ## Spawning the background thread 18 | 19 | Next we will spawn a thread, and pass both a clone of our application event sender, and the background event receiver. The glib crate provides a [clone macro](https://gtk-rs.org/docs/glib/macro.clone.html#passing-a-strong-reference) which can be used 20 | 21 | ```rust 22 | // Take ownership of a copy of the UI event sender (tx), 23 | // and the background event receiver (brx). 24 | std::thread::spawn(glib::clone!(@strong tx => move || { 25 | // Fetch the executor registered for this thread 26 | utils::thread_context() 27 | // Block this thread on an event loop future 28 | .block_on(background::run(tx, brx)); 29 | })); 30 | ``` 31 | 32 | We're going to attach the background sender to our `App` in the future, so we need to update our call to `App::new()` to take both channels as input parameters. 33 | 34 | ```rust 35 | let mut app = App::new(app, tx, btx); 36 | ``` 37 | 38 | ## background.rs 39 | 40 | Our background event loop is going to start with an async function that looks like this. It all take a sender for events we need to pass back to the UI, and the receiver for receiving events from the UI. 41 | 42 | 43 | ```rust 44 | use crate::Event; 45 | use async_channel::{Receiver, Sender}; 46 | use std::path::{Path, PathBuf}; 47 | use std::time::SystemTime; 48 | use std::{fs, io}; 49 | 50 | pub async fn run(tx: Sender, rx: Receiver) { 51 | 52 | } 53 | ``` 54 | 55 | ### XDG 56 | 57 | On first startup, our application will load the most recently-modified task in memory. Applications should adhere to the XDG standards when they are making decisions about where to store files used by their application. Using the `xdg` crate, we can get the prefix for your application with the following code: 58 | 59 | ```rust 60 | let xdg_dirs = xdg::BaseDirectories::with_prefix(crate::APP_ID) 61 | .unwrap(); 62 | ``` 63 | 64 | Because the directory will not exist on a first startup, we need to ensure it's created: 65 | 66 | 67 | ```rust 68 | let data_home = xdg_dirs.get_data_home(); 69 | 70 | let _ = fs::create_dir_all(&data_home); 71 | ``` 72 | 73 | With the data directory for our app now created, we'll search it for the most recently-created file in this directory, and read that file into memory to pass back to our app: 74 | 75 | ```rust 76 | if let Some(path) = most_recent_file(&data_home).unwrap() { 77 | if let Ok(data) = std::fs::read_to_string(&path) { 78 | let _ = tx.send(Event::Load(data)).await; 79 | } 80 | } 81 | ``` 82 | 83 | ### Fetching the most-recent file 84 | 85 | The function, `most_recent_file()` contains the following for reference: 86 | 87 | ```rust 88 | fn most_recent_file(path: &Path) -> io::Result> { 89 | let mut most_recent = SystemTime::UNIX_EPOCH; 90 | let mut target = None; 91 | 92 | for entry in fs::read_dir(path)?.filter_map(Result::ok) { 93 | if entry.file_type().map_or(false, |kind| kind.is_file()) { 94 | if let Ok(modified) = entry.metadata() 95 | .and_then(|m| m.modified()) 96 | { 97 | if modified > most_recent { 98 | target = Some(entry.path()); 99 | most_recent = modified; 100 | } 101 | } 102 | } 103 | } 104 | 105 | Ok(target) 106 | } 107 | ``` 108 | 109 | ### Handling Events 110 | 111 | And then finally, we will start handling the events we receive from the UI. The first being a request to save notes to a file, and the other a request to quit the application. 112 | 113 | ```rust 114 | /// Events that the background thread's event loop will respond to 115 | pub enum BgEvent { 116 | // Save tasks to a file 117 | Save(PathBuf, String), 118 | 119 | // Exit the from the event loop 120 | Quit 121 | } 122 | ``` 123 | 124 | The `Quit` event will break from the event loop and then reply to the application that we have finished any task we were waiting on, and it is now safe to exit the application. 125 | 126 | ```rust 127 | while let Ok(event) = rx.recv().await { 128 | match event { 129 | BgEvent::Save(path, data) => { 130 | let path = xdg_dirs.place_data_file(path).unwrap(); 131 | std::fs::write(&path, data.as_bytes()).unwrap(); 132 | }, 133 | 134 | BgEvent::Quit => break 135 | } 136 | } 137 | 138 | let _ = tx.send(Event::Quit).await; 139 | ``` 140 | 141 | ### Review 142 | 143 | At the end, your file should look like this: 144 | 145 | ```rust 146 | pub async fn run(tx: Sender, rx: Receiver) { 147 | let xdg_dirs = xdg::BaseDirectories::with_prefix(crate::APP_ID).unwrap(); 148 | 149 | let data_home = xdg_dirs.get_data_home(); 150 | 151 | let _ = fs::create_dir_all(&data_home); 152 | 153 | if let Some(path) = most_recent_file(&data_home).unwrap() { 154 | if let Ok(data) = std::fs::read_to_string(&path) { 155 | let _ = tx.send(Event::Load(data)).await; 156 | } 157 | } 158 | 159 | while let Ok(event) = rx.recv().await { 160 | match event { 161 | BgEvent::Save(path, data) => { 162 | let path = xdg_dirs.place_data_file(path).unwrap(); 163 | std::fs::write(&path, data.as_bytes()).unwrap(); 164 | }, 165 | 166 | BgEvent::Quit => break 167 | } 168 | } 169 | 170 | let _ = tx.send(Event::Quit).await; 171 | } 172 | ``` -------------------------------------------------------------------------------- /src/2x04-task-widget.md: -------------------------------------------------------------------------------- 1 | # Creating the Task Widget Struct 2 | 3 | Before we work on the core of the application itself, I typically start by creating the widgets which the application will build upon. Every task in the application will consist of three widgets: 4 | 5 | - `gtk::Entry` for editing the text of a task 6 | - `gtk::Button` for inserting a new task below this task 7 | - `gtk::Button` for removing this task 8 | 9 | ## widgets.rs 10 | 11 | Take note that because our tasks are created dynamically at runtime, we'll want a way of tracking them, which we can achieve with a `SlotMap`. The `Task` struct is going to contain each of the widgets owned by this task, as well as the row where this task was stored. 12 | 13 | ```rust 14 | use crate::{utils::spawn, Event, TaskEntity}; 15 | use async_channel::Sender; 16 | use glib::{clone, SignalHandlerId}; 17 | use gtk::prelude::*; 18 | 19 | pub struct Task { 20 | pub entry: gtk::Entry, 21 | pub insert: gtk::Button, 22 | pub remove: gtk::Button, 23 | 24 | // Tracks our position in the list 25 | pub row: i32, 26 | } 27 | ``` 28 | 29 | Now we can construct our widgets. 30 | 31 | 32 | ```rust 33 | impl Task { 34 | pub fn new(row: i32) -> Self { 35 | 36 | } 37 | } 38 | ``` 39 | 40 | The text entry will horizontally expanded to consume as much space as possible 41 | 42 | ```rust 43 | let entry = cascade! { 44 | gtk::Entry::new(); 45 | ..set_hexpand(true); 46 | ..show(); 47 | }; 48 | ``` 49 | 50 | Then we'll create our two buttons. It's good practice to use icons over text, because text requires translations. 51 | 52 | ```rust 53 | let insert = cascade! { 54 | gtk::Button::from_icon_name(Some("list-add-symbolic"), gtk::IconSize::Button); 55 | ..show(); 56 | }; 57 | 58 | let remove = cascade! { 59 | gtk::Button::from_icon_name(Some("list-remove-symbolic"), gtk::IconSize::Button); 60 | ..show(); 61 | }; 62 | ``` 63 | 64 | We're not going to program these widgets just yet. Just return them as is to program later: 65 | 66 | ```rust 67 | impl Task { 68 | pub fn new(row: i32) -> Self { 69 | Self { 70 | insert: cascade! { 71 | gtk::Button::from_icon_name(Some("list-add-symbolic"), gtk::IconSize::Button); 72 | ..show(); 73 | }, 74 | 75 | remove: cascade! { 76 | gtk::Button::from_icon_name(Some("list-remove-symbolic"), gtk::IconSize::Button); 77 | ..show(); 78 | }, 79 | 80 | entry: cascade! { 81 | gtk::Entry::new(); 82 | ..set_hexpand(true); 83 | ..show(); 84 | }, 85 | 86 | row, 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | You can find available system icons for your applications using [IconLibray](https://www.flathub.org/apps/details/org.gnome.design.IconLibrary). -------------------------------------------------------------------------------- /src/2x05-app.md: -------------------------------------------------------------------------------- 1 | # Creating the App 2 | 3 | ## app.rs 4 | 5 | Now we can get to creating our `App` struct. This will contain all of the values that we will work with throughout the lifetime of our application. 6 | 7 | ```rust 8 | use crate::{Event, BgEvent, TaskEntity}; 9 | use crate::widgets::Task; 10 | use crate::utils::spawn; 11 | 12 | use async_channel::Sender; 13 | use glib::clone; 14 | use glib::SourceId; 15 | use gtk::prelude::*; 16 | use slotmap::SlotMap; 17 | 18 | pub struct App { 19 | pub container: gtk::Grid, 20 | pub tasks: SlotMap, 21 | pub scheduled_write: Option, 22 | pub tx: Sender, 23 | pub btx: Sender, 24 | } 25 | ``` 26 | 27 | All of our task widgets are going to be stored within the `container: gtk::Grid`. Each row of this grid will be associated with a task. The first column will contain the `gtk::Entry`, and the subsequent two columsn are the `gtk::Button`s. By using a grid, we can easily keep our widgets perfectly aligned in a grid. 28 | 29 | The `tasks: SlotMap` field will contain all the tasks we're currently maintaining. This will be important for looking up which row a task was assigned to. 30 | 31 | When an entry has been modified, we're going to spawn a signal that waits until 5 seconds have passed since the last modification before sending an event to the background thread to save the contents of our task list, whose source ID is stored in `scheduled_write: Option`. 32 | 33 | And without requiring much explanation, `tx` and `btx` are handles for sending UI and background events. 34 | 35 | ### Setting up the App 36 | 37 | ```rust 38 | impl App { 39 | pub fn new( 40 | app: >k::Application, 41 | tx: Sender, 42 | btx: Sender 43 | ) -> Self { 44 | 45 | } 46 | } 47 | ``` 48 | 49 | The first step will be creating the `gtk::Grid` that we are going to assign our widgets to. Each column and row will have 4 units of padding around them, and the widget itself will also have some padding. 50 | 51 | ```rust 52 | let container = cascade! { 53 | gtk::Grid::new(); 54 | ..set_column_spacing(4); 55 | ..set_row_spacing(4); 56 | ..set_border_width(4); 57 | ..show(); 58 | }; 59 | ``` 60 | 61 | Because it will be possible for there to be more tasks than a window can display at one time, this widget will be wrapped within a `gtk::ScrolledWindow`. By defining that the `hscrollbar-policy` is `Never`, this will prevent the scrolling window from horizontally scrolling, but will permit vertical scrolling as necessary. 62 | 63 | ```rust 64 | let scrolled = gtk::ScrolledWindowBuilder::new() 65 | .hscrollbar_policy(gtk::PolicyType::Never) 66 | .build(); 67 | 68 | scrolled.add(&container); 69 | ``` 70 | 71 | Now we get to setting up our window, which we can create from the `>k::Application` we received. Note that we are connecting our sender along with the scroller to the delete event. When the window is being destroyed, we are going to detach the scroller from the window so that it does not get destroyed alongside it. The purpose of doing so is to keep our `gtk::Entry` task widgets alive long enough for us to salvage the text in them to save their contents to the disk before we exit the application. Our sender is going to pass a UI event notifying our event handler about the window having been closed. 72 | 73 | ```rust 74 | let _window = cascade! { 75 | gtk::ApplicationWindow::new(app); 76 | ..set_title("Todo"); 77 | ..add(&scrolled); 78 | ..connect_delete_event(clone!(@strong tx, @strong scrolled => move |win, _| { 79 | // Detach to preserve widgets after destruction of window 80 | win.remove(&scrolled); 81 | 82 | let tx = tx.clone(); 83 | spawn(async move { 84 | let _ = tx.send(Event::Closed).await; 85 | }); 86 | gtk::Inhibit(false) 87 | })); 88 | ..show_all(); 89 | }; 90 | 91 | gtk::Window::set_default_icon_name("icon-name-here"); 92 | ``` 93 | 94 | The last step is putting our app together, creating the first row, and returning the `App` struct: 95 | 96 | ```rust 97 | let mut app = Self { 98 | container, 99 | tasks: SlotMap::with_key(), 100 | scheduled_write: None, 101 | tx, 102 | btx, 103 | }; 104 | 105 | app.insert_row(0); 106 | 107 | app 108 | ``` 109 | 110 | Your file should now look like so: 111 | 112 | ```rust 113 | use crate::{Event, BgEvent, TaskEntity}; 114 | use crate::widgets::Task; 115 | use crate::utils::spawn; 116 | 117 | use async_channel::Sender; 118 | use glib::clone; 119 | use glib::SourceId; 120 | use gtk::prelude::*; 121 | use slotmap::SlotMap; 122 | 123 | pub struct App { 124 | pub container: gtk::Grid, 125 | pub tasks: SlotMap, 126 | pub scheduled_write: Option, 127 | pub tx: Sender, 128 | pub btx: Sender, 129 | } 130 | 131 | impl App { 132 | pub fn new(app: >k::Application, tx: Sender, btx: Sender) -> Self { 133 | let container = cascade! { 134 | gtk::Grid::new(); 135 | ..set_column_spacing(4); 136 | ..set_row_spacing(4); 137 | ..set_border_width(4); 138 | ..show(); 139 | }; 140 | 141 | let scrolled = gtk::ScrolledWindowBuilder::new() 142 | .hscrollbar_policy(gtk::PolicyType::Never) 143 | .build(); 144 | 145 | scrolled.add(&container); 146 | 147 | let _window = cascade! { 148 | gtk::ApplicationWindow::new(app); 149 | ..set_title("Todo"); 150 | ..set_default_size(400, 600); 151 | ..add(&scrolled); 152 | ..connect_delete_event(clone!(@strong tx, @strong scrolled => move |win, _| { 153 | // Detach to preserve widgets after destruction of window 154 | win.remove(&scrolled); 155 | 156 | let tx = tx.clone(); 157 | spawn(async move { 158 | let _ = tx.send(Event::Closed).await; 159 | }); 160 | gtk::Inhibit(false) 161 | })); 162 | ..show_all(); 163 | }; 164 | 165 | gtk::Window::set_default_icon_name("icon-name-here"); 166 | 167 | let mut app = Self { 168 | container, 169 | tasks: SlotMap::with_key(), 170 | scheduled_write: None, 171 | tx, 172 | btx, 173 | }; 174 | 175 | app.insert_row(0); 176 | 177 | app 178 | } 179 | } 180 | ``` -------------------------------------------------------------------------------- /src/2x06-tasks.md: -------------------------------------------------------------------------------- 1 | # Inserting and Removing Tasks 2 | 3 | ## app.rs 4 | 5 | ### Inserting a Row 6 | 7 | Back to our `App` struct, we're going to work on the ability to insert a row by the row indice. 8 | 9 | ```rust 10 | fn insert_row(&mut self, row: i32) -> TaskEntity { 11 | 12 | } 13 | ``` 14 | 15 | When inserting a row, we will want to increment the row value of each task is below the row being added. We can achieve that by iterating our SlotMap of tasks by value, mutably. task that has a row that is greater or equal to the row being inserted will be incremented by 1. 16 | 17 | ```rust 18 | // Increment the row value of each Task is below the new row 19 | for task in self.tasks.values_mut() { 20 | if task.row >= row { 21 | task.row += 1; 22 | } 23 | } 24 | ``` 25 | 26 | Then we instruct our `gtk::Grid` to insert this new row, pushing down all rows beneath it: 27 | 28 | ```rust 29 | self.container.insert_row(row); 30 | ``` 31 | 32 | Next we'll create our task widgets, and assign them to the grid. The `.attach()` method takes the widget to assign, followed by the column, row, width, and height parameters. 33 | 34 | ```rust 35 | let task = Task::new(row); 36 | 37 | self.container.attach(&task.entry, 0, row, 1, 1); 38 | self.container.attach(&task.insert, 1, row, 1, 1); 39 | self.container.attach(&task.remove, 2, row, 1, 1); 40 | ``` 41 | 42 | We should also ensure that the newly-added `gtk::Entry` will grab the focus of our keyboard 43 | 44 | ```rust 45 | task.entry.grab_focus(); 46 | ``` 47 | 48 | Now we can assign this newly-created `Task` to our SlotMap. This will return a key, which we will use as identifiers to the signals we're now going to connect. 49 | 50 | ```rust 51 | let entity = self.tasks.insert(task); 52 | self.tasks[entity].connect(self.tx.clone(), entity); 53 | return entity; 54 | ``` 55 | 56 | Your method should now look like this: 57 | 58 | ```rust 59 | fn insert_row(&mut self, row: i32) -> TaskEntity { 60 | // Increment the row value of each Task is below the new row 61 | for task in self.tasks.values_mut() { 62 | if task.row >= row { 63 | task.row += 1; 64 | } 65 | } 66 | 67 | self.container.insert_row(row); 68 | let task = Task::new(row); 69 | 70 | self.container.attach(&task.entry, 0, row, 1, 1); 71 | self.container.attach(&task.insert, 1, row, 1, 1); 72 | self.container.attach(&task.remove, 2, row, 1, 1); 73 | 74 | task.entry.grab_focus(); 75 | 76 | let entity = self.tasks.insert(task); 77 | self.tasks[entity].connect(self.tx.clone(), entity); 78 | return entity; 79 | } 80 | ``` 81 | 82 | ## widgets.rs 83 | 84 | It is at this point where we are going to start connecting the signals to our task widgets. Add the following method to your `Task` struct: 85 | 86 | ```rust 87 | pub fn connect(&mut self, tx: Sender, entity: TaskEntity) { 88 | 89 | } 90 | 91 | ``` 92 | 93 | First we will have the entry send `Event::Modified` whenever it has changed: 94 | 95 | ```rust 96 | self.entry.connect_changed(clone!(@strong tx => move |_| { 97 | let tx = tx.clone(); 98 | spawn(async move { 99 | let _ = tx.send(Event::Modified).await; 100 | }); 101 | })); 102 | ``` 103 | 104 | Then we will program insert button to send `Event::Insert(entity)` when it has been clicked. Although we will only send this signal if the entry for this task is empty. Note that we are taking the entry widget by weak reference. This will prevent a potential cyclic reference when two widgets happen to depend on each other in their signals. The `clone!` macro will automatically handle creating the weak reference, and upgrading that reference in our signal. 105 | 106 | ```rust 107 | self.insert 108 | .connect_clicked(clone!(@strong tx, @weak self.entry as entry => move |_| { 109 | if entry.get_text_length() == 0 { 110 | return; 111 | } 112 | 113 | let tx = tx.clone(); 114 | spawn(async move { 115 | let _ = tx.send(Event::Insert(entity)).await; 116 | }); 117 | })); 118 | ``` 119 | 120 | Then the remove button: 121 | 122 | ```rust 123 | self.remove.connect_clicked(clone!(@strong tx => move |_| { 124 | let tx = tx.clone(); 125 | spawn(async move { 126 | let _ = tx.send(Event::Remove(entity)).await; 127 | }); 128 | })); 129 | ``` 130 | 131 | And to respond to when the user presses the Enter key, which should be treated as equivalent to clicking the insert button: 132 | 133 | ```rust 134 | self.entry 135 | .connect_activate(clone!(@weak self.entry as entry => move |_| { 136 | if entry.get_text_length() == 0 { 137 | return; 138 | } 139 | 140 | let tx = tx.clone(); 141 | spawn(async move { 142 | let _ = tx.send(Event::Insert(entity)).await; 143 | }); 144 | })); 145 | } 146 | ``` 147 | 148 | 149 | ## app.rs 150 | 151 | Moving back to our app module, we'll add another method for inserting a row. Because our application is going to insert new rows from received events via the `TaskEntity` that was received in the `Insert(TaskEntity)` event, we need to add the method that our application is going to call. After fetching the task from the SlotMap, we can use the conveniently-stored `row` value to determine where we're going to insert a new row. 152 | 153 | ```rust 154 | pub fn insert(&mut self, entity: TaskEntity) { 155 | let mut insert_at = 0; 156 | 157 | if let Some(task) = self.tasks.get(entity) { 158 | insert_at = task.row + 1; 159 | } 160 | 161 | self.insert_row(insert_at); 162 | } 163 | ``` 164 | 165 | Finally, we get to removing tasks. When we receive that `Remove(TaskEntity)` event, we're going to call `app.remove(entity)`. We'll ignore any requests to delete the last task from the list, since that would render our application unusable. If we're allowed to remove a task, we'll remove the task from the SlotMap, and call `grid.remove_row(&widget)` on our `container` to remove all the widgets from that task's row from the container. The widgets will be automatically destroyed after returning from this function, because the last remaining strong references to them have been wiped out. 166 | 167 | ```rust 168 | pub fn remove(&mut self, entity: TaskEntity) { 169 | if self.tasks.len() == 1 { 170 | return; 171 | } 172 | self.remove_(entity); 173 | } 174 | 175 | fn remove_(&mut self, entity: TaskEntity) { 176 | if let Some(removed) = self.tasks.remove(entity) { 177 | self.container.remove_row(removed.row); 178 | 179 | // Decrement the row value of the tasks that were below the removed row 180 | for task in self.tasks.values_mut() { 181 | if task.row > removed.row { 182 | task.row -= 1; 183 | } 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | And of course, similar to having to increment the row values on insert, we'll do the reverse on removal of a widget. -------------------------------------------------------------------------------- /src/2x07-save.md: -------------------------------------------------------------------------------- 1 | # Signaling when to Save 2 | 3 | ## app.rs 4 | 5 | There are two scenarios where we will save our tasks to a file. When the application has been closed, and every 5 seconds after the last modification. To start, lets add a signal that waits 5 seconds before sending the `SyncToDisk` event: 6 | 7 | ```rust 8 | pub fn modified(&mut self) { 9 | if let Some(id) = self.scheduled_write.take() { 10 | glib::source_remove(id); 11 | } 12 | 13 | let tx = self.tx.clone(); 14 | self.scheduled_write = Some(glib::timeout_add_local(5000, move || { 15 | let tx = tx.clone(); 16 | spawn(async move { 17 | let _ = tx.send(Event::SyncToDisk).await; 18 | }); 19 | 20 | glib::Continue(false) 21 | })); 22 | } 23 | ``` 24 | 25 | `glib::timeout_add_local(5000, ...)` will schedule the provided closure to execute on local context after 5 seconds. This function returns an ID which we're storing in the `scheduled_write` property of our `App`. If we receive the `Modified` event again before the 5 seconds have passed, the previous signal will be removed and a new one registered in its place. That'll ensure that it doesn't trigger until after 5 seconds of idle keyboard time has passed. 26 | 27 | Next will be programming the `SyncToDisk` event. We're simply going to collect the text from each non-empty task widget in our slotmap, and combine it into a single string to pass to the background for saving. The `fomat` formatter from the `fomat_macros` crate provides a very intuitive means to achieve this. 28 | 29 | ```rust 30 | pub async fn sync_to_disk(&mut self) { 31 | self.scheduled_write = None; 32 | 33 | let contents = fomat_macros::fomat!( 34 | for node in self.tasks.values() { 35 | if node.entry.get_text_length() != 0 { 36 | (node.entry.get_text()) "\n" 37 | } 38 | } 39 | ); 40 | 41 | let _ = self.btx.send(BgEvent::Save("Task".into(), contents)).await; 42 | } 43 | ``` 44 | 45 | Finally, we can handle that `Closed` event that was sent when the `ApplicationWindow` was destroyed: 46 | 47 | ```rust 48 | pub async fn closed(&mut self) { 49 | self.sync_to_disk().await; 50 | let _ = self.btx.send(BgEvent::Quit).await; 51 | } 52 | ``` -------------------------------------------------------------------------------- /src/2x08-load.md: -------------------------------------------------------------------------------- 1 | # Loading tasks from a file 2 | 3 | ## app.rs 4 | 5 | If in the future, we implement the ability to open a different list, we'll need a way of clearing the UI of the previous list. This simply involves popping out every task in the map and removing them one by one. 6 | 7 | ```rust 8 | pub fn clear(&mut self) { 9 | while let Some(entity) = self.tasks.keys().next() { 10 | self.remove_(entity); 11 | } 12 | } 13 | ``` 14 | 15 | When we receive the contents of a list to load into our UI, we're going to split the string by newlines and create a row for each one, then insert that text into their entries. 16 | 17 | ```rust 18 | pub fn load(&mut self, data: String) { 19 | self.clear(); 20 | 21 | for (row, line) in data.lines().enumerate() { 22 | let entity = self.insert_row(row as i32); 23 | self.tasks[entity].set_text(line); 24 | } 25 | } 26 | ``` 27 | 28 | ## widgets.rs 29 | 30 | Because we are automatically filling out the contents of the `Task::entry` for each task that we load from a file, and we are listening to any changes made to these entries when we send the `Modified` event, we need to block that signal when we are setting the text in the entry. Add the following new property to `Task`: 31 | 32 | ```rust 33 | entry_signal: Option, 34 | ``` 35 | 36 | Which we'll need to assign to `None` in our `Task::new()` method. Then change the `connect_changed` signal for the entry to the following: 37 | 38 | ```rust 39 | let signal = self.entry.connect_changed(clone!(@strong tx => move |_| { 40 | let tx = tx.clone(); 41 | spawn(async move { 42 | let _ = tx.send(Event::Modified).await; 43 | }); 44 | })); 45 | 46 | self.entry_signal = Some(signal); 47 | ``` 48 | 49 | Now we can safely add a method for setting the text on this entry, first by blocking that signal, setting the text, and unblocking it: 50 | 51 | ```rust 52 | pub fn set_text(&mut self, text: &str) { 53 | let signal = self.entry_signal.as_ref().unwrap(); 54 | self.entry.block_signal(signal); 55 | self.entry.set_text(text); 56 | self.entry.unblock_signal(signal); 57 | } 58 | ``` -------------------------------------------------------------------------------- /src/2x09-check-buttons.md: -------------------------------------------------------------------------------- 1 | # Marking & Removing Done Tasks with CheckButtons 2 | 3 | We are going to remove the remove buttons from each task and replace them with check buttons. To remove tasks from the list, we will replace the application's title bar with a `gtk::HeaderBar`, and place a delete button here that will show when any tasks have been checked. 4 | 5 | ## main.rs 6 | 7 | We're adding two new events to our `Event`: 8 | 9 | ```rust 10 | Delete, 11 | Toggled(bool), 12 | ``` 13 | 14 | Handling it in our event handler: 15 | 16 | ```rust 17 | Event::Toggled(active) => app.toggled(active), 18 | Event::Delete => app.delete(), 19 | ``` 20 | 21 | ## widgets.rs 22 | 23 | We can track if tasks are completed with check marks, and remove them together in a batch. Add a new field to our `Task` struct to add a `gtk::CheckButton`: 24 | 25 | ```rust 26 | check: gtk::CheckButton 27 | ``` 28 | 29 | Since we're going to use these check marks for removal operations, we can remove the `remove` button as well. 30 | 31 | Then construct the widget and return it: 32 | 33 | ```rust 34 | Self { 35 | check: cascade! { 36 | gtk::CheckButton::new(); 37 | ..show(); 38 | }, 39 | 40 | insert: cascade! { 41 | gtk::Button::from_icon_name(Some("list-add-symbolic"), gtk::IconSize::Button); 42 | ..show(); 43 | }, 44 | 45 | entry: cascade! { 46 | gtk::Entry::new(); 47 | ..set_hexpand(true); 48 | ..show(); 49 | }, 50 | 51 | entry_signal: None, 52 | row, 53 | } 54 | ``` 55 | 56 | Then we can add an event for when the button is toggled: 57 | 58 | 59 | ```rust 60 | self.check.connect_toggled(clone!(@strong tx => move |check| { 61 | let tx = tx.clone(); 62 | let check = check.clone(); 63 | spawn(async move { 64 | let _ = tx.send(Event::Toggled(check.get_active())).await; 65 | }) 66 | })); 67 | ``` 68 | 69 | ## app.rs 70 | 71 | ### Task Widgets 72 | 73 | Then modify the attachments of these widgets in the app: 74 | 75 | ```rust 76 | self.container.attach(&task.check, 0, row, 1, 1); 77 | self.container.attach(&task.entry, 1, row, 1, 1); 78 | self.container.attach(&task.insert, 2, row, 1, 1); 79 | ``` 80 | 81 | ### Delete Button 82 | 83 | Now we're going to create the delete button, with both an icon and a label. By default, a button is only permitted to have either an image or a label, but we can force it to show both by setting the `always_show_image` property. We also don't want this button to be shown when the window is shown, so we need to call `.set_no_show_all(true)`. Since this button performs a destructive action, we should style it as such with `.get_style_context().add_class(>k::STYLE_CLASS_DESTRUCTIVE_ACTION)`. 84 | 85 | ```rust 86 | let delete_button = cascade! { 87 | gtk::Button::from_icon_name(Some("edit-delete-symbolic"), gtk::IconSize::Button); 88 | ..set_label("Delete"); 89 | // Show the icon alongside the label 90 | ..set_always_show_image(true); 91 | // Don't show this when the window calls `.show_all()` 92 | ..set_no_show_all(true); 93 | // Give this a destructive styling to signal that the action is destructive 94 | ..get_style_context().add_class(>k::STYLE_CLASS_DESTRUCTIVE_ACTION); 95 | // Send the `Delete` event on click 96 | ..connect_clicked(clone!(@strong tx => move |_| { 97 | let tx = tx.clone(); 98 | spawn(async move { 99 | let _ = tx.send(Event::Delete).await; 100 | }); 101 | })); 102 | }; 103 | ``` 104 | 105 | This button widget will be attached to the title bar via the `gtk::HeaderBar`: 106 | 107 | ```rust 108 | let headerbar = cascade! { 109 | gtk::HeaderBar::new(); 110 | ..pack_end(&delete_button); 111 | ..set_title(Some("ToDo")); 112 | ..set_show_close_button(true); 113 | }; 114 | ``` 115 | 116 | Then modify our `ApplicationWindow` to change `.set_title()` for the following: 117 | 118 | ```rust 119 | ..set_titlebar(Some(&headerbar)); 120 | ``` 121 | 122 | And update our `App` struct to add the delete button. 123 | 124 | ```rust 125 | delete_button: gtk::Button 126 | ``` 127 | 128 | ### Handling Toggle Events 129 | 130 | We will show the delete button only when there is at least one active task checked. We can achieve this by adding another property to the `App` struct to track how many tasks are actively checked. 131 | 132 | ```rust 133 | checks_active: u32 134 | ``` 135 | 136 | By default, assigning it in our constructor to `0` of course 137 | 138 | ```rust 139 | checks_active: 0 140 | ``` 141 | 142 | Then we'll add the toggled method for handling the toggle events. If the event is toggled active, we increment the number. We do the reverse when it is unchecked. If the number is non-zero, we set the button as visible. 143 | 144 | ```rust 145 | pub fn toggled(&mut self, active: bool) { 146 | if active { 147 | self.checks_active += 1; 148 | } else { 149 | self.checks_active -= 1; 150 | } 151 | 152 | self.delete_button.set_visible(self.checks_active != 0); 153 | } 154 | ``` 155 | 156 | ### Handling Delete Events 157 | 158 | When we've been requested to delete tasks that were marked as active, we'll iterate through our tasks and collect the entity IDs of each task that is active. We need to collect these into a vector on the side so that we're not modifying our task list as we're iterating across it. Once we have a list of tasks to remove, we'll call our remove method with each entity ID. Finally, we'll set the `checks_active` back to `0` and hide the button. 159 | 160 | ```rust 161 | pub fn delete(&mut self) { 162 | let remove_list = self.tasks.iter() 163 | .filter(|(_, task)| task.check.get_active()) 164 | .map(|(id, _)| id) 165 | .collect::>(); 166 | 167 | for id in remove_list { 168 | self.remove(id); 169 | } 170 | 171 | self.checks_active = 0; 172 | self.delete_button.set_visible(false); 173 | } 174 | ``` -------------------------------------------------------------------------------- /src/2x10-multi-list.md: -------------------------------------------------------------------------------- 1 | # Managing Multiple ToDo Lists 2 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Basics](./1x00-intro.md) 4 | - [About GTK](./1x01-about-gtk.md) 5 | - [Getting Started](./1x02-getting-started.md) 6 | - [Using GLib as an Async Runtime](./1x03-glib-runtime.md) 7 | - [Event-Driven Approach](./1x04-event-driven.md) 8 | - [Creating a Window with a Button](./1x05-window.md) 9 | - [GTK Widget Reference](./1x06-gtk-widgets.md) 10 | - [ToDo](./2x00-intro.md) 11 | - [Using gtk::Application](2x01-gtk-application.md) 12 | - [Modeling Our Events](2x02-events.md) 13 | - [Loading and Savings in the Background](2x03-background-events.md) 14 | - [Creating the Task Widget Struct](2x04-task-widget.md) 15 | - [Creating the Base App](2x05-app.md) 16 | - [Inserting and Removing Tasks](2x06-tasks.md) 17 | - [Signaling When to Save](2x07-save.md) 18 | - [Loading Tasks From a File](2x08-load.md) 19 | - [Marking & Removing Done Tasks with CheckButtons](2x09-check-buttons.md) 20 | - [Managing Multiple ToDo Lists](2x10-multi-list.md) --------------------------------------------------------------------------------