From 9008489147a265f79f3874d566954bc1a9b843bf Mon Sep 17 00:00:00 2001 From: Sanpi Date: Sat, 14 Dec 2024 21:14:07 +0100 Subject: [PATCH] Migrates to relm4 --- Cargo.lock | 652 +++++++++++++++++++++----------------- Cargo.toml | 16 +- src/add.rs | 30 -- src/agenda.rs | 346 ++++++++++++-------- src/application/mod.rs | 536 ++++++++++++++++++++++++++++++- src/application/widget.rs | 541 ------------------------------- src/date.rs | 10 +- src/done.rs | 59 ++-- src/edit.rs | 385 +++++++++++----------- src/flag.rs | 59 ++-- src/inbox.rs | 61 ++-- src/logger.rs | 270 +++++----------- src/main.rs | 7 +- src/search.rs | 64 ++-- src/tasks/list.rs | 2 +- src/widgets/calendar.rs | 276 +++++++--------- src/widgets/circle.rs | 82 ++--- src/widgets/filter.rs | 231 ++++++++------ src/widgets/keywords.rs | 255 +++++++-------- src/widgets/mod.rs | 12 +- src/widgets/priority.rs | 191 +++++------ src/widgets/recurrence.rs | 151 +++++++++ src/widgets/repeat.rs | 156 --------- src/widgets/tags.rs | 112 ++++--- src/widgets/task.rs | 294 +++++++---------- src/widgets/tasks.rs | 126 ++++---- 26 files changed, 2469 insertions(+), 2455 deletions(-) delete mode 100644 src/add.rs delete mode 100644 src/application/widget.rs create mode 100644 src/widgets/recurrence.rs delete mode 100644 src/widgets/repeat.rs diff --git a/Cargo.lock b/Cargo.lock index a4d7679..559f390 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" @@ -90,36 +90,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "anyhow" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" - -[[package]] -name = "atk" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf" -dependencies = [ - "atk-sys", - "bitflags 1.3.2", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -167,23 +137,21 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cairo-rs" -version = "0.16.7" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" +checksum = "d7fa699e1d7ae691001a811dda5ef0e3e42e1d4119b26426352989df9e94e3e6" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "cairo-sys-rs", "glib", "libc", - "once_cell", - "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.16.3" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" +checksum = "428290f914b9b86089f60f5d8a9f6e440508e1bcff23b25afd51502b0a2da88f" dependencies = [ "glib-sys", "libc", @@ -192,18 +160,18 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.31" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "shlex", ] [[package]] name = "cfg-expr" -version = "0.15.8" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789" dependencies = [ "smallvec", "target-lexicon", @@ -217,9 +185,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -251,15 +219,15 @@ version = "0.1.0" dependencies = [ "chrono", "dotenvy", - "gtk", + "gtk4", "human-panic", "log", "notify", "pulldown-cmark", "rand", "regex", - "relm", - "relm-derive", + "relm4", + "relm4-components", "todo-txt", "xdg", ] @@ -292,6 +260,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + [[package]] name = "fragile" version = "2.0.0" @@ -307,6 +287,21 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -314,6 +309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -347,9 +343,15 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -362,37 +364,24 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", ] -[[package]] -name = "gdk" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - [[package]] name = "gdk-pixbuf" -version = "0.16.7" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" +checksum = "c4c29071a9e92337d8270a85cb0510cda4ac478be26d09ad027cc1d081911b19" dependencies = [ - "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -401,9 +390,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.16.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" +checksum = "687343b059b91df5f3fbd87b4307038fa9e647fcc0461d0d3f93e94fee20bf3d" dependencies = [ "gio-sys", "glib-sys", @@ -413,10 +402,25 @@ dependencies = [ ] [[package]] -name = "gdk-sys" -version = "0.16.0" +name = "gdk4" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75933c4a86e8a2428814d367e22c733304fdfabc87f415750fd2f55409b6ee48" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" +checksum = "20af0656d543aed3e57ac4120ef76d091c3c42ab1e0507a8febde7cd005640e2" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -445,8 +449,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -457,11 +463,10 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio" -version = "0.16.7" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" +checksum = "8826d2a9ad56ce3de1f04bea0bea0daff6f5f1c913cc834996cfea1f9401361c" dependencies = [ - "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -469,32 +474,30 @@ dependencies = [ "gio-sys", "glib", "libc", - "once_cell", "pin-project-lite", "smallvec", - "thiserror", ] [[package]] name = "gio-sys" -version = "0.16.3" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +checksum = "b965df6f3534c84816b5c1a7d9efcb5671ae790822de5abe8e299797039529bc" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "glib" -version = "0.16.9" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16aa2475c9debed5a32832cb5ff2af5a3f9e1ab9e69df58eaadc1ab2004d6eba" +checksum = "86bd3e4ee7998ab5a135d900db56930cc19ad16681adf245daff54f618b9d5e1" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "futures-channel", "futures-core", "futures-executor", @@ -505,31 +508,28 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "once_cell", + "memchr", "smallvec", - "thiserror", ] [[package]] name = "glib-macros" -version = "0.16.8" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1a9325847aa46f1e96ffea37611b9d51fc4827e67f79e7de502a297560a67b" +checksum = "e7d21ca27acfc3e91da70456edde144b4ac7c36f78ee77b10189b3eb4901c156" dependencies = [ - "anyhow", - "heck 0.4.1", + "heck", "proc-macro-crate", - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "glib-sys" -version = "0.16.3" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +checksum = "3d0b1827e8621fc42c0dfb228e5d57ff6a71f9699e666ece8113f979ad87c2de" dependencies = [ "libc", "system-deps", @@ -537,9 +537,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.16.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +checksum = "a4c674d2ff8478cf0ec29d2be730ed779fef54415a2fb4b565c52def62696462" dependencies = [ "glib-sys", "libc", @@ -547,71 +547,116 @@ dependencies = [ ] [[package]] -name = "gtk" -version = "0.16.2" +name = "graphene-rs" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" +checksum = "1f53144c7fe78292705ff23935f1477d511366fb2f73c43d63b37be89076d2fe" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e741797dc5081e59877a4d72c442c72d61efdd99161a0b1c1b29b6b988934b99" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36933c1e79df378aa6e606576e680358a9582ed8c16f33e94899636e6fa6df6" dependencies = [ - "atk", - "bitflags 1.3.2", "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", + "gdk4", "glib", - "gtk-sys", - "gtk3-macros", + "graphene-rs", + "gsk4-sys", "libc", - "once_cell", "pango", - "pkg-config", ] [[package]] -name = "gtk-sys" -version = "0.16.0" +name = "gsk4-sys" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" +checksum = "0877a9d485bd9ba5262b0c9bce39e63750e525e3aebeb359d271ca1f0e111f1d" dependencies = [ - "atk-sys", "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", + "gdk4-sys", "glib-sys", "gobject-sys", + "graphene-sys", "libc", "pango-sys", "system-deps", ] [[package]] -name = "gtk3-macros" -version = "0.16.3" +name = "gtk4" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9376d14d7e33486c54823a42bef296e882b9f25cb4c52b52f4d1d57bbadb5b6d" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "096eb63c6fedf03bafe65e5924595785eaf1bcb7200dac0f2cbe9c9738f05ad8" +checksum = "a7c518d5dd41c57385c7cd30af52e261820c897fc1144e558bb88c303d048ae2" dependencies = [ - "anyhow", "proc-macro-crate", - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] -name = "hashbrown" -version = "0.15.2" +name = "gtk4-sys" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "e653b0a9001ba9be1ffddb9373bfe9a111f688222f5aeee2841481300d91b55a" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] [[package]] -name = "heck" -version = "0.4.1" +name = "hashbrown" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -619,12 +664,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "human-panic" version = "2.0.2" @@ -666,9 +705,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -711,10 +750,11 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -738,17 +778,11 @@ dependencies = [ "libc", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" -version = "0.2.161" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libredox" @@ -761,6 +795,16 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -799,17 +843,25 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "log", "wasi", "windows-sys 0.52.0", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "nom" version = "7.1.3" @@ -874,9 +926,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "os_info" -version = "3.8.2" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce" dependencies = [ "log", "serde", @@ -885,23 +937,21 @@ dependencies = [ [[package]] name = "pango" -version = "0.16.5" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" +checksum = "71e34e7ca2c52e3933d7e5251409a82b83725fa9d6d48fbdaacec056b3a0554a" dependencies = [ - "bitflags 1.3.2", "gio", "glib", "libc", - "once_cell", "pango-sys", ] [[package]] name = "pango-sys" -version = "0.16.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" +checksum = "84fd65917bf12f06544ae2bbc200abf9fc0a513a5a88a0fa81013893aef2b838" dependencies = [ "glib-sys", "gobject-sys", @@ -938,43 +988,18 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1039,9 +1064,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -1060,9 +1085,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1076,31 +1101,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "relm" -version = "0.24.1" +name = "relm4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031126cd60d39b0b42d02b9c9ee7870372ea926c9278131484b86f8798b1bb59" +checksum = "30837553c1a8cfea1a404c83ec387c5c8ff9358e1060b057c274c5daa5035ad1" dependencies = [ - "cairo-rs", + "flume", "fragile", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "libc", - "log", + "futures", + "gtk4", + "once_cell", + "relm4-css", + "relm4-macros", + "tokio", + "tracing", ] [[package]] -name = "relm-derive" -version = "0.24.0" +name = "relm4-components" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b7ffb79cebb8a9d0c9f85dda4ebe38e820cb8f2b00ff0f9b0c949364c6c9fe1" +checksum = "fb3d67f2982131c5e6047af4278d8fe750266767e57b58bc15f2e11e190eef36" +dependencies = [ + "once_cell", + "relm4", + "tracker", +] + +[[package]] +name = "relm4-css" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3b924557df1cddc687b60b313c4b76620fdbf0e463afa4b29f67193ccf37f9" + +[[package]] +name = "relm4-macros" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a895a7455441a857d100ca679bd24a92f91d28b5e3df63296792ac1af2eddde" dependencies = [ - "lazy_static", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -1127,30 +1169,36 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.213" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.213" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn", ] [[package]] @@ -1184,21 +1232,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] -name = "syn" -version = "1.0.109" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "lock_api", ] [[package]] name = "syn" -version = "2.0.85" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1207,12 +1253,12 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.2.2" +version = "7.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" dependencies = [ "cfg-expr", - "heck 0.5.0", + "heck", "pkg-config", "toml", "version-compare", @@ -1226,22 +1272,22 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn", ] [[package]] @@ -1258,6 +1304,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "pin-project-lite", +] + [[package]] name = "toml" version = "0.8.19" @@ -1267,7 +1323,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.22", + "toml_edit", ] [[package]] @@ -1281,26 +1337,66 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.15" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", - "winnow 0.5.40", + "winnow", ] [[package]] -name = "toml_edit" -version = "0.22.22" +name = "tracing" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.6.20", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracker" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5c98457ff700aaeefcd4a4a492096e78a2af1dd8523c66e94a3adb0fdbd415" +dependencies = [ + "tracker-macros", +] + +[[package]] +name = "tracker-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc19eb2373ccf3d1999967c26c3d44534ff71ae5d8b9dacf78f4b13132229e48" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1311,9 +1407,9 @@ checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" @@ -1342,12 +1438,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -1366,9 +1456,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -1377,24 +1467,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.85", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1402,38 +1491,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" - -[[package]] -name = "winapi" -version = "0.3.9" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "winapi-util" @@ -1444,12 +1517,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.52.0" @@ -1541,15 +1608,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.6.20" @@ -1583,5 +1641,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 0f15efd..6209e06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,22 +8,26 @@ rust-version = "1.80" [dependencies] dotenvy = "0.15" -gtk = "0.16" human-panic = "2.0" notify = "7.0" pulldown-cmark = "0.12" -rand = "^0.8" -regex = "^1.0" -relm = "0.24" -relm-derive = "0.24" +rand = "0.8" +regex = "1.0" +relm4 = "0.9" +relm4-components = "0.9" xdg = "2.1" [dependencies.chrono] version = "0.4" default-features = false +[dependencies.gtk] +package = "gtk4" +version = "0.9" +features = ["v4_12"] + [dependencies.log] -version = "^0.4" +version = "0.4" features = ["std"] [dependencies.todo-txt] diff --git a/src/add.rs b/src/add.rs deleted file mode 100644 index ea1ff83..0000000 --- a/src/add.rs +++ /dev/null @@ -1,30 +0,0 @@ -use gtk::prelude::*; - -#[derive(relm_derive::Msg)] -pub enum Msg { - Add(String), -} - -#[relm_derive::widget] -impl relm::Widget for Widget { - fn model(_: ()) {} - - fn update(&mut self, event: Msg) { - match event { - Msg::Add(_) => self.widgets.entry.set_text(""), - } - } - - view! { - gtk::Box { - orientation: gtk::Orientation::Vertical, - #[name="entry"] - gtk::Entry { - activate(entry) => Msg::Add(entry.text().to_string()), - }, - gtk::Label { - text: "Create a new task +project @context due:2042-01-01", - }, - } - } -} diff --git a/src/agenda.rs b/src/agenda.rs index 7c1df3c..eeeefef 100644 --- a/src/agenda.rs +++ b/src/agenda.rs @@ -1,106 +1,144 @@ -use crate::widgets::tasks::Msg::{Complete, Edit}; -use crate::widgets::Tasks; +use chrono::Datelike as _; use gtk::prelude::*; -#[derive(relm_derive::Msg)] -pub enum Msg { +#[derive(Debug)] +pub enum MsgInput { + CalendarChange(Change), + DateSelect(chrono::NaiveDate), + Update, +} + +#[derive(Debug)] +pub enum Change { + PrevMonth, + PrevYear, + NextMonth, + NextYear, +} + +#[derive(Debug)] +pub enum MsgOutput { Complete(Box), Edit(Box), - Selected, - Select(chrono::DateTime), - Update, +} + +macro_rules! create { + ($sender:ident) => {{ + let component = crate::widgets::tasks::Model::builder().launch(()).forward( + $sender.output_sender(), + |output| match output { + crate::widgets::task::MsgOutput::Complete(task) => MsgOutput::Complete(task), + crate::widgets::task::MsgOutput::Edit(task) => MsgOutput::Edit(task), + }, + ); + component + .widget() + .set_vscrollbar_policy(gtk::PolicyType::Never); + + component + }}; } macro_rules! update { - ($self:ident, $exp:ident, $task:ident, $get:ident, $list:ident, $date:ident) => { + ($self:ident, $exp:ident, $task:ident, $get:ident, $list:ident, $date:ident) => {{ + use relm4::ComponentController as _; + let tasks = $self.$get(&$list, $date); - $self.widgets.$exp.set_expanded(!tasks.is_empty()); - $self.widgets.$exp.set_sensitive(!tasks.is_empty()); - $self - .components - .$task - .emit(crate::widgets::tasks::Msg::Update(tasks)); - }; + $self.$exp.set_expanded(!tasks.is_empty()); + $self.$exp.set_sensitive(!tasks.is_empty()); + $self.$task.emit(crate::widgets::tasks::Msg::Update(tasks)); + }}; } -impl Widget { - fn update_tasks(&self) { - self.widgets.calendar.clear_marks(); +pub struct Model { + calendar: gtk::Calendar, + date: chrono::NaiveDate, + month_exp: gtk::Expander, + month: relm4::Controller, + past_exp: gtk::Expander, + past: relm4::Controller, + today_exp: gtk::Expander, + today: relm4::Controller, + tomorrow_exp: gtk::Expander, + tomorrow: relm4::Controller, + week_exp: gtk::Expander, + week: relm4::Controller, +} +impl Model { + fn update_tasks(&self) { let list = crate::application::tasks(); - let (y, m, d) = self.widgets.calendar.date(); - let date = chrono::naive::NaiveDate::from_ymd_opt(y as i32, m + 1, d); - - update!(self, past_exp, past, get_past_tasks, list, date); - update!(self, today_exp, today, get_today_tasks, list, date); - update!(self, tomorrow_exp, tomorrow, get_tomorrow_tasks, list, date); - update!(self, week_exp, week, get_week_tasks, list, date); - update!(self, month_exp, month, get_month_tasks, list, date); + let date = crate::date::from_glib(self.calendar.date()); + + update!(self, past_exp, past, past_tasks, list, date); + update!(self, today_exp, today, today_tasks, list, date); + update!(self, tomorrow_exp, tomorrow, tomorrow_tasks, list, date); + update!(self, week_exp, week, week_tasks, list, date); + update!(self, month_exp, month, month_tasks, list, date); } - fn get_past_tasks( + fn past_tasks( &self, list: &crate::tasks::List, - date: Option, + date: chrono::naive::NaiveDate, ) -> Vec { - self.get_tasks(list, None, date) + self.tasks(list, None, Some(date)) } - fn get_today_tasks( + fn today_tasks( &self, list: &crate::tasks::List, - date: Option, + date: chrono::naive::NaiveDate, ) -> Vec { - self.get_tasks(list, date, date.and_then(|x| x.succ_opt())) + self.tasks(list, Some(date), Some(date + chrono::Duration::days(1))) } - fn get_tomorrow_tasks( + fn tomorrow_tasks( &self, list: &crate::tasks::List, - date: Option, + date: chrono::naive::NaiveDate, ) -> Vec { - self.get_tasks( + self.tasks( list, - date.and_then(|x| x.succ_opt()), - date.map(|x| x + chrono::Duration::days(2)), + Some(date + chrono::Duration::days(1)), + Some(date + chrono::Duration::days(2)), ) } - fn get_week_tasks( + fn week_tasks( &self, list: &crate::tasks::List, - date: Option, + date: chrono::naive::NaiveDate, ) -> Vec { - self.get_tasks( + self.tasks( list, - date.map(|x| x + chrono::Duration::days(2)), - date.map(|x| x + chrono::Duration::weeks(1)), + Some(date + chrono::Duration::days(2)), + Some(date + chrono::Duration::weeks(1)), ) } - fn get_month_tasks( + fn month_tasks( &self, list: &crate::tasks::List, - date: Option, + date: chrono::naive::NaiveDate, ) -> Vec { - self.get_tasks( + self.tasks( list, - date.map(|x| x + chrono::Duration::weeks(1)), - date.map(|x| x + chrono::Duration::weeks(4)), + Some(date + chrono::Duration::weeks(1)), + Some(date + chrono::Duration::weeks(4)), ) } - fn get_tasks( + fn tasks( &self, list: &crate::tasks::List, start: Option, end: Option, ) -> Vec { - let (_, month, _) = self.widgets.calendar.date(); let preferences = crate::application::preferences(); - let tasks: Vec = list + let tasks = list .tasks .iter() .filter(|x| { @@ -116,117 +154,157 @@ impl Widget { false } }) - .map(|x| { - use chrono::Datelike; + .cloned() + .collect(); - let due_date = x.due_date.unwrap(); + tasks + } - if due_date.month0() == month { - self.widgets.calendar.mark_day(due_date.day()); - } + fn update_marks(&self) { + use chrono::Datelike as _; - x.clone() - }) - .collect(); + self.calendar.clear_marks(); - tasks + let list = crate::application::tasks(); + let date = self.calendar.date(); + let month = date.month() as u32; + let year = date.year(); + + for task in list.tasks { + let Some(due_date) = task.due_date else { + continue; + }; + + if due_date.year() == year && due_date.month() == month { + self.calendar.mark_day(due_date.day()); + } + } } } -#[relm_derive::widget] -impl relm::Widget for Widget { - fn model(_: ()) {} +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = chrono::NaiveDate; + type Input = MsgInput; + type Output = MsgOutput; + + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; + use relm4::ComponentController as _; + + let model = Self { + calendar: gtk::Calendar::new(), + date: init, + month: create!(sender), + month_exp: gtk::Expander::new(None), + past: create!(sender), + past_exp: gtk::Expander::new(None), + today: create!(sender), + today_exp: gtk::Expander::new(None), + tomorrow: create!(sender), + tomorrow_exp: gtk::Expander::new(None), + week: create!(sender), + week_exp: gtk::Expander::new(None), + }; + + let calendar = &model.calendar; + let month_exp = &model.month_exp; + let past_exp = &model.past_exp; + let today_exp = &model.today_exp; + let tomorrow_exp = &model.tomorrow_exp; + let week_exp = &model.week_exp; + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } + } - fn update(&mut self, event: Msg) { - use Msg::*; + fn update(&mut self, msg: Self::Input, _: relm4::ComponentSender) { + use MsgInput::*; - match event { - Complete(_) | Edit(_) => (), - Selected | Update => self.update_tasks(), - Select(date) => { - use chrono::Datelike; + match msg { + CalendarChange(change) => { + self.date = match change { + Change::NextMonth => self.date.checked_add_months(chrono::Months::new(1)), + Change::NextYear => self.date.checked_add_months(chrono::Months::new(12)), + Change::PrevMonth => self.date.checked_sub_months(chrono::Months::new(1)), + Change::PrevYear => self.date.checked_sub_months(chrono::Months::new(12)), + } + .unwrap(); - self.widgets - .calendar - .select_month(date.month0(), date.year() as u32); - self.widgets.calendar.select_day(date.day()); + self.update_marks(); } + DateSelect(date) => self.date = date, + Update => (), } + + self.update_tasks(); } view! { gtk::Box { - orientation: gtk::Orientation::Horizontal, - spacing: 10, + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 10, + gtk::Box { - orientation: gtk::Orientation::Vertical, - #[name="calendar"] - gtk::Calendar { - day_selected => Msg::Selected, + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + + #[local_ref] + calendar -> gtk::Calendar { + #[watch] + set_day: model.date.day() as i32, + #[watch] + set_month: model.date.month() as i32 - 1, + #[watch] + set_year: model.date.year(), + + connect_day_selected[sender] => move |this| { + sender.input(MsgInput::DateSelect(crate::date::from_glib(this.date()))); + }, + connect_next_month => MsgInput::CalendarChange(Change::NextMonth), + connect_next_year => MsgInput::CalendarChange(Change::NextYear), + connect_prev_month => MsgInput::CalendarChange(Change::PrevMonth), + connect_prev_year => MsgInput::CalendarChange(Change::PrevYear), }, gtk::Button { - child: { - padding: 5, - }, - label: "Today", - clicked => Msg::Select(chrono::Local::now()), + set_label: "Today", + connect_clicked => MsgInput::DateSelect(crate::date::today()), }, }, gtk::ScrolledWindow { - child: { - expand: true, - }, gtk::Box { - orientation: gtk::Orientation::Vertical, - #[name="past_exp"] - gtk::Expander { - label: Some("Past due"), - #[name="past"] - Tasks { - vscrollbar_policy: gtk::PolicyType::Never, - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), - }, + set_hexpand: true, + set_orientation: gtk::Orientation::Vertical, + set_vexpand: true, + + #[local_ref] + past_exp -> gtk::Expander { + set_child: Some(model.past.widget()), + set_label: Some("Past due"), }, - #[name="today_exp"] - gtk::Expander { - label: Some("Today"), - #[name="today"] - Tasks { - vscrollbar_policy: gtk::PolicyType::Never, - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), - }, + #[local_ref] + today_exp -> gtk::Expander { + set_child: Some(model.today.widget()), + set_label: Some("Today"), }, - #[name="tomorrow_exp"] - gtk::Expander { - label: Some("Tomorrow"), - #[name="tomorrow"] - Tasks { - vscrollbar_policy: gtk::PolicyType::Never, - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), - }, + #[local_ref] + tomorrow_exp -> gtk::Expander { + set_child: Some(model.tomorrow.widget()), + set_label: Some("Tomorrow"), }, - #[name="week_exp"] - gtk::Expander { - label: Some("This week"), - #[name="week"] - Tasks { - vscrollbar_policy: gtk::PolicyType::Never, - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), - }, + #[local_ref] + week_exp -> gtk::Expander { + set_child: Some(model.week.widget()), + set_label: Some("This week"), }, - #[name="month_exp"] - gtk::Expander { - label: Some("This month"), - #[name="month"] - Tasks { - vscrollbar_policy: gtk::PolicyType::Never, - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), - } + #[local_ref] + month_exp -> gtk::Expander { + set_child: Some(model.month.widget()), + set_label: Some("This month"), }, }, }, diff --git a/src/application/mod.rs b/src/application/mod.rs index 62a9dff..8ad3489 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -1,11 +1,539 @@ mod globals; mod preferences; -mod widget; pub use globals::preferences::get as preferences; -pub use globals::tasks::add as add_task; pub use globals::tasks::get as tasks; -pub use preferences::Preferences; -pub use widget::Widget; + +use globals::tasks::add as add_task; +use preferences::Preferences; + +use gtk::glib::clone; +use gtk::prelude::*; +use relm4::ComponentController as _; pub const NAME: &str = env!("CARGO_PKG_NAME"); + +#[derive(Clone, Copy, Debug)] +#[repr(u32)] +enum Page { + Inbox = 0, + Projects, + Contexts, + Agenda, + Flag, + Done, + Search, +} + +impl From for Page { + fn from(n: u32) -> Self { + match n { + 0 => Page::Inbox, + 1 => Page::Projects, + 2 => Page::Contexts, + 3 => Page::Agenda, + 4 => Page::Flag, + 5 => Page::Done, + 6 => Page::Search, + _ => panic!("Invalid page {n}"), + } + } +} + +impl From for u32 { + fn from(page: Page) -> u32 { + unsafe { std::mem::transmute(page) } + } +} + +#[derive(Debug)] +pub enum Msg { + Add(String), + Complete(Box), + Edit(Box), + EditCancel, + EditDone(Box), + Refresh, + Search(String), +} + +pub struct Model { + agenda: relm4::Controller, + contexts: relm4::Controller, + defered_button: gtk::CheckButton, + done_button: gtk::CheckButton, + done: relm4::Controller, + edit: relm4::Controller, + flag: relm4::Controller, + inbox: relm4::Controller, + logger: relm4::Controller, + notebook: gtk::Notebook, + projects: relm4::Controller, + search: relm4::Controller, + #[allow(dead_code)] + xdg: xdg::BaseDirectories, +} + +impl Model { + fn load_style(&self) { + let css = gtk::CssProvider::new(); + if let Some(stylesheet) = self.stylesheet() { + css.load_from_path(stylesheet); + + gtk::style_context_add_provider_for_display( + >k::gdk::Display::default().unwrap(), + &css, + 0, + ); + } else { + log::error!("Unable to find stylesheet"); + } + } + + fn stylesheet(&self) -> Option { + let mut stylesheet = "style_light.css"; + + if let Ok(theme) = std::env::var("GTK_THEME") { + if theme.ends_with(":dark") { + stylesheet = "style_dark.css"; + } + } else if let Some(setting) = gtk::Settings::default() { + if setting.is_gtk_application_prefer_dark_theme() { + stylesheet = "style_dark.css"; + } + } + + self.find_data_file(stylesheet) + } + + fn add_tab_widgets(&self, notebook: >k::Notebook) { + let n = notebook.n_pages(); + + for x in 0..n { + let page = notebook.nth_page(Some(x)).unwrap(); + let widget = self.tab_widget(x); + + notebook.set_tab_label(&page, Some(&widget)); + } + } + + fn tab_widget(&self, n: u32) -> gtk::Box { + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.set_homogeneous(false); + + let title = match n.into() { + Page::Inbox => "inbox", + Page::Projects => "projects", + Page::Contexts => "contexts", + Page::Agenda => "agenda", + Page::Flag => "flag", + Page::Done => "done", + Page::Search => "search", + }; + + if let Some(filename) = self.find_data_file(&format!("{title}.png")) { + let image = gtk::Image::from_file(filename); + image.set_icon_size(gtk::IconSize::Large); + vbox.append(&image); + } else { + log::error!("Unable to find resource '{title}.png'"); + } + + let label = gtk::Label::new(Some(title)); + vbox.append(&label); + + vbox + } + + #[cfg(not(debug_assertions))] + fn find_data_file(&self, stylesheet: &str) -> Option { + self.xdg.find_data_file(stylesheet) + } + + #[cfg(debug_assertions)] + #[allow(clippy::unnecessary_wraps)] + fn find_data_file(&self, stylesheet: &str) -> Option { + let mut path = std::path::PathBuf::new(); + + path.push("resources"); + path.push(stylesheet); + + Some(path) + } + + fn add(&mut self, text: &str) { + match add_task(text) { + Ok(_) => self.update_tasks(), + Err(err) => log::error!("Unable to create task: '{err}'"), + } + } + + fn complete(&mut self, task: &crate::tasks::Task) { + let id = task.id; + let mut list = tasks(); + + if let Some(ref mut t) = list.tasks.get_mut(id) { + if t.finished { + t.uncomplete(); + } else { + t.complete(); + } + } else { + return; + } + + let t = list.tasks[id].clone(); + + if t.finished { + if let Some(ref recurrence) = t.recurrence { + let due = if recurrence.strict && t.due_date.is_some() { + t.due_date.unwrap() + } else { + crate::date::today() + }; + + let mut new: crate::tasks::Task = t.clone(); + new.uncomplete(); + new.create_date = Some(crate::date::today()); + new.due_date = Some(recurrence.clone() + due); + + if let Some(threshold_date) = t.threshold_date { + new.threshold_date = Some(recurrence.clone() + threshold_date); + } + + list.append(new); + } + } + + match list.write() { + Ok(_) => { + if list.tasks[id].finished { + log::info!("Task done"); + } else { + log::info!("Task undone"); + } + } + Err(err) => log::error!("Unable to save tasks: {err}"), + }; + + self.update_tasks(); + } + + fn edit(&mut self, task: &crate::tasks::Task) { + self.edit + .emit(crate::edit::MsgInput::Set(Box::new(task.clone()))); + self.edit.widget().set_visible(true); + } + + fn save(&mut self, task: &crate::tasks::Task) { + let id = task.id; + let mut list = tasks(); + + if list.tasks.get_mut(id).is_some() { + list.tasks[id] = task.clone(); + } + + match list.write() { + Ok(_) => (), + Err(err) => log::error!("Unable to save tasks: {err}"), + }; + + log::info!("Task updated"); + + self.update_tasks(); + self.edit.widget().set_visible(false); + } + + fn search(&self, query: &str) { + if query.is_empty() { + self.notebook.set_current_page(Some(Page::Inbox.into())); + self.search.widget().set_visible(false); + } else { + self.search.widget().set_visible(true); + self.notebook.set_current_page(Some(Page::Search.into())); + } + + self.search + .emit(crate::search::MsgInput::UpdateFilter(query.to_string())); + } + + fn update_tasks(&self) { + let todo_file = match std::env::var("TODO_FILE") { + Ok(todo_file) => todo_file, + Err(err) => { + eprintln!("Launch this program via todo.sh: {err}"); + std::process::exit(1); + } + }; + + let done_file = match std::env::var("DONE_FILE") { + Ok(done_file) => done_file, + Err(err) => { + eprintln!("Launch this program via todo.sh: {err}"); + std::process::exit(1); + } + }; + + let list = crate::tasks::List::from_files(&todo_file, &done_file); + globals::tasks::replace(list); + + globals::preferences::replace(crate::application::Preferences { + defered: self.defered_button.is_active(), + done: self.done_button.is_active(), + }); + + self.agenda.sender().emit(crate::agenda::MsgInput::Update); + self.contexts + .sender() + .emit(crate::widgets::tags::MsgInput::Update); + self.done.sender().emit(crate::done::Msg::Update); + self.projects + .sender() + .emit(crate::widgets::tags::MsgInput::Update); + self.flag.sender().emit(crate::flag::Msg::Update); + self.inbox.sender().emit(crate::inbox::Msg::Update); + self.search.sender().emit(crate::search::MsgInput::Update); + } + + fn watch(&self, sender: relm4::ComponentSender) { + use notify::Watcher as _; + + let todo_dir = match std::env::var("TODO_DIR") { + Ok(todo_dir) => todo_dir, + Err(err) => { + eprintln!("Launch this program via todo.sh: {err}"); + std::process::exit(1); + } + }; + + let mut watcher = notify::recommended_watcher(move |res| match res { + Ok(_) => { + sender.input(Msg::Refresh); + log::info!("Tasks reloaded"); + } + Err(e) => log::warn!("watch error: {e:?}"), + }) + .unwrap(); + + log::debug!("watching {todo_dir} for changes"); + + if let Err(err) = watcher.watch( + std::path::PathBuf::from(todo_dir).as_path(), + notify::RecursiveMode::Recursive, + ) { + log::warn!("Unable to setup hot reload: {err}"); + } + } +} + +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = (); + type Input = Msg; + type Output = (); + + fn init( + _init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; + + let logger = crate::logger::Model::builder().launch(()).detach(); + + let agenda = crate::agenda::Model::builder() + .launch(crate::date::today()) + .forward(sender.input_sender(), |output| match output { + crate::agenda::MsgOutput::Complete(task) => Msg::Complete(task), + crate::agenda::MsgOutput::Edit(task) => Msg::Edit(task), + }); + + let contexts = crate::widgets::tags::Model::builder() + .launch(crate::widgets::tags::Type::Contexts) + .forward(sender.input_sender(), |output| match output { + crate::widgets::tags::MsgOutput::Complete(task) => Msg::Complete(task), + crate::widgets::tags::MsgOutput::Edit(task) => Msg::Edit(task), + }); + + let done = + crate::done::Model::builder() + .launch(()) + .forward(sender.input_sender(), |output| match output { + crate::widgets::task::MsgOutput::Complete(task) => Msg::Complete(task), + crate::widgets::task::MsgOutput::Edit(task) => Msg::Edit(task), + }); + + let edit = crate::edit::Model::builder() + .launch(crate::tasks::Task::new()) + .forward(sender.input_sender(), |output| match output { + crate::edit::MsgOutput::Cancel => Msg::EditCancel, + crate::edit::MsgOutput::Done(task) => Msg::EditDone(task), + }); + + let flag = + crate::flag::Model::builder() + .launch(()) + .forward(sender.input_sender(), |output| match output { + crate::widgets::task::MsgOutput::Complete(task) => Msg::Complete(task), + crate::widgets::task::MsgOutput::Edit(task) => Msg::Edit(task), + }); + + let inbox = + crate::inbox::Model::builder() + .launch(()) + .forward(sender.input_sender(), |output| match output { + crate::widgets::task::MsgOutput::Complete(task) => Msg::Complete(task), + crate::widgets::task::MsgOutput::Edit(task) => Msg::Edit(task), + }); + + let projects = crate::widgets::tags::Model::builder() + .launch(crate::widgets::tags::Type::Projects) + .forward(sender.input_sender(), |output| match output { + crate::widgets::tags::MsgOutput::Complete(task) => Msg::Complete(task), + crate::widgets::tags::MsgOutput::Edit(task) => Msg::Edit(task), + }); + + let search = crate::search::Model::builder() + .launch(String::new()) + .forward(sender.input_sender(), |output| match output { + crate::widgets::task::MsgOutput::Complete(task) => Msg::Complete(task), + crate::widgets::task::MsgOutput::Edit(task) => Msg::Edit(task), + }); + + let defered_button = gtk::CheckButton::with_label("Display defered tasks"); + defered_button.connect_toggled(clone!( + #[strong] + sender, + move |_| sender.input(Msg::Refresh) + )); + + let done_button = gtk::CheckButton::with_label("Display done tasks"); + done_button.connect_toggled(clone!( + #[strong] + sender, + move |_| sender.input(Msg::Refresh) + )); + + let model = Self { + agenda, + contexts, + defered_button, + done_button, + done, + edit, + flag, + inbox, + logger, + notebook: gtk::Notebook::new(), + projects, + search, + xdg: xdg::BaseDirectories::with_prefix(NAME.to_lowercase()).unwrap(), + }; + + let notebook = &model.notebook; + let widgets = view_output!(); + + model.load_style(); + model.add_tab_widgets(notebook); + model.update_tasks(); + model.search.widget().set_visible(false); + model.watch(sender); + + relm4::ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, _sender: relm4::ComponentSender) { + match msg { + Msg::Add(task) => self.add(&task), + Msg::Complete(task) => self.complete(&task), + Msg::EditCancel => self.edit.widget().set_visible(false), + Msg::EditDone(task) => self.save(&task), + Msg::Edit(task) => self.edit(&task), + Msg::Refresh => self.update_tasks(), + Msg::Search(query) => self.search(&query), + } + } + + view! { + gtk::ApplicationWindow { + set_title: NAME.into(), + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + gtk::HeaderBar { + set_title_widget: Some(>k::Label::new(NAME.into())), + + pack_start = >k::Button { + set_icon_name: "view-refresh", + set_tooltip_text: "Refresh".into(), + + connect_clicked => Msg::Refresh, + }, + pack_start = >k::MenuButton { + set_icon_name: "list-add", + set_tooltip_text: "Add".into(), + #[wrap(Some)] + set_popover = >k::Popover { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + gtk::Entry { + connect_activate[sender] => move |this| { + sender.input(Msg::Add(this.text().to_string())); + } + }, + gtk::Label { + set_text: "Create a new task +project @context due:2042-01-01", + }, + }, + }, + }, + pack_start = >k::MenuButton { + set_icon_name: "preferences-system", + set_tooltip_text: "Preferences".into(), + #[wrap(Some)] + set_popover = >k::Popover { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + append: &model.defered_button, + append: &model.done_button, + }, + }, + }, + + pack_end = model.logger.widget(), + pack_end = >k::SearchEntry { + connect_search_changed[sender] => move |this| { + sender.input(Msg::Search(this.text().to_string())); + }, + }, + }, + gtk::Paned { + set_hexpand: true, + set_vexpand: true, + set_orientation: gtk::Orientation::Horizontal, + set_wide_handle: true, + + #[wrap(Some)] + #[local_ref] + set_start_child = notebook -> gtk::Notebook { + set_tab_pos: gtk::PositionType::Left, + + append_page: (model.inbox.widget(), None::<>k::Label>), + append_page: (model.projects.widget(), None::<>k::Label>), + append_page: (model.contexts.widget(), None::<>k::Label>), + append_page: (model.agenda.widget(), None::<>k::Label>), + append_page: (model.flag.widget(), None::<>k::Label>), + append_page: (model.done.widget(), None::<>k::Label>), + append_page: (model.search.widget(), None::<>k::Label>), + }, + #[wrap(Some)] + set_end_child = model.edit.widget(), + }, + }, + connect_close_request => move |_| { + relm4::main_application().quit(); + gtk::glib::Propagation::Stop + }, + } + } +} diff --git a/src/application/widget.rs b/src/application/widget.rs deleted file mode 100644 index c80ce9e..0000000 --- a/src/application/widget.rs +++ /dev/null @@ -1,541 +0,0 @@ -use gtk::prelude::*; - -use crate::agenda::Msg::Complete as AgendaComplete; -use crate::agenda::Msg::Edit as AgendaEdit; -use crate::agenda::Widget as AgendaWidget; -use crate::done::Msg::Complete as DoneComplete; -use crate::done::Msg::Edit as DoneEdit; -use crate::done::Widget as DoneWidget; -use crate::edit::Msg::{Cancel, Done}; -use crate::edit::Widget as EditWidget; -use crate::flag::Msg::Complete as FlagComplete; -use crate::flag::Msg::Edit as FlagEdit; -use crate::flag::Widget as FlagWidget; -use crate::inbox::Msg::Complete as InboxComplete; -use crate::inbox::Msg::Edit as InboxEdit; -use crate::inbox::Widget as InboxWidget; -use crate::logger::Widget as LoggerWidget; -use crate::search::Msg::Complete as SearchComplete; -use crate::search::Msg::Edit as SearchEdit; -use crate::search::Widget as SearchWidget; -use crate::widgets::tags::Msg::Complete as TagsComplete; -use crate::widgets::tags::Msg::Edit as TagsEdit; -use crate::widgets::Tags as TagsWidget; - -#[repr(u32)] -enum Page { - Inbox = 0, - Projects, - Contexts, - Agenda, - Flag, - Done, - Search, -} - -impl std::convert::From for Page { - fn from(n: u32) -> Self { - match n { - 0 => Page::Inbox, - 1 => Page::Projects, - 2 => Page::Contexts, - 3 => Page::Agenda, - 4 => Page::Flag, - 5 => Page::Done, - 6 => Page::Search, - _ => panic!("Invalid page {n}"), - } - } -} - -impl From for i32 { - fn from(page: Page) -> i32 { - unsafe { std::mem::transmute(page) } - } -} - -pub struct Model { - relm: relm::Relm, - add: relm::Component, - add_popover: gtk::Popover, - pref_popover: gtk::Popover, - defered_button: gtk::CheckButton, - done_button: gtk::CheckButton, - #[allow(dead_code)] - xdg: xdg::BaseDirectories, -} - -#[derive(relm_derive::Msg)] -pub enum Msg { - Add, - Create(String), - Complete(Box), - Edit(Box), - EditCancel, - EditDone(Box), - Preferences, - Refresh, - Search(String), - SwitchPage, -} - -impl Widget { - fn load_style(&self) { - let screen = gtk::prelude::GtkWindowExt::screen(&self.widgets.window).unwrap(); - let css = gtk::CssProvider::new(); - if let Some(stylesheet) = self.get_stylesheet() { - match css.load_from_path(stylesheet.to_str().unwrap()) { - Ok(_) => (), - Err(err) => log::error!("Invalid CSS: {err}"), - } - - gtk::StyleContext::add_provider_for_screen(&screen, &css, 0); - } else { - log::error!("Unable to find stylesheet"); - } - } - - fn get_stylesheet(&self) -> Option { - let mut stylesheet = "style_light.css"; - - if let Ok(theme) = std::env::var("GTK_THEME") { - if theme.ends_with(":dark") { - stylesheet = "style_dark.css"; - } - } else if let Some(setting) = self.widgets.window.settings() { - use gtk::traits::SettingsExt; - - if setting.is_gtk_application_prefer_dark_theme() { - stylesheet = "style_dark.css"; - } - } - - self.find_data_file(stylesheet) - } - - #[cfg(not(debug_assertions))] - fn find_data_file(&self, stylesheet: &str) -> Option { - self.model.xdg.find_data_file(stylesheet) - } - - #[cfg(debug_assertions)] - #[allow(clippy::unnecessary_wraps)] - fn find_data_file(&self, stylesheet: &str) -> Option { - let mut path = std::path::PathBuf::new(); - - path.push("resources"); - path.push(stylesheet); - - Some(path) - } - - fn init_add_popover(&mut self) { - let add = &self.model.add; - - relm::connect!(add@crate::add::Msg::Add(ref text), self.model.relm, Msg::Create(text.clone())); - - self.model - .add_popover - .set_relative_to(Some(&self.widgets.add_button)); - self.model.add_popover.hide(); - } - - fn init_pref_popover(&self) { - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.show(); - - relm::connect!( - self.model.relm, - self.model.defered_button, - connect_toggled(_), - Msg::Refresh - ); - vbox.add(&self.model.defered_button); - self.model.defered_button.show(); - - relm::connect!( - self.model.relm, - self.model.done_button, - connect_toggled(_), - Msg::Refresh - ); - vbox.add(&self.model.done_button); - self.model.done_button.show(); - - self.model - .pref_popover - .set_relative_to(Some(&self.widgets.pref_button)); - self.model.pref_popover.add(&vbox); - self.model.pref_popover.hide(); - } - - fn replace_tab_widgets(&self) { - let n = self.widgets.notebook.n_pages(); - - for x in 0..n { - let page = self.widgets.notebook.nth_page(Some(x)).unwrap(); - let widget = self.get_tab_widget(x); - - self.widgets.notebook.set_tab_label(&page, Some(&widget)); - } - } - - fn get_tab_widget(&self, n: u32) -> gtk::Box { - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - let title = match n.into() { - Page::Inbox => "inbox", - Page::Projects => "projects", - Page::Contexts => "contexts", - Page::Agenda => "agenda", - Page::Flag => "flag", - Page::Done => "done", - Page::Search => "search", - }; - - if let Some(filename) = self.find_data_file(format!("{title}.png").as_str()) { - let image = gtk::Image::from_file(filename); - vbox.pack_start(&image, false, false, 0); - } else { - log::error!("Unable to find resource '{title}.png'"); - } - - let label = gtk::Label::new(Some(title)); - vbox.pack_start(&label, false, false, 0); - - vbox.show_all(); - - vbox - } - - fn add(&self) { - self.model.add_popover.popup(); - } - - fn create(&mut self, text: &str) { - match super::add_task(text) { - Ok(_) => self.update_tasks(), - Err(err) => log::error!("Unable to create task: '{err}'"), - } - - self.model.add_popover.popdown(); - } - - fn complete(&mut self, task: &crate::tasks::Task) { - let id = task.id; - let mut list = super::tasks(); - - if let Some(ref mut t) = list.tasks.get_mut(id) { - if t.finished { - t.uncomplete(); - } else { - t.complete(); - } - } else { - return; - } - - let t = list.tasks[id].clone(); - - if t.finished { - if let Some(ref recurrence) = t.recurrence { - let due = if recurrence.strict && t.due_date.is_some() { - t.due_date.unwrap() - } else { - crate::date::today() - }; - - let mut new: crate::tasks::Task = t.clone(); - new.uncomplete(); - new.create_date = Some(crate::date::today()); - new.due_date = Some(recurrence.clone() + due); - - if let Some(threshold_date) = t.threshold_date { - new.threshold_date = Some(recurrence.clone() + threshold_date); - } - - list.append(new); - } - } - - match list.write() { - Ok(_) => { - if list.tasks[id].finished { - log::info!("Task done"); - } else { - log::info!("Task undone"); - } - } - Err(err) => log::error!("Unable to save tasks: {err}"), - }; - - self.update_tasks(); - } - - fn edit(&mut self, task: &crate::tasks::Task) { - use relm::Widget; - - self.components - .edit - .emit(crate::edit::Msg::Set(Box::new(task.clone()))); - self.widgets.edit.show(); - - let (width, _) = self.root().size(); - self.widgets.paned.set_position(width - 436); - } - - fn save(&mut self, task: &crate::tasks::Task) { - let id = task.id; - let mut list = super::tasks(); - - if list.tasks.get_mut(id).is_some() { - list.tasks[id] = task.clone(); - } - - match list.write() { - Ok(_) => (), - Err(err) => log::error!("Unable to save tasks: {err}"), - }; - - log::info!("Task updated"); - - self.update_tasks(); - self.widgets.edit.hide(); - } - - fn search(&self, text: &str) { - if text.is_empty() { - self.widgets.notebook.set_page(Page::Inbox.into()); - self.widgets.search.hide(); - } else { - self.widgets.search.show(); - self.widgets.notebook.set_page(Page::Search.into()); - } - - self.components - .search - .emit(crate::search::Msg::UpdateFilter(text.to_string())); - } - - fn update_tasks(&mut self) { - let todo_file = match std::env::var("TODO_FILE") { - Ok(todo_file) => todo_file, - Err(err) => { - eprintln!("Launch this program via todo.sh: {err}"); - std::process::exit(1); - } - }; - - let done_file = match std::env::var("DONE_FILE") { - Ok(done_file) => done_file, - Err(err) => { - eprintln!("Launch this program via todo.sh: {err}"); - std::process::exit(1); - } - }; - - let list = crate::tasks::List::from_files(&todo_file, &done_file); - super::globals::tasks::replace(list); - - super::globals::preferences::replace(crate::application::Preferences { - defered: self.model.defered_button.is_active(), - done: self.model.done_button.is_active(), - }); - - self.components.inbox.emit(crate::inbox::Msg::Update); - self.components - .projects - .emit(crate::widgets::tags::Msg::Update); - self.components - .contexts - .emit(crate::widgets::tags::Msg::Update); - self.components.agenda.emit(crate::agenda::Msg::Update); - self.components.done.emit(crate::done::Msg::Update); - self.components.flag.emit(crate::flag::Msg::Update); - self.components.search.emit(crate::search::Msg::Update); - } - - fn preferences(&self) { - self.model.pref_popover.popup(); - } - - fn watch(&self) { - use notify::Watcher; - - let stream = self.model.relm.stream().clone(); - - let (_, sender) = relm::Channel::new(move |_| { - stream.emit(Msg::Refresh); - log::info!("Tasks reloaded"); - }); - - let todo_dir = match std::env::var("TODO_DIR") { - Ok(todo_dir) => todo_dir, - Err(err) => { - eprintln!("Launch this program via todo.sh: {err}"); - std::process::exit(1); - } - }; - - let mut watcher = notify::recommended_watcher(move |res| match res { - Ok(_) => { - sender.send(()).expect("send message"); - } - Err(e) => log::warn!("watch error: {e:?}"), - }) - .unwrap(); - - log::debug!("watching {todo_dir} for changes"); - - if let Err(err) = watcher.watch( - std::path::PathBuf::from(todo_dir).as_path(), - notify::RecursiveMode::Recursive, - ) { - log::warn!("Unable to setup hot reload: {err}"); - } - } -} - -#[relm_derive::widget] -impl relm::Widget for Widget { - fn init_view(&mut self) { - self.widgets.edit.hide(); - self.widgets.search.hide(); - - self.load_style(); - self.init_add_popover(); - self.init_pref_popover(); - self.replace_tab_widgets(); - self.update_tasks(); - - self.watch(); - } - - fn model(relm: &relm::Relm, _: ()) -> Model { - use relm::ContainerWidget; - - let add_popover = gtk::Popover::new(None::<>k::Button>); - - Model { - relm: relm.clone(), - add: add_popover.add_widget::(()), - add_popover, - pref_popover: gtk::Popover::new(None::<>k::Button>), - defered_button: gtk::CheckButton::with_label("Display defered tasks"), - done_button: gtk::CheckButton::with_label("Display done tasks"), - xdg: xdg::BaseDirectories::with_prefix(super::NAME.to_lowercase()).unwrap(), - } - } - - fn update(&mut self, event: Msg) { - use Msg::*; - - match event { - Add => self.add(), - Create(text) => self.create(&text), - Complete(task) => self.complete(&task), - Edit(task) => self.edit(&task), - EditDone(task) => self.save(&task), - EditCancel | SwitchPage => self.widgets.edit.hide(), - Preferences => self.preferences(), - Refresh => self.update_tasks(), - Search(text) => self.search(&text), - } - } - - view! { - #[name="window"] - gtk::Window { - title: super::NAME, - gtk::Box { - orientation: gtk::Orientation::Vertical, - gtk::HeaderBar { - title: Some(super::NAME), - show_close_button: true, - gtk::ToolButton { - icon_name: Some("view-refresh"), - label: Some("Refresh"), - clicked => Msg::Refresh, - }, - #[name="add_button"] - gtk::ToolButton { - icon_name: Some("list-add"), - label: Some("Add"), - clicked => Msg::Add, - }, - #[name="pref_button"] - gtk::ToolButton { - icon_name: Some("preferences-system"), - label: Some("Preferences"), - clicked => Msg::Preferences, - }, - gtk::SearchEntry { - child: { - pack_type: gtk::PackType::End, - }, - search_changed(entry) => Msg::Search(entry.text().to_string()), - }, - LoggerWidget { - child: { - pack_type: gtk::PackType::End, - }, - }, - }, - #[name="paned"] - gtk::Paned { - child: { - expand: true, - fill: true, - }, - orientation: gtk::Orientation::Horizontal, - wide_handle: true, - #[name="notebook"] - gtk::Notebook { - tab_pos: gtk::PositionType::Left, - #[name="inbox"] - InboxWidget { - InboxComplete(ref task) => Msg::Complete(task.clone()), - InboxEdit(ref task) => Msg::Edit(task.clone()), - }, - #[name="projects"] - TagsWidget(crate::widgets::tags::Type::Projects) { - TagsComplete(ref task) => Msg::Complete(task.clone()), - TagsEdit(ref task) => Msg::Edit(task.clone()), - }, - #[name="contexts"] - TagsWidget(crate::widgets::tags::Type::Contexts) { - TagsComplete(ref task) => Msg::Complete(task.clone()), - TagsEdit(ref task) => Msg::Edit(task.clone()), - }, - #[name="agenda"] - AgendaWidget { - AgendaComplete(ref task) => Msg::Complete(task.clone()), - AgendaEdit(ref task) => Msg::Edit(task.clone()), - }, - #[name="flag"] - FlagWidget { - FlagComplete(ref task) => Msg::Complete(task.clone()), - FlagEdit(ref task) => Msg::Edit(task.clone()), - }, - #[name="done"] - DoneWidget { - DoneComplete(ref task) => Msg::Complete(task.clone()), - DoneEdit(ref task) => Msg::Edit(task.clone()), - }, - #[name="search"] - SearchWidget { - SearchComplete(ref task) => Msg::Complete(task.clone()), - SearchEdit(ref task) => Msg::Edit(task.clone()), - }, - switch_page(_, _, _) => Msg::SwitchPage, - }, - #[name="edit"] - EditWidget { - Cancel => Msg::EditCancel, - Done(ref task) => Msg::EditDone(task.clone()), - }, - }, - }, - delete_event(_, _) => (gtk::main_quit(), gtk::Inhibit(false)), - } - } -} diff --git a/src/date.rs b/src/date.rs index d9f9ebe..aed28f3 100644 --- a/src/date.rs +++ b/src/date.rs @@ -1,3 +1,11 @@ -pub fn today() -> chrono::naive::NaiveDate { +pub fn today() -> chrono::NaiveDate { chrono::Local::now().date_naive() } + +pub fn from_glib(value: gtk::glib::DateTime) -> chrono::NaiveDate { + let y = value.year(); + let m = value.month() as u32; + let d = value.day_of_month() as u32; + + chrono::NaiveDate::from_ymd_opt(y, m, d).unwrap() +} diff --git a/src/done.rs b/src/done.rs index ef5f7a6..1464b81 100644 --- a/src/done.rs +++ b/src/done.rs @@ -1,42 +1,61 @@ -use crate::widgets::tasks::Msg::{Complete, Edit}; -use crate::widgets::Tasks; +use gtk::prelude::*; -#[derive(relm_derive::Msg)] +#[derive(Debug)] pub enum Msg { - Complete(Box), - Edit(Box), Update, } -impl Widget { +pub struct Model { + tasks: relm4::Controller, +} + +impl Model { fn update_tasks(&mut self) { + use relm4::ComponentController as _; + let list = crate::application::tasks(); let tasks = list.tasks.iter().filter(|x| x.finished).cloned().collect(); - self.components - .tasks + self.tasks + .sender() .emit(crate::widgets::tasks::Msg::Update(tasks)); } } -#[relm_derive::widget] -impl relm::Widget for Widget { - fn model(_: ()) {} +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = (); + type Input = Msg; + type Output = crate::widgets::task::MsgOutput; + + fn init( + _init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; + use relm4::ComponentController as _; - fn update(&mut self, event: Msg) { - use Msg::*; + let tasks = crate::widgets::tasks::Model::builder() + .launch(()) + .forward(sender.output_sender(), std::convert::identity); + + let model = Self { tasks }; + + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } + } - match event { - Complete(_) | Edit(_) => (), - Update => self.update_tasks(), + fn update(&mut self, msg: Self::Input, _sender: relm4::ComponentSender) { + match msg { + Msg::Update => self.update_tasks(), } } view! { - #[name="tasks"] - Tasks { - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), + gtk::Box { + append: model.tasks.widget(), } } } diff --git a/src/edit.rs b/src/edit.rs index 076a62a..9020752 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -1,258 +1,261 @@ -use crate::widgets::calendar::Msg::Updated as CalendarUpdated; -use crate::widgets::keywords::Msg::Updated as KeywordsUpdated; -use crate::widgets::priority::Msg::Updated as PriorityUpdated; -use crate::widgets::repeat::Msg::Updated as RepeatUpdated; -use crate::widgets::{Calendar, Keywords, Priority, Repeat}; use gtk::prelude::*; +use relm4::ComponentController as _; -#[derive(relm_derive::Msg)] -pub enum Msg { - Cancel, - EditKeyword(std::collections::BTreeMap), - Flag, - Done(Box), +#[derive(Debug)] +pub enum MsgInput { Ok, + Flag(bool), Set(Box), UpdateDate(DateType, Option), - UpdateRepeat(Option), - UpdatePriority(u8), + UpdateKeywords(std::collections::BTreeMap), + UpdatePriority(todo_txt::Priority), + UpdateRecurrence(Option), +} + +#[derive(Debug)] +pub enum MsgOutput { + Cancel, + Done(Box), } pub struct Model { + buffer: gtk::TextBuffer, + created: relm4::Controller, + due: relm4::Controller, + finish: relm4::Controller, + keywords: relm4::Controller, + priority: relm4::Controller, + recurrence: relm4::Controller, + threshold: relm4::Controller, task: crate::tasks::Task, - relm: relm::Relm, } -#[derive(Clone, Copy)] +impl Model { + fn update_date(&mut self, date_type: DateType, date: Option) { + use DateType::*; + + match date_type { + Due => self.task.due_date = date, + Threshold => self.task.threshold_date = date, + Finish => { + self.task.finish_date = date; + self.task.finished = date.is_some(); + } + } + } +} + +#[derive(Clone, Copy, Debug)] pub enum DateType { Due, Threshold, Finish, } -impl Widget { - fn set_task(&mut self, task: &crate::tasks::Task) { - self.model.task = task.clone(); - - self.widgets.subject.set_text(task.subject.as_str()); - self.components - .priority - .emit(crate::widgets::priority::Msg::Set( - task.priority.clone().into(), - )); - self.widgets.flag.set_active(task.flagged); - self.components - .due - .emit(crate::widgets::calendar::Msg::Set(task.due_date)); - self.components - .threshold - .emit(crate::widgets::calendar::Msg::Set(task.threshold_date)); - if task.create_date.is_some() { - self.components - .created - .emit(crate::widgets::calendar::Msg::Set(task.create_date)); - self.widgets.created.show(); - } else { - self.widgets.created.hide(); - } - self.components - .repeat - .emit(crate::widgets::repeat::Msg::Set(task.recurrence.clone())); - self.components - .finish - .emit(crate::widgets::calendar::Msg::Set(task.finish_date)); - self.components - .keywords - .emit(crate::widgets::keywords::Msg::Set(task.tags.clone())); - - let note = task.note.content().unwrap_or_default(); - let buffer = self.widgets.note.buffer().unwrap(); - buffer.set_text(note.as_str()); - } +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = crate::tasks::Task; + type Input = MsgInput; + type Output = MsgOutput; - fn get_task(&self) -> crate::tasks::Task { - let mut task = self.model.task.clone(); + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; - task.subject = self.widgets.subject.text().to_string(); + let created = crate::widgets::calendar::Model::builder() + .launch("Created") + .detach(); + created.widget().set_sensitive(false); - let new_note = self.get_note(); - task.note = match task.note { - todo_txt::task::Note::Long { ref filename, .. } => todo_txt::task::Note::Long { - filename: filename.to_string(), - content: new_note, - }, - _ => { - if new_note.is_empty() { - todo_txt::task::Note::None - } else { - todo_txt::task::Note::Short(new_note) + let due = crate::widgets::calendar::Model::builder() + .launch("Due") + .forward(sender.input_sender(), |output| match output { + crate::widgets::calendar::MsgOutput::Updated(date) => { + MsgInput::UpdateDate(DateType::Due, date) } - } - }; + }); - task - } + let keywords = crate::widgets::keywords::Model::builder() + .launch(init.tags.clone()) + .forward(sender.input_sender(), |output| match output { + crate::widgets::keywords::MsgOutput::Updated(keywords) => { + MsgInput::UpdateKeywords(keywords) + } + }); - fn get_note(&self) -> String { - let Some(buffer) = self.widgets.note.buffer() else { - return String::new(); - }; - let start = buffer.start_iter(); - let end = buffer.end_iter(); + let finish = crate::widgets::calendar::Model::builder() + .launch("Completed") + .forward(sender.input_sender(), |output| match output { + crate::widgets::calendar::MsgOutput::Updated(date) => { + MsgInput::UpdateDate(DateType::Finish, date) + } + }); - buffer.text(&start, &end, false).expect("").to_string() - } + let priority = crate::widgets::priority::Model::builder() + .launch(init.priority.clone()) + .forward(sender.input_sender(), |output| match output { + crate::widgets::priority::MsgOutput::Updated(priority) => { + MsgInput::UpdatePriority(priority) + } + }); - fn edit_keywords(&mut self, keywords: &std::collections::BTreeMap) { - self.model.task.tags = keywords.clone(); - } + let recurrence = crate::widgets::recurrence::Model::builder() + .launch(init.recurrence.clone()) + .forward(sender.input_sender(), |output| match output { + crate::widgets::recurrence::MsgOutput::Updated(recurrence) => { + MsgInput::UpdateRecurrence(recurrence) + } + }); - fn flag(&mut self) { - self.model.task.flagged = self.widgets.flag.is_active(); - } + let threshold = crate::widgets::calendar::Model::builder() + .launch("Defer until") + .forward(sender.input_sender(), |output| match output { + crate::widgets::calendar::MsgOutput::Updated(date) => { + MsgInput::UpdateDate(DateType::Threshold, date) + } + }); - fn update_date(&mut self, date_type: DateType, date: Option) { - use DateType::*; + let model = Self { + buffer: gtk::TextBuffer::new(None), + created, + due, + finish, + threshold, + keywords, + priority, + task: init, + recurrence, + }; - match date_type { - Due => self.model.task.due_date = date, - Threshold => self.model.task.threshold_date = date, - Finish => { - self.model.task.finish_date = date; - self.model.task.finished = date.is_some(); - } - } - } + let widgets = view_output!(); - fn update_repeat(&mut self, recurrence: &Option) { - self.model.task.recurrence.clone_from(recurrence); - } + let note = model.task.note.content().unwrap_or_default(); + widgets.note.set_buffer(Some(&model.buffer)); + model.buffer.set_text(¬e); - fn update_priority(&mut self, priority: u8) { - self.model.task.priority = priority.into(); + relm4::ComponentParts { model, widgets } } -} -#[allow(clippy::cognitive_complexity)] -#[relm_derive::widget] -impl relm::Widget for Widget { - fn init_view(&mut self) { - self.widgets.note.set_height_request(150); - self.widgets.created.set_sensitive(false); - } + fn update(&mut self, msg: Self::Input, sender: relm4::ComponentSender) { + use MsgInput::*; - fn model(relm: &relm::Relm, _: ()) -> Model { - Model { - task: crate::tasks::Task::new(), - relm: relm.clone(), - } - } + match msg { + Flag(flagged) => self.task.flagged = flagged, + Ok => { + let start = self.buffer.start_iter(); + let end = self.buffer.start_iter(); + self.task.note = self.buffer.text(&start, &end, true).to_string().into(); - fn update(&mut self, event: Msg) { - use Msg::*; - - match event { - Cancel | Done(_) => (), - EditKeyword(ref keywords) => self.edit_keywords(keywords), - Flag => self.flag(), - Ok => self - .model - .relm - .stream() - .emit(Msg::Done(Box::new(self.get_task()))), - Set(task) => self.set_task(&task), - UpdateDate(ref date_type, ref date) => self.update_date(*date_type, *date), - UpdateRepeat(ref recurrence) => self.update_repeat(recurrence), - UpdatePriority(priority) => self.update_priority(priority), + sender + .output(MsgOutput::Done(Box::new(self.task.clone()))) + .ok(); + } + Set(task) => { + self.task = *task; + self.created.emit(crate::widgets::calendar::MsgInput::Set( + self.task.create_date, + )); + self.due + .emit(crate::widgets::calendar::MsgInput::Set(self.task.due_date)); + self.finish.emit(crate::widgets::calendar::MsgInput::Set( + self.task.finish_date, + )); + self.keywords.emit(crate::widgets::keywords::MsgInput::Set( + self.task.tags.clone(), + )); + self.threshold.emit(crate::widgets::calendar::MsgInput::Set( + self.task.threshold_date, + )); + } + UpdateDate(date_type, date) => self.update_date(date_type, date), + UpdateKeywords(keywords) => self.task.tags = keywords, + UpdatePriority(priority) => self.task.priority = priority, + UpdateRecurrence(recurrence) => self.task.recurrence = recurrence, } } view! { gtk::ScrolledWindow { + set_visible: false, + set_width_request: 172, + gtk::Box { - orientation: gtk::Orientation::Vertical, - spacing: 10, + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + gtk::Frame { - label: Some("Subject"), - #[name="subject"] + set_label: Some("Subject"), gtk::Entry { - activate => Msg::Ok, + #[watch] + set_text: &model.task.subject, + connect_activate => MsgInput::Ok, }, }, gtk::Frame { - label: Some("Priority"), + set_label: Some("Priority"), gtk::Box { - orientation: gtk::Orientation::Horizontal, - #[name="priority"] - Priority { - PriorityUpdated(priority) => Msg::UpdatePriority(priority), - }, - #[name="flag"] + set_orientation: gtk::Orientation::Horizontal, + + append: model.priority.widget(), + gtk::ToggleButton { - child: { - expand: true, + set_hexpand: true, + set_halign: gtk::Align::Center, + set_icon_name: "emblem-favorite", + set_tooltip_text: Some("Flag"), + #[watch] + set_active: model.task.flagged, + + connect_toggled[sender] => move |button| { + sender.input(MsgInput::Flag(button.is_active())); }, - halign: gtk::Align::Center, - image: Some(>k::Image::from_icon_name(Some("emblem-favorite"), gtk::IconSize::SmallToolbar)), - tooltip_text: Some("Flag"), - toggled => Msg::Flag, }, }, }, gtk::Frame { - label: Some("Date"), - gtk::Box { - spacing: 10, - orientation: gtk::Orientation::Vertical, - #[name="threshold"] - Calendar("Defer until".to_string()) { - CalendarUpdated(date) => Msg::UpdateDate(DateType::Threshold, date), - }, - #[name="due"] - Calendar("Due".to_string()) { - CalendarUpdated(date) => Msg::UpdateDate(DateType::Due, date), - }, - #[name="finish"] - Calendar("Completed".to_string()) { - CalendarUpdated(date) => Msg::UpdateDate(DateType::Finish, date), - }, - #[name="created"] - Calendar("Created".to_string()), - }, + set_label: Some("Repeat"), + set_child: Some(model.recurrence.widget()), }, gtk::Frame { - label: Some("Repeat"), - #[name="repeat"] - Repeat { - RepeatUpdated(ref recurrence) => Msg::UpdateRepeat(recurrence.clone()), + set_label: Some("Date"), + gtk::Box { + set_spacing: 10, + set_orientation: gtk::Orientation::Vertical, + + append: model.threshold.widget(), + append: model.due.widget(), + append: model.finish.widget(), + append: model.created.widget(), }, }, gtk::Frame { - label: Some("Keywords"), - #[name="keywords"] - Keywords { - KeywordsUpdated(ref keywords) => Msg::EditKeyword(keywords.clone()), - }, + set_label: Some("Keywords"), + + set_child: Some(model.keywords.widget()), }, gtk::Frame { - label: Some("Note"), - #[name="note"] + set_label: Some("Note"), + + #[name = "note"] gtk::TextView { + set_hexpand: true, + set_vexpand: true, }, }, gtk::ActionBar { - child: { - pack_type: gtk::PackType::End, + pack_start = >k::Button { + set_label: "Ok", + + connect_clicked => MsgInput::Ok, }, - gtk::ButtonBox { - orientation: gtk::Orientation::Horizontal, - gtk::Button { - label: "Ok", - clicked => Msg::Ok, - }, - gtk::Button { - label: "Cancel", - clicked => Msg::Cancel, + pack_start = >k::Button { + set_label: "Cancel", + + connect_clicked[sender] => move |_| { + sender.output(MsgOutput::Cancel).ok(); }, }, }, diff --git a/src/flag.rs b/src/flag.rs index cd67b86..fc60778 100644 --- a/src/flag.rs +++ b/src/flag.rs @@ -1,15 +1,18 @@ -use crate::widgets::tasks::Msg::{Complete, Edit}; -use crate::widgets::Tasks; +use gtk::prelude::*; -#[derive(relm_derive::Msg)] +#[derive(Debug)] pub enum Msg { - Complete(Box), - Edit(Box), Update, } -impl Widget { +pub struct Model { + tasks: relm4::Controller, +} + +impl Model { fn update_tasks(&mut self) { + use relm4::ComponentController as _; + let today = crate::date::today(); let list = crate::application::tasks(); let preferences = crate::application::preferences(); @@ -27,30 +30,46 @@ impl Widget { .cloned() .collect(); - self.components - .tasks + self.tasks + .sender() .emit(crate::widgets::tasks::Msg::Update(tasks)); } } -#[relm_derive::widget] -impl relm::Widget for Widget { - fn model(_: ()) {} +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = (); + type Input = Msg; + type Output = crate::widgets::task::MsgOutput; + + fn init( + _init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; + use relm4::ComponentController as _; - fn update(&mut self, event: Msg) { - use Msg::*; + let tasks = crate::widgets::tasks::Model::builder() + .launch(()) + .forward(sender.output_sender(), std::convert::identity); + + let model = Self { tasks }; + + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } + } - match event { - Complete(_) | Edit(_) => (), - Update => self.update_tasks(), + fn update(&mut self, msg: Self::Input, _sender: relm4::ComponentSender) { + match msg { + Msg::Update => self.update_tasks(), } } view! { - #[name="tasks"] - Tasks { - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), + gtk::Box { + append: model.tasks.widget(), } } } diff --git a/src/inbox.rs b/src/inbox.rs index 6ef3408..479f708 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -1,15 +1,18 @@ -use crate::widgets::tasks::Msg::{Complete, Edit}; -use crate::widgets::Tasks; +use gtk::prelude::*; -#[derive(relm_derive::Msg)] +#[derive(Debug)] pub enum Msg { - Complete(Box), - Edit(Box), Update, } -impl Widget { - fn update_tasks(&self) { +pub struct Model { + tasks: relm4::Controller, +} + +impl Model { + fn update_tasks(&mut self) { + use relm4::ComponentController as _; + let today = crate::date::today(); let list = crate::application::tasks(); @@ -27,30 +30,46 @@ impl Widget { .cloned() .collect(); - self.components - .tasks + self.tasks + .sender() .emit(crate::widgets::tasks::Msg::Update(tasks)); } } -#[relm_derive::widget] -impl relm::Widget for Widget { - fn model() {} +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = (); + type Input = Msg; + type Output = crate::widgets::task::MsgOutput; + + fn init( + _init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; + use relm4::ComponentController as _; - fn update(&mut self, event: Msg) { - use Msg::*; + let tasks = crate::widgets::tasks::Model::builder() + .launch(()) + .forward(sender.output_sender(), std::convert::identity); + + let model = Self { tasks }; + + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } + } - match event { - Complete(_) | Edit(_) => (), - Update => self.update_tasks(), + fn update(&mut self, msg: Self::Input, _sender: relm4::ComponentSender) { + match msg { + Msg::Update => self.update_tasks(), } } view! { - #[name="tasks"] - Tasks { - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), + gtk::Box { + append: model.tasks.widget(), } } } diff --git a/src/logger.rs b/src/logger.rs index 436365a..b9bcdc2 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,23 +1,10 @@ +use gtk::glib::clone; use gtk::prelude::*; type ChannelData = (log::Level, String); type Sender = std::sync::mpsc::Sender; type Receiver = std::sync::mpsc::Receiver; -#[derive(relm_derive::Msg)] -pub enum Msg { - Clear, - Hide, - Read(gtk::ListBoxRow), - Show, - Update(gtk::ListBox), -} - -pub struct Model { - relm: relm::Relm, - popover: gtk::Popover, -} - pub struct Log { tx: std::sync::Mutex, } @@ -45,205 +32,124 @@ impl log::Log for Log { } thread_local!( - static GLOBAL: std::cell::RefCell> + static GLOBAL: std::cell::RefCell, Receiver)>> = const { std::cell::RefCell::new(None) } ); -impl Widget { - fn init(&self) { - let (tx, rx) = std::sync::mpsc::channel(); - let log = Log::new(tx); - - log::set_max_level(log::LevelFilter::Info); - log::set_boxed_logger(Box::new(log)).unwrap_or_default(); - - let popover = &self.model.popover; - popover.set_height_request(500); - popover.set_relative_to(Some(&self.widgets.toggle)); - popover.set_border_width(5); - relm::connect!(self.model.relm, popover, connect_hide(_), Msg::Hide); - - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - popover.add(&vbox); - - let context = popover.style_context(); - context.add_class("log"); - - let scrolled_window = - gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); - scrolled_window.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic); - vbox.pack_start(&scrolled_window, true, true, 0); +#[derive(Debug)] +pub enum Msg { + Add(ChannelData), + Clear, + Read(gtk::ListBoxRow), +} - let list_box = gtk::ListBox::new(); - relm::connect!( - self.model.relm, - list_box, - connect_row_activated(_, row), - Msg::Read(row.clone()) - ); - relm::connect!( - self.model.relm, - list_box, - connect_add(sender, _), - Msg::Update(sender.clone()) - ); - relm::connect!( - self.model.relm, - list_box, - connect_remove(sender, _), - Msg::Update(sender.clone()) - ); - scrolled_window.add(&list_box); - - let clear = gtk::Button::with_label("Clear all"); - clear.set_image(Some(>k::Image::from_icon_name( - Some("list-remove-all"), - gtk::IconSize::SmallToolbar, - ))); - relm::connect!(self.model.relm, clear, connect_clicked(_), Msg::Clear); - vbox.pack_start(&clear, false, false, 0); - - vbox.show_all(); - - GLOBAL.with(move |global| *global.borrow_mut() = Some((list_box, rx))); - gtk::glib::idle_add(Self::receive); - } +pub struct Model { + count: usize, + list_box: gtk::ListBox, +} - fn receive() -> gtk::glib::Continue { +impl Model { + fn receive() -> gtk::glib::ControlFlow { GLOBAL.with(|global| { - if let Some((ref list_box, ref rx)) = *global.borrow() { - if let Ok((level, text)) = rx.try_recv() { - Self::add_message(list_box, level, &text); + if let Some((ref sender, ref rx)) = *global.borrow() { + if let Ok(message) = rx.try_recv() { + sender.input(Msg::Add(message)); } } }); - gtk::glib::Continue(true) + gtk::glib::ControlFlow::Continue } - fn add_message(list_box: >k::ListBox, level: log::Level, text: &str) { + fn add_message(&self, level: log::Level, text: &str) { let class = level.to_string(); let label = gtk::Label::new(Some(text)); - label.show(); + label.add_css_class(&class.to_lowercase()); - let context = label.style_context(); - context.add_class(&class.to_lowercase()); - - list_box.add(&label); - } - - fn clear(&self) { - GLOBAL.with(|global| { - if let Some((ref list_box, _)) = *global.borrow() { - list_box.foreach(|row| list_box.remove(row)); - } - }); - } - - fn hide(&self) { - self.model.popover.hide(); - self.widgets.toggle.set_active(false); + self.list_box.append(&label); } +} - fn read(&self, row: >k::ListBoxRow) { - GLOBAL.with(|global| { - if let Some((ref list_box, _)) = *global.borrow() { - list_box.remove(row); - } - }); - } +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = (); + type Input = Msg; + type Output = (); + + fn init( + _init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let (tx, rx) = std::sync::mpsc::channel(); + let log = Log::new(tx); - fn show(&self) { - self.model.popover.show(); - } + log::set_max_level(log::LevelFilter::Info); + log::set_boxed_logger(Box::new(log)).unwrap_or_default(); - fn update_count(&self, list_box: >k::ListBox) { - use std::str::FromStr; - - let count = list_box.children().len(); - if count == 0 { - self.widgets.toggle.hide(); - } else { - self.widgets.toggle.show(); - }; - self.widgets.count.set_label(&count.to_string()); - - let mut max_level = log::Level::Trace; - - for row in list_box.children() { - let Some(label) = row.downcast::().unwrap().child() else { - continue; - }; - let context = label.style_context(); - let level = context - .list_classes() - .iter() - .find_map(|class| log::Level::from_str(class).ok()) - .unwrap_or(log::Level::Info); - - if level < max_level { - max_level = level; - } + let list_box = gtk::ListBox::new(); + list_box.connect_row_activated(clone!( + #[strong] + sender, + move |_, row| sender.input(Msg::Read(row.clone())) + )); - if max_level == log::Level::Error { - break; - } - } + let model = Self { count: 0, list_box }; - let context = self.widgets.count.style_context(); - context.add_class(&max_level.to_string().to_lowercase()); - } -} + let widgets = view_output!(); -#[relm_derive::widget] -impl relm::Widget for Widget { - fn init_view(&mut self) { - self.init(); - } + GLOBAL.with(move |global| *global.borrow_mut() = Some((sender, rx))); + gtk::glib::idle_add(Self::receive); - fn model(relm: &relm::Relm, _: ()) -> Model { - Model { - relm: relm.clone(), - popover: gtk::Popover::new(None::<>k::Button>), - } + relm4::ComponentParts { model, widgets } } - fn update(&mut self, event: Msg) { - use Msg::*; - - match event { - Clear => self.clear(), - Hide => self.hide(), - Read(row) => self.read(&row), - Show => self.show(), - Update(sender) => self.update_count(&sender), + fn update(&mut self, msg: Self::Input, _sender: relm4::ComponentSender) { + match msg { + Msg::Add((level, text)) => { + self.add_message(level, &text); + self.count += 1; + } + Msg::Clear => { + self.list_box.remove_all(); + self.count = 0; + } + Msg::Read(row) => { + self.list_box.remove(&row); + self.count = self.count.saturating_sub(1); + } } } view! { - #[name="toggle"] - #[style_name="log"] - gtk::ToggleButton { - gtk::Box { - spacing: 10, - gtk::Label { - label: "Notifications", - }, - #[name="count"] - #[style_name="count"] - gtk::Label { - label: "0", + gtk::MenuButton { + #[watch] + set_visible: model.count > 0, + #[watch] + set_label: &format!("Notifications {}", model.count), + set_direction: gtk::ArrowType::Down, + + #[wrap(Some)] + set_popover = >k::Popover { + add_css_class: "log", + set_height_request: 500, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + gtk::ScrolledWindow { + set_vexpand: true, + set_policy: (gtk::PolicyType::Never, gtk::PolicyType::Automatic), + set_child: Some(&model.list_box), + }, + gtk::Button { + set_label: "Clear all", + set_icon_name: "list-remove-all", + connect_clicked => Msg::Clear, + }, }, - gtk::Image { - icon_name: Some("go-down-symbolic"), - }, - }, - toggled(e) => if e.is_active() { - Msg::Show - } else { - Msg::Hide }, - } + }, } } diff --git a/src/main.rs b/src/main.rs index 9f81f7e..7c582be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,5 @@ #![warn(warnings)] -use relm::Widget; - -mod add; mod agenda; mod application; mod date; @@ -27,7 +24,9 @@ fn main() { std::process::exit(0); } - crate::application::Widget::run(()).unwrap(); + let app = relm4::RelmApp::new("txt.todo.effitask") + .with_args(Vec::new()); + app.run::(()); } fn usage(program: &str) { diff --git a/src/search.rs b/src/search.rs index 7935aec..53bf98d 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,26 +1,29 @@ -use crate::widgets::tasks::Msg::{Complete, Edit}; -use crate::widgets::Tasks; +use gtk::prelude::*; +use relm4::ComponentController as _; -#[derive(relm_derive::Msg)] -pub enum Msg { - Complete(Box), - Edit(Box), +#[derive(Debug)] +pub enum MsgInput { Update, UpdateFilter(String), } -impl Widget { +pub struct Model { + query: String, + tasks: relm4::Controller, +} + +impl Model { fn update_tasks(&mut self) { self.update(); } fn update_filter(&mut self, filter: &str) { - self.model = filter.to_string(); + self.query = filter.to_string(); self.update(); } fn update(&self) { - let filter = self.model.to_lowercase(); + let filter = self.query.to_lowercase(); let list = crate::application::tasks(); let tasks = list @@ -30,33 +33,46 @@ impl Widget { .cloned() .collect(); - self.components - .tasks - .emit(crate::widgets::tasks::Msg::Update(tasks)); + self.tasks.emit(crate::widgets::tasks::Msg::Update(tasks)); } } -#[relm_derive::widget] -impl relm::Widget for Widget { - fn model(_: ()) -> String { - String::new() +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = String; + type Input = MsgInput; + type Output = crate::widgets::task::MsgOutput; + + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; + + let tasks = crate::widgets::tasks::Model::builder() + .launch(()) + .forward(sender.output_sender(), std::convert::identity); + + let model = Self { query: init, tasks }; + + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } } - fn update(&mut self, event: Msg) { - use Msg::*; + fn update(&mut self, msg: Self::Input, _: relm4::ComponentSender) { + use MsgInput::*; - match event { - Complete(_) | Edit(_) => (), + match msg { Update => self.update_tasks(), UpdateFilter(filter) => self.update_filter(&filter), } } view! { - #[name="tasks"] - Tasks { - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), + gtk::Box { + append: model.tasks.widget(), } } } diff --git a/src/tasks/list.rs b/src/tasks/list.rs index aa43b3b..e232bcc 100644 --- a/src/tasks/list.rs +++ b/src/tasks/list.rs @@ -156,7 +156,7 @@ impl List { } pub fn add(&mut self, text: &str) -> Result<(), String> { - use std::str::FromStr; + use std::str::FromStr as _; let mut task = crate::tasks::Task::from_str(text) .map_err(|_| format!("Unable to convert task: '{text}'"))?; diff --git a/src/widgets/calendar.rs b/src/widgets/calendar.rs index b62a613..7dd07f3 100644 --- a/src/widgets/calendar.rs +++ b/src/widgets/calendar.rs @@ -1,211 +1,153 @@ +use chrono::Datelike as _; use gtk::prelude::*; pub struct Model { - label: String, + date: Option, + entry: gtk::Entry, + label: &'static str, popover: gtk::Popover, - calendar: gtk::Calendar, - relm: relm::Relm, } -#[derive(relm_derive::Msg)] -pub enum Msg { +#[derive(Debug)] +pub enum MsgInput { Add(todo_txt::task::Period), - DateSelected, + DateSelected(gtk::glib::DateTime), DateUpdated, - Sensitive, Set(Option), - ShowCalendar, - Updated(Option), } -impl Calendar { - fn add(&self, period: todo_txt::task::Period) { - let mut date = crate::date::today(); - - let text = self.widgets.entry.text(); - - if !text.is_empty() { - date = match chrono::NaiveDate::parse_from_str(text.as_str(), "%Y-%m-%d") { - Ok(date) => date, - Err(_) => { - log::error!("Invalid date format, use YYYY-MM-DD"); - return; - } - }; - } - - date = period + date; - self.set_date(Some(date)); - self.date_updated(); - } - - fn date_selected(&self) { - let (y, m, d) = self.model.calendar.date(); - - self.widgets - .entry - .set_text(format!("{y}-{}-{d}", m + 1).as_str()); - self.model.popover.popdown(); - - self.date_updated(); - } - - fn date_updated(&self) { - let mut date = None; - let text = self.widgets.entry.text(); - - if !text.is_empty() { - date = match chrono::NaiveDate::parse_from_str(text.as_str(), "%Y-%m-%d") { - Ok(date) => Some(date), - Err(_) => { - log::error!("Invalid date format, use YYYY-MM-DD"); - return; - } - }; - } - - self.model.relm.stream().emit(Msg::Updated(date)); - } +#[derive(Debug)] +pub enum MsgOutput { + Updated(Option), +} - fn set_date(&self, date: Option) { - if let Some(date) = date { - use chrono::Datelike; - - self.widgets - .entry - .set_text(date.format("%Y-%m-%d").to_string().as_str()); - self.model - .calendar - .select_month(date.month() - 1, date.year() as u32); - self.model.calendar.select_day(date.day()); - } else { - self.widgets.entry.set_text(""); - } +impl Model { + fn add(&mut self, sender: relm4::ComponentSender, period: todo_txt::task::Period) { + self.date = Some(period + self.date.unwrap_or_else(crate::date::today)); + sender.output(MsgOutput::Updated(self.date)).ok(); } - fn sensitive(&self) { - use relm::Widget; + fn date_selected(&mut self, sender: relm4::ComponentSender, date: gtk::glib::DateTime) { + self.date = Some(crate::date::from_glib(date)); - if self.root().is_sensitive() { - self.widgets.buttons.show(); - } else { - self.widgets.buttons.hide(); - } + self.popover.popdown(); + sender.output(MsgOutput::Updated(self.date)).ok(); } } -#[relm_derive::widget] -impl relm::Widget for Calendar { - fn init_view(&mut self) { - self.widgets - .entry - .set_icon_from_icon_name(gtk::EntryIconPosition::Primary, Some("x-office-calendar")); - - self.widgets.label.set_size_request(200, -1); - self.widgets.label.set_text(self.model.label.as_str()); - - relm::connect!( - self.model.relm, - self.model.calendar, - connect_day_selected(_), - Msg::DateSelected - ); - self.model.calendar.show(); - self.model - .popover - .set_relative_to(Some(&self.widgets.entry)); - self.model - .popover - .set_pointing_to(>k::gdk::Rectangle::new(15, 15, 0, 0)); - self.model.popover.add(&self.model.calendar); - self.model.popover.hide(); +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = &'static str; + type Input = MsgInput; + type Output = MsgOutput; + + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = Self { + entry: gtk::Entry::new(), + date: None, + label: init, + popover: gtk::Popover::new(), + }; + + let entry = &model.entry; + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } } - fn model(relm: &relm::Relm, label: String) -> Model { - Model { - label, - popover: gtk::Popover::new(None::<>k::Calendar>), - calendar: gtk::Calendar::new(), - relm: relm.clone(), - } - } + fn update(&mut self, msg: Self::Input, sender: relm4::ComponentSender) { + use MsgInput::*; - fn update(&mut self, event: Msg) { - use Msg::*; - - match event { - Add(period) => self.add(period), - DateSelected => self.date_selected(), - DateUpdated => self.date_updated(), - Sensitive => self.sensitive(), - Set(date) => self.set_date(date), - ShowCalendar => self.model.popover.popup(), - Updated(_) => (), + match msg { + Add(period) => self.add(sender, period), + DateSelected(date) => self.date_selected(sender, date), + Set(date) => self.date = date, + DateUpdated => { + sender.output(MsgOutput::Updated(self.date)).ok(); + } } } view! { + #[name = "r#box"] gtk::Box { - orientation: gtk::Orientation::Horizontal, - spacing: 10, - sensitive_notify => Msg::Sensitive, + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 10, - #[name="label"] gtk::Label { - child: { - expand: true, - fill: true, - }, - xalign: 1., - yalign: 0., + set_hexpand: true, + set_text: &model.label, + set_width_request: 200, + set_xalign: 1., + set_yalign: 0., }, gtk::Box { - orientation: gtk::Orientation::Vertical, - #[name="entry"] - gtk::Entry { - child: { - expand: true, - fill: true, + set_orientation: gtk::Orientation::Vertical, + gtk::Box { + gtk::MenuButton { + set_icon_name: "x-office-calendar", + #[wrap(Some)] + set_popover = >k::Popover { + gtk::Calendar { + #[watch] + set_day?: model.date.map(|x| x.day() as i32), + #[watch] + set_month?: model.date.map(|x| x.month() as i32 - 1), + #[watch] + set_year?: model.date.map(|x| x.year()), + + connect_day_selected[sender] => move |this| { + sender.input(MsgInput::DateSelected(this.date())); + }, + }, + }, + }, + #[local_ref] + entry -> gtk::Entry { + set_hexpand: true, + #[watch] + set_text?: &model.date.map(|x| x.format("%Y-%m-%d").to_string()), + set_width_request: 214, + + connect_move_focus[sender] => move |_, _| { + sender.input(MsgInput::DateUpdated); + }, }, - width_request: 214, - focus_out_event(_, _) => (Msg::DateUpdated, gtk::Inhibit(false)), - icon_press(_, _, _) => Msg::ShowCalendar, }, - #[name="buttons"] gtk::Box { - orientation: gtk::Orientation::Horizontal, + set_halign: gtk::Align::End, + set_orientation: gtk::Orientation::Horizontal, + #[watch] + set_visible: r#box.is_sensitive(), + gtk::Button { - child: { - pack_type: gtk::PackType::End, - }, - label: "+1y", - tooltip_text: Some("Add one year"), - clicked => Msg::Add(todo_txt::task::Period::Year), + set_label: "+1y", + set_tooltip_text: Some("Add one year"), + + connect_clicked => MsgInput::Add(todo_txt::task::Period::Year), }, gtk::Button { - child: { - pack_type: gtk::PackType::End, - }, - label: "+1m", - tooltip_text: Some("Add one month"), - clicked => Msg::Add(todo_txt::task::Period::Month), + set_label: "+1m", + set_tooltip_text: Some("Add one month"), + + connect_clicked => MsgInput::Add(todo_txt::task::Period::Month), }, gtk::Button { - child: { - pack_type: gtk::PackType::End, - }, - label: "+1w", - tooltip_text: Some("Add one month"), - clicked => Msg::Add(todo_txt::task::Period::Week), + set_label: "+1w", + set_tooltip_text: Some("Add one month"), + + connect_clicked => MsgInput::Add(todo_txt::task::Period::Week), }, gtk::Button { - child: { - pack_type: gtk::PackType::End, - }, - label: "+1d", - tooltip_text: Some("Add one month"), - clicked => Msg::Add(todo_txt::task::Period::Day), + set_label: "+1d", + set_tooltip_text: Some("Add one month"), + + connect_clicked => MsgInput::Add(todo_txt::task::Period::Day), }, }, }, diff --git a/src/widgets/circle.rs b/src/widgets/circle.rs index 2b19255..fb669de 100644 --- a/src/widgets/circle.rs +++ b/src/widgets/circle.rs @@ -1,20 +1,17 @@ use gtk::prelude::*; -#[derive(relm_derive::Msg)] -pub enum Msg { - Draw, -} - -pub struct Model { - draw_handler: relm::DrawHandler, - task: crate::tasks::Task, -} - -impl Circle { - fn draw(&mut self) -> Result<(), gtk::cairo::Error> { - let context = self.model.draw_handler.get_context()?; - let task = &self.model.task; - let center = self.center(); +pub struct Model {} + +impl Model { + fn draw( + task: &crate::tasks::Task, + drawing_area: >k::DrawingArea, + context: >k::cairo::Context, + ) -> Result<(), gtk::cairo::Error> { + let center = f64::min( + f64::from(drawing_area.width_request()) / 2., + f64::from(drawing_area.height_request()) / 2., + ); if task.finished || task.due_date.is_none() { context.set_source_rgb(0.8, 0.8, 0.8); @@ -34,8 +31,8 @@ impl Circle { context.close_path(); if task.finished { - let width = self.widgets.drawing_area.width_request(); - let height = self.widgets.drawing_area.height_request(); + let width = drawing_area.width_request(); + let height = drawing_area.height_request(); context.save()?; context.fill()?; @@ -80,42 +77,33 @@ impl Circle { Ok(()) } - - fn center(&self) -> f64 { - f64::min( - f64::from(self.widgets.drawing_area.width_request()) / 2., - f64::from(self.widgets.drawing_area.height_request()) / 2., - ) - } } -#[relm_derive::widget] -impl relm::Widget for Circle { - fn init_view(&mut self) { - self.model.draw_handler.init(&self.widgets.drawing_area); - } +impl relm4::SimpleComponent for Model { + type Init = crate::tasks::Task; + type Input = (); + type Output = (); + type Root = gtk::DrawingArea; + type Widgets = (); - fn model(task: crate::tasks::Task) -> Model { - Model { - draw_handler: relm::DrawHandler::new().expect("draw handler"), - task, - } + fn init_root() -> Self::Root { + gtk::DrawingArea::new() } - fn update(&mut self, event: Msg) { - use Msg::*; + fn init( + init: Self::Init, + root: Self::Root, + _sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + root.set_height_request(60); + root.set_width_request(60); - match event { - Draw => self.draw().unwrap(), - } - } + let model = Self {}; - view! { - #[name="drawing_area"] - gtk::DrawingArea { - height_request: 60, - width_request: 60, - draw(_, _) => (Msg::Draw, gtk::Inhibit(false)), - } + root.set_draw_func(move |drawing_area, context, _w, _h| { + Self::draw(&init, drawing_area, context).ok(); + }); + + relm4::ComponentParts { model, widgets: () } } } diff --git a/src/widgets/filter.rs b/src/widgets/filter.rs index 1f90f3f..ce2eb4b 100644 --- a/src/widgets/filter.rs +++ b/src/widgets/filter.rs @@ -1,15 +1,7 @@ -use crate::widgets::tasks::Msg::{Complete, Edit}; -use crate::widgets::Tasks; -use gtk::prelude::*; +#![allow(deprecated)] -#[derive(relm_derive::Msg)] -pub enum Msg { - Complete(Box), - Edit(Box), - Filters(Vec), - UpdateFilters(Vec<(String, (u32, u32))>), - UpdateTasks(Vec), -} +use gtk::prelude::*; +use relm4::ComponentController as _; #[repr(u32)] enum Column { @@ -31,29 +23,49 @@ impl From for i32 { } } -impl Filter { +#[derive(Debug)] +pub enum MsgInput { + SelectionChange, + UpdateFilters(Vec<(String, (u32, u32))>), + UpdateTasks(Vec), +} + +#[derive(Debug)] +pub enum MsgOutput { + Complete(Box), + Edit(Box), + Filters(Vec), +} + +pub struct Model { + store: gtk::TreeStore, + filters: std::collections::BTreeMap, + tasks: relm4::Controller, + tree_view: gtk::TreeView, +} + +impl Model { fn update_filters(&mut self, filters: Vec<(String, (u32, u32))>) { - let selection = self.widgets.filters.selection(); + let selection = self.tree_view.selection(); let (paths, _) = selection.selected_rows(); - self.model.clear(); + self.filters.clear(); + self.store.clear(); let mut root = std::collections::HashMap::new(); for filter in filters { self.append(&mut root, filter); } - self.widgets.filters.expand_all(); + self.tree_view.expand_all(); for path in paths { - self.widgets - .filters - .set_cursor(&path, None as Option<>k::TreeViewColumn>, false); + gtk::prelude::TreeViewExt::set_cursor(&self.tree_view, &path, None, false); } } fn append( - &self, + &mut self, root: &mut std::collections::HashMap, filter: (String, (u32, u32)), ) { @@ -71,26 +83,27 @@ impl Filter { self.append(root, (parent.clone(), (0, 0))); } - let row = self.model.append(root.get(&parent)); + let row = self.store.append(root.get(&parent)); - self.model + self.store .set_value(&row, Column::Title.into(), &title.to_value()); - self.model + self.store .set_value(&row, Column::Raw.into(), &filter.to_value()); - self.model + self.store .set_value(&row, Column::Progress.into(), &progress.to_value()); let tooltip = format!("{done}/{total}"); - self.model + self.store .set_value(&row, Column::Tooltip.into(), &tooltip.to_value()); - root.insert(filter, row); + root.insert(filter.clone(), row); + + let path = self.store.path(&row); + self.filters.insert(path, filter); } fn update_tasks(&self, tasks: Vec) { - self.components - .tasks - .emit(crate::widgets::tasks::Msg::Update(tasks)); + self.tasks.emit(super::tasks::Msg::Update(tasks)); } fn select_range(treeview: >k::TreeView, path: >k::TreePath) { @@ -105,51 +118,34 @@ impl Filter { let end_iter = model .iter_nth_child(Some(&start_iter), n_child - 1) .unwrap(); - let end = model.path(&end_iter).unwrap(); + let end = model.path(&end_iter); treeview.selection().select_range(start, &end); } } } -#[relm_derive::widget] -impl relm::Widget for Filter { - fn init_view(&mut self) { - self.widgets.filters.set_size_request(200, -1); - self.widgets - .scroll - .set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic); - self.widgets.filters.set_model(Some(&self.model)); - self.widgets - .filters - .selection() - .set_mode(gtk::SelectionMode::Multiple); - - let column = gtk::TreeViewColumn::new(); - self.widgets.filters.append_column(&column); - - let cell = gtk::CellRendererProgress::new(); - cell.set_text_xalign(0.); - gtk::prelude::CellLayoutExt::pack_start(&column, &cell, true); - gtk::prelude::TreeViewColumnExt::add_attribute( - &column, - &cell, - "text", - Column::Title.into(), - ); - gtk::prelude::TreeViewColumnExt::add_attribute( - &column, - &cell, - "value", - Column::Progress.into(), +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = (); + type Input = MsgInput; + type Output = MsgOutput; + + fn init( + _init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; + + let tasks = crate::widgets::tasks::Model::builder().launch(()).forward( + sender.output_sender(), + |output| match output { + super::task::MsgOutput::Complete(task) => MsgOutput::Complete(task), + super::task::MsgOutput::Edit(task) => MsgOutput::Edit(task), + }, ); - self.widgets - .filters - .set_tooltip_column(Column::Tooltip.into()); - } - - fn model(_: ()) -> gtk::TreeStore { let columns = vec![ gtk::glib::types::Type::STRING, gtk::glib::types::Type::STRING, @@ -157,14 +153,56 @@ impl relm::Widget for Filter { gtk::glib::types::Type::STRING, ]; - gtk::TreeStore::new(&columns) + let model = Self { + tasks, + filters: std::collections::BTreeMap::new(), + store: gtk::TreeStore::new(&columns), + tree_view: gtk::TreeView::new(), + }; + + let filters = &model.tree_view; + let widgets = view_output!(); + + filters.set_model(Some(&model.store)); + + let selection = filters.selection(); + selection.set_mode(gtk::SelectionMode::Multiple); + selection.connect_changed(move |_| { + sender.input(MsgInput::SelectionChange); + }); + + let column = gtk::TreeViewColumn::new(); + filters.append_column(&column); + + let cell = gtk::CellRendererProgress::new(); + cell.set_text_xalign(0.); + gtk::prelude::CellLayoutExt::pack_start(&column, &cell, true); + column.add_attribute(&cell, "text", Column::Title.into()); + column.add_attribute(&cell, "value", Column::Progress.into()); + + filters.set_tooltip_column(Column::Tooltip.into()); + + relm4::ComponentParts { model, widgets } } - fn update(&mut self, event: Msg) { - use Msg::*; + fn update(&mut self, msg: Self::Input, sender: relm4::ComponentSender) { + use MsgInput::*; - match event { - Complete(_) | Edit(_) | Filters(_) => (), + match msg { + SelectionChange => { + let mut filters = Vec::new(); + + let (paths, _) = self.tree_view.selection().selected_rows(); + + for path in paths { + match self.filters.get(&path) { + Some(value) => filters.push(value.clone()), + None => continue, + }; + } + + sender.output(MsgOutput::Filters(filters)).ok(); + } UpdateFilters(filters) => self.update_filters(filters), UpdateTasks(tasks) => self.update_tasks(tasks), } @@ -172,41 +210,26 @@ impl relm::Widget for Filter { view! { gtk::Paned { - orientation: gtk::Orientation::Horizontal, - wide_handle: true, - #[name="scroll"] - gtk::ScrolledWindow { - #[name="filters"] - gtk::TreeView { - headers_visible: false, - enable_tree_lines: true, - row_activated(treeview, path, _) => Self::select_range(treeview, path), - selection.changed(ref mut selection) => { - let mut filters = Vec::new(); - let (paths, list_model) = selection.selected_rows(); - - for path in paths { - let Some(iter) = list_model.iter(&path) else { - continue; - }; - - match list_model.value(&iter, Column::Raw.into()).get() { - Ok(Some(value)) => filters.push(value), - Ok(None) | Err(_) => continue, - }; - } - - Msg::Filters(filters) - }, - } - }, - gtk::ScrolledWindow { - #[name="tasks"] - Tasks { - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), + set_orientation: gtk::Orientation::Horizontal, + set_position: 200, + set_wide_handle: true, + + #[wrap(Some)] + set_start_child = >k::ScrolledWindow { + set_policy: (gtk::PolicyType::Never, gtk::PolicyType::Automatic), + + #[local_ref] + filters -> gtk::TreeView { + set_enable_tree_lines: true, + set_headers_visible: false, + + connect_row_activated => |treeview, path, _| Self::select_range(treeview, path), }, - } + }, + #[wrap(Some)] + set_end_child = >k::ScrolledWindow { + set_child: Some(model.tasks.widget()), + }, } } } diff --git a/src/widgets/keywords.rs b/src/widgets/keywords.rs index fd5a29e..70dbb4c 100644 --- a/src/widgets/keywords.rs +++ b/src/widgets/keywords.rs @@ -1,21 +1,28 @@ +#![allow(deprecated)] + use gtk::prelude::*; -#[derive(relm_derive::Msg)] -pub enum Msg { +#[derive(Debug)] +pub enum MsgInput { Add, Delete, Edit(Column, gtk::TreePath, String), Set(std::collections::BTreeMap), +} + +#[derive(Debug)] +pub enum MsgOutput { Updated(std::collections::BTreeMap), } pub struct Model { + keywords: std::collections::BTreeMap, store: gtk::ListStore, - relm: relm::Relm, + tree_view: gtk::TreeView, } #[repr(u32)] -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Column { Name = 0, Value = 1, @@ -33,186 +40,186 @@ impl From for i32 { } } -impl Keywords { +impl Model { fn add(&mut self) { - let iter = self.model.store.append(); - let path = self.model.store.path(&iter).unwrap(); - let column = self.widgets.tree_view.column(Column::Name.into()); + let iter = self.store.append(); + let path = self.store.path(&iter); + let column = self.tree_view.column(Column::Name.into()); - self.widgets - .tree_view - .set_cursor(&path, column.as_ref(), true); + self.keywords + .insert(path.clone(), (String::new(), String::new())); + gtk::prelude::TreeViewExt::set_cursor(&self.tree_view, &path, column.as_ref(), true); } - fn delete(&mut self) { - let selection = self.widgets.tree_view.selection(); + fn delete(&mut self, sender: relm4::ComponentSender) { + let selection = self.tree_view.selection(); let (rows, _) = selection.selected_rows(); let references = rows .iter() - .map(|x| gtk::TreeRowReference::new(&self.model.store, x)); + .map(|x| gtk::TreeRowReference::new(&self.store, x)); for reference in references.flatten() { if let Some(path) = reference.path() { - if let Some(iter) = self.model.store.iter(&path) { - self.model.store.remove(&iter); + self.keywords.remove(&path); + if let Some(iter) = self.store.iter(&path) { + self.store.remove(&iter); } } } - self.model.relm.stream().emit(Msg::Updated(self.keywords())); + sender.output(MsgOutput::Updated(self.keywords())).ok(); } - fn edit(&mut self, column: Column, path: >k::TreePath, new_text: &str) { - let iter = self.model.store.iter(path).unwrap(); + fn edit( + &mut self, + sender: relm4::ComponentSender, + column: Column, + path: >k::TreePath, + new_text: &str, + ) { + if let Some(keyword) = self.keywords.get_mut(path) { + match column { + Column::Name => keyword.0 = new_text.to_string(), + Column::Value => keyword.1 = new_text.to_string(), + } + } - self.model - .store + let iter = self.store.iter(path).unwrap(); + self.store .set_value(&iter, column.into(), &new_text.to_value()); - self.model.relm.stream().emit(Msg::Updated(self.keywords())); + sender.output(MsgOutput::Updated(self.keywords())).ok(); } fn keywords(&self) -> std::collections::BTreeMap { - let mut keywords = std::collections::BTreeMap::new(); - - let Some(iter) = self.model.store.iter_first() else { - return keywords; - }; + self.keywords.values().cloned().collect() + } - while let Ok(Some(name)) = self.model.store.value(&iter, Column::Name.into()).get() { - let Ok(Some(value)) = self.model.store.value(&iter, Column::Value.into()).get() else { - break; - }; + fn set(&mut self, tags: std::collections::BTreeMap) { + self.keywords.clear(); + self.store.clear(); - keywords.insert(name, value); + for (name, value) in tags { + let iter = self.store.append(); - if !self.model.store.iter_next(&iter) { - break; - } - } + self.store + .set_value(&iter, Column::Name.into(), &name.to_value()); + self.store + .set_value(&iter, Column::Value.into(), &value.to_value()); - keywords - } + let path = self.store.path(&iter); - fn set(&mut self, keywords: &std::collections::BTreeMap) { - self.model.store.clear(); - - for (name, value) in keywords { - let row = self.model.store.append(); - self.model - .store - .set_value(&row, Column::Name.into(), &name.to_value()); - self.model - .store - .set_value(&row, Column::Value.into(), &value.to_value()); + self.keywords.insert(path, (name, value)); } } } -#[relm_derive::widget] -impl relm::Widget for Keywords { - fn init_view(&mut self) { - self.widgets - .scroll - .set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic); - self.widgets.scroll.set_height_request(150); - self.widgets.tree_view.set_model(Some(&self.model.store)); - self.widgets - .tree_view - .selection() - .set_mode(gtk::SelectionMode::Multiple); +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = std::collections::BTreeMap; + type Input = MsgInput; + type Output = MsgOutput; + + fn init( + _init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use gtk::glib::clone; + + let columns = vec![ + gtk::glib::types::Type::STRING, + gtk::glib::types::Type::STRING, + ]; + + let model = Self { + keywords: std::collections::BTreeMap::new(), + store: gtk::ListStore::new(&columns), + tree_view: gtk::TreeView::new(), + }; + + let tree_view = &model.tree_view; + let widgets = view_output!(); + + tree_view.set_model(Some(&model.store)); + tree_view.selection().set_mode(gtk::SelectionMode::Multiple); let column = gtk::TreeViewColumn::new(); column.set_title("name"); - self.widgets.tree_view.append_column(&column); + tree_view.append_column(&column); let cell = gtk::CellRendererText::new(); cell.set_editable(true); - relm::connect!( - self.model.relm, - cell, - connect_edited(_, path, new_text), - Msg::Edit(Column::Name, path, new_text.to_string()) - ); + cell.connect_edited(clone!( + #[strong] + sender, + move |_, path, new_text| sender.input(MsgInput::Edit( + Column::Name, + path, + new_text.to_string() + )) + )); + gtk::prelude::CellLayoutExt::pack_start(&column, &cell, true); - gtk::prelude::TreeViewColumnExt::add_attribute( - &column, - &cell, - "text", - Column::Value.into(), - ); + column.add_attribute(&cell, "text", Column::Name.into()); let column = gtk::TreeViewColumn::new(); column.set_title("value"); - self.widgets.tree_view.append_column(&column); + tree_view.append_column(&column); let cell = gtk::CellRendererText::new(); cell.set_editable(true); - relm::connect!( - self.model.relm, - cell, - connect_edited(_, path, new_text), - Msg::Edit(Column::Value, path, new_text.to_string()) - ); - gtk::prelude::CellLayoutExt::pack_start(&column, &cell, true); - gtk::prelude::TreeViewColumnExt::add_attribute( - &column, - &cell, - "text", - Column::Value.into(), - ); - } + cell.connect_edited(clone!( + #[strong] + sender, + move |_, path, new_text| sender.input(MsgInput::Edit( + Column::Value, + path, + new_text.to_string() + )) + )); - fn model(relm: &relm::Relm, _: ()) -> Model { - let columns = vec![ - gtk::glib::types::Type::STRING, - gtk::glib::types::Type::STRING, - ]; + gtk::prelude::CellLayoutExt::pack_start(&column, &cell, true); + column.add_attribute(&cell, "text", Column::Value.into()); - Model { - store: gtk::ListStore::new(&columns), - relm: relm.clone(), - } + relm4::ComponentParts { model, widgets } } - fn update(&mut self, event: Msg) { - use Msg::*; + fn update(&mut self, msg: Self::Input, sender: relm4::ComponentSender) { + use MsgInput::*; - match event { + match msg { Add => self.add(), - Delete => self.delete(), - Edit(ref column, ref path, ref new_text) => self.edit(column.clone(), path, new_text), - Set(keywords) => self.set(&keywords), - Updated(_) => (), + Delete => self.delete(sender), + Edit(ref column, ref path, ref new_text) => { + self.edit(sender, column.clone(), path, new_text) + } + Set(keywords) => self.set(keywords), } } view! { gtk::Box { - orientation: gtk::Orientation::Vertical, - #[name="scroll"] + set_orientation: gtk::Orientation::Vertical, gtk::ScrolledWindow { - child: { - expand: true, - fill: true, - }, - #[name="tree_view"] - gtk::TreeView { - headers_visible: true, + set_height_request: 150, + set_policy: (gtk::PolicyType::Never, gtk::PolicyType::Automatic), + + #[local_ref] + tree_view -> gtk::TreeView { + set_headers_visible: true, + set_hexpand: true, + set_vexpand: true, }, }, gtk::ActionBar { - child: { - expand: false, - fill: true, - }, - gtk::Button { - image: Some(>k::Image::from_icon_name(Some("list-add"), gtk::IconSize::SmallToolbar)), - clicked => Msg::Add, + pack_start = >k::Button { + set_icon_name: "list-add", + connect_clicked => MsgInput::Add, }, - gtk::Button { - image: Some(>k::Image::from_icon_name(Some("list-remove"), gtk::IconSize::SmallToolbar)), - clicked => Msg::Delete, + pack_start = >k::Button { + set_icon_name: "list-remove", + connect_clicked => MsgInput::Delete, }, }, }, diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 4adabe2..32ebdd4 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -3,17 +3,7 @@ pub mod circle; pub mod filter; pub mod keywords; pub mod priority; -pub mod repeat; +pub mod recurrence; pub mod tags; pub mod task; pub mod tasks; - -pub use calendar::Calendar; -pub use circle::Circle; -pub use filter::Filter; -pub use keywords::Keywords; -pub use priority::Priority; -pub use repeat::Repeat; -pub use tags::Tags; -pub use task::Task; -pub use tasks::Tasks; diff --git a/src/widgets/priority.rs b/src/widgets/priority.rs index c8417af..ce58c7a 100644 --- a/src/widgets/priority.rs +++ b/src/widgets/priority.rs @@ -1,130 +1,133 @@ use gtk::prelude::*; -#[derive(relm_derive::Msg)] -pub enum Msg { +#[derive(Debug)] +pub enum MsgInput { More, - Set(u8), - Updated(u8), } -impl Priority { - fn more(&self) { - self.widgets.hbox.hide(); - self.widgets.button.show(); - } +#[derive(Debug)] +pub enum MsgOutput { + Updated(todo_txt::Priority), +} - fn less(&self) { - self.widgets.hbox.show(); - self.widgets.button.hide(); - } +pub struct Model { + priority: todo_txt::Priority, + show_more: bool, +} - fn set(&self, priority: u8) { - self.widgets.button.set_value(f64::from(priority)); - - match priority { - 0 => self.widgets.a.set_active(true), - 1 => self.widgets.b.set_active(true), - 2 => self.widgets.c.set_active(true), - 3 => self.widgets.d.set_active(true), - 4 => self.widgets.e.set_active(true), - 26 => self.widgets.z.set_active(true), - _ => (), - } +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = todo_txt::Priority; + type Input = MsgInput; + type Output = MsgOutput; - if priority < 5 || priority == 26 { - self.less(); - } else { - self.more(); - } - } + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = Self { + priority: init, + show_more: false, + }; - fn updated(&self, priority: u8) { - self.widgets.button.set_value(f64::from(priority)); - } -} + let widgets = view_output!(); -#[relm_derive::widget] -impl relm::Widget for Priority { - fn init_view(&mut self) { - self.widgets - .button - .set_adjustment(>k::Adjustment::new(0., 0., 27., 1., 5., 1.)); - self.widgets.button.hide(); - - self.widgets.b.join_group(Some(&self.widgets.a)); - self.widgets.c.join_group(Some(&self.widgets.a)); - self.widgets.d.join_group(Some(&self.widgets.a)); - self.widgets.e.join_group(Some(&self.widgets.a)); - self.widgets.z.join_group(Some(&self.widgets.a)); + relm4::ComponentParts { model, widgets } } - fn model(_: ()) {} - - fn update(&mut self, event: Msg) { - use Msg::*; - - match event { - More => self.more(), - Set(priority) => self.set(priority), - Updated(priority) => self.updated(priority), + fn update(&mut self, msg: Self::Input, _sender: relm4::ComponentSender) { + match msg { + MsgInput::More => self.show_more = true, } } view! { gtk::Box { - orientation: gtk::Orientation::Vertical, - #[name="hbox"] + set_orientation: gtk::Orientation::Vertical, + gtk::Box { - orientation: gtk::Orientation::Horizontal, - #[name="a"] - gtk::RadioButton { - label: "A", - mode: false, - toggled => Msg::Updated(0), + set_orientation: gtk::Orientation::Horizontal, + #[watch] + set_visible: !model.show_more, + + append: group = >k::ToggleButton { + set_active: model.priority == 0, + set_label: "A", + + connect_toggled[sender] => move |_| { + sender.output(MsgOutput::Updated(0.into())).ok(); + }, }, #[name="b"] - gtk::RadioButton { - label: "B", - mode: false, - toggled => Msg::Updated(1), + gtk::ToggleButton { + set_active: model.priority == 1, + set_group: Some(&group), + set_label: "B", + + connect_toggled[sender] => move |_| { + sender.output(MsgOutput::Updated(1.into())).ok(); + }, }, #[name="c"] - gtk::RadioButton { - label: "C", - mode: false, - toggled => Msg::Updated(2), + gtk::ToggleButton { + set_active: model.priority == 2, + set_group: Some(&group), + set_label: "C", + + connect_toggled[sender] => move |_| { + sender.output(MsgOutput::Updated(2.into())).ok(); + }, }, #[name="d"] - gtk::RadioButton { - label: "D", - mode: false, - toggled => Msg::Updated(3), + gtk::ToggleButton { + set_active: model.priority == 3, + set_group: Some(&group), + set_label: "D", + + connect_toggled[sender] => move |_| { + sender.output(MsgOutput::Updated(3.into())).ok(); + }, }, #[name="e"] - gtk::RadioButton { - label: "E", - mode: false, - toggled => Msg::Updated(4), + gtk::ToggleButton { + set_active: model.priority == 4, + set_group: Some(&group), + set_label: "E", + + connect_toggled[sender] => move |_| { + sender.output(MsgOutput::Updated(4.into())).ok(); + }, }, gtk::Button { - label: "…", - tooltip_text: Some("More"), - clicked => Msg::More, + set_label: "…", + set_tooltip_text: Some("More"), + + connect_clicked => MsgInput::More, }, #[name="z"] - gtk::RadioButton { - label: "Z", - mode: false, - clicked => Msg::Updated(26), + gtk::ToggleButton { + set_active: model.priority == 26, + set_group: Some(&group), + set_label: "Z", + + connect_clicked[sender] => move |_| { + sender.output(MsgOutput::Updated(26.into())).ok(); + }, }, }, - #[name="button"] gtk::SpinButton { - focus_out_event(button, _) => ( - Msg::Updated(button.value() as u8), - gtk::Inhibit(false) - ), + set_adjustment: >k::Adjustment::new(0., 0., 27., 1., 5., 1.), + set_climb_rate: 1., + set_digits: 0, + #[watch] + set_visible: model.show_more, + + connect_value_changed[sender] => move |button| { + let priority = (button.value() as u8).into(); + sender.output(MsgOutput::Updated(priority)).ok(); + }, }, - } + }, } } diff --git a/src/widgets/recurrence.rs b/src/widgets/recurrence.rs new file mode 100644 index 0000000..07840d4 --- /dev/null +++ b/src/widgets/recurrence.rs @@ -0,0 +1,151 @@ +use gtk::prelude::*; + +#[derive(Debug)] +pub enum MsgInput { + Update, +} + +#[derive(Debug)] +pub enum MsgOutput { + Updated(Option), +} + +impl Model { + fn recurrence(&self) -> Option { + let num = self.num.value() as i64; + + if num == 0 { + return None; + } + + let period = if self.day.value() { + todo_txt::task::Period::Day + } else if self.week.value() { + todo_txt::task::Period::Week + } else if self.month.value() { + todo_txt::task::Period::Month + } else if self.year.value() { + todo_txt::task::Period::Year + } else { + return None; + }; + + Some(todo_txt::task::Recurrence { + num, + period, + strict: self.strict.value(), + }) + } + + fn set(&mut self, recurrence: Option) { + self.num.set_value( + recurrence + .as_ref() + .map(|x| x.num as f64) + .unwrap_or_default(), + ); + + match recurrence.as_ref().map(|x| x.period) { + Some(todo_txt::task::Period::Day) => self.day.set_value(true), + Some(todo_txt::task::Period::Week) => self.week.set_value(true), + Some(todo_txt::task::Period::Month) => self.month.set_value(true), + Some(todo_txt::task::Period::Year) => self.year.set_value(true), + None => (), + }; + + self.strict + .set_value(recurrence.as_ref().map(|x| x.strict).unwrap_or_default()); + } +} + +#[derive(Default)] +pub struct Model { + num: relm4::binding::F64Binding, + day: relm4::binding::BoolBinding, + week: relm4::binding::BoolBinding, + month: relm4::binding::BoolBinding, + year: relm4::binding::BoolBinding, + strict: relm4::binding::BoolBinding, +} + +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = Option; + type Input = MsgInput; + type Output = MsgOutput; + + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::binding::ConnectBindingExt as _; + + let mut model = Self::default(); + model.set(init); + + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: relm4::ComponentSender) { + use MsgInput::*; + + match msg { + Update => { + sender.output(MsgOutput::Updated(self.recurrence())).ok(); + } + } + } + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + gtk::SpinButton::with_binding(&model.num) { + set_adjustment: >k::Adjustment::new(0., 0., usize::MAX as f64, 1., 5., 1.), + + connect_value_changed => MsgInput::Update, + }, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + append: group = >k::ToggleButton::with_binding(&model.day) { + set_label: "d", + set_tooltip_text: Some("Day"), + + connect_toggled => MsgInput::Update, + }, + gtk::ToggleButton::with_binding(&model.week) { + set_label: "w", + set_tooltip_text: Some("Week"), + set_group: Some(&group), + + connect_toggled => MsgInput::Update, + }, + gtk::ToggleButton::with_binding(&model.month) { + set_label: "m", + set_tooltip_text: Some("Month"), + set_group: Some(&group), + + connect_toggled => MsgInput::Update, + }, + gtk::ToggleButton::with_binding(&model.year) { + set_label: "y", + set_tooltip_text: Some("Year"), + set_group: Some(&group), + + connect_toggled => MsgInput::Update, + }, + gtk::CheckButton::with_binding(&model.strict) { + set_halign: gtk::Align::Center, + set_hexpand: true, + set_label: Some("Strict"), + set_tooltip_text: Some("Use real due date as offset, not today"), + + connect_toggled => MsgInput::Update, + }, + }, + } + } +} diff --git a/src/widgets/repeat.rs b/src/widgets/repeat.rs deleted file mode 100644 index f39a058..0000000 --- a/src/widgets/repeat.rs +++ /dev/null @@ -1,156 +0,0 @@ -use gtk::prelude::*; - -#[derive(relm_derive::Msg)] -pub enum Msg { - Set(Option), - Updated(Option), - UpdateNum, - UpdatePeriod, - UpdateStrict, -} - -impl Repeat { - fn set_recurrence(&self, recurrence: Option) { - self.widgets.day.set_active(false); - self.widgets.week.set_active(false); - self.widgets.month.set_active(false); - self.widgets.year.set_active(false); - - if let Some(recurrence) = recurrence { - use todo_txt::task::Period::*; - - self.widgets - .num - .set_text(format!("{}", recurrence.num).as_str()); - self.widgets.strict.set_active(recurrence.strict); - - match recurrence.period { - Day => self.widgets.day.set_active(true), - Week => self.widgets.week.set_active(true), - Month => self.widgets.month.set_active(true), - Year => self.widgets.year.set_active(true), - } - } else { - self.widgets.num.set_text(""); - self.widgets.strict.set_active(false); - } - } - - fn update_recurrence(&self) { - let recurrence = self.get_recurrence(); - - self.model.stream().emit(Msg::Updated(recurrence)); - } - - fn get_recurrence(&self) -> Option { - let num = self.widgets.num.value() as i64; - - if num == 0 { - return None; - } - - let strict = self.widgets.strict.is_active(); - - let period = if self.widgets.day.is_active() { - todo_txt::task::Period::Day - } else if self.widgets.week.is_active() { - todo_txt::task::Period::Week - } else if self.widgets.month.is_active() { - todo_txt::task::Period::Month - } else if self.widgets.year.is_active() { - todo_txt::task::Period::Year - } else { - return None; - }; - - Some(todo_txt::task::Recurrence { - num, - period, - strict, - }) - } -} - -#[relm_derive::widget] -impl relm::Widget for Repeat { - fn init_view(&mut self) { - self.widgets.num.set_adjustment(>k::Adjustment::new( - 0., - 0., - usize::MAX as f64, - 1., - 5., - 1., - )); - self.set_recurrence(None); - - self.widgets.week.join_group(Some(&self.widgets.day)); - self.widgets.month.join_group(Some(&self.widgets.day)); - self.widgets.year.join_group(Some(&self.widgets.day)); - } - - fn model(relm: &relm::Relm, _: ()) -> relm::Relm { - relm.clone() - } - - fn update(&mut self, event: Msg) { - use Msg::*; - - match event { - Set(recurrence) => self.set_recurrence(recurrence), - Updated(_) => (), - UpdateNum | UpdatePeriod | UpdateStrict => self.update_recurrence(), - } - } - - view! { - gtk::Box { - orientation: gtk::Orientation::Vertical, - #[name="num"] - gtk::SpinButton { - focus_out_event(_, _) => (Msg::UpdateNum, gtk::Inhibit(false)), - }, - gtk::Box { - orientation: gtk::Orientation::Horizontal, - #[name="day"] - gtk::RadioButton { - label: "d", - tooltip_text: Some("Day"), - mode: false, - toggled => Msg::UpdatePeriod, - }, - #[name="week"] - gtk::RadioButton { - label: "w", - tooltip_text: Some("Week"), - mode: false, - toggled => Msg::UpdatePeriod, - }, - #[name="month"] - gtk::RadioButton { - label: "m", - tooltip_text: Some("Month"), - mode: false, - toggled => Msg::UpdatePeriod, - }, - #[name="year"] - gtk::RadioButton { - label: "y", - tooltip_text: Some("Year"), - mode: false, - toggled => Msg::UpdatePeriod, - }, - #[name="strict"] - gtk::CheckButton { - child: { - expand: true, - }, - halign: gtk::Align::Center, - label: "Strict", - tooltip_text: Some("Use real due date as offset, not today"), - toggled => Msg::UpdateStrict, - }, - }, - } - } -} diff --git a/src/widgets/tags.rs b/src/widgets/tags.rs index e094a16..37f1500 100644 --- a/src/widgets/tags.rs +++ b/src/widgets/tags.rs @@ -1,5 +1,5 @@ -use crate::widgets::filter::Msg::{Complete, Edit, Filters}; -use crate::widgets::Filter; +use gtk::prelude::*; +use relm4::ComponentController as _; #[derive(Clone, Copy)] pub enum Type { @@ -7,39 +7,49 @@ pub enum Type { Contexts, } -#[derive(relm_derive::Msg)] -pub enum Msg { +#[derive(Debug)] +pub enum MsgInput { Complete(Box), Edit(Box), UpdateFilters(Vec), Update, } -impl Tags { - fn update_tags(&self, tag: Type) { +#[derive(Debug)] +pub enum MsgOutput { + Complete(Box), + Edit(Box), +} + +pub struct Model { + tag: Type, + filter: relm4::Controller, +} + +impl Model { + fn update_tags(&self) { let list = crate::application::tasks(); - let tags = match tag { + let tags = match self.tag { Type::Projects => list.projects(), Type::Contexts => list.contexts(), }; let tags = tags .iter() - .map(|x| (x.clone(), self.get_progress(tag, &list, x))) + .map(|x| (x.clone(), self.progress(&list, x))) .filter(|&(_, (done, total))| done != total) .collect(); - self.components - .filter - .emit(crate::widgets::filter::Msg::UpdateFilters(tags)); + self.filter + .emit(crate::widgets::filter::MsgInput::UpdateFilters(tags)); } - fn get_progress(&self, tag: Type, list: &crate::tasks::List, current: &str) -> (u32, u32) { + fn progress(&self, list: &crate::tasks::List, current: &str) -> (u32, u32) { list.tasks .iter() .filter(|x| { - for tag in self.get_tags(tag, x) { - if tag == current || tag.starts_with(format!("{current}-").as_str()) { + for tag in self.tags(x) { + if tag == current || tag.starts_with(&format!("{current}-")) { return true; } } @@ -55,7 +65,7 @@ impl Tags { }) } - fn update_tasks(&self, tag: Type, filters: &[String]) { + fn update_tasks(&self, filters: &[String]) { let today = crate::date::today(); let preferences = crate::application::preferences(); let list = crate::application::tasks(); @@ -64,7 +74,7 @@ impl Tags { .tasks .iter() .filter(|x| { - let tags = self.get_tags(tag, x); + let tags = self.tags(x); (preferences.done || !x.finished) && !tags.is_empty() @@ -76,19 +86,22 @@ impl Tags { .cloned() .collect(); - self.components - .filter - .emit(crate::widgets::filter::Msg::UpdateTasks(tasks)); + self.filter + .emit(crate::widgets::filter::MsgInput::UpdateTasks(tasks)); } - fn get_tags<'a>(&self, tag: Type, task: &'a crate::tasks::Task) -> &'a [String] { - match tag { + fn tags<'a>(&self, task: &'a crate::tasks::Task) -> &'a [String] { + match self.tag { Type::Projects => task.projects(), Type::Contexts => task.contexts(), } } fn has_filter(tags: &[String], filters: &[String]) -> bool { + if filters.is_empty() { + return true; + } + for filter in filters { if tags.contains(filter) { return true; @@ -99,31 +112,56 @@ impl Tags { } } -#[relm_derive::widget] -impl relm::Widget for Tags { - fn model(tag: Type) -> Type { - tag +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = Type; + type Input = MsgInput; + type Output = MsgOutput; + + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use relm4::Component as _; + + let filter = + super::filter::Model::builder() + .launch(()) + .forward(sender.input_sender(), |output| match output { + super::filter::MsgOutput::Complete(task) => MsgInput::Complete(task), + super::filter::MsgOutput::Edit(task) => MsgInput::Edit(task), + super::filter::MsgOutput::Filters(filters) => MsgInput::UpdateFilters(filters), + }); + + let model = Self { tag: init, filter }; + + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } } - fn update(&mut self, event: Msg) { - use Msg::*; + fn update(&mut self, msg: Self::Input, sender: relm4::ComponentSender) { + use MsgInput::*; - match event { - Complete(_) | Edit(_) => (), + match msg { + Complete(task) => { + sender.output(MsgOutput::Complete(task)).ok(); + } + Edit(task) => { + sender.output(MsgOutput::Edit(task)).ok(); + } Update => { - self.update_tags(self.model); - self.update_tasks(self.model, &[]); + self.update_tags(); + self.update_tasks(&[]); } - UpdateFilters(filters) => self.update_tasks(self.model, &filters), + UpdateFilters(filters) => self.update_tasks(&filters), } } view! { - #[name="filter"] - Filter { - Complete(ref task) => Msg::Complete(task.clone()), - Edit(ref task) => Msg::Edit(task.clone()), - Filters(ref filter) => Msg::UpdateFilters(filter.clone()), + gtk::Box { + append: model.filter.widget(), } } } diff --git a/src/widgets/task.rs b/src/widgets/task.rs index 02e4438..c46c7d4 100644 --- a/src/widgets/task.rs +++ b/src/widgets/task.rs @@ -1,94 +1,22 @@ -use crate::widgets::Circle; use gtk::prelude::*; -#[derive(relm_derive::Msg)] -pub enum Msg { - Click(gtk::gdk::EventButton), +#[derive(Debug)] +pub enum MsgInput { + Click, + Toggle, +} +#[derive(Debug)] +pub enum MsgOutput { Complete(Box), Edit(Box), - ShowNote, - Toggle, } pub struct Model { - note_label: gtk::Label, - note: gtk::Popover, task: crate::tasks::Task, - relm: relm::Relm, + circle: relm4::Controller, } -#[allow(clippy::cognitive_complexity)] -#[relm_derive::widget] -impl relm::Widget for Task { - fn init_view(&mut self) { - let task = &self.model.task; - - let context = self.root().style_context(); - - if task.finished { - context.add_class("finished"); - } - - if !task.priority.is_lowest() { - let priority = (b'a' + u8::from(task.priority.clone())) as char; - context.add_class(format!("pri_{priority}").as_str()); - } - - let note = task.note.content(); - if note.is_some() { - self.model - .note - .set_relative_to(Some(&self.widgets.note_button)); - self.model.note.add(&self.model.note_label); - } else { - self.widgets.note_button.hide(); - } - - if task.tags.is_empty() { - self.widgets.keywords.hide(); - } else { - let text = task - .tags - .iter() - .map(|(k, v)| format!("{k}: {v}")) - .collect::>() - .join(" · "); - - self.widgets.keywords_label.set_text(&text); - } - - if let Some(threshold) = task.threshold_date { - let date = self.date_alias(threshold); - self.widgets - .threshold_label - .set_text(format!("Deferred until {date}").as_str()); - } else { - self.widgets.threshold_label.hide(); - } - - if task.threshold_date.is_some() && task.due_date.is_some() { - self.widgets.arrow_label.show(); - } else { - self.widgets.arrow_label.hide(); - } - - if let Some(due) = task.due_date { - let date = self.date_alias(due); - - let today = crate::date::today(); - - if due < today { - context.add_class("past"); - } - - self.widgets - .due_label - .set_text(format!("due: {date}").as_str()); - } else { - self.widgets.due_label.hide(); - } - } - +impl Model { fn date_alias(&self, date: chrono::NaiveDate) -> String { let today = crate::date::today(); @@ -102,125 +30,145 @@ impl relm::Widget for Task { date.format("%Y-%m-%d").to_string() } } +} - fn model(relm: &relm::Relm, task: crate::tasks::Task) -> Model { - use crate::tasks::Markup; +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = crate::tasks::Task; + type Input = MsgInput; + type Output = MsgOutput; - let note_label = gtk::Label::new(None); - note_label.show(); + fn init( + init: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + use crate::tasks::Markup as _; + use relm4::Component as _; + use relm4::ComponentController as _; - if let Some(ref note) = task.note.markup() { - note_label.set_markup(note); - } + let circle = crate::widgets::circle::Model::builder() + .launch(init.clone()) + .detach(); + + let model = Self { task: init, circle }; - let note = gtk::Popover::new(None::<>k::Button>); - note.set_position(gtk::PositionType::Right); + let widgets = view_output!(); - Model { - note_label, - note, - task, - relm: relm.clone(), + if let Some(due) = model.task.due_date { + let today = crate::date::today(); + + if due < today { + widgets.due_label.add_css_class("past"); + } } - } - fn update(&mut self, event: Msg) { - use Msg::*; - - match event { - Click(event) => { - if event.event_type() == gtk::gdk::EventType::DoubleButtonPress { - self.model - .relm - .stream() - .emit(Edit(Box::new(self.model.task.clone()))); - } + let gesture = gtk::GestureClick::new(); + gesture.connect_pressed(move |_, n_press, _, _| { + if n_press == 2 { + sender.input(MsgInput::Click); } - Complete(_) | Edit(_) => (), - ShowNote => self.model.note.popup(), - Toggle => self - .model - .relm - .stream() - .emit(Complete(Box::new(self.model.task.clone()))), + }); + root.add_controller(gesture); + + if !model.task.priority.is_lowest() { + let priority = (b'a' + u8::from(model.task.priority.clone())) as char; + root.add_css_class(&format!("pri_{priority}")); } + + relm4::ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: relm4::ComponentSender) { + match msg { + MsgInput::Toggle => sender + .output(MsgOutput::Complete(Box::new(self.task.clone()))) + .ok(), + MsgInput::Click => sender + .output(MsgOutput::Edit(Box::new(self.task.clone()))) + .ok(), + }; } view! { - #[style_class="task"] - gtk::EventBox { - button_press_event(_, event) => (Msg::Click(event.clone()), gtk::Inhibit(false)), + gtk::Box { + add_css_class: "task", + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + gtk::Box { - orientation: gtk::Orientation::Horizontal, - spacing: 5, + set_orientation: gtk::Orientation::Vertical, + gtk::Box { - orientation: gtk::Orientation::Vertical, - child: { - expand: true, - fill: true, + set_hexpand: true, + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + + gtk::CheckButton { + set_active: model.task.finished, + + connect_toggled => MsgInput::Toggle, + }, + gtk::Label { + set_markup: model.task.markup_subject().as_str(), + set_xalign: 0., + }, + }, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + + gtk::MenuButton { + set_icon_name: "text-x-generic", + set_visible: model.task.has_note(), + + #[wrap(Some)] + set_popover = >k::Popover { + set_position: gtk::PositionType::Right, + + gtk::Label { + set_markup?: &model.task.note.markup(), + }, + }, }, + #[name="keywords"] gtk::Box { - spacing: 5, - orientation: gtk::Orientation::Horizontal, - gtk::CheckButton { - active: self.model.task.finished, - toggled => Msg::Toggle, + set_visible: !model.task.tags.is_empty(), + + gtk::Image { + set_icon_name: Some("mail-attachment"), }, + #[name="keywords_label"] gtk::Label { - child: { - expand: true, - fill: true, - }, - markup: self.model.task.markup_subject().as_str(), - xalign: 0., + set_text: &model.task.tags.iter().map(|(k, v)| format!("{k}: {v}")).collect::>().join(" · "), }, }, gtk::Box { - spacing: 5, - orientation: gtk::Orientation::Horizontal, - #[name="note_button"] - gtk::Button { - image: Some(>k::Image::from_icon_name(Some("text-x-generic"), gtk::IconSize::LargeToolbar)), - clicked => Msg::ShowNote, + add_css_class: "date", + set_halign: gtk::Align::End, + set_hexpand: true, + set_spacing: 5, + set_valign: gtk::Align::End, + + gtk::Label { + add_css_class: "threshold", + set_text?: &model.task.threshold_date.map(|x| format!("Deferred until {}", model.date_alias(x))), + set_visible: model.task.threshold_date.is_some(), }, - #[name="keywords"] - gtk::Box { - gtk::Image { - icon_name: Some("mail-attachment"), - }, - #[name="keywords_label"] - gtk::Label { - }, + gtk::Label { + set_text: " ➡ ", + set_visible: model.task.threshold_date.is_some() && model.task.due_date.is_some(), }, - #[style_class="date"] - gtk::Box { - spacing: 5, - child: { - pack_type: gtk::PackType::End, - }, - #[name="threshold_label"] - #[style_class="threshold"] - gtk::Label { - }, - #[name="arrow_label"] - gtk::Label { - text: " ➡ ", - }, - #[name="due_label"] - #[style_class="due"] - gtk::Label { - }, + #[name="due_label"] + gtk::Label { + add_css_class: "due", + set_text?: &model.task.due_date.map(|x| format!("due {}", model.date_alias(x))), + set_visible: model.task.due_date.is_some(), }, }, }, - #[name="circle"] - Circle(self.model.task.clone()) { - child: { - expand: false, - fill: true, - }, - }, }, + append: model.circle.widget(), } } } diff --git a/src/widgets/tasks.rs b/src/widgets/tasks.rs index 73b29fc..1673c37 100644 --- a/src/widgets/tasks.rs +++ b/src/widgets/tasks.rs @@ -1,100 +1,94 @@ use gtk::prelude::*; -use relm::ContainerWidget; -#[derive(relm_derive::Msg)] +#[derive(Debug)] pub enum Msg { - Edit(Box), - Complete(Box), Update(Vec), } pub struct Model { - children: Vec>, - relm: relm::Relm, + children: Vec>, + list_box: gtk::ListBox, } -impl Tasks { - fn update_tasks(&mut self, tasks: &[crate::tasks::Task]) { +impl Model { + fn update_tasks(&mut self, sender: relm4::ComponentSender, tasks: &[crate::tasks::Task]) { + use relm4::Component as _; + use relm4::ComponentController as _; + self.clear(); if tasks.is_empty() { - self.widgets.list_box.hide(); - self.widgets.label.show(); - } else { - self.widgets.list_box.show(); - self.widgets.label.hide(); - - let mut sorted_tasks = tasks.to_owned(); - sorted_tasks.sort(); - sorted_tasks.reverse(); - - for task in &sorted_tasks { - let child = self - .widgets - .list_box - .add_widget::(task.clone()); - - relm::connect!( - child@crate::widgets::task::Msg::Complete(ref task), - self.model.relm, - Msg::Complete(task.clone()) - ); - relm::connect!( - child@crate::widgets::task::Msg::Edit(ref task), - self.model.relm, - Msg::Edit(task.clone()) - ); - - self.model.children.push(child); - } + self.list_box.set_visible(false); + return; + } + + let mut sorted_tasks = tasks.to_owned(); + sorted_tasks.sort(); + sorted_tasks.reverse(); + + for task in &sorted_tasks { + let child = super::task::Model::builder() + .launch(task.clone()) + .forward(sender.output_sender(), std::convert::identity); + + self.list_box.append(child.widget()); + + self.children.push(child); } } fn clear(&mut self) { - for child in self.widgets.list_box.children() { - self.widgets.list_box.remove(&child); - } - self.model.children = Vec::new(); + self.list_box.remove_all(); + self.children = Vec::new(); } } -#[relm_derive::widget] -impl relm::Widget for Tasks { - fn model(relm: &relm::Relm, _: ()) -> Model { - Model { +#[relm4::component(pub)] +impl relm4::SimpleComponent for Model { + type Init = (); + type Input = Msg; + type Output = crate::widgets::task::MsgOutput; + + fn init( + _init: Self::Init, + root: Self::Root, + _sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let list_box = gtk::ListBox::new(); + list_box.set_hexpand(true); + list_box.set_vexpand(true); + + let model = Self { children: Vec::new(), - relm: relm.clone(), - } + list_box, + }; + + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } } - fn update(&mut self, event: Msg) { + fn update(&mut self, msg: Self::Input, sender: relm4::ComponentSender) { use Msg::*; - match event { - Complete(_) | Edit(_) => (), - Update(tasks) => self.update_tasks(&tasks), + match msg { + Update(tasks) => self.update_tasks(sender, &tasks), } } view! { gtk::ScrolledWindow { gtk::Box { - #[name="list_box"] - gtk::ListBox { - child: { - fill: true, - expand: true, - }, - }, - #[name="label"] + append: &model.list_box, + gtk::Label { - child: { - fill: true, - expand: true, - }, - text: "Nothing to do :)", + #[watch] + set_visible: model.children.is_empty(), + set_hexpand: true, + set_vexpand: true, + set_text: "Nothing to do :)", }, - } - } + }, + }, } }