hephae_text/
def.rs

1//! Defines common types of Hephae Text.
2
3use std::{io::Error as IoError, slice::Iter, sync::Mutex};
4
5use async_channel::Sender;
6use bevy_asset::{AssetLoader, LoadContext, io::Reader, prelude::*};
7use bevy_derive::{Deref, DerefMut};
8use bevy_ecs::{
9    prelude::*,
10    system::{
11        SystemParamItem,
12        lifetimeless::{Read, SQuery},
13    },
14};
15use bevy_hierarchy::prelude::*;
16use bevy_math::prelude::*;
17use bevy_reflect::prelude::*;
18use cosmic_text::{
19    Align, Buffer, Metrics, Stretch, Style, Weight, Wrap, fontdb::ID as FontId, ttf_parser::FaceParsingError,
20};
21use fixedbitset::FixedBitSet;
22#[cfg(feature = "locale")]
23use hephae_locale::prelude::*;
24use smallvec::SmallVec;
25use thiserror::Error;
26
27use crate::atlas::FontAtlas;
28
29/// A TTF font asset.
30#[derive(Asset, TypePath, Clone)]
31pub struct Font {
32    /// The font ID in the database.
33    pub id: FontId,
34    /// The family name of the font.
35    pub name: String,
36    /// The font style.
37    pub style: Style,
38    /// The font weight.
39    pub weight: Weight,
40    /// The font stretch.
41    pub stretch: Stretch,
42}
43
44/// Errors that may arise when loading a [`Font`].
45#[derive(Error, Debug)]
46pub enum FontError {
47    /// The async channel between the asset thread and main world thread has been closed.
48    #[error("the async channel to add fonts to the database was closed")]
49    ChannelClosed,
50    /// An IO error occurred.
51    #[error(transparent)]
52    Io(#[from] IoError),
53    /// Invalid asset bytes.
54    #[error(transparent)]
55    Face(#[from] FaceParsingError),
56}
57
58/// Asset loader for [`Font`]s.
59pub struct FontLoader {
60    pub(crate) add_to_database: Sender<(Vec<u8>, Sender<Result<Font, FaceParsingError>>)>,
61}
62
63impl AssetLoader for FontLoader {
64    type Asset = Font;
65    type Settings = ();
66    type Error = FontError;
67
68    #[inline]
69    async fn load(
70        &self,
71        reader: &mut dyn Reader,
72        _: &Self::Settings,
73        _: &mut LoadContext<'_>,
74    ) -> Result<Self::Asset, Self::Error> {
75        let mut bytes = Vec::new();
76        reader.read_to_end(&mut bytes).await?;
77
78        let (sender, receiver) = async_channel::bounded(1);
79        if self.add_to_database.send((bytes, sender)).await.is_err() {
80            return Err(FontError::ChannelClosed);
81        }
82
83        let font = receiver.recv().await.map_err(|_| FontError::ChannelClosed)??;
84        Ok(font)
85    }
86
87    #[inline]
88    fn extensions(&self) -> &[&str] {
89        &["ttf"]
90    }
91}
92
93/// Main text component. May have children entities that have [`TextSpan`] and [`TextFont`]
94/// component.
95#[derive(Component, Reflect, Clone, Default, Deref, DerefMut)]
96#[reflect(Component, Default)]
97#[require(TextStructure, TextGlyphs)]
98pub struct Text {
99    /// The text span.
100    #[deref]
101    pub text: String,
102    /// Defines how the text should wrap in case of insufficient space.
103    pub wrap: TextWrap,
104    /// Defines how the text should align over extra horizontal space.
105    pub align: TextAlign,
106}
107
108impl Text {
109    /// Convenience method to create a new text without wrapping and with left-align.
110    #[inline]
111    pub fn new(text: impl ToString) -> Self {
112        Self {
113            text: text.to_string(),
114            ..Self::default()
115        }
116    }
117}
118
119#[cfg(feature = "locale")]
120impl LocaleTarget for Text {
121    #[inline]
122    fn update(&mut self, src: &str) {
123        src.clone_into(self);
124    }
125}
126
127/// Defines how the text should wrap in case of insufficient space.
128#[derive(Reflect, Eq, PartialEq, Copy, Clone, Default)]
129#[reflect(Default)]
130pub enum TextWrap {
131    /// Don't wrap.
132    #[default]
133    None,
134    /// Individual letters may wrap, similar to the behavior seen in command-lines.
135    Glyph,
136    /// Individual words may wrap, similar to the behavior seen in text documents.
137    Word,
138    /// Words or letters may wrap, preferring the former.
139    WordOrGlyph,
140}
141
142impl From<TextWrap> for Wrap {
143    #[inline]
144    fn from(value: TextWrap) -> Self {
145        match value {
146            TextWrap::None => Self::None,
147            TextWrap::Glyph => Self::Glyph,
148            TextWrap::Word => Self::Word,
149            TextWrap::WordOrGlyph => Self::WordOrGlyph,
150        }
151    }
152}
153
154/// Defines how the text should align over extra horizontal space.
155#[derive(Reflect, Default, Eq, PartialEq, Clone, Copy)]
156#[reflect(Default)]
157pub enum TextAlign {
158    /// Aligns the text left.
159    #[default]
160    Left,
161    /// Aligns the text right.
162    Right,
163    /// Aligns the text at the center.
164    Center,
165    /// Similar to left, but makes the left and right border of the text parallel.
166    Justified,
167    /// Aligns right for left-to-right fonts, left otherwise.
168    End,
169}
170
171impl From<TextAlign> for Align {
172    #[inline]
173    fn from(value: TextAlign) -> Self {
174        match value {
175            TextAlign::Left => Self::Left,
176            TextAlign::Right => Self::Right,
177            TextAlign::Center => Self::Center,
178            TextAlign::Justified => Self::Justified,
179            TextAlign::End => Self::End,
180        }
181    }
182}
183
184/// Defines the font of a [`Text`] or a [`TextSpan`].
185#[derive(Component, Reflect, Clone)]
186#[reflect(Component, Default)]
187pub struct TextFont {
188    /// The font handle.
189    pub font: Handle<Font>,
190    /// The font size. Note that frequently changing this will result in high memory usage.
191    pub font_size: f32,
192    /// The relative line height of the font.
193    pub line_height: f32,
194    /// Whether to antialias the font glyphs.
195    pub antialias: bool,
196}
197
198impl Default for TextFont {
199    #[inline]
200    fn default() -> Self {
201        Self {
202            font: Handle::default(),
203            font_size: 16.,
204            line_height: 1.2,
205            antialias: true,
206        }
207    }
208}
209
210/// Type of [`Query`] that may be passed to [`TextStructure::iter`], with a `'static` lifetime.
211pub type STextQuery = SQuery<(Option<Read<Text>>, Option<Read<TextSpan>>, Option<Read<TextFont>>)>;
212/// Type of [`Query`] that may be passed to [`TextStructure::iter`].
213pub type TextQuery<'w, 's> = SystemParamItem<'w, 's, STextQuery>;
214
215/// Contains entities that make up a full text buffer. Listen to [`Changed<TextStructure>`] if you
216/// want to recompute [`TextGlyphs`].
217#[derive(Component, Clone, Deref, DerefMut, Default)]
218pub struct TextStructure(SmallVec<[(Entity, usize); 1]>);
219impl TextStructure {
220    /// Iterates textual entities for use in [`FontLayout`](crate::layout::FontLayout).
221    #[inline]
222    pub fn iter<'w, 's>(&'w self, query: &'w TextQuery<'w, 's>) -> TextStructureIter<'w, 's> {
223        TextStructureIter {
224            inner: self.0.iter(),
225            fonts: SmallVec::new_const(),
226            query,
227        }
228    }
229}
230
231/// Iterates textual entities for use in [`FontLayout`](crate::layout::FontLayout).
232pub struct TextStructureIter<'w, 's: 'w> {
233    inner: Iter<'w, (Entity, usize)>,
234    fonts: SmallVec<[(&'w TextFont, usize); 4]>,
235    query: &'w TextQuery<'w, 's>,
236}
237
238impl<'w> Iterator for TextStructureIter<'w, '_> {
239    type Item = (&'w str, &'w TextFont);
240
241    fn next(&mut self) -> Option<Self::Item> {
242        const DEFAULT_FONT: TextFont = TextFont {
243            font: Handle::Weak(AssetId::Uuid {
244                uuid: AssetId::<Font>::DEFAULT_UUID,
245            }),
246            font_size: 24.,
247            line_height: 1.2,
248            antialias: true,
249        };
250
251        let &(e, depth) = self.inner.next()?;
252        let (text, span, font) = self.query.get(e).ok()?;
253        let str = match (text, span) {
254            (Some(text), ..) => text.as_str(),
255            (None, Some(span)) => span.as_str(),
256            (None, None) => return None,
257        };
258
259        let font = font.unwrap_or_else(|| {
260            loop {
261                let &(last_font, last_depth) = self.fonts.last().unwrap_or(&(&DEFAULT_FONT, 0));
262                if depth > 0 && last_depth >= depth {
263                    self.fonts.pop();
264                } else {
265                    self.fonts.push((last_font, depth));
266                    break last_font;
267                }
268            }
269        });
270
271        Some((str, font))
272    }
273}
274
275/// Contains the computed glyphs of a text entity.
276#[derive(Component)]
277pub struct TextGlyphs {
278    /// The glyphs, ready to be rendered.
279    pub glyphs: Vec<TextGlyph>,
280    /// The size of the text.
281    pub size: Vec2,
282    pub(crate) buffer: Mutex<Buffer>,
283}
284
285impl Default for TextGlyphs {
286    #[inline]
287    fn default() -> Self {
288        Self {
289            glyphs: Vec::new(),
290            size: Vec2::ZERO,
291            buffer: Mutex::new(Buffer::new_empty(Metrics::new(f32::MIN_POSITIVE, f32::MIN_POSITIVE))),
292        }
293    }
294}
295
296/// A single information about how a glyph can be rendered.
297#[derive(Copy, Clone)]
298pub struct TextGlyph {
299    /// Positional offset of the glyph relative to the text box's bottom-left corner.
300    pub origin: Vec2,
301    /// The size of this glyph.
302    pub size: Vec2,
303    /// The atlas that this glyph uses.
304    pub atlas: AssetId<FontAtlas>,
305    /// The index of this glyph information in its [`FontAtlas`].
306    pub index: usize,
307}
308
309/// May be added to child entities of [`Text`].
310#[derive(Component, Reflect, Default, Deref, DerefMut)]
311#[reflect(Component, Default)]
312pub struct TextSpan(pub String);
313
314#[cfg(feature = "locale")]
315impl LocaleTarget for TextSpan {
316    #[inline]
317    fn update(&mut self, src: &str) {
318        src.clone_into(self);
319    }
320}
321
322/// Computes and marks [`TextStructure`] as changed as necessary, for convenience of systems
323/// wishing to listen for change-detection.
324pub fn compute_structure(
325    mut text_query: Query<(Entity, &mut TextStructure, Option<&Children>)>,
326    recurse_query: Query<(Entity, Option<&Children>, &Parent), (With<TextSpan>, Without<Text>)>,
327    changed_query: Query<(
328        Entity,
329        Option<Ref<Text>>,
330        Option<Ref<TextSpan>>,
331        Option<Ref<Parent>>,
332        Option<Ref<Children>>,
333    )>,
334    parent_query: Query<&Parent>,
335    mut removed_span: RemovedComponents<TextSpan>,
336    mut iterated: Local<FixedBitSet>,
337    mut removed: Local<FixedBitSet>,
338    mut old: Local<SmallVec<[(Entity, usize); 1]>>,
339) {
340    iterated.clear();
341    removed.clear();
342
343    for e in removed_span.read() {
344        removed.grow_and_insert(e.index() as usize);
345    }
346
347    'out: for (e, text, span, parent, children) in &changed_query {
348        iterated.grow((e.index() + 1) as usize);
349        if iterated.put(e.index() as usize) {
350            continue 'out;
351        }
352
353        let parent_changed = parent.as_ref().is_some_and(Ref::is_changed);
354        let children_changed = children.is_some_and(|children| children.is_changed());
355
356        if match (text.as_ref(), span) {
357            (Some(text), ..) => text.is_added() || children_changed,
358            (None, Some(span)) => span.is_added() || parent_changed || children_changed,
359            (None, None) => {
360                if removed.contains(e.index() as usize) {
361                    true
362                } else {
363                    continue 'out;
364                }
365            }
366        } {
367            let Ok((root, mut structure, children)) = (if text.is_some() {
368                text_query.get_mut(e)
369            } else {
370                let Some(mut e) = parent.map(|p| p.get()) else {
371                    continue 'out;
372                };
373                loop {
374                    iterated.grow((e.index() + 1) as usize);
375                    if iterated.put(e.index() as usize) {
376                        continue 'out;
377                    }
378
379                    match text_query.get_mut(e) {
380                        Ok(structure) => break Ok(structure),
381                        Err(..) => match parent_query.get(e) {
382                            Ok(parent) => {
383                                e = parent.get();
384                                continue;
385                            }
386                            Err(..) => continue 'out,
387                        },
388                    }
389                }
390            }) else {
391                continue 'out;
392            };
393
394            let inner = &mut structure.bypass_change_detection().0;
395            old.append(inner);
396
397            fn recurse(
398                structure: &mut SmallVec<[(Entity, usize); 1]>,
399                depth: usize,
400                parent: Entity,
401                children: &[Entity],
402                recurse_query: &Query<(Entity, Option<&Children>, &Parent), (With<TextSpan>, Without<Text>)>,
403            ) {
404                for (e, children, actual_parent) in recurse_query.iter_many(children) {
405                    assert_eq!(
406                        actual_parent.get(),
407                        parent,
408                        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
409                    );
410
411                    structure.push((e, depth));
412                    if let Some(children) = children {
413                        recurse(structure, depth + 1, e, children, recurse_query);
414                    }
415                }
416            }
417
418            inner.clear();
419            inner.push((root, 0));
420            if let Some(children) = children {
421                recurse(inner, 1, root, children, &recurse_query);
422            }
423
424            if &*old != inner {
425                structure.set_changed();
426            }
427
428            old.clear();
429        }
430    }
431}
432
433/// Computes and marks [`TextStructure`] as changed as necessary, for convenience of systems
434/// wishing to listen for change-detection.
435pub fn notify_structure(
436    mut root_query: Query<&mut TextStructure>,
437    changed_query: Query<
438        (Option<Ref<Text>>, Option<Ref<TextSpan>>, Option<Ref<TextFont>>),
439        Or<(With<Text>, With<TextSpan>)>,
440    >,
441) {
442    'out: for mut structure in &mut root_query {
443        let inner = &structure.bypass_change_detection().0;
444        for (text, span, font) in changed_query.iter_many(inner.iter().map(|&(e, ..)| e)) {
445            if text.is_some_and(|text| text.is_changed()) ||
446                span.is_some_and(|span| span.is_changed()) ||
447                font.is_some_and(|font| font.is_changed())
448            {
449                structure.set_changed();
450                continue 'out;
451            }
452        }
453    }
454}