Skip to content

Commit e8099c0

Browse files
hanslerJohn Hansler
andauthored
Add support for OpenType features in text (e.g. ligatures, smallcaps) (#19020)
# 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: - 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 builder methods compared to cosmic-text's. - 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 mirrors what cosmic-text does as well. I added some consts for commonly used features. ## Testing I extended the "text" example. Run: `cargo run --example text` --- ## Showcase Screenshot: ![opentype_features](https://github.com/user-attachments/assets/08167404-e7c1-4a9f-b21c-d3370d7e4924) --------- Co-authored-by: John Hansler <[email protected]>
1 parent 18a7c30 commit e8099c0

File tree

8 files changed

+390
-1
lines changed

8 files changed

+390
-1
lines changed

assets/fonts/EBGaramond-LICENSE

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
Copyright (c) 2010-2013 Georg Duffner (http://www.georgduffner.at)
2+
3+
All "EB Garamond" Font Software is licensed under the SIL Open Font License, Version 1.1.
4+
This license is copied below, and is also available with a FAQ at:
5+
http://scripts.sil.org/OFL
6+
7+
8+
-----------------------------------------------------------
9+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10+
-----------------------------------------------------------
11+
12+
PREAMBLE
13+
The goals of the Open Font License (OFL) are to stimulate worldwide
14+
development of collaborative font projects, to support the font creation
15+
efforts of academic and linguistic communities, and to provide a free and
16+
open framework in which fonts may be shared and improved in partnership
17+
with others.
18+
19+
The OFL allows the licensed fonts to be used, studied, modified and
20+
redistributed freely as long as they are not sold by themselves. The
21+
fonts, including any derivative works, can be bundled, embedded,
22+
redistributed and/or sold with any software provided that any reserved
23+
names are not used by derivative works. The fonts and derivatives,
24+
however, cannot be released under any other type of license. The
25+
requirement for fonts to remain under this license does not apply
26+
to any document created using the fonts or their derivatives.
27+
28+
DEFINITIONS
29+
"Font Software" refers to the set of files released by the Copyright
30+
Holder(s) under this license and clearly marked as such. This may
31+
include source files, build scripts and documentation.
32+
33+
"Reserved Font Name" refers to any names specified as such after the
34+
copyright statement(s).
35+
36+
"Original Version" refers to the collection of Font Software components as
37+
distributed by the Copyright Holder(s).
38+
39+
"Modified Version" refers to any derivative made by adding to, deleting,
40+
or substituting -- in part or in whole -- any of the components of the
41+
Original Version, by changing formats or by porting the Font Software to a
42+
new environment.
43+
44+
"Author" refers to any designer, engineer, programmer, technical
45+
writer or other person who contributed to the Font Software.
46+
47+
PERMISSION & CONDITIONS
48+
Permission is hereby granted, free of charge, to any person obtaining
49+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50+
redistribute, and sell modified and unmodified copies of the Font
51+
Software, subject to the following conditions:
52+
53+
1) Neither the Font Software nor any of its individual components,
54+
in Original or Modified Versions, may be sold by itself.
55+
56+
2) Original or Modified Versions of the Font Software may be bundled,
57+
redistributed and/or sold with any software, provided that each copy
58+
contains the above copyright notice and this license. These can be
59+
included either as stand-alone text files, human-readable headers or
60+
in the appropriate machine-readable metadata fields within text or
61+
binary files as long as those fields can be easily viewed by the user.
62+
63+
3) No Modified Version of the Font Software may use the Reserved Font
64+
Name(s) unless explicit written permission is granted by the corresponding
65+
Copyright Holder. This restriction only applies to the primary font name as
66+
presented to the users.
67+
68+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69+
Software shall not be used to promote, endorse or advertise any
70+
Modified Version, except to acknowledge the contribution(s) of the
71+
Copyright Holder(s) and the Author(s) or with their explicit written
72+
permission.
73+
74+
5) The Font Software, modified or unmodified, in part or in whole,
75+
must be distributed entirely under this license, and must not be
76+
distributed under any other license. The requirement for fonts to
77+
remain under this license does not apply to any document created
78+
using the Font Software.
79+
80+
TERMINATION
81+
This license becomes null and void if any of the above conditions are
82+
not met.
83+
84+
DISCLAIMER
85+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93+
OTHER DEALINGS IN THE FONT SOFTWARE.
484 KB
Binary file not shown.

crates/bevy_text/src/pipeline.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ fn get_attrs<'a>(
667667
}
668668
.scale(scale_factor as f32),
669669
)
670+
.font_features((&text_font.font_features).into())
670671
.color(cosmic_text::Color(color.to_linear().as_u32()))
671672
}
672673

crates/bevy_text/src/text.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use bevy_derive::{Deref, DerefMut};
55
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
66
use bevy_reflect::prelude::*;
77
use bevy_utils::{default, once};
8+
use core::fmt::{Debug, Formatter};
9+
use core::str::from_utf8;
810
use cosmic_text::{Buffer, Metrics};
911
use serde::{Deserialize, Serialize};
1012
use smallvec::SmallVec;
@@ -266,6 +268,8 @@ pub struct TextFont {
266268
pub font_size: f32,
267269
/// The antialiasing method to use when rendering text.
268270
pub font_smoothing: FontSmoothing,
271+
/// OpenType features for .otf fonts that support them.
272+
pub font_features: FontFeatures,
269273
}
270274

271275
impl TextFont {
@@ -304,11 +308,197 @@ impl Default for TextFont {
304308
Self {
305309
font: Default::default(),
306310
font_size: 20.0,
311+
font_features: FontFeatures::default(),
307312
font_smoothing: Default::default(),
308313
}
309314
}
310315
}
311316

317+
/// An OpenType font feature tag.
318+
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)]
319+
pub struct FontFeatureTag([u8; 4]);
320+
321+
impl FontFeatureTag {
322+
/// Replaces character combinations like fi, fl with ligatures.
323+
pub const STANDARD_LIGATURES: FontFeatureTag = FontFeatureTag::new(b"liga");
324+
325+
/// Enables ligatures based on character context.
326+
pub const CONTEXTUAL_LIGATURES: FontFeatureTag = FontFeatureTag::new(b"clig");
327+
328+
/// Enables optional ligatures for stylistic use (e.g., ct, st).
329+
pub const DISCRETIONARY_LIGATURES: FontFeatureTag = FontFeatureTag::new(b"dlig");
330+
331+
/// Adjust glyph shapes based on surrounding letters.
332+
pub const CONTEXTUAL_ALTERNATES: FontFeatureTag = FontFeatureTag::new(b"calt");
333+
334+
/// Use alternate glyph designs.
335+
pub const STYLISTIC_ALTERNATES: FontFeatureTag = FontFeatureTag::new(b"salt");
336+
337+
/// Replaces lowercase letters with small caps.
338+
pub const SMALL_CAPS: FontFeatureTag = FontFeatureTag::new(b"smcp");
339+
340+
/// Replaces uppercase letters with small caps.
341+
pub const CAPS_TO_SMALL_CAPS: FontFeatureTag = FontFeatureTag::new(b"c2sc");
342+
343+
/// Replaces characters with swash versions (often decorative).
344+
pub const SWASH: FontFeatureTag = FontFeatureTag::new(b"swsh");
345+
346+
/// Enables alternate glyphs for large sizes or titles.
347+
pub const TITLING_ALTERNATES: FontFeatureTag = FontFeatureTag::new(b"titl");
348+
349+
/// Converts numbers like 1/2 into true fractions (½).
350+
pub const FRACTIONS: FontFeatureTag = FontFeatureTag::new(b"frac");
351+
352+
/// Formats characters like 1st, 2nd properly.
353+
pub const ORDINALS: FontFeatureTag = FontFeatureTag::new(b"ordn");
354+
355+
/// Uses a slashed version of zero (0) to differentiate from O.
356+
pub const SLASHED_ZERO: FontFeatureTag = FontFeatureTag::new(b"ordn");
357+
358+
/// Replaces figures with superscript figures, e.g. for indicating footnotes.
359+
pub const SUPERSCRIPT: FontFeatureTag = FontFeatureTag::new(b"sups");
360+
361+
/// Replaces figures with subscript figures.
362+
pub const SUBSCRIPT: FontFeatureTag = FontFeatureTag::new(b"subs");
363+
364+
/// Changes numbers to "oldstyle" form, which fit better in the flow of sentences or other text.
365+
pub const OLDSTYLE_FIGURES: FontFeatureTag = FontFeatureTag::new(b"onum");
366+
367+
/// Changes numbers to "lining" form, which are better suited for standalone numbers. When
368+
/// enabled, the bottom of all numbers will be aligned with each other.
369+
pub const LINING_FIGURES: FontFeatureTag = FontFeatureTag::new(b"lnum");
370+
371+
/// Changes numbers to be of proportional width. When enabled, numbers may have varying widths.
372+
pub const PROPORTIONAL_FIGURES: FontFeatureTag = FontFeatureTag::new(b"pnum");
373+
374+
/// Changes numbers to be of uniform (tabular) width. When enabled, all numbers will have the
375+
/// same width.
376+
pub const TABULAR_FIGURES: FontFeatureTag = FontFeatureTag::new(b"tnum");
377+
378+
/// Varies the stroke thickness. Values must be in the range of 0 to 1000.
379+
pub const WEIGHT: FontFeatureTag = FontFeatureTag::new(b"wght");
380+
381+
/// Varies the width of text from narrower to wider. Must be a value greater than 0. A value of
382+
/// 100 is typically considered standard width.
383+
pub const WIDTH: FontFeatureTag = FontFeatureTag::new(b"wdth");
384+
385+
/// Varies between upright and slanted text. Must be a value greater than -90 and less than +90.
386+
/// A value of 0 is upright.
387+
pub const SLANT: FontFeatureTag = FontFeatureTag::new(b"slnt");
388+
389+
/// Create a new [`FontFeatureTag`] from raw bytes.
390+
pub const fn new(src: &[u8; 4]) -> Self {
391+
Self(*src)
392+
}
393+
}
394+
395+
impl Debug for FontFeatureTag {
396+
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
397+
// OpenType tags are always ASCII, so this match will succeed for valid tags. This gives us
398+
// human-readable debug output, e.g. FontFeatureTag("liga").
399+
match from_utf8(&self.0) {
400+
Ok(s) => write!(f, "FontFeatureTag(\"{}\")", s),
401+
Err(_) => write!(f, "FontFeatureTag({:?})", self.0),
402+
}
403+
}
404+
}
405+
406+
/// OpenType features for .otf fonts that support them.
407+
///
408+
/// Examples features include ligatures, small-caps, and fractional number display. For the complete
409+
/// list of OpenType features, see the spec at
410+
/// `<https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist>`.
411+
///
412+
/// # Usage:
413+
/// ```
414+
/// use bevy_text::{FontFeatureTag, FontFeatures};
415+
///
416+
/// // Create using the builder
417+
/// let font_features = FontFeatures::builder()
418+
/// .enable(FontFeatureTag::STANDARD_LIGATURES)
419+
/// .set(FontFeatureTag::WEIGHT, 300)
420+
/// .build();
421+
///
422+
/// // Create from a list
423+
/// let more_font_features: FontFeatures = [
424+
/// FontFeatureTag::STANDARD_LIGATURES,
425+
/// FontFeatureTag::OLDSTYLE_FIGURES,
426+
/// FontFeatureTag::TABULAR_FIGURES
427+
/// ].into();
428+
/// ```
429+
#[derive(Clone, Debug, Default, Reflect, PartialEq)]
430+
pub struct FontFeatures {
431+
features: Vec<(FontFeatureTag, u32)>,
432+
}
433+
434+
impl FontFeatures {
435+
/// Create a new [`FontFeaturesBuilder`].
436+
pub fn builder() -> FontFeaturesBuilder {
437+
FontFeaturesBuilder::default()
438+
}
439+
}
440+
441+
/// A builder for [`FontFeatures`].
442+
#[derive(Clone, Default)]
443+
pub struct FontFeaturesBuilder {
444+
features: Vec<(FontFeatureTag, u32)>,
445+
}
446+
447+
impl FontFeaturesBuilder {
448+
/// Enable an OpenType feature.
449+
///
450+
/// Most OpenType features are on/off switches, so this is a convenience method that sets the
451+
/// feature's value to "1" (enabled). For non-boolean features, see [`FontFeaturesBuilder::set`].
452+
pub fn enable(self, feature_tag: FontFeatureTag) -> Self {
453+
self.set(feature_tag, 1)
454+
}
455+
456+
/// Set an OpenType feature to a specific value.
457+
///
458+
/// For most features, the [`FontFeaturesBuilder::enable`] method should be used instead. A few
459+
/// features, such as "wght", take numeric values, so this method may be used for these cases.
460+
pub fn set(mut self, feature_tag: FontFeatureTag, value: u32) -> Self {
461+
self.features.push((feature_tag, value));
462+
self
463+
}
464+
465+
/// Build a [`FontFeatures`] from the values set within this builder.
466+
pub fn build(self) -> FontFeatures {
467+
FontFeatures {
468+
features: self.features,
469+
}
470+
}
471+
}
472+
473+
/// Allow [`FontFeatures`] to be built from a list. This is suitable for the standard case when each
474+
/// listed feature is a boolean type. If any features require a numeric value (like "wght"), use
475+
/// [`FontFeaturesBuilder`] instead.
476+
impl<T> From<T> for FontFeatures
477+
where
478+
T: IntoIterator<Item = FontFeatureTag>,
479+
{
480+
fn from(value: T) -> Self {
481+
FontFeatures {
482+
features: value.into_iter().map(|x| (x, 1)).collect(),
483+
}
484+
}
485+
}
486+
487+
impl From<&FontFeatures> for cosmic_text::FontFeatures {
488+
fn from(font_features: &FontFeatures) -> Self {
489+
cosmic_text::FontFeatures {
490+
features: font_features
491+
.features
492+
.iter()
493+
.map(|(tag, value)| cosmic_text::Feature {
494+
tag: cosmic_text::FeatureTag::new(&tag.0),
495+
value: *value,
496+
})
497+
.collect(),
498+
}
499+
}
500+
}
501+
312502
/// Specifies the height of each line of text for `Text` and `Text2d`
313503
///
314504
/// Default is 1.2x the font size

examples/dev_tools/fps_overlay.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fn main() {
2626
font: default(),
2727
// We could also disable font smoothing,
2828
font_smoothing: FontSmoothing::default(),
29+
..default()
2930
},
3031
// We can also change color of the overlay
3132
text_color: OverlayColor::GREEN,

0 commit comments

Comments
 (0)