rust_widgets 2.8.4

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Real shaping: what a script's *rules* do to a string, on top of what its face *has*.
//!
//! # The two axes, again
//!
//! [BLUE23 §0A.4](docs/plans/archive/blue23.md) keeps two questions apart, and this module is the
//! second one:
//!
//! * *Does a face have this character?* — [`crate::render::text::GlyphSource`], answered by data;
//! * *What glyphs does the script say this **string** becomes?* — this module, answered by the
//!   face's layout tables (`GSUB`, `GPOS`) and the script's rules.
//!
//! A CJK face needs the first and not the second: one character is one cell. An Arabic face
//! needs the second and cannot fake it — `بيت` is three characters and, drawn as three isolated
//! letters, is not a word in any state a reader would call *close*. That asymmetry is why
//! BLUE23 calls the two orthogonal rather than one "font support" axis.
//!
//! # Why the engine is opt-in
//!
//! Shaping is only meaningful with a face, so it is compiled in only when one is
//! ([`crate::render::text::shaping::active_faces`] is empty otherwise) and the model in
//! [`crate::render::text::line`] stays the whole story for a default build. The default build's
//! bytes do not change.
//!
//! # Why the engine is built per call rather than stored
//!
//! A shaper face carries a mutable layout-plan cache, which is why it is not `Sync` — and
//! [`crate::render::TextShaper`] is required to be `Send + Sync` because a renderer is. Rather
//! than hide that behind a lock (and pay for the lock on every measurement), the face is parsed
//! from its bytes on each call: parsing a table directory is a few dozen reads, this is an
//! opt-in path, and the alternative is shared mutable state in the one place the three
//! constraints say there must be none.

use crate::compat::Vec;
use crate::core::Font;
use crate::render::text::font_assets::{active_faces, FaceBytes};
use crate::render::{ShapedGlyphRun, TextShaper};

/// The face whose family name matches `font.family()`, if any.
///
/// # Why the face is named rather than guessed
///
/// [`crate::core::Font`] already carries the answer to "which face?" — its `family` — and this
/// layer honours it. The alternative, picking the first face that happens to cover the text,
/// would change every label's **advance** the moment a vector face was enabled, for every
/// caller, including those drawing Latin text that never needed a second face. Metrics are
/// layout: a feature that silently re-laid-out every control would be a surprise, not an
/// upgrade. Naming the face makes the choice the caller's, which is also what makes "enable the
/// feature and nothing changes until you ask for the family" a stated behaviour instead of a
/// lucky one.
///
/// The match is case-insensitive because a font family name is an identifier a person types.
///
/// # Host-registered faces
///
/// With `runtime-fonts` on, a host may have registered a face under the family name it named — and
/// that is the whole point of registering one. A family lookup that only saw the compiled list would
/// make the host's own font unreachable by name, which is the one way a caller *does* name it. So
/// the registered faces are searched first, then the compiled ones.
pub(crate) fn face_for_family(family: &str) -> Option<FaceBytes> {
    #[cfg(feature = "runtime-fonts")]
    {
        let (runtime, len) = crate::render::text::runtime_fonts::active_runtime_faces();
        for face in &runtime[..len] {
            if face.name.eq_ignore_ascii_case(family) {
                return Some(*face);
            }
        }
    }
    active_faces().iter().copied().find(|face| face.name.eq_ignore_ascii_case(family))
}

/// The face that will **paint** `text`, which is the face whose metrics must size it.
///
/// # The defect this corrects, in measured numbers
///
/// Measurement used [`face_for_family`], which only succeeds when the caller names a face this build
/// ships. Every theme in this crate names `"Arial"`, this crate ships `"Open Sans"`, so the lookup
/// answered `None` and every Latin cluster fell back to the **flat estimate** in
/// [`crate::render::text::estimate_cluster_advance`] — `0.6 em` for *every* non-wide cluster, the same
/// width for `i` as for `w`. Measured against Open Sans' own `hmtx` at 14 px:
///
/// ```text
///   ch   real(px)  estimate   drift        'widget' pen positions
///    w     10.25      8.40    +1.85        0.00 / 0.00
///    i      3.53      8.40    -4.87       10.25 / 8.40   <- next glyph starts 1.85 px early
///    d      8.57      8.40    +0.17       13.79 /16.80   <- and the one after, 3.01 px late
///    g      7.72      8.40    -0.68       22.35 /25.20
///    e      7.86      8.40    -0.54       30.08 /33.60
///    t      5.47      8.40    -2.93       37.94 /42.00
/// ```
///
/// So `w` and `i` overlapped by 1.85 px while the gap after `i` was 3.01 px too wide — the cramping
/// and the stretched gaps in one word, which is what a flat factor applied to a proportional face
/// has to produce. (The totals nearly agree by luck: 43.4 px of real ink against 50.4 px reserved,
/// because over- and under-estimates cancel. Per-glyph they do not, and per-glyph is what a reader
/// sees.)
///
/// # Why the fallback is by *coverage*, and why that is not "silently re-laying-out"
///
/// The concern [`face_for_family`] documents is real: a face's metrics must not change for a caller
/// that did not ask. But the relevant question is not "did the caller name a face" — it is "which
/// face will the ink come from". [`VectorSource`](crate::render::text::VectorSource) selects by
/// coverage and ignores the family entirely, so on this build `"Arial"` text is **painted by Open
/// Sans** whatever the metrics say. Sizing it by the 0.6 em estimate is therefore not conservatism,
/// it is measuring one font and drawing another — the "measure with A, draw with B" defect this crate
/// has already paid for once.
///
/// Honouring the family first keeps the documented behaviour exactly: a caller that names a shipped
/// face gets that face, byte for byte as before. Coverage is only the fallback for a family this build
/// does not ship — which, for every theme in this crate today, is the case that is *currently* wrong.
///
/// # What this does not change
///
/// A build with **no** vector face still resolves `None` here, so the estimate remains the whole
/// answer and the default build's output is unchanged. The fallback needs a face to fall back to.
fn face_for_metrics(text: &str, font: &Font) -> Option<FaceBytes> {
    // Named first, so the documented contract is untouched.
    if let Some(named) = face_for_family(font.family()) {
        return Some(named);
    }
    // Otherwise the face the ink will actually come from. The probe character is the first strong
    // non-whitespace one, which is the same rule `face_for_text` uses — a run is shaped by the face
    // that covers what it says.
    let probe = text.chars().find(|ch| !ch.is_whitespace())?;
    // Coverage, through the shared lookup: `face_for_char` is the one place that knows a host face
    // outranks a compiled one, so measurement and drawing cannot disagree about which face wins.
    crate::render::text::font_assets::face_for_char(probe)
}

/// The first face that covers `text`'s first strong character.
///
/// "First strong character" is the rule the bidirectional algorithm uses to pick a paragraph's
/// base direction (UAX #9 P2). It is the right rule for a caller that has said *nothing* about
/// which face it wants — the case [`RustybuzzShaper::from_active_faces`] exists for — and it is
/// deliberately not the rule the renderer's measurement path uses: see [`face_for_family`].
fn face_for_text(text: &str) -> Option<FaceBytes> {
    let probe = text.chars().find(|ch| !ch.is_whitespace())?;
    crate::render::text::font_assets::face_for_char(probe)
}

/// A parsed view of `face`'s bytes, or `None` if they are not a font this crate can read.
///
/// Kept private: parsing is the one operation that can fail, and every caller wants a different
/// thing from the result. Exposing the parser would leak the font crate into this module's API.
fn face_parser(face: &FaceBytes) -> Option<ttf_parser::Face<'static>> {
    ttf_parser::Face::parse(face.bytes, 0).ok()
}

/// The advance of each cluster, summed over the **glyphs** that cluster shaped into.
///
/// `ranges` is the caller's clusters as `(byte_start, byte_end)` pairs in logical order — the
/// same list [`crate::render::text::line`] built for the bidirectional reorder. The result is
/// indexed the same way, so the caller can read it straight into its own clusters.
///
/// # Why a sum, and why by byte range
///
/// Shaping is not one-glyph-per-cluster. A ligature turns two characters into one glyph; Arabic
/// joining substitutes a contextual form for a letter; a cluster can also decompose into several
/// glyphs. What the renderer needs is the width each *cluster* occupies, so every glyph's
/// advance is credited to the cluster its reported byte index falls inside — by range, not by
/// counting, because counting would mis-attribute a cluster that spans more than one character.
pub(crate) fn cluster_advances(
    text: &str,
    ranges: &[(usize, usize)],
    font: &Font,
    scale: f32,
) -> Option<Vec<f32>> {
    let face = face_for_metrics(text, font)?;
    // The cached shaper parse: this runs for every measured and every drawn label, and
    // `text_line` measures before `draw_text` shapes, so a single text control parsed the font
    // twice a frame. Measured, one `measure_text("M")` cost ~18us, which multiplied out to the
    // 25ms a frame of `demo/control` spent rendering 36 controls.
    super::font_assets::with_shaper_face(face, |parsed| {
        cluster_advances_in_face(text, ranges, font, scale, parsed)
    })
    .flatten()
}

/// Computes the per-cluster advances using an already-parsed shaper `face`.
fn cluster_advances_in_face(
    text: &str,
    ranges: &[(usize, usize)],
    font: &Font,
    scale: f32,
    parsed: &rustybuzz::Face<'_>,
) -> Option<Vec<f32>> {
    let units_per_em = parsed.units_per_em() as f32;
    if units_per_em <= 0.0 {
        return None;
    }
    let mut buffer = rustybuzz::UnicodeBuffer::new();
    buffer.push_str(text);
    // The direction is guessed from the text, for the same reason the face is named by its
    // family: a label's base direction is a property of its content, and the reorder itself is
    // `bidi`'s job and happens later — shaping works in logical order.
    buffer.guess_segment_properties();
    let output = rustybuzz::shape(parsed, &[], buffer);

    // The pixel size of one font unit. An `em` is the font size, so a unit is
    // `font_size / units_per_em` logical pixels — the real advance, rather than a per-cluster
    // guess. `scale` converts to device pixels, as everywhere else in the text layer.
    let unit_px = font.size() * scale / units_per_em;

    let mut advances = vec![0.0f32; ranges.len()];
    for (info, position) in output.glyph_infos().iter().zip(output.glyph_positions()) {
        let byte = info.cluster as usize;
        // `ranges` is sorted and disjoint, so the cluster containing `byte` is a partition point.
        let index = ranges.partition_point(|&(start, _)| start <= byte);
        if let Some(slot) = advances.get_mut(index.saturating_sub(1)) {
            *slot += position.x_advance as f32 * unit_px;
        }
    }
    Some(advances)
}

/// A shaper backed by a real font's layout tables.
///
/// # What it adds over [`crate::render::SimpleTextShaper`]
///
/// Glyph **identities** and real advances: `SimpleTextShaper` numbers characters sequentially
/// and charges each a fixed fraction of an em, which is enough to lay out a left-to-right Latin
/// label and cannot express that two characters became one glyph, that a letter changed shape
/// because of its neighbour, or that a pair is kerned.
///
/// # What it needs
///
/// A face. [`Self::from_active_faces`] picks the first of this build's opt-in faces that covers
/// the text; a caller with its own face constructs one with [`Self::new`]. With no face, every
/// method falls back to the model — this type never guesses a glyph.
pub struct RustybuzzShaper {
    face: FaceBytes,
}

impl RustybuzzShaper {
    /// A shaper over `face`, or `None` when its bytes are not a font this crate can read.
    pub fn new(face: FaceBytes) -> Option<Self> {
        face_parser(&face)?;
        Some(Self { face })
    }

    /// A shaper over the first of this build's opt-in faces that covers `text`, if any.
    ///
    /// This is the convenience for a caller that has not named a family: it picks by coverage.
    /// Presentation goes through [`Self::new`] with the face [`face_for_family`] selected, so a
    /// label's *metrics* never change behind a caller's back.
    ///
    /// Returns `None` on a build with no vector face enabled, which is the honest answer: there
    /// is nothing to shape with, and the caller should use the model.
    pub fn from_active_faces(text: &str) -> Option<Self> {
        Self::new(face_for_text(text)?)
    }

    /// The name of the face this shaper reads.
    pub fn face_name(&self) -> &'static str {
        self.face.name
    }

    /// Shape `text` into one run of positioned glyphs, in logical order.
    ///
    /// Positions are in logical pixels; the run's `width` is the sum of the glyph advances, and
    /// its `height` is the font's line box — the same box
    /// [`crate::render::text::line`]'s model reports.
    pub fn shape_face(&self, text: &str, font_size: f32, scale: f32) -> Option<ShapedGlyphRun> {
        let parsed = rustybuzz::Face::from_slice(self.face.bytes, 0)?;
        let units_per_em = parsed.units_per_em() as f32;
        if units_per_em <= 0.0 {
            return None;
        }
        let mut buffer = rustybuzz::UnicodeBuffer::new();
        buffer.push_str(text);
        buffer.guess_segment_properties();
        let output = rustybuzz::shape(&parsed, &[], buffer);

        let unit_px = font_size * scale / units_per_em;
        let mut glyph_ids = Vec::with_capacity(output.len());
        let mut positions = Vec::with_capacity(output.len());
        let mut pen_x = 0.0f32;
        for (info, position) in output.glyph_infos().iter().zip(output.glyph_positions()) {
            glyph_ids.push(info.glyph_id);
            pen_x += position.x_offset as f32 * unit_px;
            positions.push((pen_x, position.y_offset as f32 * unit_px));
            pen_x += position.x_advance as f32 * unit_px;
        }
        Some(ShapedGlyphRun {
            width: pen_x,
            height: font_size * 1.2,
            glyph_ids,
            positions,
            font_size,
        })
    }
}

impl TextShaper for RustybuzzShaper {
    /// One run of real glyphs, or — if the face cannot be read — an empty run rather than a
    /// fabricated one.
    fn shape(&self, text: &str, font_size: f32) -> Vec<ShapedGlyphRun> {
        self.shape_face(text, font_size, 1.0).into_iter().collect()
    }

    fn measure_width(&self, text: &str, font_size: f32) -> f32 {
        self.shape_face(text, font_size, 1.0).map(|run| run.width).unwrap_or(0.0)
    }

    fn measure_height(&self, _text: &str, font_size: f32) -> f32 {
        font_size * 1.2
    }

    fn char_advance(&self, c: char, font_size: f32) -> f32 {
        self.measure_width(&c.to_string(), font_size)
    }
}

#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex"))]
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_face_is_chosen_by_the_first_strong_character() {
        // `from_active_faces` is the "caller named nothing" convenience, so it picks by
        // coverage. The renderer does not use this rule — it honours `Font::family`, so that
        // enabling a face never changes a label's advance behind the caller's back.
        let face = face_for_text("hello").expect("a Latin face is enabled");
        assert!(face.bytes.len() > 1000);

        #[cfg(feature = "fonts-complex")]
        assert_eq!(
            face_for_text("\u{628}\u{64a}\u{62a}").expect("an Arabic face is enabled").name,
            "Noto Naskh Arabic"
        );
    }

    /// A family name selects the face, case-insensitively, and an unknown family does not.
    ///
    /// Gated on `fonts-vector-latin` specifically, not on "a vector feature": the body asserts the
    /// **Open Sans** face, which only that feature ships. Gating this on the module's own
    /// `any(fonts-vector-latin, fonts-complex)` window made it fail on a `fonts-complex`-only build,
    /// where the face it names genuinely does not exist — the test was asserting a fact about the
    /// wrong build configuration.
    #[cfg(feature = "fonts-vector-latin")]
    #[test]
    fn the_face_a_family_names_is_the_face_that_is_used() {
        assert_eq!(
            face_for_family("open sans").map(|face| face.name),
            Some("Open Sans"),
            "a family name is matched case-insensitively"
        );
        assert!(
            face_for_family("Arial").is_none(),
            "a family this build does not carry leaves the model in charge"
        );
    }

    #[test]
    fn shaping_a_ligature_produces_fewer_glyphs_than_characters() {
        // "ffi" is a standard Latin ligature. The model would report three clusters; a real
        // face reports the glyphs the face's `GSUB` says the string becomes.
        let shaper = RustybuzzShaper::from_active_faces("ffi").expect("a face is enabled");
        let shaped = shaper.shape_face("ffi", 16.0, 1.0).expect("the run shapes");
        assert!(
            shaped.glyph_ids.len() <= 3,
            "a ligature may not produce more glyphs than characters: {:?}",
            shaped.glyph_ids
        );
        assert!(shaped.width > 0.0, "a shaped run has a width");
    }

    #[test]
    fn arabic_joining_selects_contextual_forms() {
        // The criterion BLUE23 §0A.4 states for shaping: an Arabic word must not shape to the
        // same glyphs as its letters in isolation. `بيت` (house) is three letters whose medial
        // forms differ from their isolated ones, so the shaped run's glyph identities must
        // differ from the per-character `cmap` lookups.
        #[cfg(feature = "fonts-complex")]
        {
            let shaper =
                RustybuzzShaper::from_active_faces("\u{628}\u{64a}\u{62a}").expect("a face");
            let shaped = shaper.shape_face("\u{628}\u{64a}\u{62a}", 16.0, 1.0).expect("shapes");
            let faces = crate::render::text::font_assets::active_faces();
            let arabic = faces
                .iter()
                .find(|face| face.name == "Noto Naskh Arabic")
                .expect("the Arabic face is enabled");
            let parsed = ttf_parser::Face::parse(arabic.bytes, 0).expect("parses");
            let isolated: Vec<u32> = "\u{628}\u{64a}\u{62a}"
                .chars()
                .filter_map(|ch| parsed.glyph_index(ch).map(|id| id.0 as u32))
                .collect();
            assert_eq!(isolated.len(), 3, "the face has all three letters");
            assert_ne!(
                shaped.glyph_ids, isolated,
                "joining must substitute the contextual forms, not the isolated ones"
            );
        }
    }

    #[test]
    fn real_advances_differ_from_the_models_flat_factor() {
        // The point of reading the face: a proportional face's letters are not all 0.6 em. If
        // they were, the model would be enough and this feature would be worth nothing.
        let shaper = RustybuzzShaper::from_active_faces("iI").expect("a face is enabled");
        let thin = shaper.char_advance('i', 16.0);
        let thick = shaper.char_advance('W', 16.0);
        assert!(thick > thin, "W ({thick}) must be wider than i ({thin}) in a proportional face");
    }

    #[test]
    fn a_build_with_no_vector_face_says_so_rather_than_guessing() {
        // The contract that keeps this honest on a build with no data: no face, no shaper.
        if active_faces().is_empty() {
            assert!(RustybuzzShaper::from_active_faces("abc").is_none());
        }
    }
}