Browse Source

Add color to rendering and write docs

main
Jens Pitkänen 7 months ago
parent
commit
ac664cd1ae
6 changed files with 162 additions and 51 deletions
  1. +44
    -16
      README.md
  2. +4
    -2
      examples/basic.rs
  3. +20
    -11
      examples/testbed.rs
  4. BIN
      misc/screenshot-01.png
  5. +87
    -20
      src/lib.rs
  6. +7
    -2
      src/rect_allocator.rs

+ 44
- 16
README.md View File

@ -1,34 +1,62 @@
# fontdue-sdl2
A crate for drawing text with [sdl2::render][sdl2::render], using
[fontdue][fontdue] for rasterization. This library is mostly
glue-code, all the good parts are from the aforementioned libraries.
[fontdue][fontdue] for rasterization. This library is glue-code, all
the good parts are from the aforementioned libraries.
The motivation for this crate is to allow easier, rustier, and better
quality text rendering in projects that use SDL2's render module,
compared to sdl2_ttf.
Note that fontdue is already a "full solution to text rendering", it's
a very application-facing crate. This crate exists so that I don't
need to write a text rendering cache for each SDL2 project I work on,
and that part isn't implemented in fontdue probably because it varies
based on rendering technology. This is a solution for fontdue + SDL2.
## Documentation
Read the docs on [docs.rs][docs].
## Design decisions
## Compared to sdl2_ttf
This library draws each glyph as its own quad, from a single gylph
cache texture. This is very fast on modern GPUs, as it can be done in
a single draw call. Per-text-area caching can still be achieved by
rendering this library's results into a render texture.
## Performance and shortcomings
Reserving a spot on the glyph cache texture is currently proportional
to the amount of previously allocated glyphs, so it gets slower over
time. Some ad-hoc testing shows about 20 microseconds per glyph in
release, up to 200 microseconds in debug, for a few paragraphs at
different sizes. This rarely affects overall performance badly
however, since this only needs to be done once per
character/size/color/font combo. But there is still a lot of room for
optimization.
Currently the crate is still a work in progress, so there are a few
missing features:
- The glyph cache texture isn't resized on the fly, the texture is
always 1024x1024.
- Unused glyphs can't be overwritten, every glyph that gets written
will exist in the cache forever.
(The library isn't ready yet, but this is the idea I'm working towards.)
Removing unused glyphs (and detecting them, for that matter) may be
too performance intensive in the end, so I may release a 1.0 after I
implement on-the-fly texture resizing. Manual clearing of the cache
might also be added, though the usefulness of that is questionable, as
you could simply create a new FontTexture for a similar effect.
- This library consumes fontdue data (such as layout and fonts) and
then calls sdl2::render to render that on screen. This library only
owns caches, such as the LayoutCache and a glyph cache.
- Major difference to sdl2_ttf: this library draws each glyph as its
own quad, from a single gylph cache texture. This is very fast on
modern GPUs, as it can be done in a single draw call. Per-text-area
caching can still be achieved by rendering this library's results
into a render texture.
## Screenshot
## Screenshots
This mostly shows off fontdue (the text rasterization) and SDL2 (the
window and rendering), but I think rendering crates should have
screenshots for first impressions. This screenshot was taken of the
[testbed.rs](examples/testbed.rs) example.
These will mostly show off fontdue (the text) and SDL2 (the window),
but I think rendering crates should have screenshots for first
impressions. TODO.
![Screenshot of some text from Wikipedia rendered using this crate.](misc/screenshot-01.png)
## License


+ 4
- 2
examples/basic.rs View File

@ -5,6 +5,7 @@ use fontdue::Font;
use fontdue_sdl2::FontTexture;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::pixels::Color;
pub fn main() -> Result<(), String> {
env_logger::init();
@ -27,9 +28,10 @@ pub fn main() -> Result<(), String> {
let font = include_bytes!("roboto/Roboto-Regular.ttf") as &[u8];
let roboto_regular = Font::from_bytes(font, fontdue::FontSettings::default()).unwrap();
let fonts = &[roboto_regular];
let color = Color::RGB(0xFF, 0xFF, 0);
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(fonts, &TextStyle::new("Hello ", 32.0, 0));
layout.append(fonts, &TextStyle::new("world!", 16.0, 0));
layout.append(fonts, &TextStyle::with_user_data("Hello ", 32.0, 0, color));
layout.append(fonts, &TextStyle::with_user_data("world!", 16.0, 0, color));
// sdl2 / fontdue-sdl2:
canvas.clear();


+ 20
- 11
examples/testbed.rs View File

@ -45,6 +45,7 @@ pub fn main() -> Result<(), String> {
max_width: Some(780.0),
..LayoutSettings::default()
};
let mut show_glyph_cache = false;
let mut event_pump = sdl_context.event_pump()?;
@ -56,28 +57,36 @@ pub fn main() -> Result<(), String> {
keycode: Some(Keycode::Escape),
..
} => break 'running,
Event::KeyDown {
keycode: Some(Keycode::Space),
..
} => show_glyph_cache = !show_glyph_cache,
_ => {}
}
}
let (width, height) = canvas.output_size().unwrap();
let (width, _) = canvas.output_size().unwrap();
let c = Color::RGB(0, 0, 0);
let c2 = Color::RGB(0, 0x44, 0);
layout_settings.max_width = Some(width as f32 - 20.0);
layout.reset(&layout_settings);
layout.append(fonts, &TextStyle::new(TEXT_TITLE, 30.0, 2));
layout.append(fonts, &TextStyle::new(TEXT_SUBTITLE, 16.0, 0));
layout.append(fonts, &TextStyle::new(TEXT_START, 16.0, 1));
layout.append(fonts, &TextStyle::new(TEXT_TAIL, 16.0, 0));
layout.append(fonts, &TextStyle::new(TEXT_MORE_1, 64.0, 0));
layout.append(fonts, &TextStyle::new(TEXT_MORE_2, 12.0, 0));
layout.append(fonts, &TextStyle::with_user_data(TEXT_TITLE, 30.0, 2, c));
layout.append(fonts, &TextStyle::with_user_data(TEXT_SUBTITLE, 16.0, 0, c));
layout.append(fonts, &TextStyle::with_user_data(TEXT_START, 16.0, 1, c));
layout.append(fonts, &TextStyle::with_user_data(TEXT_TAIL, 16.0, 0, c));
layout.append(fonts, &TextStyle::with_user_data(TEXT_MORE_1, 16.0, 0, c2));
layout.append(fonts, &TextStyle::with_user_data(TEXT_MORE_2, 16.0, 0, c));
canvas.set_draw_color(Color::RGB(0xFF, 0xFF, 0xFE));
canvas.clear();
font_texture.draw_text(&mut canvas, fonts, layout.glyphs())?;
let glyph_cache_rect = Rect::new(width as i32 - 270, height as i32 - 270, 256, 256);
canvas.set_draw_color(Color::RGB(0xEE, 0xEE, 0xEE));
let _ = canvas.fill_rect(glyph_cache_rect);
let _ = canvas.copy(&font_texture.texture, None, glyph_cache_rect);
if show_glyph_cache {
let glyph_cache_rect = Rect::new(0, 0, 1024, 1024);
canvas.set_draw_color(Color::RGB(0xEE, 0xEE, 0xEE));
let _ = canvas.fill_rect(glyph_cache_rect);
let _ = canvas.copy(&font_texture.texture, None, glyph_cache_rect);
}
canvas.present();
}


BIN
misc/screenshot-01.png View File

Before After
Width: 802  |  Height: 627  |  Size: 87 KiB

+ 87
- 20
src/lib.rs View File

@ -1,4 +1,73 @@
use fontdue::layout::{GlyphPosition, GlyphRasterConfig};
//! # fontdue-sdl2
//!
//! This crate is glue code for rendering text with [sdl2][sdl2],
//! rasterized and laid out by [fontdue][fontdue].
//!
//! # Usage
//!
//! First, set up fontdue and layout some glyphs with the [`Color`]
//! included as user data:
//!
//! ```
//! # use fontdue::{Font, layout::{Layout, TextStyle, CoordinateSystem}};
//! # use fontdue_sdl2::FontTexture;
//! # use sdl2::pixels::Color;
//! let font = include_bytes!("../examples/roboto/Roboto-Regular.ttf") as &[u8];
//! let roboto_regular = Font::from_bytes(font, Default::default()).unwrap();
//! let fonts = &[roboto_regular];
//! let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
//! layout.append(fonts, &TextStyle::with_user_data(
//! "Hello, World!", // The text to lay out
//! 32.0, // The font size
//! 0, // The font index (Roboto Regular)
//! Color::RGB(0xFF, 0xFF, 0) // The color of the text
//! ));
//! ```
//!
//! Then draw them using a [`FontTexture`]. It needs a
//! [`TextureCreator`], just as any SDL [`Texture`].
//!
//! ```
//! # use fontdue::{Font, layout::{Layout, TextStyle, CoordinateSystem}};
//! # use fontdue_sdl2::FontTexture;
//! # use sdl2::pixels::Color;
//! # let sdl_context = sdl2::init().unwrap();
//! # let video_subsystem = sdl_context.video().unwrap();
//! # let window = video_subsystem.window("fontdue-sdl2 example", 800, 600).position_centered().build().unwrap();
//! # let mut canvas = window.into_canvas().build().unwrap();
//! # let texture_creator = canvas.texture_creator();
//! # let font = include_bytes!("../examples/roboto/Roboto-Regular.ttf") as &[u8];
//! # let roboto_regular = Font::from_bytes(font, Default::default()).unwrap();
//! # let fonts = &[roboto_regular];
//! # let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
//! # layout.append(fonts, &TextStyle::with_user_data(
//! # "Hello, World!", // The text to lay out
//! # 32.0, // The font size
//! # 0, // The font index (Roboto Regular)
//! # Color::RGB(0xFF, 0xFF, 0) // The color of the text
//! # ));
//! # canvas.clear();
//! let mut font_texture = FontTexture::new(&texture_creator).unwrap();
//! let _ = font_texture.draw_text(&mut canvas, fonts, layout.glyphs());
//! # canvas.present();
//! ```
//!
//! Note that drawing text can fail if there are issues with the
//! rendering setup. It's often valid to simply ignore, or crash.
//!
//! The [`FontTexture`] is intended to be created once, at the
//! beginning of your program, and then used throughout. Generally,
//! you should treat [`FontTexture`] similarly to the [`Font`]-slice
//! passed to fontdue, and associate each [`FontTexture`] with a
//! specific `&[Font]`. See the [`FontTexture`] documentation for more
//! information.
//!
//! See `examples/basic.rs` for a complete example program.
//!
//! [fontdue]: https://docs.rs/fontdue
//! [sdl2]: https://docs.rs/sdl2
use fontdue::layout::GlyphPosition;
use fontdue::Font;
use sdl2::pixels::{Color, PixelFormatEnum};
use sdl2::rect::Rect;
@ -7,7 +76,7 @@ use sdl2::render::{BlendMode, Canvas, RenderTarget, Texture, TextureCreator};
mod rect_allocator;
use rect_allocator::{CacheReservation, RectAllocator};
/// A text-rendering-enabled wrapper for [Texture].
/// A text-rendering-enabled wrapper for [`Texture`].
pub struct FontTexture<'r> {
/// The texture containing rendered glyphs in a tightly packed
/// manner.
@ -17,20 +86,18 @@ pub struct FontTexture<'r> {
}
impl FontTexture<'_> {
/// Creates a new [FontTexture] for rendering text.
/// Creates a new [`FontTexture`] for rendering text.
///
/// Consider the lifetimes of this structure and the given
/// [TextureCreator] as you would a
/// [Texture] created with one, that is why this
/// structure is named "FontTexture".
/// [`TextureCreator`] as you would a [`Texture`] created with
/// one, that is why this structure is named "FontTexture".
///
/// # Important note
///
/// Only use a single set of [Font]s for each
/// [FontTexture]. Glyphs with the same index but different font
/// are hard to differentiate, so using different sets of Fonts
/// when rendering with a single FontTexture will lead to wrong
/// results.
/// Only use a single `&[Font]` for each [`FontTexture`]. Glyphs
/// with the same index but different font are hard to
/// differentiate, so using different sets of Fonts when rendering
/// with a single FontTexture will lead to wrong results.
///
/// # Errors
///
@ -41,8 +108,8 @@ impl FontTexture<'_> {
use sdl2::render::TextureValueError::*;
let mut texture = match texture_creator.create_texture_streaming(
Some(PixelFormatEnum::RGBA8888),
256,
256,
1024,
1024,
) {
Ok(t) => t,
Err(WidthOverflows(_))
@ -54,7 +121,7 @@ impl FontTexture<'_> {
};
texture.set_blend_mode(BlendMode::Blend);
let rect_allocator = RectAllocator::new(256, 256);
let rect_allocator = RectAllocator::new(1024, 1024);
Ok(FontTexture {
texture,
@ -65,14 +132,14 @@ impl FontTexture<'_> {
/// Renders text to the given canvas, using the given fonts and
/// glyphs.
///
/// The canvas should be the same one that the [TextureCreator]
/// used in [FontTexture::new] was created from.
/// The canvas should be the same one that the [`TextureCreator`]
/// used in [`FontTexture::new`] was created from.
///
/// The font-slice should be the same one that is passed to
/// [Layout::append](fontdue::layout::Layout::append).
/// [`Layout::append`](fontdue::layout::Layout::append).
///
/// The glyphs should be from
/// [Layout::glyphs](fontdue::layout::Layout::glyphs).
/// [`Layout::glyphs`](fontdue::layout::Layout::glyphs).
///
/// # Errors
///
@ -86,7 +153,7 @@ impl FontTexture<'_> {
&mut self,
canvas: &mut Canvas<RT>,
fonts: &[Font],
glyphs: &[GlyphPosition],
glyphs: &[GlyphPosition<Color>],
) -> Result<(), String> {
struct RenderableGlyph {
texture_rect: Rect,
@ -110,7 +177,7 @@ impl FontTexture<'_> {
glyph.width as u32,
glyph.height as u32,
);
let color = Color::RGB(0x0, 0x0, 0x0);
let color = glyph.user_data;
match self.rect_allocator.get_rect_in_texture(*glyph) {
CacheReservation::AlreadyRasterized(texture_rect) => {


+ 7
- 2
src/rect_allocator.rs View File

@ -1,10 +1,12 @@
use fontdue::layout::{GlyphPosition, GlyphRasterConfig};
use sdl2::pixels::Color;
use sdl2::rect::Rect;
use std::collections::HashMap;
#[derive(Clone, PartialEq, Eq, Hash)]
struct GlyphKey {
glyph: GlyphRasterConfig,
color: Color,
}
pub enum CacheReservation {
@ -26,8 +28,11 @@ impl RectAllocator {
}
}
pub fn get_rect_in_texture(&mut self, glyph: GlyphPosition) -> CacheReservation {
let key = GlyphKey { glyph: glyph.key };
pub fn get_rect_in_texture(&mut self, glyph: GlyphPosition<Color>) -> CacheReservation {
let key = GlyphKey {
glyph: glyph.key,
color: glyph.user_data,
};
if let Some(already_reserved) = self.reserved_rects.get(&key) {
CacheReservation::AlreadyRasterized(*already_reserved)
} else if let Some(new_rect) = self.get_empty_slot(glyph.width as u32, glyph.height as u32)


Loading…
Cancel
Save