Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
abac10f
Added parley depedency
ickshonpe Oct 16, 2025
e8cc12e
Changed font asset to hold font collection information returned from …
ickshonpe Oct 16, 2025
f0bad8d
New Font type wrapping a `Blob` and `Collection`
ickshonpe Oct 16, 2025
970c1f7
fixed default font asset loader
ickshonpe Oct 16, 2025
7183b03
Fixed asset loader for Font
ickshonpe Oct 16, 2025
4af0ac2
Added `Context` module with `TextContext` resource wrapping `FontCont…
ickshonpe Oct 16, 2025
9c6641d
Added `register_font_assets_system` that registers new `Font` assets …
ickshonpe Oct 16, 2025
570f81f
Added layout and scale context to TextContext, made Font's field's pub
ickshonpe Oct 17, 2025
c355a17
Renamed `Justify` to `TextAlign`. New variants `Start` and `End`.
ickshonpe Oct 17, 2025
0b9cd59
Implement some sort of wretched text layout
ickshonpe Oct 17, 2025
0be7d6e
Remove old unneeded params.
ickshonpe Oct 18, 2025
62a99c4
Removed old tests and most of the old UI text implementation to get t…
ickshonpe Oct 20, 2025
e10b6de
Fixed Text2d coords.
ickshonpe Oct 20, 2025
743ae2e
Map bevy LineHeight to parley LineHeight in its eval function
ickshonpe Oct 20, 2025
e074206
updated more examples
ickshonpe Oct 20, 2025
83a343f
Added support for LineBreak
ickshonpe Oct 20, 2025
c8173dc
reuse parley Layout
ickshonpe Oct 20, 2025
883e464
Enabled basic change detection for Text2d
ickshonpe Oct 20, 2025
f201e14
split up hierarhy updates
ickshonpe Oct 20, 2025
e68ca3e
Clean up change detection
ickshonpe Oct 20, 2025
7bd68e5
Removed unused
ickshonpe Oct 20, 2025
c08f946
Renamed ComputedLayout to ComputedTextLayout
ickshonpe Oct 20, 2025
75f6319
Reenable text measurement for UI layout
ickshonpe Oct 20, 2025
0b66657
MeasureArgs takes ComputedTextLayout not ComputedTextBlock
ickshonpe Oct 20, 2025
d796629
Restored some of the ui text wiget module code
ickshonpe Oct 20, 2025
148e825
added new update_text_system
ickshonpe Oct 20, 2025
90a6b23
added TextMeasureInfo
ickshonpe Oct 20, 2025
29b0992
Reimplemented `compute_size` using `parley::Layout`
ickshonpe Oct 20, 2025
34e81a3
Finished new UI systems.
ickshonpe Oct 21, 2025
d8abacd
Fixed Text requirements
ickshonpe Oct 21, 2025
5f9d874
restored most of the old measure func's design
ickshonpe Oct 21, 2025
4f950b1
updated stress tests
ickshonpe Oct 21, 2025
853dad9
Merge branch 'main' into bevy-text-parley
ickshonpe Oct 21, 2025
749d607
Extract colors per slice in bevy_sprite_render
ickshonpe Oct 21, 2025
1357140
Fixed text updates
ickshonpe Oct 21, 2025
fbaba13
trigger recompute text by setting needs_measure flags
ickshonpe Oct 21, 2025
65d15ee
Renamed systems
ickshonpe Oct 21, 2025
1d3c28f
clean up
ickshonpe Oct 21, 2025
83861ba
use the text section index for the parley Brush
ickshonpe Oct 21, 2025
936ffee
Fixed span indices, was using hierarchy depth, not enumerating.
ickshonpe Oct 21, 2025
f48540b
cleanup
ickshonpe Oct 21, 2025
9d1465f
use () brush for for indexed style
ickshonpe Oct 21, 2025
7a94323
Required `TextTarget` on `TextSpan`
ickshonpe Oct 22, 2025
b3a1756
Renamed `TextFlags` fields:
ickshonpe Oct 22, 2025
0ea9600
Removed `Text`/`Text2d` specific detect changes systems
ickshonpe Oct 22, 2025
99997c1
update_text2d_layout checks for root text entity changes
ickshonpe Oct 22, 2025
148edac
`register_font_assets_system` sets all `TextFont`s changed when new f…
ickshonpe Oct 22, 2025
8ee5abf
Removed `update_roots`, cleaned up system schedule, fixed conflicts
ickshonpe Oct 22, 2025
6c07edf
Removed unneeded
ickshonpe Oct 22, 2025
b012384
Merge branch 'main' into bevy-text-parley-deco
ickshonpe Oct 23, 2025
0981369
Another fix for merge with main
ickshonpe Oct 23, 2025
fb66661
* Added new `SectionGeometry` struct to replace tuple used in `TextLa…
ickshonpe Oct 24, 2025
c97483a
Cleanup, made strikethrough example work.
ickshonpe Oct 24, 2025
aee450c
Merge branch 'main' into bevy-text-parley-deco
ickshonpe Oct 25, 2025
5229098
removed unused imports
ickshonpe Oct 25, 2025
348dab2
Applied changes from `RunGeometry` PR.
ickshonpe Oct 25, 2025
36ed358
Merge branch 'main' into bevy-text-parley
ickshonpe Oct 29, 2025
2f5336e
Reverted changes to text example
ickshonpe Oct 29, 2025
1737b6f
Reverted Justify renaming
ickshonpe Oct 29, 2025
8618817
Use the asset path as the family name in the font collections,, store…
ickshonpe Oct 29, 2025
e294c61
Added basic doc comments
ickshonpe Oct 29, 2025
c6cb6f3
reverted example changes
ickshonpe Oct 29, 2025
5bb31fe
Revert remaining systems
ickshonpe Oct 29, 2025
bd97a43
Collect entities in ComputedTextBlock.
ickshonpe Oct 29, 2025
9b55551
Merge branch 'main' into bevy-text-parley-minimal
ickshonpe Oct 29, 2025
e3e5e58
Fixed some examples
ickshonpe Oct 29, 2025
3b12e79
Merge branch 'main' into bevy-text-parley-minimal
ickshonpe Oct 30, 2025
c6a8896
Removed unneeded changed checks in text2d
ickshonpe Oct 30, 2025
fcceae9
Removed redundant change detection and queries from text widget
ickshonpe Oct 30, 2025
6346f10
use parley fast-line-height changes
ickshonpe Nov 4, 2025
3fdc0eb
Reverted changes to text_debug example
ickshonpe Nov 6, 2025
4091816
Merge branch 'main' into bevy-text-parley-minimal
ickshonpe Nov 6, 2025
3e472cf
Call `path` not `asset_path` on LoadContext in font_loader.
ickshonpe Nov 6, 2025
578f990
Merge branch 'main' into bevy-text-parley-minimal-update
ickshonpe Nov 25, 2025
5cee541
Reimplemented font features for Parley
ickshonpe Nov 25, 2025
b44242a
updated parley version to 0.7.0
ickshonpe Nov 25, 2025
59c1815
Removed unused import
ickshonpe Nov 25, 2025
876c267
Eliminate some allocations and cloning by passing the `TextReader` in…
ickshonpe Nov 25, 2025
beaaa60
Removed unused
ickshonpe Nov 25, 2025
d136c76
Renamed `shape_text_from_reader` to `shape_text`.
ickshonpe Nov 25, 2025
05b32e5
Added back `TextPipeline` with the resuable spans buffers.
ickshonpe Nov 25, 2025
bdbc507
silence unused warning in `many_text2d`
ickshonpe Nov 25, 2025
a968c1b
`FontFeatures` refactor. Internally `FontFeatures` now wraps a parley…
ickshonpe Nov 25, 2025
7531298
Removed unused import.
ickshonpe Nov 25, 2025
6da9cce
Added `clear` method to `TextLayoutInfo`.
ickshonpe Nov 25, 2025
2c39d57
Changed `update_text_layout_info` to take a `TextLayoutInfo` mutable …
ickshonpe Nov 25, 2025
bfe06e1
removed unneeded deref
ickshonpe Nov 25, 2025
f9e73f1
Add draft release content
ickshonpe Nov 25, 2025
626b4e2
Merge branch 'main' into bevy-text-parley-minimal
ickshonpe Nov 26, 2025
bf83a3d
Fixed typo
ickshonpe Nov 26, 2025
19d91d9
Merge branch 'bevy-text-parley-minimal' of https://github.com/ickshon…
ickshonpe Nov 26, 2025
7982fdb
fixed migration note formatting
ickshonpe Nov 26, 2025
4be8f9c
Added `font_smoothing: FontSmoothing` parameter to `get_outlined_gly…
ickshonpe Nov 26, 2025
936ce74
Add the `FontSmoothing` setting to layout brush
ickshonpe Nov 26, 2025
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
7 changes: 4 additions & 3 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ use bevy_camera::{
visibility::VisibilitySystems,
};
use bevy_mesh::{Mesh, Mesh2d};
#[cfg(feature = "bevy_text")]
use bevy_text::TextSystems;
#[cfg(feature = "bevy_picking")]
pub use picking_backend::*;
pub use sprite::*;
Expand Down Expand Up @@ -81,13 +83,12 @@ impl Plugin for SpritePlugin {
PostUpdate,
(
bevy_text::detect_text_needs_rerender::<Text2d>,
update_text2d_layout
.after(bevy_camera::CameraUpdateSystems)
.after(bevy_text::free_unused_font_atlases_system),
update_text2d_layout.after(bevy_camera::CameraUpdateSystems),
calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
)
.chain()
.in_set(bevy_text::Text2dUpdateSystems)
.after(TextSystems::RegisterFontAssets)
.after(bevy_app::AnimationSystems),
);

Expand Down
250 changes: 57 additions & 193 deletions crates/bevy_sprite/src/text2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@ use bevy_camera::visibility::{
use bevy_camera::Camera;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::entity::EntityHashSet;

use bevy_ecs::change_detection::DetectChanges;
use bevy_ecs::system::Res;
use bevy_ecs::{
change_detection::{DetectChanges, Ref},
change_detection::Ref,
component::Component,
entity::Entity,
prelude::ReflectComponent,
query::{Changed, Without},
system::{Commands, Local, Query, Res, ResMut},
system::{Commands, Local, Query, ResMut},
};
use bevy_image::prelude::*;
use bevy_math::{FloatOrd, Vec2, Vec3};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_text::{
ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSet, LineBreak, LineHeight, SwashCache,
TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline,
TextReader, TextRoot, TextSpanAccess, TextWriter,
update_text_layout_info, ComputedTextBlock, ComputedTextLayout, Font, FontAtlasSet, FontCx,
LayoutCx, LineHeight, ScaleCx, TextBounds, TextColor, TextFont, TextHead, TextLayout,
TextLayoutInfo, TextPipeline, TextReader, TextSpanAccess, TextWriter,
};
use bevy_transform::components::Transform;
use core::any::TypeId;
Expand Down Expand Up @@ -88,7 +90,10 @@ use core::any::TypeId;
Anchor,
Visibility,
VisibilityClass,
Transform
ComputedTextBlock,
TextLayoutInfo,
Transform,
ComputedTextLayout
)]
#[component(on_add = visibility::add_visibility_class::<Sprite>)]
pub struct Text2d(pub String);
Expand All @@ -100,7 +105,7 @@ impl Text2d {
}
}

impl TextRoot for Text2d {}
impl TextHead for Text2d {}

impl TextSpanAccess for Text2d {
fn read_span(&self) -> &str {
Expand Down Expand Up @@ -160,25 +165,25 @@ impl Default for Text2dShadow {
/// It does not modify or observe existing ones.
pub fn update_text2d_layout(
mut target_scale_factors: Local<Vec<(f32, RenderLayers)>>,
// Text items which should be reprocessed again, generally when the font hasn't loaded yet.
mut queue: Local<EntityHashSet>,
mut text_pipeline: ResMut<TextPipeline>,
mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>,
camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_set: ResMut<FontAtlasSet>,
mut text_pipeline: ResMut<TextPipeline>,
mut font_cx: ResMut<FontCx>,
mut layout_cx: ResMut<LayoutCx>,
mut scale_cx: ResMut<ScaleCx>,
mut text_query: Query<(
Entity,
Option<&RenderLayers>,
Ref<TextLayout>,
Ref<TextBounds>,
&mut TextLayoutInfo,
&mut ComputedTextBlock,
&mut ComputedTextLayout,
)>,
mut text_reader: Text2dReader,
mut font_system: ResMut<CosmicFontSystem>,
mut swash_cache: ResMut<SwashCache>,
) {
target_scale_factors.clear();
target_scale_factors.extend(
Expand All @@ -197,8 +202,15 @@ pub fn update_text2d_layout(
let mut previous_scale_factor = 0.;
let mut previous_mask = &RenderLayers::none();

for (entity, maybe_entity_mask, block, bounds, text_layout_info, mut computed) in
&mut text_query
for (
entity,
maybe_entity_mask,
block,
bounds,
mut text_layout_info,
mut computed,
mut layout,
) in &mut text_query
{
let entity_mask = maybe_entity_mask.unwrap_or_default();

Expand All @@ -219,55 +231,39 @@ pub fn update_text2d_layout(
*scale_factor
};

if scale_factor != text_layout_info.scale_factor
|| computed.needs_rerender()
if !(computed.needs_rerender()
|| block.is_changed()
|| bounds.is_changed()
|| (!queue.is_empty() && queue.remove(&entity))
|| scale_factor != text_layout_info.scale_factor)
{
let text_bounds = TextBounds {
width: if block.linebreak == LineBreak::NoWrap {
None
} else {
bounds.width.map(|width| width * scale_factor)
},
height: bounds.height.map(|height| height * scale_factor),
};

let text_layout_info = text_layout_info.into_inner();
match text_pipeline.queue_text(
text_layout_info,
&fonts,
text_reader.iter(entity),
scale_factor as f64,
&block,
text_bounds,
&mut font_atlas_set,
&mut texture_atlases,
&mut textures,
computed.as_mut(),
&mut font_system,
&mut swash_cache,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, let's add this entity to the
// queue for further processing
queue.insert(entity);
}
Err(
e @ (TextError::FailedToAddGlyph(_)
| TextError::FailedToGetGlyphImage(_)
| TextError::MissingAtlasLayout
| TextError::MissingAtlasTexture
| TextError::InconsistentAtlasState),
) => {
panic!("Fatal error when processing text: {e}.");
}
Ok(()) => {
text_layout_info.scale_factor = scale_factor;
text_layout_info.size *= scale_factor.recip();
}
}
continue;
}

computed.needs_rerender = false;
computed.entities.clear();

text_pipeline.shape_text(
entity,
&mut text_reader,
&mut layout.0,
&mut font_cx.0,
&mut layout_cx.0,
scale_factor,
block.linebreak,
&fonts,
&mut computed.entities,
);

update_text_layout_info(
&mut text_layout_info,
&mut layout.0,
bounds.width.map(|w| w * scale_factor),
block.justify.into(),
&mut scale_cx,
&mut font_atlas_set,
&mut texture_atlases,
&mut textures,
);
}
}

Expand Down Expand Up @@ -307,135 +303,3 @@ pub fn calculate_bounds_text2d(
}
}
}

#[cfg(test)]
mod tests {

use bevy_app::{App, Update};
use bevy_asset::{load_internal_binary_asset, Handle};
use bevy_camera::{ComputedCameraValues, RenderTargetInfo};
use bevy_ecs::schedule::IntoScheduleConfigs;
use bevy_math::UVec2;
use bevy_text::{detect_text_needs_rerender, TextIterScratch};

use super::*;

const FIRST_TEXT: &str = "Sample text.";
const SECOND_TEXT: &str = "Another, longer sample text.";

fn setup() -> (App, Entity) {
let mut app = App::new();
app.init_resource::<Assets<Font>>()
.init_resource::<Assets<Image>>()
.init_resource::<Assets<TextureAtlasLayout>>()
.init_resource::<FontAtlasSet>()
.init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>()
.init_resource::<SwashCache>()
.init_resource::<TextIterScratch>()
.add_systems(
Update,
(
detect_text_needs_rerender::<Text2d>,
update_text2d_layout,
calculate_bounds_text2d,
)
.chain(),
);

let mut visible_entities = VisibleEntities::default();
visible_entities.push(Entity::PLACEHOLDER, TypeId::of::<Sprite>());

app.world_mut().spawn((
Camera {
computed: ComputedCameraValues {
target_info: Some(RenderTargetInfo {
physical_size: UVec2::splat(1000),
scale_factor: 1.,
}),
..Default::default()
},
..Default::default()
},
visible_entities,
));

// A font is needed to ensure the text is laid out with an actual size.
load_internal_binary_asset!(
app,
Handle::default(),
"../../bevy_text/src/FiraMono-subset.ttf",
|bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() }
);

let entity = app.world_mut().spawn(Text2d::new(FIRST_TEXT)).id();

(app, entity)
}

#[test]
fn calculate_bounds_text2d_create_aabb() {
let (mut app, entity) = setup();

assert!(!app
.world()
.get_entity(entity)
.expect("Could not find entity")
.contains::<Aabb>());

// Creates the AABB after text layouting.
app.update();

let aabb = app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Text should have an AABB");

// Text2D AABB does not have a depth.
assert_eq!(aabb.center.z, 0.0);
assert_eq!(aabb.half_extents.z, 0.0);

// AABB has an actual size.
assert!(aabb.half_extents.x > 0.0 && aabb.half_extents.y > 0.0);
}

#[test]
fn calculate_bounds_text2d_update_aabb() {
let (mut app, entity) = setup();

// Creates the initial AABB after text layouting.
app.update();

let first_aabb = *app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find initial AABB");

let mut entity_ref = app
.world_mut()
.get_entity_mut(entity)
.expect("Could not find entity");
*entity_ref
.get_mut::<Text2d>()
.expect("Missing Text2d on entity") = Text2d::new(SECOND_TEXT);

// Recomputes the AABB.
app.update();

let second_aabb = *app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find second AABB");

// Check that the height is the same, but the width is greater.
approx::assert_abs_diff_eq!(first_aabb.half_extents.y, second_aabb.half_extents.y);
assert!(FIRST_TEXT.len() < SECOND_TEXT.len());
assert!(first_aabb.half_extents.x < second_aabb.half_extents.x);
}
}
3 changes: 2 additions & 1 deletion crates/bevy_text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ bevy_platform = { path = "../bevy_platform", version = "0.18.0-dev", default-fea

# other
wgpu-types = { version = "26", default-features = false }
cosmic-text = { version = "0.15", features = ["shape-run-cache"] }
thiserror = { version = "2", default-features = false }
serde = { version = "1", features = ["derive"] }
smallvec = { version = "1", default-features = false }
sys-locale = "0.3.0"
tracing = { version = "0.1", default-features = false, features = ["std"] }
parley = { version = "0.7.0" }
swash = { version = "0.2.6" }

[lints]
workspace = true
Expand Down
20 changes: 20 additions & 0 deletions crates/bevy_text/src/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use bevy_derive::Deref;
use bevy_derive::DerefMut;
use bevy_ecs::resource::Resource;
use parley::FontContext;
use parley::LayoutContext;
use swash::scale::ScaleContext;

use crate::FontSmoothing;

/// Font context
#[derive(Resource, Default, Deref, DerefMut)]
pub struct FontCx(pub FontContext);

/// Text layout context
#[derive(Resource, Default, Deref, DerefMut)]
pub struct LayoutCx(pub LayoutContext<(u32, FontSmoothing)>);

/// Text scaler context
#[derive(Resource, Default, Deref, DerefMut)]
pub struct ScaleCx(pub ScaleContext);
3 changes: 1 addition & 2 deletions crates/bevy_text/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use cosmic_text::CacheKey;
use thiserror::Error;

#[derive(Debug, PartialEq, Eq, Error)]
Expand All @@ -13,7 +12,7 @@ pub enum TextError {
FailedToAddGlyph(u16),
/// Failed to get scaled glyph image for cache key
#[error("failed to get scaled glyph image for cache key: {0:?}")]
FailedToGetGlyphImage(CacheKey),
FailedToGetGlyphImage(u16),
/// Missing texture atlas layout for the font
#[error("missing texture atlas layout for the font")]
MissingAtlasLayout,
Expand Down
Loading
Loading