Honor variable font instances

This commit is contained in:
SGavrl committed 2026-07-30 23:27:51 +00:00
1 parent f61493fbd1
commit 52d2d89cb6
41 files changed
+11484 -65

No files matched your search

+187
View File
@@ -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
View File
@@ -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"]
+201
View File
@@ -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.
+21
View File
@@ -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.
+94
View File
@@ -0,0 +1,94 @@
# COSMIC Text
[![crates.io](https://img.shields.io/crates/v/cosmic-text.svg)](https://crates.io/crates/cosmic-text)
[![docs.rs](https://docs.rs/cosmic-text/badge.svg)](https://docs.rs/cosmic-text)
![license](https://img.shields.io/crates/l/cosmic-text.svg)
[![Rust workflow](https://github.com/pop-os/cosmic-text/workflows/Rust/badge.svg?event=push)](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
[![Arabic screenshot](screenshots/arabic.png)](screenshots/arabic.png)
Hindi translation of Universal Declaration of Human Rights
[![Hindi screenshot](screenshots/hindi.png)](screenshots/hindi.png)
Simplified Chinese translation of Universal Declaration of Human Rights
[![Simplified Chinses screenshot](screenshots/chinese-simplified.png)](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
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+694
View File
@@ -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)));
}
}
+40
View File
@@ -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(&paragraph[0..i])
} else {
Some(paragraph)
}
}
}
+1485
View File
File diff suppressed because it is too large. Load diff
+310
View File
@@ -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
}
}
+81
View File
@@ -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);
}
}
+156
View File
@@ -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
View File
@@ -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,
);
}
}
+366
View File
@@ -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
View File
@@ -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);
}
}
+1173
View File
File diff suppressed because it is too large. Load diff
+110
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"],
_ => &[],
}
}
+218
View File
@@ -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
View File
@@ -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
View File
@@ -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));
}
+148
View File
@@ -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"),
}
}
}
+150
View File
@@ -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
View File
@@ -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)));
}
+20
View File
@@ -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()
}
+1953
View File
File diff suppressed because it is too large. Load diff
+49
View File
@@ -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()
}
}
+214
View File
@@ -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");
}
}
}
}
}