//! 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<App> {
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<ggez::GameError> 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()),
)
}