Make the font atlas use a color image (#7298)

* [x] I have followed the instructions in the PR template

Splitting this out from the Parley work as requested. This removes
`FontImage` and makes the font atlas use a `ColorImage`. It converts
alpha to coverage at glyph-drawing time, not at delta-upload time.

This doesn't do much now, but will allow for color emoji rendering once
we start using Parley.

I've changed things around so that we pass in `text_alpha_to_coverage`
to the `Fonts` the same way we do with `pixels_per_point` and
`max_texture_side`, reusing the existing code to check if the setting
differs and recreating the font atlas if so. I'm not quite sure why this
wasn't done in the first place.

I've left `ImageData` as an enum for now, in case we want to add support
for more texture pixel formats in the future (which I personally think
would be worthwhile). If you'd like, I can just remove that enum
entirely.
This commit is contained in:
valadaptive 2025-07-04 07:15:48 -04:00 committed by GitHub
parent d94386de3d
commit 7ac137bfc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 147 additions and 243 deletions

View File

@ -564,19 +564,6 @@ impl Renderer {
); );
Cow::Borrowed(&image.pixels) Cow::Borrowed(&image.pixels)
} }
epaint::ImageData::Font(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
"Mismatch between texture size and texel count"
);
profiling::scope!("font -> sRGBA");
Cow::Owned(
image
.srgba_pixels(Default::default())
.collect::<Vec<epaint::Color32>>(),
)
}
}; };
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());

View File

@ -78,7 +78,7 @@ impl Default for WrappedTextureManager {
// Will be filled in later // Will be filled in later
let font_id = tex_mngr.alloc( let font_id = tex_mngr.alloc(
"egui_font_texture".into(), "egui_font_texture".into(),
epaint::FontImage::new([0, 0]).into(), epaint::ColorImage::filled([0, 0], Color32::TRANSPARENT).into(),
Default::default(), Default::default(),
); );
assert_eq!( assert_eq!(
@ -610,6 +610,8 @@ impl ContextImpl {
log::trace!("Adding new fonts"); log::trace!("Adding new fonts");
} }
let text_alpha_from_coverage = self.memory.options.style().visuals.text_alpha_from_coverage;
let mut is_new = false; let mut is_new = false;
let fonts = self let fonts = self
@ -624,13 +626,14 @@ impl ContextImpl {
Fonts::new( Fonts::new(
pixels_per_point, pixels_per_point,
max_texture_side, max_texture_side,
text_alpha_from_coverage,
self.font_definitions.clone(), self.font_definitions.clone(),
) )
}); });
{ {
profiling::scope!("Fonts::begin_pass"); profiling::scope!("Fonts::begin_pass");
fonts.begin_pass(pixels_per_point, max_texture_side); fonts.begin_pass(pixels_per_point, max_texture_side, text_alpha_from_coverage);
} }
if is_new && self.memory.options.preload_font_glyphs { if is_new && self.memory.options.preload_font_glyphs {
@ -1921,16 +1924,6 @@ impl Context {
} }
} }
pub(crate) fn reset_font_atlas(&self) {
let pixels_per_point = self.pixels_per_point();
let fonts = self.read(|ctx| {
ctx.fonts
.get(&pixels_per_point.into())
.map(|current_fonts| current_fonts.lock().fonts.definitions().clone())
});
self.memory_mut(|mem| mem.new_font_definitions = fonts);
}
/// Tell `egui` which fonts to use. /// Tell `egui` which fonts to use.
/// ///
/// The default `egui` fonts only support latin and cyrillic alphabets, /// The default `egui` fonts only support latin and cyrillic alphabets,
@ -2066,19 +2059,10 @@ impl Context {
/// You can use [`Ui::style_mut`] to change the style of a single [`Ui`]. /// You can use [`Ui::style_mut`] to change the style of a single [`Ui`].
pub fn set_style_of(&self, theme: Theme, style: impl Into<Arc<Style>>) { pub fn set_style_of(&self, theme: Theme, style: impl Into<Arc<Style>>) {
let style = style.into(); let style = style.into();
let mut recreate_font_atlas = false; self.options_mut(|opt| match theme {
self.options_mut(|opt| { Theme::Dark => opt.dark_style = style,
let dest = match theme { Theme::Light => opt.light_style = style,
Theme::Dark => &mut opt.dark_style,
Theme::Light => &mut opt.light_style,
};
recreate_font_atlas =
dest.visuals.text_alpha_from_coverage != style.visuals.text_alpha_from_coverage;
*dest = style;
}); });
if recreate_font_atlas {
self.reset_font_atlas();
}
} }
/// The [`crate::Visuals`] used by all subsequent windows, panels etc. /// The [`crate::Visuals`] used by all subsequent windows, panels etc.
@ -2475,28 +2459,7 @@ impl ContextImpl {
} }
// Inform the backend of all textures that have been updated (including font atlas). // Inform the backend of all textures that have been updated (including font atlas).
let textures_delta = { let textures_delta = self.tex_manager.0.write().take_delta();
// HACK to get much nicer looking text in light mode.
// This assumes all text is black-on-white in light mode,
// and white-on-black in dark mode, which is not necessarily true,
// but often close enough.
// Of course this fails for cases when there is black-on-white text in dark mode,
// and white-on-black text in light mode.
let text_alpha_from_coverage =
self.memory.options.style().visuals.text_alpha_from_coverage;
let mut textures_delta = self.tex_manager.0.write().take_delta();
for (_, delta) in &mut textures_delta.set {
if let ImageData::Font(font) = &mut delta.image {
delta.image =
ImageData::Color(font.to_color_image(text_alpha_from_coverage).into());
}
}
textures_delta
};
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output); let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
@ -3094,17 +3057,9 @@ impl Context {
options.ui(ui); options.ui(ui);
let text_alpha_from_coverage_changed =
prev_options.style().visuals.text_alpha_from_coverage
!= options.style().visuals.text_alpha_from_coverage;
if options != prev_options { if options != prev_options {
self.options_mut(move |o| *o = options); self.options_mut(move |o| *o = options);
} }
if text_alpha_from_coverage_changed {
ui.ctx().reset_font_atlas();
}
} }
fn fonts_tweak_ui(&self, ui: &mut Ui) { fn fonts_tweak_ui(&self, ui: &mut Ui) {

View File

@ -467,7 +467,7 @@ pub use emath::{
remap_clamp, vec2, remap_clamp, vec2,
}; };
pub use epaint::{ pub use epaint::{
ClippedPrimitive, ColorImage, CornerRadius, FontImage, ImageData, Margin, Mesh, PaintCallback, ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex, PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta}, textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},

View File

@ -168,6 +168,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let fonts = egui::epaint::text::Fonts::new( let fonts = egui::epaint::text::Fonts::new(
pixels_per_point, pixels_per_point,
max_texture_side, max_texture_side,
egui::epaint::AlphaFromCoverage::default(),
egui::FontDefinitions::default(), egui::FontDefinitions::default(),
); );
{ {
@ -210,7 +211,11 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let mut rng = rand::rng(); let mut rng = rand::rng();
b.iter(|| { b.iter(|| {
fonts.begin_pass(pixels_per_point, max_texture_side); fonts.begin_pass(
pixels_per_point,
max_texture_side,
egui::epaint::AlphaFromCoverage::default(),
);
// Delete a random character, simulating a user making an edit in a long file: // Delete a random character, simulating a user making an edit in a long file:
let mut new_string = string.clone(); let mut new_string = string.clone();

View File

@ -534,23 +534,6 @@ impl Painter {
self.upload_texture_srgb(delta.pos, image.size, delta.options, data); self.upload_texture_srgb(delta.pos, image.size, delta.options, data);
} }
egui::ImageData::Font(image) => {
assert_eq!(
image.width() * image.height(),
image.pixels.len(),
"Mismatch between texture size and texel count"
);
let data: Vec<u8> = {
profiling::scope!("font -> sRGBA");
image
.srgba_pixels(Default::default())
.flat_map(|a| a.to_array())
.collect()
};
self.upload_texture_srgb(delta.pos, image.size, delta.options, &data);
}
}; };
} }

View File

@ -1,8 +1,8 @@
use criterion::{Criterion, black_box, criterion_group, criterion_main}; use criterion::{Criterion, black_box, criterion_group, criterion_main};
use epaint::{ use epaint::{
ClippedShape, Color32, Mesh, PathStroke, Pos2, Rect, Shape, Stroke, TessellationOptions, AlphaFromCoverage, ClippedShape, Color32, Mesh, PathStroke, Pos2, Rect, Shape, Stroke,
Tessellator, TextureAtlas, Vec2, pos2, tessellator::Path, TessellationOptions, Tessellator, TextureAtlas, Vec2, pos2, tessellator::Path,
}; };
#[global_allocator] #[global_allocator]
@ -66,7 +66,7 @@ fn tessellate_circles(c: &mut Criterion) {
let pixels_per_point = 2.0; let pixels_per_point = 2.0;
let options = TessellationOptions::default(); let options = TessellationOptions::default();
let atlas = TextureAtlas::new([4096, 256]); let atlas = TextureAtlas::new([4096, 256], AlphaFromCoverage::default());
let font_tex_size = atlas.size(); let font_tex_size = atlas.size();
let prepared_discs = atlas.prepared_discs(); let prepared_discs = atlas.prepared_discs();

View File

@ -7,24 +7,20 @@ use std::sync::Arc;
/// ///
/// To load an image file, see [`ColorImage::from_rgba_unmultiplied`]. /// To load an image file, see [`ColorImage::from_rgba_unmultiplied`].
/// ///
/// In order to paint the image on screen, you first need to convert it to /// This is currently an enum with only one variant, but more image types may be added in the future.
/// ///
/// See also: [`ColorImage`], [`FontImage`]. /// See also: [`ColorImage`].
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageData { pub enum ImageData {
/// RGBA image. /// RGBA image.
Color(Arc<ColorImage>), Color(Arc<ColorImage>),
/// Used for the font texture.
Font(FontImage),
} }
impl ImageData { impl ImageData {
pub fn size(&self) -> [usize; 2] { pub fn size(&self) -> [usize; 2] {
match self { match self {
Self::Color(image) => image.size, Self::Color(image) => image.size,
Self::Font(image) => image.size,
} }
} }
@ -38,7 +34,7 @@ impl ImageData {
pub fn bytes_per_pixel(&self) -> usize { pub fn bytes_per_pixel(&self) -> usize {
match self { match self {
Self::Color(_) | Self::Font(_) => 4, Self::Color(_) => 4,
} }
} }
} }
@ -271,6 +267,37 @@ impl ColorImage {
} }
Self::new([width, height], output) Self::new([width, height], output)
} }
/// Clone a sub-region as a new image.
pub fn region_by_pixels(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self {
assert!(
x + w <= self.width(),
"x + w should be <= self.width(), but x: {}, w: {}, width: {}",
x,
w,
self.width()
);
assert!(
y + h <= self.height(),
"y + h should be <= self.height(), but y: {}, h: {}, height: {}",
y,
h,
self.height()
);
let mut pixels = Vec::with_capacity(w * h);
for y in y..y + h {
let offset = y * self.width() + x;
pixels.extend(&self.pixels[offset..(offset + w)]);
}
assert_eq!(
pixels.len(),
w * h,
"pixels.len should be w * h, but got {}",
pixels.len()
);
Self::new([w, h], pixels)
}
} }
impl std::ops::Index<(usize, usize)> for ColorImage { impl std::ops::Index<(usize, usize)> for ColorImage {
@ -371,127 +398,12 @@ impl AlphaFromCoverage {
} }
} }
/// A single-channel image designed for the font texture.
///
/// Each value represents "coverage", i.e. how much a texel is covered by a character.
///
/// This is roughly interpreted as the opacity of a white image.
#[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FontImage {
/// width, height
pub size: [usize; 2],
/// The coverage value.
///
/// Often you want to use [`Self::srgba_pixels`] instead.
pub pixels: Vec<f32>,
}
impl FontImage {
pub fn new(size: [usize; 2]) -> Self {
Self {
size,
pixels: vec![0.0; size[0] * size[1]],
}
}
#[inline]
pub fn width(&self) -> usize {
self.size[0]
}
#[inline]
pub fn height(&self) -> usize {
self.size[1]
}
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
#[inline]
pub fn srgba_pixels(
&self,
alpha_from_coverage: AlphaFromCoverage,
) -> impl ExactSizeIterator<Item = Color32> + '_ {
self.pixels
.iter()
.map(move |&coverage| alpha_from_coverage.color_from_coverage(coverage))
}
/// Convert this coverage image to a [`ColorImage`].
pub fn to_color_image(&self, alpha_from_coverage: AlphaFromCoverage) -> ColorImage {
profiling::function_scope!();
let pixels = self.srgba_pixels(alpha_from_coverage).collect();
ColorImage::new(self.size, pixels)
}
/// Clone a sub-region as a new image.
pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self {
assert!(
x + w <= self.width(),
"x + w should be <= self.width(), but x: {}, w: {}, width: {}",
x,
w,
self.width()
);
assert!(
y + h <= self.height(),
"y + h should be <= self.height(), but y: {}, h: {}, height: {}",
y,
h,
self.height()
);
let mut pixels = Vec::with_capacity(w * h);
for y in y..y + h {
let offset = y * self.width() + x;
pixels.extend(&self.pixels[offset..(offset + w)]);
}
assert_eq!(
pixels.len(),
w * h,
"pixels.len should be w * h, but got {}",
pixels.len()
);
Self {
size: [w, h],
pixels,
}
}
}
impl std::ops::Index<(usize, usize)> for FontImage {
type Output = f32;
#[inline]
fn index(&self, (x, y): (usize, usize)) -> &f32 {
let [w, h] = self.size;
assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}");
&self.pixels[y * w + x]
}
}
impl std::ops::IndexMut<(usize, usize)> for FontImage {
#[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 {
let [w, h] = self.size;
assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}");
&mut self.pixels[y * w + x]
}
}
impl From<FontImage> for ImageData {
#[inline(always)]
fn from(image: FontImage) -> Self {
Self::Font(image)
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// A change to an image. /// A change to an image.
/// ///
/// Either a whole new image, or an update to a rectangular region of it. /// Either a whole new image, or an update to a rectangular region of it.
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[must_use = "The painter must take care of this"] #[must_use = "The painter must take care of this"]
pub struct ImageDelta { pub struct ImageDelta {

View File

@ -50,7 +50,7 @@ pub use self::{
color::ColorMode, color::ColorMode,
corner_radius::CornerRadius, corner_radius::CornerRadius,
corner_radius_f32::CornerRadiusF32, corner_radius_f32::CornerRadiusF32,
image::{AlphaFromCoverage, ColorImage, FontImage, ImageData, ImageDelta}, image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta},
margin::Margin, margin::Margin,
margin_f32::*, margin_f32::*,
mesh::{Mesh, Mesh16, Vertex}, mesh::{Mesh, Mesh16, Vertex},

View File

@ -179,7 +179,12 @@ mod tests {
#[test] #[test]
fn text_bounding_box_under_rotation() { fn text_bounding_box_under_rotation() {
let fonts = Fonts::new(1.0, 1024, FontDefinitions::default()); let fonts = Fonts::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);
let font = FontId::monospace(12.0); let font = FontId::monospace(12.0);
let mut t = crate::Shape::text( let mut t = crate::Shape::text(

View File

@ -279,12 +279,13 @@ impl FontImpl {
} else { } else {
let glyph_pos = { let glyph_pos = {
let atlas = &mut self.atlas.lock(); let atlas = &mut self.atlas.lock();
let text_alpha_from_coverage = atlas.text_alpha_from_coverage;
let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height)); let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
glyph.draw(|x, y, v| { glyph.draw(|x, y, v| {
if 0.0 < v { if 0.0 < v {
let px = glyph_pos.0 + x as usize; let px = glyph_pos.0 + x as usize;
let py = glyph_pos.1 + y as usize; let py = glyph_pos.1 + y as usize;
image[(px, py)] = v; image[(px, py)] = text_alpha_from_coverage.color_from_coverage(v);
} }
}); });
glyph_pos glyph_pos

View File

@ -1,7 +1,7 @@
use std::{collections::BTreeMap, sync::Arc}; use std::{collections::BTreeMap, sync::Arc};
use crate::{ use crate::{
TextureAtlas, AlphaFromCoverage, TextureAtlas,
mutex::{Mutex, MutexGuard}, mutex::{Mutex, MutexGuard},
text::{ text::{
Galley, LayoutJob, LayoutSection, Galley, LayoutJob, LayoutSection,
@ -430,36 +430,56 @@ impl Fonts {
pub fn new( pub fn new(
pixels_per_point: f32, pixels_per_point: f32,
max_texture_side: usize, max_texture_side: usize,
text_alpha_from_coverage: AlphaFromCoverage,
definitions: FontDefinitions, definitions: FontDefinitions,
) -> Self { ) -> Self {
let fonts_and_cache = FontsAndCache { let fonts_and_cache = FontsAndCache {
fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions), fonts: FontsImpl::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
definitions,
),
galley_cache: Default::default(), galley_cache: Default::default(),
}; };
Self(Arc::new(Mutex::new(fonts_and_cache))) Self(Arc::new(Mutex::new(fonts_and_cache)))
} }
/// Call at the start of each frame with the latest known /// Call at the start of each frame with the latest known
/// `pixels_per_point` and `max_texture_side`. /// `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`.
/// ///
/// Call after painting the previous frame, but before using [`Fonts`] for the new frame. /// Call after painting the previous frame, but before using [`Fonts`] for the new frame.
/// ///
/// This function will react to changes in `pixels_per_point` and `max_texture_side`, /// This function will react to changes in `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`,
/// as well as notice when the font atlas is getting full, and handle that. /// as well as notice when the font atlas is getting full, and handle that.
pub fn begin_pass(&self, pixels_per_point: f32, max_texture_side: usize) { pub fn begin_pass(
&self,
pixels_per_point: f32,
max_texture_side: usize,
text_alpha_from_coverage: AlphaFromCoverage,
) {
let mut fonts_and_cache = self.0.lock(); let mut fonts_and_cache = self.0.lock();
let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point; let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point;
let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side; let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side;
let text_alpha_from_coverage_changed =
fonts_and_cache.fonts.atlas.lock().text_alpha_from_coverage != text_alpha_from_coverage;
let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8; let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8;
let needs_recreate = let needs_recreate = pixels_per_point_changed
pixels_per_point_changed || max_texture_side_changed || font_atlas_almost_full; || max_texture_side_changed
|| text_alpha_from_coverage_changed
|| font_atlas_almost_full;
if needs_recreate { if needs_recreate {
let definitions = fonts_and_cache.fonts.definitions.clone(); let definitions = fonts_and_cache.fonts.definitions.clone();
*fonts_and_cache = FontsAndCache { *fonts_and_cache = FontsAndCache {
fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions), fonts: FontsImpl::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
definitions,
),
galley_cache: Default::default(), galley_cache: Default::default(),
}; };
} }
@ -497,7 +517,7 @@ impl Fonts {
/// The full font atlas image. /// The full font atlas image.
#[inline] #[inline]
pub fn image(&self) -> crate::FontImage { pub fn image(&self) -> crate::ColorImage {
self.lock().fonts.atlas.lock().image().clone() self.lock().fonts.atlas.lock().image().clone()
} }
@ -642,6 +662,7 @@ impl FontsImpl {
pub fn new( pub fn new(
pixels_per_point: f32, pixels_per_point: f32,
max_texture_side: usize, max_texture_side: usize,
text_alpha_from_coverage: AlphaFromCoverage,
definitions: FontDefinitions, definitions: FontDefinitions,
) -> Self { ) -> Self {
assert!( assert!(
@ -651,7 +672,7 @@ impl FontsImpl {
let texture_width = max_texture_side.at_most(16 * 1024); let texture_width = max_texture_side.at_most(16 * 1024);
let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways. let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways.
let atlas = TextureAtlas::new([texture_width, initial_height]); let atlas = TextureAtlas::new([texture_width, initial_height], text_alpha_from_coverage);
let atlas = Arc::new(Mutex::new(atlas)); let atlas = Arc::new(Mutex::new(atlas));
@ -1120,6 +1141,7 @@ mod tests {
let mut fonts = FontsImpl::new( let mut fonts = FontsImpl::new(
pixels_per_point, pixels_per_point,
max_texture_side, max_texture_side,
AlphaFromCoverage::default(),
FontDefinitions::default(), FontDefinitions::default(),
); );

View File

@ -1034,11 +1034,18 @@ fn is_cjk_break_allowed(c: char) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::AlphaFromCoverage;
use super::{super::*, *}; use super::{super::*, *};
#[test] #[test]
fn test_zero_max_width() { fn test_zero_max_width() {
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);
let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default()); let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default());
layout_job.wrap.max_width = 0.0; layout_job.wrap.max_width = 0.0;
let galley = layout(&mut fonts, layout_job.into()); let galley = layout(&mut fonts, layout_job.into());
@ -1049,7 +1056,12 @@ mod tests {
fn test_truncate_with_newline() { fn test_truncate_with_newline() {
// No matter where we wrap, we should be appending the newline character. // No matter where we wrap, we should be appending the newline character.
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);
let text_format = TextFormat { let text_format = TextFormat {
font_id: FontId::monospace(12.0), font_id: FontId::monospace(12.0),
..Default::default() ..Default::default()
@ -1094,7 +1106,12 @@ mod tests {
#[test] #[test]
fn test_cjk() { fn test_cjk() {
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);
let mut layout_job = LayoutJob::single_section( let mut layout_job = LayoutJob::single_section(
"日本語とEnglishの混在した文章".into(), "日本語とEnglishの混在した文章".into(),
TextFormat::default(), TextFormat::default(),
@ -1109,7 +1126,12 @@ mod tests {
#[test] #[test]
fn test_pre_cjk() { fn test_pre_cjk() {
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);
let mut layout_job = LayoutJob::single_section( let mut layout_job = LayoutJob::single_section(
"日本語とEnglishの混在した文章".into(), "日本語とEnglishの混在した文章".into(),
TextFormat::default(), TextFormat::default(),
@ -1124,7 +1146,12 @@ mod tests {
#[test] #[test]
fn test_truncate_width() { fn test_truncate_width() {
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);
let mut layout_job = let mut layout_job =
LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default()); LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default());
layout_job.wrap.max_width = f32::INFINITY; layout_job.wrap.max_width = f32::INFINITY;

View File

@ -1,6 +1,7 @@
use ecolor::Color32;
use emath::{Rect, remap_clamp}; use emath::{Rect, remap_clamp};
use crate::{FontImage, ImageDelta}; use crate::{AlphaFromCoverage, ColorImage, ImageDelta};
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Rectu { struct Rectu {
@ -57,7 +58,7 @@ pub struct PreparedDisc {
/// More characters can be added, possibly expanding the texture. /// More characters can be added, possibly expanding the texture.
#[derive(Clone)] #[derive(Clone)]
pub struct TextureAtlas { pub struct TextureAtlas {
image: FontImage, image: ColorImage,
/// What part of the image that is dirty /// What part of the image that is dirty
dirty: Rectu, dirty: Rectu,
@ -72,18 +73,22 @@ pub struct TextureAtlas {
/// pre-rasterized discs of radii `2^i`, where `i` is the index. /// pre-rasterized discs of radii `2^i`, where `i` is the index.
discs: Vec<PrerasterizedDisc>, discs: Vec<PrerasterizedDisc>,
/// Controls how to convert glyph coverage to alpha.
pub(crate) text_alpha_from_coverage: AlphaFromCoverage,
} }
impl TextureAtlas { impl TextureAtlas {
pub fn new(size: [usize; 2]) -> Self { pub fn new(size: [usize; 2], text_alpha_from_coverage: AlphaFromCoverage) -> Self {
assert!(size[0] >= 1024, "Tiny texture atlas"); assert!(size[0] >= 1024, "Tiny texture atlas");
let mut atlas = Self { let mut atlas = Self {
image: FontImage::new(size), image: ColorImage::filled(size, Color32::TRANSPARENT),
dirty: Rectu::EVERYTHING, dirty: Rectu::EVERYTHING,
cursor: (0, 0), cursor: (0, 0),
row_height: 0, row_height: 0,
overflowed: false, overflowed: false,
discs: vec![], // will be filled in below discs: vec![], // will be filled in below
text_alpha_from_coverage,
}; };
// Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color: // Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color:
@ -93,7 +98,7 @@ impl TextureAtlas {
(0, 0), (0, 0),
"Expected the first allocation to be at (0, 0), but was at {pos:?}" "Expected the first allocation to be at (0, 0), but was at {pos:?}"
); );
image[pos] = 1.0; image[pos] = Color32::WHITE;
// Allocate a series of anti-aliased discs used to render small filled circles: // Allocate a series of anti-aliased discs used to render small filled circles:
// TODO(emilk): these circles can be packed A LOT better. // TODO(emilk): these circles can be packed A LOT better.
@ -116,7 +121,7 @@ impl TextureAtlas {
let coverage = let coverage =
remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0); 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)] = image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] =
coverage; text_alpha_from_coverage.color_from_coverage(coverage);
} }
} }
atlas.discs.push(PrerasterizedDisc { atlas.discs.push(PrerasterizedDisc {
@ -184,7 +189,7 @@ impl TextureAtlas {
/// The full font atlas image. /// The full font atlas image.
#[inline] #[inline]
pub fn image(&self) -> &FontImage { pub fn image(&self) -> &ColorImage {
&self.image &self.image
} }
@ -200,14 +205,14 @@ impl TextureAtlas {
} else { } else {
let pos = [dirty.min_x, dirty.min_y]; let pos = [dirty.min_x, dirty.min_y];
let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y]; let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y];
let region = self.image.region(pos, size); let region = self.image.region_by_pixels(pos, size);
Some(ImageDelta::partial(pos, region, texture_options)) Some(ImageDelta::partial(pos, region, texture_options))
} }
} }
/// Returns the coordinates of where the rect ended up, /// Returns the coordinates of where the rect ended up,
/// and invalidates the region. /// and invalidates the region.
pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut FontImage) { 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 /// On some low-precision GPUs (my old iPad) characters get muddled up
/// if we don't add some empty pixels between the characters. /// if we don't add some empty pixels between the characters.
/// On modern high-precision GPUs this is not needed. /// On modern high-precision GPUs this is not needed.
@ -254,13 +259,15 @@ impl TextureAtlas {
} }
} }
fn resize_to_min_height(image: &mut FontImage, required_height: usize) -> bool { fn resize_to_min_height(image: &mut ColorImage, required_height: usize) -> bool {
while required_height >= image.height() { while required_height >= image.height() {
image.size[1] *= 2; // double the height image.size[1] *= 2; // double the height
} }
if image.width() * image.height() > image.pixels.len() { if image.width() * image.height() > image.pixels.len() {
image.pixels.resize(image.width() * image.height(), 0.0); image
.pixels
.resize(image.width() * image.height(), Color32::TRANSPARENT);
true true
} else { } else {
false false

View File

@ -271,7 +271,7 @@ pub enum TextureWrapMode {
/// What has been allocated and freed during the last period. /// What has been allocated and freed during the last period.
/// ///
/// These are commands given to the integration painter. /// These are commands given to the integration painter.
#[derive(Clone, Default, PartialEq)] #[derive(Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[must_use = "The painter must take care of this"] #[must_use = "The painter must take care of this"]
pub struct TexturesDelta { pub struct TexturesDelta {