Improve text rendering in light mode (#7290)
This changes how we convert glyph coverage to alpha (and ultimately a color), but only in light mode. This is a bit of a hack, because it doesn't fix dark-on-light text in _dark mode_ (if you have any), but for the common case this PR is a huge improvement. You can also tweak this yourself now using `Visuals::text_alpha_from_coverage` or from the UI (bottom of the image):  ## Before / After   ## Black text Before/after If you think the text above looks too weak, it's only because of the default text color. Here's how it looks like with perfectly `#000000` black text:  
This commit is contained in:
parent
8bedaf6e5b
commit
dc79998044
|
|
@ -571,7 +571,11 @@ impl Renderer {
|
||||||
"Mismatch between texture size and texel count"
|
"Mismatch between texture size and texel count"
|
||||||
);
|
);
|
||||||
profiling::scope!("font -> sRGBA");
|
profiling::scope!("font -> sRGBA");
|
||||||
Cow::Owned(image.srgba_pixels(None).collect::<Vec<epaint::Color32>>())
|
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());
|
||||||
|
|
|
||||||
|
|
@ -1876,6 +1876,16 @@ 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,
|
||||||
|
|
@ -2011,10 +2021,19 @@ 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();
|
||||||
self.options_mut(|opt| match theme {
|
let mut recreate_font_atlas = false;
|
||||||
Theme::Dark => opt.dark_style = style,
|
self.options_mut(|opt| {
|
||||||
Theme::Light => opt.light_style = style,
|
let dest = match theme {
|
||||||
|
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.
|
||||||
|
|
@ -2411,7 +2430,28 @@ 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 = self.tex_manager.0.write().take_delta();
|
let textures_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);
|
||||||
|
|
||||||
|
|
@ -3009,9 +3049,17 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -408,11 +408,11 @@ impl Options {
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
theme_preference.radio_buttons(ui);
|
theme_preference.radio_buttons(ui);
|
||||||
|
|
||||||
std::sync::Arc::make_mut(match theme {
|
let style = std::sync::Arc::make_mut(match theme {
|
||||||
Theme::Dark => dark_style,
|
Theme::Dark => dark_style,
|
||||||
Theme::Light => light_style,
|
Theme::Light => light_style,
|
||||||
})
|
});
|
||||||
.ui(ui);
|
style.ui(ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
CollapsingHeader::new("✒ Painting")
|
CollapsingHeader::new("✒ Painting")
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
#![allow(clippy::if_same_then_else)]
|
#![allow(clippy::if_same_then_else)]
|
||||||
|
|
||||||
use emath::Align;
|
use emath::Align;
|
||||||
use epaint::{CornerRadius, Shadow, Stroke, text::FontTweak};
|
use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, text::FontTweak};
|
||||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -921,6 +921,9 @@ pub struct Visuals {
|
||||||
/// this is more to provide a convenient summary of the rest of the settings.
|
/// this is more to provide a convenient summary of the rest of the settings.
|
||||||
pub dark_mode: bool,
|
pub dark_mode: bool,
|
||||||
|
|
||||||
|
/// ADVANCED: Controls how we render text.
|
||||||
|
pub text_alpha_from_coverage: AlphaFromCoverage,
|
||||||
|
|
||||||
/// Override default text color for all text.
|
/// Override default text color for all text.
|
||||||
///
|
///
|
||||||
/// This is great for setting the color of text for any widget.
|
/// This is great for setting the color of text for any widget.
|
||||||
|
|
@ -1374,6 +1377,7 @@ impl Visuals {
|
||||||
pub fn dark() -> Self {
|
pub fn dark() -> Self {
|
||||||
Self {
|
Self {
|
||||||
dark_mode: true,
|
dark_mode: true,
|
||||||
|
text_alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT,
|
||||||
override_text_color: None,
|
override_text_color: None,
|
||||||
weak_text_alpha: 0.6,
|
weak_text_alpha: 0.6,
|
||||||
weak_text_color: None,
|
weak_text_color: None,
|
||||||
|
|
@ -1436,6 +1440,7 @@ impl Visuals {
|
||||||
pub fn light() -> Self {
|
pub fn light() -> Self {
|
||||||
Self {
|
Self {
|
||||||
dark_mode: false,
|
dark_mode: false,
|
||||||
|
text_alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT,
|
||||||
widgets: Widgets::light(),
|
widgets: Widgets::light(),
|
||||||
selection: Selection::light(),
|
selection: Selection::light(),
|
||||||
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
||||||
|
|
@ -2068,6 +2073,7 @@ impl Visuals {
|
||||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||||
let Self {
|
let Self {
|
||||||
dark_mode,
|
dark_mode,
|
||||||
|
text_alpha_from_coverage,
|
||||||
override_text_color: _,
|
override_text_color: _,
|
||||||
weak_text_alpha,
|
weak_text_alpha,
|
||||||
weak_text_color,
|
weak_text_color,
|
||||||
|
|
@ -2216,6 +2222,10 @@ impl Visuals {
|
||||||
"Weak text color",
|
"Weak text color",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
text_alpha_from_coverage_ui(ui, text_alpha_from_coverage);
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.collapsing("Text cursor", |ui| {
|
ui.collapsing("Text cursor", |ui| {
|
||||||
|
|
@ -2326,6 +2336,40 @@ impl Visuals {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn text_alpha_from_coverage_ui(ui: &mut Ui, text_alpha_from_coverage: &mut AlphaFromCoverage) {
|
||||||
|
let mut dark_mode_special =
|
||||||
|
*text_alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Text rendering:");
|
||||||
|
|
||||||
|
ui.checkbox(&mut dark_mode_special, "Dark-mode special");
|
||||||
|
|
||||||
|
if dark_mode_special {
|
||||||
|
*text_alpha_from_coverage = AlphaFromCoverage::TwoCoverageMinusCoverageSq;
|
||||||
|
} else {
|
||||||
|
let mut gamma = match text_alpha_from_coverage {
|
||||||
|
AlphaFromCoverage::Linear => 1.0,
|
||||||
|
AlphaFromCoverage::Gamma(gamma) => *gamma,
|
||||||
|
AlphaFromCoverage::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut gamma)
|
||||||
|
.speed(0.01)
|
||||||
|
.range(0.1..=4.0)
|
||||||
|
.prefix("Gamma: "),
|
||||||
|
);
|
||||||
|
|
||||||
|
if gamma == 1.0 {
|
||||||
|
*text_alpha_from_coverage = AlphaFromCoverage::Linear;
|
||||||
|
} else {
|
||||||
|
*text_alpha_from_coverage = AlphaFromCoverage::Gamma(gamma);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
impl TextCursorStyle {
|
impl TextCursorStyle {
|
||||||
fn ui(&mut self, ui: &mut Ui) {
|
fn ui(&mut self, ui: &mut Ui) {
|
||||||
let Self {
|
let Self {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2198a523fb986e90fa3a42f047499f5b1c791075e7c3822b45509d9880073966
|
oid sha256:34d85b6015112ea2733f7246f8daabfb9d983523e187339e4d26bfc1f3a3bba3
|
||||||
size 60272
|
size 59460
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7bb371a477f58c90ac72aed45a081f3177ea968f090e3739bdb5044ade29f4be
|
oid sha256:4f51d75010cd1213daa6a1282d352655e64b69da7bca478011ea055a2e5349bc
|
||||||
size 144295
|
size 146500
|
||||||
|
|
|
||||||
|
|
@ -544,7 +544,7 @@ impl Painter {
|
||||||
let data: Vec<u8> = {
|
let data: Vec<u8> = {
|
||||||
profiling::scope!("font -> sRGBA");
|
profiling::scope!("font -> sRGBA");
|
||||||
image
|
image
|
||||||
.srgba_pixels(None)
|
.srgba_pixels(Default::default())
|
||||||
.flat_map(|a| a.to_array())
|
.flat_map(|a| a.to_array())
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,59 @@ impl std::fmt::Debug for ColorImage {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// How to convert font coverage values into alpha and color values.
|
||||||
|
//
|
||||||
|
// This whole thing is less than rigorous.
|
||||||
|
// Ideally we should do this in a shader instead, and use different computations
|
||||||
|
// for different text colors.
|
||||||
|
// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub enum AlphaFromCoverage {
|
||||||
|
/// `alpha = coverage`.
|
||||||
|
///
|
||||||
|
/// Looks good for black-on-white text, i.e. light mode.
|
||||||
|
///
|
||||||
|
/// Same as [`Self::Gamma`]`(1.0)`, but more efficient.
|
||||||
|
Linear,
|
||||||
|
|
||||||
|
/// `alpha = coverage^gamma`.
|
||||||
|
Gamma(f32),
|
||||||
|
|
||||||
|
/// `alpha = 2 * coverage - coverage^2`
|
||||||
|
///
|
||||||
|
/// This looks good for white-on-black text, i.e. dark mode.
|
||||||
|
///
|
||||||
|
/// Very similar to a gamma of 0.5, but produces sharper text.
|
||||||
|
/// See <https://www.desmos.com/calculator/w0ndf5blmn> for a comparison to gamma=0.5.
|
||||||
|
#[default]
|
||||||
|
TwoCoverageMinusCoverageSq,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlphaFromCoverage {
|
||||||
|
/// A good-looking default for light mode (black-on-white text).
|
||||||
|
pub const LIGHT_MODE_DEFAULT: Self = Self::Linear;
|
||||||
|
|
||||||
|
/// A good-looking default for dark mode (white-on-black text).
|
||||||
|
pub const DARK_MODE_DEFAULT: Self = Self::TwoCoverageMinusCoverageSq;
|
||||||
|
|
||||||
|
/// Convert coverage to alpha.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn alpha_from_coverage(&self, coverage: f32) -> f32 {
|
||||||
|
match self {
|
||||||
|
Self::Linear => coverage,
|
||||||
|
Self::Gamma(gamma) => coverage.powf(*gamma),
|
||||||
|
Self::TwoCoverageMinusCoverageSq => 2.0 * coverage - coverage * coverage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn color_from_coverage(&self, coverage: f32) -> Color32 {
|
||||||
|
let alpha = self.alpha_from_coverage(coverage);
|
||||||
|
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A single-channel image designed for the font texture.
|
/// A single-channel image designed for the font texture.
|
||||||
///
|
///
|
||||||
/// Each value represents "coverage", i.e. how much a texel is covered by a character.
|
/// Each value represents "coverage", i.e. how much a texel is covered by a character.
|
||||||
|
|
@ -354,30 +407,21 @@ impl FontImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
|
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
|
||||||
///
|
|
||||||
/// `gamma` should normally be set to `None`.
|
|
||||||
///
|
|
||||||
/// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn srgba_pixels(&self, gamma: Option<f32>) -> impl ExactSizeIterator<Item = Color32> + '_ {
|
pub fn srgba_pixels(
|
||||||
// This whole function is less than rigorous.
|
&self,
|
||||||
// Ideally we should do this in a shader instead, and use different computations
|
alpha_from_coverage: AlphaFromCoverage,
|
||||||
// for different text colors.
|
) -> impl ExactSizeIterator<Item = Color32> + '_ {
|
||||||
// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis.
|
self.pixels
|
||||||
self.pixels.iter().map(move |coverage| {
|
.iter()
|
||||||
let alpha = if let Some(gamma) = gamma {
|
.map(move |&coverage| alpha_from_coverage.color_from_coverage(coverage))
|
||||||
coverage.powf(gamma)
|
}
|
||||||
} else {
|
|
||||||
// alpha = coverage * coverage; // recommended by the article for WHITE text (using linear blending)
|
|
||||||
|
|
||||||
// The following is recommended by the article for BLACK text (using linear blending).
|
/// Convert this coverage image to a [`ColorImage`].
|
||||||
// Very similar to a gamma of 0.5, but produces sharper text.
|
pub fn to_color_image(&self, alpha_from_coverage: AlphaFromCoverage) -> ColorImage {
|
||||||
// In practice it works well for all text colors (better than a gamma of 0.5, for instance).
|
profiling::function_scope!();
|
||||||
// See https://www.desmos.com/calculator/w0ndf5blmn for a visual comparison.
|
let pixels = self.srgba_pixels(alpha_from_coverage).collect();
|
||||||
2.0 * coverage - coverage * coverage
|
ColorImage::new(self.size, pixels)
|
||||||
};
|
|
||||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone a sub-region as a new image.
|
/// Clone a sub-region as a new image.
|
||||||
|
|
|
||||||
|
|
@ -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::{ColorImage, FontImage, ImageData, ImageDelta},
|
image::{AlphaFromCoverage, ColorImage, FontImage, ImageData, ImageDelta},
|
||||||
margin::Margin,
|
margin::Margin,
|
||||||
margin_f32::*,
|
margin_f32::*,
|
||||||
mesh::{Mesh, Mesh16, Vertex},
|
mesh::{Mesh, Mesh16, Vertex},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue