use alloc::sync::Arc;
use bevy_asset::{AssetId, Assets};
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
reflect::ReflectComponent,
resource::Resource,
system::{Query, ResMut},
};
use bevy_image::prelude::*;
use bevy_log::{once, warn};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use crate::{
add_glyph_to_atlas, error::TextError, get_glyph_atlas_info, ComputedTextBlock, Font,
FontAtlasKey, FontAtlasSet, FontHinting, FontSmoothing, Justify, LineBreak, LineHeight,
PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout,
};
use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};
#[derive(Resource, Deref, DerefMut)]
pub struct CosmicFontSystem(pub cosmic_text::FontSystem);
impl Default for CosmicFontSystem {
fn default() -> Self {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
let db = cosmic_text::fontdb::Database::new();
Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db))
}
}
#[derive(Resource)]
pub struct SwashCache(pub cosmic_text::SwashCache);
impl Default for SwashCache {
fn default() -> Self {
Self(cosmic_text::SwashCache::new())
}
}
#[derive(Clone)]
pub struct FontFaceInfo {
pub stretch: cosmic_text::fontdb::Stretch,
pub style: cosmic_text::fontdb::Style,
pub family_name: Arc<str>,
}
#[derive(Default, Resource)]
pub struct TextPipeline {
pub map_handle_to_font_id: HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
spans_buffer: Vec<(
usize,
&'static str,
&'static TextFont,
FontFaceInfo,
LineHeight,
)>,
glyph_info: Vec<(AssetId<Font>, FontSmoothing, f32, f32, f32, f32, u16)>,
}
impl TextPipeline {
pub fn update_buffer<'a>(
&mut self,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color, LineHeight)>,
linebreak: LineBreak,
justify: Justify,
bounds: TextBounds,
scale_factor: f64,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
hinting: FontHinting,
) -> Result<(), TextError> {
computed.needs_rerender = false;
let font_system = &mut font_system.0;
let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color, LineHeight)> =
core::mem::take(&mut self.spans_buffer)
.into_iter()
.map(
|_| -> (usize, &str, &TextFont, FontFaceInfo, Color, LineHeight) {
unreachable!()
},
)
.collect();
computed.entities.clear();
for (span_index, (entity, depth, span, text_font, color, line_height)) in
text_spans.enumerate()
{
computed.entities.push(TextEntity { entity, depth });
if span.is_empty() {
continue;
}
if !fonts.contains(text_font.font.id()) {
spans.clear();
self.spans_buffer = spans
.into_iter()
.map(
|_| -> (
usize,
&'static str,
&'static TextFont,
FontFaceInfo,
LineHeight,
) { unreachable!() },
)
.collect();
return Err(TextError::NoSuchFont);
}
let face_info = load_font_to_fontdb(
text_font,
font_system,
&mut self.map_handle_to_font_id,
fonts,
);
if scale_factor <= 0.0 || text_font.font_size <= 0.0 {
once!(warn!(
"Text span {entity} has a font size <= 0.0. Nothing will be displayed.",
));
continue;
}
spans.push((span_index, span, text_font, face_info, color, line_height));
}
let spans_iter = spans.iter().map(
|(span_index, span, text_font, font_info, color, line_height)| {
(
*span,
get_attrs(
*span_index,
text_font,
*line_height,
*color,
font_info,
scale_factor,
),
)
},
);
let buffer = &mut computed.buffer;
buffer.set_hinting(font_system, hinting.into());
buffer.set_wrap(
font_system,
match linebreak {
LineBreak::WordBoundary => Wrap::Word,
LineBreak::AnyCharacter => Wrap::Glyph,
LineBreak::WordOrCharacter => Wrap::WordOrGlyph,
LineBreak::NoWrap => Wrap::None,
},
);
buffer.set_rich_text(
font_system,
spans_iter,
&Attrs::new(),
Shaping::Advanced,
Some(justify.into()),
);
let width = (bounds.width.is_none() && justify != Justify::Left)
.then(|| buffer_dimensions(buffer).x)
.or(bounds.width);
buffer.set_size(font_system, width, bounds.height);
spans.clear();
self.spans_buffer = spans
.into_iter()
.map(
|_| -> (
usize,
&'static str,
&'static TextFont,
FontFaceInfo,
LineHeight,
) { unreachable!() },
)
.collect();
Ok(())
}
pub fn create_text_measure<'a>(
&mut self,
entity: Entity,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color, LineHeight)>,
scale_factor: f64,
layout: &TextLayout,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
hinting: FontHinting,
) -> Result<TextMeasureInfo, TextError> {
const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0);
computed.needs_rerender = false;
self.update_buffer(
fonts,
text_spans,
layout.linebreak,
layout.justify,
MIN_WIDTH_CONTENT_BOUNDS,
scale_factor,
computed,
font_system,
hinting,
)?;
let buffer = &mut computed.buffer;
let min_width_content_size = buffer_dimensions(buffer);
let max_width_content_size = {
let font_system = &mut font_system.0;
buffer.set_size(font_system, None, None);
buffer_dimensions(buffer)
};
Ok(TextMeasureInfo {
min: min_width_content_size,
max: max_width_content_size,
entity,
})
}
pub fn get_font_id(&self, asset_id: AssetId<Font>) -> Option<cosmic_text::fontdb::ID> {
self.map_handle_to_font_id
.get(&asset_id)
.cloned()
.map(|(id, _)| id)
}
pub fn update_text_layout_info<'a>(
&mut self,
layout_info: &mut TextLayoutInfo,
text_font_query: Query<&'a TextFont>,
scale_factor: f64,
font_atlas_set: &mut FontAtlasSet,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
swash_cache: &mut SwashCache,
bounds: TextBounds,
justify: Justify,
) -> Result<(), TextError> {
computed.needs_rerender = false;
layout_info.glyphs.clear();
layout_info.run_geometry.clear();
layout_info.size = Default::default();
self.glyph_info.clear();
for text_font in text_font_query.iter_many(computed.entities.iter().map(|e| e.entity)) {
let mut section_info = (
text_font.font.id(),
text_font.font_smoothing,
text_font.font_size,
0.0,
0.0,
0.0,
text_font.weight.clamp().0,
);
if let Some((id, _)) = self.map_handle_to_font_id.get(§ion_info.0)
&& let Some(font) = font_system.get_font(*id, cosmic_text::Weight(section_info.6))
{
let swash = font.as_swash();
let metrics = swash.metrics(&[]);
let upem = metrics.units_per_em as f32;
let scalar = section_info.2 * scale_factor as f32 / upem;
section_info.3 = (metrics.strikeout_offset * scalar).round();
section_info.4 = (metrics.stroke_size * scalar).round().max(1.);
section_info.5 = (metrics.underline_offset * scalar).round();
}
self.glyph_info.push(section_info);
}
let buffer = &mut computed.buffer;
let width = (bounds.width.is_none() && justify != Justify::Left)
.then(|| buffer_dimensions(buffer).x)
.or(bounds.width);
buffer.set_size(font_system, width, bounds.height);
let mut box_size = Vec2::ZERO;
let result = buffer.layout_runs().try_for_each(|run| {
box_size.x = box_size.x.max(run.line_w);
box_size.y += run.line_height;
let mut current_section: Option<usize> = None;
let mut start = 0.;
let mut end = 0.;
let result = run
.glyphs
.iter()
.map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i))
.try_for_each(|(layout_glyph, line_y, line_i)| {
match current_section {
Some(section) => {
if section != layout_glyph.metadata {
layout_info.run_geometry.push(RunGeometry {
span_index: section,
bounds: Rect::new(
start,
run.line_top,
end,
run.line_top + run.line_height,
),
strikethrough_y: (run.line_y - self.glyph_info[section].3)
.round(),
strikethrough_thickness: self.glyph_info[section].4,
underline_y: (run.line_y - self.glyph_info[section].5).round(),
underline_thickness: self.glyph_info[section].4,
});
start = end.max(layout_glyph.x);
current_section = Some(layout_glyph.metadata);
}
end = layout_glyph.x + layout_glyph.w;
}
None => {
current_section = Some(layout_glyph.metadata);
start = layout_glyph.x;
end = start + layout_glyph.w;
}
}
let mut temp_glyph;
let span_index = layout_glyph.metadata;
let font_id = self.glyph_info[span_index].0;
let font_smoothing = self.glyph_info[span_index].1;
let layout_glyph = if font_smoothing == FontSmoothing::None {
temp_glyph = layout_glyph.clone();
temp_glyph.x = temp_glyph.x.round();
temp_glyph.y = temp_glyph.y.round();
temp_glyph.w = temp_glyph.w.round();
temp_glyph.x_offset = temp_glyph.x_offset.round();
temp_glyph.y_offset = temp_glyph.y_offset.round();
temp_glyph.line_height_opt = temp_glyph.line_height_opt.map(f32::round);
&temp_glyph
} else {
layout_glyph
};
let physical_glyph = layout_glyph.physical((0., 0.), 1.);
let font_atlases = font_atlas_set
.entry(FontAtlasKey(
font_id,
physical_glyph.cache_key.font_size_bits,
font_smoothing,
))
.or_default();
let atlas_info = get_glyph_atlas_info(font_atlases, physical_glyph.cache_key)
.map(Ok)
.unwrap_or_else(|| {
add_glyph_to_atlas(
font_atlases,
texture_atlases,
textures,
&mut font_system.0,
&mut swash_cache.0,
layout_glyph,
font_smoothing,
)
})?;
let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap();
let location = atlas_info.location;
let glyph_rect = texture_atlas.textures[location.glyph_index];
let left = location.offset.x as f32;
let top = location.offset.y as f32;
let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height());
let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32;
let y =
line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0;
let position = Vec2::new(x, y);
let pos_glyph = PositionedGlyph {
position,
size: glyph_size.as_vec2(),
atlas_info,
span_index,
byte_index: layout_glyph.start,
byte_length: layout_glyph.end - layout_glyph.start,
line_index: line_i,
};
layout_info.glyphs.push(pos_glyph);
Ok(())
});
if let Some(section) = current_section {
layout_info.run_geometry.push(RunGeometry {
span_index: section,
bounds: Rect::new(start, run.line_top, end, run.line_top + run.line_height),
strikethrough_y: (run.line_y - self.glyph_info[section].3).round(),
strikethrough_thickness: self.glyph_info[section].4,
underline_y: (run.line_y - self.glyph_info[section].5).round(),
underline_thickness: self.glyph_info[section].4,
});
}
result
});
result?;
layout_info.size = box_size.ceil();
Ok(())
}
}
#[derive(Component, Clone, Default, Debug, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct TextLayoutInfo {
pub scale_factor: f32,
pub glyphs: Vec<PositionedGlyph>,
pub run_geometry: Vec<RunGeometry>,
pub size: Vec2,
}
impl TextLayoutInfo {
pub fn clear(&mut self) {
self.scale_factor = 1.;
self.glyphs.clear();
self.run_geometry.clear();
self.size = Vec2::ZERO;
}
}
#[derive(Default, Debug, Clone, Reflect)]
pub struct RunGeometry {
pub span_index: usize,
pub bounds: Rect,
pub strikethrough_y: f32,
pub strikethrough_thickness: f32,
pub underline_y: f32,
pub underline_thickness: f32,
}
impl RunGeometry {
pub fn strikethrough_position(&self) -> Vec2 {
Vec2::new(
self.bounds.center().x,
self.strikethrough_y + 0.5 * self.strikethrough_thickness,
)
}
pub fn strikethrough_size(&self) -> Vec2 {
Vec2::new(self.bounds.size().x, self.strikethrough_thickness)
}
pub fn underline_position(&self) -> Vec2 {
Vec2::new(
self.bounds.center().x,
self.underline_y + 0.5 * self.underline_thickness,
)
}
pub fn underline_size(&self) -> Vec2 {
Vec2::new(self.bounds.size().x, self.underline_thickness)
}
}
#[derive(Debug)]
pub struct TextMeasureInfo {
pub min: Vec2,
pub max: Vec2,
pub entity: Entity,
}
impl TextMeasureInfo {
pub fn compute_size(
&mut self,
bounds: TextBounds,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
) -> Vec2 {
computed
.buffer
.set_size(&mut font_system.0, bounds.width, bounds.height);
buffer_dimensions(&computed.buffer)
}
}
pub fn load_font_to_fontdb(
text_font: &TextFont,
font_system: &mut cosmic_text::FontSystem,
map_handle_to_font_id: &mut HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
fonts: &Assets<Font>,
) -> FontFaceInfo {
let font_id = text_font.font.id();
let (face_id, family_name) = map_handle_to_font_id.entry(font_id).or_insert_with(|| {
let font = fonts.get(font_id).expect(
"Tried getting a font that was not available, probably due to not being loaded yet",
);
let data = Arc::clone(&font.data);
let ids = font_system
.db_mut()
.load_font_source(cosmic_text::fontdb::Source::Binary(data));
let face_id = *ids.last().unwrap();
let face = font_system.db().face(face_id).unwrap();
let family_name = Arc::from(face.families[0].0.as_str());
(face_id, family_name)
});
let face = font_system.db().face(*face_id).unwrap();
FontFaceInfo {
stretch: face.stretch,
style: face.style,
family_name: family_name.clone(),
}
}
fn get_attrs<'a>(
span_index: usize,
text_font: &TextFont,
line_height: LineHeight,
color: Color,
face_info: &'a FontFaceInfo,
scale_factor: f64,
) -> Attrs<'a> {
Attrs::new()
.metadata(span_index)
.family(Family::Name(&face_info.family_name))
.stretch(face_info.stretch)
.style(face_info.style)
.weight(text_font.weight.into())
.metrics(
Metrics {
font_size: text_font.font_size,
line_height: line_height.eval(text_font.font_size),
}
.scale(scale_factor as f32),
)
.font_features((&text_font.font_features).into())
.color(cosmic_text::Color(color.to_linear().as_u32()))
}
fn buffer_dimensions(buffer: &Buffer) -> Vec2 {
let mut size = Vec2::ZERO;
for run in buffer.layout_runs() {
size.x = size.x.max(run.line_w);
size.y += run.line_height;
}
size.ceil()
}
pub(crate) fn trim_cosmic_cache(mut font_system: ResMut<CosmicFontSystem>) {
font_system.0.shape_run_cache.trim(2);
}