ggez 0.4.4

A lightweight game framework for making 2D games with minimum friction, inspired by Love2D.
Documentation
//! This example demonstrates how to use `TextCached` to draw TrueType font texts efficiently.
//! Powered by `gfx_glyph` crate.

extern crate ggez;
extern crate rand;

use ggez::conf::{WindowMode, WindowSetup};
use ggez::event;
use ggez::graphics::{self, Color, DrawParam, Font, FontId, HorizontalAlign as HAlign, Layout,
                     Point2, Scale, TextCached, TextFragment};
use ggez::timer;
use ggez::{Context, ContextBuilder, GameResult};
use std::collections::BTreeMap;
use std::env;
use std::f32;
use std::path;

/// Creates a random RGB color.
fn random_color() -> Color {
    Color::new(
        rand::random::<f32>(),
        rand::random::<f32>(),
        rand::random::<f32>(),
        1.0,
    )
}

struct App {
    // Doesn't have to be a `BTreeMap`; it's handy if you care about specific elements,
    // want to retrieve them by trivial handles, and have to preserve ordering.
    texts: BTreeMap<&'static str, TextCached>,
}

impl App {
    fn new(ctx: &mut Context) -> GameResult<App> {
        let mut texts = BTreeMap::new();

        // This is the simplest way to create a drawable text;
        // the color, font, and scale will be default: white, DejaVuSerif, 16px unform.
        // Note that you don't even have to load a font: DejaVuSerif is baked into `ggez` itself.
        let text = TextCached::new("Hello, World!")?;
        // Store the text in `App`s hashmap, for drawing in main loop.
        texts.insert("0_hello", text);

        // This is what actually happens in `TextCached::new()`: the `&str` gets automatically
        // converted into a `TextFragment`. There are several "convenience consructors"
        // like this, refer to `TextFragment` docs for a list.
        let mut text = TextCached::new(TextFragment {
            // `TextFragment` stores a string, and optional parameters which will override those
            // of `TextCached` itself. This allows inlining differently formatted lines, words,
            // or even individual letters, into the same block of text.
            text: "Small red fragment".to_string(),
            color: Some(Color::new(1.0, 0.0, 0.0, 1.0)),
            // `FontId` is a handle to a loaded TTF, stored inside the context.
            // `FontId::default()` always exists and maps to DejaVuSerif.
            font_id: Some(FontId::default()),
            scale: Some(Scale::uniform(10.0)),
            // This doesn't do anything at this point; can be used to omit fields in declarations.
            ..Default::default()
        })?;

        // More fragments can be appended at any time.
        text.add_fragment(" default fragment, should be long enough to showcase everything")
            // `add_fragment()` can be chained, along with most `TextCached` methods.
            .add_fragment((" magenta fragment", Color::new(1.0, 0.0, 1.0, 1.0)))
            .add_fragment(" another default fragment, to really drive the point home");

        // This loads a new TrueType font into the context and returns a
        // `Font::GlyphFont`, which can be used interchangeably with the
        // `FontId` it contains throughout most of `TextCached` interface.
        let fancy_font = Font::new_glyph_font(ctx, "/Tangerine_Regular.ttf")?;

        // `Font::GlyphFont` is really only a handle, and can be cloned around.
        text.add_fragment((" fancy fragment", fancy_font.clone(), Scale::uniform(25.0)))
            .add_fragment(" and a default one, for symmetry");
        // Store a copy of the built text, retain original for further modifications.
        texts.insert("1_demo_text_1", text.clone());

        // Text can be wrapped by setting it's bounds, in screen coordinates;
        // vertical bound will cut off the extra off the bottom.
        text.set_bounds(Point2::new(400.0, f32::INFINITY), None);
        texts.insert("1_demo_text_2", text.clone());

        // Alignment within the bounds can be set by optional `Layout`; refer to `gfx_glyph` docs.
        text.set_bounds(
            Point2::new(500.0, f32::INFINITY),
            Some(Layout::default().h_align(HAlign::Right)),
        );
        texts.insert("1_demo_text_3", text.clone());

        // This can be used to set the font and scale unformatted fragments will use.
        // Color is specified when drawing (or queueing), via `DrawParam`.
        // Side note: TrueType fonts aren't very consistent between themselves in terms
        // of apparent scale - this font with default scale will appear too small.
        text.set_font(fancy_font.clone(), Scale::uniform(16.0))
            .set_bounds(
                Point2::new(300.0, f32::INFINITY),
                Some(Layout::default().h_align(HAlign::Center)),
            );
        texts.insert("1_demo_text_4", text);

        // These methods can be combined to easily create a variety of simple effects.
        let chroma_string = "Not quite a rainbow.";
        // `new_empty()` exists pretty much specifically for this usecase.
        let mut chroma_text = TextCached::new_empty()?;
        for ch in chroma_string.chars() {
            chroma_text.add_fragment((ch.to_string(), random_color()));
        }
        texts.insert("2_rainbow", chroma_text);

        let wonky_string = "So, so wonky.";
        let mut wonky_text = TextCached::new_empty()?;
        for ch in wonky_string.chars() {
            wonky_text.add_fragment(TextFragment {
                text: ch.to_string(),
                scale: Some(Scale::uniform(10.0 + 24.0 * rand::random::<f32>())),
                ..Default::default()
            });
        }
        texts.insert("3_wonky", wonky_text);

        Ok(App { texts })
    }
}

impl event::EventHandler for App {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        const DESIRED_FPS: u32 = 60;
        while timer::check_update_time(ctx, DESIRED_FPS) {}
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        graphics::clear(ctx);

        // `TextCached` can be used in "immediate mode", but it's slightly less efficient
        // in most cases, and horrifically less efficient in a few select ones
        // (using `.width()` or `.height()`, for example).
        let fps = timer::get_fps(ctx);
        let fps_display = TextCached::new(format!("FPS: {}", fps))?;
        // When drawing through these calls, `DrawParam` will work as they are documented.
        graphics::draw(ctx, &fps_display, Point2::new(200.0, 0.0), 0.0)?;

        let mut height = 0.0;
        for (_key, text) in &self.texts {
            // Calling `.queue()` for all bits of text that can share a `DrawParam`,
            // followed with `::draw_queued()` with said params, is the intended way.
            text.queue(ctx, Point2::new(20.0, 20.0 + height), None);
            height += 20.0 + text.height(ctx) as f32;
        }
        // When drawing via `draw_queued()`, `.offset` in `DrawParam` will be
        // in screen coordinates, and `.color` will be ignored.
        TextCached::draw_queued(ctx, DrawParam::default())?;

        // Individual fragments within the `TextCached` can be replaced;
        // this can be used for inlining animated sentences, words, etc.
        if let Some(text) = self.texts.get_mut("1_demo_text_3") {
            // `.fragments()` returns a slice of contained fragments.
            let string = text.fragments()[3].text.clone();
            // Fragments are indexed in order of their creation, starting at 0 (of course).
            text.replace_fragment(3, (string, random_color()));
        }

        // Another animation example. Note, this is very inefficient as-is.
        let wobble_string = "WOBBLE";
        let mut wobble = TextCached::new_empty()?;
        for ch in wobble_string.chars() {
            wobble.add_fragment(TextFragment {
                text: ch.to_string(),
                scale: Some(Scale::uniform(10.0 + 6.0 * rand::random::<f32>())),
                ..Default::default()
            });
        }
        let wobble_width = wobble.width(ctx);
        let wobble_height = wobble.height(ctx);
        wobble.queue(
            ctx,
            Point2::new(0.0, 0.0),
            Some(Color::new(0.0, 1.0, 1.0, 1.0)),
        );
        TextCached::new(format!(
            "width: {}\nheight: {}",
            wobble_width, wobble_height
        ))?.queue(ctx, Point2::new(0.0, 20.0), None);
        TextCached::draw_queued(
            ctx,
            DrawParam {
                dest: Point2::new(500.0, 300.0),
                shear: Point2::new(-0.3, 0.0),
                rotation: -0.5,
                ..Default::default()
            },
        )?;

        graphics::present(ctx);
        timer::yield_now();
        Ok(())
    }

    fn resize_event(&mut self, ctx: &mut Context, width: u32, height: u32) {
        graphics::set_screen_coordinates(
            ctx,
            graphics::Rect::new(0.0, 0.0, width as f32, height as f32),
        ).unwrap();
    }
}

pub fn main() {
    let ctx = &mut ContextBuilder::new("text_cached", "ggez")
        .window_setup(
            WindowSetup::default()
                .title("Cached text example!")
                .resizable(true),
        )
        .window_mode(WindowMode::default().dimensions(640, 480))
        .build()
        .unwrap();

    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        let mut path = path::PathBuf::from(manifest_dir);
        path.push("resources");
        ctx.filesystem.mount(&path, true);
    }

    let state = &mut App::new(ctx).unwrap();
    if let Err(e) = event::run(ctx, state) {
        println!("Error encountered: {}", e);
    } else {
        println!("Game exited cleanly.");
    }
}