diff --git a/Cargo.toml b/Cargo.toml index ce3b87468e628..8873a239f35c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3480,6 +3480,17 @@ description = "Demonstrates how Display and Visibility work in the UI." category = "UI (User Interface)" wasm = true +[[example]] +name = "font_weights" +path = "examples/ui/font_weights.rs" +doc-scrape-examples = true + +[package.metadata.example.font_weights] +name = "Font Weights" +description = "Demonstrates how to use font weights." +category = "UI (User Interface)" +wasm = true + [[example]] name = "window_fallthrough" path = "examples/ui/window_fallthrough.rs" diff --git a/assets/fonts/MonaSans-VariableFont.ttf b/assets/fonts/MonaSans-VariableFont.ttf new file mode 100644 index 0000000000000..a49f373ecfd8a Binary files /dev/null and b/assets/fonts/MonaSans-VariableFont.ttf differ diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000000000..ccc02aabb5b4e --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Mona Sans Project Authors (https://github.com/github/mona-sans), with Reserved Font Name "Mona" + +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: +https://openfontlicense.org + + +----------------------------------------------------------- +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. diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 2beb5b1897034..592489e15220a 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -59,8 +59,8 @@ pub use text_access::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, Justify, LineBreak, Strikethrough, StrikethroughColor, TextColor, TextError, - TextFont, TextLayout, TextSpan, Underline, UnderlineColor, + Font, FontWeight, Justify, LineBreak, Strikethrough, StrikethroughColor, TextColor, + TextError, TextFont, TextLayout, TextSpan, Underline, UnderlineColor, }; } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 40a428510dfa8..e716d552345b1 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -62,8 +62,6 @@ pub struct FontFaceInfo { pub stretch: cosmic_text::fontdb::Stretch, /// Allows italic or oblique faces to be selected pub style: cosmic_text::fontdb::Style, - /// The degree of blackness or stroke thickness - pub weight: cosmic_text::fontdb::Weight, /// Font family name pub family_name: Arc, } @@ -86,7 +84,7 @@ pub struct TextPipeline { LineHeight, )>, /// Buffered vec for collecting info for glyph assembly. - glyph_info: Vec<(AssetId, FontSmoothing, f32, f32, f32, f32)>, + glyph_info: Vec<(AssetId, FontSmoothing, f32, f32, f32, f32, u16)>, } impl TextPipeline { @@ -270,6 +268,7 @@ impl TextPipeline { 0., 0., 0., + text_font.weight.clamp().0, )); }); @@ -288,18 +287,13 @@ impl TextPipeline { update_result?; - for (font, _, size, strikethrough_offset, stroke, underline_offset) in + for (font, _, size, strikethrough_offset, stroke, underline_offset, weight) in self.glyph_info.iter_mut() { let Some((id, _)) = self.map_handle_to_font_id.get(font) else { continue; }; - let weight = font_system - .db() - .face(*id) - .map(|f| f.weight) - .unwrap_or(cosmic_text::Weight::NORMAL); - if let Some(font) = font_system.get_font(*id, weight) { + if let Some(font) = font_system.get_font(*id, cosmic_text::Weight(*weight)) { let swash = font.as_swash(); let metrics = swash.metrics(&[]); let upem = metrics.units_per_em as f32; @@ -528,23 +522,19 @@ impl TextPipeline { 0.0, 0.0, 0.0, + text_font.weight.clamp().0, ); - if let Some((id, _)) = self.map_handle_to_font_id.get(§ion_info.0) { - let weight = font_system - .db() - .face(*id) - .map(|f| f.weight) - .unwrap_or(cosmic_text::Weight::NORMAL); - if let Some(font) = font_system.get_font(*id, weight) { - let swash = font.as_swash(); - let metrics = swash.metrics(&[]); - let upem = metrics.units_per_em as f32; - let scalar = section_info.2 * scale_factor as f32 / upem; - section_info.3 = (metrics.strikeout_offset * scalar).round(); - section_info.4 = (metrics.stroke_size * scalar).round().max(1.); - section_info.5 = (metrics.underline_offset * scalar).round(); - } + if let Some((id, _)) = self.map_handle_to_font_id.get(§ion_info.0) + && let Some(font) = font_system.get_font(*id, cosmic_text::Weight(section_info.6)) + { + let swash = font.as_swash(); + let metrics = swash.metrics(&[]); + let upem = metrics.units_per_em as f32; + let scalar = section_info.2 * scale_factor as f32 / upem; + section_info.3 = (metrics.strikeout_offset * scalar).round(); + section_info.4 = (metrics.stroke_size * scalar).round().max(1.); + section_info.5 = (metrics.underline_offset * scalar).round(); } self.glyph_info.push(section_info); } @@ -681,7 +671,7 @@ impl TextPipeline { // Check result. result?; - layout_info.size = box_size; + layout_info.size = box_size.ceil(); Ok(()) } } @@ -824,7 +814,6 @@ pub fn load_font_to_fontdb( FontFaceInfo { stretch: face.stretch, style: face.style, - weight: face.weight, family_name: family_name.clone(), } } @@ -843,7 +832,7 @@ fn get_attrs<'a>( .family(Family::Name(&face_info.family_name)) .stretch(face_info.stretch) .style(face_info.style) - .weight(face_info.weight) + .weight(text_font.weight.into()) .metrics( Metrics { font_size: text_font.font_size, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 6b022487bca6e..7093e4b35ad3a 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -266,6 +266,10 @@ pub struct TextFont { /// A new font atlas is generated for every combination of font handle and scaled font size /// which can have a strong performance impact. pub font_size: f32, + /// How thick or bold the strokes of a font appear. + /// + /// Font weights can be any value between 1 and 1000, inclusive. + pub weight: FontWeight, /// The antialiasing method to use when rendering text. pub font_smoothing: FontSmoothing, /// OpenType features for .otf fonts that support them. @@ -308,12 +312,81 @@ impl Default for TextFont { Self { font: Default::default(), font_size: 20.0, + weight: FontWeight::NORMAL, font_features: FontFeatures::default(), font_smoothing: Default::default(), } } } +/// How thick or bold the strokes of a font appear. +/// +/// Valid font weights range from 1 to 1000, inclusive. +/// Weights above 1000 are clamped to 1000. +/// A weight of 0 is treated as [`FontWeight::DEFAULT`]. +/// +/// `` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)] +pub struct FontWeight(pub u16); + +impl FontWeight { + /// Weight 100. + pub const THIN: FontWeight = FontWeight(100); + + /// Weight 200. + pub const EXTRA_LIGHT: FontWeight = FontWeight(200); + + /// Weight 300. + pub const LIGHT: FontWeight = FontWeight(300); + + /// Weight 400. + pub const NORMAL: FontWeight = FontWeight(400); + + /// Weight 500. + pub const MEDIUM: FontWeight = FontWeight(500); + + /// Weight 600. + pub const SEMIBOLD: FontWeight = FontWeight(600); + + /// Weight 700. + pub const BOLD: FontWeight = FontWeight(700); + + /// Weight 800 + pub const EXTRA_BOLD: FontWeight = FontWeight(800); + + /// Weight 900. + pub const BLACK: FontWeight = FontWeight(900); + + /// Weight 950. + pub const EXTRA_BLACK: FontWeight = FontWeight(950); + + /// The default font weight. + pub const DEFAULT: FontWeight = Self::NORMAL; + + /// Clamp the weight value to between 1 and 1000. + /// Values of 0 are mapped to `Weight::DEFAULT`. + pub const fn clamp(mut self) -> Self { + if self.0 == 0 { + self = Self::DEFAULT; + } else if 1000 < self.0 { + self.0 = 1000; + } + Self(self.0) + } +} + +impl Default for FontWeight { + fn default() -> Self { + Self::DEFAULT + } +} + +impl From for cosmic_text::Weight { + fn from(value: FontWeight) -> Self { + cosmic_text::Weight(value.clamp().0) + } +} + /// An OpenType font feature tag. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)] pub struct FontFeatureTag([u8; 4]); diff --git a/examples/README.md b/examples/README.md index 324fac7163be3..c1c395590f79c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -567,6 +567,7 @@ Example | Description [Feathers Widgets](../examples/ui/feathers.rs) | Gallery of Feathers Widgets [Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text [Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) +[Font Weights](../examples/ui/font_weights.rs) | Demonstrates how to use font weights. [Ghost Nodes](../examples/ui/ghost_nodes.rs) | Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy [Gradients](../examples/ui/gradients.rs) | An example demonstrating gradients [Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior diff --git a/examples/ui/font_weights.rs b/examples/ui/font_weights.rs new file mode 100644 index 0000000000000..1391e404ebc39 --- /dev/null +++ b/examples/ui/font_weights.rs @@ -0,0 +1,128 @@ +//! This example demonstrates how to use font weights with text. + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + let font = asset_server.load("fonts/MonaSans-VariableFont.ttf"); + + 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 Weights"), + TextFont { + font: font.clone(), + font_size: 32.0, + ..default() + }, + Underline, + ), + ( + Node { + flex_direction: FlexDirection::Column, + padding: px(8.).all(), + row_gap: px(8.), + ..default() + }, + children![ + ( + Text::new("Weight 100 (Thin)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::THIN, // 100 + ..default() + }, + ), + ( + Text::new("Weight 200 (Extra Light)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::EXTRA_LIGHT, // 200 + ..default() + }, + ), + ( + Text::new("Weight 300 (Light)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::LIGHT, // 300 + ..default() + }, + ), + ( + Text::new("Weight 400 (Normal)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::NORMAL, // 400 + ..default() + }, + ), + ( + Text::new("Weight 500 (Medium)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::MEDIUM, // 500 + ..default() + }, + ), + ( + Text::new("Weight 600 (Semibold)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::SEMIBOLD, // 600 + ..default() + }, + ), + ( + Text::new("Weight 700 (Bold)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::BOLD, // 700 + ..default() + }, + ), + ( + Text::new("Weight 800 (Extra Bold)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::EXTRA_BOLD, // 800 + ..default() + }, + ), + ( + Text::new("Weight 900 (Black)"), + TextFont { + font: font.clone(), + font_size: 32.0, + weight: FontWeight::BLACK, // 900 + ..default() + }, + ), + ] + ), + ], + )); +} diff --git a/release-content/release-notes/font_weights.md b/release-content/release-notes/font_weights.md new file mode 100644 index 0000000000000..670ab18433b81 --- /dev/null +++ b/release-content/release-notes/font_weights.md @@ -0,0 +1,9 @@ +--- +title: "Font weight support" +authors: ["@ickshonpe"] +pull_requests: [22038] +--- + +Adds support for font weights. + +`TextFont` now has a `weight: FontWeight` field. `FontWeight` newtypes a `u16`, values inside the range 1 and 1000 are valid. Values outside the range are clamped.