//! This example demonstrates how to use `Text` to draw TrueType font texts efficiently. extern crate good_web_game as ggez; use ggez::event; use ggez::graphics::{self, Align, Color, DrawParam, Font, PxScale, Text, TextFragment}; use ggez::miniquad; use ggez::timer; use ggez::{Context, GameResult}; use glam::Vec2; use std::collections::BTreeMap; use std::env; /// Creates a random RGB color. fn random_color(rng: &mut oorandom::Rand32) -> Color { Color::new(rng.rand_float(), rng.rand_float(), rng.rand_float(), 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, Text>, rng: oorandom::Rand32, } impl App { #[allow(clippy::needless_update)] fn new(ctx: &mut Context) -> GameResult { let mut texts = BTreeMap::new(); // We just use a fixed RNG seed for simplicity. let mut rng = oorandom::Rand32::new(314159); // This is the simplest way to create a drawable text; // the color, font, and scale will be default: white, LiberationMono-Regular, 16px unform. // Note that you don't even have to load a font: LiberationMono-Regular is baked into `ggez` itself. let text = Text::new("Hello, World!"); // Store the text in `App`s map, for drawing in main loop. texts.insert("0_hello", text); // This is what actually happens in `Text::new()`: the `&str` gets // automatically converted into a `TextFragment`. let mut text = Text::new(TextFragment { // `TextFragment` stores a string, and optional parameters which will override those // of `Text` 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)), // `Font` is a handle to a loaded TTF, stored inside the `Context`. // `Font::default()` always exists and maps to LiberationMono-Regular. font: Some(graphics::Font::default()), scale: Some(PxScale::from(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(" default fragment, should be long enough to showcase everything") // `add()` can be chained, along with most `Text` methods. .add(TextFragment::new(" magenta fragment").color(Color::new(1.0, 0.0, 1.0, 1.0))) .add(" another default fragment, to really drive the point home"); // This loads a new TrueType font into the context and // returns a `Font` referring to it. let fancy_font = Font::new(ctx, "/Tangerine_Regular.ttf")?; // `Font` is really only an integer handle, and can be copied around. text.add( TextFragment::new(" fancy fragment") .font(fancy_font) .scale(PxScale::from(25.0)), ) .add(" 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. // Alignment within the bounds can be set by `Align` enum. text.set_bounds(Vec2::new(400.0, f32::INFINITY), Align::Left); texts.insert("1_demo_text_2", text.clone()); text.set_bounds(Vec2::new(500.0, f32::INFINITY), Align::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, PxScale::from(16.0)) .set_bounds(Vec2::new(300.0, f32::INFINITY), Align::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."; // `default()` exists pretty much specifically for this usecase. let mut chroma_text = Text::default(); for ch in chroma_string.chars() { chroma_text.add(TextFragment::new(ch).color(random_color(&mut rng))); } texts.insert("2_rainbow", chroma_text); let wonky_string = "So, so wonky."; let mut wonky_text = Text::default(); for ch in wonky_string.chars() { wonky_text .add(TextFragment::new(ch).scale(PxScale::from(10.0 + 24.0 * rng.rand_float()))); } texts.insert("3_wonky", wonky_text); Ok(App { texts, rng }) } } impl event::EventHandler for App { fn update( &mut self, ctx: &mut Context, _quad_ctx: &mut miniquad::GraphicsContext, ) -> GameResult { const DESIRED_FPS: u32 = 60; while timer::check_update_time(ctx, DESIRED_FPS) {} Ok(()) } fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); // `Text` 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::fps(ctx); let fps_display = Text::new(format!("FPS: {}", fps)); // When drawing through these calls, `DrawParam` will work as they are documented. graphics::draw( ctx, quad_ctx, &fps_display, (Vec2::new(200.0, 0.0), Color::WHITE), )?; let mut height = 0.0; for text in self.texts.values() { // Calling `.queue()` for all bits of text that can share a `DrawParam`, // followed with `::draw_queued()` with said params, is the intended way. graphics::queue_text(ctx, text, Vec2::new(20.0, 20.0 + height), None); //height += 20.0 + text.height(ctx) as f32; 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. graphics::draw_queued_text( ctx, quad_ctx, DrawParam::default(), None, graphics::FilterMode::Linear, )?; // Individual fragments within the `Text` 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_mut()` returns a mutable slice of contained fragments. // Fragments are indexed in order of their addition, starting at 0 (of course). text.fragments_mut()[3].color = Some(random_color(&mut self.rng)); } // Another animation example. Note, this is very inefficient as-is. let wobble_string = "WOBBLE"; let mut wobble = Text::default(); for ch in wobble_string.chars() { wobble.add( TextFragment::new(ch).scale(PxScale::from(10.0 + 6.0 * self.rng.rand_float())), ); } let wobble_width = wobble.width(ctx); let wobble_height = wobble.height(ctx); graphics::queue_text( ctx, &wobble, Vec2::new(0.0, 0.0), Some(Color::new(0.0, 1.0, 1.0, 1.0)), ); let t = Text::new(format!( "width: {}\nheight: {}", wobble_width, wobble_height )); graphics::queue_text(ctx, &t, Vec2::new(0.0, 20.0), None); graphics::draw_queued_text( ctx, quad_ctx, DrawParam::new() .dest(Vec2::new(500.0, 300.0)) .rotation(-0.5), None, graphics::FilterMode::Linear, )?; graphics::present(ctx, quad_ctx)?; //timer::yield_now(); Ok(()) } fn resize_event( &mut self, ctx: &mut Context, _quad_ctx: &mut miniquad::GraphicsContext, width: f32, height: f32, ) { graphics::set_screen_coordinates(ctx, graphics::Rect::new(0.0, 0.0, width, height)) .unwrap(); } } pub fn main() -> GameResult { if cfg!(debug_assertions) && env::var("yes_i_really_want_debug_mode").is_err() { eprintln!( "Note: Release mode will improve performance greatly.\n \ e.g. use `cargo run --example text --release`" ); } ggez::start( ggez::conf::Conf::default().cache(Some(include_bytes!("resources.tar"))), |context, _quad_ctx| Box::new(App::new(context).unwrap()), ) }