good_web_game/graphics/
text.rs

1use glyph_brush::GlyphPositioner;
2use glyph_brush::{self, FontId, Layout, Section, Text as GbText};
3pub use glyph_brush::{ab_glyph::PxScale, GlyphBrush, HorizontalAlign as Align};
4use std::borrow::Cow;
5use std::cell::RefCell;
6use std::convert::TryFrom;
7use std::fmt;
8use std::io::Read;
9use std::path;
10use std::rc::Rc;
11
12use super::*;
13
14/// A handle referring to a loaded Truetype font.
15///
16/// This is just an integer referring to a loaded font stored in the
17/// `Context`, so is cheap to copy.  Note that fonts are cached and
18/// currently never *removed* from the cache, since that would
19/// invalidate the whole cache and require re-loading all the other
20/// fonts.  So, you do not want to load a font more than once.
21#[derive(Debug, Copy, Clone, PartialEq, Eq)]
22pub struct Font {
23    font_id: FontId,
24    // Add DebugId?  It makes Font::default() less convenient.
25}
26
27/// The font cache of the engine.
28///
29/// This type can be useful to measure text efficiently without being tied to
30/// the `Context` lifetime.
31#[derive(Clone, Debug)]
32pub struct FontCache {
33    glyph_brush: Rc<RefCell<GlyphBrush<DrawParam>>>,
34}
35
36impl FontCache {
37    /// Returns the width and height of the formatted and wrapped text.
38    pub fn dimensions(&self, text: &Text) -> Rect {
39        text.calculate_dimensions(&mut self.glyph_brush.borrow_mut())
40    }
41}
42
43/// A piece of text with optional color, font and font scale information.
44/// Drawing text generally involves one or more of these.
45/// These options take precedence over any similar field/argument.
46/// Implements `From` for `char`, `&str`, `String` and
47/// `(String, Font, PxScale)`.
48#[derive(Clone, Debug)]
49pub struct TextFragment {
50    /// Text string itself.
51    pub text: String,
52    /// Fragment's color, defaults to text's color.
53    pub color: Option<Color>,
54    /// Fragment's font, defaults to text's font.
55    pub font: Option<Font>,
56    /// Fragment's scale, defaults to text's scale.
57    pub scale: Option<PxScale>,
58}
59
60impl Default for TextFragment {
61    fn default() -> Self {
62        TextFragment {
63            text: "".into(),
64            color: None,
65            font: None,
66            scale: None,
67        }
68    }
69}
70
71impl TextFragment {
72    /// Creates a new fragment from `String` or `&str`.
73    pub fn new<T: Into<Self>>(text: T) -> Self {
74        text.into()
75    }
76
77    /// Set fragment's color, overrides text's color.
78    pub fn color<C: Into<Color>>(mut self, color: C) -> TextFragment {
79        self.color = Some(color.into());
80        self
81    }
82
83    /// Set fragment's font, overrides text's font.
84    pub fn font(mut self, font: Font) -> TextFragment {
85        self.font = Some(font);
86        self
87    }
88
89    /// Set fragment's scale, overrides text's scale. Default is 16.0
90    pub fn scale<S: Into<PxScale>>(mut self, scale: S) -> TextFragment {
91        self.scale = Some(scale.into());
92        self
93    }
94}
95
96impl<'a> From<&'a str> for TextFragment {
97    fn from(text: &'a str) -> TextFragment {
98        TextFragment {
99            text: text.to_owned(),
100            ..Default::default()
101        }
102    }
103}
104
105impl From<char> for TextFragment {
106    fn from(ch: char) -> TextFragment {
107        TextFragment {
108            text: ch.to_string(),
109            ..Default::default()
110        }
111    }
112}
113
114impl From<String> for TextFragment {
115    fn from(text: String) -> TextFragment {
116        TextFragment {
117            text,
118            ..Default::default()
119        }
120    }
121}
122
123impl<T> From<(T, Font, f32)> for TextFragment
124where
125    T: Into<TextFragment>,
126{
127    fn from((text, font, scale): (T, Font, f32)) -> TextFragment {
128        text.into().font(font).scale(PxScale::from(scale))
129    }
130}
131
132/// Cached font metrics that we can keep attached to a `Text`
133/// so we don't have to keep recalculating them.
134#[derive(Clone, Debug, Default)]
135struct CachedMetrics {
136    string: Option<String>,
137    width: Option<f32>,
138    height: Option<f32>,
139    glyph_positions: Vec<mint::Point2<f32>>,
140}
141
142/// Drawable text object.  Essentially a list of [`TextFragment`](struct.TextFragment.html)'s
143/// and some cached size information.
144///
145/// It implements [`Drawable`](trait.Drawable.html) so it can be drawn immediately with
146/// [`graphics::draw()`](fn.draw.html), or many of them can be queued with [`graphics::queue_text()`](fn.queue_text.html)
147/// and then all drawn at once with [`graphics::draw_queued_text()`](fn.draw_queued_text.html).
148#[derive(Debug, Clone)]
149pub struct Text {
150    fragments: Vec<TextFragment>,
151    blend_mode: Option<BlendMode>,
152    filter_mode: FilterMode,
153    bounds: Point2,
154    layout: Layout<glyph_brush::BuiltInLineBreaker>,
155    font_id: FontId,
156    font_scale: PxScale,
157    cached_metrics: RefCell<CachedMetrics>,
158}
159
160impl Default for Text {
161    fn default() -> Self {
162        Text {
163            fragments: Vec::new(),
164            blend_mode: None,
165            filter_mode: FilterMode::Linear,
166            bounds: Point2::new(f32::INFINITY, f32::INFINITY),
167            layout: Layout::default(),
168            font_id: FontId::default(),
169            font_scale: PxScale::from(Font::DEFAULT_FONT_SCALE),
170            cached_metrics: RefCell::new(CachedMetrics::default()),
171        }
172    }
173}
174
175impl Text {
176    /// Creates a `Text` from a `TextFragment`.
177    ///
178    /// ```rust
179    /// # use ggez::graphics::Text;
180    /// # fn main() {
181    /// let text = Text::new("foo");
182    /// # }
183    /// ```
184    pub fn new<F>(fragment: F) -> Text
185    where
186        F: Into<TextFragment>,
187    {
188        let mut text = Text::default();
189        let _ = text.add(fragment);
190        text
191    }
192
193    /// Appends a `TextFragment` to the `Text`.
194    pub fn add<F>(&mut self, fragment: F) -> &mut Text
195    where
196        F: Into<TextFragment>,
197    {
198        self.fragments.push(fragment.into());
199        self.invalidate_cached_metrics();
200        self
201    }
202
203    /// Returns a read-only slice of all `TextFragment`'s.
204    pub fn fragments(&self) -> &[TextFragment] {
205        &self.fragments
206    }
207
208    /// Returns a mutable slice with all fragments.
209    pub fn fragments_mut(&mut self) -> &mut [TextFragment] {
210        self.invalidate_cached_metrics();
211        &mut self.fragments
212    }
213
214    /// Specifies rectangular dimensions to try and fit contents inside of,
215    /// by wrapping, and alignment within the bounds.  To disable wrapping,
216    /// give it a layout with `f32::INF` for the x value.
217    pub fn set_bounds<P>(&mut self, bounds: P, alignment: Align) -> &mut Text
218    where
219        P: Into<mint::Point2<f32>>,
220    {
221        self.bounds = Point2::from(bounds.into());
222        if self.bounds.x == f32::INFINITY {
223            // Layouts don't make any sense if we don't wrap text at all.
224            self.layout = Layout::default();
225        } else {
226            self.layout = self.layout.h_align(alignment);
227        }
228        self.invalidate_cached_metrics();
229        self
230    }
231
232    /// Specifies text's font and font scale; used for fragments that don't have their own.
233    pub fn set_font(&mut self, font: Font, font_scale: PxScale) -> &mut Text {
234        self.font_id = font.font_id;
235        self.font_scale = font_scale;
236        self.invalidate_cached_metrics();
237        self
238    }
239
240    /// Converts `Text` to a type `glyph_brush` can understand and queue.
241    fn generate_varied_section(&self, relative_dest: Point2, color: Option<Color>) -> Section {
242        let sections: Vec<GbText> = self
243            .fragments
244            .iter()
245            .map(|fragment| {
246                let color = fragment.color.or(color).unwrap_or(Color::WHITE);
247                let font_id = fragment
248                    .font
249                    .map(|font| font.font_id)
250                    .unwrap_or(self.font_id);
251                let scale = fragment.scale.unwrap_or(self.font_scale);
252                GbText::default()
253                    .with_text(&fragment.text)
254                    .with_font_id(font_id)
255                    .with_scale(scale)
256                    .with_color(<[f32; 4]>::from(color))
257            })
258            .collect();
259
260        let relative_dest_x = {
261            // This positions text within bounds with relative_dest being to the left, always.
262            let mut dest_x = relative_dest.x;
263            if self.bounds.x != f32::INFINITY {
264                use glyph_brush::Layout::Wrap;
265                match self.layout {
266                    Wrap {
267                        h_align: Align::Center,
268                        ..
269                    } => dest_x += self.bounds.x * 0.5,
270                    Wrap {
271                        h_align: Align::Right,
272                        ..
273                    } => dest_x += self.bounds.x,
274                    _ => (),
275                }
276            }
277            dest_x
278        };
279        let relative_dest = (relative_dest_x, relative_dest.y);
280        Section {
281            screen_position: relative_dest,
282            bounds: (self.bounds.x, self.bounds.y),
283            layout: self.layout,
284            text: sections,
285        }
286    }
287
288    fn invalidate_cached_metrics(&mut self) {
289        if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
290            *metrics = CachedMetrics::default();
291            // Returning early avoids a double-borrow in the "else"
292            // part.
293            return;
294        }
295        warn!("Cached metrics RefCell has been poisoned.");
296        self.cached_metrics = RefCell::new(CachedMetrics::default());
297    }
298
299    /// Returns the string that the text represents.
300    pub fn contents(&self) -> String {
301        if let Ok(metrics) = self.cached_metrics.try_borrow() {
302            if let Some(ref string) = metrics.string {
303                return string.clone();
304            }
305        }
306        let string_accm: String = self
307            .fragments
308            .iter()
309            .map(|frag| frag.text.as_str())
310            .collect();
311
312        if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
313            metrics.string = Some(string_accm.clone());
314        }
315        string_accm
316    }
317
318    /// Calculates, caches, and returns position of the glyphs
319    fn calculate_glyph_positions(
320        &self,
321        gb: &mut GlyphBrush<DrawParam>,
322    ) -> std::cell::Ref<Vec<mint::Point2<f32>>> {
323        if let Ok(metrics) = self.cached_metrics.try_borrow() {
324            if !metrics.glyph_positions.is_empty() {
325                return std::cell::Ref::map(metrics, |metrics| &metrics.glyph_positions);
326            }
327        }
328        let glyph_positions: Vec<mint::Point2<f32>> = {
329            let varied_section = self.generate_varied_section(Point2::new(0.0, 0.0), None);
330            use glyph_brush::GlyphCruncher;
331            gb.glyphs(varied_section)
332                .map(|glyph| glyph.glyph.position)
333                .map(|pos| mint::Point2 { x: pos.x, y: pos.y })
334                .collect()
335        };
336        if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
337            metrics.glyph_positions = glyph_positions;
338        } else {
339            panic!();
340        }
341        if let Ok(metrics) = self.cached_metrics.try_borrow() {
342            std::cell::Ref::map(metrics, |metrics| &metrics.glyph_positions)
343        } else {
344            panic!()
345        }
346    }
347
348    /// Returns a Vec containing the coordinates of the formatted and wrapped text.
349    pub fn glyph_positions(&self, context: &Context) -> std::cell::Ref<Vec<mint::Point2<f32>>> {
350        self.calculate_glyph_positions(&mut context.gfx_context.glyph_brush.borrow_mut())
351    }
352
353    /// Calculates, caches, and returns width and height of formatted and wrapped text.
354    fn calculate_dimensions(&self, gb: &mut GlyphBrush<DrawParam>) -> Rect {
355        if let Ok(metrics) = self.cached_metrics.try_borrow() {
356            if let (Some(width), Some(height)) = (metrics.width, metrics.height) {
357                return Rect {
358                    x: 0.0,
359                    y: 0.0,
360                    w: width,
361                    h: height,
362                };
363            }
364        }
365        let mut max_width = 0.0;
366        let mut max_height = 0.0;
367        {
368            let varied_section = self.generate_varied_section(Point2::new(0.0, 0.0), None);
369            use glyph_brush::GlyphCruncher;
370            if let Some(bounds) = gb.glyph_bounds(varied_section) {
371                max_width = bounds.width().ceil();
372                max_height = bounds.height().ceil();
373            }
374        }
375        if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
376            metrics.width = Some(max_width);
377            metrics.height = Some(max_height);
378        }
379        Rect {
380            x: 0.0,
381            y: 0.0,
382            w: max_width,
383            h: max_height,
384        }
385    }
386
387    /// Returns a Rect containing the width and height of the formatted and wrapped text.
388    pub fn dimensions(&self, context: &Context) -> Rect {
389        self.calculate_dimensions(&mut context.gfx_context.glyph_brush.borrow_mut())
390    }
391
392    /// Returns the width of formatted and wrapped text, in screen coordinates.
393    pub fn width(&self, context: &Context) -> f32 {
394        self.dimensions(context).w
395    }
396
397    /// Returns the height of formatted and wrapped text, in screen coordinates.
398    pub fn height(&self, context: &Context) -> f32 {
399        self.dimensions(context).h
400    }
401}
402
403impl Drawable for Text {
404    fn draw(
405        &self,
406        ctx: &mut Context,
407        quad_ctx: &mut miniquad::graphics::GraphicsContext,
408        param: DrawParam,
409    ) -> GameResult {
410        // Converts fraction-of-bounding-box to screen coordinates, as required by `draw_queued()`.
411        queue_text(ctx, self, Point2::new(0.0, 0.0), Some(param.color));
412        draw_queued_text(ctx, quad_ctx, param, self.blend_mode, self.filter_mode)
413    }
414
415    fn dimensions(&self, ctx: &mut Context) -> Option<Rect> {
416        Some(self.dimensions(ctx))
417    }
418
419    fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
420        self.blend_mode = mode;
421    }
422
423    fn blend_mode(&self) -> Option<BlendMode> {
424        self.blend_mode
425    }
426}
427
428impl Font {
429    /// Default size for fonts.
430    pub const DEFAULT_FONT_SCALE: f32 = 16.0;
431
432    /// Load a new TTF font from the given file.
433    pub fn new<P>(context: &mut Context, path: P) -> GameResult<Font>
434    where
435        P: AsRef<path::Path> + fmt::Debug,
436    {
437        use crate::filesystem;
438        let mut stream = filesystem::open(context, path.as_ref())?;
439        let mut buf = Vec::new();
440        let _ = stream.read_to_end(&mut buf)?;
441
442        Font::new_glyph_font_bytes(context, &buf)
443    }
444
445    /// Loads a new TrueType font from given bytes and into a `gfx::GlyphBrush` owned
446    /// by the `Context`.
447    pub fn new_glyph_font_bytes(context: &mut Context, bytes: &[u8]) -> GameResult<Self> {
448        // Take a Cow here to avoid this clone where unnecessary?
449        // Nah, let's not complicate things more than necessary.
450        let font = glyph_brush::ab_glyph::FontArc::try_from_vec(bytes.to_vec()).unwrap();
451        let font_id = context.gfx_context.glyph_brush.borrow_mut().add_font(font);
452
453        Ok(Font { font_id })
454    }
455
456    /// Returns the baked-in bytes of default font (currently `LiberationSans-Regular.ttf`).
457    pub(crate) fn default_font_bytes() -> &'static [u8] {
458        include_bytes!(concat!(
459            env!("CARGO_MANIFEST_DIR"),
460            "/resources/LiberationMono-Regular.ttf"
461        ))
462    }
463}
464
465impl Default for Font {
466    fn default() -> Self {
467        Font { font_id: FontId(0) }
468    }
469}
470
471/// Obtains the font cache.
472pub fn font_cache(context: &Context) -> FontCache {
473    FontCache {
474        glyph_brush: context.gfx_context.glyph_brush.clone(),
475    }
476}
477
478/// Queues the `Text` to be drawn by [`draw_queued_text()`](fn.draw_queued_text.html).
479/// `relative_dest` is relative to the [`DrawParam::dest`](struct.DrawParam.html#structfield.dest)
480/// passed to `draw_queued()`. Note, any `Text` drawn via [`graphics::draw()`](fn.draw.html)
481/// will also draw everything already the queue.
482pub fn queue_text<P>(context: &mut Context, batch: &Text, relative_dest: P, color: Option<Color>)
483where
484    P: Into<mint::Point2<f32>>,
485{
486    let p = Point2::from(relative_dest.into());
487    let varied_section = batch.generate_varied_section(p, color);
488    context
489        .gfx_context
490        .glyph_brush
491        .borrow_mut()
492        .queue(varied_section);
493}
494
495/// Exposes `glyph_brush`'s drawing API in case `ggez`'s text drawing is insufficient.
496/// It takes `glyph_brush`'s `VariedSection` and `GlyphPositioner`, which give you lower-
497/// level control over how text is drawn.
498pub fn queue_text_raw<'a, S, G>(context: &mut Context, section: S, custom_layout: Option<&G>)
499where
500    S: Into<Cow<'a, Section<'a>>>,
501    G: GlyphPositioner,
502{
503    let brush = &mut context.gfx_context.glyph_brush.borrow_mut();
504    match custom_layout {
505        Some(layout) => brush.queue_custom_layout(section, layout),
506        None => brush.queue(section),
507    }
508}
509
510/// Draws all of the [`Text`](struct.Text.html)s added via [`queue_text()`](fn.queue_text.html).
511///
512/// the `DrawParam` applies to everything in the queue; offset is in
513/// screen coordinates; color is ignored - specify it when using
514/// `queue_text()` instead.
515///
516/// Note that all text will, and in fact must, be drawn with the same
517/// `BlendMode` and `FilterMode`.  This is unfortunate but currently
518/// unavoidable, see [this issue](https://github.com/ggez/ggez/issues/561)
519/// for more info.
520pub fn draw_queued_text<D>(
521    ctx: &mut Context,
522    quad_ctx: &mut miniquad::graphics::GraphicsContext,
523    param: D,
524    blend: Option<BlendMode>,
525    filter: FilterMode,
526) -> GameResult
527where
528    D: Into<DrawParam>,
529{
530    let param: DrawParam = param.into();
531
532    let gb = &mut ctx.gfx_context.glyph_brush;
533    let gc = &ctx.gfx_context.glyph_cache.texture;
534
535    let action = gb.borrow_mut().process_queued(
536        |rect, tex_data| {
537            // transform single byte alpha texture to (255, 255, 255, a)
538            let mut tex_data_chunks: Vec<u8> = vec![255; tex_data.len() * 4];
539            for i in 0..tex_data.len() {
540                tex_data_chunks[i * 4 + 3] = tex_data[i];
541            }
542
543            update_texture(quad_ctx, gc, rect, &tex_data_chunks[..])
544        },
545        to_vertex,
546    );
547    match action {
548        Ok(glyph_brush::BrushAction::ReDraw) => {
549            let spritebatch = ctx.gfx_context.glyph_state.clone();
550            let spritebatch = &mut *spritebatch.borrow_mut();
551            spritebatch.set_blend_mode(blend);
552            spritebatch.set_filter(filter);
553            draw(ctx, quad_ctx, &*spritebatch, param)?;
554        }
555        Ok(glyph_brush::BrushAction::Draw(drawparams)) => {
556            // Gotta clone the image to avoid double-borrow's.
557            let spritebatch = ctx.gfx_context.glyph_state.clone();
558            let spritebatch = &mut *spritebatch.borrow_mut();
559            spritebatch.clear();
560            spritebatch.set_blend_mode(blend);
561            spritebatch.set_filter(filter);
562            for p in &drawparams {
563                // Ignore returned sprite index.
564                let _ = spritebatch.add(*p);
565            }
566            draw(ctx, quad_ctx, &*spritebatch, param)?;
567        }
568        Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => {
569            let (new_width, new_height) = suggested;
570            let data = vec![255; 4 * new_width as usize * new_height as usize];
571            let new_glyph_cache = Image::from_rgba8(
572                ctx,
573                quad_ctx,
574                u16::try_from(new_width).unwrap(),
575                u16::try_from(new_height).unwrap(),
576                &data,
577            )?;
578            ctx.gfx_context.glyph_cache = new_glyph_cache.clone();
579            let spritebatch = ctx.gfx_context.glyph_state.clone();
580            let spritebatch = &mut *spritebatch.borrow_mut();
581            let _ = spritebatch.set_image(new_glyph_cache);
582            ctx.gfx_context
583                .glyph_brush
584                .borrow_mut()
585                .resize_texture(new_width, new_height);
586        }
587    }
588    Ok(())
589}
590
591fn update_texture(
592    ctx: &mut miniquad::Context,
593    texture: &miniquad::Texture,
594    rect: glyph_brush::Rectangle<u32>,
595    tex_data: &[u8],
596) {
597    let offset = [
598        i32::try_from(rect.min[0]).unwrap(),
599        i32::try_from(rect.min[1]).unwrap(),
600    ];
601    let size = [
602        i32::try_from(rect.width()).unwrap(),
603        i32::try_from(rect.height()).unwrap(),
604    ];
605
606    texture.update_texture_part(ctx, offset[0], offset[1], size[0], size[1], tex_data);
607}
608
609/// I THINK what we're going to need to do is have a
610/// `SpriteBatch` that actually does the stuff and stores the
611/// UV's and verts and such, while
612///
613/// Basically, `glyph_brush`'s "`to_vertex`" callback is really
614/// `to_quad`; in the default code it
615fn to_vertex(v: glyph_brush::GlyphVertex) -> DrawParam {
616    let src_rect = Rect {
617        x: v.tex_coords.min.x,
618        y: v.tex_coords.min.y,
619        w: v.tex_coords.max.x - v.tex_coords.min.x,
620        h: v.tex_coords.max.y - v.tex_coords.min.y,
621    };
622    // it LOOKS like pixel_coords are the output coordinates?
623    // I'm not sure though...
624    let dest_pt = Point2::new(v.pixel_coords.min.x, v.pixel_coords.min.y);
625    DrawParam::default()
626        .src(src_rect)
627        .dest(dest_pt)
628        .color(v.extra.color.into())
629}