mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 05:10:03 -04:00
Pixel-perfect fonts
This commit is contained in:
@@ -51,7 +51,10 @@ fn show_font_texture(texture: &Texture, gui: &mut Region) {
|
||||
"Font texture size: {} x {} (hover to zoom)",
|
||||
texture.width, texture.height
|
||||
)));
|
||||
let size = vec2(texture.width as f32, texture.height as f32);
|
||||
let mut size = vec2(texture.width as f32, texture.height as f32);
|
||||
if size.x > gui.width() {
|
||||
size *= gui.width() / size.x;
|
||||
}
|
||||
let interact = gui.reserve_space(size, None);
|
||||
let rect = interact.rect;
|
||||
let top_left = Vertex {
|
||||
@@ -118,10 +121,10 @@ pub struct Emigui {
|
||||
}
|
||||
|
||||
impl Emigui {
|
||||
pub fn new() -> Emigui {
|
||||
pub fn new(pixels_per_point: f32) -> Emigui {
|
||||
Emigui {
|
||||
last_input: Default::default(),
|
||||
data: Arc::new(layout::Data::new()),
|
||||
data: Arc::new(layout::Data::new(pixels_per_point)),
|
||||
style: Default::default(),
|
||||
stats: Default::default(),
|
||||
}
|
||||
@@ -182,7 +185,7 @@ impl Emigui {
|
||||
show_font_texture(self.texture(), gui);
|
||||
if *old_font_sizes != new_font_sizes {
|
||||
let mut new_data = (*self.data).clone();
|
||||
let fonts = Fonts::from_sizes(new_font_sizes);
|
||||
let fonts = Fonts::from_sizes(new_font_sizes, self.data.input.pixels_per_point);
|
||||
new_data.fonts = Arc::new(fonts);
|
||||
self.data = Arc::new(new_data);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ use crate::{
|
||||
|
||||
pub struct TextFragment {
|
||||
/// The start of each character, starting at zero.
|
||||
/// Unit: points.
|
||||
pub x_offsets: Vec<f32>,
|
||||
|
||||
/// 0 for the first line, n * line_spacing for the rest
|
||||
/// Unit: points.
|
||||
pub y_offset: f32,
|
||||
pub text: String,
|
||||
}
|
||||
@@ -27,22 +30,24 @@ impl TextFragment {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct UvRect {
|
||||
/// X/Y offset for nice rendering
|
||||
pub offset: (i16, i16),
|
||||
/// X/Y offset for nice rendering (unit: points).
|
||||
pub offset: Vec2,
|
||||
pub size: Vec2,
|
||||
|
||||
/// Top left corner.
|
||||
/// Top left corner UV in texture.
|
||||
pub min: (u16, u16),
|
||||
|
||||
/// Inclusive
|
||||
/// Bottom right corner (exclusive).
|
||||
pub max: (u16, u16),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GlyphInfo {
|
||||
id: rusttype::GlyphId,
|
||||
|
||||
/// Unit: points.
|
||||
pub advance_width: f32,
|
||||
|
||||
/// Texture coordinates. None for space.
|
||||
@@ -53,24 +58,28 @@ pub struct GlyphInfo {
|
||||
const FIRST_ASCII: usize = 32; // 32 == space
|
||||
const LAST_ASCII: usize = 126;
|
||||
|
||||
/// The interface uses points as the unit for everything.
|
||||
#[derive(Clone)]
|
||||
pub struct Font {
|
||||
font: rusttype::Font<'static>,
|
||||
/// Maximum character height
|
||||
scale: f32,
|
||||
scale_in_pixels: f32,
|
||||
pixels_per_point: f32,
|
||||
/// NUM_CHARS big
|
||||
glyph_infos: Vec<GlyphInfo>,
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(atlas: Arc<Mutex<TextureAtlas>>, font_data: &'static [u8], scale: f32) -> Font {
|
||||
pub fn new(
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
font_data: &'static [u8],
|
||||
scale_in_points: f32,
|
||||
pixels_per_point: f32,
|
||||
) -> Font {
|
||||
let font = rusttype::Font::from_bytes(font_data).expect("Error constructing Font");
|
||||
|
||||
// println!(
|
||||
// "font.v_metrics: {:?}",
|
||||
// font.v_metrics(Scale::uniform(scale))
|
||||
// );
|
||||
let scale_in_pixels = pixels_per_point * scale_in_points;
|
||||
|
||||
let glyphs: Vec<_> = Self::supported_characters()
|
||||
.map(|c| {
|
||||
@@ -81,7 +90,7 @@ impl Font {
|
||||
"Failed to find a glyph for the character '{}'",
|
||||
c
|
||||
);
|
||||
let glyph = glyph.scaled(Scale::uniform(scale));
|
||||
let glyph = glyph.scaled(Scale::uniform(scale_in_pixels));
|
||||
glyph.positioned(point(0.0, 0.0))
|
||||
})
|
||||
.collect();
|
||||
@@ -90,7 +99,7 @@ impl Font {
|
||||
let mut atlas_lock = atlas.lock().unwrap();
|
||||
|
||||
for glyph in glyphs {
|
||||
if let Some(bb) = glyph.pixel_bounding_box() {
|
||||
let uv = if let Some(bb) = glyph.pixel_bounding_box() {
|
||||
let glyph_width = bb.width() as usize;
|
||||
let glyph_height = bb.height() as usize;
|
||||
assert!(glyph_width >= 1);
|
||||
@@ -107,41 +116,53 @@ impl Font {
|
||||
}
|
||||
});
|
||||
|
||||
let offset_y = scale as i16 + bb.min.y as i16 - 4; // TODO: use font.v_metrics
|
||||
glyph_infos.push(GlyphInfo {
|
||||
id: glyph.id(),
|
||||
advance_width: glyph.unpositioned().h_metrics().advance_width,
|
||||
uv: Some(UvRect {
|
||||
offset: (bb.min.x as i16, offset_y as i16),
|
||||
min: (glyph_pos.0 as u16, glyph_pos.1 as u16),
|
||||
max: (
|
||||
(glyph_pos.0 + glyph_width - 1) as u16,
|
||||
(glyph_pos.1 + glyph_height - 1) as u16,
|
||||
),
|
||||
}),
|
||||
});
|
||||
let offset_y_in_pixels =
|
||||
scale_in_pixels as f32 + bb.min.y as f32 - 4.0 * pixels_per_point; // TODO: use font.v_metrics
|
||||
Some(UvRect {
|
||||
offset: vec2(
|
||||
bb.min.x as f32 / pixels_per_point,
|
||||
offset_y_in_pixels / pixels_per_point,
|
||||
),
|
||||
size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point,
|
||||
min: (glyph_pos.0 as u16, glyph_pos.1 as u16),
|
||||
max: (
|
||||
(glyph_pos.0 + glyph_width) as u16,
|
||||
(glyph_pos.1 + glyph_height) as u16,
|
||||
),
|
||||
})
|
||||
} else {
|
||||
// No bounding box. Maybe a space?
|
||||
glyph_infos.push(GlyphInfo {
|
||||
id: glyph.id(),
|
||||
advance_width: glyph.unpositioned().h_metrics().advance_width,
|
||||
uv: None,
|
||||
});
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
let advance_width_in_points =
|
||||
glyph.unpositioned().h_metrics().advance_width / pixels_per_point;
|
||||
|
||||
glyph_infos.push(GlyphInfo {
|
||||
id: glyph.id(),
|
||||
advance_width: advance_width_in_points,
|
||||
uv,
|
||||
});
|
||||
}
|
||||
|
||||
drop(atlas_lock);
|
||||
|
||||
Font {
|
||||
font,
|
||||
scale,
|
||||
scale_in_pixels,
|
||||
pixels_per_point,
|
||||
glyph_infos,
|
||||
atlas,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||
}
|
||||
|
||||
/// In points
|
||||
pub fn line_spacing(&self) -> f32 {
|
||||
self.scale
|
||||
self.scale_in_pixels / self.pixels_per_point
|
||||
}
|
||||
|
||||
pub fn supported_characters() -> impl Iterator<Item = char> {
|
||||
@@ -168,7 +189,7 @@ impl Font {
|
||||
|
||||
/// Returns the a single line of characters separated into words
|
||||
pub fn layout_single_line(&self, text: &str) -> Vec<TextFragment> {
|
||||
let scale = Scale::uniform(self.scale);
|
||||
let scale_in_pixels = Scale::uniform(self.scale_in_pixels);
|
||||
|
||||
let mut current_fragment = TextFragment {
|
||||
x_offsets: vec![0.0],
|
||||
@@ -176,16 +197,19 @@ impl Font {
|
||||
text: String::new(),
|
||||
};
|
||||
let mut all_fragments = vec![];
|
||||
let mut cursor_x = 0.0f32;
|
||||
let mut cursor_x_in_points = 0.0f32;
|
||||
let mut last_glyph_id = None;
|
||||
|
||||
for c in text.chars() {
|
||||
if let Some(glyph) = self.glyph_info(c) {
|
||||
if let Some(last_glyph_id) = last_glyph_id {
|
||||
cursor_x += self.font.pair_kerning(scale, last_glyph_id, glyph.id)
|
||||
cursor_x_in_points +=
|
||||
self.font
|
||||
.pair_kerning(scale_in_pixels, last_glyph_id, glyph.id)
|
||||
/ self.pixels_per_point
|
||||
}
|
||||
cursor_x += glyph.advance_width;
|
||||
cursor_x = cursor_x.round();
|
||||
cursor_x_in_points += glyph.advance_width;
|
||||
cursor_x_in_points = self.round_to_pixel(cursor_x_in_points);
|
||||
last_glyph_id = Some(glyph.id);
|
||||
|
||||
let is_space = glyph.uv.is_none();
|
||||
@@ -194,14 +218,14 @@ impl Font {
|
||||
if !current_fragment.text.is_empty() {
|
||||
all_fragments.push(current_fragment);
|
||||
current_fragment = TextFragment {
|
||||
x_offsets: vec![cursor_x],
|
||||
x_offsets: vec![cursor_x_in_points],
|
||||
y_offset: 0.0,
|
||||
text: String::new(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_fragment.text.push(c);
|
||||
current_fragment.x_offsets.push(cursor_x);
|
||||
current_fragment.x_offsets.push(cursor_x_in_points);
|
||||
}
|
||||
} else {
|
||||
// Ignore unknown glyph
|
||||
@@ -214,9 +238,13 @@ impl Font {
|
||||
all_fragments
|
||||
}
|
||||
|
||||
pub fn layout_single_line_max_width(&self, text: &str, max_width: f32) -> Vec<TextFragment> {
|
||||
pub fn layout_single_line_max_width(
|
||||
&self,
|
||||
text: &str,
|
||||
max_width_in_points: f32,
|
||||
) -> Vec<TextFragment> {
|
||||
let mut words = self.layout_single_line(text);
|
||||
if words.is_empty() || words.last().unwrap().max_x() <= max_width {
|
||||
if words.is_empty() || words.last().unwrap().max_x() <= max_width_in_points {
|
||||
return words; // Early-out
|
||||
}
|
||||
|
||||
@@ -227,7 +255,7 @@ impl Font {
|
||||
let mut cursor_y = 0.0;
|
||||
|
||||
for word in words.iter_mut().skip(1) {
|
||||
if word.max_x() - line_start_x >= max_width {
|
||||
if word.max_x() - line_start_x >= max_width_in_points {
|
||||
// Time for a new line:
|
||||
cursor_y += line_spacing;
|
||||
line_start_x = word.min_x();
|
||||
@@ -243,12 +271,16 @@ impl Font {
|
||||
}
|
||||
|
||||
/// Returns each line + total bounding box size.
|
||||
pub fn layout_multiline(&self, text: &str, max_width: f32) -> (Vec<TextFragment>, Vec2) {
|
||||
pub fn layout_multiline(
|
||||
&self,
|
||||
text: &str,
|
||||
max_width_in_points: f32,
|
||||
) -> (Vec<TextFragment>, Vec2) {
|
||||
let line_spacing = self.line_spacing();
|
||||
let mut cursor_y = 0.0;
|
||||
let mut text_fragments = Vec::new();
|
||||
for line in text.split('\n') {
|
||||
let mut line_fragments = self.layout_single_line_max_width(&line, max_width);
|
||||
let mut line_fragments = self.layout_single_line_max_width(&line, max_width_in_points);
|
||||
if let Some(last_word) = line_fragments.last() {
|
||||
let line_height = last_word.y_offset + line_spacing;
|
||||
for fragment in &mut line_fragments {
|
||||
@@ -259,7 +291,7 @@ impl Font {
|
||||
} else {
|
||||
cursor_y += line_spacing;
|
||||
}
|
||||
cursor_y = cursor_y.round();
|
||||
cursor_y = self.round_to_pixel(cursor_y);
|
||||
}
|
||||
|
||||
let mut widest_line = 0.0;
|
||||
@@ -270,80 +302,4 @@ impl Font {
|
||||
let bounding_size = vec2(widest_line, cursor_y);
|
||||
(text_fragments, bounding_size)
|
||||
}
|
||||
|
||||
pub fn debug_print_all_chars(&self) {
|
||||
let mut atlas_lock = self.atlas.lock().unwrap();
|
||||
let texture_mut = atlas_lock.texture_mut();
|
||||
|
||||
let max_width = 160;
|
||||
let scale = Scale::uniform(self.scale);
|
||||
let mut pixel_rows = vec![vec![0; max_width]; self.scale.ceil() as usize];
|
||||
let mut cursor_x = 0.0;
|
||||
let cursor_y = 0;
|
||||
let mut last_glyph_id = None;
|
||||
for c in Self::supported_characters() {
|
||||
if let Some(glyph) = self.glyph_info(c) {
|
||||
if let Some(last_glyph_id) = last_glyph_id {
|
||||
cursor_x += self.font.pair_kerning(scale, last_glyph_id, glyph.id)
|
||||
}
|
||||
if cursor_x + glyph.advance_width >= max_width as f32 {
|
||||
println!("{}", (0..max_width).map(|_| "X").collect::<String>());
|
||||
for row in pixel_rows {
|
||||
println!("{}", as_ascii(&row));
|
||||
}
|
||||
pixel_rows = vec![vec![0; max_width]; self.scale.ceil() as usize];
|
||||
cursor_x = 0.0;
|
||||
}
|
||||
if let Some(uv) = glyph.uv {
|
||||
for x in uv.min.0..=uv.max.0 {
|
||||
for y in uv.min.1..=uv.max.1 {
|
||||
let pixel = texture_mut[(x as usize, y as usize)];
|
||||
let rx = uv.offset.0 + x as i16 - uv.min.0 as i16;
|
||||
let ry = uv.offset.1 + y as i16 - uv.min.1 as i16;
|
||||
let px = (cursor_x + rx as f32).round();
|
||||
let py = cursor_y + ry;
|
||||
if 0.0 <= px && 0 <= py {
|
||||
pixel_rows[py as usize][px as usize] = pixel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor_x += glyph.advance_width;
|
||||
last_glyph_id = Some(glyph.id);
|
||||
}
|
||||
}
|
||||
println!("{}", (0..max_width).map(|_| "X").collect::<String>());
|
||||
}
|
||||
}
|
||||
|
||||
fn as_ascii(pixels: &[u8]) -> String {
|
||||
pixels
|
||||
.iter()
|
||||
.map(|pixel| {
|
||||
if *pixel == 0 {
|
||||
' '
|
||||
} else if *pixel < 85 {
|
||||
'.'
|
||||
} else if *pixel < 170 {
|
||||
'o'
|
||||
} else if *pixel < 255 {
|
||||
'O'
|
||||
} else {
|
||||
'X'
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn font_test() {
|
||||
let atlas = TextureAtlas::new(128, 8);
|
||||
let atlas = Arc::new(Mutex::new(atlas));
|
||||
let font_data = include_bytes!("../fonts/Roboto-Regular.ttf");
|
||||
let font = Font::new(atlas, font_data, 13.0);
|
||||
font.debug_print_all_chars();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,22 +21,24 @@ pub enum TextStyle {
|
||||
pub type FontSizes = BTreeMap<TextStyle, f32>;
|
||||
|
||||
pub struct Fonts {
|
||||
pixels_per_point: f32,
|
||||
sizes: FontSizes,
|
||||
fonts: BTreeMap<TextStyle, Font>,
|
||||
texture: Texture,
|
||||
}
|
||||
|
||||
impl Fonts {
|
||||
pub fn new() -> Fonts {
|
||||
pub fn new(pixels_per_point: f32) -> Fonts {
|
||||
let mut sizes = FontSizes::new();
|
||||
sizes.insert(TextStyle::Body, 18.0);
|
||||
sizes.insert(TextStyle::Button, 22.0);
|
||||
sizes.insert(TextStyle::Heading, 28.0);
|
||||
Fonts::from_sizes(sizes)
|
||||
Fonts::from_sizes(sizes, pixels_per_point)
|
||||
}
|
||||
|
||||
pub fn from_sizes(sizes: FontSizes) -> Fonts {
|
||||
pub fn from_sizes(sizes: FontSizes, pixels_per_point: f32) -> Fonts {
|
||||
let mut fonts = Fonts {
|
||||
pixels_per_point,
|
||||
sizes: Default::default(),
|
||||
fonts: Default::default(),
|
||||
texture: Default::default(),
|
||||
@@ -69,7 +71,12 @@ impl Fonts {
|
||||
self.sizes = sizes.clone();
|
||||
self.fonts = sizes
|
||||
.into_iter()
|
||||
.map(|(text_style, size)| (text_style, Font::new(atlas.clone(), typeface_data, size)))
|
||||
.map(|(text_style, size)| {
|
||||
(
|
||||
text_style,
|
||||
Font::new(atlas.clone(), typeface_data, size, self.pixels_per_point),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
self.texture = atlas.lock().unwrap().texture().clone();
|
||||
|
||||
|
||||
@@ -189,10 +189,10 @@ impl Clone for Data {
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn new() -> Data {
|
||||
pub fn new(pixels_per_point: f32) -> Data {
|
||||
Data {
|
||||
options: Default::default(),
|
||||
fonts: Arc::new(Fonts::new()),
|
||||
fonts: Arc::new(Fonts::new(pixels_per_point)),
|
||||
input: Default::default(),
|
||||
memory: Default::default(),
|
||||
graphics: Default::default(),
|
||||
@@ -319,6 +319,10 @@ impl Region {
|
||||
self.dir
|
||||
}
|
||||
|
||||
pub fn cursor(&self) -> Vec2 {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Sub-regions:
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@ impl Vec2 {
|
||||
}
|
||||
|
||||
impl std::ops::AddAssign for Vec2 {
|
||||
fn add_assign(&mut self, other: Vec2) {
|
||||
fn add_assign(&mut self, rhs: Vec2) {
|
||||
*self = Vec2 {
|
||||
x: self.x + other.x,
|
||||
y: self.y + other.y,
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,13 @@ impl std::ops::Sub for Vec2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::MulAssign<f32> for Vec2 {
|
||||
fn mul_assign(&mut self, rhs: f32) {
|
||||
self.x *= rhs;
|
||||
self.y *= rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f32> for Vec2 {
|
||||
type Output = Vec2;
|
||||
fn mul(self, factor: f32) -> Vec2 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const ANTI_ALIAS: bool = true;
|
||||
const AA_SIZE: f32 = 1.0;
|
||||
const AA_SIZE: f32 = 0.5; // TODO: 1.0 / pixels_per_point
|
||||
|
||||
/// Outputs render info in a format suitable for e.g. OpenGL.
|
||||
use crate::{
|
||||
@@ -336,23 +336,15 @@ impl Frame {
|
||||
for (c, x_offset) in text.chars().zip(x_offsets.iter()) {
|
||||
if let Some(glyph) = font.uv_rect(c) {
|
||||
let mut top_left = Vertex {
|
||||
pos: *pos
|
||||
+ vec2(
|
||||
x_offset + (glyph.offset.0 as f32),
|
||||
glyph.offset.1 as f32,
|
||||
),
|
||||
uv: (glyph.min.0, glyph.min.1),
|
||||
pos: *pos + glyph.offset + vec2(*x_offset, 0.0),
|
||||
uv: glyph.min,
|
||||
color: *color,
|
||||
};
|
||||
top_left.pos.x = top_left.pos.x.round(); // Pixel-perfection.
|
||||
top_left.pos.y = top_left.pos.y.round(); // Pixel-perfection.
|
||||
top_left.pos.x = font.round_to_pixel(top_left.pos.x); // Pixel-perfection.
|
||||
top_left.pos.y = font.round_to_pixel(top_left.pos.y); // Pixel-perfection.
|
||||
let bottom_right = Vertex {
|
||||
pos: top_left.pos
|
||||
+ vec2(
|
||||
(1 + glyph.max.0 - glyph.min.0) as f32,
|
||||
(1 + glyph.max.1 - glyph.min.1) as f32,
|
||||
),
|
||||
uv: (glyph.max.0 + 1, glyph.max.1 + 1),
|
||||
pos: top_left.pos + glyph.size,
|
||||
uv: glyph.max,
|
||||
color: *color,
|
||||
};
|
||||
frame.add_rect(top_left, bottom_right);
|
||||
|
||||
@@ -17,6 +17,9 @@ pub struct RawInput {
|
||||
|
||||
/// Size of the screen in points.
|
||||
pub screen_size: Vec2,
|
||||
|
||||
/// Also known as device pixel ratio, > 1 for HDPI screens.
|
||||
pub pixels_per_point: f32,
|
||||
}
|
||||
|
||||
/// What the gui maintains
|
||||
@@ -36,6 +39,9 @@ pub struct GuiInput {
|
||||
|
||||
/// Size of the screen in points.
|
||||
pub screen_size: Vec2,
|
||||
|
||||
/// Also known as device pixel ratio, > 1 for HDPI screens.
|
||||
pub pixels_per_point: f32,
|
||||
}
|
||||
|
||||
impl GuiInput {
|
||||
@@ -46,6 +52,7 @@ impl GuiInput {
|
||||
mouse_released: last.mouse_down && !new.mouse_down,
|
||||
mouse_pos: new.mouse_pos,
|
||||
screen_size: new.screen_size,
|
||||
pixels_per_point: new.pixels_per_point,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user