Skip to main content

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        let origin = self.origin;
453        let window = self.env.window.id;
454        let sess = &mut *self.session.state();
455        let _ = sess.resources.sound(spec.src);
456        sess.audio.declare(window, key, origin, spec);
457    }
458
459    /// The playback an `audio` node of this window holds, if it is
460    /// mounted — after the frame that declared it has finished.
461    pub fn playback_of(&self, key: Key) -> Option<crate::audio::PlaybackId> {
462        self.audio.playback_of(self.env.window.id, key)
463    }
464
465    /// Drains the audio commands queued since the last drain. Frame
466    /// drivers apply them to a real device after each input dispatch and
467    /// after each frame; headless drivers may simply never call.
468    pub fn take_audio_commands(&mut self) -> Vec<crate::audio::AudioCommand> {
469        self.audio.take_commands()
470    }
471
472    /// The driver reports a playback finished on its own (not stopped).
473    /// A tagged playback becomes an `ended` event, pending like a `resize`
474    /// (see `take_pending_events`).
475    pub fn audio_ended(&mut self, playback: crate::audio::PlaybackId) {
476        let ended = self.session.state().audio.ended(playback);
477        if let Some(ev) = ended {
478            self.pending.push(ev);
479        }
480    }
481
482    /// The driver reports it stopped a playback that was still running,
483    /// `at` seconds into the sound. When that stop was a one-shot `audio`
484    /// node going away (or changing its `src`) without
485    /// [`finish`](crate::audio::AudioSpec::finish), the node is named in a
486    /// [`TRUNCATED_PLAYBACK`](crate::diag::TRUNCATED_PLAYBACK) warning;
487    /// anything else — a stop that landed after the sound ended, an
488    /// imperative [`Self::stop`], a loop, a released playback — reports
489    /// nothing. The driver stays key-blind, as [`Self::audio_ended`] is.
490    pub fn audio_truncated(&mut self, playback: crate::audio::PlaybackId, at: f64) {
491        let cut = self.session.state().audio.truncated(playback);
492        if let Some((key, why)) = cut {
493            self.diag
494                .raise(crate::diag::truncated_playback(key, why, at));
495        }
496    }
497
498    /// The driver refused a play: the device's voices are all held, or
499    /// the sound failed to decode. The playback never started, so it can
500    /// never report `ended` — a tagged one gets
501    /// `{kind="sound", phase="refused", playback, tag}` instead, pending
502    /// like an `ended` is, so a view waiting on the sound is unstuck and
503    /// can tell the two apart. [`crate::diag::PLAYBACK_REFUSED`] is raised
504    /// on the node that asked either way, behind the usual diagnostics
505    /// gate, so an untagged refusal is not silent.
506    pub fn audio_refused(&mut self, playback: crate::audio::PlaybackId) {
507        let (event, warning) = self.session.state().audio.refused(playback);
508        if let Some(ev) = event {
509            self.pending.push(ev);
510        }
511        self.warn(warning);
512    }
513
514    /// Drops what this window shaped when a registered family's weights
515    /// changed since it last looked: text shaped with a bold
516    /// synthesized for a family that has since gained its Bold, or at a
517    /// face since removed, would otherwise stay as it was until evicted.
518    /// Shaped text and cell tables shape again on their next draw; an
519    /// editor keeps its text and takes the new weights. Rare — a face of
520    /// a family already registered coming or going — so dropping every
521    /// family's text is cheaper than knowing which.
522    pub(crate) fn sync_weights(&mut self) {
523        let sess = self.session.state();
524        if sess.weights_rev == self.weights_rev {
525            return;
526        }
527        self.weights_rev = sess.weights_rev;
528        self.text.forget_shaped();
529        self.cells.forget_shaped();
530        self.edit.reweigh(&sess.resources);
531    }
532
533    /// Re-reads the session's font family names into the mirror
534    /// `font_family` lends from, when a registration has moved since.
535    pub(crate) fn sync_font_names(&mut self) {
536        let sess = self.session.state();
537        if sess.fonts_rev == self.fonts_rev {
538            return;
539        }
540        self.fonts_rev = sess.fonts_rev;
541        self.font_names.clear();
542        for (id, entry) in sess.resources.fonts.iter() {
543            self.font_names.insert(id, entry.family.as_str().into());
544        }
545    }
546
547    /// Replaces an image's pixels in place; see `Resources::update_image`.
548    /// If the image had been drawn from the atlas
549    /// its slot is forgotten — one eviction, once — and from here on it is
550    /// texture-backed. A foreign or removed handle warns and changes
551    /// nothing, and so does a buffer that is not `width × height × 4`
552    /// bytes; the return says whether the pixels were taken.
553    pub fn update_image(
554        &mut self,
555        id: crate::resources::ImageId,
556        width: u32,
557        height: u32,
558        rgba: Vec<u8>,
559    ) -> bool {
560        let taken = self
561            .session
562            .state()
563            .resources
564            .update_image(id, width, height, rgba);
565        if taken {
566            self.atlas.evict_image(id);
567        }
568        taken
569    }
570
571    /// Replaces an image's pixels by writing them into a buffer the core
572    /// recycles; see `Resources::update_image_with`. `fill` gets
573    /// `width × height × 4` bytes holding an earlier frame's pixels and
574    /// writes every one. Where [`Self::update_image`] takes a buffer the
575    /// app allocated — and frees the one it replaces — this one stops
576    /// allocating after a stream's third update, which on Windows is most
577    /// of what a 1080p update cost. Render into `fill`'s
578    /// slice rather than into a buffer of your own to skip the copy too.
579    /// `fill` runs while the session's resources are borrowed, so it must
580    /// not reach them through another window's `Core`; that panics.
581    pub fn update_image_with(
582        &mut self,
583        id: crate::resources::ImageId,
584        width: u32,
585        height: u32,
586        fill: impl FnOnce(&mut [u8]),
587    ) -> bool {
588        let taken = self
589            .session
590            .state()
591            .resources
592            .update_image_with(id, width, height, fill);
593        if taken {
594            self.atlas.evict_image(id);
595        }
596        taken
597    }
598
599    /// The pixels behind an image handle — its size and a shared handle on
600    /// the bytes — for a host that renders the display list itself and
601    /// meets a `QuadKind::Texture` quad. `None` for a dead or foreign
602    /// handle, which is also noted as a miss.
603    pub fn image_pixels(
604        &self,
605        id: crate::resources::ImageId,
606    ) -> Option<(u32, u32, std::sync::Arc<Vec<u8>>)> {
607        // A path's own texture is this core's, not the registry's: the
608        // handle a `Texture` quad names is answered either way.
609        if let Some(px) = self.path_textures.pixels(id) {
610            return Some(px);
611        }
612        let sess = self.session.state();
613        sess.resources
614            .image(id)
615            .map(|e| (e.width, e.height, e.rgba.clone()))
616    }
617
618    /// Unregisters an image and forgets its atlas slot — every other
619    /// window's atlas forgets its own at that window's next frame — or,
620    /// for a texture-backed one, has the next display list any window
621    /// builds tell the backend to drop the texture.
622    pub fn remove_image(&mut self, id: crate::resources::ImageId) {
623        self.session.state().remove_image(id);
624        self.atlas.evict_image(id);
625    }
626
627    /// What the session removed and no display list has carried yet,
628    /// onto this frame's — a backend frees a texture or a pipeline
629    /// once, on whichever window draws next, since both are the device's
630    /// and the device is shared. And this window's own atlas slots for
631    /// images the registry no longer holds, when a removal has moved the
632    /// revision since this core last looked: the atlas is per window, so
633    /// the removing core's eviction reached only its own.
634    /// How many `path` masks this core draws from textures of their own
635    /// rather than the atlas — too big for a page, or animating (ADR
636    /// 0040, decisions 7 and 8). For a test of which road a path took.
637    pub fn path_texture_count(&self) -> usize {
638        self.path_textures.len()
639    }
640
641    pub(crate) fn sync_dropped(&mut self) {
642        // A path's own texture the last frame did not draw goes with the
643        // removed images (ADR 0040, decisions 7 and 8).
644        self.display
645            .dropped_textures
646            .extend(self.path_textures.sweep(self.frame_no));
647        let mut sess = self.session.state();
648        self.display
649            .dropped_textures
650            .append(&mut sess.dropped.images);
651        self.display
652            .dropped_fragments
653            .append(&mut sess.dropped.fragments);
654        if sess.images_rev != self.images_rev {
655            self.images_rev = sess.images_rev;
656            let live = &sess.resources.images;
657            self.atlas.retain_images(|id| live.contains_key(id));
658        }
659    }
660}
661
662/// Every family name the faces `ids` answer to, for
663/// [`Resources::reweigh`](crate::resources::Resources::reweigh).
664fn families_of(
665    db: &cosmic_text::fontdb::Database,
666    ids: &[cosmic_text::fontdb::ID],
667) -> rustc_hash::FxHashSet<String> {
668    ids.iter()
669        .filter_map(|&id| db.face(id))
670        .flat_map(|face| face.families.iter().map(|(name, _)| name.clone()))
671        .collect()
672}
673
674impl crate::session::Session {
675    /// `Core::add_system_font`'s registration, on the session every window
676    /// shares: a query against the font database's scan and an idempotent
677    /// registry entry, with no file opened. Here rather than on `Core` so
678    /// a binding's prop parser, which holds the token lookup's borrow of
679    /// the core, can name a family while it parses.
680    pub(crate) fn register_family(&self, name: &str) -> Option<crate::resources::FontId> {
681        use cosmic_text::fontdb::{Family, Query};
682        let sess = &mut *self.state();
683        sess.share_faces_once();
684        let db = sess.fonts.db();
685        let query = Query {
686            families: &[Family::Name(name)],
687            ..Default::default()
688        };
689        let id = db.query(&query)?;
690        // The canonical spelling, so the style matches the way fontdb does.
691        let family = db.face(id)?.families.first()?.0.clone();
692        if let Some((id, _)) = sess
693            .resources
694            .fonts
695            .iter()
696            .find(|(_, f)| f.faces.is_empty() && f.family == family)
697        {
698            return Some(id);
699        }
700        sess.fonts_rev += 1;
701        let touched = std::iter::once(family.clone()).collect();
702        let id = sess.resources.add_font(family, Vec::new());
703        if sess.resources.reweigh(sess.fonts.db(), &touched, Some(id)) {
704            sess.weights_rev += 1;
705        }
706        Some(id)
707    }
708}
709
710impl crate::session::SessionState {
711    /// [`share_faces`], the first time an app names a family, unless a
712    /// load has shared them already, and never again.
713    pub(crate) fn share_faces_once(&mut self) {
714        if !self.faces_shared {
715            self.share_loaded_faces();
716        }
717    }
718
719    /// [`share_faces`] after a load, so the faces it added are shared with
720    /// the rest. Sharing before the load, as DX24 first did,
721    /// left every face a file brought in reading its file again each time
722    /// a text shaped in a new family, weight or style: kawoosh loads 167
723    /// files it ships, and each new family cost a frame ~5 ms in opens.
724    /// The walk maps only what is unshared, so a load after the first
725    /// maps its own faces and nothing else. Not for `register_family`,
726    /// which runs every frame a view names a family: `db_mut` empties
727    /// cosmic-text's match cache.
728    pub(crate) fn share_loaded_faces(&mut self) {
729        self.faces_shared = true;
730        share_faces(self.fonts.db_mut());
731    }
732
733    /// Names the family `Mono` shapes as while the app names fallbacks
734    /// (`Resources::mono`, backlog RG118): the face the generic monospace
735    /// family is pinned to, by name, so the app's list is asked straight
736    /// after it rather than after every monospaced face. None with no
737    /// list, or where no installed face is of the pinned name (cosmic-text's
738    /// own default on a machine without it), where the generic family's
739    /// walk of the monospaced faces is the better first answer. With the
740    /// name, the weights the family's faces cover, so a single-weight face
741    /// is not passed over for bold (backlog RG123).
742    ///
743    /// Asked again wherever the faces can change under it — the list, a
744    /// rescan, and a font the app loads or removes, which can be the
745    /// pinned family's only face or a new weight of it (RG123) — and true
746    /// when the answer moved, for the caller to have every window shape
747    /// again.
748    pub(crate) fn name_mono(&mut self) -> bool {
749        let db = self.fonts.db();
750        let name = db.family_name(&cosmic_text::Family::Monospace);
751        let installed = db
752            .faces()
753            .any(|face| face.families.iter().any(|(f, _)| f == name));
754        let mono = (!self.resources.fallback.is_empty() && installed)
755            .then(|| (name.to_string(), crate::weights::Weights::of(db, name)));
756        if mono == self.resources.mono {
757            return false;
758        }
759        self.resources.mono = mono;
760        true
761    }
762
763    /// [`Self::name_mono`] after a font the app loaded or removed, with
764    /// every window shaping again when `Mono` moved.
765    fn repin_mono(&mut self) {
766        if self.name_mono() {
767            self.weights_rev += 1;
768        }
769    }
770
771    /// `Core::reload_system_fonts`' half on the session: the faces the
772    /// scan this session holds had and `fresh` has not leave the database,
773    /// those `fresh` has and it had not are loaded, and the rest stay
774    /// where they are, handles and all — a new database would hand out
775    /// fresh ids, and every window's atlas, raster axes and shaped text
776    /// key glyphs by id. Returns how many faces came and went.
777    pub(crate) fn apply_system_fonts(
778        &mut self,
779        fresh: std::sync::Arc<crate::text::SystemFonts>,
780    ) -> usize {
781        use cosmic_text::fontdb::{ID, Source};
782        let old = std::mem::replace(&mut self.system, fresh.clone());
783        let gone: rustc_hash::FxHashSet<_> = old.faces.difference(&fresh.faces).collect();
784        let came: Vec<_> = fresh.faces.difference(&old.faces).collect();
785        if gone.is_empty() && came.is_empty() {
786            return 0;
787        }
788        let db = self.fonts.db_mut();
789        let leaving: Vec<ID> = db
790            .faces()
791            .filter(|face| crate::text::face_file(face).is_some_and(|key| gone.contains(&key)))
792            .map(|face| face.id)
793            .collect();
794        let mut touched = families_of(db, &leaving);
795        for &id in &leaving {
796            db.remove_face(id);
797        }
798        // What came, a file at a time: fontdb loads every face of a file,
799        // and only the ones the scan kept (measurable, and not already
800        // here through a sibling that stayed) are wanted.
801        let mut by_file = rustc_hash::FxHashMap::<&std::path::Path, Vec<u32>>::default();
802        for (path, index) in &came {
803            by_file.entry(path.as_path()).or_default().push(*index);
804        }
805        let mut arrived = Vec::new();
806        for (path, indices) in by_file {
807            for id in db.load_font_source(Source::File(path.to_path_buf())) {
808                let wanted = db
809                    .face(id)
810                    .is_some_and(|face| indices.contains(&face.index));
811                if wanted {
812                    arrived.push(id);
813                } else {
814                    db.remove_face(id);
815                }
816            }
817        }
818        touched.extend(families_of(db, &arrived));
819        crate::text::pin_default_families(db);
820        // cosmic-text works out its monospaced faces, and which scripts
821        // each covers, when the font system is built; rebuilt over the
822        // same database, the ids stay and the lists are new.
823        let fonts = std::mem::replace(
824            &mut self.fonts,
825            cosmic_text::FontSystem::new_with_locale_and_db(String::new(), Default::default()),
826        );
827        let (locale, db) = fonts.into_locale_and_db();
828        self.fonts = crate::text::font_system_over(locale, db, &self.resources.fallback);
829        if self.faces_shared {
830            share_faces(self.fonts.db_mut());
831        }
832        // The scan can pin `Mono` to another face.
833        self.name_mono();
834        self.resources.reweigh(self.fonts.db(), &touched, None);
835        // Every window shapes again, whatever reweigh found: a family no
836        // one registered can still be what fallback picks.
837        self.fonts_rev += 1;
838        self.weights_rev += 1;
839        self.system_fonts_rev += 1;
840        leaving.len() + arrived.len()
841    }
842}
843
844/// Maps every file-backed face in the database once and shares the
845/// mapping, as cosmic-text does for each face it loads.
846///
847/// The first time a text shapes in a family (a weight, a style) it has not
848/// shaped in, cosmic-text ranks every face in the database against it, and
849/// for each face of another weight it reads that face's `wght` axis — with
850/// the face unshared, opening and mapping its file for that one read. On a
851/// Mac's 1,311 faces that was 9.7 ms a new family, in release; kawoosh's
852/// fonts pane, drawing each of 613 families in itself, warmed them ahead of
853/// time to keep it off the frames. Shared, the read is a slice of a mapping
854/// already made: 0.42 ms a family, for 30 ms once. Done on the first font
855/// an app registers — a family by name, bytes, a file or a folder — where
856/// the per-family cost starts to add up, and after every load from then on
857/// so a loaded file's own faces are shared too; an app on the stock
858/// three pays what it always did.
859///
860/// The mapping is what fontdb's `make_shared_face_data` documents as
861/// unsafe: a font file another process rewrites while it is mapped can
862/// show the change, and may crash the read. cosmic-text already takes that
863/// risk for every face it shapes with; this takes it for every installed
864/// face, which on a desktop are the system's and the user's fonts.
865pub(crate) fn share_faces(db: &mut cosmic_text::fontdb::Database) -> usize {
866    use cosmic_text::fontdb::Source;
867    let ids: Vec<_> = db
868        .faces()
869        .filter(|f| matches!(f.source, Source::File(_)))
870        .map(|f| f.id)
871        .collect();
872    let mut shared = 0;
873    for id in ids {
874        // A face whose file was shared through a sibling face is skipped by
875        // fontdb itself: `make_shared_face_data` updates every face of the
876        // file at once and answers the existing mapping after that.
877        // SAFETY: see the function's doc — the mapping cosmic-text makes
878        // for every face it loads, made for the rest.
879        if unsafe { db.make_shared_face_data(id) }.is_some() {
880            shared += 1;
881        }
882    }
883    shared
884}
885
886#[cfg(test)]
887mod tests {
888    use super::*;
889
890    /// The atlas's kept page lives for the frame it was kept for and no
891    /// longer: `finish` drops it, where the next `begin_frame` did — on a
892    /// window that goes idle after emptying a 4096 page, never.
893    #[test]
894    fn the_emptied_atlas_page_is_dropped_when_its_frame_finishes() {
895        let frame = |core: &mut Core| {
896            let mut ui = core.frame(crate::Size::new(200.0, 100.0), 1.0);
897            ui.text("kept for a frame", crate::TextStyle::new(14.0));
898            ui.finish();
899        };
900        let mut core = Core::new();
901        frame(&mut core);
902        core.atlas.reset_next_frame();
903        core.begin_frame(crate::Size::new(200.0, 100.0), 1.0);
904        assert!(
905            core.atlas.keeps_prev(),
906            "the frame that emptied it keeps it"
907        );
908        core.finish_frame();
909        assert!(!core.atlas.keeps_prev());
910    }
911
912    /// `reload_system_fonts`' session half, against a scan made up for
913    /// the test (installing a font on the machine running it is not the
914    /// test's to do): the session's own scan less one installed face, plus
915    /// a font file written for it. The new face is found by name, the
916    /// gone one is gone, one that stayed keeps its id, every window is
917    /// told to shape again and hears one `fonts` event, and the same scan
918    /// a second time changes nothing.
919    #[test]
920    fn a_rescan_brings_in_what_came_drops_what_went_and_keeps_the_rest() {
921        use crate::text::{SystemFonts, face_file};
922        let mut core = Core::new();
923        let held = core.session.state().system.clone();
924        let dir = std::env::temp_dir().join(format!("kui-rescan-{}", std::process::id()));
925        std::fs::create_dir_all(&dir).unwrap();
926        let installed = dir.join("rescan.ttf");
927        std::fs::write(
928            &installed,
929            crate::testing::font_face("Kui Rescan Face", 400, false, false),
930        )
931        .unwrap();
932
933        // A file's face is uninstalled whole: one index of a file can be
934        // several faces (Ubuntu's variable `Ubuntu[wdth,wght].ttf` is two
935        // at index 0), and a scan keys on the file, so removing one id
936        // would leave the file installed. The face that stays is another
937        // file's.
938        let mut db = held.db().clone();
939        let uninstalled = db.faces().find_map(face_file);
940        let leaving: Vec<_> = db
941            .faces()
942            .filter(|f| uninstalled.is_some() && face_file(f) == uninstalled)
943            .map(|f| f.id)
944            .collect();
945        let stays = db
946            .faces()
947            .filter_map(|f| Some((f.id, face_file(f)?)))
948            .find(|(_, key)| Some(key) != uninstalled.as_ref());
949        for &id in &leaving {
950            db.remove_face(id);
951        }
952        db.load_font_file(&installed).unwrap();
953        let fresh = std::sync::Arc::new(SystemFonts::from_db(held.locale().into(), db));
954
955        let framed = |core: &mut Core| {
956            core.frame(crate::Size::new(100.0, 100.0), 1.0).finish();
957            let evs = core.take_pending_events();
958            evs.iter()
959                .filter_map(|e| e.kind().map(str::to_owned))
960                .collect::<Vec<_>>()
961        };
962        let mut other = Core::new_in(&core.session);
963        assert!(
964            framed(&mut core).is_empty(),
965            "the first frame establishes the set"
966        );
967        assert!(framed(&mut other).is_empty());
968        let face_of = |core: &Core, key: &(std::path::PathBuf, u32)| {
969            let sess = core.session.state();
970            sess.fonts
971                .db()
972                .faces()
973                .find(|f| face_file(f).as_ref() == Some(key))
974                .map(|f| f.id)
975        };
976        let weights = core.session.state().weights_rev;
977        let changed = core.session.state().apply_system_fonts(fresh.clone());
978        assert_eq!(changed, 1 + leaving.len());
979        assert!(
980            core.session.state().weights_rev > weights,
981            "every window shapes again"
982        );
983        assert!(
984            core.add_system_font("Kui Rescan Face").is_some(),
985            "the installed face is found"
986        );
987        if let Some(key) = &uninstalled {
988            assert_eq!(face_of(&core, key), None, "the uninstalled face is gone");
989        }
990        if let Some((id, key)) = &stays {
991            assert_eq!(
992                face_of(&core, key),
993                Some(*id),
994                "a face that stayed keeps its id"
995            );
996        }
997        assert_eq!(framed(&mut core), ["fonts"], "one event, on the root");
998        assert_eq!(
999            framed(&mut other),
1000            ["fonts"],
1001            "and one in every window of the session"
1002        );
1003        assert!(framed(&mut core).is_empty(), "once");
1004        assert_eq!(core.session.state().apply_system_fonts(fresh), 0);
1005        assert!(
1006            framed(&mut core).is_empty(),
1007            "a scan that found nothing new is no event"
1008        );
1009        let _ = std::fs::remove_dir_all(&dir);
1010    }
1011
1012    /// The real rescan: with nothing installed or removed since the
1013    /// session's scan, nothing changes and no frame is asked for — and a
1014    /// session made after it starts from it.
1015    #[test]
1016    fn a_rescan_of_an_unchanged_system_changes_nothing() {
1017        let mut core = Core::new();
1018        core.frame(crate::Size::new(100.0, 100.0), 1.0).finish();
1019        core.take_pending_events();
1020        assert_eq!(core.reload_system_fonts(), 0);
1021        // Nor does a font the app loads itself raise a `fonts` event.
1022        core.add_font_data(crate::testing::font_face(
1023            "Kui Rescan App",
1024            400,
1025            false,
1026            false,
1027        ))
1028        .expect("the app's own font loads");
1029        core.frame(crate::Size::new(100.0, 100.0), 1.0).finish();
1030        assert!(
1031            !core
1032                .take_pending_events()
1033                .iter()
1034                .any(|e| e.kind() == Some("fonts"))
1035        );
1036        let after = Core::new();
1037        assert!(std::sync::Arc::ptr_eq(
1038            &after.session.state().system,
1039            &core.session.state().system
1040        ));
1041    }
1042
1043    /// DX24: naming a family shares the database's file-backed faces, once;
1044    /// before it, an app on the stock families has mapped nothing.
1045    #[test]
1046    fn the_first_named_family_shares_the_faces_once() {
1047        use cosmic_text::fontdb::Source;
1048        let mut core = Core::new();
1049        let file_backed = |core: &Core| {
1050            let sess = core.session.state();
1051            sess.fonts
1052                .db()
1053                .faces()
1054                .filter(|f| matches!(f.source, Source::File(_)))
1055                .count()
1056        };
1057        let before = file_backed(&core);
1058        assert!(!core.session.state().faces_shared);
1059        let name = core.system_fonts().into_iter().map(|f| f.family).next();
1060        let Some(name) = name else {
1061            return; // a machine with no installed fonts has nothing to share
1062        };
1063        core.add_system_font(&name);
1064        assert!(core.session.state().faces_shared);
1065        assert_eq!(
1066            file_backed(&core),
1067            0,
1068            "all {before} file-backed faces shared"
1069        );
1070        // A second name does not walk them again.
1071        assert_eq!(share_faces(core.session.state().fonts.db_mut()), 0);
1072    }
1073
1074    /// DX25: a file loaded by path or in a folder is shared with the rest,
1075    /// its own faces too, whether it is the first font or a later one.
1076    /// Sharing before the load left them reading their file each time a
1077    /// text shaped in a new family.
1078    #[test]
1079    fn a_loaded_file_is_shared_too() {
1080        use cosmic_text::fontdb::Source;
1081        let file_backed = |core: &Core| {
1082            let sess = core.session.state();
1083            sess.fonts
1084                .db()
1085                .faces()
1086                .filter(|f| matches!(f.source, Source::File(_)))
1087                .count()
1088        };
1089        let dir = std::env::temp_dir().join(format!("kui-dx25-{}", std::process::id()));
1090        let (one, two) = (dir.join("one"), dir.join("two"));
1091        for d in [&one, &two] {
1092            std::fs::create_dir_all(d).unwrap();
1093            std::fs::write(d.join("face.ttf"), crate::testing::liga_font()).unwrap();
1094        }
1095
1096        let mut core = Core::new();
1097        core.load_font_file(one.join("face.ttf"))
1098            .expect("the first font");
1099        assert_eq!(file_backed(&core), 0, "the first file's faces shared");
1100        core.load_font_file(two.join("face.ttf"))
1101            .expect("a later font");
1102        assert_eq!(file_backed(&core), 0, "a later file's faces shared");
1103
1104        let mut fresh = Core::new();
1105        assert!(fresh.load_fonts_dir(&one) >= 1);
1106        assert!(fresh.session.state().faces_shared);
1107        assert_eq!(file_backed(&fresh), 0, "a folder's faces shared");
1108        std::fs::remove_dir_all(&dir).ok();
1109    }
1110
1111    /// A session whose generic monospace family is pinned to `name`, as
1112    /// the scan pins it to the machine's (`pin_default_families`); a test
1113    /// cannot install a face, so the face is one the app loads.
1114    fn pinned_to(name: &str) -> Core {
1115        let core = Core::new();
1116        core.session
1117            .state()
1118            .fonts
1119            .db_mut()
1120            .set_monospace_family(name);
1121        core
1122    }
1123
1124    /// How far apart two `M`s are drawn in `Mono` at 20 px, bold or not:
1125    /// 10 px in the fixture's face, which no installed face's is.
1126    fn mono_m_apart(core: &mut Core, bold: bool) -> Option<f32> {
1127        let span = crate::text::Span {
1128            bold,
1129            ..crate::text::Span::new("MM")
1130        };
1131        let mut ui = core.frame(crate::Size::new(400.0, 100.0), 1.0);
1132        ui.rich_text(&[span], crate::TextStyle::new(20.0).mono());
1133        ui.finish();
1134        let (dl, _) = core.output();
1135        let xs: Vec<f32> = dl
1136            .quads
1137            .iter()
1138            .filter(|q| q.kind != crate::QuadKind::Solid)
1139            .map(|q| q.rect.x)
1140            .collect();
1141        xs.get(1).zip(xs.first()).map(|(b, a)| b - a)
1142    }
1143
1144    /// A pinned `Mono` of a single weight stays in its family for bold
1145    /// (backlog RG123). By name cosmic-text takes a face of the family only
1146    /// at a weight it has, and asked at 700 a regular-only family was
1147    /// passed over for the proportional lists: the pinned name is asked at
1148    /// the weights its faces cover, as a registered family is.
1149    #[test]
1150    fn a_single_weight_pinned_mono_stays_itself_in_bold() {
1151        use crate::testing::{font_face, han_only_face};
1152        let mut core = pinned_to("Kui RG123 Mono");
1153        core.add_font_data(font_face("Kui RG123 Mono", 400, false, true))
1154            .expect("the fixture registers");
1155        // A list that has no `M`, so it cannot stand in for the pinned face.
1156        let han = core
1157            .add_font_data(han_only_face("Kui RG123 Han"))
1158            .expect("a face with 字");
1159        core.set_fallback_fonts(&[han]);
1160        assert_eq!(
1161            core.session
1162                .state()
1163                .resources
1164                .mono
1165                .as_ref()
1166                .map(|m| m.0.as_str()),
1167            Some("Kui RG123 Mono"),
1168            "Mono is pinned by name"
1169        );
1170        assert_eq!(mono_m_apart(&mut core, false), Some(10.0), "regular");
1171        assert_eq!(mono_m_apart(&mut core, true), Some(10.0), "and bold");
1172    }
1173
1174    /// The pin follows the faces the app loads and removes (backlog
1175    /// RG123): a pinned family whose only face the app removes leaves
1176    /// `Mono` generic, not a name nothing answers to, and one that arrives
1177    /// after the list was set pins it. Each move has every window shape
1178    /// again.
1179    #[test]
1180    fn the_mono_pin_follows_the_faces_the_app_loads_and_removes() {
1181        use crate::testing::{font_face, han_only_face};
1182        let mut core = pinned_to("Kui RG123 Late Mono");
1183        let han = core
1184            .add_font_data(han_only_face("Kui RG123 Late Han"))
1185            .expect("a face with 字");
1186        core.set_fallback_fonts(&[han]);
1187        let pinned = |core: &Core| {
1188            core.session
1189                .state()
1190                .resources
1191                .mono
1192                .as_ref()
1193                .map(|m| m.0.clone())
1194        };
1195        assert_eq!(pinned(&core), None, "no face of the name yet");
1196        let weights = core.session.state().weights_rev;
1197        let mono = core
1198            .add_font_data(font_face("Kui RG123 Late Mono", 400, false, true))
1199            .expect("the fixture registers");
1200        assert_eq!(
1201            pinned(&core).as_deref(),
1202            Some("Kui RG123 Late Mono"),
1203            "the face loaded after the list pins it"
1204        );
1205        assert!(
1206            core.session.state().weights_rev > weights,
1207            "and shapes again"
1208        );
1209        assert_eq!(mono_m_apart(&mut core, false), Some(10.0));
1210        let weights = core.session.state().weights_rev;
1211        core.remove_font(mono);
1212        assert_eq!(pinned(&core), None, "its only face gone, Mono is generic");
1213        assert!(
1214            core.session.state().weights_rev > weights,
1215            "and shapes again"
1216        );
1217        assert_eq!(
1218            core.session
1219                .state()
1220                .resources
1221                .family_of(crate::FontFamily::Mono),
1222            cosmic_text::Family::Monospace
1223        );
1224    }
1225}