From a7a67402e8e915c5ff6aee1db59b46f38233e10a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 20 May 2026 10:21:52 +0100 Subject: [PATCH 01/16] In `update_editable_text_styles`, reresolve `FontSource`s on changes to the font assets. When FontSource resolution failed because an asset wasn't yet loaded, `update_editable_text_styles` didn't resolve the FontSource again the next frame. --- crates/bevy_ui/src/widget/text_input_layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 4522911024ace..d849af16aca73 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -188,7 +188,7 @@ pub fn update_editable_text_styles( )); } - if text_font.is_changed() { + if text_font.is_changed() || fonts.is_changed() { let Ok(font_family) = resolve_font_source(&text_font.font, fonts.as_ref()) else { continue; }; From 20a117885bbff0b998bba8415ad344fe75086ae9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 21 May 2026 13:45:28 +0100 Subject: [PATCH 02/16] New Css and List FontSource variants --- Cargo.toml | 11 ++ crates/bevy_text/src/lib.rs | 7 +- crates/bevy_text/src/parley_context.rs | 56 +++--- crates/bevy_text/src/pipeline.rs | 44 ++--- crates/bevy_text/src/text.rs | 164 ++++++++++++++---- .../bevy_ui/src/widget/text_input_layout.rs | 45 ++++- examples/testbed/ui.rs | 14 +- examples/ui/text/css_font_list.rs | 85 +++++++++ examples/ui/text/generic_font_families.rs | 25 ++- examples/ui/text/ime_support.rs | 2 +- 10 files changed, 354 insertions(+), 99 deletions(-) create mode 100644 examples/ui/text/css_font_list.rs diff --git a/Cargo.toml b/Cargo.toml index 739f8638c3acc..ef3df5bcc2f4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3940,6 +3940,17 @@ description = "Demonstrates how to use generic font families" category = "UI (User Interface)" wasm = true +[[example]] +name = "css_font_list" +path = "examples/ui/text/css_font_list.rs" +doc-scrape-examples = true + +[package.metadata.example.css_font_list] +name = "CSS Font List" +description = "Demonstrates how to select fonts using CSS font-family lists" +category = "UI (User Interface)" +wasm = true + [[example]] name = "text" path = "examples/ui/text/text.rs" diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 2fe5dfda8995f..73c63bfaac556 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -67,9 +67,10 @@ pub use text_edit::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, FontHinting, FontSize, FontSmoothing, FontSource, FontStyle, FontWeight, FontWidth, - Justify, LineBreak, Strikethrough, StrikethroughColor, TextColor, TextError, TextFont, - TextLayout, TextSpan, Underline, UnderlineColor, + Font, FontFamilyEntry, FontHinting, FontSize, FontSmoothing, FontSource, FontStyle, + FontWeight, FontWidth, GenericFontFamily, Justify, LineBreak, Strikethrough, + StrikethroughColor, TextColor, TextError, TextFont, TextLayout, TextSpan, Underline, + UnderlineColor, }; } diff --git a/crates/bevy_text/src/parley_context.rs b/crates/bevy_text/src/parley_context.rs index f33c576b141d5..6c9acf397225b 100644 --- a/crates/bevy_text/src/parley_context.rs +++ b/crates/bevy_text/src/parley_context.rs @@ -1,10 +1,10 @@ use crate::TextError; -use crate::{FontSmoothing, FontSource}; +use crate::{FontSmoothing, FontSource, GenericFontFamily}; use bevy_derive::Deref; use bevy_derive::DerefMut; use bevy_ecs::resource::Resource; +use parley::FontContext; use parley::LayoutContext; -use parley::{FontContext, GenericFamily}; use swash::scale::ScaleContext; #[derive(Copy, Clone, PartialEq, Default, Debug)] @@ -42,24 +42,16 @@ impl FontCx { /// up the `Font` asset instead. pub fn get_family<'a>(&'a mut self, source: &'a FontSource) -> Option<&'a str> { let generic_family = match source { - FontSource::Handle(_) => return None, FontSource::Family(family) => return Some(family.as_str()), - FontSource::Serif => GenericFamily::Serif, - FontSource::SansSerif => GenericFamily::SansSerif, - FontSource::Cursive => GenericFamily::Cursive, - FontSource::Fantasy => GenericFamily::Fantasy, - FontSource::Monospace => GenericFamily::Monospace, - FontSource::SystemUi => GenericFamily::SystemUi, - FontSource::UiSerif => GenericFamily::UiSerif, - FontSource::UiSansSerif => GenericFamily::UiSansSerif, - FontSource::UiMonospace => GenericFamily::UiMonospace, - FontSource::UiRounded => GenericFamily::UiRounded, - FontSource::Emoji => GenericFamily::Emoji, - FontSource::Math => GenericFamily::Math, - FontSource::FangSong => GenericFamily::FangSong, + FontSource::Handle(_) | FontSource::Css(_) | FontSource::List(_) => return None, + FontSource::Generic(generic_family) => *generic_family, }; - let family_id = self.0.collection.generic_families(generic_family).next(); + let family_id = self + .0 + .collection + .generic_families(generic_family.into()) + .next(); family_id.and_then(|id| self.0.collection.family_name(id)) } @@ -73,7 +65,7 @@ impl FontCx { /// These methods will return an error if the provided family name does not already exist in the font collection. pub fn set_generic_family( &mut self, - generic: GenericFamily, + generic: impl Into, family_name: &str, ) -> Result<(), TextError> { self.collection @@ -81,73 +73,73 @@ impl FontCx { .ok_or(TextError::NoSuchFontFamily(family_name.to_string())) .map(|id| { self.collection - .set_generic_families(generic, core::iter::once(id)); + .set_generic_families(generic.into(), core::iter::once(id)); }) } /// Sets the serif generic family mapping. pub fn set_serif_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::Serif, family_name) + self.set_generic_family(GenericFontFamily::Serif, family_name) } /// Sets the sans-serif generic family mapping. pub fn set_sans_serif_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::SansSerif, family_name) + self.set_generic_family(GenericFontFamily::SansSerif, family_name) } /// Sets the cursive generic family mapping. pub fn set_cursive_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::Cursive, family_name) + self.set_generic_family(GenericFontFamily::Cursive, family_name) } /// Sets the fantasy generic family mapping. pub fn set_fantasy_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::Fantasy, family_name) + self.set_generic_family(GenericFontFamily::Fantasy, family_name) } /// Sets the monospace generic family mapping. pub fn set_monospace_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::Monospace, family_name) + self.set_generic_family(GenericFontFamily::Monospace, family_name) } /// Sets the system-ui generic family mapping. pub fn set_system_ui_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::SystemUi, family_name) + self.set_generic_family(GenericFontFamily::SystemUi, family_name) } /// Sets the ui-serif generic family mapping. pub fn set_ui_serif_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::UiSerif, family_name) + self.set_generic_family(GenericFontFamily::UiSerif, family_name) } /// Sets the ui-sans-serif generic family mapping. pub fn set_ui_sans_serif_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::UiSansSerif, family_name) + self.set_generic_family(GenericFontFamily::UiSansSerif, family_name) } /// Sets the ui-monospace generic family mapping. pub fn set_ui_monospace_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::UiMonospace, family_name) + self.set_generic_family(GenericFontFamily::UiMonospace, family_name) } /// Sets the ui-rounded generic family mapping. pub fn set_ui_rounded_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::UiRounded, family_name) + self.set_generic_family(GenericFontFamily::UiRounded, family_name) } /// Sets the emoji generic family mapping. pub fn set_emoji_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::Emoji, family_name) + self.set_generic_family(GenericFontFamily::Emoji, family_name) } /// Sets the math generic family mapping. pub fn set_math_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::Math, family_name) + self.set_generic_family(GenericFontFamily::Math, family_name) } /// Sets the fangsong generic family mapping. pub fn set_fang_song_family(&mut self, family_name: &str) -> Result<(), TextError> { - self.set_generic_family(GenericFamily::FangSong, family_name) + self.set_generic_family(GenericFontFamily::FangSong, family_name) } } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 7ec9d9d69cdd7..09cf23eeed8f5 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -425,29 +425,31 @@ pub fn resolve_font_source<'a>( ))) } FontSource::Family(family) => FontFamily::named(family.as_str()), - generic => { + FontSource::Css(source) => FontFamily::Source(Cow::Borrowed(source.as_str())), + FontSource::List(list) => FontFamily::List(Cow::Owned( + list.iter() + .map(|family| match family { + crate::FontFamilyEntry::Named(name) => { + parley::FontFamilyName::Named(Cow::Borrowed(name.as_str())) + } + crate::FontFamilyEntry::Generic(generic_family) => { + #[cfg(not(feature = "system_font_discovery"))] + bevy_log::error_once!( "A generic FontSource ({generic_family:?}) was used, but the `system_font_discovery` \ + feature is not enabled. Text may not render. Enable the feature to allow Bevy \ + to discover system fonts."); + parley::FontFamilyName::Generic((*generic_family).into()) + } + }) + .collect(), + )), + FontSource::Generic(generic_family) => { #[cfg(not(feature = "system_font_discovery"))] - bevy_log::error_once!( - "A generic FontSource ({generic:?}) was used, but the `system_font_discovery` \ + bevy_log::error_once!( "A generic FontSource ({generic_family:?}) was used, but the `system_font_discovery` \ feature is not enabled. Text may not render. Enable the feature to allow Bevy \ - to discover system fonts." - ); - match generic { - FontSource::Handle(_) | FontSource::Family(_) => unreachable!(), - FontSource::Serif => parley::GenericFamily::Serif.into(), - FontSource::SansSerif => parley::GenericFamily::SansSerif.into(), - FontSource::Cursive => parley::GenericFamily::Cursive.into(), - FontSource::Fantasy => parley::GenericFamily::Fantasy.into(), - FontSource::Monospace => parley::GenericFamily::Monospace.into(), - FontSource::SystemUi => parley::GenericFamily::SystemUi.into(), - FontSource::UiSerif => parley::GenericFamily::UiSerif.into(), - FontSource::UiSansSerif => parley::GenericFamily::UiSansSerif.into(), - FontSource::UiMonospace => parley::GenericFamily::UiMonospace.into(), - FontSource::UiRounded => parley::GenericFamily::UiRounded.into(), - FontSource::Emoji => parley::GenericFamily::Emoji.into(), - FontSource::Math => parley::GenericFamily::Math.into(), - FontSource::FangSong => parley::GenericFamily::FangSong.into(), - } + to discover system fonts."); + FontFamily::Single(parley::FontFamilyName::Generic( + generic_family.clone().into(), + )) } }) } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 45d21dc461217..f690e85d18e06 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -264,11 +264,11 @@ impl From for parley::Alignment { } } -#[derive(Clone, Debug, Reflect, PartialEq, FromTemplate)] /// Determines how the font face for a text sections is selected. /// /// A [`FontSource`] can be a handle to a font asset, a font family name, -/// or a generic font category that is resolved using Parley's font database. +/// a CSS font-family list, an ordered family list, or a generic font category +/// that is resolved using Parley's font database. /// /// Font family fallback (selection of a font when the requested font is not found) /// is automatically handled by [`parley::fontique`]. @@ -279,6 +279,7 @@ impl From for parley::Alignment { /// /// You can check which font family is used for a given [`FontSource`] /// by calling [`FontCx::get_family`](crate::FontCx::get_family). +#[derive(Clone, Debug, Reflect, PartialEq, FromTemplate)] pub enum FontSource { /// Use a specific font face referenced by a [`Font`] asset handle. /// @@ -291,10 +292,122 @@ pub enum FontSource { Handle(Handle), /// Resolve the font by family name using the font database. Family(SmolStr), + /// Font family list in CSS format. + /// + /// For example: `"Arial, Noto Sans, sans-serif"`. + Css(SmolStr), + /// Ordered list of font families. + List(#[template(built_in)] Vec), + /// Resolve the font using a generic font family. + Generic(GenericFontFamily), +} + +impl FontSource { + /// Font family list in CSS format. + /// + /// For example: `"Arial, 'Noto Sans', sans-serif"`. + pub fn css(source: impl Into) -> Self { + Self::Css(source.into()) + } + + /// Creates an ordered list of font families. + pub fn list(list: I) -> Self + where + I: IntoIterator, + F: Into, + { + Self::List(list.into_iter().map(Into::into).collect()) + } +} + +impl Default for FontSource { + fn default() -> Self { + Self::Handle(Handle::default()) + } +} + +impl From> for FontSource { + fn from(handle: Handle) -> Self { + Self::Handle(handle) + } +} + +impl From<&Handle> for FontSource { + fn from(handle: &Handle) -> Self { + Self::Handle(handle.clone()) + } +} + +impl From for FontSource { + fn from(family: SmolStr) -> Self { + FontSource::Family(family) + } +} + +impl From<&str> for FontSource { + fn from(family: &str) -> Self { + FontSource::Family(family.into()) + } +} + +impl From for FontSource { + fn from(family: FontFamilyEntry) -> Self { + match family { + FontFamilyEntry::Named(family) => FontSource::Family(family), + FontFamilyEntry::Generic(generic) => FontSource::Generic(generic), + } + } +} + +impl From for FontSource { + fn from(generic: GenericFontFamily) -> Self { + Self::Generic(generic) + } +} + +impl From> for FontSource { + fn from(list: Vec) -> Self { + Self::List(list) + } +} + +impl From<[FontFamilyEntry; N]> for FontSource { + fn from(list: [FontFamilyEntry; N]) -> Self { + Self::List(list.into()) + } +} + +/// A single named or generic font family used in a [`FontSource::List`]. +#[derive(Clone, Debug, Reflect, PartialEq, Eq, FromTemplate)] +pub enum FontFamilyEntry { + /// A named font family. + #[default] + Named(SmolStr), + /// A generic font family. + Generic(GenericFontFamily), +} + +impl From<&str> for FontFamilyEntry { + fn from(family: &str) -> Self { + Self::Named(family.into()) + } +} + +impl From for FontFamilyEntry { + fn from(generic: GenericFontFamily) -> Self { + Self::Generic(generic) + } +} + +/// Generic font families that are resolved through Parley's font database. +#[derive(Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash, FromTemplate)] +#[repr(u8)] +pub enum GenericFontFamily { /// 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. + #[default] Serif, /// Fonts without serifs. /// @@ -339,33 +452,23 @@ pub enum FontSource { FangSong, } -impl Default for FontSource { - fn default() -> Self { - Self::Handle(Handle::default()) - } -} - -impl From> for FontSource { - fn from(handle: Handle) -> Self { - Self::Handle(handle) - } -} - -impl From<&Handle> for FontSource { - fn from(handle: &Handle) -> Self { - Self::Handle(handle.clone()) - } -} - -impl From for FontSource { - fn from(family: SmolStr) -> Self { - FontSource::Family(family) - } -} - -impl From<&str> for FontSource { - fn from(family: &str) -> Self { - FontSource::Family(family.into()) +impl From for parley::GenericFamily { + fn from(generic: GenericFontFamily) -> Self { + match generic { + GenericFontFamily::Serif => Self::Serif, + GenericFontFamily::SansSerif => Self::SansSerif, + GenericFontFamily::Cursive => Self::Cursive, + GenericFontFamily::Fantasy => Self::Fantasy, + GenericFontFamily::Monospace => Self::Monospace, + GenericFontFamily::SystemUi => Self::SystemUi, + GenericFontFamily::UiSerif => Self::UiSerif, + GenericFontFamily::UiSansSerif => Self::UiSansSerif, + GenericFontFamily::UiMonospace => Self::UiMonospace, + GenericFontFamily::UiRounded => Self::UiRounded, + GenericFontFamily::Emoji => Self::Emoji, + GenericFontFamily::Math => Self::Math, + GenericFontFamily::FangSong => Self::FangSong, + } } } @@ -377,7 +480,8 @@ pub struct TextFont { /// Specifies the font face used for this text section. /// /// A `FontSource` can be a handle to a font asset, a font family name, - /// or a generic font category that is resolved using Parley's + /// a CSS font-family list, an ordered family list, or a generic font category + /// that is resolved using Parley's /// [`FontContext`](`parley::FontContext`) which is accessible through the /// [`FontCx`](`crate::FontCx`) resource. pub font: FontSource, diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 4522911024ace..1582df6677cd3 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -91,15 +91,48 @@ pub fn update_editable_text_content_size( let mut query = font_context .collection .query(&mut font_context.source_cache); - match resolve_font_source(&text_font.font, fonts.as_ref()).ok()? { - parley::FontFamily::Single(parley::FontFamilyName::Named(name)) => { - query.set_families([parley::fontique::QueryFamily::Named(name.as_ref())]); + + let font_family = resolve_font_source(&text_font.font, fonts.as_ref()).ok()?; + let mut parsed_font_families = Vec::new(); + let mut query_families = Vec::new(); + match &font_family { + parley::FontFamily::Source(source) => { + parsed_font_families.extend( + parley::FontFamilyName::parse_css_list(source.as_ref()) + .map_while(Result::ok), + ); + for family in &parsed_font_families { + match family { + parley::FontFamilyName::Named(name) => query_families + .push(parley::fontique::QueryFamily::Named(name.as_ref())), + parley::FontFamilyName::Generic(generic) => query_families + .push(parley::fontique::QueryFamily::Generic(*generic)), + } + } } - parley::FontFamily::Single(parley::FontFamilyName::Generic(generic)) => { - query.set_families([parley::fontique::QueryFamily::Generic(generic)]); + parley::FontFamily::Single(family) => match family { + parley::FontFamilyName::Named(name) => { + query_families.push(parley::fontique::QueryFamily::Named(name.as_ref())); + } + parley::FontFamilyName::Generic(generic) => { + query_families.push(parley::fontique::QueryFamily::Generic(*generic)); + } + }, + parley::FontFamily::List(families) => { + for family in families.iter() { + match family { + parley::FontFamilyName::Named(name) => query_families + .push(parley::fontique::QueryFamily::Named(name.as_ref())), + parley::FontFamilyName::Generic(generic) => query_families + .push(parley::fontique::QueryFamily::Generic(*generic)), + } + } } - _ => return None, } + if query_families.is_empty() { + return None; + } + query.set_families(query_families); query.set_attributes(parley::fontique::Attributes::new( text_font.width.into(), text_font.style.into(), diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 1d89c77198893..f4de715fe21dd 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -221,7 +221,7 @@ mod text { Text::new("Hello World."), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), - font_size: FontSize::Px(200.), + font_size: FontSize::Px(100.), ..default() }, )); @@ -251,6 +251,16 @@ mod text { hinting, )); + content.with_child(( + Text::new("Font from css font list"), + TextFont { + font: FontSource::css( + "'Comic Sans', Arial, 'Noto Sans', sans-serif, 'fonts/FiraSans-Bold.ttf'", + ), + ..Default::default() + }, + )); + content.with_child(( Text::new("white "), TextFont { @@ -682,6 +692,8 @@ mod text { } } +mod fonts {} + mod grid { use bevy::{color::palettes::css::*, prelude::*}; diff --git a/examples/ui/text/css_font_list.rs b/examples/ui/text/css_font_list.rs new file mode 100644 index 0000000000000..3b28b195ba196 --- /dev/null +++ b/examples/ui/text/css_font_list.rs @@ -0,0 +1,85 @@ +//! This example demonstrates selecting fonts from CSS font-family lists. + +use bevy::prelude::*; + +const FONT_ASSETS: &[&str] = &[ + "fonts/FiraSans-Bold.ttf", + "fonts/FiraMono-Medium.ttf", + "fonts/MonaSans-VariableFont.ttf", + "fonts/EBGaramond12-Regular.otf", +]; + +#[derive(Resource)] +struct LoadedFontAssets { + _handles: Vec>, +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2d); + commands.insert_resource(LoadedFontAssets { + _handles: FONT_ASSETS + .iter() + .map(|font_asset| asset_server.load(*font_asset)) + .collect(), + }); + commands.spawn(( + Node { + flex_direction: FlexDirection::Column, + align_self: AlignSelf::Center, + justify_self: JustifySelf::Center, + row_gap: px(25), + ..default() + }, + children![ + ( + Text::new("CSS Font Lists"), + TextFont::from_font_size(FontSize::Px(32.)), + Underline, + ), + ( + Node { + flex_direction: FlexDirection::Row, + column_gap: px(20), + padding: px(16).left(), + ..default() + }, + Children::spawn(SpawnIter( + (0..FONT_ASSETS.len()) + .map(|start| { + FONT_ASSETS + .iter() + .cycle() + .skip(start) + .take(FONT_ASSETS.len()) + .map(|font_asset| format!("{font_asset}")) + .collect::>() + .join(", ") + }) + .map(|list| { + ( + Text::new(list.replace(", ", "\n")), + TextFont { + font: FontSource::css(list), + font_size: FontSize::Px(16.), + ..default() + }, + Node { + padding: px(4.).all(), + ..default() + }, + TextLayout::no_wrap(), + Outline::default(), + ) + }) + )), + ), + ], + )); +} diff --git a/examples/ui/text/generic_font_families.rs b/examples/ui/text/generic_font_families.rs index c57b8f71dd946..4b774fc18f37d 100644 --- a/examples/ui/text/generic_font_families.rs +++ b/examples/ui/text/generic_font_families.rs @@ -63,11 +63,26 @@ fn setup(mut commands: Commands, mut font_system: ResMut) { }; for (source, description) in [ - (FontSource::SansSerif, "generic sans serif font"), - (FontSource::Serif, "generic serif font"), - (FontSource::Fantasy, "generic fantasy font"), - (FontSource::Cursive, "generic cursive font"), - (FontSource::Monospace, "generic monospace font"), + ( + FontSource::Generic(GenericFontFamily::SansSerif), + "generic sans serif font", + ), + ( + FontSource::Generic(GenericFontFamily::Serif), + "generic serif font", + ), + ( + FontSource::Generic(GenericFontFamily::Fantasy), + "generic fantasy font", + ), + ( + FontSource::Generic(GenericFontFamily::Cursive), + "generic cursive font", + ), + ( + FontSource::Generic(GenericFontFamily::Monospace), + "generic monospace font", + ), ] { builder.spawn(( Text::new(description), diff --git a/examples/ui/text/ime_support.rs b/examples/ui/text/ime_support.rs index c87a5d7abaf16..5efed827b6c8b 100644 --- a/examples/ui/text/ime_support.rs +++ b/examples/ui/text/ime_support.rs @@ -56,7 +56,7 @@ fn setup(mut commands: Commands) { // includes support for Chinese, Japanese, and Korean characters. // Note that using system fonts requires the "bevy/system-fonts" feature to be enabled. TextFont { - font: FontSource::SansSerif, + font: FontSource::Generic(GenericFontFamily::SansSerif), font_size: FontSize::Px(32.0), ..default() }, From 0480ca603dfc934751c0992a5a2bd7b8b6a9072e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 21 May 2026 13:54:02 +0100 Subject: [PATCH 03/16] Added generic helper fns to FontSource --- crates/bevy_text/src/text.rs | 86 ++++++++++++++++++++++- examples/ui/text/generic_font_families.rs | 25 ++----- 2 files changed, 90 insertions(+), 21 deletions(-) diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index f690e85d18e06..89fbeeb09edbb 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -318,6 +318,90 @@ impl FontSource { { Self::List(list.into_iter().map(Into::into).collect()) } + + /// 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. + pub const fn serif() -> Self { + Self::Generic(GenericFontFamily::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. + pub const fn sans_serif() -> Self { + Self::Generic(GenericFontFamily::SansSerif) + } + + /// Fonts that use a cursive or handwritten style. + /// + /// Glyphs often resemble connected or flowing pen or brush strokes rather + /// than printed letterforms. + pub const fn cursive() -> Self { + Self::Generic(GenericFontFamily::Cursive) + } + + /// Decorative or expressive fonts. + /// + /// Fantasy fonts are primarily intended for display purposes and may + /// prioritize visual style over readability. + pub const fn fantasy() -> Self { + Self::Generic(GenericFontFamily::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. + pub const fn monospace() -> Self { + Self::Generic(GenericFontFamily::Monospace) + } + + /// The default user interface system font. + pub const fn system_ui() -> Self { + Self::Generic(GenericFontFamily::SystemUi) + } + + /// Alternative serif font for user interfaces. + pub const fn ui_serif() -> Self { + Self::Generic(GenericFontFamily::UiSerif) + } + + /// Alternative sans-serif font for user interfaces. + pub const fn ui_sans_serif() -> Self { + Self::Generic(GenericFontFamily::UiSansSerif) + } + + /// Alternative monospace font for user interfaces. + pub const fn ui_monospace() -> Self { + Self::Generic(GenericFontFamily::UiMonospace) + } + + /// Fonts that have rounded features. + pub const fn ui_rounded() -> Self { + Self::Generic(GenericFontFamily::UiRounded) + } + + /// Fonts that are specifically designed to render emoji. + pub const fn emoji() -> Self { + Self::Generic(GenericFontFamily::Emoji) + } + + /// This is for the particular stylistic concerns of representing + /// mathematics: superscript and subscript, brackets that cross several + /// lines, nesting expressions, and double struck glyphs with distinct + /// meanings. + pub const fn math() -> Self { + Self::Generic(GenericFontFamily::Math) + } + + /// A particular style of Chinese characters that are between serif-style + /// Song and cursive-style Kai forms. This style is often used for + /// government documents. + pub const fn fang_song() -> Self { + Self::Generic(GenericFontFamily::FangSong) + } } impl Default for FontSource { @@ -433,7 +517,7 @@ pub enum GenericFontFamily { SystemUi, /// Alternative serif font for user interfaces. UiSerif, - /// Alternative sans-erif font for user interfaces. + /// Alternative sans-serif font for user interfaces. UiSansSerif, /// Alternative monospace font for user interfaces. UiMonospace, diff --git a/examples/ui/text/generic_font_families.rs b/examples/ui/text/generic_font_families.rs index 4b774fc18f37d..37b660960ba42 100644 --- a/examples/ui/text/generic_font_families.rs +++ b/examples/ui/text/generic_font_families.rs @@ -63,26 +63,11 @@ fn setup(mut commands: Commands, mut font_system: ResMut) { }; for (source, description) in [ - ( - FontSource::Generic(GenericFontFamily::SansSerif), - "generic sans serif font", - ), - ( - FontSource::Generic(GenericFontFamily::Serif), - "generic serif font", - ), - ( - FontSource::Generic(GenericFontFamily::Fantasy), - "generic fantasy font", - ), - ( - FontSource::Generic(GenericFontFamily::Cursive), - "generic cursive font", - ), - ( - FontSource::Generic(GenericFontFamily::Monospace), - "generic monospace font", - ), + (FontSource::sans_serif(), "generic sans serif font"), + (FontSource::serif(), "generic serif font"), + (FontSource::fantasy(), "generic fantasy font"), + (FontSource::cursive(), "generic cursive font"), + (FontSource::monospace(), "generic monospace font"), ] { builder.spawn(( Text::new(description), From 7c3997743da03146016abf4d06a8a8ef6189521c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 21 May 2026 14:23:25 +0100 Subject: [PATCH 04/16] Clean up font source resolution --- .../bevy_ui/src/widget/text_input_layout.rs | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 1582df6677cd3..4c658188c8c37 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -92,47 +92,41 @@ pub fn update_editable_text_content_size( .collection .query(&mut font_context.source_cache); - let font_family = resolve_font_source(&text_font.font, fonts.as_ref()).ok()?; - let mut parsed_font_families = Vec::new(); - let mut query_families = Vec::new(); - match &font_family { + match &resolve_font_source(&text_font.font, fonts.as_ref()).ok()? { parley::FontFamily::Source(source) => { - parsed_font_families.extend( - parley::FontFamilyName::parse_css_list(source.as_ref()) - .map_while(Result::ok), - ); - for family in &parsed_font_families { - match family { - parley::FontFamilyName::Named(name) => query_families - .push(parley::fontique::QueryFamily::Named(name.as_ref())), - parley::FontFamilyName::Generic(generic) => query_families - .push(parley::fontique::QueryFamily::Generic(*generic)), + let parsed_font_families = parley::FontFamilyName::parse_css_list(source) + .map_while(Result::ok) + .collect::>(); + query.set_families(parsed_font_families.iter().map(|family| match family { + parley::FontFamilyName::Named(name) => { + parley::fontique::QueryFamily::Named(name.as_ref()) } - } + parley::FontFamilyName::Generic(generic) => { + parley::fontique::QueryFamily::Generic(*generic) + } + })); + } + parley::FontFamily::Single(family) => { + query.set_families([match family { + parley::FontFamilyName::Named(name) => { + parley::fontique::QueryFamily::Named(name.as_ref()) + } + parley::FontFamilyName::Generic(generic) => { + parley::fontique::QueryFamily::Generic(*generic) + } + }]); } - parley::FontFamily::Single(family) => match family { - parley::FontFamilyName::Named(name) => { - query_families.push(parley::fontique::QueryFamily::Named(name.as_ref())); - } - parley::FontFamilyName::Generic(generic) => { - query_families.push(parley::fontique::QueryFamily::Generic(*generic)); - } - }, parley::FontFamily::List(families) => { - for family in families.iter() { - match family { - parley::FontFamilyName::Named(name) => query_families - .push(parley::fontique::QueryFamily::Named(name.as_ref())), - parley::FontFamilyName::Generic(generic) => query_families - .push(parley::fontique::QueryFamily::Generic(*generic)), + query.set_families(families.iter().map(|family| match family { + parley::FontFamilyName::Named(name) => { + parley::fontique::QueryFamily::Named(name.as_ref()) } - } + parley::FontFamilyName::Generic(generic) => { + parley::fontique::QueryFamily::Generic(*generic) + } + })); } } - if query_families.is_empty() { - return None; - } - query.set_families(query_families); query.set_attributes(parley::fontique::Attributes::new( text_font.width.into(), text_font.style.into(), From f33a35b62481dce5505ec009f6396108c9942465 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 21 May 2026 14:33:57 +0100 Subject: [PATCH 05/16] Clean up font source resolution --- .../bevy_ui/src/widget/text_input_layout.rs | 56 +++++++------------ 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 1582df6677cd3..454161208fc86 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -58,6 +58,17 @@ impl crate::Measure for TextInputMeasure { } } +fn query_family<'a, 'b>( + family: &'a parley::FontFamilyName<'b>, +) -> parley::fontique::QueryFamily<'a> { + match family { + parley::FontFamilyName::Named(name) => parley::fontique::QueryFamily::Named(name.as_ref()), + parley::FontFamilyName::Generic(generic) => { + parley::fontique::QueryFamily::Generic(*generic) + } + } +} + /// If `visible_lines` or `visible_width` are `Some`, sets a `ContentSize` that determines: /// - node height as `line_height * visible_lines`, using the resolved font line height. /// - node width as `advance('0') * visible_width`, where `advance('0')` is looked up from font metrics. @@ -92,47 +103,20 @@ pub fn update_editable_text_content_size( .collection .query(&mut font_context.source_cache); - let font_family = resolve_font_source(&text_font.font, fonts.as_ref()).ok()?; - let mut parsed_font_families = Vec::new(); - let mut query_families = Vec::new(); - match &font_family { + match &resolve_font_source(&text_font.font, fonts.as_ref()).ok()? { parley::FontFamily::Source(source) => { - parsed_font_families.extend( - parley::FontFamilyName::parse_css_list(source.as_ref()) - .map_while(Result::ok), - ); - for family in &parsed_font_families { - match family { - parley::FontFamilyName::Named(name) => query_families - .push(parley::fontique::QueryFamily::Named(name.as_ref())), - parley::FontFamilyName::Generic(generic) => query_families - .push(parley::fontique::QueryFamily::Generic(*generic)), - } - } + let parsed_font_families = parley::FontFamilyName::parse_css_list(source) + .map_while(Result::ok) + .collect::>(); + query.set_families(parsed_font_families.iter().map(query_family)); + } + parley::FontFamily::Single(family) => { + query.set_families([query_family(family)]); } - parley::FontFamily::Single(family) => match family { - parley::FontFamilyName::Named(name) => { - query_families.push(parley::fontique::QueryFamily::Named(name.as_ref())); - } - parley::FontFamilyName::Generic(generic) => { - query_families.push(parley::fontique::QueryFamily::Generic(*generic)); - } - }, parley::FontFamily::List(families) => { - for family in families.iter() { - match family { - parley::FontFamilyName::Named(name) => query_families - .push(parley::fontique::QueryFamily::Named(name.as_ref())), - parley::FontFamilyName::Generic(generic) => query_families - .push(parley::fontique::QueryFamily::Generic(*generic)), - } - } + query.set_families(families.iter().map(query_family)); } } - if query_families.is_empty() { - return None; - } - query.set_families(query_families); query.set_attributes(parley::fontique::Attributes::new( text_font.width.into(), text_font.style.into(), From 0992c378cd005e56e3d4a9638cd2382f28104c3b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 21 May 2026 14:52:01 +0100 Subject: [PATCH 06/16] use SmallVec --- crates/bevy_ui/src/widget/text_input_layout.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 454161208fc86..8b418ba471b5c 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -23,6 +23,7 @@ use bevy_text::{ }; use bevy_time::{Real, Time}; use parley::{BoundingBox, PositionedLayoutItem, StyleProperty}; +use smallvec::SmallVec; use swash::FontRef; use taffy::MaybeMath; @@ -105,10 +106,13 @@ pub fn update_editable_text_content_size( match &resolve_font_source(&text_font.font, fonts.as_ref()).ok()? { parley::FontFamily::Source(source) => { - let parsed_font_families = parley::FontFamilyName::parse_css_list(source) - .map_while(Result::ok) - .collect::>(); - query.set_families(parsed_font_families.iter().map(query_family)); + query.set_families( + parley::FontFamilyName::parse_css_list(source) + .map_while(Result::ok) + .collect::>() + .iter() + .map(query_family), + ); } parley::FontFamily::Single(family) => { query.set_families([query_family(family)]); From 8778e8c5a82e7ca76cbbbe03144725ad4b62a57c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 09:22:11 +0100 Subject: [PATCH 07/16] renamed and updated example --- Cargo.toml | 8 ++++---- examples/README.md | 1 + examples/ui/text/{css_font_list.rs => font_lists.rs} | 7 +++++-- 3 files changed, 10 insertions(+), 6 deletions(-) rename examples/ui/text/{css_font_list.rs => font_lists.rs} (93%) diff --git a/Cargo.toml b/Cargo.toml index ef3df5bcc2f4d..99b25045aad68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3941,12 +3941,12 @@ category = "UI (User Interface)" wasm = true [[example]] -name = "css_font_list" -path = "examples/ui/text/css_font_list.rs" +name = "font_lists" +path = "examples/ui/text/font_lists.rs" doc-scrape-examples = true -[package.metadata.example.css_font_list] -name = "CSS Font List" +[package.metadata.example.font_lists] +name = "Font Lists" description = "Demonstrates how to select fonts using CSS font-family lists" category = "UI (User Interface)" wasm = true diff --git a/examples/README.md b/examples/README.md index 8390a8051e570..0840bd00f36d4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -603,6 +603,7 @@ Example | Description [Feathers Widgets](../examples/ui/widgets/feathers_gallery.rs) | Gallery of Feathers Widgets [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 Lists](../examples/ui/text/font_lists.rs) | Demonstrates how to select fonts using CSS font-family lists [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. diff --git a/examples/ui/text/css_font_list.rs b/examples/ui/text/font_lists.rs similarity index 93% rename from examples/ui/text/css_font_list.rs rename to examples/ui/text/font_lists.rs index 3b28b195ba196..943c70e181cb0 100644 --- a/examples/ui/text/css_font_list.rs +++ b/examples/ui/text/font_lists.rs @@ -3,6 +3,7 @@ use bevy::prelude::*; const FONT_ASSETS: &[&str] = &[ + "Gabriola", "fonts/FiraSans-Bold.ttf", "fonts/FiraMono-Medium.ttf", "fonts/MonaSans-VariableFont.ttf", @@ -39,14 +40,16 @@ fn setup(mut commands: Commands, asset_server: Res) { }, children![ ( - Text::new("CSS Font Lists"), + Text::new("Font Lists"), TextFont::from_font_size(FontSize::Px(32.)), Underline, ), ( Node { flex_direction: FlexDirection::Row, - column_gap: px(20), + flex_wrap: FlexWrap::Wrap, + column_gap: px(30), + row_gap: px(30), padding: px(16).left(), ..default() }, From a79e9d5d12eff3aacb5dbc80cf7234df60dc8593 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 09:27:09 +0100 Subject: [PATCH 08/16] Fixed "using `clone` on type `GenericFontFamily` which implements the `Copy` trait" --- crates/bevy_text/src/pipeline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 09cf23eeed8f5..f26a3be545614 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -448,7 +448,7 @@ pub fn resolve_font_source<'a>( feature is not enabled. Text may not render. Enable the feature to allow Bevy \ to discover system fonts."); FontFamily::Single(parley::FontFamilyName::Generic( - generic_family.clone().into(), + (*generic_family).into(), )) } }) From 3c329ecb5dce7be5cd2ab078565336ac3eb4e634 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 09:29:31 +0100 Subject: [PATCH 09/16] Fixed `FontSource` in `ime_support` example. --- examples/ui/text/ime_support.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ui/text/ime_support.rs b/examples/ui/text/ime_support.rs index 5efed827b6c8b..ea42661dfbae8 100644 --- a/examples/ui/text/ime_support.rs +++ b/examples/ui/text/ime_support.rs @@ -56,7 +56,7 @@ fn setup(mut commands: Commands) { // includes support for Chinese, Japanese, and Korean characters. // Note that using system fonts requires the "bevy/system-fonts" feature to be enabled. TextFont { - font: FontSource::Generic(GenericFontFamily::SansSerif), + font: FontSource::sans_serif(), font_size: FontSize::Px(32.0), ..default() }, From 9f729e24d8e9750b00446eaabe5885d2ab7d324b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 15:04:20 +0100 Subject: [PATCH 10/16] In `load_font_assets_into_font_collection` set the family name from the loaded collection, instead of the asset path. Store the changed family ids and asset paths, and set the any TextFonts that refer to them as changed. --- crates/bevy_text/src/font.rs | 91 +++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 3c0f804305d42..a66230192b253 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,16 +1,16 @@ -use crate::ComputedTextBlock; use crate::FontCx; +use crate::FontSource; +use crate::TextFont; use bevy_asset::Asset; use bevy_asset::AssetId; use bevy_asset::Assets; +use bevy_ecs::change_detection::DetectChangesMut; use bevy_ecs::system::Local; use bevy_ecs::system::Query; -use bevy_ecs::system::Res; use bevy_ecs::system::ResMut; use bevy_platform::collections::HashSet; use bevy_reflect::TypePath; use parley::fontique::Blob; -use parley::fontique::FontInfoOverride; use smol_str::SmolStr; /// An [`Asset`] that contains the data for a loaded font, if loaded as an asset. @@ -29,8 +29,9 @@ use smol_str::SmolStr; pub struct Font { /// Content of a font file as bytes pub data: Blob, - /// Font family name. - /// If the font file is a collection with multiple families, the first family name from the last font is used. + /// Font family name used to resolve this asset when referenced by handle. + /// If the font file is a collection with multiple families, this is the family name from the + /// first font face in the collection. pub family_name: SmolStr, } @@ -46,32 +47,76 @@ impl Font { /// Add new font assets to the internal font collection. pub fn load_font_assets_into_font_collection( - fonts: Res>, + mut fonts: ResMut>, mut loaded_fonts: Local>>, mut font_cx: ResMut, - mut text_block_query: Query<&mut ComputedTextBlock>, + mut text_font_query: Query<&mut TextFont>, ) { - let mut new_fonts_added = false; - loaded_fonts.retain(|id| fonts.contains(*id)); - for (id, font) in fonts.iter() { - if loaded_fonts.insert(id) { - font_cx.0.collection.register_fonts( - font.data.clone(), - Some(FontInfoOverride { - family_name: Some(font.family_name.as_str()), - ..Default::default() - }), - ); - new_fonts_added = true; + let new_asset_ids: Vec<_> = fonts.ids().filter(|id| loaded_fonts.insert(*id)).collect(); + + if new_asset_ids.is_empty() { + return; + } + + let mut new_family_ids = Vec::new(); + for asset_id in new_asset_ids.iter() { + let font_data = fonts + .get(*asset_id) + .expect("AssetId should have a corresponding asset") + .data + .clone(); + + let new_fonts = font_cx.collection.register_fonts(font_data, None); + + if let Some((_, family_id)) = new_fonts + .iter() + .flat_map(|(family_id, fonts)| { + fonts + .iter() + .map(move |font_info| (font_info.index(), *family_id)) + }) + .min_by_key(|(index, _)| *index) + && let Some(family_name) = font_cx.0.collection.family_name(family_id) + && let Some(font) = fonts.get_mut_untracked(*asset_id) + { + font.family_name = family_name.into(); + new_family_ids.extend(new_fonts.iter().map(|(family_id, _)| *family_id)); } } - // Whenever new fonts are added, update all text blocks so they use the new fonts. - if new_fonts_added { - for mut block in text_block_query.iter_mut() { - block.needs_rerender = true; + for mut text_font in text_font_query.iter_mut() { + if match &text_font.font { + FontSource::Handle(handle) => new_asset_ids.contains(&handle.id()), + FontSource::Family(name) => font_cx + .collection + .family_id(name) + .is_some_and(|id| new_family_ids.contains(&id)), + generic_source => { + let generic_family = match generic_source { + FontSource::Handle(_) | FontSource::Family(_) => unreachable!(), + FontSource::Serif => parley::GenericFamily::Serif, + FontSource::SansSerif => parley::GenericFamily::SansSerif, + FontSource::Cursive => parley::GenericFamily::Cursive, + FontSource::Fantasy => parley::GenericFamily::Fantasy, + FontSource::Monospace => parley::GenericFamily::Monospace, + FontSource::SystemUi => parley::GenericFamily::SystemUi, + FontSource::UiSerif => parley::GenericFamily::UiSerif, + FontSource::UiSansSerif => parley::GenericFamily::UiSansSerif, + FontSource::UiMonospace => parley::GenericFamily::UiMonospace, + FontSource::UiRounded => parley::GenericFamily::UiRounded, + FontSource::Emoji => parley::GenericFamily::Emoji, + FontSource::Math => parley::GenericFamily::Math, + FontSource::FangSong => parley::GenericFamily::FangSong, + }; + font_cx + .collection + .generic_families(generic_family) + .any(|id| new_family_ids.contains(&id)) + } + } { + text_font.set_changed(); } } } From 4416de9e5b99b899108df6e5f5be1c5deb87d469 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 15:08:44 +0100 Subject: [PATCH 11/16] reverteded `text_layout_info` changes --- crates/bevy_ui/src/widget/text_input_layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index d849af16aca73..4522911024ace 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -188,7 +188,7 @@ pub fn update_editable_text_styles( )); } - if text_font.is_changed() || fonts.is_changed() { + if text_font.is_changed() { let Ok(font_family) = resolve_font_source(&text_font.font, fonts.as_ref()) else { continue; }; From c57d5dff7c39f7e7db0ed3093b1d9451aa920440 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 15:26:25 +0100 Subject: [PATCH 12/16] Updated load_font_assets_into_font_collection change detection to support lists --- crates/bevy_text/src/font.rs | 50 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index a66230192b253..2da2c9f9d9f8d 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,4 +1,5 @@ use crate::FontCx; +use crate::FontFamilyEntry; use crate::FontSource; use crate::TextFont; use bevy_asset::Asset; @@ -11,6 +12,7 @@ use bevy_ecs::system::ResMut; use bevy_platform::collections::HashSet; use bevy_reflect::TypePath; use parley::fontique::Blob; +use parley::FontFamilyName; use smol_str::SmolStr; /// An [`Asset`] that contains the data for a loaded font, if loaded as an asset. @@ -91,30 +93,34 @@ pub fn load_font_assets_into_font_collection( FontSource::Handle(handle) => new_asset_ids.contains(&handle.id()), FontSource::Family(name) => font_cx .collection - .family_id(name) + .family_id(name.as_str()) .is_some_and(|id| new_family_ids.contains(&id)), - generic_source => { - let generic_family = match generic_source { - FontSource::Handle(_) | FontSource::Family(_) => unreachable!(), - FontSource::Serif => parley::GenericFamily::Serif, - FontSource::SansSerif => parley::GenericFamily::SansSerif, - FontSource::Cursive => parley::GenericFamily::Cursive, - FontSource::Fantasy => parley::GenericFamily::Fantasy, - FontSource::Monospace => parley::GenericFamily::Monospace, - FontSource::SystemUi => parley::GenericFamily::SystemUi, - FontSource::UiSerif => parley::GenericFamily::UiSerif, - FontSource::UiSansSerif => parley::GenericFamily::UiSansSerif, - FontSource::UiMonospace => parley::GenericFamily::UiMonospace, - FontSource::UiRounded => parley::GenericFamily::UiRounded, - FontSource::Emoji => parley::GenericFamily::Emoji, - FontSource::Math => parley::GenericFamily::Math, - FontSource::FangSong => parley::GenericFamily::FangSong, - }; - font_cx + FontSource::Css(source) => FontFamilyName::parse_css_list(source.as_str()) + .map_while(Result::ok) + .any(|family| match family { + FontFamilyName::Named(name) => font_cx + .collection + .family_id(name.as_ref()) + .is_some_and(|id| new_family_ids.contains(&id)), + FontFamilyName::Generic(generic_family) => font_cx + .collection + .generic_families(generic_family) + .any(|id| new_family_ids.contains(&id)), + }), + FontSource::List(items) => items.iter().any(|family| match family { + FontFamilyEntry::Named(name) => font_cx .collection - .generic_families(generic_family) - .any(|id| new_family_ids.contains(&id)) - } + .family_id(name.as_str()) + .is_some_and(|id| new_family_ids.contains(&id)), + FontFamilyEntry::Generic(generic_family) => font_cx + .collection + .generic_families((*generic_family).into()) + .any(|id| new_family_ids.contains(&id)), + }), + FontSource::Generic(generic_family) => font_cx + .collection + .generic_families((*generic_family).into()) + .any(|id| new_family_ids.contains(&id)), } { text_font.set_changed(); } From 448c128d282a3029e9f1a5e2c17c470a798db6f5 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 15:56:55 +0100 Subject: [PATCH 13/16] example --- examples/ui/text/font_lists.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/ui/text/font_lists.rs b/examples/ui/text/font_lists.rs index 943c70e181cb0..804209d5f70f2 100644 --- a/examples/ui/text/font_lists.rs +++ b/examples/ui/text/font_lists.rs @@ -3,13 +3,20 @@ use bevy::prelude::*; const FONT_ASSETS: &[&str] = &[ - "Gabriola", "fonts/FiraSans-Bold.ttf", "fonts/FiraMono-Medium.ttf", "fonts/MonaSans-VariableFont.ttf", "fonts/EBGaramond12-Regular.otf", ]; +const FONT_NAMES: &[&str] = &[ + "Gabriola", + "Fira Sans Bold", + "Fira Mono", + "Mona Sans ExtraLight", + "EB Garamond 12", +]; + #[derive(Resource)] struct LoadedFontAssets { _handles: Vec>, @@ -54,13 +61,13 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, Children::spawn(SpawnIter( - (0..FONT_ASSETS.len()) + (0..FONT_NAMES.len()) .map(|start| { - FONT_ASSETS + FONT_NAMES .iter() .cycle() .skip(start) - .take(FONT_ASSETS.len()) + .take(FONT_NAMES.len()) .map(|font_asset| format!("{font_asset}")) .collect::>() .join(", ") From e6894b3201e2aced85bac50d1073962a19756e23 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 16:04:06 +0100 Subject: [PATCH 14/16] fixed example --- examples/ui/text/font_lists.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ui/text/font_lists.rs b/examples/ui/text/font_lists.rs index 804209d5f70f2..f3a9065b5e471 100644 --- a/examples/ui/text/font_lists.rs +++ b/examples/ui/text/font_lists.rs @@ -11,10 +11,10 @@ const FONT_ASSETS: &[&str] = &[ const FONT_NAMES: &[&str] = &[ "Gabriola", - "Fira Sans Bold", + "Fira Sans", "Fira Mono", - "Mona Sans ExtraLight", - "EB Garamond 12", + "Mona Sans", + "EB Garamond", ]; #[derive(Resource)] From 77cc4944e5498284978f0069f33b3480b1d4e73b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 16:17:11 +0100 Subject: [PATCH 15/16] renamed variable --- examples/ui/text/font_lists.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ui/text/font_lists.rs b/examples/ui/text/font_lists.rs index f3a9065b5e471..bd41e2a68a398 100644 --- a/examples/ui/text/font_lists.rs +++ b/examples/ui/text/font_lists.rs @@ -68,7 +68,7 @@ fn setup(mut commands: Commands, asset_server: Res) { .cycle() .skip(start) .take(FONT_NAMES.len()) - .map(|font_asset| format!("{font_asset}")) + .map(|font_name| format!("{font_name}")) .collect::>() .join(", ") }) From eaac8eb91cb31161ed0a11395754d12e62174e40 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 May 2026 20:16:36 +0100 Subject: [PATCH 16/16] renamed variable --- examples/ui/text/font_lists.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ui/text/font_lists.rs b/examples/ui/text/font_lists.rs index f3a9065b5e471..bd41e2a68a398 100644 --- a/examples/ui/text/font_lists.rs +++ b/examples/ui/text/font_lists.rs @@ -68,7 +68,7 @@ fn setup(mut commands: Commands, asset_server: Res) { .cycle() .skip(start) .take(FONT_NAMES.len()) - .map(|font_asset| format!("{font_asset}")) + .map(|font_name| format!("{font_name}")) .collect::>() .join(", ") })