diff --git a/README.md b/README.md index e7189c37..bdd384a6 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,4 @@ Supports MARKDEEP. A place for you ideas. Stored on your computer (local storage # Credits / Licenses ProggyClean.ttf, Copyright (c) 2004, 2005 Tristan Grimmer. MIT License. http://www.proggyfonts.net/ +Roboto-Regular.ttf: Apache License, Version 2.0 diff --git a/docs/emgui_wasm_bg.wasm b/docs/emgui_wasm_bg.wasm index 3bc1b878..4aa49673 100644 Binary files a/docs/emgui_wasm_bg.wasm and b/docs/emgui_wasm_bg.wasm differ diff --git a/emgui/fonts/DejaVuSans.ttf b/emgui/fonts/DejaVuSans.ttf new file mode 100644 index 00000000..e5f7eecc Binary files /dev/null and b/emgui/fonts/DejaVuSans.ttf differ diff --git a/emgui/src/emgui.rs b/emgui/src/emgui.rs index d57a258c..aa71b94c 100644 --- a/emgui/src/emgui.rs +++ b/emgui/src/emgui.rs @@ -1,4 +1,4 @@ -use crate::{layout, style, types::*}; +use crate::{font::Font, layout, style, types::*}; /// Encapsulates input, layout and painting for ease of use. #[derive(Clone)] @@ -9,10 +9,10 @@ pub struct Emgui { } impl Emgui { - pub fn new() -> Emgui { + pub fn new(font: Font) -> Emgui { Emgui { last_input: Default::default(), - layout: layout::Layout::new(), + layout: layout::Layout::new(font), style: Default::default(), } } diff --git a/emgui/src/font.rs b/emgui/src/font.rs index df9619f1..55618ad1 100644 --- a/emgui/src/font.rs +++ b/emgui/src/font.rs @@ -3,14 +3,13 @@ use rusttype::{point, Scale}; #[derive(Clone, Copy, Debug, PartialEq)] -pub struct GlyphInfo { - /// X offset for nice rendering - pub offset_x: u16, +pub struct UvRect { + /// X/Y offset for nice rendering + pub offset_x: i16, /// Y offset for nice rendering - pub offset_y: u16, + pub offset_y: i16, - // Texture coordinates: pub min_x: u16, pub min_y: u16, @@ -21,19 +20,29 @@ pub struct GlyphInfo { pub max_y: u16, } -/// Printable ascii characters [33, 126], which excludes 32 (space) and 127 (DEL) -const NUM_CHARS: usize = 94; -const FIRST_ASCII: usize = 33; -/// Inclusive +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GlyphInfo { + id: rusttype::GlyphId, + + pub advance_width: f32, + + /// Texture coordinates. None for space. + pub uv: Option, +} + +/// Printable ASCII characters [32, 126], which excludes control codes. +const FIRST_ASCII: usize = 32; // 32 == space const LAST_ASCII: usize = 126; +const NUM_CHARS: usize = LAST_ASCII - FIRST_ASCII + 1; // TODO: break out texture atlas into separate struct, and fill it dynamically, potentially from multiple fonts. #[derive(Clone)] pub struct Font { + font: rusttype::Font<'static>, /// Maximum character height scale: usize, /// NUM_CHARS big - char_rects: Vec, + glyph_infos: Vec, atlas_width: usize, atlas_height: usize, atlas: Vec, @@ -41,7 +50,8 @@ pub struct Font { impl Font { pub fn new(scale: usize) -> Font { - let font_data = include_bytes!("../fonts/ProggyClean.ttf"); + // let font_data = include_bytes!("../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE. + let font_data = include_bytes!("../fonts/DejaVuSans.ttf"); // 20 works nicely for this. let font = rusttype::Font::from_bytes(font_data as &[u8]).expect("Error constructing Font"); // println!( @@ -76,66 +86,79 @@ impl Font { let mut cursor_y = 0; let mut row_height = 1; - let mut char_rects = vec![]; + let mut glyph_infos = vec![]; for glyph in glyphs { - let bb = glyph - .pixel_bounding_box() - .expect("Failed to get pixel bounding box"); - let glyph_width = bb.width() as usize; - let glyph_height = bb.height() as usize; - assert!(glyph_width >= 1); - assert!(glyph_height >= 1); - assert!(glyph_width <= atlas_width); - if cursor_x + glyph_width > atlas_width { - // New row: - cursor_x = 0; - cursor_y += row_height; - row_height = 0; - } - - row_height = row_height.max(glyph_height); - while cursor_y + row_height >= atlas_height { - atlas_height *= 2; - } - if atlas_width * atlas_height > atlas.len() { - atlas.resize(atlas_width * atlas_height, 0); - } - - glyph.draw(|x, y, v| { - if v > 0.0 { - let x = x as usize; - let y = y as usize; - let px = cursor_x + x as usize; - let py = cursor_y + y as usize; - atlas[py * atlas_width + px] = (v * 255.0).round() as u8; + 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); + assert!(glyph_height >= 1); + assert!(glyph_width <= atlas_width); + if cursor_x + glyph_width > atlas_width { + // New row: + cursor_x = 0; + cursor_y += row_height; + row_height = 0; } - }); - let offset_y = scale as i32 + bb.min.y - 3; // TODO: use font.v_metrics - assert!(0 <= bb.min.x); - assert!(0 <= offset_y && offset_y < scale as i32); - char_rects.push(GlyphInfo { - offset_x: bb.min.x as u16, - offset_y: offset_y as u16, - min_x: cursor_x as u16, - min_y: cursor_y as u16, - max_x: (cursor_x + glyph_width - 1) as u16, - max_y: (cursor_y + glyph_height - 1) as u16, - }); + row_height = row_height.max(glyph_height); + while cursor_y + row_height >= atlas_height { + atlas_height *= 2; + } + if atlas_width * atlas_height > atlas.len() { + atlas.resize(atlas_width * atlas_height, 0); + } - cursor_x += glyph_width; + glyph.draw(|x, y, v| { + if v > 0.0 { + let x = x as usize; + let y = y as usize; + let px = cursor_x + x as usize; + let py = cursor_y + y as usize; + atlas[py * atlas_width + px] = (v * 255.0).round() as u8; + } + }); + + let offset_y = scale as i16 + bb.min.y as i16 - 3; // TODO: use font.v_metrics + glyph_infos.push(GlyphInfo { + id: glyph.id(), + advance_width: glyph.unpositioned().h_metrics().advance_width, + uv: Some(UvRect { + offset_x: bb.min.x as i16, + offset_y: offset_y as i16, + min_x: cursor_x as u16, + min_y: cursor_y as u16, + max_x: (cursor_x + glyph_width - 1) as u16, + max_y: (cursor_y + glyph_height - 1) as u16, + }), + }); + + cursor_x += glyph_width; + } else { + // No bounding box. Maybe a space? + glyph_infos.push(GlyphInfo { + id: glyph.id(), + advance_width: glyph.unpositioned().h_metrics().advance_width, + uv: None, + }); + } } Font { + font, scale, - char_rects, + glyph_infos, atlas_width, atlas_height, atlas, } } + pub fn line_spacing(&self) -> f32 { + self.scale as f32 + } + pub fn supported_characters() -> impl Iterator { (FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char) } @@ -156,10 +179,19 @@ impl Font { self.atlas[y * self.atlas_width + x] } - pub fn glyph_info(&self, c: char) -> Option { + pub fn uv_rect(&self, c: char) -> Option { let c = c as usize; if FIRST_ASCII <= c && c <= LAST_ASCII { - Some(self.char_rects[c - FIRST_ASCII]) + self.glyph_infos[c - FIRST_ASCII].uv + } else { + None + } + } + + fn glyph_info(&self, c: char) -> Option { + let c = c as usize; + if FIRST_ASCII <= c && c <= LAST_ASCII { + Some(self.glyph_infos[c - FIRST_ASCII]) } else { None } @@ -168,13 +200,23 @@ impl Font { /// Returns the start (X) of each character, starting at zero, plus the total width. /// i.e. returns text.chars().count() + 1 numbers. pub fn layout_single_line(&self, text: &str) -> Vec { + let scale = Scale::uniform(self.scale as f32); + let mut x_offsets = Vec::new(); - let mut x = 0.0; + let mut cursor_x = 0.0f32; + let mut last_glyph_id = None; for c in text.chars() { - x_offsets.push(x); - x += 7.0; // TODO: kerning + cursor_x = cursor_x.round(); + x_offsets.push(cursor_x); + 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 += glyph.advance_width; + last_glyph_id = Some(glyph.id); + } } - x_offsets.push(x); + x_offsets.push(cursor_x); x_offsets } @@ -189,28 +231,40 @@ impl Font { pub fn debug_print_all_chars(&self) { let max_width = 160; + let scale = Scale::uniform(self.scale as f32); let mut pixel_rows = vec![vec![0; max_width]; self.scale]; - let mut cursor_x = 0; + let mut cursor_x = 0.0; let mut cursor_y = 0; + let mut last_glyph_id = None; for c in Self::supported_characters() { - if let Some(glyph_info) = self.glyph_info(c) { - for x in glyph_info.min_x..=glyph_info.max_x { - for y in glyph_info.min_y..=glyph_info.max_y { - let pixel = self.pixel(x, y); - let rx = glyph_info.offset_x + x - glyph_info.min_x; - let ry = glyph_info.offset_y + y - glyph_info.min_y; - pixel_rows[cursor_y + ry as usize][cursor_x + rx as usize] = pixel; - } + 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 += 7; // TODO - if cursor_x + 7 >= max_width { + if cursor_x + glyph.advance_width >= max_width as f32 { println!("{}", (0..max_width).map(|_| "X").collect::()); for row in pixel_rows { println!("{}", as_ascii(&row)); } pixel_rows = vec![vec![0; max_width]; self.scale]; - cursor_x = 0; + cursor_x = 0.0; } + if let Some(uv) = glyph.uv { + for x in uv.min_x..=uv.max_x { + for y in uv.min_y..=uv.max_y { + let pixel = self.pixel(x as u16, y as u16); + let rx = uv.offset_x + x as i16 - uv.min_x as i16; + let ry = uv.offset_y + y as i16 - uv.min_y 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::()); @@ -220,7 +274,19 @@ impl Font { fn as_ascii(pixels: &[u8]) -> String { pixels .iter() - .map(|pixel| if *pixel == 0 { ' ' } else { 'X' }) + .map(|pixel| { + if *pixel == 0 { + ' ' + } else if *pixel < 85 { + '.' + } else if *pixel < 170 { + 'o' + } else if *pixel < 255 { + 'O' + } else { + 'X' + } + }) .collect() } diff --git a/emgui/src/layout.rs b/emgui/src/layout.rs index 9e9079b7..8db34d7f 100644 --- a/emgui/src/layout.rs +++ b/emgui/src/layout.rs @@ -6,10 +6,6 @@ use crate::{font::Font, math::*, types::*}; #[derive(Clone, Copy, Debug, Serialize)] pub struct LayoutOptions { - /// The width and height of a single character (including any spacing). - /// All text is monospace! - pub char_size: Vec2, - /// Horizontal and vertical padding within a window frame. pub window_padding: Vec2, @@ -33,7 +29,6 @@ pub struct LayoutOptions { impl Default for LayoutOptions { fn default() -> Self { LayoutOptions { - char_size: vec2(7.2, 14.0), item_spacing: vec2(8.0, 4.0), window_padding: vec2(6.0, 6.0), indent: 21.0, @@ -165,10 +160,10 @@ pub struct Layout { } impl Layout { - pub fn new() -> Layout { + pub fn new(font: Font) -> Layout { Layout { options: Default::default(), - font: Font::new(13), + font, input: Default::default(), memory: Default::default(), id: Default::default(), @@ -293,7 +288,7 @@ impl Layout { let (slider_rect, interact) = self.reserve_space( Vec2 { x: self.options.width, - y: self.options.char_size.y, + y: self.font.line_spacing(), }, Some(id), ); @@ -463,7 +458,7 @@ impl Layout { } fn layout_text(&self, text: &str) -> (TextFragments, Vec2) { - let char_size = self.options.char_size; + let line_spacing = self.font.line_spacing(); let mut cursor_y = 0.0; let mut max_width = 0.0; let mut text_fragments = Vec::new(); @@ -476,7 +471,7 @@ impl Layout { text: line.into(), }); - cursor_y += char_size.y; + cursor_y += line_spacing; max_width = line_width.max(max_width); } let bounding_size = vec2(max_width, cursor_y); diff --git a/emgui/src/lib.rs b/emgui/src/lib.rs index 349353b6..d3d5d87a 100644 --- a/emgui/src/lib.rs +++ b/emgui/src/lib.rs @@ -16,6 +16,7 @@ pub mod types; pub use crate::{ emgui::Emgui, + font::Font, layout::Layout, layout::LayoutOptions, painter::{Frame, Painter, Vertex}, diff --git a/emgui/src/painter.rs b/emgui/src/painter.rs index 2e534c4b..0d524e1f 100644 --- a/emgui/src/painter.rs +++ b/emgui/src/painter.rs @@ -198,10 +198,8 @@ pub struct Painter { } impl Painter { - pub fn new() -> Painter { - Painter { - font: Font::new(13), - } + pub fn new(font: Font) -> Painter { + Painter { font } } /// 8-bit row-major font atlas texture, (width, height, pixels). @@ -347,8 +345,8 @@ impl Painter { x_offsets, } => { for (c, x_offset) in text.chars().zip(x_offsets.iter()) { - if let Some(glyph) = self.font.glyph_info(c) { - let top_left = Vertex { + if let Some(glyph) = self.font.uv_rect(c) { + let mut top_left = Vertex { pos: *pos + vec2( x_offset + (glyph.offset_x as f32), @@ -357,6 +355,8 @@ impl Painter { uv: (glyph.min_x, glyph.min_y), color: *color, }; + top_left.pos.x = top_left.pos.x.round(); // Pixel-perfection. + top_left.pos.y = top_left.pos.y.round(); // Pixel-perfection. let bottom_right = Vertex { pos: top_left.pos + vec2( diff --git a/emgui_wasm/src/app.rs b/emgui_wasm/src/app.rs index 7864f25f..e6bb89bf 100644 --- a/emgui_wasm/src/app.rs +++ b/emgui_wasm/src/app.rs @@ -96,8 +96,6 @@ impl GuiSettings for emgui::LayoutOptions { if gui.button("Reset LayoutOptions").clicked { *self = Default::default(); } - gui.slider_f32("char_size.x", &mut self.char_size.x, 0.0, 20.0); - gui.slider_f32("char_size.y", &mut self.char_size.y, 0.0, 20.0); gui.slider_f32("item_spacing.x", &mut self.item_spacing.x, 0.0, 10.0); gui.slider_f32("item_spacing.y", &mut self.item_spacing.y, 0.0, 10.0); gui.slider_f32("window_padding.x", &mut self.window_padding.x, 0.0, 10.0); diff --git a/emgui_wasm/src/lib.rs b/emgui_wasm/src/lib.rs index 734c0ba3..0e9db165 100644 --- a/emgui_wasm/src/lib.rs +++ b/emgui_wasm/src/lib.rs @@ -9,7 +9,7 @@ extern crate emgui; use std::sync::Mutex; -use emgui::{Emgui, Frame, RawInput}; +use emgui::{Emgui, Font, Frame, RawInput}; use wasm_bindgen::prelude::*; @@ -42,9 +42,13 @@ mod webgl; // serde_json::to_string(&commands).unwrap() // } +fn font() -> Font { + Font::new(20) // TODO: don't create this multiple times +} + #[wasm_bindgen] pub fn new_webgl_painter(canvas_id: &str) -> Result { - let emgui_painter = emgui::Painter::new(); // TODO: don't create this twice + let emgui_painter = emgui::Painter::new(font()); // TODO: don't create this twice webgl::Painter::new(canvas_id, emgui_painter.texture()) } @@ -58,8 +62,8 @@ impl State { fn new() -> State { State { app: Default::default(), - emgui: Emgui::new(), - emgui_painter: emgui::Painter::new(), + emgui: Emgui::new(font()), + emgui_painter: emgui::Painter::new(font()), } } diff --git a/emgui_wasm/src/webgl.rs b/emgui_wasm/src/webgl.rs index 280fc08a..58971644 100644 --- a/emgui_wasm/src/webgl.rs +++ b/emgui_wasm/src/webgl.rs @@ -64,7 +64,8 @@ impl Painter { .unwrap(); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32); - gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::NEAREST as i32); + // gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::NEAREST as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as i32); // --------------------------------------------------------------------