1use alloc::sync::Arc;
2
3use bevy_asset::{AssetId, Assets};
4use bevy_color::Color;
5use bevy_derive::{Deref, DerefMut};
6use bevy_ecs::{
7 component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource,
8 system::ResMut,
9};
10use bevy_image::prelude::*;
11use bevy_log::{once, warn};
12use bevy_math::{Rect, UVec2, Vec2};
13use bevy_platform::collections::HashMap;
14use bevy_reflect::{std_traits::ReflectDefault, Reflect};
15
16use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};
17
18use crate::{
19 error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak,
20 PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout,
21};
22
23#[derive(Resource, Deref, DerefMut)]
29pub struct CosmicFontSystem(pub cosmic_text::FontSystem);
30
31impl Default for CosmicFontSystem {
32 fn default() -> Self {
33 let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
34 let db = cosmic_text::fontdb::Database::new();
35 Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db))
37 }
38}
39
40#[derive(Resource)]
46pub struct SwashCache(pub cosmic_text::SwashCache);
47
48impl Default for SwashCache {
49 fn default() -> Self {
50 Self(cosmic_text::SwashCache::new())
51 }
52}
53
54#[derive(Clone)]
56pub struct FontFaceInfo {
57 pub stretch: cosmic_text::fontdb::Stretch,
59 pub style: cosmic_text::fontdb::Style,
61 pub weight: cosmic_text::fontdb::Weight,
63 pub family_name: Arc<str>,
65}
66
67#[derive(Default, Resource)]
71pub struct TextPipeline {
72 pub map_handle_to_font_id: HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
74 spans_buffer: Vec<(usize, &'static str, &'static TextFont, FontFaceInfo)>,
78 glyph_info: Vec<(AssetId<Font>, FontSmoothing)>,
80}
81
82impl TextPipeline {
83 pub fn update_buffer<'a>(
87 &mut self,
88 fonts: &Assets<Font>,
89 text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
90 linebreak: LineBreak,
91 justify: Justify,
92 bounds: TextBounds,
93 scale_factor: f64,
94 computed: &mut ComputedTextBlock,
95 font_system: &mut CosmicFontSystem,
96 ) -> Result<(), TextError> {
97 let font_system = &mut font_system.0;
98
99 let mut max_font_size: f32 = 0.;
102 let mut max_line_height: f32 = 0.0;
103 let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color)> =
104 core::mem::take(&mut self.spans_buffer)
105 .into_iter()
106 .map(|_| -> (usize, &str, &TextFont, FontFaceInfo, Color) { unreachable!() })
107 .collect();
108
109 computed.entities.clear();
110
111 for (span_index, (entity, depth, span, text_font, color)) in text_spans.enumerate() {
112 computed.entities.push(TextEntity { entity, depth });
114
115 if span.is_empty() {
116 continue;
117 }
118 if !fonts.contains(text_font.font.id()) {
120 spans.clear();
121 self.spans_buffer = spans
122 .into_iter()
123 .map(
124 |_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) {
125 unreachable!()
126 },
127 )
128 .collect();
129
130 return Err(TextError::NoSuchFont);
131 }
132
133 max_font_size = max_font_size.max(text_font.font_size);
135 max_line_height = max_line_height.max(text_font.line_height.eval(text_font.font_size));
136
137 let face_info = load_font_to_fontdb(
139 text_font,
140 font_system,
141 &mut self.map_handle_to_font_id,
142 fonts,
143 );
144
145 if scale_factor <= 0.0 || text_font.font_size <= 0.0 {
147 once!(warn!(
148 "Text span {entity} has a font size <= 0.0. Nothing will be displayed.",
149 ));
150
151 continue;
152 }
153 spans.push((span_index, span, text_font, face_info, color));
154 }
155
156 let mut metrics = Metrics::new(max_font_size, max_line_height).scale(scale_factor as f32);
157 metrics.font_size = metrics.font_size.max(0.000001);
161 metrics.line_height = metrics.line_height.max(0.000001);
162
163 let spans_iter = spans
170 .iter()
171 .map(|(span_index, span, text_font, font_info, color)| {
172 (
173 *span,
174 get_attrs(*span_index, text_font, *color, font_info, scale_factor),
175 )
176 });
177
178 let buffer = &mut computed.buffer;
180 buffer.set_metrics_and_size(font_system, metrics, bounds.width, bounds.height);
181
182 buffer.set_wrap(
183 font_system,
184 match linebreak {
185 LineBreak::WordBoundary => Wrap::Word,
186 LineBreak::AnyCharacter => Wrap::Glyph,
187 LineBreak::WordOrCharacter => Wrap::WordOrGlyph,
188 LineBreak::NoWrap => Wrap::None,
189 },
190 );
191
192 buffer.set_rich_text(
193 font_system,
194 spans_iter,
195 &Attrs::new(),
196 Shaping::Advanced,
197 Some(justify.into()),
198 );
199
200 buffer.shape_until_scroll(font_system, false);
201
202 if bounds.width.is_none() && justify != Justify::Left {
205 let dimensions = buffer_dimensions(buffer);
206 buffer.set_size(font_system, Some(dimensions.x), bounds.height);
208 }
209
210 spans.clear();
212 self.spans_buffer = spans
213 .into_iter()
214 .map(|_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) { unreachable!() })
215 .collect();
216
217 Ok(())
218 }
219
220 pub fn queue_text<'a>(
225 &mut self,
226 layout_info: &mut TextLayoutInfo,
227 fonts: &Assets<Font>,
228 text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
229 scale_factor: f64,
230 layout: &TextLayout,
231 bounds: TextBounds,
232 font_atlas_sets: &mut FontAtlasSets,
233 texture_atlases: &mut Assets<TextureAtlasLayout>,
234 textures: &mut Assets<Image>,
235 computed: &mut ComputedTextBlock,
236 font_system: &mut CosmicFontSystem,
237 swash_cache: &mut SwashCache,
238 ) -> Result<(), TextError> {
239 layout_info.glyphs.clear();
240 layout_info.section_rects.clear();
241 layout_info.size = Default::default();
242
243 computed.needs_rerender = false;
245
246 let mut glyph_info = core::mem::take(&mut self.glyph_info);
248 glyph_info.clear();
249 let text_spans = text_spans.inspect(|(_, _, _, text_font, _)| {
250 glyph_info.push((text_font.font.id(), text_font.font_smoothing));
251 });
252
253 let update_result = self.update_buffer(
254 fonts,
255 text_spans,
256 layout.linebreak,
257 layout.justify,
258 bounds,
259 scale_factor,
260 computed,
261 font_system,
262 );
263 if let Err(err) = update_result {
264 self.glyph_info = glyph_info;
265 return Err(err);
266 }
267
268 let buffer = &mut computed.buffer;
269 let box_size = buffer_dimensions(buffer);
270
271 let result = buffer.layout_runs().try_for_each(|run| {
272 let mut current_section: Option<usize> = None;
273 let mut start = 0.;
274 let mut end = 0.;
275 let result = run
276 .glyphs
277 .iter()
278 .map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i))
279 .try_for_each(|(layout_glyph, line_y, line_i)| {
280 match current_section {
281 Some(section) => {
282 if section != layout_glyph.metadata {
283 layout_info.section_rects.push((
284 computed.entities[section].entity,
285 Rect::new(
286 start,
287 run.line_top,
288 end,
289 run.line_top + run.line_height,
290 ),
291 ));
292 start = end.max(layout_glyph.x);
293 current_section = Some(layout_glyph.metadata);
294 }
295 end = layout_glyph.x + layout_glyph.w;
296 }
297 None => {
298 current_section = Some(layout_glyph.metadata);
299 start = layout_glyph.x;
300 end = start + layout_glyph.w;
301 }
302 }
303
304 let mut temp_glyph;
305 let span_index = layout_glyph.metadata;
306 let font_id = glyph_info[span_index].0;
307 let font_smoothing = glyph_info[span_index].1;
308
309 let layout_glyph = if font_smoothing == FontSmoothing::None {
310 temp_glyph = layout_glyph.clone();
313 temp_glyph.x = temp_glyph.x.round();
314 temp_glyph.y = temp_glyph.y.round();
315 temp_glyph.w = temp_glyph.w.round();
316 temp_glyph.x_offset = temp_glyph.x_offset.round();
317 temp_glyph.y_offset = temp_glyph.y_offset.round();
318 temp_glyph.line_height_opt = temp_glyph.line_height_opt.map(f32::round);
319
320 &temp_glyph
321 } else {
322 layout_glyph
323 };
324
325 let font_atlas_set = font_atlas_sets.sets.entry(font_id).or_default();
326
327 let physical_glyph = layout_glyph.physical((0., 0.), 1.);
328
329 let atlas_info = font_atlas_set
330 .get_glyph_atlas_info(physical_glyph.cache_key, font_smoothing)
331 .map(Ok)
332 .unwrap_or_else(|| {
333 font_atlas_set.add_glyph_to_atlas(
334 texture_atlases,
335 textures,
336 &mut font_system.0,
337 &mut swash_cache.0,
338 layout_glyph,
339 font_smoothing,
340 )
341 })?;
342
343 let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap();
344 let location = atlas_info.location;
345 let glyph_rect = texture_atlas.textures[location.glyph_index];
346 let left = location.offset.x as f32;
347 let top = location.offset.y as f32;
348 let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height());
349
350 let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32;
352 let y =
353 line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0;
354
355 let position = Vec2::new(x, y);
356
357 let pos_glyph = PositionedGlyph {
358 position,
359 size: glyph_size.as_vec2(),
360 atlas_info,
361 span_index,
362 byte_index: layout_glyph.start,
363 byte_length: layout_glyph.end - layout_glyph.start,
364 line_index: line_i,
365 };
366 layout_info.glyphs.push(pos_glyph);
367 Ok(())
368 });
369 if let Some(section) = current_section {
370 layout_info.section_rects.push((
371 computed.entities[section].entity,
372 Rect::new(start, run.line_top, end, run.line_top + run.line_height),
373 ));
374 }
375
376 result
377 });
378
379 self.glyph_info = glyph_info;
381
382 result?;
384
385 layout_info.size = box_size;
386 Ok(())
387 }
388
389 pub fn create_text_measure<'a>(
394 &mut self,
395 entity: Entity,
396 fonts: &Assets<Font>,
397 text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
398 scale_factor: f64,
399 layout: &TextLayout,
400 computed: &mut ComputedTextBlock,
401 font_system: &mut CosmicFontSystem,
402 ) -> Result<TextMeasureInfo, TextError> {
403 const MIN_WIDTH_CONTENT_BOUNDS: TextBounds = TextBounds::new_horizontal(0.0);
404
405 computed.needs_rerender = false;
408
409 self.update_buffer(
410 fonts,
411 text_spans,
412 layout.linebreak,
413 layout.justify,
414 MIN_WIDTH_CONTENT_BOUNDS,
415 scale_factor,
416 computed,
417 font_system,
418 )?;
419
420 let buffer = &mut computed.buffer;
421 let min_width_content_size = buffer_dimensions(buffer);
422
423 let max_width_content_size = {
424 let font_system = &mut font_system.0;
425 buffer.set_size(font_system, None, None);
426 buffer_dimensions(buffer)
427 };
428
429 Ok(TextMeasureInfo {
430 min: min_width_content_size,
431 max: max_width_content_size,
432 entity,
433 })
434 }
435
436 pub fn get_font_id(&self, asset_id: AssetId<Font>) -> Option<cosmic_text::fontdb::ID> {
438 self.map_handle_to_font_id
439 .get(&asset_id)
440 .cloned()
441 .map(|(id, _)| id)
442 }
443}
444
445#[derive(Component, Clone, Default, Debug, Reflect)]
450#[reflect(Component, Default, Debug, Clone)]
451pub struct TextLayoutInfo {
452 pub scale_factor: f32,
454 pub glyphs: Vec<PositionedGlyph>,
456 pub section_rects: Vec<(Entity, Rect)>,
459 pub size: Vec2,
461}
462
463#[derive(Debug)]
467pub struct TextMeasureInfo {
468 pub min: Vec2,
470 pub max: Vec2,
472 pub entity: Entity,
474}
475
476impl TextMeasureInfo {
477 pub fn compute_size(
479 &mut self,
480 bounds: TextBounds,
481 computed: &mut ComputedTextBlock,
482 font_system: &mut CosmicFontSystem,
483 ) -> Vec2 {
484 computed
487 .buffer
488 .set_size(&mut font_system.0, bounds.width, bounds.height);
489 buffer_dimensions(&computed.buffer)
490 }
491}
492
493pub fn load_font_to_fontdb(
495 text_font: &TextFont,
496 font_system: &mut cosmic_text::FontSystem,
497 map_handle_to_font_id: &mut HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
498 fonts: &Assets<Font>,
499) -> FontFaceInfo {
500 let font_handle = text_font.font.clone();
501 let (face_id, family_name) = map_handle_to_font_id
502 .entry(font_handle.id())
503 .or_insert_with(|| {
504 let font = fonts.get(font_handle.id()).expect(
505 "Tried getting a font that was not available, probably due to not being loaded yet",
506 );
507 let data = Arc::clone(&font.data);
508 let ids = font_system
509 .db_mut()
510 .load_font_source(cosmic_text::fontdb::Source::Binary(data));
511
512 let face_id = *ids.last().unwrap();
514 let face = font_system.db().face(face_id).unwrap();
515 let family_name = Arc::from(face.families[0].0.as_str());
516
517 (face_id, family_name)
518 });
519 let face = font_system.db().face(*face_id).unwrap();
520
521 FontFaceInfo {
522 stretch: face.stretch,
523 style: face.style,
524 weight: face.weight,
525 family_name: family_name.clone(),
526 }
527}
528
529fn get_attrs<'a>(
531 span_index: usize,
532 text_font: &TextFont,
533 color: Color,
534 face_info: &'a FontFaceInfo,
535 scale_factor: f64,
536) -> Attrs<'a> {
537 Attrs::new()
538 .metadata(span_index)
539 .family(Family::Name(&face_info.family_name))
540 .stretch(face_info.stretch)
541 .style(face_info.style)
542 .weight(face_info.weight)
543 .metrics(
544 Metrics {
545 font_size: text_font.font_size,
546 line_height: text_font.line_height.eval(text_font.font_size),
547 }
548 .scale(scale_factor as f32),
549 )
550 .color(cosmic_text::Color(color.to_linear().as_u32()))
551}
552
553fn buffer_dimensions(buffer: &Buffer) -> Vec2 {
555 let (width, height) = buffer
556 .layout_runs()
557 .map(|run| (run.line_w, run.line_height))
558 .reduce(|(w1, h1), (w2, h2)| (w1.max(w2), h1 + h2))
559 .unwrap_or((0.0, 0.0));
560
561 Vec2::new(width, height).ceil()
562}
563
564pub(crate) fn trim_cosmic_cache(mut font_system: ResMut<CosmicFontSystem>) {
566 font_system.0.shape_run_cache.trim(2);
572}