mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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:
parent
58474d7c4a
commit
25b62f9577
42 changed files with 3809 additions and 13 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
|
92
examples/2d/text2d_pipelined.rs
Normal file
92
examples/2d/text2d_pipelined.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
225
examples/ui/ui_pipelined.rs
Normal 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()
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
33
pipelined/bevy_text2/Cargo.toml
Normal file
33
pipelined/bevy_text2/Cargo.toml
Normal 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"]}
|
10
pipelined/bevy_text2/src/error.rs
Normal file
10
pipelined/bevy_text2/src/error.rs
Normal 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),
|
||||
}
|
46
pipelined/bevy_text2/src/font.rs
Normal file
46
pipelined/bevy_text2/src/font.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
107
pipelined/bevy_text2/src/font_atlas.rs
Normal file
107
pipelined/bevy_text2/src/font_atlas.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
122
pipelined/bevy_text2/src/font_atlas_set.rs
Normal file
122
pipelined/bevy_text2/src/font_atlas_set.rs
Normal 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,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
25
pipelined/bevy_text2/src/font_loader.rs
Normal file
25
pipelined/bevy_text2/src/font_loader.rs
Normal 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"]
|
||||
}
|
||||
}
|
181
pipelined/bevy_text2/src/glyph_brush.rs
Normal file
181
pipelined/bevy_text2/src/glyph_brush.rs
Normal 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
|
||||
}
|
||||
}
|
58
pipelined/bevy_text2/src/lib.rs
Normal file
58
pipelined/bevy_text2/src/lib.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
130
pipelined/bevy_text2/src/pipeline.rs
Normal file
130
pipelined/bevy_text2/src/pipeline.rs
Normal 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(§ion.style.font, font);
|
||||
let font_size = scale_value(section.style.font_size, scale_factor);
|
||||
|
||||
scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size));
|
||||
|
||||
let section = SectionText {
|
||||
font_id,
|
||||
scale: PxScale::from(font_size),
|
||||
text: §ion.value,
|
||||
};
|
||||
|
||||
Ok(section)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let section_glyphs = self
|
||||
.brush
|
||||
.compute_glyphs(§ions, bounds, text_alignment)?;
|
||||
|
||||
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,
|
||||
§ions,
|
||||
font_atlas_set_storage,
|
||||
fonts,
|
||||
texture_atlases,
|
||||
textures,
|
||||
)?;
|
||||
|
||||
self.glyph_map.insert(id, TextLayoutInfo { glyphs, size });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
158
pipelined/bevy_text2/src/text.rs
Normal file
158
pipelined/bevy_text2/src/text.rs
Normal 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,
|
||||
}
|
182
pipelined/bevy_text2/src/text2d.rs
Normal file
182
pipelined/bevy_text2/src/text2d.rs
Normal 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
|
||||
}
|
35
pipelined/bevy_ui2/Cargo.toml
Normal file
35
pipelined/bevy_ui2/Cargo.toml
Normal 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"] }
|
46
pipelined/bevy_ui2/src/anchors.rs
Normal file
46
pipelined/bevy_ui2/src/anchors.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
120
pipelined/bevy_ui2/src/entity.rs
Normal file
120
pipelined/bevy_ui2/src/entity.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
173
pipelined/bevy_ui2/src/flex/convert.rs
Normal file
173
pipelined/bevy_ui2/src/flex/convert.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
293
pipelined/bevy_ui2/src/flex/mod.rs
Normal file
293
pipelined/bevy_ui2/src/flex/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
157
pipelined/bevy_ui2/src/focus.rs
Normal file
157
pipelined/bevy_ui2/src/focus.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
96
pipelined/bevy_ui2/src/lib.rs
Normal file
96
pipelined/bevy_ui2/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
29
pipelined/bevy_ui2/src/margins.rs
Normal file
29
pipelined/bevy_ui2/src/margins.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
14
pipelined/bevy_ui2/src/render/camera.rs
Normal file
14
pipelined/bevy_ui2/src/render/camera.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
409
pipelined/bevy_ui2/src/render/mod.rs
Normal file
409
pipelined/bevy_ui2/src/render/mod.rs
Normal 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),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
132
pipelined/bevy_ui2/src/render/pipeline.rs
Normal file
132
pipelined/bevy_ui2/src/render/pipeline.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
197
pipelined/bevy_ui2/src/render/render_pass.rs
Normal file
197
pipelined/bevy_ui2/src/render/render_pass.rs
Normal 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
|
||||
}
|
||||
}
|
38
pipelined/bevy_ui2/src/render/ui.wgsl
Normal file
38
pipelined/bevy_ui2/src/render/ui.wgsl
Normal 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;
|
||||
}
|
288
pipelined/bevy_ui2/src/ui_node.rs
Normal file
288
pipelined/bevy_ui2/src/ui_node.rs
Normal 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)
|
||||
}
|
||||
}
|
160
pipelined/bevy_ui2/src/update.rs
Normal file
160
pipelined/bevy_ui2/src/update.rs
Normal 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);
|
||||
}
|
||||
}
|
7
pipelined/bevy_ui2/src/widget/button.rs
Normal file
7
pipelined/bevy_ui2/src/widget/button.rs
Normal 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;
|
42
pipelined/bevy_ui2/src/widget/image.rs
Normal file
42
pipelined/bevy_ui2/src/widget/image.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
pipelined/bevy_ui2/src/widget/mod.rs
Normal file
7
pipelined/bevy_ui2/src/widget/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod button;
|
||||
mod image;
|
||||
mod text;
|
||||
|
||||
pub use button::*;
|
||||
pub use image::*;
|
||||
pub use text::*;
|
134
pipelined/bevy_ui2/src/widget/text.rs
Normal file
134
pipelined/bevy_ui2/src/widget/text.rs
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue