bevy/crates/bevy_text/src/pipeline.rs
James Liu 56bcbb0975
Forbid unsafe in most crates in the engine (#12684)
# Objective
Resolves #3824. `unsafe` code should be the exception, not the norm in
Rust. It's obviously needed for various use cases as it's interfacing
with platforms and essentially running the borrow checker at runtime in
the ECS, but the touted benefits of Bevy is that we are able to heavily
leverage Rust's safety, and we should be holding ourselves accountable
to that by minimizing our unsafe footprint.

## Solution
Deny `unsafe_code` workspace wide. Add explicit exceptions for the
following crates, and forbid it in almost all of the others.

* bevy_ecs - Obvious given how much unsafe is needed to achieve
performant results
* bevy_ptr - Works with raw pointers, even more low level than bevy_ecs.
 * bevy_render - due to needing to integrate with wgpu
 * bevy_window - due to needing to integrate with raw_window_handle
* bevy_utils - Several unsafe utilities used by bevy_ecs. Ideally moved
into bevy_ecs instead of made publicly usable.
 * bevy_reflect - Required for the unsafe type casting it's doing.
 * bevy_transform - for the parallel transform propagation
 * bevy_gizmos  - For the SystemParam impls it has.
* bevy_assets - To support reflection. Might not be required, not 100%
sure yet.
* bevy_mikktspace - due to being a conversion from a C library. Pending
safe rewrite.
* bevy_dynamic_plugin - Inherently unsafe due to the dynamic loading
nature.

Several uses of unsafe were rewritten, as they did not need to be using
them:

* bevy_text - a case of `Option::unchecked` could be rewritten as a
normal for loop and match instead of an iterator.
* bevy_color - the Pod/Zeroable implementations were replaceable with
bytemuck's derive macros.
2024-03-27 03:30:08 +00:00

206 lines
6.5 KiB
Rust

use crate::{
compute_text_bounds, error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font,
FontAtlasSets, JustifyText, PositionedGlyph, Text, TextSection, TextSettings, YAxisOrientation,
};
use ab_glyph::PxScale;
use bevy_asset::{AssetId, Assets, Handle};
use bevy_ecs::component::Component;
use bevy_ecs::prelude::ReflectComponent;
use bevy_ecs::system::Resource;
use bevy_math::Vec2;
use bevy_reflect::prelude::ReflectDefault;
use bevy_reflect::Reflect;
use bevy_render::texture::Image;
use bevy_sprite::TextureAtlasLayout;
use bevy_utils::HashMap;
use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText, ToSectionText};
#[derive(Default, Resource)]
pub struct TextPipeline {
brush: GlyphBrush,
map_font_id: HashMap<AssetId<Font>, FontId>,
}
/// Render information for a corresponding [`Text`] component.
///
/// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`].
#[derive(Component, Clone, Default, Debug, Reflect)]
#[reflect(Component, Default)]
pub struct TextLayoutInfo {
pub glyphs: Vec<PositionedGlyph>,
pub logical_size: Vec2,
}
impl TextPipeline {
pub fn get_or_insert_font_id(&mut self, handle: &Handle<Font>, font: &Font) -> FontId {
let brush = &mut self.brush;
*self
.map_font_id
.entry(handle.id())
.or_insert_with(|| brush.add_font(handle.id(), font.font.clone()))
}
#[allow(clippy::too_many_arguments)]
pub fn queue_text(
&mut self,
fonts: &Assets<Font>,
sections: &[TextSection],
scale_factor: f32,
text_alignment: JustifyText,
linebreak_behavior: BreakLineOn,
bounds: Vec2,
font_atlas_sets: &mut FontAtlasSets,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
text_settings: &TextSettings,
y_axis_orientation: YAxisOrientation,
) -> Result<TextLayoutInfo, TextError> {
let mut scaled_fonts = Vec::with_capacity(sections.len());
let sections = sections
.iter()
.map(|section| {
let font = fonts
.get(&section.style.font)
.ok_or(TextError::NoSuchFont)?;
let font_id = self.get_or_insert_font_id(&section.style.font, font);
let font_size = scale_value(section.style.font_size, scale_factor);
scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size));
let section = SectionText {
font_id,
scale: PxScale::from(font_size),
text: &section.value,
};
Ok(section)
})
.collect::<Result<Vec<_>, _>>()?;
let section_glyphs =
self.brush
.compute_glyphs(&sections, bounds, text_alignment, linebreak_behavior)?;
if section_glyphs.is_empty() {
return Ok(TextLayoutInfo::default());
}
let size = compute_text_bounds(&section_glyphs, |index| scaled_fonts[index]).size();
let glyphs = self.brush.process_glyphs(
section_glyphs,
&sections,
font_atlas_sets,
fonts,
texture_atlases,
textures,
text_settings,
y_axis_orientation,
)?;
Ok(TextLayoutInfo {
glyphs,
logical_size: size,
})
}
}
#[derive(Debug, Clone)]
pub struct TextMeasureSection {
pub text: Box<str>,
pub scale: f32,
pub font_id: FontId,
}
#[derive(Debug, Clone, Default)]
pub struct TextMeasureInfo {
pub fonts: Box<[ab_glyph::FontArc]>,
pub sections: Box<[TextMeasureSection]>,
pub justification: JustifyText,
pub linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker,
pub min: Vec2,
pub max: Vec2,
}
impl TextMeasureInfo {
pub fn from_text(
text: &Text,
fonts: &Assets<Font>,
scale_factor: f32,
) -> Result<TextMeasureInfo, TextError> {
let sections = &text.sections;
let mut auto_fonts = Vec::with_capacity(sections.len());
let mut out_sections = Vec::with_capacity(sections.len());
for (i, section) in sections.iter().enumerate() {
match fonts.get(&section.style.font) {
Some(font) => {
auto_fonts.push(font.font.clone());
out_sections.push(TextMeasureSection {
font_id: FontId(i),
scale: scale_value(section.style.font_size, scale_factor),
text: section.value.clone().into_boxed_str(),
});
}
None => return Err(TextError::NoSuchFont),
}
}
Ok(Self::new(
auto_fonts,
out_sections,
text.justify,
text.linebreak_behavior.into(),
))
}
fn new(
fonts: Vec<ab_glyph::FontArc>,
sections: Vec<TextMeasureSection>,
justification: JustifyText,
linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker,
) -> Self {
let mut info = Self {
fonts: fonts.into_boxed_slice(),
sections: sections.into_boxed_slice(),
justification,
linebreak_behavior,
min: Vec2::ZERO,
max: Vec2::ZERO,
};
let min = info.compute_size(Vec2::new(0.0, f32::INFINITY));
let max = info.compute_size(Vec2::INFINITY);
info.min = min;
info.max = max;
info
}
pub fn compute_size(&self, bounds: Vec2) -> Vec2 {
let sections = &self.sections;
let geom = SectionGeometry {
bounds: (bounds.x, bounds.y),
..Default::default()
};
let section_glyphs = glyph_brush_layout::Layout::default()
.h_align(self.justification.into())
.line_breaker(self.linebreak_behavior)
.calculate_glyphs(&self.fonts, &geom, sections);
compute_text_bounds(&section_glyphs, |index| {
let font = &self.fonts[index];
let font_size = self.sections[index].scale;
ab_glyph::Font::into_scaled(font, font_size)
})
.size()
}
}
impl ToSectionText for TextMeasureSection {
#[inline(always)]
fn to_section_text(&self) -> SectionText<'_> {
SectionText {
text: &self.text,
scale: PxScale::from(self.scale),
font_id: self.font_id,
}
}
}