Text rework (#15591)

**Ready for review. Examples migration progress: 100%.**

# Objective

- Implement https://github.com/bevyengine/bevy/discussions/15014

## Solution

This implements [cart's
proposal](https://github.com/bevyengine/bevy/discussions/15014#discussioncomment-10574459)
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>
This commit is contained in:
UkoeHB 2024-10-09 13:35:36 -05:00 committed by GitHub
parent 0b2e0cfaca
commit c2c19e5ae4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
146 changed files with 3102 additions and 2712 deletions

View file

@ -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;
@ -72,6 +74,7 @@ impl Default for FpsOverlayConfig {
font: Handle::<Font>::default(),
font_size: 32.0,
color: Color::WHITE,
..default()
},
enabled: true,
}
@ -95,22 +98,25 @@ 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}");
}
}
}
@ -118,12 +124,13 @@ fn update_text(diagnostic: Res<DiagnosticsStore>, mut query: Query<&mut Text, Wi
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();
});
}
}

View file

@ -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
@ -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.
///

View file

@ -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",
@ -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"

View file

@ -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`.*)
///

View file

@ -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
@ -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,
}
}

View file

@ -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.
@ -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`],
@ -43,6 +43,7 @@ mod glyph;
mod pipeline;
mod text;
mod text2d;
mod text_access;
pub use cosmic_text;
@ -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::*;
@ -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.
@ -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);

View file

@ -16,8 +16,8 @@ use bevy_utils::HashMap;
use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};
use crate::{
error::TextError, CosmicBuffer, Font, FontAtlasSets, FontSmoothing, JustifyText, LineBreak,
PositionedGlyph, TextBounds, TextSection, TextStyle, YAxisOrientation,
error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText,
LineBreak, PositionedGlyph, TextBlock, TextBounds, TextEntity, TextStyle, YAxisOrientation,
};
/// A wrapper resource around a [`cosmic_text::FontSystem`]
@ -60,7 +60,7 @@ struct FontFaceInfo {
family_name: Arc<str>,
}
/// The `TextPipeline` is used to layout and render [`Text`](crate::Text).
/// The `TextPipeline` is used to layout and render text blocks (see `Text`/[`Text2d`](crate::Text2d)).
///
/// See the [crate-level documentation](crate) for more information.
#[derive(Default, Resource)]
@ -71,6 +71,8 @@ pub struct TextPipeline {
///
/// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10).
spans_buffer: Vec<(usize, &'static str, &'static TextStyle, FontFaceInfo)>,
/// Buffered vec for collecting info for glyph assembly.
glyph_info: Vec<(AssetId<Font>, FontSmoothing)>,
}
impl TextPipeline {
@ -81,12 +83,12 @@ impl TextPipeline {
pub fn update_buffer<'a>(
&mut self,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (&'a str, &'a TextStyle)>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextStyle)>,
linebreak: LineBreak,
justify: JustifyText,
bounds: TextBounds,
scale_factor: f64,
buffer: &mut CosmicBuffer,
alignment: JustifyText,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
) -> Result<(), TextError> {
let font_system = &mut font_system.0;
@ -100,7 +102,9 @@ impl TextPipeline {
.map(|_| -> (usize, &str, &TextStyle, FontFaceInfo) { unreachable!() })
.collect();
for (span_index, (span, style)) in text_spans.enumerate() {
computed.entities.clear();
for (span_index, (entity, depth, span, style)) in text_spans.enumerate() {
// Return early if a font is not loaded yet.
if !fonts.contains(style.font.id()) {
spans.clear();
@ -116,6 +120,9 @@ impl TextPipeline {
return Err(TextError::NoSuchFont);
}
// Save this span entity in the computed text block.
computed.entities.push(TextEntity { entity, depth });
// Get max font size for use in cosmic Metrics.
font_size = font_size.max(style.font_size);
@ -152,6 +159,7 @@ impl TextPipeline {
});
// Update the buffer.
let buffer = &mut computed.buffer;
buffer.set_metrics(font_system, metrics);
buffer.set_size(font_system, bounds.width, bounds.height);
@ -170,7 +178,7 @@ impl TextPipeline {
// PERF: https://github.com/pop-os/cosmic-text/issues/166:
// Setting alignment afterwards appears to invalidate some layouting performed by `set_text` which is presumably not free?
for buffer_line in buffer.lines.iter_mut() {
buffer_line.set_align(Some(alignment.into()));
buffer_line.set_align(Some(justify.into()));
}
buffer.shape_until_scroll(font_system, false);
@ -189,47 +197,54 @@ impl TextPipeline {
/// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s
/// which contain information for rendering the text.
#[allow(clippy::too_many_arguments)]
pub fn queue_text(
pub fn queue_text<'a>(
&mut self,
layout_info: &mut TextLayoutInfo,
fonts: &Assets<Font>,
sections: &[TextSection],
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextStyle)>,
scale_factor: f64,
text_alignment: JustifyText,
linebreak: LineBreak,
font_smoothing: FontSmoothing,
block: &TextBlock,
bounds: TextBounds,
font_atlas_sets: &mut FontAtlasSets,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
y_axis_orientation: YAxisOrientation,
buffer: &mut CosmicBuffer,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
swash_cache: &mut SwashCache,
) -> Result<(), TextError> {
layout_info.glyphs.clear();
layout_info.size = Default::default();
if sections.is_empty() {
return Ok(());
}
// Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries.
computed.needs_rerender = false;
self.update_buffer(
// Extract font ids from the iterator while traversing it.
let mut glyph_info = core::mem::take(&mut self.glyph_info);
glyph_info.clear();
let text_spans = text_spans.inspect(|(_, _, _, style)| {
glyph_info.push((style.font.id(), style.font_smoothing));
});
let update_result = self.update_buffer(
fonts,
sections
.iter()
.map(|section| (section.value.as_str(), &section.style)),
linebreak,
text_spans,
block.linebreak,
block.justify,
bounds,
scale_factor,
buffer,
text_alignment,
computed,
font_system,
)?;
);
if let Err(err) = update_result {
self.glyph_info = glyph_info;
return Err(err);
}
let buffer = &mut computed.buffer;
let box_size = buffer_dimensions(buffer);
buffer
let result = buffer
.layout_runs()
.flat_map(|run| {
run.glyphs
@ -238,6 +253,9 @@ impl TextPipeline {
})
.try_for_each(|(layout_glyph, line_y)| {
let mut temp_glyph;
let span_index = layout_glyph.metadata;
let font_id = glyph_info[span_index].0;
let font_smoothing = glyph_info[span_index].1;
let layout_glyph = if font_smoothing == FontSmoothing::None {
// If font smoothing is disabled, round the glyph positions and sizes,
@ -255,10 +273,7 @@ impl TextPipeline {
layout_glyph
};
let section_index = layout_glyph.metadata;
let font_handle = sections[section_index].style.font.clone_weak();
let font_atlas_set = font_atlas_sets.sets.entry(font_handle.id()).or_default();
let font_atlas_set = font_atlas_sets.sets.entry(font_id).or_default();
let physical_glyph = layout_glyph.physical((0., 0.), 1.);
@ -296,10 +311,16 @@ impl TextPipeline {
// TODO: recreate the byte index, that keeps track of where a cursor is,
// when glyphs are not limited to single byte representation, relevant for #1319
let pos_glyph =
PositionedGlyph::new(position, glyph_size.as_vec2(), atlas_info, section_index);
PositionedGlyph::new(position, glyph_size.as_vec2(), atlas_info, span_index);
layout_info.glyphs.push(pos_glyph);
Ok(())
})?;
});
// Return the scratch vec.
self.glyph_info = glyph_info;
// Check result.
result?;
layout_info.size = box_size;
Ok(())
@ -310,32 +331,34 @@ impl TextPipeline {
/// Produces a [`TextMeasureInfo`] which can be used by a layout system
/// to measure the text area on demand.
#[allow(clippy::too_many_arguments)]
pub fn create_text_measure(
pub fn create_text_measure<'a>(
&mut self,
entity: Entity,
fonts: &Assets<Font>,
sections: &[TextSection],
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextStyle)>,
scale_factor: f64,
linebreak: LineBreak,
buffer: &mut CosmicBuffer,
text_alignment: JustifyText,
block: &TextBlock,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
) -> Result<TextMeasureInfo, TextError> {
const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0);
// Clear this here at the focal point of measured text rendering to ensure the field's lifecycle has
// strong boundaries.
computed.needs_rerender = false;
self.update_buffer(
fonts,
sections
.iter()
.map(|section| (section.value.as_str(), &section.style)),
linebreak,
text_spans,
block.linebreak,
block.justify,
MIN_WIDTH_CONTENT_BOUNDS,
scale_factor,
buffer,
text_alignment,
computed,
font_system,
)?;
let buffer = &mut computed.buffer;
let min_width_content_size = buffer_dimensions(buffer);
let max_width_content_size = {
@ -360,9 +383,10 @@ impl TextPipeline {
}
}
/// Render information for a corresponding [`Text`](crate::Text) component.
/// Render information for a corresponding text block.
///
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`].
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`] when an entity has
/// [`TextBlock`] and [`ComputedTextBlock`] components.
#[derive(Component, Clone, Default, Debug, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct TextLayoutInfo {
@ -372,7 +396,7 @@ pub struct TextLayoutInfo {
pub size: Vec2,
}
/// Size information for a corresponding [`Text`](crate::Text) component.
/// Size information for a corresponding [`ComputedTextBlock`] component.
///
/// Generated via [`TextPipeline::create_text_measure`].
#[derive(Debug)]
@ -390,13 +414,15 @@ impl TextMeasureInfo {
pub fn compute_size(
&mut self,
bounds: TextBounds,
buffer: &mut Buffer,
computed: &mut ComputedTextBlock,
font_system: &mut cosmic_text::FontSystem,
) -> Vec2 {
// Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed'
// whenever a canonical state is required.
buffer.set_size(font_system, bounds.width, bounds.height);
buffer_dimensions(buffer)
computed
.buffer
.set_size(font_system, bounds.width, bounds.height);
buffer_dimensions(&computed.buffer)
}
}

View file

@ -1,20 +1,22 @@
use bevy_asset::Handle;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_reflect::prelude::*;
use bevy_utils::default;
use cosmic_text::{Buffer, Metrics};
use serde::{Deserialize, Serialize};
use crate::Font;
pub use cosmic_text::{
self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle,
Weight as FontWeight,
};
use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent};
use bevy_asset::Handle;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
use bevy_hierarchy::{Children, Parent};
use bevy_reflect::prelude::*;
use bevy_utils::warn_once;
use cosmic_text::{Buffer, Metrics};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
/// Wrapper for [`cosmic_text::Buffer`]
#[derive(Component, Deref, DerefMut, Debug, Clone)]
#[derive(Deref, DerefMut, Debug, Clone)]
pub struct CosmicBuffer(pub Buffer);
impl Default for CosmicBuffer {
@ -23,160 +25,199 @@ impl Default for CosmicBuffer {
}
}
/// A component that is the entry point for rendering text.
/// A sub-entity of a [`TextBlock`].
///
/// It contains all of the text value and styling information.
#[derive(Component, Debug, Clone, Default, Reflect)]
/// Returned by [`ComputedTextBlock::entities`].
#[derive(Debug, Copy, Clone)]
pub struct TextEntity {
/// The entity.
pub entity: Entity,
/// Records the hierarchy depth of the entity within a `TextBlock`.
pub depth: usize,
}
/// Computed information for a [`TextBlock`].
///
/// Automatically updated by 2d and UI text systems.
#[derive(Component, Debug, Clone)]
pub struct ComputedTextBlock {
/// Buffer for managing text layout and creating [`TextLayoutInfo`].
///
/// This is private because buffer contents are always refreshed from ECS state when writing glyphs to
/// `TextLayoutInfo`. If you want to control the buffer contents manually or use the `cosmic-text`
/// editor, then you need to not use `TextBlock` and instead manually implement the conversion to
/// `TextLayoutInfo`.
pub(crate) buffer: CosmicBuffer,
/// Entities for all text spans in the block, including the root-level text.
///
/// The [`TextEntity::depth`] field can be used to reconstruct the hierarchy.
pub(crate) entities: SmallVec<[TextEntity; 1]>,
/// Flag set when any change has been made to this block that should cause it to be rerendered.
///
/// Includes:
/// - [`TextBlock`] changes.
/// - [`TextStyle`] or `Text2d`/`Text`/`TextSpan` changes anywhere in the block's entity hierarchy.
// TODO: This encompasses both structural changes like font size or justification and non-structural
// changes like text color and font smoothing. This field currently causes UI to 'remeasure' text, even if
// the actual changes are non-structural and can be handled by only rerendering and not remeasuring. A full
// solution would probably require splitting TextBlock and TextStyle into structural/non-structural
// components for more granular change detection. A cost/benefit analysis is needed.
pub(crate) needs_rerender: bool,
}
impl ComputedTextBlock {
/// Accesses entities in this block.
///
/// Can be used to look up [`TextStyle`] components for glyphs in [`TextLayoutInfo`] using the `span_index`
/// stored there.
pub fn entities(&self) -> &[TextEntity] {
&self.entities
}
/// Indicates if the text needs to be refreshed in [`TextLayoutInfo`].
///
/// Updated automatically by [`detect_text_needs_rerender`] and cleared
/// by [`TextPipeline`](crate::TextPipeline) methods.
pub fn needs_rerender(&self) -> bool {
self.needs_rerender
}
}
impl Default for ComputedTextBlock {
fn default() -> Self {
Self {
buffer: CosmicBuffer::default(),
entities: SmallVec::default(),
needs_rerender: true,
}
}
}
/// Component with text format settings for a block of text.
///
/// A block of text is composed of text spans, which each have a separate string value and [`TextStyle`]. Text
/// spans associated with a text block are collected into [`ComputedTextBlock`] for layout, and then inserted
/// to [`TextLayoutInfo`] for rendering.
///
/// See [`Text2d`](crate::Text2d) for the core component of 2d text, and `Text` in `bevy_ui` for UI text.
#[derive(Component, Debug, Copy, Clone, Default, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct Text {
/// The text's sections
pub sections: Vec<TextSection>,
#[require(ComputedTextBlock, TextLayoutInfo)]
pub struct TextBlock {
/// The text's internal alignment.
/// Should not affect its position within a container.
pub justify: JustifyText,
/// How the text should linebreak when running out of the bounds determined by `max_size`
/// How the text should linebreak when running out of the bounds determined by `max_size`.
pub linebreak: LineBreak,
/// The antialiasing method to use when rendering text.
pub font_smoothing: FontSmoothing,
}
impl Text {
/// Constructs a [`Text`] with a single section.
///
/// ```
/// # use bevy_asset::Handle;
/// # use bevy_color::Color;
/// # use bevy_text::{Font, Text, TextStyle, JustifyText};
/// #
/// # let font_handle: Handle<Font> = Default::default();
/// #
/// // Basic usage.
/// let hello_world = Text::from_section(
/// // Accepts a String or any type that converts into a String, such as &str.
/// "hello world!",
/// TextStyle {
/// font: font_handle.clone().into(),
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// );
///
/// let hello_bevy = Text::from_section(
/// "hello world\nand bevy!",
/// TextStyle {
/// font: font_handle.into(),
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// ) // You can still add text justifaction.
/// .with_justify(JustifyText::Center);
/// ```
pub fn from_section(value: impl Into<String>, style: TextStyle) -> Self {
Self {
sections: vec![TextSection::new(value, style)],
..default()
}
impl TextBlock {
/// Makes a new [`TextBlock`].
pub const fn new(justify: JustifyText, linebreak: LineBreak) -> Self {
Self { justify, linebreak }
}
/// Constructs a [`Text`] from a list of sections.
///
/// ```
/// # use bevy_asset::Handle;
/// # use bevy_color::Color;
/// # use bevy_color::palettes::basic::{RED, BLUE};
/// # use bevy_text::{Font, Text, TextStyle, TextSection};
/// #
/// # let font_handle: Handle<Font> = Default::default();
/// #
/// let hello_world = Text::from_sections([
/// TextSection::new(
/// "Hello, ",
/// TextStyle {
/// font: font_handle.clone().into(),
/// font_size: 60.0,
/// color: BLUE.into(),
/// },
/// ),
/// TextSection::new(
/// "World!",
/// TextStyle {
/// font: font_handle.into(),
/// font_size: 60.0,
/// color: RED.into(),
/// },
/// ),
/// ]);
/// ```
pub fn from_sections(sections: impl IntoIterator<Item = TextSection>) -> Self {
Self {
sections: sections.into_iter().collect(),
..default()
}
/// Makes a new [`TextBlock`] with the specified [`JustifyText`].
pub fn new_with_justify(justify: JustifyText) -> Self {
Self::default().with_justify(justify)
}
/// Returns this [`Text`] with a new [`JustifyText`].
/// Makes a new [`TextBlock`] with the specified [`LineBreak`].
pub fn new_with_linebreak(linebreak: LineBreak) -> Self {
Self::default().with_linebreak(linebreak)
}
/// Makes a new [`TextBlock`] with soft wrapping disabled.
/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.
pub fn new_with_no_wrap() -> Self {
Self::default().with_no_wrap()
}
/// Returns this [`TextBlock`] with the specified [`JustifyText`].
pub const fn with_justify(mut self, justify: JustifyText) -> Self {
self.justify = justify;
self
}
/// Returns this [`Text`] with soft wrapping disabled.
/// Returns this [`TextBlock`] with the specified [`LineBreak`].
pub const fn with_linebreak(mut self, linebreak: LineBreak) -> Self {
self.linebreak = linebreak;
self
}
/// Returns this [`TextBlock`] with soft wrapping disabled.
/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.
pub const fn with_no_wrap(mut self) -> Self {
self.linebreak = LineBreak::NoWrap;
self
}
}
/// Returns this [`Text`] with the specified [`FontSmoothing`].
pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self {
self.font_smoothing = font_smoothing;
self
/// A span of UI text in a tree of spans under an entity with [`TextBlock`], such as `Text` or `Text2d`.
///
/// Spans are collected in hierarchy traversal order into a [`ComputedTextBlock`] for layout.
///
/*
```
# use bevy_asset::Handle;
# use bevy_color::Color;
# use bevy_color::palettes::basic::{RED, BLUE};
# use bevy_ecs::World;
# use bevy_text::{Font, TextBlock, TextStyle, TextSection};
# let font_handle: Handle<Font> = Default::default();
# let mut world = World::default();
#
world.spawn((
TextBlock::default(),
TextStyle {
font: font_handle.clone().into(),
font_size: 60.0,
color: BLUE.into(),
}
))
.with_child((
TextSpan::new("Hello!"),
TextStyle {
font: font_handle.into(),
font_size: 60.0,
color: RED.into(),
}
));
```
*/
#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)]
#[reflect(Component, Default, Debug)]
#[require(TextStyle)]
pub struct TextSpan(pub String);
impl TextSpan {
/// Makes a new text span component.
pub fn new(text: impl Into<String>) -> Self {
Self(text.into())
}
}
/// Contains the value of the text in a section and how it should be styled.
#[derive(Debug, Default, Clone, Reflect)]
#[reflect(Default)]
pub struct TextSection {
/// The content (in `String` form) of the text in the section.
pub value: String,
/// The style of the text in the section, including the font face, font size, and color.
pub style: TextStyle,
}
impl TextSpanComponent for TextSpan {}
impl TextSection {
/// Create a new [`TextSection`].
pub fn new(value: impl Into<String>, style: TextStyle) -> Self {
Self {
value: value.into(),
style,
}
impl TextSpanAccess for TextSpan {
fn read_span(&self) -> &str {
self.as_str()
}
/// Create an empty [`TextSection`] from a style. Useful when the value will be set dynamically.
pub const fn from_style(style: TextStyle) -> Self {
Self {
value: String::new(),
style,
}
fn write_span(&mut self) -> &mut String {
&mut *self
}
}
impl From<&str> for TextSection {
impl From<&str> for TextSpan {
fn from(value: &str) -> Self {
Self {
value: value.into(),
..default()
}
Self(String::from(value))
}
}
impl From<String> for TextSection {
impl From<String> for TextSpan {
fn from(value: String) -> Self {
Self {
value,
..Default::default()
}
Self(value)
}
}
@ -216,9 +257,10 @@ impl From<JustifyText> for cosmic_text::Align {
}
}
#[derive(Clone, Debug, Reflect)]
/// `TextStyle` determines the style of the text in a section, specifically
/// `TextStyle` determines the style of a text span within a [`TextBlock`], specifically
/// the font face, the font size, and the color.
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct TextStyle {
/// The specific font face to use, as a `Handle` to a [`Font`] asset.
///
@ -238,6 +280,16 @@ pub struct TextStyle {
pub font_size: f32,
/// The color of the text for this section.
pub color: Color,
/// The antialiasing method to use when rendering text.
pub font_smoothing: FontSmoothing,
}
impl TextStyle {
/// Returns this [`TextBlock`] with the specified [`FontSmoothing`].
pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self {
self.font_smoothing = font_smoothing;
self
}
}
impl Default for TextStyle {
@ -246,6 +298,7 @@ impl Default for TextStyle {
font: Default::default(),
font_size: 20.0,
color: Color::WHITE,
font_smoothing: Default::default(),
}
}
}
@ -293,3 +346,112 @@ pub enum FontSmoothing {
// TODO: Add subpixel antialias support
// SubpixelAntiAliased,
}
/// System that detects changes to text blocks and sets `ComputedTextBlock::should_rerender`.
///
/// Generic over the root text component and text span component. For example, [`Text2d`](crate::Text2d)/[`TextSpan`] for
/// 2d or `Text`/[`TextSpan`] for UI.
pub fn detect_text_needs_rerender<Root: Component>(
changed_roots: Query<
Entity,
(
Or<(
Changed<Root>,
Changed<TextStyle>,
Changed<TextBlock>,
Changed<Children>,
)>,
With<Root>,
With<TextStyle>,
With<TextBlock>,
),
>,
changed_spans: Query<
(Entity, Option<&Parent>, Has<TextBlock>),
(
Or<(
Changed<TextSpan>,
Changed<TextStyle>,
Changed<Children>,
Changed<Parent>, // Included to detect broken text block hierarchies.
Added<TextBlock>,
)>,
With<TextSpan>,
With<TextStyle>,
),
>,
mut computed: Query<(
Option<&Parent>,
Option<&mut ComputedTextBlock>,
Has<TextSpan>,
)>,
) {
// Root entity:
// - Root component changed.
// - TextStyle on root changed.
// - TextBlock changed.
// - Root children changed (can include additions and removals).
for root in changed_roots.iter() {
let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else {
warn_once!("found entity {:?} with a root text component ({}) but no ComputedTextBlock; this warning only \
prints once", root, core::any::type_name::<Root>());
continue;
};
computed.needs_rerender = true;
}
// Span entity:
// - Span component changed.
// - Span TextStyle changed.
// - Span children changed (can include additions and removals).
for (entity, maybe_span_parent, has_text_block) in changed_spans.iter() {
if has_text_block {
warn_once!("found entity {:?} with a TextSpan that has a TextBlock, which should only be on root \
text entities (that have {}); this warning only prints once",
entity, core::any::type_name::<Root>());
}
let Some(span_parent) = maybe_span_parent else {
warn_once!(
"found entity {:?} with a TextSpan that has no parent; it should have an ancestor \
with a root text component ({}); this warning only prints once",
entity,
core::any::type_name::<Root>()
);
continue;
};
let mut parent: Entity = **span_parent;
// Search for the nearest ancestor with ComputedTextBlock.
// Note: We assume the perf cost from duplicate visits in the case that multiple spans in a block are visited
// is outweighed by the expense of tracking visited spans.
loop {
let Ok((maybe_parent, maybe_computed, has_span)) = computed.get_mut(parent) else {
warn_once!("found entity {:?} with a TextSpan that is part of a broken hierarchy with a Parent \
component that points at non-existent entity {:?}; this warning only prints once",
entity, parent);
break;
};
if let Some(mut computed) = maybe_computed {
computed.needs_rerender = true;
break;
}
if !has_span {
warn_once!("found entity {:?} with a TextSpan that has an ancestor ({}) that does not have a text \
span component or a ComputedTextBlock component; this warning only prints once",
entity, parent);
break;
}
let Some(next_parent) = maybe_parent else {
warn_once!(
"found entity {:?} with a TextSpan that has no ancestor with the root text \
component ({}); this warning only prints once",
entity,
core::any::type_name::<Root>()
);
break;
};
parent = **next_parent;
}
}
}

View file

@ -1,69 +1,129 @@
use crate::pipeline::CosmicFontSystem;
use crate::{
CosmicBuffer, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, Text, TextBounds,
TextError, TextLayoutInfo, TextPipeline, YAxisOrientation,
ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBlock,
TextBounds, TextError, TextLayoutInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess,
TextStyle, TextWriter, YAxisOrientation,
};
use bevy_asset::Assets;
use bevy_color::LinearRgba;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::component::Component;
use bevy_ecs::{
bundle::Bundle,
change_detection::{DetectChanges, Ref},
entity::Entity,
event::EventReader,
prelude::With,
prelude::{ReflectComponent, With},
query::{Changed, Without},
system::{Commands, Local, Query, Res, ResMut},
};
use bevy_math::Vec2;
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_render::sync_world::TemporaryRenderEntity;
use bevy_render::view::Visibility;
use bevy_render::{
primitives::Aabb,
texture::Image,
view::{InheritedVisibility, NoFrustumCulling, ViewVisibility, Visibility},
view::{NoFrustumCulling, ViewVisibility},
Extract,
};
use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, SpriteSource, TextureAtlasLayout};
use bevy_transform::prelude::{GlobalTransform, Transform};
use bevy_transform::components::Transform;
use bevy_transform::prelude::GlobalTransform;
use bevy_utils::HashSet;
use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged};
/// The bundle of components needed to draw text in a 2D scene via a `Camera2d`.
/// The top-level 2D text component.
///
/// Adding `Text2d` to an entity will pull in required components for setting up 2d text.
/// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs)
#[derive(Bundle, Clone, Debug, Default)]
pub struct Text2dBundle {
/// Contains the text.
///
/// With `Text2dBundle` the alignment field of `Text` only affects the internal alignment of a block of text and not its
/// relative position which is controlled by the `Anchor` component.
/// This means that for a block of text consisting of only one line that doesn't wrap, the `alignment` field will have no effect.
pub text: Text,
/// Cached buffer for layout with cosmic-text
pub buffer: CosmicBuffer,
/// How the text is positioned relative to its transform.
///
/// `text_anchor` does not affect the internal alignment of the block of text, only
/// its position.
pub text_anchor: Anchor,
/// The maximum width and height of the text.
pub text_2d_bounds: TextBounds,
/// The transform of the text.
pub transform: Transform,
/// The global transform of the text.
pub global_transform: GlobalTransform,
/// The visibility properties of the text.
pub visibility: Visibility,
/// Inherited visibility of an entity.
pub inherited_visibility: InheritedVisibility,
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
pub view_visibility: ViewVisibility,
/// Contains the size of the text and its glyph's position and scale data. Generated via [`TextPipeline::queue_text`]
pub text_layout_info: TextLayoutInfo,
/// Marks that this is a [`SpriteSource`].
///
/// This is needed for visibility computation to work properly.
pub sprite_source: SpriteSource,
///
/// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into
/// a [`TextBlock`]. See [`TextSpan`](crate::TextSpan) for the component used by children of entities with [`Text2d`].
///
/// With `Text2d` the `justify` field of [`TextBlock`] only affects the internal alignment of a block of text and not its
/// relative position, which is controlled by the [`Anchor`] component.
/// This means that for a block of text consisting of only one line that doesn't wrap, the `justify` field will have no effect.
///
/*
```
# use bevy_asset::Handle;
# use bevy_color::Color;
# use bevy_color::palettes::basic::BLUE;
# use bevy_ecs::World;
# use bevy_text::{Font, JustifyText, Text2d, TextBlock, TextStyle};
#
# let font_handle: Handle<Font> = Default::default();
# let mut world = World::default();
#
// Basic usage.
world.spawn(Text2d::new("hello world!"));
// With non-default style.
world.spawn((
Text2d::new("hello world!"),
TextStyle {
font: font_handle.clone().into(),
font_size: 60.0,
color: BLUE.into(),
}
));
// With text justification.
world.spawn((
Text2d::new("hello world\nand bevy!"),
TextBlock::new_with_justify(JustifyText::Center)
));
```
*/
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component, Default, Debug)]
#[require(
TextBlock,
TextStyle,
TextBounds,
Anchor,
SpriteSource,
Visibility,
Transform
)]
pub struct Text2d(pub String);
impl Text2d {
/// Makes a new 2d text component.
pub fn new(text: impl Into<String>) -> Self {
Self(text.into())
}
}
impl TextRoot for Text2d {}
impl TextSpanAccess for Text2d {
fn read_span(&self) -> &str {
self.as_str()
}
fn write_span(&mut self) -> &mut String {
&mut *self
}
}
impl From<&str> for Text2d {
fn from(value: &str) -> Self {
Self(String::from(value))
}
}
impl From<String> for Text2d {
fn from(value: String) -> Self {
Self(value)
}
}
/// 2d alias for [`TextReader`].
pub type TextReader2d<'w, 's> = TextReader<'w, 's, Text2d>;
/// 2d alias for [`TextWriter`].
pub type TextWriter2d<'w, 's> = TextWriter<'w, 's, Text2d>;
/// This system extracts the sprites from the 2D text components and adds them to the
/// "render world".
pub fn extract_text2d_sprite(
@ -75,12 +135,13 @@ pub fn extract_text2d_sprite(
Query<(
Entity,
&ViewVisibility,
&Text,
&ComputedTextBlock,
&TextLayoutInfo,
&Anchor,
&GlobalTransform,
)>,
>,
text_styles: Extract<Query<&TextStyle>>,
) {
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
let scale_factor = windows
@ -89,8 +150,14 @@ pub fn extract_text2d_sprite(
.unwrap_or(1.0);
let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.));
for (original_entity, view_visibility, text, text_layout_info, anchor, global_transform) in
text2d_query.iter()
for (
original_entity,
view_visibility,
computed_block,
text_layout_info,
anchor,
global_transform,
) in text2d_query.iter()
{
if !view_visibility.get() {
continue;
@ -102,17 +169,26 @@ pub fn extract_text2d_sprite(
* GlobalTransform::from_translation(alignment_translation.extend(0.))
* scaling;
let mut color = LinearRgba::WHITE;
let mut current_section = usize::MAX;
let mut current_span = usize::MAX;
for PositionedGlyph {
position,
atlas_info,
section_index,
span_index,
..
} in &text_layout_info.glyphs
{
if *section_index != current_section {
color = LinearRgba::from(text.sections[*section_index].style.color);
current_section = *section_index;
if *span_index != current_span {
color = text_styles
.get(
computed_block
.entities()
.get(*span_index)
.map(|t| t.entity)
.unwrap_or(Entity::PLACEHOLDER),
)
.map(|style| LinearRgba::from(style.color))
.unwrap_or_default();
current_span = *span_index;
}
let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();
@ -154,11 +230,12 @@ pub fn update_text2d_layout(
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(
Entity,
Ref<Text>,
Ref<TextBlock>,
Ref<TextBounds>,
&mut TextLayoutInfo,
&mut CosmicBuffer,
&mut ComputedTextBlock,
)>,
mut text_reader: TextReader2d,
mut font_system: ResMut<CosmicFontSystem>,
mut swash_cache: ResMut<SwashCache>,
) {
@ -173,10 +250,14 @@ pub fn update_text2d_layout(
let inverse_scale_factor = scale_factor.recip();
for (entity, text, bounds, text_layout_info, mut buffer) in &mut text_query {
if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) {
for (entity, block, bounds, text_layout_info, mut computed) in &mut text_query {
if factor_changed
|| computed.needs_rerender()
|| bounds.is_changed()
|| queue.remove(&entity)
{
let text_bounds = TextBounds {
width: if text.linebreak == LineBreak::NoWrap {
width: if block.linebreak == LineBreak::NoWrap {
None
} else {
bounds.width.map(|width| scale_value(width, scale_factor))
@ -190,17 +271,15 @@ pub fn update_text2d_layout(
match text_pipeline.queue_text(
text_layout_info,
&fonts,
&text.sections,
text_reader.iter(entity),
scale_factor.into(),
text.justify,
text.linebreak,
text.font_smoothing,
&block,
text_bounds,
&mut font_atlas_sets,
&mut texture_atlases,
&mut textures,
YAxisOrientation::BottomToTop,
buffer.as_mut(),
computed.as_mut(),
&mut font_system,
&mut swash_cache,
) {
@ -265,7 +344,8 @@ mod tests {
use bevy_app::{App, Update};
use bevy_asset::{load_internal_binary_asset, Handle};
use bevy_ecs::{event::Events, schedule::IntoSystemConfigs};
use bevy_utils::default;
use crate::{detect_text_needs_rerender, TextIterScratch};
use super::*;
@ -282,12 +362,15 @@ mod tests {
.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.after(update_text2d_layout),
),
calculate_bounds_text2d,
)
.chain(),
);
// A font is needed to ensure the text is laid out with an actual size.
@ -298,13 +381,7 @@ mod tests {
|bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() }
);
let entity = app
.world_mut()
.spawn((Text2dBundle {
text: Text::from_section(FIRST_TEXT, default()),
..default()
},))
.id();
let entity = app.world_mut().spawn(Text2d::new(FIRST_TEXT)).id();
(app, entity)
}
@ -356,8 +433,8 @@ mod tests {
.get_entity_mut(entity)
.expect("Could not find entity");
*entity_ref
.get_mut::<Text>()
.expect("Missing Text on entity") = Text::from_section(SECOND_TEXT, default());
.get_mut::<Text2d>()
.expect("Missing Text2d on entity") = Text2d::new(SECOND_TEXT);
// Recomputes the AABB.
app.update();

View file

@ -0,0 +1,381 @@
use bevy_ecs::{
prelude::*,
system::{Query, SystemParam},
};
use bevy_hierarchy::Children;
use crate::{TextSpan, TextStyle};
/// Helper trait for using the [`TextReader`] and [`TextWriter`] system params.
pub trait TextSpanAccess: Component {
/// Gets the text span's string.
fn read_span(&self) -> &str;
/// Gets mutable reference to the text span's string.
fn write_span(&mut self) -> &mut String;
}
/// Helper trait for the root text component in a text block.
pub trait TextRoot: TextSpanAccess + From<String> {}
/// Helper trait for the text span components in a text block.
pub trait TextSpanComponent: TextSpanAccess + From<String> {}
#[derive(Resource, Default)]
pub(crate) struct TextIterScratch {
stack: Vec<(&'static Children, usize)>,
}
impl TextIterScratch {
fn take<'a>(&mut self) -> Vec<(&'a Children, usize)> {
core::mem::take(&mut self.stack)
.into_iter()
.map(|_| -> (&Children, usize) { unreachable!() })
.collect()
}
fn recover(&mut self, mut stack: Vec<(&Children, usize)>) {
stack.clear();
self.stack = stack
.into_iter()
.map(|_| -> (&'static Children, usize) { unreachable!() })
.collect();
}
}
/// System parameter for reading text spans in a [`TextBlock`](crate::TextBlock).
///
/// `R` is the root text component, and `S` is the text span component on children.
#[derive(SystemParam)]
pub struct TextReader<'w, 's, R: TextRoot> {
// This is a local to avoid system ambiguities when TextReaders run in parallel.
scratch: Local<'s, TextIterScratch>,
roots: Query<'w, 's, (&'static R, &'static TextStyle, Option<&'static Children>)>,
spans: Query<
'w,
's,
(
&'static TextSpan,
&'static TextStyle,
Option<&'static Children>,
),
>,
}
impl<'w, 's, R: TextRoot> TextReader<'w, 's, R> {
/// Returns an iterator over text spans in a text block, starting with the root entity.
pub fn iter(&mut self, root_entity: Entity) -> TextSpanIter<R> {
let stack = self.scratch.take();
TextSpanIter {
scratch: &mut self.scratch,
root_entity: Some(root_entity),
stack,
roots: &self.roots,
spans: &self.spans,
}
}
/// Gets a text span within a text block at a specific index in the flattened span list.
pub fn get(
&mut self,
root_entity: Entity,
index: usize,
) -> Option<(Entity, usize, &str, &TextStyle)> {
self.iter(root_entity).nth(index)
}
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
pub fn get_text(&mut self, root_entity: Entity, index: usize) -> Option<&str> {
self.get(root_entity, index).map(|(_, _, text, _)| text)
}
/// Gets the [`TextStyle`] of a text span within a text block at a specific index in the flattened span list.
pub fn get_style(&mut self, root_entity: Entity, index: usize) -> Option<&TextStyle> {
self.get(root_entity, index).map(|(_, _, _, style)| style)
}
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn text(&mut self, root_entity: Entity, index: usize) -> &str {
self.get_text(root_entity, index).unwrap()
}
/// Gets the [`TextStyle`] of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn style(&mut self, root_entity: Entity, index: usize) -> &TextStyle {
self.get_style(root_entity, index).unwrap()
}
}
/// Iterator returned by [`TextReader::iter`].
///
/// Iterates all spans in a text block according to hierarchy traversal order.
/// Does *not* flatten interspersed ghost nodes. Only contiguous spans are traversed.
// TODO: Use this iterator design in UiChildrenIter to reduce allocations.
pub struct TextSpanIter<'a, R: TextRoot> {
scratch: &'a mut TextIterScratch,
root_entity: Option<Entity>,
/// Stack of (children, next index into children).
stack: Vec<(&'a Children, usize)>,
roots: &'a Query<'a, 'a, (&'static R, &'static TextStyle, Option<&'static Children>)>,
spans: &'a Query<
'a,
'a,
(
&'static TextSpan,
&'static TextStyle,
Option<&'static Children>,
),
>,
}
impl<'a, R: TextRoot> Iterator for TextSpanIter<'a, R> {
/// Item = (entity in text block, hierarchy depth in the block, span text, span style).
type Item = (Entity, usize, &'a str, &'a TextStyle);
fn next(&mut self) -> Option<Self::Item> {
// Root
if let Some(root_entity) = self.root_entity.take() {
if let Ok((text, style, maybe_children)) = self.roots.get(root_entity) {
if let Some(children) = maybe_children {
self.stack.push((children, 0));
}
return Some((root_entity, 0, text.read_span(), style));
}
return None;
}
// Span
loop {
let (children, idx) = self.stack.last_mut()?;
loop {
let Some(child) = children.get(*idx) else {
break;
};
// Increment to prep the next entity in this stack level.
*idx += 1;
let entity = *child;
let Ok((span, style, maybe_children)) = self.spans.get(entity) else {
continue;
};
let depth = self.stack.len();
if let Some(children) = maybe_children {
self.stack.push((children, 0));
}
return Some((entity, depth, span.read_span(), style));
}
// All children at this stack entry have been iterated.
self.stack.pop();
}
}
}
impl<'a, R: TextRoot> Drop for TextSpanIter<'a, R> {
fn drop(&mut self) {
// Return the internal stack.
let stack = core::mem::take(&mut self.stack);
self.scratch.recover(stack);
}
}
/// System parameter for reading and writing text spans in a [`TextBlock`](crate::TextBlock).
///
/// `R` is the root text component, and `S` is the text span component on children.
#[derive(SystemParam)]
pub struct TextWriter<'w, 's, R: TextRoot> {
// This is a resource because two TextWriters can't run in parallel.
scratch: ResMut<'w, TextIterScratch>,
roots: Query<'w, 's, (&'static mut R, &'static mut TextStyle), Without<TextSpan>>,
spans: Query<'w, 's, (&'static mut TextSpan, &'static mut TextStyle), Without<R>>,
children: Query<'w, 's, &'static Children>,
}
impl<'w, 's, R: TextRoot> TextWriter<'w, 's, R> {
/// Gets a mutable reference to a text span within a text block at a specific index in the flattened span list.
pub fn get(
&mut self,
root_entity: Entity,
index: usize,
) -> Option<(Entity, usize, Mut<String>, Mut<TextStyle>)> {
// Root
if index == 0 {
let (text, style) = self.roots.get_mut(root_entity).ok()?;
return Some((
root_entity,
0,
text.map_unchanged(|t| t.write_span()),
style,
));
}
// Prep stack.
let mut stack: Vec<(&Children, usize)> = self.scratch.take();
if let Ok(children) = self.children.get(root_entity) {
stack.push((children, 0));
}
// Span
let mut count = 1;
let (depth, entity) = 'l: loop {
let Some((children, idx)) = stack.last_mut() else {
self.scratch.recover(stack);
return None;
};
loop {
let Some(child) = children.get(*idx) else {
// All children at this stack entry have been iterated.
stack.pop();
break;
};
// Increment to prep the next entity in this stack level.
*idx += 1;
if !self.spans.contains(*child) {
continue;
};
count += 1;
if count - 1 == index {
let depth = stack.len();
self.scratch.recover(stack);
break 'l (depth, *child);
}
if let Ok(children) = self.children.get(*child) {
stack.push((children, 0));
break;
}
}
};
// Note: We do this outside the loop due to borrow checker limitations.
let (text, style) = self.spans.get_mut(entity).unwrap();
Some((entity, depth, text.map_unchanged(|t| t.write_span()), style))
}
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
pub fn get_text(&mut self, root_entity: Entity, index: usize) -> Option<Mut<String>> {
self.get(root_entity, index).map(|(_, _, text, _)| text)
}
/// Gets the [`TextStyle`] of a text span within a text block at a specific index in the flattened span list.
pub fn get_style(&mut self, root_entity: Entity, index: usize) -> Option<Mut<TextStyle>> {
self.get(root_entity, index).map(|(_, _, _, style)| style)
}
/// Gets the text value of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn text(&mut self, root_entity: Entity, index: usize) -> Mut<String> {
self.get_text(root_entity, index).unwrap()
}
/// Gets the [`TextStyle`] of a text span within a text block at a specific index in the flattened span list.
///
/// Panics if there is no span at the requested index.
pub fn style(&mut self, root_entity: Entity, index: usize) -> Mut<TextStyle> {
self.get_style(root_entity, index).unwrap()
}
/// Invokes a callback on each span in a text block, starting with the root entity.
pub fn for_each(
&mut self,
root_entity: Entity,
mut callback: impl FnMut(Entity, usize, Mut<String>, Mut<TextStyle>),
) {
self.for_each_until(root_entity, |a, b, c, d| {
(callback)(a, b, c, d);
true
});
}
/// Invokes a callback on each span's string value in a text block, starting with the root entity.
pub fn for_each_text(&mut self, root_entity: Entity, mut callback: impl FnMut(Mut<String>)) {
self.for_each(root_entity, |_, _, text, _| {
(callback)(text);
});
}
/// Invokes a callback on each span's [`TextStyle`] in a text block, starting with the root entity.
pub fn for_each_style(
&mut self,
root_entity: Entity,
mut callback: impl FnMut(Mut<TextStyle>),
) {
self.for_each(root_entity, |_, _, _, style| {
(callback)(style);
});
}
/// Invokes a callback on each span in a text block, starting with the root entity.
///
/// Traversal will stop when the callback returns `false`.
// TODO: find a way to consolidate get and for_each_until, or provide a real iterator. Lifetime issues are challenging here.
pub fn for_each_until(
&mut self,
root_entity: Entity,
mut callback: impl FnMut(Entity, usize, Mut<String>, Mut<TextStyle>) -> bool,
) {
// Root
let Ok((text, style)) = self.roots.get_mut(root_entity) else {
return;
};
if !(callback)(
root_entity,
0,
text.map_unchanged(|t| t.write_span()),
style,
) {
return;
}
// Prep stack.
let mut stack: Vec<(&Children, usize)> = self.scratch.take();
if let Ok(children) = self.children.get(root_entity) {
stack.push((children, 0));
}
// Span
loop {
let depth = stack.len();
let Some((children, idx)) = stack.last_mut() else {
self.scratch.recover(stack);
return;
};
loop {
let Some(child) = children.get(*idx) else {
// All children at this stack entry have been iterated.
stack.pop();
break;
};
// Increment to prep the next entity in this stack level.
*idx += 1;
let entity = *child;
let Ok((text, style)) = self.spans.get_mut(entity) else {
continue;
};
if !(callback)(entity, depth, text.map_unchanged(|t| t.write_span()), style) {
self.scratch.recover(stack);
return;
}
if let Ok(children) = self.children.get(entity) {
stack.push((children, 0));
break;
}
}
}
}
}

View file

@ -1,5 +1,6 @@
use crate::{
prelude::{Button, Label},
widget::UiTextReader,
Node, UiChildren, UiImage,
};
use bevy_a11y::{
@ -15,18 +16,19 @@ use bevy_ecs::{
world::Ref,
};
use bevy_render::{camera::CameraUpdateSystem, prelude::Camera};
use bevy_text::Text;
use bevy_transform::prelude::GlobalTransform;
fn calc_name(texts: &Query<&Text>, children: impl Iterator<Item = Entity>) -> Option<Box<str>> {
fn calc_name(
text_reader: &mut UiTextReader,
children: impl Iterator<Item = Entity>,
) -> Option<Box<str>> {
let mut name = None;
for child in children {
if let Ok(text) = texts.get(child) {
let values = text
.sections
.iter()
.map(|v| v.value.to_string())
.collect::<Vec<String>>();
let values = text_reader
.iter(child)
.map(|(_, _, text, _)| text.into())
.collect::<Vec<String>>();
if !values.is_empty() {
name = Some(values.join(" "));
}
}
@ -60,10 +62,10 @@ fn button_changed(
mut commands: Commands,
mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Button>>,
ui_children: UiChildren,
texts: Query<&Text>,
mut text_reader: UiTextReader,
) {
for (entity, accessible) in &mut query {
let name = calc_name(&texts, ui_children.iter_ui_children(entity));
let name = calc_name(&mut text_reader, ui_children.iter_ui_children(entity));
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Button);
if let Some(name) = name {
@ -87,10 +89,10 @@ fn image_changed(
mut commands: Commands,
mut query: Query<(Entity, Option<&mut AccessibilityNode>), (Changed<UiImage>, Without<Button>)>,
ui_children: UiChildren,
texts: Query<&Text>,
mut text_reader: UiTextReader,
) {
for (entity, accessible) in &mut query {
let name = calc_name(&texts, ui_children.iter_ui_children(entity));
let name = calc_name(&mut text_reader, ui_children.iter_ui_children(entity));
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Image);
if let Some(name) = name {
@ -112,13 +114,13 @@ fn image_changed(
fn label_changed(
mut commands: Commands,
mut query: Query<(Entity, &Text, Option<&mut AccessibilityNode>), Changed<Label>>,
mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Label>>,
mut text_reader: UiTextReader,
) {
for (entity, text, accessible) in &mut query {
let values = text
.sections
.iter()
.map(|v| v.value.to_string())
for (entity, accessible) in &mut query {
let values = text_reader
.iter(entity)
.map(|(_, _, text, _)| text.into())
.collect::<Vec<String>>();
let name = Some(values.join(" ").into_boxed_str());
if let Some(mut accessible) = accessible {

View file

@ -22,7 +22,7 @@ use derive_more::derive::{Display, Error, From};
use ui_surface::UiSurface;
#[cfg(feature = "bevy_text")]
use bevy_text::CosmicBuffer;
use bevy_text::ComputedTextBlock;
#[cfg(feature = "bevy_text")]
use bevy_text::CosmicFontSystem;
@ -125,7 +125,7 @@ pub fn ui_layout_system(
Option<&Outline>,
Option<&ScrollPosition>,
)>,
#[cfg(feature = "bevy_text")] mut buffer_query: Query<&mut CosmicBuffer>,
#[cfg(feature = "bevy_text")] mut buffer_query: Query<&mut ComputedTextBlock>,
#[cfg(feature = "bevy_text")] mut font_system: ResMut<CosmicFontSystem>,
) {
let UiLayoutSystemBuffers {

View file

@ -200,7 +200,7 @@ impl UiSurface {
camera: Entity,
render_target_resolution: UVec2,
#[cfg(feature = "bevy_text")] buffer_query: &'a mut bevy_ecs::prelude::Query<
&mut bevy_text::CosmicBuffer,
&mut bevy_text::ComputedTextBlock,
>,
#[cfg(feature = "bevy_text")] font_system: &'a mut bevy_text::cosmic_text::FontSystem,
) {
@ -302,8 +302,8 @@ with UI components as a child of an entity without UI components, your UI layout
fn get_text_buffer<'a>(
needs_buffer: bool,
ctx: &mut NodeMeasure,
query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::CosmicBuffer>,
) -> Option<&'a mut bevy_text::cosmic_text::Buffer> {
query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,
) -> Option<&'a mut bevy_text::ComputedTextBlock> {
// We avoid a query lookup whenever the buffer is not required.
if !needs_buffer {
return None;
@ -311,8 +311,8 @@ fn get_text_buffer<'a>(
let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else {
return None;
};
let Ok(buffer) = query.get_mut(info.entity) else {
let Ok(computed) = query.get_mut(info.entity) else {
return None;
};
Some(buffer.into_inner())
Some(computed.into_inner())
}

View file

@ -8,7 +8,7 @@
//! This crate contains Bevy's UI system, which can be used to create UI for both 2D and 3D games
//! # Basic usage
//! Spawn UI elements with [`node_bundles::ButtonBundle`], [`node_bundles::ImageBundle`], [`node_bundles::TextBundle`] and [`node_bundles::NodeBundle`]
//! Spawn UI elements with [`node_bundles::ButtonBundle`], [`node_bundles::ImageBundle`], [`Text`](prelude::Text) and [`node_bundles::NodeBundle`]
//! This UI is laid out with the Flexbox and CSS Grid layout models (see <https://cssreference.io/flexbox/>)
pub mod measurement;
@ -49,8 +49,12 @@ pub mod prelude {
#[doc(hidden)]
pub use {
crate::{
geometry::*, node_bundles::*, ui_material::*, ui_node::*, widget::Button,
widget::Label, Interaction, UiMaterialHandle, UiMaterialPlugin, UiScale,
geometry::*,
node_bundles::*,
ui_material::*,
ui_node::*,
widget::{Button, Label, Text, UiTextReader, UiTextWriter},
Interaction, UiMaterialHandle, UiMaterialPlugin, UiScale,
},
// `bevy_sprite` re-exports for texture slicing
bevy_sprite::{BorderRect, ImageScaleMode, SliceScaleMode, TextureSlicer},
@ -177,6 +181,7 @@ impl Plugin for UiPlugin {
.in_set(UiSystem::Layout)
.before(TransformSystem::TransformPropagate)
// Text and Text2D operate on disjoint sets of entities
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>)
.ambiguous_with(bevy_text::update_text2d_layout),
ui_stack_system
.in_set(UiSystem::Stack)
@ -217,17 +222,25 @@ impl Plugin for UiPlugin {
/// A function that should be called from [`UiPlugin::build`] when [`bevy_text`] is enabled.
#[cfg(feature = "bevy_text")]
fn build_text_interop(app: &mut App) {
use crate::widget::TextFlags;
use crate::widget::TextNodeFlags;
use bevy_text::TextLayoutInfo;
use widget::Text;
app.register_type::<TextLayoutInfo>()
.register_type::<TextFlags>();
.register_type::<TextNodeFlags>()
.register_type::<Text>();
app.add_systems(
PostUpdate,
(
widget::measure_text_system
(
bevy_text::detect_text_needs_rerender::<Text>,
widget::measure_text_system,
)
.chain()
.in_set(UiSystem::Prepare)
// Text and Text2d are independent.
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>)
// Potential conflict: `Assets<Image>`
// Since both systems will only ever insert new [`Image`] assets,
// they will never observe each other's effects.
@ -239,6 +252,7 @@ fn build_text_interop(app: &mut App) {
.in_set(UiSystem::PostLayout)
.after(bevy_text::remove_dropped_font_atlas_sets)
// Text2d and bevy_ui text are entirely on separate entities
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>)
.ambiguous_with(bevy_text::update_text2d_layout)
.ambiguous_with(bevy_text::calculate_bounds_text2d),
),

View file

@ -23,7 +23,7 @@ pub struct MeasureArgs<'a> {
#[cfg(feature = "bevy_text")]
pub font_system: &'a mut bevy_text::cosmic_text::FontSystem,
#[cfg(feature = "bevy_text")]
pub buffer: Option<&'a mut bevy_text::cosmic_text::Buffer>,
pub buffer: Option<&'a mut bevy_text::ComputedTextBlock>,
// When `bevy_text` is disabled, use `PhantomData` in order to keep lifetime in type signature.
#[cfg(not(feature = "bevy_text"))]
pub font_system: core::marker::PhantomData<&'a mut ()>,

View file

@ -9,20 +9,11 @@ use bevy_ecs::bundle::Bundle;
use bevy_render::view::{InheritedVisibility, ViewVisibility, Visibility};
use bevy_transform::prelude::{GlobalTransform, Transform};
#[cfg(feature = "bevy_text")]
use {
crate::widget::TextFlags,
bevy_color::Color,
bevy_text::{
CosmicBuffer, JustifyText, LineBreak, Text, TextLayoutInfo, TextSection, TextStyle,
},
};
/// The basic UI node.
///
/// Contains the [`Node`] component and other components required to make a container.
///
/// See [`node_bundles`](crate::node_bundles) for more specialized bundles like [`TextBundle`].
/// See [`node_bundles`](crate::node_bundles) for more specialized bundles like [`ImageBundle`].
#[derive(Bundle, Clone, Debug, Default)]
pub struct NodeBundle {
/// Describes the logical size of the node
@ -109,109 +100,6 @@ pub struct ImageBundle {
pub z_index: ZIndex,
}
#[cfg(feature = "bevy_text")]
/// A UI node that is text
///
/// The positioning of this node is controlled by the UI layout system. If you need manual control,
/// use [`Text2dBundle`](bevy_text::Text2dBundle).
#[derive(Bundle, Debug, Default)]
pub struct TextBundle {
/// Describes the logical size of the node
pub node: Node,
/// Styles which control the layout (size and position) of the node and its children
/// In some cases these styles also affect how the node drawn/painted.
pub style: Style,
/// Contains the text of the node
pub text: Text,
/// Cached cosmic buffer for layout
pub buffer: CosmicBuffer,
/// Text layout information
pub text_layout_info: TextLayoutInfo,
/// Text system flags
pub text_flags: TextFlags,
/// The calculated size based on the given image
pub calculated_size: ContentSize,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The transform of the node
///
/// This component is automatically managed by the UI layout system.
/// To alter the position of the `TextBundle`, use the properties of the [`Style`] component.
pub transform: Transform,
/// The global transform of the node
///
/// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems.
pub global_transform: GlobalTransform,
/// Describes the visibility properties of the node
pub visibility: Visibility,
/// Inherited visibility of an entity.
pub inherited_visibility: InheritedVisibility,
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
pub view_visibility: ViewVisibility,
/// Indicates the depth at which the node should appear in the UI
pub z_index: ZIndex,
/// The background color that will fill the containing node
pub background_color: BackgroundColor,
}
#[cfg(feature = "bevy_text")]
impl TextBundle {
/// Create a [`TextBundle`] from a single section.
///
/// See [`Text::from_section`] for usage.
pub fn from_section(value: impl Into<String>, style: TextStyle) -> Self {
Self {
text: Text::from_section(value, style),
..Default::default()
}
}
/// Create a [`TextBundle`] from a list of sections.
///
/// See [`Text::from_sections`] for usage.
pub fn from_sections(sections: impl IntoIterator<Item = TextSection>) -> Self {
Self {
text: Text::from_sections(sections),
..Default::default()
}
}
/// Returns this [`TextBundle`] with a new [`JustifyText`] on [`Text`].
pub const fn with_text_justify(mut self, justify: JustifyText) -> Self {
self.text.justify = justify;
self
}
/// Returns this [`TextBundle`] with a new [`Style`].
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
/// Returns this [`TextBundle`] with a new [`BackgroundColor`].
pub const fn with_background_color(mut self, color: Color) -> Self {
self.background_color = BackgroundColor(color);
self
}
/// Returns this [`TextBundle`] with soft wrapping disabled.
/// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur.
pub const fn with_no_wrap(mut self) -> Self {
self.text.linebreak = LineBreak::NoWrap;
self
}
}
#[cfg(feature = "bevy_text")]
impl<I> From<I> for TextBundle
where
I: Into<TextSection>,
{
fn from(value: I) -> Self {
Self::from_sections(vec![value.into()])
}
}
/// A UI node that is a button
///
/// # Extra behaviours

View file

@ -40,11 +40,7 @@ use bevy_render::{
use bevy_sprite::TextureAtlasLayout;
use bevy_sprite::{BorderRect, ImageScaleMode, SpriteAssetEvents, TextureAtlas};
#[cfg(feature = "bevy_text")]
use bevy_text::PositionedGlyph;
#[cfg(feature = "bevy_text")]
use bevy_text::Text;
#[cfg(feature = "bevy_text")]
use bevy_text::TextLayoutInfo;
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextLayoutInfo, TextStyle};
use bevy_transform::components::GlobalTransform;
use bevy_utils::HashMap;
use box_shadow::BoxShadowPlugin;
@ -593,18 +589,26 @@ pub fn extract_text_sections(
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&Text,
&ComputedTextBlock,
&TextLayoutInfo,
)>,
>,
text_styles: Extract<Query<&TextStyle>>,
mapping: Extract<Query<&RenderEntity>>,
) {
let mut start = 0;
let mut end = 1;
let default_ui_camera = default_ui_camera.get();
for (uinode, global_transform, view_visibility, clip, camera, text, text_layout_info) in
&uinode_query
for (
uinode,
global_transform,
view_visibility,
clip,
camera,
computed_block,
text_layout_info,
) in &uinode_query
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera) else {
continue;
@ -642,16 +646,31 @@ pub fn extract_text_sections(
transform.translation = transform.translation.round();
transform.translation *= inverse_scale_factor;
let mut color = LinearRgba::WHITE;
let mut current_span = usize::MAX;
for (
i,
PositionedGlyph {
position,
atlas_info,
section_index,
span_index,
..
},
) in text_layout_info.glyphs.iter().enumerate()
{
if *span_index != current_span {
color = text_styles
.get(
computed_block
.entities()
.get(*span_index)
.map(|t| t.entity)
.unwrap_or(Entity::PLACEHOLDER),
)
.map(|style| LinearRgba::from(style.color))
.unwrap_or_default();
current_span = *span_index;
}
let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();
let mut rect = atlas.textures[atlas_info.location.glyph_index].as_rect();
@ -668,8 +687,7 @@ pub fn extract_text_sections(
.glyphs
.get(i + 1)
.map(|info| {
info.section_index != *section_index
|| info.atlas_info.texture != atlas_info.texture
info.span_index != current_span || info.atlas_info.texture != atlas_info.texture
})
.unwrap_or(true)
{
@ -679,7 +697,7 @@ pub fn extract_text_sections(
id,
ExtractedUiNode {
stack_index: uinode.stack_index,
color: LinearRgba::from(text.sections[*section_index].style.color),
color,
image: atlas_info.texture.id(),
clip: clip.map(|clip| clip.clip),
camera_entity: render_camera_entity.id(),

View file

@ -2520,7 +2520,7 @@ impl<'w, 's> DefaultUiCamera<'w, 's> {
/// Marker for controlling whether Ui is rendered with or without anti-aliasing
/// in a camera. By default, Ui is always anti-aliased.
///
/// **Note:** This does not affect text anti-aliasing. For that, use the `font_smoothing` property of the [`bevy_text::Text`] component.
/// **Note:** This does not affect text anti-aliasing. For that, use the `font_smoothing` property of the [`TextStyle`](bevy_text::TextStyle) component.
///
/// ```
/// use bevy_core_pipeline::prelude::*;

View file

@ -88,7 +88,7 @@ impl Measure for ImageMeasure {
}
#[cfg(feature = "bevy_text")]
type UpdateImageFilter = (With<Node>, Without<bevy_text::Text>);
type UpdateImageFilter = (With<Node>, Without<crate::prelude::Text>);
#[cfg(not(feature = "bevy_text"))]
type UpdateImageFilter = With<Node>;

View file

@ -1,11 +1,13 @@
use crate::{
ContentSize, DefaultUiCamera, FixedMeasure, Measure, MeasureArgs, Node, NodeMeasure,
TargetCamera, UiScale,
ContentSize, DefaultUiCamera, FixedMeasure, FocusPolicy, Measure, MeasureArgs, Node,
NodeMeasure, Style, TargetCamera, UiScale, ZIndex,
};
use bevy_asset::Assets;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChanges,
entity::{Entity, EntityHashMap},
prelude::{Component, DetectChanges},
prelude::Component,
query::With,
reflect::ReflectComponent,
system::{Local, Query, Res, ResMut},
@ -13,37 +15,132 @@ use bevy_ecs::{
};
use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{camera::Camera, texture::Image};
use bevy_render::{camera::Camera, texture::Image, view::Visibility};
use bevy_sprite::TextureAtlasLayout;
use bevy_text::{
scale_value, CosmicBuffer, CosmicFontSystem, Font, FontAtlasSets, JustifyText, LineBreak,
SwashCache, Text, TextBounds, TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline,
YAxisOrientation,
scale_value, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache,
TextBlock, TextBounds, TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader,
TextRoot, TextSpanAccess, TextStyle, TextWriter, YAxisOrientation,
};
use bevy_transform::components::Transform;
use bevy_utils::{tracing::error, Entry};
use taffy::style::AvailableSpace;
/// Text system flags
/// UI text system flags.
///
/// Used internally by [`measure_text_system`] and [`text_system`] to schedule text for processing.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct TextFlags {
/// If set a new measure function for the text node will be created
needs_new_measure_func: bool,
/// If set the text will be recomputed
pub struct TextNodeFlags {
/// If set then a new measure function for the text node will be created.
needs_measure_fn: bool,
/// If set then the text will be recomputed.
needs_recompute: bool,
}
impl Default for TextFlags {
impl Default for TextNodeFlags {
fn default() -> Self {
Self {
needs_new_measure_func: true,
needs_measure_fn: true,
needs_recompute: true,
}
}
}
/// The top-level UI text component.
///
/// Adding [`Text`] to an entity will pull in required components for setting up a UI text node.
///
/// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into
/// a [`TextBlock`]. See [`TextSpan`](bevy_text::TextSpan) for the component used by children of entities with [`Text`].
///
/// Note that [`Transform`] on this entity is managed automatically by the UI layout system.
///
/*
```
# use bevy_asset::Handle;
# use bevy_color::Color;
# use bevy_color::palettes::basic::BLUE;
# use bevy_ecs::World;
# use bevy_text::{Font, JustifyText, TextBlock, TextStyle};
# use bevy_ui::Text;
#
# let font_handle: Handle<Font> = Default::default();
# let mut world = World::default();
#
// Basic usage.
world.spawn(Text::new("hello world!"));
// With non-default style.
world.spawn((
Text::new("hello world!"),
TextStyle {
font: font_handle.clone().into(),
font_size: 60.0,
color: BLUE.into(),
}
));
// With text justification.
world.spawn((
Text::new("hello world\nand bevy!"),
TextBlock::new_with_justify(JustifyText::Center)
));
```
*/
#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)]
#[reflect(Component, Default, Debug)]
#[require(
TextBlock,
TextStyle,
TextNodeFlags,
Node,
Style, // TODO: Remove when Node uses required components.
ContentSize, // TODO: Remove when Node uses required components.
FocusPolicy, // TODO: Remove when Node uses required components.
ZIndex, // TODO: Remove when Node uses required components.
Visibility, // TODO: Remove when Node uses required components.
Transform // TODO: Remove when Node uses required components.
)]
pub struct Text(pub String);
impl Text {
/// Makes a new text component.
pub fn new(text: impl Into<String>) -> Self {
Self(text.into())
}
}
impl TextRoot for Text {}
impl TextSpanAccess for Text {
fn read_span(&self) -> &str {
self.as_str()
}
fn write_span(&mut self) -> &mut String {
&mut *self
}
}
impl From<&str> for Text {
fn from(value: &str) -> Self {
Self(String::from(value))
}
}
impl From<String> for Text {
fn from(value: String) -> Self {
Self(value)
}
}
/// UI alias for [`TextReader`].
pub type UiTextReader<'w, 's> = TextReader<'w, 's, Text>;
/// UI alias for [`TextWriter`].
pub type UiTextWriter<'w, 's> = TextWriter<'w, 's, Text>;
/// Text measurement for UI layout. See [`NodeMeasure`].
pub struct TextMeasure {
pub info: TextMeasureInfo,
}
@ -103,42 +200,41 @@ impl Measure for TextMeasure {
#[allow(clippy::too_many_arguments)]
#[inline]
fn create_text_measure(
fn create_text_measure<'a>(
entity: Entity,
fonts: &Assets<Font>,
scale_factor: f64,
text: Ref<Text>,
spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextStyle)>,
block: Ref<TextBlock>,
text_pipeline: &mut TextPipeline,
mut content_size: Mut<ContentSize>,
mut text_flags: Mut<TextFlags>,
buffer: &mut CosmicBuffer,
text_alignment: JustifyText,
mut text_flags: Mut<TextNodeFlags>,
mut computed: Mut<ComputedTextBlock>,
font_system: &mut CosmicFontSystem,
) {
match text_pipeline.create_text_measure(
entity,
fonts,
&text.sections,
spans,
scale_factor,
text.linebreak,
buffer,
text_alignment,
&block,
computed.as_mut(),
font_system,
) {
Ok(measure) => {
if text.linebreak == LineBreak::NoWrap {
if block.linebreak == LineBreak::NoWrap {
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max }));
} else {
content_size.set(NodeMeasure::Text(TextMeasure { info: measure }));
}
// Text measure func created successfully, so set `TextFlags` to schedule a recompute
text_flags.needs_new_measure_func = false;
// Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute
text_flags.needs_measure_fn = false;
text_flags.needs_recompute = true;
}
Err(TextError::NoSuchFont) => {
// Try again next frame
text_flags.needs_new_measure_func = true;
text_flags.needs_measure_fn = true;
}
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
panic!("Fatal error when processing text: {e}.");
@ -167,21 +263,24 @@ pub fn measure_text_system(
mut text_query: Query<
(
Entity,
Ref<Text>,
Ref<TextBlock>,
&mut ContentSize,
&mut TextFlags,
&mut TextNodeFlags,
&mut ComputedTextBlock,
Option<&TargetCamera>,
&mut CosmicBuffer,
),
With<Node>,
>,
mut text_reader: UiTextReader,
mut text_pipeline: ResMut<TextPipeline>,
mut font_system: ResMut<CosmicFontSystem>,
) {
scale_factors_buffer.clear();
for (entity, text, content_size, text_flags, camera, mut buffer) in &mut text_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
for (entity, block, content_size, text_flags, computed, maybe_camera) in &mut text_query {
let Some(camera_entity) = maybe_camera
.map(TargetCamera::entity)
.or(default_ui_camera.get())
else {
continue;
};
@ -196,22 +295,22 @@ pub fn measure_text_system(
* ui_scale.0,
),
};
// Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure().
if last_scale_factors.get(&camera_entity) != Some(&scale_factor)
|| text.is_changed()
|| text_flags.needs_new_measure_func
|| computed.needs_rerender()
|| text_flags.needs_measure_fn
|| content_size.is_added()
{
let text_alignment = text.justify;
create_text_measure(
entity,
&fonts,
scale_factor.into(),
text,
text_reader.iter(entity),
block,
&mut text_pipeline,
content_size,
text_flags,
buffer.as_mut(),
text_alignment,
computed,
&mut font_system,
);
}
@ -222,6 +321,7 @@ pub fn measure_text_system(
#[allow(clippy::too_many_arguments)]
#[inline]
fn queue_text(
entity: Entity,
fonts: &Assets<Font>,
text_pipeline: &mut TextPipeline,
font_atlas_sets: &mut FontAtlasSets,
@ -229,65 +329,64 @@ fn queue_text(
textures: &mut Assets<Image>,
scale_factor: f32,
inverse_scale_factor: f32,
text: &Text,
block: &TextBlock,
node: Ref<Node>,
mut text_flags: Mut<TextFlags>,
mut text_flags: Mut<TextNodeFlags>,
text_layout_info: Mut<TextLayoutInfo>,
buffer: &mut CosmicBuffer,
computed: &mut ComputedTextBlock,
text_reader: &mut UiTextReader,
font_system: &mut CosmicFontSystem,
swash_cache: &mut SwashCache,
) {
// Skip the text node if it is waiting for a new measure func
if !text_flags.needs_new_measure_func {
let physical_node_size = if text.linebreak == LineBreak::NoWrap {
// With `NoWrap` set, no constraints are placed on the width of the text.
TextBounds::UNBOUNDED
} else {
// `scale_factor` is already multiplied by `UiScale`
TextBounds::new(
node.unrounded_size.x * scale_factor,
node.unrounded_size.y * scale_factor,
)
};
if text_flags.needs_measure_fn {
return;
}
let text_layout_info = text_layout_info.into_inner();
match text_pipeline.queue_text(
text_layout_info,
fonts,
&text.sections,
scale_factor.into(),
text.justify,
text.linebreak,
text.font_smoothing,
physical_node_size,
font_atlas_sets,
texture_atlases,
textures,
YAxisOrientation::TopToBottom,
buffer,
font_system,
swash_cache,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, try again next frame
text_flags.needs_recompute = true;
}
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
panic!("Fatal error when processing text: {e}.");
}
Ok(()) => {
text_layout_info.size.x =
scale_value(text_layout_info.size.x, inverse_scale_factor);
text_layout_info.size.y =
scale_value(text_layout_info.size.y, inverse_scale_factor);
text_flags.needs_recompute = false;
}
let physical_node_size = if block.linebreak == LineBreak::NoWrap {
// With `NoWrap` set, no constraints are placed on the width of the text.
TextBounds::UNBOUNDED
} else {
// `scale_factor` is already multiplied by `UiScale`
TextBounds::new(
node.unrounded_size.x * scale_factor,
node.unrounded_size.y * 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.into(),
block,
physical_node_size,
font_atlas_sets,
texture_atlases,
textures,
YAxisOrientation::TopToBottom,
computed,
font_system,
swash_cache,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, try again next frame
text_flags.needs_recompute = true;
}
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
panic!("Fatal error when processing text: {e}.");
}
Ok(()) => {
text_layout_info.size.x = scale_value(text_layout_info.size.x, inverse_scale_factor);
text_layout_info.size.y = scale_value(text_layout_info.size.y, inverse_scale_factor);
text_flags.needs_recompute = false;
}
}
}
/// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component,
/// or when the `needs_recompute` field of [`TextFlags`] is set to true.
/// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true.
/// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`].
///
/// ## World Resources
@ -307,20 +406,26 @@ pub fn text_system(
mut font_atlas_sets: ResMut<FontAtlasSets>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(
Entity,
Ref<Node>,
&Text,
&TextBlock,
&mut TextLayoutInfo,
&mut TextFlags,
&mut TextNodeFlags,
&mut ComputedTextBlock,
Option<&TargetCamera>,
&mut CosmicBuffer,
)>,
mut text_reader: UiTextReader,
mut font_system: ResMut<CosmicFontSystem>,
mut swash_cache: ResMut<SwashCache>,
) {
scale_factors_buffer.clear();
for (node, text, text_layout_info, text_flags, camera, mut buffer) in &mut text_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
for (entity, node, block, text_layout_info, text_flags, mut computed, maybe_camera) in
&mut text_query
{
let Some(camera_entity) = maybe_camera
.map(TargetCamera::entity)
.or(default_ui_camera.get())
else {
continue;
};
@ -342,6 +447,7 @@ pub fn text_system(
|| text_flags.needs_recompute
{
queue_text(
entity,
&fonts,
&mut text_pipeline,
&mut font_atlas_sets,
@ -349,11 +455,12 @@ pub fn text_system(
&mut textures,
scale_factor,
inverse_scale_factor,
text,
block,
node,
text_flags,
text_layout_info,
buffer.as_mut(),
computed.as_mut(),
&mut text_reader,
&mut font_system,
&mut swash_cache,
);

View file

@ -64,15 +64,15 @@ fn setup(
}
#[cfg(not(target_arch = "wasm32"))]
commands.spawn(
TextBundle::from_section("Press space to toggle wireframes", TextStyle::default())
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
commands.spawn((
Text::new("Press space to toggle wireframes"),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
#[cfg(not(target_arch = "wasm32"))]

View file

@ -57,14 +57,15 @@ fn setup(
));
// UI
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
// ------------------------------------------------------------------------------------------------
@ -78,11 +79,10 @@ fn update_bloom_settings(
) {
let bloom = camera.single_mut();
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
match bloom {
(entity, Some(mut bloom)) => {
*text = "Bloom (Toggle: Space)\n".to_string();
**text = "Bloom (Toggle: Space)\n".to_string();
text.push_str(&format!("(Q/A) Intensity: {}\n", bloom.intensity));
text.push_str(&format!(
"(W/S) Low-frequency boost: {}\n",
@ -173,7 +173,7 @@ fn update_bloom_settings(
}
(entity, None) => {
*text = "Bloom: Off (Toggle: Space)".to_string();
**text = "Bloom: Off (Toggle: Space)".to_string();
if keycode.just_pressed(KeyCode::Space) {
commands.entity(entity).insert(Bloom::default());

View file

@ -78,7 +78,6 @@ fn update_text(mut text: Query<&mut Text>, cur_state: Res<State<Test>>) {
}
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
text.clear();
text.push_str("Intersection test:\n");
@ -272,14 +271,15 @@ fn setup(mut commands: Commands) {
Intersects::default(),
));
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
fn draw_filled_circle(gizmos: &mut Gizmos, position: Vec2, color: Srgba) {

View file

@ -134,17 +134,13 @@ fn setup(
));
// create a minimal UI explaining how to interact with the example
commands.spawn(TextBundle {
text: Text::from_section(
"Left Arrow Key: Animate Left Sprite\nRight Arrow Key: Animate Right Sprite",
TextStyle::default(),
),
style: Style {
commands.spawn((
Text::new("Left Arrow Key: Animate Left Sprite\nRight Arrow Key: Animate Right Sprite"),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
..default()
});
));
}

View file

@ -85,12 +85,13 @@ fn spawn_sprites(
cmd.insert(scale_mode);
}
cmd.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text::from_section(label, text_style).with_justify(JustifyText::Center),
transform: Transform::from_xyz(0., -0.5 * size.y - 10., 0.0),
text_anchor: bevy::sprite::Anchor::TopCenter,
..default()
});
builder.spawn((
Text2d::new(label),
text_style,
TextBlock::new_with_justify(JustifyText::Center),
Transform::from_xyz(0., -0.5 * size.y - 10., 0.0),
bevy::sprite::Anchor::TopCenter,
));
});
position.x += 0.5 * size.x + gap;
}

View file

@ -45,29 +45,24 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
// Demonstrate changing translation
commands.spawn((
Text2dBundle {
text: Text::from_section("translation", text_style.clone())
.with_justify(text_justification),
..default()
},
Text2d::new("translation"),
text_style.clone(),
TextBlock::new_with_justify(text_justification),
AnimateTranslation,
));
// Demonstrate changing rotation
commands.spawn((
Text2dBundle {
text: Text::from_section("rotation", text_style.clone())
.with_justify(text_justification),
..default()
},
Text2d::new("rotation"),
text_style.clone(),
TextBlock::new_with_justify(text_justification),
AnimateRotation,
));
// Demonstrate changing scale
commands.spawn((
Text2dBundle {
text: Text::from_section("scale", text_style).with_justify(text_justification),
transform: Transform::from_translation(Vec3::new(400.0, 0.0, 0.0)),
..default()
},
Text2d::new("scale"),
text_style,
TextBlock::new_with_justify(text_justification),
Transform::from_translation(Vec3::new(400.0, 0.0, 0.0)),
AnimateScale,
));
// Demonstrate text wrapping
@ -84,22 +79,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
Transform::from_translation(box_position.extend(0.0)),
))
.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text {
sections: vec![TextSection::new(
"this text wraps in the box\n(Unicode linebreaks)",
slightly_smaller_text_style.clone(),
)],
justify: JustifyText::Left,
linebreak: LineBreak::WordBoundary,
..default()
},
builder.spawn((
Text2d::new("this text wraps in the box\n(Unicode linebreaks)"),
slightly_smaller_text_style.clone(),
TextBlock::new(JustifyText::Left, LineBreak::WordBoundary),
// Wrap text in the rectangle
text_2d_bounds: TextBounds::from(box_size),
TextBounds::from(box_size),
// ensure the text is drawn on top of the box
transform: Transform::from_translation(Vec3::Z),
..default()
});
Transform::from_translation(Vec3::Z),
));
});
let other_box_size = Vec2::new(300.0, 200.0);
@ -110,31 +98,25 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
Transform::from_translation(other_box_position.extend(0.0)),
))
.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text {
sections: vec![TextSection::new(
"this text wraps in the box\n(AnyCharacter linebreaks)",
slightly_smaller_text_style.clone(),
)],
justify: JustifyText::Left,
linebreak: LineBreak::AnyCharacter,
..default()
},
builder.spawn((
Text2d::new("this text wraps in the box\n(AnyCharacter linebreaks)"),
slightly_smaller_text_style.clone(),
TextBlock::new(JustifyText::Left, LineBreak::AnyCharacter),
// Wrap text in the rectangle
text_2d_bounds: TextBounds::from(other_box_size),
TextBounds::from(other_box_size),
// ensure the text is drawn on top of the box
transform: Transform::from_translation(Vec3::Z),
..default()
});
Transform::from_translation(Vec3::Z),
));
});
// Demonstrate font smoothing off
commands.spawn(Text2dBundle {
text: Text::from_section("FontSmoothing::None", slightly_smaller_text_style.clone())
commands.spawn((
Text2d::new("FontSmoothing::None"),
slightly_smaller_text_style
.clone()
.with_font_smoothing(FontSmoothing::None),
transform: Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)),
..default()
});
Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)),
));
for (text_anchor, color) in [
(Anchor::TopLeft, Color::Srgba(RED)),
@ -142,27 +124,21 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
(Anchor::BottomRight, Color::Srgba(BLUE)),
(Anchor::BottomLeft, Color::Srgba(YELLOW)),
] {
commands.spawn(Text2dBundle {
text: Text {
sections: vec![TextSection::new(
format!(" Anchor::{text_anchor:?} "),
TextStyle {
color,
..slightly_smaller_text_style.clone()
},
)],
..Default::default()
commands.spawn((
Text2d::new(format!(" Anchor::{text_anchor:?} ")),
TextStyle {
color,
..slightly_smaller_text_style.clone()
},
transform: Transform::from_translation(250. * Vec3::Y),
Transform::from_translation(250. * Vec3::Y),
text_anchor,
..default()
});
));
}
}
fn animate_translation(
time: Res<Time>,
mut query: Query<&mut Transform, (With<Text>, With<AnimateTranslation>)>,
mut query: Query<&mut Transform, (With<Text2d>, With<AnimateTranslation>)>,
) {
for mut transform in &mut query {
transform.translation.x = 100.0 * ops::sin(time.elapsed_seconds()) - 400.0;
@ -172,7 +148,7 @@ fn animate_translation(
fn animate_rotation(
time: Res<Time>,
mut query: Query<&mut Transform, (With<Text>, With<AnimateRotation>)>,
mut query: Query<&mut Transform, (With<Text2d>, With<AnimateRotation>)>,
) {
for mut transform in &mut query {
transform.rotation = Quat::from_rotation_z(ops::cos(time.elapsed_seconds()));
@ -181,7 +157,7 @@ fn animate_rotation(
fn animate_scale(
time: Res<Time>,
mut query: Query<&mut Transform, (With<Text>, With<AnimateScale>)>,
mut query: Query<&mut Transform, (With<Text2d>, With<AnimateScale>)>,
) {
// Consider changing font-size instead of scaling the transform. Scaling a Text2D will scale the
// rendered quad, resulting in a pixellated look.

View file

@ -277,12 +277,13 @@ fn create_label(
text: &str,
text_style: TextStyle,
) {
commands.spawn(Text2dBundle {
text: Text::from_section(text, text_style).with_justify(JustifyText::Center),
transform: Transform {
commands.spawn((
Text2d::new(text),
text_style,
TextBlock::new_with_justify(JustifyText::Center),
Transform {
translation: Vec3::new(translation.0, translation.1, translation.2),
..default()
},
..default()
});
));
}

View file

@ -88,14 +88,15 @@ fn setup(
commands.spawn(Camera2d);
// Text used to show controls
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
/// This system lets you toggle various wireframe settings
@ -105,7 +106,7 @@ fn update_colors(
mut wireframe_colors: Query<&mut Wireframe2dColor>,
mut text: Query<&mut Text>,
) {
text.single_mut().sections[0].value = format!(
**text.single_mut() = format!(
"Controls
---------------
Z - Toggle global

View file

@ -133,15 +133,15 @@ fn setup(
));
#[cfg(not(target_arch = "wasm32"))]
commands.spawn(
TextBundle::from_section("Press space to toggle wireframes", TextStyle::default())
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
commands.spawn((
Text::new("Press space to toggle wireframes"),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
fn rotate(mut query: Query<&mut Transform, With<Shape>>, time: Res<Time>) {

View file

@ -81,18 +81,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_status: Res
/// Spawns the help text.
fn spawn_text(commands: &mut Commands, app_status: &AppStatus) {
commands.spawn(
TextBundle {
text: app_status.create_help_text(),
..default()
}
.with_style(Style {
commands.spawn((
app_status.create_help_text(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
/// For each material, creates a version with the anisotropy removed.
@ -287,10 +284,10 @@ impl AppStatus {
};
// Build the `Text` object.
Text::from_section(
format!("{}\n{}", material_variant_help_text, light_help_text),
TextStyle::default(),
)
Text(format!(
"{}\n{}",
material_variant_help_text, light_help_text
))
}
}

View file

@ -191,8 +191,7 @@ fn update_ui(
) {
let (fxaa, smaa, taa, cas, msaa) = camera.single();
let mut ui = ui.single_mut();
let ui = &mut ui.sections[0].value;
let ui = &mut **ui.single_mut();
*ui = "Antialias Method\n".to_string();
@ -328,14 +327,15 @@ fn setup(
));
// example instructions
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
/// Writes a simple menu item that can be on or off.

View file

@ -85,17 +85,13 @@ fn setup_terrain_scene(
}
fn setup_instructions(mut commands: Commands) {
commands.spawn(
TextBundle::from_section(
"Press Spacebar to Toggle Atmospheric Fog.\nPress S to Toggle Directional Light Fog Influence.",
TextStyle::default(),
)
.with_style(Style {
commands.spawn((Text::new("Press Spacebar to Toggle Atmospheric Fog.\nPress S to Toggle Directional Light Fog Influence."),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
})
);
}

View file

@ -129,26 +129,24 @@ fn setup(
let text_style = TextStyle::default();
commands.spawn(
TextBundle::from_section(
"Left / Right - Rotate Camera\nC - Toggle Compensation Curve\nM - Toggle Metering Mask\nV - Visualize Metering Mask",
text_style.clone(),
)
.with_style(Style {
commands.spawn((Text::new("Left / Right - Rotate Camera\nC - Toggle Compensation Curve\nM - Toggle Metering Mask\nV - Visualize Metering Mask"),
text_style.clone(), Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
})
);
commands.spawn((
TextBundle::from_section("", text_style).with_style(Style {
Text::default(),
text_style,
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
right: Val::Px(12.0),
..default()
}),
},
ExampleDisplay,
));
}
@ -207,7 +205,7 @@ fn example_control_system(
};
let mut display = display.single_mut();
display.sections[0].value = format!(
**display = format!(
"Compensation Curve: {}\nMetering Mask: {}",
if auto_exposure.compensation_curve == resources.basic_compensation_curve {
"Enabled"

View file

@ -173,26 +173,25 @@ fn setup(
..default()
};
commands.spawn(
TextBundle::from_section(
"Up / Down — Increase / Decrease Alpha\nLeft / Right — Rotate Camera\nH - Toggle HDR\nSpacebar — Toggle Unlit\nC — Randomize Colors",
commands.spawn((Text::new("Up / Down — Increase / Decrease Alpha\nLeft / Right — Rotate Camera\nH - Toggle HDR\nSpacebar — Toggle Unlit\nC — Randomize Colors"),
text_style.clone(),
)
.with_style(Style {
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
})
);
commands.spawn((
TextBundle::from_section("", text_style).with_style(Style {
Text::default(),
text_style,
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
right: Val::Px(12.0),
..default()
}),
},
ExampleDisplay,
));
@ -209,15 +208,16 @@ fn setup(
ExampleLabel { entity },
))
.with_children(|parent| {
parent.spawn(
TextBundle::from_section(label, label_text_style.clone())
.with_style(Style {
position_type: PositionType::Absolute,
bottom: Val::ZERO,
..default()
})
.with_no_wrap(),
);
parent.spawn((
Text::new(label),
label_text_style.clone(),
Style {
position_type: PositionType::Absolute,
bottom: Val::ZERO,
..default()
},
TextBlock::default().with_no_wrap(),
));
});
};
@ -328,7 +328,7 @@ fn example_control_system(
}
let mut display = display.single_mut();
display.sections[0].value = format!(
**display = format!(
" HDR: {}\nAlpha: {:.2}",
if camera.hdr { "ON " } else { "OFF" },
state.alpha

View file

@ -84,14 +84,15 @@ fn setup_scene(
}
// example instructions
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
// ------------------------------------------------------------------------------------------------
@ -105,11 +106,10 @@ fn update_bloom_settings(
) {
let bloom = camera.single_mut();
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
match bloom {
(entity, Some(mut bloom)) => {
*text = "Bloom (Toggle: Space)\n".to_string();
**text = "Bloom (Toggle: Space)\n".to_string();
text.push_str(&format!("(Q/A) Intensity: {}\n", bloom.intensity));
text.push_str(&format!(
"(W/S) Low-frequency boost: {}\n",
@ -200,7 +200,7 @@ fn update_bloom_settings(
}
(entity, None) => {
*text = "Bloom: Off (Toggle: Space)".to_string();
**text = "Bloom: Off (Toggle: Space)".to_string();
if keycode.just_pressed(KeyCode::Space) {
commands.entity(entity).insert(Bloom::NATURAL);

View file

@ -217,18 +217,15 @@ fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
/// Spawns the help text.
fn spawn_text(commands: &mut Commands, light_mode: &LightMode) {
commands.spawn(
TextBundle {
text: light_mode.create_help_text(),
..default()
}
.with_style(Style {
commands.spawn((
light_mode.create_help_text(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
/// Moves the light around.
@ -320,6 +317,6 @@ impl LightMode {
LightMode::Directional => "Press Space to switch to a point light",
};
Text::from_section(help_text, TextStyle::default())
Text::new(help_text)
}
}

View file

@ -314,23 +314,20 @@ fn add_help_text(
font: &Handle<Font>,
currently_selected_option: &SelectedColorGradingOption,
) {
commands
.spawn(TextBundle {
style: Style {
position_type: PositionType::Absolute,
left: Val::Px(12.0),
top: Val::Px(12.0),
..default()
},
..TextBundle::from_section(
create_help_text(currently_selected_option),
TextStyle {
font: font.clone(),
..default()
},
)
})
.insert(HelpText);
commands.spawn((
Text::new(create_help_text(currently_selected_option)),
TextStyle {
font: font.clone(),
..default()
},
Style {
position_type: PositionType::Absolute,
left: Val::Px(12.0),
top: Val::Px(12.0),
..default()
},
HelpText,
));
}
/// Adds some text to the scene.
@ -340,12 +337,13 @@ fn add_text<'a>(
font: &Handle<Font>,
color: Color,
) -> EntityCommands<'a> {
parent.spawn(TextBundle::from_section(
label,
parent.spawn((
Text::new(label),
TextStyle {
font: font.clone(),
font_size: 15.0,
color,
..default()
},
))
}
@ -561,8 +559,9 @@ fn update_ui_state(
&mut BorderColor,
&ColorGradingOptionWidget,
)>,
mut button_text: Query<(&mut Text, &ColorGradingOptionWidget), Without<HelpText>>,
mut help_text: Query<&mut Text, With<HelpText>>,
button_text: Query<(Entity, &ColorGradingOptionWidget), (With<Text>, Without<HelpText>)>,
help_text: Query<Entity, With<HelpText>>,
mut writer: UiTextWriter,
cameras: Query<Ref<ColorGrading>>,
currently_selected_option: Res<SelectedColorGradingOption>,
) {
@ -590,7 +589,7 @@ fn update_ui_state(
});
// Update the buttons.
for (mut text, widget) in button_text.iter_mut() {
for (entity, widget) in button_text.iter() {
// Set the text color.
let color = if *currently_selected_option == widget.option {
@ -599,24 +598,24 @@ fn update_ui_state(
Color::WHITE
};
for section in &mut text.sections {
section.style.color = color;
}
writer.for_each_style(entity, |mut style| {
style.color = color;
});
// Update the displayed value, if this is the currently-selected option.
if widget.widget_type == ColorGradingOptionWidgetType::Value
&& *currently_selected_option == widget.option
{
if let Some(ref value_label) = value_label {
for section in &mut text.sections {
section.value.clone_from(value_label);
}
writer.for_each_text(entity, |mut text| {
text.clone_from(value_label);
});
}
}
}
// Update the help text.
help_text.single_mut().sections[0].value = create_help_text(&currently_selected_option);
*writer.text(help_text.single(), 0) = create_help_text(&currently_selected_option);
}
/// Creates the help text at the top left of the window.

View file

@ -188,14 +188,15 @@ fn setup(
));
// Example instructions
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
#[derive(Resource)]
@ -292,7 +293,6 @@ fn switch_mode(
mut mode: Local<DefaultRenderMode>,
) {
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
text.clear();

View file

@ -92,18 +92,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_settings: R
)));
// Spawn the help text.
commands.spawn(
TextBundle {
text: create_text(&app_settings),
..default()
}
.with_style(Style {
commands.spawn((
create_text(&app_settings),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
/// Adjusts the focal distance and f-number per user inputs.
@ -219,7 +216,7 @@ fn update_text(mut texts: Query<&mut Text>, app_settings: Res<AppSettings>) {
/// Regenerates the app text component per the current app settings.
fn create_text(app_settings: &AppSettings) -> Text {
Text::from_section(app_settings.help_text(), TextStyle::default())
app_settings.help_text().into()
}
impl From<AppSettings> for Option<DepthOfField> {

View file

@ -116,14 +116,15 @@ fn setup_pyramid_scene(
}
fn setup_instructions(mut commands: Commands) {
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
fn update_system(
@ -148,12 +149,10 @@ fn update_system(
.looking_at(Vec3::ZERO, Vec3::Y);
// Fog Information
text.sections[0].value = format!("Fog Falloff: {:?}\nFog Color: {:?}", fog.falloff, fog.color);
**text = format!("Fog Falloff: {:?}\nFog Color: {:?}", fog.falloff, fog.color);
// Fog Falloff Mode Switching
text.sections[0]
.value
.push_str("\n\n1 / 2 / 3 - Fog Falloff Mode");
text.push_str("\n\n1 / 2 / 3 - Fog Falloff Mode");
if keycode.pressed(KeyCode::Digit1) {
if let FogFalloff::Linear { .. } = fog.falloff {
@ -192,9 +191,7 @@ fn update_system(
ref mut end,
} = &mut fog.falloff
{
text.sections[0]
.value
.push_str("\nA / S - Move Start Distance\nZ / X - Move End Distance");
text.push_str("\nA / S - Move Start Distance\nZ / X - Move End Distance");
if keycode.pressed(KeyCode::KeyA) {
*start -= delta * 3.0;
@ -212,7 +209,7 @@ fn update_system(
// Exponential Fog Controls
if let FogFalloff::Exponential { ref mut density } = &mut fog.falloff {
text.sections[0].value.push_str("\nA / S - Change Density");
text.push_str("\nA / S - Change Density");
if keycode.pressed(KeyCode::KeyA) {
*density -= delta * 0.5 * *density;
@ -227,7 +224,7 @@ fn update_system(
// ExponentialSquared Fog Controls
if let FogFalloff::ExponentialSquared { ref mut density } = &mut fog.falloff {
text.sections[0].value.push_str("\nA / S - Change Density");
text.push_str("\nA / S - Change Density");
if keycode.pressed(KeyCode::KeyA) {
*density -= delta * 0.5 * *density;
@ -241,9 +238,7 @@ fn update_system(
}
// RGBA Controls
text.sections[0]
.value
.push_str("\n\n- / = - Red\n[ / ] - Green\n; / ' - Blue\n. / ? - Alpha");
text.push_str("\n\n- / = - Red\n[ / ] - Green\n; / ' - Blue\n. / ? - Alpha");
// We're performing various operations in the sRGB color space,
// so we convert the fog color to sRGB here, then modify it,

View file

@ -57,18 +57,15 @@ fn setup(
commands.spawn((PointLight::default(), camera_and_light_transform));
// Text to describe the controls.
commands.spawn(
TextBundle::from_section(
"Controls:\nSpace: Change UVs\nX/Y/Z: Rotate\nR: Reset orientation",
TextStyle::default(),
)
.with_style(Style {
commands.spawn((
Text::new("Controls:\nSpace: Change UVs\nX/Y/Z: Rotate\nR: Reset orientation"),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
// System to receive input from the user,

View file

@ -295,18 +295,15 @@ fn spawn_fox(commands: &mut Commands, assets: &ExampleAssets) {
}
fn spawn_text(commands: &mut Commands, app_status: &AppStatus) {
commands.spawn(
TextBundle {
text: app_status.create_text(),
..default()
}
.with_style(Style {
commands.spawn((
app_status.create_text(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
// A system that updates the help text.
@ -343,16 +340,14 @@ impl AppStatus {
ExampleModel::Fox => SWITCH_TO_SPHERE_HELP_TEXT,
};
Text::from_section(
format!(
"{CLICK_TO_MOVE_HELP_TEXT}
format!(
"{CLICK_TO_MOVE_HELP_TEXT}
{voxels_help_text}
{irradiance_volume_help_text}
{rotation_help_text}
{switch_mesh_help_text}"
),
TextStyle::default(),
)
.into()
}
}

View file

@ -207,41 +207,40 @@ fn setup(
));
// example instructions
let style = TextStyle::default();
commands.spawn(
TextBundle::from_sections(vec![
TextSection::new(
format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops),
style.clone(),
),
TextSection::new(
format!(
"Shutter speed: 1/{:.0}s\n",
1.0 / parameters.shutter_speed_s
),
style.clone(),
),
TextSection::new(
format!("Sensitivity: ISO {:.0}\n", parameters.sensitivity_iso),
style.clone(),
),
TextSection::new("\n\n", style.clone()),
TextSection::new("Controls\n", style.clone()),
TextSection::new("---------------\n", style.clone()),
TextSection::new("Arrow keys - Move objects\n", style.clone()),
TextSection::new("1/2 - Decrease/Increase aperture\n", style.clone()),
TextSection::new("3/4 - Decrease/Increase shutter speed\n", style.clone()),
TextSection::new("5/6 - Decrease/Increase sensitivity\n", style.clone()),
TextSection::new("R - Reset exposure", style),
])
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
commands
.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
))
.with_children(|p| {
p.spawn(TextSpan(format!(
"Aperture: f/{:.0}\n",
parameters.aperture_f_stops,
)));
p.spawn(TextSpan(format!(
"Shutter speed: 1/{:.0}s\n",
1.0 / parameters.shutter_speed_s
)));
p.spawn(TextSpan(format!(
"Sensitivity: ISO {:.0}\n",
parameters.sensitivity_iso
)));
p.spawn(TextSpan::new("\n\n"));
p.spawn(TextSpan::new("Controls\n"));
p.spawn(TextSpan::new("---------------\n"));
p.spawn(TextSpan::new("Arrow keys - Move objects\n"));
p.spawn(TextSpan::new("1/2 - Decrease/Increase aperture\n"));
p.spawn(TextSpan::new("Arrow keys - Move objects\n"));
p.spawn(TextSpan::new("3/4 - Decrease/Increase shutter speed\n"));
p.spawn(TextSpan::new("5/6 - Decrease/Increase sensitivity\n"));
p.spawn(TextSpan::new("R - Reset exposure"));
});
// camera
commands.spawn((
@ -255,10 +254,11 @@ fn update_exposure(
key_input: Res<ButtonInput<KeyCode>>,
mut parameters: ResMut<Parameters>,
mut exposure: Query<&mut Exposure>,
mut text: Query<&mut Text>,
text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
) {
// TODO: Clamp values to a reasonable range
let mut text = text.single_mut();
let entity = text.single();
if key_input.just_pressed(KeyCode::Digit2) {
parameters.aperture_f_stops *= 2.0;
} else if key_input.just_pressed(KeyCode::Digit1) {
@ -278,12 +278,12 @@ fn update_exposure(
*parameters = Parameters::default();
}
text.sections[0].value = format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops);
text.sections[1].value = format!(
*writer.text(entity, 1) = format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops);
*writer.text(entity, 2) = format!(
"Shutter speed: 1/{:.0}s\n",
1.0 / parameters.shutter_speed_s
);
text.sections[2].value = format!("Sensitivity: ISO {:.0}\n", parameters.sensitivity_iso);
*writer.text(entity, 3) = format!("Sensitivity: ISO {:.0}\n", parameters.sensitivity_iso);
*exposure.single_mut() = Exposure::from_physical_camera(**parameters);
}

View file

@ -34,19 +34,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// a place to display the extras on screen
commands.spawn((
TextBundle::from_section(
"",
TextStyle {
font_size: 15.,
..default()
},
)
.with_style(Style {
Text::default(),
TextStyle {
font_size: 15.,
..default()
},
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
},
ExampleDisplay,
));
}
@ -88,7 +86,6 @@ fn check_for_gltf_extras(
);
gltf_extra_infos_lines.push(formatted_extras);
}
let mut display = display.single_mut();
display.sections[0].value = gltf_extra_infos_lines.join("\n");
**display.single_mut() = gltf_extra_infos_lines.join("\n");
}
}

View file

@ -231,29 +231,30 @@ fn spawn_trees(
}
fn setup_ui(mut commands: Commands) {
let style = TextStyle::default();
commands.spawn(
TextBundle::from_sections(vec![
TextSection::new(String::new(), style.clone()),
TextSection::new(String::new(), style.clone()),
TextSection::new("1/2: -/+ shutter angle (blur amount)\n", style.clone()),
TextSection::new("3/4: -/+ sample count (blur quality)\n", style.clone()),
TextSection::new("Spacebar: cycle camera\n", style.clone()),
])
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
commands
.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
))
.with_children(|p| {
p.spawn(TextSpan::default());
p.spawn(TextSpan::default());
p.spawn(TextSpan::new("1/2: -/+ shutter angle (blur amount)\n"));
p.spawn(TextSpan::new("3/4: -/+ sample count (blur quality)\n"));
p.spawn(TextSpan::new("3/4: -/+ sample count (blur quality)\n"));
});
}
fn keyboard_inputs(
mut motion_blur: Query<&mut MotionBlur>,
presses: Res<ButtonInput<KeyCode>>,
mut text: Query<&mut Text>,
text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
mut camera: ResMut<CameraMode>,
) {
let mut motion_blur = motion_blur.single_mut();
@ -273,9 +274,9 @@ fn keyboard_inputs(
}
motion_blur.shutter_angle = motion_blur.shutter_angle.clamp(0.0, 1.0);
motion_blur.samples = motion_blur.samples.clamp(0, 64);
let mut text = text.single_mut();
text.sections[0].value = format!("Shutter angle: {:.2}\n", motion_blur.shutter_angle);
text.sections[1].value = format!("Samples: {:.5}\n", motion_blur.samples);
let entity = text.single();
*writer.text(entity, 1) = format!("Shutter angle: {:.2}\n", motion_blur.shutter_angle);
*writer.text(entity, 2) = format!("Samples: {:.5}\n", motion_blur.samples);
}
/// Parametric function for a looping race track. `offset` will return the point offset

View file

@ -50,14 +50,13 @@ fn setup(
));
// spawn help text
commands.spawn((
TextBundle::from_sections([
TextSection::new("Press T to toggle OIT\n", TextStyle::default()),
TextSection::new("OIT Enabled", TextStyle::default()),
TextSection::new("\nPress C to cycle test scenes", TextStyle::default()),
]),
RenderLayers::layer(1),
));
commands
.spawn((Text::default(), RenderLayers::layer(1)))
.with_children(|p| {
p.spawn(TextSpan::new("Press T to toggle OIT\n"));
p.spawn(TextSpan::new("OIT Enabled"));
p.spawn(TextSpan::new("\nPress C to cycle test scenes"));
});
// spawn default scene
spawn_spheres(&mut commands, &mut meshes, &mut materials);
@ -65,13 +64,14 @@ fn setup(
fn toggle_oit(
mut commands: Commands,
mut text: Query<&mut Text>,
text: Single<Entity, With<Text>>,
keyboard_input: Res<ButtonInput<KeyCode>>,
q: Query<(Entity, Has<OrderIndependentTransparencySettings>), With<Camera3d>>,
mut text_writer: UiTextWriter,
) {
if keyboard_input.just_pressed(KeyCode::KeyT) {
let (e, has_oit) = q.single();
text.single_mut().sections[1].value = if has_oit {
*text_writer.text(*text, 2) = if has_oit {
// Removing the component will completely disable OIT for this camera
commands
.entity(e)

View file

@ -80,7 +80,8 @@ fn update_parallax_depth_scale(
mut materials: ResMut<Assets<StandardMaterial>>,
mut target_depth: Local<TargetDepth>,
mut depth_update: Local<bool>,
mut text: Query<&mut Text>,
mut writer: UiTextWriter,
text: Query<Entity, With<Text>>,
) {
if input.just_pressed(KeyCode::Digit1) {
target_depth.0 -= DEPTH_UPDATE_STEP;
@ -93,12 +94,11 @@ fn update_parallax_depth_scale(
*depth_update = true;
}
if *depth_update {
let mut text = text.single_mut();
for (_, mat) in materials.iter_mut() {
let current_depth = mat.parallax_depth_scale;
let new_depth = current_depth.lerp(target_depth.0, DEPTH_CHANGE_RATE);
mat.parallax_depth_scale = new_depth;
text.sections[0].value = format!("Parallax depth scale: {new_depth:.5}\n");
*writer.text(text.single(), 1) = format!("Parallax depth scale: {new_depth:.5}\n");
if (new_depth - current_depth).abs() <= 0.000000001 {
*depth_update = false;
}
@ -109,7 +109,8 @@ fn update_parallax_depth_scale(
fn switch_method(
input: Res<ButtonInput<KeyCode>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut text: Query<&mut Text>,
text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
mut current: Local<CurrentMethod>,
) {
if input.just_pressed(KeyCode::Space) {
@ -117,8 +118,8 @@ fn switch_method(
} else {
return;
}
let mut text = text.single_mut();
text.sections[2].value = format!("Method: {}\n", *current);
let text_entity = text.single();
*writer.text(text_entity, 3) = format!("Method: {}\n", *current);
for (_, mat) in materials.iter_mut() {
mat.parallax_mapping_method = current.0;
@ -129,7 +130,8 @@ fn update_parallax_layers(
input: Res<ButtonInput<KeyCode>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut target_layers: Local<TargetLayers>,
mut text: Query<&mut Text>,
text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
) {
if input.just_pressed(KeyCode::Digit3) {
target_layers.0 -= 1.0;
@ -140,8 +142,8 @@ fn update_parallax_layers(
return;
}
let layer_count = ops::exp2(target_layers.0);
let mut text = text.single_mut();
text.sections[1].value = format!("Layers: {layer_count:.0}\n");
let text_entity = text.single();
*writer.text(text_entity, 2) = format!("Layers: {layer_count:.0}\n");
for (_, mat) in materials.iter_mut() {
mat.max_parallax_layer_count = layer_count;
@ -293,35 +295,30 @@ fn setup(
commands.spawn(background_cube_bundle(Vec3::new(0., 0., 45.)));
commands.spawn(background_cube_bundle(Vec3::new(0., 0., -45.)));
let style = TextStyle::default();
// example instructions
commands.spawn(
TextBundle::from_sections(vec![
TextSection::new(
format!("Parallax depth scale: {parallax_depth_scale:.5}\n"),
style.clone(),
),
TextSection::new(
format!("Layers: {max_parallax_layer_count:.0}\n"),
style.clone(),
),
TextSection::new(format!("{parallax_mapping_method}\n"), style.clone()),
TextSection::new("\n\n", style.clone()),
TextSection::new("Controls:\n", style.clone()),
TextSection::new("Left click - Change view angle\n", style.clone()),
TextSection::new(
commands
.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
))
.with_children(|p| {
p.spawn(TextSpan(format!(
"Parallax depth scale: {parallax_depth_scale:.5}\n"
)));
p.spawn(TextSpan(format!("Layers: {max_parallax_layer_count:.0}\n")));
p.spawn(TextSpan(format!("{parallax_mapping_method}\n")));
p.spawn(TextSpan::new("\n\n"));
p.spawn(TextSpan::new("Controls:\n"));
p.spawn(TextSpan::new("Left click - Change view angle\n"));
p.spawn(TextSpan::new(
"1/2 - Decrease/Increase parallax depth scale\n",
style.clone(),
),
TextSection::new("3/4 - Decrease/Increase layer count\n", style.clone()),
TextSection::new("Space - Switch parallaxing algorithm\n", style),
])
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
));
p.spawn(TextSpan::new("3/4 - Decrease/Increase layer count\n"));
p.spawn(TextSpan::new("Space - Switch parallaxing algorithm\n"));
});
}

View file

@ -58,57 +58,50 @@ fn setup(
));
// labels
commands.spawn(
TextBundle::from_section(
"Perceptual Roughness",
TextStyle {
font_size: 30.0,
..default()
},
)
.with_style(Style {
commands.spawn((
Text::new("Perceptual Roughness"),
TextStyle {
font_size: 30.0,
..default()
},
Style {
position_type: PositionType::Absolute,
top: Val::Px(20.0),
left: Val::Px(100.0),
..default()
}),
);
},
));
commands.spawn(TextBundle {
text: Text::from_section(
"Metallic",
TextStyle {
font_size: 30.0,
..default()
},
),
style: Style {
commands.spawn((
Text::new("Metallic"),
TextStyle {
font_size: 30.0,
..default()
},
Style {
position_type: PositionType::Absolute,
top: Val::Px(130.0),
right: Val::ZERO,
..default()
},
transform: Transform {
Transform {
rotation: Quat::from_rotation_z(std::f32::consts::PI / 2.0),
..default()
},
..default()
});
));
commands.spawn((
TextBundle::from_section(
"Loading Environment Map...",
TextStyle {
font_size: 30.0,
..default()
},
)
.with_style(Style {
Text::new("Loading Environment Map..."),
TextStyle {
font_size: 30.0,
..default()
},
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(20.0),
right: Val::Px(20.0),
..default()
}),
},
EnvironmentMapLabel,
));

View file

@ -250,15 +250,17 @@ fn spawn_buttons(commands: &mut Commands) {
fn update_radio_buttons(
mut widgets: Query<
(
Entity,
Option<&mut BackgroundColor>,
Option<&mut Text>,
Has<Text>,
&WidgetClickSender<AppSetting>,
),
Or<(With<RadioButton>, With<RadioButtonText>)>,
>,
app_status: Res<AppStatus>,
mut writer: UiTextWriter,
) {
for (image, text, sender) in widgets.iter_mut() {
for (entity, image, has_text, sender) in widgets.iter_mut() {
let selected = match **sender {
AppSetting::LightType(light_type) => light_type == app_status.light_type,
AppSetting::ShadowFilter(shadow_filter) => shadow_filter == app_status.shadow_filter,
@ -268,8 +270,8 @@ fn update_radio_buttons(
if let Some(mut bg_color) = image {
widgets::update_ui_radio_button(&mut bg_color, selected);
}
if let Some(mut text) = text {
widgets::update_ui_radio_button_text(&mut text, selected);
if has_text {
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
}
}
}

View file

@ -122,18 +122,15 @@ fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) {
/// Spawns the help text at the bottom of the screen.
fn spawn_text(commands: &mut Commands, app_settings: &AppSettings) {
commands.spawn(
TextBundle {
text: create_help_text(app_settings),
..default()
}
.with_style(Style {
commands.spawn((
create_help_text(app_settings),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
impl Default for AppSettings {
@ -146,13 +143,11 @@ impl Default for AppSettings {
/// Creates help text at the bottom of the screen.
fn create_help_text(app_settings: &AppSettings) -> Text {
Text::from_section(
format!(
"Chromatic aberration intensity: {} (Press Left or Right to change)",
app_settings.chromatic_aberration_intensity
),
TextStyle::default(),
format!(
"Chromatic aberration intensity: {} (Press Left or Right to change)",
app_settings.chromatic_aberration_intensity
)
.into()
}
/// Handles requests from the user to change the chromatic aberration intensity.

View file

@ -152,18 +152,15 @@ fn spawn_reflection_probe(commands: &mut Commands, cubemaps: &Cubemaps) {
// Spawns the help text.
fn spawn_text(commands: &mut Commands, app_status: &AppStatus) {
// Create the text.
commands.spawn(
TextBundle {
text: app_status.create_text(),
..default()
}
.with_style(Style {
commands.spawn((
app_status.create_text(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
// Adds a world environment map to the camera. This separate system is needed because the camera is
@ -276,13 +273,11 @@ impl AppStatus {
START_ROTATION_HELP_TEXT
};
Text::from_section(
format!(
"{}\n{}\n{}",
self.reflection_mode, rotation_help_text, REFLECTION_MODE_HELP_TEXT
),
TextStyle::default(),
format!(
"{}\n{}\n{}",
self.reflection_mode, rotation_help_text, REFLECTION_MODE_HELP_TEXT
)
.into()
}
}

View file

@ -102,8 +102,6 @@ fn setup(
MeshMaterial3d(white_handle),
));
let style = TextStyle::default();
commands
.spawn((
NodeBundle {
@ -117,55 +115,42 @@ fn setup(
},
GlobalZIndex(i32::MAX),
))
.with_children(|c| {
c.spawn(TextBundle::from_sections([
TextSection::new("Controls:\n", style.clone()),
TextSection::new("R / Z - reset biases to default / zero\n", style.clone()),
TextSection::new(
.with_children(|p| {
p.spawn(Text::default()).with_children(|p| {
p.spawn(TextSpan::new("Controls:\n"));
p.spawn(TextSpan::new("R / Z - reset biases to default / zero\n"));
p.spawn(TextSpan::new(
"L - switch between directional and point lights [",
style.clone(),
),
TextSection::new("DirectionalLight", style.clone()),
TextSection::new("]\n", style.clone()),
TextSection::new(
));
p.spawn(TextSpan::new("DirectionalLight"));
p.spawn(TextSpan::new("]\n"));
p.spawn(TextSpan::new(
"F - switch directional light filter methods [",
style.clone(),
),
TextSection::new("Hardware2x2", style.clone()),
TextSection::new("]\n", style.clone()),
TextSection::new("1/2 - change point light depth bias [", style.clone()),
TextSection::new("0.00", style.clone()),
TextSection::new("]\n", style.clone()),
TextSection::new("3/4 - change point light normal bias [", style.clone()),
TextSection::new("0.0", style.clone()),
TextSection::new("]\n", style.clone()),
TextSection::new("5/6 - change direction light depth bias [", style.clone()),
TextSection::new("0.00", style.clone()),
TextSection::new("]\n", style.clone()),
TextSection::new(
));
p.spawn(TextSpan::new("Hardware2x2"));
p.spawn(TextSpan::new("]\n"));
p.spawn(TextSpan::new("1/2 - change point light depth bias ["));
p.spawn(TextSpan::new("0.00"));
p.spawn(TextSpan::new("]\n"));
p.spawn(TextSpan::new("3/4 - change point light normal bias ["));
p.spawn(TextSpan::new("0.0"));
p.spawn(TextSpan::new("]\n"));
p.spawn(TextSpan::new("5/6 - change direction light depth bias ["));
p.spawn(TextSpan::new("0.00"));
p.spawn(TextSpan::new("]\n"));
p.spawn(TextSpan::new(
"7/8 - change direction light normal bias [",
style.clone(),
),
TextSection::new("0.0", style.clone()),
TextSection::new("]\n", style.clone()),
TextSection::new(
));
p.spawn(TextSpan::new("0.0"));
p.spawn(TextSpan::new("]\n"));
p.spawn(TextSpan::new(
"left/right/up/down/pgup/pgdown - adjust light position (looking at 0,0,0) [",
style.clone(),
),
TextSection::new(
format!("{:.1},", light_transform.translation.x),
style.clone(),
),
TextSection::new(
format!(" {:.1},", light_transform.translation.y),
style.clone(),
),
TextSection::new(
format!(" {:.1}", light_transform.translation.z),
style.clone(),
),
TextSection::new("]\n", style.clone()),
]));
));
p.spawn(TextSpan(format!("{:.1},", light_transform.translation.x)));
p.spawn(TextSpan(format!(" {:.1},", light_transform.translation.y)));
p.spawn(TextSpan(format!(" {:.1}", light_transform.translation.z)));
p.spawn(TextSpan::new("]\n"));
});
});
}
@ -173,12 +158,13 @@ fn toggle_light(
input: Res<ButtonInput<KeyCode>>,
mut point_lights: Query<&mut PointLight>,
mut directional_lights: Query<&mut DirectionalLight>,
mut example_text: Query<&mut Text>,
example_text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
) {
if input.just_pressed(KeyCode::KeyL) {
for mut light in &mut point_lights {
light.intensity = if light.intensity == 0.0 {
example_text.single_mut().sections[3].value = "PointLight".to_string();
*writer.text(example_text.single(), 4) = "PointLight".to_string();
100000000.0
} else {
0.0
@ -186,7 +172,7 @@ fn toggle_light(
}
for mut light in &mut directional_lights {
light.illuminance = if light.illuminance == 0.0 {
example_text.single_mut().sections[3].value = "DirectionalLight".to_string();
*writer.text(example_text.single(), 4) = "DirectionalLight".to_string();
100000.0
} else {
0.0
@ -198,7 +184,8 @@ fn toggle_light(
fn adjust_light_position(
input: Res<ButtonInput<KeyCode>>,
mut lights: Query<&mut Transform, With<Lights>>,
mut example_text: Query<&mut Text>,
example_text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
) {
let mut offset = Vec3::ZERO;
if input.just_pressed(KeyCode::ArrowLeft) {
@ -220,13 +207,13 @@ fn adjust_light_position(
offset.y += 1.0;
}
if offset != Vec3::ZERO {
let mut example_text = example_text.single_mut();
let example_text = example_text.single();
for mut light in &mut lights {
light.translation += offset;
light.look_at(Vec3::ZERO, Vec3::Y);
example_text.sections[21].value = format!("{:.1},", light.translation.x);
example_text.sections[22].value = format!(" {:.1},", light.translation.y);
example_text.sections[23].value = format!(" {:.1}", light.translation.z);
*writer.text(example_text, 22) = format!("{:.1},", light.translation.x);
*writer.text(example_text, 23) = format!(" {:.1},", light.translation.y);
*writer.text(example_text, 24) = format!(" {:.1}", light.translation.z);
}
}
}
@ -234,7 +221,8 @@ fn adjust_light_position(
fn cycle_filter_methods(
input: Res<ButtonInput<KeyCode>>,
mut filter_methods: Query<&mut ShadowFilteringMethod>,
mut example_text: Query<&mut Text>,
example_text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
) {
if input.just_pressed(KeyCode::KeyF) {
for mut filter_method in &mut filter_methods {
@ -253,7 +241,7 @@ fn cycle_filter_methods(
ShadowFilteringMethod::Hardware2x2
}
};
example_text.single_mut().sections[6].value = filter_method_string;
*writer.text(example_text.single(), 7) = filter_method_string;
}
}
}
@ -261,7 +249,8 @@ fn cycle_filter_methods(
fn adjust_point_light_biases(
input: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut PointLight>,
mut example_text: Query<&mut Text>,
example_text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
) {
let depth_bias_step_size = 0.01;
let normal_bias_step_size = 0.1;
@ -287,15 +276,16 @@ fn adjust_point_light_biases(
light.shadow_normal_bias = 0.0;
}
example_text.single_mut().sections[9].value = format!("{:.2}", light.shadow_depth_bias);
example_text.single_mut().sections[12].value = format!("{:.1}", light.shadow_normal_bias);
*writer.text(example_text.single(), 10) = format!("{:.2}", light.shadow_depth_bias);
*writer.text(example_text.single(), 13) = format!("{:.1}", light.shadow_normal_bias);
}
}
fn adjust_directional_light_biases(
input: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut DirectionalLight>,
mut example_text: Query<&mut Text>,
example_text: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
) {
let depth_bias_step_size = 0.01;
let normal_bias_step_size = 0.1;
@ -321,7 +311,7 @@ fn adjust_directional_light_biases(
light.shadow_normal_bias = 0.0;
}
example_text.single_mut().sections[15].value = format!("{:.2}", light.shadow_depth_bias);
example_text.single_mut().sections[18].value = format!("{:.1}", light.shadow_normal_bias);
*writer.text(example_text.single(), 16) = format!("{:.2}", light.shadow_depth_bias);
*writer.text(example_text.single(), 19) = format!("{:.1}", light.shadow_normal_bias);
}
}

View file

@ -95,16 +95,15 @@ fn setup(
},
))
.with_children(|parent| {
parent.spawn(
TextBundle::from_section(*camera_name, TextStyle::default()).with_style(
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.),
left: Val::Px(12.),
..default()
},
),
);
parent.spawn((
Text::new(*camera_name),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.),
left: Val::Px(12.),
..default()
},
));
buttons_panel(parent);
});
}
@ -150,7 +149,7 @@ fn setup(
},
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(caption, TextStyle::default()));
parent.spawn(Text::new(caption));
});
}
}

View file

@ -126,14 +126,15 @@ fn setup(
Transform::from_xyz(-4.0, 5.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn(
TextBundle::from_section(INSTRUCTIONS, TextStyle::default()).with_style(Style {
commands.spawn((
Text::new(INSTRUCTIONS),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
fn light_sway(time: Res<Time>, mut query: Query<(&mut Transform, &mut SpotLight)>) {

View file

@ -78,14 +78,15 @@ fn setup(
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
));
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
fn update(
@ -166,7 +167,6 @@ fn update(
}
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
text.clear();
let (o, l, m, h, u) = match ssao.map(|s| s.quality_level) {

View file

@ -251,38 +251,33 @@ fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
// Spawns the help text.
fn spawn_text(commands: &mut Commands, app_settings: &AppSettings) {
commands.spawn(
TextBundle {
text: create_text(app_settings),
..default()
}
.with_style(Style {
commands.spawn((
create_text(app_settings),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
// Creates or recreates the help text.
fn create_text(app_settings: &AppSettings) -> Text {
Text::from_section(
format!(
"{}\n{}\n{}",
match app_settings.displayed_model {
DisplayedModel::Cube => SWITCH_TO_FLIGHT_HELMET_HELP_TEXT,
DisplayedModel::FlightHelmet => SWITCH_TO_CUBE_HELP_TEXT,
},
if app_settings.ssr_on {
TURN_SSR_OFF_HELP_TEXT
} else {
TURN_SSR_ON_HELP_TEXT
},
MOVE_CAMERA_HELP_TEXT
),
TextStyle::default(),
format!(
"{}\n{}\n{}",
match app_settings.displayed_model {
DisplayedModel::Cube => SWITCH_TO_FLIGHT_HELMET_HELP_TEXT,
DisplayedModel::FlightHelmet => SWITCH_TO_CUBE_HELP_TEXT,
},
if app_settings.ssr_on {
TURN_SSR_OFF_HELP_TEXT
} else {
TURN_SSR_ON_HELP_TEXT
},
MOVE_CAMERA_HELP_TEXT
)
.into()
}
impl MaterialExtension for Water {

View file

@ -81,14 +81,15 @@ fn setup(
));
// ui
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
fn setup_basic_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
@ -169,25 +170,22 @@ fn setup_image_viewer_scene(
HDRViewer,
));
commands
.spawn((
TextBundle::from_section(
"Drag and drop an HDR or EXR file",
TextStyle {
font_size: 36.0,
color: Color::BLACK,
..default()
},
)
.with_text_justify(JustifyText::Center)
.with_style(Style {
align_self: AlignSelf::Center,
margin: UiRect::all(Val::Auto),
..default()
}),
SceneNumber(3),
))
.insert(Visibility::Hidden);
commands.spawn((
Text::new("Drag and drop an HDR or EXR file"),
TextStyle {
font_size: 36.0,
color: Color::BLACK,
..default()
},
TextBlock::new_with_justify(JustifyText::Center),
Style {
align_self: AlignSelf::Center,
margin: UiRect::all(Val::Auto),
..default()
},
SceneNumber(3),
Visibility::Hidden,
));
}
// ----------------------------------------------------------------------------
@ -403,13 +401,13 @@ fn update_ui(
*hide_ui = !*hide_ui;
}
let old_text = &text_query.single().sections[0].value;
let old_text = text_query.single();
if *hide_ui {
if !old_text.is_empty() {
// single_mut() always triggers change detection,
// so only access if text actually needs changing
text_query.single_mut().sections[0].value.clear();
text_query.single_mut().clear();
}
return;
}
@ -534,7 +532,7 @@ fn update_ui(
if text != old_text.as_str() {
// single_mut() always triggers change detection,
// so only access if text actually changed
text_query.single_mut().sections[0].value = text;
**text_query.single_mut() = text;
}
}

View file

@ -331,15 +331,14 @@ fn setup(
));
// Controls Text
let text_style = TextStyle::default();
commands.spawn((
TextBundle::from_section("", text_style).with_style(Style {
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
},
ExampleDisplay,
));
}
@ -549,8 +548,7 @@ fn example_control_system(
Quat::from_euler(EulerRot::XYZ, 0.0, rotation, 0.0),
);
let mut display = display.single_mut();
display.sections[0].value = format!(
**display.single_mut() = format!(
concat!(
" J / K / L / ; Screen Space Specular Transmissive Quality: {:?}\n",
" O / P Screen Space Specular Transmissive Steps: {}\n",

View file

@ -149,18 +149,15 @@ fn setup(
});
// Create the text.
commands.spawn(
TextBundle {
text: app_status.create_text(),
..default()
}
.with_style(Style {
commands.spawn((
app_status.create_text(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
// We need to add the `VisibilityRange` components manually, as glTF currently
@ -297,31 +294,29 @@ fn update_help_text(mut text_query: Query<&mut Text>, app_status: Res<AppStatus>
impl AppStatus {
// Creates and returns help text reflecting the app status.
fn create_text(&self) -> Text {
Text::from_section(
format!(
"\
format!(
"\
{} (1) Switch from high-poly to low-poly based on camera distance
{} (2) Show only the high-poly model
{} (3) Show only the low-poly model
Press 1, 2, or 3 to switch which model is shown
Press WASD or use the mouse wheel to move the camera",
if self.show_one_model_only.is_none() {
'>'
} else {
' '
},
if self.show_one_model_only == Some(MainModel::HighPoly) {
'>'
} else {
' '
},
if self.show_one_model_only == Some(MainModel::LowPoly) {
'>'
} else {
' '
},
),
TextStyle::default(),
if self.show_one_model_only.is_none() {
'>'
} else {
' '
},
if self.show_one_model_only == Some(MainModel::HighPoly) {
'>'
} else {
' '
},
if self.show_one_model_only == Some(MainModel::LowPoly) {
'>'
} else {
' '
},
)
.into()
}
}

View file

@ -123,38 +123,33 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_settings: R
));
// Add the help text.
commands.spawn(
TextBundle {
text: create_text(&app_settings),
..default()
}
.with_style(Style {
commands.spawn((
create_text(&app_settings),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
fn create_text(app_settings: &AppSettings) -> Text {
Text::from_section(
format!(
"{}\n{}\n{}",
"Press WASD or the arrow keys to change the direction of the directional light",
if app_settings.volumetric_pointlight {
"Press P to turn volumetric point light off"
} else {
"Press P to turn volumetric point light on"
},
if app_settings.volumetric_spotlight {
"Press L to turn volumetric spot light off"
} else {
"Press L to turn volumetric spot light on"
}
),
TextStyle::default(),
format!(
"{}\n{}\n{}",
"Press WASD or the arrow keys to change the direction of the directional light",
if app_settings.volumetric_pointlight {
"Press P to turn volumetric point light off"
} else {
"Press P to turn volumetric point light on"
},
if app_settings.volumetric_spotlight {
"Press L to turn volumetric spot light off"
} else {
"Press L to turn volumetric spot light on"
}
)
.into()
}
/// A system that makes directional lights in the glTF scene into volumetric

View file

@ -99,14 +99,15 @@ fn setup(
));
// Text used to show controls
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
/// This system let's you toggle various wireframe settings
@ -116,7 +117,7 @@ fn update_colors(
mut wireframe_colors: Query<&mut WireframeColor, With<Wireframe>>,
mut text: Query<&mut Text>,
) {
text.single_mut().sections[0].value = format!(
**text.single_mut() = format!(
"Controls
---------------
Z - Toggle global

View file

@ -40,22 +40,22 @@ fn main() {
}
impl AnimatableProperty for FontSizeProperty {
type Component = Text;
type Component = TextStyle;
type Property = f32;
fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
Some(&mut component.sections.get_mut(0)?.style.font_size)
Some(&mut component.font_size)
}
}
impl AnimatableProperty for TextColorProperty {
type Component = Text;
type Component = TextStyle;
type Property = Srgba;
fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
match component.sections.get_mut(0)?.style.color {
match component.color {
Color::Srgba(ref mut color) => Some(color),
_ => None,
}
@ -170,17 +170,16 @@ fn setup(
// Build the text node.
let player = builder.parent_entity();
builder
.spawn(
TextBundle::from_section(
"Bevy",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 24.0,
color: Color::Srgba(Srgba::RED),
},
)
.with_text_justify(JustifyText::Center),
)
.spawn((
Text::new("Bevy"),
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 24.0,
color: Color::Srgba(Srgba::RED),
..default()
},
TextBlock::new_with_justify(JustifyText::Center),
))
// Mark as an animation target.
.insert(AnimationTarget {
id: animation_target_id,

View file

@ -37,11 +37,12 @@ impl AnimationEvent for MessageEvent {
fn edit_message(
mut event_reader: EventReader<MessageEvent>,
mut text: Single<&mut Text, With<MessageText>>,
text: Single<(&mut Text2d, &mut TextStyle), With<MessageText>>,
) {
let (mut text, mut style) = text.into_inner();
for event in event_reader.read() {
text.sections[0].value = event.value.clone();
text.sections[0].style.color = event.color;
text.0 = event.value.clone();
style.color = event.color;
}
}
@ -67,16 +68,11 @@ fn setup(
// The text that will be changed by animation events.
commands.spawn((
MessageText,
Text2dBundle {
text: Text::from_section(
"",
TextStyle {
font_size: 119.0,
color: Color::NONE,
..Default::default()
},
),
..Default::default()
Text2d::default(),
TextStyle {
font_size: 119.0,
color: Color::NONE,
..default()
},
));
@ -112,10 +108,9 @@ fn setup(
}
// Slowly fade out the text opacity.
fn animate_text_opacity(mut query: Query<&mut Text>, time: Res<Time>) {
for mut text in &mut query {
let color = &mut text.sections[0].style.color;
let a = color.alpha();
color.set_alpha(a - time.delta_seconds());
fn animate_text_opacity(mut styles: Query<&mut TextStyle>, time: Res<Time>) {
for mut style in &mut styles {
let a = style.color.alpha();
style.color.set_alpha(a - time.delta_seconds());
}
}

View file

@ -250,16 +250,15 @@ fn setup_scene(
/// Places the help text at the top left of the window.
fn setup_help_text(commands: &mut Commands) {
commands.spawn(TextBundle {
text: Text::from_section(HELP_TEXT, TextStyle::default()),
style: Style {
commands.spawn((
Text::new(HELP_TEXT),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
..default()
});
));
}
/// Initializes the node UI widgets.
@ -271,18 +270,15 @@ fn setup_node_rects(commands: &mut Commands) {
};
let text = commands
.spawn(TextBundle {
text: Text::from_section(
node_string,
TextStyle {
font_size: 16.0,
color: ANTIQUE_WHITE.into(),
..default()
},
)
.with_justify(JustifyText::Center),
..default()
})
.spawn((
Text::new(node_string),
TextStyle {
font_size: 16.0,
color: ANTIQUE_WHITE.into(),
..default()
},
TextBlock::new_with_justify(JustifyText::Center),
))
.id();
let container = {
@ -444,7 +440,7 @@ fn update_ui(
// Update the node labels with the current weights.
let mut text_iter = text_query.iter_many_mut(children);
if let Some(mut text) = text_iter.fetch_next() {
text.sections[0].value = format!(
**text = format!(
"{}\n{:.2}",
clip_node.text, animation_weights.weights[clip_node.index]
);

View file

@ -156,18 +156,15 @@ fn setup_scene(
// Creates the UI.
fn setup_ui(mut commands: Commands) {
// Add help text.
commands.spawn(
TextBundle::from_section(
"Click on a button to toggle animations for its associated bones",
TextStyle::default(),
)
.with_style(Style {
commands.spawn((
Text::new("Click on a button to toggle animations for its associated bones"),
Style {
position_type: PositionType::Absolute,
left: Val::Px(12.0),
top: Val::Px(12.0),
..default()
}),
);
},
));
// Add the buttons that allow the user to toggle mask groups on and off.
commands
@ -286,14 +283,14 @@ fn add_mask_group_control(parent: &mut ChildBuilder, label: &str, width: Val, ma
background_color: Color::BLACK.into(),
..default()
})
.with_child(TextBundle {
text: Text::from_section(label, label_text_style.clone()),
style: Style {
.with_child((
Text::new(label),
label_text_style.clone(),
Style {
margin: UiRect::vertical(Val::Px(3.0)),
..default()
},
..default()
});
));
builder
.spawn(NodeBundle {
@ -337,29 +334,24 @@ fn add_mask_group_control(parent: &mut ChildBuilder, label: &str, width: Val, ma
border_color: BorderColor(Color::WHITE),
..default()
})
.with_child(
TextBundle {
style: Style {
flex_grow: 1.0,
margin: UiRect::vertical(Val::Px(3.0)),
..default()
},
text: Text::from_section(
format!("{:?}", label),
if index > 0 {
button_text_style.clone()
} else {
selected_button_text_style.clone()
},
),
.with_child((
Text(format!("{:?}", label)),
if index > 0 {
button_text_style.clone()
} else {
selected_button_text_style.clone()
},
TextBlock::new_with_justify(JustifyText::Center),
Style {
flex_grow: 1.0,
margin: UiRect::vertical(Val::Px(3.0)),
..default()
}
.with_text_justify(JustifyText::Center),
)
.insert(AnimationControl {
group_id: mask_group_id,
label: *label,
});
},
AnimationControl {
group_id: mask_group_id,
label: *label,
},
));
}
});
});
@ -482,7 +474,8 @@ fn handle_button_toggles(
// A system that updates the UI based on the current app state.
fn update_ui(
mut animation_controls: Query<(&AnimationControl, &mut BackgroundColor, &Children)>,
mut texts: Query<&mut Text>,
texts: Query<Entity, With<Text>>,
mut writer: UiTextWriter,
app_state: Res<AppState>,
) {
for (animation_control, mut background_color, kids) in animation_controls.iter_mut() {
@ -496,13 +489,13 @@ fn update_ui(
};
for &kid in kids {
let Ok(mut text) = texts.get_mut(kid) else {
let Ok(text) = texts.get(kid) else {
continue;
};
for section in &mut text.sections {
section.style.color = if enabled { Color::BLACK } else { Color::WHITE };
}
writer.for_each_style(text, |mut style| {
style.color = if enabled { Color::BLACK } else { Color::WHITE };
});
}
}
}

View file

@ -63,22 +63,17 @@ fn setup(mut commands: Commands) {
let color = Hsla::hsl(i as f32 / 11.0 * 360.0, 0.8, 0.75).into();
commands
.spawn((
Text2dBundle {
text: Text::from_section(
format!("{:?}", function),
TextStyle {
color,
..text_style.clone()
},
),
transform: Transform::from_xyz(
i as f32 * 113.0 - 1280.0 / 2.0 + 25.0,
-100.0 - ((j as f32 * 250.0) - 300.0),
0.0,
),
text_anchor: Anchor::TopLeft,
..default()
Text2d(format!("{:?}", function)),
TextStyle {
color,
..text_style.clone()
},
Transform::from_xyz(
i as f32 * 113.0 - 1280.0 / 2.0 + 25.0,
-100.0 - ((j as f32 * 250.0) - 300.0),
0.0,
),
Anchor::TopLeft,
SelectedEaseFunction(*function, color),
))
.with_children(|p| {
@ -93,14 +88,15 @@ fn setup(mut commands: Commands) {
});
}
}
commands.spawn(
TextBundle::from_section("", TextStyle::default()).with_style(Style {
commands.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
const SIZE_PER_FUNCTION: f32 = 95.0;
@ -109,7 +105,7 @@ fn display_curves(
mut gizmos: Gizmos,
ease_functions: Query<(&SelectedEaseFunction, &Transform, &Children)>,
mut transforms: Query<&mut Transform, Without<SelectedEaseFunction>>,
mut ui: Query<&mut Text, With<Node>>,
mut ui_text: Single<&mut Text>,
time: Res<Time>,
) {
let samples = 100;
@ -119,7 +115,7 @@ fn display_curves(
let now = ((time.elapsed_seconds() % (duration + time_margin * 2.0) - time_margin) / duration)
.clamp(0.0, 1.0);
ui.single_mut().sections[0].value = format!("Progress: {:.2}", now);
ui_text.0 = format!("Progress: {:.2}", now);
for (SelectedEaseFunction(function, color), transform, children) in &ease_functions {
// Draw a box around the curve

View file

@ -143,10 +143,7 @@ fn print_logs(
commands.entity(root_entity).with_children(|child| {
for event in events.read() {
child.spawn(TextBundle::from_section(
&event.message,
TextStyle::default(),
));
child.spawn(Text::new(&event.message));
}
});
}

View file

@ -19,16 +19,15 @@ fn main() {
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn(TextBundle {
text: Text::from_section("Press P to panic", TextStyle::default()),
style: Style {
commands.spawn((
Text::new("Press P to panic"),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
..default()
});
));
}
fn panic_on_p(keys: Res<ButtonInput<KeyCode>>) {

View file

@ -147,13 +147,9 @@ fn spawn_text(mut commands: Commands) {
},
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Space: swap meshes by mutating a Handle<Mesh>",
TextStyle::default(),
));
parent.spawn(TextBundle::from_section(
parent.spawn(Text::new("Space: swap meshes by mutating a Handle<Mesh>"));
parent.spawn(Text::new(
"Return: mutate the mesh itself, changing all copies of it",
TextStyle::default(),
));
});
}

View file

@ -105,13 +105,11 @@ fn spawn_text(mut commands: Commands) {
},
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
parent.spawn(Text::new(
"Space: swap image texture paths by mutating a Handle<Image>",
TextStyle::default(),
));
parent.spawn(TextBundle::from_section(
parent.spawn(Text::new(
"Return: mutate the image Asset itself, changing all copies of it",
TextStyle::default(),
));
});
}

View file

@ -181,21 +181,13 @@ fn setup_ui(mut commands: Commands) {
})
.with_children(|b| {
b.spawn((
TextBundle {
text: Text {
sections: vec![TextSection {
value: "Loading...".to_owned(),
style: TextStyle {
font_size: 53.0,
color: Color::BLACK,
..Default::default()
},
}],
justify: JustifyText::Right,
..Default::default()
},
Text::new("Loading...".to_owned()),
TextStyle {
font_size: 53.0,
color: Color::BLACK,
..Default::default()
},
TextBlock::new_with_justify(JustifyText::Right),
LoadingText,
));
});
@ -278,7 +270,7 @@ fn get_async_loading_state(
if is_loaded {
next_loading_state.set(LoadingState::Loaded);
if let Ok(mut text) = text.get_single_mut() {
"Loaded!".clone_into(&mut text.sections[0].value);
"Loaded!".clone_into(&mut **text);
}
}
}

View file

@ -57,18 +57,18 @@ fn spawn_text(mut commands: Commands, mut reader: EventReader<StreamEvent>) {
let text_style = TextStyle::default();
for (per_frame, event) in reader.read().enumerate() {
commands.spawn(Text2dBundle {
text: Text::from_section(event.0.to_string(), text_style.clone())
.with_justify(JustifyText::Center),
transform: Transform::from_xyz(per_frame as f32 * 100.0, 300.0, 0.0),
..default()
});
commands.spawn((
Text2d::new(event.0.to_string()),
text_style.clone(),
TextBlock::new_with_justify(JustifyText::Center),
Transform::from_xyz(per_frame as f32 * 100.0, 300.0, 0.0),
));
}
}
fn move_text(
mut commands: Commands,
mut texts: Query<(Entity, &mut Transform), With<Text>>,
mut texts: Query<(Entity, &mut Transform), With<Text2d>>,
time: Res<Time>,
) {
for (entity, mut position) in &mut texts {

View file

@ -59,18 +59,15 @@ fn setup(
});
// example instructions
commands.spawn(
TextBundle::from_section(
"Up/Down/Left/Right: Move Listener\nSpace: Toggle Emitter Movement",
TextStyle::default(),
)
.with_style(Style {
commands.spawn((
Text::new("Up/Down/Left/Right: Move Listener\nSpace: Toggle Emitter Movement"),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
// camera
commands.spawn(Camera2d);

View file

@ -58,18 +58,15 @@ fn setup(
));
// example instructions
commands.spawn(
TextBundle::from_section(
"Up/Down/Left/Right: Move Listener\nSpace: Toggle Emitter Movement",
TextStyle::default(),
)
.with_style(Style {
commands.spawn((
Text::new("Up/Down/Left/Right: Move Listener\nSpace: Toggle Emitter Movement"),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
// camera
commands.spawn((

View file

@ -63,15 +63,15 @@ fn setup_scene(
}
fn setup_instructions(mut commands: Commands) {
commands.spawn(
TextBundle::from_section("Hold space to trigger a screen shake", TextStyle::default())
.with_style(Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
commands.spawn((
Text::new("Hold space to trigger a screen shake"),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
fn setup_camera(mut commands: Commands) {

View file

@ -49,18 +49,15 @@ fn setup_scene(
}
fn setup_instructions(mut commands: Commands) {
commands.spawn(
TextBundle::from_section(
"Move the light with WASD.\nThe camera will smoothly track the light.",
TextStyle::default(),
)
.with_style(Style {
commands.spawn((
Text::new("Move the light with WASD.\nThe camera will smoothly track the light."),
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
fn setup_camera(mut commands: Commands) {

View file

@ -95,18 +95,9 @@ fn instructions(mut commands: Commands) {
},
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Mouse up or down: pitch",
TextStyle::default(),
));
parent.spawn(TextBundle::from_section(
"Mouse left or right: yaw",
TextStyle::default(),
));
parent.spawn(TextBundle::from_section(
"Mouse buttons: roll",
TextStyle::default(),
));
parent.spawn(Text::new("Mouse up or down: pitch"));
parent.spawn(Text::new("Mouse left or right: yaw"));
parent.spawn(Text::new("Mouse buttons: roll"));
});
}

View file

@ -203,16 +203,11 @@ fn spawn_text(mut commands: Commands) {
},
..default()
})
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
concat!(
"Move the camera with your mouse.\n",
"Press arrow up to decrease the FOV of the world model.\n",
"Press arrow down to increase the FOV of the world model."
),
TextStyle::default(),
));
});
.with_child(Text::new(concat!(
"Move the camera with your mouse.\n",
"Press arrow up to decrease the FOV of the world model.\n",
"Press arrow down to increase the FOV of the world model."
)));
}
fn move_player(

View file

@ -104,13 +104,9 @@ fn instructions(mut commands: Commands) {
},
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Scroll mouse wheel to zoom in/out",
TextStyle::default(),
));
parent.spawn(TextBundle::from_section(
parent.spawn(Text::new("Scroll mouse wheel to zoom in/out"));
parent.spawn(Text::new(
"Space: switch between orthographic and perspective projections",
TextStyle::default(),
));
});
}

View file

@ -3,6 +3,7 @@
use bevy::{
dev_tools::fps_overlay::{FpsOverlayConfig, FpsOverlayPlugin},
prelude::*,
text::FontSmoothing,
};
struct OverlayColor;
@ -25,6 +26,8 @@ fn main() {
color: OverlayColor::GREEN,
// If we want, we can use a custom font
font: default(),
// We could also disable font smoothing,
font_smoothing: FontSmoothing::default(),
},
enabled: true,
},
@ -52,15 +55,12 @@ fn setup(mut commands: Commands) {
..default()
})
.with_children(|c| {
c.spawn(TextBundle::from_section(
concat!(
"Press 1 to toggle the overlay color.\n",
"Press 2 to decrease the overlay size.\n",
"Press 3 to increase the overlay size.\n",
"Press 4 to toggle the overlay visibility."
),
TextStyle::default(),
));
c.spawn(Text::new(concat!(
"Press 1 to toggle the overlay color.\n",
"Press 2 to decrease the overlay size.\n",
"Press 3 to increase the overlay size.\n",
"Press 4 to toggle the overlay visibility."
)));
});
}

View file

@ -71,22 +71,22 @@ struct Explode;
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn(
TextBundle::from_section(
commands.spawn((
Text::new(
"Click on a \"Mine\" to trigger it.\n\
When it explodes it will trigger all overlapping mines.",
TextStyle {
color: Color::WHITE,
..default()
},
)
.with_style(Style {
),
TextStyle {
color: Color::WHITE,
..default()
},
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.),
left: Val::Px(12.),
..default()
}),
);
},
));
let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);

View file

@ -79,40 +79,37 @@ fn evaluate_callbacks(query: Query<(Entity, &Callback), With<Triggered>>, mut co
}
}
fn system_a(mut query: Query<&mut Text>) {
let mut text = query.single_mut();
text.sections[2].value = String::from("A");
fn system_a(query: Query<Entity, With<Text>>, mut writer: UiTextWriter) {
*writer.text(query.single(), 3) = String::from("A");
info!("A: One shot system registered with Commands was triggered");
}
fn system_b(mut query: Query<&mut Text>) {
let mut text = query.single_mut();
text.sections[2].value = String::from("B");
fn system_b(query: Query<Entity, With<Text>>, mut writer: UiTextWriter) {
*writer.text(query.single(), 3) = String::from("B");
info!("B: One shot system registered with World was triggered");
}
fn setup_ui(mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn(
TextBundle::from_sections([
TextSection::new(
"Press A or B to trigger a one-shot system\n",
TextStyle::default(),
),
TextSection::new("Last Triggered: ", TextStyle::default()),
TextSection::new(
"-",
commands
.spawn((
Text::default(),
TextBlock::new_with_justify(JustifyText::Center),
Style {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
))
.with_children(|p| {
p.spawn(TextSpan::new("Press A or B to trigger a one-shot system\n"));
p.spawn(TextSpan::new("Last Triggered: "));
p.spawn((
TextSpan::new("-"),
TextStyle {
color: bevy::color::palettes::css::ORANGE.into(),
..default()
},
),
])
.with_text_justify(JustifyText::Center)
.with_style(Style {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
}),
);
));
});
}

View file

@ -175,22 +175,20 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMu
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/cakeBirthday.glb"));
// scoreboard
commands.spawn(
TextBundle::from_section(
"Score:",
TextStyle {
font_size: 33.0,
color: Color::srgb(0.5, 0.5, 1.0),
..default()
},
)
.with_style(Style {
commands.spawn((
Text::new("Score:"),
TextStyle {
font_size: 33.0,
color: Color::srgb(0.5, 0.5, 1.0),
..default()
},
Style {
position_type: PositionType::Absolute,
top: Val::Px(5.0),
left: Val::Px(5.0),
..default()
}),
);
},
));
commands.insert_resource(Random(rng));
}
@ -380,8 +378,7 @@ fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Tra
// update the score displayed during the game
fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) {
let mut text = query.single_mut();
text.sections[0].value = format!("Sugar Rush: {}", game.score);
**query.single_mut() = format!("Sugar Rush: {}", game.score);
}
// restart the game when pressing spacebar
@ -406,14 +403,12 @@ fn display_score(mut commands: Commands, game: Res<Game>) {
},
..default()
})
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
format!("Cake eaten: {}", game.cake_eaten),
TextStyle {
font_size: 67.0,
color: Color::srgb(0.5, 0.5, 1.0),
..default()
},
));
});
.with_child((
Text::new(format!("Cake eaten: {}", game.cake_eaten)),
TextStyle {
font_size: 67.0,
color: Color::srgb(0.5, 0.5, 1.0),
..default()
},
));
}

View file

@ -216,30 +216,30 @@ fn setup(
));
// Scoreboard
commands.spawn((
ScoreboardUi,
TextBundle::from_sections([
TextSection::new(
"Score: ",
TextStyle {
font_size: SCOREBOARD_FONT_SIZE,
color: TEXT_COLOR,
..default()
},
),
TextSection::from_style(TextStyle {
commands
.spawn((
Text::new("Score: "),
TextStyle {
font_size: SCOREBOARD_FONT_SIZE,
color: TEXT_COLOR,
..default()
},
ScoreboardUi,
Style {
position_type: PositionType::Absolute,
top: SCOREBOARD_TEXT_PADDING,
left: SCOREBOARD_TEXT_PADDING,
..default()
},
))
.with_child((
TextSpan::default(),
TextStyle {
font_size: SCOREBOARD_FONT_SIZE,
color: SCORE_COLOR,
..default()
}),
])
.with_style(Style {
position_type: PositionType::Absolute,
top: SCOREBOARD_TEXT_PADDING,
left: SCOREBOARD_TEXT_PADDING,
..default()
}),
));
},
));
// Walls
commands.spawn(WallBundle::new(WallLocation::Left));
@ -334,9 +334,12 @@ fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>
}
}
fn update_scoreboard(score: Res<Score>, mut query: Query<&mut Text, With<ScoreboardUi>>) {
let mut text = query.single_mut();
text.sections[1].value = score.to_string();
fn update_scoreboard(
score: Res<Score>,
query: Query<Entity, (With<ScoreboardUi>, With<Text>)>,
mut writer: UiTextWriter,
) {
*writer.text(query.single(), 1) = score.to_string();
}
fn check_for_collisions(

View file

@ -134,30 +134,34 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
};
commands.spawn((
TextBundle::from_sections([
TextSection::new("Contributor showcase", text_style.clone()),
TextSection::from_style(TextStyle {
commands
.spawn((
Text::new("Contributor showcase"),
text_style.clone(),
ContributorDisplay,
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.),
left: Val::Px(12.),
..default()
},
))
.with_child((
TextSpan::default(),
TextStyle {
font_size: 30.,
..text_style
}),
])
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.),
left: Val::Px(12.),
..default()
}),
ContributorDisplay,
));
},
));
}
/// Finds the next contributor to display and selects the entity
fn selection(
mut timer: ResMut<SelectionTimer>,
mut contributor_selection: ResMut<ContributorSelection>,
mut text_query: Query<&mut Text, With<ContributorDisplay>>,
text_query: Query<Entity, (With<ContributorDisplay>, With<Text>)>,
mut query: Query<(&Contributor, &mut Sprite, &mut Transform)>,
mut writer: UiTextWriter,
time: Res<Time>,
) {
if !timer.0.tick(time.delta()).just_finished() {
@ -182,8 +186,14 @@ fn selection(
let entity = contributor_selection.order[contributor_selection.idx];
if let Ok((contributor, mut sprite, mut transform)) = query.get_mut(entity) {
let mut text = text_query.single_mut();
select(&mut sprite, contributor, &mut transform, &mut text);
let entity = text_query.single();
select(
&mut sprite,
contributor,
&mut transform,
entity,
&mut writer,
);
}
}
@ -193,19 +203,20 @@ fn select(
sprite: &mut Sprite,
contributor: &Contributor,
transform: &mut Transform,
text: &mut Text,
entity: Entity,
writer: &mut UiTextWriter,
) {
sprite.color = SELECTED.with_hue(contributor.hue).into();
transform.translation.z = 100.0;
text.sections[0].value.clone_from(&contributor.name);
text.sections[1].value = format!(
writer.text(entity, 0).clone_from(&contributor.name);
*writer.text(entity, 1) = format!(
"\n{} commit{}",
contributor.num_commits,
if contributor.num_commits > 1 { "s" } else { "" }
);
text.sections[0].style.color = sprite.color;
writer.style(entity, 0).color = sprite.color;
}
/// Change the tint color to the "deselected" color and push

View file

@ -113,14 +113,9 @@ fn setup(
..default()
};
commands.spawn((
Text2dBundle {
text: Text::from_section(
"Press Space to play on your desktop! Press it again to return.\nRight click Bevy logo to exit.",
text_style.clone(),
),
transform: Transform::from_xyz(0.0, -300.0, 100.0),
..default()
},
Text2d::new("Press Space to play on your desktop! Press it again to return.\nRight click Bevy logo to exit."),
text_style.clone(),
Transform::from_xyz(0.0, -300.0, 100.0),
InstructionsText,
));

View file

@ -173,54 +173,52 @@ mod game {
background_color: Color::BLACK.into(),
..default()
})
.with_children(|parent| {
// Display two lines of text, the second one with the current settings
parent.spawn(
TextBundle::from_section(
"Will be back to the menu shortly...",
.with_children(|p| {
p.spawn((
Text::new("Will be back to the menu shortly..."),
TextStyle {
font_size: 67.0,
color: TEXT_COLOR,
..default()
},
Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
},
));
p.spawn((
Text::default(),
Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
},
))
.with_children(|p| {
p.spawn((
TextSpan(format!("quality: {:?}", *display_quality)),
TextStyle {
font_size: 67.0,
font_size: 50.0,
color: BLUE.into(),
..default()
},
));
p.spawn((
TextSpan::new(" - "),
TextStyle {
font_size: 50.0,
color: TEXT_COLOR,
..default()
},
)
.with_style(Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
}),
);
parent.spawn(
TextBundle::from_sections([
TextSection::new(
format!("quality: {:?}", *display_quality),
TextStyle {
font_size: 50.0,
color: BLUE.into(),
..default()
},
),
TextSection::new(
" - ",
TextStyle {
font_size: 50.0,
color: TEXT_COLOR,
..default()
},
),
TextSection::new(
format!("volume: {:?}", *volume),
TextStyle {
font_size: 50.0,
color: LIME.into(),
..default()
},
),
])
.with_style(Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
}),
);
));
p.spawn((
TextSpan(format!("volume: {:?}", *volume)),
TextStyle {
font_size: 50.0,
color: LIME.into(),
..default()
},
));
});
});
});
// Spawn a 5 seconds timer to trigger going back to the menu
@ -433,20 +431,18 @@ mod menu {
})
.with_children(|parent| {
// Display the game name
parent.spawn(
TextBundle::from_section(
"Bevy Game Menu UI",
TextStyle {
font_size: 67.0,
color: TEXT_COLOR,
..default()
},
)
.with_style(Style {
parent.spawn((
Text::new("Bevy Game Menu UI"),
TextStyle {
font_size: 67.0,
color: TEXT_COLOR,
..default()
},
Style {
margin: UiRect::all(Val::Px(50.0)),
..default()
}),
);
},
));
// Display three buttons for each action available from the main menu:
// - new game
@ -468,10 +464,7 @@ mod menu {
image: UiImage::new(icon),
..default()
});
parent.spawn(TextBundle::from_section(
"New Game",
button_text_style.clone(),
));
parent.spawn((Text::new("New Game"), button_text_style.clone()));
});
parent
.spawn((
@ -489,10 +482,7 @@ mod menu {
image: UiImage::new(icon),
..default()
});
parent.spawn(TextBundle::from_section(
"Settings",
button_text_style.clone(),
));
parent.spawn((Text::new("Settings"), button_text_style.clone()));
});
parent
.spawn((
@ -510,7 +500,7 @@ mod menu {
image: UiImage::new(icon),
..default()
});
parent.spawn(TextBundle::from_section("Quit", button_text_style));
parent.spawn((Text::new("Quit"), button_text_style));
});
});
});
@ -573,10 +563,7 @@ mod menu {
action,
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
text,
button_text_style.clone(),
));
parent.spawn((Text::new(text), button_text_style.clone()));
});
}
});
@ -637,8 +624,8 @@ mod menu {
})
.with_children(|parent| {
// Display a label for the current setting
parent.spawn(TextBundle::from_section(
"Display Quality",
parent.spawn((
Text::new("Display Quality"),
button_text_style.clone(),
));
// Display a button for each possible value
@ -660,8 +647,8 @@ mod menu {
quality_setting,
));
entity.with_children(|parent| {
parent.spawn(TextBundle::from_section(
format!("{quality_setting:?}"),
parent.spawn((
Text::new(format!("{quality_setting:?}")),
button_text_style.clone(),
));
});
@ -681,7 +668,7 @@ mod menu {
MenuButtonAction::BackToSettings,
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section("Back", button_text_style));
parent.spawn((Text::new("Back"), button_text_style));
});
});
});
@ -738,10 +725,7 @@ mod menu {
..default()
})
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Volume",
button_text_style.clone(),
));
parent.spawn((Text::new("Volume"), button_text_style.clone()));
for volume_setting in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
let mut entity = parent.spawn((
ButtonBundle {
@ -769,9 +753,7 @@ mod menu {
},
MenuButtonAction::BackToSettings,
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section("Back", button_text_style));
});
.with_child((Text::new("Back"), button_text_style));
});
});
}

View file

@ -91,12 +91,7 @@ fn setup(mut commands: Commands) {
},
..default()
})
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Press 1 or 2 to load a new scene.",
text_style,
));
});
.with_child((Text::new("Press 1 or 2 to load a new scene."), text_style));
}
// Selects the level you want to load.
@ -275,12 +270,7 @@ fn load_loading_screen(mut commands: Commands) {
},
LoadingScreen,
))
.with_children(|parent| {
parent.spawn(TextBundle::from_sections([TextSection::new(
"Loading...",
text_style.clone(),
)]));
});
.with_child((Text::new("Loading..."), text_style.clone()));
}
// Determines when to show the loading screen

View file

@ -103,7 +103,7 @@ fn build_ui(
mut stepping: ResMut<Stepping>,
mut state: ResMut<State>,
) {
let mut text_sections = Vec::new();
let mut text_spans = Vec::new();
let mut always_run = Vec::new();
let Ok(schedule_order) = stepping.schedules() else {
@ -114,8 +114,8 @@ fn build_ui(
// each label
for label in schedule_order {
let schedule = schedules.get(*label).unwrap();
text_sections.push(TextSection::new(
format!("{label:?}\n"),
text_spans.push((
TextSpan(format!("{label:?}\n")),
TextStyle {
font: asset_server.load(FONT_BOLD),
color: FONT_COLOR,
@ -138,11 +138,12 @@ fn build_ui(
// Add an entry to our systems list so we can find where to draw
// the cursor when the stepping cursor is at this system
state.systems.push((*label, node_id, text_sections.len()));
// we add plus 1 to account for the empty root span
state.systems.push((*label, node_id, text_spans.len() + 1));
// Add a text section for displaying the cursor for this system
text_sections.push(TextSection::new(
" ",
text_spans.push((
TextSpan::new(" "),
TextStyle {
color: FONT_COLOR,
..default()
@ -150,8 +151,8 @@ fn build_ui(
));
// add the name of the system to the ui
text_sections.push(TextSection::new(
format!("{}\n", system.name()),
text_spans.push((
TextSpan(format!("{}\n", system.name())),
TextStyle {
color: FONT_COLOR,
..default()
@ -164,22 +165,25 @@ fn build_ui(
stepping.always_run_node(label, node);
}
commands.spawn((
SteppingUi,
TextBundle {
text: Text::from_sections(text_sections),
style: Style {
commands
.spawn((
Text::default(),
SteppingUi,
Style {
position_type: PositionType::Absolute,
top: state.ui_top,
left: state.ui_left,
padding: UiRect::all(Val::Px(10.0)),
..default()
},
background_color: BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.33)),
visibility: Visibility::Hidden,
..default()
},
));
BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.33)),
Visibility::Hidden,
))
.with_children(|p| {
for span in text_spans {
p.spawn(span);
}
});
}
fn build_stepping_hint(mut commands: Commands) {
@ -190,20 +194,20 @@ fn build_stepping_hint(mut commands: Commands) {
};
info!("{}", hint_text);
// stepping description box
commands.spawn((TextBundle::from_sections([TextSection::new(
hint_text,
commands.spawn((
Text::new(hint_text),
TextStyle {
font_size: 15.0,
color: FONT_COLOR,
..default()
},
)])
.with_style(Style {
position_type: PositionType::Absolute,
bottom: Val::Px(5.0),
left: Val::Px(5.0),
..default()
}),));
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(5.0),
left: Val::Px(5.0),
..default()
},
));
}
fn handle_input(keyboard_input: Res<ButtonInput<KeyCode>>, mut stepping: ResMut<Stepping>) {
@ -239,14 +243,15 @@ fn update_ui(
mut commands: Commands,
state: Res<State>,
stepping: Res<Stepping>,
mut ui: Query<(Entity, &mut Text, &Visibility), With<SteppingUi>>,
ui: Query<(Entity, &Visibility), With<SteppingUi>>,
mut writer: UiTextWriter,
) {
if ui.is_empty() {
return;
}
// ensure the UI is only visible when stepping is enabled
let (ui, mut text, vis) = ui.single_mut();
let (ui, vis) = ui.single();
match (vis, stepping.is_enabled()) {
(Visibility::Hidden, true) => {
commands.entity(ui).insert(Visibility::Inherited);
@ -274,6 +279,6 @@ fn update_ui(
} else {
" "
};
text.sections[*text_index].value = mark.to_string();
*writer.text(ui, *text_index) = mark.to_string();
}
}

View file

@ -20,22 +20,21 @@ struct MyRoundGizmos {}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
// text
commands.spawn(
TextBundle::from_section(
commands.spawn((
Text::new(
"Hold 'Left' or 'Right' to change the line width of straight gizmos\n\
Hold 'Up' or 'Down' to change the line width of round gizmos\n\
Press '1' / '2' to toggle the visibility of straight / round gizmos\n\
Press 'U' / 'I' to cycle through line styles\n\
Press 'J' / 'K' to cycle through line joins",
TextStyle::default(),
)
.with_style(Style {
),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.),
left: Val::Px(12.),
..default()
}),
);
},
));
}
fn draw_example_collection(

View file

@ -51,8 +51,8 @@ fn setup(
));
// example instructions
commands.spawn(
TextBundle::from_section(
commands.spawn((
Text::new(
"Press 'T' to toggle drawing gizmos on top of everything else in the scene\n\
Press 'P' to toggle perspective for line gizmos\n\
Hold 'Left' or 'Right' to change the line width of straight gizmos\n\
@ -61,15 +61,14 @@ fn setup(
Press 'B' to show all AABB boxes\n\
Press 'U' or 'I' to cycle through line styles for straight or round gizmos\n\
Press 'J' or 'K' to cycle through line joins for straight or round gizmos",
TextStyle::default(),
)
.with_style(Style {
),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
}
fn draw_example_collection(

View file

@ -102,41 +102,37 @@ fn setup(
// Example instructions and gizmo config.
{
let text_style = TextStyle::default();
commands.spawn(
TextBundle::from_section(
commands.spawn((
Text::new(
"Press 'D' to toggle drawing gizmos on top of everything else in the scene\n\
Hold 'Left' or 'Right' to change the line width of the gizmos\n\
Press 'A' to toggle drawing of the light gizmos\n\
Press 'C' to cycle between the light gizmos coloring modes",
text_style.clone(),
)
.with_style(Style {
),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
let (_, light_config) = config_store.config_mut::<LightGizmoConfigGroup>();
light_config.draw_all = true;
light_config.color = LightGizmoColor::MatchLightColor;
commands.spawn((
TextBundle::from_sections([
TextSection::new("Gizmo color mode: ", text_style.clone()),
TextSection::new(gizmo_color_text(light_config), text_style),
])
.with_style(Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
GizmoColorText,
));
commands
.spawn((
Text::new("Gizmo color mode: "),
GizmoColorText,
Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
))
.with_child(TextSpan(gizmo_color_text(light_config)));
}
}
@ -150,7 +146,8 @@ fn update_config(
mut config_store: ResMut<GizmoConfigStore>,
keyboard: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
mut color_text_query: Query<&mut Text, With<GizmoColorText>>,
color_text_query: Query<Entity, With<GizmoColorText>>,
mut writer: UiTextWriter,
) {
if keyboard.just_pressed(KeyCode::KeyD) {
for (_, config, _) in config_store.iter_mut() {
@ -177,6 +174,6 @@ fn update_config(
LightGizmoColor::MatchLightColor => LightGizmoColor::ByLightType,
LightGizmoColor::ByLightType => LightGizmoColor::Manual(GRAY.into()),
};
color_text_query.single_mut().sections[1].value = gizmo_color_text(light_config);
*writer.text(color_text_query.single(), 1) = gizmo_color_text(light_config);
}
}

View file

@ -132,8 +132,8 @@ pub fn spawn_ui_text<'a>(
label: &str,
color: Color,
) -> EntityCommands<'a> {
parent.spawn(TextBundle::from_section(
label,
parent.spawn((
Text::new(label),
TextStyle {
font_size: 18.0,
color,
@ -168,10 +168,10 @@ pub fn update_ui_radio_button(background_color: &mut BackgroundColor, selected:
/// Updates the style of the label of a radio button to reflect its selected
/// status.
pub fn update_ui_radio_button_text(text: &mut Text, selected: bool) {
pub fn update_ui_radio_button_text(entity: Entity, writer: &mut UiTextWriter, selected: bool) {
let text_color = if selected { Color::BLACK } else { Color::WHITE };
for section in &mut text.sections {
section.style.color = text_color;
}
writer.for_each_style(entity, |mut style| {
style.color = text_color;
});
}

View file

@ -34,47 +34,49 @@ fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
// sections that will hold text input.
let font = asset_server.load("fonts/FiraMono-Medium.ttf");
commands.spawn(
TextBundle::from_sections([
TextSection::from("Click to toggle IME. Press return to start a new line.\n\n"),
TextSection::from("IME Enabled: "),
TextSection::from("false\n"),
TextSection::from("IME Active: "),
TextSection::from("false\n"),
TextSection::from("IME Buffer: "),
TextSection {
value: "\n".to_string(),
style: TextStyle {
commands
.spawn((
Text::default(),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
))
.with_children(|p| {
p.spawn(TextSpan::new(
"Click to toggle IME. Press return to start a new line.\n\n",
));
p.spawn(TextSpan::new("IME Enabled: "));
p.spawn(TextSpan::new("false\n"));
p.spawn(TextSpan::new("IME Active: "));
p.spawn(TextSpan::new("false\n"));
p.spawn(TextSpan::new("IME Buffer: "));
p.spawn((
TextSpan::new("\n"),
TextStyle {
font: font.clone(),
..default()
},
},
])
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
));
});
commands.spawn(Text2dBundle {
text: Text::from_section(
"".to_string(),
TextStyle {
font,
font_size: 100.0,
..default()
},
),
..default()
});
commands.spawn((
Text2d::new(""),
TextStyle {
font,
font_size: 100.0,
..default()
},
));
}
fn toggle_ime(
input: Res<ButtonInput<MouseButton>>,
mut windows: Query<&mut Window>,
mut text: Query<&mut Text, With<Node>>,
status_text: Query<Entity, (With<Node>, With<Text>)>,
mut ui_writer: UiTextWriter,
) {
if input.just_pressed(MouseButton::Left) {
let mut window = windows.single_mut();
@ -82,8 +84,7 @@ fn toggle_ime(
window.ime_position = window.cursor_position().unwrap();
window.ime_enabled = !window.ime_enabled;
let mut text = text.single_mut();
text.sections[2].value = format!("{}\n", window.ime_enabled);
*ui_writer.text(status_text.single(), 3) = format!("{}\n", window.ime_enabled);
}
}
@ -107,25 +108,26 @@ fn bubbling_text(
fn listen_ime_events(
mut events: EventReader<Ime>,
mut status_text: Query<&mut Text, With<Node>>,
mut edit_text: Query<&mut Text, (Without<Node>, Without<Bubble>)>,
status_text: Query<Entity, (With<Node>, With<Text>)>,
mut edit_text: Query<&mut Text2d, (Without<Node>, Without<Bubble>)>,
mut ui_writer: UiTextWriter,
) {
for event in events.read() {
match event {
Ime::Preedit { value, cursor, .. } if !cursor.is_none() => {
status_text.single_mut().sections[6].value = format!("{value}\n");
*ui_writer.text(status_text.single(), 7) = format!("{value}\n");
}
Ime::Preedit { cursor, .. } if cursor.is_none() => {
status_text.single_mut().sections[6].value = "\n".to_string();
*ui_writer.text(status_text.single(), 7) = "\n".to_string();
}
Ime::Commit { value, .. } => {
edit_text.single_mut().sections[0].value.push_str(value);
edit_text.single_mut().push_str(value);
}
Ime::Enabled { .. } => {
status_text.single_mut().sections[4].value = "true\n".to_string();
*ui_writer.text(status_text.single(), 5) = "true\n".to_string();
}
Ime::Disabled { .. } => {
status_text.single_mut().sections[4].value = "false\n".to_string();
*ui_writer.text(status_text.single(), 5) = "false\n".to_string();
}
_ => (),
}
@ -135,7 +137,7 @@ fn listen_ime_events(
fn listen_keyboard_input_events(
mut commands: Commands,
mut events: EventReader<KeyboardInput>,
mut edit_text: Query<&mut Text, (Without<Node>, Without<Bubble>)>,
mut edit_text: Query<(&mut Text2d, &TextStyle), (Without<Node>, Without<Bubble>)>,
) {
for event in events.read() {
// Only trigger changes when the key is first pressed.
@ -145,30 +147,28 @@ fn listen_keyboard_input_events(
match &event.logical_key {
Key::Enter => {
let mut text = edit_text.single_mut();
if text.sections[0].value.is_empty() {
let (mut text, style) = edit_text.single_mut();
if text.is_empty() {
continue;
}
let old_value = mem::take(&mut text.sections[0].value);
let old_value = mem::take(&mut **text);
commands.spawn((
Text2dBundle {
text: Text::from_section(old_value, text.sections[0].style.clone()),
..default()
},
Text2d::new(old_value),
style.clone(),
Bubble {
timer: Timer::from_seconds(5.0, TimerMode::Once),
},
));
}
Key::Space => {
edit_text.single_mut().sections[0].value.push(' ');
edit_text.single_mut().0.push(' ');
}
Key::Backspace => {
edit_text.single_mut().sections[0].value.pop();
edit_text.single_mut().0.pop();
}
Key::Character(character) => {
edit_text.single_mut().sections[0].value.push_str(character);
edit_text.single_mut().0.push_str(character);
}
_ => continue,
}

View file

@ -90,15 +90,9 @@ fn setup(mut commands: Commands) {
..default()
})
.with_children(|parent| {
parent.spawn(TextBundle::from_section(instructions_text, style.clone()));
parent.spawn((
SplineModeText,
TextBundle::from_section(spline_mode_text, style.clone()),
));
parent.spawn((
CyclingModeText,
TextBundle::from_section(cycling_mode_text, style.clone()),
));
parent.spawn((Text::new(instructions_text), style.clone()));
parent.spawn((SplineModeText, Text(spline_mode_text), style.clone()));
parent.spawn((CyclingModeText, Text(cycling_mode_text), style.clone()));
});
}
@ -264,9 +258,7 @@ fn update_spline_mode_text(
let new_text = format!("Spline: {}", *spline_mode);
for mut spline_mode_text in spline_mode_text.iter_mut() {
if let Some(section) = spline_mode_text.sections.first_mut() {
section.value.clone_from(&new_text);
}
(**spline_mode_text).clone_from(&new_text);
}
}
@ -281,9 +273,7 @@ fn update_cycling_mode_text(
let new_text = format!("{}", *cycling_mode);
for mut cycling_mode_text in cycling_mode_text.iter_mut() {
if let Some(section) = cycling_mode_text.sections.first_mut() {
section.value.clone_from(&new_text);
}
(**cycling_mode_text).clone_from(&new_text);
}
}

View file

@ -156,19 +156,14 @@ fn setup(
));
// Example instructions
commands.spawn(
TextBundle::from_section(
"Press 'B' to toggle between no bounding shapes, bounding boxes (AABBs) and bounding spheres / circles\n\
Press 'Space' to switch between 3D and 2D",
TextStyle::default(),
)
.with_style(Style {
commands.spawn((Text::new("Press 'B' to toggle between no bounding shapes, bounding boxes (AABBs) and bounding spheres / circles\n\
Press 'Space' to switch between 3D and 2D"),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
}));
}
// Rotate the 2D shapes.

View file

@ -109,23 +109,22 @@ fn setup(
})));
// Instructions for the example:
commands.spawn(
TextBundle::from_section(
commands.spawn((
Text::new(
"Controls:\n\
M: Toggle between sampling boundary and interior.\n\
R: Restart (erase all samples).\n\
S: Add one random sample.\n\
D: Add 100 random samples.\n\
Rotate camera by holding left mouse and panning left/right.",
TextStyle::default(),
)
.with_style(Style {
),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
// The mode starts with interior points.
commands.insert_resource(Mode::Interior);

View file

@ -367,22 +367,6 @@ fn setup_text(mut commands: Commands, cameras: Query<(Entity, &Camera)>) {
.iter()
.find_map(|(entity, camera)| camera.is_active.then_some(entity))
.expect("run condition ensures existence");
let text = format!("{text}", text = PrimitiveSelected::default());
let style = TextStyle::default();
let instructions = "Press 'C' to switch between 2D and 3D mode\n\
Press 'Up' or 'Down' to switch to the next/previous primitive";
let text = [
TextSection::new("Primitive: ", style.clone()),
TextSection::new(text, style.clone()),
TextSection::new("\n\n", style.clone()),
TextSection::new(instructions, style.clone()),
TextSection::new("\n\n", style.clone()),
TextSection::new(
"(If nothing is displayed, there's no rendering support yet)",
style.clone(),
),
];
commands
.spawn((
HeaderNode,
@ -396,22 +380,40 @@ fn setup_text(mut commands: Commands, cameras: Query<(Entity, &Camera)>) {
},
TargetCamera(active_camera),
))
.with_children(|parent| {
parent.spawn((
.with_children(|p| {
p.spawn((
Text::default(),
HeaderText,
TextBundle::from_sections(text).with_text_justify(JustifyText::Center),
));
TextBlock::new_with_justify(JustifyText::Center),
))
.with_children(|p| {
p.spawn(TextSpan::new("Primitive: "));
p.spawn(TextSpan(format!(
"{text}",
text = PrimitiveSelected::default()
)));
p.spawn(TextSpan::new("\n\n"));
p.spawn(TextSpan::new(
"Press 'C' to switch between 2D and 3D mode\n\
Press 'Up' or 'Down' to switch to the next/previous primitive",
));
p.spawn(TextSpan::new("\n\n"));
p.spawn(TextSpan::new(
"(If nothing is displayed, there's no rendering support yet)",
));
});
});
}
fn update_text(
primitive_state: Res<State<PrimitiveSelected>>,
mut header: Query<&mut Text, With<HeaderText>>,
header: Query<Entity, With<HeaderText>>,
mut writer: UiTextWriter,
) {
let new_text = format!("{text}", text = primitive_state.get());
header.iter_mut().for_each(|mut header_text| {
if let Some(kind) = header_text.sections.get_mut(1) {
kind.value.clone_from(&new_text);
header.iter().for_each(|header_text| {
if let Some(mut text) = writer.get_text(header_text, 2) {
(*text).clone_from(&new_text);
};
});
}

View file

@ -375,8 +375,8 @@ fn setup(
});
// Instructions for the example:
commands.spawn(
TextBundle::from_section(
commands.spawn((
Text::new(
"Controls:\n\
M: Toggle between sampling boundary and interior.\n\
A: Toggle automatic spawning & despawning of points.\n\
@ -387,15 +387,14 @@ fn setup(
Zoom camera by scrolling via mouse or +/-.\n\
Move camera by L/R arrow keys.\n\
Tab: Toggle this text",
TextStyle::default(),
)
.with_style(Style {
),
Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
},
));
// No points are scheduled to spawn initially.
commands.insert_resource(SpawnQueue(0));

View file

@ -120,19 +120,15 @@ fn setup_scene(
},
..default()
})
.with_children(|b| {
b.spawn(
TextBundle::from_section(
"Test Button",
TextStyle {
font_size: 30.0,
color: Color::BLACK,
..default()
},
)
.with_text_justify(JustifyText::Center),
);
});
.with_child((
Text::new("Test Button"),
TextStyle {
font_size: 30.0,
color: Color::BLACK,
..default()
},
TextBlock::new_with_justify(JustifyText::Center),
));
}
fn button_handler(

Some files were not shown because too many files have changed in this diff Show more