egui icon indicating copy to clipboard operation
egui copied to clipboard

Improve text

Open emilk opened this issue 5 years ago • 18 comments

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

emilk avatar Nov 30 '20 05:11 emilk

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 avatar Jan 03 '21 16:01 msklywenn

@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").

emilk avatar Jan 03 '21 17:01 emilk

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.

msklywenn avatar Jan 03 '21 17:01 msklywenn

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!

emilk avatar Jan 03 '21 19:01 emilk

There's no urgent need. I just thought it would fit nicely into that todo list up there :)

msklywenn avatar Jan 03 '21 19:01 msklywenn

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.

manokara avatar Jan 18 '21 14:01 manokara

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;

KentaTheBugMaker avatar Feb 15 '21 17:02 KentaTheBugMaker

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

KentaTheBugMaker avatar Feb 15 '21 20:02 KentaTheBugMaker

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] });
                //
                }

        }
    }

KentaTheBugMaker avatar Feb 16 '21 11:02 KentaTheBugMaker

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);
}

KentaTheBugMaker avatar Feb 16 '21 15:02 KentaTheBugMaker

I tested WenQuanYiMicroHei with unmodded epaint works fine so i recommend WenQuanYiMicroHei font include

KentaTheBugMaker avatar Feb 16 '21 15:02 KentaTheBugMaker

I'll link swash, though tests are still a WIP.

kirawi avatar Sep 05 '21 03:09 kirawi

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:

makoConstruct avatar Oct 04 '21 22:10 makoConstruct

I think swash supports that as well, but I'm not entirely sure.

kirawi avatar Oct 04 '21 22:10 kirawi

pop-os is doing some stuff which might of interest to this issue. https://github.com/pop-os/cosmic-text

coderedart avatar Nov 11 '22 13:11 coderedart

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?

NatanFreeman avatar Jan 06 '23 13:01 NatanFreeman

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.

lopo12123 avatar Oct 03 '23 13:10 lopo12123

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.

woelper avatar Oct 19 '23 09:10 woelper