diff --git a/Cargo.toml b/Cargo.toml index 0c160b3aea850..46742ee51acd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3808,6 +3808,17 @@ description = "Demonstrates how to use font weights." category = "UI (User Interface)" wasm = true +[[example]] +name = "font_variations" +path = "examples/ui/text/font_variations.rs" +doc-scrape-examples = true + +[package.metadata.example.font_variations] +name = "Font Variations" +description = "Demonstrates how to use OpenType font variations." +category = "UI (User Interface)" +wasm = true + [[example]] name = "window_fallthrough" path = "examples/ui/window_fallthrough.rs" diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index f62e44b9c3098..515c1d75042d8 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -224,6 +224,10 @@ impl TextPipeline { ); builder.push( StyleProperty::FontFeatures((§ion.text_font.font_features).into()), + range.clone(), + ); + builder.push( + StyleProperty::FontVariations((§ion.text_font.font_variations).into()), range, ); } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 206a20539e120..45d21dc461217 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -9,7 +9,7 @@ use bevy_utils::{default, once}; use core::fmt::{Debug, Formatter}; use core::str::from_utf8; use parley::setting::Tag; -use parley::{FontFeature, Layout}; +use parley::{FontFeature, FontVariation, Layout}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol_str::SmolStr; @@ -404,6 +404,8 @@ pub struct TextFont { pub font_smoothing: FontSmoothing, /// OpenType features for .otf fonts that support them. pub font_features: FontFeatures, + /// OpenType variations for variable fonts that support them. + pub font_variations: FontVariations, } impl TextFont { @@ -466,6 +468,7 @@ impl Default for TextFont { weight: FontWeight::NORMAL, width: FontWidth::NORMAL, font_features: FontFeatures::default(), + font_variations: FontVariations::default(), font_smoothing: Default::default(), } } @@ -915,6 +918,102 @@ impl From<&FontFeatures> for parley::style::FontFeatures<'static> { } } +/// An OpenType font variation tag. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)] +pub struct FontVariationTag([u8; 4]); + +impl FontVariationTag { + /// Varies the stroke thickness. The range is typically 1 to 1000. + pub const WEIGHT: FontVariationTag = FontVariationTag::new(b"wght"); + + /// Varies the width of glyphs from narrower to wider. The range is typically 50 to 200 with + /// 100 being standard width. + pub const WIDTH: FontVariationTag = FontVariationTag::new(b"wdth"); + + /// Varies between upright and slanted glyphs. The range is typically between -90 and +90 degrees, + /// where 0 is upright. + pub const SLANT: FontVariationTag = FontVariationTag::new(b"slnt"); + + /// Varies the design of glyphs for different optical sizes (physical font size). + /// The range is typically 6 to 72. + pub const OPTICAL_SIZE: FontVariationTag = FontVariationTag::new(b"opsz"); + + /// Create a new [`FontVariationTag`] from raw bytes. + pub const fn new(src: &[u8; 4]) -> Self { + Self(*src) + } +} + +impl Debug for FontVariationTag { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match from_utf8(&self.0) { + Ok(s) => write!(f, "FontVariationTag(\"{}\")", s), + Err(_) => write!(f, "FontVariationTag({:?})", self.0), + } + } +} + +/// OpenType font variations for variable fonts that support them. +/// +/// Variable fonts expose named axes (e.g. `wght`, `FILL`) that accept continuous `f32` values. +/// This is distinct from [`FontFeatures`], which mainly controls on/off OpenType layout features. +/// +/// # Usage +/// ``` +/// use bevy_text::{FontVariationTag, FontVariations}; +/// +/// let variations = FontVariations::builder() +/// .set(FontVariationTag::WEIGHT, 400.0) +/// .build(); +/// ``` +#[derive(Clone, Debug, Default, Reflect, PartialEq)] +pub struct FontVariations { + variations: Vec<(FontVariationTag, f32)>, +} + +impl FontVariations { + /// Create a new [`FontVariationsBuilder`]. + pub fn builder() -> FontVariationsBuilder { + FontVariationsBuilder::default() + } +} + +/// A builder for [`FontVariations`]. +#[derive(Clone, Default)] +pub struct FontVariationsBuilder { + variations: Vec<(FontVariationTag, f32)>, +} + +impl FontVariationsBuilder { + /// Set a font variation to a specific value. + pub fn set(mut self, tag: FontVariationTag, value: f32) -> Self { + self.variations.push((tag, value)); + self + } + + /// Build a [`FontVariations`] from the values set within this builder. + pub fn build(self) -> FontVariations { + FontVariations { + variations: self.variations, + } + } +} + +impl From<&FontVariations> for parley::style::FontVariations<'static> { + fn from(font_variations: &FontVariations) -> Self { + parley::style::FontVariations::List( + font_variations + .variations + .iter() + .map(|(tag, value)| FontVariation { + tag: Tag::new(&tag.0), + value: *value, + }) + .collect(), + ) + } +} + /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size diff --git a/examples/README.md b/examples/README.md index 0871132574246..8390a8051e570 100644 --- a/examples/README.md +++ b/examples/README.md @@ -604,6 +604,7 @@ Example | Description [Flex Layout](../examples/ui/layout/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text [Font Atlas Debug](../examples/ui/text/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) [Font Queries](../examples/ui/text/font_query.rs) | Demonstrates font querying +[Font Variations](../examples/ui/text/font_variations.rs) | Demonstrates how to use OpenType font variations. [Font Weights](../examples/ui/text/font_weights.rs) | Demonstrates how to use font weights. [Generic Font Families](../examples/ui/text/generic_font_families.rs) | Demonstrates how to use generic font families [Ghost Nodes](../examples/ui/layout/ghost_nodes.rs) | Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy diff --git a/examples/ui/text/font_variations.rs b/examples/ui/text/font_variations.rs new file mode 100644 index 0000000000000..3da5bab295c09 --- /dev/null +++ b/examples/ui/text/font_variations.rs @@ -0,0 +1,61 @@ +//! This example demonstrates how to use font variations to control variable font axes. + +use bevy::prelude::*; +use bevy::text::{FontVariationTag, FontVariations}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + let font: FontSource = asset_server.load("fonts/MonaSans-VariableFont.ttf").into(); + + commands.spawn(Camera2d); + + commands.spawn(( + Node { + flex_direction: FlexDirection::Column, + align_self: AlignSelf::Center, + justify_self: JustifySelf::Center, + align_items: AlignItems::Center, + ..default() + }, + children![ + ( + Text::new("Font Variations (wght axis)"), + TextFont { + font: font.clone(), + font_size: FontSize::Px(32.0), + ..default() + }, + Underline, + ), + ( + Node { + flex_direction: FlexDirection::Column, + padding: px(8.).all(), + row_gap: px(8.), + ..default() + }, + Children::spawn(SpawnIter( + [100, 200, 300, 400, 500, 600, 700, 800, 900] + .into_iter() + .map(move |weight| ( + Text(format!("wght {weight}")), + TextFont { + font: font.clone(), + font_size: FontSize::Px(32.0), + font_variations: FontVariations::builder() + .set(FontVariationTag::WEIGHT, weight as f32) + .build(), + ..default() + }, + )) + )), + ), + ], + )); +}