kui_core/runtime/resources_api.rs
1//! Resources through a core: fonts, sounds and images live in the
2//! session's registry (`resources`), so registering one here registers
3//! it for every window; playback is commands the driver drains
4//! (`audio`). Nothing here touches a device.
5
6use super::*;
7
8impl Core {
9 /// Turns the sounds nodes asked for (`click_sound` / `hover_sound`)
10 /// into play commands. Declarative sounds carry no tag, so they never
11 /// report `ended`.
12 pub(crate) fn flush_sound_requests(&mut self) {
13 let window = self.env.window.id;
14 let mut sess = self.session.state();
15 for sound in self.interaction.take_sound_requests() {
16 sess.audio.play(
17 OriginId::HOST,
18 window,
19 Key::ROOT,
20 sound,
21 crate::audio::PlayOptions::default(),
22 );
23 }
24 }
25
26 // -- Fragments ------------------------------------------------------
27
28 /// Registers a WGSL fragment function for a `fragment` node.
29 /// The app writes one function:
30 ///
31 /// ```wgsl
32 /// fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32>
33 /// ```
34 ///
35 /// and the core wraps it in the prelude and epilogue that give it the
36 /// node's rounded box, the inherited clip, the group opacity and the
37 /// blend (see `crate::fragment`). `None` when the source does not
38 /// compile, with a `fragment-rejected` warning carrying naga's message
39 /// in the app's own line numbers — so a bad shader is a warning at
40 /// registration, in a headless test included, and never a blank box in
41 /// a window.
42 ///
43 /// Idempotent by source: the same text gets the same handle without
44 /// validating again, so a view may call this every frame. Registering
45 /// at startup is still the advice, because the *backend* builds a
46 /// pipeline the first time it sees a handle.
47 pub fn add_fragment(&mut self, wgsl: &str) -> Option<crate::resources::FragmentId> {
48 if let Some(id) = self.session.state().resources.find_fragment(wgsl) {
49 return Some(id);
50 }
51 if let Err(message) = crate::fragment::validate(wgsl) {
52 self.warn(crate::diag::Warning {
53 code: crate::diag::FRAGMENT_REJECTED,
54 key: crate::key::Key::ROOT,
55 message: format!("fragment source rejected: {message}"),
56 });
57 return None;
58 }
59 Some(self.session.state().resources.add_fragment(wgsl))
60 }
61
62 /// Forgets a registered fragment. Nodes still naming it draw nothing,
63 /// and the next frame any window draws has the backend drop the
64 /// pipelines it built for it.
65 pub fn remove_fragment(&mut self, id: crate::resources::FragmentId) {
66 self.session.state().remove_fragment(id);
67 // The stock polygon's handle, if that is what went: the next
68 // `polygon` node registers it again rather than drawing nothing.
69 if self.stock_polygon == Some(id) {
70 self.stock_polygon = None;
71 }
72 }
73
74 /// The whole WGSL module behind a fragment handle — the app's source
75 /// between the core's prelude and epilogue — which is what a backend
76 /// compiles. A host rendering the display list itself asks for this
77 /// rather than assembling its own, so what it compiles is what the
78 /// core validated.
79 pub fn fragment_module_source(&self, id: crate::resources::FragmentId) -> Option<String> {
80 let sess = self.session.state();
81 let app = sess.resources.fragment(id)?;
82 Some(crate::fragment::module_source(app))
83 }
84
85 // -- Fonts ----------------------------------------------------------
86
87 /// Registers a font from its file bytes (TTF/OTF/TTC); `None` when the
88 /// data holds no usable face — none the font database can read, or
89 /// none whose glyphs can be measured (no `head`, `hhea` or `hmtx`).
90 /// Shape with it via `TextStyle::font`.
91 pub fn add_font_data(&mut self, data: Vec<u8>) -> Option<crate::resources::FontId> {
92 use cosmic_text::fontdb::Source;
93 let id = {
94 let sess = &mut *self.session.state();
95 let ids = sess
96 .fonts
97 .db_mut()
98 .load_font_source(Source::Binary(std::sync::Arc::new(data)));
99 sess.share_loaded_faces();
100 let db = sess.fonts.db_mut();
101 let ids = crate::text::keep_measurable(db, ids.to_vec());
102 let family = db.face(*ids.first()?)?.families.first()?.0.clone();
103 sess.fonts_rev += 1;
104 let touched = families_of(db, &ids);
105 let id = sess.resources.add_font(family, ids.to_vec());
106 if sess.resources.reweigh(sess.fonts.db(), &touched, Some(id)) {
107 sess.weights_rev += 1;
108 }
109 sess.repin_mono();
110 id
111 };
112 self.sync_font_names();
113 Some(id)
114 }
115
116 /// Registers a font file (TTF/OTF/TTC) by path, memory-mapped by the
117 /// font database; `None` when it cannot be read or holds no usable
118 /// face (see [`add_font_data`](Self::add_font_data)). Shape with it
119 /// via `TextStyle::font`.
120 pub fn load_font_file(
121 &mut self,
122 path: impl Into<std::path::PathBuf>,
123 ) -> Option<crate::resources::FontId> {
124 use cosmic_text::fontdb::Source;
125 let id = {
126 let sess = &mut *self.session.state();
127 let ids = sess
128 .fonts
129 .db_mut()
130 .load_font_source(Source::File(path.into()));
131 // Its own faces too, not only the ones before it (DX25).
132 sess.share_loaded_faces();
133 let db = sess.fonts.db_mut();
134 let ids = crate::text::keep_measurable(db, ids.to_vec());
135 let family = db.face(*ids.first()?)?.families.first()?.0.clone();
136 sess.fonts_rev += 1;
137 let touched = families_of(db, &ids);
138 let id = sess.resources.add_font(family, ids.to_vec());
139 if sess.resources.reweigh(sess.fonts.db(), &touched, Some(id)) {
140 sess.weights_rev += 1;
141 }
142 sess.repin_mono();
143 id
144 };
145 self.sync_font_names();
146 Some(id)
147 }
148
149 /// Loads every font file under `dir` (recursively) into the font
150 /// database, so their families become available to `add_system_font`
151 /// by name; returns how many faces were added. A face whose glyphs
152 /// cannot be measured is left out and not counted. A
153 /// bundled `fonts/` folder next to the app is the usual case.
154 pub fn load_fonts_dir(&mut self, dir: impl AsRef<std::path::Path>) -> usize {
155 let sess = &mut *self.session.state();
156 let db = sess.fonts.db_mut();
157 let before: rustc_hash::FxHashSet<_> = db.faces().map(|face| face.id).collect();
158 db.load_fonts_dir(dir);
159 sess.share_loaded_faces();
160 let db = sess.fonts.db_mut();
161 let added = db
162 .faces()
163 .map(|face| face.id)
164 .filter(|id| !before.contains(id))
165 .collect();
166 let added = crate::text::keep_measurable(db, added);
167 // A registered family may have gained a bold (backlog F100).
168 let touched = families_of(db, &added);
169 if sess.resources.reweigh(sess.fonts.db(), &touched, None) {
170 sess.weights_rev += 1;
171 }
172 sess.repin_mono();
173 added.len()
174 }
175
176 /// Scans the system's fonts again and brings this session's font
177 /// database up to it: a face installed since the last scan joins it,
178 /// one uninstalled leaves it. Returns how many faces came and went, 0
179 /// when nothing did.
180 ///
181 /// The system's fonts are scanned once a process, and every session
182 /// starts from that scan, so a font the user installs while the app
183 /// runs is not seen until something asks. The winit runner
184 /// (`kui-native`) asks itself when macOS or Windows says the installed
185 /// fonts changed; a host with its own windowing, or on Linux, where
186 /// fontconfig says nothing, calls this when it has reason to think the
187 /// set changed (a "fonts" pane opening, the window taking focus back).
188 /// It opens every font file on the system — tens of milliseconds on a
189 /// Mac's 1300 faces — so it is not a per-frame call. Sessions made after
190 /// it start from the new scan; another
191 /// session that already exists keeps what it has until it calls this
192 /// too.
193 ///
194 /// Faces still installed keep their handles and their place in every
195 /// cache; fonts the app loaded itself (`add_font_data`,
196 /// `load_font_file`, `load_fonts_dir`) are not touched. Every window
197 /// of the session shapes its text again on its next frame, since
198 /// fallback can land on a new face anywhere; every one of them owes
199 /// that frame (`animating`), and each window's next frame reports a `fonts` event to
200 /// the host, for an app that keeps the font list in its model. A face
201 /// whose file was replaced in place, under the same
202 /// path, is not read again.
203 pub fn reload_system_fonts(&mut self) -> usize {
204 let fresh = crate::text::rescan_system_fonts();
205 let changed = self.session.state().apply_system_fonts(fresh);
206 if changed > 0 {
207 self.sync_font_names();
208 self.request_frame();
209 }
210 changed
211 }
212
213 /// The handle for a font family by name (`"Menlo"`, `"Antonio"`) —
214 /// installed on the system or loaded with `load_fonts_dir` /
215 /// `load_font_file`; `None` when no face matches (see
216 /// `system_font_families`). Idempotent: the same family gets the same
217 /// handle, so views can call it every frame.
218 pub fn add_system_font(&mut self, name: &str) -> Option<crate::resources::FontId> {
219 let id = self.session.register_family(name)?;
220 self.sync_font_names();
221 Some(id)
222 }
223
224 /// The families asked, in order, for a character the text's own
225 /// family has no glyph for — before the platform's fallback list,
226 /// whose first choice is the system's interface face: proportional on
227 /// macOS, so a Cyrillic word in a Latin-only monospaced family was
228 /// set in San Francisco. An editor names its icon face, the face it
229 /// ships and a monospaced one the machine has; a family that has not
230 /// got the character is passed over, and after the last the
231 /// platform's list runs as before. For every family and every kind of
232 /// text in the session — plain, spans, an editor, a cell grid — and
233 /// kept across `reload_system_fonts`. `FontFamily::Mono` asks them
234 /// straight after its own face, ahead of the machine's other
235 /// monospaced faces, which with no list it walks first (backlog
236 /// RG118). A handle that names no font is left out; an empty list is
237 /// the platform's alone.
238 ///
239 /// Not a per-frame call for a list that changes: a new list builds
240 /// the font system again over the same faces and every window of the
241 /// session shapes its text again: each owes a frame (`animating`), so
242 /// a driver draws the windows the call was not made through as well
243 /// (backlog RG118). The same list twice is nothing.
244 pub fn set_fallback_fonts(&mut self, fonts: &[crate::resources::FontId]) {
245 {
246 let sess = &mut *self.session.state();
247 let names: Vec<String> = fonts
248 .iter()
249 .filter_map(|&id| sess.resources.font_family(id).map(str::to_string))
250 .collect();
251 if names == sess.resources.fallback {
252 return;
253 }
254 sess.resources.fallback = names;
255 let old = std::mem::replace(
256 &mut sess.fonts,
257 cosmic_text::FontSystem::new_with_locale_and_db(String::new(), Default::default()),
258 );
259 let (locale, db) = old.into_locale_and_db();
260 sess.fonts = crate::text::font_system_over(locale, db, &sess.resources.fallback);
261 if sess.faces_shared {
262 share_faces(sess.fonts.db_mut());
263 }
264 sess.name_mono();
265 sess.weights_rev += 1;
266 }
267 self.request_frame();
268 }
269
270 /// The families `set_fallback_fonts` named, in order.
271 pub fn fallback_fonts(&self) -> Vec<String> {
272 self.session.state().resources.fallback.clone()
273 }
274
275 /// Forgets a registered font; faces loaded from bytes leave the font
276 /// database. Styles still naming it shape as sans-serif.
277 pub fn remove_font(&mut self, id: crate::resources::FontId) {
278 {
279 let sess = &mut *self.session.state();
280 let Some(entry) = sess.resources.remove_font(id) else {
281 return;
282 };
283 sess.fonts_rev += 1;
284 let db = sess.fonts.db_mut();
285 let touched = families_of(db, &entry.faces);
286 for face in entry.faces {
287 db.remove_face(face);
288 }
289 if sess.resources.reweigh(sess.fonts.db(), &touched, None) {
290 sess.weights_rev += 1;
291 }
292 sess.repin_mono();
293 }
294 self.sync_font_names();
295 }
296
297 /// The registered family name behind a font handle, if it is live.
298 /// Read from this window's mirror of the session's fonts (see
299 /// `session`'s module doc), which every registration refreshes and so
300 /// does every frame — a font another window registered mid-frame shows
301 /// up here on the next one.
302 pub fn font_family(&self, id: crate::resources::FontId) -> Option<&str> {
303 self.font_names.get(&id).map(|s| &**s)
304 }
305
306 /// Family names of every installed font the core can see (sorted,
307 /// deduplicated) — what `add_system_font` accepts. The names of
308 /// [`system_fonts`](Self::system_fonts), which says what each is.
309 pub fn system_font_families(&self) -> Vec<String> {
310 self.system_fonts().into_iter().map(|f| f.family).collect()
311 }
312
313 /// Every family the core can see, installed or loaded, one per family
314 /// and sorted by name — the families `system_font_families` names —
315 /// with what its faces say they are: monospaced, the weights, an
316 /// italic. Read from what the font database recorded
317 /// when it scanned each face, so a fonts pane showing the monospaced
318 /// ones first costs no file loaded and no glyph shaped. A face whose
319 /// glyphs cannot be measured never entered the database,
320 /// so a family of only such faces — macOS's GB18030 Bitmap — is not
321 /// listed.
322 pub fn system_fonts(&self) -> Vec<crate::resources::SystemFont> {
323 use crate::resources::SystemFont;
324 use cosmic_text::fontdb::Style;
325 let sess = self.session.state();
326 let mut by_family = std::collections::BTreeMap::<&str, SystemFont>::new();
327 for face in sess.fonts.db().faces() {
328 let Some((name, _)) = face.families.first() else {
329 continue;
330 };
331 let font = by_family.entry(name).or_insert_with(|| SystemFont {
332 family: name.clone(),
333 monospaced: true,
334 weights: Vec::new(),
335 italic: false,
336 });
337 font.monospaced &= face.monospaced;
338 font.italic |= face.style != Style::Normal;
339 font.weights.push(face.weight.0);
340 }
341 by_family
342 .into_values()
343 .map(|mut font| {
344 font.weights.sort_unstable();
345 font.weights.dedup();
346 font
347 })
348 .collect()
349 }
350
351 /// The installed families `FontFamily::Sans`, `Serif` and `Mono` shape
352 /// with, in that order — pinned per platform when the session's font
353 /// database was built, so the devtools can say which face
354 /// "mono" is on this machine.
355 pub(crate) fn default_font_families(&self) -> [String; 3] {
356 crate::text::default_families(&self.session.state().fonts).map(str::to_string)
357 }
358
359 // -- Audio ----------------------------------------------------------
360 // Sounds are resources, playback is commands the driver drains; see
361 // `audio`. Nothing here touches a device.
362
363 /// Registers a sound from its encoded file bytes (wav/ogg/mp3/flac —
364 /// the driver's backend decodes; the core only keeps the bytes).
365 pub fn add_sound(&mut self, bytes: Vec<u8>) -> crate::resources::SoundId {
366 self.session.state().resources.add_sound(bytes)
367 }
368
369 /// Forgets a sound; the driver drops its decoded copy. Playbacks
370 /// already running keep going.
371 pub fn remove_sound(&mut self, id: crate::resources::SoundId) {
372 let sess = &mut *self.session.state();
373 if sess.resources.remove_sound(id).is_some() {
374 sess.audio.unload(id);
375 }
376 }
377
378 /// Starts a playback; the returned id addresses it in `stop` /
379 /// `set_volume` / `pause` / `resume`. With a tag in the options, the
380 /// playback finishing on its own comes back as
381 /// `{kind="sound", phase="ended", playback, tag}` on the current
382 /// origin's root — the host's, or the extension's during its view.
383 pub fn play(
384 &mut self,
385 sound: crate::resources::SoundId,
386 opts: crate::audio::PlayOptions,
387 ) -> crate::audio::PlaybackId {
388 let origin = self.origin;
389 let window = self.env.window.id;
390 let sess = &mut *self.session.state();
391 // The driver's backend resolves the handle when it plays; a
392 // headless app has no driver, so a foreign handle is noticed here.
393 let _ = sess.resources.sound(sound);
394 sess.audio.play(origin, window, Key::ROOT, sound, opts)
395 }
396
397 /// Stops a playback, fading over `fade_ms` (0 = at once). A stopped
398 /// playback never reports `ended`.
399 pub fn stop(&mut self, playback: crate::audio::PlaybackId, fade_ms: f32) {
400 self.session.state().audio.stop(playback, fade_ms);
401 }
402
403 /// Sets a playback's volume (linear amplitude), tweening over `tween_ms`.
404 pub fn set_volume(&mut self, playback: crate::audio::PlaybackId, volume: f32, tween_ms: f32) {
405 self.session
406 .state()
407 .audio
408 .set_volume(playback, volume, tween_ms);
409 }
410
411 pub fn pause(&mut self, playback: crate::audio::PlaybackId, fade_ms: f32) {
412 self.session.state().audio.pause(playback, fade_ms);
413 }
414
415 pub fn resume(&mut self, playback: crate::audio::PlaybackId, fade_ms: f32) {
416 self.session.state().audio.resume(playback, fade_ms);
417 }
418
419 /// Sets the master volume (linear amplitude), tweening over `tween_ms`.
420 pub fn set_master_volume(&mut self, volume: f32, tween_ms: f32) {
421 self.session.state().audio.master_volume(volume, tween_ms);
422 }
423
424 /// An `audio` node: a playback retained by key for as long as the view
425 /// keeps declaring it — present means playing (once, or looped),
426 /// gone means stopped; `volume` / `paused` changes apply live, a
427 /// changed `src` restarts. The key is auto-assigned from the tree
428 /// position; see `audio_node_keyed` for a stable label. Draws nothing
429 /// and takes no layout space. The mount is this window's: it is
430 /// reconciled against this window's frames, and another window's
431 /// frame declaring nothing leaves it playing.
432 pub fn audio_node(&mut self, spec: crate::audio::AudioSpec) -> Key {
433 if self.tree.is_empty() {
434 return Key::ROOT;
435 }
436 let key = self.auto_key();
437 self.declare_audio(key, spec);
438 key
439 }
440
441 /// `audio_node` with a label-derived key (stable across reorders).
442 pub fn audio_node_keyed(&mut self, label: &str, spec: crate::audio::AudioSpec) -> Key {
443 if self.tree.is_empty() {
444 return Key::ROOT;
445 }
446 let key = self.child_key(label);
447 self.declare_audio(key, spec);
448 key
449 }
450
451 fn declare_audio(&mut self, key: Key, spec: crate::audio::AudioSpec) {
452 self.taint_kept("it declared audio");
453 let origin = self.origin;
454 let window = self.env.window.id;
455 let sess = &mut *self.session.state();
456 let _ = sess.resources.sound(spec.src);
457 sess.audio.declare(window, key, origin, spec);
458 }
459
460 /// The playback an `audio` node of this window holds, if it is
461 /// mounted — after the frame that declared it has finished.
462 pub fn playback_of(&self, key: Key) -> Option<crate::audio::PlaybackId> {
463 self.audio.playback_of(self.env.window.id, key)
464 }
465
466 /// Drains the audio commands queued since the last drain. Frame
467 /// drivers apply them to a real device after each input dispatch and
468 /// after each frame; headless drivers may simply never call.
469 pub fn take_audio_commands(&mut self) -> Vec<crate::audio::AudioCommand> {
470 self.audio.take_commands()
471 }
472
473 /// The driver reports a playback finished on its own (not stopped).
474 /// A tagged playback becomes an `ended` event, pending like a `resize`
475 /// (see `take_pending_events`).
476 pub fn audio_ended(&mut self, playback: crate::audio::PlaybackId) {
477 let ended = self.session.state().audio.ended(playback);
478 if let Some(ev) = ended {
479 self.pending.push(ev);
480 }
481 }
482
483 /// The driver reports it stopped a playback that was still running,
484 /// `at` seconds into the sound. When that stop was a one-shot `audio`
485 /// node going away (or changing its `src`) without
486 /// [`finish`](crate::audio::AudioSpec::finish), the node is named in a
487 /// [`TRUNCATED_PLAYBACK`](crate::diag::TRUNCATED_PLAYBACK) warning;
488 /// anything else — a stop that landed after the sound ended, an
489 /// imperative [`Self::stop`], a loop, a released playback — reports
490 /// nothing. The driver stays key-blind, as [`Self::audio_ended`] is.
491 pub fn audio_truncated(&mut self, playback: crate::audio::PlaybackId, at: f64) {
492 let cut = self.session.state().audio.truncated(playback);
493 if let Some((key, why)) = cut {
494 self.diag
495 .raise(crate::diag::truncated_playback(key, why, at));
496 }
497 }
498
499 /// The driver refused a play: the device's voices are all held, or
500 /// the sound failed to decode. The playback never started, so it can
501 /// never report `ended` — a tagged one gets
502 /// `{kind="sound", phase="refused", playback, tag}` instead, pending
503 /// like an `ended` is, so a view waiting on the sound is unstuck and
504 /// can tell the two apart. [`crate::diag::PLAYBACK_REFUSED`] is raised
505 /// on the node that asked either way, behind the usual diagnostics
506 /// gate, so an untagged refusal is not silent.
507 pub fn audio_refused(&mut self, playback: crate::audio::PlaybackId) {
508 let (event, warning) = self.session.state().audio.refused(playback);
509 if let Some(ev) = event {
510 self.pending.push(ev);
511 }
512 self.warn(warning);
513 }
514
515 /// Drops what this window shaped when a registered family's weights
516 /// changed since it last looked: text shaped with a bold
517 /// synthesized for a family that has since gained its Bold, or at a
518 /// face since removed, would otherwise stay as it was until evicted.
519 /// Shaped text and cell tables shape again on their next draw; an
520 /// editor keeps its text and takes the new weights. Rare — a face of
521 /// a family already registered coming or going — so dropping every
522 /// family's text is cheaper than knowing which.
523 pub(crate) fn sync_weights(&mut self) {
524 let sess = self.session.state();
525 if sess.weights_rev == self.weights_rev {
526 return;
527 }
528 self.weights_rev = sess.weights_rev;
529 self.text.forget_shaped();
530 self.cells.forget_shaped();
531 self.edit.reweigh(&sess.resources);
532 }
533
534 /// Re-reads the session's font family names into the mirror
535 /// `font_family` lends from, when a registration has moved since.
536 pub(crate) fn sync_font_names(&mut self) {
537 let sess = self.session.state();
538 if sess.fonts_rev == self.fonts_rev {
539 return;
540 }
541 self.fonts_rev = sess.fonts_rev;
542 self.font_names.clear();
543 for (id, entry) in sess.resources.fonts.iter() {
544 self.font_names.insert(id, entry.family.as_str().into());
545 }
546 }
547
548 /// Replaces an image's pixels in place; see `Resources::update_image`.
549 /// If the image had been drawn from the atlas
550 /// its slot is forgotten — one eviction, once — and from here on it is
551 /// texture-backed. A foreign or removed handle warns and changes
552 /// nothing, and so does a buffer that is not `width × height × 4`
553 /// bytes; the return says whether the pixels were taken.
554 pub fn update_image(
555 &mut self,
556 id: crate::resources::ImageId,
557 width: u32,
558 height: u32,
559 rgba: Vec<u8>,
560 ) -> bool {
561 let taken = self
562 .session
563 .state()
564 .resources
565 .update_image(id, width, height, rgba);
566 if taken {
567 self.atlas.evict_image(id);
568 }
569 taken
570 }
571
572 /// Replaces an image's pixels by writing them into a buffer the core
573 /// recycles; see `Resources::update_image_with`. `fill` gets
574 /// `width × height × 4` bytes holding an earlier frame's pixels and
575 /// writes every one. Where [`Self::update_image`] takes a buffer the
576 /// app allocated — and frees the one it replaces — this one stops
577 /// allocating after a stream's third update, which on Windows is most
578 /// of what a 1080p update cost. Render into `fill`'s
579 /// slice rather than into a buffer of your own to skip the copy too.
580 /// `fill` runs while the session's resources are borrowed, so it must
581 /// not reach them through another window's `Core`; that panics.
582 pub fn update_image_with(
583 &mut self,
584 id: crate::resources::ImageId,
585 width: u32,
586 height: u32,
587 fill: impl FnOnce(&mut [u8]),
588 ) -> bool {
589 let taken = self
590 .session
591 .state()
592 .resources
593 .update_image_with(id, width, height, fill);
594 if taken {
595 self.atlas.evict_image(id);
596 }
597 taken
598 }
599
600 /// The pixels behind an image handle — its size and a shared handle on
601 /// the bytes — for a host that renders the display list itself and
602 /// meets a `QuadKind::Texture` quad. `None` for a dead or foreign
603 /// handle, which is also noted as a miss.
604 pub fn image_pixels(
605 &self,
606 id: crate::resources::ImageId,
607 ) -> Option<(u32, u32, std::sync::Arc<Vec<u8>>)> {
608 // A path's own texture is this core's, not the registry's: the
609 // handle a `Texture` quad names is answered either way.
610 if let Some(px) = self.path_textures.pixels(id) {
611 return Some(px);
612 }
613 let sess = self.session.state();
614 sess.resources
615 .image(id)
616 .map(|e| (e.width, e.height, e.rgba.clone()))
617 }
618
619 /// Unregisters an image and forgets its atlas slot — every other
620 /// window's atlas forgets its own at that window's next frame — or,
621 /// for a texture-backed one, has the next display list any window
622 /// builds tell the backend to drop the texture.
623 pub fn remove_image(&mut self, id: crate::resources::ImageId) {
624 self.session.state().remove_image(id);
625 self.atlas.evict_image(id);
626 }
627
628 /// What the session removed and no display list has carried yet,
629 /// onto this frame's — a backend frees a texture or a pipeline
630 /// once, on whichever window draws next, since both are the device's
631 /// and the device is shared. And this window's own atlas slots for
632 /// images the registry no longer holds, when a removal has moved the
633 /// revision since this core last looked: the atlas is per window, so
634 /// the removing core's eviction reached only its own.
635 /// How many `path` masks this core draws from textures of their own
636 /// rather than the atlas — too big for a page, or animating (ADR
637 /// 0040, decisions 7 and 8). For a test of which road a path took.
638 pub fn path_texture_count(&self) -> usize {
639 self.path_textures.len()
640 }
641
642 pub(crate) fn sync_dropped(&mut self) {
643 // A path's own texture the last frame did not draw goes with the
644 // removed images (ADR 0040, decisions 7 and 8).
645 self.display
646 .dropped_textures
647 .extend(self.path_textures.sweep(self.frame_no));
648 let mut sess = self.session.state();
649 self.display
650 .dropped_textures
651 .append(&mut sess.dropped.images);
652 self.display
653 .dropped_fragments
654 .append(&mut sess.dropped.fragments);
655 if sess.images_rev != self.images_rev {
656 self.images_rev = sess.images_rev;
657 let live = &sess.resources.images;
658 self.atlas.retain_images(|id| live.contains_key(id));
659 }
660 }
661}
662
663/// Every family name the faces `ids` answer to, for
664/// [`Resources::reweigh`](crate::resources::Resources::reweigh).
665fn families_of(
666 db: &cosmic_text::fontdb::Database,
667 ids: &[cosmic_text::fontdb::ID],
668) -> rustc_hash::FxHashSet<String> {
669 ids.iter()
670 .filter_map(|&id| db.face(id))
671 .flat_map(|face| face.families.iter().map(|(name, _)| name.clone()))
672 .collect()
673}
674
675impl crate::session::Session {
676 /// `Core::add_system_font`'s registration, on the session every window
677 /// shares: a query against the font database's scan and an idempotent
678 /// registry entry, with no file opened. Here rather than on `Core` so
679 /// a binding's prop parser, which holds the token lookup's borrow of
680 /// the core, can name a family while it parses.
681 pub(crate) fn register_family(&self, name: &str) -> Option<crate::resources::FontId> {
682 use cosmic_text::fontdb::{Family, Query};
683 let sess = &mut *self.state();
684 sess.share_faces_once();
685 let db = sess.fonts.db();
686 let query = Query {
687 families: &[Family::Name(name)],
688 ..Default::default()
689 };
690 let id = db.query(&query)?;
691 // The canonical spelling, so the style matches the way fontdb does.
692 let family = db.face(id)?.families.first()?.0.clone();
693 if let Some((id, _)) = sess
694 .resources
695 .fonts
696 .iter()
697 .find(|(_, f)| f.faces.is_empty() && f.family == family)
698 {
699 return Some(id);
700 }
701 sess.fonts_rev += 1;
702 let touched = std::iter::once(family.clone()).collect();
703 let id = sess.resources.add_font(family, Vec::new());
704 if sess.resources.reweigh(sess.fonts.db(), &touched, Some(id)) {
705 sess.weights_rev += 1;
706 }
707 Some(id)
708 }
709}
710
711impl crate::session::SessionState {
712 /// [`share_faces`], the first time an app names a family, unless a
713 /// load has shared them already, and never again.
714 pub(crate) fn share_faces_once(&mut self) {
715 if !self.faces_shared {
716 self.share_loaded_faces();
717 }
718 }
719
720 /// [`share_faces`] after a load, so the faces it added are shared with
721 /// the rest. Sharing before the load, as DX24 first did,
722 /// left every face a file brought in reading its file again each time
723 /// a text shaped in a new family, weight or style: kawoosh loads 167
724 /// files it ships, and each new family cost a frame ~5 ms in opens.
725 /// The walk maps only what is unshared, so a load after the first
726 /// maps its own faces and nothing else. Not for `register_family`,
727 /// which runs every frame a view names a family: `db_mut` empties
728 /// cosmic-text's match cache.
729 pub(crate) fn share_loaded_faces(&mut self) {
730 self.faces_shared = true;
731 share_faces(self.fonts.db_mut());
732 }
733
734 /// Names the family `Mono` shapes as while the app names fallbacks
735 /// (`Resources::mono`, backlog RG118): the face the generic monospace
736 /// family is pinned to, by name, so the app's list is asked straight
737 /// after it rather than after every monospaced face. None with no
738 /// list, or where no installed face is of the pinned name (cosmic-text's
739 /// own default on a machine without it), where the generic family's
740 /// walk of the monospaced faces is the better first answer. With the
741 /// name, the weights the family's faces cover, so a single-weight face
742 /// is not passed over for bold (backlog RG123).
743 ///
744 /// Asked again wherever the faces can change under it — the list, a
745 /// rescan, and a font the app loads or removes, which can be the
746 /// pinned family's only face or a new weight of it (RG123) — and true
747 /// when the answer moved, for the caller to have every window shape
748 /// again.
749 pub(crate) fn name_mono(&mut self) -> bool {
750 let db = self.fonts.db();
751 let name = db.family_name(&cosmic_text::Family::Monospace);
752 let installed = db
753 .faces()
754 .any(|face| face.families.iter().any(|(f, _)| f == name));
755 let mono = (!self.resources.fallback.is_empty() && installed)
756 .then(|| (name.to_string(), crate::weights::Weights::of(db, name)));
757 if mono == self.resources.mono {
758 return false;
759 }
760 self.resources.mono = mono;
761 true
762 }
763
764 /// [`Self::name_mono`] after a font the app loaded or removed, with
765 /// every window shaping again when `Mono` moved.
766 fn repin_mono(&mut self) {
767 if self.name_mono() {
768 self.weights_rev += 1;
769 }
770 }
771
772 /// `Core::reload_system_fonts`' half on the session: the faces the
773 /// scan this session holds had and `fresh` has not leave the database,
774 /// those `fresh` has and it had not are loaded, and the rest stay
775 /// where they are, handles and all — a new database would hand out
776 /// fresh ids, and every window's atlas, raster axes and shaped text
777 /// key glyphs by id. Returns how many faces came and went.
778 pub(crate) fn apply_system_fonts(
779 &mut self,
780 fresh: std::sync::Arc<crate::text::SystemFonts>,
781 ) -> usize {
782 use cosmic_text::fontdb::{ID, Source};
783 let old = std::mem::replace(&mut self.system, fresh.clone());
784 let gone: rustc_hash::FxHashSet<_> = old.faces.difference(&fresh.faces).collect();
785 let came: Vec<_> = fresh.faces.difference(&old.faces).collect();
786 if gone.is_empty() && came.is_empty() {
787 return 0;
788 }
789 let db = self.fonts.db_mut();
790 let leaving: Vec<ID> = db
791 .faces()
792 .filter(|face| crate::text::face_file(face).is_some_and(|key| gone.contains(&key)))
793 .map(|face| face.id)
794 .collect();
795 let mut touched = families_of(db, &leaving);
796 for &id in &leaving {
797 db.remove_face(id);
798 }
799 // What came, a file at a time: fontdb loads every face of a file,
800 // and only the ones the scan kept (measurable, and not already
801 // here through a sibling that stayed) are wanted.
802 let mut by_file = rustc_hash::FxHashMap::<&std::path::Path, Vec<u32>>::default();
803 for (path, index) in &came {
804 by_file.entry(path.as_path()).or_default().push(*index);
805 }
806 let mut arrived = Vec::new();
807 for (path, indices) in by_file {
808 for id in db.load_font_source(Source::File(path.to_path_buf())) {
809 let wanted = db
810 .face(id)
811 .is_some_and(|face| indices.contains(&face.index));
812 if wanted {
813 arrived.push(id);
814 } else {
815 db.remove_face(id);
816 }
817 }
818 }
819 touched.extend(families_of(db, &arrived));
820 crate::text::pin_default_families(db);
821 // cosmic-text works out its monospaced faces, and which scripts
822 // each covers, when the font system is built; rebuilt over the
823 // same database, the ids stay and the lists are new.
824 let fonts = std::mem::replace(
825 &mut self.fonts,
826 cosmic_text::FontSystem::new_with_locale_and_db(String::new(), Default::default()),
827 );
828 let (locale, db) = fonts.into_locale_and_db();
829 self.fonts = crate::text::font_system_over(locale, db, &self.resources.fallback);
830 if self.faces_shared {
831 share_faces(self.fonts.db_mut());
832 }
833 // The scan can pin `Mono` to another face.
834 self.name_mono();
835 self.resources.reweigh(self.fonts.db(), &touched, None);
836 // Every window shapes again, whatever reweigh found: a family no
837 // one registered can still be what fallback picks.
838 self.fonts_rev += 1;
839 self.weights_rev += 1;
840 self.system_fonts_rev += 1;
841 leaving.len() + arrived.len()
842 }
843}
844
845/// Maps every file-backed face in the database once and shares the
846/// mapping, as cosmic-text does for each face it loads.
847///
848/// The first time a text shapes in a family (a weight, a style) it has not
849/// shaped in, cosmic-text ranks every face in the database against it, and
850/// for each face of another weight it reads that face's `wght` axis — with
851/// the face unshared, opening and mapping its file for that one read. On a
852/// Mac's 1,311 faces that was 9.7 ms a new family, in release; kawoosh's
853/// fonts pane, drawing each of 613 families in itself, warmed them ahead of
854/// time to keep it off the frames. Shared, the read is a slice of a mapping
855/// already made: 0.42 ms a family, for 30 ms once. Done on the first font
856/// an app registers — a family by name, bytes, a file or a folder — where
857/// the per-family cost starts to add up, and after every load from then on
858/// so a loaded file's own faces are shared too; an app on the stock
859/// three pays what it always did.
860///
861/// The mapping is what fontdb's `make_shared_face_data` documents as
862/// unsafe: a font file another process rewrites while it is mapped can
863/// show the change, and may crash the read. cosmic-text already takes that
864/// risk for every face it shapes with; this takes it for every installed
865/// face, which on a desktop are the system's and the user's fonts.
866pub(crate) fn share_faces(db: &mut cosmic_text::fontdb::Database) -> usize {
867 use cosmic_text::fontdb::Source;
868 let ids: Vec<_> = db
869 .faces()
870 .filter(|f| matches!(f.source, Source::File(_)))
871 .map(|f| f.id)
872 .collect();
873 let mut shared = 0;
874 for id in ids {
875 // A face whose file was shared through a sibling face is skipped by
876 // fontdb itself: `make_shared_face_data` updates every face of the
877 // file at once and answers the existing mapping after that.
878 // SAFETY: see the function's doc — the mapping cosmic-text makes
879 // for every face it loads, made for the rest.
880 if unsafe { db.make_shared_face_data(id) }.is_some() {
881 shared += 1;
882 }
883 }
884 shared
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890
891 /// The atlas's kept page lives for the frame it was kept for and no
892 /// longer: `finish` drops it, where the next `begin_frame` did — on a
893 /// window that goes idle after emptying a 4096 page, never.
894 #[test]
895 fn the_emptied_atlas_page_is_dropped_when_its_frame_finishes() {
896 let frame = |core: &mut Core| {
897 let mut ui = core.frame(crate::Size::new(200.0, 100.0), 1.0);
898 ui.text("kept for a frame", crate::TextStyle::new(14.0));
899 ui.finish();
900 };
901 let mut core = Core::new();
902 frame(&mut core);
903 core.atlas.reset_next_frame();
904 core.begin_frame(crate::Size::new(200.0, 100.0), 1.0);
905 assert!(
906 core.atlas.keeps_prev(),
907 "the frame that emptied it keeps it"
908 );
909 core.finish_frame();
910 assert!(!core.atlas.keeps_prev());
911 }
912
913 /// `reload_system_fonts`' session half, against a scan made up for
914 /// the test (installing a font on the machine running it is not the
915 /// test's to do): the session's own scan less one installed face, plus
916 /// a font file written for it. The new face is found by name, the
917 /// gone one is gone, one that stayed keeps its id, every window is
918 /// told to shape again and hears one `fonts` event, and the same scan
919 /// a second time changes nothing.
920 #[test]
921 fn a_rescan_brings_in_what_came_drops_what_went_and_keeps_the_rest() {
922 use crate::text::{SystemFonts, face_file};
923 let mut core = Core::new();
924 let held = core.session.state().system.clone();
925 let dir = std::env::temp_dir().join(format!("kui-rescan-{}", std::process::id()));
926 std::fs::create_dir_all(&dir).unwrap();
927 let installed = dir.join("rescan.ttf");
928 std::fs::write(
929 &installed,
930 crate::testing::font_face("Kui Rescan Face", 400, false, false),
931 )
932 .unwrap();
933
934 // A file's face is uninstalled whole: one index of a file can be
935 // several faces (Ubuntu's variable `Ubuntu[wdth,wght].ttf` is two
936 // at index 0), and a scan keys on the file, so removing one id
937 // would leave the file installed. The face that stays is another
938 // file's.
939 let mut db = held.db().clone();
940 let uninstalled = db.faces().find_map(face_file);
941 let leaving: Vec<_> = db
942 .faces()
943 .filter(|f| uninstalled.is_some() && face_file(f) == uninstalled)
944 .map(|f| f.id)
945 .collect();
946 let stays = db
947 .faces()
948 .filter_map(|f| Some((f.id, face_file(f)?)))
949 .find(|(_, key)| Some(key) != uninstalled.as_ref());
950 for &id in &leaving {
951 db.remove_face(id);
952 }
953 db.load_font_file(&installed).unwrap();
954 let fresh = std::sync::Arc::new(SystemFonts::from_db(held.locale().into(), db));
955
956 let framed = |core: &mut Core| {
957 core.frame(crate::Size::new(100.0, 100.0), 1.0).finish();
958 let evs = core.take_pending_events();
959 evs.iter()
960 .filter_map(|e| e.kind().map(str::to_owned))
961 .collect::<Vec<_>>()
962 };
963 let mut other = Core::new_in(&core.session);
964 assert!(
965 framed(&mut core).is_empty(),
966 "the first frame establishes the set"
967 );
968 assert!(framed(&mut other).is_empty());
969 let face_of = |core: &Core, key: &(std::path::PathBuf, u32)| {
970 let sess = core.session.state();
971 sess.fonts
972 .db()
973 .faces()
974 .find(|f| face_file(f).as_ref() == Some(key))
975 .map(|f| f.id)
976 };
977 let weights = core.session.state().weights_rev;
978 let changed = core.session.state().apply_system_fonts(fresh.clone());
979 assert_eq!(changed, 1 + leaving.len());
980 assert!(
981 core.session.state().weights_rev > weights,
982 "every window shapes again"
983 );
984 assert!(
985 core.add_system_font("Kui Rescan Face").is_some(),
986 "the installed face is found"
987 );
988 if let Some(key) = &uninstalled {
989 assert_eq!(face_of(&core, key), None, "the uninstalled face is gone");
990 }
991 if let Some((id, key)) = &stays {
992 assert_eq!(
993 face_of(&core, key),
994 Some(*id),
995 "a face that stayed keeps its id"
996 );
997 }
998 assert_eq!(framed(&mut core), ["fonts"], "one event, on the root");
999 assert_eq!(
1000 framed(&mut other),
1001 ["fonts"],
1002 "and one in every window of the session"
1003 );
1004 assert!(framed(&mut core).is_empty(), "once");
1005 assert_eq!(core.session.state().apply_system_fonts(fresh), 0);
1006 assert!(
1007 framed(&mut core).is_empty(),
1008 "a scan that found nothing new is no event"
1009 );
1010 let _ = std::fs::remove_dir_all(&dir);
1011 }
1012
1013 /// The real rescan: with nothing installed or removed since the
1014 /// session's scan, nothing changes and no frame is asked for — and a
1015 /// session made after it starts from it.
1016 #[test]
1017 fn a_rescan_of_an_unchanged_system_changes_nothing() {
1018 let mut core = Core::new();
1019 core.frame(crate::Size::new(100.0, 100.0), 1.0).finish();
1020 core.take_pending_events();
1021 assert_eq!(core.reload_system_fonts(), 0);
1022 // Nor does a font the app loads itself raise a `fonts` event.
1023 core.add_font_data(crate::testing::font_face(
1024 "Kui Rescan App",
1025 400,
1026 false,
1027 false,
1028 ))
1029 .expect("the app's own font loads");
1030 core.frame(crate::Size::new(100.0, 100.0), 1.0).finish();
1031 assert!(
1032 !core
1033 .take_pending_events()
1034 .iter()
1035 .any(|e| e.kind() == Some("fonts"))
1036 );
1037 let after = Core::new();
1038 assert!(std::sync::Arc::ptr_eq(
1039 &after.session.state().system,
1040 &core.session.state().system
1041 ));
1042 }
1043
1044 /// DX24: naming a family shares the database's file-backed faces, once;
1045 /// before it, an app on the stock families has mapped nothing.
1046 #[test]
1047 fn the_first_named_family_shares_the_faces_once() {
1048 use cosmic_text::fontdb::Source;
1049 let mut core = Core::new();
1050 let file_backed = |core: &Core| {
1051 let sess = core.session.state();
1052 sess.fonts
1053 .db()
1054 .faces()
1055 .filter(|f| matches!(f.source, Source::File(_)))
1056 .count()
1057 };
1058 let before = file_backed(&core);
1059 assert!(!core.session.state().faces_shared);
1060 let name = core.system_fonts().into_iter().map(|f| f.family).next();
1061 let Some(name) = name else {
1062 return; // a machine with no installed fonts has nothing to share
1063 };
1064 core.add_system_font(&name);
1065 assert!(core.session.state().faces_shared);
1066 assert_eq!(
1067 file_backed(&core),
1068 0,
1069 "all {before} file-backed faces shared"
1070 );
1071 // A second name does not walk them again.
1072 assert_eq!(share_faces(core.session.state().fonts.db_mut()), 0);
1073 }
1074
1075 /// DX25: a file loaded by path or in a folder is shared with the rest,
1076 /// its own faces too, whether it is the first font or a later one.
1077 /// Sharing before the load left them reading their file each time a
1078 /// text shaped in a new family.
1079 #[test]
1080 fn a_loaded_file_is_shared_too() {
1081 use cosmic_text::fontdb::Source;
1082 let file_backed = |core: &Core| {
1083 let sess = core.session.state();
1084 sess.fonts
1085 .db()
1086 .faces()
1087 .filter(|f| matches!(f.source, Source::File(_)))
1088 .count()
1089 };
1090 let dir = std::env::temp_dir().join(format!("kui-dx25-{}", std::process::id()));
1091 let (one, two) = (dir.join("one"), dir.join("two"));
1092 for d in [&one, &two] {
1093 std::fs::create_dir_all(d).unwrap();
1094 std::fs::write(d.join("face.ttf"), crate::testing::liga_font()).unwrap();
1095 }
1096
1097 let mut core = Core::new();
1098 core.load_font_file(one.join("face.ttf"))
1099 .expect("the first font");
1100 assert_eq!(file_backed(&core), 0, "the first file's faces shared");
1101 core.load_font_file(two.join("face.ttf"))
1102 .expect("a later font");
1103 assert_eq!(file_backed(&core), 0, "a later file's faces shared");
1104
1105 let mut fresh = Core::new();
1106 assert!(fresh.load_fonts_dir(&one) >= 1);
1107 assert!(fresh.session.state().faces_shared);
1108 assert_eq!(file_backed(&fresh), 0, "a folder's faces shared");
1109 std::fs::remove_dir_all(&dir).ok();
1110 }
1111
1112 /// A session whose generic monospace family is pinned to `name`, as
1113 /// the scan pins it to the machine's (`pin_default_families`); a test
1114 /// cannot install a face, so the face is one the app loads.
1115 fn pinned_to(name: &str) -> Core {
1116 let core = Core::new();
1117 core.session
1118 .state()
1119 .fonts
1120 .db_mut()
1121 .set_monospace_family(name);
1122 core
1123 }
1124
1125 /// How far apart two `M`s are drawn in `Mono` at 20 px, bold or not:
1126 /// 10 px in the fixture's face, which no installed face's is.
1127 fn mono_m_apart(core: &mut Core, bold: bool) -> Option<f32> {
1128 let span = crate::text::Span {
1129 bold,
1130 ..crate::text::Span::new("MM")
1131 };
1132 let mut ui = core.frame(crate::Size::new(400.0, 100.0), 1.0);
1133 ui.rich_text(&[span], crate::TextStyle::new(20.0).mono());
1134 ui.finish();
1135 let (dl, _) = core.output();
1136 let xs: Vec<f32> = dl
1137 .quads
1138 .iter()
1139 .filter(|q| q.kind != crate::QuadKind::Solid)
1140 .map(|q| q.rect.x)
1141 .collect();
1142 xs.get(1).zip(xs.first()).map(|(b, a)| b - a)
1143 }
1144
1145 /// A pinned `Mono` of a single weight stays in its family for bold
1146 /// (backlog RG123). By name cosmic-text takes a face of the family only
1147 /// at a weight it has, and asked at 700 a regular-only family was
1148 /// passed over for the proportional lists: the pinned name is asked at
1149 /// the weights its faces cover, as a registered family is.
1150 #[test]
1151 fn a_single_weight_pinned_mono_stays_itself_in_bold() {
1152 use crate::testing::{font_face, han_only_face};
1153 let mut core = pinned_to("Kui RG123 Mono");
1154 core.add_font_data(font_face("Kui RG123 Mono", 400, false, true))
1155 .expect("the fixture registers");
1156 // A list that has no `M`, so it cannot stand in for the pinned face.
1157 let han = core
1158 .add_font_data(han_only_face("Kui RG123 Han"))
1159 .expect("a face with 字");
1160 core.set_fallback_fonts(&[han]);
1161 assert_eq!(
1162 core.session
1163 .state()
1164 .resources
1165 .mono
1166 .as_ref()
1167 .map(|m| m.0.as_str()),
1168 Some("Kui RG123 Mono"),
1169 "Mono is pinned by name"
1170 );
1171 assert_eq!(mono_m_apart(&mut core, false), Some(10.0), "regular");
1172 assert_eq!(mono_m_apart(&mut core, true), Some(10.0), "and bold");
1173 }
1174
1175 /// The pin follows the faces the app loads and removes (backlog
1176 /// RG123): a pinned family whose only face the app removes leaves
1177 /// `Mono` generic, not a name nothing answers to, and one that arrives
1178 /// after the list was set pins it. Each move has every window shape
1179 /// again.
1180 #[test]
1181 fn the_mono_pin_follows_the_faces_the_app_loads_and_removes() {
1182 use crate::testing::{font_face, han_only_face};
1183 let mut core = pinned_to("Kui RG123 Late Mono");
1184 let han = core
1185 .add_font_data(han_only_face("Kui RG123 Late Han"))
1186 .expect("a face with 字");
1187 core.set_fallback_fonts(&[han]);
1188 let pinned = |core: &Core| {
1189 core.session
1190 .state()
1191 .resources
1192 .mono
1193 .as_ref()
1194 .map(|m| m.0.clone())
1195 };
1196 assert_eq!(pinned(&core), None, "no face of the name yet");
1197 let weights = core.session.state().weights_rev;
1198 let mono = core
1199 .add_font_data(font_face("Kui RG123 Late Mono", 400, false, true))
1200 .expect("the fixture registers");
1201 assert_eq!(
1202 pinned(&core).as_deref(),
1203 Some("Kui RG123 Late Mono"),
1204 "the face loaded after the list pins it"
1205 );
1206 assert!(
1207 core.session.state().weights_rev > weights,
1208 "and shapes again"
1209 );
1210 assert_eq!(mono_m_apart(&mut core, false), Some(10.0));
1211 let weights = core.session.state().weights_rev;
1212 core.remove_font(mono);
1213 assert_eq!(pinned(&core), None, "its only face gone, Mono is generic");
1214 assert!(
1215 core.session.state().weights_rev > weights,
1216 "and shapes again"
1217 );
1218 assert_eq!(
1219 core.session
1220 .state()
1221 .resources
1222 .family_of(crate::FontFamily::Mono),
1223 cosmic_text::Family::Monospace
1224 );
1225 }
1226}