Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/bevy_text/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use cosmic_text::fontdb::ID;
use cosmic_text::skrifa::raw::ReadError;
use cosmic_text::skrifa::FontRef;
use smallvec::SmallVec;
use smol_str::SmolStr;

use crate::ComputedTextBlock;
use crate::CosmicFontSystem;
Expand All @@ -33,6 +34,9 @@ pub struct Font {
pub data: Arc<Vec<u8>>,
/// Ids for fonts in font file
pub ids: SmallVec<[ID; 8]>,
/// Font family name.
/// If the font file is a collection with multiple families, the first family name from the last font is used.
pub family_name: SmolStr,
}

impl Font {
Expand All @@ -42,6 +46,7 @@ impl Font {
Ok(Self {
data: Arc::new(font_data),
ids: SmallVec::new(),
family_name: SmolStr::default(),
})
}
}
Expand All @@ -65,6 +70,15 @@ pub fn load_font_assets_into_fontdb_system(
.load_font_source(cosmic_text::fontdb::Source::Binary(data))
.into_iter()
.collect();
// TODO: it is assumed this is the right font face
font.family_name = font_system
.db()
.face(*font.ids.last().unwrap())
.unwrap()
.families[0]
.0
.as_str()
.into();
new_fonts_added = true;
}
}
Expand Down
129 changes: 35 additions & 94 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use alloc::sync::Arc;

use bevy_asset::{AssetId, Assets};
use bevy_asset::Assets;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
Expand All @@ -10,7 +8,6 @@ use bevy_ecs::{
use bevy_image::prelude::*;
use bevy_log::{once, warn};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use smol_str::SmolStr;

Expand All @@ -19,7 +16,7 @@ use crate::{
FontAtlasKey, FontAtlasSet, FontHinting, FontSmoothing, FontSource, Justify, LineBreak,
LineHeight, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout,
};
use cosmic_text::{fontdb::ID, Attrs, Buffer, Family, Metrics, Shaping, Wrap};
use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};

/// A wrapper resource around a [`cosmic_text::FontSystem`]
///
Expand Down Expand Up @@ -64,18 +61,10 @@ pub struct FontFaceInfo {
/// See the [crate-level documentation](crate) for more information.
#[derive(Default, Resource)]
pub struct TextPipeline {
/// Identifies a font [`ID`] by its [`Font`] [`Asset`](bevy_asset::Asset).
pub map_handle_to_font_id: HashMap<AssetId<Font>, (ID, Arc<str>)>,
/// Buffered vec for collecting spans.
///
/// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10).
spans_buffer: Vec<(
usize,
&'static str,
&'static TextFont,
FontFaceInfo,
LineHeight,
)>,
spans_buffer: Vec<(&'static str, Attrs<'static>)>,
}

impl TextPipeline {
Expand All @@ -100,15 +89,10 @@ impl TextPipeline {

// Collect span information into a vec. This is necessary because font loading requires mut access
// to FontSystem, which the cosmic-text Buffer also needs.
let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color, LineHeight)> =
core::mem::take(&mut self.spans_buffer)
.into_iter()
.map(
|_| -> (usize, &str, &TextFont, FontFaceInfo, Color, LineHeight) {
unreachable!()
},
)
.collect();
let mut spans: Vec<(&str, Attrs)> = core::mem::take(&mut self.spans_buffer)
.into_iter()
.map(|_| -> (&str, Attrs) { unreachable!() })
.collect();

computed.entities.clear();

Expand All @@ -122,46 +106,28 @@ impl TextPipeline {
continue;
}

let family_name: SmolStr = match &text_font.font {
let family: Family = match &text_font.font {
FontSource::Handle(handle) => {
if let Some(font) = fonts.get(handle.id()) {
let data = Arc::clone(&font.data);
let ids = font_system
.db_mut()
.load_font_source(cosmic_text::fontdb::Source::Binary(data));

// TODO: it is assumed this is the right font face
font_system
.db()
.face(*ids.last().unwrap())
.unwrap()
.families[0]
.0
.as_str()
.into()
} else {
let Some(font) = fonts.get(handle.id()) else {
// Return early if a font is not loaded yet.
spans.clear();
self.spans_buffer = spans
.into_iter()
.map(
|_| -> (
usize,
&'static str,
&'static TextFont,
FontFaceInfo,
LineHeight,
) { unreachable!() },
)
.map(|_| -> (&'static str, Attrs<'static>) { unreachable!() })
.collect();
return Err(TextError::NoSuchFont);
}
};

Family::Name(font.family_name.as_str())
}
FontSource::Family(family) => family.clone(),
FontSource::Family(family) => Family::Name(family.as_str()),
FontSource::Serif => Family::Serif,
FontSource::SansSerif => Family::SansSerif,
FontSource::Cursive => Family::Cursive,
FontSource::Fantasy => Family::Fantasy,
FontSource::Monospace => Family::Monospace,
};

let face_info = FontFaceInfo { family_name };

// Save spans that aren't zero-sized.
if scale_factor <= 0.0 || text_font.font_size <= 0.0 {
once!(warn!(
Expand All @@ -170,30 +136,21 @@ impl TextPipeline {

continue;
}
spans.push((span_index, span, text_font, face_info, color, line_height));

let attrs = get_attrs(
span_index,
text_font,
line_height,
color,
family,
scale_factor,
);

// spans.push((span_index, span, text_font, face_info, color, line_height));
spans.push((span, attrs));
}

// Map text sections to cosmic-text spans, and ignore sections with negative or zero fontsizes,
// since they cannot be rendered by cosmic-text.
//
// The section index is stored in the metadata of the spans, and could be used
// to look up the section the span came from and is not used internally
// in cosmic-text.
let spans_iter = spans.iter().map(
|(span_index, span, text_font, font_info, color, line_height)| {
(
*span,
get_attrs(
*span_index,
text_font,
*line_height,
*color,
font_info,
scale_factor,
),
)
},
);
let spans_iter = spans.iter().map(|(span, attrs)| (*span, attrs.clone()));

// Update the buffer.
let buffer = &mut computed.buffer;
Expand Down Expand Up @@ -230,15 +187,7 @@ impl TextPipeline {
spans.clear();
self.spans_buffer = spans
.into_iter()
.map(
|_| -> (
usize,
&'static str,
&'static TextFont,
FontFaceInfo,
LineHeight,
) { unreachable!() },
)
.map(|_| -> (&'static str, Attrs<'static>) { unreachable!() })
.collect();

Ok(())
Expand Down Expand Up @@ -293,14 +242,6 @@ impl TextPipeline {
})
}

/// Returns the [`cosmic_text::fontdb::ID`] for a given [`Font`] asset.
pub fn get_font_id(&self, asset_id: AssetId<Font>) -> Option<ID> {
self.map_handle_to_font_id
.get(&asset_id)
.cloned()
.map(|(id, _)| id)
}

/// Update [`TextLayoutInfo`] with the new [`PositionedGlyph`] layout.
pub fn update_text_layout_info(
&mut self,
Expand Down Expand Up @@ -567,12 +508,12 @@ fn get_attrs<'a>(
text_font: &TextFont,
line_height: LineHeight,
color: Color,
face_info: &'a FontFaceInfo,
family: Family<'a>,
scale_factor: f64,
) -> Attrs<'a> {
Attrs::new()
.metadata(span_index)
.family(Family::Name(&face_info.family_name))
.family(family)
.stretch(text_font.width.into())
.style(text_font.style.into())
.weight(text_font.weight.into())
Expand Down
25 changes: 25 additions & 0 deletions crates/bevy_text/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,31 @@ pub enum FontSource {
Handle(Handle<Font>),
/// Resolve the font by family name using the font database.
Family(SmolStr),
/// Fonts with serifs — small decorative strokes at the ends of letterforms.
///
/// Serif fonts are typically used for long passages of text and represent
/// a more traditional or formal typographic style.
Serif,
/// Fonts without serifs.
///
/// Sans-serif fonts generally have low stroke contrast and plain stroke
/// endings, making them common for UI text and on-screen reading.
SansSerif,
/// Fonts that use a cursive or handwritten style.
///
/// Glyphs often resemble connected or flowing pen or brush strokes rather
/// than printed letterforms.
Cursive,
/// Decorative or expressive fonts.
///
/// Fantasy fonts are primarily intended for display purposes and may
/// prioritize visual style over readability.
Fantasy,
/// Fonts in which all glyphs have the same fixed advance width.
///
/// Monospace fonts are commonly used for code, tabular data, and text
/// where vertical alignment is important.
Monospace,
}

impl Default for FontSource {
Expand Down
28 changes: 28 additions & 0 deletions examples/ui/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
FpsText,
));

commands.spawn((
Node {
flex_direction: FlexDirection::Column,
left: px(250),
top: px(250),
..Default::default()
},
children![
(
Text::new("hello sans serif"),
TextFont::from(FontSource::SansSerif)
),
(Text::new("hello serif"), TextFont::from(FontSource::Serif)),
(
Text::new("hello fantasy"),
TextFont::from(FontSource::Fantasy)
),
(
Text::new("hello cursive"),
TextFont::from(FontSource::Cursive)
),
(
Text::new("hello monospace"),
TextFont::from(FontSource::Monospace)
)
],
));

// Text with OpenType features
let opentype_font_handle: FontSource =
asset_server.load("fonts/EBGaramond12-Regular.otf").into();
Expand Down
7 changes: 7 additions & 0 deletions release-content/release-notes/generic_font_families.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: "Generic Font Families"
authors: ["@ickshonpe"]
pull_requests: []
---

Support for generic font families has been added through new `FontSource` variants: `Serif`, `SansSerif`, `Cursive`, `Fantasy`, and `Monospace`.
Loading