├── .gitattributes ├── .github └── workflows │ ├── pages.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── layout.rs ├── ci.sh ├── deny.toml ├── editor-test.sh ├── editor.sh ├── examples ├── editor-test │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── editor │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── multiview │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── rich-text │ ├── Cargo.toml │ └── src │ │ └── main.rs └── terminal │ ├── Cargo.toml │ └── src │ └── main.rs ├── fonts ├── FiraMono-LICENSE ├── FiraMono-Medium.ttf ├── Inter-LICENSE ├── Inter-Regular.ttf ├── NotoSans-LICENSE ├── NotoSans-Regular.ttf ├── NotoSansArabic.ttf └── NotoSansHebrew.ttf ├── multiview.sh ├── redoxer.sh ├── rich-text.sh ├── sample ├── arabic.txt ├── bengali.txt ├── crlf.txt ├── em.txt ├── emoji-zjw.txt ├── emoji.txt ├── fallback.txt ├── farsi.txt ├── han.txt ├── hebrew.txt ├── hello.txt ├── ligature.txt ├── lioneatingpoet.txt ├── mono.txt ├── proportional.txt ├── tabs.txt └── thai.txt ├── screenshots ├── arabic.png ├── chinese-simplified.png └── hindi.png ├── src ├── attrs.rs ├── bidi_para.rs ├── buffer.rs ├── buffer_line.rs ├── cached.rs ├── cursor.rs ├── edit │ ├── editor.rs │ ├── mod.rs │ ├── syntect.rs │ └── vi.rs ├── font │ ├── fallback │ │ ├── macos.rs │ │ ├── mod.rs │ │ ├── other.rs │ │ ├── unix.rs │ │ └── windows.rs │ ├── mod.rs │ └── system.rs ├── glyph_cache.rs ├── layout.rs ├── lib.rs ├── line_ending.rs ├── math.rs ├── shape.rs ├── shape_run_cache.rs └── swash.rs ├── terminal.sh ├── test.sh └── tests ├── common └── mod.rs ├── editor_modified_state.rs ├── images ├── a_hebrew_paragraph.png ├── a_hebrew_word.png ├── an_arabic_paragraph.png ├── an_arabic_word.png ├── some_english_mixed_with_arabic.png └── some_english_mixed_with_hebrew.png ├── shaping_and_rendering.rs ├── wrap_stability.rs └── wrap_word_fallback.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | *.ttf filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | pages: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Build documentation 16 | run: cargo doc --verbose 17 | - name: Deploy documentation 18 | uses: peaceiris/actions-gh-pages@v3 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | publish_dir: ./target/doc 22 | force_orphan: true 23 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | cargo-deny: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: EmbarkStudios/cargo-deny-action@v1 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | lfs: true 24 | - name: Checkout LFS objects 25 | run: git lfs checkout 26 | - uses: actions-rs/clippy-check@v1 27 | with: 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | args: --all-features 30 | - name: Run Rust CI 31 | run: ./ci.sh 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | sample/udhr* 3 | target 4 | **/.DS_Store 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.14.2] - 2025-04-14 9 | 10 | ### Fixed 11 | 12 | - Ensure MSRV of 1.75.0 13 | 14 | ## [0.14.1] - 2025-04-04 15 | 16 | ### Added 17 | 18 | - Allow font to be stored as `peniko::Font` with `peniko` feature 19 | 20 | ## [0.14.0] - 2025-03-31 21 | 22 | ### Added 23 | 24 | - Add configurable font fallback lists using `Fallback` trait 25 | - Add letter spacing support in `Attrs` 26 | - Add arbitrary variable font features in `Attrs` 27 | 28 | ## [0.13.2] - 2025-03-11 29 | 30 | ### Fixed 31 | 32 | - Fix build for Android targets 33 | 34 | ## [0.13.1] - 2025-03-10 35 | 36 | ### Fixed 37 | 38 | - Fix glyph start+end indices in `Basic` shaping 39 | 40 | ## [0.13.0] - 2025-03-10 41 | 42 | ### Added 43 | 44 | - Add `Buffer::set_tab_width` function 45 | - Add `AttrsList::spans_iter` and use it in `Buffer::append` 46 | - Add alignment option to `Buffer::set_rich_text` 47 | - Add `SwashCache::get_outline_commands_uncached` 48 | 49 | ### Fixed 50 | 51 | - Fix typo in fallback font name `Noto Sans CJK JP` 52 | - Fix the character index used for getting a glyph attribute in basic shaping 53 | - Avoid debug assertion when handling `Motion::BufferEnd` 54 | - Handle single wrapped line scrolling 55 | - Reduce memory usage and loading time of FontSystem 56 | - Resolve lints 57 | - Use FreeMono as Braille script fallback 58 | - Load fonts prior to setting defaults 59 | 60 | ### Changed 61 | 62 | - Use `smol_str` for family name in `FamilyOwned` 63 | - Optimize `Buffer::set_rich_text` for when the buffer is reconstructed 64 | - Move `ShapeBuffer` to `FontSystem` 65 | - Cache the monospace fallbacks buffer in `FontSystem` 66 | - Apply fallback font to more Unix-like operating systems 67 | - Use hinting for `swash_outline_commands` 68 | - Update swash to `0.2.0` and hook up `std` feature 69 | - Update minimum supported Rust version to `1.75` 70 | - Update default fonts (for COSMIC, users should set their own as desired) 71 | 72 | ### Removed 73 | 74 | - Drop `ShapePlanCache` 75 | 76 | ## [0.12.1] - 2024-06-31 77 | 78 | ### Changed 79 | 80 | - Make collection of monospace fallback information optional 81 | 82 | ## [0.12.0] - 2024-06-18 83 | 84 | ### Added 85 | 86 | - Cache codepoint support info for monospace fonts 87 | - Store a sorted list of monospace font ids in font system 88 | - Add line ending abstraction 89 | - Horizontal scroll support in Buffer 90 | - Concurrently load and parse fonts 91 | - Add metrics to attributes 92 | - Support expanding tabs 93 | - Add an option to set selected text color 94 | - Add Edit::cursor_position 95 | - Allow layout to be calculated without specifying width 96 | - Allow for undefined buffer width and/or height 97 | - Add method to set syntax highlighting by file extension 98 | 99 | ### Fixed 100 | 101 | - Fix no_std build 102 | - Handle inverted Ranges in add_span 103 | - Fix undo and redo updating editor modified status 104 | - Ensure at least one line is in Buffer 105 | 106 | ### Changed 107 | 108 | - Enable vi feature for docs.rs build 109 | - Convert editor example to winit 110 | - Refactor scrollbar width handling for editor example 111 | - Convert rich-text example to winit 112 | - Only try monospace fonts that support at least one requested script 113 | - Skip trying monospace fallbacks if default font supports all codepoints 114 | - Make vertical scroll by pixels instead of layout lines 115 | - Upgrade dependencies and re-export ttf-parser 116 | 117 | ## [0.11.2] - 2024-02-08 118 | 119 | ### Fixed 120 | 121 | - Fix glyph start and end when using `shape-run-cache` 122 | 123 | ## [0.11.1] - 2024-02-08 124 | 125 | ### Added 126 | 127 | - Add `shape-run-cache` feature, that can significantly improve shaping performance 128 | 129 | ### Removed 130 | 131 | - Remove editor-libcosmic, see cosmic-edit instead 132 | 133 | ## [0.11.0] - 2024-02-07 134 | 135 | ### Added 136 | 137 | - Add function to set metrics and size simultaneously 138 | - Cache `rustybuzz` shape plans 139 | - Add capability to synthesize italic 140 | - New wrapping option `WordOrGlyph` to allow word to glyph fallback 141 | 142 | ### Fixed 143 | 144 | - `Buffer::set_rich_text`: Only add attrs if they do not match the defaults 145 | - Do not use Emoji fonts as monospace fallback 146 | - Refresh the attrs more often in basic shaping 147 | - `Buffer`: fix max scroll going one line beyond end 148 | - Improve reliability of `layout_cursor` 149 | - Handle multiple BiDi paragraphs in `ShapeLine` gracefully 150 | - Improved monospace font fallback 151 | - Only commit a previous word range if we had an existing visual line 152 | 153 | ### Changed 154 | 155 | - Update terminal example using `colored` 156 | - Significant improvements for `Editor`, `SyntaxEditor`, and `ViEditor` 157 | - Require default Attrs to be specified in `Buffer::set_rich_text` 158 | - Bump `fontdb` to `0.16` 159 | - Allow Clone of layout structs 160 | - Move cursor motions to new `Motion` enum, move handling to `Buffer` 161 | - Ensure that all shaping and layout uses scratch buffer 162 | - `BufferLine`: user `layout_in_buffer` to implement layout 163 | - `BufferLine`: remove wrap from struct, as wrap is passed to layout 164 | - Refactor of scroll and shaping 165 | - Move `color` and `x_opt` out of Cursor 166 | - Add size limit to `font_matches_cache` and clear it when it is reached 167 | - Update `swash` to `0.1.12` 168 | - Set default buffer wrap to `WordOrGlyph` 169 | 170 | ## Removed 171 | - Remove patch to load Redox system fonts, as fontdb does it now 172 | 173 | ## [0.10.0] - 2023-10-19 174 | 175 | ### Added 176 | 177 | - Added `Buffer::set_rich_text` method 178 | - Add `Align::End` for end-based alignment 179 | - Add more `Debug` implementations 180 | - Add feature to warn on missing glyphs 181 | - Add easy conversions for tuples/arrays for `Color` 182 | - Derive `Clone` for `AttrsList` 183 | - Add feature to allow `fontdb` to get `fontconfig` information 184 | - Add benchmarks to accurately gauge improvements 185 | - Add image render tests 186 | - Allow BSD-2-Clause and BSD-3-Clause licneses in cargo-deny 187 | 188 | ### Fixed 189 | 190 | - Fix `no_std` build 191 | - Fix `BufferLine::set_align` docs to not mention shape reset is performed 192 | - Fix width computed during unconstrained layout and add test for it 193 | - Set `cursor_moved` to true in `Editor::insert_string` 194 | - Fix `NextWord` action in `Editor` when line ends with word boundaries 195 | - Fix building `editor-libcosmic` with `vi` feature 196 | - Respect `fontconfig` font aliases when enabled 197 | - Fix rendering of RTL words 198 | 199 | ### Changed 200 | 201 | - Unify `no_std` and `std` impls of `FontSystem` 202 | - Move `hashbrown` behind `no_std` feature 203 | - Require either `std` or `no_std` feature to be specified 204 | - Use a scratch buffer to reduce allocations 205 | - Enable `std` feature with `fontconfig` feature 206 | - Enable `fontconfig` feature by default 207 | - Refactor code in `ShapeLine::layout` 208 | - Set MSRV to `1.65` 209 | - Make `Edit::copy_selection` immutable 210 | - Rewrite `PreviousWord` logic in `Editor` with iterators 211 | - Use attributes at cursor position for insertions in `Editor` 212 | - Update all dependencies 213 | - Use `self_cell` for creating self-referential struct 214 | 215 | ## [0.9.0] - 2023-07-06 216 | 217 | ### Added 218 | 219 | - Add `Shaping` enum to allow selecting the shaping strategy 220 | - Add `Buffer::new_empty` to create `Buffer` without `FontSystem` 221 | - Add `BidiParagraphs` iterator 222 | - Allow setting `Cursor` color 223 | - Allow setting `Editor` cursor 224 | - Add `PhysicalGlyph` that allows computing `CacheKey` after layout 225 | - Add light syntax highlighter to `libcosmic` example 226 | 227 | ### Fixed 228 | 229 | - Fix WebAssembly support 230 | - Fix alignment when not wrapping 231 | - Fallback to monospaced font if Monospace family is not found 232 | - Align glyphs in a `LayoutRun` to baseline 233 | 234 | ### Changed 235 | 236 | - Update `fontdb` to 0.14.1 237 | - Replace ouroboros with aliasable 238 | - Use `BidiParagraphs` iterator instead of `str::Lines` 239 | - Update `libcosmic` version 240 | 241 | ### Removed 242 | 243 | - `LayoutGlyph` no longer has `x_int` and `y_int`, use `PhysicalGlyph` instead 244 | 245 | ## [0.8.0] - 2023-04-03 246 | 247 | ### Added 248 | 249 | - `FontSystem::new_with_fonts` helper 250 | - Alignment and justification 251 | - `FontSystem::db_mut` provides mutable access to `fontdb` database 252 | - `rustybuzz` is re-exported 253 | 254 | ### Fixed 255 | 256 | - Fix some divide by zero panics 257 | - Redox now uses `std` `FontSystem` 258 | - Layout system improvements 259 | - `BufferLinke::set_text` has been made more efficient 260 | - Fix potential panic on window resize 261 | 262 | ### Changed 263 | 264 | - Use `f32` instead of `i32` for lengths 265 | - `FontSystem` no longer self-referencing 266 | - `SwashCash` no longer keeps reference to `FontSystem` 267 | 268 | ### Removed 269 | 270 | - `Attrs::monospaced` is removed, use `Family::Monospace` instead 271 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmic-text" 3 | description = "Pure Rust multi-line text handling" 4 | version = "0.14.2" 5 | authors = ["Jeremy Soller "] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | documentation = "https://docs.rs/cosmic-text/latest/cosmic_text/" 9 | repository = "https://github.com/pop-os/cosmic-text" 10 | rust-version = "1.75" 11 | 12 | [dependencies] 13 | bitflags = "2.4.1" 14 | cosmic_undo_2 = { version = "0.2.0", optional = true } 15 | fontdb = { version = "0.23", default-features = false } 16 | hashbrown = { version = "0.14.1", optional = true, default-features = false } 17 | libm = { version = "0.2.8", optional = true } 18 | log = "0.4.20" 19 | modit = { version = "0.1.4", optional = true } 20 | rangemap = "1.4.0" 21 | rustc-hash = { version = "1.1.0", default-features = false } 22 | rustybuzz = { version = "0.14", default-features = false, features = ["libm"] } 23 | self_cell = "1.0.1" 24 | smol_str = { version = "0.2.2", default-features = false } 25 | syntect = { version = "5.1.0", optional = true } 26 | sys-locale = { version = "0.3.1", optional = true } 27 | ttf-parser = { version = "0.21", default-features = false } 28 | unicode-linebreak = "0.1.5" 29 | unicode-script = "0.5.5" 30 | unicode-segmentation = "1.10.1" 31 | 32 | [dependencies.swash] 33 | version = "0.2.0" 34 | default-features = false 35 | features = ["render", "scale"] 36 | optional = true 37 | 38 | [dependencies.unicode-bidi] 39 | version = "0.3.13" 40 | default-features = false 41 | features = ["hardcoded-data"] 42 | 43 | [dependencies.peniko] 44 | version = "0.4.0" 45 | optional = true 46 | 47 | [features] 48 | default = ["std", "swash", "fontconfig"] 49 | fontconfig = ["fontdb/fontconfig", "std"] 50 | monospace_fallback = [] 51 | no_std = ["rustybuzz/libm", "hashbrown", "dep:libm"] 52 | peniko = ["dep:peniko"] 53 | shape-run-cache = [] 54 | std = [ 55 | "fontdb/memmap", 56 | "fontdb/std", 57 | "rustybuzz/std", 58 | "swash?/std", 59 | "sys-locale", 60 | "ttf-parser/std", 61 | "unicode-bidi/std", 62 | ] 63 | vi = ["modit", "syntect", "cosmic_undo_2"] 64 | wasm-web = ["sys-locale?/js"] 65 | warn_on_missing_glyphs = [] 66 | 67 | [[bench]] 68 | name = "layout" 69 | harness = false 70 | 71 | [workspace] 72 | members = ["examples/*"] 73 | 74 | [dev-dependencies] 75 | tiny-skia = "0.11.2" 76 | criterion = { version = "0.5.1", default-features = false, features = [ 77 | "cargo_bench_support", 78 | ] } 79 | 80 | [profile.test] 81 | opt-level = 1 82 | 83 | [package.metadata.docs.rs] 84 | features = ["vi"] 85 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 System76 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 | # COSMIC Text 2 | 3 | [![crates.io](https://img.shields.io/crates/v/cosmic-text.svg)](https://crates.io/crates/cosmic-text) 4 | [![docs.rs](https://docs.rs/cosmic-text/badge.svg)](https://docs.rs/cosmic-text) 5 | ![license](https://img.shields.io/crates/l/cosmic-text.svg) 6 | [![Rust workflow](https://github.com/pop-os/cosmic-text/workflows/Rust/badge.svg?event=push)](https://github.com/pop-os/cosmic-text/actions) 7 | 8 | Pure Rust multi-line text handling. 9 | 10 | COSMIC Text provides advanced text shaping, layout, and rendering wrapped up 11 | into a simple abstraction. Shaping is provided by rustybuzz, and supports a 12 | wide variety of advanced shaping operations. Rendering is provided by swash, 13 | which supports ligatures and color emoji. Layout is implemented custom, in safe 14 | Rust, and supports bidirectional text. Font fallback is also a custom 15 | implementation, reusing some of the static fallback lists in browsers such as 16 | Chromium and Firefox. Linux, macOS, and Windows are supported with the full 17 | feature set. Other platforms may need to implement font fallback capabilities. 18 | 19 | ## Screenshots 20 | 21 | Arabic translation of Universal Declaration of Human Rights 22 | [![Arabic screenshot](screenshots/arabic.png)](screenshots/arabic.png) 23 | 24 | Hindi translation of Universal Declaration of Human Rights 25 | [![Hindi screenshot](screenshots/hindi.png)](screenshots/hindi.png) 26 | 27 | Simplified Chinese translation of Universal Declaration of Human Rights 28 | [![Simplified Chinses screenshot](screenshots/chinese-simplified.png)](screenshots/chinese-simplified.png) 29 | 30 | ## Roadmap 31 | 32 | The following features must be supported before this is "ready": 33 | 34 | - [x] Font loading (using fontdb) 35 | - [x] Preset fonts 36 | - [x] System fonts 37 | - [x] Text styles (bold, italic, etc.) 38 | - [x] Per-buffer 39 | - [x] Per-span 40 | - [x] Font shaping (using rustybuzz) 41 | - [x] Cache results 42 | - [x] RTL 43 | - [x] Bidirectional rendering 44 | - [x] Font fallback 45 | - [x] Choose font based on locale to work around "unification" 46 | - [x] Per-line granularity 47 | - [x] Per-character granularity 48 | - [x] Font layout 49 | - [x] Click detection 50 | - [x] Simple wrapping 51 | - [ ] Wrapping with indentation 52 | - [ ] No wrapping 53 | - [ ] Ellipsize 54 | - [x] Font rendering (using swash) 55 | - [x] Cache results 56 | - [x] Font hinting 57 | - [x] Ligatures 58 | - [x] Color emoji 59 | - [x] Text editing 60 | - [x] Performance improvements 61 | - [x] Text selection 62 | - [x] Can automatically recreate https://unicode.org/udhr/ without errors (see below) 63 | - [x] Bidirectional selection 64 | - [ ] Copy/paste 65 | - [x] no_std support (with `default-features = false`) 66 | - [ ] no_std font loading 67 | - [x] no_std shaping 68 | - [x] no_std layout 69 | - [ ] no_std rendering 70 | 71 | The UDHR (Universal Declaration of Human Rights) test involves taking the entire 72 | set of UDHR translations (almost 500 languages), concatenating them as one file 73 | (which ends up being 8 megabytes!), then via the `editor-test` example, 74 | automatically simulating the entry of that file into cosmic-text per-character, 75 | with the use of backspace and delete tested per character and per line. Then, 76 | the final contents of the buffer is compared to the original file. All of the 77 | 106746 lines are correct. 78 | 79 | ## License 80 | 81 | Licensed under either of 82 | 83 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 84 | http://www.apache.org/licenses/LICENSE-2.0) 85 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 86 | http://opensource.org/licenses/MIT) 87 | 88 | at your option. 89 | 90 | ### Contribution 91 | 92 | Unless you explicitly state otherwise, any contribution intentionally submitted 93 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 94 | dual licensed as above, without any additional terms or conditions. 95 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function build { 4 | cargo build --release "$@" 5 | cargo clippy --no-deps "$@" 6 | } 7 | 8 | set -ex 9 | 10 | echo Check formatting 11 | cargo fmt --check 12 | 13 | echo Build with default features 14 | build 15 | 16 | echo Install target for no_std build 17 | # This is necessary because Rust otherwise may silently use std regardless. 18 | rustup target add thumbv8m.main-none-eabihf 19 | 20 | echo Build with only no_std feature 21 | build --no-default-features --features no_std --target thumbv8m.main-none-eabihf 22 | 23 | echo Build with only std feature 24 | build --no-default-features --features std 25 | 26 | echo Build with only std and swash features 27 | build --no-default-features --features std,swash 28 | 29 | echo Build with only std and syntect features 30 | build --no-default-features --features std,syntect 31 | 32 | echo Build with only std and vi features 33 | build --no-default-features --features std,vi 34 | 35 | echo Build with all features 36 | build --all-features 37 | 38 | echo Run tests 39 | cargo test 40 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # Root options 13 | 14 | # If 1 or more target triples (and optionally, target_features) are specified, 15 | # only the specified targets will be checked when running `cargo deny check`. 16 | # This means, if a particular package is only ever used as a target specific 17 | # dependency, such as, for example, the `nix` crate only being used via the 18 | # `target_family = "unix"` configuration, that only having windows targets in 19 | # this list would mean the nix crate, as well as any of its exclusive 20 | # dependencies not shared by any other crates, would be ignored, as the target 21 | # list here is effectively saying which targets you are building for. 22 | targets = [ 23 | # The triple can be any string, but only the target triples built in to 24 | # rustc (as of 1.40) can be checked against actual config expressions 25 | #{ triple = "x86_64-unknown-linux-musl" }, 26 | # You can also specify which target_features you promise are enabled for a 27 | # particular target. target_features are currently not validated against 28 | # the actual valid features supported by the target architecture. 29 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 30 | ] 31 | # When creating the dependency graph used as the source of truth when checks are 32 | # executed, this field can be used to prune crates from the graph, removing them 33 | # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate 34 | # is pruned from the graph, all of its dependencies will also be pruned unless 35 | # they are connected to another crate in the graph that hasn't been pruned, 36 | # so it should be used with care. The identifiers are [Package ID Specifications] 37 | # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) 38 | #exclude = [] 39 | # If true, metadata will be collected with `--all-features`. Note that this can't 40 | # be toggled off if true, if you want to conditionally enable `--all-features` it 41 | # is recommended to pass `--all-features` on the cmd line instead 42 | all-features = false 43 | # If true, metadata will be collected with `--no-default-features`. The same 44 | # caveat with `all-features` applies 45 | no-default-features = true 46 | # If set, these feature will be enabled when collecting metadata. If `--features` 47 | # is specified on the cmd line they will take precedence over this option. 48 | #features = [] 49 | # When outputting inclusion graphs in diagnostics that include features, this 50 | # option can be used to specify the depth at which feature edges will be added. 51 | # This option is included since the graphs can be quite large and the addition 52 | # of features from the crate(s) to all of the graph roots can be far too verbose. 53 | # This option can be overridden via `--feature-depth` on the cmd line 54 | feature-depth = 1 55 | 56 | # This section is considered when running `cargo deny check advisories` 57 | # More documentation for the advisories section can be found here: 58 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 59 | [advisories] 60 | # The path where the advisory database is cloned/fetched into 61 | db-path = "~/.cargo/advisory-db" 62 | # The url(s) of the advisory databases to use 63 | db-urls = ["https://github.com/rustsec/advisory-db"] 64 | # The lint level for security vulnerabilities 65 | vulnerability = "deny" 66 | # The lint level for unmaintained crates 67 | unmaintained = "warn" 68 | # The lint level for crates that have been yanked from their source registry 69 | yanked = "warn" 70 | # The lint level for crates with security notices. Note that as of 71 | # 2019-12-17 there are no security notice advisories in 72 | # https://github.com/rustsec/advisory-db 73 | notice = "warn" 74 | # A list of advisory IDs to ignore. Note that ignored advisories will still 75 | # output a note when they are encountered. 76 | ignore = [] 77 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 78 | # lower than the range specified will be ignored. Note that ignored advisories 79 | # will still output a note when they are encountered. 80 | # * None - CVSS Score 0.0 81 | # * Low - CVSS Score 0.1 - 3.9 82 | # * Medium - CVSS Score 4.0 - 6.9 83 | # * High - CVSS Score 7.0 - 8.9 84 | # * Critical - CVSS Score 9.0 - 10.0 85 | #severity-threshold = 86 | 87 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 88 | # If this is false, then it uses a built-in git library. 89 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 90 | # See Git Authentication for more information about setting up git authentication. 91 | #git-fetch-with-cli = true 92 | 93 | # This section is considered when running `cargo deny check licenses` 94 | # More documentation for the licenses section can be found here: 95 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 96 | [licenses] 97 | # The lint level for crates which do not have a detectable license 98 | unlicensed = "deny" 99 | # List of explicitly allowed licenses 100 | # See https://spdx.org/licenses/ for list of possible licenses 101 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 102 | allow = [ 103 | "MIT", 104 | "Apache-2.0", 105 | "Unicode-DFS-2016", 106 | "BSD-3-Clause", 107 | "BSD-2-Clause", 108 | "Zlib", 109 | #"Apache-2.0 WITH LLVM-exception", 110 | ] 111 | # List of explicitly disallowed licenses 112 | # See https://spdx.org/licenses/ for list of possible licenses 113 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 114 | deny = [ 115 | #"Nokia", 116 | ] 117 | # Lint level for licenses considered copyleft 118 | copyleft = "warn" 119 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 120 | # * both - The license will be approved if it is both OSI-approved *AND* FSF 121 | # * either - The license will be approved if it is either OSI-approved *OR* FSF 122 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF 123 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved 124 | # * neither - This predicate is ignored and the default lint level is used 125 | allow-osi-fsf-free = "neither" 126 | # Lint level used when no other predicates are matched 127 | # 1. License isn't in the allow or deny lists 128 | # 2. License isn't copyleft 129 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" 130 | default = "deny" 131 | # The confidence threshold for detecting a license from license text. 132 | # The higher the value, the more closely the license text must be to the 133 | # canonical license text of a valid SPDX license file. 134 | # [possible values: any between 0.0 and 1.0]. 135 | confidence-threshold = 0.8 136 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 137 | # aren't accepted for every possible crate as with the normal allow list 138 | exceptions = [ 139 | # Each entry is the crate and version constraint, and its specific allow 140 | # list 141 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 142 | ] 143 | 144 | # Some crates don't have (easily) machine readable licensing information, 145 | # adding a clarification entry for it allows you to manually specify the 146 | # licensing information 147 | #[[licenses.clarify]] 148 | # The name of the crate the clarification applies to 149 | #name = "ring" 150 | # The optional version constraint for the crate 151 | #version = "*" 152 | # The SPDX expression for the license requirements of the crate 153 | #expression = "MIT AND ISC AND OpenSSL" 154 | # One or more files in the crate's source used as the "source of truth" for 155 | # the license expression. If the contents match, the clarification will be used 156 | # when running the license check, otherwise the clarification will be ignored 157 | # and the crate will be checked normally, which may produce warnings or errors 158 | # depending on the rest of your configuration 159 | #license-files = [ 160 | # Each entry is a crate relative path, and the (opaque) hash of its contents 161 | #{ path = "LICENSE", hash = 0xbd0eed23 } 162 | #] 163 | 164 | [licenses.private] 165 | # If true, ignores workspace crates that aren't published, or are only 166 | # published to private registries. 167 | # To see how to mark a crate as unpublished (to the official registry), 168 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 169 | ignore = false 170 | # One or more private registries that you might publish crates to, if a crate 171 | # is only published to private registries, and ignore is true, the crate will 172 | # not have its license(s) checked 173 | registries = [ 174 | #"https://sekretz.com/registry 175 | ] 176 | 177 | # This section is considered when running `cargo deny check bans`. 178 | # More documentation about the 'bans' section can be found here: 179 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 180 | [bans] 181 | # Lint level for when multiple versions of the same crate are detected 182 | multiple-versions = "deny" 183 | # Lint level for when a crate version requirement is `*` 184 | wildcards = "allow" 185 | # The graph highlighting used when creating dotgraphs for crates 186 | # with multiple versions 187 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 188 | # * simplest-path - The path to the version with the fewest edges is highlighted 189 | # * all - Both lowest-version and simplest-path are used 190 | highlight = "all" 191 | # The default lint level for `default` features for crates that are members of 192 | # the workspace that is being checked. This can be overriden by allowing/denying 193 | # `default` on a crate-by-crate basis if desired. 194 | workspace-default-features = "allow" 195 | # The default lint level for `default` features for external crates that are not 196 | # members of the workspace. This can be overriden by allowing/denying `default` 197 | # on a crate-by-crate basis if desired. 198 | external-default-features = "allow" 199 | # List of crates that are allowed. Use with care! 200 | allow = [ 201 | #{ name = "ansi_term", version = "=0.11.0" }, 202 | ] 203 | # List of crates to deny 204 | deny = [ 205 | # Each entry the name of a crate and a version range. If version is 206 | # not specified, all versions will be matched. 207 | #{ name = "ansi_term", version = "=0.11.0" }, 208 | # 209 | # Wrapper crates can optionally be specified to allow the crate when it 210 | # is a direct dependency of the otherwise banned crate 211 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 212 | ] 213 | 214 | # List of features to allow/deny 215 | # Each entry the name of a crate and a version range. If version is 216 | # not specified, all versions will be matched. 217 | #[[bans.features]] 218 | #name = "reqwest" 219 | # Features to not allow 220 | #deny = ["json"] 221 | # Features to allow 222 | #allow = [ 223 | # "rustls", 224 | # "__rustls", 225 | # "__tls", 226 | # "hyper-rustls", 227 | # "rustls", 228 | # "rustls-pemfile", 229 | # "rustls-tls-webpki-roots", 230 | # "tokio-rustls", 231 | # "webpki-roots", 232 | #] 233 | # If true, the allowed features must exactly match the enabled feature set. If 234 | # this is set there is no point setting `deny` 235 | #exact = true 236 | 237 | # Certain crates/versions that will be skipped when doing duplicate detection. 238 | skip = [ 239 | { name = "bitflags" }, 240 | { name = "syn" }, 241 | #{ name = "ansi_term", version = "=0.11.0" }, 242 | ] 243 | # Similarly to `skip` allows you to skip certain crates during duplicate 244 | # detection. Unlike skip, it also includes the entire tree of transitive 245 | # dependencies starting at the specified crate, up to a certain depth, which is 246 | # by default infinite. 247 | skip-tree = [ 248 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 249 | ] 250 | 251 | # This section is considered when running `cargo deny check sources`. 252 | # More documentation about the 'sources' section can be found here: 253 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 254 | [sources] 255 | # Lint level for what to happen when a crate from a crate registry that is not 256 | # in the allow list is encountered 257 | unknown-registry = "warn" 258 | # Lint level for what to happen when a crate from a git repository that is not 259 | # in the allow list is encountered 260 | unknown-git = "warn" 261 | # List of URLs for allowed crate registries. Defaults to the crates.io index 262 | # if not specified. If it is specified but empty, no registries are allowed. 263 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 264 | # List of URLs for allowed Git repositories 265 | allow-git = [] 266 | -------------------------------------------------------------------------------- /editor-test.sh: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | RUST_LOG="cosmic_text=debug,editor_test=debug" cargo run --release --package editor-test -- "$@" 4 | -------------------------------------------------------------------------------- /editor.sh: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | RUST_LOG="cosmic_text=debug,editor=debug" cargo run --release --package editor -- "$@" 4 | -------------------------------------------------------------------------------- /examples/editor-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "editor-test" 3 | version = "0.1.0" 4 | authors = ["Jeremy Soller "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | cosmic-text = { path = "../../" } 11 | env_logger = "0.10" 12 | fontdb = "0.13" 13 | log = "0.4" 14 | orbclient = "0.3.35" 15 | unicode-segmentation = "1.7" 16 | -------------------------------------------------------------------------------- /examples/editor-test/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use cosmic_text::{ 4 | Action, BidiParagraphs, BorrowedWithFontSystem, Buffer, Color, Edit, Editor, FontSystem, 5 | Metrics, Motion, SwashCache, 6 | }; 7 | use orbclient::{EventOption, Renderer, Window, WindowFlag}; 8 | use std::{env, fs, process, time::Instant}; 9 | use unicode_segmentation::UnicodeSegmentation; 10 | 11 | fn redraw( 12 | window: &mut Window, 13 | editor: &mut BorrowedWithFontSystem, 14 | swash_cache: &mut SwashCache, 15 | ) { 16 | let bg_color = orbclient::Color::rgb(0x34, 0x34, 0x34); 17 | let font_color = Color::rgb(0xFF, 0xFF, 0xFF); 18 | let cursor_color = Color::rgb(0xFF, 0xFF, 0xFF); 19 | let selection_color = Color::rgba(0xFF, 0xFF, 0xFF, 0x33); 20 | let selected_text_color = Color::rgb(0xF0, 0xF0, 0xFF); 21 | 22 | editor.shape_as_needed(true); 23 | if editor.redraw() { 24 | let instant = Instant::now(); 25 | 26 | window.set(bg_color); 27 | 28 | editor.draw( 29 | swash_cache, 30 | font_color, 31 | cursor_color, 32 | selection_color, 33 | selected_text_color, 34 | |x, y, w, h, color| { 35 | window.rect(x, y, w, h, orbclient::Color { data: color.0 }); 36 | }, 37 | ); 38 | 39 | window.sync(); 40 | 41 | editor.set_redraw(false); 42 | 43 | let duration = instant.elapsed(); 44 | log::debug!("redraw: {:?}", duration); 45 | } 46 | } 47 | 48 | fn main() { 49 | env_logger::init(); 50 | 51 | let display_scale = 1.0; 52 | let mut font_system = FontSystem::new(); 53 | 54 | let mut window = Window::new_flags( 55 | -1, 56 | -1, 57 | 1024, 58 | 768, 59 | &format!("COSMIC TEXT - {}", font_system.locale()), 60 | &[WindowFlag::Async], 61 | ) 62 | .unwrap(); 63 | 64 | let font_sizes = [ 65 | Metrics::new(10.0, 14.0).scale(display_scale), // Caption 66 | Metrics::new(14.0, 20.0).scale(display_scale), // Body 67 | Metrics::new(20.0, 28.0).scale(display_scale), // Title 4 68 | Metrics::new(24.0, 32.0).scale(display_scale), // Title 3 69 | Metrics::new(28.0, 36.0).scale(display_scale), // Title 2 70 | Metrics::new(32.0, 44.0).scale(display_scale), // Title 1 71 | ]; 72 | let font_size_default = 1; // Body 73 | 74 | let mut buffer = Buffer::new(&mut font_system, font_sizes[font_size_default]); 75 | buffer 76 | .borrow_with(&mut font_system) 77 | .set_size(Some(window.width() as f32), Some(window.height() as f32)); 78 | 79 | let mut editor = Editor::new(buffer); 80 | 81 | let mut editor = editor.borrow_with(&mut font_system); 82 | 83 | let mut swash_cache = SwashCache::new(); 84 | 85 | let text = if let Some(arg) = env::args().nth(1) { 86 | fs::read_to_string(&arg).expect("failed to open file") 87 | } else { 88 | #[cfg(feature = "mono")] 89 | let default_text = include_str!("../../../sample/mono.txt"); 90 | #[cfg(not(feature = "mono"))] 91 | let default_text = include_str!("../../../sample/proportional.txt"); 92 | default_text.to_string() 93 | }; 94 | 95 | let test_start = Instant::now(); 96 | 97 | for line in BidiParagraphs::new(&text) { 98 | log::debug!("Line {:?}", line); 99 | 100 | for grapheme in line.graphemes(true) { 101 | for c in grapheme.chars() { 102 | log::trace!("Insert {:?}", c); 103 | 104 | // Test backspace of character 105 | { 106 | let cursor = editor.cursor(); 107 | editor.action(Action::Insert(c)); 108 | editor.action(Action::Backspace); 109 | assert_eq!(cursor, editor.cursor()); 110 | } 111 | 112 | // Finally, normal insert of character 113 | editor.action(Action::Insert(c)); 114 | } 115 | 116 | // Test delete of EGC 117 | { 118 | let cursor = editor.cursor(); 119 | editor.action(Action::Motion(Motion::Previous)); 120 | editor.action(Action::Delete); 121 | for c in grapheme.chars() { 122 | editor.action(Action::Insert(c)); 123 | } 124 | assert_eq!( 125 | (cursor.line, cursor.index), 126 | (editor.cursor().line, editor.cursor().index) 127 | ); 128 | } 129 | } 130 | 131 | // Test backspace of newline 132 | { 133 | let cursor = editor.cursor(); 134 | editor.action(Action::Enter); 135 | editor.action(Action::Backspace); 136 | assert_eq!(cursor, editor.cursor()); 137 | } 138 | 139 | // Test delete of newline 140 | { 141 | let cursor = editor.cursor(); 142 | editor.action(Action::Enter); 143 | editor.action(Action::Motion(Motion::Previous)); 144 | editor.action(Action::Delete); 145 | assert_eq!(cursor, editor.cursor()); 146 | } 147 | 148 | // Finally, normal enter 149 | editor.action(Action::Enter); 150 | 151 | redraw(&mut window, &mut editor, &mut swash_cache); 152 | 153 | for event in window.events() { 154 | if let EventOption::Quit(_) = event.to_option() { 155 | process::exit(1) 156 | } 157 | } 158 | } 159 | 160 | let test_elapsed = test_start.elapsed(); 161 | log::info!("Test completed in {:?}", test_elapsed); 162 | 163 | let mut wrong = 0; 164 | editor.with_buffer(|buffer| { 165 | for (line_i, line) in text.lines().enumerate() { 166 | let buffer_line = &buffer.lines[line_i]; 167 | if buffer_line.text() != line { 168 | log::error!("line {}: {:?} != {:?}", line_i, buffer_line.text(), line); 169 | wrong += 1; 170 | } 171 | } 172 | }); 173 | if wrong == 0 { 174 | log::info!("All lines matched!"); 175 | process::exit(0); 176 | } else { 177 | log::error!("{} lines did not match!", wrong); 178 | process::exit(1); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /examples/editor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "editor" 3 | version = "0.1.0" 4 | authors = ["Jeremy Soller "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | cosmic-text = { path = "../..", features = ["syntect"] } 11 | env_logger = "0.10" 12 | fontdb = "0.13" 13 | log = "0.4" 14 | softbuffer = "0.4" 15 | tiny-skia = "0.11" 16 | unicode-segmentation = "1.7" 17 | winit = "0.29" 18 | 19 | [features] 20 | default = [] 21 | vi = ["cosmic-text/vi"] 22 | -------------------------------------------------------------------------------- /examples/multiview/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multiview" 3 | version = "0.1.0" 4 | authors = ["Jeremy Soller "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | cosmic-text = { path = "../.." } 11 | env_logger = "0.10" 12 | fontdb = "0.13" 13 | log = "0.4" 14 | softbuffer = "0.4" 15 | tiny-skia = "0.11" 16 | unicode-segmentation = "1.7" 17 | winit = "0.29" 18 | -------------------------------------------------------------------------------- /examples/multiview/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use cosmic_text::{ 4 | Action, Attrs, Buffer, Edit, Family, FontSystem, Metrics, Scroll, Shaping, SwashCache, 5 | }; 6 | use std::{collections::HashMap, env, fs, num::NonZeroU32, rc::Rc, slice}; 7 | use tiny_skia::{Color, Paint, PixmapMut, Rect, Transform}; 8 | use winit::{ 9 | event::{ElementState, Event, KeyEvent, WindowEvent}, 10 | event_loop::{ControlFlow, EventLoop}, 11 | keyboard::{Key, NamedKey}, 12 | window::{Window as WinitWindow, WindowBuilder}, 13 | }; 14 | 15 | fn main() { 16 | env_logger::init(); 17 | 18 | let path = if let Some(arg) = env::args().nth(1) { 19 | arg 20 | } else { 21 | "sample/hello.txt".to_string() 22 | }; 23 | 24 | let mut font_system = FontSystem::new(); 25 | 26 | let mut swash_cache = SwashCache::new(); 27 | 28 | let mut buffer = Buffer::new_empty(Metrics::new(14.0, 20.0)); 29 | 30 | let mut buffer = buffer.borrow_with(&mut font_system); 31 | 32 | let attrs = Attrs::new().family(Family::Monospace); 33 | match fs::read_to_string(&path) { 34 | Ok(text) => buffer.set_text(&text, &attrs, Shaping::Advanced), 35 | Err(err) => { 36 | log::error!("failed to load {:?}: {}", path, err); 37 | } 38 | } 39 | 40 | let event_loop = EventLoop::new().unwrap(); 41 | 42 | struct Window { 43 | window: Rc, 44 | context: softbuffer::Context>, 45 | surface: softbuffer::Surface, Rc>, 46 | scroll: Scroll, 47 | } 48 | let mut windows = HashMap::new(); 49 | for _ in 0..2 { 50 | let window = Rc::new(WindowBuilder::new().build(&event_loop).unwrap()); 51 | let context = softbuffer::Context::new(window.clone()).unwrap(); 52 | let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); 53 | windows.insert( 54 | window.id(), 55 | Window { 56 | window, 57 | context, 58 | surface, 59 | scroll: Scroll::default(), 60 | }, 61 | ); 62 | } 63 | 64 | event_loop 65 | .run(move |event, elwt| { 66 | elwt.set_control_flow(ControlFlow::Wait); 67 | 68 | match event { 69 | Event::WindowEvent { 70 | window_id, 71 | event: WindowEvent::RedrawRequested, 72 | } => { 73 | if let Some(Window { 74 | window, 75 | surface, 76 | scroll, 77 | .. 78 | }) = windows.get_mut(&window_id) 79 | { 80 | let (width, height) = { 81 | let size = window.inner_size(); 82 | (size.width, size.height) 83 | }; 84 | surface 85 | .resize( 86 | NonZeroU32::new(width).unwrap(), 87 | NonZeroU32::new(height).unwrap(), 88 | ) 89 | .unwrap(); 90 | 91 | let mut surface_buffer = surface.buffer_mut().unwrap(); 92 | let surface_buffer_u8 = unsafe { 93 | slice::from_raw_parts_mut( 94 | surface_buffer.as_mut_ptr() as *mut u8, 95 | surface_buffer.len() * 4, 96 | ) 97 | }; 98 | let mut pixmap = 99 | PixmapMut::from_bytes(surface_buffer_u8, width, height).unwrap(); 100 | pixmap.fill(Color::from_rgba8(0, 0, 0, 0xFF)); 101 | 102 | // Set scroll to view scroll 103 | buffer.set_scroll(*scroll); 104 | // Set size, will relayout and shape until scroll if changed 105 | buffer.set_size(Some(width as f32), Some(height as f32)); 106 | // Shape until scroll, ensures scroll is clamped 107 | //TODO: ability to prune with multiple views? 108 | buffer.shape_until_scroll(true); 109 | // Update scroll after buffer clamps it 110 | *scroll = buffer.scroll(); 111 | 112 | let mut paint = Paint::default(); 113 | paint.anti_alias = false; 114 | let transform = Transform::identity(); 115 | buffer.draw( 116 | &mut swash_cache, 117 | cosmic_text::Color::rgb(0xFF, 0xFF, 0xFF), 118 | |x, y, w, h, color| { 119 | paint.set_color_rgba8(color.r(), color.g(), color.b(), color.a()); 120 | pixmap.fill_rect( 121 | Rect::from_xywh(x as f32, y as f32, w as f32, h as f32) 122 | .unwrap(), 123 | &paint, 124 | transform, 125 | None, 126 | ); 127 | }, 128 | ); 129 | 130 | surface_buffer.present().unwrap(); 131 | } 132 | } 133 | Event::WindowEvent { 134 | event: 135 | WindowEvent::KeyboardInput { 136 | event: 137 | KeyEvent { 138 | logical_key, 139 | text, 140 | state, 141 | .. 142 | }, 143 | .. 144 | }, 145 | window_id, 146 | } => { 147 | if let Some(Window { window, scroll, .. }) = windows.get_mut(&window_id) { 148 | if state == ElementState::Pressed { 149 | match logical_key { 150 | Key::Named(NamedKey::ArrowDown) => { 151 | scroll.vertical += buffer.metrics().line_height; 152 | } 153 | Key::Named(NamedKey::ArrowUp) => { 154 | scroll.vertical -= buffer.metrics().line_height; 155 | } 156 | Key::Named(NamedKey::PageDown) => { 157 | scroll.vertical += buffer.size().1.unwrap_or(0.0); 158 | } 159 | Key::Named(NamedKey::PageUp) => { 160 | scroll.vertical -= buffer.size().1.unwrap_or(0.0); 161 | } 162 | _ => {} 163 | } 164 | } 165 | println!("{:?} {:?} {:?}", logical_key, text, state); 166 | window.request_redraw(); 167 | } 168 | } 169 | Event::WindowEvent { 170 | event: WindowEvent::CloseRequested, 171 | window_id: _, 172 | } => { 173 | //TODO: just close one window 174 | elwt.exit(); 175 | } 176 | _ => {} 177 | } 178 | }) 179 | .unwrap(); 180 | } 181 | -------------------------------------------------------------------------------- /examples/rich-text/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rich-text" 3 | version = "0.1.0" 4 | authors = ["Jeremy Soller "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | cosmic-text = { path = "../.." } 11 | env_logger = "0.10" 12 | fontdb = "0.13" 13 | log = "0.4" 14 | softbuffer = "0.4" 15 | tiny-skia = "0.11" 16 | unicode-segmentation = "1.7" 17 | winit = "0.29" 18 | -------------------------------------------------------------------------------- /examples/terminal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "terminal" 3 | version = "0.1.0" 4 | authors = ["Jeremy Soller "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | colored = "2.0" 11 | cosmic-text = { path = "../../" } 12 | env_logger = "0.10" 13 | fontdb = "0.13" 14 | log = "0.4" 15 | -------------------------------------------------------------------------------- /examples/terminal/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | //! Run this example with `cargo run --package terminal` 4 | //! or `cargo run --package terminal -- "my own text"` 5 | 6 | use colored::Colorize; 7 | use cosmic_text::{Attrs, Buffer, Color, FontSystem, Metrics, Shaping, SwashCache}; 8 | use std::fmt::Write; 9 | 10 | fn main() { 11 | // A FontSystem provides access to detected system fonts, create one per application 12 | let mut font_system = FontSystem::new(); 13 | 14 | // A SwashCache stores rasterized glyphs, create one per application 15 | let mut swash_cache = SwashCache::new(); 16 | 17 | // Text metrics indicate the font size and line height of a buffer 18 | const FONT_SIZE: f32 = 14.0; 19 | const LINE_HEIGHT: f32 = FONT_SIZE * 1.2; 20 | let metrics = Metrics::new(FONT_SIZE, LINE_HEIGHT); 21 | 22 | // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget 23 | let mut buffer = Buffer::new(&mut font_system, metrics); 24 | 25 | let mut buffer = buffer.borrow_with(&mut font_system); 26 | 27 | // Set a size for the text buffer, in pixels 28 | let width = 80.0; 29 | // The height is unbounded 30 | buffer.set_size(Some(width), None); 31 | 32 | // Attributes indicate what font to choose 33 | let attrs = Attrs::new(); 34 | 35 | // Add some text! 36 | let text = std::env::args() 37 | .nth(1) 38 | .unwrap_or(" Hi, Rust! 🦀 ".to_string()); 39 | buffer.set_text(&text, &attrs, Shaping::Advanced); 40 | 41 | // Perform shaping as desired 42 | buffer.shape_until_scroll(true); 43 | 44 | // Default text color (0xFF, 0xFF, 0xFF is white) 45 | const TEXT_COLOR: Color = Color::rgb(0xFF, 0xFF, 0xFF); 46 | 47 | // Set up the canvas 48 | let height = LINE_HEIGHT * buffer.layout_runs().count() as f32; 49 | let mut canvas = vec![vec![None; width as usize]; height as usize]; 50 | 51 | // Draw to the canvas 52 | buffer.draw(&mut swash_cache, TEXT_COLOR, |x, y, w, h, color| { 53 | let a = color.a(); 54 | if a == 0 || x < 0 || x >= width as i32 || y < 0 || y >= height as i32 || w != 1 || h != 1 { 55 | // Ignore alphas of 0, or invalid x, y coordinates, or unimplemented sizes 56 | return; 57 | } 58 | 59 | // Scale by alpha (mimics blending with black) 60 | let scale = |c: u8| (c as i32 * a as i32 / 255).clamp(0, 255) as u8; 61 | 62 | let r = scale(color.r()); 63 | let g = scale(color.g()); 64 | let b = scale(color.b()); 65 | canvas[y as usize][x as usize] = Some((r, g, b)); 66 | }); 67 | 68 | // Render the canvas 69 | let mut output = String::new(); 70 | 71 | for row in canvas { 72 | for pixel in row { 73 | let (r, g, b) = pixel.unwrap_or((0, 0, 0)); 74 | write!(&mut output, "{}", " ".on_truecolor(r, g, b)).ok(); 75 | } 76 | writeln!(&mut output).ok(); 77 | } 78 | 79 | print!("{}", output); 80 | } 81 | -------------------------------------------------------------------------------- /fonts/FiraMono-LICENSE: -------------------------------------------------------------------------------- 1 | Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /fonts/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5f9173ce3d05fadef74c7eed06570d54e4f75bd0cd9860726fb2987a7f848292 3 | size 173516 4 | -------------------------------------------------------------------------------- /fonts/Inter-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3127f0b873387ee37e2040135a06e9e9c05030f509eb63689529becf28b50384 3 | size 310252 4 | -------------------------------------------------------------------------------- /fonts/NotoSans-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Google Inc. All Rights Reserved. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /fonts/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2ec33f84606cbaa0a1a944488e14f97faf2f6a25ecdd8354f5358f06da13c7d9 3 | size 556216 4 | -------------------------------------------------------------------------------- /fonts/NotoSansArabic.ttf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ee489b994b3e62def9874c918145e32b133b625abaf98cec60502bdb40102c56 3 | size 765740 4 | -------------------------------------------------------------------------------- /fonts/NotoSansHebrew.ttf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3d4fef85b449ade4d165de982969374fa30b2a5fe7bc679f5a3f5bfc047fb703 3 | size 183688 4 | -------------------------------------------------------------------------------- /multiview.sh: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | RUST_LOG="cosmic_text=debug,multiview=debug" cargo run --release --package multiview -- "$@" 4 | -------------------------------------------------------------------------------- /redoxer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | rm -rf target/redoxer 6 | mkdir -p target/redoxer 7 | 8 | redoxer install \ 9 | --no-track \ 10 | --path examples/editor-orbclient \ 11 | --root "target/redoxer" 12 | 13 | args=(env RUST_LOG=cosmic_text=debug,editor_orbclient=debug /root/bin/editor-orbclient) 14 | if [ -f "$1" ] 15 | then 16 | filename="$(basename "$1")" 17 | cp "$1" "target/redoxer/${filename}" 18 | args+=("${filename}") 19 | fi 20 | 21 | cd target/redoxer 22 | 23 | # TODO: remove need for linking fonts 24 | redoxer exec \ 25 | --gui \ 26 | --folder . \ 27 | "${args[@]}" 28 | -------------------------------------------------------------------------------- /rich-text.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=cosmic_text=debug,rich_text=debug cargo run --release --package rich-text -- "$@" 2 | -------------------------------------------------------------------------------- /sample/arabic.txt: -------------------------------------------------------------------------------- 1 | I like to render اللغة العربية in Rust! 2 | 3 | عندما يريد العالم أن ‪يتكلّم ‬ ، فهو يتحدّث بلغة يونيكود. تسجّل الآن لحضور المؤتمر الدولي العاشر ليونيكود (Unicode Conference)، الذي سيعقد في 10-12 آذار 1997 بمدينة مَايِنْتْس، ألمانيا. و سيجمع المؤتمر بين خبراء من كافة قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء مناقشة سبل استخدام يونكود في النظم القائمة وفيما يخص التطبيقات الحاسوبية، الخطوط، تصميم النصوص والحوسبة متعددة اللغات. 4 | -------------------------------------------------------------------------------- /sample/bengali.txt: -------------------------------------------------------------------------------- 1 | ধারা ১ সমস্ত মানুষ স্বাধীনভাবে সমান মর্যাদা এবং অধিকার নিয়ে জন্মগ্রহণ করে। তাঁদের বিবেক এবং বুদ্ধি আছে; সুতরাং সকলেরই একে অপরের প্রতি ভ্রাতৃত্বসুলভ মনোভাব নিয়ে আচরণ করা উচিত। 2 | -------------------------------------------------------------------------------- /sample/crlf.txt: -------------------------------------------------------------------------------- 1 | These are two lines 2 | in a CRLF file 3 | -------------------------------------------------------------------------------- /sample/em.txt: -------------------------------------------------------------------------------- 1 | — — — — — — — — 2 |  — — — — — — — 3 | — — — — — — — — 4 |  — — — — — — — 5 | -------------------------------------------------------------------------------- /sample/emoji-zjw.txt: -------------------------------------------------------------------------------- 1 | I want more terminals to be able to handle ZWJ sequence emoji characters. For example, the service dog emoji 🐕‍🦺 is actually 3 Unicode characters. Kitty handles this fairly well. All VTE-based terminals, however, show "🐶🦺". 2 | -------------------------------------------------------------------------------- /sample/fallback.txt: -------------------------------------------------------------------------------- 1 | TE🤯STالعربيةTE🤣ST你好 2 | العربيةTE🤯STالعربيةTE🤣ST你好العربية 3 | -------------------------------------------------------------------------------- /sample/farsi.txt: -------------------------------------------------------------------------------- 1 | من از سیستم عامل پاپ! ۲۲.۰۴ استفاده میکنم. 2 | متن راست به چپ Left to right text ادامه‌ی متن راست به چپ با اعداد ۱/۲۳۴ و English numbers 1.234. 3 | مَتنِ فارسی و انگلیسی (English). 4 | کشیده نویسی (کشــــــــــــیده‌نویسی). 5 | علائم تُ تِ تَ تً تٍ تْ تٌ. 6 | لام. 7 | ویرگول (؛). 8 | تای تأنیث یا ه دو نقطه (ة). 9 | علامت تشدید (ــّـ). 10 | Testing LEFT‑TO‑RIGHT ISOLATE (U+2066) and POP DIRECTIONAL ISOLATE (U+2069): 11 | He said: "بهتره از ⁦Rust⁩ استفاده کنی". 12 | -------------------------------------------------------------------------------- /sample/han.txt: -------------------------------------------------------------------------------- 1 | https://en.wikipedia.org/wiki/Han_unification#Examples_of_language-dependent_glyphs 2 | 今 3 | 令 4 | 免 5 | 入 6 | 全 7 | 关 8 | 具 9 | 刃 10 | 化 11 | 外 12 | 情 13 | 才 14 | 抵 15 | 次 16 | 海 17 | 画 18 | 直 19 | 真 20 | 示 21 | 神 22 | 空 23 | 者 24 | 草 25 | 蔥 26 | 角 27 | 道 28 | 雇 29 | 骨 30 | -------------------------------------------------------------------------------- /sample/hebrew.txt: -------------------------------------------------------------------------------- 1 | כאשר העולם רוצה לדבר, הוא מדבר ב־Unicode. הירשמו כעת לכנס Unicode הבינלאומי העשירי, שייערך בין התאריכים 12־10 במרץ 1997, בְּמָיְינְץ שבגרמניה. בכנס ישתתפו מומחים מכל ענפי התעשייה בנושא האינטרנט העולמי וה־Unicode, בהתאמה לשוק הבינלאומי והמקומי, ביישום Unicode במערכות הפעלה וביישומים, בגופנים, בפריסת טקסט ובמחשוב רב־לשוני. 2 | 3 | Many computer programs fail to display bidirectional text correctly. For example, this page is mostly LTR English script, and here is the RTL Hebrew name Sarah: שרה, spelled sin (ש) on the right, resh (ר) in the middle, and heh (ה) on the left. 4 | -------------------------------------------------------------------------------- /sample/hello.txt: -------------------------------------------------------------------------------- 1 | https://en.wiktionary.org/wiki/hello#Translations 2 | Abkhaz: бзиа збаша (bzia zbaša), мыш бзи (məš bzi), (to a man) бзиара убааит (bziara ubaaiṭ), (to a woman) бзиара ббааит (bziara bbaaiṭ), (to more than one person) бзиара жәбааит pl (bziara ž°baaiṭ) 3 | Afrikaans: hallo (af), goeiedag 4 | Ainu: イランカラㇷ゚テ (irankarapte) 5 | Albanian: tungjatjeta (sq), tung (informal), ç'kemi m or f 6 | 7 | Arbëreshë Albanian: falem (sq) 8 | 9 | Alemannic German: sälü, hoi, hello 10 | Aleut: aang, draas 11 | Ambonese Malay: wai 12 | American Sign Language: B@Sfhead-PalmForward B@FromSfhead-PalmForward 13 | Amharic: ሰላም (sälam) 14 | Apache: 15 | 16 | Jicarilla: dá nzhǫ́ 17 | Western Apache: dagotʼee, daʼanzho, yaʼateh 18 | 19 | Arabic: السَّلَامُ عَلَيْكُمْ‎ (ar) (as-salāmu ʿalaykum), سَلَام‎ (ar) (salām), مَرْحَبًا‎ (ar) (marḥaban), أَهْلًا‎ (ar) (ʾahlan) 20 | 21 | Egyptian Arabic: اهلاً‎ (ahlan) 22 | Iraqi Arabic: هلو‎ (helaww) 23 | 24 | Archi: салам алейкум (salam alejkum), варчӀами (warčʼami) 25 | Armenian: բարև (hy) (barew), ողջույն (hy) (ołǰuyn) 26 | Assamese: নমস্কাৰ (nomoskar) (very formal), আচ্চেলামো আলাইকোম (asselamü alaiküm) (formal, used among Muslims), হেল’ (helö) 27 | Assyrian Neo-Aramaic: ܫܠܵܡܵܐ‎ (šlama), (to a man) ܫܠܵܡܵܐ ܥܲܠܘܼܟ݂‎ (šlama ʿāloḳ), (to a woman) ܫܠܵܡܵܐ ܥܲܠܵܟ݂ܝ‎ (šlama ʿālaḳ), (to more than one person) ܫܠܵܡܵܐ ܥܲܠܵܘܟ݂ܘܿܢ‎ (šlama ʿāloḳon) 28 | Asturian: hola (ast) 29 | Azerbaijani: salam (az), səlam (South Azerbaijani), hər vaxtınız xeyir olsun, hər vaxtınız xeyir 30 | Bashkir: сәләм (säläm) 31 | Basque: kaixo (eu) 32 | Bats: please add this translation if you can 33 | Bavarian: servus, grias di, pfiati (Timau) 34 | Belarusian: віта́ю (vitáju), здаро́ў (zdaróŭ) (colloquial), до́бры дзень (dóbry dzjenʹ) (good day) 35 | Bengali: নমস্কার (bn) (nômôśkar), আসসালামুআলাইকুম (aśśalamualaikum), সালাম (śalam), হ্যালো (hjalo) 36 | Bhojpuri: प्रणाम (praṇām) 37 | Bouyei: mengz ndil 38 | Bulgarian: здра́сти (bg) (zdrásti) (familiar), здраве́й (bg) sg (zdravéj) (familiar), здраве́йте (bg) pl (zdravéjte) (formal) 39 | Burmese: မင်္ဂလာပါ (my) (mangga.lapa) , ဟဲလို (my) (hai:lui) (colloquial) 40 | Catalan: hola (ca) 41 | Cayuga: sgę́:nǫʔ 42 | Central Atlas Tamazight: ⴰⵣⵓⵍ (azul) 43 | Chamorro: håfa adai 44 | Chechen: маршалла ду хьоьга (maršalla du ḥöga) (to one person), маршалла ду шуьга (maršalla du šüga) (to a group of people), ассаламу ӏалайкум (assalamu ʿalajkum) 45 | Cherokee: ᎣᏏᏲ (chr) (osiyo) 46 | Chichewa: moni 47 | Chickasaw: chokma 48 | Chinese: 49 | 50 | Cantonese: 你好 (nei5 hou2), 哈佬 (haa1 lou2) 51 | Dungan: ни хо (ni ho), сэляму (seli͡amu), хома (homa) 52 | Hakka: 你好 (ngì-hó) 53 | Mandarin: 你好 (zh) (nǐ hǎo), 您好 (zh) (nín hǎo) (polite), 你們好, 你们好 (nǐmen hǎo) (to a group of people), 好 (zh) (hǎo) (following an address form or name), 嗨 (zh) (hāi), 哈囉 (zh), 哈啰 (zh) (hāluó) 54 | Min Dong: 汝好 (nṳ̄ hō̤) 55 | Min Nan: 汝好 (lí hó) 56 | Xiang: please add this translation if you can 57 | Wu: 儂好, 侬好 (non hau) 58 | 59 | Choctaw: halito 60 | Chukchi: еттык (ettyk) (formal), ети (eti) (informal), етти (etti) (informal) 61 | Coptic: ⲛⲟϥⲣⲓ (nofri) 62 | Cornish: dydh da 63 | Corsican: bonghjornu 64 | Cree: 65 | 66 | Plains Cree: tânisi 67 | 68 | Czech: ahoj (cs), nazdar (cs) (informal), servus (cs) (informal), dobrý den (cs) (formal) 69 | Danish: hej (da), dav (da), god dag (formal), hallo (da) 70 | Dhivehi: އައްސަލާމު ޢަލައިކުމް‎ (assalāmu ʿalaikum̊) 71 | Dutch: hallo (nl), hoi (nl), hai (nl), hé (nl), dag (nl) (informal), goeiedag (nl), goededag (nl), goedendag (nl), goeiendag (nl) (formal) 72 | Esperanto: saluton (eo) 73 | Estonian: tere (et), hei (et) 74 | Faroese: hey, halló 75 | Fijian: bula (fj) 76 | Finnish: terve (fi), moi (fi), hei (fi), moikka (fi) 77 | French: bonjour (fr), salut (fr) (informal), coucou (fr)(informal), cocorico (fr) 78 | Friulian: mandi 79 | Galician: ola (gl), oula, ouga 80 | Georgian: გამარჯობა (ka) (gamarǯoba), ჰეი (hei) 81 | German: hallo (de), guten Tag (de), servus (de), moin (de), grüß Gott (de) (Southern German, Austria) 82 | 83 | Alemannic German: grüezi 84 | 85 | Gilbertese: mauri 86 | Gothic: 𐌷𐌰𐌹𐌻𐍃 (hails), 𐌷𐌰𐌹𐌻𐌰 (haila) 87 | Greek: γεια (el) (geia), γεια σου sg (geia sou), γεια σας pl (geia sas), χαίρετε (el) (chaírete) 88 | 89 | Ancient: χαῖρε sg (khaîre), χαίρετε pl (khaírete), χαῖρε καί ὑγίαινε sg (khaîre kaí hugíaine) 90 | 91 | Greenlandic: aluu (kl) 92 | Guaraní: maitei (gn) 93 | Gujarati: નમસ્તે (namaste), નમસ્કાર (namaskār) 94 | Haitian Creole: bonjou 95 | Hausa: sannu 96 | Hawaiian: aloha 97 | Hebrew: שָׁלוֹם‎ (he) (shalóm), שָׁלוֹם עָלֵיכֶם‎ (he) (shalóm 'aleikhém) 98 | Hindi: नमस्ते (hi) (namaste), नमस्कार (hi) (namaskār), सलाम (hi) (salām) (used by Muslims), सत श्री अकाल (sat śrī akāl) (Sikh, hello/goodbye), हेलो (hi) (helo), हलो (halo), सत्य (hi) (satya), आदाब (hi) (ādāb) 99 | Hmong: 100 | Green Hmong: nyob zoo 101 | White Hmong: nyob zoo 102 | Hungarian: szia (hu), sziasztok (hu) pl (informal), szervusz (hu), szervusztok pl (somewhat formal), heló (hu), helló (hu) (informal), jó napot (hu), jó napot kívánok (hu) (formal), üdvözlöm 103 | Icelandic: halló (is), hæ (is), góðan dag (is), góðan daginn (is) 104 | Ido: hola (io) 105 | Igbo: kèdu 106 | Indonesian: hai (id), salam (id) 107 | Interlingua: bon die, salute (ia) 108 | Irish: Dia dhuit (formal, singular), Dia dhaoibh (formal, plural), Dia's Muire dhuit (formal, singular, response), Dia's Muire dhaoibh (formal, plural, response) 109 | Isan: please add this translation if you can 110 | Italian: ciao (it), salve (it), buongiorno (it), saluti (it) m pl 111 | Iu Mien: yiem longx nyei 112 | Jamaican Creole: ello, wah gwaan 113 | Japanese: おはよう (ja) (ohayō) (morning), こんにちは (ja) (konnichi wa) (daytime), こんばんは (ja) (konban wa) (evening) 114 | Javanese: halo 115 | Jeju: 반갑수다 (ban-gapsuda), 펜안ᄒᆞ우꽈 (pen-anhawukkwa), 펜안 (pen-an) 116 | Judeo-Tat: шолум (şolum) 117 | Kabardian: уузыншэм (wwzənšăm) 118 | Kabyle: azul 119 | Kalmyk: мендвт (mendvt), менд (mend) (informal) 120 | Kannada: ತುಳಿಲು (kn) (tuḷilu), ನಮಸ್ಕಾರ (kn) (namaskāra) 121 | Karachay-Balkar: кюнюгюз ашхы болсун (künügüz aşxı bolsun), ассаламу алейкум (assalamu aleykum) 122 | Karelian: terveh, hei 123 | Kazakh: сәлем (kk) (sälem) (informal), сәлеметсіздер (sälemetsızder) (formal) 124 | Khmer: ជំរាបសួរ (cumriəp suə), សួស្តី (suəsdəy) 125 | Khün: please add this translation if you can 126 | Kinyarwanda: muraho 127 | Korean: 안녕하십니까 (annyeonghasimnikka) (formal), 안녕하세요 (ko) (annyeonghaseyo) (neutrally formal), 안녕(安寧) (ko) (annyeong) (informal) 128 | Krio: kushɛ 129 | Kurdish: 130 | 131 | Northern Kurdish: merheba (ku), silav (ku), selam (ku) 132 | 133 | Kyrgyz: саламатсыздарбы (salamatsızdarbı), салам (ky) (salam) 134 | Ladino: shalom, bonjur, buenos diyas 135 | Lak: салам (salam) 136 | Lakota: háu 137 | Lao: ສະບາຍດີ (sa bāi dī) 138 | Latin: salvē (la) sg, salvēte (la) pl; avē (la) sg, avēte pl 139 | Latvian: sveiki (informal to more than one person or people of indeterminate gender), sveiks (to a man), sveika (to a woman), čau (informal) 140 | Laz: გეგაჯგინას (gegaǯginas) 141 | Lezgi: салам (salam) 142 | Lithuanian: labas (lt), sveikas (lt) (informal), sveiki (lt) (formal) 143 | Livonian: tēriņtš 144 | Luo: msawa 145 | Lü: ᦍᦲᧃᦡᦲ (yiinḋii) 146 | Luxembourgish: hallo 147 | Macedonian: здраво (zdravo) 148 | Malagasy: manao ahoana? (mg), salama (mg) (Tsimihety) 149 | Malay: helo (ms), apa khabar (ms), salam (ms) 150 | Malayalam: ഹലോ (halō), നമസ്തേ (ml) (namastē), നമസ്കാരം (ml) (namaskāraṃ) 151 | Maltese: bonġu (mt) (before noon), bonswa (after noon), nsellimlek (formal one to one person), nsellmilkom (formal one to more than one person), nsellmulek (formal more than one person to one person), nsellmulkom (formal more than one person to more than one persons) 152 | Manchu: ᠰᠠᡳᠶᡡᠨ (saiyūn) 153 | Maori: kia ora (mi) (informal), tēnā koe (formal to one person), tēnā kōrua (formal to two people), tēnā koutou (formal to three or more people) 154 | Mapudungun: mari mari 155 | Maranungku: yo 156 | Marathi: नमस्कार (mr) (namaskār) 157 | Michif: tánishi, boñjour 158 | Mingrelian: გომორძგუა (gomorʒgua) 159 | Mohawk: sekoh 160 | Mongolian: 161 | 162 | Cyrillic: сайн уу? (mn) (sayn uu?) (informal), сайн байна уу? (mn) (sayn bayna uu?) 163 | 164 | Mopan Maya: dʼyoos 165 | Nahuatl: niltze (nah), panoltih 166 | Navajo: yáʼátʼééh 167 | Neapolitan: uè 168 | Nepali: नमस्ते (ne) (namaste), नमस्कार (ne) (namaskār) 169 | Norman: baon-n-jour (Guernsey), banjour (Guernsey), boujouo (continental Normandy), bouônjour (Jersey), bwõju (Sark) 170 | Northern Thai: สบายดีก่อ 171 | Norwegian: 172 | 173 | Bokmål: hallo (no), hei (no), god dag (no) (formal), halla (no) (informal), heisann 174 | 175 | Ojibwe: boozhoo 176 | Okinawan: はいさい m (haisai), はいたい f (haitai), はい n (hai) 177 | Old English: wes hāl 178 | Oriya: ନମସ୍କାର (or) (nômôskarô) 179 | Ossetian: салам (salam), байрай (bajraj), арфӕ (arfæ) 180 | Palauan: alii 181 | Pashto: سلام‎ (ps) (salām), سلام الېک‎ (slāmālék), السلام عليکم‎ (as-salám alaykúm) 182 | Persian: سلام‎ (fa) (salâm), سلام علیکم‎ (salâmo alaykom) (religious), درود‎ (fa) (dorud) (literary) 183 | Pitcairn-Norfolk: watawieh 184 | Polish: cześć (pl) (informal), witaj (pl), witajcie, witam (more formal), dzień dobry (pl) (formal), siema (pl) (informal), halo (pl) (on phone), serwus (pl) (colloquial), cześka (colloquial), siemanero (colloquial) 185 | Portuguese: oi (pt), olá (pt), (slang) e aí? (pt) 186 | Punjabi: ਸਤਿ ਸ਼੍ਰੀ ਅਕਾਲ (sati śrī akāl) 187 | Rapa Nui: 'iorana 188 | Romani: te aves baxtalo (to a male), te aves baxtali (to a female), te aven baxtale (to two or more people) 189 | Romanian: salut (ro), bună (ro), noroc (ro) (informal), bună ziua (formal), servus (ro) 190 | Russian: приве́т (ru) (privét) (informal), здоро́во (ru) (zdoróvo) (colloquial), здра́вствуйте (ru) (zdrávstvujte) (formal, first "в" is silent), до́брый день (ru) (dóbryj denʹ), здра́вствуй (ru) (zdrávstvuj) (informal, first "в" is silent), салю́т (ru) (saljút) 191 | Rusyn: наздар (nazdar) 192 | Sami: 193 | 194 | Inari Sami: tiervâ 195 | Northern: dearvva, būres 196 | Skolt: tiõrv 197 | Southern: buaregh 198 | 199 | Samoan: talofa 200 | Sanskrit: नमस्कार (sa) (namaskāra), नमस्ते (namaste), नमो नमः (namo namaḥ) (formal) 201 | Scots: hullo 202 | Scottish Gaelic: halò (informal), latha math (formal), (informal) hòigh 203 | Serbo-Croatian: 204 | 205 | Cyrillic: здра̏во, ћа̑о, ме̏рха̄ба, селам, бог, бок 206 | Roman: zdrȁvo (sh), ćȃo (sh), mȅrhāba, selam (sh), bog, bok (sh) 207 | 208 | Sesotho: lumela 209 | Shan: please add this translation if you can 210 | Shona: mhoro 211 | Sichuan Yi: please add this translation if you can 212 | Sicilian: ciao, salutamu 213 | Sindhi: هيلو‎ 214 | Sinhalese: හලෝ (halō), ආයුබෝවන් (si) (āyubōwan) 215 | Situ: please add this translation if you can 216 | Slovak: ahoj (sk), nazdar (informal), servus (sk) (informal), dobrý deň (formal) 217 | Slovene: žívjo, zdrávo (informal), dóber dán, pozdravljeni (formal) 218 | Somali: ma nabad baa, waa nabad 219 | Sorbian: 220 | 221 | Lower Sorbian: dobry źeń 222 | 223 | Sotho: dumela (st) 224 | Spanish: hola (es), buenos días (es), qué tal, buenas tardes (es) 225 | Sundanese: halo 226 | Svan: ხოჩა ლადა̈ღ (xoča ladäɣ) 227 | Swahili: jambo (sw), salaam 228 | Swedish: hallå (sv), hej (sv), god dag (sv) (formal), tjena (sv), hejsan (sv) (informal), tja (sv) 229 | Tagalog: kamusta (tl)/kumusta (tl), musta (tl) (slang), hoy (tl), huy, oy/oi (informal), uy/ui (informal) 230 | Tajik: салом (salom) 231 | Tamil: வணக்கம் (ta) (vaṇakkam) 232 | Tangsa: äshazhoix 233 | Tatar: сәлам (tt) (sälam) 234 | Telugu: నమసకారం (namasakāraṁ), బాగున్నారా (bāgunnārā) 235 | Tetum: please add this translation if you can 236 | Thai: สวัสดี (th) (sà-wàt-dii), สวัสดีครับ (male speaker), สวัสดีค่ะ (female speaker), หวัดดี (wàt-dii) 237 | Tibetan: བཀྲ་ཤིས་བདེ་ལེགས (bkra shis bde legs) 238 | Tigrinya: ሰላም (sälam) 239 | Tongan: mālō e lelei 240 | Tswana: dumela (tn) (singular, as in dumela, rra, "hello sir"), dumelang (tn) (plural, as in dumelang, borra, "hello gentlemen") 241 | Turkish: merhaba (tr), selam (tr) 242 | Turkmen: salam 243 | Tuvan: экии (ekii) 244 | Udmurt: ӟеч (dźeć), чырткем (ćyrtkem), умой (umoj) 245 | Ukrainian: приві́т (uk) (pryvít) (informal), здоро́в був (uk) (zdoróv buv) (informal), добри́день (uk) (dobrýdenʹ) (neutral or formal), чоло́м (čolóm) 246 | Urdu: سلام علیکم‎ (salām-o-alaikum), اسلام علیکم‎ (literally “Peace be upon you”), اسلام علیکم ورحمۃاللہ وبرکاتہ‎ (literally “Peace be upon you & May Allah bless”), آداب‎ (ur) (ādāb) 247 | Uyghur: سالام‎ (salam) 248 | Uzbek: salom (uz) 249 | Venetian: ciao (vec) 250 | Vietnamese: xin chào (vi), chào (vi) 251 | Volapük: glidis 252 | Walloon: bondjoû (wa), a (wa), diewåde (wa) (old) 253 | Welsh: helo (cy), bore da (good morning), dydd da (good day), hylo 254 | West Frisian: hallo, hoi 255 | Winnebago: haho (male speaker), hą (female speaker), hinįkaraginʼ 256 | Xhosa: molo sg, molweni pl 257 | Xibe: ᠪᠠᡳᡨᠠᡴᡡ 258 | ᠨᠠ (baitakū na) 259 | Yakut: эҕэрдэ (eğerde), дорообо (doroobo) (informal) 260 | Yiddish: שלום־עליכם‎ (sholem-aleykhem), אַ גוטן‎ (yi) (a gutn), גוט־מאָרגן‎ (yi) (gut-morgn) 261 | Yoruba: Pẹlẹ o 262 | Yup'ik: waqaa, cama-i 263 | Zapotec: padiull 264 | Zazaki: sılam, namaste 265 | Zhuang: mwngz ndei 266 | Zulu: sawubona (zu) (familiar), sanibonani (plural, respectful) 267 | -------------------------------------------------------------------------------- /sample/ligature.txt: -------------------------------------------------------------------------------- 1 | fi ffi 🐕‍🦺 fi ffi 2 | fi تما 🐕‍🦺 ffi تما 3 | ffi fi 🐕‍🦺 ffi fi 4 | تما تما 🐕‍🦺 تما 5 | تما ffi 🐕‍🦺 تما fi تما 6 | تما تما 🐕‍🦺 تما 7 | -------------------------------------------------------------------------------- /sample/lioneatingpoet.txt: -------------------------------------------------------------------------------- 1 | 《施氏食狮史》 2 | 石室诗士施氏,嗜狮,誓食十狮。 3 | 氏时时适市视狮。 4 | 十时,适十狮适市。 5 | 是时,适施氏适市。 6 | 氏视是十狮,恃矢势,使是十狮逝世。 7 | 氏拾是十狮尸,适石室。 8 | 石室湿,氏使侍拭石室。 9 | 石室拭,氏始试食是十狮。 10 | 食时,始识是十狮尸,实十石狮尸。 11 | 试释是事。 12 | -------------------------------------------------------------------------------- /sample/mono.txt: -------------------------------------------------------------------------------- 1 | https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt 2 | 3 | UTF-8 encoded sample plain-text file 4 | ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 5 | 6 | Markus Kuhn [ˈmaʳkʊs kuːn] — 2002-07-25 CC BY 7 | 8 | 9 | The ASCII compatible UTF-8 encoding used in this plain-text file 10 | is defined in Unicode, ISO 10646-1, and RFC 2279. 11 | 12 | 13 | Using Unicode/UTF-8, you can write in emails and source code things such as 14 | 15 | Mathematics and sciences: 16 | 17 | ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ⎧⎡⎛┌─────┐⎞⎤⎫ 18 | ⎪⎢⎜│a²+b³ ⎟⎥⎪ 19 | ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β), ⎪⎢⎜│───── ⎟⎥⎪ 20 | ⎪⎢⎜⎷ c₈ ⎟⎥⎪ 21 | ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ, ⎨⎢⎜ ⎟⎥⎬ 22 | ⎪⎢⎜ ∞ ⎟⎥⎪ 23 | ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (⟦A⟧ ⇔ ⟪B⟫), ⎪⎢⎜ ⎲ ⎟⎥⎪ 24 | ⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪ 25 | 2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm ⎩⎣⎝i=1 ⎠⎦⎭ 26 | 27 | Linguistics and dictionaries: 28 | 29 | ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn 30 | Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ] 31 | 32 | APL: 33 | 34 | ((V⍳V)=⍳⍴V)/V←,V ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈ 35 | 36 | Nicer typography in plain text files: 37 | 38 | ╔══════════════════════════════════════════╗ 39 | ║ ║ 40 | ║ • ‘single’ and “double” quotes ║ 41 | ║ ║ 42 | ║ • Curly apostrophes: “We’ve been here” ║ 43 | ║ ║ 44 | ║ • Latin-1 apostrophe and accents: '´` ║ 45 | ║ ║ 46 | ║ • ‚deutsche‘ „Anführungszeichen“ ║ 47 | ║ ║ 48 | ║ • †, ‡, ‰, •, 3–4, —, −5/+5, ™, … ║ 49 | ║ ║ 50 | ║ • ASCII safety test: 1lI|, 0OD, 8B ║ 51 | ║ ╭─────────╮ ║ 52 | ║ • the euro symbol: │ 14.95 € │ ║ 53 | ║ ╰─────────╯ ║ 54 | ╚══════════════════════════════════════════╝ 55 | 56 | Combining characters: 57 | 58 | STARGΛ̊TE SG-1, a = v̇ = r̈, a⃑ ⊥ b⃑ 59 | 60 | Greek (in Polytonic): 61 | 62 | The Greek anthem: 63 | 64 | Σὲ γνωρίζω ἀπὸ τὴν κόψη 65 | τοῦ σπαθιοῦ τὴν τρομερή, 66 | σὲ γνωρίζω ἀπὸ τὴν ὄψη 67 | ποὺ μὲ βία μετράει τὴ γῆ. 68 | 69 | ᾿Απ᾿ τὰ κόκκαλα βγαλμένη 70 | τῶν ῾Ελλήνων τὰ ἱερά 71 | καὶ σὰν πρῶτα ἀνδρειωμένη 72 | χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά! 73 | 74 | From a speech of Demosthenes in the 4th century BC: 75 | 76 | Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι, 77 | ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς 78 | λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ 79 | τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿ 80 | εἰς τοῦτο προήκοντα, ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ 81 | πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν 82 | οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι, 83 | οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν 84 | ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον 85 | τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι 86 | γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν 87 | προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους 88 | σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ 89 | τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ 90 | τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς 91 | τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον. 92 | 93 | Δημοσθένους, Γ´ ᾿Ολυνθιακὸς 94 | 95 | Georgian: 96 | 97 | From a Unicode conference invitation: 98 | 99 | გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო 100 | კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს, 101 | ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს 102 | ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი, 103 | ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება 104 | ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში, 105 | ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში. 106 | 107 | Russian: 108 | 109 | From a Unicode conference invitation: 110 | 111 | Зарегистрируйтесь сейчас на Десятую Международную Конференцию по 112 | Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии. 113 | Конференция соберет широкий круг экспертов по вопросам глобального 114 | Интернета и Unicode, локализации и интернационализации, воплощению и 115 | применению Unicode в различных операционных системах и программных 116 | приложениях, шрифтах, верстке и многоязычных компьютерных системах. 117 | 118 | Thai (UCS Level 2): 119 | 120 | Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese 121 | classic 'San Gua'): 122 | 123 | [----------------------------|------------------------] 124 | ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช พระปกเกศกองบู๊กู้ขึ้นใหม่ 125 | สิบสองกษัตริย์ก่อนหน้าแลถัดไป สององค์ไซร้โง่เขลาเบาปัญญา 126 | ทรงนับถือขันทีเป็นที่พึ่ง บ้านเมืองจึงวิปริตเป็นนักหนา 127 | โฮจิ๋นเรียกทัพทั่วหัวเมืองมา หมายจะฆ่ามดชั่วตัวสำคัญ 128 | เหมือนขับไสไล่เสือจากเคหา รับหมาป่าเข้ามาเลยอาสัญ 129 | ฝ่ายอ้องอุ้นยุแยกให้แตกกัน ใช้สาวนั้นเป็นชนวนชื่นชวนใจ 130 | พลันลิฉุยกุยกีกลับก่อเหตุ ช่างอาเพศจริงหนาฟ้าร้องไห้ 131 | ต้องรบราฆ่าฟันจนบรรลัย ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ 132 | 133 | (The above is a two-column text. If combining characters are handled 134 | correctly, the lines of the second column should be aligned with the 135 | | character above.) 136 | 137 | Ethiopian: 138 | 139 | Proverbs in the Amharic language: 140 | 141 | ሰማይ አይታረስ ንጉሥ አይከሰስ። 142 | ብላ ካለኝ እንደአባቴ በቆመጠኝ። 143 | ጌጥ ያለቤቱ ቁምጥና ነው። 144 | ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው። 145 | የአፍ ወለምታ በቅቤ አይታሽም። 146 | አይጥ በበላ ዳዋ ተመታ። 147 | ሲተረጉሙ ይደረግሙ። 148 | ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል። 149 | ድር ቢያብር አንበሳ ያስር። 150 | ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም። 151 | እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም። 152 | የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ። 153 | ሥራ ከመፍታት ልጄን ላፋታት። 154 | ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል። 155 | የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ። 156 | ተንጋሎ ቢተፉ ተመልሶ ባፉ። 157 | ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው። 158 | እግርህን በፍራሽህ ልክ ዘርጋ። 159 | 160 | Runes: 161 | 162 | ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ 163 | 164 | (Old English, which transcribed into Latin reads 'He cwaeth that he 165 | bude thaem lande northweardum with tha Westsae.' and means 'He said 166 | that he lived in the northern land near the Western Sea.') 167 | 168 | Braille: 169 | 170 | ⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌ 171 | 172 | ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞ 173 | ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎ 174 | ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂ 175 | ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙ 176 | ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑ 177 | ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲ 178 | 179 | ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ 180 | 181 | ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹ 182 | ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞ 183 | ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕ 184 | ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹ 185 | ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎ 186 | ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎ 187 | ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳ 188 | ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞ 189 | ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ 190 | 191 | (The first couple of paragraphs of "A Christmas Carol" by Dickens) 192 | 193 | Compact font selection example text: 194 | 195 | ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789 196 | abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ 197 | –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд 198 | ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა 199 | 200 | Greetings in various languages: 201 | 202 | Hello world, Καλημέρα κόσμε, コンニチハ 203 | 204 | Box drawing alignment tests: █ 205 | ▉ 206 | ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ 207 | ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ 208 | ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ 209 | ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ 210 | ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ 211 | ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ 212 | ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ ▗▄▖▛▀▜ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ 213 | ▝▀▘▙▄▟ 214 | -------------------------------------------------------------------------------- /sample/tabs.txt: -------------------------------------------------------------------------------- 1 | Tabs: 2 | One Two Three Four 3 | Two Three Four 4 | Three Four 5 | Four 6 | -------------------------------------------------------------------------------- /sample/thai.txt: -------------------------------------------------------------------------------- 1 | ข้อ 1 มนุษย์ทั้งหลายเกิดมามีอิสระและเสมอภาคกันในเกียรติศักด[เกียรติศักดิ์]และสิทธิ ต่างมีเหตุผลและมโนธรรม และควรปฏิบัติต่อกันด้วยเจตนารมณ์แห่งภราดรภาพ 2 | 3 | ข้อ 2 ทุกคนย่อมมีสิทธิและอิสรภาพบรรดาที่กำหนดไว้ในปฏิญญานี้ โดยปราศจากความแตกต่างไม่ว่าชนิดใด ๆ ดังเช่น เชื้อชาติ ผิว เพศ ภาษา ศาสนา ความคิดเห็นทางการเมืองหรือทางอื่น เผ่าพันธุ์แห่งชาติ หรือสังคม ทรัพย์สิน กำเนิด หรือสถานะอื่น ๆ อนึ่งจะไม่มีความแตกต่างใด ๆ ตามมูลฐานแห่งสถานะทางการเมือง ทางการศาล หรือทางการระหว่างประเทศของประเทศหรือดินแดนที่บุคคลสังกัด ไม่ว่าดินแดนนี้จะเป็นเอกราช อยู่ในความพิทักษ์มิได้ปกครองตนเอง หรืออยู่ภายใต้การจำกัดอธิปไตยใด ๆ ทั้งสิ้น 4 | -------------------------------------------------------------------------------- /screenshots/arabic.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:38994360da746e9331c0f0ffcc637d3342be5d0d568c25ffeb7d908b5c40be95 3 | size 213934 4 | -------------------------------------------------------------------------------- /screenshots/chinese-simplified.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:84f494ab596674e29b47f4b367bd2695ef21d63010e568fa432edea4f94265ca 3 | size 263370 4 | -------------------------------------------------------------------------------- /screenshots/hindi.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:28ddfa78fffabea00d1caedee477b071c0a40064701d54ef9371b3a596d91fa5 3 | size 256033 4 | -------------------------------------------------------------------------------- /src/bidi_para.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use unicode_bidi::{bidi_class, BidiClass, BidiInfo, ParagraphInfo}; 4 | 5 | /// An iterator over the paragraphs in the input text. 6 | /// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour. 7 | #[derive(Debug)] 8 | pub struct BidiParagraphs<'text> { 9 | text: &'text str, 10 | info: alloc::vec::IntoIter, 11 | } 12 | 13 | impl<'text> BidiParagraphs<'text> { 14 | /// Create an iterator to split the input text into paragraphs 15 | /// in accordance with `unicode-bidi` behaviour. 16 | pub fn new(text: &'text str) -> Self { 17 | let info = BidiInfo::new(text, None); 18 | let info = info.paragraphs.into_iter(); 19 | Self { text, info } 20 | } 21 | } 22 | 23 | impl<'text> Iterator for BidiParagraphs<'text> { 24 | type Item = &'text str; 25 | 26 | fn next(&mut self) -> Option { 27 | let para = self.info.next()?; 28 | let paragraph = &self.text[para.range]; 29 | // `para.range` includes the newline that splits the line, so remove it if present 30 | let mut char_indices = paragraph.char_indices(); 31 | if let Some(i) = char_indices.next_back().and_then(|(i, c)| { 32 | // `BidiClass::B` is a Paragraph_Separator (various newline characters) 33 | (bidi_class(c) == BidiClass::B).then_some(i) 34 | }) { 35 | Some(¶graph[0..i]) 36 | } else { 37 | Some(paragraph) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/buffer_line.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "std"))] 2 | use alloc::{string::String, vec::Vec}; 3 | use core::mem; 4 | 5 | use crate::{ 6 | Align, Attrs, AttrsList, Cached, FontSystem, LayoutLine, LineEnding, ShapeLine, Shaping, Wrap, 7 | }; 8 | 9 | /// A line (or paragraph) of text that is shaped and laid out 10 | #[derive(Clone, Debug)] 11 | pub struct BufferLine { 12 | text: String, 13 | ending: LineEnding, 14 | attrs_list: AttrsList, 15 | align: Option, 16 | shape_opt: Cached, 17 | layout_opt: Cached>, 18 | shaping: Shaping, 19 | metadata: Option, 20 | } 21 | 22 | impl BufferLine { 23 | /// Create a new line with the given text and attributes list 24 | /// Cached shaping and layout can be done using the [`Self::shape`] and 25 | /// [`Self::layout`] functions 26 | pub fn new>( 27 | text: T, 28 | ending: LineEnding, 29 | attrs_list: AttrsList, 30 | shaping: Shaping, 31 | ) -> Self { 32 | Self { 33 | text: text.into(), 34 | ending, 35 | attrs_list, 36 | align: None, 37 | shape_opt: Cached::Empty, 38 | layout_opt: Cached::Empty, 39 | shaping, 40 | metadata: None, 41 | } 42 | } 43 | 44 | /// Resets the current line with new internal values. 45 | /// 46 | /// Avoids deallocating internal caches so they can be reused. 47 | pub fn reset_new>( 48 | &mut self, 49 | text: T, 50 | ending: LineEnding, 51 | attrs_list: AttrsList, 52 | shaping: Shaping, 53 | ) { 54 | self.text = text.into(); 55 | self.ending = ending; 56 | self.attrs_list = attrs_list; 57 | self.align = None; 58 | self.shape_opt.set_unused(); 59 | self.layout_opt.set_unused(); 60 | self.shaping = shaping; 61 | self.metadata = None; 62 | } 63 | 64 | /// Get current text 65 | pub fn text(&self) -> &str { 66 | &self.text 67 | } 68 | 69 | /// Set text and attributes list 70 | /// 71 | /// Will reset shape and layout if it differs from current text and attributes list. 72 | /// Returns true if the line was reset 73 | pub fn set_text>( 74 | &mut self, 75 | text: T, 76 | ending: LineEnding, 77 | attrs_list: AttrsList, 78 | ) -> bool { 79 | let text = text.as_ref(); 80 | if text != self.text || ending != self.ending || attrs_list != self.attrs_list { 81 | self.text.clear(); 82 | self.text.push_str(text); 83 | self.ending = ending; 84 | self.attrs_list = attrs_list; 85 | self.reset(); 86 | true 87 | } else { 88 | false 89 | } 90 | } 91 | 92 | /// Consume this line, returning only its text contents as a String. 93 | pub fn into_text(self) -> String { 94 | self.text 95 | } 96 | 97 | /// Get line ending 98 | pub fn ending(&self) -> LineEnding { 99 | self.ending 100 | } 101 | 102 | /// Set line ending 103 | /// 104 | /// Will reset shape and layout if it differs from current line ending. 105 | /// Returns true if the line was reset 106 | pub fn set_ending(&mut self, ending: LineEnding) -> bool { 107 | if ending != self.ending { 108 | self.ending = ending; 109 | self.reset_shaping(); 110 | true 111 | } else { 112 | false 113 | } 114 | } 115 | 116 | /// Get attributes list 117 | pub fn attrs_list(&self) -> &AttrsList { 118 | &self.attrs_list 119 | } 120 | 121 | /// Set attributes list 122 | /// 123 | /// Will reset shape and layout if it differs from current attributes list. 124 | /// Returns true if the line was reset 125 | pub fn set_attrs_list(&mut self, attrs_list: AttrsList) -> bool { 126 | if attrs_list != self.attrs_list { 127 | self.attrs_list = attrs_list; 128 | self.reset_shaping(); 129 | true 130 | } else { 131 | false 132 | } 133 | } 134 | 135 | /// Get the Text alignment 136 | pub fn align(&self) -> Option { 137 | self.align 138 | } 139 | 140 | /// Set the text alignment 141 | /// 142 | /// Will reset layout if it differs from current alignment. 143 | /// Setting to None will use `Align::Right` for RTL lines, and `Align::Left` for LTR lines. 144 | /// Returns true if the line was reset 145 | pub fn set_align(&mut self, align: Option) -> bool { 146 | if align != self.align { 147 | self.align = align; 148 | self.reset_layout(); 149 | true 150 | } else { 151 | false 152 | } 153 | } 154 | 155 | /// Append line at end of this line 156 | /// 157 | /// The wrap setting of the appended line will be lost 158 | pub fn append(&mut self, other: Self) { 159 | let len = self.text.len(); 160 | self.text.push_str(other.text()); 161 | 162 | if other.attrs_list.defaults() != self.attrs_list.defaults() { 163 | // If default formatting does not match, make a new span for it 164 | self.attrs_list 165 | .add_span(len..len + other.text().len(), &other.attrs_list.defaults()); 166 | } 167 | 168 | for (other_range, attrs) in other.attrs_list.spans_iter() { 169 | // Add previous attrs spans 170 | let range = other_range.start + len..other_range.end + len; 171 | self.attrs_list.add_span(range, &attrs.as_attrs()); 172 | } 173 | 174 | self.reset(); 175 | } 176 | 177 | /// Split off new line at index 178 | pub fn split_off(&mut self, index: usize) -> Self { 179 | let text = self.text.split_off(index); 180 | let attrs_list = self.attrs_list.split_off(index); 181 | self.reset(); 182 | 183 | let mut new = Self::new(text, self.ending, attrs_list, self.shaping); 184 | new.align = self.align; 185 | new 186 | } 187 | 188 | /// Reset shaping, layout, and metadata caches 189 | pub fn reset(&mut self) { 190 | self.metadata = None; 191 | self.reset_shaping(); 192 | } 193 | 194 | /// Reset shaping and layout caches 195 | pub fn reset_shaping(&mut self) { 196 | self.shape_opt.set_unused(); 197 | self.reset_layout(); 198 | } 199 | 200 | /// Reset only layout cache 201 | pub fn reset_layout(&mut self) { 202 | self.layout_opt.set_unused(); 203 | } 204 | 205 | /// Shape line, will cache results 206 | #[allow(clippy::missing_panics_doc)] 207 | pub fn shape(&mut self, font_system: &mut FontSystem, tab_width: u16) -> &ShapeLine { 208 | if self.shape_opt.is_unused() { 209 | let mut line = self 210 | .shape_opt 211 | .take_unused() 212 | .unwrap_or_else(ShapeLine::empty); 213 | line.build( 214 | font_system, 215 | &self.text, 216 | &self.attrs_list, 217 | self.shaping, 218 | tab_width, 219 | ); 220 | self.shape_opt.set_used(line); 221 | self.layout_opt.set_unused(); 222 | } 223 | self.shape_opt.get().expect("shape not found") 224 | } 225 | 226 | /// Get line shaping cache 227 | pub fn shape_opt(&self) -> Option<&ShapeLine> { 228 | self.shape_opt.get() 229 | } 230 | 231 | /// Layout line, will cache results 232 | #[allow(clippy::missing_panics_doc)] 233 | pub fn layout( 234 | &mut self, 235 | font_system: &mut FontSystem, 236 | font_size: f32, 237 | width_opt: Option, 238 | wrap: Wrap, 239 | match_mono_width: Option, 240 | tab_width: u16, 241 | ) -> &[LayoutLine] { 242 | if self.layout_opt.is_unused() { 243 | let align = self.align; 244 | let mut layout = self 245 | .layout_opt 246 | .take_unused() 247 | .unwrap_or_else(|| Vec::with_capacity(1)); 248 | let shape = self.shape(font_system, tab_width); 249 | shape.layout_to_buffer( 250 | &mut font_system.shape_buffer, 251 | font_size, 252 | width_opt, 253 | wrap, 254 | align, 255 | &mut layout, 256 | match_mono_width, 257 | ); 258 | self.layout_opt.set_used(layout); 259 | } 260 | self.layout_opt.get().expect("layout not found") 261 | } 262 | 263 | /// Get line layout cache 264 | pub fn layout_opt(&self) -> Option<&Vec> { 265 | self.layout_opt.get() 266 | } 267 | 268 | /// Get line metadata. This will be None if [`BufferLine::set_metadata`] has not been called 269 | /// after the last reset of shaping and layout caches 270 | pub fn metadata(&self) -> Option { 271 | self.metadata 272 | } 273 | 274 | /// Set line metadata. This is stored until the next line reset 275 | pub fn set_metadata(&mut self, metadata: usize) { 276 | self.metadata = Some(metadata); 277 | } 278 | 279 | /// Makes an empty buffer line. 280 | /// 281 | /// The buffer line is in an invalid state after this is called. See [`Self::reset_new`]. 282 | pub(crate) fn empty() -> Self { 283 | Self { 284 | text: String::default(), 285 | ending: LineEnding::default(), 286 | attrs_list: AttrsList::new(&Attrs::new()), 287 | align: None, 288 | shape_opt: Cached::Empty, 289 | layout_opt: Cached::Empty, 290 | shaping: Shaping::Advanced, 291 | metadata: None, 292 | } 293 | } 294 | 295 | /// Reclaim attributes list memory that isn't needed any longer. 296 | /// 297 | /// The buffer line is in an invalid state after this is called. See [`Self::reset_new`]. 298 | pub(crate) fn reclaim_attrs(&mut self) -> AttrsList { 299 | mem::replace(&mut self.attrs_list, AttrsList::new(&Attrs::new())) 300 | } 301 | 302 | /// Reclaim text memory that isn't needed any longer. 303 | /// 304 | /// The buffer line is in an invalid state after this is called. See [`Self::reset_new`]. 305 | pub(crate) fn reclaim_text(&mut self) -> String { 306 | let mut text = mem::take(&mut self.text); 307 | text.clear(); 308 | text 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/cached.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | use core::mem; 3 | 4 | /// Helper for caching a value when the value is optionally present in the 'unused' state. 5 | #[derive(Clone, Debug)] 6 | pub enum Cached { 7 | Empty, 8 | Unused(T), 9 | Used(T), 10 | } 11 | 12 | impl Cached { 13 | /// Gets the value if in state `Self::Used`. 14 | pub fn get(&self) -> Option<&T> { 15 | match self { 16 | Self::Empty | Self::Unused(_) => None, 17 | Self::Used(t) => Some(t), 18 | } 19 | } 20 | 21 | /// Gets the value mutably if in state `Self::Used`. 22 | pub fn get_mut(&mut self) -> Option<&mut T> { 23 | match self { 24 | Self::Empty | Self::Unused(_) => None, 25 | Self::Used(t) => Some(t), 26 | } 27 | } 28 | 29 | /// Checks if the value is empty or unused. 30 | pub fn is_unused(&self) -> bool { 31 | match self { 32 | Self::Empty | Self::Unused(_) => true, 33 | Self::Used(_) => false, 34 | } 35 | } 36 | 37 | /// Checks if the value is used (i.e. cached for access). 38 | pub fn is_used(&self) -> bool { 39 | match self { 40 | Self::Empty | Self::Unused(_) => false, 41 | Self::Used(_) => true, 42 | } 43 | } 44 | 45 | /// Takes the buffered value if in state `Self::Unused`. 46 | pub fn take_unused(&mut self) -> Option { 47 | if matches!(*self, Self::Unused(_)) { 48 | let Self::Unused(val) = mem::replace(self, Self::Empty) else { 49 | unreachable!() 50 | }; 51 | Some(val) 52 | } else { 53 | None 54 | } 55 | } 56 | 57 | /// Takes the cached value if in state `Self::Used`. 58 | pub fn take_used(&mut self) -> Option { 59 | if matches!(*self, Self::Used(_)) { 60 | let Self::Used(val) = mem::replace(self, Self::Empty) else { 61 | unreachable!() 62 | }; 63 | Some(val) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | /// Moves the value from `Self::Used` to `Self::Unused`. 70 | #[allow(clippy::missing_panics_doc)] 71 | pub fn set_unused(&mut self) { 72 | if matches!(*self, Self::Used(_)) { 73 | *self = Self::Unused(self.take_used().expect("cached value should be used")); 74 | } 75 | } 76 | 77 | /// Sets the value to `Self::Used`. 78 | pub fn set_used(&mut self, val: T) { 79 | *self = Self::Used(val); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/cursor.rs: -------------------------------------------------------------------------------- 1 | /// Current cursor location 2 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] 3 | pub struct Cursor { 4 | /// Index of [`BufferLine`] in [`Buffer::lines`] 5 | pub line: usize, 6 | /// First-byte-index of glyph at cursor (will insert behind this glyph) 7 | pub index: usize, 8 | /// Whether to associate the cursor with the run before it or the run after it if placed at the 9 | /// boundary between two runs 10 | pub affinity: Affinity, 11 | } 12 | 13 | impl Cursor { 14 | /// Create a new cursor 15 | pub const fn new(line: usize, index: usize) -> Self { 16 | Self::new_with_affinity(line, index, Affinity::Before) 17 | } 18 | 19 | /// Create a new cursor, specifying the affinity 20 | pub const fn new_with_affinity(line: usize, index: usize, affinity: Affinity) -> Self { 21 | Self { 22 | line, 23 | index, 24 | affinity, 25 | } 26 | } 27 | } 28 | 29 | /// Whether to associate cursors placed at a boundary between runs with the run before or after it. 30 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] 31 | pub enum Affinity { 32 | #[default] 33 | Before, 34 | After, 35 | } 36 | 37 | impl Affinity { 38 | pub fn before(&self) -> bool { 39 | *self == Self::Before 40 | } 41 | 42 | pub fn after(&self) -> bool { 43 | *self == Self::After 44 | } 45 | 46 | pub fn from_before(before: bool) -> Self { 47 | if before { 48 | Self::Before 49 | } else { 50 | Self::After 51 | } 52 | } 53 | 54 | pub fn from_after(after: bool) -> Self { 55 | if after { 56 | Self::After 57 | } else { 58 | Self::Before 59 | } 60 | } 61 | } 62 | 63 | /// The position of a cursor within a [`Buffer`]. 64 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] 65 | pub struct LayoutCursor { 66 | /// Index of [`BufferLine`] in [`Buffer::lines`] 67 | pub line: usize, 68 | /// Index of [`LayoutLine`] in [`BufferLine::layout`] 69 | pub layout: usize, 70 | /// Index of [`LayoutGlyph`] in [`LayoutLine::glyphs`] 71 | pub glyph: usize, 72 | } 73 | 74 | impl LayoutCursor { 75 | /// Create a new [`LayoutCursor`] 76 | pub const fn new(line: usize, layout: usize, glyph: usize) -> Self { 77 | Self { 78 | line, 79 | layout, 80 | glyph, 81 | } 82 | } 83 | } 84 | 85 | /// A motion to perform on a [`Cursor`] 86 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 87 | pub enum Motion { 88 | /// Apply specific [`LayoutCursor`] 89 | LayoutCursor(LayoutCursor), 90 | /// Move cursor to previous character ([`Self::Left`] in LTR, [`Self::Right`] in RTL) 91 | Previous, 92 | /// Move cursor to next character ([`Self::Right`] in LTR, [`Self::Left`] in RTL) 93 | Next, 94 | /// Move cursor left 95 | Left, 96 | /// Move cursor right 97 | Right, 98 | /// Move cursor up 99 | Up, 100 | /// Move cursor down 101 | Down, 102 | /// Move cursor to start of line 103 | Home, 104 | /// Move cursor to start of line, skipping whitespace 105 | SoftHome, 106 | /// Move cursor to end of line 107 | End, 108 | /// Move cursor to start of paragraph 109 | ParagraphStart, 110 | /// Move cursor to end of paragraph 111 | ParagraphEnd, 112 | /// Move cursor up one page 113 | PageUp, 114 | /// Move cursor down one page 115 | PageDown, 116 | /// Move cursor up or down by a number of pixels 117 | Vertical(i32), 118 | /// Move cursor to previous word boundary 119 | PreviousWord, 120 | /// Move cursor to next word boundary 121 | NextWord, 122 | /// Move cursor to next word boundary to the left 123 | LeftWord, 124 | /// Move cursor to next word boundary to the right 125 | RightWord, 126 | /// Move cursor to the start of the document 127 | BufferStart, 128 | /// Move cursor to the end of the document 129 | BufferEnd, 130 | /// Move cursor to specific line 131 | GotoLine(usize), 132 | } 133 | 134 | /// Scroll position in [`Buffer`] 135 | #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] 136 | pub struct Scroll { 137 | /// Index of [`BufferLine`] in [`Buffer::lines`]. This will be adjusted as needed if layout is 138 | /// out of bounds 139 | pub line: usize, 140 | /// Pixel offset from the start of the [`BufferLine`]. This will be adjusted as needed 141 | /// if it is negative or exceeds the height of the [`BufferLine::layout`] lines. 142 | pub vertical: f32, 143 | /// The horizontal position of scroll in fractional pixels 144 | pub horizontal: f32, 145 | } 146 | 147 | impl Scroll { 148 | /// Create a new scroll 149 | pub const fn new(line: usize, vertical: f32, horizontal: f32) -> Self { 150 | Self { 151 | line, 152 | vertical, 153 | horizontal, 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/edit/mod.rs: -------------------------------------------------------------------------------- 1 | use alloc::sync::Arc; 2 | #[cfg(not(feature = "std"))] 3 | use alloc::{string::String, vec::Vec}; 4 | use core::cmp; 5 | use unicode_segmentation::UnicodeSegmentation; 6 | 7 | use crate::{AttrsList, BorrowedWithFontSystem, Buffer, Cursor, FontSystem, Motion}; 8 | 9 | pub use self::editor::*; 10 | mod editor; 11 | 12 | #[cfg(feature = "syntect")] 13 | pub use self::syntect::*; 14 | #[cfg(feature = "syntect")] 15 | mod syntect; 16 | 17 | #[cfg(feature = "vi")] 18 | pub use self::vi::*; 19 | #[cfg(feature = "vi")] 20 | mod vi; 21 | 22 | /// An action to perform on an [`Editor`] 23 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 24 | pub enum Action { 25 | /// Move the cursor with some motion 26 | Motion(Motion), 27 | /// Escape, clears selection 28 | Escape, 29 | /// Insert character at cursor 30 | Insert(char), 31 | /// Create new line 32 | Enter, 33 | /// Delete text behind cursor 34 | Backspace, 35 | /// Delete text in front of cursor 36 | Delete, 37 | // Indent text (typically Tab) 38 | Indent, 39 | // Unindent text (typically Shift+Tab) 40 | Unindent, 41 | /// Mouse click at specified position 42 | Click { 43 | x: i32, 44 | y: i32, 45 | }, 46 | /// Mouse double click at specified position 47 | DoubleClick { 48 | x: i32, 49 | y: i32, 50 | }, 51 | /// Mouse triple click at specified position 52 | TripleClick { 53 | x: i32, 54 | y: i32, 55 | }, 56 | /// Mouse drag to specified position 57 | Drag { 58 | x: i32, 59 | y: i32, 60 | }, 61 | /// Scroll specified number of lines 62 | Scroll { 63 | lines: i32, 64 | }, 65 | } 66 | 67 | #[derive(Debug)] 68 | pub enum BufferRef<'buffer> { 69 | Owned(Buffer), 70 | Borrowed(&'buffer mut Buffer), 71 | Arc(Arc), 72 | } 73 | 74 | impl Clone for BufferRef<'_> { 75 | fn clone(&self) -> Self { 76 | match self { 77 | Self::Owned(buffer) => Self::Owned(buffer.clone()), 78 | Self::Borrowed(buffer) => Self::Owned((*buffer).clone()), 79 | Self::Arc(buffer) => Self::Arc(buffer.clone()), 80 | } 81 | } 82 | } 83 | 84 | impl From for BufferRef<'_> { 85 | fn from(buffer: Buffer) -> Self { 86 | Self::Owned(buffer) 87 | } 88 | } 89 | 90 | impl<'buffer> From<&'buffer mut Buffer> for BufferRef<'buffer> { 91 | fn from(buffer: &'buffer mut Buffer) -> Self { 92 | Self::Borrowed(buffer) 93 | } 94 | } 95 | 96 | impl From> for BufferRef<'_> { 97 | fn from(arc: Arc) -> Self { 98 | Self::Arc(arc) 99 | } 100 | } 101 | 102 | /// A unique change to an editor 103 | #[derive(Clone, Debug)] 104 | pub struct ChangeItem { 105 | /// Cursor indicating start of change 106 | pub start: Cursor, 107 | /// Cursor indicating end of change 108 | pub end: Cursor, 109 | /// Text to be inserted or deleted 110 | pub text: String, 111 | /// Insert if true, delete if false 112 | pub insert: bool, 113 | } 114 | 115 | impl ChangeItem { 116 | // Reverse change item (in place) 117 | pub fn reverse(&mut self) { 118 | self.insert = !self.insert; 119 | } 120 | } 121 | 122 | /// A set of change items grouped into one logical change 123 | #[derive(Clone, Debug, Default)] 124 | pub struct Change { 125 | /// Change items grouped into one change 126 | pub items: Vec, 127 | } 128 | 129 | impl Change { 130 | // Reverse change (in place) 131 | pub fn reverse(&mut self) { 132 | self.items.reverse(); 133 | for item in self.items.iter_mut() { 134 | item.reverse(); 135 | } 136 | } 137 | } 138 | 139 | /// Selection mode 140 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 141 | pub enum Selection { 142 | /// No selection 143 | None, 144 | /// Normal selection 145 | Normal(Cursor), 146 | /// Select by lines 147 | Line(Cursor), 148 | /// Select by words 149 | Word(Cursor), 150 | //TODO: Select block 151 | } 152 | 153 | /// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor` 154 | pub trait Edit<'buffer> { 155 | /// Mutably borrows `self` together with an [`FontSystem`] for more convenient methods 156 | fn borrow_with<'font_system>( 157 | &'font_system mut self, 158 | font_system: &'font_system mut FontSystem, 159 | ) -> BorrowedWithFontSystem<'font_system, Self> 160 | where 161 | Self: Sized, 162 | { 163 | BorrowedWithFontSystem { 164 | inner: self, 165 | font_system, 166 | } 167 | } 168 | 169 | /// Get the internal [`BufferRef`] 170 | fn buffer_ref(&self) -> &BufferRef<'buffer>; 171 | 172 | /// Get the internal [`BufferRef`] 173 | fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer>; 174 | 175 | /// Get the internal [`Buffer`] 176 | fn with_buffer T, T>(&self, f: F) -> T { 177 | match self.buffer_ref() { 178 | BufferRef::Owned(buffer) => f(buffer), 179 | BufferRef::Borrowed(buffer) => f(buffer), 180 | BufferRef::Arc(buffer) => f(buffer), 181 | } 182 | } 183 | 184 | /// Get the internal [`Buffer`], mutably 185 | fn with_buffer_mut T, T>(&mut self, f: F) -> T { 186 | match self.buffer_ref_mut() { 187 | BufferRef::Owned(buffer) => f(buffer), 188 | BufferRef::Borrowed(buffer) => f(buffer), 189 | BufferRef::Arc(buffer) => f(Arc::make_mut(buffer)), 190 | } 191 | } 192 | 193 | /// Get the [`Buffer`] redraw flag 194 | fn redraw(&self) -> bool { 195 | self.with_buffer(|buffer| buffer.redraw()) 196 | } 197 | 198 | /// Set the [`Buffer`] redraw flag 199 | fn set_redraw(&mut self, redraw: bool) { 200 | self.with_buffer_mut(|buffer| buffer.set_redraw(redraw)); 201 | } 202 | 203 | /// Get the current cursor 204 | fn cursor(&self) -> Cursor; 205 | 206 | /// Set the current cursor 207 | fn set_cursor(&mut self, cursor: Cursor); 208 | 209 | /// Get the current selection position 210 | fn selection(&self) -> Selection; 211 | 212 | /// Set the current selection position 213 | fn set_selection(&mut self, selection: Selection); 214 | 215 | /// Get the bounds of the current selection 216 | //TODO: will not work with Block select 217 | fn selection_bounds(&self) -> Option<(Cursor, Cursor)> { 218 | self.with_buffer(|buffer| { 219 | let cursor = self.cursor(); 220 | match self.selection() { 221 | Selection::None => None, 222 | Selection::Normal(select) => match select.line.cmp(&cursor.line) { 223 | cmp::Ordering::Greater => Some((cursor, select)), 224 | cmp::Ordering::Less => Some((select, cursor)), 225 | cmp::Ordering::Equal => { 226 | /* select.line == cursor.line */ 227 | if select.index < cursor.index { 228 | Some((select, cursor)) 229 | } else { 230 | /* select.index >= cursor.index */ 231 | Some((cursor, select)) 232 | } 233 | } 234 | }, 235 | Selection::Line(select) => { 236 | let start_line = cmp::min(select.line, cursor.line); 237 | let end_line = cmp::max(select.line, cursor.line); 238 | let end_index = buffer.lines[end_line].text().len(); 239 | Some((Cursor::new(start_line, 0), Cursor::new(end_line, end_index))) 240 | } 241 | Selection::Word(select) => { 242 | let (mut start, mut end) = match select.line.cmp(&cursor.line) { 243 | cmp::Ordering::Greater => (cursor, select), 244 | cmp::Ordering::Less => (select, cursor), 245 | cmp::Ordering::Equal => { 246 | /* select.line == cursor.line */ 247 | if select.index < cursor.index { 248 | (select, cursor) 249 | } else { 250 | /* select.index >= cursor.index */ 251 | (cursor, select) 252 | } 253 | } 254 | }; 255 | 256 | // Move start to beginning of word 257 | { 258 | let line = &buffer.lines[start.line]; 259 | start.index = line 260 | .text() 261 | .unicode_word_indices() 262 | .rev() 263 | .map(|(i, _)| i) 264 | .find(|&i| i < start.index) 265 | .unwrap_or(0); 266 | } 267 | 268 | // Move end to end of word 269 | { 270 | let line = &buffer.lines[end.line]; 271 | end.index = line 272 | .text() 273 | .unicode_word_indices() 274 | .map(|(i, word)| i + word.len()) 275 | .find(|&i| i > end.index) 276 | .unwrap_or(line.text().len()); 277 | } 278 | 279 | Some((start, end)) 280 | } 281 | } 282 | }) 283 | } 284 | 285 | /// Get the current automatic indentation setting 286 | fn auto_indent(&self) -> bool; 287 | 288 | /// Enable or disable automatic indentation 289 | fn set_auto_indent(&mut self, auto_indent: bool); 290 | 291 | /// Get the current tab width 292 | fn tab_width(&self) -> u16; 293 | 294 | /// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored 295 | fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16); 296 | 297 | /// Shape lines until scroll, after adjusting scroll if the cursor moved 298 | fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool); 299 | 300 | /// Delete text starting at start Cursor and ending at end Cursor 301 | fn delete_range(&mut self, start: Cursor, end: Cursor); 302 | 303 | /// Insert text at specified cursor with specified `attrs_list` 304 | fn insert_at(&mut self, cursor: Cursor, data: &str, attrs_list: Option) -> Cursor; 305 | 306 | /// Copy selection 307 | fn copy_selection(&self) -> Option; 308 | 309 | /// Delete selection, adjusting cursor and returning true if there was a selection 310 | // Also used by backspace, delete, insert, and enter when there is a selection 311 | fn delete_selection(&mut self) -> bool; 312 | 313 | /// Insert a string at the current cursor or replacing the current selection with the given 314 | /// attributes, or with the previous character's attributes if None is given. 315 | fn insert_string(&mut self, data: &str, attrs_list: Option) { 316 | self.delete_selection(); 317 | let new_cursor = self.insert_at(self.cursor(), data, attrs_list); 318 | self.set_cursor(new_cursor); 319 | } 320 | 321 | /// Apply a change 322 | fn apply_change(&mut self, change: &Change) -> bool; 323 | 324 | /// Start collecting change 325 | fn start_change(&mut self); 326 | 327 | /// Get completed change 328 | fn finish_change(&mut self) -> Option; 329 | 330 | /// Perform an [Action] on the editor 331 | fn action(&mut self, font_system: &mut FontSystem, action: Action); 332 | 333 | /// Get X and Y position of the top left corner of the cursor 334 | fn cursor_position(&self) -> Option<(i32, i32)>; 335 | } 336 | 337 | impl<'buffer, E: Edit<'buffer>> BorrowedWithFontSystem<'_, E> { 338 | /// Get the internal [`Buffer`], mutably 339 | pub fn with_buffer_mut) -> T, T>( 340 | &mut self, 341 | f: F, 342 | ) -> T { 343 | self.inner.with_buffer_mut(|buffer| { 344 | let mut borrowed = BorrowedWithFontSystem { 345 | inner: buffer, 346 | font_system: self.font_system, 347 | }; 348 | f(&mut borrowed) 349 | }) 350 | } 351 | 352 | /// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored 353 | pub fn set_tab_width(&mut self, tab_width: u16) { 354 | self.inner.set_tab_width(self.font_system, tab_width); 355 | } 356 | 357 | /// Shape lines until scroll, after adjusting scroll if the cursor moved 358 | pub fn shape_as_needed(&mut self, prune: bool) { 359 | self.inner.shape_as_needed(self.font_system, prune); 360 | } 361 | 362 | /// Perform an [Action] on the editor 363 | pub fn action(&mut self, action: Action) { 364 | self.inner.action(self.font_system, action); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/font/fallback/macos.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use unicode_script::Script; 4 | 5 | use super::Fallback; 6 | 7 | /// A platform-specific font fallback list, for `MacOS`. 8 | #[derive(Debug)] 9 | pub struct PlatformFallback; 10 | 11 | impl Fallback for PlatformFallback { 12 | fn common_fallback(&self) -> &'static [&'static str] { 13 | common_fallback() 14 | } 15 | 16 | fn forbidden_fallback(&self) -> &'static [&'static str] { 17 | forbidden_fallback() 18 | } 19 | 20 | fn script_fallback( 21 | &self, 22 | script: unicode_script::Script, 23 | locale: &str, 24 | ) -> &'static [&'static str] { 25 | script_fallback(script, locale) 26 | } 27 | } 28 | 29 | // Fallbacks to use after any script specific fallbacks 30 | fn common_fallback() -> &'static [&'static str] { 31 | &[ 32 | ".SF NS", 33 | "Menlo", 34 | "Apple Color Emoji", 35 | "Geneva", 36 | "Arial Unicode MS", 37 | ] 38 | } 39 | 40 | // Fallbacks to never use 41 | fn forbidden_fallback() -> &'static [&'static str] { 42 | &[".LastResort"] 43 | } 44 | 45 | fn han_unification(locale: &str) -> &'static [&'static str] { 46 | match locale { 47 | // Japan 48 | "ja" => &["Hiragino Sans"], 49 | // Korea 50 | "ko" => &["Apple SD Gothic Neo"], 51 | // Hong Kong 52 | "zh-HK" => &["PingFang HK"], 53 | // Taiwan 54 | "zh-TW" => &["PingFang TC"], 55 | // Simplified Chinese is the default (also catches "zh-CN" for China) 56 | _ => &["PingFang SC"], 57 | } 58 | } 59 | 60 | // Fallbacks to use per script 61 | fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] { 62 | //TODO: abstract style (sans/serif/monospaced) 63 | //TODO: pull more data from about:config font.name-list.sans-serif in Firefox 64 | match script { 65 | Script::Adlam => &["Noto Sans Adlam"], 66 | Script::Arabic => &["Geeza Pro"], 67 | Script::Armenian => &["Noto Sans Armenian"], 68 | Script::Bengali => &["Bangla Sangam MN"], 69 | Script::Buhid => &["Noto Sans Buhid"], 70 | Script::Canadian_Aboriginal => &["Euphemia UCAS"], 71 | Script::Chakma => &["Noto Sans Chakma"], 72 | Script::Devanagari => &["Devanagari Sangam MN"], 73 | Script::Ethiopic => &["Kefa"], 74 | Script::Gothic => &["Noto Sans Gothic"], 75 | Script::Grantha => &["Grantha Sangam MN"], 76 | Script::Gujarati => &["Gujarati Sangam MN"], 77 | Script::Gurmukhi => &["Gurmukhi Sangam MN"], 78 | Script::Han => han_unification(locale), 79 | Script::Hangul => han_unification("ko"), 80 | Script::Hanunoo => &["Noto Sans Hanunoo"], 81 | Script::Hebrew => &["Arial"], 82 | Script::Hiragana => han_unification("ja"), 83 | Script::Javanese => &["Noto Sans Javanese"], 84 | Script::Kannada => &["Noto Sans Kannada"], 85 | Script::Katakana => han_unification("ja"), 86 | Script::Khmer => &["Khmer Sangam MN"], 87 | Script::Lao => &["Lao Sangam MN"], 88 | Script::Malayalam => &["Malayalam Sangam MN"], 89 | Script::Mongolian => &["Noto Sans Mongolian"], 90 | Script::Myanmar => &["Noto Sans Myanmar"], 91 | Script::Oriya => &["Noto Sans Oriya"], 92 | Script::Sinhala => &["Sinhala Sangam MN"], 93 | Script::Syriac => &["Noto Sans Syriac"], 94 | Script::Tagalog => &["Noto Sans Tagalog"], 95 | Script::Tagbanwa => &["Noto Sans Tagbanwa"], 96 | Script::Tai_Le => &["Noto Sans Tai Le"], 97 | Script::Tai_Tham => &["Noto Sans Tai Tham"], 98 | Script::Tai_Viet => &["Noto Sans Tai Viet"], 99 | Script::Tamil => &["InaiMathi"], 100 | Script::Telugu => &["Telugu Sangam MN"], 101 | Script::Thaana => &["Noto Sans Thaana"], 102 | Script::Thai => &["Ayuthaya"], 103 | Script::Tibetan => &["Kailasa"], 104 | Script::Tifinagh => &["Noto Sans Tifinagh"], 105 | Script::Vai => &["Noto Sans Vai"], 106 | //TODO: Use han_unification? 107 | Script::Yi => &["Noto Sans Yi", "PingFang SC"], 108 | _ => &[], 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/font/fallback/other.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use unicode_script::Script; 4 | 5 | use super::Fallback; 6 | 7 | /// An empty platform-specific font fallback list. 8 | #[derive(Debug)] 9 | pub struct PlatformFallback; 10 | 11 | impl Fallback for PlatformFallback { 12 | fn common_fallback(&self) -> &'static [&'static str] { 13 | common_fallback() 14 | } 15 | 16 | fn forbidden_fallback(&self) -> &'static [&'static str] { 17 | forbidden_fallback() 18 | } 19 | 20 | fn script_fallback( 21 | &self, 22 | script: unicode_script::Script, 23 | locale: &str, 24 | ) -> &'static [&'static str] { 25 | script_fallback(script, locale) 26 | } 27 | } 28 | 29 | // Fallbacks to use after any script specific fallbacks 30 | fn common_fallback() -> &'static [&'static str] { 31 | &[] 32 | } 33 | 34 | // Fallbacks to never use 35 | fn forbidden_fallback() -> &'static [&'static str] { 36 | &[] 37 | } 38 | 39 | // Fallbacks to use per script 40 | fn script_fallback(_script: Script, _locale: &str) -> &'static [&'static str] { 41 | &[] 42 | } 43 | -------------------------------------------------------------------------------- /src/font/fallback/unix.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use unicode_script::Script; 4 | 5 | use super::Fallback; 6 | 7 | /// A platform-specific font fallback list, for Unix. 8 | #[derive(Debug)] 9 | pub struct PlatformFallback; 10 | 11 | impl Fallback for PlatformFallback { 12 | fn common_fallback(&self) -> &'static [&'static str] { 13 | common_fallback() 14 | } 15 | 16 | fn forbidden_fallback(&self) -> &'static [&'static str] { 17 | forbidden_fallback() 18 | } 19 | 20 | fn script_fallback( 21 | &self, 22 | script: unicode_script::Script, 23 | locale: &str, 24 | ) -> &'static [&'static str] { 25 | script_fallback(script, locale) 26 | } 27 | } 28 | 29 | // Fallbacks to use after any script specific fallbacks 30 | fn common_fallback() -> &'static [&'static str] { 31 | //TODO: abstract style (sans/serif/monospaced) 32 | &[ 33 | /* Sans-serif fallbacks */ 34 | "Noto Sans", 35 | /* More sans-serif fallbacks */ 36 | "DejaVu Sans", 37 | "FreeSans", 38 | /* Mono fallbacks */ 39 | "Noto Sans Mono", 40 | "DejaVu Sans Mono", 41 | "FreeMono", 42 | /* Symbols fallbacks */ 43 | "Noto Sans Symbols", 44 | "Noto Sans Symbols2", 45 | /* Emoji fallbacks*/ 46 | "Noto Color Emoji", 47 | //TODO: Add CJK script here for doublewides? 48 | ] 49 | } 50 | 51 | // Fallbacks to never use 52 | fn forbidden_fallback() -> &'static [&'static str] { 53 | &[] 54 | } 55 | 56 | fn han_unification(locale: &str) -> &'static [&'static str] { 57 | match locale { 58 | // Japan 59 | "ja" => &["Noto Sans CJK JP"], 60 | // Korea 61 | "ko" => &["Noto Sans CJK KR"], 62 | // Hong Kong 63 | "zh-HK" => &["Noto Sans CJK HK"], 64 | // Taiwan 65 | "zh-TW" => &["Noto Sans CJK TC"], 66 | // Simplified Chinese is the default (also catches "zh-CN" for China) 67 | _ => &["Noto Sans CJK SC"], 68 | } 69 | } 70 | 71 | // Fallbacks to use per script 72 | fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] { 73 | //TODO: abstract style (sans/serif/monospaced) 74 | match script { 75 | Script::Adlam => &["Noto Sans Adlam", "Noto Sans Adlam Unjoined"], 76 | Script::Arabic => &["Noto Sans Arabic"], 77 | Script::Armenian => &["Noto Sans Armenian"], 78 | Script::Bengali => &["Noto Sans Bengali"], 79 | Script::Bopomofo => han_unification(locale), 80 | //TODO: DejaVu Sans would typically be selected for braille characters, 81 | // but this breaks alignment when used alongside monospaced text. 82 | // By requesting the use of FreeMono first, this issue can be avoided. 83 | Script::Braille => &["FreeMono"], 84 | Script::Buhid => &["Noto Sans Buhid"], 85 | Script::Chakma => &["Noto Sans Chakma"], 86 | Script::Cherokee => &["Noto Sans Cherokee"], 87 | Script::Deseret => &["Noto Sans Deseret"], 88 | Script::Devanagari => &["Noto Sans Devanagari"], 89 | Script::Ethiopic => &["Noto Sans Ethiopic"], 90 | Script::Georgian => &["Noto Sans Georgian"], 91 | Script::Gothic => &["Noto Sans Gothic"], 92 | Script::Grantha => &["Noto Sans Grantha"], 93 | Script::Gujarati => &["Noto Sans Gujarati"], 94 | Script::Gurmukhi => &["Noto Sans Gurmukhi"], 95 | Script::Han => han_unification(locale), 96 | Script::Hangul => han_unification("ko"), 97 | Script::Hanunoo => &["Noto Sans Hanunoo"], 98 | Script::Hebrew => &["Noto Sans Hebrew"], 99 | Script::Hiragana => han_unification("ja"), 100 | Script::Javanese => &["Noto Sans Javanese"], 101 | Script::Kannada => &["Noto Sans Kannada"], 102 | Script::Katakana => han_unification("ja"), 103 | Script::Khmer => &["Noto Sans Khmer"], 104 | Script::Lao => &["Noto Sans Lao"], 105 | Script::Malayalam => &["Noto Sans Malayalam"], 106 | Script::Mongolian => &["Noto Sans Mongolian"], 107 | Script::Myanmar => &["Noto Sans Myanmar"], 108 | Script::Oriya => &["Noto Sans Oriya"], 109 | Script::Runic => &["Noto Sans Runic"], 110 | Script::Sinhala => &["Noto Sans Sinhala"], 111 | Script::Syriac => &["Noto Sans Syriac"], 112 | Script::Tagalog => &["Noto Sans Tagalog"], 113 | Script::Tagbanwa => &["Noto Sans Tagbanwa"], 114 | Script::Tai_Le => &["Noto Sans Tai Le"], 115 | Script::Tai_Tham => &["Noto Sans Tai Tham"], 116 | Script::Tai_Viet => &["Noto Sans Tai Viet"], 117 | Script::Tamil => &["Noto Sans Tamil"], 118 | Script::Telugu => &["Noto Sans Telugu"], 119 | Script::Thaana => &["Noto Sans Thaana"], 120 | Script::Thai => &["Noto Sans Thai"], 121 | //TODO: no sans script? 122 | Script::Tibetan => &["Noto Serif Tibetan"], 123 | Script::Tifinagh => &["Noto Sans Tifinagh"], 124 | Script::Vai => &["Noto Sans Vai"], 125 | //TODO: Use han_unification? 126 | Script::Yi => &["Noto Sans Yi", "Noto Sans CJK SC"], 127 | _ => &[], 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/font/fallback/windows.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use unicode_script::Script; 4 | 5 | use super::Fallback; 6 | 7 | /// A platform-specific font fallback list, for Windows. 8 | #[derive(Debug)] 9 | pub struct PlatformFallback; 10 | 11 | impl Fallback for PlatformFallback { 12 | fn common_fallback(&self) -> &'static [&'static str] { 13 | common_fallback() 14 | } 15 | 16 | fn forbidden_fallback(&self) -> &'static [&'static str] { 17 | forbidden_fallback() 18 | } 19 | 20 | fn script_fallback( 21 | &self, 22 | script: unicode_script::Script, 23 | locale: &str, 24 | ) -> &'static [&'static str] { 25 | script_fallback(script, locale) 26 | } 27 | } 28 | 29 | // Fallbacks to use after any script specific fallbacks 30 | fn common_fallback() -> &'static [&'static str] { 31 | //TODO: abstract style (sans/serif/monospaced) 32 | &[ 33 | "Segoe UI", 34 | "Segoe UI Emoji", 35 | "Segoe UI Symbol", 36 | "Segoe UI Historic", 37 | //TODO: Add CJK script here for doublewides? 38 | ] 39 | } 40 | 41 | // Fallbacks to never use 42 | fn forbidden_fallback() -> &'static [&'static str] { 43 | &[] 44 | } 45 | 46 | fn han_unification(locale: &str) -> &'static [&'static str] { 47 | //TODO! 48 | match locale { 49 | // Japan 50 | "ja" => &["Yu Gothic"], 51 | // Korea 52 | "ko" => &["Malgun Gothic"], 53 | // Hong Kong" 54 | "zh-HK" => &["MingLiU_HKSCS"], 55 | // Taiwan 56 | "zh-TW" => &["Microsoft JhengHei UI"], 57 | // Simplified Chinese is the default (also catches "zh-CN" for China) 58 | _ => &["Microsoft YaHei UI"], 59 | } 60 | } 61 | 62 | // Fallbacks to use per script 63 | fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] { 64 | //TODO: better match https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc#L99 65 | match script { 66 | Script::Adlam => &["Ebrima"], 67 | Script::Bengali => &["Nirmala UI"], 68 | Script::Canadian_Aboriginal => &["Gadugi"], 69 | Script::Chakma => &["Nirmala UI"], 70 | Script::Cherokee => &["Gadugi"], 71 | Script::Devanagari => &["Nirmala UI"], 72 | Script::Ethiopic => &["Ebrima"], 73 | Script::Gujarati => &["Nirmala UI"], 74 | Script::Gurmukhi => &["Nirmala UI"], 75 | Script::Han => han_unification(locale), 76 | Script::Hangul => han_unification("ko"), 77 | Script::Hiragana => han_unification("ja"), 78 | Script::Javanese => &["Javanese Text"], 79 | Script::Kannada => &["Nirmala UI"], 80 | Script::Katakana => han_unification("ja"), 81 | Script::Khmer => &["Leelawadee UI"], 82 | Script::Lao => &["Leelawadee UI"], 83 | Script::Malayalam => &["Nirmala UI"], 84 | Script::Mongolian => &["Mongolian Baiti"], 85 | Script::Myanmar => &["Myanmar Text"], 86 | Script::Oriya => &["Nirmala UI"], 87 | Script::Sinhala => &["Nirmala UI"], 88 | Script::Tamil => &["Nirmala UI"], 89 | Script::Telugu => &["Nirmala UI"], 90 | Script::Thaana => &["MV Boli"], 91 | Script::Thai => &["Leelawadee UI"], 92 | Script::Tibetan => &["Microsoft Himalaya"], 93 | Script::Tifinagh => &["Ebrima"], 94 | Script::Vai => &["Ebrima"], 95 | Script::Yi => &["Microsoft Yi Baiti"], 96 | _ => &[], 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/font/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | // re-export ttf_parser 4 | pub use ttf_parser; 5 | // re-export peniko::Font; 6 | #[cfg(feature = "peniko")] 7 | pub use peniko::Font as PenikoFont; 8 | 9 | use core::fmt; 10 | 11 | use alloc::sync::Arc; 12 | #[cfg(not(feature = "std"))] 13 | use alloc::vec::Vec; 14 | 15 | use rustybuzz::Face as RustybuzzFace; 16 | use self_cell::self_cell; 17 | 18 | pub(crate) mod fallback; 19 | pub use fallback::{Fallback, PlatformFallback}; 20 | 21 | pub use self::system::*; 22 | mod system; 23 | 24 | self_cell!( 25 | struct OwnedFace { 26 | owner: Arc + Send + Sync>, 27 | 28 | #[covariant] 29 | dependent: RustybuzzFace, 30 | } 31 | ); 32 | 33 | struct FontMonospaceFallback { 34 | monospace_em_width: Option, 35 | scripts: Vec<[u8; 4]>, 36 | unicode_codepoints: Vec, 37 | } 38 | 39 | /// A font 40 | pub struct Font { 41 | #[cfg(feature = "swash")] 42 | swash: (u32, swash::CacheKey), 43 | rustybuzz: OwnedFace, 44 | #[cfg(not(feature = "peniko"))] 45 | data: Arc + Send + Sync>, 46 | #[cfg(feature = "peniko")] 47 | data: peniko::Font, 48 | id: fontdb::ID, 49 | monospace_fallback: Option, 50 | } 51 | 52 | impl fmt::Debug for Font { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | f.debug_struct("Font") 55 | .field("id", &self.id) 56 | .finish_non_exhaustive() 57 | } 58 | } 59 | 60 | impl Font { 61 | pub fn id(&self) -> fontdb::ID { 62 | self.id 63 | } 64 | 65 | pub fn monospace_em_width(&self) -> Option { 66 | self.monospace_fallback 67 | .as_ref() 68 | .and_then(|x| x.monospace_em_width) 69 | } 70 | 71 | pub fn scripts(&self) -> &[[u8; 4]] { 72 | self.monospace_fallback.as_ref().map_or(&[], |x| &x.scripts) 73 | } 74 | 75 | pub fn unicode_codepoints(&self) -> &[u32] { 76 | self.monospace_fallback 77 | .as_ref() 78 | .map_or(&[], |x| &x.unicode_codepoints) 79 | } 80 | 81 | pub fn data(&self) -> &[u8] { 82 | #[cfg(not(feature = "peniko"))] 83 | { 84 | (*self.data).as_ref() 85 | } 86 | #[cfg(feature = "peniko")] 87 | { 88 | self.data.data.data() 89 | } 90 | } 91 | 92 | pub fn rustybuzz(&self) -> &RustybuzzFace<'_> { 93 | self.rustybuzz.borrow_dependent() 94 | } 95 | 96 | #[cfg(feature = "peniko")] 97 | pub fn as_peniko(&self) -> PenikoFont { 98 | self.data.clone() 99 | } 100 | 101 | #[cfg(feature = "swash")] 102 | pub fn as_swash(&self) -> swash::FontRef<'_> { 103 | let swash = &self.swash; 104 | swash::FontRef { 105 | data: self.data(), 106 | offset: swash.0, 107 | key: swash.1, 108 | } 109 | } 110 | } 111 | 112 | impl Font { 113 | pub fn new(db: &fontdb::Database, id: fontdb::ID) -> Option { 114 | let info = db.face(id)?; 115 | 116 | let monospace_fallback = if cfg!(feature = "monospace_fallback") { 117 | db.with_face_data(id, |font_data, face_index| { 118 | let face = ttf_parser::Face::parse(font_data, face_index).ok()?; 119 | let monospace_em_width = info 120 | .monospaced 121 | .then(|| { 122 | let hor_advance = face.glyph_hor_advance(face.glyph_index(' ')?)? as f32; 123 | let upem = face.units_per_em() as f32; 124 | Some(hor_advance / upem) 125 | }) 126 | .flatten(); 127 | 128 | if info.monospaced && monospace_em_width.is_none() { 129 | None?; 130 | } 131 | 132 | let scripts = face 133 | .tables() 134 | .gpos 135 | .into_iter() 136 | .chain(face.tables().gsub) 137 | .flat_map(|table| table.scripts) 138 | .map(|script| script.tag.to_bytes()) 139 | .collect(); 140 | 141 | let mut unicode_codepoints = Vec::new(); 142 | 143 | face.tables() 144 | .cmap? 145 | .subtables 146 | .into_iter() 147 | .filter(|subtable| subtable.is_unicode()) 148 | .for_each(|subtable| { 149 | unicode_codepoints.reserve(1024); 150 | subtable.codepoints(|code_point| { 151 | if subtable.glyph_index(code_point).is_some() { 152 | unicode_codepoints.push(code_point); 153 | } 154 | }); 155 | }); 156 | 157 | unicode_codepoints.shrink_to_fit(); 158 | 159 | Some(FontMonospaceFallback { 160 | monospace_em_width, 161 | scripts, 162 | unicode_codepoints, 163 | }) 164 | })? 165 | } else { 166 | None 167 | }; 168 | 169 | let data = match &info.source { 170 | fontdb::Source::Binary(data) => Arc::clone(data), 171 | #[cfg(feature = "std")] 172 | fontdb::Source::File(path) => { 173 | log::warn!("Unsupported fontdb Source::File('{}')", path.display()); 174 | return None; 175 | } 176 | #[cfg(feature = "std")] 177 | fontdb::Source::SharedFile(_path, data) => Arc::clone(data), 178 | }; 179 | 180 | Some(Self { 181 | id: info.id, 182 | monospace_fallback, 183 | #[cfg(feature = "swash")] 184 | swash: { 185 | let swash = swash::FontRef::from_index((*data).as_ref(), info.index as usize)?; 186 | (swash.offset, swash.key) 187 | }, 188 | rustybuzz: OwnedFace::try_new(Arc::clone(&data), |data| { 189 | RustybuzzFace::from_slice((**data).as_ref(), info.index).ok_or(()) 190 | }) 191 | .ok()?, 192 | #[cfg(not(feature = "peniko"))] 193 | data, 194 | #[cfg(feature = "peniko")] 195 | data: peniko::Font::new(peniko::Blob::new(data), info.index), 196 | }) 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod test { 202 | #[test] 203 | fn test_fonts_load_time() { 204 | use crate::FontSystem; 205 | use sys_locale::get_locale; 206 | 207 | #[cfg(not(target_arch = "wasm32"))] 208 | let now = std::time::Instant::now(); 209 | 210 | let mut db = fontdb::Database::new(); 211 | let locale = get_locale().expect("Local available"); 212 | db.load_system_fonts(); 213 | FontSystem::new_with_locale_and_db(locale, db); 214 | 215 | #[cfg(not(target_arch = "wasm32"))] 216 | println!("Fonts load time {}ms.", now.elapsed().as_millis()); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/glyph_cache.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | bitflags::bitflags! { 4 | /// Flags that change rendering 5 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 6 | #[repr(transparent)] 7 | pub struct CacheKeyFlags: u32 { 8 | /// Skew by 14 degrees to synthesize italic 9 | const FAKE_ITALIC = 1; 10 | } 11 | } 12 | 13 | /// Key for building a glyph cache 14 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 15 | pub struct CacheKey { 16 | /// Font ID 17 | pub font_id: fontdb::ID, 18 | /// Glyph ID 19 | pub glyph_id: u16, 20 | /// `f32` bits of font size 21 | pub font_size_bits: u32, 22 | /// Binning of fractional X offset 23 | pub x_bin: SubpixelBin, 24 | /// Binning of fractional Y offset 25 | pub y_bin: SubpixelBin, 26 | /// [`CacheKeyFlags`] 27 | pub flags: CacheKeyFlags, 28 | } 29 | 30 | impl CacheKey { 31 | pub fn new( 32 | font_id: fontdb::ID, 33 | glyph_id: u16, 34 | font_size: f32, 35 | pos: (f32, f32), 36 | flags: CacheKeyFlags, 37 | ) -> (Self, i32, i32) { 38 | let (x, x_bin) = SubpixelBin::new(pos.0); 39 | let (y, y_bin) = SubpixelBin::new(pos.1); 40 | ( 41 | Self { 42 | font_id, 43 | glyph_id, 44 | font_size_bits: font_size.to_bits(), 45 | x_bin, 46 | y_bin, 47 | flags, 48 | }, 49 | x, 50 | y, 51 | ) 52 | } 53 | } 54 | 55 | /// Binning of subpixel position for cache optimization 56 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 57 | pub enum SubpixelBin { 58 | Zero, 59 | One, 60 | Two, 61 | Three, 62 | } 63 | 64 | impl SubpixelBin { 65 | pub fn new(pos: f32) -> (i32, Self) { 66 | let trunc = pos as i32; 67 | let fract = pos - trunc as f32; 68 | 69 | if pos.is_sign_negative() { 70 | if fract > -0.125 { 71 | (trunc, Self::Zero) 72 | } else if fract > -0.375 { 73 | (trunc - 1, Self::Three) 74 | } else if fract > -0.625 { 75 | (trunc - 1, Self::Two) 76 | } else if fract > -0.875 { 77 | (trunc - 1, Self::One) 78 | } else { 79 | (trunc - 1, Self::Zero) 80 | } 81 | } else { 82 | #[allow(clippy::collapsible_else_if)] 83 | if fract < 0.125 { 84 | (trunc, Self::Zero) 85 | } else if fract < 0.375 { 86 | (trunc, Self::One) 87 | } else if fract < 0.625 { 88 | (trunc, Self::Two) 89 | } else if fract < 0.875 { 90 | (trunc, Self::Three) 91 | } else { 92 | (trunc + 1, Self::Zero) 93 | } 94 | } 95 | } 96 | 97 | pub fn as_float(&self) -> f32 { 98 | match self { 99 | Self::Zero => 0.0, 100 | Self::One => 0.25, 101 | Self::Two => 0.5, 102 | Self::Three => 0.75, 103 | } 104 | } 105 | } 106 | 107 | #[test] 108 | fn test_subpixel_bins() { 109 | // POSITIVE TESTS 110 | 111 | // Maps to 0.0 112 | assert_eq!(SubpixelBin::new(0.0), (0, SubpixelBin::Zero)); 113 | assert_eq!(SubpixelBin::new(0.124), (0, SubpixelBin::Zero)); 114 | 115 | // Maps to 0.25 116 | assert_eq!(SubpixelBin::new(0.125), (0, SubpixelBin::One)); 117 | assert_eq!(SubpixelBin::new(0.25), (0, SubpixelBin::One)); 118 | assert_eq!(SubpixelBin::new(0.374), (0, SubpixelBin::One)); 119 | 120 | // Maps to 0.5 121 | assert_eq!(SubpixelBin::new(0.375), (0, SubpixelBin::Two)); 122 | assert_eq!(SubpixelBin::new(0.5), (0, SubpixelBin::Two)); 123 | assert_eq!(SubpixelBin::new(0.624), (0, SubpixelBin::Two)); 124 | 125 | // Maps to 0.75 126 | assert_eq!(SubpixelBin::new(0.625), (0, SubpixelBin::Three)); 127 | assert_eq!(SubpixelBin::new(0.75), (0, SubpixelBin::Three)); 128 | assert_eq!(SubpixelBin::new(0.874), (0, SubpixelBin::Three)); 129 | 130 | // Maps to 1.0 131 | assert_eq!(SubpixelBin::new(0.875), (1, SubpixelBin::Zero)); 132 | assert_eq!(SubpixelBin::new(0.999), (1, SubpixelBin::Zero)); 133 | assert_eq!(SubpixelBin::new(1.0), (1, SubpixelBin::Zero)); 134 | assert_eq!(SubpixelBin::new(1.124), (1, SubpixelBin::Zero)); 135 | 136 | // NEGATIVE TESTS 137 | 138 | // Maps to 0.0 139 | assert_eq!(SubpixelBin::new(-0.0), (0, SubpixelBin::Zero)); 140 | assert_eq!(SubpixelBin::new(-0.124), (0, SubpixelBin::Zero)); 141 | 142 | // Maps to 0.25 143 | assert_eq!(SubpixelBin::new(-0.125), (-1, SubpixelBin::Three)); 144 | assert_eq!(SubpixelBin::new(-0.25), (-1, SubpixelBin::Three)); 145 | assert_eq!(SubpixelBin::new(-0.374), (-1, SubpixelBin::Three)); 146 | 147 | // Maps to 0.5 148 | assert_eq!(SubpixelBin::new(-0.375), (-1, SubpixelBin::Two)); 149 | assert_eq!(SubpixelBin::new(-0.5), (-1, SubpixelBin::Two)); 150 | assert_eq!(SubpixelBin::new(-0.624), (-1, SubpixelBin::Two)); 151 | 152 | // Maps to 0.75 153 | assert_eq!(SubpixelBin::new(-0.625), (-1, SubpixelBin::One)); 154 | assert_eq!(SubpixelBin::new(-0.75), (-1, SubpixelBin::One)); 155 | assert_eq!(SubpixelBin::new(-0.874), (-1, SubpixelBin::One)); 156 | 157 | // Maps to 1.0 158 | assert_eq!(SubpixelBin::new(-0.875), (-1, SubpixelBin::Zero)); 159 | assert_eq!(SubpixelBin::new(-0.999), (-1, SubpixelBin::Zero)); 160 | assert_eq!(SubpixelBin::new(-1.0), (-1, SubpixelBin::Zero)); 161 | assert_eq!(SubpixelBin::new(-1.124), (-1, SubpixelBin::Zero)); 162 | } 163 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use core::fmt::Display; 4 | 5 | #[cfg(not(feature = "std"))] 6 | use alloc::vec::Vec; 7 | 8 | use crate::{math, CacheKey, CacheKeyFlags, Color}; 9 | 10 | /// A laid out glyph 11 | #[derive(Clone, Debug)] 12 | pub struct LayoutGlyph { 13 | /// Start index of cluster in original line 14 | pub start: usize, 15 | /// End index of cluster in original line 16 | pub end: usize, 17 | /// Font size of the glyph 18 | pub font_size: f32, 19 | /// Line height of the glyph, will override buffer setting 20 | pub line_height_opt: Option, 21 | /// Font id of the glyph 22 | pub font_id: fontdb::ID, 23 | /// Font id of the glyph 24 | pub glyph_id: u16, 25 | /// X offset of hitbox 26 | pub x: f32, 27 | /// Y offset of hitbox 28 | pub y: f32, 29 | /// Width of hitbox 30 | pub w: f32, 31 | /// Unicode `BiDi` embedding level, character is left-to-right if `level` is divisible by 2 32 | pub level: unicode_bidi::Level, 33 | /// X offset in line 34 | /// 35 | /// If you are dealing with physical coordinates, use [`Self::physical`] to obtain a 36 | /// [`PhysicalGlyph`] for rendering. 37 | /// 38 | /// This offset is useful when you are dealing with logical units and you do not care or 39 | /// cannot guarantee pixel grid alignment. For instance, when you want to use the glyphs 40 | /// for vectorial text, apply linear transformations to the layout, etc. 41 | pub x_offset: f32, 42 | /// Y offset in line 43 | /// 44 | /// If you are dealing with physical coordinates, use [`Self::physical`] to obtain a 45 | /// [`PhysicalGlyph`] for rendering. 46 | /// 47 | /// This offset is useful when you are dealing with logical units and you do not care or 48 | /// cannot guarantee pixel grid alignment. For instance, when you want to use the glyphs 49 | /// for vectorial text, apply linear transformations to the layout, etc. 50 | pub y_offset: f32, 51 | /// Optional color override 52 | pub color_opt: Option, 53 | /// Metadata from `Attrs` 54 | pub metadata: usize, 55 | /// [`CacheKeyFlags`] 56 | pub cache_key_flags: CacheKeyFlags, 57 | } 58 | 59 | #[derive(Clone, Debug)] 60 | pub struct PhysicalGlyph { 61 | /// Cache key, see [`CacheKey`] 62 | pub cache_key: CacheKey, 63 | /// Integer component of X offset in line 64 | pub x: i32, 65 | /// Integer component of Y offset in line 66 | pub y: i32, 67 | } 68 | 69 | impl LayoutGlyph { 70 | pub fn physical(&self, offset: (f32, f32), scale: f32) -> PhysicalGlyph { 71 | let x_offset = self.font_size * self.x_offset; 72 | let y_offset = self.font_size * self.y_offset; 73 | 74 | let (cache_key, x, y) = CacheKey::new( 75 | self.font_id, 76 | self.glyph_id, 77 | self.font_size * scale, 78 | ( 79 | (self.x + x_offset) * scale + offset.0, 80 | math::truncf((self.y - y_offset) * scale + offset.1), // Hinting in Y axis 81 | ), 82 | self.cache_key_flags, 83 | ); 84 | 85 | PhysicalGlyph { cache_key, x, y } 86 | } 87 | } 88 | 89 | /// A line of laid out glyphs 90 | #[derive(Clone, Debug)] 91 | pub struct LayoutLine { 92 | /// Width of the line 93 | pub w: f32, 94 | /// Maximum ascent of the glyphs in line 95 | pub max_ascent: f32, 96 | /// Maximum descent of the glyphs in line 97 | pub max_descent: f32, 98 | /// Maximum line height of any spans in line 99 | pub line_height_opt: Option, 100 | /// Glyphs in line 101 | pub glyphs: Vec, 102 | } 103 | 104 | /// Wrapping mode 105 | #[derive(Debug, Eq, PartialEq, Clone, Copy)] 106 | pub enum Wrap { 107 | /// No wrapping 108 | None, 109 | /// Wraps at a glyph level 110 | Glyph, 111 | /// Wraps at the word level 112 | Word, 113 | /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself 114 | WordOrGlyph, 115 | } 116 | 117 | impl Display for Wrap { 118 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 119 | match self { 120 | Self::None => write!(f, "No Wrap"), 121 | Self::Word => write!(f, "Word Wrap"), 122 | Self::WordOrGlyph => write!(f, "Word Wrap or Character"), 123 | Self::Glyph => write!(f, "Character"), 124 | } 125 | } 126 | } 127 | 128 | /// Align or justify 129 | #[derive(Debug, Eq, PartialEq, Clone, Copy)] 130 | pub enum Align { 131 | Left, 132 | Right, 133 | Center, 134 | Justified, 135 | End, 136 | } 137 | 138 | impl Display for Align { 139 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 140 | match self { 141 | Self::Left => write!(f, "Left"), 142 | Self::Right => write!(f, "Right"), 143 | Self::Center => write!(f, "Center"), 144 | Self::Justified => write!(f, "Justified"), 145 | Self::End => write!(f, "End"), 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | //! # COSMIC Text 4 | //! 5 | //! This library provides advanced text handling in a generic way. It provides abstractions for 6 | //! shaping, font discovery, font fallback, layout, rasterization, and editing. Shaping utilizes 7 | //! rustybuzz, font discovery utilizes fontdb, and the rasterization is optional and utilizes 8 | //! swash. The other features are developed internal to this library. 9 | //! 10 | //! It is recommended that you start by creating a [`FontSystem`], after which you can create a 11 | //! [`Buffer`], provide it with some text, and then inspect the layout it produces. At this 12 | //! point, you can use the `SwashCache` to rasterize glyphs into either images or pixels. 13 | //! 14 | //! ``` 15 | //! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics, Shaping}; 16 | //! 17 | //! // A FontSystem provides access to detected system fonts, create one per application 18 | //! let mut font_system = FontSystem::new(); 19 | //! 20 | //! // A SwashCache stores rasterized glyphs, create one per application 21 | //! let mut swash_cache = SwashCache::new(); 22 | //! 23 | //! // Text metrics indicate the font size and line height of a buffer 24 | //! let metrics = Metrics::new(14.0, 20.0); 25 | //! 26 | //! // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget 27 | //! let mut buffer = Buffer::new(&mut font_system, metrics); 28 | //! 29 | //! // Borrow buffer together with the font system for more convenient method calls 30 | //! let mut buffer = buffer.borrow_with(&mut font_system); 31 | //! 32 | //! // Set a size for the text buffer, in pixels 33 | //! buffer.set_size(Some(80.0), Some(25.0)); 34 | //! 35 | //! // Attributes indicate what font to choose 36 | //! let attrs = Attrs::new(); 37 | //! 38 | //! // Add some text! 39 | //! buffer.set_text("Hello, Rust! 🦀\n", &attrs, Shaping::Advanced); 40 | //! 41 | //! // Perform shaping as desired 42 | //! buffer.shape_until_scroll(true); 43 | //! 44 | //! // Inspect the output runs 45 | //! for run in buffer.layout_runs() { 46 | //! for glyph in run.glyphs.iter() { 47 | //! println!("{:#?}", glyph); 48 | //! } 49 | //! } 50 | //! 51 | //! // Create a default text color 52 | //! let text_color = Color::rgb(0xFF, 0xFF, 0xFF); 53 | //! 54 | //! // Draw the buffer (for performance, instead use SwashCache directly) 55 | //! buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| { 56 | //! // Fill in your code here for drawing rectangles 57 | //! }); 58 | //! ``` 59 | 60 | // Not interested in these lints 61 | #![allow(clippy::new_without_default)] 62 | // TODO: address occurrences and then deny 63 | // 64 | // Overflows can produce unpredictable results and are only checked in debug builds 65 | #![allow(clippy::arithmetic_side_effects)] 66 | // Indexing a slice can cause panics and that is something we always want to avoid 67 | #![allow(clippy::indexing_slicing)] 68 | // Soundness issues 69 | // 70 | // Dereferencing unaligned pointers may be undefined behavior 71 | #![deny(clippy::cast_ptr_alignment)] 72 | // Avoid panicking in without information about the panic. Use expect 73 | #![deny(clippy::unwrap_used)] 74 | // Ensure all types have a debug impl 75 | #![deny(missing_debug_implementations)] 76 | // This is usually a serious issue - a missing import of a define where it is interpreted 77 | // as a catch-all variable in a match, for example 78 | #![deny(unreachable_patterns)] 79 | // Ensure that all must_use results are used 80 | #![deny(unused_must_use)] 81 | // Style issues 82 | // 83 | // Documentation not ideal 84 | #![warn(clippy::doc_markdown)] 85 | // Document possible errors 86 | #![warn(clippy::missing_errors_doc)] 87 | // Document possible panics 88 | #![warn(clippy::missing_panics_doc)] 89 | // Ensure semicolons are present 90 | #![warn(clippy::semicolon_if_nothing_returned)] 91 | // Ensure numbers are readable 92 | #![warn(clippy::unreadable_literal)] 93 | #![cfg_attr(not(feature = "std"), no_std)] 94 | extern crate alloc; 95 | 96 | #[cfg(not(any(feature = "std", feature = "no_std")))] 97 | compile_error!("Either the `std` or `no_std` feature must be enabled"); 98 | 99 | pub use self::attrs::*; 100 | mod attrs; 101 | 102 | pub use self::bidi_para::*; 103 | mod bidi_para; 104 | 105 | pub use self::buffer::*; 106 | mod buffer; 107 | 108 | pub use self::buffer_line::*; 109 | mod buffer_line; 110 | 111 | pub use self::cached::*; 112 | mod cached; 113 | 114 | pub use self::glyph_cache::*; 115 | mod glyph_cache; 116 | 117 | pub use self::cursor::*; 118 | mod cursor; 119 | 120 | pub use self::edit::*; 121 | mod edit; 122 | 123 | pub use self::font::*; 124 | mod font; 125 | 126 | pub use self::layout::*; 127 | mod layout; 128 | 129 | pub use self::line_ending::*; 130 | mod line_ending; 131 | 132 | pub use self::shape::*; 133 | mod shape; 134 | 135 | pub use self::shape_run_cache::*; 136 | mod shape_run_cache; 137 | 138 | #[cfg(feature = "swash")] 139 | pub use self::swash::*; 140 | #[cfg(feature = "swash")] 141 | mod swash; 142 | 143 | mod math; 144 | 145 | type BuildHasher = core::hash::BuildHasherDefault; 146 | 147 | #[cfg(feature = "std")] 148 | type HashMap = std::collections::HashMap; 149 | #[cfg(not(feature = "std"))] 150 | type HashMap = hashbrown::HashMap; 151 | -------------------------------------------------------------------------------- /src/line_ending.rs: -------------------------------------------------------------------------------- 1 | use core::ops::Range; 2 | 3 | /// Line ending 4 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 5 | pub enum LineEnding { 6 | /// Use `\n` for line ending (POSIX-style) 7 | #[default] 8 | Lf, 9 | /// Use `\r\n` for line ending (Windows-style) 10 | CrLf, 11 | /// Use `\r` for line ending (many legacy systems) 12 | Cr, 13 | /// Use `\n\r` for line ending (some legacy systems) 14 | LfCr, 15 | /// No line ending 16 | None, 17 | } 18 | 19 | impl LineEnding { 20 | /// Get the line ending as a str 21 | pub fn as_str(&self) -> &'static str { 22 | match self { 23 | Self::Lf => "\n", 24 | Self::CrLf => "\r\n", 25 | Self::Cr => "\r", 26 | Self::LfCr => "\n\r", 27 | Self::None => "", 28 | } 29 | } 30 | } 31 | 32 | /// Iterator over lines terminated by [`LineEnding`] 33 | #[derive(Debug)] 34 | pub struct LineIter<'a> { 35 | string: &'a str, 36 | start: usize, 37 | end: usize, 38 | } 39 | 40 | impl<'a> LineIter<'a> { 41 | /// Create an iterator of lines in a string slice 42 | pub fn new(string: &'a str) -> Self { 43 | Self { 44 | string, 45 | start: 0, 46 | end: string.len(), 47 | } 48 | } 49 | } 50 | 51 | impl Iterator for LineIter<'_> { 52 | type Item = (Range, LineEnding); 53 | fn next(&mut self) -> Option { 54 | let start = self.start; 55 | match self.string[start..self.end].find(['\r', '\n']) { 56 | Some(i) => { 57 | let end = start + i; 58 | self.start = end; 59 | let after = &self.string[end..]; 60 | let ending = if after.starts_with("\r\n") { 61 | LineEnding::CrLf 62 | } else if after.starts_with("\n\r") { 63 | LineEnding::LfCr 64 | } else if after.starts_with("\n") { 65 | LineEnding::Lf 66 | } else if after.starts_with("\r") { 67 | LineEnding::Cr 68 | } else { 69 | //TODO: this should not be possible 70 | LineEnding::None 71 | }; 72 | self.start += ending.as_str().len(); 73 | Some((start..end, ending)) 74 | } 75 | None => { 76 | if self.start < self.end { 77 | self.start = self.end; 78 | Some((start..self.end, LineEnding::None)) 79 | } else { 80 | None 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | //TODO: DoubleEndedIterator 88 | 89 | #[test] 90 | fn test_line_iter() { 91 | let string = "LF\nCRLF\r\nCR\rLFCR\n\rNONE"; 92 | let mut iter = LineIter::new(string); 93 | assert_eq!(iter.next(), Some((0..2, LineEnding::Lf))); 94 | assert_eq!(iter.next(), Some((3..7, LineEnding::CrLf))); 95 | assert_eq!(iter.next(), Some((9..11, LineEnding::Cr))); 96 | assert_eq!(iter.next(), Some((12..16, LineEnding::LfCr))); 97 | assert_eq!(iter.next(), Some((18..22, LineEnding::None))); 98 | } 99 | -------------------------------------------------------------------------------- /src/math.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "std"))] 2 | pub use libm::{floorf, roundf, truncf}; 3 | 4 | #[cfg(feature = "std")] 5 | #[inline] 6 | pub fn floorf(x: f32) -> f32 { 7 | x.floor() 8 | } 9 | 10 | #[cfg(feature = "std")] 11 | #[inline] 12 | pub fn roundf(x: f32) -> f32 { 13 | x.round() 14 | } 15 | 16 | #[cfg(feature = "std")] 17 | #[inline] 18 | pub fn truncf(x: f32) -> f32 { 19 | x.trunc() 20 | } 21 | -------------------------------------------------------------------------------- /src/shape_run_cache.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "std"))] 2 | use alloc::{string::String, vec::Vec}; 3 | use core::ops::Range; 4 | 5 | use crate::{AttrsOwned, HashMap, ShapeGlyph}; 6 | 7 | /// Key for caching shape runs. 8 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 9 | pub struct ShapeRunKey { 10 | pub text: String, 11 | pub default_attrs: AttrsOwned, 12 | pub attrs_spans: Vec<(Range, AttrsOwned)>, 13 | } 14 | 15 | /// A helper structure for caching shape runs. 16 | #[derive(Clone, Default)] 17 | pub struct ShapeRunCache { 18 | age: u64, 19 | cache: HashMap)>, 20 | } 21 | 22 | impl ShapeRunCache { 23 | /// Get cache item, updating age if found 24 | pub fn get(&mut self, key: &ShapeRunKey) -> Option<&Vec> { 25 | self.cache.get_mut(key).map(|(age, glyphs)| { 26 | *age = self.age; 27 | &*glyphs 28 | }) 29 | } 30 | 31 | /// Insert cache item with current age 32 | pub fn insert(&mut self, key: ShapeRunKey, glyphs: Vec) { 33 | self.cache.insert(key, (self.age, glyphs)); 34 | } 35 | 36 | /// Remove anything in the cache with an age older than `keep_ages` 37 | pub fn trim(&mut self, keep_ages: u64) { 38 | self.cache 39 | .retain(|_key, (age, _glyphs)| *age + keep_ages >= self.age); 40 | // Increase age 41 | self.age += 1; 42 | } 43 | } 44 | 45 | impl core::fmt::Debug for ShapeRunCache { 46 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 47 | f.debug_tuple("ShapeRunCache").finish() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/swash.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | #[cfg(not(feature = "std"))] 4 | use alloc::vec::Vec; 5 | use core::fmt; 6 | use swash::scale::{image::Content, ScaleContext}; 7 | use swash::scale::{Render, Source, StrikeWith}; 8 | use swash::zeno::{Format, Vector}; 9 | 10 | use crate::{CacheKey, CacheKeyFlags, Color, FontSystem, HashMap}; 11 | 12 | pub use swash::scale::image::{Content as SwashContent, Image as SwashImage}; 13 | pub use swash::zeno::{Angle, Command, Placement, Transform}; 14 | 15 | fn swash_image( 16 | font_system: &mut FontSystem, 17 | context: &mut ScaleContext, 18 | cache_key: CacheKey, 19 | ) -> Option { 20 | let font = match font_system.get_font(cache_key.font_id) { 21 | Some(some) => some, 22 | None => { 23 | log::warn!("did not find font {:?}", cache_key.font_id); 24 | return None; 25 | } 26 | }; 27 | 28 | // Build the scaler 29 | let mut scaler = context 30 | .builder(font.as_swash()) 31 | .size(f32::from_bits(cache_key.font_size_bits)) 32 | .hint(true) 33 | .build(); 34 | 35 | // Compute the fractional offset-- you'll likely want to quantize this 36 | // in a real renderer 37 | let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float()); 38 | 39 | // Select our source order 40 | Render::new(&[ 41 | // Color outline with the first palette 42 | Source::ColorOutline(0), 43 | // Color bitmap with best fit selection mode 44 | Source::ColorBitmap(StrikeWith::BestFit), 45 | // Standard scalable outline 46 | Source::Outline, 47 | ]) 48 | // Select a subpixel format 49 | .format(Format::Alpha) 50 | // Apply the fractional offset 51 | .offset(offset) 52 | .transform(if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) { 53 | Some(Transform::skew( 54 | Angle::from_degrees(14.0), 55 | Angle::from_degrees(0.0), 56 | )) 57 | } else { 58 | None 59 | }) 60 | // Render the image 61 | .render(&mut scaler, cache_key.glyph_id) 62 | } 63 | 64 | fn swash_outline_commands( 65 | font_system: &mut FontSystem, 66 | context: &mut ScaleContext, 67 | cache_key: CacheKey, 68 | ) -> Option> { 69 | use swash::zeno::PathData as _; 70 | 71 | let font = match font_system.get_font(cache_key.font_id) { 72 | Some(some) => some, 73 | None => { 74 | log::warn!("did not find font {:?}", cache_key.font_id); 75 | return None; 76 | } 77 | }; 78 | 79 | // Build the scaler 80 | let mut scaler = context 81 | .builder(font.as_swash()) 82 | .size(f32::from_bits(cache_key.font_size_bits)) 83 | .hint(true) 84 | .build(); 85 | 86 | // Scale the outline 87 | let outline = scaler 88 | .scale_outline(cache_key.glyph_id) 89 | .or_else(|| scaler.scale_color_outline(cache_key.glyph_id))?; 90 | 91 | // Get the path information of the outline 92 | let path = outline.path(); 93 | 94 | // Return the commands 95 | Some(path.commands().collect()) 96 | } 97 | 98 | /// Cache for rasterizing with the swash scaler 99 | pub struct SwashCache { 100 | context: ScaleContext, 101 | pub image_cache: HashMap>, 102 | pub outline_command_cache: HashMap>>, 103 | } 104 | 105 | impl fmt::Debug for SwashCache { 106 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 107 | f.pad("SwashCache { .. }") 108 | } 109 | } 110 | 111 | impl SwashCache { 112 | /// Create a new swash cache 113 | pub fn new() -> Self { 114 | Self { 115 | context: ScaleContext::new(), 116 | image_cache: HashMap::default(), 117 | outline_command_cache: HashMap::default(), 118 | } 119 | } 120 | 121 | /// Create a swash Image from a cache key, without caching results 122 | pub fn get_image_uncached( 123 | &mut self, 124 | font_system: &mut FontSystem, 125 | cache_key: CacheKey, 126 | ) -> Option { 127 | swash_image(font_system, &mut self.context, cache_key) 128 | } 129 | 130 | /// Create a swash Image from a cache key, caching results 131 | pub fn get_image( 132 | &mut self, 133 | font_system: &mut FontSystem, 134 | cache_key: CacheKey, 135 | ) -> &Option { 136 | self.image_cache 137 | .entry(cache_key) 138 | .or_insert_with(|| swash_image(font_system, &mut self.context, cache_key)) 139 | } 140 | 141 | /// Creates outline commands 142 | pub fn get_outline_commands( 143 | &mut self, 144 | font_system: &mut FontSystem, 145 | cache_key: CacheKey, 146 | ) -> Option<&[swash::zeno::Command]> { 147 | self.outline_command_cache 148 | .entry(cache_key) 149 | .or_insert_with(|| swash_outline_commands(font_system, &mut self.context, cache_key)) 150 | .as_deref() 151 | } 152 | 153 | /// Creates outline commands, without caching results 154 | pub fn get_outline_commands_uncached( 155 | &mut self, 156 | font_system: &mut FontSystem, 157 | cache_key: CacheKey, 158 | ) -> Option> { 159 | swash_outline_commands(font_system, &mut self.context, cache_key) 160 | } 161 | 162 | /// Enumerate pixels in an Image, use `with_image` for better performance 163 | pub fn with_pixels( 164 | &mut self, 165 | font_system: &mut FontSystem, 166 | cache_key: CacheKey, 167 | base: Color, 168 | mut f: F, 169 | ) { 170 | if let Some(image) = self.get_image(font_system, cache_key) { 171 | let x = image.placement.left; 172 | let y = -image.placement.top; 173 | 174 | match image.content { 175 | Content::Mask => { 176 | let mut i = 0; 177 | for off_y in 0..image.placement.height as i32 { 178 | for off_x in 0..image.placement.width as i32 { 179 | //TODO: blend base alpha? 180 | f( 181 | x + off_x, 182 | y + off_y, 183 | Color(((image.data[i] as u32) << 24) | base.0 & 0xFF_FF_FF), 184 | ); 185 | i += 1; 186 | } 187 | } 188 | } 189 | Content::Color => { 190 | let mut i = 0; 191 | for off_y in 0..image.placement.height as i32 { 192 | for off_x in 0..image.placement.width as i32 { 193 | //TODO: blend base alpha? 194 | f( 195 | x + off_x, 196 | y + off_y, 197 | Color::rgba( 198 | image.data[i], 199 | image.data[i + 1], 200 | image.data[i + 2], 201 | image.data[i + 3], 202 | ), 203 | ); 204 | i += 4; 205 | } 206 | } 207 | } 208 | Content::SubpixelMask => { 209 | log::warn!("TODO: SubpixelMask"); 210 | } 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /terminal.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=cosmic_text=debug,terminal=debug cargo run --release --package terminal -- "$@" 2 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | echo Run CI script 6 | ./ci.sh 7 | 8 | echo Build documentation 9 | cargo doc 10 | 11 | echo Build all examples 12 | cargo build --release --all 13 | 14 | echo Run terminal example 15 | target/release/terminal 16 | 17 | echo Run editor-test example 18 | env RUST_LOG=editor_test=info target/release/editor-test 19 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use cosmic_text::{ 4 | fontdb::Database, Attrs, AttrsOwned, Buffer, Color, Family, FontSystem, Metrics, Shaping, 5 | SwashCache, 6 | }; 7 | use tiny_skia::{Paint, Pixmap, Rect, Transform}; 8 | 9 | /// The test configuration. 10 | /// The text in the test will be rendered as image using the one of the fonts found under the 11 | /// `fonts` directory in this repository. 12 | /// The image will then be compared to an image with the name `name` under the `tests/images` 13 | /// directory in this repository. 14 | /// If the images do not match the test will fail. 15 | /// NOTE: if an environment variable `GENERATE_IMAGES` is set, the test will create and save 16 | /// the images instead. 17 | #[derive(Debug)] 18 | pub struct DrawTestCfg { 19 | /// The name of the test. 20 | /// Will be used for the image name under the `tests/images` directory in this repository. 21 | name: String, 22 | /// The text to render to image 23 | text: String, 24 | /// The name, details of the font to be used. 25 | /// Expected to be one of the fonts found under the `fonts` directory in this repository. 26 | font: AttrsOwned, 27 | 28 | font_size: f32, 29 | line_height: f32, 30 | canvas_width: u32, 31 | canvas_height: u32, 32 | } 33 | 34 | impl Default for DrawTestCfg { 35 | fn default() -> Self { 36 | let font = Attrs::new().family(Family::Serif); 37 | Self { 38 | name: "default".into(), 39 | font: AttrsOwned::new(&font), 40 | text: "".into(), 41 | font_size: 16.0, 42 | line_height: 20.0, 43 | canvas_width: 300, 44 | canvas_height: 300, 45 | } 46 | } 47 | } 48 | 49 | impl DrawTestCfg { 50 | pub fn new(name: impl Into) -> Self { 51 | Self { 52 | name: name.into(), 53 | ..Default::default() 54 | } 55 | } 56 | 57 | pub fn text(mut self, text: impl Into) -> Self { 58 | self.text = text.into(); 59 | self 60 | } 61 | 62 | pub fn font_attrs(mut self, attrs: Attrs) -> Self { 63 | self.font = AttrsOwned::new(&attrs); 64 | self 65 | } 66 | 67 | pub fn font_size(mut self, font_size: f32, line_height: f32) -> Self { 68 | self.font_size = font_size; 69 | self.line_height = line_height; 70 | self 71 | } 72 | 73 | pub fn canvas(mut self, width: u32, height: u32) -> Self { 74 | self.canvas_width = width; 75 | self.canvas_height = height; 76 | self 77 | } 78 | 79 | pub fn validate_text_rendering(self) { 80 | let repo_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); 81 | // Create a db with just the fonts in our fonts dir to make sure we only test those 82 | let fonts_path = PathBuf::from(&repo_dir).join("fonts"); 83 | let mut font_db = Database::new(); 84 | font_db.load_fonts_dir(fonts_path); 85 | let mut font_system = FontSystem::new_with_locale_and_db("En-US".into(), font_db); 86 | let mut swash_cache = SwashCache::new(); 87 | let metrics = Metrics::new(self.font_size, self.line_height); 88 | let mut buffer = Buffer::new(&mut font_system, metrics); 89 | let mut buffer = buffer.borrow_with(&mut font_system); 90 | let margins = 5; 91 | buffer.set_size( 92 | Some((self.canvas_width - margins * 2) as f32), 93 | Some((self.canvas_height - margins * 2) as f32), 94 | ); 95 | buffer.set_text(&self.text, &self.font.as_attrs(), Shaping::Advanced); 96 | buffer.shape_until_scroll(true); 97 | 98 | // Black 99 | let text_color = Color::rgb(0x00, 0x00, 0x00); 100 | 101 | let mut pixmap = Pixmap::new(self.canvas_width, self.canvas_height).unwrap(); 102 | pixmap.fill(tiny_skia::Color::WHITE); 103 | 104 | buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| { 105 | let mut paint = Paint { 106 | anti_alias: true, 107 | ..Paint::default() 108 | }; 109 | paint.set_color_rgba8(color.r(), color.g(), color.b(), color.a()); 110 | let rect = Rect::from_xywh( 111 | (x + margins as i32) as f32, 112 | (y + margins as i32) as f32, 113 | w as f32, 114 | h as f32, 115 | ) 116 | .unwrap(); 117 | pixmap.fill_rect(rect, &paint, Transform::identity(), None); 118 | }); 119 | 120 | let image_name = format!("{}.png", self.name); 121 | let reference_image_path = PathBuf::from(&repo_dir) 122 | .join("tests") 123 | .join("images") 124 | .join(image_name); 125 | 126 | let generate_images = std::env::var("GENERATE_IMAGES") 127 | .map(|v| { 128 | let val = v.trim().to_ascii_lowercase(); 129 | ["t", "true", "1"].iter().any(|&v| v == val) 130 | }) 131 | .unwrap_or_default(); 132 | 133 | if generate_images { 134 | pixmap.save_png(reference_image_path).unwrap(); 135 | } else { 136 | let reference_image_data = std::fs::read(reference_image_path).unwrap(); 137 | let image_data = pixmap.encode_png().unwrap(); 138 | assert_eq!( 139 | reference_image_data, image_data, 140 | "rendering failed of {self:?}" 141 | ) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/editor_modified_state.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "vi")] 2 | 3 | use std::sync::OnceLock; 4 | 5 | use cosmic_text::{Buffer, Cursor, Edit, Metrics, SyntaxEditor, SyntaxSystem, ViEditor}; 6 | 7 | static SYNTAX_SYSTEM: OnceLock = OnceLock::new(); 8 | 9 | // New editor for tests 10 | fn editor() -> ViEditor<'static, 'static> { 11 | // More or less copied from cosmic-edit 12 | let font_size: f32 = 14.0; 13 | let line_height = (font_size * 1.4).ceil(); 14 | 15 | let metrics = Metrics::new(font_size, line_height); 16 | let buffer = Buffer::new_empty(metrics); 17 | let editor = SyntaxEditor::new( 18 | buffer, 19 | SYNTAX_SYSTEM.get_or_init(SyntaxSystem::new), 20 | "base16-eighties.dark", 21 | ) 22 | .expect("Default theme `base16-eighties.dark` should be found"); 23 | 24 | ViEditor::new(editor) 25 | } 26 | 27 | // Tests that inserting into an empty editor correctly sets the editor as modified. 28 | #[test] 29 | fn insert_in_empty_editor_sets_changed() { 30 | let mut editor = editor(); 31 | 32 | assert!(!editor.changed()); 33 | editor.start_change(); 34 | editor.insert_at(Cursor::new(0, 0), "Robert'); DROP TABLE Students;--", None); 35 | editor.finish_change(); 36 | assert!(editor.changed()); 37 | } 38 | 39 | // Tests an edge case where a save point is never set. 40 | // Undoing changes should set the editor back to unmodified. 41 | #[test] 42 | fn insert_and_undo_in_unsaved_editor_is_unchanged() { 43 | let mut editor = editor(); 44 | 45 | assert!(!editor.changed()); 46 | editor.start_change(); 47 | editor.insert_at(Cursor::new(0, 0), "loop {}", None); 48 | editor.finish_change(); 49 | assert!(editor.changed()); 50 | 51 | // Undoing the above change should set the editor as unchanged even if the save state is unset 52 | editor.start_change(); 53 | editor.undo(); 54 | editor.finish_change(); 55 | assert!(!editor.changed()); 56 | } 57 | 58 | #[test] 59 | fn undo_to_save_point_sets_editor_to_unchanged() { 60 | let mut editor = editor(); 61 | 62 | // Latest saved change is the first change 63 | editor.start_change(); 64 | let cursor = editor.insert_at(Cursor::new(0, 0), "Ferris is Rust's ", None); 65 | editor.finish_change(); 66 | assert!( 67 | editor.changed(), 68 | "Editor should be set to changed after insertion" 69 | ); 70 | editor.save_point(); 71 | assert!( 72 | !editor.changed(), 73 | "Editor should be set to unchanged after setting a save point" 74 | ); 75 | 76 | // A new insert should set the editor as modified and the pivot should still be on the first 77 | // change from earlier 78 | editor.start_change(); 79 | editor.insert_at(cursor, "mascot", None); 80 | editor.finish_change(); 81 | assert!( 82 | editor.changed(), 83 | "Editor should be set to changed after inserting text after a save point" 84 | ); 85 | 86 | // Undoing the latest change should set the editor to unmodified again 87 | editor.start_change(); 88 | editor.undo(); 89 | editor.finish_change(); 90 | assert!( 91 | !editor.changed(), 92 | "Editor should be set to unchanged after undoing to save point" 93 | ); 94 | } 95 | 96 | #[test] 97 | fn redoing_to_save_point_sets_editor_as_unchanged() { 98 | let mut editor = editor(); 99 | 100 | // Initial change 101 | assert!( 102 | !editor.changed(), 103 | "Editor should start in an unchanged state" 104 | ); 105 | editor.start_change(); 106 | editor.insert_at(Cursor::new(0, 0), "editor.start_change();", None); 107 | editor.finish_change(); 108 | assert!( 109 | editor.changed(), 110 | "Editor should be set as modified after insert() and finish_change()" 111 | ); 112 | editor.save_point(); 113 | assert!( 114 | !editor.changed(), 115 | "Editor should be unchanged after setting a save point" 116 | ); 117 | 118 | // Change to undo then redo 119 | editor.start_change(); 120 | editor.insert_at(Cursor::new(1, 0), "editor.finish_change()", None); 121 | editor.finish_change(); 122 | assert!( 123 | editor.changed(), 124 | "Editor should be set as modified after insert() and finish_change()" 125 | ); 126 | editor.save_point(); 127 | assert!( 128 | !editor.changed(), 129 | "Editor should be unchanged after setting a save point" 130 | ); 131 | 132 | editor.undo(); 133 | assert!( 134 | editor.changed(), 135 | "Undoing past save point should set editor as changed" 136 | ); 137 | editor.redo(); 138 | assert!( 139 | !editor.changed(), 140 | "Redoing to save point should set editor as unchanged" 141 | ); 142 | } 143 | 144 | #[test] 145 | fn redoing_past_save_point_sets_editor_to_changed() { 146 | let mut editor = editor(); 147 | 148 | // Save point change to undo to and then redo past. 149 | editor.start_change(); 150 | editor.insert_string("Walt Whitman ", None); 151 | editor.finish_change(); 152 | 153 | // Set save point to the change above. 154 | assert!( 155 | editor.changed(), 156 | "Editor should be set as modified after insert() and finish_change()" 157 | ); 158 | editor.save_point(); 159 | assert!( 160 | !editor.changed(), 161 | "Editor should be unchanged after setting a save point" 162 | ); 163 | 164 | editor.start_change(); 165 | editor.insert_string("Allen Ginsberg ", None); 166 | editor.finish_change(); 167 | 168 | editor.start_change(); 169 | editor.insert_string("Jack Kerouac ", None); 170 | editor.finish_change(); 171 | 172 | assert!(editor.changed(), "Editor should be modified insertion"); 173 | 174 | // Undo to Whitman 175 | editor.undo(); 176 | editor.undo(); 177 | assert!( 178 | !editor.changed(), 179 | "Editor should be unmodified after undoing to the save point" 180 | ); 181 | 182 | // Redo to Kerouac 183 | editor.redo(); 184 | editor.redo(); 185 | assert!( 186 | editor.changed(), 187 | "Editor should be modified after redoing past the save point" 188 | ); 189 | } 190 | 191 | #[test] 192 | fn undoing_past_save_point_sets_editor_to_changed() { 193 | let mut editor = editor(); 194 | 195 | editor.start_change(); 196 | editor.insert_string("Robert Fripp ", None); 197 | editor.finish_change(); 198 | 199 | // Save point change to undo past. 200 | editor.start_change(); 201 | editor.insert_string("Thurston Moore ", None); 202 | editor.finish_change(); 203 | 204 | assert!(editor.changed(), "Editor should be changed after insertion"); 205 | editor.save_point(); 206 | assert!( 207 | !editor.changed(), 208 | "Editor should be unchanged after setting a save point" 209 | ); 210 | 211 | editor.start_change(); 212 | editor.insert_string("Kim Deal ", None); 213 | editor.finish_change(); 214 | 215 | // Undo to the first change 216 | editor.undo(); 217 | editor.undo(); 218 | assert!( 219 | editor.changed(), 220 | "Editor should be changed after undoing past save point" 221 | ); 222 | } 223 | 224 | // #[test] 225 | // fn undo_all_changes() { 226 | // unimplemented!() 227 | // } 228 | -------------------------------------------------------------------------------- /tests/images/a_hebrew_paragraph.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2f72802a3ee4244c55f3ee0e1841a0f07dfe19904c57e29f8ab1d18f0e06dcab 3 | size 23101 4 | -------------------------------------------------------------------------------- /tests/images/a_hebrew_word.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e0bc89c5a0fc5b9e79cf30bf66fd456cbf8e3fdca0f8dff110ab2706030e0c43 3 | size 3421 4 | -------------------------------------------------------------------------------- /tests/images/an_arabic_paragraph.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3e7cab5e17e253b6a3ba175fb08468cf79f69877122e1470ccc3a17f3e297512 3 | size 23740 4 | -------------------------------------------------------------------------------- /tests/images/an_arabic_word.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2805960705f701c297ff700840b9fcc296bd0803a0a33d2c7209d7c6ba208c2e 3 | size 3697 4 | -------------------------------------------------------------------------------- /tests/images/some_english_mixed_with_arabic.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d292af9a584bbb04d89b515db917c175f57267602f934c6a9dd30906a75a99aa 3 | size 22853 4 | -------------------------------------------------------------------------------- /tests/images/some_english_mixed_with_hebrew.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e46e15f44eef847fd76a751ec2b198862cafc61e821ac2b5dbdc7c33480a73be 3 | size 57239 4 | -------------------------------------------------------------------------------- /tests/shaping_and_rendering.rs: -------------------------------------------------------------------------------- 1 | use common::DrawTestCfg; 2 | use cosmic_text::Attrs; 3 | use fontdb::Family; 4 | 5 | mod common; 6 | 7 | #[test] 8 | fn test_hebrew_word_rendering() { 9 | let attrs = Attrs::new().family(Family::Name("Noto Sans")); 10 | DrawTestCfg::new("a_hebrew_word") 11 | .font_size(36., 40.) 12 | .font_attrs(attrs) 13 | .text("בדיקה") 14 | .canvas(120, 60) 15 | .validate_text_rendering(); 16 | } 17 | 18 | #[test] 19 | fn test_hebrew_paragraph_rendering() { 20 | let paragraph = "השועל החום המהיר קופץ מעל הכלב העצלן"; 21 | let attrs = Attrs::new().family(Family::Name("Noto Sans")); 22 | DrawTestCfg::new("a_hebrew_paragraph") 23 | .font_size(36., 40.) 24 | .font_attrs(attrs) 25 | .text(paragraph) 26 | .canvas(400, 110) 27 | .validate_text_rendering(); 28 | } 29 | 30 | #[test] 31 | fn test_english_mixed_with_hebrew_paragraph_rendering() { 32 | let paragraph = "Many computer programs fail to display bidirectional text correctly. For example, this page is mostly LTR English script, and here is the RTL Hebrew name Sarah: שרה, spelled sin (ש) on the right, resh (ר) in the middle, and heh (ה) on the left."; 33 | let attrs = Attrs::new().family(Family::Name("Noto Sans")); 34 | DrawTestCfg::new("some_english_mixed_with_hebrew") 35 | .font_size(16., 20.) 36 | .font_attrs(attrs) 37 | .text(paragraph) 38 | .canvas(400, 120) 39 | .validate_text_rendering(); 40 | } 41 | 42 | #[test] 43 | fn test_arabic_word_rendering() { 44 | let attrs = Attrs::new().family(Family::Name("Noto Sans")); 45 | DrawTestCfg::new("an_arabic_word") 46 | .font_size(36., 40.) 47 | .font_attrs(attrs) 48 | .text("خالصة") 49 | .canvas(120, 60) 50 | .validate_text_rendering(); 51 | } 52 | 53 | #[test] 54 | fn test_arabic_paragraph_rendering() { 55 | let paragraph = "الثعلب البني السريع يقفز فوق الكلب الكسول"; 56 | let attrs = Attrs::new().family(Family::Name("Noto Sans")); 57 | DrawTestCfg::new("an_arabic_paragraph") 58 | .font_size(36., 40.) 59 | .font_attrs(attrs) 60 | .text(paragraph) 61 | .canvas(400, 110) 62 | .validate_text_rendering(); 63 | } 64 | 65 | #[test] 66 | fn test_english_mixed_with_arabic_paragraph_rendering() { 67 | let paragraph = "I like to render اللغة العربية in Rust!"; 68 | let attrs = Attrs::new().family(Family::Name("Noto Sans")); 69 | DrawTestCfg::new("some_english_mixed_with_arabic") 70 | .font_size(36., 40.) 71 | .font_attrs(attrs) 72 | .text(paragraph) 73 | .canvas(400, 110) 74 | .validate_text_rendering(); 75 | } 76 | -------------------------------------------------------------------------------- /tests/wrap_stability.rs: -------------------------------------------------------------------------------- 1 | use cosmic_text::{ 2 | fontdb, Align, Attrs, AttrsList, BidiParagraphs, Buffer, Family, FontSystem, LayoutLine, 3 | Metrics, ShapeLine, Shaping, Weight, Wrap, 4 | }; 5 | 6 | // Test for https://github.com/pop-os/cosmic-text/issues/134 7 | // 8 | // Being able to get the same wrapping when feeding the measured width back into ShapeLine::layout 9 | // as the new width limit is very useful for certain UI layout use cases. 10 | #[test] 11 | fn stable_wrap() { 12 | let font_size = 18.0; 13 | let attrs = AttrsList::new( 14 | &Attrs::new() 15 | .family(Family::Name("FiraMono")) 16 | .weight(Weight::MEDIUM), 17 | ); 18 | let mut font_system = 19 | FontSystem::new_with_locale_and_db("en-US".into(), fontdb::Database::new()); 20 | let font = std::fs::read("fonts/FiraMono-Medium.ttf").unwrap(); 21 | font_system.db_mut().load_font_data(font); 22 | 23 | let mut check_wrap = |text: &_, wrap, align_opt, start_width_opt| { 24 | let line = ShapeLine::new(&mut font_system, text, &attrs, Shaping::Advanced, 8); 25 | 26 | let layout_unbounded = line.layout(font_size, start_width_opt, wrap, align_opt, None); 27 | let max_width = layout_unbounded.iter().map(|l| l.w).fold(0.0, f32::max); 28 | let new_limit = match start_width_opt { 29 | Some(start_width) => f32::min(start_width, max_width), 30 | None => max_width, 31 | }; 32 | 33 | let layout_bounded = line.layout(font_size, Some(new_limit), wrap, align_opt, None); 34 | let bounded_max_width = layout_bounded.iter().map(|l| l.w).fold(0.0, f32::max); 35 | 36 | // For debugging: 37 | // dbg_layout_lines(text, &layout_unbounded); 38 | // dbg_layout_lines(text, &layout_bounded); 39 | 40 | assert_eq!( 41 | (max_width, layout_unbounded.len()), 42 | (bounded_max_width, layout_bounded.len()), 43 | "Wrap \"{wrap:?}\" and align \"{align_opt:?}\" with text: \"{text}\"", 44 | ); 45 | for (u, b) in layout_unbounded[1..].iter().zip(layout_bounded[1..].iter()) { 46 | assert_eq!( 47 | u.w, b.w, 48 | "Wrap {wrap:?} and align \"{align_opt:?}\" with text: \"{text}\"", 49 | ); 50 | } 51 | }; 52 | 53 | let hello_sample = std::fs::read_to_string("sample/hello.txt").unwrap(); 54 | let cases = [ 55 | "(6) SomewhatBoringDisplayTransform", 56 | "", 57 | " ", 58 | " ", 59 | " ", 60 | " ", 61 | ] 62 | .into_iter() 63 | // This has several cases where the line would wrap when the computed width was used as the 64 | // width limit. 65 | .chain(BidiParagraphs::new(&hello_sample)); 66 | 67 | for text in cases { 68 | for wrap in [Wrap::None, Wrap::Glyph, Wrap::Word, Wrap::WordOrGlyph] { 69 | for align_opt in [ 70 | None, 71 | Some(Align::Left), 72 | Some(Align::Right), 73 | Some(Align::Center), 74 | //TODO: Align::Justified 75 | Some(Align::End), 76 | ] { 77 | for start_width_opt in [ 78 | None, 79 | Some(f32::MAX), 80 | Some(80.0), 81 | Some(198.2132), 82 | Some(20.0), 83 | Some(4.0), 84 | Some(300.0), 85 | ] { 86 | check_wrap(text, wrap, align_opt, start_width_opt); 87 | let with_spaces = format!("{text} "); 88 | check_wrap(&with_spaces, wrap, align_opt, start_width_opt); 89 | let with_spaces_2 = format!("{text} "); 90 | check_wrap(&with_spaces_2, wrap, align_opt, start_width_opt); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | #[test] 98 | fn wrap_extra_line() { 99 | let mut font_system = FontSystem::new(); 100 | let metrics = Metrics::new(14.0, 20.0); 101 | 102 | let mut buffer = Buffer::new(&mut font_system, metrics); 103 | 104 | let mut buffer = buffer.borrow_with(&mut font_system); 105 | 106 | // Add some text! 107 | buffer.set_wrap(Wrap::Word); 108 | buffer.set_text("Lorem ipsum dolor sit amet, qui minim labore adipisicing\n\nweeewoooo minim sint cillum sint consectetur cupidatat.", &Attrs::new().family(cosmic_text::Family::Name("Inter")), Shaping::Advanced); 109 | 110 | // Set a size for the text buffer, in pixels 111 | buffer.set_size(Some(50.0), Some(1000.0)); 112 | 113 | // Perform shaping as desired 114 | buffer.shape_until_scroll(false); 115 | 116 | let empty_lines = buffer.layout_runs().filter(|x| x.line_w == 0.).count(); 117 | let overflow_lines = buffer.layout_runs().filter(|x| x.line_w > 50.).count(); 118 | 119 | assert_eq!(empty_lines, 1); 120 | assert_eq!(overflow_lines, 4); 121 | } 122 | 123 | #[allow(dead_code)] 124 | fn dbg_layout_lines(text: &str, lines: &[LayoutLine]) { 125 | for line in lines { 126 | let mut s = String::new(); 127 | for glyph in line.glyphs.iter() { 128 | s.push_str(&text[glyph.start..glyph.end]); 129 | } 130 | println!("\"{s}\""); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/wrap_word_fallback.rs: -------------------------------------------------------------------------------- 1 | use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping, Wrap}; 2 | 3 | // Tests the ability to fallback to glyph wrapping when a word can't fit on a line by itself. 4 | // No line should ever overflow the buffer size. 5 | #[test] 6 | fn wrap_word_fallback() { 7 | let mut font_system = 8 | FontSystem::new_with_locale_and_db("en-US".into(), fontdb::Database::new()); 9 | let font = std::fs::read("fonts/Inter-Regular.ttf").unwrap(); 10 | font_system.db_mut().load_font_data(font); 11 | let metrics = Metrics::new(14.0, 20.0); 12 | 13 | let mut buffer = Buffer::new(&mut font_system, metrics); 14 | 15 | let mut buffer = buffer.borrow_with(&mut font_system); 16 | 17 | buffer.set_wrap(Wrap::WordOrGlyph); 18 | buffer.set_text("Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.", &Attrs::new().family(cosmic_text::Family::Name("Inter")), Shaping::Advanced); 19 | buffer.set_size(Some(50.0), Some(1000.0)); 20 | 21 | buffer.shape_until_scroll(false); 22 | 23 | let measured_size = measure(&buffer); 24 | 25 | assert!( 26 | measured_size <= buffer.size().0.unwrap_or(0.0), 27 | "Measured width is larger than buffer width\n{} <= {}", 28 | measured_size, 29 | buffer.size().0.unwrap_or(0.0) 30 | ); 31 | } 32 | 33 | fn measure(buffer: &Buffer) -> f32 { 34 | buffer 35 | .layout_runs() 36 | .fold(0.0f32, |width, run| width.max(run.line_w)) 37 | } 38 | --------------------------------------------------------------------------------