Add FontFeatureBuilder and add release notes

- 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.
This commit is contained in:
John Hansler 2025-05-05 12:22:19 -07:00
parent 96c02a161b
commit e052901e43
3 changed files with 185 additions and 33 deletions

View File

@ -8,7 +8,6 @@ use bevy_asset::Handle;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
use bevy_platform::collections::HashMap;
use bevy_reflect::prelude::*;
use bevy_utils::once;
use cosmic_text::{Buffer, Metrics};
@ -362,51 +361,150 @@ impl Default for TextFont {
/// OpenType features for .otf fonts that support them.
///
/// Examples features include:
/// "liga": Standard ligatures
/// "clig": Contextual ligatures
/// "dlig": Discretionary ligatures
/// "smcp": Small-caps
/// "ss01", "ss02", ..., "ss20": Stylistic alternates
/// "frac": Fractions, formatting numbers like 1/2
/// "ordn": Ordinals, formatting characters like "1st" or "2nd" properly
/// For the complete list of OpenType features, see the spec at
/// Examples features include ligatures, small-caps, and fractional number display. For the complete
/// list of OpenType features, see the spec at
/// `<https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist>`.
///
/// The above font features can be enabled via [`FontFeatures::enable`]. Some font features take
/// numeric values, rather than being on/off switches. For example, for weights of variable
/// fonts, the font feature "wght" can be any value between 100 and 900, with lower values
/// corresponding to lighter weights and higher values corresponding to heavier weights. For
/// these features, use [`FontFeatures::set`] to assign a specific value.
/// # Usage:
/// ```
/// use bevy_text::{FontFeatures, FontFeaturesBuilder};
///
/// // Create using FontFeaturesBuilder
/// let font_features = FontFeaturesBuilder::new()
/// .enable(FontFeatures::STANDARD_LIGATURES)
/// .set(FontFeatures::WEIGHT, 300)
/// .build();
///
/// // Create from a list
/// let more_font_features: FontFeatures = [
/// FontFeatures::STANDARD_LIGATURES,
/// FontFeatures::OLDSTYLE_FIGURES,
/// FontFeatures::TABULAR_FIGURES
/// ].into();
/// ```
#[derive(Clone, Debug, Default, Reflect)]
pub struct FontFeatures {
features: HashMap<[u8; 4], u32>,
features: Vec<([u8; 4], u32)>,
}
impl FontFeatures {
/// Create a new [`FontFeatures`].
/// Replaces character combinations like fi, fl with ligatures.
pub const STANDARD_LIGATURES: [u8; 4] = *b"liga";
/// Enables ligatures based on character context.
pub const CONTEXTUAL_LIGATURES: [u8; 4] = *b"clig";
/// Enables optional ligatures for stylistic use (e.g., ct, st).
pub const DISCRETIONARY_LIGATURES: [u8; 4] = *b"dlig";
/// Adjust glyph shapes based on surrounding letters.
pub const CONTEXTUAL_ALTERNATES: [u8; 4] = *b"calt";
/// Use alternate glyph designs.
pub const STYLISTIC_ALTERNATES: [u8; 4] = *b"salt";
/// Replaces lowercase letters with small caps.
pub const SMALL_CAPS: [u8; 4] = *b"smcp";
/// Replaces uppercase letters with small caps.
pub const CAPS_TO_SMALL_CAPS: [u8; 4] = *b"c2sc";
/// Replaces characters with swash versions (often decorative).
pub const SWASH: [u8; 4] = *b"swsh";
/// Enables alternate glyphs for large sizes or titles.
pub const TITLING_ALTERNATES: [u8; 4] = *b"titl";
/// Converts numbers like 1/2 into true fractions (½).
pub const FRACTIONS: [u8; 4] = *b"frac";
/// Formats characters like 1st, 2nd properly.
pub const ORDINALS: [u8; 4] = *b"ordn";
/// Uses a slashed version of zero (0) to differentiate from O.
pub const SLASHED_ZERO: [u8; 4] = *b"ordn";
/// Replaces figures with superscript figures, e.g. for indicating footnotes.
pub const SUPERSCRIPT: [u8; 4] = *b"sups";
/// Replaces figures with subscript figures.
pub const SUBSCRIPT: [u8; 4] = *b"subs";
/// Changes numbers to "oldstyle" form, which fit better in the flow of sentences or other text.
pub const OLDSTYLE_FIGURES: [u8; 4] = *b"onum";
/// Changes numbers to "lining" form, which are better suited for standalone numbers. When
/// enabled, the bottom of all numbers will be aligned with each other.
pub const LINING_FIGURES: [u8; 4] = *b"lnum";
/// Changes numbers to be of proportional width. When enabled, numbers may have varying widths.
pub const PROPORTIONAL_FIGURES: [u8; 4] = *b"pnum";
/// Changes numbers to be of uniform (tabular) width. When enabled, all numbers will have the
/// same width.
pub const TABULAR_FIGURES: [u8; 4] = *b"tnum";
/// Varies the stroke thickness. Values must be in the range of 0 to 1000.
pub const WEIGHT: [u8; 4] = *b"wght";
/// Varies the width of text from narrower to wider. Must be a value greater than 0. A value of
/// 100 is typically considered standard width.
pub const WIDTH: [u8; 4] = *b"wdth";
/// Varies between upright and slanted text. Must be a value greater than -90 and less than +90.
/// A value of 0 is upright.
pub const SLANT: [u8; 4] = *b"slnt";
}
/// A builder for [`FontFeatures`].
#[derive(Clone, Default)]
pub struct FontFeaturesBuilder {
features: Vec<([u8; 4], u32)>,
}
impl FontFeaturesBuilder {
/// Create a new [`FontFeaturesBuilder`].
pub fn new() -> Self {
FontFeatures {
features: HashMap::new(),
}
FontFeaturesBuilder::default()
}
/// Enable an OpenType feature.
///
/// Most OpenType features are on/off switches, so this is a convenience method that sets the
/// feature's value to "1" (enabled). For non-boolean features, see [`FontFeatures::set`].
pub fn enable(self, feature: &[u8; 4]) -> Self {
/// feature's value to "1" (enabled). For non-boolean features, see [`FontFeaturesBuilder::set`].
pub fn enable(self, feature: [u8; 4]) -> Self {
self.set(feature, 1)
}
/// Set an OpenType feature to a specific value.
///
/// For most features, the [`FontFeatures::enable`] method should be used instead. A few
/// For most features, the [`FontFeaturesBuilder::enable`] method should be used instead. A few
/// features, such as "wght", take numeric values, so this method may be used for these cases.
pub fn set(mut self, feature: &[u8; 4], value: u32) -> Self {
self.features.insert(*feature, value);
pub fn set(mut self, feature: [u8; 4], value: u32) -> Self {
self.features.push((feature, value));
self
}
/// Build a [`FontFeatures`] from the values set within this builder.
pub fn build(self) -> FontFeatures {
FontFeatures {
features: self.features,
}
}
}
/// Allow [`FontFeatures`] to be built from a list. This is suitable for the standard case when each
/// listed feature is a boolean type. If any features require a numeric value (like "wght"), use
/// [`FontFeaturesBuilder`] instead.
impl<T> From<T> for FontFeatures
where
T: IntoIterator<Item = [u8; 4]>,
{
fn from(value: T) -> Self {
FontFeatures {
features: value.into_iter().map(|x| (x, 1)).collect(),
}
}
}
impl From<&FontFeatures> for cosmic_text::FontFeatures {

View File

@ -3,6 +3,7 @@
//! It displays the current FPS in the top left corner, as well as text that changes color
//! in the bottom right. For text within a scene, please see the text2d example.
use bevy::text::FontFeaturesBuilder;
use bevy::{
color::palettes::css::GOLD,
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
@ -109,13 +110,25 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
))
.with_children(|parent| {
let text_rows = [
("Smallcaps: ", b"smcp", "Hello World"),
("Ligatures: ", b"liga", "fi fl ff ffi ffl"),
("Fractions: ", b"frac", "12/134"),
("Superscript: ", b"sups", "Up here!"),
("Subscript: ", b"subs", "Down here!"),
("Old-style figures: ", b"onum", "1234567890"),
("Lining figures: ", b"lnum", "1234567890"),
("Smallcaps: ", FontFeatures::SMALL_CAPS, "Hello World"),
(
"Ligatures: ",
FontFeatures::STANDARD_LIGATURES,
"fi fl ff ffi ffl",
),
("Fractions: ", FontFeatures::FRACTIONS, "12/134"),
("Superscript: ", FontFeatures::SUPERSCRIPT, "Up here!"),
("Subscript: ", FontFeatures::SUBSCRIPT, "Down here!"),
(
"Oldstyle figures: ",
FontFeatures::OLDSTYLE_FIGURES,
"1234567890",
),
(
"Lining figures: ",
FontFeatures::LINING_FIGURES,
"1234567890",
),
];
for (title, feature, text) in text_rows {
@ -132,7 +145,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
TextFont {
font: opentype_font_handle.clone(),
font_size: 24.0,
font_features: FontFeatures::new().enable(feature),
font_features: FontFeaturesBuilder::new().enable(feature).build(),
..default()
},
));

View File

@ -0,0 +1,41 @@
---
title: OpenType Font Features
authors: ["@hansler"]
pull_requests: [19020]
---
OpenType font features allow fine-grained control over how text is displayed, including [ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)), [small caps](https://en.wikipedia.org/wiki/Small_caps), and [many more](https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist).
These features can now be used in Bevy, allowing users to add typographic polish (like discretionary ligatures and oldstyle numerals) to their UI. It also allows complex scripts like Arabic or Devanagari to render more correctly with their intended ligatures.
Example usage:
```rust
commands.spawn((
TextSpan::new("Ligatures: ff, fi, fl, ffi, ffl"),
TextFont {
font: opentype_font_handle,
font_features: FontFeaturesBuilder::new()
.enable(FontFeatures::STANDARD_LIGATURES)
.set(FontFeatures::WIDTH, 300)
.build(),
..default()
},
));
```
FontFeatures can also be constructed from a list:
```rust
TextFont {
font: opentype_font_handle,
font_features: [
FontFeatures::STANDARD_LIGATURES,
FontFeatures::STYLISTIC_ALTERNATES,
FontFeatures::SLASHED_ZERO
].into(),
..default()
}
```
Note that OpenType font features are only available for `.otf` fonts that support them, and different fonts may support
different subsets of OpenType features.