egui
egui copied to clipboard
Improve text
Tracking issue for improved text rendering
- [x] Wrapping text mid-word (https://github.com/emilk/egui/issues/55)
- [x] Emoji
- [x] More fonts than those in
TextStyle(https://github.com/emilk/egui/pull/1154) - [ ] https://github.com/emilk/egui/issues/62
- [ ] https://github.com/emilk/egui/issues/1016
- [ ] https://github.com/emilk/egui/issues/2532
- [ ] https://github.com/emilk/egui/issues/3378
- [ ] https://github.com/emilk/egui/issues/5233
- [ ] Colored emoji
A few links (thanks @parasyte):
- https://raphlinus.github.io/rust/skribo/text/2019/02/27/text-layout-kickoff.html
- https://yeslogic.com/blog/allsorts-rust-font-shaping-engine.html
- https://kas-gui.github.io/blog/why-kas-text.html
I think storing the text has a signed distance field instead of a simple white on black bitmap should also be considered. (scales better and uses less memory) https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf
It also allows for all sorts of effects (outline, shadows...)
@msklywenn SDF text has some benefits, like being able to scale text, but come with the downside that the Egui fragment shader will no longer be a simple texture sampler. Currently the same shader supports showing text as well as images. If we switch to a distance field font we would need a more complicated shader taking in both a distance field texture and a normal texture to conveniently support both use cases (or have two shaders and switch between them which comes with its own downsides). I'd like to keep Egui integration as simple as possible for now.
I would also dispute that SDF fonts use less memory - that is only really true for when one want to write really large characters (which Egui doesn't really need). For small characters SDF:s actually tend to use more texels (margin for the blurry "glow").
Basic SDF rendering only needs alpha testing. To have one shader compatible with bitmap and SDF rendering, a solution would be to have an offset be sent to the shader and applied before discarding. With an offset of 0, discarding would never happen and bitmap display would occur just like it does now. With a -0.5 offset, negative alpha values would be discarded. That offset could then also be negated and added so that the pixels that pass don't look half translucent.
In my experience, SDF looks better even at low resolution as it can be thought of a cheap antialiasing. Also, the scaling of SDF resolves the problem of hi-DPI screens, it is not just to render big texts.
On low-DPI screens alpha-tested text will look horrible, but I take your point of having a single texture sampler and the programmatically controlling whether to treat it as an SDF or as a color image. Still, there is a lot of complexity here:
- Quickly generating SDF:s from TTF:s
- Adding new vertex attributes to switch shader mode
- More complicated shader
The major benefit would include being able to have dynamic text size instead of picking from a small set of sizes. Is there an urgent need for this? Otherwise I don't see SDF text as being a high priority. If you feel differently please open a separate issue on it and we can continue the discussion there!
There's no urgent need. I just thought it would fit nicely into that todo list up there :)
The major benefit would include being able to have dynamic text size instead of picking from a small set of sizes.
Meanwhile, there could be a way to specify a specific font and size in text widgets instead of limiting it to TextStyles. Perhaps doing something like this:
// app setup
font_definitions.custom_styles.insert(
"noto-sans-bold-14".to_owned(),
(FontFamily::Custom("noto-sans-bold".to_owned()), 14)
);
// ui update
ui.add(Label::new("foo").text_style(TextStyle::Custom("noto-sans-bold-14".to_owned()));
Where the string in FontFamily::Custom would be a key into the font_data map. The String in TextStyle::Custom could even be a &'static str for simplicity. Having the liberty of using different font styles for different sections of the UI is necessary for my use case.
Platform
GPU. Intel hd graphics 620 Mesa 20.3.4 OS. Fedora 33 egui-platform. egui_wgpu_backend 0.4 I override font by under code but japanese text is blank. I checked font and rusttype but there is no problem. Is there any platform dependent problem in text rendering?
let mut font_def=FontDefinitions::default();
let mut font_data:BTreeMap<String,Cow<'static,[u8]>> =BTreeMap::new();
let mut fonts_for_family:BTreeMap<FontFamily, Vec<String>>=BTreeMap::new();
font_data.insert("NotoSans".to_owned(), Cow::Borrowed(include_bytes!("../NotoSans-Light.ttf")));
font_data.insert("NotoSansCJKjp".to_owned(), Cow::Borrowed(include_bytes!("../NotoSansCJKjp-Light.otf")));
font_def.font_data=font_data;
//font_def.family_and_size.insert(TextStyle::Body,(FontFamily::Proportional,24.0));
fonts_for_family.insert(FontFamily::Monospace,vec![
"NotoSans".to_owned(),
"NotoSansCJKjp".to_owned(),
]);
fonts_for_family.insert(FontFamily::Proportional,vec![
"NotoSans".to_owned(),
"NotoSansCJKjp".to_owned(),
]);
font_def.fonts_for_family=fonts_for_family;
I squashed 40000+ chars to 3000+ chars NotoSansCJKjp-Light and expand texture atlas to 0x4000 by 0x4000 from original 2048 by 64 but all japanese texts are tofu (white block) and hit some limits in GPU(MAX_TEXTURE_SIZE) we may need to introduce 3d texture to texture atlas
rusttype image sample use layout and i tried generate glyph atlas for some short japanese text in my code below "日本語" by notoSansCJKjp-Light.otf font glyph.pixel_bounding_box() returns None
pub fn add_new_glyph_to_atlas(&mut self,c:GlyphId){
let glyph=self.font.glyph(c).scaled(Scale::uniform(self.scale as f32));
let glyph =glyph.positioned(Point{ x: 0.0, y: 0.0 });
match glyph.pixel_bounding_box(){
None => {
// no glyph
println!("no glyph for GlyphId {}",c.0)
}
Some(bb) => {
//if texture size over allocate new line
println!("origin.x {} max.x {}",self.texture.origin[0],bb.max.x);
if (self.texture.origin[0]+ bb.max.x as usize )>self.texture.dimension[0] {
//add line
self.texture.origin[1]+=self.scale as usize;
//extend texture height
self.texture.dimension[1]+=self.scale as usize;
//allocate new line
self.texture.data.extend_from_slice(&vec![0;self.texture.dimension[0]*self.scale as usize]);
// origin.x to 0
self.texture.origin[0]=0;
println!("alloc new line")
}else {
self.texture.origin[0]+=bb.max.x as usize;
}
glyph.draw(|x, y, v| {
//write data to texture
self.texture.data[
(self.texture.origin[1] + y as usize) * self.texture.dimension[0] +(self.texture.origin[0] + x as usize)
] = (v * 255.0) as u8;
});
//write where the top left
self.font_cache.insert(c,Rect{ position: [self.texture.origin[0],self.texture.origin[1]], size: [self.scale as usize,self.scale as usize] });
//
}
}
}
I found darty workable font caching for japanese but NotoSansCJKjp-Light.otf is not workable with rusttype
use std::collections::HashMap;
use rusttype::{Scale, Point, GlyphId, point, PositionedGlyph};
use image::ImageFormat;
use std::sync::Arc;
pub struct Texture{
dimension:[usize;2],
data:Vec<u8>,
origin:[usize;2],
}
pub struct Font{
font_cache:FontCache,
scale:i32,
texture:Texture,
glyph_per_line:i32,
font:Arc<rusttype::Font<'static>>,
name:&'static str
}
impl Font{
pub fn new_from_bytes(data: &'static[u8],name:&'static str,gpl:i32,scale:i32) ->Self{
let font=rusttype::Font::try_from_bytes(data).expect("Invalid font data");
println!(" glyphs {}", font.glyph_count());
// left one pixel margin for avoid debris
let one_line=( scale+1)*gpl;
Self{ font_cache: Default::default(), scale, texture: Texture { dimension: [one_line as usize,1+scale as usize], data:vec![0;(one_line*(scale+1))as usize], origin: [0,0] }, glyph_per_line: gpl, font:Arc::new(font), name }
}
pub fn add_new_glyph_to_atlas(&mut self,c:char){
// is font_cache contains glyph for c?
// then skip rasterize
if self.font_cache.contains_key(&c){
return
}
let tmp_str=c.to_string();
let fc=self.font.clone();
let glyphs=fc.layout(&tmp_str,Scale::uniform(self.scale as f32),point(0.0,0.0));
for glyph in glyphs{
if let Some(bounding_box)=glyph.pixel_bounding_box(){
//if texture size over allocate new line
if (self.texture.origin[0]+1+self.scale as usize )>self.texture.dimension[0] {
//add line
self.texture.origin[1]+=1+self.scale as usize;
//extend texture height
self.texture.dimension[1]+=1+self.scale as usize;
//allocate new line
self.texture.data.extend_from_slice(&vec![0;self.texture.dimension[0]*(2+self.scale as usize)]);
// origin.x to 1
self.texture.origin[0]=1;
println!("alloc new line")
}else {
self.texture.origin[0]=self.texture.origin[0] +1+ self.scale as usize;
}
glyph.draw(|x, y, v| {
//write data to texture
println!("Debug {}",self.texture.origin[1] );
let address=(self.texture.origin[1] + y as usize) * self.texture.dimension[0] +(self.texture.origin[0] + x as usize);
if address<self.texture.data.len() {
self.texture.data[address] = (v * 255.0) as u8;
}
});
//write where the top left
self.font_cache.insert(c,Rect{ position: [self.texture.origin[0],self.texture.origin[1]], size: [self.scale as usize,self.scale as usize] });
}else{
println!("No glyph for {}",c)
}
}
}
}
#[derive(Copy, Clone,Eq, PartialEq,Ord, PartialOrd)]
pub struct Rect{
position:[usize;2],
size:[usize;2],
}
pub type FontCache=HashMap<char,Rect>;
#[test]
fn text_rusttype_texture_atlas(){
let test_text="日本語でレンダリング!";
let mut font =Font::new_from_bytes(include_bytes!("../WenQuanYiMicroHei.ttf"), "WenQuanyiMicroHei", 10,32);
for ch in test_text.chars(){
font.add_new_glyph_to_atlas(ch)
}
let img =image::GrayImage::from_raw(font.texture.dimension[0] as u32,font.texture.dimension[1] as u32,font.texture.data).expect("Failed to create image");
img.save_with_format("WenQuanYiMicroHei-32.bmp",ImageFormat::Bmp);
}
I tested WenQuanYiMicroHei with unmodded epaint works fine so i recommend WenQuanYiMicroHei font include
I'll link swash, though tests are still a WIP.
You probably also want to look into font hinting, it nudges the edges of the vectors to make text look more crisp on older screens. It used to be standard, for newer higher resolution screens it's not so important. I just wish that the old screens wouldn't be left behind :older_adult: :shinto_shrine:
I think swash supports that as well, but I'm not entirely sure.
pop-os is doing some stuff which might of interest to this issue. https://github.com/pop-os/cosmic-text
I can't use this for my purpose without right-to-left character support. Is there a workout I can use until this is added?
Maybe this should be included as part of your category "All" of Unicode.
Is there currently a way to use font icons, such as Material Symbols and iconfont? If possible, it will be very convenient to use some simple plain icons.
Reasons to use font icon instead of image: When I use png or svg as the icon, there will be obvious distortion (such as jagged or missing elements) when the size is small.
Maybe this should be included as part of your category
"All" of Unicode.Is there currently a way to use font icons, such as Material Symbols and iconfont? If possible, it will be very convenient to use some simple plain icons.
Reasons to use font icon instead of image: When I use png or svg as the icon, there will be obvious distortion (such as jagged or missing elements) when the size is small.
Yes! There are even crates for that, for example for Phosphor icons: https://lib.rs/crates/egui-phosphor Basically you just add the font and use the correct codepoint - you can have a look at how egui-phosphor does it.