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