commit 8e80c0a69cd847822ccbaa69d0b01eb11ffab0b8 Author: Kenny Levinsen Date: Wed Jan 11 00:28:54 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..19c8739 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1161 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330223a1aecc308757b9926e9391c9b47f8ef2dbd8aea9df88312aea18c5e8d6" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "calloop" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19457a0da465234abd76134a5c2a910c14bd3c5558463e4396ab9a37a328e465" +dependencies = [ + "log", + "nix 0.25.1", + "slotmap", + "thiserror", + "vec_map", +] + +[[package]] +name = "cc" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "const-cstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cxx" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dbus" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "fontconfig" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b0653582d9c82645f95bacd0dc87b49a3126f7a9633be8cafa9c6d17d525847" +dependencies = [ + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" + +[[package]] +name = "libdbus-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "memmap2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", + "static_assertions", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e6affeb1632d6ff6a23d2cd40ffed138e82f1532571a26f527c8a284bb2fbb" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "pest" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065" +dependencies = [ + "once_cell", + "pest", + "sha1", +] + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rcalc_lib" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83922b4c02be427b60b94ca2b4118a6a4fb2b9df4ad88c50c56d9ebd93455c9" +dependencies = [ + "dtoa", + "lazy_static", + "num-bigint", + "num-complex", + "num-rational", + "num-traits", + "pest", + "pest_derive", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rusttype" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff8374aa04134254b7995b63ad3dc41c7f7236f69528b28553da7d72efaa967" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "ttf-parser" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wayland-backend" +version = "0.1.0-beta.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50598bd298e1e01de02ad3cdcfcfcdb961189e40fea41d9325bc07ba81f8740d" +dependencies = [ + "cc", + "downcast-rs", + "io-lifetimes", + "nix 0.25.1", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.30.0-beta.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae06ffbf163db5cc6b40f9aa0ec66e7e1c22b979a327daf209814277f18834e1" +dependencies = [ + "bitflags", + "calloop", + "futures-channel", + "futures-core", + "log", + "nix 0.25.1", + "thiserror", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.30.0-beta.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8491945e2705791d58695ae166bb05bfb1f01f2b7b848faa73966e3462269a2f" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.30.0-beta.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28407261ad2bab693652acd84e8abcf1d2bef7270b5cc840ec8c6bed7c715838" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", + "syn", +] + +[[package]] +name = "wayland-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b2a02ac608e07132978689a6f9bf4214949c85998c247abadd4f4129b1aa06" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "winapi" +version = "0.3.9" +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" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[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-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "wldash2" +version = "0.1.0" +dependencies = [ + "bitflags", + "calloop", + "chrono", + "dbus", + "fontconfig", + "fuzzy-matcher", + "itertools", + "memmap", + "nix 0.26.1", + "rcalc_lib", + "rust-ini", + "rusttype", + "shlex", + "tempfile", + "unicode-segmentation", + "walkdir", + "wayland-client", + "wayland-protocols", + "xkbcommon", +] + +[[package]] +name = "xkbcommon" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbee136714379ab22da0280207fdb7f47e0bb940adea97731b65598b8c7a92e" +dependencies = [ + "libc", + "memmap2", +] + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec657fd32bbcbeaef5c7bc8e10b3db95b143fab8db0a50079773dbf936fd4f73" +dependencies = [ + "const-cstr", + "dlib", + "once_cell", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..686767f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "wldash2" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +wayland-client = { version = "0.30.0-beta.13", features = ["calloop", "log"] } +wayland-protocols = { version = "0.30.0-beta.13", features = ["client", "unstable", "staging"] } +tempfile = "3.3" +nix = "0.26" +memmap = "0.7" +fontconfig = "0.6.0" +rusttype = "^0.9" +calloop = { version = "0.10" } +chrono = "0.4" + +walkdir = "2.3" +itertools = "0.10" +rust-ini = "0.18" + +bitflags = "1.3" + +unicode-segmentation = "1.10" +fuzzy-matcher = "0.3" +shlex = "1.1" + +xkbcommon = { version = "0.5", features = ["wayland"] } +dbus = "0.9" + +rcalc_lib = "1.0" diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000..8a2dc0f --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,321 @@ +use std::{ + ffi::CStr, + fs::File, + io, + os::unix::io::{FromRawFd, RawFd}, + time::SystemTime, + time::UNIX_EPOCH, +}; + +#[cfg(target_os = "linux")] +use nix::sys::memfd; +use nix::{ + errno::Errno, + fcntl, + sys::{mman, stat}, + unistd, +}; + +use memmap::MmapMut; + +use wayland_client::{ + protocol::{wl_buffer, wl_shm, wl_shm_pool}, + Dispatch, QueueHandle, +}; + +use crate::color::Color; +use crate::widgets::Geometry; + +fn create_shm_fd() -> io::Result { + // Only try memfd on linux + #[cfg(target_os = "linux")] + loop { + match memfd::memfd_create( + CStr::from_bytes_with_nul(b"wldash\0").unwrap(), + memfd::MemFdCreateFlag::MFD_CLOEXEC, + ) { + Ok(fd) => return Ok(fd), + Err(Errno::EINTR) => continue, + Err(Errno::ENOSYS) => break, + Err(errno) => return Err(io::Error::from(errno)), + } + } + + // Fallback to using shm_open + let sys_time = SystemTime::now(); + let mut mem_file_handle = format!( + "/wldash-{}", + sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + loop { + match mman::shm_open( + mem_file_handle.as_str(), + fcntl::OFlag::O_CREAT + | fcntl::OFlag::O_EXCL + | fcntl::OFlag::O_RDWR + | fcntl::OFlag::O_CLOEXEC, + stat::Mode::S_IRUSR | stat::Mode::S_IWUSR, + ) { + Ok(fd) => match mman::shm_unlink(mem_file_handle.as_str()) { + Ok(_) => return Ok(fd), + Err(errno) => match unistd::close(fd) { + Ok(_) => return Err(io::Error::from(errno)), + Err(errno) => return Err(io::Error::from(errno)), + }, + }, + Err(Errno::EEXIST) => { + // If a file with that handle exists then change the handle + mem_file_handle = format!( + "/wldash-{}", + sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + continue; + } + Err(Errno::EINTR) => continue, + Err(errno) => return Err(io::Error::from(errno)), + } + } +} + +pub struct ShmBuffer { + pub file: File, + pub mmap: MmapMut, + pub buffer: wl_buffer::WlBuffer, + pub refcnt: u32, +} + +impl ShmBuffer { + pub fn new( + shm: &wl_shm::WlShm, + qh: &QueueHandle, + width: i32, + height: i32, + stride: i32, + format: wl_shm::Format, + ) -> io::Result + where + D: Dispatch + Dispatch + 'static, + { + let mem_fd = create_shm_fd()?; + let file = unsafe { File::from_raw_fd(mem_fd) }; + file.set_len((height * stride) as u64)?; + let pool = shm.create_pool(mem_fd, height * stride, &qh, ()); + let mmap = unsafe { MmapMut::map_mut(&file).unwrap() }; + let buffer = pool.create_buffer(0, width, height, stride, format, &qh, ()); + + Ok(ShmBuffer { + refcnt: 0, + file, + mmap, + buffer, + }) + } + + pub fn acquire(&mut self) { + self.refcnt += 1; + } + + pub fn release(&mut self) { + self.refcnt -= 1; + } +} + +pub struct BufferManager { + pub buffers: Vec, +} + +impl BufferManager { + pub fn clear_buffers(&mut self) { + for buf in self.buffers.iter_mut() { + buf.buffer.destroy() + } + self.buffers.clear() + } + + pub fn next_buffer(&mut self) -> Option<&mut ShmBuffer> { + for buf in self.buffers.iter_mut() { + if buf.refcnt == 0 { + return Some(buf); + } + } + None + } + + pub fn add_buffer( + &mut self, + wl_shm: &wl_shm::WlShm, + dimensions: (i32, i32), + qh: &QueueHandle, + ) where + D: Dispatch + Dispatch + 'static, + { + let mut buf = ShmBuffer::new( + wl_shm, + qh, + dimensions.0, + dimensions.1, + dimensions.0 * 4, + wl_shm::Format::Argb8888, + ) + .expect("unable to add buffer"); + BufferView::new(&mut buf.mmap, (dimensions.0 as u32, dimensions.1 as u32)); + self.buffers.push(buf); + } +} + +pub struct BufferView<'a> { + buf: &'a mut MmapMut, + dimensions: (u32, u32), + subdimensions: Option<(u32, u32, u32, u32)>, +} + +impl<'a> BufferView<'a> { + pub fn new(buf: &'a mut MmapMut, dimensions: (u32, u32)) -> BufferView { + BufferView { + buf, + dimensions, + subdimensions: None, + } + } + + pub fn get_bounds(&self) -> (u32, u32, u32, u32) { + if let Some(subdim) = self.subdimensions { + subdim + } else { + (0, 0, self.dimensions.0, self.dimensions.1) + } + } + + pub fn subdimensions( + &mut self, + subdimensions: (u32, u32, u32, u32), + ) -> Result { + let bounds = self.get_bounds(); + if subdimensions.0 + subdimensions.2 > bounds.2 + || subdimensions.1 + subdimensions.3 > bounds.3 + { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!( + "cannot create subdimensions larger than buffer: {:?} > {:?}", + subdimensions, bounds + ), + )); + } + + Ok(BufferView { + buf: self.buf, + dimensions: self.dimensions, + subdimensions: Some(( + subdimensions.0 + bounds.0, + subdimensions.1 + bounds.1, + subdimensions.2, + subdimensions.3, + )), + }) + } + + pub fn subgeometry(&mut self, geo: Geometry) -> Result { + self.subdimensions((geo.x, geo.y, geo.width, geo.height)) + } + + pub fn offset(&mut self, offset: (u32, u32)) -> Result { + let bounds = self.get_bounds(); + if offset.0 > bounds.2 || offset.1 > bounds.3 { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!( + "cannot create offset outside buffer: {:?} > {:?}", + offset, bounds + ), + )); + } + + Ok(BufferView { + buf: self.buf, + dimensions: self.dimensions, + subdimensions: Some(( + offset.0 + bounds.0, + offset.1 + bounds.1, + bounds.2 - offset.0, + bounds.3 - offset.1, + )), + }) + } + + pub fn limit(&mut self, dimensions: (u32, u32)) -> Result { + let bounds = self.get_bounds(); + if dimensions.0 > bounds.2 || dimensions.1 > bounds.3 { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!( + "cannot create offset outside buffer: {:?} > {:?}", + dimensions, bounds + ), + )); + } + + Ok(BufferView { + buf: self.buf, + dimensions: self.dimensions, + subdimensions: Some((bounds.0, bounds.1, dimensions.0, dimensions.1)), + }) + } + + pub fn memset(&mut self, c: &Color) { + let v = c.as_argb8888(); + if let Some(subdim) = self.subdimensions { + unsafe { + let ptr = self.buf.as_mut_ptr(); + for y in subdim.1..(subdim.1 + subdim.3) { + for x in subdim.0..(subdim.0 + subdim.2) { + *((ptr as *mut u32).offset((x + y * self.dimensions.0) as isize)) = v; + } + } + } + } else { + unsafe { + let ptr = self.buf.as_mut_ptr(); + for p in 0..(self.dimensions.0 * self.dimensions.1) { + *((ptr as *mut u32).offset(p as isize)) = v; + } + } + } + } + + pub fn put(&mut self, pos: (u32, u32), c: &Color) -> Result<(), ::std::io::Error> { + let true_pos = if let Some(subdim) = self.subdimensions { + if pos.0 >= subdim.2 || pos.1 >= subdim.3 { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!( + "put({:?}) is not within subdimensions of buffer ({:?})", + pos, subdim + ), + )); + } + (pos.0 + subdim.0, pos.1 + subdim.1) + } else { + if pos.0 >= self.dimensions.0 || pos.1 >= self.dimensions.1 { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!( + "put({:?}) is not within dimensions of buffer ({:?})", + pos, self.dimensions + ), + )); + } + pos + }; + + unsafe { + let ptr = self + .buf + .as_mut_ptr() + .offset(4 * (true_pos.0 + (true_pos.1 * self.dimensions.0)) as isize); + *(ptr as *mut u32) = c.as_argb8888(); + }; + + Ok(()) + } +} diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..67675b2 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,71 @@ +//use serde::{Deserialize, Serialize}; + +//#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] +pub struct Color { + red: f32, + green: f32, + blue: f32, + opacity: f32, +} + +impl Color { + pub fn new(red: f32, green: f32, blue: f32, opacity: f32) -> Color { + let (red, green, blue, opacity) = clamp(red, green, blue, opacity); + Color { + red, + green, + blue, + opacity, + } + } + + pub fn blend(&self, other: &Color, ratio: f32) -> Color { + let ratio = clamp_f32(ratio, 0.0, 1.0); + + Color { + red: blend_f32(self.red, other.red, ratio), + green: blend_f32(self.green, other.green, ratio), + blue: blend_f32(self.blue, other.blue, ratio), + opacity: blend_f32(self.opacity, other.opacity, ratio), + } + } + + #[inline] + pub fn as_argb8888(&self) -> u32 { + ((255.0 * self.opacity) as u32 & 0xFF) << 24 + | ((255.0 * self.red) as u32 & 0xFF) << 16 + | ((255.0 * self.green) as u32 & 0xFF) << 8 + | ((255.0 * self.blue) as u32 & 0xFF) + } +} + +#[inline] +fn clamp(r: f32, g: f32, b: f32, o: f32) -> (f32, f32, f32, f32) { + clamp_naive(r, g, b, o) +} + +#[inline] +fn clamp_naive(r: f32, g: f32, b: f32, o: f32) -> (f32, f32, f32, f32) { + ( + clamp_f32(r, 0.0, 1.0), + clamp_f32(g, 0.0, 1.0), + clamp_f32(b, 0.0, 1.0), + clamp_f32(o, 0.0, 1.0), + ) +} + +#[inline] +fn clamp_f32(v: f32, a: f32, b: f32) -> f32 { + if v > b { + b + } else if v < a { + a + } else { + v + } +} + +#[inline] +fn blend_f32(a: f32, b: f32, r: f32) -> f32 { + a + ((b - a) * r) +} diff --git a/src/draw.rs b/src/draw.rs new file mode 100644 index 0000000..747dc6c --- /dev/null +++ b/src/draw.rs @@ -0,0 +1,339 @@ +use crate::buffer::BufferView; +use crate::{color::Color, fonts::FontRef}; + +use rusttype::{point, Scale}; +use std::collections::HashMap; + +struct CachedGlyph { + dimensions: (u32, u32), + origin: (i32, i32), + render: Vec, + advance: i32, +} + +impl CachedGlyph { + fn new(font: FontRef, size: f32, ch: char) -> CachedGlyph { + let scale = Scale::uniform(size); + let v_metrics = font.v_metrics(scale); + let glyph = font.glyph(ch).scaled(scale); + let advance = glyph.h_metrics().advance_width as i32; + let glyph = glyph.positioned(point(0.0, v_metrics.ascent)); + + if let Some(bounding_box) = glyph.pixel_bounding_box() { + let origin = (bounding_box.min.x, bounding_box.min.y); + + let dimensions = ( + (bounding_box.max.x - bounding_box.min.x) as u32, + (bounding_box.max.y - bounding_box.min.y) as u32, + ); + let mut render = vec![0.0; (dimensions.0 * dimensions.1) as usize]; + glyph.draw(|x, y, o| { + let pos = x + (y * dimensions.0); + render[pos as usize] = o; + }); + CachedGlyph { + origin, + dimensions, + render, + advance, + } + } else { + CachedGlyph { + origin: (0, 0), + dimensions: (0, 0), + render: Vec::new(), + advance: (size / 4.0) as i32, + } + } + } + + fn draw(&self, buf: &mut BufferView, pos: (i32, i32), bg: &Color, c: &Color) { + let mut x = 0; + let mut y = 0; + for v in &self.render { + let _ = buf.put( + ( + (x + pos.0 + self.origin.0) as u32, + (y + pos.1 + self.origin.1) as u32, + ), + &bg.blend(&c, *v), + ); + + if x == self.dimensions.0 as i32 - 1 { + y += 1; + x = 0; + } else { + x += 1; + } + } + } +} + +pub struct Font<'a> { + glyphs: HashMap, + font: FontRef<'a>, + size: f32, +} + +impl<'a> Font<'a> { + pub fn new(font: FontRef<'a>, size: f32) -> Font { + Font { + glyphs: HashMap::new(), + font, + size, + } + } + + pub fn add_str_to_cache(&mut self, s: &str) { + for ch in s.chars() { + if self.glyphs.get(&ch).is_none() { + let glyph = CachedGlyph::new(self.font, self.size, ch); + self.glyphs.insert(ch, glyph); + } + } + } + + pub fn draw_text( + &self, + buf: &mut BufferView, + bg: &Color, + c: &Color, + s: &str, + ) -> Result<(u32, u32), ::std::io::Error> { + let mut x_off = 0; + let mut off = 0; + let mut glyphs = Vec::with_capacity(s.len()); + for ch in s.chars() { + let glyph = match self.glyphs.get(&ch) { + Some(glyph) => glyph, + None => { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!("glyph for {:} not in cache", ch), + )) + } + }; + glyphs.push(glyph); + if glyph.origin.1 < off { + off = glyph.origin.1 + } + } + for glyph in glyphs { + glyph.draw(buf, (x_off, -off), bg, c); + x_off += glyph.advance; + } + + Ok((x_off as u32, self.size as u32)) + } + + pub fn draw_text_with_cursor( + &self, + buf: &mut BufferView, + bg: &Color, + c: &Color, + s: &str, + cursor: usize, + ) -> Result<(u32, u32), ::std::io::Error> { + let mut x_off: i32 = 0; + let mut off: i32 = 0; + let mut glyphs = Vec::with_capacity(s.len()); + for ch in s.chars() { + let glyph = match self.glyphs.get(&ch) { + Some(glyph) => glyph, + None => { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!("glyph for {:} not in cache", ch), + )) + } + }; + glyphs.push(glyph); + if glyph.origin.1 < off { + off = glyph.origin.1 + } + } + + let height = buf.get_bounds().3; + for (i, glyph) in glyphs.iter().enumerate() { + if i == cursor { + self.draw_cursor(buf, c, x_off as u32, height)?; + } + glyph.draw(buf, (x_off, -off), bg, c); + x_off += glyph.advance; + } + if cursor == glyphs.len() { + self.draw_cursor(buf, c, x_off as u32, height)?; + } + + Ok((x_off as u32, self.size as u32)) + } + + pub fn draw_cursor( + &self, + buf: &mut BufferView, + c: &Color, + offset: u32, + height: u32, + ) -> Result<(), ::std::io::Error> { + // draw cursor + for i in 1..height { + buf.put((offset, i), c)? + } + Ok(()) + } + + pub fn auto_widest(&mut self, s: &str) -> Result { + self.add_str_to_cache(s); + let mut max = 0; + for ch in s.chars() { + let glyph = match self.glyphs.get(&ch) { + Some(glyph) => glyph, + None => { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!("glyph for {:} not in cache", ch), + )) + } + }; + let width = glyph.advance; + if width > max { + max = width + } + } + Ok(max as u32) + } + + pub fn auto_draw_text( + &mut self, + buf: &mut BufferView, + bg: &Color, + c: &Color, + s: &str, + ) -> Result<(u32, u32), ::std::io::Error> { + self.add_str_to_cache(s); + self.draw_text(buf, bg, c, s) + } + + pub fn auto_draw_text_with_cursor( + &mut self, + buf: &mut BufferView, + bg: &Color, + c: &Color, + s: &str, + cursor: usize, + ) -> Result<(u32, u32), ::std::io::Error> { + self.add_str_to_cache(s); + self.draw_text_with_cursor(buf, bg, c, s, cursor) + } + + pub fn draw_text_fixed_width( + &self, + buf: &mut BufferView, + bg: &Color, + c: &Color, + distances: &[u32], + s: &str, + ) -> Result<(u32, u32), ::std::io::Error> { + let mut x_off = 0; + let mut off = 0; + let mut glyphs = Vec::with_capacity(s.len()); + for ch in s.chars() { + let glyph = match self.glyphs.get(&ch) { + Some(glyph) => glyph, + None => { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!("glyph for {:} not in cache", ch), + )) + } + }; + glyphs.push(glyph); + if glyph.origin.1 < off { + off = glyph.origin.1 + } + } + for (idx, glyph) in glyphs.into_iter().enumerate() { + glyph.draw(buf, (x_off, -off), bg, c); + x_off += distances[idx] as i32; + } + + Ok((x_off as u32, self.size as u32)) + } + + pub fn draw_text_individual_colors( + &self, + buf: &mut BufferView, + bg: &Color, + color: &[Color], + s: &str, + ) -> Result<(u32, u32), ::std::io::Error> { + let mut x_off = 0; + let mut off = 0; + let mut glyphs = Vec::with_capacity(s.len()); + for ch in s.chars() { + let glyph = match self.glyphs.get(&ch) { + Some(glyph) => glyph, + None => { + return Err(::std::io::Error::new( + ::std::io::ErrorKind::Other, + format!("glyph for {:} not in cache", ch), + )) + } + }; + glyphs.push(glyph); + if glyph.origin.1 < off { + off = glyph.origin.1 + } + } + for (idx, glyph) in glyphs.into_iter().enumerate() { + glyph.draw(buf, (x_off, -off), bg, &color[idx]); + x_off += glyph.advance; + } + + Ok((x_off as u32, self.size as u32)) + } + + pub fn auto_draw_text_individual_colors( + &mut self, + buf: &mut BufferView, + bg: &Color, + color: &[Color], + s: &str, + ) -> Result<(u32, u32), ::std::io::Error> { + self.add_str_to_cache(s); + self.draw_text_individual_colors(buf, bg, color, s) + } +} + +pub fn draw_box(buf: &mut BufferView, c: &Color, dim: (u32, u32)) -> Result<(), ::std::io::Error> { + for x in 0..dim.0 { + let _ = buf.put((x, 0), c); + let _ = buf.put((x, dim.1 - 1), c); + } + for y in 0..dim.1 { + buf.put((0, y), c)?; + buf.put((dim.0 - 1, y), c)?; + } + + Ok(()) +} + +pub fn draw_bar( + buf: &mut BufferView, + color: &Color, + length: u32, + height: u32, + fill: f32, +) -> Result<(), ::std::io::Error> { + let mut fill_pos = ((length as f32) * fill) as u32; + if fill_pos > length { + fill_pos = length; + } + for y in 0..height { + for x in 0..fill_pos { + let _ = buf.put((x, y), color); + } + } + + Ok(()) +} diff --git a/src/fonts.rs b/src/fonts.rs new file mode 100644 index 0000000..7171a16 --- /dev/null +++ b/src/fonts.rs @@ -0,0 +1,128 @@ +//! Utility module for fonts + +use crate::draw; +use fontconfig::Fontconfig as FontConfig; +use rusttype::Font; +use std::{ + collections::HashMap, + fs::File, + hash, + io::Read, + path::{Path, PathBuf}, +}; + +/// FontRef is used to store Fonts on widgets. +pub type FontRef<'a> = &'a rusttype::Font<'a>; + +/// FontSeeker is a marker struct that is used to look up fonts +pub(crate) struct FontSeeker; + +impl FontSeeker { + /// Acts like fc-match. + /// Given a string, it matches it to a font file and returns its path. + pub(crate) fn from_string(name: &str) -> PathBuf { + let fc = FontConfig::new().unwrap(); + fc.find(name, None).unwrap().path + } +} + +/// FontLoader is a marker struct that is used to load files +pub(crate) struct FontLoader; + +impl FontLoader { + /// Given a path, loads it as a Font, which can be rendered to the screen. + pub(crate) fn from_path<'a, P>(path: P) -> Option> + where + P: AsRef, + { + let mut file = File::open(path).expect("Font file not found"); + let mut data = match file.metadata() { + Ok(metadata) => Vec::with_capacity(metadata.len() as usize), + Err(_) => vec![], + }; + file.read_to_end(&mut data).unwrap(); + Font::try_from_vec(data) + } +} + +#[derive(Debug, Copy, Clone)] +struct ComparableF32(f32); +impl ComparableF32 { + fn key(&self) -> u64 { + self.0.to_bits() as u64 + } +} + +impl hash::Hash for ComparableF32 { + fn hash(&self, state: &mut H) + where + H: hash::Hasher, + { + self.key().hash(state) + } +} + +impl PartialEq for ComparableF32 { + fn eq(&self, other: &ComparableF32) -> bool { + self.key() == other.key() + } +} + +impl Eq for ComparableF32 {} + +pub struct FontMap { + fonts: HashMap<(&'static str, ComparableF32), draw::Font<'static>>, + required_fonts: HashMap<&'static str, (&'static str, Vec)>, +} + +impl FontMap { + pub fn new() -> FontMap { + FontMap { + fonts: HashMap::new(), + required_fonts: HashMap::new(), + } + } + + pub fn queue_font_name(&mut self, font_name: &'static str, size: f32) { + match self.required_fonts.get_mut(font_name) { + Some(v) => v.1.push(size), + None => { + self.required_fonts + .insert(font_name, (font_name, vec![size])); + } + } + } + + pub fn queue_font_path(&mut self, font_name: &'static str, font_path: &'static str, size: f32) { + match self.required_fonts.get_mut(font_name) { + Some(v) => v.1.push(size), + None => { + self.required_fonts + .insert(font_name, (font_path, vec![size])); + } + } + } + + pub fn load_fonts(&mut self) { + for (font_name, v) in self.required_fonts.iter() { + let path = if v.0.starts_with("/") { + std::path::PathBuf::from(v.0) + } else { + FontSeeker::from_string(v.0) + }; + let fontref = Box::leak(Box::new( + FontLoader::from_path(path).expect("unable to load font"), + )); + for size in &v.1 { + let font = draw::Font::new(fontref, *size); + self.fonts.insert((font_name, ComparableF32(*size)), font); + } + } + } + + pub fn get_font(&mut self, font_name: &'static str, size: f32) -> &mut draw::Font<'static> { + self.fonts + .get_mut(&(font_name, ComparableF32(size))) + .expect("no font at specified size") + } +} diff --git a/src/keyboard/keysyms.rs b/src/keyboard/keysyms.rs new file mode 100644 index 0000000..276584a --- /dev/null +++ b/src/keyboard/keysyms.rs @@ -0,0 +1,19 @@ +#![allow(missing_docs, non_upper_case_globals, unused_parens, clippy::all)] + +pub const XKB_KEY_BackSpace: u32 = 0xff08; /* Back space, back char */ +//pub const XKB_KEY_Tab: u32 = 0xff09; +pub const XKB_KEY_Return: u32 = 0xff0d; /* Return, enter */ +//pub const XKB_KEY_Escape: u32 = 0xff1b; +pub const XKB_KEY_Delete: u32 = 0xffff; /* Delete, rubout */ +pub const XKB_KEY_Home: u32 = 0xff50; +pub const XKB_KEY_Left: u32 = 0xff51; /* Move left, left arrow */ +pub const XKB_KEY_Up: u32 = 0xff52; /* Move up, up arrow */ +pub const XKB_KEY_Right: u32 = 0xff53; /* Move right, right arrow */ +pub const XKB_KEY_Down: u32 = 0xff54; /* Move down, down arrow */ +pub const XKB_KEY_End: u32 = 0xff57; /* EOL */ + +//pub const XKB_KEY_ISO_Left_Tab: u32 = 0xfe20; +pub const XKB_KEY_e: u32 = 0x0065; /* U+0065 LATIN SMALL LETTER E */ +pub const XKB_KEY_a: u32 = 0x0061; /* U+0061 LATIN SMALL LETTER A */ +//pub const XKB_KEY_c: u32 = 0x0063; /* U+0063 LATIN SMALL LETTER C */ +//pub const XKB_KEY_u: u32 = 0x0075; /* U+0075 LATIN SMALL LETTER U */ diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs new file mode 100644 index 0000000..6b40547 --- /dev/null +++ b/src/keyboard/mod.rs @@ -0,0 +1,136 @@ +//! Utilities for keymap interpretation of keyboard input +//! +//! This module provides an implementation for `wl_keyboard` +//! objects using `libxkbcommon` to interpret the keyboard input +//! given the user keymap. +//! +//! The entry point of this module is the [`map_keyboard`](fn.map_keyboard.html) +//! function which, given a `wl_seat` and a callback, setup keymap interpretation +//! and key repetition for the `wl_keyboard` of this seat. +//! +//! Key repetition relies on an event source, that needs to be inserted in your +//! calloop event loop. Not doing so will prevent key repetition to work +//! (but the rest of the functionnality will not be affected). + +use std::default::Default; +use std::os::unix::io::{AsRawFd, OwnedFd}; + +use wayland_client::{protocol::wl_keyboard, WEnum}; +use xkbcommon::xkb; + +pub mod keysyms; +mod state; + +use self::state::KbState; +pub use self::state::ModifiersState; + +pub struct KeyEvent { + pub serial: u32, + pub time: u32, + pub rawkey: u32, + pub keysym: u32, + pub state: WEnum, + pub modifiers: ModifiersState, + pub utf8: Option, + pub repeats: bool, +} + +pub struct Keyboard { + state: Option, +} + +impl Keyboard { + pub fn new() -> Keyboard { + Keyboard { state: None } + } + + pub fn keymap(&mut self, format: WEnum, fd: OwnedFd, size: u32) { + match format { + WEnum::Value(wl_keyboard::KeymapFormat::XkbV1) => { + self.state = Some( + KbState::new_from_fd(fd.as_raw_fd(), size as usize) + .expect("unable to load keymap"), + ); + } + WEnum::Value(wl_keyboard::KeymapFormat::NoKeymap) => { + // TODO: how to handle this (hopefully never occuring) case? + } + _ => unreachable!(), + } + } + + pub fn key( + &mut self, + serial: u32, + time: u32, + key: u32, + key_state: WEnum, + ) -> KeyEvent { + if let Some(state) = &mut self.state { + let (sym, utf8, repeats) = { + // Get the values to generate a key event + let sym = state.get_one_sym_raw(key); + let utf8 = if key_state == WEnum::Value(wl_keyboard::KeyState::Pressed) { + match state.compose_feed(sym) { + Some(xkb::compose::FeedResult::Accepted) => { + if let Some(status) = state.compose_status() { + match status { + xkb::compose::Status::Composed => state.compose_get_utf8(), + xkb::compose::Status::Nothing => state.get_utf8_raw(key), + _ => None, + } + } else { + state.get_utf8_raw(key) + } + } + Some(_) => { + // XKB_COMPOSE_FEED_IGNORED + None + } + None => { + // XKB COMPOSE is not initialized + state.get_utf8_raw(key) + } + } + } else { + None + }; + let repeats = state.key_repeats(key + 8); + (sym, utf8, repeats) + }; + KeyEvent { + rawkey: key, + keysym: sym, + state: key_state, + modifiers: state.mods_state(), + serial, + time, + utf8, + repeats, + } + } else { + KeyEvent { + rawkey: key, + keysym: 0, + state: key_state, + modifiers: Default::default(), + utf8: None, + repeats: false, + serial, + time, + } + } + } + + pub fn modifiers( + &mut self, + mods_depressed: u32, + mods_latched: u32, + mods_locked: u32, + group: u32, + ) { + if let Some(state) = &mut self.state { + state.update_modifiers(mods_depressed, mods_latched, mods_locked, group); + } + } +} diff --git a/src/keyboard/state.rs b/src/keyboard/state.rs new file mode 100644 index 0000000..ba5ed64 --- /dev/null +++ b/src/keyboard/state.rs @@ -0,0 +1,158 @@ +use std::{env, os::unix::io::RawFd}; + +use xkbcommon::xkb; + +pub struct KbState { + xkb_context: xkb::Context, + xkb_keymap: xkb::Keymap, + xkb_state: xkb::State, + xkb_compose_table: Option, + xkb_compose_state: Option, + mods_state: ModifiersState, +} + +/// Represents the current state of the keyboard modifiers +/// +/// Each field of this struct represents a modifier and is `true` if this modifier is active. +/// +/// For some modifiers, this means that the key is currently pressed, others are toggled +/// (like caps lock). +#[derive(Copy, Clone, Debug, Default)] +pub struct ModifiersState { + /// The "control" key + pub ctrl: bool, + /// The "alt" key + pub alt: bool, + /// The "shift" key + pub shift: bool, + /// The "Caps lock" key + pub caps_lock: bool, + /// The "logo" key + /// + /// Also known as the "windows" key on most keyboards + pub logo: bool, + /// The "Num lock" key + pub num_lock: bool, +} + +impl ModifiersState { + fn new() -> ModifiersState { + ModifiersState::default() + } + + fn update_with(&mut self, state: &xkb::State) { + self.ctrl = state.mod_name_is_active("Control", xkb::STATE_MODS_EFFECTIVE); + self.alt = state.mod_name_is_active("Mod1", xkb::STATE_MODS_EFFECTIVE); + self.shift = state.mod_name_is_active("Shift", xkb::STATE_MODS_EFFECTIVE); + self.caps_lock = state.mod_name_is_active("Lock", xkb::STATE_MODS_EFFECTIVE); + self.num_lock = state.mod_name_is_active("Mod2", xkb::STATE_MODS_EFFECTIVE); + self.logo = state.mod_name_is_active("Mod4", xkb::STATE_MODS_EFFECTIVE); + } +} + +impl KbState { + pub(crate) fn update_modifiers( + &mut self, + mods_depressed: u32, + mods_latched: u32, + mods_locked: u32, + group: u32, + ) { + let mask = + self.xkb_state + .update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); + if (mask & xkb::STATE_MODS_EFFECTIVE) > 0 { + // effective value of mods have changed, we need to update our state + self.mods_state.update_with(&self.xkb_state); + } + } + + pub(crate) fn get_one_sym_raw(&mut self, keycode: u32) -> u32 { + self.xkb_state.key_get_one_sym(keycode + 8) + } + + pub(crate) fn get_utf8_raw(&mut self, keycode: u32) -> Option { + Some(self.xkb_state.key_get_utf8(keycode + 8)) + } + + pub(crate) fn compose_feed(&mut self, keysym: u32) -> Option { + match &mut self.xkb_compose_state { + Some(compose_state) => Some(compose_state.feed(keysym)), + None => None, + } + } + + pub(crate) fn compose_status(&mut self) -> Option { + match &mut self.xkb_compose_state { + Some(compose_state) => Some(compose_state.status()), + None => None, + } + } + + pub(crate) fn compose_get_utf8(&mut self) -> Option { + match &mut self.xkb_compose_state { + Some(compose_state) => compose_state.utf8(), + None => None, + } + } + + pub(crate) fn new_from_fd(fd: RawFd, size: usize) -> Result { + let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let xkb_keymap = unsafe { + xkb::Keymap::new_from_fd( + &xkb_context, + fd, + size, + xkb::KEYMAP_FORMAT_TEXT_V1, + xkb::KEYMAP_COMPILE_NO_FLAGS, + )? + .unwrap() + }; + let xkb_state = xkb::State::new(&xkb_keymap); + let mut me = KbState { + xkb_context, + xkb_keymap, + xkb_state, + xkb_compose_table: None, + xkb_compose_state: None, + mods_state: ModifiersState::new(), + }; + + me.init_compose(); + + Ok(me) + } + + pub(crate) fn init_compose(&mut self) { + let locale = env::var_os("LC_ALL") + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LC_CTYPE")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LANG")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .unwrap_or_else(|| "C".into()); + + let compose_table = match xkb::compose::Table::new_from_locale( + &self.xkb_context, + &locale, + xkb::compose::COMPILE_NO_FLAGS, + ) { + Ok(t) => t, + Err(_) => return, + }; + + let compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS); + + self.xkb_compose_table = Some(compose_table); + self.xkb_compose_state = Some(compose_state); + } + + pub(crate) fn key_repeats(&mut self, keycode: xkb::Keycode) -> bool { + self.xkb_keymap.key_repeats(keycode) + } + + #[inline] + pub(crate) fn mods_state(&self) -> ModifiersState { + self.mods_state + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1841b40 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,605 @@ +mod buffer; +mod color; +mod draw; +mod fonts; +mod keyboard; +mod utils; +mod widgets; + +use wayland_client::{ + protocol::{ + wl_buffer, wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, wl_shm_pool, + wl_surface, + }, + Connection, Dispatch, QueueHandle, WEnum, WaylandSource, +}; + +use wayland_protocols::xdg::{ + activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1}, + shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}, +}; + +use calloop::{ + timer::{TimeoutAction, Timer}, + EventLoop, +}; +use chrono::{Duration, Local, Timelike}; + +use buffer::{BufferManager, BufferView}; +use color::Color; +use fonts::FontMap; +use keyboard::Keyboard; +use widgets::{ + Battery, Clock, Date, Geometry, HorizontalLayout, IndexedLayout, Interface, Layout, Line, + Margin, VerticalLayout, Widget, WidgetUpdater, +}; + +use std::{cell::RefCell, env, mem, rc::Rc, thread}; + +enum MaybeFontMap { + Waiting(thread::JoinHandle), + Ready(Rc>), + Invalid, +} + +impl MaybeFontMap { + fn unwrap(&self) -> Rc> { + match self { + MaybeFontMap::Ready(f) => f.clone(), + _ => panic!("fontmap not yet ready"), + } + } + + fn resolve(&mut self) { + if matches!(self, MaybeFontMap::Waiting(_)) { + let s = mem::replace(self, MaybeFontMap::Invalid); + match s { + MaybeFontMap::Waiting(handle) => { + *self = MaybeFontMap::Ready(Rc::new(RefCell::new(handle.join().unwrap()))); + } + _ => unreachable!(), + } + } + } +} + +struct State { + running: bool, + dirty: bool, + base_surface: Option, + wm_base: Option, + xdg_surface: Option<(xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel)>, + wl_shm: Option, + xdg_activation: Option, + configured: bool, + dimensions: (i32, i32), + bufmgr: BufferManager, + widgets: Vec>, + keyboard: Keyboard, + fonts: MaybeFontMap, +} + +impl State { + fn add_buffer(&mut self, qh: &QueueHandle) { + self.bufmgr.add_buffer( + self.wl_shm.as_ref().expect("missing wl_shm"), + self.dimensions, + qh, + ); + } + + fn activate(&mut self, qh: &QueueHandle) { + let key = match env::var("XDG_ACTIVATION_TOKEN") { + Ok(token) => token, + Err(_) => return, + }; + match (&self.base_surface, &self.xdg_activation) { + (Some(surface), Some(activation)) => { + activation.activate(key, surface); + let activation_token = activation.get_activation_token(qh, ()); + activation_token.commit(); + env::remove_var("XDG_ACTIVATION_TOKEN"); + } + _ => (), + } + } +} + +impl WidgetUpdater for State { + fn geometry_update( + &mut self, + idx: usize, + fonts: &mut FontMap, + geometry: &Geometry, + ) -> Geometry { + self.widgets[idx].geometry_update(fonts, geometry) + } +} + +fn main() { + let font_thread = thread::spawn(move || { + let mut fm = FontMap::new(); + // Hard-coding font paths make things a lot faster + fm.queue_font_path("sans", "/usr/share/fonts/noto/NotoSans-Regular.ttf", 96.); + fm.queue_font_path("sans", "/usr/share/fonts/noto/NotoSans-Regular.ttf", 40.); + fm.queue_font_path("sans", "/usr/share/fonts/noto/NotoSans-Regular.ttf", 24.); + fm.queue_font_path( + "monospace", + "/usr/share/fonts/noto/NotoSansMono-Regular.ttf", + 32., + ); + fm.load_fonts(); + fm + }); + let conn = Connection::connect_to_env().unwrap(); + + let event_queue = conn.new_event_queue(); + let qhandle: QueueHandle = event_queue.handle(); + + let display = conn.display(); + display.get_registry(&qhandle, ()); + + let bufmgr = BufferManager { + buffers: Vec::new(), + }; + + let (ping_sender, ping_source) = calloop::ping::make_ping().unwrap(); + + let clock = Box::new(Clock::new()); + let date = Box::new(Date::new()); + let line = Box::new(Line::new()); + let launcher = Box::new(Interface::new()); + let battery = Box::new(Battery::new(ping_sender)); + + let mut event_loop: EventLoop = + EventLoop::try_new().expect("Failed to initialize the event loop!"); + + let handle = event_loop.handle(); + handle + .insert_source( + WaylandSource::new(event_queue).expect("Could not create WaylandSource!"), + |_event, queue, mut state| queue.dispatch_pending(&mut state), + ) + .expect("Failed to insert event source!"); + + handle + .insert_source(ping_source, |(), &mut (), state| { + state.widgets[2].set_dirty(true); + state.dirty = true; + }) + .expect("Failed to insert ping source!"); + + let clock_source = Timer::from_duration(std::time::Duration::from_secs(1)); + handle + .insert_source(clock_source, |_event, _metadata, state| { + let now = Local::now().naive_local(); + let target = (now + Duration::seconds(60)) + .with_second(0) + .unwrap() + .with_nanosecond(0) + .unwrap(); + let d = target - now; + state.widgets[0].set_dirty(true); + state.widgets[1].set_dirty(true); + state.dirty = true; + + // The timer event source requires us to return a TimeoutAction to + // specify if the timer should be rescheduled. In our case we just drop it. + TimeoutAction::ToDuration(d.to_std().unwrap()) + }) + .expect("Failed to insert event source!"); + + let mut state = State { + running: true, + dirty: true, + base_surface: None, + wm_base: None, + xdg_surface: None, + wl_shm: None, + xdg_activation: None, + configured: false, + dimensions: (320, 240), + bufmgr: bufmgr, + widgets: vec![clock, date, battery, line, launcher], + keyboard: Keyboard::new(), + fonts: MaybeFontMap::Waiting(font_thread), + }; + + let mut damage = Vec::new(); + for _ in 0..state.widgets.len() { + damage.push(Geometry::new()); + } + + while state.running { + event_loop + .dispatch(None, &mut state) + .expect("Could not dispatch event loop"); + + if !state.configured || !state.dirty { + continue; + } + + let mut drew = false; + let mut force = false; + if state.bufmgr.buffers.len() == 0 { + state.add_buffer(&qhandle); + force = true; + } + + // We expect to see the usual shm optimization + let buf = match state.bufmgr.next_buffer() { + Some(b) => b, + None => { + // We are still waiting for our buffer + continue; + } + }; + + state.dirty = false; + buf.acquire(); + let mut bufview = BufferView::new( + &mut buf.mmap, + (state.dimensions.0 as u32, state.dimensions.1 as u32), + ); + + let bg = Color::new(0., 0., 0., 1.); + + let surface = state.base_surface.as_ref().unwrap().clone(); + for (idx, widget) in state.widgets.iter_mut().enumerate() { + if force || widget.get_dirty() { + widget.set_dirty(false); + drew = true; + + let geo = widget.geometry(); + let old_damage = damage[idx]; + + if !force { + bufview.subgeometry(old_damage).unwrap().memset(&bg); + } else { + bufview.subgeometry(geo).unwrap().memset(&bg); + } + + let mut subview = bufview.subgeometry(geo).unwrap(); + let new_damage = widget.draw(&mut state.fonts.unwrap().borrow_mut(), &mut subview); + let combined_damage = new_damage.expand(old_damage); + damage[idx] = new_damage; + surface.damage_buffer( + combined_damage.x as i32, + combined_damage.y as i32, + combined_damage.width as i32, + combined_damage.height as i32, + ); + //draw_box(&mut subview, &Color::new(1.0, 0.5, 0.0, 1.0), (geo.width, geo.height)); + } + } + if !drew { + buf.release(); + continue; + } + + if force { + surface.damage_buffer(0, 0, 0x7FFFFFFF, 0x7FFFFFFF) + } + surface.attach(Some(&buf.buffer), 0, 0); + surface.commit(); + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, interface, .. + } = event + { + match &interface[..] { + "wl_compositor" => { + let compositor = + registry.bind::(name, 4, qh, ()); + let surface = compositor.create_surface(qh, ()); + state.base_surface = Some(surface); + state.activate(qh); + + if state.wm_base.is_some() && state.xdg_surface.is_none() { + state.init_xdg_surface(qh); + } + } + "wl_shm" => { + state.wl_shm = Some(registry.bind::(name, 1, qh, ())); + } + "wl_seat" => { + registry.bind::(name, 1, qh, ()); + } + "xdg_wm_base" => { + let wm_base = registry.bind::(name, 1, qh, ()); + state.wm_base = Some(wm_base); + + if state.base_surface.is_some() && state.xdg_surface.is_none() { + state.init_xdg_surface(qh); + } + } + "xdg_activation_v1" => { + state.xdg_activation = Some( + registry.bind::(name, 1, qh, ()), + ); + state.activate(qh); + } + _ => {} + } + } + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + _: &wl_compositor::WlCompositor, + _: wl_compositor::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + _: &wl_surface::WlSurface, + _: wl_surface::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + _: &wl_shm::WlShm, + _: wl_shm::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + _: &wl_shm_pool::WlShmPool, + _: wl_shm_pool::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + buffer: &wl_buffer::WlBuffer, + _: wl_buffer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + for buf in state.bufmgr.buffers.iter_mut() { + if &buf.buffer == buffer { + buf.release(); + } + } + } +} + +impl State { + fn init_xdg_surface(&mut self, qh: &QueueHandle) { + let wm_base = self.wm_base.as_ref().unwrap(); + let base_surface = self.base_surface.as_ref().unwrap(); + + let xdg_surface = wm_base.get_xdg_surface(base_surface, qh, ()); + let toplevel = xdg_surface.get_toplevel(qh, ()); + toplevel.set_title("wldash".into()); + + base_surface.commit(); + + self.xdg_surface = Some((xdg_surface, toplevel)); + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + wm_base: &xdg_wm_base::XdgWmBase, + event: xdg_wm_base::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + xdg_surface: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial, .. } = event { + xdg_surface.ack_configure(serial); + state.configured = true; + state.dirty = true; + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: xdg_toplevel::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + xdg_toplevel::Event::Configure { width, height, .. } => { + if (width, height) == (0, 0) { + return; + } + if state.dimensions != (width, height) { + state.bufmgr.clear_buffers(); + state.dimensions = (width, height); + let mut layout = VerticalLayout { + widgets: vec![ + Box::new(HorizontalLayout { + widgets: vec![ + Box::new(IndexedLayout { widget_idx: 0 }), + Box::new(Margin { + widget: Box::new(IndexedLayout { widget_idx: 1 }), + margin: (16, 8, 0, 0), + }), + Box::new(Margin { + widget: Box::new(IndexedLayout { widget_idx: 2 }), + margin: (16, 8, 8, 0), + }), + ], + }), + Box::new(IndexedLayout { widget_idx: 3 }), + Box::new(IndexedLayout { widget_idx: 4 }), + ], + }; + + state.fonts.resolve(); + let fonts = state.fonts.unwrap(); + layout.geometry_update( + &mut fonts.borrow_mut(), + &Geometry { + x: 0, + y: 0, + width: width as u32, + height: height as u32, + }, + state, + ); + } + } + xdg_toplevel::Event::Close => { + state.running = false; + } + _ => (), + } + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + if capabilities.contains(wl_seat::Capability::Keyboard) { + seat.get_keyboard(qh, ()); + } + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + wl_keyboard::Event::Key { + serial, + time, + key, + state: kbstate, + } => { + if key == 1 { + state.running = false; + return; + } + let k = state.keyboard.key(serial, time, key, kbstate); + for widget in state.widgets.iter_mut() { + widget.keyboard_input(&k); + } + state.dirty = true; + } + wl_keyboard::Event::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + .. + } => { + state + .keyboard + .modifiers(mods_depressed, mods_latched, mods_locked, group); + } + wl_keyboard::Event::Keymap { format, fd, size } => { + state.keyboard.keymap(format, fd, size); + } + _ => (), + } + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + _: &xdg_activation_v1::XdgActivationV1, + _: xdg_activation_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &xdg_activation_token_v1::XdgActivationTokenV1, + event: xdg_activation_token_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + xdg_activation_token_v1::Event::Done { token } => { + for widget in state.widgets.iter_mut() { + widget.token_update(&token); + } + } + _ => (), + } + } +} diff --git a/src/utils/desktop.rs b/src/utils/desktop.rs new file mode 100644 index 0000000..8457685 --- /dev/null +++ b/src/utils/desktop.rs @@ -0,0 +1,112 @@ +use ini::{Ini, ParseOption}; +use itertools::Itertools; +use std::cmp::Ordering; +use std::env; +use std::error::Error; +use std::io::Error as io_error; +use std::io::ErrorKind; +use walkdir::WalkDir; + +#[derive(Clone, Debug, Eq, Hash)] +pub struct Desktop { + pub entry_type: String, + pub name: String, + pub no_display: bool, + pub hidden: bool, + pub exec: Option, + pub url: Option, + pub term: bool, + pub keywords: Vec, +} + +impl Desktop { + fn parse(f: &str) -> Result> { + let file = Ini::load_from_file_opt( + f, + ParseOption { + enabled_quote: false, + enabled_escape: false, + }, + )?; + match file.section(Some("Desktop Entry")) { + Some(desktop) => Ok(Desktop { + entry_type: desktop.get("Type").unwrap_or(&"".to_string()).to_string(), + name: desktop.get("Name").unwrap_or(&"".to_string()).to_string(), + term: desktop.get("Terminal").unwrap_or(&"".to_string()) == "true", + no_display: desktop.get("NoDisplay").unwrap_or(&"".to_string()) == "true", + hidden: desktop.get("Hidden").unwrap_or(&"".to_string()) == "true", + exec: desktop.get("Exec").map(|x| x.to_string()), + url: desktop.get("URL").map(|x| x.to_string()), + keywords: desktop + .get("Keywords") + .map(|x| { + x.split(';') + .map(|y| y.trim().to_string()) + .filter(|z| z != "") + .collect() + }) + .unwrap_or_else(|| vec![]), + }), + None => Err(Box::new(io_error::new( + ErrorKind::NotFound, + "no desktop entry in file", + ))), + } + } + + fn parse_dir(d: &str) -> Result, Box> { + let mut files: Vec = Vec::with_capacity(16); + for entry in WalkDir::new(d) { + let entry = entry?; + let path = entry.path(); + + if let Ok(d) = Desktop::parse(path.to_str().unwrap()) { + files.push(d) + } + } + + Ok(files) + } +} + +impl Ord for Desktop { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialOrd for Desktop { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Desktop { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +pub fn load_desktop_files() -> Vec { + let home = env::var_os("HOME").unwrap().into_string().unwrap(); + + let xdg_data_home = match env::var_os("XDG_DATA_HOME") { + Some(s) => s.into_string().unwrap(), + None => format!("{}/.local/share", home), + }; + let xdg_data_dirs = match env::var_os("XDG_DATA_DIRS") { + Some(s) => s.into_string().unwrap(), + None => "/usr/local/share:/usr/share".to_string(), + }; + + std::iter::once(xdg_data_home.as_str()) + .chain(xdg_data_dirs.split(':')) + .map(|p| Desktop::parse_dir(&format!("{}/applications", p))) + .filter_map(Result::ok) + .flatten() + .filter(|d| { + !d.hidden && !d.no_display && (d.entry_type == "Application" || d.entry_type == "Link") + }) + .unique_by(|x| x.name.clone()) + .collect() +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..6cf24c7 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod desktop; diff --git a/src/widgets/bar_widget.rs b/src/widgets/bar_widget.rs new file mode 100644 index 0000000..67164ba --- /dev/null +++ b/src/widgets/bar_widget.rs @@ -0,0 +1,98 @@ +use std::{ + cmp::min, + default::Default, +}; + +use crate::{ + buffer::BufferView, + color::Color, + draw::{draw_bar, draw_box}, + fonts::FontMap, + widgets::{Geometry, Widget}, +}; + +const BAR_FONT: &str = "sans"; +const BAR_SIZE: f32 = 24.; + +pub trait BarWidgetImpl { + fn get_dirty(&self) -> bool; + fn set_dirty(&mut self, dirty: bool); + fn update(&mut self); + fn name(&self) -> &'static str; + fn value(&self) -> f32; + fn color(&self) -> Color; +} + +pub struct BarWidget { + geometry: Geometry, + inner_widget: Box, +} + +impl BarWidget { + pub fn new(inner_widget: Box) -> BarWidget { + BarWidget { + geometry: Default::default(), + inner_widget, + } + } +} + +impl Widget for BarWidget { + fn geometry(&self) -> Geometry { + self.geometry + } + + fn get_dirty(&self) -> bool { + self.inner_widget.get_dirty() + } + + fn set_dirty(&mut self, dirty: bool) { + self.inner_widget.set_dirty(dirty) + } + + fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry { + let font = fonts.get_font(BAR_FONT, BAR_SIZE); + let fg = Color::new(1., 1., 1., 1.); + let bg = Color::new(0., 0., 0., 1.); + + font.auto_draw_text(view, &bg, &fg, self.inner_widget.name()) + .unwrap(); + let size = BAR_SIZE.ceil() as u32; + let bar_offset = 4 * size; + let val = self.inner_widget.value(); + + let c = self.inner_widget.color(); + draw_bar( + &mut view.offset((bar_offset, 0)).unwrap(), + &c, + self.geometry.width - bar_offset, + size, + val, + ) + .unwrap(); + + draw_box( + &mut view.offset((bar_offset, 0)).unwrap(), + &c, + (self.geometry.width - bar_offset, size), + ) + .unwrap(); + self.geometry + } + + fn geometry_update(&mut self, _fonts: &mut FontMap, geometry: &Geometry) -> Geometry { + let w = min(geometry.width, 768); + let mut x = geometry.x; + if geometry.width > w { + x += geometry.width - w; + } + self.geometry = Geometry { + x: x, + y: geometry.y, + width: w, + height: BAR_SIZE.ceil() as u32, + }; + self.geometry + } +} + diff --git a/src/widgets/battery.rs b/src/widgets/battery.rs new file mode 100644 index 0000000..57fb3fd --- /dev/null +++ b/src/widgets/battery.rs @@ -0,0 +1,144 @@ +use std::{ + thread, + time::Duration, + sync::{Arc, Mutex}, +}; + +use calloop::ping::Ping; + +use crate::{ + color::Color, + widgets::bar_widget::{ + BarWidget, + BarWidgetImpl, + }, +}; + +enum UpowerBatteryState { + Charging, + Discharging, + Empty, + Full, + NotCharging, + Unknown, +} + +struct InnerBattery { + value: f32, + state: UpowerBatteryState, +} + +fn start_monitor(inner: Arc>, ping: Ping) { + thread::spawn(move || { + use dbus::blocking::LocalConnection; + use dbus::blocking::stdintf::org_freedesktop_dbus::Properties; + use dbus::blocking::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged; + let conn = LocalConnection::new_system().unwrap(); + let device_path = "/org/freedesktop/UPower/devices/DisplayDevice"; + let proxy = conn.with_proxy("org.freedesktop.UPower", device_path, Duration::from_millis(500)); + + let capacity: f64 = proxy.get("org.freedesktop.UPower.Device", "Percentage").expect("unable to get property"); + let state: u32 = proxy.get("org.freedesktop.UPower.Device", "State").expect("unable to get state"); + + { + let mut inner = inner.lock().unwrap(); + inner.value = capacity as f32 / 100.; + inner.state = match state { + 1 => UpowerBatteryState::Charging, + 2 => UpowerBatteryState::Discharging, + 3 => UpowerBatteryState::Empty, + 4 => UpowerBatteryState::Full, + 5 => UpowerBatteryState::NotCharging, + 6 => UpowerBatteryState::Discharging, + _ => UpowerBatteryState::Unknown, + } + } + + + proxy.match_signal(move |c: PropertiesPropertiesChanged, _: &LocalConnection, _: &dbus::Message| { + if c.interface_name != "org.freedesktop.UPower.Device" { + return true; + } + let mut inner = inner.lock().unwrap(); + for (key, value) in c.changed_properties { + match key.as_str() { + "State" => { + inner.state = match value.0.as_u64().unwrap() { + 1 => UpowerBatteryState::Charging, + 2 => UpowerBatteryState::Discharging, + 3 => UpowerBatteryState::Empty, + 4 => UpowerBatteryState::Full, + 5 => UpowerBatteryState::NotCharging, + 6 => UpowerBatteryState::Discharging, + _ => UpowerBatteryState::Unknown, + }; + ping.ping(); + }, + "Percentage" => { + inner.value = value.0.as_f64().unwrap() as f32 / 100.; + ping.ping(); + }, + _ => (), + } + } + true + }).unwrap(); + + loop { conn.process(Duration::from_millis(60000)).unwrap(); } + }); +} + +pub struct Battery { + inner: Arc>, + dirty: bool, +} + +impl Battery { + pub fn new(ping: Ping) -> BarWidget { + let battery = Battery{ + inner: Arc::new(Mutex::new(InnerBattery{ + value: 0., + state: UpowerBatteryState::Unknown, + })), + dirty: false, + }; + start_monitor(battery.inner.clone(), ping); + BarWidget::new(Box::new(battery)) + } +} + + +impl BarWidgetImpl for Battery { + fn update(&mut self) {} + fn get_dirty(&self) -> bool { + self.dirty + } + fn set_dirty(&mut self, dirty: bool) { + self.dirty = dirty + } + fn name(&self) -> &'static str { + "battery" + } + fn value(&self) -> f32 { + let inner = self.inner.lock().unwrap(); + inner.value + } + fn color(&self) -> Color { + let inner = self.inner.lock().unwrap(); + match inner.state { + UpowerBatteryState::Discharging | UpowerBatteryState::Unknown => { + if inner.value > 0.1 { + Color::new(1.0, 1.0, 1.0, 1.0) + } else { + Color::new(1.0, 0.5, 0.0, 1.0) + } + } + UpowerBatteryState::Charging | UpowerBatteryState::Full => { + Color::new(0.5, 1.0, 0.5, 1.0) + } + UpowerBatteryState::NotCharging | UpowerBatteryState::Empty => { + Color::new(1.0, 0.5, 0.5, 1.0) + } + } + } +} diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs new file mode 100644 index 0000000..91b4fce --- /dev/null +++ b/src/widgets/clock.rs @@ -0,0 +1,76 @@ +use std::default::Default; + +use crate::{ + buffer::BufferView, + color::Color, + fonts::FontMap, + widgets::{Geometry, Widget}, +}; + +use chrono::{Local, Timelike}; + +const CLOCK_FONT: &str = "sans"; +const CLOCK_SIZE: f32 = 96.; + +pub struct Clock { + dirty: bool, + geometry: Geometry, + digit_width: u32, + colon_width: u32, +} + +impl<'a> Clock { + pub fn new() -> Clock { + Clock { + dirty: true, + geometry: Default::default(), + digit_width: 0, + colon_width: 0, + } + } +} + +impl<'a> Widget for Clock { + fn set_dirty(&mut self, dirty: bool) { + self.dirty = dirty; + } + fn get_dirty(&self) -> bool { + self.dirty + } + + fn geometry(&self) -> Geometry { + self.geometry + } + + fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry { + let time = Local::now().naive_local(); + let fg = Color::new(1., 1., 1., 1.); + let bg = Color::new(0., 0., 0., 1.); + + let digit = self.digit_width; + let colon = self.colon_width; + let font = fonts.get_font(CLOCK_FONT, CLOCK_SIZE); + font.draw_text_fixed_width( + view, + &bg, + &fg, + &[digit, digit, colon, digit, digit], + &format!("{:02}:{:02}", time.hour(), time.minute()), + ) + .unwrap(); + self.geometry + } + + fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry { + let font = fonts.get_font(CLOCK_FONT, CLOCK_SIZE); + self.digit_width = font.auto_widest("0123456789").unwrap(); + self.colon_width = font.auto_widest(":").unwrap(); + self.geometry = Geometry { + x: geometry.x, + y: geometry.y, + width: self.digit_width * 4 + self.colon_width, + height: CLOCK_SIZE.ceil() as u32, + }; + self.geometry + } +} diff --git a/src/widgets/date.rs b/src/widgets/date.rs new file mode 100644 index 0000000..7544308 --- /dev/null +++ b/src/widgets/date.rs @@ -0,0 +1,72 @@ +use std::default::Default; + +use crate::{ + buffer::BufferView, + color::Color, + fonts::FontMap, + widgets::{Geometry, Widget}, +}; + +use chrono::{Datelike, Local}; + +const DATE_FONT: &str = "sans"; +const DATE_SIZE: f32 = 40.; + +pub struct Date { + dirty: bool, + geometry: Geometry, +} + +impl Date { + pub fn new() -> Date { + Date { + dirty: true, + geometry: Default::default(), + } + } +} + +impl<'a> Widget for Date { + fn set_dirty(&mut self, dirty: bool) { + self.dirty = dirty; + } + fn get_dirty(&self) -> bool { + self.dirty + } + + fn geometry(&self) -> Geometry { + self.geometry + } + + fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry { + let time = Local::now().naive_local(); + let fg = Color::new(1., 1., 1., 1.); + let bg = Color::new(0., 0., 0., 1.); + let mut date_line = view.offset((0, 8)).unwrap(); + let font = fonts.get_font(DATE_FONT, DATE_SIZE); + font.auto_draw_text( + &mut date_line, + &bg, + &fg, + &format!( + "{}, {:02}/{:02}/{:4}", + time.weekday(), + time.day(), + time.month(), + time.year() + ), + ) + .unwrap(); + self.geometry + } + + fn geometry_update(&mut self, _fonts: &mut FontMap, geometry: &Geometry) -> Geometry { + self.geometry = Geometry { + x: geometry.x, + y: geometry.y, + width: 256, + height: DATE_SIZE.ceil() as u32, + }; + self.geometry + } +} diff --git a/src/widgets/launcher.rs b/src/widgets/launcher.rs new file mode 100644 index 0000000..b4c0a8f --- /dev/null +++ b/src/widgets/launcher.rs @@ -0,0 +1,566 @@ +use std::cmp::{min, Ordering}; +use std::collections::HashMap; +use std::default::Default; +use std::process::{exit, Command}; + +use crate::{ + buffer::BufferView, + color::Color, + fonts::FontMap, + keyboard::{keysyms, KeyEvent}, + utils::desktop::{load_desktop_files, Desktop}, + widgets::{Geometry, Widget}, +}; + +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; +use unicode_segmentation::UnicodeSegmentation; +use wayland_client::{protocol::wl_keyboard, WEnum}; + +use std::sync::{Arc, Mutex}; +use std::thread; + +const LAUNCHER_FONT: &str = "monospace"; +const LAUNCHER_SIZE: f32 = 32.; + +struct Matcher { + matches: HashMap, +} + +impl Matcher { + fn new() -> Self { + Self { + matches: HashMap::new(), + } + } + + fn try_match(&mut self, dtop: Desktop, val: &str, input: &str, prio: f32) { + let fuzzy_matcher = SkimMatcherV2::default(); + if let Some(ma) = fuzzy_matcher.fuzzy_match(val, input) { + let ma = ((ma as f32) * prio) as i64; + if let Some(ma_old) = self.matches.get(&dtop) { + // Skip over new matches for the same program that are worse + // than the one we already have. + if ma_old > &ma { + return; + } + } + + self.matches.insert(dtop, ma); + } + } + + fn matches(&self) -> Vec { + let mut m = self + .matches + .iter() + .map(|(key, ma)| (*ma, key.clone())) + .collect::>(); + + m.sort_by(|(ma1, d1), (ma2, d2)| { + if ma1 > ma2 { + Ordering::Less + } else if ma1 < ma2 { + Ordering::Greater + } else { + d1.cmp(d2) + } + }); + + m.into_iter().map(|(_, x)| x).collect() + } +} + +enum PromptMode { + Normal, + Shell, + Calc, +} + +struct Prompt { + input: String, + mode: PromptMode, + cursor: usize, +} + +impl Prompt { + fn new() -> Prompt { + Prompt { + input: String::new(), + mode: PromptMode::Normal, + cursor: 0, + } + } + + fn move_cursor(&mut self, distance: isize) { + let new_cursor = self.cursor as isize + distance; + if new_cursor < 0 { + self.cursor = 0; + return; + } else if new_cursor > self.input.len() as isize { + self.cursor = self.input.len() + } else { + self.cursor = new_cursor as usize; + } + } + + fn append(&mut self, v: &str) { + if self.input.len() == 0 { + match v { + "!" => { + self.mode = PromptMode::Shell; + return; + }, + "=" => { + self.mode = PromptMode::Calc; + return; + }, + _ => (), + } + } + let indices: Vec<(usize, &str)> = self.input.grapheme_indices(true).collect(); + if self.cursor == indices.len() { + self.input += &v; + } else { + let index_at = indices[self.cursor].0; + self.input.insert_str(index_at, &v); + } + self.cursor += 1; + } + + fn backspace(&mut self) { + let mut indices: Vec<(usize, &str)> = self.input.grapheme_indices(true).collect(); + if !indices.is_empty() && self.cursor > 0 { + self.cursor -= 1; + indices.remove(self.cursor); + self.input = indices.iter().fold("".into(), |acc, el| acc + el.1); + } else { + self.mode = PromptMode::Normal; + } + } + + fn delete(&mut self) { + let mut indices: Vec<(usize, &str)> = self.input.grapheme_indices(true).collect(); + if !indices.is_empty() && self.cursor < indices.len() { + indices.remove(self.cursor); + self.input = indices.iter().fold("".into(), |acc, el| acc + el.1); + } else { + self.mode = PromptMode::Normal; + } + } + + fn home(&mut self) { + self.cursor = 0; + } + + fn end(&mut self) { + self.cursor = self.input.len(); + } +} + +trait InterfaceWidget { + fn trigger(&mut self, intf: &InnerInterface); + fn draw(&mut self, intf: &InnerInterface, fonts: &mut FontMap, view: &mut BufferView) -> Geometry; + fn update(&mut self, intf: &InnerInterface); +} + +struct Launcher { + next_token: Option, + options: Arc>>, + matches: Vec, +} + +impl Launcher { + fn new() -> Launcher { + let options = Arc::new(Mutex::new(Vec::new())); + { + let options = Arc::clone(&options); + thread::spawn(move || { + let mut options = options.lock().unwrap(); + *options = load_desktop_files(); + }); + } + + Launcher { + next_token: None, + options: options, + matches: Vec::new(), + } + } + + fn exec(&self, args: Vec) { + let mut cmd = Command::new(args[0].clone()); + if let Some(token) = &self.next_token { + cmd.env("XDG_ACTIVATION_TOKEN", token); + } + cmd.args(&args[1..]).spawn().unwrap(); + exit(0); + } +} + +impl InterfaceWidget for Launcher { + fn trigger(&mut self, intf: &InnerInterface) { + if self.matches.len() > intf.selection { + let d = &self.matches[intf.selection]; + if let Some(exec) = &d.exec { + let exec = exec + .replace("%f", "") + .replace("%F", "") + .replace("%u", "") + .replace("%U", ""); + + let lexed = shlex::split(&exec).unwrap(); + if !lexed.is_empty() { + self.exec(lexed); + } + } + } + } + + fn update(&mut self, intf: &InnerInterface) { + let mut matcher = Matcher::new(); + let options = self.options.lock().unwrap(); + for desktop in options.iter() { + matcher.try_match( + desktop.clone(), + &desktop.name.to_lowercase(), + &intf.prompt.input.to_lowercase(), + 1.0, + ); + for keyword in desktop.keywords.iter() { + matcher.try_match( + desktop.clone(), + &keyword.to_lowercase(), + &intf.prompt.input.to_lowercase(), + 0.5, + ); + } + } + + self.matches = matcher.matches(); + } + + fn draw(&mut self, intf: &InnerInterface, fonts: &mut FontMap, view: &mut BufferView) -> Geometry { + let fg = Color::new(1., 1., 1., 1.); + let bg = Color::new(0., 0., 0., 1.); + + let line_height = LAUNCHER_SIZE.ceil() as u32; + + // Draw line + let mut prompt_offset = intf.geometry.height - line_height; + let mut prompt_line = view.offset((0, prompt_offset)).unwrap(); + let mut prompt_border = prompt_line.limit((intf.geometry.width, 1)).unwrap(); + prompt_border.memset(&fg); + + // Draw prompt + let font = fonts.get_font(LAUNCHER_FONT, LAUNCHER_SIZE); + font.auto_draw_text_with_cursor( + &mut prompt_line, + &bg, + &fg, + &format!(" {}", &intf.prompt.input), + intf.prompt.cursor + 3, + ) + .unwrap(); + + let font = fonts.get_font(LAUNCHER_FONT, LAUNCHER_SIZE); + font.auto_draw_text( + &mut prompt_line, + &bg, + &fg, + ">", + ) + .unwrap(); + + // Draw entries + let dimfg = Color::new(0.5, 0.5, 0.5, 1.0); + prompt_offset -= 16; + + for (idx, m) in self.matches.iter().enumerate() { + if prompt_offset < line_height { + break; + } + prompt_offset -= line_height; + let mut line = view.offset((0, prompt_offset)).unwrap(); + + if idx == intf.selection { + let fuzzy_matcher = SkimMatcherV2::default(); + let (_, indices) = fuzzy_matcher + .fuzzy_indices(&m.name.to_lowercase(), &intf.prompt.input.to_lowercase()) + .unwrap_or((0, vec![])); + + let mut colors = Vec::with_capacity(m.name.len()); + for pos in 0..m.name.len() { + if indices.contains(&pos) { + colors.push(Color::new(1.0, 0.65, 0., 1.0)); + } else { + colors.push(Color::new(0.75, 0.75, 0.75, 1.0)); + } + } + font.auto_draw_text_individual_colors(&mut line, &bg, &colors, &m.name) + .unwrap(); + } else { + font.auto_draw_text(&mut line, &bg, &dimfg, &m.name) + .unwrap(); + } + } + + let content_height = min(intf.geometry.height, (self.matches.len() + 1) as u32 * line_height + 16); + Geometry{ + x: intf.geometry.x, + y: intf.geometry.y + intf.geometry.height - content_height, + width: intf.geometry.width, + height: content_height, + } + } +} + +struct Shell (); + +impl InterfaceWidget for Shell { + fn trigger(&mut self, intf: &InnerInterface) { + let args = vec![ + "/bin/sh".to_string(), + "-c".to_string(), + intf.prompt.input.clone(), + ]; + Command::new(args[0].clone()) + .args(&args[1..]) + .spawn() + .unwrap(); + exit(0); + } + + fn update(&mut self, _intf: &InnerInterface) {} + + fn draw(&mut self, intf: &InnerInterface, fonts: &mut FontMap, view: &mut BufferView) -> Geometry { + let fg = Color::new(1., 1., 1., 1.); + let bg = Color::new(0., 0., 0., 1.); + + let line_height = LAUNCHER_SIZE.ceil() as u32; + + // Draw line + let prompt_offset = intf.geometry.height - line_height; + let mut prompt_line = view.offset((0, prompt_offset)).unwrap(); + let mut prompt_border = prompt_line.limit((intf.geometry.width, 1)).unwrap(); + prompt_border.memset(&fg); + + // Draw prompt + let font = fonts.get_font(LAUNCHER_FONT, LAUNCHER_SIZE); + font.auto_draw_text_with_cursor( + &mut prompt_line, + &bg, + &fg, + &format!(" {}", &intf.prompt.input), + intf.prompt.cursor + 3, + ) + .unwrap(); + let font = fonts.get_font(LAUNCHER_FONT, LAUNCHER_SIZE); + font.auto_draw_text( + &mut prompt_line, + &bg, + &Color::new(1., 0.75, 0.5, 1.), + "!", + ) + .unwrap(); + + Geometry{ + x: intf.geometry.x, + y: intf.geometry.y + intf.geometry.height - line_height, + width: intf.geometry.width, + height: line_height, + } + } +} + +struct Calc { + old: Vec, + result: Option, +} + +impl InterfaceWidget for Calc { + fn trigger(&mut self, intf: &InnerInterface) { + if let Some(res) = &self.result { + self.old.push(format!("{} = {}", &intf.prompt.input, res)); + } + } + + fn update(&mut self, intf: &InnerInterface) { + let res = rcalc_lib::parse::eval(&intf.prompt.input, &mut rcalc_lib::parse::CalcState::new()) + .map(|x| format!("{}", x)) + .map_err(|x| format!("{}", x)); + self.result = match res { + Ok(v) => Some(v), + Err(_) => None, + }; + } + + fn draw(&mut self, intf: &InnerInterface, fonts: &mut FontMap, view: &mut BufferView) -> Geometry { + let fg = Color::new(1., 1., 1., 1.); + let bg = Color::new(0., 0., 0., 1.); + + let line_height = LAUNCHER_SIZE.ceil() as u32; + + // Draw line + let mut prompt_offset = intf.geometry.height - (LAUNCHER_SIZE.ceil() as u32); + let mut prompt_line = view.offset((0, prompt_offset)).unwrap(); + let mut prompt_border = prompt_line.limit((intf.geometry.width, 1)).unwrap(); + prompt_border.memset(&fg); + + // Draw prompt + let font = fonts.get_font(LAUNCHER_FONT, LAUNCHER_SIZE); + let res_off = font.auto_draw_text_with_cursor( + &mut prompt_line, + &bg, + &fg, + &format!(" {} ", &intf.prompt.input), + intf.prompt.cursor + 3, + ) + .unwrap(); + + let font = fonts.get_font(LAUNCHER_FONT, LAUNCHER_SIZE); + font.auto_draw_text( + &mut prompt_line, + &bg, + &Color::new(1., 0.75, 0.5, 1.), + "=", + ) + .unwrap(); + + if let Some(res) = &self.result { + let resfg = Color::new(1.0, 0.65, 0., 1.0); + let mut result_line = view.offset((res_off.0, prompt_offset)).unwrap(); + font.auto_draw_text(&mut result_line, &bg, &resfg, &format!(" = {}", &res)).unwrap(); + } + + for m in self.old.iter().rev() { + let dimfg = Color::new(0.5, 0.5, 0.5, 1.0); + if prompt_offset < LAUNCHER_SIZE.ceil() as u32 { + break; + } + prompt_offset -= LAUNCHER_SIZE.ceil() as u32; + let mut result_line = view.offset((0, prompt_offset)).unwrap(); + font.auto_draw_text(&mut result_line, &bg, &dimfg, &m).unwrap(); + } + + let content_height = min(intf.geometry.height, (self.old.len() + 1) as u32 * line_height + 16); + Geometry{ + x: intf.geometry.x, + y: intf.geometry.y + intf.geometry.height - content_height, + width: intf.geometry.width, + height: content_height, + } + } +} + +struct InnerInterface { + dirty: bool, + geometry: Geometry, + selection: usize, + prompt: Prompt, +} + +pub struct Interface { + launcher: Launcher, + shell: Shell, + calc: Calc, + inner: InnerInterface, +} + +impl Interface { + pub fn new() -> Interface { + Interface{ + launcher: Launcher::new(), + shell: Shell{}, + calc: Calc{ + old: Vec::new(), + result: None, + }, + inner: InnerInterface{ + dirty: false, + geometry: Default::default(), + selection: 0, + prompt: Prompt::new(), + }, + } + } +} + +impl Widget for Interface { + fn set_dirty(&mut self, dirty: bool) { + self.inner.dirty = dirty; + } + fn get_dirty(&self) -> bool { + self.inner.dirty + } + + fn geometry(&self) -> Geometry { + self.inner.geometry + } + + fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry { + match self.inner.prompt.mode { + PromptMode::Shell => self.shell.draw(&self.inner, fonts, view), + PromptMode::Normal => self.launcher.draw(&self.inner, fonts, view), + PromptMode::Calc => self.calc.draw(&self.inner, fonts, view), + } + } + + fn geometry_update(&mut self, _fonts: &mut FontMap, geometry: &Geometry) -> Geometry { + self.inner.geometry = geometry.clone(); + self.inner.geometry + } + + fn keyboard_input(&mut self, event: &KeyEvent) { + if event.state != WEnum::Value(wl_keyboard::KeyState::Pressed) { + return; + } + self.inner.dirty = true; + let widget = match self.inner.prompt.mode { + PromptMode::Shell => &mut self.shell as &mut dyn InterfaceWidget, + PromptMode::Normal => &mut self.launcher as &mut dyn InterfaceWidget, + PromptMode::Calc => &mut self.calc as &mut dyn InterfaceWidget, + }; + match event.keysym { + keysyms::XKB_KEY_a if event.modifiers.ctrl => self.inner.prompt.home(), + keysyms::XKB_KEY_e if event.modifiers.ctrl => self.inner.prompt.end(), + keysyms::XKB_KEY_Home => self.inner.prompt.home(), + keysyms::XKB_KEY_End => self.inner.prompt.end(), + keysyms::XKB_KEY_BackSpace => { + self.inner.prompt.backspace(); + self.inner.selection = 0; + widget.update(&self.inner); + } + keysyms::XKB_KEY_Delete => { + self.inner.prompt.delete(); + self.inner.selection = 0; + widget.update(&self.inner); + } + keysyms::XKB_KEY_Return => widget.trigger(&self.inner), + keysyms::XKB_KEY_Left => self.inner.prompt.move_cursor(-1), + keysyms::XKB_KEY_Right => self.inner.prompt.move_cursor(1), + keysyms::XKB_KEY_Up => { + self.inner.selection += 1; + } + keysyms::XKB_KEY_Down => { + if self.inner.selection > 0 { + self.inner.selection -= 1; + } + } + _ => { + if let Some(utf8) = &event.utf8 { + self.inner.prompt.append(&utf8); + self.inner.selection = 0; + widget.update(&self.inner); + } else { + self.inner.dirty = false; + } + } + } + } + + fn token_update(&mut self, token: &str) { + self.launcher.next_token = Some(token.to_string()); + } +} + diff --git a/src/widgets/line.rs b/src/widgets/line.rs new file mode 100644 index 0000000..705097b --- /dev/null +++ b/src/widgets/line.rs @@ -0,0 +1,39 @@ +use std::default::Default; + +use crate::{ + buffer::BufferView, + color::Color, + fonts::FontMap, + widgets::{Geometry, Widget}, +}; + +#[derive(Default)] +pub struct Line { + geometry: Geometry, +} + +impl Line { + pub fn new() -> Line { + Default::default() + } +} + +impl Widget for Line { + fn geometry(&self) -> Geometry { + self.geometry + } + fn draw(&mut self, _fonts: &mut FontMap, view: &mut BufferView) -> Geometry { + view.memset(&Color::new(1., 1., 1., 1.)); + self.geometry + } + + fn geometry_update(&mut self, _fonts: &mut FontMap, geometry: &Geometry) -> Geometry { + self.geometry = Geometry { + x: geometry.x, + y: geometry.y, + width: geometry.width, + height: 1, + }; + self.geometry + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 0000000..a95977a --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1,17 @@ +mod bar_widget; +mod battery; +mod clock; +mod date; +mod launcher; +mod line; +mod widget; + +pub use battery::Battery; +pub use clock::Clock; +pub use date::Date; +pub use launcher::Interface; +pub use line::Line; +pub use widget::{ + Geometry, HorizontalLayout, IndexedLayout, Layout, Margin, VerticalLayout, Widget, + WidgetUpdater, +}; diff --git a/src/widgets/widget.rs b/src/widgets/widget.rs new file mode 100644 index 0000000..d1c40b3 --- /dev/null +++ b/src/widgets/widget.rs @@ -0,0 +1,166 @@ +use std::cmp::{min, max}; +use std::fmt; + +use crate::{buffer::BufferView, fonts::FontMap, keyboard::KeyEvent}; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Geometry { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +impl fmt::Display for Geometry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "(x: {}, y: {}, width: {}, height: {})", + self.x, self.y, self.width, self.height + ) + } +} + +impl Geometry { + pub fn new() -> Geometry { + Geometry{ + x: 0, + y: 0, + width: 0, + height: 0, + } + } + + pub fn expand(&self, other: Geometry) -> Geometry { + let x = min(self.x, other.x); + let y = min(self.y, other.y); + Geometry { + x, y, + width: max(self.width + self.x, other.width + other.x) - x, + height: max(self.height + self.y, other.height + other.height) - y, + } + } +} + +pub trait Widget { + fn get_dirty(&self) -> bool { + false + } + fn set_dirty(&mut self, _dirty: bool) {} + fn geometry(&self) -> Geometry; + fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry; + fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry; + fn keyboard_input(&mut self, _event: &KeyEvent) {} + fn token_update(&mut self, _token: &str) {} +} + +pub trait Layout { + fn geometry_update( + &mut self, + fonts: &mut FontMap, + geometry: &Geometry, + user_data: &mut U, + ) -> Geometry; +} +pub struct HorizontalLayout { + pub widgets: Vec>>, +} + +impl Layout for HorizontalLayout { + fn geometry_update( + &mut self, + fonts: &mut FontMap, + geometry: &Geometry, + user_data: &mut U, + ) -> Geometry { + let mut geo = geometry.clone(); + let mut max_width = 0; + let mut max_height = 0; + for w in self.widgets.iter_mut() { + let result = w.geometry_update(fonts, &geo, user_data); + geo.x = result.x + result.width; + geo.width -= result.width; + max_width += result.width; + max_height = max(result.height, max_height); + } + geo.width = max_width; + geo.height = max_height; + geo + } +} + +pub struct VerticalLayout { + pub widgets: Vec>>, +} + +impl Layout for VerticalLayout { + fn geometry_update( + &mut self, + fonts: &mut FontMap, + geometry: &Geometry, + user_data: &mut U, + ) -> Geometry { + let mut geo = geometry.clone(); + let mut max_width = 0; + let mut max_height = 0; + for w in self.widgets.iter_mut() { + let result = w.geometry_update(fonts, &geo, user_data); + geo.y = result.y + result.height; + geo.height -= result.height; + max_width = max(result.width, max_width); + max_height += result.height; + } + geo.width = max_width; + geo.height = max_height; + geo + } +} + +pub trait WidgetUpdater { + fn geometry_update(&mut self, idx: usize, fonts: &mut FontMap, geometry: &Geometry) + -> Geometry; +} + +pub struct IndexedLayout { + pub widget_idx: usize, +} + +impl Layout for IndexedLayout { + fn geometry_update( + &mut self, + fonts: &mut FontMap, + geometry: &Geometry, + user_data: &mut U, + ) -> Geometry { + user_data.geometry_update(self.widget_idx, fonts, geometry) + } +} + +pub struct Margin { + pub widget: Box>, + pub margin: (u32, u32, u32, u32), +} + +impl Layout for Margin { + fn geometry_update( + &mut self, + fonts: &mut FontMap, + geometry: &Geometry, + user_data: &mut U, + ) -> Geometry { + let geo = Geometry{ + x: geometry.x + self.margin.0, + y: geometry.y + self.margin.1, + width: geometry.width - self.margin.0 - self.margin.2, + height: geometry.height - self.margin.1 - self.margin.3, + }; + + let out = self.widget.geometry_update(fonts, &geo, user_data); + Geometry { + x: out.x - self.margin.0, + y: out.y - self.margin.1, + width: out.width + self.margin.0 + self.margin.2, + height: out.height + self.margin.1 + self.margin.3, + } + } +}