kui-core 0.1.0-alpha.47

kui contract: flat per-frame tree, clay-style flex layout, text stack, events as data, quad display list
Documentation
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
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
//! Registered fonts: installed families by name, font files from bytes,
//! stale handles falling back to sans, shaping through `TextStyle::font`,
//! and what the font database says each family is.

use kui_core::{Core, NodeSpec, Size, TextStyle};

fn glyph_quads(core: &mut Core, style: TextStyle) -> usize {
    let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
    ui.text_in(NodeSpec::column(), "Fonts", style);
    ui.finish();
    let (dl, _) = core.output();
    dl.quads
        .iter()
        .filter(|q| q.kind != kui_core::QuadKind::Solid)
        .count()
}

#[test]
fn installed_family_registers_by_name_and_unknown_is_rejected() {
    let mut core = Core::new();
    assert!(core.add_system_font("kui-no-such-family-2026").is_none());
    let families = core.system_font_families();
    let Some(name) = families.first().cloned() else {
        // The one font test the fixture face cannot stand in for: it is
        // about the *installed* set, which a fontless CI image has none of.
        eprintln!("skipped: no installed font on this machine");
        return;
    };
    let id = core
        .add_system_font(&name)
        .expect("installed family registers");
    assert_eq!(core.font_family(id), Some(name.as_str()));
    assert!(glyph_quads(&mut core, TextStyle::new(20.0).font(id)) > 0);

    core.remove_font(id);
    assert!(core.font_family(id).is_none());
    // A stale handle still shapes (as sans-serif) rather than failing.
    assert!(glyph_quads(&mut core, TextStyle::new(20.0).font(id)) > 0);
}

#[test]
fn font_file_bytes_register_and_garbage_is_rejected() {
    let mut core = Core::new();
    assert!(core.add_font_data(vec![0u8; 64]).is_none());
    // The face the tests carry with them (backlog AR48): this used to look
    // for a system font at four paths and pass green where none was.
    let id = core
        .add_font_data(kui_core::testing::liga_font())
        .expect("a real font file registers");
    let family = core
        .font_family(id)
        .expect("family name recorded")
        .to_string();
    assert_eq!(family, "Kui Liga");
    assert!(glyph_quads(&mut core, TextStyle::new(20.0).font(id)) > 0);
    // Two styles differing only in font must not share a shaping cache entry.
    let a = glyph_quads(&mut core, TextStyle::new(20.0));
    let b = glyph_quads(&mut core, TextStyle::new(20.0).font(id));
    assert!(a > 0 && b > 0);
    core.remove_font(id);
    assert!(core.font_family(id).is_none());
}

#[test]
fn font_folders_and_files_load_by_path_and_names_resolve_idempotently() {
    let mut core = Core::new();
    assert!(core.load_font_file("/no/such/font.ttf").is_none());
    assert_eq!(core.load_fonts_dir("/no/such/dir"), 0);
    // A private folder with one font in it, the "bundled fonts/ dir" case:
    // the fixture face written out, so the path doors are exercised on a
    // machine with no font of its own.
    let dir = std::env::temp_dir().join(format!("kui-fonts-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("bundled.ttf");
    std::fs::write(&file, kui_core::testing::liga_font()).unwrap();

    let by_file = core.load_font_file(&file).expect("file loads by path");
    let family = core.font_family(by_file).unwrap().to_string();
    assert!(glyph_quads(&mut core, TextStyle::new(20.0).font(by_file)) > 0);

    let mut fresh = Core::new();
    assert!(fresh.load_fonts_dir(&dir) >= 1);
    let a = fresh
        .add_system_font(&family)
        .expect("loaded family resolves by name");
    let b = fresh.add_system_font(&family).unwrap();
    assert_eq!(a, b, "the same family keeps one handle");
    assert!(glyph_quads(&mut fresh, TextStyle::new(20.0).font(a)) > 0);
    std::fs::remove_dir_all(&dir).ok();
}

/// What the font database knows of each family, listed without loading or
/// shaping anything (backlog F97): whether every face is fixed-pitch, the
/// weights, an italic. Families of the fixture face stand in for installed
/// ones: a monospaced one in three faces, a proportional one, and one whose
/// faces disagree, which is not monospaced.
#[test]
fn system_fonts_say_what_each_family_is() {
    use kui_core::SystemFont;
    use kui_core::testing::font_face;
    let mut core = Core::new();
    let faces = [
        font_face("Kui F97 Mono", 400, false, true),
        font_face("Kui F97 Mono", 700, false, true),
        font_face("Kui F97 Mono", 400, true, true),
        font_face("Kui F97 Sans", 300, false, false),
        font_face("Kui F97 Mixed", 700, false, false),
        font_face("Kui F97 Mixed", 400, false, true),
    ];
    let ids: Vec<_> = faces
        .into_iter()
        .map(|bytes| core.add_font_data(bytes).expect("the fixture registers"))
        .collect();

    let fonts = core.system_fonts();
    let names: Vec<&str> = fonts.iter().map(|f| f.family.as_str()).collect();
    let mut sorted = names.clone();
    sorted.sort();
    sorted.dedup();
    assert_eq!(names, sorted, "sorted by family, one row each");
    assert_eq!(
        core.system_font_families(),
        names,
        "the same families system_font_families names"
    );

    let find = |family: &str| fonts.iter().find(|f| f.family == family).cloned();
    assert_eq!(
        find("Kui F97 Mono"),
        Some(SystemFont {
            family: "Kui F97 Mono".into(),
            monospaced: true,
            weights: vec![400, 700],
            italic: true,
        })
    );
    assert_eq!(
        find("Kui F97 Sans"),
        Some(SystemFont {
            family: "Kui F97 Sans".into(),
            monospaced: false,
            weights: vec![300],
            italic: false,
        })
    );
    let mixed = find("Kui F97 Mixed").expect("listed");
    assert!(!mixed.monospaced, "one proportional face and it is not");
    assert_eq!(
        mixed.weights,
        vec![400, 700],
        "sorted, whatever the load order"
    );

    // Each family's name is one add_system_font takes.
    let mono = core.add_system_font("Kui F97 Mono").expect("resolves");
    assert_eq!(core.font_family(mono), Some("Kui F97 Mono"));

    for id in ids {
        core.remove_font(id);
    }
    assert!(
        !core
            .system_fonts()
            .iter()
            .any(|f| f.family.starts_with("Kui F97")),
        "gone with their faces"
    );
}

/// A face whose glyph advances cannot be measured — no `head`, `hhea` or
/// `hmtx`, the way macOS's GB18030 Bitmap has none — is refused wherever
/// a face comes in (backlog F98): from bytes, from a file, from a folder,
/// so it is never a family to list, name or fall back to.
#[test]
fn a_face_whose_advances_cannot_be_measured_is_refused() {
    use kui_core::testing::{han_face, unmeasurable_face};
    let mut core = Core::new();
    assert!(
        core.add_font_data(unmeasurable_face("Kui F98 Bitmap"))
            .is_none(),
        "no usable face in the bytes"
    );

    let dir = std::env::temp_dir().join(format!("kui-f98-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let bitmap = dir.join("bitmap.ttf");
    std::fs::write(&bitmap, unmeasurable_face("Kui F98 Bitmap")).unwrap();
    std::fs::write(dir.join("han.ttf"), han_face("Kui F98 Han")).unwrap();
    assert!(core.load_font_file(&bitmap).is_none(), "nor in the file");
    assert_eq!(core.load_fonts_dir(&dir), 1, "the folder's other face");
    std::fs::remove_dir_all(&dir).ok();

    let families = core.system_font_families();
    assert!(families.iter().any(|f| f == "Kui F98 Han"));
    assert!(
        !families.iter().any(|f| f == "Kui F98 Bitmap"),
        "not offered in the list"
    );
    assert!(core.add_system_font("Kui F98 Bitmap").is_none());
}

/// Han text in `mono` on the fonts this machine has (backlog F98): on a
/// Mac, 字 fell back to GB18030 Bitmap, whose advances are infinite, and
/// the frame overflowed in a debug build or placed what followed at
/// infinity. Every glyph lands at a finite place, left to right.
#[test]
fn han_in_mono_draws_at_finite_places() {
    use kui_core::{FontFamily, QuadKind};
    let mut core = Core::new();
    let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
    ui.text_in(
        NodeSpec::column(),
        "字 a",
        TextStyle::new(14.0).family(FontFamily::Mono),
    );
    ui.finish();
    let (dl, _) = core.output();
    let glyphs: Vec<_> = dl
        .quads
        .iter()
        .filter(|q| q.kind != QuadKind::Solid)
        .map(|q| q.rect)
        .collect();
    for r in &glyphs {
        assert!(
            r.x.is_finite() && r.y.is_finite() && r.w.is_finite() && r.h.is_finite(),
            "a glyph at {r:?}"
        );
        assert!(
            r.x >= 0.0 && r.x + r.w <= 400.0,
            "a glyph off the line: {r:?}"
        );
    }
    // Where some installed face maps 字, it is drawn and the `a` starts past it.
    if let [han, a] = glyphs[..] {
        assert!(han.x + han.w <= a.x, "字 before a: {glyphs:?}");
    }
}

/// The glyph quads of `spans` in `style`, drawn as one rich text: each
/// glyph's x and width.
fn rich_glyphs(core: &mut Core, spans: &[kui_core::Span<'_>], style: TextStyle) -> Vec<(f32, f32)> {
    let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
    ui.with(NodeSpec::column(), |ui| ui.rich_text(spans, style));
    ui.finish();
    let (dl, _) = core.output();
    dl.quads
        .iter()
        .filter(|q| q.kind != kui_core::QuadKind::Solid)
        .map(|q| (q.rect.x, q.rect.w))
        .collect()
}

/// The glyph quads of one row of cells in `style`, each `flags`: each
/// glyph's x and width.
fn cell_glyphs(core: &mut Core, text: &str, flags: u8, style: TextStyle) -> Vec<(f32, f32)> {
    use kui_core::cells::{Cell, CellGrid};
    let cells: Vec<Cell> = text
        .chars()
        .map(|ch| Cell {
            flags,
            ..Cell::new(ch, 0xffffffff, 0)
        })
        .collect();
    let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
    ui.configure_root(NodeSpec::column().fill());
    ui.cells(
        &CellGrid {
            rows: 1,
            cols: cells.len(),
            cells: &cells,
            style,
            cursor: None,
            origin_line: 0,
        },
        NodeSpec::default(),
    );
    ui.finish();
    let (dl, _) = core.output();
    dl.quads
        .iter()
        .filter(|q| q.kind != kui_core::QuadKind::Solid)
        .map(|q| (q.rect.x, q.rect.w))
        .collect()
}

/// Every glyph `advance` from the one before it and `width` wide: glyphs
/// of the fixture family, whose advance is half an em.
fn assert_fixture(glyphs: &[(f32, f32)], advance: f32, width: f32, what: &str) {
    assert_eq!(glyphs.len(), 4, "{what}: {glyphs:?}");
    for pair in glyphs.windows(2) {
        assert_eq!(pair[1].0 - pair[0].0, advance, "{what}: {glyphs:?}");
    }
    for &(_, w) in glyphs {
        assert_eq!(w, width, "{what}: {glyphs:?}");
    }
}

/// A variable family whose `wght` axis is not on the CSS scale — Berkeley
/// Mono Variable's, 100 (Regular) to 150 (Bold) with an `OS/2` weight of
/// 400 — draws its Regular for regular and its Bold for bold, in text and
/// in cells (backlog F100). Bold, asked at 700, fell outside the axis and
/// was drawn in another family, one glyph to a cell in a terminal; regular
/// took 400 as the coordinate and was drawn at the Bold. The fixture's
/// square is 400 units wide at the Regular and 500 at the Bold; a family
/// with a bold face is registered beside it, where bold used to go.
#[test]
fn bold_of_a_variable_family_off_the_css_scale_is_its_bold_instance() {
    use kui_core::Span;
    use kui_core::cells::flags;
    use kui_core::testing::{font_face, variable_face};
    let mut core = Core::new();
    let axis = [("Regular", 100), ("Bold", 150)];
    let id = core
        .add_font_data(variable_face("Kui F100 Var", false, [100, 100, 150], &axis))
        .expect("the variable fixture registers");
    core.add_font_data(variable_face("Kui F100 Var", true, [100, 100, 150], &axis))
        .expect("its italic");
    core.add_font_data(font_face("Kui F100 Decoy", 700, false, false))
        .expect("a bold face of another family");
    // 20 px: an advance of 10, a square of 8 at the Regular and 10 at the Bold.
    let style = TextStyle::new(20.0).font(id).line_height(24.0);
    let plain = Span::new("aaaa");
    let bold = Span {
        bold: true,
        ..plain
    };
    let both = Span {
        italic: true,
        ..bold
    };
    assert_fixture(
        &rich_glyphs(&mut core, &[plain], style),
        10.0,
        8.0,
        "regular",
    );
    assert_fixture(&rich_glyphs(&mut core, &[bold], style), 10.0, 10.0, "bold");
    assert_fixture(
        &cell_glyphs(&mut core, "aaaa", 0, style),
        10.0,
        8.0,
        "regular cells",
    );
    assert_fixture(
        &cell_glyphs(&mut core, "aaaa", flags::BOLD, style),
        10.0,
        10.0,
        "bold cells",
    );
    // Bold italic from the italic face (upright squares too, so no lean
    // is synthesized) at its Bold.
    let flags = flags::BOLD | flags::ITALIC;
    assert_fixture(
        &rich_glyphs(&mut core, &[both], style),
        10.0,
        10.0,
        "bold italic",
    );
    assert_fixture(
        &cell_glyphs(&mut core, "aaaa", flags, style),
        10.0,
        10.0,
        "bold italic cells",
    );
}

/// A family of one regular face draws bold in that face, its outline
/// grown, and italic in that face, leaning — never in another family
/// that has the weight or the style (backlog F100). Bold used to go to
/// the other family's bold face, the fixture's square at its own width.
#[test]
fn bold_and_italic_of_a_family_without_them_are_synthesized_in_it() {
    use kui_core::Span;
    use kui_core::cells::flags;
    use kui_core::testing::font_face;
    let mut core = Core::new();
    let id = core
        .add_font_data(font_face("Kui F100 Mono", 400, false, true))
        .expect("the fixture registers");
    core.add_font_data(font_face("Kui F100 Decoy", 700, false, false))
        .expect("a bold face of another family");
    core.add_font_data(font_face("Kui F100 Decoy", 400, true, false))
        .expect("an italic face of another family");
    let style = TextStyle::new(20.0).font(id).line_height(24.0);
    let plain = Span::new("aaaa");
    let regular = rich_glyphs(&mut core, &[plain], style);
    assert_fixture(&regular, 10.0, 8.0, "regular");
    let cases = [
        (
            Span {
                bold: true,
                ..plain
            },
            flags::BOLD,
            "bold",
        ),
        (
            Span {
                italic: true,
                ..plain
            },
            flags::ITALIC,
            "italic",
        ),
    ];
    for (span, flag, what) in cases {
        let text = rich_glyphs(&mut core, &[span], style);
        let cells = cell_glyphs(&mut core, "aaaa", flag, style);
        for glyphs in [&text, &cells] {
            assert_eq!(glyphs.len(), 4, "{what}: {glyphs:?}");
            assert!(
                glyphs.windows(2).all(|p| p[1].0 - p[0].0 == 10.0),
                "{what}, a glyph to a cell: {glyphs:?}"
            );
            assert!(
                glyphs.iter().all(|g| g.1 > 8.0),
                "{what} wider than regular: {glyphs:?}"
            );
        }
    }
}

/// A family with no 400 face — a Light-only file — draws in that family
/// in plain text and in an editor, as it does in a span and a cell: every
/// path asks it at its own regular (the regression pass over F100: only
/// spans and cells did, so one style drew in two families).
#[test]
fn a_family_with_no_400_face_draws_in_it_everywhere() {
    use kui_core::testing::font_face;
    use kui_core::{EditOptions, Span};
    let mut core = Core::new();
    let id = core
        .add_font_data(font_face("Kui F100 Light", 300, false, false))
        .expect("the fixture registers");
    let style = TextStyle::new(20.0).font(id).line_height(24.0);
    assert_fixture(
        &rich_glyphs(&mut core, &[Span::new("aaaa")], style),
        10.0,
        8.0,
        "span",
    );
    assert_fixture(
        &cell_glyphs(&mut core, "aaaa", 0, style),
        10.0,
        8.0,
        "cells",
    );
    let glyphs = |core: &mut Core| -> Vec<(f32, f32)> {
        let (dl, _) = core.output();
        dl.quads
            .iter()
            .filter(|q| q.kind == kui_core::QuadKind::GlyphMask)
            .map(|q| (q.rect.x, q.rect.w))
            .collect()
    };
    let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
    ui.text_in(NodeSpec::column(), "aaaa", style);
    ui.finish();
    assert_fixture(&glyphs(&mut core), 10.0, 8.0, "plain text");
    let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
    ui.text_edit(
        "field",
        "aaaa",
        &EditOptions {
            style,
            ..Default::default()
        },
        NodeSpec::column().width(kui_core::Sizing::Fixed(300.0)),
    );
    ui.finish();
    assert_fixture(&glyphs(&mut core), 10.0, 8.0, "editor");
}

/// A face of a registered family loaded or removed under text already
/// shaped in it reaches that text: plain text, spans, cells and an
/// editor shape again at the weights the family is asked at now (RG59).
/// They were keyed by content and style alone, so text shaped before a
/// family gained its regular kept the face it had, and bold synthesized
/// before the family's Bold was loaded stayed synthetic — or stayed that
/// Bold after it was removed.
#[test]
fn a_face_loaded_or_removed_under_shaped_text_reaches_it() {
    use kui_core::testing::{font_face, han_face};
    use kui_core::{EditOptions, Span};
    let mut core = Core::new();
    // A Light face with no 字: the family's regular is 300, and 字 falls
    // back to another family.
    let light = core
        .add_font_data(font_face("Kui RG59", 300, false, false))
        .expect("the fixture registers");
    let style = TextStyle::new(20.0).font(light).line_height(24.0);
    let glyphs = |core: &mut Core| -> Vec<(f32, f32)> {
        let (dl, _) = core.output();
        dl.quads
            .iter()
            .filter(|q| q.kind == kui_core::QuadKind::GlyphMask)
            .map(|q| (q.rect.x, q.rect.w))
            .collect()
    };
    // Each path's glyphs of 字字字字.
    let every_path = |core: &mut Core| -> Vec<(&str, Vec<(f32, f32)>)> {
        let han = "字字字字";
        let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
        ui.text_in(NodeSpec::column(), han, style);
        ui.finish();
        let plain = glyphs(core);
        let spans = rich_glyphs(core, &[Span::new(han)], style);
        let cells = cell_glyphs(core, han, 0, style);
        let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
        ui.text_edit(
            "field",
            han,
            &EditOptions {
                style,
                ..Default::default()
            },
            NodeSpec::column().width(kui_core::Sizing::Fixed(300.0)),
        );
        ui.finish();
        let editor = glyphs(core);
        vec![
            ("plain text", plain),
            ("span", spans),
            ("cells", cells),
            ("editor", editor),
        ]
    };
    let bold = Span {
        bold: true,
        ..Span::new("aaaa")
    };
    every_path(&mut core);
    let synthetic = rich_glyphs(&mut core, &[bold], style);
    assert!(synthetic.iter().all(|g| g.1 > 8.0), "{synthetic:?}");
    // A Regular with 字 comes: the family's regular is 400, and 字 is
    // the fixture's, half an em apart.
    core.add_font_data(han_face("Kui RG59"))
        .expect("a regular face of it");
    let paths = every_path(&mut core);
    let square = paths[0].1.first().map(|g| g.1);
    for (what, glyphs) in paths {
        assert_eq!(glyphs.len(), 4, "{what}: {glyphs:?}");
        assert!(
            glyphs.windows(2).all(|p| p[1].0 - p[0].0 == 10.0)
                && glyphs.iter().all(|g| Some(g.1) == square),
            "{what} shaped at the family's new regular: {glyphs:?}"
        );
    }
    // Its Bold comes: bold is that face, drawn as it is.
    let bold_face = core
        .add_font_data(font_face("Kui RG59", 700, false, false))
        .expect("a bold face of it");
    let real = rich_glyphs(&mut core, &[bold], style);
    assert_fixture(&real, 10.0, 8.0, "bold, the family's Bold");
    // And goes: bold is synthesized again.
    core.remove_font(bold_face);
    let synthetic = rich_glyphs(&mut core, &[bold], style);
    assert!(
        synthetic.iter().all(|g| g.1 > 8.0),
        "bold synthesized again: {synthetic:?}"
    );
}

/// F121: the families an app names are asked for a character the text's
/// own family lacks before the platform's list — in plain text and in a
/// cell grid alike — and the platform's runs again when the list is empty.
#[test]
fn the_apps_fallback_fonts_are_asked_before_the_platforms() {
    use kui_core::testing::{font_face, han_face};
    let mut core = Core::new();
    let own = core
        .add_font_data(font_face("Kui F121", 400, false, false))
        .expect("the fixture registers");
    // The fixture's 字 is half an em wide: no installed face's is.
    let han = core
        .add_font_data(han_face("Kui F121 Han"))
        .expect("a face with 字");
    let style = TextStyle::new(20.0).font(own).line_height(24.0);
    let plain = |core: &mut Core| -> Vec<f32> {
        let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
        ui.text_in(NodeSpec::column(), "字字", style);
        ui.finish();
        let (dl, _) = core.output();
        dl.quads
            .iter()
            .filter(|q| q.kind != kui_core::QuadKind::Solid)
            .map(|q| q.rect.x)
            .collect()
    };
    // An editor already open on the text, its lines shaped (RG114).
    let editor = |core: &mut Core| -> Vec<f32> {
        let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
        ui.text_edit(
            "field",
            "字字",
            &kui_core::EditOptions {
                style,
                ..Default::default()
            },
            NodeSpec::column().width(kui_core::Sizing::Fixed(300.0)),
        );
        ui.finish();
        let (dl, _) = core.output();
        dl.quads
            .iter()
            .filter(|q| q.kind == kui_core::QuadKind::GlyphMask)
            .map(|q| q.rect.x)
            .collect()
    };
    let apart = |xs: &[f32]| xs.get(1).zip(xs.first()).map(|(b, a)| b - a);
    let before = plain(&mut core);
    // Whether the machine has a face of its own with 字 for its list to
    // name. A bare CI image has none - DejaVu alone - and there the
    // fixture is the one face that can answer, list or no list, so the
    // two orders draw the same and only the app's half can be told. (The
    // first alpha.37 tag failed its release's `check` on exactly that.)
    let platform_has_one = apart(&before) != Some(10.0);
    let cells_before = cell_glyphs(&mut core, "字", 0, style);
    let editor_before = editor(&mut core);
    assert_eq!(
        apart(&editor_before) != Some(10.0),
        platform_has_one,
        "an editor asks whom plain text asks: {editor_before:?}"
    );

    core.set_fallback_fonts(&[han]);
    assert_eq!(core.fallback_fonts(), ["Kui F121 Han"]);
    let after = plain(&mut core);
    assert_eq!(apart(&after), Some(10.0), "the app's: {after:?}");
    let cells_after = cell_glyphs(&mut core, "字", 0, style);
    if platform_has_one {
        assert_ne!(cells_after, cells_before, "a grid asks them too");
    }
    let editor_after = editor(&mut core);
    assert_eq!(apart(&editor_after), Some(10.0), "an open editor too");

    core.set_fallback_fonts(&[]);
    assert_eq!(plain(&mut core), before, "and the platform's again");
    assert_eq!(cell_glyphs(&mut core, "字", 0, style), cells_before);
    assert_eq!(editor(&mut core), editor_before);
}

/// `mono` text asks the app's fallback fonts straight after its own face
/// (backlog RG118). cosmic-text's generic monospace family asks every
/// monospaced face on the machine for what its face lacks first, so a
/// monospaced face with the character beat the app's choice — on a Mac,
/// Courier New's italic for Hebrew, PT Mono for ₽, Cascadia Code for
/// braille. Here the monospaced face is a fixture whose 字 is 14 px at
/// 20, and the app's choice one whose 字 is 10.
#[test]
fn mono_text_asks_the_apps_fallback_fonts_before_the_other_monospaced_faces() {
    use kui_core::testing::{han_face, mono_han_face};
    let mut core = Core::new();
    if core.system_font_families().is_empty() {
        // No installed face for `Mono` to be pinned to, so it stays the
        // generic family, which walks the monospaced faces first.
        eprintln!("skipped: no installed font on this machine");
        return;
    }
    core.add_font_data(mono_han_face("Kui RG118 Mono Han"))
        .expect("a monospaced face with 字");
    let han = core
        .add_font_data(han_face("Kui RG118 Han"))
        .expect("a face with 字");
    let style = TextStyle::new(20.0)
        .family(kui_core::FontFamily::Mono)
        .line_height(24.0);
    let apart = |core: &mut Core| -> Option<f32> {
        let mut ui = core.frame(Size::new(400.0, 100.0), 1.0);
        ui.text_in(NodeSpec::column(), "字字", style);
        ui.finish();
        let (dl, _) = core.output();
        let xs: Vec<f32> = dl
            .quads
            .iter()
            .filter(|q| q.kind != kui_core::QuadKind::Solid)
            .map(|q| q.rect.x)
            .collect();
        xs.get(1).zip(xs.first()).map(|(b, a)| b - a)
    };
    core.set_fallback_fonts(&[han]);
    assert_eq!(apart(&mut core), Some(10.0), "the app's 字, not 14 px");
    // In a grid, the app's square: 8 px of ink, where the monospaced
    // face's is 12 (or less, fitted to the cell).
    let cells = cell_glyphs(&mut core, "字", 0, style);
    assert_eq!(cells.iter().map(|c| c.1).collect::<Vec<_>>(), [8.0]);
    // With no list the monospaced faces are asked first again, the
    // fixture among them now that the font system was built over it.
    core.set_fallback_fonts(&[]);
    assert_ne!(apart(&mut core), Some(10.0), "not the app's without a list");
}