[text] support non-latin characters by dynamically adding them to atlas
This commit is contained in:
parent
0e870dae3e
commit
bb367752cf
10 changed files with 178 additions and 103 deletions
18
CHANGELOG.md
Normal file
18
CHANGELOG.md
Normal 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
|
6
TODO.md
6
TODO.md
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue