diff --git a/_release-content/migration-guides/font_source_resolve_default_source_parameter.md b/_release-content/migration-guides/font_source_resolve_default_source_parameter.md new file mode 100644 index 0000000000000..fd3852a7f7d9f --- /dev/null +++ b/_release-content/migration-guides/font_source_resolve_default_source_parameter.md @@ -0,0 +1,8 @@ +--- +title: New `default_source` parameter on `FontSource::resolve_font_family` +pull_requests: [25847] +--- + +The `FontSource::resolve_font_family` method now takes a `default_source` argument that is used to resolve `FontSource::Default`. + +You should set the value of the `DefaultFontSource` resource to set the default font for your app now. diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 2fe8383af35b7..5979d4ce697d1 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,9 +22,9 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LetterSpacing, LineBreak, - LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, - TextLayoutInfo, TextPipeline, TextReader, TextSection, TextWriter, + ComputedTextBlock, DefaultFontSource, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, + LetterSpacing, LineBreak, LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, + TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextSection, TextWriter, }; use bevy_transform::components::Transform; use bevy_window::{PrimaryWindow, Window}; @@ -171,6 +171,7 @@ pub fn update_text2d_layout( mut reprocess_queue: Local, mut textures: ResMut>, fonts: Res>, + default_font_source: Res, camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, mut font_atlas_set: ResMut, mut text_pipeline: ResMut, @@ -278,6 +279,7 @@ pub fn update_text2d_layout( &mut layout_cx, logical_viewport_size, *rem_size, + &default_font_source.0, ) { Err( TextError::NoSuchFont @@ -453,6 +455,7 @@ mod tests { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_systems( Update, ( diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index e683ae9a18486..2d899e8c2ffde 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -1,12 +1,14 @@ +use crate::DefaultFontSource; 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::change_detection::{DetectChanges, 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; @@ -54,6 +56,7 @@ pub fn load_font_assets_into_font_collection( mut loaded_fonts: Local>>, mut font_cx: ResMut, mut text_font_query: Query<&mut TextFont>, + default_font_source: Res, ) { let font_removed = loaded_fonts.iter().any(|id| !fonts.contains(*id)); let new_asset_ids: Vec<_> = if font_removed { @@ -66,7 +69,7 @@ pub fn load_font_assets_into_font_collection( fonts.ids().filter(|id| loaded_fonts.insert(*id)).collect() }; - if new_asset_ids.is_empty() && !font_removed { + if new_asset_ids.is_empty() && !font_removed && !default_font_source.is_changed() { return; } @@ -110,6 +113,7 @@ pub fn load_font_assets_into_font_collection( .flatten() .into_iter() .any(|source| match source { + FontSource::Default => true, FontSource::Handle(handle) => new_asset_ids.contains(&handle.id()), FontSource::Family(name) => font_cx .collection @@ -145,14 +149,66 @@ pub fn load_font_assets_into_font_collection( mod tests { use bevy_app::{App, Update}; use bevy_asset::Assets; - use bevy_ecs::change_detection::DetectChanges; use super::*; + #[test] + fn default_font_changes_invalidate_default_text_fonts() { + let mut app = App::new(); + app.init_resource::>() + .init_resource::() + .init_resource::() + .add_systems(Update, load_font_assets_into_font_collection); + let default_entity = app.world_mut().spawn(TextFont::default()).id(); + let nested_entity = app + .world_mut() + .spawn(TextFont { + font: FontSource::list([FontSource::list([FontSource::Default])]), + ..Default::default() + }) + .id(); + let explicit_entity = app.world_mut().spawn(TextFont::from("Other")).id(); + let entities = [default_entity, nested_entity, explicit_entity]; + app.update(); + + for insert_asset in [false, true] { + let ticks = entities.map(|entity| { + app.world() + .entity(entity) + .get_ref::() + .unwrap() + .last_changed() + }); + if insert_asset { + app.world_mut() + .resource_mut::>() + .add(Font::from_bytes( + include_bytes!("FiraMono-subset.ttf").to_vec(), + )); + } else { + app.world_mut().resource_mut::().0 = + FontSource::family("Fira Mono"); + } + app.update(); + for (entity, tick) in entities.into_iter().zip(ticks) { + assert_eq!( + tick != app + .world() + .entity(entity) + .get_ref::() + .unwrap() + .last_changed(), + entity != explicit_entity + ); + } + } + } + #[test] fn font_asset_registration_and_cleanup() { let mut app = App::new(); app.init_resource::>() + .init_resource::() .init_resource::() .add_systems(Update, load_font_assets_into_font_collection); @@ -202,6 +258,7 @@ mod tests { fn text_font_is_set_changed_when_its_font_asset_is_inserted() { let mut app = App::new(); app.init_resource::>() + .init_resource::() .init_resource::() .add_systems(Update, load_font_assets_into_font_collection); @@ -252,6 +309,7 @@ mod tests { fn textfonts_are_not_set_changed_when_a_font_asset_is_inserted_for_other_textfonts() { let mut app = App::new(); app.init_resource::>() + .init_resource::() .init_resource::() .add_systems(Update, load_font_assets_into_font_collection); diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 3d15d739d7226..70a1c5dba2242 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -126,6 +126,7 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_systems( PostUpdate, ( diff --git a/crates/bevy_text/src/parley_context.rs b/crates/bevy_text/src/parley_context.rs index 3e9ce1d51c1bf..b178b60725e11 100644 --- a/crates/bevy_text/src/parley_context.rs +++ b/crates/bevy_text/src/parley_context.rs @@ -50,8 +50,11 @@ impl FontCx { pub fn get_family<'a>(&'a mut self, source: &'a FontSource) -> Option<&'a str> { let generic_family = match source { FontSource::Family(family) => return Some(family.as_str()), - FontSource::Handle(_) | FontSource::Families(_) | FontSource::List(_) => return None, FontSource::Generic(generic_family) => *generic_family, + FontSource::Handle(_) + | FontSource::Families(_) + | FontSource::List(_) + | FontSource::Default => return None, }; let family_id = self diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index e2ebe3d0c5ddb..58803493853a5 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -82,6 +82,7 @@ impl TextPipeline { layout_cx: &mut LayoutCx, logical_viewport_size: Vec2, base_rem_size: RemSize, + default_font_source: &FontSource, ) -> Result<(), TextError> { computed.entities.clear(); computed.needs_rerender = false; @@ -126,7 +127,10 @@ impl TextPipeline { } if matches!(text_font.font, FontSource::Handle(_)) - && text_font.font.resolve_font_family(fonts).is_err() + && text_font + .font + .resolve_font_family(fonts, default_font_source) + .is_err() { return Err(TextError::NoSuchFont); } @@ -227,7 +231,9 @@ impl TextPipeline { continue; } - let resolved_family = text_font.font.resolve_font_family(fonts)?; + let resolved_family = text_font + .font + .resolve_font_family(fonts, default_font_source)?; builder.push(StyleProperty::FontFamily(resolved_family), range.clone()); builder.push( @@ -307,6 +313,7 @@ impl TextPipeline { layout_cx: &mut LayoutCx, logical_viewport_size: Vec2, base_rem_size: RemSize, + default_font_source: &FontSource, ) -> Result { const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0); @@ -324,6 +331,7 @@ impl TextPipeline { layout_cx, logical_viewport_size, base_rem_size, + default_font_source, )?; let layout_buffer = &mut computed.layout; diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 3f80adb23cc67..095b06876d694 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -280,7 +280,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)] +#[derive(Clone, Debug, Reflect, PartialEq, FromTemplate, Default)] pub enum FontSource { /// Use a specific font face referenced by a [`Font`] asset handle. /// @@ -288,8 +288,7 @@ pub enum FontSource { /// * if `default_font` feature is enabled (enabled by default in `bevy` crate), /// `FiraMono-subset.ttf` compiled into the library is used. /// * otherwise no text will be rendered, unless a custom font is loaded into the default font - /// handle. - #[default] + /// handle. Handle(Handle), /// Resolve the font by family name using the font database. Family(SmolStr), @@ -301,6 +300,9 @@ pub enum FontSource { List(#[template(built_in)] Vec), /// Resolve the font using a generic font family. Generic(GenericFontFamily), + /// Use the default font source from the [`DefaultFontSource`] resource. + #[default] + Default, } impl FontSource { @@ -444,8 +446,20 @@ impl FontSource { pub fn resolve_font_family<'a>( &'a self, fonts: &'a Assets, + default_source: &'a Self, ) -> Result, TextError> { + if matches!(self, FontSource::Default) && !matches!(default_source, FontSource::Default) { + return default_source.resolve_font_family(fonts, &FontSource::Default); + } + Ok(match self { + FontSource::Default => FontFamily::named( + fonts + .get(Handle::default().id()) + .ok_or(TextError::NoSuchFont)? + .alias + .as_str(), + ), FontSource::Handle(handle) => { FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( fonts @@ -462,6 +476,7 @@ impl FontSource { match font_sources.as_slice() { [] => FontFamily::List(Cow::Owned(Vec::new())), [source] => match *source { + FontSource::Default => source.resolve_font_family(fonts, default_source)?, FontSource::Handle(handle) => { FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( fonts @@ -494,6 +509,23 @@ impl FontSource { let mut families = Vec::new(); for source in font_sources.iter().copied() { match source { + FontSource::Default => { + match source.resolve_font_family(fonts, default_source)? { + FontFamily::Single(family) => families.push(family), + FontFamily::List(list) => { + families.extend(list.into_owned()); + } + FontFamily::Source(source) => { + for family in parley::FontFamilyName::parse_css_list( + source.as_ref(), + ) + .map_while(Result::ok) + { + families.push(family.into_owned()); + } + } + } + } FontSource::Handle(handle) => { families.push(parley::FontFamilyName::Named(Cow::Borrowed( fonts @@ -543,12 +575,6 @@ impl FontSource { } } -impl Default for FontSource { - fn default() -> Self { - Self::Handle(Handle::default()) - } -} - impl From> for FontSource { fn from(handle: Handle) -> Self { Self::Handle(handle) @@ -591,6 +617,11 @@ impl From<[FontSource; N]> for FontSource { } } +/// The default font source used to resolve `FontSource::Default`. +/// `DefaultFontSource(FontSource::Default)` is mapped to the default Font asset. +#[derive(Clone, Debug, Reflect, PartialEq, Default, Resource, Deref, DerefMut)] +pub struct DefaultFontSource(pub FontSource); + /// Generic font families that are resolved through Parley's font database. #[derive(Default, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] #[repr(u8)] @@ -1728,7 +1759,7 @@ mod tests { let list = FontSource::List(Vec::new()); assert!(list.flatten().is_empty()); assert_eq!( - list.resolve_font_family(&Assets::::default()), + list.resolve_font_family(&Assets::::default(), &FontSource::Default), Ok(FontFamily::List(Cow::Owned(Vec::new()))) ); } @@ -1742,7 +1773,7 @@ mod tests { let source = FontSource::list([FontSource::from(dummy_font_handle)]); assert_eq!( - source.resolve_font_family(&fonts), + source.resolve_font_family(&fonts, &FontSource::Default), Ok(FontFamily::Single(parley::FontFamilyName::Named( Cow::Borrowed("Dummy Font") ))) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 9afa952b860ff..ee9cef33e4f5e 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -19,9 +19,10 @@ use bevy_log::warn_once; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LetterSpacing, LineBreak, - LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, - TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextSection, TextWriter, + ComputedTextBlock, DefaultFontSource, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, + LetterSpacing, LineBreak, LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, + TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextSection, + TextWriter, }; use taffy::{style::AvailableSpace, MaybeMath, ResolveOrZero}; use tracing::error; @@ -305,6 +306,7 @@ pub fn measure_text_system( mut font_system: ResMut, mut layout_cx: ResMut, rem_size: Res, + default_font_source: Res, ) { for ( entity, @@ -340,6 +342,7 @@ pub fn measure_text_system( &mut layout_cx, computed_target.logical_size(), *rem_size, + &default_font_source.0, ) { Ok(measure) => { if block.linebreak == LineBreak::NoWrap { diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 0e9385d4e4966..04d5a17fca67d 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -18,9 +18,10 @@ use bevy_platform::hash::FixedHasher; use bevy_text::{ add_glyph_to_atlas, cursor_reveal_rect, get_glyph_atlas_info, scrollable_text_layout_width, - EditableText, EditableTextGeneration, Font, FontAtlasKey, FontAtlasSet, FontCx, FontHinting, - FontSize, GlyphCacheKey, LayoutCx, LineBreak, LineHeight, PositionedGlyph, RemSize, - RunGeometry, ScaleCx, TextBrush, TextFont, TextLayout, TextLayoutInfo, TextLineYBounds, + DefaultFontSource, EditableText, EditableTextGeneration, Font, FontAtlasKey, FontAtlasSet, + FontCx, FontHinting, FontSize, GlyphCacheKey, LayoutCx, LineBreak, LineHeight, PositionedGlyph, + RemSize, RunGeometry, ScaleCx, TextBrush, TextFont, TextLayout, TextLayoutInfo, + TextLineYBounds, }; use bevy_time::{Real, Time}; use parley::{BoundingBox, PositionedLayoutItem, StyleProperty}; @@ -90,6 +91,7 @@ pub fn update_editable_text_content_size( fonts: Res>, mut font_cx: ResMut, rem_size: Res, + default_font_source: Res, ) { for (editable_text, text_font, line_height, target, mut content_size, mut size_state) in &mut text_input_query @@ -116,7 +118,11 @@ pub fn update_editable_text_content_size( .collection .query(&mut font_context.source_cache); - match text_font.font.resolve_font_family(fonts.as_ref()).ok()? { + match text_font + .font + .resolve_font_family(fonts.as_ref(), &default_font_source.0) + .ok()? + { parley::FontFamily::Source(source) => { query.set_families( parley::FontFamilyName::parse_css_list(&source) @@ -199,6 +205,7 @@ pub fn update_editable_text_styles( Ref, )>, rem_size: Res, + default_font_source: Res, ) { for (mut editable_text, text_font, line_height, target, text_layout) in editable_text_query.iter_mut() @@ -223,7 +230,9 @@ pub fn update_editable_text_styles( } if text_font.is_changed() - && let Ok(resolved_family) = text_font.font.resolve_font_family(fonts.as_ref()) + && let Ok(resolved_family) = text_font + .font + .resolve_font_family(fonts.as_ref(), &default_font_source.0) { let family = resolved_family.into_owned(); let style_set = editable_text.editor.edit_styles();