feat(font): GPU sanitized font-database assembly (Q#F6)

assemble() replaces FontSystem::new() + post-hoc load_font_data
with an explicit fontdb::Database built in today's order -- system
fonts first, the bundled JetBrains Mono second (its fontdb::ID
retained) -- then the parameterized same-family collision filter
(sanitize_font_database removes every NON-monospace face
advertising the default family; the bundled face survives by
construction), cosmic-text's generic-family defaults, and only
then FontSystem::new_with_locale_and_db (sys-locale with the
"en-US" fallback, the same resolution cosmic-text's own
constructor performs; new pmacs-gpu dependency) -- so the internal
monospace-ID set is computed over the final database, bundle
included. FontDefaults { default_family, bundled_id } lands on
State as the total-fallback anchor for the resolution work in the
next commit; query_normal_face is the shared normal-style query
(the same fontdb::Query the base Attrs imply). Debug assertions
pin both anchors present and monospaced at assembly. All 69 GPU
tests pass unchanged -- assembly preserves today's pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
This commit is contained in:
Levi Neuwirth 2026-07-15 14:39:04 +01:00
parent 661b4968d9
commit e9bafdacd6
3 changed files with 117 additions and 3 deletions

1
Cargo.lock generated
View File

@ -2579,6 +2579,7 @@ dependencies = [
"loro", "loro",
"pmacs-protocol", "pmacs-protocol",
"pollster", "pollster",
"sys-locale",
"wgpu", "wgpu",
"winit", "winit",
] ]

View File

@ -50,6 +50,10 @@ env_logger = "0.11.10"
# ends up with two cosmic-text versions resolving to the same name. # ends up with two cosmic-text versions resolving to the same name.
glyphon = "0.11.0" glyphon = "0.11.0"
loro = "=1.12.0" loro = "=1.12.0"
# Locale for cosmic-text FontSystem construction (Q#F6 sanitized db
# assembly) -- the same crate cosmic-text uses internally, so the
# resolved locale matches what FontSystem::new() would have picked.
sys-locale = "0.3"
# Session 1's wire-types crate. Pulled in now so the dep graph is # Session 1's wire-types crate. Pulled in now so the dep graph is
# settled from session 2 forward; protocol consumption itself lands # settled from session 2 forward; protocol consumption itself lands
# in session 3. # in session 3.

View File

@ -30,7 +30,7 @@ use std::sync::{Arc, Mutex};
use glyphon::{ use glyphon::{
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, fontdb,
}; };
use loro::{ContainerTrait, ExportMode}; use loro::{ContainerTrait, ExportMode};
use pmacs_protocol::{ use pmacs_protocol::{
@ -53,6 +53,106 @@ use crate::attach::{AttachClient, AttachEvent};
/// Bundled font (SIL Open Font License 1.1 — see `fonts/OFL.txt`). /// Bundled font (SIL Open Font License 1.1 — see `fonts/OFL.txt`).
const JETBRAINS_MONO: &[u8] = include_bytes!("../fonts/JetBrainsMono-Regular.ttf"); const JETBRAINS_MONO: &[u8] = include_bytes!("../fonts/JetBrainsMono-Regular.ttf");
/// The default font family — the query `family: None` (and every
/// rejected requested family) resolves through (framing Q#F6). The
/// bundle guarantees the query is never empty; a monospaced
/// system-installed face of the same family may legitimately win by
/// insertion order (availability, not face identity).
const DEFAULT_FONT_FAMILY: &str = "JetBrains Mono";
/// What sanitized assembly retained (framing Q#F6): the default
/// family name and the bundled face's ID, both asserted present and
/// monospaced at assembly time. The rejected-family fallback and the
/// `family: None` default both resolve through
/// `Family::Name(&default_family)` against the sanitized db, so the
/// fallback is total and cannot recurse into a proportional
/// collision.
struct FontDefaults {
default_family: String,
bundled_id: fontdb::ID,
}
/// Remove every NON-monospace face that advertises `default_family`
/// (framing Q#F6, round 3 finding 4): fontdb returns the first
/// surviving equally-good candidate in insertion order, so a
/// closer-weight proportional system face could otherwise win
/// bold/italic queries even when the normal query selected a valid
/// monospaced face. Parameterized by family + bundled ID so tests
/// run the PRODUCTION path with an unreserved fixture family. The
/// bundled face is monospaced and survives by construction.
fn sanitize_font_database(db: &mut fontdb::Database, default_family: &str, bundled_id: fontdb::ID) {
let doomed: Vec<fontdb::ID> = db
.faces()
.filter(|f| {
!f.monospaced
&& f.id != bundled_id
&& f.families.iter().any(|(name, _)| name == default_family)
})
.map(|f| f.id)
.collect();
for id in doomed {
db.remove_face(id);
}
}
/// Sanitized, current-order font database + `FontSystem` (framing
/// Q#F6): system fonts FIRST (what `FontSystem::new()` does today),
/// the bundled bytes second — retaining the bundled `fontdb::ID` —
/// then the collision filter, then cosmic-text's current
/// generic-family defaults, and only then the `FontSystem`
/// construction, so its internal monospace-ID set includes every
/// surviving monospaced face (the bundle included). The locale is
/// resolved exactly as cosmic-text does (`sys_locale`, `"en-US"`
/// fallback).
fn build_font_system() -> (FontSystem, FontDefaults) {
let mut db = fontdb::Database::new();
db.load_system_fonts();
let bundled_ids =
db.load_font_source(fontdb::Source::Binary(std::sync::Arc::new(JETBRAINS_MONO)));
let bundled_id = *bundled_ids
.first()
.expect("bundled JetBrains Mono contains one face");
sanitize_font_database(&mut db, DEFAULT_FONT_FAMILY, bundled_id);
db.set_monospace_family("Noto Sans Mono");
db.set_sans_serif_family("Open Sans");
db.set_serif_family("DejaVu Serif");
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
let font_system = FontSystem::new_with_locale_and_db(locale, db);
let defaults = FontDefaults {
default_family: DEFAULT_FONT_FAMILY.to_owned(),
bundled_id,
};
// Assembly-time assertions (framing Q#F6): the default query and
// the bundled face are present and monospaced — the total
// fallback depends on both.
debug_assert!(
font_system
.db()
.face(defaults.bundled_id)
.is_some_and(|f| f.monospaced),
"the bundled face must survive sanitization and be monospaced"
);
debug_assert!(
query_normal_face(font_system.db(), &defaults.default_family)
.and_then(|id| font_system.db().face(id))
.is_some_and(|f| f.monospaced),
"the default family query must resolve to a monospaced face"
);
(font_system, defaults)
}
/// The normal-style query for `family` — the same `fontdb::Query`
/// implied by the base `Attrs` installed on all seven buffers
/// (normal weight, normal style, normal stretch).
fn query_normal_face(db: &fontdb::Database, family: &str) -> Option<fontdb::ID> {
db.query(&fontdb::Query {
families: &[fontdb::Family::Name(family)],
weight: fontdb::Weight::NORMAL,
stretch: fontdb::Stretch::Normal,
style: fontdb::Style::Normal,
})
}
/// Initial window size in logical pixels. /// Initial window size in logical pixels.
const INITIAL_WIDTH: u32 = 800; const INITIAL_WIDTH: u32 = 800;
const INITIAL_HEIGHT: u32 = 200; const INITIAL_HEIGHT: u32 = 200;
@ -385,6 +485,11 @@ struct State {
surface: Option<wgpu::Surface<'static>>, surface: Option<wgpu::Surface<'static>>,
config: wgpu::SurfaceConfiguration, config: wgpu::SurfaceConfiguration,
font_system: FontSystem, font_system: FontSystem,
/// What sanitized assembly retained (Q#F6): the default family
/// and the bundled face ID — the total-fallback anchors for
/// every font resolution.
#[allow(dead_code)] // consumed by apply_font_facts (next commit)
font_defaults: FontDefaults,
swash_cache: SwashCache, swash_cache: SwashCache,
viewport: Viewport, viewport: Viewport,
atlas: TextAtlas, atlas: TextAtlas,
@ -1842,8 +1947,11 @@ impl State {
// Pipelines, atlas, and any offscreen texture must all share the // Pipelines, atlas, and any offscreen texture must all share the
// render-target format; `config.format` is the single source. // render-target format; `config.format` is the single source.
let format = config.format; let format = config.format;
let mut font_system = FontSystem::new(); // Q#F6: sanitized current-order assembly — system fonts, the
font_system.db_mut().load_font_data(JETBRAINS_MONO.to_vec()); // bundled face (ID retained), the same-family collision
// filter, generic defaults, THEN FontSystem construction so
// its monospace-ID set sees the final database.
let (mut font_system, font_defaults) = build_font_system();
let swash_cache = SwashCache::new(); let swash_cache = SwashCache::new();
let cache = Cache::new(&device); let cache = Cache::new(&device);
let mut viewport = Viewport::new(&device, &cache); let mut viewport = Viewport::new(&device, &cache);
@ -1957,6 +2065,7 @@ impl State {
surface, surface,
config, config,
font_system, font_system,
font_defaults,
swash_cache, swash_cache,
viewport, viewport,
atlas, atlas,