mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
UI: sharper, higher-contrast text, most visible on Windows (#76)
- Glyphs are rendered on whole pixels: egui's sub-pixel binning draws each glyph at quarter-pixel offsets, which it notes makes text blurrier, most visibly at the 100-150 % scaling common on Windows. - Light theme: a mild gamma (0.75) on glyph coverage darkens the antialiased edges of dark text, which egui renders linearly (thin and grey next to the system's text). The dark theme keeps egui's curve. - The faintest text colour met only 3.3:1 (light) and 3.9:1 (dark). It is now #6B6B73 / #97979E, so every text colour meets WCAG AA (4.5:1) on every surface in both themes, enforced by a test. Compared in headless renders at 1x and 2x: crisper stems at 1x, little change at 2x. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
90c56e9ff0
commit
71fcf5be8e
@@ -55,7 +55,7 @@ impl Tokens {
|
||||
divider: Color32::from_rgb(0xE8, 0xE8, 0xEB),
|
||||
text: Color32::from_rgb(0x22, 0x22, 0x26),
|
||||
text_muted: Color32::from_rgb(0x5E, 0x5E, 0x66),
|
||||
text_faint: Color32::from_rgb(0x8E, 0x8E, 0x96),
|
||||
text_faint: Color32::from_rgb(0x6B, 0x6B, 0x73),
|
||||
icon: Color32::from_rgb(0x44, 0x44, 0x4B),
|
||||
hover: Color32::from_rgb(0xF0, 0xF0, 0xF3),
|
||||
pressed: Color32::from_rgb(0xE4, 0xE4, 0xE9),
|
||||
@@ -79,7 +79,7 @@ impl Tokens {
|
||||
divider: Color32::from_rgb(0x33, 0x33, 0x39),
|
||||
text: Color32::from_rgb(0xEC, 0xEC, 0xEF),
|
||||
text_muted: Color32::from_rgb(0xAE, 0xAE, 0xB6),
|
||||
text_faint: Color32::from_rgb(0x80, 0x80, 0x89),
|
||||
text_faint: Color32::from_rgb(0x97, 0x97, 0x9E),
|
||||
icon: Color32::from_rgb(0xD4, 0xD4, 0xDA),
|
||||
hover: Color32::from_rgb(0x34, 0x34, 0x3A),
|
||||
pressed: Color32::from_rgb(0x3E, 0x3E, 0x45),
|
||||
@@ -180,6 +180,15 @@ pub fn apply(ctx: &egui::Context, kind: ThemeKind) {
|
||||
v.widgets.active.weak_bg_fill = t.pressed;
|
||||
v.widgets.active.bg_fill = t.pressed;
|
||||
v.widgets.open.weak_bg_fill = t.hover;
|
||||
// Crisper text at 100–150 % scaling (#76): glyphs sit on whole pixels instead of being
|
||||
// rendered at quarter-pixel offsets, which egui notes blurs them. In the light theme, a mild
|
||||
// gamma darkens the antialiased edges of dark text (egui's default is linear, which reads thin
|
||||
// and grey next to the system's text); the dark theme keeps egui's own curve. At 200 % both
|
||||
// make little difference.
|
||||
v.text_options.subpixel_binning = false;
|
||||
if !t.dark() {
|
||||
v.text_options.color_transfer_function = egui::epaint::FontColorTransferFunction::Gamma(0.75);
|
||||
}
|
||||
ctx.set_visuals(v);
|
||||
ctx.global_style_mut(|s| {
|
||||
s.spacing.item_spacing = egui::vec2(8.0, 6.0);
|
||||
@@ -194,3 +203,35 @@ pub fn apply(ctx: &egui::Context, kind: ThemeKind) {
|
||||
s.interaction.tooltip_delay = 0.35;
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// WCAG 2 contrast ratio between two opaque colours.
|
||||
fn contrast(a: Color32, b: Color32) -> f32 {
|
||||
let lum = |c: Color32| {
|
||||
let lin = |v: u8| {
|
||||
let s = v as f32 / 255.0;
|
||||
if s <= 0.04045 { s / 12.92 } else { ((s + 0.055) / 1.055).powf(2.4) }
|
||||
};
|
||||
0.2126 * lin(c.r()) + 0.7152 * lin(c.g()) + 0.0722 * lin(c.b())
|
||||
};
|
||||
let (x, y) = (lum(a) + 0.05, lum(b) + 0.05);
|
||||
x.max(y) / x.min(y)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_is_readable_on_every_surface() {
|
||||
// #76: the faintest text (hints, zoom level, captions) was 3.3:1 in the light theme.
|
||||
for kind in [ThemeKind::Light, ThemeKind::Dark] {
|
||||
let t = Tokens::for_kind(kind);
|
||||
for (name, fg) in [("text", t.text), ("text_muted", t.text_muted), ("text_faint", t.text_faint)] {
|
||||
for bg in [t.chrome, t.panel, t.card, t.pasteboard] {
|
||||
let r = contrast(fg, bg);
|
||||
assert!(r >= 4.5, "{kind:?} {name} on {bg:?}: {r:.2}:1, WCAG AA needs 4.5:1");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1586,8 +1586,8 @@ milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = ["view.theme"]
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/ui.rs::theme_and_view_options_apply"]
|
||||
notes = ""
|
||||
evidence = ["crates/ui-egui/tests/ui.rs::theme_and_view_options_apply", "crates/ui-egui/src/theme.rs::text_is_readable_on_every_surface"]
|
||||
notes = "every text colour meets WCAG AA (4.5:1) on every surface in both themes; glyphs are pixel-aligned for sharper text at 100–150 % scaling"
|
||||
|
||||
[[feature]]
|
||||
id = "view.theme-system"
|
||||
|
||||
Reference in New Issue
Block a user