mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
0b2e0cfaca
commit
c2c19e5ae4
146 changed files with 3102 additions and 2712 deletions
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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`.*)
|
||||
///
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(), §ion.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(), §ion.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
381
crates/bevy_text/src/text_access.rs
Normal file
381
crates/bevy_text/src/text_access.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
|
|
|
@ -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 ()>,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
});
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
});
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¤tly_selected_option);
|
||||
*writer.text(help_text.single(), 0) = create_help_text(¤tly_selected_option);
|
||||
}
|
||||
|
||||
/// Creates the help text at the top left of the window.
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)>) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>>) {
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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((
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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()
|
||||
}),
|
||||
);
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue