Glyphs on demand: FontFace replaces fontdue, DejaVu ahead of the CJK collection in the fallback chain; App::take_clipboard_request and App::on_pointer_secondary
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s

Every face fontdue opened was parsed whole: `Font::from_bytes` walks the `cmap` and builds the outline of every glyph before returning. For the theme's Sora that is a few hundred glyphs and nobody notices. For a fallback it is the whole file: the first string with one arrow or one bullet resolved through the chain to `NotoSansCJK-Regular.ttc`, and a compositor or a notification daemon that had drawn a dozen labels was suddenly holding 330 MB of outlines for 44 810 glyphs, after 1.7 s of CPU, to paint the hundred or so glyphs a screen ever shows. Measured on forge: 240 MB before the first app-switcher label, 542 MB after it, and never given back, because the table is live, not garbage.
`FontFace`. The new `src/font_face.rs` is the face type the whole crate now rasterises with, built on the `ttf-parser` that rustybuzz already brings in and on tiny-skia, which already fills paths. `from_bytes( data, index )` keeps the bytes and reads only what a face needs up front: units per em and the vertical line metrics. A `ttf_parser::Face` is rebuilt on every use rather than kept, since parsing one is table-offset arithmetic, in the noise next to the outline work it guards. Each glyph's outline is extracted the first time it is asked for, through `Outliner`, into a `tiny_skia::Path`, and cached for the process lifetime in a `HashMap<u16, Arc<Outline>>`; codepoint lookups are cached the same way. Rasterising fills that path into a `Mask` and hands back the 8-bit coverage, top row first, which is what both canvases stored before. The metrics keep fontdue's conventions, so no widget changed: sizes scale by `px / units_per_em`, `Metrics::xmin` / `ymin` are whole-pixel placements with the fractional remainder folded into the raster offset, `bounds` is the outline box in subpixels, `LineMetrics` carries ascent, descent, line gap and their sum. One thing is deliberately not what `glyf` declares: the box comes from the points the outline actually reaches, quadratic and cubic extrema included, because declared boxes may be loose and a box wider than the ink shifts the glyph on the pixel grid. Tests cover the turning-point arithmetic and the line-metric scaling. `FontFace`, `Metrics`, `LineMetrics` and `OutlineBounds` are exported, since `Canvas::font` and `font_for` hand them out.
What changed around it. `fontdue` leaves `Cargo.toml`. The two canvases, their text and setup modules, the registry, the rich text and text widgets swap `Arc<Font>` for `Arc<FontFace>` and nothing else; the glyph caches still key on `(glyph_id, size_bits, font_id)`. `FontRegistry` now reads a source into an `Arc<Vec<u8>>` and gives the face the bytes it will pull outlines from, so the registry no longer owns a second copy; its parse error names the file rather than a library message. `FontHandle` keeps `font` and `bytes` side by side, now naming the same buffer, so shaping needs no second read and the comment that explained the duplication is gone. `system_fonts` builds fallback faces from the bytes each slot already holds, and the slot stays two-staged: the bytes are read once to probe coverage with `ttf-parser`, the face is built only when a glyph in that slot is needed, and a face that is built no longer costs anything until it draws.
The chain. DejaVu Sans moves from last resort to just before the CJK collection. It owns the geometric shapes, arrows and box-drawing blocks, which are what a user interface most often needs outside its own family; behind the collection, every one of those pulled the 19 MB file in, since probing a slot for coverage reads it whole. The CJK entries stay last for the same reason.
The two hooks are for a host that embeds another UI, trust in this case, and complete the ones `App` already had. `take_clipboard_request` is the counterpart of nothing: an app could paste from the system clipboard through its text fields but had no way to put text into it. Return `Some( text )` once and the run loop stores it and publishes it as the seat's selection through the data device, the same path `handle_copy` takes for Ctrl+C in a text field, so other applications see it. `on_pointer_secondary` reports a right-button press that no widget of ltk's own answered, in physical pixels like `on_pointer_button`: `on_pointer_right_click` already dispatched a widget's `on_long_press` and the built-in text menu, and in the remaining case only dismissed an open menu; it now also tells the app, which can forward the press to an embedded view that has a context menu of its own.
Not changed. Variable fonts are read at their default instance, as before. Colour glyphs, `COLR` or bitmap, are not rendered by either rasteriser; the outline path draws what it can. The glyph caches are still unbounded per face, but they now hold what was drawn, not what the file contains.
This commit is contained in:
2026-09-25 07:43:47 +02:00
parent 494fb26f9e
commit b591950f67
20 changed files with 638 additions and 115 deletions
+12 -12
View File
@@ -4,7 +4,7 @@
//! Rendering surface used by every widget.
//!
//! [`Canvas`] is a thin enum wrapper over the per-frame rendering
//! backend. The CPU backend is [`SoftwareCanvas`] (tiny-skia + fontdue
//! backend. The CPU backend is [`SoftwareCanvas`] (tiny-skia, glyphs
//! rasterised into a `Pixmap`). The GPU backend is
//! [`crate::gles_render::GlesCanvas`] (EGL + GLES2/3).
//!
@@ -34,7 +34,7 @@
use std::cell::Cell;
use std::sync::Arc;
use fontdue::{ Font, LineMetrics, Metrics };
use crate::font_face::{ FontFace, LineMetrics, Metrics };
use tiny_skia::{ Mask, Pixmap };
use crate::gles_render::{ BorrowedGlesTexture, GlesCanvas, GlesVersion };
@@ -81,7 +81,7 @@ pub fn is_software_render() -> bool
/// Cache key for a rasterized glyph. `size_bits` is the f32 bit
/// pattern of `size * dpi_scale`; `font_id` is the address of the
/// `Arc<Font>` used for the rasterisation, so distinct weights /
/// `Arc<FontFace>` used for the rasterisation, so distinct weights /
/// families of the same `(glyph_id, size)` do not collide on the
/// cache. `glyph_id` is the per-font glyph index returned by
/// HarfBuzz shaping, so cached entries persist across script
@@ -107,7 +107,7 @@ pub ( super ) struct GlyphEntry
// ─── SoftwareCanvas ──────────────────────────────────────────────────────────
/// Software rendering backend backed by a tiny-skia [`Pixmap`] and a
/// fontdue [`Font`].
/// [`FontFace`].
///
/// Wrapped by [`Canvas`] so the GPU backend can be slotted in by the
/// runtime without changing widget code. Widgets themselves never see
@@ -122,10 +122,10 @@ pub struct SoftwareCanvas
/// specific family through [`SoftwareCanvas::font_for`] keep
/// working. Populated from
/// `crate::system_fonts::default_handle` at construction time.
pub font: Arc<Font>,
pub font: Arc<FontFace>,
/// Raw bytes of the default font. Kept alongside `font` so the
/// HarfBuzz shaper (rustybuzz) can be invoked without re-reading
/// the file — fontdue does not expose its internal byte buffer.
/// the file; the face names the same buffer.
pub font_bytes: Arc<Vec<u8>>,
/// TTC sub-face index for the default font (0 for single-face
/// files; collection index for `.ttc` archives).
@@ -176,7 +176,7 @@ pub struct SoftwareCanvas
/// inlinable in the hot path.
pub enum Canvas
{
/// CPU rasterisation via tiny-skia + fontdue, written to a
/// CPU rasterisation via tiny-skia, written to a
/// `wl_shm` buffer.
Software( SoftwareCanvas ),
/// GPU rasterisation via EGL + GLES 2/3. Presents via
@@ -495,10 +495,10 @@ impl Canvas
}
}
/// Shared font handle. Exposed so widgets that need raw `fontdue`
/// Shared font handle. Exposed so widgets that need raw face
/// access (e.g. `Text` for ascent/descent) do not have to go
/// through wrappers for every metric they read.
pub fn font( &self ) -> &Font
pub fn font( &self ) -> &FontFace
{
match self
{
@@ -523,7 +523,7 @@ impl Canvas
/// Resolve a specific font via the theme registry, falling back
/// to the system-default [`Self::font`] when no registry is
/// installed or the triple cannot be satisfied.
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<Font>
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<FontFace>
{
match self
{
@@ -911,7 +911,7 @@ impl Canvas
/// Draw `text` with an explicitly supplied font instead of the
/// canvas default. Use [`Self::font_for`] to resolve a `(family,
/// weight, style)` triple from the active theme registry first.
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<Font> )
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<FontFace> )
{
match self
{
@@ -932,7 +932,7 @@ impl Canvas
/// Width of `text` rendered with `font`. Mirrors
/// [`Self::measure_text`] but bypasses the canvas default font so
/// text laid out at one weight and drawn at another stays aligned.
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<FontFace> ) -> f32
{
match self
{