feat: configurable per-slot font weight for bundled Cascadia Code#1628
feat: configurable per-slot font weight for bundled Cascadia Code#1628nikicat wants to merge 5 commits into
Conversation
Adds an optional `weight = <number>` field to each per-slot section
(`fonts.regular`, `fonts.bold`, `fonts.italic`, `fonts.bold-italic`).
When the slot is served by the bundled Cascadia Code variable font, the
value is baked into the `wght` axis at shape and rasterize time — so a
user who wants a lighter regular face can write `weight = 350` instead
of installing a system "Cascadia Code Light".
Implementation notes:
- `from_static_slice_with_wght` now takes the `wght` axis value and the
logical CSS-style weight as separate parameters, decoupling the
rendered axis from the cascade-match weight. `is_bold()` and the
bold-spec lookup walk see `Weight::BOLD` for the bold slots even when
the user picks a lighter axis value (e.g. 500). The two were
conflated in a single argument before, which would have demoted
`is_bold()` to false and broken bold-fallback routing for any
sub-700 override.
- `is_bold()` and the synthesis-weight check in `from_data` now read
`swash::Weight::BOLD` instead of the bare `Weight(700)`. The 700
threshold lives in one place.
- The terminal grid path (`grid_emit::shape_run_swash` and
`rasterize_glyph_native`) was not applying any `wght` variation; it
built swash shape/scale contexts straight from the font bytes. A new
per-`GridGlyphRasterizer` cache (`wght_variation_cache`) mirrors the
one in `sugarloaf::text` and feeds the variation into both
`shaper.variations` and `scaler.variations`.
- Config live-reload now actually propagates a `weight` change:
- `Screen::update_config` drops `self.grid_rasterizer` (its
per-font_id caches still pointed at the previous library's faces)
and clears `self.grids` so each panel's `GridRenderer` rebuilds
with an empty glyph atlas — otherwise atlas hits would keep
serving the old outlines.
- Each panel's `pending_update` gets `TerminalDamage::Full` so the
renderer's per-panel dirty gate lets the frame through.
- The `UpdateConfig` event handler in `application.rs` now calls
`route.request_redraw()`; previously the handler mutated state but
never scheduled a frame, so the next paint waited for an external
event (focus, input, blink).
macOS rasterize gets the variation for free via the CTFont handle that
`from_static_slice_with_wght` builds with `with_wght_variation`.
Tests:
- `sugarloaf::font::fonts::toml_tests::weight_field_parses_from_toml`
pins the TOML→`SugarloafFont` deserialization.
- `sugarloaf::font::alias_tests::load_applies_per_slot_weight_overrides`
walks the full `SugarloafFonts → FontLibrary::load` path and asserts
each slot's resulting `wght_variation`.
- `sugarloaf::font::alias_tests::fallback_user_weight_decouples_axis_from_logical_weight`
pins the decoupling: a user-set bold weight of 500 still satisfies
`is_bold()` while rendering at the requested axis value.
- The existing `fallback_bold_slot_reports_is_bold` was updated for
the new `load_fallback_from_memory(slot, weight_override)` signature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7d4678e)
`weight` actually deserializes now (previous commit), so the per-slot signatures should reflect that and the documented bold defaults need to match `WGHT_BOLD` (700, not 800). Also drop the phantom `width` column — `SugarloafFont` has no such field; a user setting `width = "..."` would either be silently dropped or error depending on serde mode. Adds a one-line note on each slot explaining that `weight` only takes effect for the bundled Cascadia Code (the axis is what's variable), and pointing system-font users at `style = "Light"` etc. Other pre-existing drift in this section (size = 18.0 vs actual 14.0, style = "Normal" vs actual "default", undocumented absence of `extras` / `emoji`) is left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> (cherry picked from commit 51774ef)
Bold and bold-italic glyphs could render at the variable font's default 400 weight instead of 700, with *which* glyphs affected varying per window. The bundled Cascadia variable font carries its slot weight in a `wght` axis applied at rasterize time, but `rasterize_glyph_native` read that value from `wght_variation_cache` — a per-font_id side cache that only `shape_run_swash` populates. Shaping is skipped on a shaped-run cache hit (`run_cache_get(..).is_some()`), so a glyph could reach rasterization with no cache entry; the old code then `flatten()`ed the miss to `None` and rasterized at the default instance. Because the glyph atlas key carries no weight, that lighter outline was then cached permanently. Whichever glyphs happened to be shaped-this-frame vs served from the run cache decided the boldness, hence the per-window variance. Resolve `wght` from the FontLibrary on a cache miss (the same `or_insert_with` lookup `shape_run_swash` uses) so the rasterized weight is a function of font identity, not frame/shape ordering. `font_library` is threaded through `ensure_glyph_by_id`; the macOS path takes it as `_font_library` since it bakes the axis into the CTFont handle at load. Adds `bold_slot_rasterizes_bold_without_prior_shaping`, which rasterizes a bold-slot glyph with and without a pre-populated side cache and asserts the outlines match; it fails on the old code (ink 35790 vs 46114). Regression from 7d4678e ("feat: configurable per-slot font weight"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 4734999)
|
Pushed a follow-up commit fixing a regression in this same feature, found after opening the PR: fix(font): resolve wght axis at rasterize time, not via shape side-cache Bold / bold-italic glyphs could render at the variable font's default 400 weight instead of 700, with which glyphs were affected varying per window. |
Formatting-only; no behavior change. (CI `cargo fmt --check` gate.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
swash's ScaleContext/ShapeContext keep a single coordinate buffer that every builder(..).variations(..) reuses. variations() only resize()s and overwrites the axes it is handed -- it never clears stale entries (unlike normalized_coords(), which does). So a slot with no variation (wght None -- italic or regular-at-default) passed an empty variation set, the resize was a no-op, and the buffer kept the *previous* build's wght. Because the grid renderer shares one scale_ctx across every font_id, an unweighted italic glyph rasterized right after a bold (wght 700) glyph inherited the stale coordinate and rendered bold -- the rare "false bold" artifact. Rare because it needs a weighted build to immediately precede an unweighted one; far more frequent under dim text, which fragments runs and interleaves more weights through the shared context (~30-50% there). Follow-up to 4734999: that fix made rasterize resolve wght deterministically per font_id, but the None path still walked into this swash footgun. Force a clean slate with normalized_coords(empty) before variations() at both the rasterize and shape sites. Regression test unweighted_slot_unaffected_by_prior_bold_rasterize drives a bold then an unweighted glyph through one rasterizer and pins that the unweighted glyph matches its no-prior-bold baseline (ink 35790, not bold's 46114). Fails before the fix, passes after. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Pushed a follow-up fix ( The earlier It's rare because it needs a weighted build to immediately precede an unweighted one — and much more frequent under dim text (~30–50%), which fragments runs and interleaves more weights through the shared context. Fix: |
Format the wght-rasterize regression tests to match rustfmt, aligning nb/fixes with the same change on the feat/per-slot-font-weight PR branch (raphamorim#1628). No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Adds an optional
weight = <number>field to each per-slot font section(
fonts.regular,fonts.bold,fonts.italic,fonts.bold-italic). When theslot is served by the bundled Cascadia Code variable font, the value is baked
into the
wghtaxis at shape and rasterize time — so a user who wants a lighterregular face can write
weight = 350instead of installing a system "CascadiaCode Light". Refs #1577.
Details
from_static_slice_with_wghttakes thewghtaxis value and the logicalCSS-style weight as separate parameters, decoupling the rendered axis from the
cascade-match weight — so
is_bold()and bold-fallback routing still seeWeight::BOLDfor bold slots even when the user picks a lighter axis (e.g. 500).grid_emit::shape_run_swash/rasterize_glyph_native)now applies the
wghtvariation via a per-rasterizer cache mirroringsugarloaf::text; macOS gets it through theCTFonthandle.weightchange: the grid rasterizer and per-panelglyph atlases are rebuilt and a redraw is scheduled (the
UpdateConfighandlerpreviously mutated state without requesting a frame).
swash::Weight::BOLD.weightdeserializes, bolddefaults corrected to
WGHT_BOLD(700), and the phantomwidthcolumn dropped.Testing
cargo test -p sugarloaf— TOML deserialization, fullFontLibrary::loadper-slot
wght_variation, and axis/logical-weight decoupling.Refs #1577
🤖 Generated with Claude Code