use ab_glyph::{Font as _, ScaleFont as _};
use bevy::{
asset::Assets,
ecs::{
entity::Entity,
event::EventReader,
system::{Local, Query, Res, ResMut},
},
math::{FloatOrd, Vec2},
prelude::*,
render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
texture::Image,
},
sprite::DynamicTextureAtlasBuilder,
text::{BreakLineOn, Font, GlyphAtlasInfo, PositionedGlyph, TextError, TextLayoutInfo},
utils::{HashMap, HashSet},
window::{PrimaryWindow, Window, WindowScaleFactorChanged},
};
use glyph_brush_layout::GlyphPositioner as _;
use crate::{render_context::TextLayout, Canvas};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CanvasTextId {
canvas_entity: Entity,
text_id: u32,
}
impl CanvasTextId {
pub fn from_raw(canvas_entity: Entity, text_id: u32) -> Self {
Self {
canvas_entity,
text_id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct ScaledGlyph {
pub glyph_id: ab_glyph::GlyphId,
pub font_size: FloatOrd,
}
#[derive(Debug, Clone, Copy)]
struct AtlasGlyph {
pub glyph_index: usize,
pub bounds: Rect,
pub px_size: Vec2,
}
#[derive(Resource)]
pub struct KeithTextPipeline {
font_map: HashMap<AssetId<Font>, glyph_brush_layout::FontId>,
font_handles: Vec<Handle<Font>>,
fonts: Vec<ab_glyph::FontArc>,
glyphs: HashMap<ScaledGlyph, AtlasGlyph>,
atlas_packer: DynamicTextureAtlasBuilder,
atlas_layout_handle: Handle<TextureAtlasLayout>,
pub atlas_texture_handle: Handle<Image>,
}
const DEBUG_FILL_ATLAS: bool = true;
impl FromWorld for KeithTextPipeline {
fn from_world(world: &mut World) -> Self {
let mut images = world.resource_mut::<Assets<Image>>();
let atlas_image = if DEBUG_FILL_ATLAS {
let data: Vec<u8> = (0..1024)
.map(|y| {
(0..1024)
.map(move |x| [(x / 4) as u8, (y / 4) as u8, 255u8, 255u8])
.flatten()
})
.flatten()
.collect();
Image::new(
Extent3d {
width: 1024,
height: 1024,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
TextureFormat::Rgba8Unorm,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
)
} else {
Image::new_fill(
Extent3d {
width: 1024,
height: 1024,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0, 0, 0, 0],
TextureFormat::Rgba8Unorm,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
)
};
let atlas_texture_handle = images.add(atlas_image);
let mut texture_atlas_layouts = world.resource_mut::<Assets<TextureAtlasLayout>>();
let atlas_layout_handle =
texture_atlas_layouts.add(TextureAtlasLayout::new_empty(UVec2::splat(1024)));
let initial_size = UVec2::splat(1024);
Self {
font_map: default(),
font_handles: vec![],
fonts: vec![],
glyphs: default(),
atlas_packer: DynamicTextureAtlasBuilder::new(initial_size, 0),
atlas_layout_handle,
atlas_texture_handle,
}
}
}
impl KeithTextPipeline {
pub fn calc_layout(
&mut self,
fonts: &Assets<Font>,
images: &mut Assets<Image>,
texture_atlas_layouts: &mut Assets<TextureAtlasLayout>,
text_layout: &mut TextLayout,
scale_factor: f32,
) -> Result<TextLayoutInfo, TextError> {
trace!("calc_layout() text_layout_id={}", text_layout.id);
let atlas_layout = texture_atlas_layouts
.get_mut(&self.atlas_layout_handle)
.unwrap();
let mut scaled_fonts = Vec::with_capacity(text_layout.sections.len());
let sections = text_layout
.sections
.iter()
.map(|section| {
let font = fonts
.get(§ion.style.font)
.ok_or(TextError::NoSuchFont)?;
let font_id = self.get_or_insert_font_id(§ion.style.font, font);
let font_size_px = section.style.font_size * scale_factor;
scaled_fonts.push(ab_glyph::Font::as_scaled(&font.font, font_size_px));
let section = glyph_brush_layout::SectionText {
text: §ion.value,
scale: ab_glyph::PxScale::from(font_size_px),
font_id,
};
Ok(section)
})
.collect::<Result<Vec<_>, _>>()?;
let phys_bounds_px = text_layout.bounds * scale_factor;
let geom = glyph_brush_layout::SectionGeometry {
bounds: (phys_bounds_px.x, phys_bounds_px.y),
..Default::default()
};
let line_breaker: glyph_brush_layout::BuiltInLineBreaker = BreakLineOn::NoWrap.into();
let section_glyphs = glyph_brush_layout::Layout::default()
.h_align(text_layout.justify.into())
.v_align(glyph_brush_layout::VerticalAlign::Top)
.line_breaker(line_breaker) .calculate_glyphs(&self.fonts, &geom, §ions);
let typographic_size_px =
Self::calc_typographic_size(§ion_glyphs, |index| scaled_fonts[index]).size();
let align_x = (match text_layout.justify {
JustifyText::Left => -0.5,
JustifyText::Center => 0.,
JustifyText::Right => 0.5,
} - text_layout.anchor.as_vec().x)
* typographic_size_px.x;
let align_y = (0.5 - text_layout.anchor.as_vec().y) * (-typographic_size_px.y);
let alignment_translation_px = Vec2::new(align_x, align_y);
trace!(
"-> typographic_size_px={:?}px anchor={:?} alignment_translation_px={:?} text_layout.bounds={:?}",
typographic_size_px,
text_layout.anchor,
alignment_translation_px,
text_layout.bounds
);
let mut text_layout_info = TextLayoutInfo {
logical_size: typographic_size_px,
..default()
};
for section_glyph in section_glyphs {
let glyph_brush_layout::SectionGlyph {
section_index,
byte_index,
glyph,
font_id,
} = section_glyph;
let position = Vec2::new(glyph.position.x, glyph.position.y);
let scale = Vec2::new(glyph.scale.x, glyph.scale.y);
let section = sections[section_index];
let font_size = section.scale.y.round(); let scaled_glyph = ScaledGlyph {
glyph_id: glyph.id,
font_size: FloatOrd(font_size),
};
trace!(
"- Glyph #{:?} pos={:?} scale={:?} font_id={:?} font_size={:?}",
glyph.id,
position,
scale,
font_id,
font_size
);
let atlas_glyph = if let Some(atlas_glyph) = self.glyphs.get(&scaled_glyph) {
trace!(
" -> Already present in atlas at index #{} (px_size:{:?})",
atlas_glyph.glyph_index,
atlas_glyph.px_size,
);
*atlas_glyph
} else {
let glyph_id = glyph.id;
if let Some(outlined_glyph) = self.fonts[section.font_id.0].outline_glyph(glyph) {
let bounds = outlined_glyph.px_bounds();
let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph);
let Some(glyph_index) = self.atlas_packer.add_texture(
atlas_layout,
images,
&glyph_texture,
&self.atlas_texture_handle,
) else {
warn!("Atlas full!");
continue;
};
let tex_rect = atlas_layout.textures[glyph_index];
let mut bounds =
Rect::new(bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y);
bounds.min.x -= position.x;
bounds.min.y -= position.y;
bounds.max.x -= position.x;
bounds.max.y -= position.y;
let px_size = tex_rect.size().as_vec2();
let atlas_glyph = AtlasGlyph {
glyph_index,
bounds,
px_size,
};
self.glyphs.insert(scaled_glyph, atlas_glyph);
debug!(" -> Inserted new glyph #{glyph_id:?} at index {glyph_index} into atlas. bounds={bounds:?} (px_size:{px_size:?})");
atlas_glyph
} else {
continue;
}
};
let size = atlas_glyph.px_size;
trace!(
"size_px={:?} atlas_glyph.bounds={:?}",
size,
atlas_glyph.bounds
);
let mut position = position + atlas_glyph.bounds.min;
position += alignment_translation_px;
position -= 1.0;
trace!(" PositionedGlyph: pos_px={position:?} size_px={size:?}");
text_layout_info.glyphs.push(PositionedGlyph {
position,
size,
atlas_info: GlyphAtlasInfo {
texture_atlas: self.atlas_layout_handle.clone(),
texture: self.atlas_texture_handle.clone(),
glyph_index: atlas_glyph.glyph_index,
},
section_index,
byte_index,
});
}
return Ok(text_layout_info);
}
fn get_or_insert_font_id(
&mut self,
handle: &Handle<Font>,
font: &Font,
) -> glyph_brush_layout::FontId {
*self.font_map.entry(handle.id()).or_insert_with(|| {
let id = self.fonts.len();
self.fonts.push(font.font.clone());
self.font_handles.push(handle.clone());
glyph_brush_layout::FontId(id)
})
}
fn calc_typographic_size<T>(
section_glyphs: &[glyph_brush_layout::SectionGlyph],
get_scaled_font: impl Fn(usize) -> ab_glyph::PxScaleFont<T>,
) -> Rect
where
T: ab_glyph::Font,
{
let mut text_bounds = Rect {
min: Vec2::splat(f32::MAX),
max: Vec2::splat(f32::MIN),
};
for sg in section_glyphs {
let scaled_font = get_scaled_font(sg.section_index);
let glyph = &sg.glyph;
text_bounds = text_bounds.union(Rect {
min: Vec2::new(glyph.position.x, 0.),
max: Vec2::new(
glyph.position.x + scaled_font.h_advance(glyph.id),
glyph.position.y - scaled_font.descent(),
),
});
}
text_bounds
}
}
pub fn process_glyphs(
mut font_queue: Local<HashSet<Entity>>,
mut images: ResMut<Assets<Image>>,
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
fonts: Res<Assets<Font>>,
q_window: Query<&Window, With<PrimaryWindow>>,
mut ev_window_scale_factor_changed: EventReader<WindowScaleFactorChanged>,
mut text_pipeline: ResMut<KeithTextPipeline>,
mut canvas_query: Query<(Entity, &mut Canvas)>,
) {
trace!("process_glyphs");
let scale_factor_changed = ev_window_scale_factor_changed.read().last().is_some();
let Ok(window) = q_window.get_single() else {
return;
};
let scale_factor = window.scale_factor() as f64;
let inv_scale_factor = 1. / scale_factor;
for (entity, mut canvas) in canvas_query.iter_mut() {
if !scale_factor_changed && !canvas.has_text() && !font_queue.remove(&entity) {
continue;
}
for text_layout in canvas.text_layouts_mut() {
trace!(
"Queue text: id={} anchor={:?} alignment={:?} bounds={:?}",
text_layout.id,
text_layout.anchor,
text_layout.justify,
text_layout.bounds
);
match text_pipeline.calc_layout(
&fonts,
&mut images,
&mut texture_atlas_layouts,
text_layout,
scale_factor as f32,
) {
Ok(text_layout_info) => {
text_layout.calculated_size = Vec2::new(
scale_value(text_layout_info.logical_size.x, inv_scale_factor),
scale_value(text_layout_info.logical_size.y, inv_scale_factor),
);
text_layout.layout_info = Some(text_layout_info);
}
Err(text_error) => error!("Failed to calculate layout for text: {:?}", text_error),
}
}
}
}
pub(crate) fn scale_value(value: f32, factor: f64) -> f32 {
(value as f64 * factor) as f32
}