mirror of
https://github.com/emilk/egui.git
synced 2026-06-27 15:13:12 -04:00
<!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review your PR, but my time is limited! --> * Closes N/A * [x] I have followed the instructions in the PR template I'll probably come back to this and clean it up a bit. This PR reimplements ab_glyph's functionality on top of Skrifa, a somewhat lower-level font API that's being used in Chrome now. Skrifa doesn't perform rasterization itself, so I'm using [vello_cpu](https://github.com/linebender/vello) from the Linebender project for rasterization. It's still in its early days, but I believe it's already quite fast. It also supports color and gradient fills, so color emoji support will be easier. Skrifa also supports font hinting, which should make text look a bit nicer / less blurry. Here's the current ab_glyph rendering: <img width="1592" height="1068" alt="image" src="https://github.com/user-attachments/assets/2385b66e-23f8-4c6e-b8c2-ea90e0eea4e4" /> Here's Skrifa *without* hinting--it looks almost identical, but there are some subpixel differences, probably due to rasterizer behavior: <img width="1592" height="1068" alt="image" src="https://github.com/user-attachments/assets/a815f3e9-65ac-4940-bc00-571177bef53d" /> Here's Skrifa *with* hinting: <img width="1592" height="1068" alt="image" src="https://github.com/user-attachments/assets/d6cc0669-3537-4377-bba9-ed5ef09664db" /> Hinting does make the horizontal strokes look a bit bolder, which makes me wonder once again about increasing the font weight from "light" to "regular". --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
279 lines
8.5 KiB
Rust
279 lines
8.5 KiB
Rust
use ecolor::Color32;
|
|
use emath::{Rect, remap_clamp};
|
|
|
|
use crate::{ColorImage, ImageDelta, TextOptions};
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
struct Rectu {
|
|
/// inclusive
|
|
min_x: usize,
|
|
|
|
/// inclusive
|
|
min_y: usize,
|
|
|
|
/// exclusive
|
|
max_x: usize,
|
|
|
|
/// exclusive
|
|
max_y: usize,
|
|
}
|
|
|
|
impl Rectu {
|
|
const NOTHING: Self = Self {
|
|
min_x: usize::MAX,
|
|
min_y: usize::MAX,
|
|
max_x: 0,
|
|
max_y: 0,
|
|
};
|
|
const EVERYTHING: Self = Self {
|
|
min_x: 0,
|
|
min_y: 0,
|
|
max_x: usize::MAX,
|
|
max_y: usize::MAX,
|
|
};
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
struct PrerasterizedDisc {
|
|
r: f32,
|
|
uv: Rectu,
|
|
}
|
|
|
|
/// A pre-rasterized disc (filled circle), somewhere in the texture atlas.
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub struct PreparedDisc {
|
|
/// The radius of this disc in texels.
|
|
pub r: f32,
|
|
|
|
/// Width in texels.
|
|
pub w: f32,
|
|
|
|
/// Where in the texture atlas the disc is.
|
|
/// Normalized in 0-1 range.
|
|
pub uv: Rect,
|
|
}
|
|
|
|
/// Contains font data in an atlas, where each character occupied a small rectangle.
|
|
///
|
|
/// More characters can be added, possibly expanding the texture.
|
|
#[derive(Clone)]
|
|
pub struct TextureAtlas {
|
|
image: ColorImage,
|
|
|
|
/// What part of the image that is dirty
|
|
dirty: Rectu,
|
|
|
|
/// Used for when allocating new rectangles.
|
|
cursor: (usize, usize),
|
|
|
|
row_height: usize,
|
|
|
|
/// Set when someone requested more space than was available.
|
|
overflowed: bool,
|
|
|
|
/// pre-rasterized discs of radii `2^i`, where `i` is the index.
|
|
discs: Vec<PrerasterizedDisc>,
|
|
|
|
/// Controls how to convert glyph coverage to alpha.
|
|
options: TextOptions,
|
|
}
|
|
|
|
impl TextureAtlas {
|
|
pub fn new(size: [usize; 2], options: TextOptions) -> Self {
|
|
assert!(size[0] >= 1024, "Tiny texture atlas");
|
|
let mut atlas = Self {
|
|
image: ColorImage::filled(size, Color32::TRANSPARENT),
|
|
dirty: Rectu::EVERYTHING,
|
|
cursor: (0, 0),
|
|
row_height: 0,
|
|
overflowed: false,
|
|
discs: vec![], // will be filled in below
|
|
options,
|
|
};
|
|
|
|
// Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color:
|
|
let (pos, image) = atlas.allocate((1, 1));
|
|
assert_eq!(
|
|
pos,
|
|
(0, 0),
|
|
"Expected the first allocation to be at (0, 0), but was at {pos:?}"
|
|
);
|
|
image[pos] = Color32::WHITE;
|
|
|
|
// Allocate a series of anti-aliased discs used to render small filled circles:
|
|
// TODO(emilk): these circles can be packed A LOT better.
|
|
// In fact, the whole texture atlas could be packed a lot better.
|
|
// for r in [1, 2, 4, 8, 16, 32, 64] {
|
|
// let w = 2 * r + 3;
|
|
// let hw = w as i32 / 2;
|
|
const LARGEST_CIRCLE_RADIUS: f32 = 8.0; // keep small so that the initial texture atlas is small
|
|
for i in 0.. {
|
|
let r = 2.0_f32.powf(i as f32 / 2.0 - 1.0);
|
|
if r > LARGEST_CIRCLE_RADIUS {
|
|
break;
|
|
}
|
|
let hw = (r + 0.5).ceil() as i32;
|
|
let w = (2 * hw + 1) as usize;
|
|
let ((x, y), image) = atlas.allocate((w, w));
|
|
for dx in -hw..=hw {
|
|
for dy in -hw..=hw {
|
|
let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
|
|
let coverage =
|
|
remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0);
|
|
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] =
|
|
options.alpha_from_coverage.color_from_coverage(coverage);
|
|
}
|
|
}
|
|
atlas.discs.push(PrerasterizedDisc {
|
|
r,
|
|
uv: Rectu {
|
|
min_x: x,
|
|
min_y: y,
|
|
max_x: x + w,
|
|
max_y: y + w,
|
|
},
|
|
});
|
|
}
|
|
|
|
atlas
|
|
}
|
|
|
|
pub fn options(&self) -> &TextOptions {
|
|
&self.options
|
|
}
|
|
|
|
pub fn size(&self) -> [usize; 2] {
|
|
self.image.size
|
|
}
|
|
|
|
/// Returns the locations and sizes of pre-rasterized discs (filled circles) in this atlas.
|
|
pub fn prepared_discs(&self) -> Vec<PreparedDisc> {
|
|
let size = self.size();
|
|
let inv_w = 1.0 / size[0] as f32;
|
|
let inv_h = 1.0 / size[1] as f32;
|
|
self.discs
|
|
.iter()
|
|
.map(|disc| {
|
|
let r = disc.r;
|
|
let Rectu {
|
|
min_x,
|
|
min_y,
|
|
max_x,
|
|
max_y,
|
|
} = disc.uv;
|
|
let w = max_x - min_x;
|
|
let uv = Rect::from_min_max(
|
|
emath::pos2(min_x as f32 * inv_w, min_y as f32 * inv_h),
|
|
emath::pos2(max_x as f32 * inv_w, max_y as f32 * inv_h),
|
|
);
|
|
PreparedDisc { r, w: w as f32, uv }
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn max_height(&self) -> usize {
|
|
// the initial width is set to the max size
|
|
self.image.height().max(self.image.width())
|
|
}
|
|
|
|
/// When this get high, it might be time to clear and start over!
|
|
pub fn fill_ratio(&self) -> f32 {
|
|
if self.overflowed {
|
|
1.0
|
|
} else {
|
|
(self.cursor.1 + self.row_height) as f32 / self.max_height() as f32
|
|
}
|
|
}
|
|
|
|
/// The texture options suitable for a font texture
|
|
#[inline]
|
|
pub fn texture_options() -> crate::textures::TextureOptions {
|
|
crate::textures::TextureOptions::LINEAR
|
|
}
|
|
|
|
/// The full font atlas image.
|
|
#[inline]
|
|
pub fn image(&self) -> &ColorImage {
|
|
&self.image
|
|
}
|
|
|
|
/// Call to get the change to the image since last call.
|
|
pub fn take_delta(&mut self) -> Option<ImageDelta> {
|
|
let texture_options = Self::texture_options();
|
|
|
|
let dirty = std::mem::replace(&mut self.dirty, Rectu::NOTHING);
|
|
if dirty == Rectu::NOTHING {
|
|
None
|
|
} else if dirty == Rectu::EVERYTHING {
|
|
Some(ImageDelta::full(self.image.clone(), texture_options))
|
|
} else {
|
|
let pos = [dirty.min_x, dirty.min_y];
|
|
let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y];
|
|
let region = self.image.region_by_pixels(pos, size);
|
|
Some(ImageDelta::partial(pos, region, texture_options))
|
|
}
|
|
}
|
|
|
|
/// Returns the coordinates of where the rect ended up,
|
|
/// and invalidates the region.
|
|
pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut ColorImage) {
|
|
/// On some low-precision GPUs (my old iPad) characters get muddled up
|
|
/// if we don't add some empty pixels between the characters.
|
|
/// On modern high-precision GPUs this is not needed.
|
|
const PADDING: usize = 1;
|
|
|
|
assert!(
|
|
w <= self.image.width(),
|
|
"Tried to allocate a {} wide glyph in a {} wide texture atlas",
|
|
w,
|
|
self.image.width()
|
|
);
|
|
if self.cursor.0 + w > self.image.width() {
|
|
// New row:
|
|
self.cursor.0 = 0;
|
|
self.cursor.1 += self.row_height + PADDING;
|
|
self.row_height = 0;
|
|
}
|
|
|
|
self.row_height = self.row_height.max(h);
|
|
|
|
let required_height = self.cursor.1 + self.row_height;
|
|
|
|
if required_height > self.max_height() {
|
|
// This is a bad place to be - we need to start reusing space :/
|
|
|
|
log::warn!("epaint texture atlas overflowed!");
|
|
|
|
self.cursor = (0, self.image.height() / 3); // Restart a bit down - the top of the atlas has too many important things in it
|
|
self.overflowed = true; // this will signal the user that we need to recreate the texture atlas next frame.
|
|
} else if resize_to_min_height(&mut self.image, required_height) {
|
|
self.dirty = Rectu::EVERYTHING;
|
|
}
|
|
|
|
let pos = self.cursor;
|
|
self.cursor.0 += w + PADDING;
|
|
|
|
self.dirty.min_x = self.dirty.min_x.min(pos.0);
|
|
self.dirty.min_y = self.dirty.min_y.min(pos.1);
|
|
self.dirty.max_x = self.dirty.max_x.max(pos.0 + w);
|
|
self.dirty.max_y = self.dirty.max_y.max(pos.1 + h);
|
|
|
|
(pos, &mut self.image)
|
|
}
|
|
}
|
|
|
|
fn resize_to_min_height(image: &mut ColorImage, required_height: usize) -> bool {
|
|
while required_height >= image.height() {
|
|
image.size[1] *= 2; // double the height
|
|
}
|
|
|
|
if image.width() * image.height() > image.pixels.len() {
|
|
image
|
|
.pixels
|
|
.resize(image.width() * image.height(), Color32::TRANSPARENT);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|