├── .github ├── dependabot.yml └── workflows │ ├── check.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .markdownlint.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bacon.toml ├── cliff.toml ├── examples ├── README.md ├── alignment.rs ├── alignment.tape ├── demo.rs ├── demo.tape ├── pixel_size.rs ├── pixel_size.tape ├── stopwatch.rs └── stopwatch.tape ├── release-plz.toml ├── src ├── big_text.rs ├── lib.rs └── pixel_size.rs └── typos.toml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "cargo" # See documentation for possible values 13 | directory: "/" # Location of package manifests 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | jobs: 7 | check: 8 | permissions: 9 | checks: write 10 | uses: joshka/github-workflows/.github/workflows/rust-check.yml@main 11 | with: 12 | msrv: 1.74.0 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | pull-requests: write 3 | contents: write 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | jobs: 11 | release-plz: 12 | uses: joshka/github-workflows/.github/workflows/rust-release-plz.yml@main 13 | permissions: 14 | pull-requests: write 15 | contents: write 16 | secrets: 17 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | uses: joshka/github-workflows/.github/workflows/rust-test.yml@main 10 | secrets: 11 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | no-inline-html: 2 | allowed_elements: 3 | - br 4 | - details 5 | - summary 6 | line-length: 7 | line_length: 100 8 | heading_line_length: 100 9 | code_block_line_length: 100 10 | code_blocks: true 11 | tables: true 12 | headings: true 13 | strict: false 14 | stern: false 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.4.5] - 2024-06-25 6 | 7 | ### ⚙️ Miscellaneous Tasks 8 | 9 | - *(deps)* Bump ratatui version ([#45](https://github.com/joshka/tui-big-text/pull/45)) 10 | - Use https://github.com/joshka/github-workflows/ 11 | - Set msrv to 1.74 12 | - Update git cliff config 13 | 14 | 15 | ## [unreleased] 16 | 17 | ### ⚙️ Miscellaneous Tasks 18 | 19 | - Use faster release-plz 20 | - *(deps)* Bump ratatui version ([#45](https://github.com/joshka/tui-big-text/issues/45)) 21 | - Use https://github.com/joshka/github-workflows/ 22 | - Set msrv to 1.74 23 | 24 | ## [0.4.4] - 2024-05-28 25 | 26 | ### ⚙️ Miscellaneous Tasks 27 | 28 | - *(deps)* Update ratatui to 0.26.3 and itertools to 0.13.0 29 | - Release ([#43](https://github.com/joshka/tui-big-text/issues/43)) 30 | 31 | ## [0.4.3] - 2024-04-12 32 | 33 | ### 🚀 Features 34 | 35 | - Add alignment support for BigText ([#41](https://github.com/joshka/tui-big-text/issues/41)) 36 | 37 | ### ⚙️ Miscellaneous Tasks 38 | 39 | - Release v0.4.3 ([#42](https://github.com/joshka/tui-big-text/issues/42)) 40 | 41 | ## [0.4.2] - 2024-02-26 42 | 43 | ### 🚀 Features 44 | 45 | - Add BigText::builder() 46 | 47 | ### 📚 Documentation 48 | 49 | - Add link to docs.rs in cargo.toml 50 | - Add pixel height example to main readme 51 | - Tweak readme 52 | - Update main demo example and README 53 | 54 | ### ⚙️ Miscellaneous Tasks 55 | 56 | - Release ([#39](https://github.com/joshka/tui-big-text/issues/39)) 57 | 58 | ### Build 59 | 60 | - *(deps)* Update derive_builder requirement from 0.13.0 to 0.20.0 ([#38](https://github.com/joshka/tui-big-text/issues/38)) 61 | 62 | ## [0.4.1] - 2024-02-15 63 | 64 | ### ⚙️ Miscellaneous Tasks 65 | 66 | - Release ([#37](https://github.com/joshka/tui-big-text/issues/37)) 67 | 68 | ## [0.4.0] - 2024-02-08 69 | 70 | ### 🚀 Features 71 | 72 | - Add sextant-based fonts ([#26](https://github.com/joshka/tui-big-text/issues/26)) 73 | 74 | ### 🐛 Bug Fixes 75 | 76 | - Typos 77 | 78 | ### 🚜 Refactor 79 | 80 | - Split big_text and pixel_size into modules for readability 81 | 82 | ### ⚙️ Miscellaneous Tasks 83 | 84 | - *(deps)* Reorder cargo.toml and doc 85 | - Release ([#35](https://github.com/joshka/tui-big-text/issues/35)) 86 | 87 | ### Build 88 | 89 | - *(deps)* Bump codecov/codecov-action from 3 to 4 ([#34](https://github.com/joshka/tui-big-text/issues/34)) 90 | 91 | ## [0.3.6] - 2024-02-05 92 | 93 | ### ⚙️ Miscellaneous Tasks 94 | 95 | - Update ratatui to 0.26 ([#32](https://github.com/joshka/tui-big-text/issues/32)) 96 | - Release ([#33](https://github.com/joshka/tui-big-text/issues/33)) 97 | 98 | ## [0.3.5] - 2024-01-30 99 | 100 | ### ⚙️ Miscellaneous Tasks 101 | 102 | - *(deps)* Update strum to 0.26.1 103 | - Release ([#31](https://github.com/joshka/tui-big-text/issues/31)) 104 | 105 | ## [0.3.4] - 2024-01-29 106 | 107 | ### ⚙️ Miscellaneous Tasks 108 | 109 | - Release ([#30](https://github.com/joshka/tui-big-text/issues/30)) 110 | 111 | ### Build 112 | 113 | - *(deps)* Update derive_builder requirement from 0.12.0 to 0.13.0 ([#29](https://github.com/joshka/tui-big-text/issues/29)) 114 | 115 | ## [0.3.3] - 2024-01-24 116 | 117 | ### 🐛 Bug Fixes 118 | 119 | - *(doc)* Builder initialization of BigTextBuilder in docs ([#27](https://github.com/joshka/tui-big-text/issues/27)) 120 | 121 | ### ⚙️ Miscellaneous Tasks 122 | 123 | - Fix missing changelog entry for PixelSize change 124 | - Create dependabot.yml 125 | - Release ([#25](https://github.com/joshka/tui-big-text/issues/25)) 126 | 127 | ## [0.3.2] - 2024-01-12 128 | 129 | ### 📚 Documentation 130 | 131 | - Improve examples 132 | 133 | ### ⚙️ Miscellaneous Tasks 134 | 135 | - *(readme)* Clean up links 136 | - *(readme)* More cleanup 137 | - Release ([#23](https://github.com/joshka/tui-big-text/issues/23)) 138 | 139 | ### Feat 140 | 141 | - Add PixelSize option ([#22](https://github.com/joshka/tui-big-text/issues/22)) 142 | 143 | ## [0.3.1] - 2023-12-23 144 | 145 | ### 📚 Documentation 146 | 147 | - Update example image ([#20](https://github.com/joshka/tui-big-text/issues/20)) 148 | 149 | ### ⚙️ Miscellaneous Tasks 150 | 151 | - Release ([#21](https://github.com/joshka/tui-big-text/issues/21)) 152 | 153 | ## [0.3.0] - 2023-12-23 154 | 155 | ### 📚 Documentation 156 | 157 | - Hello world raw mode and screenshot ([#19](https://github.com/joshka/tui-big-text/issues/19)) 158 | 159 | ### ⚙️ Miscellaneous Tasks 160 | 161 | - Add check for cargo-rdme to ensure readme is updated when lib.rs docs are ([#16](https://github.com/joshka/tui-big-text/issues/16)) 162 | - Release v0.3.0 ([#17](https://github.com/joshka/tui-big-text/issues/17)) 163 | 164 | ## [0.2.1] - 2023-10-27 165 | 166 | ### ⚙️ Miscellaneous Tasks 167 | 168 | - Bump release version to 0.2.1 169 | 170 | ## [0.2.0] - 2023-10-27 171 | 172 | ### 🐛 Bug Fixes 173 | 174 | - Update examples to build with ratatui 0.24.0 175 | 176 | ### ⚙️ Miscellaneous Tasks 177 | 178 | - Release v0.1.5 ([#15](https://github.com/joshka/tui-big-text/issues/15)) 179 | 180 | ### Build 181 | 182 | - *(ratatui)* Update dependency to ratatui 0.24.0 183 | 184 | ## [0.1.4] - 2023-09-05 185 | 186 | ### ⚙️ Miscellaneous Tasks 187 | 188 | - Update changelog 189 | - Undo release-plz fetch-depth change 190 | - Release ([#13](https://github.com/joshka/tui-big-text/issues/13)) 191 | 192 | ## [0.1.3] - 2023-09-05 193 | 194 | ### 🐛 Bug Fixes 195 | 196 | - Add doc test imports ([#8](https://github.com/joshka/tui-big-text/issues/8)) 197 | 198 | ### 🚜 Refactor 199 | 200 | - Render fn 201 | 202 | ### 📚 Documentation 203 | 204 | - Tweak readme, licenses, contributing ([#10](https://github.com/joshka/tui-big-text/issues/10)) 205 | 206 | ### 🧪 Testing 207 | 208 | - Fix coverage for expected buffers in codecov 209 | 210 | ### ⚙️ Miscellaneous Tasks 211 | 212 | - Add ci.yml ([#6](https://github.com/joshka/tui-big-text/issues/6)) 213 | - Add bacon config 214 | - Configure git-cliff ([#11](https://github.com/joshka/tui-big-text/issues/11)) 215 | - Configure release-plz fetch depth 216 | - Release ([#12](https://github.com/joshka/tui-big-text/issues/12)) 217 | 218 | ## [0.1.2] - 2023-09-05 219 | 220 | ### 📚 Documentation 221 | 222 | - Use cargo-rdme to sync lib.rs to README.md ([#4](https://github.com/joshka/tui-big-text/issues/4)) 223 | 224 | ### ⚙️ Miscellaneous Tasks 225 | 226 | - Release ([#5](https://github.com/joshka/tui-big-text/issues/5)) 227 | 228 | ## [0.1.1] - 2023-09-05 229 | 230 | ### 🚀 Features 231 | 232 | - Initial implementation 233 | 234 | ### 🐛 Bug Fixes 235 | 236 | - Render correctly when not at the origin 237 | 238 | ### 📚 Documentation 239 | 240 | - Add stopwatch example 241 | 242 | ### 🎨 Styling 243 | 244 | - Readme wrapping 245 | 246 | ### ⚙️ Miscellaneous Tasks 247 | 248 | - Fix repository link ([#1](https://github.com/joshka/tui-big-text/issues/1)) 249 | - Release 250 | 251 | 252 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | First off, thank you for considering contributing to tui-big-text. 4 | 5 | If your contribution is not straightforward, please first discuss the change you 6 | wish to make by creating a new issue before making the change. 7 | 8 | ## Reporting issues 9 | 10 | Before reporting an issue on the 11 | [issue tracker](https://github.com/joshka/tui-big-text/issues), 12 | please check that it has not already been reported by searching for some related 13 | keywords. 14 | 15 | ## Pull requests 16 | 17 | Try to do one pull request per change. 18 | 19 | ## Commit Message Format 20 | 21 | This project adheres to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | A specification for adding human and machine readable meaning to commit messages. 23 | 24 | ### Commit Message Header 25 | 26 | ``` 27 | (): 28 | │ │ │ 29 | │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end. 30 | │ │ 31 | │ └─⫸ Commit Scope 32 | │ 33 | └─⫸ Commit Type: feat|fix|build|ci|docs|perf|refactor|test|chore 34 | ``` 35 | 36 | #### Type 37 | 38 | | feat | Features | A new feature | 39 | |----------|--------------------------|--------------------------------------------------------------------------------------------------------| 40 | | fix | Bug Fixes | A bug fix | 41 | | docs | Documentation | Documentation only changes | 42 | | style | Styles | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) | 43 | | refactor | Code Refactoring | A code change that neither fixes a bug nor adds a feature | 44 | | perf | Performance Improvements | A code change that improves performance | 45 | | test | Tests | Adding missing tests or correcting existing tests | 46 | | build | Builds | Changes that affect the build system or external dependencies (example scopes: main, serde) | 47 | | ci | Continuous Integrations | Changes to our CI configuration files and scripts (example scopes: Github Actions) | 48 | | chore | Chores | Other changes that don't modify src or test files | 49 | | revert | Reverts | Reverts a previous commit | 50 | 51 | ## Developing 52 | 53 | ### Set up 54 | 55 | This is no different than other Rust projects. 56 | 57 | ```shell 58 | git clone https://github.com/joshka/tui-big-text 59 | cd tui-big-text 60 | cargo test 61 | ``` 62 | 63 | ### Useful Commands 64 | 65 | - Run Clippy: 66 | 67 | ```shell 68 | cargo clippy --all-targets --all-features --workspace 69 | ``` 70 | 71 | - Run all tests: 72 | 73 | ```shell 74 | cargo test --all-features --workspace 75 | ``` 76 | 77 | - Check to see if there are code formatting issues 78 | 79 | ```shell 80 | cargo fmt --all -- --check 81 | ``` 82 | 83 | - Format the code in the project 84 | 85 | ```shell 86 | cargo fmt --all 87 | ``` 88 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tui-big-text" 3 | version = "0.4.5" 4 | edition = "2021" 5 | description = "A simple Ratatui widget for displaying big text using the font8x8 crate in a TUI (Terminal UI)." 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/joshka/tui-big-text" 8 | documentation = "https://docs.rs/tui-big-text" 9 | authors = ["Joshka"] 10 | categories = ["command-line-interface", "gui"] 11 | keywords = ["cli", "console", "ratatui", "terminal", "tui"] 12 | rust-version = "1.74.0" 13 | 14 | [dependencies] 15 | derive_builder = "0.20.0" 16 | font8x8 = "0.3.1" 17 | itertools = "0.13.0" 18 | ratatui = "0.27.0" 19 | 20 | [dev-dependencies] 21 | anyhow = "1.0.44" 22 | crossterm = { version = "0.27.0", features = ["event-stream"] } 23 | futures = "0.3" 24 | indoc = "2.0.3" 25 | strum = { version = "0.26.1", features = ["derive"] } 26 | tokio = { version = "1.16", features = ["full"] } 27 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Josh McKinney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tui-big-text 2 | 3 | > [!IMPORTANT] 4 | > This repo has been consolidated into . All future work will 5 | > happen there. The crate will remain available as tui-big-text, but this repo is now archived. 6 | 7 | 8 | 9 | [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the 10 | glyphs from the [font8x8] crate. 11 | 12 | ![Demo](https://vhs.charm.sh/vhs-35FZxQa32pCZdRW7pmpqf6.gif) 13 | 14 | [![Crate badge]][tui-big-text] 15 | [![Docs.rs Badge]][API Docs] 16 | [![Deps.rs Badge]][Dependency Status]
17 | [![License Badge]](./LICENSE-MIT) 18 | [![Codecov.io Badge]][Code Coverage] 19 | [![Discord Badge]][Ratatui Discord] 20 | 21 | [GitHub Repository] · [API Docs] · [Examples] · [Changelog] · [Contributing] 22 | 23 | ## Installation 24 | 25 | ```shell 26 | cargo add ratatui tui-big-text 27 | ``` 28 | 29 | ## Usage 30 | 31 | Create a [`BigText`] widget using [`BigText::builder`] and pass it to [`render_widget`] to 32 | render be rendered. The builder allows you to customize the [`Style`] of the widget and the 33 | [`PixelSize`] of the glyphs. 34 | 35 | ## Examples 36 | 37 | ```rust 38 | use anyhow::Result; 39 | use ratatui::prelude::*; 40 | use tui_big_text::{BigText, PixelSize}; 41 | 42 | fn render(frame: &mut Frame) -> Result<()> { 43 | let big_text = BigText::builder() 44 | .pixel_size(PixelSize::Full) 45 | .style(Style::new().blue()) 46 | .lines(vec![ 47 | "Hello".red().into(), 48 | "World".white().into(), 49 | "~~~~~".into(), 50 | ]) 51 | .build()?; 52 | frame.render_widget(big_text, frame.size()); 53 | Ok(()) 54 | } 55 | ``` 56 | 57 | The [`PixelSize`] can be used to control how many character cells are used to represent a single 58 | pixel of the 8x8 font. It has six variants: 59 | 60 | - `Full` (default) - Each pixel is represented by a single character cell. 61 | - `HalfHeight` - Each pixel is represented by half the height of a character cell. 62 | - `HalfWidth` - Each pixel is represented by half the width of a character cell. 63 | - `Quadrant` - Each pixel is represented by a quarter of a character cell. 64 | - `ThirdHeight` - Each pixel is represented by a third of the height of a character cell. 65 | - `Sextant` - Each pixel is represented by a sixth of a character cell. 66 | 67 | ```rust 68 | BigText::builder().pixel_size(PixelSize::Full); 69 | BigText::builder().pixel_size(PixelSize::HalfHeight); 70 | BigText::builder().pixel_size(PixelSize::Quadrant); 71 | ``` 72 | 73 | ![Pixel Size](https://vhs.charm.sh/vhs-2nLycKO16vHzqg3TxDNvq4.gif) 74 | 75 | Text can be aligned to the Left / Right / Center using the `alignment` method. 76 | 77 | ```rust 78 | use ratatui::layout::Alignment; 79 | BigText::builder().alignment(Alignment::Left); 80 | BigText::builder().alignment(Alignment::Right); 81 | BigText::builder().alignment(Alignment::Center); 82 | ``` 83 | 84 | ![Alignment Example](https://vhs.charm.sh/vhs-1Yyr7BJ5vfmOmjYNywCNH3.gif) 85 | 86 | [tui-big-text]: https://crates.io/crates/tui-big-text 87 | [Ratatui]: https://crates.io/crates/ratatui 88 | [font8x8]: https://crates.io/crates/font8x8 89 | 90 | 91 | [`BigText`]: https://docs.rs/tui-big-text/latest/tui_big_text/big_text/struct.BigText.html 92 | [`BigText::builder`]: https://docs.rs/tui-big-text/latest/tui_big_text/big_text/struct.BigText.html#method.builder 93 | [`PixelSize`]: https://docs.rs/tui-big-text/latest/tui_big_text/pixel_size/enum.PixelSize.html 94 | [`render_widget`]: https://docs.rs/ratatui/latest/ratatui/struct.Frame.html#method.render_widget 95 | [`Style`]: https://docs.rs/ratatui/latest/ratatui/style/struct.Style.html 96 | 97 | [Crate badge]: https://img.shields.io/crates/v/tui-big-text?logo=rust&style=for-the-badge 98 | [Docs.rs Badge]: https://img.shields.io/docsrs/tui-big-text?logo=rust&style=for-the-badge 99 | [Deps.rs Badge]: https://deps.rs/repo/github/joshka/tui-big-text/status.svg?style=for-the-badge 100 | [License Badge]: https://img.shields.io/crates/l/tui-big-text?style=for-the-badge 101 | [Codecov.io Badge]: https://img.shields.io/codecov/c/github/joshka/tui-big-text?logo=codecov&style=for-the-badge&token=BAQ8SOKEST 102 | [Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=ratatui+discord&logo=discord&style=for-the-badge 103 | 104 | [API Docs]: https://docs.rs/crate/tui-big-text/ 105 | [Dependency Status]: https://deps.rs/repo/github/joshka/tui-big-text 106 | [Code Coverage]: https://app.codecov.io/gh/joshka/tui-big-text 107 | [Ratatui Discord]: https://discord.gg/pMCEU9hNEj 108 | 109 | [GitHub Repository]: https://github.com/joshka/tui-big-text 110 | [Examples]: https://github.com/joshka/tui-big-text/tree/main/examples 111 | [Changelog]: https://github.com/joshka/tui-big-text/blob/main/CHANGELOG.md 112 | [Contributing]: https://github.com/joshka/tui-big-text/blob/main/CONTRIBUTING.md 113 | 114 | 115 | 116 | ## License 117 | 118 | Copyright (c) 2023 Josh McKinney 119 | 120 | This project is licensed under either of 121 | 122 | - Apache License, Version 2.0 123 | ([LICENSE-APACHE](LICENSE-APACHE) or ) 124 | - MIT license 125 | ([LICENSE-MIT](LICENSE-MIT) or ) 126 | 127 | at your option. 128 | 129 | ## Contribution 130 | 131 | Unless you explicitly state otherwise, any contribution intentionally submitted 132 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 133 | dual licensed as above, without any additional terms or conditions. 134 | 135 | See [CONTRIBUTING.md](CONTRIBUTING.md). 136 | -------------------------------------------------------------------------------- /bacon.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for the bacon tool 2 | # 3 | # Bacon repository: https://github.com/Canop/bacon 4 | # Complete help on configuration: https://dystroy.org/bacon/config/ 5 | # You can also check bacon's own bacon.toml file 6 | # as an example: https://github.com/Canop/bacon/blob/main/bacon.toml 7 | 8 | default_job = "check" 9 | 10 | [jobs.check] 11 | command = ["cargo", "check", "--color", "always"] 12 | need_stdout = false 13 | 14 | [jobs.check-all] 15 | command = ["cargo", "check", "--all-targets", "--color", "always"] 16 | need_stdout = false 17 | 18 | [jobs.clippy] 19 | command = ["cargo", "clippy", "--all-targets", "--color", "always"] 20 | need_stdout = false 21 | 22 | [jobs.test] 23 | command = [ 24 | "cargo", 25 | "test", 26 | "--color", 27 | "always", 28 | "--", 29 | "--color", 30 | "always", # see https://github.com/Canop/bacon/issues/124 31 | ] 32 | need_stdout = true 33 | 34 | [jobs.doc] 35 | command = ["cargo", "doc", "--color", "always", "--no-deps"] 36 | need_stdout = false 37 | 38 | # If the doc compiles, then it opens in your browser and bacon switches 39 | # to the previous job 40 | [jobs.doc-open] 41 | command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] 42 | need_stdout = false 43 | # so that we don't open the browser at each change 44 | on_success = "back" 45 | 46 | # You can run your application and have the result displayed in bacon, 47 | # *if* it makes sense for this crate. You can run an example the same 48 | # way. Don't forget the `--color always` part or the errors won't be 49 | # properly parsed. 50 | [jobs.run] 51 | command = [ 52 | "cargo", 53 | "run", 54 | "--color", 55 | "always", 56 | # put launch parameters for your program behind a `--` separator 57 | ] 58 | need_stdout = true 59 | allow_warnings = true 60 | 61 | [jobs.coverage] 62 | command = [ 63 | "cargo", 64 | "llvm-cov", 65 | "--lcov", 66 | "--output-path", 67 | "target/lcov.info", 68 | "--color", 69 | "always", 70 | ] 71 | 72 | [jobs.rdme] 73 | command = ["cargo", "rdme", "--force"] 74 | need_stdout = true 75 | 76 | # You may define here keybindings that would be specific to 77 | # a project, for example a shortcut to launch a specific job. 78 | # Shortcuts to internal functions (scrolling, toggling, etc.) 79 | # should go in your personal global prefs.toml file instead. 80 | [keybindings] 81 | # alt-m = "job:my-job" 82 | v = "job:coverage" 83 | shift-r = "job:rdme" -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | striptags | trim | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 26 | {% if commit.breaking %}[**breaking**] {% endif %}\ 27 | {{ commit.message | upper_first }}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # template for the changelog footer 32 | footer = """ 33 | 34 | """ 35 | # remove the leading and trailing s 36 | trim = true 37 | # postprocessors 38 | postprocessors = [ 39 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 40 | ] 41 | 42 | [git] 43 | # parse the commits based on https://www.conventionalcommits.org 44 | conventional_commits = true 45 | # filter out the commits that are not conventional 46 | filter_unconventional = true 47 | # process each line of a commit as an individual commit 48 | split_commits = false 49 | # regex for preprocessing the commit messages 50 | commit_preprocessors = [ 51 | # Replace issue numbers 52 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/joshka/tui-big-text/issues/${2}))" }, 53 | # Check spelling of the commit with https://github.com/crate-ci/typos 54 | # If the spelling is incorrect, it will be automatically fixed. 55 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 56 | ] 57 | # regex for parsing and grouping commits 58 | commit_parsers = [ 59 | { message = "^feat", group = "🚀 Features" }, 60 | { message = "^fix", group = "🐛 Bug Fixes" }, 61 | { message = "^doc", group = "📚 Documentation" }, 62 | { message = "^perf", group = "⚡ Performance" }, 63 | { message = "^refactor", group = "🚜 Refactor" }, 64 | { message = "^style", group = "🎨 Styling" }, 65 | { message = "^test", group = "🧪 Testing" }, 66 | { message = "^chore\\(release\\): prepare for", skip = true }, 67 | { message = "^chore\\(pr\\)", skip = true }, 68 | { message = "^chore\\(pull\\)", skip = true }, 69 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, 70 | { body = ".*security", group = "🛡️ Security" }, 71 | { message = "^revert", group = "◀️ Revert" }, 72 | ] 73 | # protect breaking changes from being skipped due to matching a skipping commit_parser 74 | protect_breaking_commits = false 75 | # filter out the commits that are not matched by commit parsers 76 | filter_commits = false 77 | # regex for matching git tags 78 | # tag_pattern = "v[0-9].*" 79 | # regex for skipping tags 80 | # skip_tags = "" 81 | # regex for ignoring tags 82 | # ignore_tags = "" 83 | # sort the tags topologically 84 | topo_order = false 85 | # sort the commits inside sections by oldest/newest order 86 | sort_commits = "oldest" 87 | # limit the number of commits included in the changelog. 88 | # limit_commits = 42 89 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Demo 4 | 5 | ![Demo](https://vhs.charm.sh/vhs-35FZxQa32pCZdRW7pmpqf6.gif) 6 | 7 | ## Pixel Size 8 | 9 | ![Pixel Size](https://vhs.charm.sh/vhs-2nLycKO16vHzqg3TxDNvq4.gif) 10 | 11 | Note: Sextant characters are often rendered poorly in most / all fonts. 12 | 13 | ## Stopwatch 14 | 15 | ![Stopwatch](https://vhs.charm.sh/vhs-6CBkkGpIwAOeyWTyeCgDvs.gif) 16 | -------------------------------------------------------------------------------- /examples/alignment.rs: -------------------------------------------------------------------------------- 1 | use std::{io::stdout, thread::sleep, time::Duration}; 2 | 3 | use anyhow::Result; 4 | use crossterm::{ 5 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 6 | ExecutableCommand, 7 | }; 8 | use ratatui::prelude::*; 9 | use tui_big_text::{BigText, PixelSize}; 10 | 11 | fn main() -> Result<()> { 12 | stdout().execute(EnterAlternateScreen)?; 13 | enable_raw_mode()?; 14 | let backend = CrosstermBackend::new(stdout()); 15 | let mut terminal = Terminal::new(backend)?; 16 | terminal.clear()?; 17 | terminal.draw(|frame| render(frame).expect("failed to render"))?; 18 | sleep(Duration::from_secs(5)); 19 | terminal.clear()?; 20 | stdout().execute(LeaveAlternateScreen)?; 21 | disable_raw_mode()?; 22 | Ok(()) 23 | } 24 | 25 | fn render(frame: &mut Frame) -> Result<()> { 26 | let left = BigText::builder() 27 | .pixel_size(PixelSize::Quadrant) 28 | .alignment(Alignment::Left) 29 | .lines(vec!["Left".white().into()]) 30 | .build()?; 31 | 32 | let right = BigText::builder() 33 | .pixel_size(PixelSize::Quadrant) 34 | .alignment(Alignment::Right) 35 | .lines(vec!["Right".green().into()]) 36 | .build()?; 37 | 38 | let centered = BigText::builder() 39 | .pixel_size(PixelSize::Quadrant) 40 | .alignment(Alignment::Center) 41 | .lines(vec!["Centered".red().into()]) 42 | .build()?; 43 | 44 | use Constraint::*; 45 | let [top, middle, bottom] = Layout::vertical([Length(4); 3]).areas(frame.size()); 46 | 47 | frame.render_widget(left, top); 48 | frame.render_widget(right, middle); 49 | frame.render_widget(centered, bottom); 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/alignment.tape: -------------------------------------------------------------------------------- 1 | # VHS Tape (see https://github.com/charmbracelet/vhs) 2 | Output "target/alignment.gif" 3 | Set Theme "Aardvark Blue" 4 | Set Width 800 5 | Set Height 430 6 | Hide 7 | Type@0 "cargo run --example alignment --quiet" 8 | Enter 9 | Sleep 2s 10 | Show 11 | Screenshot "target/alignment.png" 12 | Sleep 1s 13 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use std::{io::stdout, thread::sleep, time::Duration}; 2 | 3 | use anyhow::Result; 4 | use crossterm::{ 5 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 6 | ExecutableCommand, 7 | }; 8 | use ratatui::{ 9 | prelude::*, 10 | widgets::{Block, BorderType}, 11 | }; 12 | use tui_big_text::BigText; 13 | 14 | fn main() -> Result<()> { 15 | stdout().execute(EnterAlternateScreen)?; 16 | enable_raw_mode()?; 17 | let backend = CrosstermBackend::new(stdout()); 18 | let mut terminal = Terminal::new(backend)?; 19 | terminal.clear()?; 20 | terminal.draw(|frame| render(frame).expect("failed to render"))?; 21 | sleep(Duration::from_secs(5)); 22 | terminal.clear()?; 23 | stdout().execute(LeaveAlternateScreen)?; 24 | disable_raw_mode()?; 25 | Ok(()) 26 | } 27 | 28 | fn render(frame: &mut Frame) -> Result<()> { 29 | let block = Block::bordered() 30 | .border_type(BorderType::Rounded) 31 | .title("Tui-big-text Demo"); 32 | frame.render_widget(&block, frame.size()); 33 | let area = block.inner(frame.size()); 34 | let big_text = BigText::builder() 35 | .style(Style::new().blue()) 36 | .lines(vec![ 37 | "Tui-".red().into(), 38 | "big-".white().into(), 39 | "text".into(), 40 | ]) 41 | .build()?; 42 | frame.render_widget(big_text, area); 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /examples/demo.tape: -------------------------------------------------------------------------------- 1 | # VHS Tape (see https://github.com/charmbracelet/vhs) 2 | Output "target/demo.gif" 3 | Set Theme "Aardvark Blue" 4 | Set Width 600 5 | Set Height 800 6 | Hide 7 | Type@0 "cargo run --example demo --quiet" 8 | Enter 9 | Sleep 2s 10 | Show 11 | Screenshot "target/demo.png" 12 | Sleep 1s 13 | -------------------------------------------------------------------------------- /examples/pixel_size.rs: -------------------------------------------------------------------------------- 1 | use std::{io::stdout, thread::sleep, time::Duration}; 2 | 3 | use anyhow::Result; 4 | use crossterm::{ 5 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 6 | ExecutableCommand, 7 | }; 8 | use ratatui::prelude::*; 9 | use tui_big_text::{BigText, PixelSize}; 10 | 11 | fn main() -> Result<()> { 12 | stdout().execute(EnterAlternateScreen)?; 13 | enable_raw_mode()?; 14 | let backend = CrosstermBackend::new(stdout()); 15 | let mut terminal = Terminal::new(backend)?; 16 | terminal.clear()?; 17 | terminal.draw(|frame| render(frame).expect("failed to render"))?; 18 | sleep(Duration::from_secs(5)); 19 | terminal.clear()?; 20 | stdout().execute(LeaveAlternateScreen)?; 21 | disable_raw_mode()?; 22 | Ok(()) 23 | } 24 | 25 | fn render(frame: &mut Frame) -> Result<()> { 26 | let full_size_text = BigText::builder() 27 | .pixel_size(PixelSize::Full) 28 | .lines(vec!["FullSize".white().into()]) 29 | .build()?; 30 | 31 | let half_height_text = BigText::builder() 32 | .pixel_size(PixelSize::HalfHeight) 33 | .lines(vec!["1/2 high".green().into()]) 34 | .build()?; 35 | 36 | let half_wide_text = BigText::builder() 37 | .pixel_size(PixelSize::HalfWidth) 38 | .lines(vec!["1/2 wide".red().into()]) 39 | .build()?; 40 | 41 | let quadrant_text = BigText::builder() 42 | .pixel_size(PixelSize::Quadrant) 43 | .lines(vec!["Quadrant".blue().into(), " 1/2*1/2".blue().into()]) 44 | .build()?; 45 | 46 | let third_text = BigText::builder() 47 | .pixel_size(PixelSize::ThirdHeight) 48 | .lines(vec!["1/3".yellow().into(), "high".yellow().into()]) 49 | .build()?; 50 | 51 | let sextant_text = BigText::builder() 52 | .pixel_size(PixelSize::Sextant) 53 | .lines(vec!["Sextant".cyan().into(), " 1/2*1/3".cyan().into()]) 54 | .build()?; 55 | 56 | // Setup layout for 6 blocks 57 | use Constraint::*; 58 | let [full, half_height, middle, bottom] = 59 | Layout::vertical([Length(8), Length(4), Length(8), Length(6)]).areas(frame.size()); 60 | let [half_wide, quadrant] = Layout::horizontal([Length(32), Length(32)]).areas(middle); 61 | let [third_height, sextant] = Layout::horizontal([Length(32), Length(32)]).areas(bottom); 62 | 63 | frame.render_widget(full_size_text, full); 64 | frame.render_widget(half_height_text, half_height); 65 | frame.render_widget(half_wide_text, half_wide); 66 | frame.render_widget(quadrant_text, quadrant); 67 | frame.render_widget(third_text, third_height); 68 | frame.render_widget(sextant_text, sextant); 69 | 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /examples/pixel_size.tape: -------------------------------------------------------------------------------- 1 | # VHS Tape (see https://github.com/charmbracelet/vhs) 2 | Output "target/pixel_size.gif" 3 | Set Theme "Aardvark Blue" 4 | Set Width 1020 5 | Set Height 810 6 | Hide 7 | Type@0 "cargo run --example pixel_size --quiet" 8 | Enter 9 | Sleep 2s 10 | Show 11 | Screenshot "target/pixel_size.png" 12 | Sleep 1s 13 | -------------------------------------------------------------------------------- /examples/stopwatch.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Stdout}, 3 | time::{Duration, Instant}, 4 | }; 5 | 6 | use anyhow::{bail, Context, Result}; 7 | use crossterm::{ 8 | event::{self, KeyCode}, 9 | execute, 10 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 11 | }; 12 | use futures::{FutureExt, StreamExt}; 13 | // use futures::{select, FutureExt, StreamExt}; 14 | use itertools::Itertools; 15 | use ratatui::{prelude::*, widgets::Paragraph}; 16 | use strum::EnumIs; 17 | use tokio::select; 18 | use tui_big_text::BigText; 19 | 20 | #[tokio::main] 21 | async fn main() -> Result<()> { 22 | let mut app = StopwatchApp::default(); 23 | app.run().await 24 | } 25 | 26 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIs)] 27 | enum AppState { 28 | #[default] 29 | Stopped, 30 | Running, 31 | Quitting, 32 | } 33 | 34 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 35 | enum Message { 36 | StartOrSplit, 37 | Stop, 38 | Tick, 39 | Quit, 40 | } 41 | 42 | #[derive(Debug, Default, Clone, PartialEq)] 43 | struct StopwatchApp { 44 | state: AppState, 45 | splits: Vec, 46 | fps_counter: FpsCounter, 47 | } 48 | 49 | impl StopwatchApp { 50 | async fn run(&mut self) -> Result<()> { 51 | let mut tui = Tui::init()?; 52 | let mut events = EventHandler::new(60.0); 53 | while !self.state.is_quitting() { 54 | self.draw(&mut tui)?; 55 | let message = events.next().await?; 56 | self.handle_message(message)?; 57 | } 58 | Ok(()) 59 | } 60 | 61 | fn handle_message(&mut self, message: Message) -> Result<()> { 62 | match message { 63 | Message::StartOrSplit => self.start_or_split(), 64 | Message::Stop => self.stop(), 65 | Message::Tick => self.tick(), 66 | Message::Quit => self.quit(), 67 | } 68 | Ok(()) 69 | } 70 | 71 | fn start_or_split(&mut self) { 72 | if self.state.is_stopped() { 73 | self.start(); 74 | } else { 75 | self.record_split(); 76 | } 77 | } 78 | 79 | fn stop(&mut self) { 80 | self.record_split(); 81 | self.state = AppState::Stopped; 82 | } 83 | 84 | fn tick(&mut self) { 85 | self.fps_counter.tick() 86 | } 87 | 88 | fn quit(&mut self) { 89 | self.state = AppState::Quitting 90 | } 91 | 92 | fn start(&mut self) { 93 | self.splits.clear(); 94 | self.state = AppState::Running; 95 | self.record_split(); 96 | } 97 | 98 | fn record_split(&mut self) { 99 | if !self.state.is_running() { 100 | return; 101 | } 102 | self.splits.push(Instant::now()); 103 | } 104 | 105 | fn elapsed(&mut self) -> Duration { 106 | if self.state.is_running() { 107 | self.splits.first().map_or(Duration::ZERO, Instant::elapsed) 108 | } else { 109 | // last - first or 0 if there are no splits 110 | let now = Instant::now(); 111 | let first = *self.splits.first().unwrap_or(&now); 112 | let last = *self.splits.last().unwrap_or(&now); 113 | last - first 114 | } 115 | } 116 | 117 | fn draw(&mut self, tui: &mut Tui) -> Result<()> { 118 | tui.draw(|frame| { 119 | let layout = layout(frame.size()); 120 | frame.render_widget(Paragraph::new("Stopwatch Example"), layout[0]); 121 | frame.render_widget(self.fps_paragraph(), layout[1]); 122 | frame.render_widget(self.timer_paragraph(), layout[2]); 123 | frame.render_widget(Paragraph::new("Splits:"), layout[3]); 124 | frame.render_widget(self.splits_paragraph(), layout[4]); 125 | frame.render_widget(self.help_paragraph(), layout[5]); 126 | }) 127 | } 128 | 129 | fn fps_paragraph(&mut self) -> Paragraph<'_> { 130 | let fps = format!("{:.2} fps", self.fps_counter.fps); 131 | Paragraph::new(fps).dim().right_aligned() 132 | } 133 | 134 | fn timer_paragraph(&mut self) -> BigText<'_> { 135 | let style = if self.state.is_running() { 136 | Style::new().green() 137 | } else { 138 | Style::new().red() 139 | }; 140 | let duration = format_duration(self.elapsed()); 141 | let lines = vec![duration.into()]; 142 | BigText::builder() 143 | .lines(lines) 144 | .style(style) 145 | .build() 146 | .unwrap() 147 | } 148 | 149 | /// Renders the splits as a list of lines. 150 | /// 151 | /// ```text 152 | /// #01 -- 00:00.693 -- 00:00.693 153 | /// #02 -- 00:00.719 -- 00:01.413 154 | /// ``` 155 | fn splits_paragraph(&mut self) -> Paragraph<'_> { 156 | let start = *self.splits.first().unwrap_or(&Instant::now()); 157 | let mut splits = self 158 | .splits 159 | .iter() 160 | .copied() 161 | .tuple_windows() 162 | .enumerate() 163 | .map(|(index, (prev, current))| format_split(index, start, prev, current)) 164 | .collect::>(); 165 | splits.reverse(); 166 | Paragraph::new(splits) 167 | } 168 | 169 | fn help_paragraph(&mut self) -> Paragraph<'_> { 170 | let space_action = if self.state.is_stopped() { 171 | "start" 172 | } else { 173 | "split" 174 | }; 175 | let help_text = Line::from(vec![ 176 | "space ".into(), 177 | space_action.dim(), 178 | " enter ".into(), 179 | "stop".dim(), 180 | " q ".into(), 181 | "quit".dim(), 182 | ]); 183 | Paragraph::new(help_text).gray() 184 | } 185 | } 186 | 187 | fn layout(area: Rect) -> Vec { 188 | let layout = Layout::vertical(vec![ 189 | Constraint::Length(2), // top bar 190 | Constraint::Length(8), // timer 191 | Constraint::Length(1), // splits header 192 | Constraint::Min(0), // splits 193 | Constraint::Length(1), // help 194 | ]) 195 | .split(area); 196 | let top_layout = Layout::horizontal(vec![ 197 | Constraint::Length(20), // title 198 | Constraint::Min(0), // fps counter 199 | ]) 200 | .split(layout[0]); 201 | 202 | // return a new vec with the top_layout rects and then rest of layout 203 | top_layout[..] 204 | .iter() 205 | .chain(layout[1..].iter()) 206 | .copied() 207 | .collect() 208 | } 209 | 210 | fn format_split<'a>(index: usize, start: Instant, previous: Instant, current: Instant) -> Line<'a> { 211 | let split = format_duration(current - previous); 212 | let elapsed = format_duration(current - start); 213 | Line::from(vec![ 214 | format!("#{:02} -- ", index + 1).into(), 215 | Span::styled(split, Style::new().yellow()), 216 | " -- ".into(), 217 | Span::styled(elapsed, Style::new()), 218 | ]) 219 | } 220 | 221 | fn format_duration(duration: Duration) -> String { 222 | format!( 223 | "{:02}:{:02}.{:03}", 224 | duration.as_secs() / 60, 225 | duration.as_secs() % 60, 226 | duration.subsec_millis() 227 | ) 228 | } 229 | 230 | #[derive(Debug, Clone, PartialEq)] 231 | struct FpsCounter { 232 | start_time: Instant, 233 | frames: u32, 234 | pub fps: f64, 235 | } 236 | 237 | impl Default for FpsCounter { 238 | fn default() -> Self { 239 | Self::new() 240 | } 241 | } 242 | 243 | impl FpsCounter { 244 | fn new() -> Self { 245 | Self { 246 | start_time: Instant::now(), 247 | frames: 0, 248 | fps: 0.0, 249 | } 250 | } 251 | 252 | fn tick(&mut self) { 253 | self.frames += 1; 254 | let now = Instant::now(); 255 | let elapsed = (now - self.start_time).as_secs_f64(); 256 | if elapsed >= 1.0 { 257 | self.fps = self.frames as f64 / elapsed; 258 | self.start_time = now; 259 | self.frames = 0; 260 | } 261 | } 262 | } 263 | 264 | /// Handles events from crossterm and emits `Message`s. 265 | struct EventHandler { 266 | crossterm_events: event::EventStream, 267 | interval: tokio::time::Interval, 268 | } 269 | 270 | impl EventHandler { 271 | /// Creates a new event handler that emits a `Message::Tick` every `1.0 / max_fps` seconds. 272 | fn new(max_fps: f32) -> Self { 273 | let period = Duration::from_secs_f32(1.0 / max_fps); 274 | Self { 275 | crossterm_events: event::EventStream::new(), 276 | interval: tokio::time::interval(period), 277 | } 278 | } 279 | 280 | async fn next(&mut self) -> Result { 281 | select! { 282 | event = self.crossterm_events.next().fuse() => Self::handle_crossterm_event(event), 283 | _ = self.interval.tick().fuse() => Ok(Message::Tick), 284 | } 285 | } 286 | 287 | fn handle_crossterm_event( 288 | event: Option>, 289 | ) -> Result { 290 | match event { 291 | Some(Ok(event::Event::Key(key))) => Ok(match key.code { 292 | KeyCode::Char('q') => Message::Quit, 293 | KeyCode::Char(' ') => Message::StartOrSplit, 294 | KeyCode::Char('s') | KeyCode::Enter => Message::Stop, 295 | _ => Message::Tick, 296 | }), 297 | Some(Err(err)) => bail!(err), 298 | None => bail!("event stream ended unexpectedly"), 299 | _ => Ok(Message::Tick), 300 | } 301 | } 302 | } 303 | 304 | struct Tui { 305 | terminal: Terminal>, 306 | } 307 | 308 | impl Tui { 309 | fn init() -> Result { 310 | let mut stdout = io::stdout(); 311 | execute!(stdout, EnterAlternateScreen).context("failed to enter alternate screen")?; 312 | let backend = CrosstermBackend::new(stdout); 313 | let mut terminal = Terminal::new(backend).context("failed to create terminal")?; 314 | enable_raw_mode().context("failed to enable raw mode")?; 315 | terminal.hide_cursor().context("failed to hide cursor")?; 316 | terminal.clear().context("failed to clear console")?; 317 | Ok(Self { terminal }) 318 | } 319 | 320 | fn draw(&mut self, frame: impl FnOnce(&mut Frame)) -> Result<()> { 321 | self.terminal.draw(frame).context("failed to draw frame")?; 322 | Ok(()) 323 | } 324 | } 325 | 326 | impl Drop for Tui { 327 | fn drop(&mut self) { 328 | disable_raw_mode().expect("failed to disable raw mode"); 329 | execute!(self.terminal.backend_mut(), LeaveAlternateScreen) 330 | .expect("failed to switch to main screen"); 331 | self.terminal.show_cursor().expect("failed to show cursor"); 332 | self.terminal.clear().expect("failed to clear console"); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /examples/stopwatch.tape: -------------------------------------------------------------------------------- 1 | # VHS Tape (see https://github.com/charmbracelet/vhs) 2 | Output "target/stopwatch.gif" 3 | Set Theme "Aardvark Blue" 4 | Set Width 1200 5 | Set Height 600 6 | Hide 7 | Type@0 "cargo run --example stopwatch --quiet" 8 | Enter 9 | Sleep 2s 10 | Show 11 | Sleep 2s 12 | Space 13 | Sleep 1s 14 | Space 15 | Sleep 2s 16 | Space 17 | Sleep 3s 18 | Space 19 | Sleep 4s 20 | Space 21 | Sleep 5s 22 | Enter 23 | Sleep 2s 24 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | changelog_config = "cliff.toml" 3 | -------------------------------------------------------------------------------- /src/big_text.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | 3 | use derive_builder::Builder; 4 | use font8x8::UnicodeFonts; 5 | use ratatui::{prelude::*, text::StyledGrapheme, widgets::Widget}; 6 | 7 | use crate::PixelSize; 8 | 9 | /// Displays one or more lines of text using 8x8 pixel characters. 10 | /// 11 | /// The text is rendered using the [font8x8](https://crates.io/crates/font8x8) crate. 12 | /// 13 | /// Using the `pixel_size` method, you can also chose, how 'big' a pixel should be. Currently a 14 | /// pixel of the 8x8 font can be represented by one full or half (horizontal/vertical/both) 15 | /// character cell of the terminal. 16 | /// 17 | /// # Examples 18 | /// 19 | /// ```rust 20 | /// use ratatui::prelude::*; 21 | /// use tui_big_text::{BigText, PixelSize}; 22 | /// 23 | /// BigText::builder() 24 | /// .pixel_size(PixelSize::Full) 25 | /// .style(Style::new().white()) 26 | /// .lines(vec![ 27 | /// "Hello".red().into(), 28 | /// "World".blue().into(), 29 | /// "=====".into(), 30 | /// ]) 31 | /// .build(); 32 | /// ``` 33 | /// 34 | /// Renders: 35 | /// 36 | /// ```plain 37 | /// ██ ██ ███ ███ 38 | /// ██ ██ ██ ██ 39 | /// ██ ██ ████ ██ ██ ████ 40 | /// ██████ ██ ██ ██ ██ ██ ██ 41 | /// ██ ██ ██████ ██ ██ ██ ██ 42 | /// ██ ██ ██ ██ ██ ██ ██ 43 | /// ██ ██ ████ ████ ████ ████ 44 | /// 45 | /// ██ ██ ███ ███ 46 | /// ██ ██ ██ ██ 47 | /// ██ ██ ████ ██ ███ ██ ██ 48 | /// ██ █ ██ ██ ██ ███ ██ ██ █████ 49 | /// ███████ ██ ██ ██ ██ ██ ██ ██ 50 | /// ███ ███ ██ ██ ██ ██ ██ ██ 51 | /// ██ ██ ████ ████ ████ ███ ██ 52 | /// 53 | /// ███ ██ ███ ██ ███ ██ ███ ██ ███ ██ 54 | /// ██ ███ ██ ███ ██ ███ ██ ███ ██ ███ 55 | /// ``` 56 | #[derive(Debug, Builder, Clone, PartialEq, Eq, Hash)] 57 | pub struct BigText<'a> { 58 | /// The text to display 59 | #[builder(setter(into))] 60 | lines: Vec>, 61 | 62 | /// The style of the widget 63 | /// 64 | /// Defaults to `Style::default()` 65 | #[builder(default, setter(into))] 66 | style: Style, 67 | 68 | /// The size of single glyphs 69 | /// 70 | /// Defaults to `BigTextSize::default()` (=> BigTextSize::Full) 71 | #[builder(default)] 72 | pixel_size: PixelSize, 73 | 74 | /// The horizontal alignmnet of the text 75 | /// 76 | /// Defaults to `Alignment::default()` (=> Alignment::Left) 77 | #[builder(default)] 78 | alignment: Alignment, 79 | } 80 | 81 | impl BigText<'static> { 82 | /// Create a new [`BigTextBuilder`] to configure a [`BigText`] widget. 83 | pub fn builder() -> BigTextBuilder<'static> { 84 | BigTextBuilder::default() 85 | } 86 | } 87 | 88 | impl Widget for BigText<'_> { 89 | fn render(self, area: Rect, buf: &mut Buffer) { 90 | let layout = layout(area, &self.pixel_size, self.alignment, &self.lines); 91 | for (line, line_layout) in self.lines.iter().zip(layout) { 92 | for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) { 93 | render_symbol(g, cell, buf, &self.pixel_size); 94 | } 95 | } 96 | } 97 | } 98 | 99 | /// Chunk the area into as many x*y cells as possible returned as a 2D iterator of `Rect`s 100 | /// representing the rows of cells. The size of each cell depends on given font size 101 | fn layout<'a>( 102 | area: Rect, 103 | pixel_size: &PixelSize, 104 | alignment: Alignment, 105 | lines: &'a [Line<'a>], 106 | ) -> impl IntoIterator> + 'a { 107 | let (step_x, step_y) = pixel_size.pixels_per_cell(); 108 | let width = 8_u16.div_ceil(step_x); 109 | let height = 8_u16.div_ceil(step_y); 110 | 111 | (area.top()..area.bottom()) 112 | .step_by(height as usize) 113 | .zip(lines.iter()) 114 | .map(move |(y, line)| { 115 | let offset = get_alignment_offset(area.width, width, alignment, line); 116 | (area.left() + offset..area.right()) 117 | .step_by(width as usize) 118 | .map(move |x| { 119 | let width = min(area.right() - x, width); 120 | let height = min(area.bottom() - y, height); 121 | Rect::new(x, y, width, height) 122 | }) 123 | }) 124 | } 125 | 126 | fn get_alignment_offset<'a>( 127 | area_width: u16, 128 | letter_width: u16, 129 | alignment: Alignment, 130 | line: &'a Line<'a>, 131 | ) -> u16 { 132 | let big_line_width = line.width() as u16 * letter_width; 133 | match alignment { 134 | Alignment::Center => (area_width / 2).saturating_sub(big_line_width / 2), 135 | Alignment::Right => area_width.saturating_sub(big_line_width), 136 | Alignment::Left => 0, 137 | } 138 | } 139 | 140 | /// Render a single grapheme into a cell by looking up the corresponding 8x8 bitmap in the 141 | /// `BITMAPS` array and setting the corresponding cells in the buffer. 142 | fn render_symbol(grapheme: StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) { 143 | buf.set_style(area, grapheme.style); 144 | let c = grapheme.symbol.chars().next().unwrap(); // TODO: handle multi-char graphemes 145 | if let Some(glyph) = font8x8::BASIC_FONTS.get(c) { 146 | render_glyph(glyph, area, buf, pixel_size); 147 | } 148 | } 149 | 150 | /// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer. 151 | fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) { 152 | let (step_x, step_y) = pixel_size.pixels_per_cell(); 153 | 154 | let glyph_vertical_index = (0..glyph.len()).step_by(step_y as usize); 155 | let glyph_horizontal_bit_selector = (0..8).step_by(step_x as usize); 156 | 157 | for (row, y) in glyph_vertical_index.zip(area.top()..area.bottom()) { 158 | for (col, x) in glyph_horizontal_bit_selector 159 | .clone() 160 | .zip(area.left()..area.right()) 161 | { 162 | let cell = buf.get_mut(x, y); 163 | let symbol_character = pixel_size.symbol_for_position(&glyph, row, col); 164 | cell.set_char(symbol_character); 165 | } 166 | } 167 | } 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use super::*; 172 | 173 | type Result = std::result::Result>; 174 | 175 | #[test] 176 | fn build() -> Result<()> { 177 | let lines = vec![Line::from(vec!["Hello".red(), "World".blue()])]; 178 | let style = Style::new().green(); 179 | let pixel_size = PixelSize::default(); 180 | let alignment = Alignment::Center; 181 | assert_eq!( 182 | BigText::builder() 183 | .lines(lines.clone()) 184 | .style(style) 185 | .alignment(Alignment::Center) 186 | .build()?, 187 | BigText { 188 | lines, 189 | style, 190 | pixel_size, 191 | alignment, 192 | } 193 | ); 194 | Ok(()) 195 | } 196 | 197 | #[test] 198 | fn render_single_line() -> Result<()> { 199 | let big_text = BigText::builder() 200 | .lines(vec![Line::from("SingleLine")]) 201 | .build()?; 202 | let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8)); 203 | big_text.render(buf.area, &mut buf); 204 | let expected = Buffer::with_lines(vec![ 205 | " ████ ██ ███ ████ ██ ", 206 | "██ ██ ██ ██ ", 207 | "███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ", 208 | " ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ", 209 | " ███ ██ ██ ██ ██ ██ ██ ██████ ██ █ ██ ██ ██ ██████ ", 210 | "██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ", 211 | " ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ", 212 | " █████ ", 213 | ]); 214 | assert_eq!(buf, expected); 215 | Ok(()) 216 | } 217 | 218 | #[test] 219 | fn render_truncated() -> Result<()> { 220 | let big_text = BigText::builder() 221 | .lines(vec![Line::from("Truncated")]) 222 | .build()?; 223 | let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6)); 224 | big_text.render(buf.area, &mut buf); 225 | let expected = Buffer::with_lines(vec![ 226 | "██████ █ ███", 227 | "█ ██ █ ██ ██", 228 | " ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██", 229 | " ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████", 230 | " ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██", 231 | " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██", 232 | ]); 233 | assert_eq!(buf, expected); 234 | Ok(()) 235 | } 236 | 237 | #[test] 238 | fn render_multiple_lines() -> Result<()> { 239 | let big_text = BigText::builder() 240 | .lines(vec![Line::from("Multi"), Line::from("Lines")]) 241 | .build()?; 242 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16)); 243 | big_text.render(buf.area, &mut buf); 244 | let expected = Buffer::with_lines(vec![ 245 | "██ ██ ███ █ ██ ", 246 | "███ ███ ██ ██ ", 247 | "███████ ██ ██ ██ █████ ███ ", 248 | "███████ ██ ██ ██ ██ ██ ", 249 | "██ █ ██ ██ ██ ██ ██ ██ ", 250 | "██ ██ ██ ██ ██ ██ █ ██ ", 251 | "██ ██ ███ ██ ████ ██ ████ ", 252 | " ", 253 | "████ ██ ", 254 | " ██ ", 255 | " ██ ███ █████ ████ █████ ", 256 | " ██ ██ ██ ██ ██ ██ ██ ", 257 | " ██ █ ██ ██ ██ ██████ ████ ", 258 | " ██ ██ ██ ██ ██ ██ ██ ", 259 | "███████ ████ ██ ██ ████ █████ ", 260 | " ", 261 | ]); 262 | assert_eq!(buf, expected); 263 | Ok(()) 264 | } 265 | 266 | #[test] 267 | fn render_widget_style() -> Result<()> { 268 | let big_text = BigText::builder() 269 | .lines(vec![Line::from("Styled")]) 270 | .style(Style::new().bold()) 271 | .build()?; 272 | let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8)); 273 | big_text.render(buf.area, &mut buf); 274 | let mut expected = Buffer::with_lines(vec![ 275 | " ████ █ ███ ███ ", 276 | "██ ██ ██ ██ ██ ", 277 | "███ █████ ██ ██ ██ ████ ██ ", 278 | " ███ ██ ██ ██ ██ ██ ██ █████ ", 279 | " ███ ██ ██ ██ ██ ██████ ██ ██ ", 280 | "██ ██ ██ █ █████ ██ ██ ██ ██ ", 281 | " ████ ██ ██ ████ ████ ███ ██ ", 282 | " █████ ", 283 | ]); 284 | expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold()); 285 | assert_eq!(buf, expected); 286 | Ok(()) 287 | } 288 | 289 | #[test] 290 | fn render_line_style() -> Result<()> { 291 | let big_text = BigText::builder() 292 | .lines(vec![ 293 | Line::from("Red".red()), 294 | Line::from("Green".green()), 295 | Line::from("Blue".blue()), 296 | ]) 297 | .build()?; 298 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 24)); 299 | big_text.render(buf.area, &mut buf); 300 | let mut expected = Buffer::with_lines(vec![ 301 | "██████ ███ ", 302 | " ██ ██ ██ ", 303 | " ██ ██ ████ ██ ", 304 | " █████ ██ ██ █████ ", 305 | " ██ ██ ██████ ██ ██ ", 306 | " ██ ██ ██ ██ ██ ", 307 | "███ ██ ████ ███ ██ ", 308 | " ", 309 | " ████ ", 310 | " ██ ██ ", 311 | "██ ██ ███ ████ ████ █████ ", 312 | "██ ███ ██ ██ ██ ██ ██ ██ ██ ", 313 | "██ ███ ██ ██ ██████ ██████ ██ ██ ", 314 | " ██ ██ ██ ██ ██ ██ ██ ", 315 | " █████ ████ ████ ████ ██ ██ ", 316 | " ", 317 | "██████ ███ ", 318 | " ██ ██ ██ ", 319 | " ██ ██ ██ ██ ██ ████ ", 320 | " █████ ██ ██ ██ ██ ██ ", 321 | " ██ ██ ██ ██ ██ ██████ ", 322 | " ██ ██ ██ ██ ██ ██ ", 323 | "██████ ████ ███ ██ ████ ", 324 | " ", 325 | ]); 326 | expected.set_style(Rect::new(0, 0, 24, 8), Style::new().red()); 327 | expected.set_style(Rect::new(0, 8, 40, 8), Style::new().green()); 328 | expected.set_style(Rect::new(0, 16, 32, 8), Style::new().blue()); 329 | assert_eq!(buf, expected); 330 | Ok(()) 331 | } 332 | 333 | #[test] 334 | fn render_half_height_single_line() -> Result<()> { 335 | let big_text = BigText::builder() 336 | .pixel_size(PixelSize::HalfHeight) 337 | .lines(vec![Line::from("SingleLine")]) 338 | .build()?; 339 | let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4)); 340 | big_text.render(buf.area, &mut buf); 341 | let expected = Buffer::with_lines(vec![ 342 | "▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ", 343 | "▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ", 344 | "▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ", 345 | " ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ", 346 | ]); 347 | assert_eq!(buf, expected); 348 | Ok(()) 349 | } 350 | 351 | #[test] 352 | fn render_half_height_truncated() -> Result<()> { 353 | let big_text = BigText::builder() 354 | .pixel_size(PixelSize::HalfHeight) 355 | .lines(vec![Line::from("Truncated")]) 356 | .build()?; 357 | let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3)); 358 | big_text.render(buf.area, &mut buf); 359 | let expected = Buffer::with_lines(vec![ 360 | "█▀██▀█ ▄█ ▀██", 361 | " ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██", 362 | " ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██", 363 | ]); 364 | assert_eq!(buf, expected); 365 | Ok(()) 366 | } 367 | 368 | #[test] 369 | fn render_half_height_multiple_lines() -> Result<()> { 370 | let big_text = BigText::builder() 371 | .pixel_size(PixelSize::HalfHeight) 372 | .lines(vec![Line::from("Multi"), Line::from("Lines")]) 373 | .build()?; 374 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8)); 375 | big_text.render(buf.area, &mut buf); 376 | let expected = Buffer::with_lines(vec![ 377 | "██▄ ▄██ ▀██ ▄█ ▀▀ ", 378 | "███████ ██ ██ ██ ▀██▀▀ ▀██ ", 379 | "██ ▀ ██ ██ ██ ██ ██ ▄ ██ ", 380 | "▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ", 381 | "▀██▀ ▀▀ ", 382 | " ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ▄█▀▀▀▀ ", 383 | " ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ", 384 | "▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ", 385 | ]); 386 | assert_eq!(buf, expected); 387 | Ok(()) 388 | } 389 | 390 | #[test] 391 | fn render_half_height_widget_style() -> Result<()> { 392 | let big_text = BigText::builder() 393 | .pixel_size(PixelSize::HalfHeight) 394 | .lines(vec![Line::from("Styled")]) 395 | .style(Style::new().bold()) 396 | .build()?; 397 | let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4)); 398 | big_text.render(buf.area, &mut buf); 399 | let mut expected = Buffer::with_lines(vec![ 400 | "▄█▀▀█▄ ▄█ ▀██ ▀██ ", 401 | "▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ", 402 | "▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ", 403 | " ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ", 404 | ]); 405 | expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold()); 406 | assert_eq!(buf, expected); 407 | Ok(()) 408 | } 409 | 410 | #[test] 411 | fn render_half_height_line_style() -> Result<()> { 412 | let big_text = BigText::builder() 413 | .pixel_size(PixelSize::HalfHeight) 414 | .lines(vec![ 415 | Line::from("Red".red()), 416 | Line::from("Green".green()), 417 | Line::from("Blue".blue()), 418 | ]) 419 | .build()?; 420 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 12)); 421 | big_text.render(buf.area, &mut buf); 422 | let mut expected = Buffer::with_lines(vec![ 423 | "▀██▀▀█▄ ▀██ ", 424 | " ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ", 425 | " ██ ▀█▄ ██▀▀▀▀ ██ ██ ", 426 | "▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ", 427 | " ▄█▀▀█▄ ", 428 | "██ ▀█▄█▀█▄ ▄█▀▀█▄ ▄█▀▀█▄ ██▀▀█▄ ", 429 | "▀█▄ ▀██ ██ ▀▀ ██▀▀▀▀ ██▀▀▀▀ ██ ██ ", 430 | " ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ", 431 | "▀██▀▀█▄ ▀██ ", 432 | " ██▄▄█▀ ██ ██ ██ ▄█▀▀█▄ ", 433 | " ██ ██ ██ ██ ██ ██▀▀▀▀ ", 434 | "▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ", 435 | ]); 436 | expected.set_style(Rect::new(0, 0, 24, 4), Style::new().red()); 437 | expected.set_style(Rect::new(0, 4, 40, 4), Style::new().green()); 438 | expected.set_style(Rect::new(0, 8, 32, 4), Style::new().blue()); 439 | assert_eq!(buf, expected); 440 | Ok(()) 441 | } 442 | 443 | #[test] 444 | fn render_half_width_single_line() -> Result<()> { 445 | let big_text = BigText::builder() 446 | .pixel_size(PixelSize::HalfWidth) 447 | .lines(vec![Line::from("SingleLine")]) 448 | .build()?; 449 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8)); 450 | big_text.render(buf.area, &mut buf); 451 | let expected = Buffer::with_lines(vec![ 452 | "▐█▌ █ ▐█ ██ █ ", 453 | "█ █ █ ▐▌ ", 454 | "█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ", 455 | "▐█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ █ ", 456 | " ▐█ █ █ █ █ █ █ ███ ▐▌ ▌ █ █ █ ███ ", 457 | "█ █ █ █ █ ▐██ █ █ ▐▌▐▌ █ █ █ █ ", 458 | "▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ", 459 | " ██▌ ", 460 | ]); 461 | assert_eq!(buf, expected); 462 | Ok(()) 463 | } 464 | 465 | #[test] 466 | fn render_half_width_truncated() -> Result<()> { 467 | let big_text = BigText::builder() 468 | .pixel_size(PixelSize::HalfWidth) 469 | .lines(vec![Line::from("Truncated")]) 470 | .build()?; 471 | let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6)); 472 | big_text.render(buf.area, &mut buf); 473 | let expected = Buffer::with_lines(vec![ 474 | "███ ▐ ▐█", 475 | "▌█▐ █ █", 476 | " █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █", 477 | " █ ▐█▐▌█ █ █ █ █ █ █ █ █ █ ▐██", 478 | " █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █", 479 | " █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █", 480 | ]); 481 | assert_eq!(buf, expected); 482 | Ok(()) 483 | } 484 | 485 | #[test] 486 | fn render_half_width_multiple_lines() -> Result<()> { 487 | let big_text = BigText::builder() 488 | .pixel_size(PixelSize::HalfWidth) 489 | .lines(vec![Line::from("Multi"), Line::from("Lines")]) 490 | .build()?; 491 | let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16)); 492 | big_text.render(buf.area, &mut buf); 493 | let expected = Buffer::with_lines(vec![ 494 | "█ ▐▌ ▐█ ▐ █ ", 495 | "█▌█▌ █ █ ", 496 | "███▌█ █ █ ▐██ ▐█ ", 497 | "███▌█ █ █ █ █ ", 498 | "█▐▐▌█ █ █ █ █ ", 499 | "█ ▐▌█ █ █ █▐ █ ", 500 | "█ ▐▌▐█▐▌▐█▌ ▐▌ ▐█▌ ", 501 | " ", 502 | "██ █ ", 503 | "▐▌ ", 504 | "▐▌ ▐█ ██▌ ▐█▌ ▐██ ", 505 | "▐▌ █ █ █ █ █ █ ", 506 | "▐▌ ▌ █ █ █ ███ ▐█▌ ", 507 | "▐▌▐▌ █ █ █ █ █ ", 508 | "███▌▐█▌ █ █ ▐█▌ ██▌ ", 509 | " ", 510 | ]); 511 | assert_eq!(buf, expected); 512 | Ok(()) 513 | } 514 | 515 | #[test] 516 | fn render_half_width_widget_style() -> Result<()> { 517 | let big_text = BigText::builder() 518 | .pixel_size(PixelSize::HalfWidth) 519 | .lines(vec![Line::from("Styled")]) 520 | .style(Style::new().bold()) 521 | .build()?; 522 | let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8)); 523 | big_text.render(buf.area, &mut buf); 524 | let mut expected = Buffer::with_lines(vec![ 525 | "▐█▌ ▐ ▐█ ▐█ ", 526 | "█ █ █ █ █ ", 527 | "█▌ ▐██ █ █ █ ▐█▌ █ ", 528 | "▐█ █ █ █ █ █ █ ▐██ ", 529 | " ▐█ █ █ █ █ ███ █ █ ", 530 | "█ █ █▐ ▐██ █ █ █ █ ", 531 | "▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌", 532 | " ██▌ ", 533 | ]); 534 | expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold()); 535 | assert_eq!(buf, expected); 536 | Ok(()) 537 | } 538 | 539 | #[test] 540 | fn render_half_width_line_style() -> Result<()> { 541 | let big_text = BigText::builder() 542 | .pixel_size(PixelSize::HalfWidth) 543 | .lines(vec![ 544 | Line::from("Red".red()), 545 | Line::from("Green".green()), 546 | Line::from("Blue".blue()), 547 | ]) 548 | .build()?; 549 | let mut buf = Buffer::empty(Rect::new(0, 0, 20, 24)); 550 | big_text.render(buf.area, &mut buf); 551 | let mut expected = Buffer::with_lines(vec![ 552 | "███ ▐█ ", 553 | "▐▌▐▌ █ ", 554 | "▐▌▐▌▐█▌ █ ", 555 | "▐██ █ █ ▐██ ", 556 | "▐▌█ ███ █ █ ", 557 | "▐▌▐▌█ █ █ ", 558 | "█▌▐▌▐█▌ ▐█▐▌ ", 559 | " ", 560 | " ██ ", 561 | "▐▌▐▌ ", 562 | "█ █▐█ ▐█▌ ▐█▌ ██▌ ", 563 | "█ ▐█▐▌█ █ █ █ █ █ ", 564 | "█ █▌▐▌▐▌███ ███ █ █ ", 565 | "▐▌▐▌▐▌ █ █ █ █ ", 566 | " ██▌██ ▐█▌ ▐█▌ █ █ ", 567 | " ", 568 | "███ ▐█ ", 569 | "▐▌▐▌ █ ", 570 | "▐▌▐▌ █ █ █ ▐█▌ ", 571 | "▐██ █ █ █ █ █ ", 572 | "▐▌▐▌ █ █ █ ███ ", 573 | "▐▌▐▌ █ █ █ █ ", 574 | "███ ▐█▌ ▐█▐▌▐█▌ ", 575 | " ", 576 | ]); 577 | expected.set_style(Rect::new(0, 0, 12, 8), Style::new().red()); 578 | expected.set_style(Rect::new(0, 8, 20, 8), Style::new().green()); 579 | expected.set_style(Rect::new(0, 16, 16, 8), Style::new().blue()); 580 | assert_eq!(buf, expected); 581 | Ok(()) 582 | } 583 | 584 | #[test] 585 | fn render_quadrant_size_single_line() -> Result<()> { 586 | let big_text = BigText::builder() 587 | .pixel_size(PixelSize::Quadrant) 588 | .lines(vec![Line::from("SingleLine")]) 589 | .build()?; 590 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4)); 591 | big_text.render(buf.area, &mut buf); 592 | let expected = Buffer::with_lines(vec![ 593 | "▟▀▙ ▀ ▝█ ▜▛ ▀ ", 594 | "▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ", 595 | "▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ", 596 | "▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ", 597 | ]); 598 | assert_eq!(buf, expected); 599 | Ok(()) 600 | } 601 | 602 | #[test] 603 | fn render_quadrant_size_truncated() -> Result<()> { 604 | let big_text = BigText::builder() 605 | .pixel_size(PixelSize::Quadrant) 606 | .lines(vec![Line::from("Truncated")]) 607 | .build()?; 608 | let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3)); 609 | big_text.render(buf.area, &mut buf); 610 | let expected = Buffer::with_lines(vec![ 611 | "▛█▜ ▟ ▝█", 612 | " █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█", 613 | " █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █", 614 | ]); 615 | assert_eq!(buf, expected); 616 | Ok(()) 617 | } 618 | 619 | #[test] 620 | fn render_quadrant_size_multiple_lines() -> Result<()> { 621 | let big_text = BigText::builder() 622 | .pixel_size(PixelSize::Quadrant) 623 | .lines(vec![Line::from("Multi"), Line::from("Lines")]) 624 | .build()?; 625 | let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8)); 626 | big_text.render(buf.area, &mut buf); 627 | let expected = Buffer::with_lines(vec![ 628 | "█▖▟▌ ▝█ ▟ ▀ ", 629 | "███▌█ █ █ ▝█▀ ▝█ ", 630 | "█▝▐▌█ █ █ █▗ █ ", 631 | "▀ ▝▘▝▀▝▘▝▀▘ ▝▘ ▝▀▘ ", 632 | "▜▛ ▀ ", 633 | "▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ", 634 | "▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ", 635 | "▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ", 636 | ]); 637 | assert_eq!(buf, expected); 638 | Ok(()) 639 | } 640 | 641 | #[test] 642 | fn render_quadrant_size_widget_style() -> Result<()> { 643 | let big_text = BigText::builder() 644 | .pixel_size(PixelSize::Quadrant) 645 | .lines(vec![Line::from("Styled")]) 646 | .style(Style::new().bold()) 647 | .build()?; 648 | let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4)); 649 | big_text.render(buf.area, &mut buf); 650 | let mut expected = Buffer::with_lines(vec![ 651 | "▟▀▙ ▟ ▝█ ▝█ ", 652 | "▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ", 653 | "▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ", 654 | "▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘", 655 | ]); 656 | expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold()); 657 | assert_eq!(buf, expected); 658 | Ok(()) 659 | } 660 | 661 | #[test] 662 | fn render_quadrant_size_line_style() -> Result<()> { 663 | let big_text = BigText::builder() 664 | .pixel_size(PixelSize::Quadrant) 665 | .lines(vec![ 666 | Line::from("Red".red()), 667 | Line::from("Green".green()), 668 | Line::from("Blue".blue()), 669 | ]) 670 | .build()?; 671 | let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12)); 672 | big_text.render(buf.area, &mut buf); 673 | let mut expected = Buffer::with_lines(vec![ 674 | "▜▛▜▖ ▝█ ", 675 | "▐▙▟▘▟▀▙ ▗▄█ ", 676 | "▐▌▜▖█▀▀ █ █ ", 677 | "▀▘▝▘▝▀▘ ▝▀▝▘ ", 678 | "▗▛▜▖ ", 679 | "█ ▜▟▜▖▟▀▙ ▟▀▙ █▀▙ ", 680 | "▜▖▜▌▐▌▝▘█▀▀ █▀▀ █ █ ", 681 | " ▀▀▘▀▀ ▝▀▘ ▝▀▘ ▀ ▀ ", 682 | "▜▛▜▖▝█ ", 683 | "▐▙▟▘ █ █ █ ▟▀▙ ", 684 | "▐▌▐▌ █ █ █ █▀▀ ", 685 | "▀▀▀ ▝▀▘ ▝▀▝▘▝▀▘ ", 686 | ]); 687 | expected.set_style(Rect::new(0, 0, 12, 4), Style::new().red()); 688 | expected.set_style(Rect::new(0, 4, 20, 4), Style::new().green()); 689 | expected.set_style(Rect::new(0, 8, 16, 4), Style::new().blue()); 690 | assert_eq!(buf, expected); 691 | Ok(()) 692 | } 693 | 694 | #[test] 695 | fn render_third_height_single_line() -> Result<()> { 696 | let big_text = BigText::builder() 697 | .pixel_size(PixelSize::ThirdHeight) 698 | .lines(vec![Line::from("SingleLine")]) 699 | .build()?; 700 | let mut buf = Buffer::empty(Rect::new(0, 0, 80, 3)); 701 | big_text.render(buf.area, &mut buf); 702 | let expected = Buffer::with_lines(vec![ 703 | "🬹█🬰🬂🬎🬋 🬭🬰🬰 🬭🬭🬭🬭🬭 🬭🬭🬭 🬭🬭 🬂██ 🬭🬭🬭🬭 🬂██🬂 🬭🬰🬰 🬭🬭🬭🬭🬭 🬭🬭🬭🬭 ", 704 | "🬭🬰🬂🬎🬹🬹 ██ ██ ██ 🬎█🬭🬭██ ██ ██🬋🬋🬎🬎 ██ 🬭🬹 ██ ██ ██ ██🬋🬋🬎🬎 ", 705 | " 🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂 🬂🬂 🬋🬋🬋🬋🬎🬂 🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂🬂🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂 🬂🬂 🬂🬂🬂🬂 ", 706 | ]); 707 | assert_eq!(buf, expected); 708 | Ok(()) 709 | } 710 | 711 | #[test] 712 | fn render_third_height_truncated() -> Result<()> { 713 | let big_text = BigText::builder() 714 | .pixel_size(PixelSize::ThirdHeight) 715 | .lines(vec![Line::from("Truncated")]) 716 | .build()?; 717 | let mut buf = Buffer::empty(Rect::new(0, 0, 70, 2)); 718 | big_text.render(buf.area, &mut buf); 719 | let expected = Buffer::with_lines(vec![ 720 | "🬎🬂██🬂🬎 🬭🬭 🬭🬭🬭 🬭🬭 🬭🬭 🬭🬭🬭🬭🬭 🬭🬭🬭🬭 🬭🬭🬭🬭 🬭🬹█🬭🬭 🬭🬭🬭🬭 🬂██", 721 | " ██ ██🬂 🬎🬎 ██ ██ ██ ██ ██ 🬰🬰 🬭🬹🬋🬋██ ██ 🬭 ██🬋🬋🬎🬎 🬹█🬂🬂██", 722 | ]); 723 | assert_eq!(buf, expected); 724 | Ok(()) 725 | } 726 | 727 | #[test] 728 | fn render_third_height_multiple_lines() -> Result<()> { 729 | let big_text = BigText::builder() 730 | .pixel_size(PixelSize::ThirdHeight) 731 | .lines(vec![Line::from("Multi"), Line::from("Lines")]) 732 | .build()?; 733 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 6)); 734 | big_text.render(buf.area, &mut buf); 735 | let expected = Buffer::with_lines(vec![ 736 | "██🬹🬭🬹██ 🬭🬭 🬭🬭 🬂██ 🬭🬹█🬭🬭 🬭🬰🬰 ", 737 | "██🬂🬎🬂██ ██ ██ ██ ██ 🬭 ██ ", 738 | "🬂🬂 🬂🬂 🬂🬂🬂 🬂🬂 🬂🬂🬂🬂 🬂🬂 🬂🬂🬂🬂 ", 739 | "🬂██🬂 🬭🬰🬰 🬭🬭🬭🬭🬭 🬭🬭🬭🬭 🬭🬭🬭🬭🬭 ", 740 | " ██ 🬭🬹 ██ ██ ██ ██🬋🬋🬎🬎 🬂🬎🬋🬋🬹🬭 ", 741 | "🬂🬂🬂🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂 🬂🬂 🬂🬂🬂🬂 🬂🬂🬂🬂🬂 ", 742 | ]); 743 | assert_eq!(buf, expected); 744 | Ok(()) 745 | } 746 | 747 | #[test] 748 | fn render_third_height_widget_style() -> Result<()> { 749 | let big_text = BigText::builder() 750 | .pixel_size(PixelSize::ThirdHeight) 751 | .lines(vec![Line::from("Styled")]) 752 | .style(Style::new().bold()) 753 | .build()?; 754 | let mut buf = Buffer::empty(Rect::new(0, 0, 48, 3)); 755 | big_text.render(buf.area, &mut buf); 756 | let mut expected = Buffer::with_lines(vec![ 757 | "🬹█🬰🬂🬎🬋 🬭🬹█🬭🬭 🬭🬭 🬭🬭 🬂██ 🬭🬭🬭🬭 🬂██ ", 758 | "🬭🬰🬂🬎🬹🬹 ██ 🬭 🬎█🬭🬭██ ██ ██🬋🬋🬎🬎 🬹█🬂🬂██ ", 759 | " 🬂🬂🬂🬂 🬂🬂 🬋🬋🬋🬋🬎🬂 🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂🬂 🬂🬂 ", 760 | ]); 761 | expected.set_style(Rect::new(0, 0, 48, 3), Style::new().bold()); 762 | assert_eq!(buf, expected); 763 | Ok(()) 764 | } 765 | 766 | #[test] 767 | fn render_third_height_line_style() -> Result<()> { 768 | let big_text = BigText::builder() 769 | .pixel_size(PixelSize::ThirdHeight) 770 | .lines(vec![ 771 | Line::from("Red".red()), 772 | Line::from("Green".green()), 773 | Line::from("Blue".blue()), 774 | ]) 775 | .build()?; 776 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 9)); 777 | big_text.render(buf.area, &mut buf); 778 | let mut expected = Buffer::with_lines(vec![ 779 | "🬂██🬂🬂█🬹 🬭🬭🬭🬭 🬂██ ", 780 | " ██🬂🬎█🬭 ██🬋🬋🬎🬎 🬹█🬂🬂██ ", 781 | "🬂🬂🬂 🬂🬂 🬂🬂🬂🬂 🬂🬂🬂 🬂🬂 ", 782 | "🬭🬹🬎🬂🬂🬎🬋 🬭🬭 🬭🬭🬭 🬭🬭🬭🬭 🬭🬭🬭🬭 🬭🬭🬭🬭🬭 ", 783 | "🬎█🬭 🬋🬹🬹 ██🬂 🬎🬎 ██🬋🬋🬎🬎 ██🬋🬋🬎🬎 ██ ██ ", 784 | " 🬂🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂 🬂🬂 ", 785 | "🬂██🬂🬂█🬹 🬂██ 🬭🬭 🬭🬭 🬭🬭🬭🬭 ", 786 | " ██🬂🬂█🬹 ██ ██ ██ ██🬋🬋🬎🬎 ", 787 | "🬂🬂🬂🬂🬂🬂 🬂🬂🬂🬂 🬂🬂🬂 🬂🬂 🬂🬂🬂🬂 ", 788 | ]); 789 | expected.set_style(Rect::new(0, 0, 24, 3), Style::new().red()); 790 | expected.set_style(Rect::new(0, 3, 40, 3), Style::new().green()); 791 | expected.set_style(Rect::new(0, 6, 32, 3), Style::new().blue()); 792 | assert_eq!(buf, expected); 793 | Ok(()) 794 | } 795 | 796 | #[test] 797 | fn render_sextant_size_single_line() -> Result<()> { 798 | let big_text = BigText::builder() 799 | .pixel_size(PixelSize::Sextant) 800 | .lines(vec![Line::from("SingleLine")]) 801 | .build()?; 802 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 3)); 803 | big_text.render(buf.area, &mut buf); 804 | let expected = Buffer::with_lines(vec![ 805 | "🬻🬒🬌 🬞🬰 🬭🬭🬏 🬞🬭🬞🬏🬁█ 🬞🬭🬏 🬨🬕 🬞🬰 🬭🬭🬏 🬞🬭🬏 ", 806 | "🬯🬊🬹 █ █ █ 🬬🬭█ █ █🬋🬎 ▐▌🬞🬓 █ █ █ █🬋🬎 ", 807 | "🬁🬂🬀 🬁🬂🬀 🬂 🬂 🬋🬋🬆 🬁🬂🬀 🬁🬂🬀 🬂🬂🬂🬀🬁🬂🬀 🬂 🬂 🬁🬂🬀 ", 808 | ]); 809 | assert_eq!(buf, expected); 810 | Ok(()) 811 | } 812 | 813 | #[test] 814 | fn render_sextant_size_truncated() -> Result<()> { 815 | let big_text = BigText::builder() 816 | .pixel_size(PixelSize::Sextant) 817 | .lines(vec![Line::from("Truncated")]) 818 | .build()?; 819 | let mut buf = Buffer::empty(Rect::new(0, 0, 35, 2)); 820 | big_text.render(buf.area, &mut buf); 821 | let expected = Buffer::with_lines(vec![ 822 | "🬆█🬊 🬭🬞🬭 🬭 🬭 🬭🬭🬏 🬞🬭🬏 🬞🬭🬏 🬞🬻🬭 🬞🬭🬏 🬁█", 823 | " █ ▐🬕🬉🬄█ █ █ █ █ 🬰 🬵🬋█ █🬞 █🬋🬎 🬻🬂█", 824 | ]); 825 | assert_eq!(buf, expected); 826 | Ok(()) 827 | } 828 | 829 | #[test] 830 | fn render_sextant_size_multiple_lines() -> Result<()> { 831 | let big_text = BigText::builder() 832 | .pixel_size(PixelSize::Sextant) 833 | .lines(vec![Line::from("Multi"), Line::from("Lines")]) 834 | .build()?; 835 | let mut buf = Buffer::empty(Rect::new(0, 0, 20, 6)); 836 | big_text.render(buf.area, &mut buf); 837 | let expected = Buffer::with_lines(vec![ 838 | "█🬱🬻▌🬭 🬭 🬁█ 🬞🬻🬭 🬞🬰 ", 839 | "█🬊🬨▌█ █ █ █🬞 █ ", 840 | "🬂 🬁🬀🬁🬂🬁🬀🬁🬂🬀 🬁🬀 🬁🬂🬀 ", 841 | "🬨🬕 🬞🬰 🬭🬭🬏 🬞🬭🬏 🬞🬭🬭 ", 842 | "▐▌🬞🬓 █ █ █ █🬋🬎 🬊🬋🬱 ", 843 | "🬂🬂🬂🬀🬁🬂🬀 🬂 🬂 🬁🬂🬀 🬂🬂🬀 ", 844 | ]); 845 | assert_eq!(buf, expected); 846 | Ok(()) 847 | } 848 | 849 | #[test] 850 | fn render_sextant_size_widget_style() -> Result<()> { 851 | let big_text = BigText::builder() 852 | .pixel_size(PixelSize::Sextant) 853 | .lines(vec![Line::from("Styled")]) 854 | .style(Style::new().bold()) 855 | .build()?; 856 | let mut buf = Buffer::empty(Rect::new(0, 0, 24, 3)); 857 | big_text.render(buf.area, &mut buf); 858 | let mut expected = Buffer::with_lines(vec![ 859 | "🬻🬒🬌 🬞🬻🬭 🬭 🬭 🬁█ 🬞🬭🬏 🬁█ ", 860 | "🬯🬊🬹 █🬞 🬬🬭█ █ █🬋🬎 🬻🬂█ ", 861 | "🬁🬂🬀 🬁🬀 🬋🬋🬆 🬁🬂🬀 🬁🬂🬀 🬁🬂🬁🬀", 862 | ]); 863 | expected.set_style(Rect::new(0, 0, 24, 3), Style::new().bold()); 864 | assert_eq!(buf, expected); 865 | Ok(()) 866 | } 867 | 868 | #[test] 869 | fn render_sextant_size_line_style() -> Result<()> { 870 | let big_text = BigText::builder() 871 | .pixel_size(PixelSize::Sextant) 872 | .lines(vec![ 873 | Line::from("Red".red()), 874 | Line::from("Green".green()), 875 | Line::from("Blue".blue()), 876 | ]) 877 | .build()?; 878 | let mut buf = Buffer::empty(Rect::new(0, 0, 20, 9)); 879 | big_text.render(buf.area, &mut buf); 880 | let mut expected = Buffer::with_lines(vec![ 881 | "🬨🬕🬨🬓🬞🬭🬏 🬁█ ", 882 | "▐🬕🬬🬏█🬋🬎 🬻🬂█ ", 883 | "🬂🬀🬁🬀🬁🬂🬀 🬁🬂🬁🬀 ", 884 | "🬵🬆🬊🬃🬭🬞🬭 🬞🬭🬏 🬞🬭🬏 🬭🬭🬏 ", 885 | "🬬🬏🬩🬓▐🬕🬉🬄█🬋🬎 █🬋🬎 █ █ ", 886 | " 🬂🬂🬀🬂🬂 🬁🬂🬀 🬁🬂🬀 🬂 🬂 ", 887 | "🬨🬕🬨🬓🬁█ 🬭 🬭 🬞🬭🬏 ", 888 | "▐🬕🬨🬓 █ █ █ █🬋🬎 ", 889 | "🬂🬂🬂 🬁🬂🬀 🬁🬂🬁🬀🬁🬂🬀 ", 890 | ]); 891 | expected.set_style(Rect::new(0, 0, 12, 3), Style::new().red()); 892 | expected.set_style(Rect::new(0, 3, 20, 3), Style::new().green()); 893 | expected.set_style(Rect::new(0, 6, 16, 3), Style::new().blue()); 894 | assert_eq!(buf, expected); 895 | Ok(()) 896 | } 897 | 898 | #[test] 899 | fn render_alignment_left() -> Result<()> { 900 | let big_text = BigText::builder() 901 | .pixel_size(PixelSize::Quadrant) 902 | .lines(vec![Line::from("Left")]) 903 | .alignment(Alignment::Left) 904 | .build()?; 905 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4)); 906 | big_text.render(buf.area, &mut buf); 907 | let expected = Buffer::with_lines(vec![ 908 | "▜▛ ▗▛▙ ▟ ", 909 | "▐▌ ▟▀▙ ▟▙ ▝█▀ ", 910 | "▐▌▗▌█▀▀ ▐▌ █▗ ", 911 | "▀▀▀▘▝▀▘ ▀▀ ▝▘ ", 912 | ]); 913 | assert_eq!(buf, expected); 914 | Ok(()) 915 | } 916 | 917 | #[test] 918 | fn render_alignment_right() -> Result<()> { 919 | let big_text = BigText::builder() 920 | .pixel_size(PixelSize::Quadrant) 921 | .lines(vec![Line::from("Right")]) 922 | .alignment(Alignment::Right) 923 | .build()?; 924 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4)); 925 | big_text.render(buf.area, &mut buf); 926 | let expected = Buffer::with_lines(vec![ 927 | " ▜▛▜▖ ▀ ▜▌ ▟ ", 928 | " ▐▙▟▘▝█ ▟▀▟▘▐▙▜▖▝█▀ ", 929 | " ▐▌▜▖ █ ▜▄█ ▐▌▐▌ █▗ ", 930 | " ▀▘▝▘▝▀▘ ▄▄▛ ▀▘▝▘ ▝▘ ", 931 | ]); 932 | assert_eq!(buf, expected); 933 | Ok(()) 934 | } 935 | 936 | #[test] 937 | fn render_alignment_center() -> Result<()> { 938 | let big_text = BigText::builder() 939 | .pixel_size(PixelSize::Quadrant) 940 | .lines(vec![Line::from("Centered"), Line::from("Lines")]) 941 | .alignment(Alignment::Center) 942 | .build()?; 943 | let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8)); 944 | big_text.render(buf.area, &mut buf); 945 | let expected = Buffer::with_lines(vec![ 946 | " ▗▛▜▖ ▟ ▝█ ", 947 | " █ ▟▀▙ █▀▙ ▝█▀ ▟▀▙ ▜▟▜▖▟▀▙ ▗▄█ ", 948 | " ▜▖▗▖█▀▀ █ █ █▗ █▀▀ ▐▌▝▘█▀▀ █ █ ", 949 | " ▀▀ ▝▀▘ ▀ ▀ ▝▘ ▝▀▘ ▀▀ ▝▀▘ ▝▀▝▘ ", 950 | " ▜▛ ▀ ", 951 | " ▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ", 952 | " ▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ", 953 | " ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ", 954 | ]); 955 | assert_eq!(buf, expected); 956 | Ok(()) 957 | } 958 | } 959 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the 2 | //! glyphs from the [font8x8] crate. 3 | //! 4 | //! ![Demo](https://vhs.charm.sh/vhs-35FZxQa32pCZdRW7pmpqf6.gif) 5 | //! 6 | //! [![Crate badge]][tui-big-text] 7 | //! [![Docs.rs Badge]][API Docs] 8 | //! [![Deps.rs Badge]][Dependency Status]
9 | //! [![License Badge]](./LICENSE-MIT) 10 | //! [![Codecov.io Badge]][Code Coverage] 11 | //! [![Discord Badge]][Ratatui Discord] 12 | //! 13 | //! [GitHub Repository] · [API Docs] · [Examples] · [Changelog] · [Contributing] 14 | //! 15 | //! # Installation 16 | //! 17 | //! ```shell 18 | //! cargo add ratatui tui-big-text 19 | //! ``` 20 | //! 21 | //! # Usage 22 | //! 23 | //! Create a [`BigText`] widget using [`BigText::builder`] and pass it to [`render_widget`] to 24 | //! render be rendered. The builder allows you to customize the [`Style`] of the widget and the 25 | //! [`PixelSize`] of the glyphs. 26 | //! 27 | //! # Examples 28 | //! 29 | //! ```rust 30 | //! use anyhow::Result; 31 | //! use ratatui::prelude::*; 32 | //! use tui_big_text::{BigText, PixelSize}; 33 | //! 34 | //! fn render(frame: &mut Frame) -> Result<()> { 35 | //! let big_text = BigText::builder() 36 | //! .pixel_size(PixelSize::Full) 37 | //! .style(Style::new().blue()) 38 | //! .lines(vec![ 39 | //! "Hello".red().into(), 40 | //! "World".white().into(), 41 | //! "~~~~~".into(), 42 | //! ]) 43 | //! .build()?; 44 | //! frame.render_widget(big_text, frame.size()); 45 | //! Ok(()) 46 | //! } 47 | //! ``` 48 | //! 49 | //! The [`PixelSize`] can be used to control how many character cells are used to represent a single 50 | //! pixel of the 8x8 font. It has six variants: 51 | //! 52 | //! - `Full` (default) - Each pixel is represented by a single character cell. 53 | //! - `HalfHeight` - Each pixel is represented by half the height of a character cell. 54 | //! - `HalfWidth` - Each pixel is represented by half the width of a character cell. 55 | //! - `Quadrant` - Each pixel is represented by a quarter of a character cell. 56 | //! - `ThirdHeight` - Each pixel is represented by a third of the height of a character cell. 57 | //! - `Sextant` - Each pixel is represented by a sixth of a character cell. 58 | //! 59 | //! ```rust 60 | //! # use tui_big_text::*; 61 | //! BigText::builder().pixel_size(PixelSize::Full); 62 | //! BigText::builder().pixel_size(PixelSize::HalfHeight); 63 | //! BigText::builder().pixel_size(PixelSize::Quadrant); 64 | //! ``` 65 | //! 66 | //! ![Pixel Size](https://vhs.charm.sh/vhs-2nLycKO16vHzqg3TxDNvq4.gif) 67 | //! 68 | //! Text can be aligned to the Left / Right / Center using the `alignment` method. 69 | //! 70 | //! ```rust 71 | //! use ratatui::layout::Alignment; 72 | //! # use tui_big_text::*; 73 | //! BigText::builder().alignment(Alignment::Left); 74 | //! BigText::builder().alignment(Alignment::Right); 75 | //! BigText::builder().alignment(Alignment::Center); 76 | //! ``` 77 | //! 78 | //! ![Alignment Example](https://vhs.charm.sh/vhs-1Yyr7BJ5vfmOmjYNywCNH3.gif) 79 | //! 80 | //! [tui-big-text]: https://crates.io/crates/tui-big-text 81 | //! [Ratatui]: https://crates.io/crates/ratatui 82 | //! [font8x8]: https://crates.io/crates/font8x8 83 | //! 84 | //! 85 | //! [`BigText`]: crate::big_text::BigText 86 | //! [`BigText::builder`]: crate::big_text::BigText#method.builder 87 | //! [`PixelSize`]: crate::pixel_size::PixelSize 88 | //! [`render_widget`]: https://docs.rs/ratatui/latest/ratatui/struct.Frame.html#method.render_widget 89 | //! [`Style`]: https://docs.rs/ratatui/latest/ratatui/style/struct.Style.html 90 | //! 91 | //! [Crate badge]: https://img.shields.io/crates/v/tui-big-text?logo=rust&style=for-the-badge 92 | //! [Docs.rs Badge]: https://img.shields.io/docsrs/tui-big-text?logo=rust&style=for-the-badge 93 | //! [Deps.rs Badge]: https://deps.rs/repo/github/joshka/tui-big-text/status.svg?style=for-the-badge 94 | //! [License Badge]: https://img.shields.io/crates/l/tui-big-text?style=for-the-badge 95 | //! [Codecov.io Badge]: https://img.shields.io/codecov/c/github/joshka/tui-big-text?logo=codecov&style=for-the-badge&token=BAQ8SOKEST 96 | //! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=ratatui+discord&logo=discord&style=for-the-badge 97 | //! 98 | //! [API Docs]: https://docs.rs/crate/tui-big-text/ 99 | //! [Dependency Status]: https://deps.rs/repo/github/joshka/tui-big-text 100 | //! [Code Coverage]: https://app.codecov.io/gh/joshka/tui-big-text 101 | //! [Ratatui Discord]: https://discord.gg/pMCEU9hNEj 102 | //! 103 | //! [GitHub Repository]: https://github.com/joshka/tui-big-text 104 | //! [Examples]: https://github.com/joshka/tui-big-text/tree/main/examples 105 | //! [Changelog]: https://github.com/joshka/tui-big-text/blob/main/CHANGELOG.md 106 | //! [Contributing]: https://github.com/joshka/tui-big-text/blob/main/CONTRIBUTING.md 107 | 108 | mod big_text; 109 | mod pixel_size; 110 | 111 | pub use big_text::{BigText, BigTextBuilder}; 112 | pub use pixel_size::PixelSize; 113 | -------------------------------------------------------------------------------- /src/pixel_size.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] 2 | pub enum PixelSize { 3 | #[default] 4 | /// A pixel from the 8x8 font is represented by a full character cell in the terminal. 5 | Full, 6 | /// A pixel from the 8x8 font is represented by a half (upper/lower) character cell in the 7 | /// terminal. 8 | HalfHeight, 9 | /// A pixel from the 8x8 font is represented by a half (left/right) character cell in the 10 | /// terminal. 11 | HalfWidth, 12 | /// A pixel from the 8x8 font is represented by a quadrant of a character cell in the 13 | /// terminal. 14 | Quadrant, 15 | /// A pixel from the 8x8 font is represented by a third (top/middle/bottom) of a character 16 | /// cell in the terminal. 17 | /// *Note: depending on how the used terminal renders characters, the generated text with 18 | /// this PixelSize might look very strange.* 19 | ThirdHeight, 20 | /// A pixel from the 8x8 font is represented by a sextant of a character cell in the 21 | /// terminal. 22 | /// *Note: depending on how the used terminal renders characters, the generated text with 23 | /// this PixelSize might look very strange.* 24 | Sextant, 25 | } 26 | 27 | impl PixelSize { 28 | /// The number of pixels that can be displayed in a single character cell for the given 29 | /// pixel size. 30 | /// 31 | /// The first value is the number of pixels in the horizontal direction, the second value is 32 | /// the number of pixels in the vertical direction. 33 | pub(crate) fn pixels_per_cell(self) -> (u16, u16) { 34 | match self { 35 | PixelSize::Full => (1, 1), 36 | PixelSize::HalfHeight => (1, 2), 37 | PixelSize::HalfWidth => (2, 1), 38 | PixelSize::Quadrant => (2, 2), 39 | PixelSize::ThirdHeight => (1, 3), 40 | PixelSize::Sextant => (2, 3), 41 | } 42 | } 43 | 44 | /// Get a symbol/char that represents the pixels at the given position with the given pixel size 45 | pub(crate) fn symbol_for_position(self, glyph: &[u8; 8], row: usize, col: i32) -> char { 46 | match self { 47 | PixelSize::Full => match glyph[row] & (1 << col) { 48 | 0 => ' ', 49 | _ => '█', 50 | }, 51 | PixelSize::HalfHeight => { 52 | let top = glyph[row] & (1 << col); 53 | let bottom = glyph[row + 1] & (1 << col); 54 | get_symbol_half_height(top, bottom) 55 | } 56 | PixelSize::HalfWidth => { 57 | let left = glyph[row] & (1 << col); 58 | let right = glyph[row] & (1 << (col + 1)); 59 | get_symbol_half_width(left, right) 60 | } 61 | PixelSize::Quadrant => { 62 | let top_left = glyph[row] & (1 << col); 63 | let top_right = glyph[row] & (1 << (col + 1)); 64 | let bottom_left = glyph[row + 1] & (1 << col); 65 | let bottom_right = glyph[row + 1] & (1 << (col + 1)); 66 | get_symbol_quadrant_size(top_left, top_right, bottom_left, bottom_right) 67 | } 68 | PixelSize::ThirdHeight => { 69 | let top = glyph[row] & (1 << col); 70 | let is_middle_available = (row + 1) < glyph.len(); 71 | let middle = if is_middle_available { 72 | glyph[row + 1] & (1 << col) 73 | } else { 74 | 0 75 | }; 76 | let is_bottom_available = (row + 2) < glyph.len(); 77 | let bottom = if is_bottom_available { 78 | glyph[row + 2] & (1 << col) 79 | } else { 80 | 0 81 | }; 82 | get_symbol_third_height(top, middle, bottom) 83 | } 84 | PixelSize::Sextant => { 85 | let top_left = glyph[row] & (1 << col); 86 | let top_right = glyph[row] & (1 << (col + 1)); 87 | let is_middle_available = (row + 1) < glyph.len(); 88 | let (middle_left, middle_right) = if is_middle_available { 89 | ( 90 | glyph[row + 1] & (1 << col), 91 | glyph[row + 1] & (1 << (col + 1)), 92 | ) 93 | } else { 94 | (0, 0) 95 | }; 96 | let is_bottom_available = (row + 2) < glyph.len(); 97 | let (bottom_left, bottom_right) = if is_bottom_available { 98 | ( 99 | glyph[row + 2] & (1 << col), 100 | glyph[row + 2] & (1 << (col + 1)), 101 | ) 102 | } else { 103 | (0, 0) 104 | }; 105 | get_symbol_sextant_size( 106 | top_left, 107 | top_right, 108 | middle_left, 109 | middle_right, 110 | bottom_left, 111 | bottom_right, 112 | ) 113 | } 114 | } 115 | } 116 | } 117 | 118 | /// Get the correct unicode symbol for two vertical "pixels" 119 | fn get_symbol_half_height(top: u8, bottom: u8) -> char { 120 | match top { 121 | 0 => match bottom { 122 | 0 => ' ', 123 | _ => '▄', 124 | }, 125 | _ => match bottom { 126 | 0 => '▀', 127 | _ => '█', 128 | }, 129 | } 130 | } 131 | 132 | /// Get the correct unicode symbol for two horizontal "pixels" 133 | fn get_symbol_half_width(left: u8, right: u8) -> char { 134 | match left { 135 | 0 => match right { 136 | 0 => ' ', 137 | _ => '▐', 138 | }, 139 | _ => match right { 140 | 0 => '▌', 141 | _ => '█', 142 | }, 143 | } 144 | } 145 | 146 | /// Get the correct unicode symbol for 2x2 "pixels" 147 | fn get_symbol_quadrant_size( 148 | top_left: u8, 149 | top_right: u8, 150 | bottom_left: u8, 151 | bottom_right: u8, 152 | ) -> char { 153 | let top_left = if top_left > 0 { 1 } else { 0 }; 154 | let top_right = if top_right > 0 { 1 } else { 0 }; 155 | let bottom_left = if bottom_left > 0 { 1 } else { 0 }; 156 | let bottom_right = if bottom_right > 0 { 1 } else { 0 }; 157 | 158 | // We use an array here instead of directlu indexing into the unicode symbols, because although 159 | // most symbols are in order in unicode, some of them are already part of another character set 160 | // and missing in this character set. 161 | const QUADRANT_SYMBOLS: [char; 16] = [ 162 | ' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█', 163 | ]; 164 | let character_index = top_left + (top_right << 1) + (bottom_left << 2) + (bottom_right << 3); 165 | 166 | QUADRANT_SYMBOLS[character_index] 167 | } 168 | 169 | /// Get the correct unicode symbol for 1x3 "pixels" 170 | fn get_symbol_third_height(top: u8, middle: u8, bottom: u8) -> char { 171 | get_symbol_sextant_size(top, top, middle, middle, bottom, bottom) 172 | } 173 | 174 | /// Get the correct unicode symbol for 2x3 "pixels" 175 | fn get_symbol_sextant_size( 176 | top_left: u8, 177 | top_right: u8, 178 | middle_left: u8, 179 | middle_right: u8, 180 | bottom_left: u8, 181 | bottom_right: u8, 182 | ) -> char { 183 | let top_left = if top_left > 0 { 1 } else { 0 }; 184 | let top_right = if top_right > 0 { 1 } else { 0 }; 185 | let middle_left = if middle_left > 0 { 1 } else { 0 }; 186 | let middle_right = if middle_right > 0 { 1 } else { 0 }; 187 | let bottom_left = if bottom_left > 0 { 1 } else { 0 }; 188 | let bottom_right = if bottom_right > 0 { 1 } else { 0 }; 189 | 190 | // We use an array here instead of directlu indexing into the unicode symbols, because although 191 | // most symbols are in order in unicode, some of them are already part of another character set 192 | // and missing in this character set. 193 | const SEXANT_SYMBOLS: [char; 64] = [ 194 | ' ', '🬀', '🬁', '🬂', '🬃', '🬄', '🬅', '🬆', '🬇', '🬈', '🬉', '🬊', '🬋', '🬌', '🬍', '🬎', '🬏', '🬐', 195 | '🬑', '🬒', '🬓', '▌', '🬔', '🬕', '🬖', '🬗', '🬘', '🬙', '🬚', '🬛', '🬜', '🬝', '🬞', '🬟', '🬠', '🬡', 196 | '🬢', '🬣', '🬤', '🬥', '🬦', '🬧', '▐', '🬨', '🬩', '🬪', '🬫', '🬬', '🬭', '🬮', '🬯', '🬰', '🬱', '🬲', 197 | '🬳', '🬴', '🬵', '🬶', '🬷', '🬸', '🬹', '🬺', '🬻', '█', 198 | ]; 199 | let character_index = top_left 200 | + (top_right << 1) 201 | + (middle_left << 2) 202 | + (middle_right << 3) 203 | + (bottom_left << 4) 204 | + (bottom_right << 5); 205 | 206 | SEXANT_SYMBOLS[character_index] 207 | } 208 | 209 | #[cfg(test)] 210 | mod tests { 211 | use super::*; 212 | 213 | type Result = std::result::Result>; 214 | 215 | #[test] 216 | fn check_quadrant_size_symbols() -> Result<()> { 217 | assert_eq!(get_symbol_quadrant_size(0, 0, 0, 0), ' '); 218 | assert_eq!(get_symbol_quadrant_size(1, 0, 0, 0), '▘'); 219 | assert_eq!(get_symbol_quadrant_size(0, 1, 0, 0), '▝'); 220 | assert_eq!(get_symbol_quadrant_size(1, 1, 0, 0), '▀'); 221 | assert_eq!(get_symbol_quadrant_size(0, 0, 1, 0), '▖'); 222 | assert_eq!(get_symbol_quadrant_size(1, 0, 1, 0), '▌'); 223 | assert_eq!(get_symbol_quadrant_size(0, 1, 1, 0), '▞'); 224 | assert_eq!(get_symbol_quadrant_size(1, 1, 1, 0), '▛'); 225 | assert_eq!(get_symbol_quadrant_size(0, 0, 0, 1), '▗'); 226 | assert_eq!(get_symbol_quadrant_size(1, 0, 0, 1), '▚'); 227 | assert_eq!(get_symbol_quadrant_size(0, 1, 0, 1), '▐'); 228 | assert_eq!(get_symbol_quadrant_size(1, 1, 0, 1), '▜'); 229 | assert_eq!(get_symbol_quadrant_size(0, 0, 1, 1), '▄'); 230 | assert_eq!(get_symbol_quadrant_size(1, 0, 1, 1), '▙'); 231 | assert_eq!(get_symbol_quadrant_size(0, 1, 1, 1), '▟'); 232 | assert_eq!(get_symbol_quadrant_size(1, 1, 1, 1), '█'); 233 | Ok(()) 234 | } 235 | 236 | #[test] 237 | fn check_sextant_size_symbols() -> Result<()> { 238 | assert_eq!(get_symbol_sextant_size(0, 0, 0, 0, 0, 0), ' '); 239 | assert_eq!(get_symbol_sextant_size(1, 0, 0, 0, 0, 0), '🬀'); 240 | assert_eq!(get_symbol_sextant_size(0, 1, 0, 0, 0, 0), '🬁'); 241 | assert_eq!(get_symbol_sextant_size(1, 1, 0, 0, 0, 0), '🬂'); 242 | assert_eq!(get_symbol_sextant_size(0, 0, 1, 0, 0, 0), '🬃'); 243 | assert_eq!(get_symbol_sextant_size(1, 0, 1, 0, 0, 0), '🬄'); 244 | assert_eq!(get_symbol_sextant_size(0, 1, 1, 0, 0, 0), '🬅'); 245 | assert_eq!(get_symbol_sextant_size(1, 1, 1, 0, 0, 0), '🬆'); 246 | assert_eq!(get_symbol_sextant_size(0, 0, 0, 1, 0, 0), '🬇'); 247 | assert_eq!(get_symbol_sextant_size(1, 0, 0, 1, 0, 0), '🬈'); 248 | assert_eq!(get_symbol_sextant_size(0, 1, 0, 1, 0, 0), '🬉'); 249 | assert_eq!(get_symbol_sextant_size(1, 1, 0, 1, 0, 0), '🬊'); 250 | assert_eq!(get_symbol_sextant_size(0, 0, 1, 1, 0, 0), '🬋'); 251 | assert_eq!(get_symbol_sextant_size(1, 0, 1, 1, 0, 0), '🬌'); 252 | assert_eq!(get_symbol_sextant_size(0, 1, 1, 1, 0, 0), '🬍'); 253 | assert_eq!(get_symbol_sextant_size(1, 1, 1, 1, 0, 0), '🬎'); 254 | assert_eq!(get_symbol_sextant_size(0, 0, 0, 0, 1, 0), '🬏'); 255 | assert_eq!(get_symbol_sextant_size(1, 0, 0, 0, 1, 0), '🬐'); 256 | assert_eq!(get_symbol_sextant_size(0, 1, 0, 0, 1, 0), '🬑'); 257 | assert_eq!(get_symbol_sextant_size(1, 1, 0, 0, 1, 0), '🬒'); 258 | assert_eq!(get_symbol_sextant_size(0, 0, 1, 0, 1, 0), '🬓'); 259 | assert_eq!(get_symbol_sextant_size(1, 0, 1, 0, 1, 0), '▌'); 260 | assert_eq!(get_symbol_sextant_size(0, 1, 1, 0, 1, 0), '🬔'); 261 | assert_eq!(get_symbol_sextant_size(1, 1, 1, 0, 1, 0), '🬕'); 262 | assert_eq!(get_symbol_sextant_size(0, 0, 0, 1, 1, 0), '🬖'); 263 | assert_eq!(get_symbol_sextant_size(1, 0, 0, 1, 1, 0), '🬗'); 264 | assert_eq!(get_symbol_sextant_size(0, 1, 0, 1, 1, 0), '🬘'); 265 | assert_eq!(get_symbol_sextant_size(1, 1, 0, 1, 1, 0), '🬙'); 266 | assert_eq!(get_symbol_sextant_size(0, 0, 1, 1, 1, 0), '🬚'); 267 | assert_eq!(get_symbol_sextant_size(1, 0, 1, 1, 1, 0), '🬛'); 268 | assert_eq!(get_symbol_sextant_size(0, 1, 1, 1, 1, 0), '🬜'); 269 | assert_eq!(get_symbol_sextant_size(1, 1, 1, 1, 1, 0), '🬝'); 270 | assert_eq!(get_symbol_sextant_size(0, 0, 0, 0, 0, 1), '🬞'); 271 | assert_eq!(get_symbol_sextant_size(1, 0, 0, 0, 0, 1), '🬟'); 272 | assert_eq!(get_symbol_sextant_size(0, 1, 0, 0, 0, 1), '🬠'); 273 | assert_eq!(get_symbol_sextant_size(1, 1, 0, 0, 0, 1), '🬡'); 274 | assert_eq!(get_symbol_sextant_size(0, 0, 1, 0, 0, 1), '🬢'); 275 | assert_eq!(get_symbol_sextant_size(1, 0, 1, 0, 0, 1), '🬣'); 276 | assert_eq!(get_symbol_sextant_size(0, 1, 1, 0, 0, 1), '🬤'); 277 | assert_eq!(get_symbol_sextant_size(1, 1, 1, 0, 0, 1), '🬥'); 278 | assert_eq!(get_symbol_sextant_size(0, 0, 0, 1, 0, 1), '🬦'); 279 | assert_eq!(get_symbol_sextant_size(1, 0, 0, 1, 0, 1), '🬧'); 280 | assert_eq!(get_symbol_sextant_size(0, 1, 0, 1, 0, 1), '▐'); 281 | assert_eq!(get_symbol_sextant_size(1, 1, 0, 1, 0, 1), '🬨'); 282 | assert_eq!(get_symbol_sextant_size(0, 0, 1, 1, 0, 1), '🬩'); 283 | assert_eq!(get_symbol_sextant_size(1, 0, 1, 1, 0, 1), '🬪'); 284 | assert_eq!(get_symbol_sextant_size(0, 1, 1, 1, 0, 1), '🬫'); 285 | assert_eq!(get_symbol_sextant_size(1, 1, 1, 1, 0, 1), '🬬'); 286 | assert_eq!(get_symbol_sextant_size(0, 0, 0, 0, 1, 1), '🬭'); 287 | assert_eq!(get_symbol_sextant_size(1, 0, 0, 0, 1, 1), '🬮'); 288 | assert_eq!(get_symbol_sextant_size(0, 1, 0, 0, 1, 1), '🬯'); 289 | assert_eq!(get_symbol_sextant_size(1, 1, 0, 0, 1, 1), '🬰'); 290 | assert_eq!(get_symbol_sextant_size(0, 0, 1, 0, 1, 1), '🬱'); 291 | assert_eq!(get_symbol_sextant_size(1, 0, 1, 0, 1, 1), '🬲'); 292 | assert_eq!(get_symbol_sextant_size(0, 1, 1, 0, 1, 1), '🬳'); 293 | assert_eq!(get_symbol_sextant_size(1, 1, 1, 0, 1, 1), '🬴'); 294 | assert_eq!(get_symbol_sextant_size(0, 0, 0, 1, 1, 1), '🬵'); 295 | assert_eq!(get_symbol_sextant_size(1, 0, 0, 1, 1, 1), '🬶'); 296 | assert_eq!(get_symbol_sextant_size(0, 1, 0, 1, 1, 1), '🬷'); 297 | assert_eq!(get_symbol_sextant_size(1, 1, 0, 1, 1, 1), '🬸'); 298 | assert_eq!(get_symbol_sextant_size(0, 0, 1, 1, 1, 1), '🬹'); 299 | assert_eq!(get_symbol_sextant_size(1, 0, 1, 1, 1, 1), '🬺'); 300 | assert_eq!(get_symbol_sextant_size(0, 1, 1, 1, 1, 1), '🬻'); 301 | assert_eq!(get_symbol_sextant_size(1, 1, 1, 1, 1, 1), '█'); 302 | Ok(()) 303 | } 304 | 305 | #[test] 306 | fn check_third_height_symbols() -> Result<()> { 307 | assert_eq!(get_symbol_third_height(0, 0, 0), ' '); 308 | assert_eq!(get_symbol_third_height(1, 0, 0), '🬂'); 309 | assert_eq!(get_symbol_third_height(0, 1, 0), '🬋'); 310 | assert_eq!(get_symbol_third_height(1, 1, 0), '🬎'); 311 | assert_eq!(get_symbol_third_height(0, 0, 1), '🬭'); 312 | assert_eq!(get_symbol_third_height(1, 0, 1), '🬰'); 313 | assert_eq!(get_symbol_third_height(0, 1, 1), '🬹'); 314 | assert_eq!(get_symbol_third_height(1, 1, 1), '█'); 315 | Ok(()) 316 | } 317 | 318 | #[test] 319 | fn check_get_symbol_for_position_in_glyph_third_height_defensive_middle() -> Result<()> { 320 | // In this test, we set all pixels of the glyph to 1 (all bytes are u8-max) 321 | // We expect that pixels out of the glyph-bounds are not set 322 | // Returned character is upper third filled only 323 | 324 | let glyph = [0xFFu8; 8]; 325 | assert_eq!( 326 | PixelSize::ThirdHeight.symbol_for_position(&glyph, 7, 0), 327 | '🬂' 328 | ); 329 | Ok(()) 330 | } 331 | 332 | #[test] 333 | fn check_get_symbol_for_position_in_glyph_sextant_size_defensive_middle() -> Result<()> { 334 | // In this test, we set all pixels of the glyph to 1 (all bytes are u8-max) 335 | // We expect that pixels out of the glyph-bounds are not set 336 | // Returned character is upper third filled only 337 | 338 | let glyph = [0xFFu8; 8]; 339 | assert_eq!(PixelSize::Sextant.symbol_for_position(&glyph, 7, 0), '🬂'); 340 | Ok(()) 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | # Don't correct the surname "Teh" 3 | ratatui = "ratatui" --------------------------------------------------------------------------------