[text] support non-latin characters by dynamically adding them to atlas

This commit is contained in:
Emil Ernerfeldt 2020-09-09 14:24:13 +02:00
parent 0e870dae3e
commit bb367752cf
10 changed files with 178 additions and 103 deletions

18
CHANGELOG.md Normal file
View file

@ -0,0 +1,18 @@
# Egui Changelog
## Unreleased
* Color picker
* Unicode characters in labels (limited by [what the default font supports](https://fonts.google.com/specimen/Comfortaa#glyphs))
## 0.1.4 - 2020-09-08
This is when I started the CHANGELOG.md, after almost two years of development. Better late than never.
* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing
* Layouts: horizontal, vertical, columns
* Text input: very basic, multiline, copy/paste
* Windows: move, resize, name, minimize and close. Automatically sized and positioned.
* Regions: resizing, vertical scrolling, collapsing headers (sections)
* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons.
* Tooltips on hover

View file

@ -34,9 +34,11 @@ TODO-list for the Egui project. If you looking for something to do, look here.
* [ ] Distinguish between touch input and mouse input
* [ ] Get modifier keys
* [ ] Keyboard shortcuts
* [ ] Copy, paste, undo, ...
* [ ] Text
* [ ] Unicode
* [ ] Shared mutable expanding texture map?
* [/] Unicode
* [x] Shared mutable expanding texture map
* [ ] Text editing of unicode
* [ ] Change text style/color and continue in same layout
* [ ] Menu bar (File, Edit, etc)
* [ ] Sub-menus

View file

@ -110,9 +110,10 @@ impl Context {
.expect("No fonts available until first call to Context::begin_frame()`")
}
/// The Egui texture, containing font characters etc..
/// Not valid until first call to `begin_frame()`
/// That's because since we don't know the proper `pixels_per_point` until then.
pub fn texture(&self) -> &paint::Texture {
pub fn texture(&self) -> Arc<paint::Texture> {
self.fonts().texture()
}

View file

@ -491,6 +491,9 @@ impl Widgets {
);
});
ui.add(label!("Some non-latin characters: Ευρηκα τ = 2×π"))
.tooltip_text("The current font supports only a few non-latin characters and Egui does not currently support right-to-left text.");
ui.horizontal(|ui| {
ui.radio_value("First", &mut self.radio, 0);
ui.radio_value("Second", &mut self.radio, 1);

View file

@ -13,6 +13,9 @@ impl Texture {
self.width,
self.height
));
if self.width <= 1 || self.height <= 1 {
return;
}
let mut size = vec2(self.width as f32, self.height as f32);
if size.x > ui.available().width() {
size *= ui.available().width() / size.x;
@ -40,8 +43,8 @@ impl Texture {
let v = remap_clamp(pos.y, rect.range_y(), 0.0..=self.height as f32 - 1.0).round();
let texel_radius = 32.0;
let u = clamp(u, texel_radius..=self.width as f32 - 1.0 - texel_radius);
let v = clamp(v, texel_radius..=self.height as f32 - 1.0 - texel_radius);
let u = u.max(texel_radius);
let v = v.max(texel_radius);
let top_left = Vertex {
pos: zoom_rect.min,

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use {
ahash::AHashMap,
parking_lot::Mutex,
parking_lot::{Mutex, RwLock},
rusttype::{point, Scale},
};
@ -176,13 +176,13 @@ pub struct GlyphInfo {
}
/// The interface uses points as the unit for everything.
#[derive(Clone)]
pub struct Font {
font: rusttype::Font<'static>,
/// Maximum character height
scale_in_pixels: f32,
pixels_per_point: f32,
glyph_infos: AHashMap<char, GlyphInfo>, // TODO: see if we can optimize if we switch to a binary search
replacement_glyph_info: GlyphInfo,
glyph_infos: RwLock<AHashMap<char, GlyphInfo>>,
atlas: Arc<Mutex<TextureAtlas>>,
}
@ -199,22 +199,40 @@ impl Font {
let font = rusttype::Font::try_from_bytes(font_data).expect("Error constructing Font");
let scale_in_pixels = pixels_per_point * scale_in_points;
let mut font = Font {
let replacement_glyph_info = allocate_glyph(
&mut atlas.lock(),
REPLACEMENT_CHAR,
&font,
scale_in_pixels,
pixels_per_point,
)
.unwrap_or_else(|| {
panic!(
"Failed to find replacement character {:?}",
REPLACEMENT_CHAR
)
});
let font = Font {
font,
scale_in_pixels,
pixels_per_point,
replacement_glyph_info,
glyph_infos: Default::default(),
atlas,
};
/// Printable ASCII characters [32, 126], which excludes control codes.
font.glyph_infos
.write()
.insert(REPLACEMENT_CHAR, font.replacement_glyph_info);
// Preload the printable ASCII characters [32, 126] (which excludes control codes):
const FIRST_ASCII: usize = 32; // 32 == space
const LAST_ASCII: usize = 126;
for c in (FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char) {
font.add_char(c);
font.glyph_info(c);
}
font.add_char(REPLACEMENT_CHAR);
font.add_char('°');
font.glyph_info('°');
font
}
@ -233,86 +251,38 @@ impl Font {
}
pub fn uv_rect(&self, c: char) -> Option<UvRect> {
self.glyph_infos.get(&c).and_then(|gi| gi.uv_rect)
self.glyph_infos.read().get(&c).and_then(|gi| gi.uv_rect)
}
fn glyph_info_or_none(&self, c: char) -> Option<&GlyphInfo> {
self.glyph_infos.get(&c)
}
fn glyph_info_or_replacemnet(&self, c: char) -> &GlyphInfo {
self.glyph_info_or_none(c)
.unwrap_or_else(|| self.glyph_info_or_none(REPLACEMENT_CHAR).unwrap())
}
fn add_char(&mut self, c: char) {
if self.glyph_infos.contains_key(&c) {
return;
fn glyph_info(&self, c: char) -> GlyphInfo {
if c == '\n' {
// Hack: else we show '\n' as '?' (REPLACEMENT_CHAR)
return self.glyph_info(' ');
}
let glyph = self.font.glyph(c);
assert_ne!(
glyph.id().0,
0,
"Failed to find a glyph for the character '{}'",
c
);
let glyph = glyph.scaled(Scale::uniform(self.scale_in_pixels));
let glyph = glyph.positioned(point(0.0, 0.0));
{
if let Some(glyph_info) = self.glyph_infos.read().get(&c) {
return *glyph_info;
}
}
let uv_rect = 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);
let mut atlas_lock = self.atlas.lock();
let glyph_pos = atlas_lock.allocate((glyph_width, glyph_height));
let texture = atlas_lock.texture_mut();
glyph.draw(|x, y, v| {
if v > 0.0 {
let px = glyph_pos.0 + x as usize;
let py = glyph_pos.1 + y as usize;
texture[(px, py)] = (v * 255.0).round() as u8;
}
});
let offset_y_in_pixels =
self.scale_in_pixels as f32 + bb.min.y as f32 - 4.0 * self.pixels_per_point; // TODO: use font.v_metrics
Some(UvRect {
offset: vec2(
bb.min.x as f32 / self.pixels_per_point,
offset_y_in_pixels / self.pixels_per_point,
),
size: vec2(glyph_width as f32, glyph_height as f32) / self.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?
None
};
let advance_width_in_points =
glyph.unpositioned().h_metrics().advance_width / self.pixels_per_point;
self.glyph_infos.insert(
// Add new character:
let glyph_info = allocate_glyph(
&mut self.atlas.lock(),
c,
GlyphInfo {
id: glyph.id(),
advance_width: advance_width_in_points,
uv_rect,
},
&self.font,
self.scale_in_pixels,
self.pixels_per_point,
);
// debug_assert!(glyph_info.is_some(), "Failed to find {:?}", c);
let glyph_info = glyph_info.unwrap_or(self.replacement_glyph_info);
self.glyph_infos.write().insert(c, glyph_info);
glyph_info
}
/// Typeset the given text onto one line.
/// Assumes there are no \n in the text.
/// Always returns exactly one frament.
/// Always returns exactly one fragment.
pub fn layout_single_line(&self, text: String) -> Galley {
let x_offsets = self.layout_single_line_fragment(&text);
let line = Line {
@ -397,7 +367,7 @@ impl Font {
let mut last_glyph_id = None;
for c in text.chars() {
let glyph = self.glyph_info_or_replacemnet(c);
let glyph = self.glyph_info(c);
if let Some(last_glyph_id) = last_glyph_id {
cursor_x_in_points +=
@ -500,3 +470,62 @@ impl Font {
out_lines
}
}
fn allocate_glyph(
atlas: &mut TextureAtlas,
c: char,
font: &rusttype::Font<'static>,
scale_in_pixels: f32,
pixels_per_point: f32,
) -> Option<GlyphInfo> {
let glyph = font.glyph(c);
if glyph.id().0 == 0 {
return None; // Failed to find a glyph for the character
}
let glyph = glyph.scaled(Scale::uniform(scale_in_pixels));
let glyph = glyph.positioned(point(0.0, 0.0));
let uv_rect = 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);
let glyph_pos = atlas.allocate((glyph_width, glyph_height));
let texture = atlas.texture_mut();
glyph.draw(|x, y, v| {
if v > 0.0 {
let px = glyph_pos.0 + x as usize;
let py = glyph_pos.1 + y as usize;
texture[(px, py)] = (v * 255.0).round() as u8;
}
});
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?
None
};
let advance_width_in_points = glyph.unpositioned().h_metrics().advance_width / pixels_per_point;
Some(GlyphInfo {
id: glyph.id(),
advance_width: advance_width_in_points,
uv_rect,
})
}

View file

@ -63,7 +63,10 @@ impl FontDefinitions {
pub struct Fonts {
definitions: FontDefinitions,
fonts: BTreeMap<TextStyle, Font>,
texture: Texture,
atlas: Arc<Mutex<TextureAtlas>>,
/// Copy of the texture in the texture atlas.
/// This is so we can return a reference to it (the texture atlas is behind a lock).
buffered_texture: Mutex<Arc<Texture>>,
}
impl Fonts {
@ -82,22 +85,25 @@ impl Fonts {
return;
}
let mut atlas = TextureAtlas::new(512, 8); // TODO: better default?
let mut atlas = TextureAtlas::new(512, 16); // TODO: better default?
// Make the top left four pixels fully white:
let pos = atlas.allocate((2, 2));
assert_eq!(pos, (0, 0));
atlas.texture_mut()[(0, 0)] = 255;
atlas.texture_mut()[(0, 1)] = 255;
atlas.texture_mut()[(1, 0)] = 255;
atlas.texture_mut()[(1, 1)] = 255;
{
let pos = atlas.allocate((2, 2));
assert_eq!(pos, (0, 0));
let tex = atlas.texture_mut();
tex[(0, 0)] = 255;
tex[(0, 1)] = 255;
tex[(1, 0)] = 255;
tex[(1, 1)] = 255;
}
let atlas = Arc::new(Mutex::new(atlas));
// TODO: figure out a way to make the wasm smaller despite including a font. Zip it?
let monospae_typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE.
let monospace_typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE.
// let monospae_typeface_data = include_bytes!("../../fonts/Roboto-Regular.ttf");
// let monospace_typeface_data = include_bytes!("../../fonts/Roboto-Regular.ttf");
let variable_typeface_data = include_bytes!("../../fonts/Comfortaa-Regular.ttf"); // Funny, hard to read
@ -112,7 +118,7 @@ impl Fonts {
.into_iter()
.map(|(text_style, (family, size))| {
let typeface_data: &[u8] = match family {
FontFamily::Monospace => monospae_typeface_data,
FontFamily::Monospace => monospace_typeface_data,
FontFamily::VariableWidth => variable_typeface_data,
};
@ -122,15 +128,28 @@ impl Fonts {
)
})
.collect();
self.texture = atlas.lock().texture().clone();
let mut hasher = ahash::AHasher::default();
self.texture.pixels.hash(&mut hasher);
self.texture.version = hasher.finish();
{
let mut atlas = atlas.lock();
let texture = atlas.texture_mut();
// Make sure we seed the texture version with something unique based on the default characters:
let mut hasher = ahash::AHasher::default();
texture.pixels.hash(&mut hasher);
texture.version = hasher.finish();
}
self.buffered_texture = Default::default(); //atlas.lock().texture().clone();
self.atlas = atlas;
}
pub fn texture(&self) -> &Texture {
&self.texture
pub fn texture(&self) -> Arc<Texture> {
let atlas = self.atlas.lock();
let mut buffered_texture = self.buffered_texture.lock();
if buffered_texture.version != atlas.texture().version {
*buffered_texture = Arc::new(atlas.texture().clone());
}
buffered_texture.clone()
}
}

View file

@ -105,7 +105,7 @@ pub fn run(
let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32;
runner.frame_times.add(raw_input.time, frame_time);
painter.paint_jobs(&display, paint_jobs, ctx.texture());
painter.paint_jobs(&display, paint_jobs, &ctx.texture());
if runner.quit {
*control_flow = glutin::event_loop::ControlFlow::Exit

View file

@ -258,7 +258,7 @@ impl Painter {
u_sampler: &self.texture,
};
// Emilib outputs colors with premultiplied alpha:
// Egui outputs colors with premultiplied alpha:
let blend_func = glium::BlendingFunction::Addition {
source: glium::LinearBlendingFactor::One,
destination: glium::LinearBlendingFactor::OneMinusSourceAlpha,

View file

@ -59,7 +59,7 @@ impl WebBackend {
self.painter.paint_jobs(
bg_color,
paint_jobs,
self.ctx.texture(),
&self.ctx.texture(),
self.ctx.pixels_per_point(),
)
}