Honor variable font instances
This commit is contained in:
1 parent
f61493fbd1
commit
52d2d89cb6
41 files changed
+11484
-65
No files matched your search
Vendored
+187
@@ -0,0 +1,187 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
name = "cosmic-text"
|
||||
version = "0.14.2"
|
||||
authors = ["Jeremy Soller <[email protected]>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Pure Rust multi-line text handling"
|
||||
documentation = "https://docs.rs/cosmic-text/latest/cosmic_text/"
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/pop-os/cosmic-text"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["vi"]
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"std",
|
||||
"swash",
|
||||
"fontconfig",
|
||||
]
|
||||
fontconfig = [
|
||||
"fontdb/fontconfig",
|
||||
"std",
|
||||
]
|
||||
monospace_fallback = []
|
||||
no_std = [
|
||||
"rustybuzz/libm",
|
||||
"hashbrown",
|
||||
"dep:libm",
|
||||
]
|
||||
peniko = ["dep:peniko"]
|
||||
shape-run-cache = []
|
||||
std = [
|
||||
"fontdb/memmap",
|
||||
"fontdb/std",
|
||||
"rustybuzz/std",
|
||||
"swash?/std",
|
||||
"sys-locale",
|
||||
"ttf-parser/std",
|
||||
"unicode-bidi/std",
|
||||
]
|
||||
vi = [
|
||||
"modit",
|
||||
"syntect",
|
||||
"cosmic_undo_2",
|
||||
]
|
||||
warn_on_missing_glyphs = []
|
||||
wasm-web = ["sys-locale?/js"]
|
||||
|
||||
[lib]
|
||||
name = "cosmic_text"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[test]]
|
||||
name = "editor_modified_state"
|
||||
path = "tests/editor_modified_state.rs"
|
||||
|
||||
[[test]]
|
||||
name = "shaping_and_rendering"
|
||||
path = "tests/shaping_and_rendering.rs"
|
||||
|
||||
[[test]]
|
||||
name = "wrap_stability"
|
||||
path = "tests/wrap_stability.rs"
|
||||
|
||||
[[test]]
|
||||
name = "wrap_word_fallback"
|
||||
path = "tests/wrap_word_fallback.rs"
|
||||
|
||||
[[bench]]
|
||||
name = "layout"
|
||||
path = "benches/layout.rs"
|
||||
harness = false
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "2.4.1"
|
||||
|
||||
[dependencies.cosmic_undo_2]
|
||||
version = "0.2.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.fontdb]
|
||||
version = "0.16"
|
||||
default-features = false
|
||||
|
||||
[dependencies.hashbrown]
|
||||
version = "0.14.1"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.libm]
|
||||
version = "0.2.8"
|
||||
optional = true
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.20"
|
||||
|
||||
[dependencies.modit]
|
||||
version = "0.1.4"
|
||||
optional = true
|
||||
|
||||
[dependencies.peniko]
|
||||
version = "0.3.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.rangemap]
|
||||
version = "1.4.0"
|
||||
|
||||
[dependencies.rustc-hash]
|
||||
version = "1.1.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.rustybuzz]
|
||||
version = "0.14"
|
||||
features = ["libm"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.self_cell]
|
||||
version = "1.0.1"
|
||||
|
||||
[dependencies.smol_str]
|
||||
version = "0.2.2"
|
||||
default-features = false
|
||||
|
||||
[dependencies.swash]
|
||||
version = "0.2.0"
|
||||
features = [
|
||||
"render",
|
||||
"scale",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.syntect]
|
||||
version = "5.1.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.sys-locale]
|
||||
version = "0.3.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.ttf-parser]
|
||||
version = "0.21"
|
||||
default-features = false
|
||||
|
||||
[dependencies.unicode-bidi]
|
||||
version = "0.3.13"
|
||||
features = ["hardcoded-data"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.unicode-linebreak]
|
||||
version = "0.1.5"
|
||||
|
||||
[dependencies.unicode-script]
|
||||
version = "0.5.5"
|
||||
|
||||
[dependencies.unicode-segmentation]
|
||||
version = "1.10.1"
|
||||
|
||||
[dev-dependencies.criterion]
|
||||
version = "0.5.1"
|
||||
features = ["cargo_bench_support"]
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.tiny-skia]
|
||||
version = "0.11.2"
|
||||
|
||||
[profile.test]
|
||||
opt-level = 1
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
[package]
|
||||
name = "cosmic-text"
|
||||
description = "Pure Rust multi-line text handling"
|
||||
version = "0.14.2"
|
||||
authors = ["Jeremy Soller <[email protected]>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
documentation = "https://docs.rs/cosmic-text/latest/cosmic_text/"
|
||||
repository = "https://github.com/pop-os/cosmic-text"
|
||||
rust-version = "1.75"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.4.1"
|
||||
cosmic_undo_2 = { version = "0.2.0", optional = true }
|
||||
fontdb = { version = "0.16", default-features = false }
|
||||
hashbrown = { version = "0.14.1", optional = true, default-features = false }
|
||||
libm = { version = "0.2.8", optional = true }
|
||||
log = "0.4.20"
|
||||
modit = { version = "0.1.4", optional = true }
|
||||
rangemap = "1.4.0"
|
||||
rustc-hash = { version = "1.1.0", default-features = false }
|
||||
rustybuzz = { version = "0.14", default-features = false, features = ["libm"] }
|
||||
self_cell = "1.0.1"
|
||||
smol_str = { version = "0.2.2", default-features = false }
|
||||
syntect = { version = "5.1.0", optional = true }
|
||||
sys-locale = { version = "0.3.1", optional = true }
|
||||
ttf-parser = { version = "0.21", default-features = false }
|
||||
unicode-linebreak = "0.1.5"
|
||||
unicode-script = "0.5.5"
|
||||
unicode-segmentation = "1.10.1"
|
||||
|
||||
[dependencies.swash]
|
||||
version = "0.2.0"
|
||||
default-features = false
|
||||
features = ["render", "scale"]
|
||||
optional = true
|
||||
|
||||
[dependencies.unicode-bidi]
|
||||
version = "0.3.13"
|
||||
default-features = false
|
||||
features = ["hardcoded-data"]
|
||||
|
||||
[dependencies.peniko]
|
||||
version = "0.3.1"
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
default = ["std", "swash", "fontconfig"]
|
||||
fontconfig = ["fontdb/fontconfig", "std"]
|
||||
monospace_fallback = []
|
||||
no_std = ["rustybuzz/libm", "hashbrown", "dep:libm"]
|
||||
peniko = ["dep:peniko"]
|
||||
shape-run-cache = []
|
||||
std = [
|
||||
"fontdb/memmap",
|
||||
"fontdb/std",
|
||||
"rustybuzz/std",
|
||||
"swash?/std",
|
||||
"sys-locale",
|
||||
"ttf-parser/std",
|
||||
"unicode-bidi/std",
|
||||
]
|
||||
vi = ["modit", "syntect", "cosmic_undo_2"]
|
||||
wasm-web = ["sys-locale?/js"]
|
||||
warn_on_missing_glyphs = []
|
||||
|
||||
[[bench]]
|
||||
name = "layout"
|
||||
harness = false
|
||||
|
||||
[workspace]
|
||||
members = ["examples/*"]
|
||||
|
||||
[dev-dependencies]
|
||||
tiny-skia = "0.11.2"
|
||||
criterion = { version = "0.5.1", default-features = false, features = [
|
||||
"cargo_bench_support",
|
||||
] }
|
||||
|
||||
[profile.test]
|
||||
opt-level = 1
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["vi"]
|
||||
Vendored
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 System76
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
# COSMIC Text
|
||||
|
||||
[](https://crates.io/crates/cosmic-text)
|
||||
[](https://docs.rs/cosmic-text)
|
||||

|
||||
[](https://github.com/pop-os/cosmic-text/actions)
|
||||
|
||||
Pure Rust multi-line text handling.
|
||||
|
||||
COSMIC Text provides advanced text shaping, layout, and rendering wrapped up
|
||||
into a simple abstraction. Shaping is provided by rustybuzz, and supports a
|
||||
wide variety of advanced shaping operations. Rendering is provided by swash,
|
||||
which supports ligatures and color emoji. Layout is implemented custom, in safe
|
||||
Rust, and supports bidirectional text. Font fallback is also a custom
|
||||
implementation, reusing some of the static fallback lists in browsers such as
|
||||
Chromium and Firefox. Linux, macOS, and Windows are supported with the full
|
||||
feature set. Other platforms may need to implement font fallback capabilities.
|
||||
|
||||
## Screenshots
|
||||
|
||||
Arabic translation of Universal Declaration of Human Rights
|
||||
[](screenshots/arabic.png)
|
||||
|
||||
Hindi translation of Universal Declaration of Human Rights
|
||||
[](screenshots/hindi.png)
|
||||
|
||||
Simplified Chinese translation of Universal Declaration of Human Rights
|
||||
[](screenshots/chinese-simplified.png)
|
||||
|
||||
## Roadmap
|
||||
|
||||
The following features must be supported before this is "ready":
|
||||
|
||||
- [x] Font loading (using fontdb)
|
||||
- [x] Preset fonts
|
||||
- [x] System fonts
|
||||
- [x] Text styles (bold, italic, etc.)
|
||||
- [x] Per-buffer
|
||||
- [x] Per-span
|
||||
- [x] Font shaping (using rustybuzz)
|
||||
- [x] Cache results
|
||||
- [x] RTL
|
||||
- [x] Bidirectional rendering
|
||||
- [x] Font fallback
|
||||
- [x] Choose font based on locale to work around "unification"
|
||||
- [x] Per-line granularity
|
||||
- [x] Per-character granularity
|
||||
- [x] Font layout
|
||||
- [x] Click detection
|
||||
- [x] Simple wrapping
|
||||
- [ ] Wrapping with indentation
|
||||
- [ ] No wrapping
|
||||
- [ ] Ellipsize
|
||||
- [x] Font rendering (using swash)
|
||||
- [x] Cache results
|
||||
- [x] Font hinting
|
||||
- [x] Ligatures
|
||||
- [x] Color emoji
|
||||
- [x] Text editing
|
||||
- [x] Performance improvements
|
||||
- [x] Text selection
|
||||
- [x] Can automatically recreate https://unicode.org/udhr/ without errors (see below)
|
||||
- [x] Bidirectional selection
|
||||
- [ ] Copy/paste
|
||||
- [x] no_std support (with `default-features = false`)
|
||||
- [ ] no_std font loading
|
||||
- [x] no_std shaping
|
||||
- [x] no_std layout
|
||||
- [ ] no_std rendering
|
||||
|
||||
The UDHR (Universal Declaration of Human Rights) test involves taking the entire
|
||||
set of UDHR translations (almost 500 languages), concatenating them as one file
|
||||
(which ends up being 8 megabytes!), then via the `editor-test` example,
|
||||
automatically simulating the entry of that file into cosmic-text per-character,
|
||||
with the use of backspace and delete tested per character and per line. Then,
|
||||
the final contents of the buffer is compared to the original file. All of the
|
||||
106746 lines are correct.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2012 Google Inc. All Rights Reserved.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+694
@@ -0,0 +1,694 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec::Vec;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use core::ops::Range;
|
||||
use rangemap::RangeMap;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::{CacheKeyFlags, Metrics};
|
||||
|
||||
pub use fontdb::{Family, Stretch, Style, Weight};
|
||||
|
||||
/// Text color
|
||||
#[derive(Clone, Copy, Debug, PartialOrd, Ord, Eq, Hash, PartialEq)]
|
||||
pub struct Color(pub u32);
|
||||
|
||||
impl Color {
|
||||
/// Create new color with red, green, and blue components
|
||||
#[inline]
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self::rgba(r, g, b, 0xFF)
|
||||
}
|
||||
|
||||
/// Create new color with red, green, blue, and alpha components
|
||||
#[inline]
|
||||
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self(((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32))
|
||||
}
|
||||
|
||||
/// Get a tuple over all of the attributes, in `(r, g, b, a)` order.
|
||||
#[inline]
|
||||
pub fn as_rgba_tuple(self) -> (u8, u8, u8, u8) {
|
||||
(self.r(), self.g(), self.b(), self.a())
|
||||
}
|
||||
|
||||
/// Get an array over all of the components, in `[r, g, b, a]` order.
|
||||
#[inline]
|
||||
pub fn as_rgba(self) -> [u8; 4] {
|
||||
[self.r(), self.g(), self.b(), self.a()]
|
||||
}
|
||||
|
||||
/// Get the red component
|
||||
#[inline]
|
||||
pub fn r(&self) -> u8 {
|
||||
((self.0 & 0x00_FF_00_00) >> 16) as u8
|
||||
}
|
||||
|
||||
/// Get the green component
|
||||
#[inline]
|
||||
pub fn g(&self) -> u8 {
|
||||
((self.0 & 0x00_00_FF_00) >> 8) as u8
|
||||
}
|
||||
|
||||
/// Get the blue component
|
||||
#[inline]
|
||||
pub fn b(&self) -> u8 {
|
||||
(self.0 & 0x00_00_00_FF) as u8
|
||||
}
|
||||
|
||||
/// Get the alpha component
|
||||
#[inline]
|
||||
pub fn a(&self) -> u8 {
|
||||
((self.0 & 0xFF_00_00_00) >> 24) as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// An owned version of [`Family`]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum FamilyOwned {
|
||||
Name(SmolStr),
|
||||
Serif,
|
||||
SansSerif,
|
||||
Cursive,
|
||||
Fantasy,
|
||||
Monospace,
|
||||
}
|
||||
|
||||
impl FamilyOwned {
|
||||
pub fn new(family: Family) -> Self {
|
||||
match family {
|
||||
Family::Name(name) => FamilyOwned::Name(SmolStr::from(name)),
|
||||
Family::Serif => FamilyOwned::Serif,
|
||||
Family::SansSerif => FamilyOwned::SansSerif,
|
||||
Family::Cursive => FamilyOwned::Cursive,
|
||||
Family::Fantasy => FamilyOwned::Fantasy,
|
||||
Family::Monospace => FamilyOwned::Monospace,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_family(&self) -> Family {
|
||||
match self {
|
||||
FamilyOwned::Name(name) => Family::Name(name),
|
||||
FamilyOwned::Serif => Family::Serif,
|
||||
FamilyOwned::SansSerif => Family::SansSerif,
|
||||
FamilyOwned::Cursive => Family::Cursive,
|
||||
FamilyOwned::Fantasy => Family::Fantasy,
|
||||
FamilyOwned::Monospace => Family::Monospace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics, but implementing Eq and Hash using u32 representation of f32
|
||||
//TODO: what are the edge cases of this?
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct CacheMetrics {
|
||||
font_size_bits: u32,
|
||||
line_height_bits: u32,
|
||||
}
|
||||
|
||||
impl From<Metrics> for CacheMetrics {
|
||||
fn from(metrics: Metrics) -> Self {
|
||||
Self {
|
||||
font_size_bits: metrics.font_size.to_bits(),
|
||||
line_height_bits: metrics.line_height.to_bits(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CacheMetrics> for Metrics {
|
||||
fn from(metrics: CacheMetrics) -> Self {
|
||||
Self {
|
||||
font_size: f32::from_bits(metrics.font_size_bits),
|
||||
line_height: f32::from_bits(metrics.line_height_bits),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A 4-byte `OpenType` feature tag identifier
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct FeatureTag([u8; 4]);
|
||||
|
||||
impl FeatureTag {
|
||||
pub const fn new(tag: &[u8; 4]) -> Self {
|
||||
Self(*tag)
|
||||
}
|
||||
|
||||
/// Kerning adjusts spacing between specific character pairs
|
||||
pub const KERNING: Self = Self::new(b"kern");
|
||||
/// Standard ligatures (fi, fl, etc.)
|
||||
pub const STANDARD_LIGATURES: Self = Self::new(b"liga");
|
||||
/// Contextual ligatures (context-dependent ligatures)
|
||||
pub const CONTEXTUAL_LIGATURES: Self = Self::new(b"clig");
|
||||
/// Contextual alternates (glyph substitutions based on context)
|
||||
pub const CONTEXTUAL_ALTERNATES: Self = Self::new(b"calt");
|
||||
/// Discretionary ligatures (optional stylistic ligatures)
|
||||
pub const DISCRETIONARY_LIGATURES: Self = Self::new(b"dlig");
|
||||
/// Small caps (lowercase to small capitals)
|
||||
pub const SMALL_CAPS: Self = Self::new(b"smcp");
|
||||
/// All small caps (uppercase and lowercase to small capitals)
|
||||
pub const ALL_SMALL_CAPS: Self = Self::new(b"c2sc");
|
||||
/// Stylistic Set 1 (font-specific alternate glyphs)
|
||||
pub const STYLISTIC_SET_1: Self = Self::new(b"ss01");
|
||||
/// Stylistic Set 2 (font-specific alternate glyphs)
|
||||
pub const STYLISTIC_SET_2: Self = Self::new(b"ss02");
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8; 4] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Feature {
|
||||
pub tag: FeatureTag,
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub struct FontFeatures {
|
||||
pub features: Vec<Feature>,
|
||||
}
|
||||
|
||||
impl FontFeatures {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
features: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, tag: FeatureTag, value: u32) -> &mut Self {
|
||||
self.features.push(Feature { tag, value });
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable a feature (set to 1)
|
||||
pub fn enable(&mut self, tag: FeatureTag) -> &mut Self {
|
||||
self.set(tag, 1)
|
||||
}
|
||||
|
||||
/// Disable a feature (set to 0)
|
||||
pub fn disable(&mut self, tag: FeatureTag) -> &mut Self {
|
||||
self.set(tag, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A 4-byte `OpenType` variation axis tag.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct VariationTag([u8; 4]);
|
||||
|
||||
impl VariationTag {
|
||||
/// Create a variation axis tag from its four-byte representation.
|
||||
pub const fn new(tag: &[u8; 4]) -> Self {
|
||||
Self(*tag)
|
||||
}
|
||||
|
||||
/// Return the four-byte representation of this axis tag.
|
||||
pub const fn as_bytes(&self) -> &[u8; 4] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A variation coordinate with stable equality and hashing for shape caches.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VariationValue(pub f32);
|
||||
|
||||
impl PartialEq for VariationValue {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.0.is_nan() {
|
||||
other.0.is_nan()
|
||||
} else {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for VariationValue {}
|
||||
|
||||
impl Hash for VariationValue {
|
||||
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||
const CANONICAL_NAN_BITS: u32 = 0x7fc0_0000;
|
||||
|
||||
let bits = if self.0.is_nan() {
|
||||
CANONICAL_NAN_BITS
|
||||
} else {
|
||||
// Add +0.0 to canonicalize -0.0 to +0.0.
|
||||
(self.0 + 0.0).to_bits()
|
||||
};
|
||||
|
||||
bits.hash(hasher);
|
||||
}
|
||||
}
|
||||
|
||||
/// One `OpenType` variation axis coordinate.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct FontVariation {
|
||||
pub tag: VariationTag,
|
||||
pub value: VariationValue,
|
||||
}
|
||||
|
||||
/// Variation coordinates applied while shaping a text span.
|
||||
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub struct FontVariations {
|
||||
variations: Vec<FontVariation>,
|
||||
}
|
||||
|
||||
impl FontVariations {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
variations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an axis coordinate. Setting the same tag again replaces its value,
|
||||
/// matching the last-value-wins behavior of CSS variation settings.
|
||||
pub fn set(&mut self, tag: VariationTag, value: f32) -> &mut Self {
|
||||
match self
|
||||
.variations
|
||||
.binary_search_by_key(&tag, |variation| variation.tag)
|
||||
{
|
||||
Ok(index) => self.variations[index].value = VariationValue(value),
|
||||
Err(index) => self.variations.insert(
|
||||
index,
|
||||
FontVariation {
|
||||
tag,
|
||||
value: VariationValue(value),
|
||||
},
|
||||
),
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.variations.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.variations.len()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &FontVariation> {
|
||||
self.variations.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for letter spacing to get around that f32 doesn't implement Eq and Hash
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LetterSpacing(pub f32);
|
||||
|
||||
impl PartialEq for LetterSpacing {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.0.is_nan() {
|
||||
other.0.is_nan()
|
||||
} else {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for LetterSpacing {}
|
||||
|
||||
impl Hash for LetterSpacing {
|
||||
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||
const CANONICAL_NAN_BITS: u32 = 0x7fc0_0000;
|
||||
|
||||
let bits = if self.0.is_nan() {
|
||||
CANONICAL_NAN_BITS
|
||||
} else {
|
||||
// Add +0.0 to canonicalize -0.0 to +0.0
|
||||
(self.0 + 0.0).to_bits()
|
||||
};
|
||||
|
||||
bits.hash(hasher);
|
||||
}
|
||||
}
|
||||
|
||||
/// Text attributes
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Attrs<'a> {
|
||||
//TODO: should this be an option?
|
||||
pub color_opt: Option<Color>,
|
||||
pub family: Family<'a>,
|
||||
/// A specific database face selected by the caller. Font fallback remains
|
||||
/// available when that face does not contain a requested glyph.
|
||||
pub font_id_opt: Option<fontdb::ID>,
|
||||
pub stretch: Stretch,
|
||||
pub style: Style,
|
||||
pub weight: Weight,
|
||||
pub metadata: usize,
|
||||
pub cache_key_flags: CacheKeyFlags,
|
||||
pub metrics_opt: Option<CacheMetrics>,
|
||||
/// Letter spacing (tracking) in EM
|
||||
pub letter_spacing_opt: Option<LetterSpacing>,
|
||||
pub font_features: FontFeatures,
|
||||
pub font_variations: FontVariations,
|
||||
}
|
||||
|
||||
impl<'a> Attrs<'a> {
|
||||
/// Create a new set of attributes with sane defaults
|
||||
///
|
||||
/// This defaults to a regular Sans-Serif font.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
color_opt: None,
|
||||
family: Family::SansSerif,
|
||||
font_id_opt: None,
|
||||
stretch: Stretch::Normal,
|
||||
style: Style::Normal,
|
||||
weight: Weight::NORMAL,
|
||||
metadata: 0,
|
||||
cache_key_flags: CacheKeyFlags::empty(),
|
||||
metrics_opt: None,
|
||||
letter_spacing_opt: None,
|
||||
font_features: FontFeatures::new(),
|
||||
font_variations: FontVariations::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set [Color]
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color_opt = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [Family]
|
||||
pub fn family(mut self, family: Family<'a>) -> Self {
|
||||
self.family = family;
|
||||
self
|
||||
}
|
||||
|
||||
/// Prefer one exact database face before ordinary family matching.
|
||||
pub fn font_id(mut self, font_id: fontdb::ID) -> Self {
|
||||
self.font_id_opt = Some(font_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [Stretch]
|
||||
pub fn stretch(mut self, stretch: Stretch) -> Self {
|
||||
self.stretch = stretch;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [Style]
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [Weight]
|
||||
pub fn weight(mut self, weight: Weight) -> Self {
|
||||
self.weight = weight;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set metadata
|
||||
pub fn metadata(mut self, metadata: usize) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [`CacheKeyFlags`]
|
||||
pub fn cache_key_flags(mut self, cache_key_flags: CacheKeyFlags) -> Self {
|
||||
self.cache_key_flags = cache_key_flags;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [`Metrics`], overriding values in buffer
|
||||
pub fn metrics(mut self, metrics: Metrics) -> Self {
|
||||
self.metrics_opt = Some(metrics.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set letter spacing (tracking) in EM
|
||||
pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
|
||||
self.letter_spacing_opt = Some(LetterSpacing(letter_spacing));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [`FontFeatures`]
|
||||
pub fn font_features(mut self, font_features: FontFeatures) -> Self {
|
||||
self.font_features = font_features;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all variation coordinates used while shaping.
|
||||
pub fn font_variations(mut self, font_variations: FontVariations) -> Self {
|
||||
self.font_variations = font_variations;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set one variation coordinate used while shaping.
|
||||
pub fn font_variation(mut self, tag: VariationTag, value: f32) -> Self {
|
||||
self.font_variations.set(tag, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if font matches
|
||||
pub fn matches(&self, face: &fontdb::FaceInfo) -> bool {
|
||||
//TODO: smarter way of including emoji
|
||||
face.post_script_name.contains("Emoji")
|
||||
|| (face.style == self.style && face.stretch == self.stretch)
|
||||
}
|
||||
|
||||
/// Check if this set of attributes can be shaped with another
|
||||
pub fn compatible(&self, other: &Self) -> bool {
|
||||
self.family == other.family
|
||||
&& self.font_id_opt == other.font_id_opt
|
||||
&& self.stretch == other.stretch
|
||||
&& self.style == other.style
|
||||
&& self.weight == other.weight
|
||||
&& self.font_variations == other.font_variations
|
||||
}
|
||||
}
|
||||
|
||||
/// Font-specific part of [`Attrs`] to be used for matching
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct FontMatchAttrs {
|
||||
family: FamilyOwned,
|
||||
stretch: Stretch,
|
||||
style: Style,
|
||||
weight: Weight,
|
||||
}
|
||||
|
||||
impl<'a> From<&Attrs<'a>> for FontMatchAttrs {
|
||||
fn from(attrs: &Attrs<'a>) -> Self {
|
||||
Self {
|
||||
family: FamilyOwned::new(attrs.family),
|
||||
stretch: attrs.stretch,
|
||||
style: attrs.style,
|
||||
weight: attrs.weight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An owned version of [`Attrs`]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct AttrsOwned {
|
||||
//TODO: should this be an option?
|
||||
pub color_opt: Option<Color>,
|
||||
pub family_owned: FamilyOwned,
|
||||
pub font_id_opt: Option<fontdb::ID>,
|
||||
pub stretch: Stretch,
|
||||
pub style: Style,
|
||||
pub weight: Weight,
|
||||
pub metadata: usize,
|
||||
pub cache_key_flags: CacheKeyFlags,
|
||||
pub metrics_opt: Option<CacheMetrics>,
|
||||
/// Letter spacing (tracking) in EM
|
||||
pub letter_spacing_opt: Option<LetterSpacing>,
|
||||
pub font_features: FontFeatures,
|
||||
pub font_variations: FontVariations,
|
||||
}
|
||||
|
||||
impl AttrsOwned {
|
||||
pub fn new(attrs: &Attrs) -> Self {
|
||||
Self {
|
||||
color_opt: attrs.color_opt,
|
||||
family_owned: FamilyOwned::new(attrs.family),
|
||||
font_id_opt: attrs.font_id_opt,
|
||||
stretch: attrs.stretch,
|
||||
style: attrs.style,
|
||||
weight: attrs.weight,
|
||||
metadata: attrs.metadata,
|
||||
cache_key_flags: attrs.cache_key_flags,
|
||||
metrics_opt: attrs.metrics_opt,
|
||||
letter_spacing_opt: attrs.letter_spacing_opt,
|
||||
font_features: attrs.font_features.clone(),
|
||||
font_variations: attrs.font_variations.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_attrs(&self) -> Attrs {
|
||||
Attrs {
|
||||
color_opt: self.color_opt,
|
||||
family: self.family_owned.as_family(),
|
||||
font_id_opt: self.font_id_opt,
|
||||
stretch: self.stretch,
|
||||
style: self.style,
|
||||
weight: self.weight,
|
||||
metadata: self.metadata,
|
||||
cache_key_flags: self.cache_key_flags,
|
||||
metrics_opt: self.metrics_opt,
|
||||
letter_spacing_opt: self.letter_spacing_opt,
|
||||
font_features: self.font_features.clone(),
|
||||
font_variations: self.font_variations.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List of text attributes to apply to a line
|
||||
//TODO: have this clean up the spans when changes are made
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct AttrsList {
|
||||
defaults: AttrsOwned,
|
||||
pub(crate) spans: RangeMap<usize, AttrsOwned>,
|
||||
}
|
||||
|
||||
impl AttrsList {
|
||||
/// Create a new attributes list with a set of default [Attrs]
|
||||
pub fn new(defaults: &Attrs) -> Self {
|
||||
Self {
|
||||
defaults: AttrsOwned::new(defaults),
|
||||
spans: RangeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default [Attrs]
|
||||
pub fn defaults(&self) -> Attrs {
|
||||
self.defaults.as_attrs()
|
||||
}
|
||||
|
||||
/// Get the current attribute spans
|
||||
pub fn spans(&self) -> Vec<(&Range<usize>, &AttrsOwned)> {
|
||||
self.spans_iter().collect()
|
||||
}
|
||||
|
||||
/// Get an iterator over the current attribute spans
|
||||
pub fn spans_iter(&self) -> impl Iterator<Item = (&Range<usize>, &AttrsOwned)> + '_ {
|
||||
self.spans.iter()
|
||||
}
|
||||
|
||||
/// Clear the current attribute spans
|
||||
pub fn clear_spans(&mut self) {
|
||||
self.spans.clear();
|
||||
}
|
||||
|
||||
/// Add an attribute span, removes any previous matching parts of spans
|
||||
pub fn add_span(&mut self, range: Range<usize>, attrs: &Attrs) {
|
||||
//do not support 1..1 or 2..1 even if by accident.
|
||||
if range.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.spans.insert(range, AttrsOwned::new(attrs));
|
||||
}
|
||||
|
||||
/// Get the attribute span for an index
|
||||
///
|
||||
/// This returns a span that contains the index
|
||||
pub fn get_span(&self, index: usize) -> Attrs {
|
||||
self.spans
|
||||
.get(&index)
|
||||
.map(|v| v.as_attrs())
|
||||
.unwrap_or(self.defaults.as_attrs())
|
||||
}
|
||||
|
||||
/// Split attributes list at an offset
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn split_off(&mut self, index: usize) -> Self {
|
||||
let mut new = Self::new(&self.defaults.as_attrs());
|
||||
let mut removes = Vec::new();
|
||||
|
||||
//get the keys we need to remove or fix.
|
||||
for span in self.spans.iter() {
|
||||
if span.0.end <= index {
|
||||
continue;
|
||||
} else if span.0.start >= index {
|
||||
removes.push((span.0.clone(), false));
|
||||
} else {
|
||||
removes.push((span.0.clone(), true));
|
||||
}
|
||||
}
|
||||
|
||||
for (key, resize) in removes {
|
||||
let (range, attrs) = self
|
||||
.spans
|
||||
.get_key_value(&key.start)
|
||||
.map(|v| (v.0.clone(), v.1.clone()))
|
||||
.expect("attrs span not found");
|
||||
self.spans.remove(key);
|
||||
|
||||
if resize {
|
||||
new.spans.insert(0..range.end - index, attrs.clone());
|
||||
self.spans.insert(range.start..index, attrs);
|
||||
} else {
|
||||
new.spans
|
||||
.insert(range.start - index..range.end - index, attrs);
|
||||
}
|
||||
}
|
||||
new
|
||||
}
|
||||
|
||||
/// Resets the attributes with new defaults.
|
||||
pub(crate) fn reset(mut self, default: &Attrs) -> Self {
|
||||
self.defaults = AttrsOwned::new(default);
|
||||
self.spans.clear();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Attrs, AttrsOwned, FontVariations, VariationTag};
|
||||
use core::hash::{Hash, Hasher};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
fn hash(value: impl Hash) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
value.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variation_coordinates_round_trip_and_change_shape_identity() {
|
||||
let defaults = Attrs::new();
|
||||
let varied = Attrs::new()
|
||||
.font_variation(VariationTag::new(b"opsz"), 32.0)
|
||||
.font_variation(VariationTag::new(b"wght"), 500.0);
|
||||
|
||||
assert!(!defaults.compatible(&varied));
|
||||
assert_ne!(AttrsOwned::new(&defaults), AttrsOwned::new(&varied));
|
||||
assert_ne!(
|
||||
hash(AttrsOwned::new(&defaults)),
|
||||
hash(AttrsOwned::new(&varied))
|
||||
);
|
||||
assert_eq!(AttrsOwned::new(&varied).as_attrs(), varied);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_an_axis_twice_replaces_its_coordinate() {
|
||||
let mut variations = FontVariations::new();
|
||||
variations
|
||||
.set(VariationTag::new(b"opsz"), 14.0)
|
||||
.set(VariationTag::new(b"opsz"), 32.0);
|
||||
|
||||
assert_eq!(variations.len(), 1);
|
||||
assert_eq!(
|
||||
variations.iter().next().map(|variation| variation.value.0),
|
||||
Some(32.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variation_order_does_not_change_shape_identity() {
|
||||
let left = Attrs::new()
|
||||
.font_variation(VariationTag::new(b"wght"), 500.0)
|
||||
.font_variation(VariationTag::new(b"opsz"), 32.0);
|
||||
let right = Attrs::new()
|
||||
.font_variation(VariationTag::new(b"opsz"), 32.0)
|
||||
.font_variation(VariationTag::new(b"wght"), 500.0);
|
||||
|
||||
assert_eq!(left, right);
|
||||
assert!(left.compatible(&right));
|
||||
assert_eq!(hash(AttrsOwned::new(&left)), hash(AttrsOwned::new(&right)));
|
||||
}
|
||||
}
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use unicode_bidi::{bidi_class, BidiClass, BidiInfo, ParagraphInfo};
|
||||
|
||||
/// An iterator over the paragraphs in the input text.
|
||||
/// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour.
|
||||
#[derive(Debug)]
|
||||
pub struct BidiParagraphs<'text> {
|
||||
text: &'text str,
|
||||
info: alloc::vec::IntoIter<ParagraphInfo>,
|
||||
}
|
||||
|
||||
impl<'text> BidiParagraphs<'text> {
|
||||
/// Create an iterator to split the input text into paragraphs
|
||||
/// in accordance with `unicode-bidi` behaviour.
|
||||
pub fn new(text: &'text str) -> Self {
|
||||
let info = BidiInfo::new(text, None);
|
||||
let info = info.paragraphs.into_iter();
|
||||
Self { text, info }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'text> Iterator for BidiParagraphs<'text> {
|
||||
type Item = &'text str;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let para = self.info.next()?;
|
||||
let paragraph = &self.text[para.range];
|
||||
// `para.range` includes the newline that splits the line, so remove it if present
|
||||
let mut char_indices = paragraph.char_indices();
|
||||
if let Some(i) = char_indices.next_back().and_then(|(i, c)| {
|
||||
// `BidiClass::B` is a Paragraph_Separator (various newline characters)
|
||||
(bidi_class(c) == BidiClass::B).then_some(i)
|
||||
}) {
|
||||
Some(¶graph[0..i])
|
||||
} else {
|
||||
Some(paragraph)
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1485
File diff suppressed because it is too large.
Load diff
+310
@@ -0,0 +1,310 @@
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::{string::String, vec::Vec};
|
||||
use core::mem;
|
||||
|
||||
use crate::{
|
||||
Align, Attrs, AttrsList, Cached, FontSystem, LayoutLine, LineEnding, ShapeLine, Shaping, Wrap,
|
||||
};
|
||||
|
||||
/// A line (or paragraph) of text that is shaped and laid out
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BufferLine {
|
||||
text: String,
|
||||
ending: LineEnding,
|
||||
attrs_list: AttrsList,
|
||||
align: Option<Align>,
|
||||
shape_opt: Cached<ShapeLine>,
|
||||
layout_opt: Cached<Vec<LayoutLine>>,
|
||||
shaping: Shaping,
|
||||
metadata: Option<usize>,
|
||||
}
|
||||
|
||||
impl BufferLine {
|
||||
/// Create a new line with the given text and attributes list
|
||||
/// Cached shaping and layout can be done using the [`Self::shape`] and
|
||||
/// [`Self::layout`] functions
|
||||
pub fn new<T: Into<String>>(
|
||||
text: T,
|
||||
ending: LineEnding,
|
||||
attrs_list: AttrsList,
|
||||
shaping: Shaping,
|
||||
) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
ending,
|
||||
attrs_list,
|
||||
align: None,
|
||||
shape_opt: Cached::Empty,
|
||||
layout_opt: Cached::Empty,
|
||||
shaping,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the current line with new internal values.
|
||||
///
|
||||
/// Avoids deallocating internal caches so they can be reused.
|
||||
pub fn reset_new<T: Into<String>>(
|
||||
&mut self,
|
||||
text: T,
|
||||
ending: LineEnding,
|
||||
attrs_list: AttrsList,
|
||||
shaping: Shaping,
|
||||
) {
|
||||
self.text = text.into();
|
||||
self.ending = ending;
|
||||
self.attrs_list = attrs_list;
|
||||
self.align = None;
|
||||
self.shape_opt.set_unused();
|
||||
self.layout_opt.set_unused();
|
||||
self.shaping = shaping;
|
||||
self.metadata = None;
|
||||
}
|
||||
|
||||
/// Get current text
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
/// Set text and attributes list
|
||||
///
|
||||
/// Will reset shape and layout if it differs from current text and attributes list.
|
||||
/// Returns true if the line was reset
|
||||
pub fn set_text<T: AsRef<str>>(
|
||||
&mut self,
|
||||
text: T,
|
||||
ending: LineEnding,
|
||||
attrs_list: AttrsList,
|
||||
) -> bool {
|
||||
let text = text.as_ref();
|
||||
if text != self.text || ending != self.ending || attrs_list != self.attrs_list {
|
||||
self.text.clear();
|
||||
self.text.push_str(text);
|
||||
self.ending = ending;
|
||||
self.attrs_list = attrs_list;
|
||||
self.reset();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume this line, returning only its text contents as a String.
|
||||
pub fn into_text(self) -> String {
|
||||
self.text
|
||||
}
|
||||
|
||||
/// Get line ending
|
||||
pub fn ending(&self) -> LineEnding {
|
||||
self.ending
|
||||
}
|
||||
|
||||
/// Set line ending
|
||||
///
|
||||
/// Will reset shape and layout if it differs from current line ending.
|
||||
/// Returns true if the line was reset
|
||||
pub fn set_ending(&mut self, ending: LineEnding) -> bool {
|
||||
if ending != self.ending {
|
||||
self.ending = ending;
|
||||
self.reset_shaping();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get attributes list
|
||||
pub fn attrs_list(&self) -> &AttrsList {
|
||||
&self.attrs_list
|
||||
}
|
||||
|
||||
/// Set attributes list
|
||||
///
|
||||
/// Will reset shape and layout if it differs from current attributes list.
|
||||
/// Returns true if the line was reset
|
||||
pub fn set_attrs_list(&mut self, attrs_list: AttrsList) -> bool {
|
||||
if attrs_list != self.attrs_list {
|
||||
self.attrs_list = attrs_list;
|
||||
self.reset_shaping();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Text alignment
|
||||
pub fn align(&self) -> Option<Align> {
|
||||
self.align
|
||||
}
|
||||
|
||||
/// Set the text alignment
|
||||
///
|
||||
/// Will reset layout if it differs from current alignment.
|
||||
/// Setting to None will use `Align::Right` for RTL lines, and `Align::Left` for LTR lines.
|
||||
/// Returns true if the line was reset
|
||||
pub fn set_align(&mut self, align: Option<Align>) -> bool {
|
||||
if align != self.align {
|
||||
self.align = align;
|
||||
self.reset_layout();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Append line at end of this line
|
||||
///
|
||||
/// The wrap setting of the appended line will be lost
|
||||
pub fn append(&mut self, other: Self) {
|
||||
let len = self.text.len();
|
||||
self.text.push_str(other.text());
|
||||
|
||||
if other.attrs_list.defaults() != self.attrs_list.defaults() {
|
||||
// If default formatting does not match, make a new span for it
|
||||
self.attrs_list
|
||||
.add_span(len..len + other.text().len(), &other.attrs_list.defaults());
|
||||
}
|
||||
|
||||
for (other_range, attrs) in other.attrs_list.spans_iter() {
|
||||
// Add previous attrs spans
|
||||
let range = other_range.start + len..other_range.end + len;
|
||||
self.attrs_list.add_span(range, &attrs.as_attrs());
|
||||
}
|
||||
|
||||
self.reset();
|
||||
}
|
||||
|
||||
/// Split off new line at index
|
||||
pub fn split_off(&mut self, index: usize) -> Self {
|
||||
let text = self.text.split_off(index);
|
||||
let attrs_list = self.attrs_list.split_off(index);
|
||||
self.reset();
|
||||
|
||||
let mut new = Self::new(text, self.ending, attrs_list, self.shaping);
|
||||
new.align = self.align;
|
||||
new
|
||||
}
|
||||
|
||||
/// Reset shaping, layout, and metadata caches
|
||||
pub fn reset(&mut self) {
|
||||
self.metadata = None;
|
||||
self.reset_shaping();
|
||||
}
|
||||
|
||||
/// Reset shaping and layout caches
|
||||
pub fn reset_shaping(&mut self) {
|
||||
self.shape_opt.set_unused();
|
||||
self.reset_layout();
|
||||
}
|
||||
|
||||
/// Reset only layout cache
|
||||
pub fn reset_layout(&mut self) {
|
||||
self.layout_opt.set_unused();
|
||||
}
|
||||
|
||||
/// Shape line, will cache results
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn shape(&mut self, font_system: &mut FontSystem, tab_width: u16) -> &ShapeLine {
|
||||
if self.shape_opt.is_unused() {
|
||||
let mut line = self
|
||||
.shape_opt
|
||||
.take_unused()
|
||||
.unwrap_or_else(ShapeLine::empty);
|
||||
line.build(
|
||||
font_system,
|
||||
&self.text,
|
||||
&self.attrs_list,
|
||||
self.shaping,
|
||||
tab_width,
|
||||
);
|
||||
self.shape_opt.set_used(line);
|
||||
self.layout_opt.set_unused();
|
||||
}
|
||||
self.shape_opt.get().expect("shape not found")
|
||||
}
|
||||
|
||||
/// Get line shaping cache
|
||||
pub fn shape_opt(&self) -> Option<&ShapeLine> {
|
||||
self.shape_opt.get()
|
||||
}
|
||||
|
||||
/// Layout line, will cache results
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn layout(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
font_size: f32,
|
||||
width_opt: Option<f32>,
|
||||
wrap: Wrap,
|
||||
match_mono_width: Option<f32>,
|
||||
tab_width: u16,
|
||||
) -> &[LayoutLine] {
|
||||
if self.layout_opt.is_unused() {
|
||||
let align = self.align;
|
||||
let mut layout = self
|
||||
.layout_opt
|
||||
.take_unused()
|
||||
.unwrap_or_else(|| Vec::with_capacity(1));
|
||||
let shape = self.shape(font_system, tab_width);
|
||||
shape.layout_to_buffer(
|
||||
&mut font_system.shape_buffer,
|
||||
font_size,
|
||||
width_opt,
|
||||
wrap,
|
||||
align,
|
||||
&mut layout,
|
||||
match_mono_width,
|
||||
);
|
||||
self.layout_opt.set_used(layout);
|
||||
}
|
||||
self.layout_opt.get().expect("layout not found")
|
||||
}
|
||||
|
||||
/// Get line layout cache
|
||||
pub fn layout_opt(&self) -> Option<&Vec<LayoutLine>> {
|
||||
self.layout_opt.get()
|
||||
}
|
||||
|
||||
/// Get line metadata. This will be None if [`BufferLine::set_metadata`] has not been called
|
||||
/// after the last reset of shaping and layout caches
|
||||
pub fn metadata(&self) -> Option<usize> {
|
||||
self.metadata
|
||||
}
|
||||
|
||||
/// Set line metadata. This is stored until the next line reset
|
||||
pub fn set_metadata(&mut self, metadata: usize) {
|
||||
self.metadata = Some(metadata);
|
||||
}
|
||||
|
||||
/// Makes an empty buffer line.
|
||||
///
|
||||
/// The buffer line is in an invalid state after this is called. See [`Self::reset_new`].
|
||||
pub(crate) fn empty() -> Self {
|
||||
Self {
|
||||
text: String::default(),
|
||||
ending: LineEnding::default(),
|
||||
attrs_list: AttrsList::new(&Attrs::new()),
|
||||
align: None,
|
||||
shape_opt: Cached::Empty,
|
||||
layout_opt: Cached::Empty,
|
||||
shaping: Shaping::Advanced,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reclaim attributes list memory that isn't needed any longer.
|
||||
///
|
||||
/// The buffer line is in an invalid state after this is called. See [`Self::reset_new`].
|
||||
pub(crate) fn reclaim_attrs(&mut self) -> AttrsList {
|
||||
mem::replace(&mut self.attrs_list, AttrsList::new(&Attrs::new()))
|
||||
}
|
||||
|
||||
/// Reclaim text memory that isn't needed any longer.
|
||||
///
|
||||
/// The buffer line is in an invalid state after this is called. See [`Self::reset_new`].
|
||||
pub(crate) fn reclaim_text(&mut self) -> String {
|
||||
let mut text = mem::take(&mut self.text);
|
||||
text.clear();
|
||||
text
|
||||
}
|
||||
}
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
use core::fmt::Debug;
|
||||
use core::mem;
|
||||
|
||||
/// Helper for caching a value when the value is optionally present in the 'unused' state.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Cached<T: Clone + Debug> {
|
||||
Empty,
|
||||
Unused(T),
|
||||
Used(T),
|
||||
}
|
||||
|
||||
impl<T: Clone + Debug> Cached<T> {
|
||||
/// Gets the value if in state `Self::Used`.
|
||||
pub fn get(&self) -> Option<&T> {
|
||||
match self {
|
||||
Self::Empty | Self::Unused(_) => None,
|
||||
Self::Used(t) => Some(t),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the value mutably if in state `Self::Used`.
|
||||
pub fn get_mut(&mut self) -> Option<&mut T> {
|
||||
match self {
|
||||
Self::Empty | Self::Unused(_) => None,
|
||||
Self::Used(t) => Some(t),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the value is empty or unused.
|
||||
pub fn is_unused(&self) -> bool {
|
||||
match self {
|
||||
Self::Empty | Self::Unused(_) => true,
|
||||
Self::Used(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the value is used (i.e. cached for access).
|
||||
pub fn is_used(&self) -> bool {
|
||||
match self {
|
||||
Self::Empty | Self::Unused(_) => false,
|
||||
Self::Used(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the buffered value if in state `Self::Unused`.
|
||||
pub fn take_unused(&mut self) -> Option<T> {
|
||||
if matches!(*self, Self::Unused(_)) {
|
||||
let Self::Unused(val) = mem::replace(self, Self::Empty) else {
|
||||
unreachable!()
|
||||
};
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the cached value if in state `Self::Used`.
|
||||
pub fn take_used(&mut self) -> Option<T> {
|
||||
if matches!(*self, Self::Used(_)) {
|
||||
let Self::Used(val) = mem::replace(self, Self::Empty) else {
|
||||
unreachable!()
|
||||
};
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the value from `Self::Used` to `Self::Unused`.
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn set_unused(&mut self) {
|
||||
if matches!(*self, Self::Used(_)) {
|
||||
*self = Self::Unused(self.take_used().expect("cached value should be used"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the value to `Self::Used`.
|
||||
pub fn set_used(&mut self, val: T) {
|
||||
*self = Self::Used(val);
|
||||
}
|
||||
}
|
||||
Vendored
+156
@@ -0,0 +1,156 @@
|
||||
/// Current cursor location
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct Cursor {
|
||||
/// Index of [`BufferLine`] in [`Buffer::lines`]
|
||||
pub line: usize,
|
||||
/// First-byte-index of glyph at cursor (will insert behind this glyph)
|
||||
pub index: usize,
|
||||
/// Whether to associate the cursor with the run before it or the run after it if placed at the
|
||||
/// boundary between two runs
|
||||
pub affinity: Affinity,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
/// Create a new cursor
|
||||
pub const fn new(line: usize, index: usize) -> Self {
|
||||
Self::new_with_affinity(line, index, Affinity::Before)
|
||||
}
|
||||
|
||||
/// Create a new cursor, specifying the affinity
|
||||
pub const fn new_with_affinity(line: usize, index: usize, affinity: Affinity) -> Self {
|
||||
Self {
|
||||
line,
|
||||
index,
|
||||
affinity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to associate cursors placed at a boundary between runs with the run before or after it.
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum Affinity {
|
||||
#[default]
|
||||
Before,
|
||||
After,
|
||||
}
|
||||
|
||||
impl Affinity {
|
||||
pub fn before(&self) -> bool {
|
||||
*self == Self::Before
|
||||
}
|
||||
|
||||
pub fn after(&self) -> bool {
|
||||
*self == Self::After
|
||||
}
|
||||
|
||||
pub fn from_before(before: bool) -> Self {
|
||||
if before {
|
||||
Self::Before
|
||||
} else {
|
||||
Self::After
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_after(after: bool) -> Self {
|
||||
if after {
|
||||
Self::After
|
||||
} else {
|
||||
Self::Before
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of a cursor within a [`Buffer`].
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct LayoutCursor {
|
||||
/// Index of [`BufferLine`] in [`Buffer::lines`]
|
||||
pub line: usize,
|
||||
/// Index of [`LayoutLine`] in [`BufferLine::layout`]
|
||||
pub layout: usize,
|
||||
/// Index of [`LayoutGlyph`] in [`LayoutLine::glyphs`]
|
||||
pub glyph: usize,
|
||||
}
|
||||
|
||||
impl LayoutCursor {
|
||||
/// Create a new [`LayoutCursor`]
|
||||
pub const fn new(line: usize, layout: usize, glyph: usize) -> Self {
|
||||
Self {
|
||||
line,
|
||||
layout,
|
||||
glyph,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A motion to perform on a [`Cursor`]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Motion {
|
||||
/// Apply specific [`LayoutCursor`]
|
||||
LayoutCursor(LayoutCursor),
|
||||
/// Move cursor to previous character ([`Self::Left`] in LTR, [`Self::Right`] in RTL)
|
||||
Previous,
|
||||
/// Move cursor to next character ([`Self::Right`] in LTR, [`Self::Left`] in RTL)
|
||||
Next,
|
||||
/// Move cursor left
|
||||
Left,
|
||||
/// Move cursor right
|
||||
Right,
|
||||
/// Move cursor up
|
||||
Up,
|
||||
/// Move cursor down
|
||||
Down,
|
||||
/// Move cursor to start of line
|
||||
Home,
|
||||
/// Move cursor to start of line, skipping whitespace
|
||||
SoftHome,
|
||||
/// Move cursor to end of line
|
||||
End,
|
||||
/// Move cursor to start of paragraph
|
||||
ParagraphStart,
|
||||
/// Move cursor to end of paragraph
|
||||
ParagraphEnd,
|
||||
/// Move cursor up one page
|
||||
PageUp,
|
||||
/// Move cursor down one page
|
||||
PageDown,
|
||||
/// Move cursor up or down by a number of pixels
|
||||
Vertical(i32),
|
||||
/// Move cursor to previous word boundary
|
||||
PreviousWord,
|
||||
/// Move cursor to next word boundary
|
||||
NextWord,
|
||||
/// Move cursor to next word boundary to the left
|
||||
LeftWord,
|
||||
/// Move cursor to next word boundary to the right
|
||||
RightWord,
|
||||
/// Move cursor to the start of the document
|
||||
BufferStart,
|
||||
/// Move cursor to the end of the document
|
||||
BufferEnd,
|
||||
/// Move cursor to specific line
|
||||
GotoLine(usize),
|
||||
}
|
||||
|
||||
/// Scroll position in [`Buffer`]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
|
||||
pub struct Scroll {
|
||||
/// Index of [`BufferLine`] in [`Buffer::lines`]. This will be adjusted as needed if layout is
|
||||
/// out of bounds
|
||||
pub line: usize,
|
||||
/// Pixel offset from the start of the [`BufferLine`]. This will be adjusted as needed
|
||||
/// if it is negative or exceeds the height of the [`BufferLine::layout`] lines.
|
||||
pub vertical: f32,
|
||||
/// The horizontal position of scroll in fractional pixels
|
||||
pub horizontal: f32,
|
||||
}
|
||||
|
||||
impl Scroll {
|
||||
/// Create a new scroll
|
||||
pub const fn new(line: usize, vertical: f32, horizontal: f32) -> Self {
|
||||
Self {
|
||||
line,
|
||||
vertical,
|
||||
horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
+922
@@ -0,0 +1,922 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::{
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use core::{cmp, iter::once};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[cfg(feature = "swash")]
|
||||
use crate::Color;
|
||||
use crate::{
|
||||
Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem,
|
||||
Cursor, Edit, FontSystem, LayoutRun, Selection, Shaping,
|
||||
};
|
||||
|
||||
/// A wrapper of [`Buffer`] for easy editing
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Editor<'buffer> {
|
||||
buffer_ref: BufferRef<'buffer>,
|
||||
cursor: Cursor,
|
||||
cursor_x_opt: Option<i32>,
|
||||
selection: Selection,
|
||||
cursor_moved: bool,
|
||||
auto_indent: bool,
|
||||
change: Option<Change>,
|
||||
}
|
||||
|
||||
fn cursor_glyph_opt(cursor: &Cursor, run: &LayoutRun) -> Option<(usize, f32)> {
|
||||
if cursor.line == run.line_i {
|
||||
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
||||
if cursor.index == glyph.start {
|
||||
return Some((glyph_i, 0.0));
|
||||
} else if cursor.index > glyph.start && cursor.index < glyph.end {
|
||||
// Guess x offset based on characters
|
||||
let mut before = 0;
|
||||
let mut total = 0;
|
||||
|
||||
let cluster = &run.text[glyph.start..glyph.end];
|
||||
for (i, _) in cluster.grapheme_indices(true) {
|
||||
if glyph.start + i < cursor.index {
|
||||
before += 1;
|
||||
}
|
||||
total += 1;
|
||||
}
|
||||
|
||||
let offset = glyph.w * (before as f32) / (total as f32);
|
||||
return Some((glyph_i, offset));
|
||||
}
|
||||
}
|
||||
match run.glyphs.last() {
|
||||
Some(glyph) => {
|
||||
if cursor.index == glyph.end {
|
||||
return Some((run.glyphs.len(), 0.0));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Some((0, 0.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn cursor_position(cursor: &Cursor, run: &LayoutRun) -> Option<(i32, i32)> {
|
||||
let (cursor_glyph, cursor_glyph_offset) = cursor_glyph_opt(cursor, run)?;
|
||||
let x = match run.glyphs.get(cursor_glyph) {
|
||||
Some(glyph) => {
|
||||
// Start of detected glyph
|
||||
if glyph.level.is_rtl() {
|
||||
(glyph.x + glyph.w - cursor_glyph_offset) as i32
|
||||
} else {
|
||||
(glyph.x + cursor_glyph_offset) as i32
|
||||
}
|
||||
}
|
||||
None => match run.glyphs.last() {
|
||||
Some(glyph) => {
|
||||
// End of last glyph
|
||||
if glyph.level.is_rtl() {
|
||||
glyph.x as i32
|
||||
} else {
|
||||
(glyph.x + glyph.w) as i32
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Start of empty line
|
||||
0
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Some((x, run.line_top as i32))
|
||||
}
|
||||
|
||||
impl<'buffer> Editor<'buffer> {
|
||||
/// Create a new [`Editor`] with the provided [`Buffer`]
|
||||
pub fn new(buffer: impl Into<BufferRef<'buffer>>) -> Self {
|
||||
Self {
|
||||
buffer_ref: buffer.into(),
|
||||
cursor: Cursor::default(),
|
||||
cursor_x_opt: None,
|
||||
selection: Selection::None,
|
||||
cursor_moved: false,
|
||||
auto_indent: false,
|
||||
change: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the editor
|
||||
#[cfg(feature = "swash")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn draw<F>(
|
||||
&self,
|
||||
font_system: &mut FontSystem,
|
||||
cache: &mut crate::SwashCache,
|
||||
text_color: Color,
|
||||
cursor_color: Color,
|
||||
selection_color: Color,
|
||||
selected_text_color: Color,
|
||||
mut f: F,
|
||||
) where
|
||||
F: FnMut(i32, i32, u32, u32, Color),
|
||||
{
|
||||
let selection_bounds = self.selection_bounds();
|
||||
self.with_buffer(|buffer| {
|
||||
for run in buffer.layout_runs() {
|
||||
let line_i = run.line_i;
|
||||
let line_y = run.line_y;
|
||||
let line_top = run.line_top;
|
||||
let line_height = run.line_height;
|
||||
|
||||
// Highlight selection
|
||||
if let Some((start, end)) = selection_bounds {
|
||||
if line_i >= start.line && line_i <= end.line {
|
||||
let mut range_opt = None;
|
||||
for glyph in run.glyphs.iter() {
|
||||
// Guess x offset based on characters
|
||||
let cluster = &run.text[glyph.start..glyph.end];
|
||||
let total = cluster.grapheme_indices(true).count();
|
||||
let mut c_x = glyph.x;
|
||||
let c_w = glyph.w / total as f32;
|
||||
for (i, c) in cluster.grapheme_indices(true) {
|
||||
let c_start = glyph.start + i;
|
||||
let c_end = glyph.start + i + c.len();
|
||||
if (start.line != line_i || c_end > start.index)
|
||||
&& (end.line != line_i || c_start < end.index)
|
||||
{
|
||||
range_opt = match range_opt.take() {
|
||||
Some((min, max)) => Some((
|
||||
cmp::min(min, c_x as i32),
|
||||
cmp::max(max, (c_x + c_w) as i32),
|
||||
)),
|
||||
None => Some((c_x as i32, (c_x + c_w) as i32)),
|
||||
};
|
||||
} else if let Some((min, max)) = range_opt.take() {
|
||||
f(
|
||||
min,
|
||||
line_top as i32,
|
||||
cmp::max(0, max - min) as u32,
|
||||
line_height as u32,
|
||||
selection_color,
|
||||
);
|
||||
}
|
||||
c_x += c_w;
|
||||
}
|
||||
}
|
||||
|
||||
if run.glyphs.is_empty() && end.line > line_i {
|
||||
// Highlight all of internal empty lines
|
||||
range_opt = Some((0, buffer.size().0.unwrap_or(0.0) as i32));
|
||||
}
|
||||
|
||||
if let Some((mut min, mut max)) = range_opt.take() {
|
||||
if end.line > line_i {
|
||||
// Draw to end of line
|
||||
if run.rtl {
|
||||
min = 0;
|
||||
} else {
|
||||
max = buffer.size().0.unwrap_or(0.0) as i32;
|
||||
}
|
||||
}
|
||||
f(
|
||||
min,
|
||||
line_top as i32,
|
||||
cmp::max(0, max - min) as u32,
|
||||
line_height as u32,
|
||||
selection_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
if let Some((x, y)) = cursor_position(&self.cursor, &run) {
|
||||
f(x, y, 1, line_height as u32, cursor_color);
|
||||
}
|
||||
|
||||
for glyph in run.glyphs.iter() {
|
||||
let physical_glyph = glyph.physical((0., 0.), 1.0);
|
||||
|
||||
let mut glyph_color = match glyph.color_opt {
|
||||
Some(some) => some,
|
||||
None => text_color,
|
||||
};
|
||||
if text_color != selected_text_color {
|
||||
if let Some((start, end)) = selection_bounds {
|
||||
if line_i >= start.line
|
||||
&& line_i <= end.line
|
||||
&& (start.line != line_i || glyph.end > start.index)
|
||||
&& (end.line != line_i || glyph.start < end.index)
|
||||
{
|
||||
glyph_color = selected_text_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache.with_pixels(
|
||||
font_system,
|
||||
physical_glyph.cache_key,
|
||||
glyph_color,
|
||||
|x, y, color| {
|
||||
f(
|
||||
physical_glyph.x + x,
|
||||
line_y as i32 + physical_glyph.y + y,
|
||||
1,
|
||||
1,
|
||||
color,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'buffer> Edit<'buffer> for Editor<'buffer> {
|
||||
fn buffer_ref(&self) -> &BufferRef<'buffer> {
|
||||
&self.buffer_ref
|
||||
}
|
||||
|
||||
fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer> {
|
||||
&mut self.buffer_ref
|
||||
}
|
||||
|
||||
fn cursor(&self) -> Cursor {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, cursor: Cursor) {
|
||||
if self.cursor != cursor {
|
||||
self.cursor = cursor;
|
||||
self.cursor_moved = true;
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
}
|
||||
|
||||
fn selection(&self) -> Selection {
|
||||
self.selection
|
||||
}
|
||||
|
||||
fn set_selection(&mut self, selection: Selection) {
|
||||
if self.selection != selection {
|
||||
self.selection = selection;
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_indent(&self) -> bool {
|
||||
self.auto_indent
|
||||
}
|
||||
|
||||
fn set_auto_indent(&mut self, auto_indent: bool) {
|
||||
self.auto_indent = auto_indent;
|
||||
}
|
||||
|
||||
fn tab_width(&self) -> u16 {
|
||||
self.with_buffer(|buffer| buffer.tab_width())
|
||||
}
|
||||
|
||||
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) {
|
||||
self.with_buffer_mut(|buffer| buffer.set_tab_width(font_system, tab_width));
|
||||
}
|
||||
|
||||
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) {
|
||||
if self.cursor_moved {
|
||||
let cursor = self.cursor;
|
||||
self.with_buffer_mut(|buffer| buffer.shape_until_cursor(font_system, cursor, prune));
|
||||
self.cursor_moved = false;
|
||||
} else {
|
||||
self.with_buffer_mut(|buffer| buffer.shape_until_scroll(font_system, prune));
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_range(&mut self, start: Cursor, end: Cursor) {
|
||||
let change_item = self.with_buffer_mut(|buffer| {
|
||||
// Collect removed data for change tracking
|
||||
let mut change_lines = Vec::new();
|
||||
|
||||
// Delete the selection from the last line
|
||||
let end_line_opt = if end.line > start.line {
|
||||
// Get part of line after selection
|
||||
let after = buffer.lines[end.line].split_off(end.index);
|
||||
|
||||
// Remove end line
|
||||
let removed = buffer.lines.remove(end.line);
|
||||
change_lines.insert(0, removed.text().to_string());
|
||||
|
||||
Some(after)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Delete interior lines (in reverse for safety)
|
||||
for line_i in (start.line + 1..end.line).rev() {
|
||||
let removed = buffer.lines.remove(line_i);
|
||||
change_lines.insert(0, removed.text().to_string());
|
||||
}
|
||||
|
||||
// Delete the selection from the first line
|
||||
{
|
||||
// Get part after selection if start line is also end line
|
||||
let after_opt = if start.line == end.line {
|
||||
Some(buffer.lines[start.line].split_off(end.index))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Delete selected part of line
|
||||
let removed = buffer.lines[start.line].split_off(start.index);
|
||||
change_lines.insert(0, removed.text().to_string());
|
||||
|
||||
// Re-add part of line after selection
|
||||
if let Some(after) = after_opt {
|
||||
buffer.lines[start.line].append(after);
|
||||
}
|
||||
|
||||
// Re-add valid parts of end line
|
||||
if let Some(end_line) = end_line_opt {
|
||||
buffer.lines[start.line].append(end_line);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeItem {
|
||||
start,
|
||||
end,
|
||||
text: change_lines.join("\n"),
|
||||
insert: false,
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(ref mut change) = self.change {
|
||||
change.items.push(change_item);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_at(
|
||||
&mut self,
|
||||
mut cursor: Cursor,
|
||||
data: &str,
|
||||
attrs_list: Option<AttrsList>,
|
||||
) -> Cursor {
|
||||
let mut remaining_split_len = data.len();
|
||||
if remaining_split_len == 0 {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
let change_item = self.with_buffer_mut(|buffer| {
|
||||
// Save cursor for change tracking
|
||||
let start = cursor;
|
||||
|
||||
// Ensure there are enough lines in the buffer to handle this cursor
|
||||
while cursor.line >= buffer.lines.len() {
|
||||
let ending = buffer
|
||||
.lines
|
||||
.last()
|
||||
.map(|line| line.ending())
|
||||
.unwrap_or_default();
|
||||
let line = BufferLine::new(
|
||||
String::new(),
|
||||
ending,
|
||||
AttrsList::new(&attrs_list.as_ref().map_or_else(
|
||||
|| {
|
||||
buffer
|
||||
.lines
|
||||
.last()
|
||||
.map_or(Attrs::new(), |line| line.attrs_list().defaults())
|
||||
},
|
||||
|x| x.defaults(),
|
||||
)),
|
||||
Shaping::Advanced,
|
||||
);
|
||||
buffer.lines.push(line);
|
||||
}
|
||||
|
||||
let line: &mut BufferLine = &mut buffer.lines[cursor.line];
|
||||
let insert_line = cursor.line + 1;
|
||||
let ending = line.ending();
|
||||
|
||||
// Collect text after insertion as a line
|
||||
let after: BufferLine = line.split_off(cursor.index);
|
||||
let after_len = after.text().len();
|
||||
|
||||
// Collect attributes
|
||||
let mut final_attrs = attrs_list.unwrap_or_else(|| {
|
||||
AttrsList::new(&line.attrs_list().get_span(cursor.index.saturating_sub(1)))
|
||||
});
|
||||
|
||||
// Append the inserted text, line by line
|
||||
// we want to see a blank entry if the string ends with a newline
|
||||
//TODO: adjust this to get line ending from data?
|
||||
let addendum = once("").filter(|_| data.ends_with('\n'));
|
||||
let mut lines_iter = data.split_inclusive('\n').chain(addendum);
|
||||
if let Some(data_line) = lines_iter.next() {
|
||||
let mut these_attrs = final_attrs.split_off(data_line.len());
|
||||
remaining_split_len -= data_line.len();
|
||||
core::mem::swap(&mut these_attrs, &mut final_attrs);
|
||||
line.append(BufferLine::new(
|
||||
data_line
|
||||
.strip_suffix(char::is_control)
|
||||
.unwrap_or(data_line),
|
||||
ending,
|
||||
these_attrs,
|
||||
Shaping::Advanced,
|
||||
));
|
||||
} else {
|
||||
panic!("str::lines() did not yield any elements");
|
||||
}
|
||||
if let Some(data_line) = lines_iter.next_back() {
|
||||
remaining_split_len -= data_line.len();
|
||||
let mut tmp = BufferLine::new(
|
||||
data_line
|
||||
.strip_suffix(char::is_control)
|
||||
.unwrap_or(data_line),
|
||||
ending,
|
||||
final_attrs.split_off(remaining_split_len),
|
||||
Shaping::Advanced,
|
||||
);
|
||||
tmp.append(after);
|
||||
buffer.lines.insert(insert_line, tmp);
|
||||
cursor.line += 1;
|
||||
} else {
|
||||
line.append(after);
|
||||
}
|
||||
for data_line in lines_iter.rev() {
|
||||
remaining_split_len -= data_line.len();
|
||||
let tmp = BufferLine::new(
|
||||
data_line
|
||||
.strip_suffix(char::is_control)
|
||||
.unwrap_or(data_line),
|
||||
ending,
|
||||
final_attrs.split_off(remaining_split_len),
|
||||
Shaping::Advanced,
|
||||
);
|
||||
buffer.lines.insert(insert_line, tmp);
|
||||
cursor.line += 1;
|
||||
}
|
||||
|
||||
assert_eq!(remaining_split_len, 0);
|
||||
|
||||
// Append the text after insertion
|
||||
cursor.index = buffer.lines[cursor.line].text().len() - after_len;
|
||||
|
||||
ChangeItem {
|
||||
start,
|
||||
end: cursor,
|
||||
text: data.to_string(),
|
||||
insert: true,
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(ref mut change) = self.change {
|
||||
change.items.push(change_item);
|
||||
}
|
||||
|
||||
cursor
|
||||
}
|
||||
|
||||
fn copy_selection(&self) -> Option<String> {
|
||||
let (start, end) = self.selection_bounds()?;
|
||||
self.with_buffer(|buffer| {
|
||||
let mut selection = String::new();
|
||||
// Take the selection from the first line
|
||||
{
|
||||
// Add selected part of line to string
|
||||
if start.line == end.line {
|
||||
selection.push_str(&buffer.lines[start.line].text()[start.index..end.index]);
|
||||
} else {
|
||||
selection.push_str(&buffer.lines[start.line].text()[start.index..]);
|
||||
selection.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Take the selection from all interior lines (if they exist)
|
||||
for line_i in start.line + 1..end.line {
|
||||
selection.push_str(buffer.lines[line_i].text());
|
||||
selection.push('\n');
|
||||
}
|
||||
|
||||
// Take the selection from the last line
|
||||
if end.line > start.line {
|
||||
// Add selected part of line to string
|
||||
selection.push_str(&buffer.lines[end.line].text()[..end.index]);
|
||||
}
|
||||
|
||||
Some(selection)
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_selection(&mut self) -> bool {
|
||||
let (start, end) = match self.selection_bounds() {
|
||||
Some(some) => some,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
// Reset cursor to start of selection
|
||||
self.cursor = start;
|
||||
|
||||
// Reset selection to None
|
||||
self.selection = Selection::None;
|
||||
|
||||
// Delete from start to end of selection
|
||||
self.delete_range(start, end);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_change(&mut self, change: &Change) -> bool {
|
||||
// Cannot apply changes if there is a pending change
|
||||
if let Some(pending) = self.change.take() {
|
||||
if !pending.items.is_empty() {
|
||||
//TODO: is this a good idea?
|
||||
log::warn!("pending change caused apply_change to be ignored!");
|
||||
self.change = Some(pending);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for item in change.items.iter() {
|
||||
//TODO: edit cursor if needed?
|
||||
if item.insert {
|
||||
self.cursor = self.insert_at(item.start, &item.text, None);
|
||||
} else {
|
||||
self.cursor = item.start;
|
||||
self.delete_range(item.start, item.end);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn start_change(&mut self) {
|
||||
if self.change.is_none() {
|
||||
self.change = Some(Change::default());
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_change(&mut self) -> Option<Change> {
|
||||
self.change.take()
|
||||
}
|
||||
|
||||
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
||||
let old_cursor = self.cursor;
|
||||
|
||||
match action {
|
||||
Action::Motion(motion) => {
|
||||
let cursor = self.cursor;
|
||||
let cursor_x_opt = self.cursor_x_opt;
|
||||
if let Some((new_cursor, new_cursor_x_opt)) = self.with_buffer_mut(|buffer| {
|
||||
buffer.cursor_motion(font_system, cursor, cursor_x_opt, motion)
|
||||
}) {
|
||||
self.cursor = new_cursor;
|
||||
self.cursor_x_opt = new_cursor_x_opt;
|
||||
}
|
||||
}
|
||||
Action::Escape => {
|
||||
match self.selection {
|
||||
Selection::None => {}
|
||||
_ => self.with_buffer_mut(|buffer| buffer.set_redraw(true)),
|
||||
}
|
||||
self.selection = Selection::None;
|
||||
}
|
||||
Action::Insert(character) => {
|
||||
if character.is_control() && !['\t', '\n', '\u{92}'].contains(&character) {
|
||||
// Filter out special chars (except for tab), use Action instead
|
||||
log::debug!("Refusing to insert control character {:?}", character);
|
||||
} else if character == '\n' {
|
||||
self.action(font_system, Action::Enter);
|
||||
} else {
|
||||
let mut str_buf = [0u8; 8];
|
||||
let str_ref = character.encode_utf8(&mut str_buf);
|
||||
self.insert_string(str_ref, None);
|
||||
}
|
||||
}
|
||||
Action::Enter => {
|
||||
//TODO: what about indenting more after opening brackets or parentheses?
|
||||
if self.auto_indent {
|
||||
let mut string = String::from("\n");
|
||||
self.with_buffer(|buffer| {
|
||||
let line = &buffer.lines[self.cursor.line];
|
||||
let text = line.text();
|
||||
for c in text.chars() {
|
||||
if c.is_whitespace() {
|
||||
string.push(c);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
self.insert_string(&string, None);
|
||||
} else {
|
||||
self.insert_string("\n", None);
|
||||
}
|
||||
|
||||
// Ensure line is properly shaped and laid out (for potential immediate commands)
|
||||
let line_i = self.cursor.line;
|
||||
self.with_buffer_mut(|buffer| {
|
||||
buffer.line_layout(font_system, line_i);
|
||||
});
|
||||
}
|
||||
Action::Backspace => {
|
||||
if self.delete_selection() {
|
||||
// Deleted selection
|
||||
} else {
|
||||
// Save current cursor as end
|
||||
let end = self.cursor;
|
||||
|
||||
if self.cursor.index > 0 {
|
||||
// Move cursor to previous character index
|
||||
self.cursor.index = self.with_buffer(|buffer| {
|
||||
buffer.lines[self.cursor.line].text()[..self.cursor.index]
|
||||
.char_indices()
|
||||
.next_back()
|
||||
.map_or(0, |(i, _)| i)
|
||||
});
|
||||
} else if self.cursor.line > 0 {
|
||||
// Move cursor to previous line
|
||||
self.cursor.line -= 1;
|
||||
self.cursor.index =
|
||||
self.with_buffer(|buffer| buffer.lines[self.cursor.line].text().len());
|
||||
}
|
||||
|
||||
if self.cursor != end {
|
||||
// Delete range
|
||||
self.delete_range(self.cursor, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Delete => {
|
||||
if self.delete_selection() {
|
||||
// Deleted selection
|
||||
} else {
|
||||
// Save current cursor as start and end
|
||||
let mut start = self.cursor;
|
||||
let mut end = self.cursor;
|
||||
|
||||
self.with_buffer(|buffer| {
|
||||
if start.index < buffer.lines[start.line].text().len() {
|
||||
let line = &buffer.lines[start.line];
|
||||
|
||||
let range_opt = line
|
||||
.text()
|
||||
.grapheme_indices(true)
|
||||
.take_while(|(i, _)| *i <= start.index)
|
||||
.last()
|
||||
.map(|(i, c)| i..(i + c.len()));
|
||||
|
||||
if let Some(range) = range_opt {
|
||||
start.index = range.start;
|
||||
end.index = range.end;
|
||||
}
|
||||
} else if start.line + 1 < buffer.lines.len() {
|
||||
end.line += 1;
|
||||
end.index = 0;
|
||||
}
|
||||
});
|
||||
|
||||
if start != end {
|
||||
self.cursor = start;
|
||||
self.delete_range(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Indent => {
|
||||
// Get start and end of selection
|
||||
let (start, end) = match self.selection_bounds() {
|
||||
Some(some) => some,
|
||||
None => (self.cursor, self.cursor),
|
||||
};
|
||||
|
||||
// For every line in selection
|
||||
let tab_width: usize = self.tab_width().into();
|
||||
for line_i in start.line..=end.line {
|
||||
// Determine indexes of last indent and first character after whitespace
|
||||
let mut after_whitespace = 0;
|
||||
let mut required_indent = 0;
|
||||
self.with_buffer(|buffer| {
|
||||
let line = &buffer.lines[line_i];
|
||||
let text = line.text();
|
||||
// Default to end of line if no non-whitespace found
|
||||
after_whitespace = text.len();
|
||||
for (count, (index, c)) in text.char_indices().enumerate() {
|
||||
if !c.is_whitespace() {
|
||||
after_whitespace = index;
|
||||
required_indent = tab_width - (count % tab_width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// No indent required (not possible?)
|
||||
if required_indent == 0 {
|
||||
required_indent = tab_width;
|
||||
}
|
||||
|
||||
self.insert_at(
|
||||
Cursor::new(line_i, after_whitespace),
|
||||
&" ".repeat(required_indent),
|
||||
None,
|
||||
);
|
||||
|
||||
// Adjust cursor
|
||||
if self.cursor.line == line_i {
|
||||
//TODO: should we be forcing cursor index to current indent location?
|
||||
if self.cursor.index < after_whitespace {
|
||||
self.cursor.index = after_whitespace;
|
||||
}
|
||||
self.cursor.index += required_indent;
|
||||
}
|
||||
|
||||
// Adjust selection
|
||||
match self.selection {
|
||||
Selection::None => {}
|
||||
Selection::Normal(ref mut select)
|
||||
| Selection::Line(ref mut select)
|
||||
| Selection::Word(ref mut select) => {
|
||||
if select.line == line_i && select.index >= after_whitespace {
|
||||
select.index += required_indent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request redraw
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
}
|
||||
Action::Unindent => {
|
||||
// Get start and end of selection
|
||||
let (start, end) = match self.selection_bounds() {
|
||||
Some(some) => some,
|
||||
None => (self.cursor, self.cursor),
|
||||
};
|
||||
|
||||
// For every line in selection
|
||||
let tab_width: usize = self.tab_width().into();
|
||||
for line_i in start.line..=end.line {
|
||||
// Determine indexes of last indent and first character after whitespace
|
||||
let mut last_indent = 0;
|
||||
let mut after_whitespace = 0;
|
||||
self.with_buffer(|buffer| {
|
||||
let line = &buffer.lines[line_i];
|
||||
let text = line.text();
|
||||
// Default to end of line if no non-whitespace found
|
||||
after_whitespace = text.len();
|
||||
for (count, (index, c)) in text.char_indices().enumerate() {
|
||||
if !c.is_whitespace() {
|
||||
after_whitespace = index;
|
||||
break;
|
||||
}
|
||||
if count % tab_width == 0 {
|
||||
last_indent = index;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// No de-indent required
|
||||
if last_indent == after_whitespace {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete one indent
|
||||
self.delete_range(
|
||||
Cursor::new(line_i, last_indent),
|
||||
Cursor::new(line_i, after_whitespace),
|
||||
);
|
||||
|
||||
// Adjust cursor
|
||||
if self.cursor.line == line_i && self.cursor.index > last_indent {
|
||||
self.cursor.index -= after_whitespace - last_indent;
|
||||
}
|
||||
|
||||
// Adjust selection
|
||||
match self.selection {
|
||||
Selection::None => {}
|
||||
Selection::Normal(ref mut select)
|
||||
| Selection::Line(ref mut select)
|
||||
| Selection::Word(ref mut select) => {
|
||||
if select.line == line_i && select.index > last_indent {
|
||||
select.index -= after_whitespace - last_indent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request redraw
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
}
|
||||
Action::Click { x, y } => {
|
||||
self.set_selection(Selection::None);
|
||||
|
||||
if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32))
|
||||
{
|
||||
if new_cursor != self.cursor {
|
||||
self.cursor = new_cursor;
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::DoubleClick { x, y } => {
|
||||
self.set_selection(Selection::None);
|
||||
|
||||
if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32))
|
||||
{
|
||||
if new_cursor != self.cursor {
|
||||
self.cursor = new_cursor;
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
self.selection = Selection::Word(self.cursor);
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
}
|
||||
Action::TripleClick { x, y } => {
|
||||
self.set_selection(Selection::None);
|
||||
|
||||
if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32))
|
||||
{
|
||||
if new_cursor != self.cursor {
|
||||
self.cursor = new_cursor;
|
||||
}
|
||||
self.selection = Selection::Line(self.cursor);
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
}
|
||||
Action::Drag { x, y } => {
|
||||
if self.selection == Selection::None {
|
||||
self.selection = Selection::Normal(self.cursor);
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
|
||||
if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32))
|
||||
{
|
||||
if new_cursor != self.cursor {
|
||||
self.cursor = new_cursor;
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Scroll { lines } => {
|
||||
self.with_buffer_mut(|buffer| {
|
||||
let mut scroll = buffer.scroll();
|
||||
//TODO: align to layout lines
|
||||
scroll.vertical += lines as f32 * buffer.metrics().line_height;
|
||||
buffer.set_scroll(scroll);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if old_cursor != self.cursor {
|
||||
self.cursor_moved = true;
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||
|
||||
/*TODO
|
||||
if let Some(glyph) = run.glyphs.get(new_cursor_glyph) {
|
||||
let font_opt = self.buffer.font_system().get_font(glyph.cache_key.font_id);
|
||||
let text_glyph = &run.text[glyph.start..glyph.end];
|
||||
log::debug!(
|
||||
"{}, {}: '{}' ('{}'): '{}' ({:?})",
|
||||
self.cursor.line,
|
||||
self.cursor.index,
|
||||
font_opt.as_ref().map_or("?", |font| font.info.family.as_str()),
|
||||
font_opt.as_ref().map_or("?", |font| font.info.post_script_name.as_str()),
|
||||
text_glyph,
|
||||
text_glyph
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_position(&self) -> Option<(i32, i32)> {
|
||||
self.with_buffer(|buffer| {
|
||||
buffer
|
||||
.layout_runs()
|
||||
.find_map(|run| cursor_position(&self.cursor, &run))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BorrowedWithFontSystem<'_, Editor<'_>> {
|
||||
#[cfg(feature = "swash")]
|
||||
pub fn draw<F>(
|
||||
&mut self,
|
||||
cache: &mut crate::SwashCache,
|
||||
text_color: Color,
|
||||
cursor_color: Color,
|
||||
selection_color: Color,
|
||||
selected_text_color: Color,
|
||||
f: F,
|
||||
) where
|
||||
F: FnMut(i32, i32, u32, u32, Color),
|
||||
{
|
||||
self.inner.draw(
|
||||
self.font_system,
|
||||
cache,
|
||||
text_color,
|
||||
cursor_color,
|
||||
selection_color,
|
||||
selected_text_color,
|
||||
f,
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+366
@@ -0,0 +1,366 @@
|
||||
use alloc::sync::Arc;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::{string::String, vec::Vec};
|
||||
use core::cmp;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{AttrsList, BorrowedWithFontSystem, Buffer, Cursor, FontSystem, Motion};
|
||||
|
||||
pub use self::editor::*;
|
||||
mod editor;
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
pub use self::syntect::*;
|
||||
#[cfg(feature = "syntect")]
|
||||
mod syntect;
|
||||
|
||||
#[cfg(feature = "vi")]
|
||||
pub use self::vi::*;
|
||||
#[cfg(feature = "vi")]
|
||||
mod vi;
|
||||
|
||||
/// An action to perform on an [`Editor`]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Action {
|
||||
/// Move the cursor with some motion
|
||||
Motion(Motion),
|
||||
/// Escape, clears selection
|
||||
Escape,
|
||||
/// Insert character at cursor
|
||||
Insert(char),
|
||||
/// Create new line
|
||||
Enter,
|
||||
/// Delete text behind cursor
|
||||
Backspace,
|
||||
/// Delete text in front of cursor
|
||||
Delete,
|
||||
// Indent text (typically Tab)
|
||||
Indent,
|
||||
// Unindent text (typically Shift+Tab)
|
||||
Unindent,
|
||||
/// Mouse click at specified position
|
||||
Click {
|
||||
x: i32,
|
||||
y: i32,
|
||||
},
|
||||
/// Mouse double click at specified position
|
||||
DoubleClick {
|
||||
x: i32,
|
||||
y: i32,
|
||||
},
|
||||
/// Mouse triple click at specified position
|
||||
TripleClick {
|
||||
x: i32,
|
||||
y: i32,
|
||||
},
|
||||
/// Mouse drag to specified position
|
||||
Drag {
|
||||
x: i32,
|
||||
y: i32,
|
||||
},
|
||||
/// Scroll specified number of lines
|
||||
Scroll {
|
||||
lines: i32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BufferRef<'buffer> {
|
||||
Owned(Buffer),
|
||||
Borrowed(&'buffer mut Buffer),
|
||||
Arc(Arc<Buffer>),
|
||||
}
|
||||
|
||||
impl Clone for BufferRef<'_> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Owned(buffer) => Self::Owned(buffer.clone()),
|
||||
Self::Borrowed(buffer) => Self::Owned((*buffer).clone()),
|
||||
Self::Arc(buffer) => Self::Arc(buffer.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Buffer> for BufferRef<'_> {
|
||||
fn from(buffer: Buffer) -> Self {
|
||||
Self::Owned(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'buffer> From<&'buffer mut Buffer> for BufferRef<'buffer> {
|
||||
fn from(buffer: &'buffer mut Buffer) -> Self {
|
||||
Self::Borrowed(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Buffer>> for BufferRef<'_> {
|
||||
fn from(arc: Arc<Buffer>) -> Self {
|
||||
Self::Arc(arc)
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique change to an editor
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChangeItem {
|
||||
/// Cursor indicating start of change
|
||||
pub start: Cursor,
|
||||
/// Cursor indicating end of change
|
||||
pub end: Cursor,
|
||||
/// Text to be inserted or deleted
|
||||
pub text: String,
|
||||
/// Insert if true, delete if false
|
||||
pub insert: bool,
|
||||
}
|
||||
|
||||
impl ChangeItem {
|
||||
// Reverse change item (in place)
|
||||
pub fn reverse(&mut self) {
|
||||
self.insert = !self.insert;
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of change items grouped into one logical change
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Change {
|
||||
/// Change items grouped into one change
|
||||
pub items: Vec<ChangeItem>,
|
||||
}
|
||||
|
||||
impl Change {
|
||||
// Reverse change (in place)
|
||||
pub fn reverse(&mut self) {
|
||||
self.items.reverse();
|
||||
for item in self.items.iter_mut() {
|
||||
item.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Selection mode
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Selection {
|
||||
/// No selection
|
||||
None,
|
||||
/// Normal selection
|
||||
Normal(Cursor),
|
||||
/// Select by lines
|
||||
Line(Cursor),
|
||||
/// Select by words
|
||||
Word(Cursor),
|
||||
//TODO: Select block
|
||||
}
|
||||
|
||||
/// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor`
|
||||
pub trait Edit<'buffer> {
|
||||
/// Mutably borrows `self` together with an [`FontSystem`] for more convenient methods
|
||||
fn borrow_with<'font_system>(
|
||||
&'font_system mut self,
|
||||
font_system: &'font_system mut FontSystem,
|
||||
) -> BorrowedWithFontSystem<'font_system, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
BorrowedWithFontSystem {
|
||||
inner: self,
|
||||
font_system,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the internal [`BufferRef`]
|
||||
fn buffer_ref(&self) -> &BufferRef<'buffer>;
|
||||
|
||||
/// Get the internal [`BufferRef`]
|
||||
fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer>;
|
||||
|
||||
/// Get the internal [`Buffer`]
|
||||
fn with_buffer<F: FnOnce(&Buffer) -> T, T>(&self, f: F) -> T {
|
||||
match self.buffer_ref() {
|
||||
BufferRef::Owned(buffer) => f(buffer),
|
||||
BufferRef::Borrowed(buffer) => f(buffer),
|
||||
BufferRef::Arc(buffer) => f(buffer),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the internal [`Buffer`], mutably
|
||||
fn with_buffer_mut<F: FnOnce(&mut Buffer) -> T, T>(&mut self, f: F) -> T {
|
||||
match self.buffer_ref_mut() {
|
||||
BufferRef::Owned(buffer) => f(buffer),
|
||||
BufferRef::Borrowed(buffer) => f(buffer),
|
||||
BufferRef::Arc(buffer) => f(Arc::make_mut(buffer)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the [`Buffer`] redraw flag
|
||||
fn redraw(&self) -> bool {
|
||||
self.with_buffer(|buffer| buffer.redraw())
|
||||
}
|
||||
|
||||
/// Set the [`Buffer`] redraw flag
|
||||
fn set_redraw(&mut self, redraw: bool) {
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(redraw));
|
||||
}
|
||||
|
||||
/// Get the current cursor
|
||||
fn cursor(&self) -> Cursor;
|
||||
|
||||
/// Set the current cursor
|
||||
fn set_cursor(&mut self, cursor: Cursor);
|
||||
|
||||
/// Get the current selection position
|
||||
fn selection(&self) -> Selection;
|
||||
|
||||
/// Set the current selection position
|
||||
fn set_selection(&mut self, selection: Selection);
|
||||
|
||||
/// Get the bounds of the current selection
|
||||
//TODO: will not work with Block select
|
||||
fn selection_bounds(&self) -> Option<(Cursor, Cursor)> {
|
||||
self.with_buffer(|buffer| {
|
||||
let cursor = self.cursor();
|
||||
match self.selection() {
|
||||
Selection::None => None,
|
||||
Selection::Normal(select) => match select.line.cmp(&cursor.line) {
|
||||
cmp::Ordering::Greater => Some((cursor, select)),
|
||||
cmp::Ordering::Less => Some((select, cursor)),
|
||||
cmp::Ordering::Equal => {
|
||||
/* select.line == cursor.line */
|
||||
if select.index < cursor.index {
|
||||
Some((select, cursor))
|
||||
} else {
|
||||
/* select.index >= cursor.index */
|
||||
Some((cursor, select))
|
||||
}
|
||||
}
|
||||
},
|
||||
Selection::Line(select) => {
|
||||
let start_line = cmp::min(select.line, cursor.line);
|
||||
let end_line = cmp::max(select.line, cursor.line);
|
||||
let end_index = buffer.lines[end_line].text().len();
|
||||
Some((Cursor::new(start_line, 0), Cursor::new(end_line, end_index)))
|
||||
}
|
||||
Selection::Word(select) => {
|
||||
let (mut start, mut end) = match select.line.cmp(&cursor.line) {
|
||||
cmp::Ordering::Greater => (cursor, select),
|
||||
cmp::Ordering::Less => (select, cursor),
|
||||
cmp::Ordering::Equal => {
|
||||
/* select.line == cursor.line */
|
||||
if select.index < cursor.index {
|
||||
(select, cursor)
|
||||
} else {
|
||||
/* select.index >= cursor.index */
|
||||
(cursor, select)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Move start to beginning of word
|
||||
{
|
||||
let line = &buffer.lines[start.line];
|
||||
start.index = line
|
||||
.text()
|
||||
.unicode_word_indices()
|
||||
.rev()
|
||||
.map(|(i, _)| i)
|
||||
.find(|&i| i < start.index)
|
||||
.unwrap_or(0);
|
||||
}
|
||||
|
||||
// Move end to end of word
|
||||
{
|
||||
let line = &buffer.lines[end.line];
|
||||
end.index = line
|
||||
.text()
|
||||
.unicode_word_indices()
|
||||
.map(|(i, word)| i + word.len())
|
||||
.find(|&i| i > end.index)
|
||||
.unwrap_or(line.text().len());
|
||||
}
|
||||
|
||||
Some((start, end))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the current automatic indentation setting
|
||||
fn auto_indent(&self) -> bool;
|
||||
|
||||
/// Enable or disable automatic indentation
|
||||
fn set_auto_indent(&mut self, auto_indent: bool);
|
||||
|
||||
/// Get the current tab width
|
||||
fn tab_width(&self) -> u16;
|
||||
|
||||
/// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored
|
||||
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16);
|
||||
|
||||
/// Shape lines until scroll, after adjusting scroll if the cursor moved
|
||||
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool);
|
||||
|
||||
/// Delete text starting at start Cursor and ending at end Cursor
|
||||
fn delete_range(&mut self, start: Cursor, end: Cursor);
|
||||
|
||||
/// Insert text at specified cursor with specified `attrs_list`
|
||||
fn insert_at(&mut self, cursor: Cursor, data: &str, attrs_list: Option<AttrsList>) -> Cursor;
|
||||
|
||||
/// Copy selection
|
||||
fn copy_selection(&self) -> Option<String>;
|
||||
|
||||
/// Delete selection, adjusting cursor and returning true if there was a selection
|
||||
// Also used by backspace, delete, insert, and enter when there is a selection
|
||||
fn delete_selection(&mut self) -> bool;
|
||||
|
||||
/// Insert a string at the current cursor or replacing the current selection with the given
|
||||
/// attributes, or with the previous character's attributes if None is given.
|
||||
fn insert_string(&mut self, data: &str, attrs_list: Option<AttrsList>) {
|
||||
self.delete_selection();
|
||||
let new_cursor = self.insert_at(self.cursor(), data, attrs_list);
|
||||
self.set_cursor(new_cursor);
|
||||
}
|
||||
|
||||
/// Apply a change
|
||||
fn apply_change(&mut self, change: &Change) -> bool;
|
||||
|
||||
/// Start collecting change
|
||||
fn start_change(&mut self);
|
||||
|
||||
/// Get completed change
|
||||
fn finish_change(&mut self) -> Option<Change>;
|
||||
|
||||
/// Perform an [Action] on the editor
|
||||
fn action(&mut self, font_system: &mut FontSystem, action: Action);
|
||||
|
||||
/// Get X and Y position of the top left corner of the cursor
|
||||
fn cursor_position(&self) -> Option<(i32, i32)>;
|
||||
}
|
||||
|
||||
impl<'buffer, E: Edit<'buffer>> BorrowedWithFontSystem<'_, E> {
|
||||
/// Get the internal [`Buffer`], mutably
|
||||
pub fn with_buffer_mut<F: FnOnce(&mut BorrowedWithFontSystem<Buffer>) -> T, T>(
|
||||
&mut self,
|
||||
f: F,
|
||||
) -> T {
|
||||
self.inner.with_buffer_mut(|buffer| {
|
||||
let mut borrowed = BorrowedWithFontSystem {
|
||||
inner: buffer,
|
||||
font_system: self.font_system,
|
||||
};
|
||||
f(&mut borrowed)
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored
|
||||
pub fn set_tab_width(&mut self, tab_width: u16) {
|
||||
self.inner.set_tab_width(self.font_system, tab_width);
|
||||
}
|
||||
|
||||
/// Shape lines until scroll, after adjusting scroll if the cursor moved
|
||||
pub fn shape_as_needed(&mut self, prune: bool) {
|
||||
self.inner.shape_as_needed(self.font_system, prune);
|
||||
}
|
||||
|
||||
/// Perform an [Action] on the editor
|
||||
pub fn action(&mut self, action: Action) {
|
||||
self.inner.action(self.font_system, action);
|
||||
}
|
||||
}
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::{string::String, vec::Vec};
|
||||
#[cfg(feature = "std")]
|
||||
use std::{fs, io, path::Path};
|
||||
use syntect::highlighting::{
|
||||
FontStyle, HighlightState, Highlighter, RangedHighlightIterator, ThemeSet,
|
||||
};
|
||||
use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
|
||||
|
||||
use crate::{
|
||||
Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, Editor,
|
||||
FontSystem, Selection, Shaping, Style, Weight,
|
||||
};
|
||||
|
||||
pub use syntect::highlighting::Theme as SyntaxTheme;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SyntaxSystem {
|
||||
pub syntax_set: SyntaxSet,
|
||||
pub theme_set: ThemeSet,
|
||||
}
|
||||
|
||||
impl SyntaxSystem {
|
||||
/// Create a new [`SyntaxSystem`]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
//TODO: store newlines in buffer
|
||||
syntax_set: SyntaxSet::load_defaults_nonewlines(),
|
||||
theme_set: ThemeSet::load_defaults(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper of [`Editor`] with syntax highlighting provided by [`SyntaxSystem`]
|
||||
#[derive(Debug)]
|
||||
pub struct SyntaxEditor<'syntax_system, 'buffer> {
|
||||
editor: Editor<'buffer>,
|
||||
syntax_system: &'syntax_system SyntaxSystem,
|
||||
syntax: &'syntax_system SyntaxReference,
|
||||
theme: &'syntax_system SyntaxTheme,
|
||||
highlighter: Highlighter<'syntax_system>,
|
||||
syntax_cache: Vec<(ParseState, ScopeStack)>,
|
||||
}
|
||||
|
||||
impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> {
|
||||
/// Create a new [`SyntaxEditor`] with the provided [`Buffer`], [`SyntaxSystem`], and theme name.
|
||||
///
|
||||
/// A good default theme name is "base16-eighties.dark".
|
||||
///
|
||||
/// Returns None if theme not found
|
||||
pub fn new(
|
||||
buffer: impl Into<BufferRef<'buffer>>,
|
||||
syntax_system: &'syntax_system SyntaxSystem,
|
||||
theme_name: &str,
|
||||
) -> Option<Self> {
|
||||
let editor = Editor::new(buffer);
|
||||
let syntax = syntax_system.syntax_set.find_syntax_plain_text();
|
||||
let theme = syntax_system.theme_set.themes.get(theme_name)?;
|
||||
let highlighter = Highlighter::new(theme);
|
||||
|
||||
Some(Self {
|
||||
editor,
|
||||
syntax_system,
|
||||
syntax,
|
||||
theme,
|
||||
highlighter,
|
||||
syntax_cache: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing
|
||||
pub fn update_theme(&mut self, theme_name: &str) -> bool {
|
||||
if let Some(theme) = self.syntax_system.theme_set.themes.get(theme_name) {
|
||||
if self.theme != theme {
|
||||
self.theme = theme;
|
||||
self.highlighter = Highlighter::new(theme);
|
||||
self.syntax_cache.clear();
|
||||
|
||||
// Reset attrs to match default foreground and no highlighting
|
||||
self.with_buffer_mut(|buffer| {
|
||||
for line in buffer.lines.iter_mut() {
|
||||
let mut attrs = line.attrs_list().defaults();
|
||||
if let Some(foreground) = self.theme.settings.foreground {
|
||||
attrs = attrs.color(Color::rgba(
|
||||
foreground.r,
|
||||
foreground.g,
|
||||
foreground.b,
|
||||
foreground.a,
|
||||
));
|
||||
}
|
||||
line.set_attrs_list(AttrsList::new(&attrs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Load text from a file, and also set syntax to the best option
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an [`io::Error`] if reading the file fails
|
||||
#[cfg(feature = "std")]
|
||||
pub fn load_text<P: AsRef<Path>>(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
path: P,
|
||||
mut attrs: crate::Attrs,
|
||||
) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
// Set attrs to match default foreground
|
||||
if let Some(foreground) = self.theme.settings.foreground {
|
||||
attrs = attrs.color(Color::rgba(
|
||||
foreground.r,
|
||||
foreground.g,
|
||||
foreground.b,
|
||||
foreground.a,
|
||||
));
|
||||
}
|
||||
|
||||
let text = fs::read_to_string(path)?;
|
||||
self.editor.with_buffer_mut(|buffer| {
|
||||
buffer.set_text(font_system, &text, &attrs, Shaping::Advanced);
|
||||
});
|
||||
|
||||
//TODO: re-use text
|
||||
self.syntax = match self.syntax_system.syntax_set.find_syntax_for_file(path) {
|
||||
Ok(Some(some)) => some,
|
||||
Ok(None) => {
|
||||
log::warn!("no syntax found for {:?}", path);
|
||||
self.syntax_system.syntax_set.find_syntax_plain_text()
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to determine syntax for {:?}: {:?}", path, err);
|
||||
self.syntax_system.syntax_set.find_syntax_plain_text()
|
||||
}
|
||||
};
|
||||
|
||||
// Clear syntax cache
|
||||
self.syntax_cache.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set syntax highlighting by file extension
|
||||
pub fn syntax_by_extension(&mut self, extension: &str) {
|
||||
self.syntax = match self
|
||||
.syntax_system
|
||||
.syntax_set
|
||||
.find_syntax_by_extension(extension)
|
||||
{
|
||||
Some(some) => some,
|
||||
None => {
|
||||
log::warn!("no syntax found for {}", extension);
|
||||
self.syntax_system.syntax_set.find_syntax_plain_text()
|
||||
}
|
||||
};
|
||||
|
||||
self.syntax_cache.clear();
|
||||
}
|
||||
|
||||
/// Get the default background color
|
||||
pub fn background_color(&self) -> Color {
|
||||
if let Some(background) = self.theme.settings.background {
|
||||
Color::rgba(background.r, background.g, background.b, background.a)
|
||||
} else {
|
||||
Color::rgb(0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default foreground (text) color
|
||||
pub fn foreground_color(&self) -> Color {
|
||||
if let Some(foreground) = self.theme.settings.foreground {
|
||||
Color::rgba(foreground.r, foreground.g, foreground.b, foreground.a)
|
||||
} else {
|
||||
Color::rgb(0xFF, 0xFF, 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default cursor color
|
||||
pub fn cursor_color(&self) -> Color {
|
||||
if let Some(some) = self.theme.settings.caret {
|
||||
Color::rgba(some.r, some.g, some.b, some.a)
|
||||
} else {
|
||||
self.foreground_color()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default selection color
|
||||
pub fn selection_color(&self) -> Color {
|
||||
if let Some(some) = self.theme.settings.selection {
|
||||
Color::rgba(some.r, some.g, some.b, some.a)
|
||||
} else {
|
||||
let foreground_color = self.foreground_color();
|
||||
Color::rgba(
|
||||
foreground_color.r(),
|
||||
foreground_color.g(),
|
||||
foreground_color.b(),
|
||||
0x33,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current syntect theme
|
||||
pub fn theme(&self) -> &SyntaxTheme {
|
||||
self.theme
|
||||
}
|
||||
|
||||
/// Draw the editor
|
||||
#[cfg(feature = "swash")]
|
||||
pub fn draw<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, mut f: F)
|
||||
where
|
||||
F: FnMut(i32, i32, u32, u32, Color),
|
||||
{
|
||||
let size = self.with_buffer(|buffer| buffer.size());
|
||||
if let Some(width) = size.0 {
|
||||
if let Some(height) = size.1 {
|
||||
f(0, 0, width as u32, height as u32, self.background_color());
|
||||
}
|
||||
}
|
||||
self.editor.draw(
|
||||
font_system,
|
||||
cache,
|
||||
self.foreground_color(),
|
||||
self.cursor_color(),
|
||||
self.selection_color(),
|
||||
self.foreground_color(),
|
||||
f,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'buffer> Edit<'buffer> for SyntaxEditor<'_, 'buffer> {
|
||||
fn buffer_ref(&self) -> &BufferRef<'buffer> {
|
||||
self.editor.buffer_ref()
|
||||
}
|
||||
|
||||
fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer> {
|
||||
self.editor.buffer_ref_mut()
|
||||
}
|
||||
|
||||
fn cursor(&self) -> Cursor {
|
||||
self.editor.cursor()
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, cursor: Cursor) {
|
||||
self.editor.set_cursor(cursor);
|
||||
}
|
||||
|
||||
fn selection(&self) -> Selection {
|
||||
self.editor.selection()
|
||||
}
|
||||
|
||||
fn set_selection(&mut self, selection: Selection) {
|
||||
self.editor.set_selection(selection);
|
||||
}
|
||||
|
||||
fn auto_indent(&self) -> bool {
|
||||
self.editor.auto_indent()
|
||||
}
|
||||
|
||||
fn set_auto_indent(&mut self, auto_indent: bool) {
|
||||
self.editor.set_auto_indent(auto_indent);
|
||||
}
|
||||
|
||||
fn tab_width(&self) -> u16 {
|
||||
self.editor.tab_width()
|
||||
}
|
||||
|
||||
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) {
|
||||
self.editor.set_tab_width(font_system, tab_width);
|
||||
}
|
||||
|
||||
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) {
|
||||
#[cfg(feature = "std")]
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let cursor = self.cursor();
|
||||
self.editor.with_buffer_mut(|buffer| {
|
||||
let metrics = buffer.metrics();
|
||||
let scroll = buffer.scroll();
|
||||
let scroll_end = scroll.vertical + buffer.size().1.unwrap_or(f32::INFINITY);
|
||||
let mut total_height = 0.0;
|
||||
let mut highlighted = 0;
|
||||
for line_i in 0..buffer.lines.len() {
|
||||
// Break out if we have reached the end of scroll and are past the cursor
|
||||
if total_height > scroll_end && line_i > cursor.line {
|
||||
break;
|
||||
}
|
||||
|
||||
let line = &mut buffer.lines[line_i];
|
||||
if line.metadata().is_some() && line_i < self.syntax_cache.len() {
|
||||
//TODO: duplicated code!
|
||||
if line_i >= scroll.line && total_height < scroll_end {
|
||||
// Perform shaping and layout of this line in order to count if we have reached scroll
|
||||
match buffer.line_layout(font_system, line_i) {
|
||||
Some(layout_lines) => {
|
||||
for layout_line in layout_lines.iter() {
|
||||
total_height +=
|
||||
layout_line.line_height_opt.unwrap_or(metrics.line_height);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
//TODO: should this be possible?
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
highlighted += 1;
|
||||
|
||||
let (mut parse_state, scope_stack) =
|
||||
if line_i > 0 && line_i <= self.syntax_cache.len() {
|
||||
self.syntax_cache[line_i - 1].clone()
|
||||
} else {
|
||||
(ParseState::new(self.syntax), ScopeStack::new())
|
||||
};
|
||||
let mut highlight_state = HighlightState::new(&self.highlighter, scope_stack);
|
||||
let ops = parse_state
|
||||
.parse_line(line.text(), &self.syntax_system.syntax_set)
|
||||
.expect("failed to parse syntax");
|
||||
let ranges = RangedHighlightIterator::new(
|
||||
&mut highlight_state,
|
||||
&ops,
|
||||
line.text(),
|
||||
&self.highlighter,
|
||||
);
|
||||
|
||||
let attrs = line.attrs_list().defaults();
|
||||
let mut attrs_list = AttrsList::new(&attrs);
|
||||
let original_attrs = attrs.clone(); // Store a clone for comparison
|
||||
for (style, _, range) in ranges {
|
||||
let span_attrs = attrs
|
||||
.clone() // Clone attrs for modification
|
||||
.color(Color::rgba(
|
||||
style.foreground.r,
|
||||
style.foreground.g,
|
||||
style.foreground.b,
|
||||
style.foreground.a,
|
||||
))
|
||||
//TODO: background
|
||||
.style(if style.font_style.contains(FontStyle::ITALIC) {
|
||||
Style::Italic
|
||||
} else {
|
||||
Style::Normal
|
||||
})
|
||||
.weight(if style.font_style.contains(FontStyle::BOLD) {
|
||||
Weight::BOLD
|
||||
} else {
|
||||
Weight::NORMAL
|
||||
}); //TODO: underline
|
||||
if span_attrs != original_attrs {
|
||||
attrs_list.add_span(range, &span_attrs);
|
||||
}
|
||||
}
|
||||
|
||||
// Update line attributes. This operation only resets if the line changes
|
||||
line.set_attrs_list(attrs_list);
|
||||
|
||||
// Perform shaping and layout of this line in order to count if we have reached scroll
|
||||
if line_i >= scroll.line && total_height < scroll_end {
|
||||
match buffer.line_layout(font_system, line_i) {
|
||||
Some(layout_lines) => {
|
||||
for layout_line in layout_lines.iter() {
|
||||
total_height +=
|
||||
layout_line.line_height_opt.unwrap_or(metrics.line_height);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
//TODO: should this be possible?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cache_item = (parse_state.clone(), highlight_state.path.clone());
|
||||
if line_i < self.syntax_cache.len() {
|
||||
if self.syntax_cache[line_i] != cache_item {
|
||||
self.syntax_cache[line_i] = cache_item;
|
||||
if line_i + 1 < buffer.lines.len() {
|
||||
buffer.lines[line_i + 1].reset();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buffer.lines[line_i].set_metadata(self.syntax_cache.len());
|
||||
self.syntax_cache.push(cache_item);
|
||||
}
|
||||
}
|
||||
|
||||
if highlighted > 0 {
|
||||
buffer.set_redraw(true);
|
||||
#[cfg(feature = "std")]
|
||||
log::debug!(
|
||||
"Syntax highlighted {} lines in {:?}",
|
||||
highlighted,
|
||||
now.elapsed()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
self.editor.shape_as_needed(font_system, prune);
|
||||
}
|
||||
|
||||
fn delete_range(&mut self, start: Cursor, end: Cursor) {
|
||||
self.editor.delete_range(start, end);
|
||||
}
|
||||
|
||||
fn insert_at(&mut self, cursor: Cursor, data: &str, attrs_list: Option<AttrsList>) -> Cursor {
|
||||
self.editor.insert_at(cursor, data, attrs_list)
|
||||
}
|
||||
|
||||
fn copy_selection(&self) -> Option<String> {
|
||||
self.editor.copy_selection()
|
||||
}
|
||||
|
||||
fn delete_selection(&mut self) -> bool {
|
||||
self.editor.delete_selection()
|
||||
}
|
||||
|
||||
fn apply_change(&mut self, change: &Change) -> bool {
|
||||
self.editor.apply_change(change)
|
||||
}
|
||||
|
||||
fn start_change(&mut self) {
|
||||
self.editor.start_change();
|
||||
}
|
||||
|
||||
fn finish_change(&mut self) -> Option<Change> {
|
||||
self.editor.finish_change()
|
||||
}
|
||||
|
||||
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
||||
self.editor.action(font_system, action);
|
||||
}
|
||||
|
||||
fn cursor_position(&self) -> Option<(i32, i32)> {
|
||||
self.editor.cursor_position()
|
||||
}
|
||||
}
|
||||
|
||||
impl BorrowedWithFontSystem<'_, SyntaxEditor<'_, '_>> {
|
||||
/// Load text from a file, and also set syntax to the best option
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an [`io::Error`] if reading the file fails
|
||||
#[cfg(feature = "std")]
|
||||
pub fn load_text<P: AsRef<Path>>(&mut self, path: P, attrs: crate::Attrs) -> io::Result<()> {
|
||||
self.inner.load_text(self.font_system, path, attrs)
|
||||
}
|
||||
|
||||
#[cfg(feature = "swash")]
|
||||
pub fn draw<F>(&mut self, cache: &mut crate::SwashCache, f: F)
|
||||
where
|
||||
F: FnMut(i32, i32, u32, u32, Color),
|
||||
{
|
||||
self.inner.draw(self.font_system, cache, f);
|
||||
}
|
||||
}
|
||||
Vendored
+1173
File diff suppressed because it is too large.
Load diff
+110
@@ -0,0 +1,110 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use unicode_script::Script;
|
||||
|
||||
use super::Fallback;
|
||||
|
||||
/// A platform-specific font fallback list, for `MacOS`.
|
||||
#[derive(Debug)]
|
||||
pub struct PlatformFallback;
|
||||
|
||||
impl Fallback for PlatformFallback {
|
||||
fn common_fallback(&self) -> &'static [&'static str] {
|
||||
common_fallback()
|
||||
}
|
||||
|
||||
fn forbidden_fallback(&self) -> &'static [&'static str] {
|
||||
forbidden_fallback()
|
||||
}
|
||||
|
||||
fn script_fallback(
|
||||
&self,
|
||||
script: unicode_script::Script,
|
||||
locale: &str,
|
||||
) -> &'static [&'static str] {
|
||||
script_fallback(script, locale)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use after any script specific fallbacks
|
||||
fn common_fallback() -> &'static [&'static str] {
|
||||
&[
|
||||
".SF NS",
|
||||
"Menlo",
|
||||
"Apple Color Emoji",
|
||||
"Geneva",
|
||||
"Arial Unicode MS",
|
||||
]
|
||||
}
|
||||
|
||||
// Fallbacks to never use
|
||||
fn forbidden_fallback() -> &'static [&'static str] {
|
||||
&[".LastResort"]
|
||||
}
|
||||
|
||||
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||
match locale {
|
||||
// Japan
|
||||
"ja" => &["Hiragino Sans"],
|
||||
// Korea
|
||||
"ko" => &["Apple SD Gothic Neo"],
|
||||
// Hong Kong
|
||||
"zh-HK" => &["PingFang HK"],
|
||||
// Taiwan
|
||||
"zh-TW" => &["PingFang TC"],
|
||||
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||
_ => &["PingFang SC"],
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use per script
|
||||
fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||
//TODO: abstract style (sans/serif/monospaced)
|
||||
//TODO: pull more data from about:config font.name-list.sans-serif in Firefox
|
||||
match script {
|
||||
Script::Adlam => &["Noto Sans Adlam"],
|
||||
Script::Arabic => &["Geeza Pro"],
|
||||
Script::Armenian => &["Noto Sans Armenian"],
|
||||
Script::Bengali => &["Bangla Sangam MN"],
|
||||
Script::Buhid => &["Noto Sans Buhid"],
|
||||
Script::Canadian_Aboriginal => &["Euphemia UCAS"],
|
||||
Script::Chakma => &["Noto Sans Chakma"],
|
||||
Script::Devanagari => &["Devanagari Sangam MN"],
|
||||
Script::Ethiopic => &["Kefa"],
|
||||
Script::Gothic => &["Noto Sans Gothic"],
|
||||
Script::Grantha => &["Grantha Sangam MN"],
|
||||
Script::Gujarati => &["Gujarati Sangam MN"],
|
||||
Script::Gurmukhi => &["Gurmukhi Sangam MN"],
|
||||
Script::Han => han_unification(locale),
|
||||
Script::Hangul => han_unification("ko"),
|
||||
Script::Hanunoo => &["Noto Sans Hanunoo"],
|
||||
Script::Hebrew => &["Arial"],
|
||||
Script::Hiragana => han_unification("ja"),
|
||||
Script::Javanese => &["Noto Sans Javanese"],
|
||||
Script::Kannada => &["Noto Sans Kannada"],
|
||||
Script::Katakana => han_unification("ja"),
|
||||
Script::Khmer => &["Khmer Sangam MN"],
|
||||
Script::Lao => &["Lao Sangam MN"],
|
||||
Script::Malayalam => &["Malayalam Sangam MN"],
|
||||
Script::Mongolian => &["Noto Sans Mongolian"],
|
||||
Script::Myanmar => &["Noto Sans Myanmar"],
|
||||
Script::Oriya => &["Noto Sans Oriya"],
|
||||
Script::Sinhala => &["Sinhala Sangam MN"],
|
||||
Script::Syriac => &["Noto Sans Syriac"],
|
||||
Script::Tagalog => &["Noto Sans Tagalog"],
|
||||
Script::Tagbanwa => &["Noto Sans Tagbanwa"],
|
||||
Script::Tai_Le => &["Noto Sans Tai Le"],
|
||||
Script::Tai_Tham => &["Noto Sans Tai Tham"],
|
||||
Script::Tai_Viet => &["Noto Sans Tai Viet"],
|
||||
Script::Tamil => &["InaiMathi"],
|
||||
Script::Telugu => &["Telugu Sangam MN"],
|
||||
Script::Thaana => &["Noto Sans Thaana"],
|
||||
Script::Thai => &["Ayuthaya"],
|
||||
Script::Tibetan => &["Kailasa"],
|
||||
Script::Tifinagh => &["Noto Sans Tifinagh"],
|
||||
Script::Vai => &["Noto Sans Vai"],
|
||||
//TODO: Use han_unification?
|
||||
Script::Yi => &["Noto Sans Yi", "PingFang SC"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::string::String;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use core::{mem, ops::Range};
|
||||
use fontdb::Family;
|
||||
use unicode_script::Script;
|
||||
|
||||
use crate::{BuildHasher, Font, FontMatchKey, FontSystem, HashMap, ShapeBuffer};
|
||||
|
||||
#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "windows")))]
|
||||
#[path = "other.rs"]
|
||||
mod platform;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[path = "macos.rs"]
|
||||
mod platform;
|
||||
|
||||
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
|
||||
#[path = "unix.rs"]
|
||||
mod platform;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows.rs"]
|
||||
mod platform;
|
||||
|
||||
/// The `Fallback` trait allows for configurable font fallback lists to be set during construction of the [`FontSystem`].
|
||||
///
|
||||
/// A custom fallback list can be added via the [`FontSystem::new_with_locale_and_db_and_fallback`] constructor.
|
||||
///
|
||||
/// A default implementation is provided by the [`PlatformFallback`] struct, which encapsulates the target platform's pre-configured fallback lists.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use unicode_script::Script;
|
||||
/// # use cosmic_text::{Fallback, FontSystem};
|
||||
/// struct MyFallback;
|
||||
/// impl Fallback for MyFallback {
|
||||
/// fn common_fallback(&self) -> &[&'static str] {
|
||||
/// &[
|
||||
/// "Segoe UI",
|
||||
/// "Segoe UI Emoji",
|
||||
/// "Segoe UI Symbol",
|
||||
/// "Segoe UI Historic",
|
||||
/// ]
|
||||
/// }
|
||||
///
|
||||
/// fn forbidden_fallback(&self) -> &[&'static str] {
|
||||
/// &[]
|
||||
/// }
|
||||
///
|
||||
/// fn script_fallback(&self, script: Script, locale: &str) -> &[&'static str] {
|
||||
/// match script {
|
||||
/// Script::Adlam => &["Ebrima"],
|
||||
/// Script::Bengali => &["Nirmala UI"],
|
||||
/// Script::Canadian_Aboriginal => &["Gadugi"],
|
||||
/// // ...
|
||||
/// _ => &[],
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let locale = "en-US".to_string();
|
||||
/// let db = fontdb::Database::new();
|
||||
/// let font_system = FontSystem::new_with_locale_and_db_and_fallback(locale, db, MyFallback);
|
||||
/// ```
|
||||
pub trait Fallback: Send + Sync {
|
||||
/// Fallbacks to use after any script specific fallbacks
|
||||
fn common_fallback(&self) -> &[&'static str];
|
||||
|
||||
/// Fallbacks to never use
|
||||
fn forbidden_fallback(&self) -> &[&'static str];
|
||||
|
||||
/// Fallbacks to use per script
|
||||
fn script_fallback(&self, script: Script, locale: &str) -> &[&'static str];
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Fallbacks {
|
||||
lists: Vec<&'static str>,
|
||||
common_fallback_range: Range<usize>,
|
||||
forbidden_fallback_range: Range<usize>,
|
||||
// PERF: Consider using NoHashHasher since Script is just an integer
|
||||
script_fallback_ranges: HashMap<Script, Range<usize>>,
|
||||
locale: String,
|
||||
}
|
||||
|
||||
impl Fallbacks {
|
||||
pub(crate) fn new(fallbacks: &dyn Fallback, scripts: &[Script], locale: &str) -> Self {
|
||||
let common_fallback = fallbacks.common_fallback();
|
||||
|
||||
let forbidden_fallback = fallbacks.forbidden_fallback();
|
||||
|
||||
let mut lists =
|
||||
Vec::with_capacity(common_fallback.len() + forbidden_fallback.len() + scripts.len());
|
||||
|
||||
let mut index = lists.len();
|
||||
let mut new_range = |lists: &Vec<&str>| {
|
||||
let old_index = index;
|
||||
index = lists.len();
|
||||
old_index..index
|
||||
};
|
||||
|
||||
lists.extend_from_slice(common_fallback);
|
||||
let common_fallback_range = new_range(&lists);
|
||||
|
||||
lists.extend_from_slice(forbidden_fallback);
|
||||
let forbidden_fallback_range = new_range(&lists);
|
||||
|
||||
let mut script_fallback_ranges =
|
||||
HashMap::with_capacity_and_hasher(scripts.len(), BuildHasher::default());
|
||||
for &script in scripts {
|
||||
let script_fallback = fallbacks.script_fallback(script, locale);
|
||||
lists.extend_from_slice(script_fallback);
|
||||
let script_fallback_range = new_range(&lists);
|
||||
script_fallback_ranges.insert(script, script_fallback_range);
|
||||
}
|
||||
|
||||
let locale = locale.to_owned();
|
||||
Self {
|
||||
lists,
|
||||
common_fallback_range,
|
||||
forbidden_fallback_range,
|
||||
script_fallback_ranges,
|
||||
locale,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn extend(&mut self, fallbacks: &dyn Fallback, scripts: &[Script]) {
|
||||
self.lists.reserve(scripts.len());
|
||||
|
||||
let mut index = self.lists.len();
|
||||
let mut new_range = |lists: &Vec<&str>| {
|
||||
let old_index = index;
|
||||
index = lists.len();
|
||||
old_index..index
|
||||
};
|
||||
|
||||
for &script in scripts {
|
||||
self.script_fallback_ranges
|
||||
.entry(script)
|
||||
.or_insert_with_key(|&script| {
|
||||
let script_fallback = fallbacks.script_fallback(script, &self.locale);
|
||||
self.lists.extend_from_slice(script_fallback);
|
||||
new_range(&self.lists)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn common_fallback(&self) -> &[&'static str] {
|
||||
&self.lists[self.common_fallback_range.clone()]
|
||||
}
|
||||
|
||||
pub(crate) fn forbidden_fallback(&self) -> &[&'static str] {
|
||||
&self.lists[self.forbidden_fallback_range.clone()]
|
||||
}
|
||||
|
||||
pub(crate) fn script_fallback(&self, script: Script) -> &[&'static str] {
|
||||
self.script_fallback_ranges
|
||||
.get(&script)
|
||||
.map_or(&[], |range| &self.lists[range.clone()])
|
||||
}
|
||||
}
|
||||
|
||||
pub use platform::PlatformFallback;
|
||||
|
||||
#[cfg(not(feature = "warn_on_missing_glyphs"))]
|
||||
use log::debug as missing_warn;
|
||||
#[cfg(feature = "warn_on_missing_glyphs")]
|
||||
use log::warn as missing_warn;
|
||||
|
||||
// Match on lowest font_weight_diff, then script_non_matches, then font_weight
|
||||
// Default font gets None for both `weight_offset` and `script_non_matches`, and thus, it is
|
||||
// always the first to be popped from the set.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) struct MonospaceFallbackInfo {
|
||||
font_weight_diff: Option<u16>,
|
||||
codepoint_non_matches: Option<usize>,
|
||||
font_weight: u16,
|
||||
id: fontdb::ID,
|
||||
}
|
||||
|
||||
pub struct FontFallbackIter<'a> {
|
||||
font_system: &'a mut FontSystem,
|
||||
font_match_keys: &'a [FontMatchKey],
|
||||
skip_font_id: Option<fontdb::ID>,
|
||||
default_families: &'a [&'a Family<'a>],
|
||||
default_i: usize,
|
||||
scripts: &'a [Script],
|
||||
word: &'a str,
|
||||
script_i: (usize, usize),
|
||||
common_i: usize,
|
||||
other_i: usize,
|
||||
end: bool,
|
||||
}
|
||||
|
||||
impl<'a> FontFallbackIter<'a> {
|
||||
pub fn new(
|
||||
font_system: &'a mut FontSystem,
|
||||
font_match_keys: &'a [FontMatchKey],
|
||||
skip_font_id: Option<fontdb::ID>,
|
||||
default_families: &'a [&'a Family<'a>],
|
||||
scripts: &'a [Script],
|
||||
word: &'a str,
|
||||
) -> Self {
|
||||
font_system
|
||||
.fallbacks
|
||||
.extend(font_system.dyn_fallback.as_ref(), scripts);
|
||||
font_system.monospace_fallbacks_buffer.clear();
|
||||
Self {
|
||||
font_system,
|
||||
font_match_keys,
|
||||
skip_font_id,
|
||||
default_families,
|
||||
default_i: 0,
|
||||
scripts,
|
||||
word,
|
||||
script_i: (0, 0),
|
||||
common_i: 0,
|
||||
other_i: 0,
|
||||
end: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_missing(&mut self, word: &str) {
|
||||
if self.end {
|
||||
missing_warn!(
|
||||
"Failed to find any fallback for {:?} locale '{}': '{}'",
|
||||
self.scripts,
|
||||
self.font_system.locale(),
|
||||
word
|
||||
);
|
||||
} else if self.other_i > 0 {
|
||||
missing_warn!(
|
||||
"Failed to find preset fallback for {:?} locale '{}', used '{}': '{}'",
|
||||
self.scripts,
|
||||
self.font_system.locale(),
|
||||
self.face_name(self.font_match_keys[self.other_i - 1].id),
|
||||
word
|
||||
);
|
||||
} else if !self.scripts.is_empty() && self.common_i > 0 {
|
||||
let family = self.font_system.fallbacks.common_fallback()[self.common_i - 1];
|
||||
missing_warn!(
|
||||
"Failed to find script fallback for {:?} locale '{}', used '{}': '{}'",
|
||||
self.scripts,
|
||||
self.font_system.locale(),
|
||||
family,
|
||||
word
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn face_name(&self, id: fontdb::ID) -> &str {
|
||||
if let Some(face) = self.font_system.db().face(id) {
|
||||
if let Some((name, _)) = face.families.first() {
|
||||
name
|
||||
} else {
|
||||
&face.post_script_name
|
||||
}
|
||||
} else {
|
||||
"invalid font id"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape_caches(&mut self) -> &mut ShapeBuffer {
|
||||
&mut self.font_system.shape_buffer
|
||||
}
|
||||
|
||||
fn face_contains_family(&self, id: fontdb::ID, family_name: &str) -> bool {
|
||||
if let Some(face) = self.font_system.db().face(id) {
|
||||
face.families.iter().any(|(name, _)| name == family_name)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn default_font_match_key(&self) -> Option<&FontMatchKey> {
|
||||
let default_family = self.default_families[self.default_i - 1];
|
||||
let default_family_name = self.font_system.db().family_name(default_family);
|
||||
|
||||
self.font_match_keys
|
||||
.iter()
|
||||
.filter(|m_key| m_key.font_weight_diff == 0)
|
||||
.filter(|m_key| Some(m_key.id) != self.skip_font_id)
|
||||
.find(|m_key| self.face_contains_family(m_key.id, default_family_name))
|
||||
}
|
||||
|
||||
fn next_item(&mut self, fallbacks: &Fallbacks) -> Option<<Self as Iterator>::Item> {
|
||||
if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
|
||||
if let Some(font) = self.font_system.get_font(fallback_info.id) {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
|
||||
let font_match_keys_iter = |is_mono| {
|
||||
self.font_match_keys
|
||||
.iter()
|
||||
.filter(move |m_key| m_key.font_weight_diff == 0 || is_mono)
|
||||
.filter(|m_key| Some(m_key.id) != self.skip_font_id)
|
||||
};
|
||||
|
||||
'DEF_FAM: while self.default_i < self.default_families.len() {
|
||||
self.default_i += 1;
|
||||
let is_mono = self.default_families[self.default_i - 1] == &Family::Monospace;
|
||||
let default_font_match_key = self.default_font_match_key().cloned();
|
||||
let word_chars_count = self.word.chars().count();
|
||||
|
||||
macro_rules! mk_mono_fallback_info {
|
||||
($m_key:expr) => {{
|
||||
let supported_cp_count_opt = self
|
||||
.font_system
|
||||
.get_font_supported_codepoints_in_word($m_key.id, self.word);
|
||||
|
||||
supported_cp_count_opt.map(|supported_cp_count| {
|
||||
let codepoint_non_matches = word_chars_count - supported_cp_count;
|
||||
|
||||
MonospaceFallbackInfo {
|
||||
font_weight_diff: Some($m_key.font_weight_diff),
|
||||
codepoint_non_matches: Some(codepoint_non_matches),
|
||||
font_weight: $m_key.font_weight,
|
||||
id: $m_key.id,
|
||||
}
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
match (is_mono, default_font_match_key.as_ref()) {
|
||||
(false, None) => break 'DEF_FAM,
|
||||
(false, Some(m_key)) => {
|
||||
if let Some(font) = self.font_system.get_font(m_key.id) {
|
||||
return Some(font);
|
||||
} else {
|
||||
break 'DEF_FAM;
|
||||
}
|
||||
}
|
||||
(true, None) => (),
|
||||
(true, Some(m_key)) => {
|
||||
// Default Monospace font
|
||||
if let Some(mut fallback_info) = mk_mono_fallback_info!(m_key) {
|
||||
fallback_info.font_weight_diff = None;
|
||||
|
||||
// Return early if default Monospace font supports all word codepoints.
|
||||
// Otherewise, add to fallbacks set
|
||||
if fallback_info.codepoint_non_matches == Some(0) {
|
||||
if let Some(font) = self.font_system.get_font(m_key.id) {
|
||||
return Some(font);
|
||||
}
|
||||
} else {
|
||||
assert!(self
|
||||
.font_system
|
||||
.monospace_fallbacks_buffer
|
||||
.insert(fallback_info));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mono_ids_for_scripts = if is_mono && !self.scripts.is_empty() {
|
||||
let scripts = self.scripts.iter().filter_map(|script| {
|
||||
let script_as_lower = script.short_name().to_lowercase();
|
||||
<[u8; 4]>::try_from(script_as_lower.as_bytes()).ok()
|
||||
});
|
||||
self.font_system.get_monospace_ids_for_scripts(scripts)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
for m_key in font_match_keys_iter(is_mono) {
|
||||
if Some(m_key.id) != default_font_match_key.as_ref().map(|m_key| m_key.id) {
|
||||
let is_mono_id = if mono_ids_for_scripts.is_empty() {
|
||||
self.font_system.is_monospace(m_key.id)
|
||||
} else {
|
||||
mono_ids_for_scripts.binary_search(&m_key.id).is_ok()
|
||||
};
|
||||
|
||||
if is_mono_id {
|
||||
let supported_cp_count_opt = self
|
||||
.font_system
|
||||
.get_font_supported_codepoints_in_word(m_key.id, self.word);
|
||||
if let Some(supported_cp_count) = supported_cp_count_opt {
|
||||
let codepoint_non_matches =
|
||||
self.word.chars().count() - supported_cp_count;
|
||||
|
||||
let fallback_info = MonospaceFallbackInfo {
|
||||
font_weight_diff: Some(m_key.font_weight_diff),
|
||||
codepoint_non_matches: Some(codepoint_non_matches),
|
||||
font_weight: m_key.font_weight,
|
||||
id: m_key.id,
|
||||
};
|
||||
assert!(self
|
||||
.font_system
|
||||
.monospace_fallbacks_buffer
|
||||
.insert(fallback_info));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If default family is Monospace fallback to first monospaced font
|
||||
if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
|
||||
if let Some(font) = self.font_system.get_font(fallback_info.id) {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while self.script_i.0 < self.scripts.len() {
|
||||
let script = self.scripts[self.script_i.0];
|
||||
|
||||
let script_families = fallbacks.script_fallback(script);
|
||||
|
||||
while self.script_i.1 < script_families.len() {
|
||||
let script_family = script_families[self.script_i.1];
|
||||
self.script_i.1 += 1;
|
||||
for m_key in font_match_keys_iter(false) {
|
||||
if self.face_contains_family(m_key.id, script_family) {
|
||||
if let Some(font) = self.font_system.get_font(m_key.id) {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!(
|
||||
"failed to find family '{}' for script {:?} and locale '{}'",
|
||||
script_family,
|
||||
script,
|
||||
self.font_system.locale(),
|
||||
);
|
||||
}
|
||||
|
||||
self.script_i.0 += 1;
|
||||
self.script_i.1 = 0;
|
||||
}
|
||||
|
||||
let common_families = fallbacks.common_fallback();
|
||||
while self.common_i < common_families.len() {
|
||||
let common_family = common_families[self.common_i];
|
||||
self.common_i += 1;
|
||||
for m_key in font_match_keys_iter(false) {
|
||||
if self.face_contains_family(m_key.id, common_family) {
|
||||
if let Some(font) = self.font_system.get_font(m_key.id) {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("failed to find family '{}'", common_family);
|
||||
}
|
||||
|
||||
//TODO: do we need to do this?
|
||||
//TODO: do not evaluate fonts more than once!
|
||||
let forbidden_families = fallbacks.forbidden_fallback();
|
||||
while self.other_i < self.font_match_keys.len() {
|
||||
let id = self.font_match_keys[self.other_i].id;
|
||||
self.other_i += 1;
|
||||
if Some(id) == self.skip_font_id {
|
||||
continue;
|
||||
}
|
||||
if forbidden_families
|
||||
.iter()
|
||||
.all(|family_name| !self.face_contains_family(id, family_name))
|
||||
{
|
||||
if let Some(font) = self.font_system.get_font(id) {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.end = true;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for FontFallbackIter<'_> {
|
||||
type Item = Arc<Font>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut fallbacks = mem::take(&mut self.font_system.fallbacks);
|
||||
let item = self.next_item(&fallbacks);
|
||||
mem::swap(&mut fallbacks, &mut self.font_system.fallbacks);
|
||||
item
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use unicode_script::Script;
|
||||
|
||||
use super::Fallback;
|
||||
|
||||
/// An empty platform-specific font fallback list.
|
||||
#[derive(Debug)]
|
||||
pub struct PlatformFallback;
|
||||
|
||||
impl Fallback for PlatformFallback {
|
||||
fn common_fallback(&self) -> &'static [&'static str] {
|
||||
common_fallback()
|
||||
}
|
||||
|
||||
fn forbidden_fallback(&self) -> &'static [&'static str] {
|
||||
forbidden_fallback()
|
||||
}
|
||||
|
||||
fn script_fallback(
|
||||
&self,
|
||||
script: unicode_script::Script,
|
||||
locale: &str,
|
||||
) -> &'static [&'static str] {
|
||||
script_fallback(script, locale)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use after any script specific fallbacks
|
||||
fn common_fallback() -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
|
||||
// Fallbacks to never use
|
||||
fn forbidden_fallback() -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
|
||||
// Fallbacks to use per script
|
||||
fn script_fallback(_script: Script, _locale: &str) -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use unicode_script::Script;
|
||||
|
||||
use super::Fallback;
|
||||
|
||||
/// A platform-specific font fallback list, for Unix.
|
||||
#[derive(Debug)]
|
||||
pub struct PlatformFallback;
|
||||
|
||||
impl Fallback for PlatformFallback {
|
||||
fn common_fallback(&self) -> &'static [&'static str] {
|
||||
common_fallback()
|
||||
}
|
||||
|
||||
fn forbidden_fallback(&self) -> &'static [&'static str] {
|
||||
forbidden_fallback()
|
||||
}
|
||||
|
||||
fn script_fallback(
|
||||
&self,
|
||||
script: unicode_script::Script,
|
||||
locale: &str,
|
||||
) -> &'static [&'static str] {
|
||||
script_fallback(script, locale)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use after any script specific fallbacks
|
||||
fn common_fallback() -> &'static [&'static str] {
|
||||
//TODO: abstract style (sans/serif/monospaced)
|
||||
&[
|
||||
/* Sans-serif fallbacks */
|
||||
"Noto Sans",
|
||||
/* More sans-serif fallbacks */
|
||||
"DejaVu Sans",
|
||||
"FreeSans",
|
||||
/* Mono fallbacks */
|
||||
"Noto Sans Mono",
|
||||
"DejaVu Sans Mono",
|
||||
"FreeMono",
|
||||
/* Symbols fallbacks */
|
||||
"Noto Sans Symbols",
|
||||
"Noto Sans Symbols2",
|
||||
/* Emoji fallbacks*/
|
||||
"Noto Color Emoji",
|
||||
//TODO: Add CJK script here for doublewides?
|
||||
]
|
||||
}
|
||||
|
||||
// Fallbacks to never use
|
||||
fn forbidden_fallback() -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||
match locale {
|
||||
// Japan
|
||||
"ja" => &["Noto Sans CJK JP"],
|
||||
// Korea
|
||||
"ko" => &["Noto Sans CJK KR"],
|
||||
// Hong Kong
|
||||
"zh-HK" => &["Noto Sans CJK HK"],
|
||||
// Taiwan
|
||||
"zh-TW" => &["Noto Sans CJK TC"],
|
||||
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||
_ => &["Noto Sans CJK SC"],
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use per script
|
||||
fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||
//TODO: abstract style (sans/serif/monospaced)
|
||||
match script {
|
||||
Script::Adlam => &["Noto Sans Adlam", "Noto Sans Adlam Unjoined"],
|
||||
Script::Arabic => &["Noto Sans Arabic"],
|
||||
Script::Armenian => &["Noto Sans Armenian"],
|
||||
Script::Bengali => &["Noto Sans Bengali"],
|
||||
Script::Bopomofo => han_unification(locale),
|
||||
//TODO: DejaVu Sans would typically be selected for braille characters,
|
||||
// but this breaks alignment when used alongside monospaced text.
|
||||
// By requesting the use of FreeMono first, this issue can be avoided.
|
||||
Script::Braille => &["FreeMono"],
|
||||
Script::Buhid => &["Noto Sans Buhid"],
|
||||
Script::Chakma => &["Noto Sans Chakma"],
|
||||
Script::Cherokee => &["Noto Sans Cherokee"],
|
||||
Script::Deseret => &["Noto Sans Deseret"],
|
||||
Script::Devanagari => &["Noto Sans Devanagari"],
|
||||
Script::Ethiopic => &["Noto Sans Ethiopic"],
|
||||
Script::Georgian => &["Noto Sans Georgian"],
|
||||
Script::Gothic => &["Noto Sans Gothic"],
|
||||
Script::Grantha => &["Noto Sans Grantha"],
|
||||
Script::Gujarati => &["Noto Sans Gujarati"],
|
||||
Script::Gurmukhi => &["Noto Sans Gurmukhi"],
|
||||
Script::Han => han_unification(locale),
|
||||
Script::Hangul => han_unification("ko"),
|
||||
Script::Hanunoo => &["Noto Sans Hanunoo"],
|
||||
Script::Hebrew => &["Noto Sans Hebrew"],
|
||||
Script::Hiragana => han_unification("ja"),
|
||||
Script::Javanese => &["Noto Sans Javanese"],
|
||||
Script::Kannada => &["Noto Sans Kannada"],
|
||||
Script::Katakana => han_unification("ja"),
|
||||
Script::Khmer => &["Noto Sans Khmer"],
|
||||
Script::Lao => &["Noto Sans Lao"],
|
||||
Script::Malayalam => &["Noto Sans Malayalam"],
|
||||
Script::Mongolian => &["Noto Sans Mongolian"],
|
||||
Script::Myanmar => &["Noto Sans Myanmar"],
|
||||
Script::Oriya => &["Noto Sans Oriya"],
|
||||
Script::Runic => &["Noto Sans Runic"],
|
||||
Script::Sinhala => &["Noto Sans Sinhala"],
|
||||
Script::Syriac => &["Noto Sans Syriac"],
|
||||
Script::Tagalog => &["Noto Sans Tagalog"],
|
||||
Script::Tagbanwa => &["Noto Sans Tagbanwa"],
|
||||
Script::Tai_Le => &["Noto Sans Tai Le"],
|
||||
Script::Tai_Tham => &["Noto Sans Tai Tham"],
|
||||
Script::Tai_Viet => &["Noto Sans Tai Viet"],
|
||||
Script::Tamil => &["Noto Sans Tamil"],
|
||||
Script::Telugu => &["Noto Sans Telugu"],
|
||||
Script::Thaana => &["Noto Sans Thaana"],
|
||||
Script::Thai => &["Noto Sans Thai"],
|
||||
//TODO: no sans script?
|
||||
Script::Tibetan => &["Noto Serif Tibetan"],
|
||||
Script::Tifinagh => &["Noto Sans Tifinagh"],
|
||||
Script::Vai => &["Noto Sans Vai"],
|
||||
//TODO: Use han_unification?
|
||||
Script::Yi => &["Noto Sans Yi", "Noto Sans CJK SC"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use unicode_script::Script;
|
||||
|
||||
use super::Fallback;
|
||||
|
||||
/// A platform-specific font fallback list, for Windows.
|
||||
#[derive(Debug)]
|
||||
pub struct PlatformFallback;
|
||||
|
||||
impl Fallback for PlatformFallback {
|
||||
fn common_fallback(&self) -> &'static [&'static str] {
|
||||
common_fallback()
|
||||
}
|
||||
|
||||
fn forbidden_fallback(&self) -> &'static [&'static str] {
|
||||
forbidden_fallback()
|
||||
}
|
||||
|
||||
fn script_fallback(
|
||||
&self,
|
||||
script: unicode_script::Script,
|
||||
locale: &str,
|
||||
) -> &'static [&'static str] {
|
||||
script_fallback(script, locale)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use after any script specific fallbacks
|
||||
fn common_fallback() -> &'static [&'static str] {
|
||||
//TODO: abstract style (sans/serif/monospaced)
|
||||
&[
|
||||
"Segoe UI",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"Segoe UI Historic",
|
||||
//TODO: Add CJK script here for doublewides?
|
||||
]
|
||||
}
|
||||
|
||||
// Fallbacks to never use
|
||||
fn forbidden_fallback() -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||
//TODO!
|
||||
match locale {
|
||||
// Japan
|
||||
"ja" => &["Yu Gothic"],
|
||||
// Korea
|
||||
"ko" => &["Malgun Gothic"],
|
||||
// Hong Kong"
|
||||
"zh-HK" => &["MingLiU_HKSCS"],
|
||||
// Taiwan
|
||||
"zh-TW" => &["Microsoft JhengHei UI"],
|
||||
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||
_ => &["Microsoft YaHei UI"],
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use per script
|
||||
fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||
//TODO: better match https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc#L99
|
||||
match script {
|
||||
Script::Adlam => &["Ebrima"],
|
||||
Script::Bengali => &["Nirmala UI"],
|
||||
Script::Canadian_Aboriginal => &["Gadugi"],
|
||||
Script::Chakma => &["Nirmala UI"],
|
||||
Script::Cherokee => &["Gadugi"],
|
||||
Script::Devanagari => &["Nirmala UI"],
|
||||
Script::Ethiopic => &["Ebrima"],
|
||||
Script::Gujarati => &["Nirmala UI"],
|
||||
Script::Gurmukhi => &["Nirmala UI"],
|
||||
Script::Han => han_unification(locale),
|
||||
Script::Hangul => han_unification("ko"),
|
||||
Script::Hiragana => han_unification("ja"),
|
||||
Script::Javanese => &["Javanese Text"],
|
||||
Script::Kannada => &["Nirmala UI"],
|
||||
Script::Katakana => han_unification("ja"),
|
||||
Script::Khmer => &["Leelawadee UI"],
|
||||
Script::Lao => &["Leelawadee UI"],
|
||||
Script::Malayalam => &["Nirmala UI"],
|
||||
Script::Mongolian => &["Mongolian Baiti"],
|
||||
Script::Myanmar => &["Myanmar Text"],
|
||||
Script::Oriya => &["Nirmala UI"],
|
||||
Script::Sinhala => &["Nirmala UI"],
|
||||
Script::Tamil => &["Nirmala UI"],
|
||||
Script::Telugu => &["Nirmala UI"],
|
||||
Script::Thaana => &["MV Boli"],
|
||||
Script::Thai => &["Leelawadee UI"],
|
||||
Script::Tibetan => &["Microsoft Himalaya"],
|
||||
Script::Tifinagh => &["Ebrima"],
|
||||
Script::Vai => &["Ebrima"],
|
||||
Script::Yi => &["Microsoft Yi Baiti"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
Vendored
+218
@@ -0,0 +1,218 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
// re-export ttf_parser
|
||||
pub use ttf_parser;
|
||||
// re-export peniko::Font;
|
||||
#[cfg(feature = "peniko")]
|
||||
pub use peniko::Font as PenikoFont;
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use alloc::sync::Arc;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use rustybuzz::Face as RustybuzzFace;
|
||||
use self_cell::self_cell;
|
||||
|
||||
pub(crate) mod fallback;
|
||||
pub use fallback::{Fallback, PlatformFallback};
|
||||
|
||||
pub use self::system::*;
|
||||
mod system;
|
||||
|
||||
self_cell!(
|
||||
struct OwnedFace {
|
||||
owner: Arc<dyn AsRef<[u8]> + Send + Sync>,
|
||||
|
||||
#[covariant]
|
||||
dependent: RustybuzzFace,
|
||||
}
|
||||
);
|
||||
|
||||
struct FontMonospaceFallback {
|
||||
monospace_em_width: Option<f32>,
|
||||
scripts: Vec<[u8; 4]>,
|
||||
unicode_codepoints: Vec<u32>,
|
||||
}
|
||||
|
||||
/// A font
|
||||
pub struct Font {
|
||||
#[cfg(feature = "swash")]
|
||||
swash: (u32, swash::CacheKey),
|
||||
rustybuzz: OwnedFace,
|
||||
#[cfg(not(feature = "peniko"))]
|
||||
data: Arc<dyn AsRef<[u8]> + Send + Sync>,
|
||||
#[cfg(feature = "peniko")]
|
||||
data: peniko::Font,
|
||||
id: fontdb::ID,
|
||||
monospace_fallback: Option<FontMonospaceFallback>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Font {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Font")
|
||||
.field("id", &self.id)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn id(&self) -> fontdb::ID {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn monospace_em_width(&self) -> Option<f32> {
|
||||
self.monospace_fallback
|
||||
.as_ref()
|
||||
.and_then(|x| x.monospace_em_width)
|
||||
}
|
||||
|
||||
pub fn scripts(&self) -> &[[u8; 4]] {
|
||||
self.monospace_fallback.as_ref().map_or(&[], |x| &x.scripts)
|
||||
}
|
||||
|
||||
pub fn unicode_codepoints(&self) -> &[u32] {
|
||||
self.monospace_fallback
|
||||
.as_ref()
|
||||
.map_or(&[], |x| &x.unicode_codepoints)
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
#[cfg(not(feature = "peniko"))]
|
||||
{
|
||||
(*self.data).as_ref()
|
||||
}
|
||||
#[cfg(feature = "peniko")]
|
||||
{
|
||||
self.data.data.data()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rustybuzz(&self) -> &RustybuzzFace<'_> {
|
||||
self.rustybuzz.borrow_dependent()
|
||||
}
|
||||
|
||||
#[cfg(feature = "peniko")]
|
||||
pub fn as_peniko(&self) -> PenikoFont {
|
||||
self.data.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "swash")]
|
||||
pub fn as_swash(&self) -> swash::FontRef<'_> {
|
||||
let swash = &self.swash;
|
||||
swash::FontRef {
|
||||
data: self.data(),
|
||||
offset: swash.0,
|
||||
key: swash.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(db: &fontdb::Database, id: fontdb::ID) -> Option<Self> {
|
||||
let info = db.face(id)?;
|
||||
|
||||
let monospace_fallback = if cfg!(feature = "monospace_fallback") {
|
||||
db.with_face_data(id, |font_data, face_index| {
|
||||
let face = ttf_parser::Face::parse(font_data, face_index).ok()?;
|
||||
let monospace_em_width = info
|
||||
.monospaced
|
||||
.then(|| {
|
||||
let hor_advance = face.glyph_hor_advance(face.glyph_index(' ')?)? as f32;
|
||||
let upem = face.units_per_em() as f32;
|
||||
Some(hor_advance / upem)
|
||||
})
|
||||
.flatten();
|
||||
|
||||
if info.monospaced && monospace_em_width.is_none() {
|
||||
None?;
|
||||
}
|
||||
|
||||
let scripts = face
|
||||
.tables()
|
||||
.gpos
|
||||
.into_iter()
|
||||
.chain(face.tables().gsub)
|
||||
.flat_map(|table| table.scripts)
|
||||
.map(|script| script.tag.to_bytes())
|
||||
.collect();
|
||||
|
||||
let mut unicode_codepoints = Vec::new();
|
||||
|
||||
face.tables()
|
||||
.cmap?
|
||||
.subtables
|
||||
.into_iter()
|
||||
.filter(|subtable| subtable.is_unicode())
|
||||
.for_each(|subtable| {
|
||||
unicode_codepoints.reserve(1024);
|
||||
subtable.codepoints(|code_point| {
|
||||
if subtable.glyph_index(code_point).is_some() {
|
||||
unicode_codepoints.push(code_point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
unicode_codepoints.shrink_to_fit();
|
||||
|
||||
Some(FontMonospaceFallback {
|
||||
monospace_em_width,
|
||||
scripts,
|
||||
unicode_codepoints,
|
||||
})
|
||||
})?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let data = match &info.source {
|
||||
fontdb::Source::Binary(data) => Arc::clone(data),
|
||||
#[cfg(feature = "std")]
|
||||
fontdb::Source::File(path) => {
|
||||
log::warn!("Unsupported fontdb Source::File('{}')", path.display());
|
||||
return None;
|
||||
}
|
||||
#[cfg(feature = "std")]
|
||||
fontdb::Source::SharedFile(_path, data) => Arc::clone(data),
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
id: info.id,
|
||||
monospace_fallback,
|
||||
#[cfg(feature = "swash")]
|
||||
swash: {
|
||||
let swash = swash::FontRef::from_index((*data).as_ref(), info.index as usize)?;
|
||||
(swash.offset, swash.key)
|
||||
},
|
||||
rustybuzz: OwnedFace::try_new(Arc::clone(&data), |data| {
|
||||
RustybuzzFace::from_slice((**data).as_ref(), info.index).ok_or(())
|
||||
})
|
||||
.ok()?,
|
||||
#[cfg(not(feature = "peniko"))]
|
||||
data,
|
||||
#[cfg(feature = "peniko")]
|
||||
data: peniko::Font::new(peniko::Blob::new(data), info.index),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn test_fonts_load_time() {
|
||||
use crate::FontSystem;
|
||||
use sys_locale::get_locale;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let mut db = fontdb::Database::new();
|
||||
let locale = get_locale().expect("Local available");
|
||||
db.load_system_fonts();
|
||||
FontSystem::new_with_locale_and_db(locale, db);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
println!("Fonts load time {}ms.", now.elapsed().as_millis());
|
||||
}
|
||||
}
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
use crate::{Attrs, Font, FontMatchAttrs, HashMap, ShapeBuffer};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::string::String;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
|
||||
// re-export fontdb and rustybuzz
|
||||
pub use fontdb;
|
||||
pub use rustybuzz;
|
||||
|
||||
use super::fallback::{Fallback, Fallbacks, MonospaceFallbackInfo, PlatformFallback};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FontMatchKey {
|
||||
pub(crate) font_weight_diff: u16,
|
||||
pub(crate) font_weight: u16,
|
||||
pub(crate) id: fontdb::ID,
|
||||
}
|
||||
|
||||
struct FontCachedCodepointSupportInfo {
|
||||
supported: Vec<u32>,
|
||||
not_supported: Vec<u32>,
|
||||
}
|
||||
|
||||
impl FontCachedCodepointSupportInfo {
|
||||
const SUPPORTED_MAX_SZ: usize = 512;
|
||||
const NOT_SUPPORTED_MAX_SZ: usize = 1024;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
supported: Vec::with_capacity(Self::SUPPORTED_MAX_SZ),
|
||||
not_supported: Vec::with_capacity(Self::NOT_SUPPORTED_MAX_SZ),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn unknown_has_codepoint(
|
||||
&mut self,
|
||||
font_codepoints: &[u32],
|
||||
codepoint: u32,
|
||||
supported_insert_pos: usize,
|
||||
not_supported_insert_pos: usize,
|
||||
) -> bool {
|
||||
let ret = font_codepoints.contains(&codepoint);
|
||||
if ret {
|
||||
// don't bother inserting if we are going to truncate the entry away
|
||||
if supported_insert_pos != Self::SUPPORTED_MAX_SZ {
|
||||
self.supported.insert(supported_insert_pos, codepoint);
|
||||
self.supported.truncate(Self::SUPPORTED_MAX_SZ);
|
||||
}
|
||||
} else {
|
||||
// don't bother inserting if we are going to truncate the entry away
|
||||
if not_supported_insert_pos != Self::NOT_SUPPORTED_MAX_SZ {
|
||||
self.not_supported
|
||||
.insert(not_supported_insert_pos, codepoint);
|
||||
self.not_supported.truncate(Self::NOT_SUPPORTED_MAX_SZ);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn has_codepoint(&mut self, font_codepoints: &[u32], codepoint: u32) -> bool {
|
||||
match self.supported.binary_search(&codepoint) {
|
||||
Ok(_) => true,
|
||||
Err(supported_insert_pos) => match self.not_supported.binary_search(&codepoint) {
|
||||
Ok(_) => false,
|
||||
Err(not_supported_insert_pos) => self.unknown_has_codepoint(
|
||||
font_codepoints,
|
||||
codepoint,
|
||||
supported_insert_pos,
|
||||
not_supported_insert_pos,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the system fonts.
|
||||
pub struct FontSystem {
|
||||
/// The locale of the system.
|
||||
locale: String,
|
||||
|
||||
/// The underlying font database.
|
||||
db: fontdb::Database,
|
||||
|
||||
/// Cache for loaded fonts from the database.
|
||||
font_cache: HashMap<fontdb::ID, Option<Arc<Font>>>,
|
||||
|
||||
/// Sorted unique ID's of all Monospace fonts in DB
|
||||
monospace_font_ids: Vec<fontdb::ID>,
|
||||
|
||||
/// Sorted unique ID's of all Monospace fonts in DB per script.
|
||||
/// A font may support multiple scripts of course, so the same ID
|
||||
/// may appear in multiple map value vecs.
|
||||
per_script_monospace_font_ids: HashMap<[u8; 4], Vec<fontdb::ID>>,
|
||||
|
||||
/// Cache for font codepoint support info
|
||||
font_codepoint_support_info_cache: HashMap<fontdb::ID, FontCachedCodepointSupportInfo>,
|
||||
|
||||
/// Cache for font matches.
|
||||
font_matches_cache: HashMap<FontMatchAttrs, Arc<Vec<FontMatchKey>>>,
|
||||
|
||||
/// Scratch buffer for shaping and laying out.
|
||||
pub(crate) shape_buffer: ShapeBuffer,
|
||||
|
||||
/// Buffer for use in `FontFallbackIter`.
|
||||
pub(crate) monospace_fallbacks_buffer: BTreeSet<MonospaceFallbackInfo>,
|
||||
|
||||
/// Cache for shaped runs
|
||||
#[cfg(feature = "shape-run-cache")]
|
||||
pub shape_run_cache: crate::ShapeRunCache,
|
||||
|
||||
/// List of fallbacks
|
||||
pub(crate) dyn_fallback: Box<dyn Fallback>,
|
||||
|
||||
/// List of fallbacks
|
||||
pub(crate) fallbacks: Fallbacks,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FontSystem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FontSystem")
|
||||
.field("locale", &self.locale)
|
||||
.field("db", &self.db)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl FontSystem {
|
||||
const FONT_MATCHES_CACHE_SIZE_LIMIT: usize = 256;
|
||||
/// Create a new [`FontSystem`], that allows access to any installed system fonts
|
||||
///
|
||||
/// # Timing
|
||||
///
|
||||
/// This function takes some time to run. On the release build, it can take up to a second,
|
||||
/// while debug builds can take up to ten times longer. For this reason, it should only be
|
||||
/// called once, and the resulting [`FontSystem`] should be shared.
|
||||
pub fn new() -> Self {
|
||||
Self::new_with_fonts(core::iter::empty())
|
||||
}
|
||||
|
||||
/// Create a new [`FontSystem`] with a pre-specified set of fonts.
|
||||
pub fn new_with_fonts(fonts: impl IntoIterator<Item = fontdb::Source>) -> Self {
|
||||
let locale = Self::get_locale();
|
||||
log::debug!("Locale: {}", locale);
|
||||
|
||||
let mut db = fontdb::Database::new();
|
||||
|
||||
Self::load_fonts(&mut db, fonts.into_iter());
|
||||
|
||||
//TODO: configurable default fonts
|
||||
db.set_monospace_family("Noto Sans Mono");
|
||||
db.set_sans_serif_family("Open Sans");
|
||||
db.set_serif_family("DejaVu Serif");
|
||||
|
||||
Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
|
||||
}
|
||||
|
||||
/// Create a new [`FontSystem`] with a pre-specified locale, font database and font fallback list.
|
||||
pub fn new_with_locale_and_db_and_fallback(
|
||||
locale: String,
|
||||
db: fontdb::Database,
|
||||
impl_fallback: impl Fallback + 'static,
|
||||
) -> Self {
|
||||
let mut monospace_font_ids = db
|
||||
.faces()
|
||||
.filter(|face_info| {
|
||||
face_info.monospaced && !face_info.post_script_name.contains("Emoji")
|
||||
})
|
||||
.map(|face_info| face_info.id)
|
||||
.collect::<Vec<_>>();
|
||||
monospace_font_ids.sort();
|
||||
|
||||
let mut per_script_monospace_font_ids: HashMap<[u8; 4], BTreeSet<fontdb::ID>> =
|
||||
HashMap::default();
|
||||
|
||||
if cfg!(feature = "monospace_fallback") {
|
||||
monospace_font_ids.iter().for_each(|&id| {
|
||||
db.with_face_data(id, |font_data, face_index| {
|
||||
let _ = ttf_parser::Face::parse(font_data, face_index).map(|face| {
|
||||
face.tables()
|
||||
.gpos
|
||||
.into_iter()
|
||||
.chain(face.tables().gsub)
|
||||
.flat_map(|table| table.scripts)
|
||||
.inspect(|script| {
|
||||
per_script_monospace_font_ids
|
||||
.entry(script.tag.to_bytes())
|
||||
.or_default()
|
||||
.insert(id);
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let per_script_monospace_font_ids = per_script_monospace_font_ids
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, Vec::from_iter(v)))
|
||||
.collect();
|
||||
|
||||
let fallbacks = Fallbacks::new(&impl_fallback, &[], &locale);
|
||||
|
||||
Self {
|
||||
locale,
|
||||
db,
|
||||
monospace_font_ids,
|
||||
per_script_monospace_font_ids,
|
||||
font_cache: Default::default(),
|
||||
font_matches_cache: Default::default(),
|
||||
font_codepoint_support_info_cache: Default::default(),
|
||||
monospace_fallbacks_buffer: BTreeSet::default(),
|
||||
#[cfg(feature = "shape-run-cache")]
|
||||
shape_run_cache: crate::ShapeRunCache::default(),
|
||||
shape_buffer: ShapeBuffer::default(),
|
||||
dyn_fallback: Box::new(impl_fallback),
|
||||
fallbacks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`FontSystem`] with a pre-specified locale and font database.
|
||||
pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self {
|
||||
Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
|
||||
}
|
||||
|
||||
/// Get the locale.
|
||||
pub fn locale(&self) -> &str {
|
||||
&self.locale
|
||||
}
|
||||
|
||||
/// Get the database.
|
||||
pub fn db(&self) -> &fontdb::Database {
|
||||
&self.db
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the database.
|
||||
pub fn db_mut(&mut self) -> &mut fontdb::Database {
|
||||
self.font_matches_cache.clear();
|
||||
&mut self.db
|
||||
}
|
||||
|
||||
/// Consume this [`FontSystem`] and return the locale and database.
|
||||
pub fn into_locale_and_db(self) -> (String, fontdb::Database) {
|
||||
(self.locale, self.db)
|
||||
}
|
||||
|
||||
/// Get a font by its ID.
|
||||
pub fn get_font(&mut self, id: fontdb::ID) -> Option<Arc<Font>> {
|
||||
self.font_cache
|
||||
.entry(id)
|
||||
.or_insert_with(|| {
|
||||
#[cfg(feature = "std")]
|
||||
unsafe {
|
||||
self.db.make_shared_face_data(id);
|
||||
}
|
||||
match Font::new(&self.db, id) {
|
||||
Some(font) => Some(Arc::new(font)),
|
||||
None => {
|
||||
log::warn!(
|
||||
"failed to load font '{}'",
|
||||
self.db.face(id)?.post_script_name
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn is_monospace(&self, id: fontdb::ID) -> bool {
|
||||
self.monospace_font_ids.binary_search(&id).is_ok()
|
||||
}
|
||||
|
||||
pub fn get_monospace_ids_for_scripts(
|
||||
&self,
|
||||
scripts: impl Iterator<Item = [u8; 4]>,
|
||||
) -> Vec<fontdb::ID> {
|
||||
let mut ret = scripts
|
||||
.filter_map(|script| self.per_script_monospace_font_ids.get(&script))
|
||||
.flat_map(|ids| ids.iter().copied())
|
||||
.collect::<Vec<_>>();
|
||||
ret.sort();
|
||||
ret.dedup();
|
||||
ret
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_font_supported_codepoints_in_word(
|
||||
&mut self,
|
||||
id: fontdb::ID,
|
||||
word: &str,
|
||||
) -> Option<usize> {
|
||||
self.get_font(id).map(|font| {
|
||||
let code_points = font.unicode_codepoints();
|
||||
let cache = self
|
||||
.font_codepoint_support_info_cache
|
||||
.entry(id)
|
||||
.or_insert_with(FontCachedCodepointSupportInfo::new);
|
||||
word.chars()
|
||||
.filter(|ch| cache.has_codepoint(code_points, u32::from(*ch)))
|
||||
.count()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_font_matches(&mut self, attrs: &Attrs<'_>) -> Arc<Vec<FontMatchKey>> {
|
||||
// Clear the cache first if it reached the size limit
|
||||
if self.font_matches_cache.len() >= Self::FONT_MATCHES_CACHE_SIZE_LIMIT {
|
||||
log::trace!("clear font mache cache");
|
||||
self.font_matches_cache.clear();
|
||||
}
|
||||
|
||||
self.font_matches_cache
|
||||
//TODO: do not create AttrsOwned unless entry does not already exist
|
||||
.entry(attrs.into())
|
||||
.or_insert_with(|| {
|
||||
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let mut font_match_keys = self
|
||||
.db
|
||||
.faces()
|
||||
.filter(|face| attrs.matches(face))
|
||||
.map(|face| FontMatchKey {
|
||||
font_weight_diff: attrs.weight.0.abs_diff(face.weight.0),
|
||||
font_weight: face.weight.0,
|
||||
id: face.id,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort so we get the keys with weight_offset=0 first
|
||||
font_match_keys.sort();
|
||||
|
||||
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
||||
{
|
||||
let elapsed = now.elapsed();
|
||||
log::debug!("font matches for {:?} in {:?}", attrs, elapsed);
|
||||
}
|
||||
|
||||
Arc::new(font_match_keys)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn get_locale() -> String {
|
||||
sys_locale::get_locale().unwrap_or_else(|| {
|
||||
log::warn!("failed to get system locale, falling back to en-US");
|
||||
String::from("en-US")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
fn get_locale() -> String {
|
||||
String::from("en-US")
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
db.load_system_fonts();
|
||||
|
||||
for source in fonts {
|
||||
db.load_font_source(source);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
log::debug!(
|
||||
"Parsed {} font faces in {}ms.",
|
||||
db.len(),
|
||||
now.elapsed().as_millis()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
|
||||
for source in fonts {
|
||||
db.load_font_source(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A value borrowed together with an [`FontSystem`]
|
||||
#[derive(Debug)]
|
||||
pub struct BorrowedWithFontSystem<'a, T> {
|
||||
pub(crate) inner: &'a mut T,
|
||||
pub(crate) font_system: &'a mut FontSystem,
|
||||
}
|
||||
|
||||
impl<T> Deref for BorrowedWithFontSystem<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for BorrowedWithFontSystem<'_, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Flags that change rendering
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(transparent)]
|
||||
pub struct CacheKeyFlags: u32 {
|
||||
/// Skew by 14 degrees to synthesize italic
|
||||
const FAKE_ITALIC = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Key for building a glyph cache
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct CacheKey {
|
||||
/// Font ID
|
||||
pub font_id: fontdb::ID,
|
||||
/// Glyph ID
|
||||
pub glyph_id: u16,
|
||||
/// `f32` bits of font size
|
||||
pub font_size_bits: u32,
|
||||
/// Binning of fractional X offset
|
||||
pub x_bin: SubpixelBin,
|
||||
/// Binning of fractional Y offset
|
||||
pub y_bin: SubpixelBin,
|
||||
/// [`CacheKeyFlags`]
|
||||
pub flags: CacheKeyFlags,
|
||||
}
|
||||
|
||||
impl CacheKey {
|
||||
pub fn new(
|
||||
font_id: fontdb::ID,
|
||||
glyph_id: u16,
|
||||
font_size: f32,
|
||||
pos: (f32, f32),
|
||||
flags: CacheKeyFlags,
|
||||
) -> (Self, i32, i32) {
|
||||
let (x, x_bin) = SubpixelBin::new(pos.0);
|
||||
let (y, y_bin) = SubpixelBin::new(pos.1);
|
||||
(
|
||||
Self {
|
||||
font_id,
|
||||
glyph_id,
|
||||
font_size_bits: font_size.to_bits(),
|
||||
x_bin,
|
||||
y_bin,
|
||||
flags,
|
||||
},
|
||||
x,
|
||||
y,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Binning of subpixel position for cache optimization
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum SubpixelBin {
|
||||
Zero,
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
}
|
||||
|
||||
impl SubpixelBin {
|
||||
pub fn new(pos: f32) -> (i32, Self) {
|
||||
let trunc = pos as i32;
|
||||
let fract = pos - trunc as f32;
|
||||
|
||||
if pos.is_sign_negative() {
|
||||
if fract > -0.125 {
|
||||
(trunc, Self::Zero)
|
||||
} else if fract > -0.375 {
|
||||
(trunc - 1, Self::Three)
|
||||
} else if fract > -0.625 {
|
||||
(trunc - 1, Self::Two)
|
||||
} else if fract > -0.875 {
|
||||
(trunc - 1, Self::One)
|
||||
} else {
|
||||
(trunc - 1, Self::Zero)
|
||||
}
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if fract < 0.125 {
|
||||
(trunc, Self::Zero)
|
||||
} else if fract < 0.375 {
|
||||
(trunc, Self::One)
|
||||
} else if fract < 0.625 {
|
||||
(trunc, Self::Two)
|
||||
} else if fract < 0.875 {
|
||||
(trunc, Self::Three)
|
||||
} else {
|
||||
(trunc + 1, Self::Zero)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_float(&self) -> f32 {
|
||||
match self {
|
||||
Self::Zero => 0.0,
|
||||
Self::One => 0.25,
|
||||
Self::Two => 0.5,
|
||||
Self::Three => 0.75,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subpixel_bins() {
|
||||
// POSITIVE TESTS
|
||||
|
||||
// Maps to 0.0
|
||||
assert_eq!(SubpixelBin::new(0.0), (0, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(0.124), (0, SubpixelBin::Zero));
|
||||
|
||||
// Maps to 0.25
|
||||
assert_eq!(SubpixelBin::new(0.125), (0, SubpixelBin::One));
|
||||
assert_eq!(SubpixelBin::new(0.25), (0, SubpixelBin::One));
|
||||
assert_eq!(SubpixelBin::new(0.374), (0, SubpixelBin::One));
|
||||
|
||||
// Maps to 0.5
|
||||
assert_eq!(SubpixelBin::new(0.375), (0, SubpixelBin::Two));
|
||||
assert_eq!(SubpixelBin::new(0.5), (0, SubpixelBin::Two));
|
||||
assert_eq!(SubpixelBin::new(0.624), (0, SubpixelBin::Two));
|
||||
|
||||
// Maps to 0.75
|
||||
assert_eq!(SubpixelBin::new(0.625), (0, SubpixelBin::Three));
|
||||
assert_eq!(SubpixelBin::new(0.75), (0, SubpixelBin::Three));
|
||||
assert_eq!(SubpixelBin::new(0.874), (0, SubpixelBin::Three));
|
||||
|
||||
// Maps to 1.0
|
||||
assert_eq!(SubpixelBin::new(0.875), (1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(0.999), (1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(1.0), (1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(1.124), (1, SubpixelBin::Zero));
|
||||
|
||||
// NEGATIVE TESTS
|
||||
|
||||
// Maps to 0.0
|
||||
assert_eq!(SubpixelBin::new(-0.0), (0, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(-0.124), (0, SubpixelBin::Zero));
|
||||
|
||||
// Maps to 0.25
|
||||
assert_eq!(SubpixelBin::new(-0.125), (-1, SubpixelBin::Three));
|
||||
assert_eq!(SubpixelBin::new(-0.25), (-1, SubpixelBin::Three));
|
||||
assert_eq!(SubpixelBin::new(-0.374), (-1, SubpixelBin::Three));
|
||||
|
||||
// Maps to 0.5
|
||||
assert_eq!(SubpixelBin::new(-0.375), (-1, SubpixelBin::Two));
|
||||
assert_eq!(SubpixelBin::new(-0.5), (-1, SubpixelBin::Two));
|
||||
assert_eq!(SubpixelBin::new(-0.624), (-1, SubpixelBin::Two));
|
||||
|
||||
// Maps to 0.75
|
||||
assert_eq!(SubpixelBin::new(-0.625), (-1, SubpixelBin::One));
|
||||
assert_eq!(SubpixelBin::new(-0.75), (-1, SubpixelBin::One));
|
||||
assert_eq!(SubpixelBin::new(-0.874), (-1, SubpixelBin::One));
|
||||
|
||||
// Maps to 1.0
|
||||
assert_eq!(SubpixelBin::new(-0.875), (-1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(-0.999), (-1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(-1.0), (-1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(-1.124), (-1, SubpixelBin::Zero));
|
||||
}
|
||||
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use core::fmt::Display;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::{math, CacheKey, CacheKeyFlags, Color};
|
||||
|
||||
/// A laid out glyph
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LayoutGlyph {
|
||||
/// Start index of cluster in original line
|
||||
pub start: usize,
|
||||
/// End index of cluster in original line
|
||||
pub end: usize,
|
||||
/// Font size of the glyph
|
||||
pub font_size: f32,
|
||||
/// Line height of the glyph, will override buffer setting
|
||||
pub line_height_opt: Option<f32>,
|
||||
/// Font id of the glyph
|
||||
pub font_id: fontdb::ID,
|
||||
/// Font id of the glyph
|
||||
pub glyph_id: u16,
|
||||
/// X offset of hitbox
|
||||
pub x: f32,
|
||||
/// Y offset of hitbox
|
||||
pub y: f32,
|
||||
/// Width of hitbox
|
||||
pub w: f32,
|
||||
/// Unicode `BiDi` embedding level, character is left-to-right if `level` is divisible by 2
|
||||
pub level: unicode_bidi::Level,
|
||||
/// X offset in line
|
||||
///
|
||||
/// If you are dealing with physical coordinates, use [`Self::physical`] to obtain a
|
||||
/// [`PhysicalGlyph`] for rendering.
|
||||
///
|
||||
/// This offset is useful when you are dealing with logical units and you do not care or
|
||||
/// cannot guarantee pixel grid alignment. For instance, when you want to use the glyphs
|
||||
/// for vectorial text, apply linear transformations to the layout, etc.
|
||||
pub x_offset: f32,
|
||||
/// Y offset in line
|
||||
///
|
||||
/// If you are dealing with physical coordinates, use [`Self::physical`] to obtain a
|
||||
/// [`PhysicalGlyph`] for rendering.
|
||||
///
|
||||
/// This offset is useful when you are dealing with logical units and you do not care or
|
||||
/// cannot guarantee pixel grid alignment. For instance, when you want to use the glyphs
|
||||
/// for vectorial text, apply linear transformations to the layout, etc.
|
||||
pub y_offset: f32,
|
||||
/// Optional color override
|
||||
pub color_opt: Option<Color>,
|
||||
/// Metadata from `Attrs`
|
||||
pub metadata: usize,
|
||||
/// [`CacheKeyFlags`]
|
||||
pub cache_key_flags: CacheKeyFlags,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PhysicalGlyph {
|
||||
/// Cache key, see [`CacheKey`]
|
||||
pub cache_key: CacheKey,
|
||||
/// Integer component of X offset in line
|
||||
pub x: i32,
|
||||
/// Integer component of Y offset in line
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl LayoutGlyph {
|
||||
pub fn physical(&self, offset: (f32, f32), scale: f32) -> PhysicalGlyph {
|
||||
let x_offset = self.font_size * self.x_offset;
|
||||
let y_offset = self.font_size * self.y_offset;
|
||||
|
||||
let (cache_key, x, y) = CacheKey::new(
|
||||
self.font_id,
|
||||
self.glyph_id,
|
||||
self.font_size * scale,
|
||||
(
|
||||
(self.x + x_offset) * scale + offset.0,
|
||||
math::truncf((self.y - y_offset) * scale + offset.1), // Hinting in Y axis
|
||||
),
|
||||
self.cache_key_flags,
|
||||
);
|
||||
|
||||
PhysicalGlyph { cache_key, x, y }
|
||||
}
|
||||
}
|
||||
|
||||
/// A line of laid out glyphs
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LayoutLine {
|
||||
/// Width of the line
|
||||
pub w: f32,
|
||||
/// Maximum ascent of the glyphs in line
|
||||
pub max_ascent: f32,
|
||||
/// Maximum descent of the glyphs in line
|
||||
pub max_descent: f32,
|
||||
/// Maximum line height of any spans in line
|
||||
pub line_height_opt: Option<f32>,
|
||||
/// Glyphs in line
|
||||
pub glyphs: Vec<LayoutGlyph>,
|
||||
}
|
||||
|
||||
/// Wrapping mode
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
pub enum Wrap {
|
||||
/// No wrapping
|
||||
None,
|
||||
/// Wraps at a glyph level
|
||||
Glyph,
|
||||
/// Wraps at the word level
|
||||
Word,
|
||||
/// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself
|
||||
WordOrGlyph,
|
||||
}
|
||||
|
||||
impl Display for Wrap {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "No Wrap"),
|
||||
Self::Word => write!(f, "Word Wrap"),
|
||||
Self::WordOrGlyph => write!(f, "Word Wrap or Character"),
|
||||
Self::Glyph => write!(f, "Character"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Align or justify
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
pub enum Align {
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
Justified,
|
||||
End,
|
||||
}
|
||||
|
||||
impl Display for Align {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Left => write!(f, "Left"),
|
||||
Self::Right => write!(f, "Right"),
|
||||
Self::Center => write!(f, "Center"),
|
||||
Self::Justified => write!(f, "Justified"),
|
||||
Self::End => write!(f, "End"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+150
@@ -0,0 +1,150 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! # COSMIC Text
|
||||
//!
|
||||
//! This library provides advanced text handling in a generic way. It provides abstractions for
|
||||
//! shaping, font discovery, font fallback, layout, rasterization, and editing. Shaping utilizes
|
||||
//! rustybuzz, font discovery utilizes fontdb, and the rasterization is optional and utilizes
|
||||
//! swash. The other features are developed internal to this library.
|
||||
//!
|
||||
//! It is recommended that you start by creating a [`FontSystem`], after which you can create a
|
||||
//! [`Buffer`], provide it with some text, and then inspect the layout it produces. At this
|
||||
//! point, you can use the `SwashCache` to rasterize glyphs into either images or pixels.
|
||||
//!
|
||||
//! ```
|
||||
//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics, Shaping};
|
||||
//!
|
||||
//! // A FontSystem provides access to detected system fonts, create one per application
|
||||
//! let mut font_system = FontSystem::new();
|
||||
//!
|
||||
//! // A SwashCache stores rasterized glyphs, create one per application
|
||||
//! let mut swash_cache = SwashCache::new();
|
||||
//!
|
||||
//! // Text metrics indicate the font size and line height of a buffer
|
||||
//! let metrics = Metrics::new(14.0, 20.0);
|
||||
//!
|
||||
//! // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget
|
||||
//! let mut buffer = Buffer::new(&mut font_system, metrics);
|
||||
//!
|
||||
//! // Borrow buffer together with the font system for more convenient method calls
|
||||
//! let mut buffer = buffer.borrow_with(&mut font_system);
|
||||
//!
|
||||
//! // Set a size for the text buffer, in pixels
|
||||
//! buffer.set_size(Some(80.0), Some(25.0));
|
||||
//!
|
||||
//! // Attributes indicate what font to choose
|
||||
//! let attrs = Attrs::new();
|
||||
//!
|
||||
//! // Add some text!
|
||||
//! buffer.set_text("Hello, Rust! 🦀\n", &attrs, Shaping::Advanced);
|
||||
//!
|
||||
//! // Perform shaping as desired
|
||||
//! buffer.shape_until_scroll(true);
|
||||
//!
|
||||
//! // Inspect the output runs
|
||||
//! for run in buffer.layout_runs() {
|
||||
//! for glyph in run.glyphs.iter() {
|
||||
//! println!("{:#?}", glyph);
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // Create a default text color
|
||||
//! let text_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||
//!
|
||||
//! // Draw the buffer (for performance, instead use SwashCache directly)
|
||||
//! buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
|
||||
//! // Fill in your code here for drawing rectangles
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
// Not interested in these lints
|
||||
#![allow(clippy::new_without_default)]
|
||||
// TODO: address occurrences and then deny
|
||||
//
|
||||
// Overflows can produce unpredictable results and are only checked in debug builds
|
||||
#![allow(clippy::arithmetic_side_effects)]
|
||||
// Indexing a slice can cause panics and that is something we always want to avoid
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
// Soundness issues
|
||||
//
|
||||
// Dereferencing unaligned pointers may be undefined behavior
|
||||
#![deny(clippy::cast_ptr_alignment)]
|
||||
// Avoid panicking in without information about the panic. Use expect
|
||||
#![deny(clippy::unwrap_used)]
|
||||
// Ensure all types have a debug impl
|
||||
#![deny(missing_debug_implementations)]
|
||||
// This is usually a serious issue - a missing import of a define where it is interpreted
|
||||
// as a catch-all variable in a match, for example
|
||||
#![deny(unreachable_patterns)]
|
||||
// Ensure that all must_use results are used
|
||||
#![deny(unused_must_use)]
|
||||
// Style issues
|
||||
//
|
||||
// Documentation not ideal
|
||||
#![warn(clippy::doc_markdown)]
|
||||
// Document possible errors
|
||||
#![warn(clippy::missing_errors_doc)]
|
||||
// Document possible panics
|
||||
#![warn(clippy::missing_panics_doc)]
|
||||
// Ensure semicolons are present
|
||||
#![warn(clippy::semicolon_if_nothing_returned)]
|
||||
// Ensure numbers are readable
|
||||
#![warn(clippy::unreadable_literal)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(not(any(feature = "std", feature = "no_std")))]
|
||||
compile_error!("Either the `std` or `no_std` feature must be enabled");
|
||||
|
||||
pub use self::attrs::*;
|
||||
mod attrs;
|
||||
|
||||
pub use self::bidi_para::*;
|
||||
mod bidi_para;
|
||||
|
||||
pub use self::buffer::*;
|
||||
mod buffer;
|
||||
|
||||
pub use self::buffer_line::*;
|
||||
mod buffer_line;
|
||||
|
||||
pub use self::cached::*;
|
||||
mod cached;
|
||||
|
||||
pub use self::glyph_cache::*;
|
||||
mod glyph_cache;
|
||||
|
||||
pub use self::cursor::*;
|
||||
mod cursor;
|
||||
|
||||
pub use self::edit::*;
|
||||
mod edit;
|
||||
|
||||
pub use self::font::*;
|
||||
mod font;
|
||||
|
||||
pub use self::layout::*;
|
||||
mod layout;
|
||||
|
||||
pub use self::line_ending::*;
|
||||
mod line_ending;
|
||||
|
||||
pub use self::shape::*;
|
||||
mod shape;
|
||||
|
||||
pub use self::shape_run_cache::*;
|
||||
mod shape_run_cache;
|
||||
|
||||
#[cfg(feature = "swash")]
|
||||
pub use self::swash::*;
|
||||
#[cfg(feature = "swash")]
|
||||
mod swash;
|
||||
|
||||
mod math;
|
||||
|
||||
type BuildHasher = core::hash::BuildHasherDefault<rustc_hash::FxHasher>;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
type HashMap<K, V> = std::collections::HashMap<K, V, BuildHasher>;
|
||||
#[cfg(not(feature = "std"))]
|
||||
type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasher>;
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
use core::ops::Range;
|
||||
|
||||
/// Line ending
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum LineEnding {
|
||||
/// Use `\n` for line ending (POSIX-style)
|
||||
#[default]
|
||||
Lf,
|
||||
/// Use `\r\n` for line ending (Windows-style)
|
||||
CrLf,
|
||||
/// Use `\r` for line ending (many legacy systems)
|
||||
Cr,
|
||||
/// Use `\n\r` for line ending (some legacy systems)
|
||||
LfCr,
|
||||
/// No line ending
|
||||
None,
|
||||
}
|
||||
|
||||
impl LineEnding {
|
||||
/// Get the line ending as a str
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Lf => "\n",
|
||||
Self::CrLf => "\r\n",
|
||||
Self::Cr => "\r",
|
||||
Self::LfCr => "\n\r",
|
||||
Self::None => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over lines terminated by [`LineEnding`]
|
||||
#[derive(Debug)]
|
||||
pub struct LineIter<'a> {
|
||||
string: &'a str,
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl<'a> LineIter<'a> {
|
||||
/// Create an iterator of lines in a string slice
|
||||
pub fn new(string: &'a str) -> Self {
|
||||
Self {
|
||||
string,
|
||||
start: 0,
|
||||
end: string.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for LineIter<'_> {
|
||||
type Item = (Range<usize>, LineEnding);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let start = self.start;
|
||||
match self.string[start..self.end].find(['\r', '\n']) {
|
||||
Some(i) => {
|
||||
let end = start + i;
|
||||
self.start = end;
|
||||
let after = &self.string[end..];
|
||||
let ending = if after.starts_with("\r\n") {
|
||||
LineEnding::CrLf
|
||||
} else if after.starts_with("\n\r") {
|
||||
LineEnding::LfCr
|
||||
} else if after.starts_with("\n") {
|
||||
LineEnding::Lf
|
||||
} else if after.starts_with("\r") {
|
||||
LineEnding::Cr
|
||||
} else {
|
||||
//TODO: this should not be possible
|
||||
LineEnding::None
|
||||
};
|
||||
self.start += ending.as_str().len();
|
||||
Some((start..end, ending))
|
||||
}
|
||||
None => {
|
||||
if self.start < self.end {
|
||||
self.start = self.end;
|
||||
Some((start..self.end, LineEnding::None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: DoubleEndedIterator
|
||||
|
||||
#[test]
|
||||
fn test_line_iter() {
|
||||
let string = "LF\nCRLF\r\nCR\rLFCR\n\rNONE";
|
||||
let mut iter = LineIter::new(string);
|
||||
assert_eq!(iter.next(), Some((0..2, LineEnding::Lf)));
|
||||
assert_eq!(iter.next(), Some((3..7, LineEnding::CrLf)));
|
||||
assert_eq!(iter.next(), Some((9..11, LineEnding::Cr)));
|
||||
assert_eq!(iter.next(), Some((12..16, LineEnding::LfCr)));
|
||||
assert_eq!(iter.next(), Some((18..22, LineEnding::None)));
|
||||
}
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub use libm::{floorf, roundf, truncf};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[inline]
|
||||
pub fn floorf(x: f32) -> f32 {
|
||||
x.floor()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[inline]
|
||||
pub fn roundf(x: f32) -> f32 {
|
||||
x.round()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[inline]
|
||||
pub fn truncf(x: f32) -> f32 {
|
||||
x.trunc()
|
||||
}
|
||||
Vendored
+1953
File diff suppressed because it is too large.
Load diff
+49
@@ -0,0 +1,49 @@
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::{string::String, vec::Vec};
|
||||
use core::ops::Range;
|
||||
|
||||
use crate::{AttrsOwned, HashMap, ShapeGlyph};
|
||||
|
||||
/// Key for caching shape runs.
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct ShapeRunKey {
|
||||
pub text: String,
|
||||
pub default_attrs: AttrsOwned,
|
||||
pub attrs_spans: Vec<(Range<usize>, AttrsOwned)>,
|
||||
}
|
||||
|
||||
/// A helper structure for caching shape runs.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ShapeRunCache {
|
||||
age: u64,
|
||||
cache: HashMap<ShapeRunKey, (u64, Vec<ShapeGlyph>)>,
|
||||
}
|
||||
|
||||
impl ShapeRunCache {
|
||||
/// Get cache item, updating age if found
|
||||
pub fn get(&mut self, key: &ShapeRunKey) -> Option<&Vec<ShapeGlyph>> {
|
||||
self.cache.get_mut(key).map(|(age, glyphs)| {
|
||||
*age = self.age;
|
||||
&*glyphs
|
||||
})
|
||||
}
|
||||
|
||||
/// Insert cache item with current age
|
||||
pub fn insert(&mut self, key: ShapeRunKey, glyphs: Vec<ShapeGlyph>) {
|
||||
self.cache.insert(key, (self.age, glyphs));
|
||||
}
|
||||
|
||||
/// Remove anything in the cache with an age older than `keep_ages`
|
||||
pub fn trim(&mut self, keep_ages: u64) {
|
||||
self.cache
|
||||
.retain(|_key, (age, _glyphs)| *age + keep_ages >= self.age);
|
||||
// Increase age
|
||||
self.age += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for ShapeRunCache {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_tuple("ShapeRunCache").finish()
|
||||
}
|
||||
}
|
||||
Vendored
+214
@@ -0,0 +1,214 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
use swash::scale::{image::Content, ScaleContext};
|
||||
use swash::scale::{Render, Source, StrikeWith};
|
||||
use swash::zeno::{Format, Vector};
|
||||
|
||||
use crate::{CacheKey, CacheKeyFlags, Color, FontSystem, HashMap};
|
||||
|
||||
pub use swash::scale::image::{Content as SwashContent, Image as SwashImage};
|
||||
pub use swash::zeno::{Angle, Command, Placement, Transform};
|
||||
|
||||
fn swash_image(
|
||||
font_system: &mut FontSystem,
|
||||
context: &mut ScaleContext,
|
||||
cache_key: CacheKey,
|
||||
) -> Option<SwashImage> {
|
||||
let font = match font_system.get_font(cache_key.font_id) {
|
||||
Some(some) => some,
|
||||
None => {
|
||||
log::warn!("did not find font {:?}", cache_key.font_id);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Build the scaler
|
||||
let mut scaler = context
|
||||
.builder(font.as_swash())
|
||||
.size(f32::from_bits(cache_key.font_size_bits))
|
||||
.hint(true)
|
||||
.build();
|
||||
|
||||
// Compute the fractional offset-- you'll likely want to quantize this
|
||||
// in a real renderer
|
||||
let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float());
|
||||
|
||||
// Select our source order
|
||||
Render::new(&[
|
||||
// Color outline with the first palette
|
||||
Source::ColorOutline(0),
|
||||
// Color bitmap with best fit selection mode
|
||||
Source::ColorBitmap(StrikeWith::BestFit),
|
||||
// Standard scalable outline
|
||||
Source::Outline,
|
||||
])
|
||||
// Select a subpixel format
|
||||
.format(Format::Alpha)
|
||||
// Apply the fractional offset
|
||||
.offset(offset)
|
||||
.transform(if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
|
||||
Some(Transform::skew(
|
||||
Angle::from_degrees(14.0),
|
||||
Angle::from_degrees(0.0),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
// Render the image
|
||||
.render(&mut scaler, cache_key.glyph_id)
|
||||
}
|
||||
|
||||
fn swash_outline_commands(
|
||||
font_system: &mut FontSystem,
|
||||
context: &mut ScaleContext,
|
||||
cache_key: CacheKey,
|
||||
) -> Option<Box<[swash::zeno::Command]>> {
|
||||
use swash::zeno::PathData as _;
|
||||
|
||||
let font = match font_system.get_font(cache_key.font_id) {
|
||||
Some(some) => some,
|
||||
None => {
|
||||
log::warn!("did not find font {:?}", cache_key.font_id);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Build the scaler
|
||||
let mut scaler = context
|
||||
.builder(font.as_swash())
|
||||
.size(f32::from_bits(cache_key.font_size_bits))
|
||||
.hint(true)
|
||||
.build();
|
||||
|
||||
// Scale the outline
|
||||
let outline = scaler
|
||||
.scale_outline(cache_key.glyph_id)
|
||||
.or_else(|| scaler.scale_color_outline(cache_key.glyph_id))?;
|
||||
|
||||
// Get the path information of the outline
|
||||
let path = outline.path();
|
||||
|
||||
// Return the commands
|
||||
Some(path.commands().collect())
|
||||
}
|
||||
|
||||
/// Cache for rasterizing with the swash scaler
|
||||
pub struct SwashCache {
|
||||
context: ScaleContext,
|
||||
pub image_cache: HashMap<CacheKey, Option<SwashImage>>,
|
||||
pub outline_command_cache: HashMap<CacheKey, Option<Box<[swash::zeno::Command]>>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for SwashCache {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.pad("SwashCache { .. }")
|
||||
}
|
||||
}
|
||||
|
||||
impl SwashCache {
|
||||
/// Create a new swash cache
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
context: ScaleContext::new(),
|
||||
image_cache: HashMap::default(),
|
||||
outline_command_cache: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a swash Image from a cache key, without caching results
|
||||
pub fn get_image_uncached(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
cache_key: CacheKey,
|
||||
) -> Option<SwashImage> {
|
||||
swash_image(font_system, &mut self.context, cache_key)
|
||||
}
|
||||
|
||||
/// Create a swash Image from a cache key, caching results
|
||||
pub fn get_image(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
cache_key: CacheKey,
|
||||
) -> &Option<SwashImage> {
|
||||
self.image_cache
|
||||
.entry(cache_key)
|
||||
.or_insert_with(|| swash_image(font_system, &mut self.context, cache_key))
|
||||
}
|
||||
|
||||
/// Creates outline commands
|
||||
pub fn get_outline_commands(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
cache_key: CacheKey,
|
||||
) -> Option<&[swash::zeno::Command]> {
|
||||
self.outline_command_cache
|
||||
.entry(cache_key)
|
||||
.or_insert_with(|| swash_outline_commands(font_system, &mut self.context, cache_key))
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
/// Creates outline commands, without caching results
|
||||
pub fn get_outline_commands_uncached(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
cache_key: CacheKey,
|
||||
) -> Option<Box<[swash::zeno::Command]>> {
|
||||
swash_outline_commands(font_system, &mut self.context, cache_key)
|
||||
}
|
||||
|
||||
/// Enumerate pixels in an Image, use `with_image` for better performance
|
||||
pub fn with_pixels<F: FnMut(i32, i32, Color)>(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
cache_key: CacheKey,
|
||||
base: Color,
|
||||
mut f: F,
|
||||
) {
|
||||
if let Some(image) = self.get_image(font_system, cache_key) {
|
||||
let x = image.placement.left;
|
||||
let y = -image.placement.top;
|
||||
|
||||
match image.content {
|
||||
Content::Mask => {
|
||||
let mut i = 0;
|
||||
for off_y in 0..image.placement.height as i32 {
|
||||
for off_x in 0..image.placement.width as i32 {
|
||||
//TODO: blend base alpha?
|
||||
f(
|
||||
x + off_x,
|
||||
y + off_y,
|
||||
Color(((image.data[i] as u32) << 24) | base.0 & 0xFF_FF_FF),
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::Color => {
|
||||
let mut i = 0;
|
||||
for off_y in 0..image.placement.height as i32 {
|
||||
for off_x in 0..image.placement.width as i32 {
|
||||
//TODO: blend base alpha?
|
||||
f(
|
||||
x + off_x,
|
||||
y + off_y,
|
||||
Color::rgba(
|
||||
image.data[i],
|
||||
image.data[i + 1],
|
||||
image.data[i + 2],
|
||||
image.data[i + 3],
|
||||
),
|
||||
);
|
||||
i += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::SubpixelMask => {
|
||||
log::warn!("TODO: SubpixelMask");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user