-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add support for OpenType features in text (e.g. ligatures, smallcaps) #19020
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
OpenType features are now supported in cosmic-text, so this provides a way to pass them through. A few notes: - I extended the existing "text" example to showcase a few different OpenType features. - OpenType features are only available for .otf fonts. Since there weren't any existing .otf fonts in the asset/ folder, I've added an SIL-licenced font so that we can showcase this in example code. - I added a "FontFeatures" struct. cosmic-text does already include its own FontFeatures struct, but 1) it does not implement Reflect, which is required by TextFont, and 2) the one I added has a couple ergonomics improvements for the public API. - OpenType font features are four characters strings, e.g. "liga". I considered representing these within an enum, but decided against this since there are hundreds of possible features, and more get added frequently, so this would require quite a bit of ongoing maintenance. Since these features are typically referred to by their four-letter name in documentation, I think the &[u8; 4] representation is appropriate, and this is what cosmic-text does as well.
Welcome, new contributor! Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨ |
It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note. Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes. |
features: HashMap<[u8; 4], u32>, | ||
} | ||
|
||
impl FontFeatures { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree that an enum with 100+ variants might be burdensome to maintain, but maybe some constants for commonly used features might be useful like:
impl FontFeatures { | |
impl FontFeatures { | |
pub const SUPERSCRIPT: [u8; 4] = b"sups"; |
and so on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, will do! I think that will be helpful. I'll update the PR on Monday.
crates/bevy_text/src/text.rs
Outdated
/// these features, use [`FontFeatures::set`] to assign a specific value. | ||
#[derive(Clone, Debug, Default, Reflect)] | ||
pub struct FontFeatures { | ||
features: HashMap<[u8; 4], u32>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about using a hashmap, maybe we should just use a vec like they do in cosmic text and rustybuzz. Maybe it doesn't matter though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I can switch this to a vec. My original thinking is that something like this would probably result in undefined behavior:
FontFeatures::new()
.set(b"wght", 200)
.set(b"wght", 600)
With a HashMap, we'd always respect the most recently set value. But it might be overkill to try to prevent that here, and instead should just defer to cosmic-text to decide what should happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a list that's applied in order, with duplicates overwriting the previous value so there's no problem, except the obvious worry that someone naively uses set
or enable
every frame leaking a huge list of features.
Maybe we could remove the enable
and set
functions and instead change it so that FontFeatures
has to be constructed from a builder, iterator or From
impl with the list of features being immutable after construction? Font features are almost always just set once at creation, with in-place mutation being relatively rare.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @ickshonpe! That makes sense. I just updated the PR, so now FontFeatures
must either be constructed from FontFeaturesBuilder
or an IntoIterator
From
impl.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the FontFeaturesBuilder
constructor should be on FontFeatures
for better discoverability, so instead of:
font_features: FontFeaturesBuilder::new().enable(SLASHED_ZERO).build()
you'd use:
font_features: FontFeatures::new().enable(SLASHED_ZERO).build()
Sorry I'm being way too fussy over this. Perhaps the hashmap was the right approach. If rust supported varidics this would be trivial hehe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate the fussiness! I myself was debating whether to instantiate the builder from the FontFeatures
impl or not. I just updated the PR to do this. One change: I named the method builder()
instead of new()
, because if I use new()
, I bump into a lint error that says "new() usually returns Self". So builder()
seems like the most common convention here.
- FontFeatures can now only be constructed via builder, "From" impl, or "Default" impl. - Switched from storing feature values in a HashMap to using a Vec instead. - Added consts for commonly-used OpenType features. - Added release notes.
Objective
OpenType features include things like smallcaps, lined vs old-style numbers, ligatures, stylistic alternate characters, fractional numbers (numerator placed above the denominator), forced monospacing for numbers, and more. There are >100 possible OpenType feature tags; see https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist for the up-to-date list. This provides a way for Bevy users to use these features when using .otf fonts that support them.
Solution
OpenType features are now supported in cosmic-text, so this just provides a way to pass them through. A few notes:
Testing
I extended the "text" example. Run:
cargo run --example text
Showcase
Screenshot:
