doukutsu-rs/src/ggez/graphics/text.rs

718 lines
23 KiB
Rust

use std::borrow::Cow;
use std::cell::RefCell;
use std::f32;
use std::fmt;
use std::io::Read;
use std::path;
use glyph_brush::{self, FontId, Layout, SectionText, VariedSection};
pub use glyph_brush::{HorizontalAlign as Align, rusttype::Scale};
use glyph_brush::GlyphPositioner;
use mint;
use crate::ggez::graphics::{BlendMode, Color, Drawable, DrawParam, FilterMode, Rect, WHITE, draw, Image, BackendSpec, GlBackendSpec, Point2};
use crate::ggez::{Context, GameResult};
use gfx::texture::ImageInfoCommon;
/// Default size for fonts.
pub const DEFAULT_FONT_SCALE: f32 = 16.0;
/// A handle referring to a loaded Truetype font.
///
/// This is just an integer referring to a loaded font stored in the
/// `Context`, so is cheap to copy. Note that fonts are cached and
/// currently never *removed* from the cache, since that would
/// invalidate the whole cache and require re-loading all the other
/// fonts. So, you do not want to load a font more than once.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Font {
font_id: FontId,
// Add DebugId? It makes Font::default() less convenient.
}
/// A piece of text with optional color, font and font scale information.
/// Drawing text generally involves one or more of these.
/// These options take precedence over any similar field/argument.
/// Implements `From` for `char`, `&str`, `String` and
/// `(String, Font, Scale)`.
#[derive(Clone, Debug)]
pub struct TextFragment {
/// Text string itself.
pub text: String,
/// Fragment's color, defaults to text's color.
pub color: Option<Color>,
/// Fragment's font, defaults to text's font.
pub font: Option<Font>,
/// Fragment's scale, defaults to text's scale.
pub scale: Option<Scale>,
}
impl Default for TextFragment {
fn default() -> Self {
TextFragment {
text: "".into(),
color: None,
font: None,
scale: None,
}
}
}
impl TextFragment {
/// Creates a new fragment from `String` or `&str`.
pub fn new<T: Into<Self>>(text: T) -> Self {
text.into()
}
/// Set fragment's color, overrides text's color.
pub fn color(mut self, color: Color) -> TextFragment {
self.color = Some(color);
self
}
/// Set fragment's font, overrides text's font.
pub fn font(mut self, font: Font) -> TextFragment {
self.font = Some(font);
self
}
/// Set fragment's scale, overrides text's scale.
pub fn scale(mut self, scale: Scale) -> TextFragment {
self.scale = Some(scale);
self
}
}
impl<'a> From<&'a str> for TextFragment {
fn from(text: &'a str) -> TextFragment {
TextFragment {
text: text.to_owned(),
..Default::default()
}
}
}
impl From<char> for TextFragment {
fn from(ch: char) -> TextFragment {
TextFragment {
text: ch.to_string(),
..Default::default()
}
}
}
impl From<String> for TextFragment {
fn from(text: String) -> TextFragment {
TextFragment {
text,
..Default::default()
}
}
}
impl<T> From<(T, Font, f32)> for TextFragment
where
T: Into<TextFragment>,
{
fn from((text, font, scale): (T, Font, f32)) -> TextFragment {
text.into().font(font).scale(Scale::uniform(scale))
}
}
/// Cached font metrics that we can keep attached to a `Text`
/// so we don't have to keep recalculating them.
#[derive(Clone, Debug)]
struct CachedMetrics {
string: Option<String>,
width: Option<u32>,
height: Option<u32>,
}
impl Default for CachedMetrics {
fn default() -> CachedMetrics {
CachedMetrics {
string: None,
width: None,
height: None,
}
}
}
/// Drawable text object. Essentially a list of [`TextFragment`](struct.TextFragment.html)'s
/// and some cached size information.
///
/// It implements [`Drawable`](trait.Drawable.html) so it can be drawn immediately with
/// [`graphics::draw()`](fn.draw.html), or many of them can be queued with [`graphics::queue_text()`](fn.queue_text.html)
/// and then all drawn at once with [`graphics::draw_queued_text()`](fn.draw_queued_text.html).
#[derive(Debug, Clone)]
pub struct Text {
fragments: Vec<TextFragment>,
blend_mode: Option<BlendMode>,
filter_mode: FilterMode,
bounds: Point2,
layout: Layout<glyph_brush::BuiltInLineBreaker>,
font_id: FontId,
font_scale: Scale,
cached_metrics: RefCell<CachedMetrics>,
}
impl Default for Text {
fn default() -> Self {
Text {
fragments: Vec::new(),
blend_mode: None,
filter_mode: FilterMode::Linear,
bounds: Point2::new(f32::INFINITY, f32::INFINITY),
layout: Layout::default(),
font_id: FontId::default(),
font_scale: Scale::uniform(DEFAULT_FONT_SCALE),
cached_metrics: RefCell::new(CachedMetrics::default()),
}
}
}
impl Text {
/// Creates a `Text` from a `TextFragment`.
///
/// ```rust
/// # use ggez::graphics::Text;
/// # fn main() {
/// let text = Text::new("foo");
/// # }
/// ```
pub fn new<F>(fragment: F) -> Text
where
F: Into<TextFragment>,
{
let mut text = Text::default();
let _ = text.add(fragment);
text
}
/// Appends a `TextFragment` to the `Text`.
pub fn add<F>(&mut self, fragment: F) -> &mut Text
where
F: Into<TextFragment>,
{
self.fragments.push(fragment.into());
self.invalidate_cached_metrics();
self
}
/// Returns a read-only slice of all `TextFragment`'s.
pub fn fragments(&self) -> &[TextFragment] {
&self.fragments
}
/// Returns a mutable slice with all fragments.
pub fn fragments_mut(&mut self) -> &mut [TextFragment] {
&mut self.fragments
}
/// Specifies rectangular dimensions to try and fit contents inside of,
/// by wrapping, and alignment within the bounds. To disable wrapping,
/// give it a layout with `f32::INF` for the x value.
pub fn set_bounds<P>(&mut self, bounds: P, alignment: Align) -> &mut Text
where
P: Into<mint::Point2<f32>>,
{
self.bounds = Point2::from(bounds.into());
if self.bounds.x == f32::INFINITY {
// Layouts don't make any sense if we don't wrap text at all.
self.layout = Layout::default();
} else {
self.layout = self.layout.h_align(alignment);
}
self.invalidate_cached_metrics();
self
}
/// Specifies text's font and font scale; used for fragments that don't have their own.
pub fn set_font(&mut self, font: Font, font_scale: Scale) -> &mut Text {
self.font_id = font.font_id;
self.font_scale = font_scale;
self.invalidate_cached_metrics();
self
}
/// Converts `Text` to a type `glyph_brush` can understand and queue.
fn generate_varied_section(
&self,
relative_dest: Point2,
color: Option<Color>,
) -> VariedSection {
let sections: Vec<SectionText> = self
.fragments
.iter()
.map(|fragment| {
let color = fragment.color.or(color).unwrap_or(WHITE);
let font_id = fragment
.font
.map(|font| font.font_id)
.unwrap_or(self.font_id);
let scale = fragment.scale.unwrap_or(self.font_scale);
SectionText {
text: &fragment.text,
color: <[f32; 4]>::from(color),
font_id,
scale,
}
})
.collect();
let relative_dest_x = {
// This positions text within bounds with relative_dest being to the left, always.
let mut dest_x = relative_dest.x;
if self.bounds.x != f32::INFINITY {
use glyph_brush::Layout::Wrap;
match self.layout {
Wrap {
h_align: Align::Center,
..
} => dest_x += self.bounds.x * 0.5,
Wrap {
h_align: Align::Right,
..
} => dest_x += self.bounds.x,
_ => (),
}
}
dest_x
};
let relative_dest = (relative_dest_x, relative_dest.y);
VariedSection {
screen_position: relative_dest,
bounds: (self.bounds.x, self.bounds.y),
layout: self.layout,
text: sections,
..Default::default()
}
}
fn invalidate_cached_metrics(&mut self) {
if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
*metrics = CachedMetrics::default();
// Returning early avoids a double-borrow in the "else"
// part.
return;
}
warn!("Cached metrics RefCell has been poisoned.");
self.cached_metrics = RefCell::new(CachedMetrics::default());
}
/// Returns the string that the text represents.
pub fn contents(&self) -> String {
if let Ok(metrics) = self.cached_metrics.try_borrow() {
if let Some(ref string) = metrics.string {
return string.clone();
}
}
let string_accm: String = self
.fragments
.iter()
.map(|frag| frag.text.as_str())
.collect();
if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
metrics.string = Some(string_accm.clone());
}
string_accm
}
/// Calculates, caches, and returns width and height of formatted and wrapped text.
fn calculate_dimensions(&self, context: &mut Context) -> (u32, u32) {
let mut max_width = 0;
let mut max_height = 0;
{
let varied_section = self.generate_varied_section(Point2::new(0.0, 0.0), None);
use glyph_brush::GlyphCruncher;
let glyphs = context.gfx_context.glyph_brush.glyphs(varied_section);
for positioned_glyph in glyphs {
if let Some(rect) = positioned_glyph.pixel_bounding_box() {
let font = positioned_glyph.font().expect("Glyph doesn't have a font");
let v_metrics = font.v_metrics(positioned_glyph.scale());
let max_y = positioned_glyph.position().y + positioned_glyph.scale().y
- v_metrics.ascent;
let max_y = max_y.ceil() as u32;
max_width = std::cmp::max(max_width, rect.max.x as u32);
max_height = std::cmp::max(max_height, max_y);
}
}
}
let (width, height) = (max_width, max_height);
if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
metrics.width = Some(width);
metrics.height = Some(height);
}
(width, height)
}
/// Returns the width and height of the formatted and wrapped text.
pub fn dimensions(&self, context: &mut Context) -> (u32, u32) {
if let Ok(metrics) = self.cached_metrics.try_borrow() {
if let (Some(width), Some(height)) = (metrics.width, metrics.height) {
return (width, height);
}
}
self.calculate_dimensions(context)
}
/// Returns the width of formatted and wrapped text, in screen coordinates.
pub fn width(&self, context: &mut Context) -> u32 {
self.dimensions(context).0
}
/// Returns the height of formatted and wrapped text, in screen coordinates.
pub fn height(&self, context: &mut Context) -> u32 {
self.dimensions(context).1
}
}
impl Drawable for Text {
fn draw(&self, ctx: &mut Context, param: DrawParam) -> GameResult {
// Converts fraction-of-bounding-box to screen coordinates, as required by `draw_queued()`.
queue_text(ctx, self, Point2::new(0.0, 0.0), Some(param.color));
draw_queued_text(ctx, param, self.blend_mode, self.filter_mode)
}
fn dimensions(&self, ctx: &mut Context) -> Option<Rect> {
let (w, h) = self.dimensions(ctx);
Some(Rect {
w: w as _,
h: h as _,
x: 0.0,
y: 0.0,
})
}
fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
self.blend_mode = mode;
}
fn blend_mode(&self) -> Option<BlendMode> {
self.blend_mode
}
}
impl Font {
/// Load a new TTF font from the given file.
pub fn new<P>(context: &mut Context, path: P) -> GameResult<Font>
where
P: AsRef<path::Path> + fmt::Debug,
{
use crate::filesystem;
let mut stream = filesystem::open(context, path.as_ref())?;
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf)?;
Font::new_glyph_font_bytes(context, &buf)
}
/// Loads a new TrueType font from given bytes and into a `gfx::GlyphBrush` owned
/// by the `Context`.
pub fn new_glyph_font_bytes(context: &mut Context, bytes: &[u8]) -> GameResult<Self> {
// Take a Cow here to avoid this clone where unnecessary?
// Nah, let's not complicate things more than necessary.
let v = bytes.to_vec();
let font_id = context.gfx_context.glyph_brush.add_font_bytes(v);
Ok(Font { font_id })
}
/// Returns the baked-in bytes of default font (currently `DejaVuSerif.ttf`).
pub(crate) fn default_font_bytes() -> &'static [u8] {
include_bytes!("DejaVuSansMono.ttf")
}
}
impl Default for Font {
fn default() -> Self {
Font { font_id: FontId(0) }
}
}
/// Queues the `Text` to be drawn by [`draw_queued_text()`](fn.draw_queued_text.html).
/// `relative_dest` is relative to the [`DrawParam::dest`](struct.DrawParam.html#structfield.dest)
/// passed to `draw_queued()`. Note, any `Text` drawn via [`graphics::draw()`](fn.draw.html)
/// will also draw everything already the queue.
pub fn queue_text<P>(context: &mut Context, batch: &Text, relative_dest: P, color: Option<Color>)
where
P: Into<mint::Point2<f32>>,
{
let p = Point2::from(relative_dest.into());
let varied_section = batch.generate_varied_section(p, color);
context.gfx_context.glyph_brush.queue(varied_section);
}
/// Exposes `glyph_brush`'s drawing API in case `ggez`'s text drawing is insufficient.
/// It takes `glyph_brush`'s `VariedSection` and `GlyphPositioner`, which give you lower-
/// level control over how text is drawn.
pub fn queue_text_raw<'a, S, G>(context: &mut Context, section: S, custom_layout: Option<&G>)
where
S: Into<Cow<'a, VariedSection<'a>>>,
G: GlyphPositioner,
{
let brush = &mut context.gfx_context.glyph_brush;
match custom_layout {
Some(layout) => brush.queue_custom_layout(section, layout),
None => brush.queue(section),
}
}
/// Draws all of the [`Text`](struct.Text.html)s added via [`queue_text()`](fn.queue_text.html).
///
/// the `DrawParam` applies to everything in the queue; offset is in
/// screen coordinates; color is ignored - specify it when using
/// `queue_text()` instead.
///
/// Note that all text will, and in fact must, be drawn with the same
/// `BlendMode` and `FilterMode`. This is unfortunate but currently
/// unavoidable, see [this issue](https://github.com/ggez/ggez/issues/561)
/// for more info.
pub fn draw_queued_text<D>(
ctx: &mut Context,
param: D,
blend: Option<BlendMode>,
filter: FilterMode,
) -> GameResult
where
D: Into<DrawParam>,
{
let param: DrawParam = param.into();
let gb = &mut ctx.gfx_context.glyph_brush;
let encoder = &mut ctx.gfx_context.encoder;
let gc = &ctx.gfx_context.glyph_cache.texture_handle;
let backend = &ctx.gfx_context.backend_spec;
let action = gb.process_queued(
|rect, tex_data| update_texture::<GlBackendSpec>(backend, encoder, gc, rect, tex_data),
to_vertex,
);
match action {
Ok(glyph_brush::BrushAction::ReDraw) => {
let spritebatch = ctx.gfx_context.glyph_state.clone();
let spritebatch = &mut *spritebatch.borrow_mut();
spritebatch.set_blend_mode(blend);
spritebatch.set_filter(filter);
draw(ctx, &*spritebatch, param)?;
}
Ok(glyph_brush::BrushAction::Draw(drawparams)) => {
// Gotta clone the image to avoid double-borrow's.
let spritebatch = ctx.gfx_context.glyph_state.clone();
let spritebatch = &mut *spritebatch.borrow_mut();
spritebatch.clear();
spritebatch.set_blend_mode(blend);
spritebatch.set_filter(filter);
for p in &drawparams {
// Ignore returned sprite index.
let _ = spritebatch.add(*p);
}
draw(ctx, &*spritebatch, param)?;
}
Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => {
let (new_width, new_height) = suggested;
let data = vec![255; 4 * new_width as usize * new_height as usize];
let new_glyph_cache =
Image::from_rgba8(ctx, new_width as u16, new_height as u16, &data)?;
ctx.gfx_context.glyph_cache = new_glyph_cache.clone();
let spritebatch = ctx.gfx_context.glyph_state.clone();
let spritebatch = &mut *spritebatch.borrow_mut();
let _ = spritebatch.set_image(new_glyph_cache);
ctx.gfx_context
.glyph_brush
.resize_texture(new_width, new_height);
}
}
Ok(())
}
fn update_texture<B>(
backend: &B,
encoder: &mut gfx::Encoder<B::Resources, B::CommandBuffer>,
texture: &gfx::handle::RawTexture<B::Resources>,
rect: glyph_brush::rusttype::Rect<u32>,
tex_data: &[u8],
) where
B: BackendSpec,
{
let offset = [rect.min.x as u16, rect.min.y as u16];
let size = [rect.width() as u16, rect.height() as u16];
let info = ImageInfoCommon {
xoffset: offset[0],
yoffset: offset[1],
zoffset: 0,
width: size[0],
height: size[1],
depth: 0,
format: (),
mipmap: 0,
};
let tex_data_chunks: Vec<[u8; 4]> = tex_data.iter().map(|c| [255, 255, 255, *c]).collect();
let typed_tex = backend.raw_to_typed_texture(texture.clone());
encoder
.update_texture::<<super::BuggoSurfaceFormat as gfx::format::Formatted>::Surface, super::BuggoSurfaceFormat>(
&typed_tex, None, info, &tex_data_chunks,
)
.unwrap();
}
/// I THINK what we're going to need to do is have a
/// `SpriteBatch` that actually does the stuff and stores the
/// UV's and verts and such, while
///
/// Basically, `glyph_brush`'s "`to_vertex`" callback is really
/// `to_quad`; in the default code it
fn to_vertex(v: glyph_brush::GlyphVertex) -> DrawParam {
let src_rect = Rect {
x: v.tex_coords.min.x,
y: v.tex_coords.min.y,
w: v.tex_coords.max.x - v.tex_coords.min.x,
h: v.tex_coords.max.y - v.tex_coords.min.y,
};
// it LOOKS like pixel_coords are the output coordinates?
// I'm not sure though...
let dest_pt = Point2::new(v.pixel_coords.min.x as f32, v.pixel_coords.min.y as f32);
DrawParam::default()
.src(src_rect)
.dest(dest_pt)
.color(v.color.into())
}
#[cfg(test)]
mod tests {
/*
use super::*;
#[test]
fn test_metrics() {
let f = Font::default_font().expect("Could not get default font");
assert_eq!(f.height(), 17);
assert_eq!(f.width("Foo!"), 33);
// http://www.catipsum.com/index.php
let text_to_wrap = "Walk on car leaving trail of paw prints on hood and windshield sniff \
other cat's butt and hang jaw half open thereafter for give attitude. \
Annoy kitten\nbrother with poking. Mrow toy mouse squeak roll over. \
Human give me attention meow.";
let (len, v) = f.wrap(text_to_wrap, 250);
println!("{} {:?}", len, v);
assert_eq!(len, 249);
/*
let wrapped_text = vec![
"Walk on car leaving trail of paw prints",
"on hood and windshield sniff other",
"cat\'s butt and hang jaw half open",
"thereafter for give attitude. Annoy",
"kitten",
"brother with poking. Mrow toy",
"mouse squeak roll over. Human give",
"me attention meow."
];
*/
let wrapped_text = vec![
"Walk on car leaving trail of paw",
"prints on hood and windshield",
"sniff other cat\'s butt and hang jaw",
"half open thereafter for give",
"attitude. Annoy kitten",
"brother with poking. Mrow toy",
"mouse squeak roll over. Human",
"give me attention meow.",
];
assert_eq!(&v, &wrapped_text);
}
// We sadly can't have this test in the general case because it needs to create a Context,
// which creates a window, which fails on a headless server like our CI systems. :/
//#[test]
#[allow(dead_code)]
fn test_wrapping() {
use conf;
let c = conf::Conf::new();
let (ctx, _) = &mut Context::load_from_conf("test_wrapping", "ggez", c)
.expect("Could not create context?");
let font = Font::default_font().expect("Could not get default font");
let text_to_wrap = "Walk on car leaving trail of paw prints on hood and windshield sniff \
other cat's butt and hang jaw half open thereafter for give attitude. \
Annoy kitten\nbrother with poking. Mrow toy mouse squeak roll over. \
Human give me attention meow.";
let wrap_length = 250;
let (len, v) = font.wrap(text_to_wrap, wrap_length);
assert!(len < wrap_length);
for line in &v {
let t = Text::new(ctx, line, &font).unwrap();
println!(
"Width is claimed to be <= {}, should be <= {}, is {}",
len,
wrap_length,
t.width()
);
// Why does this not match? x_X
//assert!(t.width() as usize <= len);
assert!(t.width() as usize <= wrap_length);
}
}
*/
}
/*
// Creates a gfx texture with the given data
fn create_texture<F, R>(
factory: &mut F,
width: u32,
height: u32,
) -> Result<(TexSurfaceHandle<R>, TexShaderView<R>), Box<dyn Error>>
where
R: gfx::Resources,
F: gfx::Factory<R>,
{
let kind = texture::Kind::D2(
width as texture::Size,
height as texture::Size,
texture::AaMode::Single,
);
let tex = factory.create_texture(
kind,
1 as texture::Level,
gfx::memory::Bind::SHADER_RESOURCE,
gfx::memory::Usage::Dynamic,
Some(<TexChannel as format::ChannelTyped>::get_channel_type()),
)?;
let view =
factory.view_texture_as_shader_resource::<TexForm>(&tex, (0, 0), format::Swizzle::new())?;
Ok((tex, view))
}
// Updates a texture with the given data (used for updating the GlyphCache texture)
#[inline]
fn update_texture<R, C>(
encoder: &mut gfx::Encoder<R, C>,
texture: &handle::Texture<R, TexSurface>,
offset: [u16; 2],
size: [u16; 2],
data: &[u8],
) where
R: gfx::Resources,
C: gfx::CommandBuffer<R>,
{
let info = texture::ImageInfoCommon {
xoffset: offset[0],
yoffset: offset[1],
zoffset: 0,
width: size[0],
height: size[1],
depth: 0,
format: (),
mipmap: 0,
};
encoder
.update_texture::<TexSurface, TexForm>(texture, None, info, data)
.unwrap();
}
*/