From e465080072b1715b07dae85fc206f916c8ff1de4 Mon Sep 17 00:00:00 2001 From: Brice DAVIER Date: Thu, 18 Nov 2021 12:46:00 +0100 Subject: [PATCH 01/19] Copy bevy_ui and bevy_text in pipelined/ --- pipelined/bevy_text2/Cargo.toml | 32 +++ pipelined/bevy_text2/src/draw.rs | 101 +++++++ pipelined/bevy_text2/src/error.rs | 10 + pipelined/bevy_text2/src/font.rs | 39 +++ pipelined/bevy_text2/src/font_atlas.rs | 100 +++++++ pipelined/bevy_text2/src/font_atlas_set.rs | 122 +++++++++ pipelined/bevy_text2/src/font_loader.rs | 25 ++ pipelined/bevy_text2/src/glyph_brush.rs | 181 +++++++++++++ pipelined/bevy_text2/src/lib.rs | 49 ++++ pipelined/bevy_text2/src/pipeline.rs | 130 +++++++++ pipelined/bevy_text2/src/text.rs | 107 ++++++++ pipelined/bevy_text2/src/text2d.rs | 200 ++++++++++++++ pipelined/bevy_ui2/Cargo.toml | 32 +++ pipelined/bevy_ui2/src/anchors.rs | 46 ++++ pipelined/bevy_ui2/src/entity.rs | 196 ++++++++++++++ pipelined/bevy_ui2/src/flex/convert.rs | 173 ++++++++++++ pipelined/bevy_ui2/src/flex/mod.rs | 293 +++++++++++++++++++++ pipelined/bevy_ui2/src/focus.rs | 151 +++++++++++ pipelined/bevy_ui2/src/lib.rs | 89 +++++++ pipelined/bevy_ui2/src/margins.rs | 29 ++ pipelined/bevy_ui2/src/render/mod.rs | 159 +++++++++++ pipelined/bevy_ui2/src/render/ui.frag | 24 ++ pipelined/bevy_ui2/src/render/ui.vert | 24 ++ pipelined/bevy_ui2/src/ui_node.rs | 257 ++++++++++++++++++ pipelined/bevy_ui2/src/update.rs | 156 +++++++++++ pipelined/bevy_ui2/src/widget/button.rs | 2 + pipelined/bevy_ui2/src/widget/image.rs | 43 +++ pipelined/bevy_ui2/src/widget/mod.rs | 7 + pipelined/bevy_ui2/src/widget/text.rs | 184 +++++++++++++ 29 files changed, 2961 insertions(+) create mode 100644 pipelined/bevy_text2/Cargo.toml create mode 100644 pipelined/bevy_text2/src/draw.rs create mode 100644 pipelined/bevy_text2/src/error.rs create mode 100644 pipelined/bevy_text2/src/font.rs create mode 100644 pipelined/bevy_text2/src/font_atlas.rs create mode 100644 pipelined/bevy_text2/src/font_atlas_set.rs create mode 100644 pipelined/bevy_text2/src/font_loader.rs create mode 100644 pipelined/bevy_text2/src/glyph_brush.rs create mode 100644 pipelined/bevy_text2/src/lib.rs create mode 100644 pipelined/bevy_text2/src/pipeline.rs create mode 100644 pipelined/bevy_text2/src/text.rs create mode 100644 pipelined/bevy_text2/src/text2d.rs create mode 100644 pipelined/bevy_ui2/Cargo.toml create mode 100644 pipelined/bevy_ui2/src/anchors.rs create mode 100644 pipelined/bevy_ui2/src/entity.rs create mode 100644 pipelined/bevy_ui2/src/flex/convert.rs create mode 100644 pipelined/bevy_ui2/src/flex/mod.rs create mode 100644 pipelined/bevy_ui2/src/focus.rs create mode 100644 pipelined/bevy_ui2/src/lib.rs create mode 100644 pipelined/bevy_ui2/src/margins.rs create mode 100644 pipelined/bevy_ui2/src/render/mod.rs create mode 100644 pipelined/bevy_ui2/src/render/ui.frag create mode 100644 pipelined/bevy_ui2/src/render/ui.vert create mode 100644 pipelined/bevy_ui2/src/ui_node.rs create mode 100644 pipelined/bevy_ui2/src/update.rs create mode 100644 pipelined/bevy_ui2/src/widget/button.rs create mode 100644 pipelined/bevy_ui2/src/widget/image.rs create mode 100644 pipelined/bevy_ui2/src/widget/mod.rs create mode 100644 pipelined/bevy_ui2/src/widget/text.rs diff --git a/pipelined/bevy_text2/Cargo.toml b/pipelined/bevy_text2/Cargo.toml new file mode 100644 index 0000000000000..613e50f93be40 --- /dev/null +++ b/pipelined/bevy_text2/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "bevy_text" +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 = "../bevy_app", version = "0.5.0" } +bevy_asset = { path = "../bevy_asset", version = "0.5.0" } +bevy_core = { path = "../bevy_core", version = "0.5.0" } +bevy_ecs = { path = "../bevy_ecs", version = "0.5.0" } +bevy_math = { path = "../bevy_math", version = "0.5.0" } +bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] } +bevy_render = { path = "../bevy_render", version = "0.5.0" } +bevy_sprite = { path = "../bevy_sprite", version = "0.5.0" } +bevy_transform = { path = "../bevy_transform", version = "0.5.0" } +bevy_window = { path = "../bevy_window", version = "0.5.0" } +bevy_utils = { path = "../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" diff --git a/pipelined/bevy_text2/src/draw.rs b/pipelined/bevy_text2/src/draw.rs new file mode 100644 index 0000000000000..e4dd639d37598 --- /dev/null +++ b/pipelined/bevy_text2/src/draw.rs @@ -0,0 +1,101 @@ +use crate::{PositionedGlyph, TextSection}; +use bevy_math::{Mat4, Vec3}; +use bevy_render::pipeline::IndexFormat; +use bevy_render::{ + draw::{Draw, DrawContext, DrawError, Drawable}, + mesh, + mesh::Mesh, + pipeline::{PipelineSpecialization, VertexBufferLayout}, + prelude::Msaa, + renderer::{BindGroup, RenderResourceBindings, RenderResourceId}, +}; +use bevy_sprite::TextureAtlasSprite; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::tracing::error; + +pub struct DrawableText<'a> { + pub render_resource_bindings: &'a mut RenderResourceBindings, + pub global_transform: GlobalTransform, + pub scale_factor: f32, + pub sections: &'a [TextSection], + pub text_glyphs: &'a Vec, + pub msaa: &'a Msaa, + pub font_quad_vertex_layout: &'a VertexBufferLayout, + pub alignment_offset: Vec3, +} + +impl<'a> Drawable for DrawableText<'a> { + fn draw(&mut self, draw: &mut Draw, context: &mut DrawContext) -> Result<(), DrawError> { + context.set_pipeline( + draw, + &bevy_sprite::SPRITE_SHEET_PIPELINE_HANDLE.typed(), + &PipelineSpecialization { + sample_count: self.msaa.samples, + vertex_buffer_layout: self.font_quad_vertex_layout.clone(), + ..Default::default() + }, + )?; + + let render_resource_context = &**context.render_resource_context; + + if let Some(RenderResourceId::Buffer(vertex_attribute_buffer_id)) = render_resource_context + .get_asset_resource( + &bevy_sprite::QUAD_HANDLE.typed::(), + mesh::VERTEX_ATTRIBUTE_BUFFER_ID, + ) + { + draw.set_vertex_buffer(0, vertex_attribute_buffer_id, 0); + } else { + error!("Could not find vertex buffer for `bevy_sprite::QUAD_HANDLE`.") + } + + let mut indices = 0..0; + if let Some(RenderResourceId::Buffer(quad_index_buffer)) = render_resource_context + .get_asset_resource( + &bevy_sprite::QUAD_HANDLE.typed::(), + mesh::INDEX_BUFFER_ASSET_INDEX, + ) + { + draw.set_index_buffer(quad_index_buffer, 0, IndexFormat::Uint32); + if let Some(buffer_info) = render_resource_context.get_buffer_info(quad_index_buffer) { + indices = 0..(buffer_info.size / 4) as u32; + } else { + panic!("Expected buffer type."); + } + } + + // set global bindings + context.set_bind_groups_from_bindings(draw, &mut [self.render_resource_bindings])?; + + for tv in self.text_glyphs { + context.set_asset_bind_groups(draw, &tv.atlas_info.texture_atlas)?; + + let sprite = TextureAtlasSprite { + index: tv.atlas_info.glyph_index, + color: self.sections[tv.section_index].style.color, + flip_x: false, + flip_y: false, + }; + + let transform = Mat4::from_rotation_translation( + self.global_transform.rotation, + self.global_transform.translation, + ) * Mat4::from_scale(self.global_transform.scale / self.scale_factor) + * Mat4::from_translation( + self.alignment_offset * self.scale_factor + tv.position.extend(0.), + ); + + let transform_buffer = context.get_uniform_buffer(&transform).unwrap(); + let sprite_buffer = context.get_uniform_buffer(&sprite).unwrap(); + let sprite_bind_group = BindGroup::build() + .add_binding(0, transform_buffer) + .add_binding(1, sprite_buffer) + .finish(); + context.create_bind_group_resource(2, &sprite_bind_group)?; + draw.set_bind_group(2, &sprite_bind_group); + draw.draw_indexed(indices.clone(), 0, 0..1); + } + + Ok(()) + } +} diff --git a/pipelined/bevy_text2/src/error.rs b/pipelined/bevy_text2/src/error.rs new file mode 100644 index 0000000000000..1bb7cf1253581 --- /dev/null +++ b/pipelined/bevy_text2/src/error.rs @@ -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), +} diff --git a/pipelined/bevy_text2/src/font.rs b/pipelined/bevy_text2/src/font.rs new file mode 100644 index 0000000000000..320751fc77b0a --- /dev/null +++ b/pipelined/bevy_text2/src/font.rs @@ -0,0 +1,39 @@ +use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph}; +use bevy_reflect::TypeUuid; +use bevy_render::texture::{Extent3d, Texture, TextureDimension, TextureFormat}; + +#[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) -> Result { + 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) -> Texture { + 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 + Texture::new( + Extent3d::new(width as u32, height as u32, 1), + TextureDimension::D2, + alpha + .iter() + .map(|a| vec![255, 255, 255, (*a * 255.0) as u8]) + .flatten() + .collect::>(), + TextureFormat::Rgba8UnormSrgb, + ) + } +} diff --git a/pipelined/bevy_text2/src/font_atlas.rs b/pipelined/bevy_text2/src/font_atlas.rs new file mode 100644 index 0000000000000..bcfa7936a47ab --- /dev/null +++ b/pipelined/bevy_text2/src/font_atlas.rs @@ -0,0 +1,100 @@ +use ab_glyph::{GlyphId, Point}; +use bevy_asset::{Assets, Handle}; +use bevy_math::Vec2; +use bevy_render::texture::{Extent3d, Texture, TextureDimension, TextureFormat}; +use bevy_sprite::{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 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 for SubpixelOffset { + fn from(_: Point) -> Self { + Self + } +} + +pub struct FontAtlas { + pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, + pub glyph_to_atlas_index: HashMap<(GlyphId, SubpixelOffset), u32>, + pub texture_atlas: Handle, +} + +impl FontAtlas { + pub fn new( + textures: &mut Assets, + texture_atlases: &mut Assets, + size: Vec2, + ) -> FontAtlas { + let atlas_texture = textures.add(Texture::new_fill( + Extent3d::new(size.x as u32, size.y as u32, 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 { + 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, + texture_atlases: &mut Assets, + glyph_id: GlyphId, + subpixel_offset: SubpixelOffset, + texture: &Texture, + ) -> 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 + } + } +} diff --git a/pipelined/bevy_text2/src/font_atlas_set.rs b/pipelined/bevy_text2/src/font_atlas_set.rs new file mode 100644 index 0000000000000..c073ab31ca3c3 --- /dev/null +++ b/pipelined/bevy_text2/src/font_atlas_set.rs @@ -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_render::texture::Texture; +use bevy_sprite::TextureAtlas; +use bevy_utils::HashMap; + +type FontSizeKey = FloatOrd; + +#[derive(TypeUuid)] +#[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"] +pub struct FontAtlasSet { + font_atlases: HashMap>, +} + +#[derive(Debug, Clone)] +pub struct GlyphAtlasInfo { + pub texture_atlas: Handle, + pub glyph_index: u32, +} + +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)> { + 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, + textures: &mut Assets, + outlined_glyph: OutlinedGlyph, + ) -> Result { + 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 { + 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, + }) + }) + } +} diff --git a/pipelined/bevy_text2/src/font_loader.rs b/pipelined/bevy_text2/src/font_loader.rs new file mode 100644 index 0000000000000..e179ec9ccf82e --- /dev/null +++ b/pipelined/bevy_text2/src/font_loader.rs @@ -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"] + } +} diff --git a/pipelined/bevy_text2/src/glyph_brush.rs b/pipelined/bevy_text2/src/glyph_brush.rs new file mode 100644 index 0000000000000..30557f20ab8b8 --- /dev/null +++ b/pipelined/bevy_text2/src/glyph_brush.rs @@ -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_render::prelude::Texture; +use bevy_sprite::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, + handles: Vec>, + 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( + &self, + sections: &[S], + bounds: Size, + text_alignment: TextAlignment, + ) -> Result, TextError> { + let geom = SectionGeometry { + bounds: (bounds.width, bounds.height), + ..Default::default() + }; + let section_glyphs = Layout::default() + .h_align(text_alignment.horizontal) + .v_align(text_alignment.vertical) + .calculate_glyphs(&self.fonts, &geom, sections); + Ok(section_glyphs) + } + + pub fn process_glyphs( + &self, + glyphs: Vec, + sections: &[SectionText], + font_atlas_set_storage: &mut Assets, + fonts: &Assets, + texture_atlases: &mut Assets, + textures: &mut Assets, + ) -> Result, 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::, _>>()?; + + 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 = 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: 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 + } +} diff --git a/pipelined/bevy_text2/src/lib.rs b/pipelined/bevy_text2/src/lib.rs new file mode 100644 index 0000000000000..ce91d4a2e8ba3 --- /dev/null +++ b/pipelined/bevy_text2/src/lib.rs @@ -0,0 +1,49 @@ +mod draw; +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 draw::*; +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, Text, Text2dBundle, TextAlignment, TextError, TextSection, TextStyle}; + #[doc(hidden)] + pub use glyph_brush_layout::{HorizontalAlign, VerticalAlign}; +} + +use bevy_app::prelude::*; +use bevy_asset::AddAsset; +use bevy_ecs::entity::Entity; +use bevy_render::RenderStage; + +pub type DefaultTextPipeline = TextPipeline; + +#[derive(Default)] +pub struct TextPlugin; + +impl Plugin for TextPlugin { + fn build(&self, app: &mut App) { + app.add_asset::() + .add_asset::() + .init_asset_loader::() + .insert_resource(DefaultTextPipeline::default()) + .add_system_to_stage(CoreStage::PostUpdate, text2d_system) + .add_system_to_stage(RenderStage::Draw, text2d::draw_text2d_system); + } +} diff --git a/pipelined/bevy_text2/src/pipeline.rs b/pipelined/bevy_text2/src/pipeline.rs new file mode 100644 index 0000000000000..87cbb48816121 --- /dev/null +++ b/pipelined/bevy_text2/src/pipeline.rs @@ -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_render::prelude::Texture; +use bevy_sprite::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 { + brush: GlyphBrush, + glyph_map: HashMap, + map_font_id: HashMap, +} + +impl Default for TextPipeline { + fn default() -> Self { + TextPipeline { + brush: GlyphBrush::default(), + glyph_map: Default::default(), + map_font_id: Default::default(), + } + } +} + +pub struct TextLayoutInfo { + pub glyphs: Vec, + pub size: Size, +} + +impl TextPipeline { + pub fn get_or_insert_font_id(&mut self, handle: &Handle, 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, + sections: &[TextSection], + scale_factor: f64, + text_alignment: TextAlignment, + bounds: Size, + font_atlas_set_storage: &mut Assets, + texture_atlases: &mut Assets, + textures: &mut Assets, + ) -> 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::, _>>()?; + + 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(()) + } +} diff --git a/pipelined/bevy_text2/src/text.rs b/pipelined/bevy_text2/src/text.rs new file mode 100644 index 0000000000000..b077c2821574c --- /dev/null +++ b/pipelined/bevy_text2/src/text.rs @@ -0,0 +1,107 @@ +use bevy_asset::Handle; +use bevy_math::Size; +use bevy_render::color::Color; +use glyph_brush_layout::{HorizontalAlign, VerticalAlign}; + +use crate::Font; + +#[derive(Debug, Default, Clone)] +pub struct Text { + pub sections: Vec, + pub alignment: TextAlignment, +} + +impl Text { + /// Constructs a [`Text`] with (initially) one section. + /// + /// ``` + /// # use bevy_asset::{AssetServer, Handle}; + /// # use bevy_render::color::Color; + /// # use bevy_text::{Font, Text, TextAlignment, TextStyle}; + /// # use glyph_brush_layout::{HorizontalAlign, VerticalAlign}; + /// # + /// # let font_handle: Handle = 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>( + value: S, + style: TextStyle, + alignment: TextAlignment, + ) -> Self { + Self { + sections: vec![TextSection { + value: value.into(), + style, + }], + alignment, + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct TextSection { + pub value: String, + pub style: TextStyle, +} + +#[derive(Debug, Clone, Copy)] +pub struct TextAlignment { + pub vertical: VerticalAlign, + pub horizontal: HorizontalAlign, +} + +impl Default for TextAlignment { + fn default() -> Self { + TextAlignment { + vertical: VerticalAlign::Top, + horizontal: HorizontalAlign::Left, + } + } +} + +#[derive(Clone, Debug)] +pub struct TextStyle { + pub font: Handle, + 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(Default, Copy, Clone, Debug)] +pub struct Text2dSize { + pub size: Size, +} diff --git a/pipelined/bevy_text2/src/text2d.rs b/pipelined/bevy_text2/src/text2d.rs new file mode 100644 index 0000000000000..88dc89575c381 --- /dev/null +++ b/pipelined/bevy_text2/src/text2d.rs @@ -0,0 +1,200 @@ +use bevy_asset::Assets; +use bevy_ecs::{ + bundle::Bundle, + entity::Entity, + query::{Changed, QueryState, With, Without}, + system::{Local, Query, QuerySet, Res, ResMut}, +}; +use bevy_math::{Size, Vec3}; +use bevy_render::{ + draw::{DrawContext, Drawable, OutsideFrustum}, + mesh::Mesh, + prelude::{Draw, Msaa, Texture, Visible}, + render_graph::base::MainPass, + renderer::RenderResourceBindings, +}; +use bevy_sprite::{TextureAtlas, QUAD_HANDLE}; +use bevy_transform::prelude::{GlobalTransform, Transform}; +use bevy_window::Windows; +use glyph_brush_layout::{HorizontalAlign, VerticalAlign}; + +use crate::{DefaultTextPipeline, DrawableText, Font, FontAtlasSet, Text, Text2dSize, TextError}; + +/// 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 draw: Draw, + pub visible: Visible, + pub text: Text, + pub transform: Transform, + pub global_transform: GlobalTransform, + pub main_pass: MainPass, + pub text_2d_size: Text2dSize, +} + +impl Default for Text2dBundle { + fn default() -> Self { + Self { + draw: Draw { + ..Default::default() + }, + visible: Visible { + is_transparent: true, + ..Default::default() + }, + text: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + main_pass: MainPass {}, + text_2d_size: Text2dSize { + size: Size::default(), + }, + } + } +} + +/// System for drawing text in a 2D scene via a 2D `OrthographicCameraBundle`. Included in the +/// default `TextPlugin`. Position is determined by the `Transform`'s translation, though scale and +/// rotation are ignored. +#[allow(clippy::type_complexity)] +pub fn draw_text2d_system( + mut context: DrawContext, + msaa: Res, + meshes: Res>, + windows: Res, + mut render_resource_bindings: ResMut, + text_pipeline: Res, + mut query: Query< + ( + Entity, + &mut Draw, + &Visible, + &Text, + &GlobalTransform, + &Text2dSize, + ), + (With, Without), + >, +) { + let font_quad = meshes.get(&QUAD_HANDLE).unwrap(); + let font_quad_vertex_layout = font_quad.get_vertex_buffer_layout(); + + let scale_factor = if let Some(window) = windows.get_primary() { + window.scale_factor() as f32 + } else { + 1. + }; + + for (entity, mut draw, visible, text, global_transform, calculated_size) in query.iter_mut() { + if !visible.is_visible { + continue; + } + + let (width, height) = (calculated_size.size.width, calculated_size.size.height); + + if let Some(text_glyphs) = text_pipeline.get_glyphs(&entity) { + 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), + }; + + let mut drawable_text = DrawableText { + render_resource_bindings: &mut render_resource_bindings, + global_transform: *global_transform, + scale_factor, + msaa: &msaa, + text_glyphs: &text_glyphs.glyphs, + font_quad_vertex_layout: &font_quad_vertex_layout, + sections: &text.sections, + alignment_offset, + }; + + drawable_text.draw(&mut draw, &mut context).unwrap(); + } + } +} + +#[derive(Debug, Default)] +pub struct QueuedText2d { + entities: Vec, +} + +/// 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, + mut textures: ResMut>, + fonts: Res>, + windows: Res, + mut texture_atlases: ResMut>, + mut font_atlas_set_storage: ResMut>, + mut text_pipeline: ResMut, + mut text_queries: QuerySet<( + QueryState, Changed)>, + QueryState<(&Text, &mut Text2dSize), With>, + )>, +) { + // 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 +} diff --git a/pipelined/bevy_ui2/Cargo.toml b/pipelined/bevy_ui2/Cargo.toml new file mode 100644 index 0000000000000..07f23d03da96f --- /dev/null +++ b/pipelined/bevy_ui2/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "bevy_ui" +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 = "../bevy_app", version = "0.5.0" } +bevy_asset = { path = "../bevy_asset", version = "0.5.0" } +bevy_core = { path = "../bevy_core", version = "0.5.0" } +bevy_derive = { path = "../bevy_derive", version = "0.5.0" } +bevy_ecs = { path = "../bevy_ecs", version = "0.5.0" } +bevy_input = { path = "../bevy_input", version = "0.5.0" } +bevy_log = { path = "../bevy_log", version = "0.5.0" } +bevy_math = { path = "../bevy_math", version = "0.5.0" } +bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] } +bevy_render = { path = "../bevy_render", version = "0.5.0" } +bevy_sprite = { path = "../bevy_sprite", version = "0.5.0" } +bevy_text = { path = "../bevy_text", version = "0.5.0" } +bevy_transform = { path = "../bevy_transform", version = "0.5.0" } +bevy_window = { path = "../bevy_window", version = "0.5.0" } +bevy_utils = { path = "../bevy_utils", version = "0.5.0" } + +# other +stretch = "0.3.2" +serde = {version = "1", features = ["derive"]} +smallvec = { version = "1.6", features = ["union", "const_generics"] } diff --git a/pipelined/bevy_ui2/src/anchors.rs b/pipelined/bevy_ui2/src/anchors.rs new file mode 100644 index 0000000000000..0e3c5c5274c2b --- /dev/null +++ b/pipelined/bevy_ui2/src/anchors.rs @@ -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, + } + } +} diff --git a/pipelined/bevy_ui2/src/entity.rs b/pipelined/bevy_ui2/src/entity.rs new file mode 100644 index 0000000000000..d7312b4baf10a --- /dev/null +++ b/pipelined/bevy_ui2/src/entity.rs @@ -0,0 +1,196 @@ +use super::Node; +use crate::{ + render::UI_PIPELINE_HANDLE, + widget::{Button, Image}, + CalculatedSize, FocusPolicy, Interaction, Style, +}; +use bevy_asset::Handle; +use bevy_ecs::bundle::Bundle; +use bevy_render::{ + camera::{Camera, DepthCalculation, OrthographicProjection, VisibleEntities, WindowOrigin}, + draw::Draw, + mesh::Mesh, + pipeline::{RenderPipeline, RenderPipelines}, + prelude::Visible, +}; +use bevy_sprite::{ColorMaterial, QUAD_HANDLE}; +use bevy_text::Text; +use bevy_transform::prelude::{GlobalTransform, Transform}; + +#[derive(Bundle, Clone, Debug)] +pub struct NodeBundle { + pub node: Node, + pub style: Style, + pub mesh: Handle, // TODO: maybe abstract this out + pub material: Handle, + pub draw: Draw, + pub visible: Visible, + pub render_pipelines: RenderPipelines, + pub transform: Transform, + pub global_transform: GlobalTransform, +} + +impl Default for NodeBundle { + fn default() -> Self { + NodeBundle { + mesh: QUAD_HANDLE.typed(), + render_pipelines: RenderPipelines::from_pipelines(vec![RenderPipeline::new( + UI_PIPELINE_HANDLE.typed(), + )]), + visible: Visible { + is_transparent: true, + ..Default::default() + }, + node: Default::default(), + style: Default::default(), + material: Default::default(), + draw: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + } + } +} + +#[derive(Bundle, Clone, Debug)] +pub struct ImageBundle { + pub node: Node, + pub style: Style, + pub image: Image, + pub calculated_size: CalculatedSize, + pub mesh: Handle, // TODO: maybe abstract this out + pub material: Handle, + pub draw: Draw, + pub visible: Visible, + pub render_pipelines: RenderPipelines, + pub transform: Transform, + pub global_transform: GlobalTransform, +} + +impl Default for ImageBundle { + fn default() -> Self { + ImageBundle { + mesh: QUAD_HANDLE.typed(), + render_pipelines: RenderPipelines::from_pipelines(vec![RenderPipeline::new( + UI_PIPELINE_HANDLE.typed(), + )]), + node: Default::default(), + image: Default::default(), + calculated_size: Default::default(), + style: Default::default(), + material: Default::default(), + draw: Default::default(), + visible: Visible { + is_transparent: true, + ..Default::default() + }, + transform: Default::default(), + global_transform: Default::default(), + } + } +} + +#[derive(Bundle, Clone, Debug)] +pub struct TextBundle { + pub node: Node, + pub style: Style, + pub draw: Draw, + pub visible: Visible, + 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, + draw: Draw { + ..Default::default() + }, + visible: Visible { + is_transparent: true, + ..Default::default() + }, + 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 mesh: Handle, // TODO: maybe abstract this out + pub material: Handle, + pub draw: Draw, + pub visible: Visible, + pub render_pipelines: RenderPipelines, + pub transform: Transform, + pub global_transform: GlobalTransform, +} + +impl Default for ButtonBundle { + fn default() -> Self { + ButtonBundle { + button: Button, + mesh: QUAD_HANDLE.typed(), + render_pipelines: RenderPipelines::from_pipelines(vec![RenderPipeline::new( + UI_PIPELINE_HANDLE.typed(), + )]), + interaction: Default::default(), + focus_policy: Default::default(), + node: Default::default(), + style: Default::default(), + material: Default::default(), + draw: Default::default(), + visible: Visible { + is_transparent: true, + ..Default::default() + }, + transform: Default::default(), + global_transform: Default::default(), + } + } +} + +#[derive(Bundle, Debug)] +pub struct UiCameraBundle { + pub camera: Camera, + pub orthographic_projection: OrthographicProjection, + pub visible_entities: VisibleEntities, + pub transform: Transform, + pub global_transform: GlobalTransform, +} + +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(crate::camera::CAMERA_UI.to_string()), + ..Default::default() + }, + orthographic_projection: OrthographicProjection { + far, + window_origin: WindowOrigin::BottomLeft, + depth_calculation: DepthCalculation::ZDifference, + ..Default::default() + }, + visible_entities: Default::default(), + transform: Transform::from_xyz(0.0, 0.0, far - 0.1), + global_transform: Default::default(), + } + } +} diff --git a/pipelined/bevy_ui2/src/flex/convert.rs b/pipelined/bevy_ui2/src/flex/convert.rs new file mode 100644 index 0000000000000..10e6839134349 --- /dev/null +++ b/pipelined/bevy_ui2/src/flex/convert.rs @@ -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, +) -> stretch::geometry::Rect { + 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) -> stretch::geometry::Size { + 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, +) -> stretch::geometry::Size { + 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 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 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 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 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 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 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 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 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 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, + } + } +} diff --git a/pipelined/bevy_ui2/src/flex/mod.rs b/pipelined/bevy_ui2/src/flex/mod.rs new file mode 100644 index 0000000000000..0bb7a573d302a --- /dev/null +++ b/pipelined/bevy_ui2/src/flex/mod.rs @@ -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, + window_nodes: HashMap, + 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() {} + _assert_send_sync::>(); + _assert_send_sync::>(); + // FIXME https://github.com/vislyhq/stretch/issues/69 + // _assert_send_sync::(); +} + +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| { + 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, + ) { + let stretch_node = self.window_nodes.get(&window_id).unwrap(); + let child_nodes = children + .map(|e| *self.entity_to_stretch.get(&e).unwrap()) + .collect::>(); + 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, + mut scale_factor_events: EventReader, + mut flex_surface: ResMut, + root_node_query: Query, Without)>, + node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With, Changed