Port bevy_ui to pipelined-rendering (#2653)

# Objective

Port bevy_ui to pipelined-rendering (see #2535 )

## Solution

I did some changes during the port:
- [X] separate color from the texture asset (as suggested [here](https://discord.com/channels/691052431525675048/743663924229963868/874353914525413406))
- [X] ~give the vertex shader a per-instance buffer instead of per-vertex buffer~ (incompatible with batching)

Remaining features to implement to reach parity with the old renderer:
- [x] textures
- [X] TextBundle

I'd also like to add these features, but they need some design discussion:
- [x] batching
- [ ] separate opaque and transparent phases
- [ ] multiple windows
- [ ] texture atlases
- [ ] (maybe) clipping
This commit is contained in:
davier 2021-12-10 22:21:23 +00:00
parent 58474d7c4a
commit 25b62f9577
42 changed files with 3809 additions and 13 deletions

View file

@ -25,6 +25,8 @@ default = [
"bevy_sprite2",
"bevy_render2",
"bevy_pbr2",
"bevy_ui2",
"bevy_text2",
"bevy_winit",
"render",
"png",
@ -59,6 +61,8 @@ bevy_render2 = ["bevy_internal/bevy_render2"]
bevy_sprite2 = ["bevy_internal/bevy_sprite2"]
bevy_pbr2 = ["bevy_internal/bevy_pbr2"]
bevy_gltf2 = ["bevy_internal/bevy_gltf2"]
bevy_ui2 = ["bevy_internal/bevy_ui2"]
bevy_text2 = ["bevy_internal/bevy_text2"]
trace_chrome = ["bevy_internal/trace_chrome"]
trace_tracy = ["bevy_internal/trace_tracy"]
@ -140,6 +144,10 @@ path = "examples/2d/sprite_sheet.rs"
name = "text2d"
path = "examples/2d/text2d.rs"
[[example]]
name = "text2d_pipelined"
path = "examples/2d/text2d_pipelined.rs"
[[example]]
name = "texture_atlas"
path = "examples/2d/texture_atlas.rs"
@ -506,6 +514,10 @@ path = "examples/ui/text_debug.rs"
name = "ui"
path = "examples/ui/ui.rs"
[[example]]
name = "ui_pipelined"
path = "examples/ui/ui_pipelined.rs"
# Window
[[example]]
name = "clear_color"

View file

@ -39,7 +39,7 @@ wayland = ["bevy_winit/wayland"]
x11 = ["bevy_winit/x11"]
# enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"]
subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas", "bevy_text2/subpixel_glyph_atlas"]
# enable systems that allow for automated testing on CI
bevy_ci_testing = ["bevy_app/bevy_ci_testing"]
@ -74,7 +74,9 @@ bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, versio
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.5.0" }
bevy_sprite2 = { path = "../../pipelined/bevy_sprite2", optional = true, version = "0.5.0" }
bevy_text = { path = "../bevy_text", optional = true, version = "0.5.0" }
bevy_text2 = { path = "../../pipelined/bevy_text2", optional = true, version = "0.5.0" }
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.5.0" }
bevy_ui2 = { path = "../../pipelined/bevy_ui2", optional = true, version = "0.5.0" }
bevy_wgpu = { path = "../bevy_wgpu", optional = true, version = "0.5.0" }
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.5.0" }
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.5.0" }

View file

@ -138,6 +138,12 @@ impl PluginGroup for PipelinedDefaultPlugins {
#[cfg(feature = "bevy_sprite2")]
group.add(bevy_sprite2::SpritePlugin::default());
#[cfg(feature = "bevy_text2")]
group.add(bevy_text2::TextPlugin::default());
#[cfg(feature = "bevy_ui2")]
group.add(bevy_ui2::UiPlugin::default());
#[cfg(feature = "bevy_pbr2")]
group.add(bevy_pbr2::PbrPlugin::default());

View file

@ -147,12 +147,24 @@ pub mod text {
pub use bevy_text::*;
}
#[cfg(feature = "bevy_text2")]
pub mod text2 {
//! Text drawing, styling, and font assets.
pub use bevy_text2::*;
}
#[cfg(feature = "bevy_ui")]
pub mod ui {
//! User interface components and widgets.
pub use bevy_ui::*;
}
#[cfg(feature = "bevy_ui2")]
pub mod ui2 {
//! User interface components and widgets.
pub use bevy_ui2::*;
}
#[cfg(feature = "bevy_winit")]
pub mod winit {
pub use bevy_winit::*;

View file

@ -0,0 +1,92 @@
use bevy::{
core::Time,
math::{Quat, Vec3},
prelude::{App, AssetServer, Commands, Component, Query, Res, Transform, With},
render2::{camera::OrthographicCameraBundle, color::Color},
text2::{HorizontalAlign, Text, Text2dBundle, TextAlignment, TextStyle, VerticalAlign},
PipelinedDefaultPlugins,
};
fn main() {
App::new()
.add_plugins(PipelinedDefaultPlugins)
.add_startup_system(setup)
.add_system(animate_translation)
.add_system(animate_rotation)
.add_system(animate_scale)
.run();
}
#[derive(Component)]
struct AnimateTranslation;
#[derive(Component)]
struct AnimateRotation;
#[derive(Component)]
struct AnimateScale;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
let text_style = TextStyle {
font,
font_size: 60.0,
color: Color::WHITE,
};
let text_alignment = TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
};
// 2d camera
commands.spawn_bundle(OrthographicCameraBundle::new_2d());
// Demonstrate changing translation
commands
.spawn_bundle(Text2dBundle {
text: Text::with_section("translation", text_style.clone(), text_alignment),
..Default::default()
})
.insert(AnimateTranslation);
// Demonstrate changing rotation
commands
.spawn_bundle(Text2dBundle {
text: Text::with_section("rotation", text_style.clone(), text_alignment),
..Default::default()
})
.insert(AnimateRotation);
// Demonstrate changing scale
commands
.spawn_bundle(Text2dBundle {
text: Text::with_section("scale", text_style, text_alignment),
..Default::default()
})
.insert(AnimateScale);
}
fn animate_translation(
time: Res<Time>,
mut query: Query<&mut Transform, (With<Text>, With<AnimateTranslation>)>,
) {
for mut transform in query.iter_mut() {
transform.translation.x = 100.0 * time.seconds_since_startup().sin() as f32 - 400.0;
transform.translation.y = 100.0 * time.seconds_since_startup().cos() as f32;
}
}
fn animate_rotation(
time: Res<Time>,
mut query: Query<&mut Transform, (With<Text>, With<AnimateRotation>)>,
) {
for mut transform in query.iter_mut() {
transform.rotation = Quat::from_rotation_z(time.seconds_since_startup().cos() as f32);
}
}
fn animate_scale(
time: Res<Time>,
mut query: Query<&mut Transform, (With<Text>, With<AnimateScale>)>,
) {
// Consider changing font-size instead of scaling the transform. Scaling a Text2D will scale the
// rendered quad, resulting in a pixellated look.
for mut transform in query.iter_mut() {
transform.translation = Vec3::new(400.0, 0.0, 0.0);
transform.scale = Vec3::splat((time.seconds_since_startup().sin() as f32 + 1.1) * 2.0);
}
}

View file

@ -89,6 +89,7 @@ Example | File | Description
`sprite` | [`2d/sprite.rs`](./2d/sprite.rs) | Renders a sprite
`sprite_sheet` | [`2d/sprite_sheet.rs`](./2d/sprite_sheet.rs) | Renders an animated sprite
`text2d` | [`2d/text2d.rs`](./2d/text2d.rs) | Generates text in 2d
`text2d_pipelined` | [`2d/text2d_pipelined.rs`](./2d/text2d_pipelined.rs) | Generates text in 2d
`sprite_flipping` | [`2d/sprite_flipping.rs`](./2d/sprite_flipping.rs) | Renders a sprite flipped along an axis
`texture_atlas` | [`2d/texture_atlas.rs`](./2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites
@ -253,6 +254,7 @@ Example | File | Description
`text` | [`ui/text.rs`](./ui/text.rs) | Illustrates creating and updating text
`text_debug` | [`ui/text_debug.rs`](./ui/text_debug.rs) | An example for debugging text layout
`ui` | [`ui/ui.rs`](./ui/ui.rs) | Illustrates various features of Bevy UI
`ui_pipelined` | [`ui/ui_pipelined.rs`](./ui/ui_pipelined.rs) | Illustrates various features of Bevy UI
## Window

225
examples/ui/ui_pipelined.rs Normal file
View file

@ -0,0 +1,225 @@
use bevy::{
prelude::{App, AssetServer, BuildChildren, Commands, Rect, Res, Size},
render2::color::Color,
text2::{Text, TextStyle},
ui2::{
entity::ImageBundle,
entity::UiCameraBundle,
entity::{NodeBundle, TextBundle},
AlignItems, JustifyContent, PositionType, Style, Val,
},
PipelinedDefaultPlugins,
};
/// This example illustrates the various features of Bevy UI.
fn main() {
App::new()
.add_plugins(PipelinedDefaultPlugins)
.add_startup_system(setup)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// ui camera
commands.spawn_bundle(UiCameraBundle::default());
// root node
commands
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
justify_content: JustifyContent::SpaceBetween,
..Default::default()
},
color: Color::NONE.into(),
..Default::default()
})
.with_children(|parent| {
// left vertical fill (border)
parent
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(200.0), Val::Percent(100.0)),
border: Rect::all(Val::Px(2.0)),
..Default::default()
},
color: Color::rgb(0.65, 0.65, 0.65).into(),
..Default::default()
})
.with_children(|parent| {
// left vertical fill (content)
parent
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
align_items: AlignItems::FlexEnd,
..Default::default()
},
color: Color::rgb(0.15, 0.15, 0.15).into(),
..Default::default()
})
.with_children(|parent| {
// text
parent.spawn_bundle(TextBundle {
style: Style {
margin: Rect::all(Val::Px(5.0)),
..Default::default()
},
text: Text::with_section(
"Text Example",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::WHITE,
},
Default::default(),
),
..Default::default()
});
});
});
// right vertical fill
parent.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(200.0), Val::Percent(100.0)),
..Default::default()
},
color: Color::rgb(0.15, 0.15, 0.15).into(),
..Default::default()
});
// absolute positioning
parent
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(200.0), Val::Px(200.0)),
position_type: PositionType::Absolute,
position: Rect {
left: Val::Px(210.0),
bottom: Val::Px(10.0),
..Default::default()
},
border: Rect::all(Val::Px(20.0)),
..Default::default()
},
color: Color::rgb(0.4, 0.4, 1.0).into(),
..Default::default()
})
.with_children(|parent| {
parent.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
..Default::default()
},
color: Color::rgb(0.8, 0.8, 1.0).into(),
..Default::default()
});
});
// render order test: reddest in the back, whitest in the front (flex center)
parent
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
position_type: PositionType::Absolute,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..Default::default()
},
color: Color::NONE.into(),
..Default::default()
})
.with_children(|parent| {
parent
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
..Default::default()
},
color: Color::rgb(1.0, 0.0, 0.0).into(),
..Default::default()
})
.with_children(|parent| {
parent.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
position_type: PositionType::Absolute,
position: Rect {
left: Val::Px(20.0),
bottom: Val::Px(20.0),
..Default::default()
},
..Default::default()
},
color: Color::rgb(1.0, 0.3, 0.3).into(),
..Default::default()
});
parent.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
position_type: PositionType::Absolute,
position: Rect {
left: Val::Px(40.0),
bottom: Val::Px(40.0),
..Default::default()
},
..Default::default()
},
color: Color::rgb(1.0, 0.5, 0.5).into(),
..Default::default()
});
parent.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
position_type: PositionType::Absolute,
position: Rect {
left: Val::Px(60.0),
bottom: Val::Px(60.0),
..Default::default()
},
..Default::default()
},
color: Color::rgb(1.0, 0.7, 0.7).into(),
..Default::default()
});
// alpha test
parent.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(100.0), Val::Px(100.0)),
position_type: PositionType::Absolute,
position: Rect {
left: Val::Px(80.0),
bottom: Val::Px(80.0),
..Default::default()
},
..Default::default()
},
color: Color::rgba(1.0, 0.9, 0.9, 0.4).into(),
..Default::default()
});
});
});
// bevy logo (flex center)
parent
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexEnd,
..Default::default()
},
color: Color::NONE.into(),
..Default::default()
})
.with_children(|parent| {
// bevy logo (image)
parent.spawn_bundle(ImageBundle {
style: Style {
size: Size::new(Val::Px(500.0), Val::Auto),
..Default::default()
},
image: asset_server.load("branding/bevy_logo_dark_big.png").into(),
..Default::default()
});
});
});
}

View file

@ -49,7 +49,6 @@ pub mod node {
pub const MAIN_PASS_DEPENDENCIES: &str = "main_pass_dependencies";
pub const MAIN_PASS_DRIVER: &str = "main_pass_driver";
pub const CLEAR_PASS_DRIVER: &str = "clear_pass_driver";
pub const VIEW: &str = "view";
}
pub mod draw_2d_graph {

View file

@ -36,6 +36,12 @@ impl Plugin for CameraPlugin {
app.register_type::<Camera>()
.register_type::<Visibility>()
.register_type::<ComputedVisibility>()
.register_type::<OrthographicProjection>()
.register_type::<PerspectiveProjection>()
.register_type::<VisibleEntities>()
.register_type::<WindowOrigin>()
.register_type::<ScalingMode>()
.register_type::<DepthCalculation>()
.register_type::<Aabb>()
.insert_resource(active_cameras)
.add_system_to_stage(CoreStage::PostUpdate, crate::camera::active_cameras_system)

View file

@ -16,6 +16,7 @@ pub use once_cell;
use crate::{
camera::CameraPlugin,
color::Color,
mesh::MeshPlugin,
render_graph::RenderGraph,
render_resource::{RenderPipelineCache, Shader, ShaderLoader},
@ -122,7 +123,8 @@ impl Plugin for RenderPlugin {
.insert_resource(queue.clone())
.add_asset::<Shader>()
.init_asset_loader::<ShaderLoader>()
.init_resource::<ScratchRenderWorld>();
.init_resource::<ScratchRenderWorld>()
.register_type::<Color>();
let render_pipeline_cache = RenderPipelineCache::new(device.clone());
let asset_server = app.world.get_resource::<AssetServer>().unwrap().clone();

View file

@ -17,6 +17,7 @@ pub use texture_atlas_builder::*;
use bevy_app::prelude::*;
use bevy_asset::{AddAsset, Assets, HandleUntyped};
use bevy_core_pipeline::Transparent2d;
use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
use bevy_reflect::TypeUuid;
use bevy_render2::{
render_phase::DrawFunctions,
@ -30,6 +31,11 @@ pub struct SpritePlugin;
pub const SPRITE_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2763343953151597127);
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum SpriteSystem {
ExtractSprite,
}
impl Plugin for SpritePlugin {
fn build(&self, app: &mut App) {
let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
@ -44,7 +50,10 @@ impl Plugin for SpritePlugin {
.init_resource::<SpriteMeta>()
.init_resource::<ExtractedSprites>()
.init_resource::<SpriteAssetEvents>()
.add_system_to_stage(RenderStage::Extract, render::extract_sprites)
.add_system_to_stage(
RenderStage::Extract,
render::extract_sprites.label(SpriteSystem::ExtractSprite),
)
.add_system_to_stage(RenderStage::Extract, render::extract_sprite_events)
.add_system_to_stage(RenderStage::Prepare, render::prepare_sprites)
.add_system_to_stage(RenderStage::Queue, queue_sprites);

View file

@ -158,23 +158,23 @@ impl SpecializedPipeline for SpritePipeline {
}
pub struct ExtractedSprite {
transform: Mat4,
color: Color,
rect: Rect,
handle: Handle<Image>,
atlas_size: Option<Vec2>,
flip_x: bool,
flip_y: bool,
pub transform: Mat4,
pub color: Color,
pub rect: Rect,
pub handle: Handle<Image>,
pub atlas_size: Option<Vec2>,
pub flip_x: bool,
pub flip_y: bool,
}
#[derive(Default)]
pub struct ExtractedSprites {
sprites: Vec<ExtractedSprite>,
pub sprites: Vec<ExtractedSprite>,
}
#[derive(Default)]
pub struct SpriteAssetEvents {
images: Vec<AssetEvent<Image>>,
pub images: Vec<AssetEvent<Image>>,
}
pub fn extract_sprite_events(

View file

@ -0,0 +1,33 @@
[package]
name = "bevy_text2"
version = "0.5.0"
edition = "2021"
description = "Provides text functionality for Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[features]
subpixel_glyph_atlas = []
[dependencies]
# bevy
bevy_app = { path = "../../crates/bevy_app", version = "0.5.0" }
bevy_asset = { path = "../../crates/bevy_asset", version = "0.5.0" }
bevy_core = { path = "../../crates/bevy_core", version = "0.5.0" }
bevy_ecs = { path = "../../crates/bevy_ecs", version = "0.5.0" }
bevy_math = { path = "../../crates/bevy_math", version = "0.5.0" }
bevy_reflect = { path = "../../crates/bevy_reflect", version = "0.5.0", features = ["bevy"] }
bevy_render2 = { path = "../bevy_render2", version = "0.5.0" }
bevy_sprite2 = { path = "../bevy_sprite2", version = "0.5.0" }
bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" }
bevy_window = { path = "../../crates/bevy_window", version = "0.5.0" }
bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" }
# other
anyhow = "1.0.4"
ab_glyph = "0.2.6"
glyph_brush_layout = "0.2.1"
thiserror = "1.0"
serde = {version = "1", features = ["derive"]}

View file

@ -0,0 +1,10 @@
use ab_glyph::GlyphId;
use thiserror::Error;
#[derive(Debug, PartialEq, Eq, Error)]
pub enum TextError {
#[error("font not found")]
NoSuchFont,
#[error("failed to add glyph to newly-created atlas {0:?}")]
FailedToAddGlyph(GlyphId),
}

View file

@ -0,0 +1,46 @@
use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph};
use bevy_reflect::TypeUuid;
use bevy_render2::{
render_resource::{Extent3d, TextureDimension, TextureFormat},
texture::Image,
};
#[derive(Debug, TypeUuid)]
#[uuid = "97059ac6-c9ba-4da9-95b6-bed82c3ce198"]
pub struct Font {
pub font: FontArc,
}
impl Font {
pub fn try_from_bytes(font_data: Vec<u8>) -> Result<Self, InvalidFont> {
let font = FontVec::try_from_vec(font_data)?;
let font = FontArc::new(font);
Ok(Font { font })
}
pub fn get_outlined_glyph_texture(outlined_glyph: OutlinedGlyph) -> Image {
let bounds = outlined_glyph.px_bounds();
let width = bounds.width() as usize;
let height = bounds.height() as usize;
let mut alpha = vec![0.0; width * height];
outlined_glyph.draw(|x, y, v| {
alpha[y as usize * width + x as usize] = v;
});
// TODO: make this texture grayscale
Image::new(
Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
alpha
.iter()
.map(|a| vec![255, 255, 255, (*a * 255.0) as u8])
.flatten()
.collect::<Vec<u8>>(),
TextureFormat::Rgba8UnormSrgb,
)
}
}

View file

@ -0,0 +1,107 @@
use ab_glyph::{GlyphId, Point};
use bevy_asset::{Assets, Handle};
use bevy_math::Vec2;
use bevy_render2::{
render_resource::{Extent3d, TextureDimension, TextureFormat},
texture::Image,
};
use bevy_sprite2::{DynamicTextureAtlasBuilder, TextureAtlas};
use bevy_utils::HashMap;
#[cfg(feature = "subpixel_glyph_atlas")]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct SubpixelOffset {
x: u16,
y: u16,
}
#[cfg(feature = "subpixel_glyph_atlas")]
impl From<Point> for SubpixelOffset {
fn from(p: Point) -> Self {
fn f(v: f32) -> u16 {
((v % 1.) * (u16::MAX as f32)) as u16
}
Self {
x: f(p.x),
y: f(p.y),
}
}
}
#[cfg(not(feature = "subpixel_glyph_atlas"))]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct SubpixelOffset;
#[cfg(not(feature = "subpixel_glyph_atlas"))]
impl From<Point> for SubpixelOffset {
fn from(_: Point) -> Self {
Self
}
}
pub struct FontAtlas {
pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
pub glyph_to_atlas_index: HashMap<(GlyphId, SubpixelOffset), usize>,
pub texture_atlas: Handle<TextureAtlas>,
}
impl FontAtlas {
pub fn new(
textures: &mut Assets<Image>,
texture_atlases: &mut Assets<TextureAtlas>,
size: Vec2,
) -> FontAtlas {
let atlas_texture = textures.add(Image::new_fill(
Extent3d {
width: size.x as u32,
height: size.y as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0, 0, 0, 0],
TextureFormat::Rgba8UnormSrgb,
));
let texture_atlas = TextureAtlas::new_empty(atlas_texture, size);
Self {
texture_atlas: texture_atlases.add(texture_atlas),
glyph_to_atlas_index: HashMap::default(),
dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1),
}
}
pub fn get_glyph_index(
&self,
glyph_id: GlyphId,
subpixel_offset: SubpixelOffset,
) -> Option<usize> {
self.glyph_to_atlas_index
.get(&(glyph_id, subpixel_offset))
.copied()
}
pub fn has_glyph(&self, glyph_id: GlyphId, subpixel_offset: SubpixelOffset) -> bool {
self.glyph_to_atlas_index
.contains_key(&(glyph_id, subpixel_offset))
}
pub fn add_glyph(
&mut self,
textures: &mut Assets<Image>,
texture_atlases: &mut Assets<TextureAtlas>,
glyph_id: GlyphId,
subpixel_offset: SubpixelOffset,
texture: &Image,
) -> bool {
let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap();
if let Some(index) =
self.dynamic_texture_atlas_builder
.add_texture(texture_atlas, textures, texture)
{
self.glyph_to_atlas_index
.insert((glyph_id, subpixel_offset), index);
true
} else {
false
}
}
}

View file

@ -0,0 +1,122 @@
use crate::{error::TextError, Font, FontAtlas};
use ab_glyph::{GlyphId, OutlinedGlyph, Point};
use bevy_asset::{Assets, Handle};
use bevy_core::FloatOrd;
use bevy_math::Vec2;
use bevy_reflect::TypeUuid;
use bevy_render2::texture::Image;
use bevy_sprite2::TextureAtlas;
use bevy_utils::HashMap;
type FontSizeKey = FloatOrd;
#[derive(TypeUuid)]
#[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"]
pub struct FontAtlasSet {
font_atlases: HashMap<FontSizeKey, Vec<FontAtlas>>,
}
#[derive(Debug, Clone)]
pub struct GlyphAtlasInfo {
pub texture_atlas: Handle<TextureAtlas>,
pub glyph_index: usize,
}
impl Default for FontAtlasSet {
fn default() -> Self {
FontAtlasSet {
font_atlases: HashMap::with_capacity_and_hasher(1, Default::default()),
}
}
}
impl FontAtlasSet {
pub fn iter(&self) -> impl Iterator<Item = (&FontSizeKey, &Vec<FontAtlas>)> {
self.font_atlases.iter()
}
pub fn has_glyph(&self, glyph_id: GlyphId, glyph_position: Point, font_size: f32) -> bool {
self.font_atlases
.get(&FloatOrd(font_size))
.map_or(false, |font_atlas| {
font_atlas
.iter()
.any(|atlas| atlas.has_glyph(glyph_id, glyph_position.into()))
})
}
pub fn add_glyph_to_atlas(
&mut self,
texture_atlases: &mut Assets<TextureAtlas>,
textures: &mut Assets<Image>,
outlined_glyph: OutlinedGlyph,
) -> Result<GlyphAtlasInfo, TextError> {
let glyph = outlined_glyph.glyph();
let glyph_id = glyph.id;
let glyph_position = glyph.position;
let font_size = glyph.scale.y;
let font_atlases = self
.font_atlases
.entry(FloatOrd(font_size))
.or_insert_with(|| {
vec![FontAtlas::new(
textures,
texture_atlases,
Vec2::new(512.0, 512.0),
)]
});
let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph);
let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool {
atlas.add_glyph(
textures,
texture_atlases,
glyph_id,
glyph_position.into(),
&glyph_texture,
)
};
if !font_atlases.iter_mut().any(add_char_to_font_atlas) {
font_atlases.push(FontAtlas::new(
textures,
texture_atlases,
Vec2::new(512.0, 512.0),
));
if !font_atlases.last_mut().unwrap().add_glyph(
textures,
texture_atlases,
glyph_id,
glyph_position.into(),
&glyph_texture,
) {
return Err(TextError::FailedToAddGlyph(glyph_id));
}
}
Ok(self
.get_glyph_atlas_info(font_size, glyph_id, glyph_position)
.unwrap())
}
pub fn get_glyph_atlas_info(
&self,
font_size: f32,
glyph_id: GlyphId,
position: Point,
) -> Option<GlyphAtlasInfo> {
self.font_atlases
.get(&FloatOrd(font_size))
.and_then(|font_atlases| {
font_atlases
.iter()
.find_map(|atlas| {
atlas
.get_glyph_index(glyph_id, position.into())
.map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak()))
})
.map(|(glyph_index, texture_atlas)| GlyphAtlasInfo {
texture_atlas,
glyph_index,
})
})
}
}

View file

@ -0,0 +1,25 @@
use crate::Font;
use anyhow::Result;
use bevy_asset::{AssetLoader, LoadContext, LoadedAsset};
use bevy_utils::BoxedFuture;
#[derive(Default)]
pub struct FontLoader;
impl AssetLoader for FontLoader {
fn load<'a>(
&'a self,
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<()>> {
Box::pin(async move {
let font = Font::try_from_bytes(bytes.into())?;
load_context.set_default_asset(LoadedAsset::new(font));
Ok(())
})
}
fn extensions(&self) -> &[&str] {
&["ttf", "otf"]
}
}

View file

@ -0,0 +1,181 @@
use ab_glyph::{Font as _, FontArc, Glyph, ScaleFont as _};
use bevy_asset::{Assets, Handle};
use bevy_math::{Size, Vec2};
use bevy_render2::texture::Image;
use bevy_sprite2::TextureAtlas;
use glyph_brush_layout::{
FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, SectionText, ToSectionText,
};
use crate::{error::TextError, Font, FontAtlasSet, GlyphAtlasInfo, TextAlignment};
pub struct GlyphBrush {
fonts: Vec<FontArc>,
handles: Vec<Handle<Font>>,
latest_font_id: FontId,
}
impl Default for GlyphBrush {
fn default() -> Self {
GlyphBrush {
fonts: Vec::new(),
handles: Vec::new(),
latest_font_id: FontId(0),
}
}
}
impl GlyphBrush {
pub fn compute_glyphs<S: ToSectionText>(
&self,
sections: &[S],
bounds: Size,
text_alignment: TextAlignment,
) -> Result<Vec<SectionGlyph>, TextError> {
let geom = SectionGeometry {
bounds: (bounds.width, bounds.height),
..Default::default()
};
let section_glyphs = Layout::default()
.h_align(text_alignment.horizontal.into())
.v_align(text_alignment.vertical.into())
.calculate_glyphs(&self.fonts, &geom, sections);
Ok(section_glyphs)
}
pub fn process_glyphs(
&self,
glyphs: Vec<SectionGlyph>,
sections: &[SectionText],
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
fonts: &Assets<Font>,
texture_atlases: &mut Assets<TextureAtlas>,
textures: &mut Assets<Image>,
) -> Result<Vec<PositionedGlyph>, TextError> {
if glyphs.is_empty() {
return Ok(Vec::new());
}
let sections_data = sections
.iter()
.map(|section| {
let handle = &self.handles[section.font_id.0];
let font = fonts.get(handle).ok_or(TextError::NoSuchFont)?;
let font_size = section.scale.y;
Ok((
handle,
font,
font_size,
ab_glyph::Font::as_scaled(&font.font, font_size),
))
})
.collect::<Result<Vec<_>, _>>()?;
let mut max_y = std::f32::MIN;
let mut min_x = std::f32::MAX;
for sg in glyphs.iter() {
let glyph = &sg.glyph;
let scaled_font = sections_data[sg.section_index].3;
max_y = max_y.max(glyph.position.y - scaled_font.descent());
min_x = min_x.min(glyph.position.x);
}
max_y = max_y.floor();
min_x = min_x.floor();
let mut positioned_glyphs = Vec::new();
for sg in glyphs {
let SectionGlyph {
section_index: _,
byte_index,
mut glyph,
font_id: _,
} = sg;
let glyph_id = glyph.id;
let glyph_position = glyph.position;
let adjust = GlyphPlacementAdjuster::new(&mut glyph);
let section_data = sections_data[sg.section_index];
if let Some(outlined_glyph) = section_data.1.font.outline_glyph(glyph) {
let bounds = outlined_glyph.px_bounds();
let handle_font_atlas: Handle<FontAtlasSet> = section_data.0.as_weak();
let font_atlas_set = font_atlas_set_storage
.get_or_insert_with(handle_font_atlas, FontAtlasSet::default);
let atlas_info = font_atlas_set
.get_glyph_atlas_info(section_data.2, glyph_id, glyph_position)
.map(Ok)
.unwrap_or_else(|| {
font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph)
})?;
let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();
let glyph_rect = texture_atlas.textures[atlas_info.glyph_index as usize];
let size = Vec2::new(glyph_rect.width(), glyph_rect.height());
let x = bounds.min.x + size.x / 2.0 - min_x;
let y = max_y - bounds.max.y + size.y / 2.0;
let position = adjust.position(Vec2::new(x, y));
positioned_glyphs.push(PositionedGlyph {
position,
size,
atlas_info,
section_index: sg.section_index,
byte_index,
});
}
}
Ok(positioned_glyphs)
}
pub fn add_font(&mut self, handle: Handle<Font>, font: FontArc) -> FontId {
self.fonts.push(font);
self.handles.push(handle);
let font_id = self.latest_font_id;
self.latest_font_id = FontId(font_id.0 + 1);
font_id
}
}
#[derive(Debug, Clone)]
pub struct PositionedGlyph {
pub position: Vec2,
pub size: Vec2,
pub atlas_info: GlyphAtlasInfo,
pub section_index: usize,
pub byte_index: usize,
}
#[cfg(feature = "subpixel_glyph_atlas")]
struct GlyphPlacementAdjuster;
#[cfg(feature = "subpixel_glyph_atlas")]
impl GlyphPlacementAdjuster {
#[inline(always)]
pub fn new(_: &mut Glyph) -> Self {
Self
}
#[inline(always)]
pub fn position(&self, p: Vec2) -> Vec2 {
p
}
}
#[cfg(not(feature = "subpixel_glyph_atlas"))]
struct GlyphPlacementAdjuster(f32);
#[cfg(not(feature = "subpixel_glyph_atlas"))]
impl GlyphPlacementAdjuster {
#[inline(always)]
pub fn new(glyph: &mut Glyph) -> Self {
let v = glyph.position.x.round();
glyph.position.x = 0.;
glyph.position.y = glyph.position.y.ceil();
Self(v)
}
#[inline(always)]
pub fn position(&self, v: Vec2) -> Vec2 {
Vec2::new(self.0, 0.) + v
}
}

View file

@ -0,0 +1,58 @@
mod error;
mod font;
mod font_atlas;
mod font_atlas_set;
mod font_loader;
mod glyph_brush;
mod pipeline;
mod text;
mod text2d;
pub use error::*;
pub use font::*;
pub use font_atlas::*;
pub use font_atlas_set::*;
pub use font_loader::*;
pub use glyph_brush::*;
pub use pipeline::*;
pub use text::*;
pub use text2d::*;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
Font, HorizontalAlign, Text, Text2dBundle, TextAlignment, TextError, TextSection,
TextStyle, VerticalAlign,
};
}
use bevy_app::prelude::*;
use bevy_asset::AddAsset;
use bevy_ecs::{entity::Entity, schedule::ParallelSystemDescriptorCoercion};
use bevy_render2::{RenderApp, RenderStage};
use bevy_sprite2::SpriteSystem;
pub type DefaultTextPipeline = TextPipeline<Entity>;
#[derive(Default)]
pub struct TextPlugin;
impl Plugin for TextPlugin {
fn build(&self, app: &mut App) {
app.add_asset::<Font>()
.add_asset::<FontAtlasSet>()
// TODO: uncomment when #2215 is fixed
// .register_type::<Text>()
.register_type::<VerticalAlign>()
.register_type::<HorizontalAlign>()
.init_asset_loader::<FontLoader>()
.insert_resource(DefaultTextPipeline::default())
.add_system_to_stage(CoreStage::PostUpdate, text2d_system);
let render_app = app.sub_app(RenderApp);
render_app.add_system_to_stage(
RenderStage::Extract,
extract_text2d_sprite.after(SpriteSystem::ExtractSprite),
);
}
}

View file

@ -0,0 +1,130 @@
use std::hash::Hash;
use ab_glyph::{PxScale, ScaleFont};
use bevy_asset::{Assets, Handle, HandleId};
use bevy_math::Size;
use bevy_render2::texture::Image;
use bevy_sprite2::TextureAtlas;
use bevy_utils::HashMap;
use glyph_brush_layout::{FontId, SectionText};
use crate::{
error::TextError, glyph_brush::GlyphBrush, scale_value, Font, FontAtlasSet, PositionedGlyph,
TextAlignment, TextSection,
};
pub struct TextPipeline<ID> {
brush: GlyphBrush,
glyph_map: HashMap<ID, TextLayoutInfo>,
map_font_id: HashMap<HandleId, FontId>,
}
impl<ID> Default for TextPipeline<ID> {
fn default() -> Self {
TextPipeline {
brush: GlyphBrush::default(),
glyph_map: Default::default(),
map_font_id: Default::default(),
}
}
}
pub struct TextLayoutInfo {
pub glyphs: Vec<PositionedGlyph>,
pub size: Size,
}
impl<ID: Hash + Eq> TextPipeline<ID> {
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.clone(), font.font.clone()))
}
pub fn get_glyphs(&self, id: &ID) -> Option<&TextLayoutInfo> {
self.glyph_map.get(id)
}
#[allow(clippy::too_many_arguments)]
pub fn queue_text(
&mut self,
id: ID,
fonts: &Assets<Font>,
sections: &[TextSection],
scale_factor: f64,
text_alignment: TextAlignment,
bounds: Size,
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
texture_atlases: &mut Assets<TextureAtlas>,
textures: &mut Assets<Image>,
) -> Result<(), TextError> {
let mut scaled_fonts = Vec::new();
let sections = sections
.iter()
.map(|section| {
let font = fonts
.get(section.style.font.id)
.ok_or(TextError::NoSuchFont)?;
let font_id = self.get_or_insert_font_id(&section.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: &section.value,
};
Ok(section)
})
.collect::<Result<Vec<_>, _>>()?;
let section_glyphs = self
.brush
.compute_glyphs(&sections, bounds, text_alignment)?;
if section_glyphs.is_empty() {
self.glyph_map.insert(
id,
TextLayoutInfo {
glyphs: Vec::new(),
size: Size::new(0., 0.),
},
);
return Ok(());
}
let mut min_x: f32 = std::f32::MAX;
let mut min_y: f32 = std::f32::MAX;
let mut max_x: f32 = std::f32::MIN;
let mut max_y: f32 = std::f32::MIN;
for sg in section_glyphs.iter() {
let scaled_font = scaled_fonts[sg.section_index];
let glyph = &sg.glyph;
min_x = min_x.min(glyph.position.x);
min_y = min_y.min(glyph.position.y - scaled_font.ascent());
max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
max_y = max_y.max(glyph.position.y - scaled_font.descent());
}
let size = Size::new(max_x - min_x, max_y - min_y);
let glyphs = self.brush.process_glyphs(
section_glyphs,
&sections,
font_atlas_set_storage,
fonts,
texture_atlases,
textures,
)?;
self.glyph_map.insert(id, TextLayoutInfo { glyphs, size });
Ok(())
}
}

View file

@ -0,0 +1,158 @@
use bevy_asset::Handle;
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_math::Size;
use bevy_reflect::{Reflect, ReflectDeserialize};
use bevy_render2::color::Color;
use serde::{Deserialize, Serialize};
use crate::Font;
#[derive(Component, Debug, Default, Clone, Reflect)]
#[reflect(Component)]
pub struct Text {
pub sections: Vec<TextSection>,
pub alignment: TextAlignment,
}
impl Text {
/// Constructs a [`Text`] with (initially) one section.
///
/// ```
/// # use bevy_asset::{AssetServer, Handle};
/// # use bevy_render2::color::Color;
/// # use bevy_text2::{Font, Text, TextAlignment, TextStyle, HorizontalAlign, VerticalAlign};
/// #
/// # let font_handle: Handle<Font> = Default::default();
/// #
/// // basic usage
/// let hello_world = Text::with_section(
/// "hello world!".to_string(),
/// TextStyle {
/// font: font_handle.clone(),
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// TextAlignment {
/// vertical: VerticalAlign::Center,
/// horizontal: HorizontalAlign::Center,
/// },
/// );
///
/// let hello_bevy = Text::with_section(
/// // accepts a String or any type that converts into a String, such as &str
/// "hello bevy!",
/// TextStyle {
/// font: font_handle,
/// font_size: 60.0,
/// color: Color::WHITE,
/// },
/// // you can still use Default
/// Default::default(),
/// );
/// ```
pub fn with_section<S: Into<String>>(
value: S,
style: TextStyle,
alignment: TextAlignment,
) -> Self {
Self {
sections: vec![TextSection {
value: value.into(),
style,
}],
alignment,
}
}
}
#[derive(Debug, Default, Clone, Reflect)]
pub struct TextSection {
pub value: String,
pub style: TextStyle,
}
#[derive(Debug, Clone, Copy, Reflect)]
pub struct TextAlignment {
pub vertical: VerticalAlign,
pub horizontal: HorizontalAlign,
}
impl Default for TextAlignment {
fn default() -> Self {
TextAlignment {
vertical: VerticalAlign::Top,
horizontal: HorizontalAlign::Left,
}
}
}
/// Describes horizontal alignment preference for positioning & bounds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
#[reflect_value(Serialize, Deserialize)]
pub enum HorizontalAlign {
/// Leftmost character is immediately to the right of the render position.<br/>
/// Bounds start from the render position and advance rightwards.
Left,
/// Leftmost & rightmost characters are equidistant to the render position.<br/>
/// Bounds start from the render position and advance equally left & right.
Center,
/// Rightmost character is immetiately to the left of the render position.<br/>
/// Bounds start from the render position and advance leftwards.
Right,
}
impl From<HorizontalAlign> for glyph_brush_layout::HorizontalAlign {
fn from(val: HorizontalAlign) -> Self {
match val {
HorizontalAlign::Left => glyph_brush_layout::HorizontalAlign::Left,
HorizontalAlign::Center => glyph_brush_layout::HorizontalAlign::Center,
HorizontalAlign::Right => glyph_brush_layout::HorizontalAlign::Right,
}
}
}
/// Describes vertical alignment preference for positioning & bounds. Currently a placeholder
/// for future functionality.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
#[reflect_value(Serialize, Deserialize)]
pub enum VerticalAlign {
/// Characters/bounds start underneath the render position and progress downwards.
Top,
/// Characters/bounds center at the render position and progress outward equally.
Center,
/// Characters/bounds start above the render position and progress upward.
Bottom,
}
impl From<VerticalAlign> for glyph_brush_layout::VerticalAlign {
fn from(val: VerticalAlign) -> Self {
match val {
VerticalAlign::Top => glyph_brush_layout::VerticalAlign::Top,
VerticalAlign::Center => glyph_brush_layout::VerticalAlign::Center,
VerticalAlign::Bottom => glyph_brush_layout::VerticalAlign::Bottom,
}
}
}
#[derive(Clone, Debug, Reflect)]
pub struct TextStyle {
pub font: Handle<Font>,
pub font_size: f32,
pub color: Color,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font: Default::default(),
font_size: 12.0,
color: Color::WHITE,
}
}
}
#[derive(Component, Default, Copy, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct Text2dSize {
pub size: Size,
}

View file

@ -0,0 +1,182 @@
use bevy_asset::Assets;
use bevy_ecs::{
bundle::Bundle,
entity::Entity,
query::{Changed, QueryState, With},
system::{Local, Query, QuerySet, Res, ResMut},
};
use bevy_math::{Mat4, Size, Vec3};
use bevy_render2::{texture::Image, RenderWorld};
use bevy_sprite2::{ExtractedSprite, ExtractedSprites, TextureAtlas};
use bevy_transform::prelude::{GlobalTransform, Transform};
use bevy_window::Windows;
use crate::{
DefaultTextPipeline, Font, FontAtlasSet, HorizontalAlign, Text, Text2dSize, TextError,
VerticalAlign,
};
/// The bundle of components needed to draw text in a 2D scene via a 2D `OrthographicCameraBundle`.
/// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs)
#[derive(Bundle, Clone, Debug)]
pub struct Text2dBundle {
pub text: Text,
pub transform: Transform,
pub global_transform: GlobalTransform,
pub text_2d_size: Text2dSize,
}
impl Default for Text2dBundle {
fn default() -> Self {
Self {
text: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
text_2d_size: Text2dSize {
size: Size::default(),
},
}
}
}
pub fn extract_text2d_sprite(
mut render_world: ResMut<RenderWorld>,
texture_atlases: Res<Assets<TextureAtlas>>,
text_pipeline: Res<DefaultTextPipeline>,
windows: Res<Windows>,
mut text2d_query: Query<(Entity, &Text, &GlobalTransform, &Text2dSize)>,
) {
let mut extracted_sprites = render_world.get_resource_mut::<ExtractedSprites>().unwrap();
let scale_factor = if let Some(window) = windows.get_primary() {
window.scale_factor() as f32
} else {
1.
};
for (entity, text, transform, calculated_size) in text2d_query.iter_mut() {
let (width, height) = (calculated_size.size.width, calculated_size.size.height);
if let Some(text_layout) = text_pipeline.get_glyphs(&entity) {
let text_glyphs = &text_layout.glyphs;
let alignment_offset = match text.alignment.vertical {
VerticalAlign::Top => Vec3::new(0.0, -height, 0.0),
VerticalAlign::Center => Vec3::new(0.0, -height * 0.5, 0.0),
VerticalAlign::Bottom => Vec3::ZERO,
} + match text.alignment.horizontal {
HorizontalAlign::Left => Vec3::ZERO,
HorizontalAlign::Center => Vec3::new(-width * 0.5, 0.0, 0.0),
HorizontalAlign::Right => Vec3::new(-width, 0.0, 0.0),
};
for text_glyph in text_glyphs {
let color = text.sections[text_glyph.section_index]
.style
.color
.as_rgba_linear();
let atlas = texture_atlases
.get(text_glyph.atlas_info.texture_atlas.clone_weak())
.unwrap();
let handle = atlas.texture.clone_weak();
let index = text_glyph.atlas_info.glyph_index as usize;
let rect = atlas.textures[index];
let atlas_size = Some(atlas.size);
let transform =
Mat4::from_rotation_translation(transform.rotation, transform.translation)
* Mat4::from_scale(transform.scale / scale_factor)
* Mat4::from_translation(
alignment_offset * scale_factor + text_glyph.position.extend(0.),
);
extracted_sprites.sprites.push(ExtractedSprite {
transform,
color,
rect,
handle,
atlas_size,
flip_x: false,
flip_y: false,
});
}
}
}
}
#[derive(Debug, Default)]
pub struct QueuedText2d {
entities: Vec<Entity>,
}
/// Updates the TextGlyphs with the new computed glyphs from the layout
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub fn text2d_system(
mut queued_text: Local<QueuedText2d>,
mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>,
windows: Res<Windows>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<DefaultTextPipeline>,
mut text_queries: QuerySet<(
QueryState<Entity, (With<Text2dSize>, Changed<Text>)>,
QueryState<(&Text, &mut Text2dSize), With<Text2dSize>>,
)>,
) {
// Adds all entities where the text or the style has changed to the local queue
for entity in text_queries.q0().iter_mut() {
queued_text.entities.push(entity);
}
if queued_text.entities.is_empty() {
return;
}
let scale_factor = if let Some(window) = windows.get_primary() {
window.scale_factor()
} else {
1.
};
// Computes all text in the local queue
let mut new_queue = Vec::new();
let mut query = text_queries.q1();
for entity in queued_text.entities.drain(..) {
if let Ok((text, mut calculated_size)) = query.get_mut(entity) {
match text_pipeline.queue_text(
entity,
&fonts,
&text.sections,
scale_factor,
text.alignment,
Size::new(f32::MAX, f32::MAX),
&mut *font_atlas_set_storage,
&mut *texture_atlases,
&mut *textures,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, let's add this entity to the
// queue for further processing
new_queue.push(entity);
}
Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {}.", e);
}
Ok(()) => {
let text_layout_info = text_pipeline.get_glyphs(&entity).expect(
"Failed to get glyphs from the pipeline that have just been computed",
);
calculated_size.size = Size {
width: scale_value(text_layout_info.size.width, 1. / scale_factor),
height: scale_value(text_layout_info.size.height, 1. / scale_factor),
};
}
}
}
}
queued_text.entities = new_queue;
}
pub fn scale_value(value: f32, factor: f64) -> f32 {
(value as f64 * factor) as f32
}

View file

@ -0,0 +1,35 @@
[package]
name = "bevy_ui2"
version = "0.5.0"
edition = "2021"
description = "A custom ECS-driven UI framework built specifically for Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../../crates/bevy_app", version = "0.5.0" }
bevy_asset = { path = "../../crates/bevy_asset", version = "0.5.0" }
bevy_core = { path = "../../crates/bevy_core", version = "0.5.0" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.5.0" }
bevy_derive = { path = "../../crates/bevy_derive", version = "0.5.0" }
bevy_ecs = { path = "../../crates/bevy_ecs", version = "0.5.0" }
bevy_input = { path = "../../crates/bevy_input", version = "0.5.0" }
bevy_log = { path = "../../crates/bevy_log", version = "0.5.0" }
bevy_math = { path = "../../crates/bevy_math", version = "0.5.0" }
bevy_reflect = { path = "../../crates/bevy_reflect", version = "0.5.0", features = ["bevy"] }
bevy_render2 = { path = "../bevy_render2", version = "0.5.0" }
bevy_sprite2 = { path = "../bevy_sprite2", version = "0.5.0" }
bevy_text2 = { path = "../bevy_text2", version = "0.5.0" }
bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" }
bevy_window = { path = "../../crates/bevy_window", version = "0.5.0" }
bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" }
# other
stretch = "0.3.2"
serde = {version = "1", features = ["derive"]}
smallvec = { version = "1.6", features = ["union", "const_generics"] }
bytemuck = { version = "1.5", features = ["derive"] }
crevice = { path = "../../crates/crevice", version = "0.8.0", features = ["glam"] }

View file

@ -0,0 +1,46 @@
#[derive(Debug, Clone)]
pub struct Anchors {
pub left: f32,
pub right: f32,
pub bottom: f32,
pub top: f32,
}
impl Anchors {
pub const BOTTOM_FULL: Anchors = Anchors::new(0.0, 1.0, 0.0, 0.0);
pub const BOTTOM_LEFT: Anchors = Anchors::new(0.0, 0.0, 0.0, 0.0);
pub const BOTTOM_RIGHT: Anchors = Anchors::new(1.0, 1.0, 0.0, 0.0);
pub const CENTER: Anchors = Anchors::new(0.5, 0.5, 0.5, 0.5);
pub const CENTER_BOTTOM: Anchors = Anchors::new(0.5, 0.5, 0.0, 0.0);
pub const CENTER_FULL_HORIZONTAL: Anchors = Anchors::new(0.0, 1.0, 0.5, 0.5);
pub const CENTER_FULL_VERTICAL: Anchors = Anchors::new(0.5, 0.5, 0.0, 1.0);
pub const CENTER_LEFT: Anchors = Anchors::new(0.0, 0.0, 0.5, 0.5);
pub const CENTER_RIGHT: Anchors = Anchors::new(1.0, 1.0, 0.5, 0.5);
pub const CENTER_TOP: Anchors = Anchors::new(0.5, 0.5, 1.0, 1.0);
pub const FULL: Anchors = Anchors::new(0.0, 1.0, 0.0, 1.0);
pub const LEFT_FULL: Anchors = Anchors::new(0.0, 0.0, 0.0, 1.0);
pub const RIGHT_FULL: Anchors = Anchors::new(1.0, 1.0, 0.0, 1.0);
pub const TOP_FULL: Anchors = Anchors::new(0.0, 1.0, 1.0, 1.0);
pub const TOP_LEFT: Anchors = Anchors::new(0.0, 0.0, 1.0, 1.0);
pub const TOP_RIGHT: Anchors = Anchors::new(1.0, 1.0, 1.0, 1.0);
pub const fn new(left: f32, right: f32, bottom: f32, top: f32) -> Self {
Anchors {
left,
right,
bottom,
top,
}
}
}
impl Default for Anchors {
fn default() -> Self {
Anchors {
left: 0.0,
right: 0.0,
bottom: 0.0,
top: 0.0,
}
}
}

View file

@ -0,0 +1,120 @@
use crate::{
widget::{Button, ImageMode},
CalculatedSize, FocusPolicy, Interaction, Node, Style, UiColor, UiImage, CAMERA_UI,
};
use bevy_ecs::bundle::Bundle;
use bevy_render2::{
camera::{Camera, DepthCalculation, OrthographicProjection, WindowOrigin},
view::VisibleEntities,
};
use bevy_text2::Text;
use bevy_transform::prelude::{GlobalTransform, Transform};
#[derive(Bundle, Clone, Debug, Default)]
pub struct NodeBundle {
pub node: Node,
pub style: Style,
pub color: UiColor,
pub image: UiImage,
pub transform: Transform,
pub global_transform: GlobalTransform,
}
#[derive(Bundle, Clone, Debug, Default)]
pub struct ImageBundle {
pub node: Node,
pub style: Style,
pub image_mode: ImageMode,
pub calculated_size: CalculatedSize,
pub color: UiColor,
pub image: UiImage,
pub transform: Transform,
pub global_transform: GlobalTransform,
}
#[derive(Bundle, Clone, Debug)]
pub struct TextBundle {
pub node: Node,
pub style: Style,
pub text: Text,
pub calculated_size: CalculatedSize,
pub focus_policy: FocusPolicy,
pub transform: Transform,
pub global_transform: GlobalTransform,
}
impl Default for TextBundle {
fn default() -> Self {
TextBundle {
focus_policy: FocusPolicy::Pass,
text: Default::default(),
node: Default::default(),
calculated_size: Default::default(),
style: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
}
}
}
#[derive(Bundle, Clone, Debug)]
pub struct ButtonBundle {
pub node: Node,
pub button: Button,
pub style: Style,
pub interaction: Interaction,
pub focus_policy: FocusPolicy,
pub color: UiColor,
pub image: UiImage,
pub transform: Transform,
pub global_transform: GlobalTransform,
}
impl Default for ButtonBundle {
fn default() -> Self {
ButtonBundle {
button: Button,
interaction: Default::default(),
focus_policy: Default::default(),
node: Default::default(),
style: Default::default(),
color: Default::default(),
image: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
}
}
}
#[derive(Bundle, Debug)]
pub struct UiCameraBundle {
pub camera: Camera,
pub orthographic_projection: OrthographicProjection,
pub transform: Transform,
pub global_transform: GlobalTransform,
// FIXME there is no frustrum culling for UI
pub visible_entities: VisibleEntities,
}
impl Default for UiCameraBundle {
fn default() -> Self {
// we want 0 to be "closest" and +far to be "farthest" in 2d, so we offset
// the camera's translation by far and use a right handed coordinate system
let far = 1000.0;
UiCameraBundle {
camera: Camera {
name: Some(CAMERA_UI.to_string()),
..Default::default()
},
orthographic_projection: OrthographicProjection {
far,
window_origin: WindowOrigin::BottomLeft,
depth_calculation: DepthCalculation::ZDifference,
..Default::default()
},
transform: Transform::from_xyz(0.0, 0.0, far - 0.1),
global_transform: Default::default(),
visible_entities: Default::default(),
}
}
}

View file

@ -0,0 +1,173 @@
use crate::{
AlignContent, AlignItems, AlignSelf, Direction, Display, FlexDirection, FlexWrap,
JustifyContent, PositionType, Style, Val,
};
use bevy_math::{Rect, Size};
pub fn from_rect(
scale_factor: f64,
rect: Rect<Val>,
) -> stretch::geometry::Rect<stretch::style::Dimension> {
stretch::geometry::Rect {
start: from_val(scale_factor, rect.left),
end: from_val(scale_factor, rect.right),
// NOTE: top and bottom are intentionally flipped. stretch has a flipped y-axis
top: from_val(scale_factor, rect.bottom),
bottom: from_val(scale_factor, rect.top),
}
}
pub fn from_f32_size(scale_factor: f64, size: Size<f32>) -> stretch::geometry::Size<f32> {
stretch::geometry::Size {
width: (scale_factor * size.width as f64) as f32,
height: (scale_factor * size.height as f64) as f32,
}
}
pub fn from_val_size(
scale_factor: f64,
size: Size<Val>,
) -> stretch::geometry::Size<stretch::style::Dimension> {
stretch::geometry::Size {
width: from_val(scale_factor, size.width),
height: from_val(scale_factor, size.height),
}
}
pub fn from_style(scale_factor: f64, value: &Style) -> stretch::style::Style {
stretch::style::Style {
overflow: stretch::style::Overflow::Visible,
display: value.display.into(),
position_type: value.position_type.into(),
direction: value.direction.into(),
flex_direction: value.flex_direction.into(),
flex_wrap: value.flex_wrap.into(),
align_items: value.align_items.into(),
align_self: value.align_self.into(),
align_content: value.align_content.into(),
justify_content: value.justify_content.into(),
position: from_rect(scale_factor, value.position),
margin: from_rect(scale_factor, value.margin),
padding: from_rect(scale_factor, value.padding),
border: from_rect(scale_factor, value.border),
flex_grow: value.flex_grow,
flex_shrink: value.flex_shrink,
flex_basis: from_val(scale_factor, value.flex_basis),
size: from_val_size(scale_factor, value.size),
min_size: from_val_size(scale_factor, value.min_size),
max_size: from_val_size(scale_factor, value.max_size),
aspect_ratio: match value.aspect_ratio {
Some(value) => stretch::number::Number::Defined(value),
None => stretch::number::Number::Undefined,
},
}
}
pub fn from_val(scale_factor: f64, val: Val) -> stretch::style::Dimension {
match val {
Val::Auto => stretch::style::Dimension::Auto,
Val::Percent(value) => stretch::style::Dimension::Percent(value / 100.0),
Val::Px(value) => stretch::style::Dimension::Points((scale_factor * value as f64) as f32),
Val::Undefined => stretch::style::Dimension::Undefined,
}
}
impl From<AlignItems> for stretch::style::AlignItems {
fn from(value: AlignItems) -> Self {
match value {
AlignItems::FlexStart => stretch::style::AlignItems::FlexStart,
AlignItems::FlexEnd => stretch::style::AlignItems::FlexEnd,
AlignItems::Center => stretch::style::AlignItems::Center,
AlignItems::Baseline => stretch::style::AlignItems::Baseline,
AlignItems::Stretch => stretch::style::AlignItems::Stretch,
}
}
}
impl From<AlignSelf> for stretch::style::AlignSelf {
fn from(value: AlignSelf) -> Self {
match value {
AlignSelf::Auto => stretch::style::AlignSelf::Auto,
AlignSelf::FlexStart => stretch::style::AlignSelf::FlexStart,
AlignSelf::FlexEnd => stretch::style::AlignSelf::FlexEnd,
AlignSelf::Center => stretch::style::AlignSelf::Center,
AlignSelf::Baseline => stretch::style::AlignSelf::Baseline,
AlignSelf::Stretch => stretch::style::AlignSelf::Stretch,
}
}
}
impl From<AlignContent> for stretch::style::AlignContent {
fn from(value: AlignContent) -> Self {
match value {
AlignContent::FlexStart => stretch::style::AlignContent::FlexStart,
AlignContent::FlexEnd => stretch::style::AlignContent::FlexEnd,
AlignContent::Center => stretch::style::AlignContent::Center,
AlignContent::Stretch => stretch::style::AlignContent::Stretch,
AlignContent::SpaceBetween => stretch::style::AlignContent::SpaceBetween,
AlignContent::SpaceAround => stretch::style::AlignContent::SpaceAround,
}
}
}
impl From<Direction> for stretch::style::Direction {
fn from(value: Direction) -> Self {
match value {
Direction::Inherit => stretch::style::Direction::Inherit,
Direction::LeftToRight => stretch::style::Direction::LTR,
Direction::RightToLeft => stretch::style::Direction::RTL,
}
}
}
impl From<Display> for stretch::style::Display {
fn from(value: Display) -> Self {
match value {
Display::Flex => stretch::style::Display::Flex,
Display::None => stretch::style::Display::None,
}
}
}
impl From<FlexDirection> for stretch::style::FlexDirection {
fn from(value: FlexDirection) -> Self {
match value {
FlexDirection::Row => stretch::style::FlexDirection::Row,
FlexDirection::Column => stretch::style::FlexDirection::Column,
FlexDirection::RowReverse => stretch::style::FlexDirection::RowReverse,
FlexDirection::ColumnReverse => stretch::style::FlexDirection::ColumnReverse,
}
}
}
impl From<JustifyContent> for stretch::style::JustifyContent {
fn from(value: JustifyContent) -> Self {
match value {
JustifyContent::FlexStart => stretch::style::JustifyContent::FlexStart,
JustifyContent::FlexEnd => stretch::style::JustifyContent::FlexEnd,
JustifyContent::Center => stretch::style::JustifyContent::Center,
JustifyContent::SpaceBetween => stretch::style::JustifyContent::SpaceBetween,
JustifyContent::SpaceAround => stretch::style::JustifyContent::SpaceAround,
JustifyContent::SpaceEvenly => stretch::style::JustifyContent::SpaceEvenly,
}
}
}
impl From<PositionType> for stretch::style::PositionType {
fn from(value: PositionType) -> Self {
match value {
PositionType::Relative => stretch::style::PositionType::Relative,
PositionType::Absolute => stretch::style::PositionType::Absolute,
}
}
}
impl From<FlexWrap> for stretch::style::FlexWrap {
fn from(value: FlexWrap) -> Self {
match value {
FlexWrap::NoWrap => stretch::style::FlexWrap::NoWrap,
FlexWrap::Wrap => stretch::style::FlexWrap::Wrap,
FlexWrap::WrapReverse => stretch::style::FlexWrap::WrapReverse,
}
}
}

View file

@ -0,0 +1,293 @@
mod convert;
use crate::{CalculatedSize, Node, Style};
use bevy_app::EventReader;
use bevy_ecs::{
entity::Entity,
query::{Changed, FilterFetch, With, Without, WorldQuery},
system::{Query, Res, ResMut},
};
use bevy_log::warn;
use bevy_math::Vec2;
use bevy_transform::prelude::{Children, Parent, Transform};
use bevy_utils::HashMap;
use bevy_window::{Window, WindowId, WindowScaleFactorChanged, Windows};
use std::fmt;
use stretch::{number::Number, Stretch};
pub struct FlexSurface {
entity_to_stretch: HashMap<Entity, stretch::node::Node>,
window_nodes: HashMap<WindowId, stretch::node::Node>,
stretch: Stretch,
}
// SAFE: as long as MeasureFunc is Send + Sync. https://github.com/vislyhq/stretch/issues/69
unsafe impl Send for FlexSurface {}
unsafe impl Sync for FlexSurface {}
fn _assert_send_sync_flex_surface_impl_safe() {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<HashMap<Entity, stretch::node::Node>>();
_assert_send_sync::<HashMap<WindowId, stretch::node::Node>>();
// FIXME https://github.com/vislyhq/stretch/issues/69
// _assert_send_sync::<Stretch>();
}
impl fmt::Debug for FlexSurface {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("FlexSurface")
.field("entity_to_stretch", &self.entity_to_stretch)
.field("window_nodes", &self.window_nodes)
.finish()
}
}
impl Default for FlexSurface {
fn default() -> Self {
Self {
entity_to_stretch: Default::default(),
window_nodes: Default::default(),
stretch: Stretch::new(),
}
}
}
impl FlexSurface {
pub fn upsert_node(&mut self, entity: Entity, style: &Style, scale_factor: f64) {
let mut added = false;
let stretch = &mut self.stretch;
let stretch_style = convert::from_style(scale_factor, style);
let stretch_node = self.entity_to_stretch.entry(entity).or_insert_with(|| {
added = true;
stretch.new_node(stretch_style, Vec::new()).unwrap()
});
if !added {
self.stretch
.set_style(*stretch_node, stretch_style)
.unwrap();
}
}
pub fn upsert_leaf(
&mut self,
entity: Entity,
style: &Style,
calculated_size: CalculatedSize,
scale_factor: f64,
) {
let stretch = &mut self.stretch;
let stretch_style = convert::from_style(scale_factor, style);
let measure = Box::new(move |constraints: stretch::geometry::Size<Number>| {
let mut size = convert::from_f32_size(scale_factor, calculated_size.size);
match (constraints.width, constraints.height) {
(Number::Undefined, Number::Undefined) => {}
(Number::Defined(width), Number::Undefined) => {
size.height = width * size.height / size.width;
size.width = width;
}
(Number::Undefined, Number::Defined(height)) => {
size.width = height * size.width / size.height;
size.height = height;
}
(Number::Defined(width), Number::Defined(height)) => {
size.width = width;
size.height = height;
}
}
Ok(size)
});
if let Some(stretch_node) = self.entity_to_stretch.get(&entity) {
self.stretch
.set_style(*stretch_node, stretch_style)
.unwrap();
self.stretch
.set_measure(*stretch_node, Some(measure))
.unwrap();
} else {
let stretch_node = stretch.new_leaf(stretch_style, measure).unwrap();
self.entity_to_stretch.insert(entity, stretch_node);
}
}
pub fn update_children(&mut self, entity: Entity, children: &Children) {
let mut stretch_children = Vec::with_capacity(children.len());
for child in children.iter() {
if let Some(stretch_node) = self.entity_to_stretch.get(child) {
stretch_children.push(*stretch_node);
} else {
warn!(
"Unstyled child in a UI entity hierarchy. You are using an entity \
without UI components as a child of an entity with UI components, results may be unexpected."
);
}
}
let stretch_node = self.entity_to_stretch.get(&entity).unwrap();
self.stretch
.set_children(*stretch_node, stretch_children)
.unwrap();
}
pub fn update_window(&mut self, window: &Window) {
let stretch = &mut self.stretch;
let node = self.window_nodes.entry(window.id()).or_insert_with(|| {
stretch
.new_node(stretch::style::Style::default(), Vec::new())
.unwrap()
});
stretch
.set_style(
*node,
stretch::style::Style {
size: stretch::geometry::Size {
width: stretch::style::Dimension::Points(window.physical_width() as f32),
height: stretch::style::Dimension::Points(window.physical_height() as f32),
},
..Default::default()
},
)
.unwrap();
}
pub fn set_window_children(
&mut self,
window_id: WindowId,
children: impl Iterator<Item = Entity>,
) {
let stretch_node = self.window_nodes.get(&window_id).unwrap();
let child_nodes = children
.map(|e| *self.entity_to_stretch.get(&e).unwrap())
.collect::<Vec<stretch::node::Node>>();
self.stretch
.set_children(*stretch_node, child_nodes)
.unwrap();
}
pub fn compute_window_layouts(&mut self) {
for window_node in self.window_nodes.values() {
self.stretch
.compute_layout(*window_node, stretch::geometry::Size::undefined())
.unwrap();
}
}
pub fn get_layout(&self, entity: Entity) -> Result<&stretch::result::Layout, FlexError> {
if let Some(stretch_node) = self.entity_to_stretch.get(&entity) {
self.stretch
.layout(*stretch_node)
.map_err(FlexError::StretchError)
} else {
warn!(
"Styled child in a non-UI entity hierarchy. You are using an entity \
with UI components as a child of an entity without UI components, results may be unexpected."
);
Err(FlexError::InvalidHierarchy)
}
}
}
#[derive(Debug)]
pub enum FlexError {
InvalidHierarchy,
StretchError(stretch::Error),
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub fn flex_node_system(
windows: Res<Windows>,
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
mut flex_surface: ResMut<FlexSurface>,
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With<Node>, Changed<Style>)>,
full_node_query: Query<(Entity, &Style, Option<&CalculatedSize>), With<Node>>,
changed_size_query: Query<
(Entity, &Style, &CalculatedSize),
(With<Node>, Changed<CalculatedSize>),
>,
children_query: Query<(Entity, &Children), (With<Node>, Changed<Children>)>,
mut node_transform_query: Query<(Entity, &mut Node, &mut Transform, Option<&Parent>)>,
) {
// update window root nodes
for window in windows.iter() {
flex_surface.update_window(window);
}
// assume one window for time being...
let logical_to_physical_factor = if let Some(primary_window) = windows.get_primary() {
primary_window.scale_factor()
} else {
1.
};
if scale_factor_events.iter().next_back().is_some() {
update_changed(
&mut *flex_surface,
logical_to_physical_factor,
full_node_query,
);
} else {
update_changed(&mut *flex_surface, logical_to_physical_factor, node_query);
}
fn update_changed<F: WorldQuery>(
flex_surface: &mut FlexSurface,
scaling_factor: f64,
query: Query<(Entity, &Style, Option<&CalculatedSize>), F>,
) where
F::Fetch: FilterFetch,
{
// update changed nodes
for (entity, style, calculated_size) in query.iter() {
// TODO: remove node from old hierarchy if its root has changed
if let Some(calculated_size) = calculated_size {
flex_surface.upsert_leaf(entity, style, *calculated_size, scaling_factor);
} else {
flex_surface.upsert_node(entity, style, scaling_factor);
}
}
}
for (entity, style, calculated_size) in changed_size_query.iter() {
flex_surface.upsert_leaf(entity, style, *calculated_size, logical_to_physical_factor);
}
// TODO: handle removed nodes
// update window children (for now assuming all Nodes live in the primary window)
if let Some(primary_window) = windows.get_primary() {
flex_surface.set_window_children(primary_window.id(), root_node_query.iter());
}
// update children
for (entity, children) in children_query.iter() {
flex_surface.update_children(entity, children);
}
// compute layouts
flex_surface.compute_window_layouts();
let physical_to_logical_factor = 1. / logical_to_physical_factor;
let to_logical = |v| (physical_to_logical_factor * v as f64) as f32;
// PERF: try doing this incrementally
for (entity, mut node, mut transform, parent) in node_transform_query.iter_mut() {
let layout = flex_surface.get_layout(entity).unwrap();
node.size = Vec2::new(
to_logical(layout.size.width),
to_logical(layout.size.height),
);
let position = &mut transform.translation;
position.x = to_logical(layout.location.x + layout.size.width / 2.0);
position.y = to_logical(layout.location.y + layout.size.height / 2.0);
if let Some(parent) = parent {
if let Ok(parent_layout) = flex_surface.get_layout(parent.0) {
position.x -= to_logical(parent_layout.size.width / 2.0);
position.y -= to_logical(parent_layout.size.height / 2.0);
}
}
}
}

View file

@ -0,0 +1,157 @@
use crate::Node;
use bevy_core::FloatOrd;
use bevy_ecs::{
entity::Entity,
prelude::Component,
reflect::ReflectComponent,
system::{Local, Query, Res},
};
use bevy_input::{mouse::MouseButton, touch::Touches, Input};
use bevy_reflect::{Reflect, ReflectDeserialize};
use bevy_transform::components::GlobalTransform;
use bevy_window::Windows;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[reflect_value(Component, Serialize, Deserialize, PartialEq)]
pub enum Interaction {
Clicked,
Hovered,
None,
}
impl Default for Interaction {
fn default() -> Self {
Interaction::None
}
}
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[reflect_value(Component, Serialize, Deserialize, PartialEq)]
pub enum FocusPolicy {
Block,
Pass,
}
impl Default for FocusPolicy {
fn default() -> Self {
FocusPolicy::Block
}
}
#[derive(Default)]
pub struct State {
entities_to_reset: SmallVec<[Entity; 1]>,
}
#[allow(clippy::type_complexity)]
pub fn ui_focus_system(
mut state: Local<State>,
windows: Res<Windows>,
mouse_button_input: Res<Input<MouseButton>>,
touches_input: Res<Touches>,
mut node_query: Query<(
Entity,
&Node,
&GlobalTransform,
Option<&mut Interaction>,
Option<&FocusPolicy>,
)>,
) {
let cursor_position = if let Some(cursor_position) = windows
.get_primary()
.and_then(|window| window.cursor_position())
{
cursor_position
} else {
return;
};
// reset entities that were both clicked and released in the last frame
for entity in state.entities_to_reset.drain(..) {
if let Ok(mut interaction) = node_query.get_component_mut::<Interaction>(entity) {
*interaction = Interaction::None;
}
}
let mouse_released =
mouse_button_input.just_released(MouseButton::Left) || touches_input.just_released(0);
if mouse_released {
for (_entity, _node, _global_transform, interaction, _focus_policy) in node_query.iter_mut()
{
if let Some(mut interaction) = interaction {
if *interaction == Interaction::Clicked {
*interaction = Interaction::None;
}
}
}
}
let mouse_clicked =
mouse_button_input.just_pressed(MouseButton::Left) || touches_input.just_released(0);
let mut moused_over_z_sorted_nodes = node_query
.iter_mut()
.filter_map(
|(entity, node, global_transform, interaction, focus_policy)| {
let position = global_transform.translation;
let ui_position = position.truncate();
let extents = node.size / 2.0;
let min = ui_position - extents;
let max = ui_position + extents;
// if the current cursor position is within the bounds of the node, consider it for
// clicking
if (min.x..max.x).contains(&cursor_position.x)
&& (min.y..max.y).contains(&cursor_position.y)
{
Some((entity, focus_policy, interaction, FloatOrd(position.z)))
} else {
if let Some(mut interaction) = interaction {
if *interaction == Interaction::Hovered {
*interaction = Interaction::None;
}
}
None
}
},
)
.collect::<Vec<_>>();
moused_over_z_sorted_nodes.sort_by_key(|(_, _, _, z)| -*z);
let mut moused_over_z_sorted_nodes = moused_over_z_sorted_nodes.into_iter();
// set Clicked or Hovered on top nodes
for (entity, focus_policy, interaction, _) in moused_over_z_sorted_nodes.by_ref() {
if let Some(mut interaction) = interaction {
if mouse_clicked {
// only consider nodes with Interaction "clickable"
if *interaction != Interaction::Clicked {
*interaction = Interaction::Clicked;
// if the mouse was simultaneously released, reset this Interaction in the next
// frame
if mouse_released {
state.entities_to_reset.push(entity);
}
}
} else if *interaction == Interaction::None {
*interaction = Interaction::Hovered;
}
}
match focus_policy.cloned().unwrap_or(FocusPolicy::Block) {
FocusPolicy::Block => {
break;
}
FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ }
}
}
// reset lower nodes to None
for (_entity, _focus_policy, interaction, _) in moused_over_z_sorted_nodes {
if let Some(mut interaction) = interaction {
if *interaction != Interaction::None {
*interaction = Interaction::None;
}
}
}
}

View file

@ -0,0 +1,96 @@
mod anchors;
mod flex;
mod focus;
mod margins;
mod render;
mod ui_node;
pub mod entity;
pub mod update;
pub mod widget;
pub use anchors::*;
pub use flex::*;
pub use focus::*;
pub use margins::*;
pub use render::*;
pub use ui_node::*;
pub mod prelude {
#[doc(hidden)]
pub use crate::{entity::*, ui_node::*, widget::Button, Anchors, Interaction, Margins};
}
use bevy_app::prelude::*;
use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
use bevy_input::InputSystem;
use bevy_math::{Rect, Size};
use bevy_transform::TransformSystem;
use update::ui_z_system;
#[derive(Default)]
pub struct UiPlugin;
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum UiSystem {
/// After this label, the ui flex state has been updated
Flex,
Focus,
}
impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<FlexSurface>()
.register_type::<AlignContent>()
.register_type::<AlignItems>()
.register_type::<AlignSelf>()
.register_type::<CalculatedSize>()
.register_type::<Direction>()
.register_type::<Display>()
.register_type::<FlexDirection>()
.register_type::<FlexWrap>()
.register_type::<FocusPolicy>()
.register_type::<Interaction>()
.register_type::<JustifyContent>()
.register_type::<Node>()
// NOTE: used by Style::aspect_ratio
.register_type::<Option<f32>>()
.register_type::<PositionType>()
.register_type::<Size<f32>>()
.register_type::<Size<Val>>()
.register_type::<Rect<Val>>()
.register_type::<Style>()
.register_type::<UiColor>()
.register_type::<UiImage>()
.register_type::<Val>()
.register_type::<widget::Button>()
.register_type::<widget::ImageMode>()
.add_system_to_stage(
CoreStage::PreUpdate,
ui_focus_system.label(UiSystem::Focus).after(InputSystem),
)
// add these stages to front because these must run before transform update systems
.add_system_to_stage(
CoreStage::PostUpdate,
widget::text_system.before(UiSystem::Flex),
)
.add_system_to_stage(
CoreStage::PostUpdate,
widget::image_node_system.before(UiSystem::Flex),
)
.add_system_to_stage(
CoreStage::PostUpdate,
flex_node_system
.label(UiSystem::Flex)
.before(TransformSystem::TransformPropagate),
)
.add_system_to_stage(
CoreStage::PostUpdate,
ui_z_system
.after(UiSystem::Flex)
.before(TransformSystem::TransformPropagate),
);
crate::render::build_ui_render(app);
}
}

View file

@ -0,0 +1,29 @@
#[derive(Debug, Clone)]
pub struct Margins {
pub left: f32,
pub right: f32,
pub bottom: f32,
pub top: f32,
}
impl Margins {
pub fn new(left: f32, right: f32, bottom: f32, top: f32) -> Self {
Margins {
left,
right,
bottom,
top,
}
}
}
impl Default for Margins {
fn default() -> Self {
Margins {
left: 0.0,
right: 0.0,
bottom: 0.0,
top: 0.0,
}
}
}

View file

@ -0,0 +1,14 @@
use bevy_ecs::prelude::*;
use bevy_render2::{camera::ActiveCameras, render_phase::RenderPhase};
pub const CAMERA_UI: &str = "camera_ui";
pub fn extract_ui_camera_phases(mut commands: Commands, active_cameras: Res<ActiveCameras>) {
if let Some(camera_ui) = active_cameras.get(CAMERA_UI) {
if let Some(entity) = camera_ui.entity {
commands
.get_or_spawn(entity)
.insert(RenderPhase::<super::TransparentUi>::default());
}
}
}

View file

@ -0,0 +1,409 @@
mod camera;
mod pipeline;
mod render_pass;
pub use camera::*;
pub use pipeline::*;
pub use render_pass::*;
use std::ops::Range;
use bevy_app::prelude::*;
use bevy_asset::{AssetEvent, Assets, Handle, HandleUntyped};
use bevy_core::FloatOrd;
use bevy_ecs::prelude::*;
use bevy_math::{const_vec3, Mat4, Vec2, Vec3, Vec4Swizzles};
use bevy_reflect::TypeUuid;
use bevy_render2::{
camera::ActiveCameras,
color::Color,
render_asset::RenderAssets,
render_graph::{RenderGraph, SlotInfo, SlotType},
render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions, RenderPhase},
render_resource::*,
renderer::{RenderDevice, RenderQueue},
texture::Image,
view::ViewUniforms,
RenderApp, RenderStage, RenderWorld,
};
use bevy_sprite2::{SpriteAssetEvents, TextureAtlas};
use bevy_text2::{DefaultTextPipeline, Text};
use bevy_transform::components::GlobalTransform;
use bevy_utils::HashMap;
use bevy_window::Windows;
use bytemuck::{Pod, Zeroable};
use crate::{Node, UiColor, UiImage};
pub mod node {
pub const UI_PASS_DRIVER: &str = "ui_pass_driver";
}
pub mod draw_ui_graph {
pub const NAME: &str = "draw_ui";
pub mod input {
pub const VIEW_ENTITY: &str = "view_entity";
}
pub mod node {
pub const UI_PASS: &str = "ui_pass";
}
}
pub const UI_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 13012847047162779583);
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum UiSystem {
ExtractNode,
}
pub fn build_ui_render(app: &mut App) {
let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
let ui_shader = Shader::from_wgsl(include_str!("ui.wgsl"));
shaders.set_untracked(UI_SHADER_HANDLE, ui_shader);
let mut active_cameras = app.world.get_resource_mut::<ActiveCameras>().unwrap();
active_cameras.add(CAMERA_UI);
let render_app = app.sub_app(RenderApp);
render_app
.init_resource::<UiPipeline>()
.init_resource::<SpecializedPipelines<UiPipeline>>()
.init_resource::<UiImageBindGroups>()
.init_resource::<UiMeta>()
.init_resource::<ExtractedUiNodes>()
.init_resource::<DrawFunctions<TransparentUi>>()
.add_render_command::<TransparentUi, DrawUi>()
.add_system_to_stage(RenderStage::Extract, extract_ui_camera_phases)
.add_system_to_stage(
RenderStage::Extract,
extract_uinodes.label(UiSystem::ExtractNode),
)
.add_system_to_stage(
RenderStage::Extract,
extract_text_uinodes.after(UiSystem::ExtractNode),
)
.add_system_to_stage(RenderStage::Prepare, prepare_uinodes)
.add_system_to_stage(RenderStage::Queue, queue_uinodes)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<TransparentUi>);
// Render graph
let ui_pass_node = UiPassNode::new(&mut render_app.world);
let mut graph = render_app.world.get_resource_mut::<RenderGraph>().unwrap();
let mut draw_ui_graph = RenderGraph::default();
draw_ui_graph.add_node(draw_ui_graph::node::UI_PASS, ui_pass_node);
let input_node_id = draw_ui_graph.set_input(vec![SlotInfo::new(
draw_ui_graph::input::VIEW_ENTITY,
SlotType::Entity,
)]);
draw_ui_graph
.add_slot_edge(
input_node_id,
draw_ui_graph::input::VIEW_ENTITY,
draw_ui_graph::node::UI_PASS,
UiPassNode::IN_VIEW,
)
.unwrap();
graph.add_sub_graph(draw_ui_graph::NAME, draw_ui_graph);
graph.add_node(node::UI_PASS_DRIVER, UiPassDriverNode);
graph
.add_node_edge(
bevy_core_pipeline::node::MAIN_PASS_DRIVER,
node::UI_PASS_DRIVER,
)
.unwrap();
}
pub struct ExtractedUiNode {
pub transform: Mat4,
pub color: Color,
pub rect: bevy_sprite2::Rect,
pub image: Handle<Image>,
pub atlas_size: Option<Vec2>,
}
#[derive(Default)]
pub struct ExtractedUiNodes {
pub uinodes: Vec<ExtractedUiNode>,
}
pub fn extract_uinodes(
mut render_world: ResMut<RenderWorld>,
images: Res<Assets<Image>>,
uinode_query: Query<(&Node, &GlobalTransform, &UiColor, &UiImage)>,
) {
let mut extracted_uinodes = render_world.get_resource_mut::<ExtractedUiNodes>().unwrap();
extracted_uinodes.uinodes.clear();
for (uinode, transform, color, image) in uinode_query.iter() {
let image = image.0.clone_weak();
// Skip loading images
if !images.contains(image.clone_weak()) {
continue;
}
extracted_uinodes.uinodes.push(ExtractedUiNode {
transform: transform.compute_matrix(),
color: color.0,
rect: bevy_sprite2::Rect {
min: Vec2::ZERO,
max: uinode.size,
},
image,
atlas_size: None,
});
}
}
pub fn extract_text_uinodes(
mut render_world: ResMut<RenderWorld>,
texture_atlases: Res<Assets<TextureAtlas>>,
text_pipeline: Res<DefaultTextPipeline>,
windows: Res<Windows>,
uinode_query: Query<(Entity, &Node, &GlobalTransform, &Text)>,
) {
let mut extracted_uinodes = render_world.get_resource_mut::<ExtractedUiNodes>().unwrap();
let scale_factor = if let Some(window) = windows.get_primary() {
window.scale_factor() as f32
} else {
1.
};
for (entity, uinode, transform, text) in uinode_query.iter() {
// Skip if size is set to zero (e.g. when a parent is set to `Display::None`)
if uinode.size == Vec2::ZERO {
continue;
}
if let Some(text_layout) = text_pipeline.get_glyphs(&entity) {
let text_glyphs = &text_layout.glyphs;
let alignment_offset = (uinode.size / -2.0).extend(0.0);
for text_glyph in text_glyphs {
let color = text.sections[text_glyph.section_index].style.color;
let atlas = texture_atlases
.get(text_glyph.atlas_info.texture_atlas.clone_weak())
.unwrap();
let texture = atlas.texture.clone_weak();
let index = text_glyph.atlas_info.glyph_index as usize;
let rect = atlas.textures[index];
let atlas_size = Some(atlas.size);
let transform =
Mat4::from_rotation_translation(transform.rotation, transform.translation)
* Mat4::from_scale(transform.scale / scale_factor)
* Mat4::from_translation(
alignment_offset * scale_factor + text_glyph.position.extend(0.),
);
extracted_uinodes.uinodes.push(ExtractedUiNode {
transform,
color,
rect,
image: texture,
atlas_size,
});
}
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct UiVertex {
pub position: [f32; 3],
pub uv: [f32; 2],
pub color: u32,
}
pub struct UiMeta {
vertices: BufferVec<UiVertex>,
view_bind_group: Option<BindGroup>,
}
impl Default for UiMeta {
fn default() -> Self {
Self {
vertices: BufferVec::new(BufferUsages::VERTEX),
view_bind_group: None,
}
}
}
const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
const_vec3!([-0.5, -0.5, 0.0]),
const_vec3!([0.5, 0.5, 0.0]),
const_vec3!([-0.5, 0.5, 0.0]),
const_vec3!([-0.5, -0.5, 0.0]),
const_vec3!([0.5, -0.5, 0.0]),
const_vec3!([0.5, 0.5, 0.0]),
];
#[derive(Component)]
pub struct UiBatch {
pub range: Range<u32>,
pub image: Handle<Image>,
pub z: f32,
}
pub fn prepare_uinodes(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut ui_meta: ResMut<UiMeta>,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
) {
ui_meta.vertices.clear();
// sort by increasing z for correct transparency
extracted_uinodes
.uinodes
.sort_by(|a, b| FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.w_axis[2])));
let mut start = 0;
let mut end = 0;
let mut current_batch_handle = Default::default();
let mut last_z = 0.0;
for extracted_uinode in extracted_uinodes.uinodes.iter() {
if current_batch_handle != extracted_uinode.image {
if start != end {
commands.spawn_bundle((UiBatch {
range: start..end,
image: current_batch_handle,
z: last_z,
},));
start = end;
}
current_batch_handle = extracted_uinode.image.clone_weak();
}
let uinode_rect = extracted_uinode.rect;
// Specify the corners of the node
let mut bottom_left = Vec2::new(uinode_rect.min.x, uinode_rect.max.y);
let mut top_left = uinode_rect.min;
let mut top_right = Vec2::new(uinode_rect.max.x, uinode_rect.min.y);
let mut bottom_right = uinode_rect.max;
let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
bottom_left /= atlas_extent;
bottom_right /= atlas_extent;
top_left /= atlas_extent;
top_right /= atlas_extent;
let uvs: [[f32; 2]; 6] = [
bottom_left.into(),
top_right.into(),
top_left.into(),
bottom_left.into(),
bottom_right.into(),
top_right.into(),
];
let rect_size = extracted_uinode.rect.size().extend(1.0);
let color = extracted_uinode.color.as_linear_rgba_f32();
// encode color as a single u32 to save space
let color = (color[0] * 255.0) as u32
| ((color[1] * 255.0) as u32) << 8
| ((color[2] * 255.0) as u32) << 16
| ((color[3] * 255.0) as u32) << 24;
for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() {
let mut final_position = *vertex_position * rect_size;
final_position = (extracted_uinode.transform * final_position.extend(1.0)).xyz();
ui_meta.vertices.push(UiVertex {
position: final_position.into(),
uv: uvs[index],
color,
});
}
last_z = extracted_uinode.transform.w_axis[2];
end += QUAD_VERTEX_POSITIONS.len() as u32;
}
// if start != end, there is one last batch to process
if start != end {
commands.spawn_bundle((UiBatch {
range: start..end,
image: current_batch_handle,
z: last_z,
},));
}
ui_meta.vertices.write_buffer(&render_device, &render_queue);
}
#[derive(Default)]
pub struct UiImageBindGroups {
pub values: HashMap<Handle<Image>, BindGroup>,
}
#[allow(clippy::too_many_arguments)]
pub fn queue_uinodes(
draw_functions: Res<DrawFunctions<TransparentUi>>,
render_device: Res<RenderDevice>,
mut ui_meta: ResMut<UiMeta>,
view_uniforms: Res<ViewUniforms>,
ui_pipeline: Res<UiPipeline>,
mut pipelines: ResMut<SpecializedPipelines<UiPipeline>>,
mut pipeline_cache: ResMut<RenderPipelineCache>,
mut image_bind_groups: ResMut<UiImageBindGroups>,
gpu_images: Res<RenderAssets<Image>>,
mut ui_batches: Query<(Entity, &UiBatch)>,
mut views: Query<&mut RenderPhase<TransparentUi>>,
events: Res<SpriteAssetEvents>,
) {
// If an image has changed, the GpuImage has (probably) changed
for event in &events.images {
match event {
AssetEvent::Created { .. } => None,
AssetEvent::Modified { handle } => image_bind_groups.values.remove(handle),
AssetEvent::Removed { handle } => image_bind_groups.values.remove(handle),
};
}
if let Some(view_binding) = view_uniforms.uniforms.binding() {
ui_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor {
entries: &[BindGroupEntry {
binding: 0,
resource: view_binding,
}],
label: Some("ui_view_bind_group"),
layout: &ui_pipeline.view_layout,
}));
let draw_ui_function = draw_functions.read().get_id::<DrawUi>().unwrap();
let pipeline = pipelines.specialize(&mut pipeline_cache, &ui_pipeline, UiPipelineKey {});
for mut transparent_phase in views.iter_mut() {
for (entity, batch) in ui_batches.iter_mut() {
image_bind_groups
.values
.entry(batch.image.clone_weak())
.or_insert_with(|| {
let gpu_image = gpu_images.get(&batch.image).unwrap();
render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&gpu_image.texture_view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&gpu_image.sampler),
},
],
label: Some("ui_material_bind_group"),
layout: &ui_pipeline.image_layout,
})
});
transparent_phase.add(TransparentUi {
draw_function: draw_ui_function,
pipeline,
entity,
sort_key: FloatOrd(batch.z),
});
}
}
}
}

View file

@ -0,0 +1,132 @@
use bevy_ecs::prelude::*;
use bevy_render2::{
render_resource::*, renderer::RenderDevice, texture::BevyDefault, view::ViewUniform,
};
use crevice::std140::AsStd140;
pub struct UiPipeline {
pub view_layout: BindGroupLayout,
pub image_layout: BindGroupLayout,
}
impl FromWorld for UiPipeline {
fn from_world(world: &mut World) -> Self {
let world = world.cell();
let render_device = world.get_resource::<RenderDevice>().unwrap();
let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(ViewUniform::std140_size_static() as u64),
},
count: None,
}],
label: Some("ui_view_layout"),
});
let image_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler {
comparison: false,
filtering: true,
},
count: None,
},
],
label: Some("ui_image_layout"),
});
UiPipeline {
view_layout,
image_layout,
}
}
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct UiPipelineKey {}
impl SpecializedPipeline for UiPipeline {
type Key = UiPipelineKey;
/// FIXME: there are no specialization for now, should this be removed?
fn specialize(&self, _key: Self::Key) -> RenderPipelineDescriptor {
let vertex_buffer_layout = VertexBufferLayout {
array_stride: 24,
step_mode: VertexStepMode::Vertex,
attributes: vec![
// Position
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
// UV
VertexAttribute {
format: VertexFormat::Float32x2,
offset: 12,
shader_location: 1,
},
VertexAttribute {
format: VertexFormat::Uint32,
offset: 20,
shader_location: 2,
},
],
};
let shader_defs = Vec::new();
RenderPipelineDescriptor {
vertex: VertexState {
shader: super::UI_SHADER_HANDLE.typed::<Shader>(),
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_buffer_layout],
},
fragment: Some(FragmentState {
shader: super::UI_SHADER_HANDLE.typed::<Shader>(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![ColorTargetState {
format: TextureFormat::bevy_default(),
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
}],
}),
layout: Some(vec![self.view_layout.clone(), self.image_layout.clone()]),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
polygon_mode: PolygonMode::Fill,
clamp_depth: false,
conservative: false,
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("ui_pipeline".into()),
}
}
}

View file

@ -0,0 +1,197 @@
use bevy_core::FloatOrd;
use bevy_ecs::{
prelude::*,
system::{lifetimeless::*, SystemParamItem},
};
use bevy_render2::{
camera::ExtractedCameraNames,
render_graph::*,
render_phase::*,
render_resource::{
CachedPipelineId, LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor,
},
renderer::*,
view::*,
};
use super::{draw_ui_graph, UiBatch, UiImageBindGroups, UiMeta, CAMERA_UI};
pub struct UiPassDriverNode;
impl bevy_render2::render_graph::Node for UiPassDriverNode {
fn run(
&self,
graph: &mut RenderGraphContext,
_render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let extracted_cameras = world.get_resource::<ExtractedCameraNames>().unwrap();
if let Some(camera_ui) = extracted_cameras.entities.get(CAMERA_UI) {
graph.run_sub_graph(draw_ui_graph::NAME, vec![SlotValue::Entity(*camera_ui)])?;
}
Ok(())
}
}
pub struct UiPassNode {
query:
QueryState<(&'static RenderPhase<TransparentUi>, &'static ViewTarget), With<ExtractedView>>,
}
impl UiPassNode {
pub const IN_VIEW: &'static str = "view";
pub fn new(world: &mut World) -> Self {
Self {
query: QueryState::new(world),
}
}
}
impl bevy_render2::render_graph::Node for UiPassNode {
fn input(&self) -> Vec<SlotInfo> {
vec![SlotInfo::new(UiPassNode::IN_VIEW, SlotType::Entity)]
}
fn update(&mut self, world: &mut World) {
self.query.update_archetypes(world);
}
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
let (transparent_phase, target) = self
.query
.get_manual(world, view_entity)
.expect("view entity should exist");
let pass_descriptor = RenderPassDescriptor {
label: Some("ui_pass"),
color_attachments: &[RenderPassColorAttachment {
view: &target.view,
resolve_target: None,
ops: Operations {
load: LoadOp::Load,
store: true,
},
}],
depth_stencil_attachment: None,
};
let draw_functions = world
.get_resource::<DrawFunctions<TransparentUi>>()
.unwrap();
let render_pass = render_context
.command_encoder
.begin_render_pass(&pass_descriptor);
let mut draw_functions = draw_functions.write();
let mut tracked_pass = TrackedRenderPass::new(render_pass);
for item in transparent_phase.items.iter() {
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
draw_function.draw(world, &mut tracked_pass, view_entity, item);
}
Ok(())
}
}
pub struct TransparentUi {
pub sort_key: FloatOrd,
pub entity: Entity,
pub pipeline: CachedPipelineId,
pub draw_function: DrawFunctionId,
}
impl PhaseItem for TransparentUi {
type SortKey = FloatOrd;
#[inline]
fn sort_key(&self) -> Self::SortKey {
self.sort_key
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.draw_function
}
}
impl EntityPhaseItem for TransparentUi {
#[inline]
fn entity(&self) -> Entity {
self.entity
}
}
impl CachedPipelinePhaseItem for TransparentUi {
#[inline]
fn cached_pipeline(&self) -> CachedPipelineId {
self.pipeline
}
}
pub type DrawUi = (
SetItemPipeline,
SetUiViewBindGroup<0>,
SetUiTextureBindGroup<1>,
DrawUiNode,
);
pub struct SetUiViewBindGroup<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetUiViewBindGroup<I> {
type Param = (SRes<UiMeta>, SQuery<Read<ViewUniformOffset>>);
fn render<'w>(
view: Entity,
_item: Entity,
(ui_meta, view_query): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let view_uniform = view_query.get(view).unwrap(); // TODO: store bind group as component?
pass.set_bind_group(
I,
ui_meta.into_inner().view_bind_group.as_ref().unwrap(),
&[view_uniform.offset],
);
RenderCommandResult::Success
}
}
pub struct SetUiTextureBindGroup<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetUiTextureBindGroup<I> {
type Param = (SRes<UiImageBindGroups>, SQuery<Read<UiBatch>>);
fn render<'w>(
_view: Entity,
item: Entity,
(image_bind_groups, query_batch): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let batch = query_batch.get(item).unwrap();
let image_bind_groups = image_bind_groups.into_inner();
pass.set_bind_group(1, image_bind_groups.values.get(&batch.image).unwrap(), &[]);
RenderCommandResult::Success
}
}
pub struct DrawUiNode;
impl EntityRenderCommand for DrawUiNode {
type Param = (SRes<UiMeta>, SQuery<Read<UiBatch>>);
fn render<'w>(
_view: Entity,
item: Entity,
(ui_meta, query_batch): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let batch = query_batch.get(item).unwrap();
pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..));
pass.draw(batch.range.clone(), 0..1);
RenderCommandResult::Success
}
}

View file

@ -0,0 +1,38 @@
[[block]]
struct View {
view_proj: mat4x4<f32>;
world_position: vec3<f32>;
};
[[group(0), binding(0)]]
var<uniform> view: View;
struct VertexOutput {
[[location(0)]] uv: vec2<f32>;
[[location(1)]] color: vec4<f32>;
[[builtin(position)]] position: vec4<f32>;
};
[[stage(vertex)]]
fn vertex(
[[location(0)]] vertex_position: vec3<f32>,
[[location(1)]] vertex_uv: vec2<f32>,
[[location(2)]] vertex_color: u32,
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
out.color = vec4<f32>((vec4<u32>(vertex_color) >> vec4<u32>(0u, 8u, 16u, 24u)) & vec4<u32>(255u)) / 255.0;
return out;
}
[[group(1), binding(0)]]
var sprite_texture: texture_2d<f32>;
[[group(1), binding(1)]]
var sprite_sampler: sampler;
[[stage(fragment)]]
fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
var color = textureSample(sprite_texture, sprite_sampler, in.uv);
color = in.color * color;
return color;
}

View file

@ -0,0 +1,288 @@
use bevy_asset::Handle;
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_math::{Rect, Size, Vec2};
use bevy_reflect::{Reflect, ReflectDeserialize};
use bevy_render2::{
color::Color,
texture::{Image, DEFAULT_IMAGE_HANDLE},
};
use serde::{Deserialize, Serialize};
use std::ops::{Add, AddAssign};
#[derive(Component, Debug, Clone, Default, Reflect)]
#[reflect(Component)]
pub struct Node {
pub size: Vec2,
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum Val {
Undefined,
Auto,
Px(f32),
Percent(f32),
}
impl Default for Val {
fn default() -> Self {
Val::Undefined
}
}
impl Add<f32> for Val {
type Output = Val;
fn add(self, rhs: f32) -> Self::Output {
match self {
Val::Undefined => Val::Undefined,
Val::Auto => Val::Auto,
Val::Px(value) => Val::Px(value + rhs),
Val::Percent(value) => Val::Percent(value + rhs),
}
}
}
impl AddAssign<f32> for Val {
fn add_assign(&mut self, rhs: f32) {
match self {
Val::Undefined | Val::Auto => {}
Val::Px(value) => *value += rhs,
Val::Percent(value) => *value += rhs,
}
}
}
#[derive(Component, Clone, PartialEq, Debug, Reflect)]
#[reflect(Component, PartialEq)]
pub struct Style {
pub display: Display,
pub position_type: PositionType,
pub direction: Direction,
pub flex_direction: FlexDirection,
pub flex_wrap: FlexWrap,
pub align_items: AlignItems,
pub align_self: AlignSelf,
pub align_content: AlignContent,
pub justify_content: JustifyContent,
pub position: Rect<Val>,
pub margin: Rect<Val>,
pub padding: Rect<Val>,
pub border: Rect<Val>,
pub flex_grow: f32,
pub flex_shrink: f32,
pub flex_basis: Val,
pub size: Size<Val>,
pub min_size: Size<Val>,
pub max_size: Size<Val>,
pub aspect_ratio: Option<f32>,
}
impl Default for Style {
fn default() -> Self {
Self {
display: Default::default(),
position_type: Default::default(),
direction: Default::default(),
flex_direction: Default::default(),
flex_wrap: Default::default(),
align_items: Default::default(),
align_self: Default::default(),
align_content: Default::default(),
justify_content: Default::default(),
position: Default::default(),
margin: Default::default(),
padding: Default::default(),
border: Default::default(),
flex_grow: 0.0,
flex_shrink: 1.0,
flex_basis: Val::Auto,
size: Size::new(Val::Auto, Val::Auto),
min_size: Size::new(Val::Auto, Val::Auto),
max_size: Size::new(Val::Auto, Val::Auto),
aspect_ratio: Default::default(),
}
}
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum AlignItems {
FlexStart,
FlexEnd,
Center,
Baseline,
Stretch,
}
impl Default for AlignItems {
fn default() -> AlignItems {
AlignItems::Stretch
}
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum AlignSelf {
Auto,
FlexStart,
FlexEnd,
Center,
Baseline,
Stretch,
}
impl Default for AlignSelf {
fn default() -> AlignSelf {
AlignSelf::Auto
}
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum AlignContent {
FlexStart,
FlexEnd,
Center,
Stretch,
SpaceBetween,
SpaceAround,
}
impl Default for AlignContent {
fn default() -> AlignContent {
AlignContent::Stretch
}
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum Direction {
Inherit,
LeftToRight,
RightToLeft,
}
impl Default for Direction {
fn default() -> Direction {
Direction::Inherit
}
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum Display {
Flex,
None,
}
impl Default for Display {
fn default() -> Display {
Display::Flex
}
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum FlexDirection {
Row,
Column,
RowReverse,
ColumnReverse,
}
impl Default for FlexDirection {
fn default() -> FlexDirection {
FlexDirection::Row
}
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum JustifyContent {
FlexStart,
FlexEnd,
Center,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
impl Default for JustifyContent {
fn default() -> JustifyContent {
JustifyContent::FlexStart
}
}
// TODO: add support for overflow settings
// #[derive(Copy, Clone, PartialEq, Debug)]
// pub enum Overflow {
// Visible,
// Hidden,
// Scroll,
// }
// impl Default for Overflow {
// fn default() -> Overflow {
// Overflow::Visible
// }
// }
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum PositionType {
Relative,
Absolute,
}
impl Default for PositionType {
fn default() -> PositionType {
PositionType::Relative
}
}
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
pub enum FlexWrap {
NoWrap,
Wrap,
WrapReverse,
}
impl Default for FlexWrap {
fn default() -> FlexWrap {
FlexWrap::NoWrap
}
}
#[derive(Component, Default, Copy, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct CalculatedSize {
pub size: Size,
}
#[derive(Component, Default, Copy, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct UiColor(pub Color);
impl From<Color> for UiColor {
fn from(color: Color) -> Self {
Self(color)
}
}
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct UiImage(pub Handle<Image>);
impl Default for UiImage {
fn default() -> Self {
Self(DEFAULT_IMAGE_HANDLE.typed())
}
}
impl From<Handle<Image>> for UiImage {
fn from(handle: Handle<Image>) -> Self {
Self(handle)
}
}

View file

@ -0,0 +1,160 @@
use super::Node;
use bevy_ecs::{
entity::Entity,
query::{With, Without},
system::Query,
};
use bevy_transform::prelude::{Children, Parent, Transform};
pub const UI_Z_STEP: f32 = 0.001;
pub fn ui_z_system(
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
mut node_query: Query<&mut Transform, With<Node>>,
children_query: Query<&Children>,
) {
let mut current_global_z = 0.0;
for entity in root_node_query.iter() {
current_global_z = update_hierarchy(
&children_query,
&mut node_query,
entity,
current_global_z,
current_global_z,
);
}
}
fn update_hierarchy(
children_query: &Query<&Children>,
node_query: &mut Query<&mut Transform, With<Node>>,
entity: Entity,
parent_global_z: f32,
mut current_global_z: f32,
) -> f32 {
current_global_z += UI_Z_STEP;
if let Ok(mut transform) = node_query.get_mut(entity) {
transform.translation.z = current_global_z - parent_global_z;
}
if let Ok(children) = children_query.get(entity) {
let current_parent_global_z = current_global_z;
for child in children.iter().cloned() {
current_global_z = update_hierarchy(
children_query,
node_query,
child,
current_parent_global_z,
current_global_z,
);
}
}
current_global_z
}
#[cfg(test)]
mod tests {
use bevy_ecs::{
component::Component,
schedule::{Schedule, Stage, SystemStage},
system::{CommandQueue, Commands},
world::World,
};
use bevy_transform::{components::Transform, hierarchy::BuildChildren};
use crate::Node;
use super::{ui_z_system, UI_Z_STEP};
#[derive(Component, PartialEq, Debug, Clone)]
struct Label(&'static str);
fn node_with_transform(name: &'static str) -> (Label, Node, Transform) {
(Label(name), Node::default(), Transform::identity())
}
fn node_without_transform(name: &'static str) -> (Label, Node) {
(Label(name), Node::default())
}
fn get_steps(transform: &Transform) -> u32 {
(transform.translation.z / UI_Z_STEP).round() as u32
}
#[test]
fn test_ui_z_system() {
let mut world = World::default();
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &world);
commands.spawn_bundle(node_with_transform("0"));
commands
.spawn_bundle(node_with_transform("1"))
.with_children(|parent| {
parent
.spawn_bundle(node_with_transform("1-0"))
.with_children(|parent| {
parent.spawn_bundle(node_with_transform("1-0-0"));
parent.spawn_bundle(node_without_transform("1-0-1"));
parent.spawn_bundle(node_with_transform("1-0-2"));
});
parent.spawn_bundle(node_with_transform("1-1"));
parent
.spawn_bundle(node_without_transform("1-2"))
.with_children(|parent| {
parent.spawn_bundle(node_with_transform("1-2-0"));
parent.spawn_bundle(node_with_transform("1-2-1"));
parent
.spawn_bundle(node_with_transform("1-2-2"))
.with_children(|_| ());
parent.spawn_bundle(node_with_transform("1-2-3"));
});
parent.spawn_bundle(node_with_transform("1-3"));
});
commands
.spawn_bundle(node_without_transform("2"))
.with_children(|parent| {
parent
.spawn_bundle(node_with_transform("2-0"))
.with_children(|_parent| ());
parent
.spawn_bundle(node_with_transform("2-1"))
.with_children(|parent| {
parent.spawn_bundle(node_with_transform("2-1-0"));
});
});
queue.apply(&mut world);
let mut schedule = Schedule::default();
let mut update_stage = SystemStage::parallel();
update_stage.add_system(ui_z_system);
schedule.add_stage("update", update_stage);
schedule.run(&mut world);
let mut actual_result = world
.query::<(&Label, &Transform)>()
.iter(&world)
.map(|(name, transform)| (name.clone(), get_steps(transform)))
.collect::<Vec<(Label, u32)>>();
actual_result.sort_unstable_by_key(|(name, _)| name.0);
let expected_result = vec![
(Label("0"), 1),
(Label("1"), 1),
(Label("1-0"), 1),
(Label("1-0-0"), 1),
// 1-0-1 has no transform
(Label("1-0-2"), 3),
(Label("1-1"), 5),
// 1-2 has no transform
(Label("1-2-0"), 1),
(Label("1-2-1"), 2),
(Label("1-2-2"), 3),
(Label("1-2-3"), 4),
(Label("1-3"), 11),
// 2 has no transform
(Label("2-0"), 1),
(Label("2-1"), 2),
(Label("2-1-0"), 1),
];
assert_eq!(actual_result, expected_result);
}
}

View file

@ -0,0 +1,7 @@
use bevy_ecs::prelude::Component;
use bevy_ecs::reflect::ReflectComponent;
use bevy_reflect::Reflect;
#[derive(Component, Debug, Default, Clone, Copy, Reflect)]
#[reflect(Component)]
pub struct Button;

View file

@ -0,0 +1,42 @@
use crate::{CalculatedSize, UiImage};
use bevy_asset::Assets;
use bevy_ecs::{
component::Component,
query::With,
reflect::ReflectComponent,
system::{Query, Res},
};
use bevy_math::Size;
use bevy_reflect::{Reflect, ReflectDeserialize};
use bevy_render2::texture::Image;
use serde::{Deserialize, Serialize};
#[derive(Component, Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect_value(Component, Serialize, Deserialize)]
pub enum ImageMode {
KeepAspect,
}
impl Default for ImageMode {
fn default() -> Self {
ImageMode::KeepAspect
}
}
pub fn image_node_system(
textures: Res<Assets<Image>>,
mut query: Query<(&mut CalculatedSize, &UiImage), With<ImageMode>>,
) {
for (mut calculated_size, image) in query.iter_mut() {
if let Some(texture) = textures.get(image.0.clone_weak()) {
let size = Size {
width: texture.texture_descriptor.size.width as f32,
height: texture.texture_descriptor.size.height as f32,
};
// Update only if size has changed to avoid needless layout calculations
if size != calculated_size.size {
calculated_size.size = size;
}
}
}
}

View file

@ -0,0 +1,7 @@
mod button;
mod image;
mod text;
pub use button::*;
pub use image::*;
pub use text::*;

View file

@ -0,0 +1,134 @@
use crate::{CalculatedSize, Style, Val};
use bevy_asset::Assets;
use bevy_ecs::{
entity::Entity,
prelude::QueryState,
query::{Changed, Or, With},
system::{Local, QuerySet, Res, ResMut},
};
use bevy_math::Size;
use bevy_render2::texture::Image;
use bevy_sprite2::TextureAtlas;
use bevy_text2::{DefaultTextPipeline, Font, FontAtlasSet, Text, TextError};
use bevy_window::Windows;
#[derive(Debug, Default)]
pub struct QueuedText {
entities: Vec<Entity>,
}
fn scale_value(value: f32, factor: f64) -> f32 {
(value as f64 * factor) as f32
}
/// Defines how min_size, size, and max_size affects the bounds of a text
/// block.
pub fn text_constraint(min_size: Val, size: Val, max_size: Val, scale_factor: f64) -> f32 {
// Needs support for percentages
match (min_size, size, max_size) {
(_, _, Val::Px(max)) => scale_value(max, scale_factor),
(Val::Px(min), _, _) => scale_value(min, scale_factor),
(Val::Undefined, Val::Px(size), Val::Undefined) => scale_value(size, scale_factor),
(Val::Auto, Val::Px(size), Val::Auto) => scale_value(size, scale_factor),
_ => f32::MAX,
}
}
/// Computes the size of a text block and updates the TextGlyphs with the
/// new computed glyphs from the layout
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub fn text_system(
mut queued_text: Local<QueuedText>,
mut last_scale_factor: Local<f64>,
mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>,
windows: Res<Windows>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<DefaultTextPipeline>,
mut text_queries: QuerySet<(
QueryState<Entity, Or<(Changed<Text>, Changed<Style>)>>,
QueryState<Entity, (With<Text>, With<Style>)>,
QueryState<(&Text, &Style, &mut CalculatedSize)>,
)>,
) {
let scale_factor = if let Some(window) = windows.get_primary() {
window.scale_factor()
} else {
1.
};
let inv_scale_factor = 1. / scale_factor;
#[allow(clippy::float_cmp)]
if *last_scale_factor == scale_factor {
// Adds all entities where the text or the style has changed to the local queue
for entity in text_queries.q0().iter() {
queued_text.entities.push(entity);
}
} else {
// If the scale factor has changed, queue all text
for entity in text_queries.q1().iter() {
queued_text.entities.push(entity);
}
*last_scale_factor = scale_factor;
}
if queued_text.entities.is_empty() {
return;
}
// Computes all text in the local queue
let mut new_queue = Vec::new();
let mut query = text_queries.q2();
for entity in queued_text.entities.drain(..) {
if let Ok((text, style, mut calculated_size)) = query.get_mut(entity) {
let node_size = Size::new(
text_constraint(
style.min_size.width,
style.size.width,
style.max_size.width,
scale_factor,
),
text_constraint(
style.min_size.height,
style.size.height,
style.max_size.height,
scale_factor,
),
);
match text_pipeline.queue_text(
entity,
&fonts,
&text.sections,
scale_factor,
text.alignment,
node_size,
&mut *font_atlas_set_storage,
&mut *texture_atlases,
&mut *textures,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, let's add this entity to the
// queue for further processing
new_queue.push(entity);
}
Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {}.", e);
}
Ok(()) => {
let text_layout_info = text_pipeline.get_glyphs(&entity).expect(
"Failed to get glyphs from the pipeline that have just been computed",
);
calculated_size.size = Size {
width: scale_value(text_layout_info.size.width, inv_scale_factor),
height: scale_value(text_layout_info.size.height, inv_scale_factor),
};
}
}
}
}
queued_text.entities = new_queue;
}