├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src ├── lib.rs ├── playhead.rs └── ruler.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: egui_timeline 2 | on: [push, pull_request] 3 | jobs: 4 | cargo-fmt-check: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install stable 9 | uses: actions-rs/toolchain@v1 10 | with: 11 | profile: minimal 12 | toolchain: stable 13 | override: true 14 | components: rustfmt 15 | - name: Run rustfmt 16 | uses: actions-rs/cargo@v1 17 | with: 18 | command: fmt 19 | args: -- --check 20 | 21 | cargo-test: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Install stable 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | profile: minimal 29 | toolchain: stable 30 | override: true 31 | - name: cargo test 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: test 35 | args: --verbose 36 | 37 | cargo-doc: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | - name: Install stable 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | profile: minimal 45 | toolchain: stable 46 | override: true 47 | - name: cargo doc 48 | uses: actions-rs/cargo@v1 49 | with: 50 | command: doc 51 | args: --verbose 52 | 53 | cargo-publish: 54 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 55 | env: 56 | CRATESIO_TOKEN: ${{ secrets.CRATESIO_TOKEN }} 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v2 60 | - name: cargo publish 61 | continue-on-error: true 62 | run: cargo publish --token $CRATESIO_TOKEN 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ab_glyph" 7 | version = "0.2.29" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" 10 | dependencies = [ 11 | "ab_glyph_rasterizer", 12 | "owned_ttf_parser", 13 | ] 14 | 15 | [[package]] 16 | name = "ab_glyph_rasterizer" 17 | version = "0.1.8" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" 20 | 21 | [[package]] 22 | name = "ahash" 23 | version = "0.8.11" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 26 | dependencies = [ 27 | "cfg-if", 28 | "once_cell", 29 | "version_check", 30 | "zerocopy", 31 | ] 32 | 33 | [[package]] 34 | name = "autocfg" 35 | version = "1.4.0" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 38 | 39 | [[package]] 40 | name = "bitflags" 41 | version = "2.6.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 44 | 45 | [[package]] 46 | name = "cfg-if" 47 | version = "1.0.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 50 | 51 | [[package]] 52 | name = "ecolor" 53 | version = "0.29.1" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" 56 | dependencies = [ 57 | "emath", 58 | ] 59 | 60 | [[package]] 61 | name = "egui" 62 | version = "0.29.1" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" 65 | dependencies = [ 66 | "ahash", 67 | "emath", 68 | "epaint", 69 | "nohash-hasher", 70 | ] 71 | 72 | [[package]] 73 | name = "egui_plot" 74 | version = "0.29.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "d8dca4871c15d51aadb79534dcf51a8189e5de3426ee7b465eb7db9a0a81ea67" 77 | dependencies = [ 78 | "ahash", 79 | "egui", 80 | "emath", 81 | ] 82 | 83 | [[package]] 84 | name = "egui_timeline" 85 | version = "0.3.0" 86 | dependencies = [ 87 | "egui", 88 | "egui_plot", 89 | ] 90 | 91 | [[package]] 92 | name = "emath" 93 | version = "0.29.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" 96 | 97 | [[package]] 98 | name = "epaint" 99 | version = "0.29.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" 102 | dependencies = [ 103 | "ab_glyph", 104 | "ahash", 105 | "ecolor", 106 | "emath", 107 | "epaint_default_fonts", 108 | "nohash-hasher", 109 | "parking_lot", 110 | ] 111 | 112 | [[package]] 113 | name = "epaint_default_fonts" 114 | version = "0.29.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" 117 | 118 | [[package]] 119 | name = "libc" 120 | version = "0.2.167" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" 123 | 124 | [[package]] 125 | name = "lock_api" 126 | version = "0.4.12" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 129 | dependencies = [ 130 | "autocfg", 131 | "scopeguard", 132 | ] 133 | 134 | [[package]] 135 | name = "nohash-hasher" 136 | version = "0.2.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" 139 | 140 | [[package]] 141 | name = "once_cell" 142 | version = "1.20.2" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 145 | 146 | [[package]] 147 | name = "owned_ttf_parser" 148 | version = "0.25.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" 151 | dependencies = [ 152 | "ttf-parser", 153 | ] 154 | 155 | [[package]] 156 | name = "parking_lot" 157 | version = "0.12.3" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 160 | dependencies = [ 161 | "lock_api", 162 | "parking_lot_core", 163 | ] 164 | 165 | [[package]] 166 | name = "parking_lot_core" 167 | version = "0.9.10" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 170 | dependencies = [ 171 | "cfg-if", 172 | "libc", 173 | "redox_syscall", 174 | "smallvec", 175 | "windows-targets", 176 | ] 177 | 178 | [[package]] 179 | name = "proc-macro2" 180 | version = "1.0.92" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 183 | dependencies = [ 184 | "unicode-ident", 185 | ] 186 | 187 | [[package]] 188 | name = "quote" 189 | version = "1.0.37" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 192 | dependencies = [ 193 | "proc-macro2", 194 | ] 195 | 196 | [[package]] 197 | name = "redox_syscall" 198 | version = "0.5.7" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 201 | dependencies = [ 202 | "bitflags", 203 | ] 204 | 205 | [[package]] 206 | name = "scopeguard" 207 | version = "1.2.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 210 | 211 | [[package]] 212 | name = "smallvec" 213 | version = "1.13.2" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 216 | 217 | [[package]] 218 | name = "syn" 219 | version = "2.0.90" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 222 | dependencies = [ 223 | "proc-macro2", 224 | "quote", 225 | "unicode-ident", 226 | ] 227 | 228 | [[package]] 229 | name = "ttf-parser" 230 | version = "0.25.1" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" 233 | 234 | [[package]] 235 | name = "unicode-ident" 236 | version = "1.0.14" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 239 | 240 | [[package]] 241 | name = "version_check" 242 | version = "0.9.5" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 245 | 246 | [[package]] 247 | name = "windows-targets" 248 | version = "0.52.6" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 251 | dependencies = [ 252 | "windows_aarch64_gnullvm", 253 | "windows_aarch64_msvc", 254 | "windows_i686_gnu", 255 | "windows_i686_gnullvm", 256 | "windows_i686_msvc", 257 | "windows_x86_64_gnu", 258 | "windows_x86_64_gnullvm", 259 | "windows_x86_64_msvc", 260 | ] 261 | 262 | [[package]] 263 | name = "windows_aarch64_gnullvm" 264 | version = "0.52.6" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 267 | 268 | [[package]] 269 | name = "windows_aarch64_msvc" 270 | version = "0.52.6" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 273 | 274 | [[package]] 275 | name = "windows_i686_gnu" 276 | version = "0.52.6" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 279 | 280 | [[package]] 281 | name = "windows_i686_gnullvm" 282 | version = "0.52.6" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 285 | 286 | [[package]] 287 | name = "windows_i686_msvc" 288 | version = "0.52.6" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 291 | 292 | [[package]] 293 | name = "windows_x86_64_gnu" 294 | version = "0.52.6" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 297 | 298 | [[package]] 299 | name = "windows_x86_64_gnullvm" 300 | version = "0.52.6" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 303 | 304 | [[package]] 305 | name = "windows_x86_64_msvc" 306 | version = "0.52.6" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 309 | 310 | [[package]] 311 | name = "zerocopy" 312 | version = "0.7.35" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 315 | dependencies = [ 316 | "zerocopy-derive", 317 | ] 318 | 319 | [[package]] 320 | name = "zerocopy-derive" 321 | version = "0.7.35" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 324 | dependencies = [ 325 | "proc-macro2", 326 | "quote", 327 | "syn", 328 | ] 329 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_timeline" 3 | description = "A general timeline widget for egui, for working with time-based media and control." 4 | version = "0.3.0" 5 | authors = ["mitchmindtree "] 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | keywords = ["egui", "timeline", "sequencer", "automation", "daw"] 9 | repository = "https://github.com/mitchmindtree/egui_timeline.git" 10 | homepage = "https://github.com/mitchmindtree/egui_timeline" 11 | edition = "2018" 12 | 13 | [dependencies] 14 | egui = "0.29.1" 15 | egui_plot = "0.29" 16 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2021 mitchmindtree 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egui_timeline [![Actions Status](https://github.com/mitchmindtree/egui_timeline/workflows/egui_timeline/badge.svg)](https://github.com/mitchmindtree/egui_timeline/actions) [![Crates.io](https://img.shields.io/crates/v/egui_timeline.svg)](https://crates.io/crates/egui_timeline) [![Crates.io](https://img.shields.io/crates/l/egui_timeline.svg)](https://github.com/mitchmindtree/egui_timeline/blob/master/LICENSE-MIT) [![docs.rs](https://docs.rs/egui_timeline/badge.svg)](https://docs.rs/egui_timeline/) 2 | 3 | A general timeline widget for egui, intended for working with time-based media and control. 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use egui_plot as plot; 2 | use std::{ 3 | hash::Hash, 4 | ops::{Range, RangeInclusive}, 5 | }; 6 | 7 | pub use playhead::{Playhead, PlayheadApi}; 8 | pub use ruler::MusicalRuler; 9 | 10 | pub mod playhead; 11 | pub mod ruler; 12 | 13 | pub const MIN_STEP_GAP: f32 = 4.0; 14 | 15 | /// The implementation required to instantiate a timeline widget. 16 | pub trait TimelineApi { 17 | /// Access to the ruler info. 18 | fn musical_ruler_info(&self) -> &dyn ruler::MusicalInfo; 19 | /// Shift the timeline start by the given number of ticks due to a scroll event. 20 | fn shift_timeline_start(&mut self, ticks: f32); 21 | /// The timeline was scrolled with with `Ctrl` held down to zoom in/out. 22 | fn zoom(&mut self, y_delta: f32); 23 | } 24 | 25 | #[derive(Clone, Debug)] 26 | pub struct Bar { 27 | /// The start and end offsets of the bar. 28 | pub tick_range: Range, 29 | /// The time signature of this bar. 30 | pub time_sig: TimeSig, 31 | } 32 | 33 | #[derive(Clone, Debug)] 34 | pub struct TimeSig { 35 | pub top: u16, 36 | pub bottom: u16, 37 | } 38 | 39 | impl TimeSig { 40 | /// The number of beats per bar of this time signature. 41 | pub fn beats_per_bar(&self) -> f32 { 42 | 4.0 * self.top as f32 / self.bottom as f32 43 | } 44 | } 45 | 46 | /// The top-level timeline widget. 47 | pub struct Timeline { 48 | /// A optional side panel with track headers. 49 | /// 50 | /// Can be useful for labelling tracks or providing convenient volume, mute, solo, etc style 51 | /// widgets. 52 | header: Option, 53 | } 54 | 55 | /// The result of setting the timeline, ready to start laying out tracks. 56 | pub struct Show { 57 | tracks: TracksCtx, 58 | ui: egui::Ui, 59 | } 60 | 61 | /// A context for instantiating tracks, either pinned or unpinned. 62 | pub struct TracksCtx { 63 | /// The rectangle encompassing the entire widget area including both header and timeline and 64 | /// both pinned and unpinned track areas. 65 | pub full_rect: egui::Rect, 66 | /// The rect encompassing the left-hand-side track headers including pinned and unpinned. 67 | pub header_full_rect: Option, 68 | /// Context specific to the timeline (non-header) area. 69 | pub timeline: TimelineCtx, 70 | } 71 | 72 | /// Some context for the timeline, providing short-hand for setting some useful widgets. 73 | pub struct TimelineCtx { 74 | /// The total visible rect of the timeline area including pinned and unpinned tracks. 75 | pub full_rect: egui::Rect, 76 | /// The total number of ticks visible on the timeline area. 77 | pub visible_ticks: f32, 78 | } 79 | 80 | /// Context for instantiating the playhead after all tracks have been set. 81 | pub struct SetPlayhead { 82 | timeline_rect: egui::Rect, 83 | /// The y position at the bottom of the last track, or the bottom of the 84 | /// tracks' scrollable area in the case that the size of the tracks 85 | /// exceed the visible height. 86 | tracks_bottom: f32, 87 | } 88 | 89 | impl Timeline { 90 | /// Begin building the timeline widget. 91 | pub fn new() -> Self { 92 | Self { header: None } 93 | } 94 | 95 | /// A optional track header side panel. 96 | /// 97 | /// Can be useful for labelling tracks or providing convenient volume, mute, solo, etc style 98 | /// widgets. 99 | pub fn header(mut self, width: f32) -> Self { 100 | self.header = Some(width); 101 | self 102 | } 103 | 104 | /// Set the timeline within the currently available rect. 105 | pub fn show(self, ui: &mut egui::Ui, timeline: &mut dyn TimelineApi) -> Show { 106 | // The full area including both headers and timeline. 107 | let full_rect = ui.available_rect_before_wrap(); 108 | // The area occupied by the timeline. 109 | let mut timeline_rect = full_rect; 110 | // The area occupied by track headers. 111 | let header_rect = self.header.map(|header_w| { 112 | let mut r = full_rect; 113 | r.set_width(header_w); 114 | timeline_rect.min.x = r.right(); 115 | r 116 | }); 117 | 118 | // Check whether or not we should scroll the timeline or zoom. 119 | if ui.rect_contains_pointer(timeline_rect) { 120 | let delta = ui.input(|i| i.smooth_scroll_delta); 121 | if ui.input(|i| i.raw.modifiers.ctrl) { 122 | if delta.x != 0.0 || delta.y != 0.0 { 123 | timeline.zoom(delta.y - delta.x); 124 | } 125 | } else { 126 | if delta.x != 0.0 { 127 | let ticks_per_point = timeline.musical_ruler_info().ticks_per_point(); 128 | timeline.shift_timeline_start(delta.x * ticks_per_point); 129 | } 130 | } 131 | } 132 | 133 | // Draw the background. 134 | let vis = ui.style().noninteractive(); 135 | let bg_stroke = egui::Stroke { 136 | width: 0.0, 137 | ..vis.bg_stroke 138 | }; 139 | ui.painter().rect(full_rect, 0.0, vis.bg_fill, bg_stroke); 140 | 141 | // The child widgets. 142 | let layout = egui::Layout::top_down(egui::Align::Min); 143 | let info = timeline.musical_ruler_info(); 144 | let visible_ticks = info.ticks_per_point() * timeline_rect.width(); 145 | let timeline = TimelineCtx { 146 | full_rect: timeline_rect, 147 | visible_ticks, 148 | }; 149 | let tracks = TracksCtx { 150 | full_rect, 151 | header_full_rect: header_rect, 152 | timeline, 153 | }; 154 | let ui = ui.new_child(egui::UiBuilder::new().max_rect(full_rect).layout(layout)); 155 | Show { tracks, ui } 156 | } 157 | } 158 | 159 | /// Relevant information for displaying a background for the timeline. 160 | pub struct BackgroundCtx<'a> { 161 | pub header_full_rect: Option, 162 | pub timeline: &'a TimelineCtx, 163 | } 164 | 165 | impl Show { 166 | /// Allows for drawing some widgets in the background before showing the grid. 167 | /// 168 | /// Can be useful for subtly colouring different ranges, etc. 169 | pub fn background(mut self, background: impl FnOnce(&BackgroundCtx, &mut egui::Ui)) -> Self { 170 | let Show { 171 | ref mut ui, 172 | ref tracks, 173 | } = self; 174 | let bg = BackgroundCtx { 175 | header_full_rect: tracks.header_full_rect, 176 | timeline: &tracks.timeline, 177 | }; 178 | background(&bg, ui); 179 | self 180 | } 181 | 182 | /// Paints the grid over the timeline `Rect`. 183 | /// 184 | /// If using a custom `background`, you may wish to call this after. 185 | pub fn paint_grid(self, info: &dyn ruler::MusicalInfo) -> Self { 186 | let vis = self.ui.style().noninteractive(); 187 | let mut stroke = vis.bg_stroke; 188 | let bar_color = stroke.color.linear_multiply(0.5); 189 | let step_even_color = stroke.color.linear_multiply(0.25); 190 | let step_odd_color = stroke.color.linear_multiply(0.125); 191 | let tl_rect = self.tracks.timeline.full_rect; 192 | let visible_len = tl_rect.width(); 193 | let mut steps = ruler::Steps::new(info, visible_len, MIN_STEP_GAP); 194 | while let Some(step) = steps.next(info) { 195 | stroke.color = match step.index_in_bar { 196 | 0 => bar_color, 197 | n if n % 2 == 0 => step_even_color, 198 | _ => step_odd_color, 199 | }; 200 | let x = tl_rect.left() + step.x; 201 | let a = egui::Pos2::new(x, tl_rect.top()); 202 | let b = egui::Pos2::new(x, tl_rect.bottom()); 203 | self.ui.painter().line_segment([a, b], stroke); 204 | } 205 | self 206 | } 207 | 208 | /// Set some tracks that should be pinned to the top. 209 | /// 210 | /// Often useful for the ruler or other tracks that should always be visible. 211 | pub fn pinned_tracks(mut self, tracks_fn: impl FnOnce(&TracksCtx, &mut egui::Ui)) -> Self { 212 | let Self { 213 | ref mut ui, 214 | ref tracks, 215 | } = self; 216 | 217 | // Use no spacing by default so we can get exact position for line separator. 218 | ui.scope(|ui| tracks_fn(tracks, ui)); 219 | 220 | // Draw a line to mark end of the pinned tracks. 221 | let remaining = ui.available_rect_before_wrap(); 222 | let a = remaining.left_top(); 223 | let b = remaining.right_top(); 224 | let stroke = ui.style().visuals.noninteractive().bg_stroke; 225 | ui.painter().line_segment([a, b], stroke); 226 | 227 | // Add the exact space so the UI is aware. 228 | ui.add_space(stroke.width); 229 | 230 | // Return to default spacing. 231 | let rect = ui.available_rect_before_wrap(); 232 | self.ui.set_clip_rect(rect); 233 | self 234 | } 235 | 236 | /// Set all remaining tracks for the timeline. 237 | /// 238 | /// These tracks will become vertically scrollable in the case that there are two many to fit 239 | /// on the view. The given `egui::Rect` is the viewport (visible area) relative to the 240 | /// timeline. 241 | pub fn tracks( 242 | mut self, 243 | tracks_fn: impl FnOnce(&TracksCtx, egui::Rect, &mut egui::Ui), 244 | ) -> SetPlayhead { 245 | let Self { 246 | ref mut ui, 247 | ref tracks, 248 | } = self; 249 | let rect = ui.available_rect_before_wrap(); 250 | let enable_scrolling = !ui.input(|i| i.modifiers.ctrl); 251 | let res = egui::ScrollArea::vertical() 252 | .max_height(rect.height()) 253 | .enable_scrolling(enable_scrolling) 254 | .animated(true) 255 | .stick_to_bottom(true) // stick to new tracks as they're added 256 | .show_viewport(ui, |ui, view| tracks_fn(tracks, view, ui)); 257 | let timeline_rect = tracks.timeline.full_rect; 258 | let tracks_bottom = res 259 | .inner_rect 260 | .bottom() 261 | .min(res.inner_rect.top() + res.content_size.y); 262 | SetPlayhead { 263 | timeline_rect, 264 | tracks_bottom, 265 | } 266 | } 267 | } 268 | 269 | impl SetPlayhead { 270 | /// Instantiate the playhead over the top of the whole timeline. 271 | pub fn playhead( 272 | &self, 273 | ui: &mut egui::Ui, 274 | info: &mut dyn PlayheadApi, 275 | playhead: Playhead, 276 | ) -> egui::Response { 277 | playhead::set(ui, info, self.timeline_rect, self.tracks_bottom, playhead) 278 | } 279 | } 280 | 281 | /// A type used to assist with setting a track with an optional `header`. 282 | pub struct TrackCtx<'a> { 283 | tracks: &'a TracksCtx, 284 | ui: &'a mut egui::Ui, 285 | available_rect: egui::Rect, 286 | header_height: f32, 287 | } 288 | 289 | impl<'a> TrackCtx<'a> { 290 | /// UI for the track's header. 291 | pub fn header(mut self, header: impl FnOnce(&mut egui::Ui)) -> Self { 292 | let header_h = self 293 | .tracks 294 | .header_full_rect 295 | .map(|mut rect| { 296 | rect.min.y = self.available_rect.min.y; 297 | let ui = &mut self.ui.new_child( 298 | egui::UiBuilder::new() 299 | .max_rect(rect) 300 | .layout(*self.ui.layout()), 301 | ); 302 | header(ui); 303 | ui.min_rect().height() 304 | }) 305 | .unwrap_or(0.0); 306 | self.header_height = header_h; 307 | self 308 | } 309 | 310 | /// Set the track, with a function for instantiating contents for the timeline. 311 | pub fn show(self, track: impl FnOnce(&TimelineCtx, &mut egui::Ui)) { 312 | // The UI and area for the track timeline. 313 | let track_h = { 314 | let mut rect = self.tracks.timeline.full_rect; 315 | rect.min.y = self.available_rect.min.y; 316 | let ui = &mut self.ui.new_child( 317 | egui::UiBuilder::new() 318 | .max_rect(rect) 319 | .layout(*self.ui.layout()), 320 | ); 321 | track(&self.tracks.timeline, ui); 322 | ui.min_rect().height() 323 | }; 324 | // Manually add space occuppied by the child UIs, otherwise `ScrollArea` won't consider the 325 | // space occuppied. TODO: Is there a better way to handle this? 326 | let w = self.tracks.full_rect.width(); 327 | let h = self.header_height.max(track_h); 328 | self.ui.scope(|ui| { 329 | ui.spacing_mut().item_spacing.y = 0.0; 330 | ui.spacing_mut().interact_size.y = 0.0; 331 | ui.horizontal(|ui| ui.add_space(w)); 332 | ui.add_space(h); 333 | }); 334 | } 335 | } 336 | 337 | impl TracksCtx { 338 | /// Begin showing the next `Track`. 339 | pub fn next<'a>(&'a self, ui: &'a mut egui::Ui) -> TrackCtx<'a> { 340 | let available_rect = ui.available_rect_before_wrap(); 341 | TrackCtx { 342 | tracks: self, 343 | ui, 344 | available_rect, 345 | header_height: 0.0, 346 | } 347 | } 348 | } 349 | 350 | impl TimelineCtx { 351 | /// The number of visible ticks across the width of the timeline. 352 | pub fn visible_ticks(&self) -> f32 { 353 | self.visible_ticks 354 | } 355 | 356 | /// Short-hand for drawing a plot within the timeline UI. 357 | /// 358 | /// The same as `egui::plot::Plot::new`, but sets some useful defaults before returning. 359 | pub fn plot_ticks(&self, id_source: impl Hash, y: RangeInclusive) -> plot::Plot { 360 | let h = 72.0; 361 | plot::Plot::new(id_source) 362 | .set_margin_fraction(egui::Vec2::ZERO) 363 | .show_grid(egui::Vec2b::FALSE) 364 | .allow_zoom(false) 365 | .allow_boxed_zoom(false) 366 | .allow_drag(false) 367 | .allow_scroll(false) 368 | .allow_boxed_zoom(false) 369 | .include_x(0.0) 370 | .include_x(self.visible_ticks) 371 | .include_y(*y.start()) 372 | .include_y(*y.end()) 373 | .show_x(false) 374 | .show_y(false) 375 | .legend(plot::Legend::default().position(plot::Corner::LeftTop)) 376 | .show_background(false) 377 | .show_axes([false; 2]) 378 | .height(h) 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/playhead.rs: -------------------------------------------------------------------------------- 1 | use super::ruler::MusicalInfo; 2 | 3 | /// For retrieving information about the playhead. 4 | pub trait Info: MusicalInfo { 5 | /// The location of the playhead in ticks relative to the start of the timeline. 6 | fn playhead_ticks(&self) -> f32; 7 | } 8 | 9 | /// For handling interaction with the playhead. 10 | pub trait Interaction { 11 | /// Set the location of the playhead in ticks. 12 | fn set_playhead_ticks(&mut self, ticks: f32); 13 | } 14 | 15 | /// For both providing info and handling interaction. 16 | pub trait PlayheadApi: Info + Interaction {} 17 | 18 | /// Playhead configuration for a timeline widget. 19 | pub struct Playhead { 20 | extend_beyond_last_track: f32, 21 | extend_to_available_height: bool, 22 | width: f32, 23 | } 24 | 25 | impl Playhead { 26 | pub const DEFAULT_EXTEND_BEYOND_LAST_TRACK: f32 = 0.0; 27 | pub const DEFAULT_EXTEND_TO_AVAILABLE_HEIGHT: bool = false; 28 | pub const DEFAULT_WIDTH: f32 = 1.0; 29 | 30 | /// Create a new default playhead. 31 | pub fn new() -> Self { 32 | Self::default() 33 | } 34 | 35 | /// Whether or not to extend the playhead to the total available height. 36 | /// 37 | /// This is useful if the timeline occupies a main `CentralPanel` and you 38 | /// want the playhead to extend across the entire available track space, 39 | /// rather than just the occupied track space. 40 | /// 41 | /// Default: `false` 42 | pub fn extend_to_available_height(mut self, b: bool) -> Self { 43 | self.extend_to_available_height = b; 44 | self 45 | } 46 | 47 | /// Extend the playhead beyond the last track by the given amount. 48 | /// 49 | /// Only applies if `extend_to_available_height` is `false`. 50 | /// 51 | /// Default: `0.0` 52 | pub fn extend_beyond_last_track(mut self, f: f32) -> Self { 53 | self.extend_beyond_last_track = f; 54 | self 55 | } 56 | 57 | /// Specify the width of the playhead rect. 58 | /// 59 | /// Default: `1.0` 60 | pub fn width(mut self, width: f32) -> Self { 61 | self.width = width; 62 | self 63 | } 64 | } 65 | 66 | impl Default for Playhead { 67 | fn default() -> Self { 68 | Self { 69 | extend_beyond_last_track: Self::DEFAULT_EXTEND_BEYOND_LAST_TRACK, 70 | extend_to_available_height: Self::DEFAULT_EXTEND_TO_AVAILABLE_HEIGHT, 71 | width: Self::DEFAULT_WIDTH, 72 | } 73 | } 74 | } 75 | 76 | impl PlayheadApi for T where T: Info + Interaction {} 77 | 78 | /// Set the playhead widget - a thin line for indicating progress through the timeline. 79 | pub fn set( 80 | ui: &mut egui::Ui, 81 | api: &mut dyn PlayheadApi, 82 | timeline_rect: egui::Rect, 83 | tracks_bottom: f32, 84 | playhead: Playhead, 85 | ) -> egui::Response { 86 | // Allocate a thin `Rect` over the timeline at the playhead. 87 | let playhead_ticks = api.playhead_ticks(); 88 | let playhead_x = timeline_rect.left() + playhead_ticks / api.ticks_per_point(); 89 | let half_w = playhead.width * 0.5; 90 | let top = timeline_rect.top(); 91 | let bottom = if playhead.extend_to_available_height { 92 | timeline_rect.bottom() 93 | } else { 94 | tracks_bottom + playhead.extend_beyond_last_track 95 | }; 96 | let min = egui::Pos2::new(playhead_x - half_w, top); 97 | let max = egui::Pos2::new(playhead_x + half_w, bottom); 98 | let rect = egui::Rect::from_min_max(min, max); 99 | let mut response = ui.allocate_rect(rect, egui::Sense::click_and_drag()); 100 | 101 | let timeline_w = timeline_rect.width(); 102 | let ticks_per_point = api.ticks_per_point(); 103 | let visible_ticks = ticks_per_point * timeline_w; 104 | 105 | // Handle interactions. 106 | if response.clicked() || response.dragged() { 107 | if let Some(pt) = response.interact_pointer_pos() { 108 | let tick = (((pt.x - timeline_rect.min.x) / timeline_w) * visible_ticks).max(0.0); 109 | api.set_playhead_ticks(tick); 110 | response.mark_changed(); 111 | } 112 | } 113 | 114 | // Draw a thin rect. 115 | if timeline_rect.x_range().contains(playhead_x) { 116 | let visuals = ui.style().interact(&response); 117 | let radius = 0.0; 118 | let stroke = egui::Stroke { 119 | width: 0.5, 120 | ..visuals.fg_stroke 121 | }; 122 | ui.painter().rect(rect, radius, visuals.bg_fill, stroke); 123 | } 124 | 125 | response 126 | } 127 | -------------------------------------------------------------------------------- /src/ruler.rs: -------------------------------------------------------------------------------- 1 | use super::Bar; 2 | 3 | /// Access to musical information required by the timeline. 4 | pub trait MusicalInfo { 5 | /// The number of ticks per beat, also known as PPQN (parts per quarter note). 6 | fn ticks_per_beat(&self) -> u32; 7 | /// The bar at the given tick offset starting from the beginning (left) of the timeline view. 8 | fn bar_at_ticks(&self, tick: f32) -> Bar; 9 | /// Affects how "zoomed" the timeline is. By default, uses 16 points per beat. 10 | fn ticks_per_point(&self) -> f32 { 11 | self.ticks_per_beat() as f32 / 16.0 12 | } 13 | } 14 | 15 | /// Respond to when the user clicks on the ruler. 16 | pub trait MusicalInteract { 17 | /// The given tick location was clicked 18 | fn click_at_tick(&mut self, tick: f32); 19 | } 20 | 21 | /// The required API for the musical ruler widget. 22 | pub trait MusicalRuler { 23 | fn info(&self) -> &dyn MusicalInfo; 24 | fn interact(&mut self) -> &mut dyn MusicalInteract; 25 | } 26 | 27 | /// Instantiate a musical ruler widget, showing bars and meters. 28 | pub fn musical(ui: &mut egui::Ui, api: &mut dyn MusicalRuler) -> egui::Response { 29 | // Allocate space for the ruler. 30 | let h = ui.spacing().interact_size.y; 31 | let w = ui.available_width(); 32 | let desired_size = egui::Vec2::new(w, h); 33 | let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click_and_drag()); 34 | 35 | // Check for clicks. 36 | let w = rect.width(); 37 | let ticks_per_point = api.info().ticks_per_point(); 38 | let visible_ticks = w * ticks_per_point; 39 | if response.clicked() || response.dragged() { 40 | if let Some(pt) = response.interact_pointer_pos() { 41 | let tick = (((pt.x - rect.min.x) / w) * visible_ticks).max(0.0); 42 | api.interact().click_at_tick(tick); 43 | response.mark_changed(); 44 | } 45 | } 46 | 47 | // Time to draw things. 48 | let vis = ui.style().noninteractive(); 49 | 50 | // Draw each of the step lines. 51 | let mut stroke = vis.fg_stroke; 52 | let bar_color = stroke.color.linear_multiply(0.5); 53 | let step_color = stroke.color.linear_multiply(0.125); 54 | let bar_y = rect.center().y; 55 | let step_even_y = rect.top() + rect.height() * 0.25; 56 | let step_odd_y = rect.top() + rect.height() * 0.125; 57 | 58 | // Iterate over the steps of the ruler to draw them. 59 | let visible_len = w; 60 | let info = api.info(); 61 | let mut steps = Steps::new(info, visible_len, super::MIN_STEP_GAP); 62 | while let Some(step) = steps.next(info) { 63 | let (y, color) = match step.index_in_bar { 64 | 0 => (bar_y, bar_color), 65 | n if n % 2 == 0 => (step_even_y, step_color), 66 | _ => (step_odd_y, step_color), 67 | }; 68 | stroke.color = color; 69 | let x = rect.left() + step.x; 70 | let a = egui::Pos2::new(x, rect.top()); 71 | let b = egui::Pos2::new(x, y); 72 | ui.painter().line_segment([a, b], stroke); 73 | } 74 | 75 | response 76 | } 77 | 78 | #[derive(Copy, Clone, Debug)] 79 | pub struct Step { 80 | /// The index of the step within the bar. 81 | /// 82 | /// The first step always indicates the start of the bar. 83 | pub index_in_bar: usize, 84 | /// The position of the step in ticks from the beginning of the start of the visible area. 85 | pub ticks: f32, 86 | /// The location of the step along the x axis from the start of the ruler. 87 | pub x: f32, 88 | } 89 | 90 | #[derive(Clone, Debug)] 91 | pub struct Steps { 92 | ticks_per_beat: f32, 93 | ticks_per_point: f32, 94 | visible_ticks: f32, 95 | min_step_ticks: f32, 96 | index_in_bar: usize, 97 | step_ticks: f32, 98 | bar: Bar, 99 | ticks: f32, 100 | } 101 | 102 | impl Steps { 103 | /// Create a new `Steps`. 104 | pub fn new(api: &dyn MusicalInfo, visible_len: f32, min_step_gap: f32) -> Self { 105 | let ticks_per_beat = api.ticks_per_beat() as f32; 106 | let ticks_per_point = api.ticks_per_point(); 107 | let visible_ticks = ticks_per_point * visible_len; 108 | let min_step_ticks = ticks_per_point * min_step_gap; 109 | Self { 110 | ticks_per_beat, 111 | ticks_per_point, 112 | visible_ticks, 113 | min_step_ticks, 114 | index_in_bar: 0, 115 | step_ticks: 0.0, 116 | bar: api.bar_at_ticks(0.0), 117 | ticks: 0.0, 118 | } 119 | } 120 | 121 | /// Produce the next `Step`. 122 | pub fn next(&mut self, api: &dyn MusicalInfo) -> Option { 123 | 'bars: loop { 124 | // If this is the first step of the bar, update step interval. 125 | if self.index_in_bar == 0 { 126 | self.ticks = self.bar.tick_range.start; 127 | let mut beat_subdivs = self.bar.time_sig.bottom / 4; 128 | self.step_ticks = self.ticks_per_beat as f32 / beat_subdivs as f32; 129 | if self.step_ticks >= self.min_step_ticks { 130 | loop { 131 | let new_beat_subdivs = beat_subdivs * 2; 132 | let new_step_ticks = self.ticks_per_beat as f32 / new_beat_subdivs as f32; 133 | if new_step_ticks <= self.min_step_ticks { 134 | break; 135 | } 136 | beat_subdivs = new_beat_subdivs; 137 | self.step_ticks = new_step_ticks; 138 | } 139 | } else { 140 | self.step_ticks = self.bar.tick_range.end - self.bar.tick_range.start; 141 | } 142 | } 143 | 144 | 'ticks: loop { 145 | if self.ticks > self.visible_ticks { 146 | return None; 147 | } 148 | if self.ticks >= self.bar.tick_range.end { 149 | self.index_in_bar = 0; 150 | self.bar = api.bar_at_ticks(self.bar.tick_range.end + 0.5); 151 | continue 'bars; 152 | } 153 | let index_in_bar = self.index_in_bar; 154 | let ticks = self.ticks; 155 | self.index_in_bar += 1; 156 | self.ticks += self.step_ticks; 157 | if ticks < 0.0 { 158 | continue 'ticks; 159 | } 160 | let x = ticks / self.ticks_per_point; 161 | let step = Step { 162 | index_in_bar, 163 | ticks, 164 | x, 165 | }; 166 | return Some(step); 167 | } 168 | } 169 | } 170 | } 171 | --------------------------------------------------------------------------------