
# Objective > Old MR: #5072 > ~~Associated UI MR: #5070~~ > Adresses #1618 Unify sprite management ## Solution - Remove the `Handle<Image>` field in `TextureAtlas` which is the main cause for all the boilerplate - Remove the redundant `TextureAtlasSprite` component - Renamed `TextureAtlas` asset to `TextureAtlasLayout` ([suggestion](https://github.com/bevyengine/bevy/pull/5103#discussion_r917281844)) - Add a `TextureAtlas` component, containing the atlas layout handle and the section index The difference between this solution and #5072 is that instead of the `enum` approach is that we can more easily manipulate texture sheets without any breaking changes for classic `SpriteBundle`s (@mockersf [comment](https://github.com/bevyengine/bevy/pull/5072#issuecomment-1165836139)) Also, this approach is more *data oriented* extracting the `Handle<Image>` and avoiding complex texture atlas manipulations to retrieve the texture in both applicative and engine code. With this method, the only difference between a `SpriteBundle` and a `SpriteSheetBundle` is an **additional** component storing the atlas handle and the index. ~~This solution can be applied to `bevy_ui` as well (see #5070).~~ EDIT: I also applied this solution to Bevy UI ## Changelog - (**BREAKING**) Removed `TextureAtlasSprite` - (**BREAKING**) Renamed `TextureAtlas` to `TextureAtlasLayout` - (**BREAKING**) `SpriteSheetBundle`: - Uses a `Sprite` instead of a `TextureAtlasSprite` component - Has a `texture` field containing a `Handle<Image>` like the `SpriteBundle` - Has a new `TextureAtlas` component instead of a `Handle<TextureAtlasLayout>` - (**BREAKING**) `DynamicTextureAtlasBuilder::add_texture` takes an additional `&Handle<Image>` parameter - (**BREAKING**) `TextureAtlasLayout::from_grid` no longer takes a `Handle<Image>` parameter - (**BREAKING**) `TextureAtlasBuilder::finish` now returns a `Result<(TextureAtlasLayout, Handle<Image>), _>` - `bevy_text`: - `GlyphAtlasInfo` stores the texture `Handle<Image>` - `FontAtlas` stores the texture `Handle<Image>` - `bevy_ui`: - (**BREAKING**) Removed `UiAtlasImage` , the atlas bundle is now identical to the `ImageBundle` with an additional `TextureAtlas` ## Migration Guide * Sprites ```diff fn my_system( mut images: ResMut<Assets<Image>>, - mut atlases: ResMut<Assets<TextureAtlas>>, + mut atlases: ResMut<Assets<TextureAtlasLayout>>, asset_server: Res<AssetServer> ) { let texture_handle: asset_server.load("my_texture.png"); - let layout = TextureAtlas::from_grid(texture_handle, Vec2::new(25.0, 25.0), 5, 5, None, None); + let layout = TextureAtlasLayout::from_grid(Vec2::new(25.0, 25.0), 5, 5, None, None); let layout_handle = atlases.add(layout); commands.spawn(SpriteSheetBundle { - sprite: TextureAtlasSprite::new(0), - texture_atlas: atlas_handle, + atlas: TextureAtlas { + layout: layout_handle, + index: 0 + }, + texture: texture_handle, ..Default::default() }); } ``` * UI ```diff fn my_system( mut images: ResMut<Assets<Image>>, - mut atlases: ResMut<Assets<TextureAtlas>>, + mut atlases: ResMut<Assets<TextureAtlasLayout>>, asset_server: Res<AssetServer> ) { let texture_handle: asset_server.load("my_texture.png"); - let layout = TextureAtlas::from_grid(texture_handle, Vec2::new(25.0, 25.0), 5, 5, None, None); + let layout = TextureAtlasLayout::from_grid(Vec2::new(25.0, 25.0), 5, 5, None, None); let layout_handle = atlases.add(layout); commands.spawn(AtlasImageBundle { - texture_atlas_image: UiTextureAtlasImage { - index: 0, - flip_x: false, - flip_y: false, - }, - texture_atlas: atlas_handle, + atlas: TextureAtlas { + layout: layout_handle, + index: 0 + }, + image: UiImage { + texture: texture_handle, + flip_x: false, + flip_y: false, + }, ..Default::default() }); } ``` --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: François <mockersf@gmail.com> Co-authored-by: IceSentry <IceSentry@users.noreply.github.com>
216 lines
6.7 KiB
Rust
216 lines
6.7 KiB
Rust
use crate::{
|
|
compute_text_bounds, error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font,
|
|
FontAtlasSets, FontAtlasWarning, JustifyText, PositionedGlyph, Text, TextSection, TextSettings,
|
|
YAxisOrientation,
|
|
};
|
|
use ab_glyph::PxScale;
|
|
use bevy_asset::{AssetId, Assets, Handle};
|
|
use bevy_ecs::component::Component;
|
|
use bevy_ecs::prelude::ReflectComponent;
|
|
use bevy_ecs::system::Resource;
|
|
use bevy_math::Vec2;
|
|
use bevy_reflect::prelude::ReflectDefault;
|
|
use bevy_reflect::Reflect;
|
|
use bevy_render::texture::Image;
|
|
use bevy_sprite::TextureAtlasLayout;
|
|
use bevy_utils::HashMap;
|
|
use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText, ToSectionText};
|
|
|
|
#[derive(Default, Resource)]
|
|
pub struct TextPipeline {
|
|
brush: GlyphBrush,
|
|
map_font_id: HashMap<AssetId<Font>, FontId>,
|
|
}
|
|
|
|
/// Render information for a corresponding [`Text`] component.
|
|
///
|
|
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`].
|
|
#[derive(Component, Clone, Default, Debug, Reflect)]
|
|
#[reflect(Component, Default)]
|
|
pub struct TextLayoutInfo {
|
|
pub glyphs: Vec<PositionedGlyph>,
|
|
pub logical_size: Vec2,
|
|
}
|
|
|
|
impl TextPipeline {
|
|
pub fn get_or_insert_font_id(&mut self, handle: &Handle<Font>, font: &Font) -> FontId {
|
|
let brush = &mut self.brush;
|
|
*self
|
|
.map_font_id
|
|
.entry(handle.id())
|
|
.or_insert_with(|| brush.add_font(handle.id(), font.font.clone()))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn queue_text(
|
|
&mut self,
|
|
fonts: &Assets<Font>,
|
|
sections: &[TextSection],
|
|
scale_factor: f32,
|
|
text_alignment: JustifyText,
|
|
linebreak_behavior: BreakLineOn,
|
|
bounds: Vec2,
|
|
font_atlas_sets: &mut FontAtlasSets,
|
|
texture_atlases: &mut Assets<TextureAtlasLayout>,
|
|
textures: &mut Assets<Image>,
|
|
text_settings: &TextSettings,
|
|
font_atlas_warning: &mut FontAtlasWarning,
|
|
y_axis_orientation: YAxisOrientation,
|
|
) -> Result<TextLayoutInfo, TextError> {
|
|
let mut scaled_fonts = Vec::with_capacity(sections.len());
|
|
let sections = sections
|
|
.iter()
|
|
.map(|section| {
|
|
let font = fonts
|
|
.get(§ion.style.font)
|
|
.ok_or(TextError::NoSuchFont)?;
|
|
let font_id = self.get_or_insert_font_id(§ion.style.font, font);
|
|
let font_size = scale_value(section.style.font_size, scale_factor);
|
|
|
|
scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size));
|
|
|
|
let section = SectionText {
|
|
font_id,
|
|
scale: PxScale::from(font_size),
|
|
text: §ion.value,
|
|
};
|
|
|
|
Ok(section)
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let section_glyphs =
|
|
self.brush
|
|
.compute_glyphs(§ions, bounds, text_alignment, linebreak_behavior)?;
|
|
|
|
if section_glyphs.is_empty() {
|
|
return Ok(TextLayoutInfo::default());
|
|
}
|
|
|
|
let size = compute_text_bounds(§ion_glyphs, |index| scaled_fonts[index]).size();
|
|
|
|
let glyphs = self.brush.process_glyphs(
|
|
section_glyphs,
|
|
§ions,
|
|
font_atlas_sets,
|
|
fonts,
|
|
texture_atlases,
|
|
textures,
|
|
text_settings,
|
|
font_atlas_warning,
|
|
y_axis_orientation,
|
|
)?;
|
|
|
|
Ok(TextLayoutInfo {
|
|
glyphs,
|
|
logical_size: size,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TextMeasureSection {
|
|
pub text: Box<str>,
|
|
pub scale: f32,
|
|
pub font_id: FontId,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct TextMeasureInfo {
|
|
pub fonts: Box<[ab_glyph::FontArc]>,
|
|
pub sections: Box<[TextMeasureSection]>,
|
|
pub justification: JustifyText,
|
|
pub linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker,
|
|
pub min: Vec2,
|
|
pub max: Vec2,
|
|
}
|
|
|
|
impl TextMeasureInfo {
|
|
pub fn from_text(
|
|
text: &Text,
|
|
fonts: &Assets<Font>,
|
|
scale_factor: f32,
|
|
) -> Result<TextMeasureInfo, TextError> {
|
|
let sections = &text.sections;
|
|
for section in sections {
|
|
if !fonts.contains(§ion.style.font) {
|
|
return Err(TextError::NoSuchFont);
|
|
}
|
|
}
|
|
let (auto_fonts, sections) = sections
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, section)| {
|
|
// SAFETY: we exited early earlier in this function if
|
|
// one of the fonts was missing.
|
|
let font = unsafe { fonts.get(§ion.style.font).unwrap_unchecked() };
|
|
(
|
|
font.font.clone(),
|
|
TextMeasureSection {
|
|
font_id: FontId(i),
|
|
scale: scale_value(section.style.font_size, scale_factor),
|
|
text: section.value.clone().into_boxed_str(),
|
|
},
|
|
)
|
|
})
|
|
.unzip();
|
|
|
|
Ok(Self::new(
|
|
auto_fonts,
|
|
sections,
|
|
text.justify,
|
|
text.linebreak_behavior.into(),
|
|
))
|
|
}
|
|
fn new(
|
|
fonts: Vec<ab_glyph::FontArc>,
|
|
sections: Vec<TextMeasureSection>,
|
|
justification: JustifyText,
|
|
linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker,
|
|
) -> Self {
|
|
let mut info = Self {
|
|
fonts: fonts.into_boxed_slice(),
|
|
sections: sections.into_boxed_slice(),
|
|
justification,
|
|
linebreak_behavior,
|
|
min: Vec2::ZERO,
|
|
max: Vec2::ZERO,
|
|
};
|
|
|
|
let min = info.compute_size(Vec2::new(0.0, f32::INFINITY));
|
|
let max = info.compute_size(Vec2::INFINITY);
|
|
info.min = min;
|
|
info.max = max;
|
|
info
|
|
}
|
|
|
|
pub fn compute_size(&self, bounds: Vec2) -> Vec2 {
|
|
let sections = &self.sections;
|
|
let geom = SectionGeometry {
|
|
bounds: (bounds.x, bounds.y),
|
|
..Default::default()
|
|
};
|
|
let section_glyphs = glyph_brush_layout::Layout::default()
|
|
.h_align(self.justification.into())
|
|
.line_breaker(self.linebreak_behavior)
|
|
.calculate_glyphs(&self.fonts, &geom, sections);
|
|
|
|
compute_text_bounds(§ion_glyphs, |index| {
|
|
let font = &self.fonts[index];
|
|
let font_size = self.sections[index].scale;
|
|
ab_glyph::Font::into_scaled(font, font_size)
|
|
})
|
|
.size()
|
|
}
|
|
}
|
|
impl ToSectionText for TextMeasureSection {
|
|
#[inline(always)]
|
|
fn to_section_text(&self) -> SectionText<'_> {
|
|
SectionText {
|
|
text: &self.text,
|
|
scale: PxScale::from(self.scale),
|
|
font_id: self.font_id,
|
|
}
|
|
}
|
|
}
|