Support multiple fonts

This commit is contained in:
Emil Ernerfeldt 2019-01-13 00:55:56 +01:00
parent 1b8a45a514
commit ca9333ec3e
13 changed files with 140 additions and 70 deletions

Binary file not shown.

View File

@ -1,7 +1,6 @@
use std::sync::Arc;
use crate::{
font::Font,
layout,
layout::{LayoutOptions, Region},
style,
@ -48,20 +47,27 @@ pub struct Emigui {
}
impl Emigui {
pub fn new(font: Arc<Font>) -> Emigui {
pub fn new() -> Emigui {
let data = Arc::new(layout::Data::new());
let fonts = data.fonts.clone();
Emigui {
last_input: Default::default(),
data: Arc::new(layout::Data::new(font.clone())),
data,
style: Default::default(),
painter: Painter::new(font),
painter: Painter::new(fonts),
stats: Default::default(),
}
}
pub fn texture(&self) -> (u16, u16, &[u8]) {
self.data.fonts.texture()
}
pub fn new_frame(&mut self, new_input: RawInput) {
let gui_input = GuiInput::from_last_and_new(&self.last_input, &new_input);
self.last_input = new_input;
// TODO: avoid this clone
let mut new_data = (*self.data).clone();
new_data.new_frame(gui_input);
self.data = Arc::new(new_data);

View File

@ -64,12 +64,8 @@ pub struct Font {
}
impl Font {
pub fn new(scale: usize, atlas: Arc<Mutex<TextureAtlas>>) -> Font {
// TODO: figure out a way to make the wasm smaller despite including a font.
// let font_data = include_bytes!("../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE.
// let font_data = include_bytes!("../fonts/DejaVuSans.ttf");
let font_data = include_bytes!("../fonts/Roboto-Regular.ttf");
let font = rusttype::Font::from_bytes(font_data as &[u8]).expect("Error constructing Font");
pub fn new(atlas: Arc<Mutex<TextureAtlas>>, font_data: &'static [u8], scale: usize) -> Font {
let font = rusttype::Font::from_bytes(font_data).expect("Error constructing Font");
// println!(
// "font.v_metrics: {:?}",

58
emigui/src/fonts.rs Normal file
View File

@ -0,0 +1,58 @@
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
use crate::{font::Font, texture_atlas::TextureAtlas};
/// TODO: rename
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum TextStyle {
Body,
Button,
Heading,
// Monospace,
}
pub struct Fonts {
fonts: BTreeMap<TextStyle, Font>,
texture: (u16, u16, Vec<u8>),
}
impl Fonts {
pub fn new() -> Fonts {
let mut atlas = TextureAtlas::new(128, 8); // TODO: better default?
// Make one white pixel for use for various stuff:
let pos = atlas.allocate((1, 1));
atlas[pos] = 255;
let atlas = Arc::new(Mutex::new(atlas));
// TODO: figure out a way to make the wasm smaller despite including a font.
// let font_data = include_bytes!("../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE.
// let font_data = include_bytes!("../fonts/DejaVuSans.ttf");
let font_data = include_bytes!("../fonts/Roboto-Regular.ttf");
let mut fonts = BTreeMap::new();
fonts.insert(TextStyle::Body, Font::new(atlas.clone(), font_data, 20));
fonts.insert(TextStyle::Button, fonts[&TextStyle::Body].clone());
fonts.insert(TextStyle::Heading, Font::new(atlas.clone(), font_data, 30));
let texture = atlas.lock().unwrap().clone().into_texture();
Fonts { fonts, texture }
}
pub fn texture(&self) -> (u16, u16, &[u8]) {
(self.texture.0, self.texture.1, &self.texture.2)
}
}
impl std::ops::Index<TextStyle> for Fonts {
type Output = Font;
fn index(&self, text_style: TextStyle) -> &Font {
&self.fonts[&text_style]
}
}

View File

@ -5,7 +5,8 @@ use std::{
};
use crate::{
font::{Font, TextFragment},
font::TextFragment,
fonts::{Fonts, TextStyle},
math::*,
types::*,
widgets::{label, Widget},
@ -143,7 +144,7 @@ impl GraphicLayers {
pub struct Data {
/// The default options for new regions
pub(crate) options: Mutex<LayoutOptions>,
pub(crate) font: Arc<Font>,
pub(crate) fonts: Arc<Fonts>,
pub(crate) input: GuiInput,
pub(crate) memory: Mutex<Memory>,
pub(crate) graphics: Mutex<GraphicLayers>,
@ -153,7 +154,7 @@ impl Clone for Data {
fn clone(&self) -> Self {
Data {
options: Mutex::new(self.options()),
font: self.font.clone(),
fonts: self.fonts.clone(),
input: self.input.clone(),
memory: Mutex::new(self.memory.lock().unwrap().clone()),
graphics: Mutex::new(self.graphics.lock().unwrap().clone()),
@ -162,10 +163,10 @@ impl Clone for Data {
}
impl Data {
pub fn new(font: Arc<Font>) -> Data {
pub fn new() -> Data {
Data {
options: Default::default(),
font,
fonts: Arc::new(Fonts::new()),
input: Default::default(),
memory: Default::default(),
graphics: Default::default(),
@ -277,8 +278,8 @@ impl Region {
self.cursor
}
pub fn font(&self) -> &Font {
&*self.data.font
pub fn fonts(&self) -> &Fonts {
&*self.data.fonts
}
pub fn width(&self) -> f32 {
@ -299,7 +300,9 @@ impl Region {
);
let text: String = text.into();
let id = self.make_child_id(&text);
let (text, text_size) = self.font().layout_multiline(&text, self.width());
let text_style = TextStyle::Heading;
let font = &self.fonts()[text_style];
let (text, text_size) = font.layout_multiline(&text, self.width());
let text_cursor = self.cursor + self.options().button_padding;
let (rect, interact) = self.reserve_space(
vec2(
@ -328,6 +331,7 @@ impl Region {
});
self.add_text(
text_cursor + vec2(self.options().start_icon_width, 0.0),
text_style,
text,
);
@ -504,11 +508,11 @@ impl Region {
})
}
pub fn add_text(&mut self, pos: Vec2, text: Vec<TextFragment>) {
pub fn add_text(&mut self, pos: Vec2, text_style: TextStyle, text: Vec<TextFragment>) {
for fragment in text {
self.add_graphic(GuiCmd::Text {
pos: pos + vec2(0.0, fragment.y_offset),
style: TextStyle::Label,
text_style,
text: fragment.text,
x_offsets: fragment.x_offsets,
});

View File

@ -8,6 +8,7 @@ extern crate serde_derive;
mod emigui;
mod font;
mod fonts;
mod layout;
pub mod math;
mod painter;
@ -18,11 +19,10 @@ pub mod widgets;
pub use crate::{
emigui::Emigui,
font::Font,
fonts::TextStyle,
layout::LayoutOptions,
layout::Region,
painter::{Frame, Painter, Vertex},
style::Style,
texture_atlas::TextureAtlas,
types::RawInput,
};

View File

@ -5,7 +5,7 @@ const AA_SIZE: f32 = 1.0;
/// Outputs render info in a format suitable for e.g. OpenGL.
use crate::{
font::Font,
fonts::Fonts,
math::{remap, vec2, Vec2, TAU},
types::{Color, PaintCmd},
};
@ -193,12 +193,12 @@ impl Frame {
#[derive(Clone)]
pub struct Painter {
font: Arc<Font>,
fonts: Arc<Fonts>,
}
impl Painter {
pub fn new(font: Arc<Font>) -> Painter {
Painter { font }
pub fn new(fonts: Arc<Fonts>) -> Painter {
Painter { fonts }
}
pub fn paint(&self, commands: &[PaintCmd]) -> Frame {
@ -336,10 +336,12 @@ impl Painter {
color,
pos,
text,
text_style,
x_offsets,
} => {
let font = &self.fonts[*text_style];
for (c, x_offset) in text.chars().zip(x_offsets.iter()) {
if let Some(glyph) = self.font.uv_rect(c) {
if let Some(glyph) = font.uv_rect(c) {
let mut top_left = Vertex {
pos: *pos
+ vec2(

View File

@ -232,15 +232,14 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
}
GuiCmd::Text {
pos,
style: text_style,
text_style,
text,
x_offsets,
} => {
let color = match text_style {
TextStyle::Label => style.text_color(),
};
let color = style.text_color();
out_commands.push(PaintCmd::Text {
color,
text_style,
pos,
text,
x_offsets,

View File

@ -1,4 +1,7 @@
use crate::math::{Rect, Vec2};
use crate::{
fonts::TextStyle,
math::{Rect, Vec2},
};
// ----------------------------------------------------------------------------
@ -88,12 +91,6 @@ pub struct InteractInfo {
pub active: bool,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TextStyle {
Label,
}
#[derive(Clone, Debug, Serialize)]
pub enum GuiCmd {
PaintCommands(Vec<PaintCmd>),
@ -130,7 +127,7 @@ pub enum GuiCmd {
/// The text should be vertically centered at the given position.
Text {
pos: Vec2,
style: TextStyle,
text_style: TextStyle,
text: String,
/// Start each character in the text, as offset from pos.
x_offsets: Vec<f32>,
@ -179,6 +176,7 @@ pub enum PaintCmd {
/// Top left corner of the first character.
pos: Vec2,
text: String,
text_style: TextStyle,
/// Start each character in the text, as offset from pos.
x_offsets: Vec<f32>,
// TODO: font info

View File

@ -1,4 +1,5 @@
use crate::{
fonts::TextStyle,
layout::{make_id, GuiResponse, Id, Region},
math::{remap_clamp, vec2, Vec2},
types::GuiCmd,
@ -15,11 +16,20 @@ pub trait Widget {
pub struct Label {
text: String,
text_style: TextStyle,
}
impl Label {
pub fn new<S: Into<String>>(text: S) -> Self {
Label { text: text.into() }
Label {
text: text.into(),
text_style: TextStyle::Body,
}
}
pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = text_style;
self
}
}
@ -29,8 +39,9 @@ pub fn label<S: Into<String>>(text: S) -> Label {
impl Widget for Label {
fn add_to(self, region: &mut Region) -> GuiResponse {
let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
region.add_text(region.cursor(), text);
let font = &region.fonts()[self.text_style];
let (text, text_size) = font.layout_multiline(&self.text, region.width());
region.add_text(region.cursor(), self.text_style, text);
let (_, interact) = region.reserve_space(text_size, None);
region.response(interact)
}
@ -51,12 +62,14 @@ impl Button {
impl Widget for Button {
fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text);
let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_style = TextStyle::Button;
let font = &region.fonts()[text_style];
let (text, text_size) = font.layout_multiline(&self.text, region.width());
let text_cursor = region.cursor() + region.options().button_padding;
let (rect, interact) =
region.reserve_space(text_size + 2.0 * region.options().button_padding, Some(id));
region.add_graphic(GuiCmd::Button { interact, rect });
region.add_text(text_cursor, text);
region.add_text(text_cursor, text_style, text);
region.response(interact)
}
}
@ -81,7 +94,9 @@ impl<'a> Checkbox<'a> {
impl<'a> Widget for Checkbox<'a> {
fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text);
let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_style = TextStyle::Button;
let font = &region.fonts()[text_style];
let (text, text_size) = font.layout_multiline(&self.text, region.width());
let text_cursor = region.cursor()
+ region.options().button_padding
+ vec2(region.options().start_icon_width, 0.0);
@ -100,7 +115,7 @@ impl<'a> Widget for Checkbox<'a> {
interact,
rect,
});
region.add_text(text_cursor, text);
region.add_text(text_cursor, text_style, text);
region.response(interact)
}
}
@ -129,7 +144,9 @@ pub fn radio<S: Into<String>>(checked: bool, text: S) -> RadioButton {
impl Widget for RadioButton {
fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text);
let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_style = TextStyle::Button;
let font = &region.fonts()[text_style];
let (text, text_size) = font.layout_multiline(&self.text, region.width());
let text_cursor = region.cursor()
+ region.options().button_padding
+ vec2(region.options().start_icon_width, 0.0);
@ -145,7 +162,7 @@ impl Widget for RadioButton {
interact,
rect,
});
region.add_text(text_cursor, text);
region.add_text(text_cursor, text_style, text);
region.response(interact)
}
}
@ -187,6 +204,9 @@ impl<'a> Slider<'a> {
impl<'a> Widget for Slider<'a> {
fn add_to(self, region: &mut Region) -> GuiResponse {
let text_style = TextStyle::Button;
let font = &region.fonts()[text_style];
if let Some(text) = &self.text {
let text_on_top = self.text_on_top.unwrap_or_default();
let full_text = format!("{}: {:.3}", text, self.value);
@ -196,8 +216,8 @@ impl<'a> Widget for Slider<'a> {
naked.text = None;
if text_on_top {
let (text, text_size) = region.font().layout_multiline(&full_text, region.width());
region.add_text(region.cursor(), text);
let (text, text_size) = font.layout_multiline(&full_text, region.width());
region.add_text(region.cursor(), text_style, text);
region.reserve_space_inner(text_size);
naked.add_to(region)
} else {
@ -215,7 +235,7 @@ impl<'a> Widget for Slider<'a> {
let (slider_rect, interact) = region.reserve_space(
Vec2 {
x: region.available_space.x,
y: region.data.font.line_spacing(),
y: font.line_spacing(),
},
id,
);

View File

@ -1,4 +1,4 @@
use emigui::{math::*, types::*, widgets::*, Region};
use emigui::{math::*, types::*, widgets::*, Region, TextStyle};
pub struct App {
checked: bool,
@ -25,6 +25,7 @@ impl Default for App {
impl App {
pub fn show_gui(&mut self, gui: &mut Region) {
gui.add(label("Emigui").text_style(TextStyle::Heading));
gui.add(label("Emigui is an Immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."));
gui.add(label(format!(

View File

@ -5,9 +5,7 @@ extern crate wasm_bindgen;
extern crate emigui;
use std::sync::{Arc, Mutex};
use emigui::{widgets::label, Emigui, Font, RawInput, TextureAtlas};
use emigui::{widgets::label, Emigui, RawInput};
use wasm_bindgen::prelude::*;
@ -32,20 +30,8 @@ pub struct State {
impl State {
fn new(canvas_id: &str) -> Result<State, JsValue> {
let mut atlas = TextureAtlas::new(128, 8); // TODO: better default?
// Make one white pixel for use for various stuff:
let pos = atlas.allocate((1, 1));
atlas[pos] = 255;
let atlas = Arc::new(Mutex::new(atlas));
let font = Arc::new(Font::new(20, atlas.clone()));
let texture = atlas.lock().unwrap().clone().into_texture();
let emigui = Emigui::new(font);
let webgl_painter = webgl::Painter::new(canvas_id, texture)?;
let emigui = Emigui::new();
let webgl_painter = webgl::Painter::new(canvas_id, emigui.texture())?;
Ok(State {
app: Default::default(),
emigui,

View File

@ -34,7 +34,7 @@ impl Painter {
pub fn new(
canvas_id: &str,
(tex_width, tex_height, pixels): (u16, u16, Vec<u8>),
(tex_width, tex_height, pixels): (u16, u16, &[u8]),
) -> Result<Painter, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id(canvas_id).unwrap();