Skip to content

Commit

Permalink
Text rework (#15591)
Browse files Browse the repository at this point in the history
**Ready for review. Examples migration progress: 100%.**

# Objective

- Implement #15014

## Solution

This implements [cart's
proposal](#15014 (comment))
faithfully except for one change. I separated `TextSpan` from
`TextSpan2d` because `TextSpan` needs to require the `GhostNode`
component, which is a `bevy_ui` component only usable by UI.

Extra changes:
- Added `EntityCommands::commands_mut` that returns a mutable reference.
This is a blocker for extension methods that return something other than
`self`. Note that `sickle_ui`'s `UiBuilder::commands` returns a mutable
reference for this reason.

## Testing

- [x] Text examples all work.

---

## Showcase

TODO: showcase-worthy

## Migration Guide

TODO: very breaking

### Accessing text spans by index

Text sections are now text sections on different entities in a
hierarchy, Use the new `TextReader` and `TextWriter` system parameters
to access spans by index.

Before:
```rust
fn refresh_text(mut query: Query<&mut Text, With<TimeText>>, time: Res<Time>) {
    let text = query.single_mut();
    text.sections[1].value = format_time(time.elapsed());
}
```

After:
```rust
fn refresh_text(
    query: Query<Entity, With<TimeText>>,
    mut writer: UiTextWriter,
    time: Res<Time>
) {
    let entity = query.single();
    *writer.text(entity, 1) = format_time(time.elapsed());
}
```

### Iterating text spans

Text spans are now entities in a hierarchy, so the new `UiTextReader`
and `UiTextWriter` system parameters provide ways to iterate that
hierarchy. The `UiTextReader::iter` method will give you a normal
iterator over spans, and `UiTextWriter::for_each` lets you visit each of
the spans.

---------

Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
  • Loading branch information
3 people authored Oct 9, 2024
1 parent 0b2e0cf commit c2c19e5
Show file tree
Hide file tree
Showing 146 changed files with 3,098 additions and 2,708 deletions.
41 changes: 24 additions & 17 deletions crates/bevy_dev_tools/src/fps_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
use bevy_ecs::{
change_detection::DetectChangesMut,
component::Component,
entity::Entity,
query::With,
schedule::{common_conditions::resource_changed, IntoSystemConfigs},
system::{Commands, Query, Res, Resource},
};
use bevy_hierarchy::{BuildChildren, ChildBuild};
use bevy_render::view::Visibility;
use bevy_text::{Font, Text, TextSection, TextStyle};
use bevy_text::{Font, TextSpan, TextStyle};
use bevy_ui::{
node_bundles::{NodeBundle, TextBundle},
node_bundles::NodeBundle,
widget::{Text, UiTextWriter},
GlobalZIndex, PositionType, Style,
};
use bevy_utils::default;
Expand Down Expand Up @@ -72,6 +74,7 @@ impl Default for FpsOverlayConfig {
font: Handle::<Font>::default(),
font_size: 32.0,
color: Color::WHITE,
..default()
},
enabled: true,
}
Expand All @@ -95,35 +98,39 @@ fn setup(mut commands: Commands, overlay_config: Res<FpsOverlayConfig>) {
},
GlobalZIndex(FPS_OVERLAY_ZINDEX),
))
.with_children(|c| {
c.spawn((
TextBundle::from_sections([
TextSection::new("FPS: ", overlay_config.text_config.clone()),
TextSection::from_style(overlay_config.text_config.clone()),
]),
.with_children(|p| {
p.spawn((
Text::new("FPS: "),
overlay_config.text_config.clone(),
FpsText,
));
))
.with_child((TextSpan::default(), overlay_config.text_config.clone()));
});
}

fn update_text(diagnostic: Res<DiagnosticsStore>, mut query: Query<&mut Text, With<FpsText>>) {
for mut text in &mut query {
fn update_text(
diagnostic: Res<DiagnosticsStore>,
query: Query<Entity, With<FpsText>>,
mut writer: UiTextWriter,
) {
for entity in &query {
if let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS) {
if let Some(value) = fps.smoothed() {
text.sections[1].value = format!("{value:.2}");
*writer.text(entity, 1) = format!("{value:.2}");
}
}
}
}

fn customize_text(
overlay_config: Res<FpsOverlayConfig>,
mut query: Query<&mut Text, With<FpsText>>,
query: Query<Entity, With<FpsText>>,
mut writer: UiTextWriter,
) {
for mut text in &mut query {
for section in text.sections.iter_mut() {
section.style = overlay_config.text_config.clone();
}
for entity in &query {
writer.for_each_style(entity, |mut style| {
*style = overlay_config.text_config.clone();
});
}
}

Expand Down
7 changes: 6 additions & 1 deletion crates/bevy_ecs/src/system/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,7 @@ pub struct EntityCommands<'a> {
pub(crate) commands: Commands<'a, 'a>,
}

impl EntityCommands<'_> {
impl<'a> EntityCommands<'a> {
/// Returns the [`Entity`] id of the entity.
///
/// # Example
Expand Down Expand Up @@ -1533,6 +1533,11 @@ impl EntityCommands<'_> {
self.commands.reborrow()
}

/// Returns a mutable reference to the underlying [`Commands`].
pub fn commands_mut(&mut self) -> &mut Commands<'a, 'a> {
&mut self.commands
}

/// Sends a [`Trigger`] targeting this entity. This will run any [`Observer`] of the `event` that
/// watches this entity.
///
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.15.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
"bevy",
Expand All @@ -36,6 +37,7 @@ derive_more = { version = "1", default-features = false, features = [
"display",
] }
serde = { version = "1", features = ["derive"] }
smallvec = "1.13"
unicode-bidi = "0.3.13"
sys-locale = "0.3.0"

Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_text/src/font_atlas_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ pub struct FontAtlasKey(pub u32, pub FontSmoothing);
/// A `FontAtlasSet` is an [`Asset`].
///
/// There is one `FontAtlasSet` for each font:
/// - When a [`Font`] is loaded as an asset and then used in [`Text`](crate::Text),
/// - When a [`Font`] is loaded as an asset and then used in [`TextStyle`](crate::TextStyle),
/// a `FontAtlasSet` asset is created from a weak handle to the `Font`.
/// - ~When a font is loaded as a system font, and then used in [`Text`](crate::Text),
/// - ~When a font is loaded as a system font, and then used in [`TextStyle`](crate::TextStyle),
/// a `FontAtlasSet` asset is created and stored with a strong handle to the `FontAtlasSet`.~
/// (*Note that system fonts are not currently supported by the `TextPipeline`.*)
///
Expand Down
15 changes: 5 additions & 10 deletions crates/bevy_text/src/glyph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ use bevy_sprite::TextureAtlasLayout;
/// Used in [`TextPipeline::queue_text`](crate::TextPipeline::queue_text) and [`crate::TextLayoutInfo`] for rendering glyphs.
#[derive(Debug, Clone, Reflect)]
pub struct PositionedGlyph {
/// The position of the glyph in the [`Text`](crate::Text)'s bounding box.
/// The position of the glyph in the text block's bounding box.
pub position: Vec2,
/// The width and height of the glyph in logical pixels.
pub size: Vec2,
/// Information about the glyph's atlas.
pub atlas_info: GlyphAtlasInfo,
/// The index of the glyph in the [`Text`](crate::Text)'s sections.
pub section_index: usize,
/// The index of the glyph in the [`ComputedTextBlock`](crate::ComputedTextBlock)'s tracked spans.
pub span_index: usize,
/// TODO: In order to do text editing, we need access to the size of glyphs and their index in the associated String.
/// For example, to figure out where to place the cursor in an input box from the mouse's position.
/// Without this, it's only possible in texts where each glyph is one byte. Cosmic text has methods for this
Expand All @@ -30,17 +30,12 @@ pub struct PositionedGlyph {

impl PositionedGlyph {
/// Creates a new [`PositionedGlyph`]
pub fn new(
position: Vec2,
size: Vec2,
atlas_info: GlyphAtlasInfo,
section_index: usize,
) -> Self {
pub fn new(position: Vec2, size: Vec2, atlas_info: GlyphAtlasInfo, span_index: usize) -> Self {
Self {
position,
size,
atlas_info,
section_index,
span_index,
byte_index: 0,
}
}
Expand Down
35 changes: 21 additions & 14 deletions crates/bevy_text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//!
//! The [`TextPipeline`] resource does all of the heavy lifting for rendering text.
//!
//! [`Text`] is first measured by creating a [`TextMeasureInfo`] in [`TextPipeline::create_text_measure`],
//! UI `Text` is first measured by creating a [`TextMeasureInfo`] in [`TextPipeline::create_text_measure`],
//! which is called by the `measure_text_system` system of `bevy_ui`.
//!
//! Note that text measurement is only relevant in a UI context.
Expand All @@ -23,7 +23,7 @@
//! or [`text2d::update_text2d_layout`] system (in a 2d world space context)
//! passes it into [`TextPipeline::queue_text`], which:
//!
//! 1. creates a [`Buffer`](cosmic_text::Buffer) from the [`TextSection`]s, generating new [`FontAtlasSet`]s if necessary.
//! 1. updates a [`Buffer`](cosmic_text::Buffer) from the [`TextSpan`]s, generating new [`FontAtlasSet`]s if necessary.
//! 2. iterates over each glyph in the [`Buffer`](cosmic_text::Buffer) to create a [`PositionedGlyph`],
//! retrieving glyphs from the cache, or rasterizing to a [`FontAtlas`] if necessary.
//! 3. [`PositionedGlyph`]s are stored in a [`TextLayoutInfo`],
Expand All @@ -43,6 +43,7 @@ mod glyph;
mod pipeline;
mod text;
mod text2d;
mod text_access;

pub use cosmic_text;

Expand All @@ -56,13 +57,17 @@ pub use glyph::*;
pub use pipeline::*;
pub use text::*;
pub use text2d::*;
pub use text_access::*;

/// The text prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[doc(hidden)]
pub use crate::{Font, JustifyText, Text, Text2dBundle, TextError, TextSection, TextStyle};
pub use crate::{
Font, JustifyText, LineBreak, Text2d, TextBlock, TextError, TextReader2d, TextSpan,
TextStyle, TextWriter2d,
};
}

use bevy_app::prelude::*;
Expand All @@ -87,7 +92,7 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf");
pub struct TextPlugin;

/// Text is rendered for two different view projections;
/// 2-dimensional text ([`Text2dBundle`]) is rendered in "world space" with a `BottomToTop` Y-axis,
/// 2-dimensional text ([`Text2d`]) is rendered in "world space" with a `BottomToTop` Y-axis,
/// while UI is rendered with a `TopToBottom` Y-axis.
/// This matters for text because the glyph positioning is different in either layout.
/// For `TopToBottom`, 0 is the top of the text, while for `BottomToTop` 0 is the bottom.
Expand All @@ -98,35 +103,37 @@ pub enum YAxisOrientation {
BottomToTop,
}

/// A convenient alias for `With<Text>`, for use with
/// [`bevy_render::view::VisibleEntities`].
pub type WithText = With<Text>;
/// System set in [`PostUpdate`] where all 2d text update systems are executed.
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub struct Update2dText;

impl Plugin for TextPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<Font>()
.register_type::<Text>()
.register_type::<Text2d>()
.register_type::<TextSpan>()
.register_type::<TextBounds>()
.init_asset_loader::<FontLoader>()
.init_resource::<FontAtlasSets>()
.init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>()
.init_resource::<SwashCache>()
.init_resource::<TextIterScratch>()
.add_systems(
PostUpdate,
(
calculate_bounds_text2d
.in_set(VisibilitySystems::CalculateBounds)
.after(update_text2d_layout),
remove_dropped_font_atlas_sets,
detect_text_needs_rerender::<Text2d>,
update_text2d_layout
.after(remove_dropped_font_atlas_sets)
// Potential conflict: `Assets<Image>`
// In practice, they run independently since `bevy_render::camera_update_system`
// will only ever observe its own render target, and `update_text2d_layout`
// will never modify a pre-existing `Image` asset.
.ambiguous_with(CameraUpdateSystem),
remove_dropped_font_atlas_sets,
),
calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
)
.chain()
.in_set(Update2dText),
)
.add_systems(Last, trim_cosmic_cache);

Expand Down
Loading

0 comments on commit c2c19e5

Please sign in to comment.