Skip to main content

kui_core/
session.rs

1//! [`Session`]: what a set of windows shares.
2//!
3//! kui is one [`Core`](crate::Core) per window, and everything per frame
4//! (the tree, the display list, focus, hit regions) stays with its
5//! window. What must not be duplicated lives in a `Session`: the font
6//! database every window shapes against, the resource registry behind
7//! every `FontId` / `ImageId` / `SoundId`, the audio store (the process
8//! has one device), and the declared window set. Any number of cores are
9//! constructed against one session with
10//! [`Core::new_in`](crate::Core::new_in), so a font, image or sound
11//! registered in one window draws and plays in every window.
12//! [`Core::new`](crate::Core::new) makes a private session, which is why a
13//! single-window app never has to know one exists.
14//!
15//! ```rust
16//! use kui_core::{Core, Session};
17//!
18//! let session = Session::new();
19//! let main = Core::new_in(&session);
20//! let popup = Core::new_in(&session);
21//!
22//! // Registered through one window, visible to both.
23//! let image = main.resources.add_image(2, 2, vec![255; 16]);
24//! assert_eq!(popup.resources.image_size(image), Some((2, 2)));
25//! assert!(main.session().is(&session));
26//! ```
27//!
28//! The shaped-text cache and the glyph atlas stay per window even though
29//! they look like caches: a cache entry is only valid for the atlas page
30//! it was packed against, and [`Core::output`](crate::Core::output) lends
31//! that page to the window's renderer.
32//!
33//! A handle belongs to its session. Handles are unique to the process, so
34//! a `FontId` / `ImageId` / `SoundId` handed to a core of another session
35//! is a miss, never an alias: it behaves as a removed handle does, and the
36//! next [`Core::take_warnings`](crate::Core::take_warnings) reports it as
37//! `foreign-resource`. Two `Core::new()`s are two sessions.
38//!
39//! The session is `Rc<RefCell<_>>`: a `Core` borrows it inside a method
40//! and drops the borrow before returning, and the windows of a
41//! multi-window app are driven one frame at a time on one thread, so the
42//! check never contends.
43
44use std::cell::{RefCell, RefMut};
45use std::rc::Rc;
46use std::sync::Arc;
47
48use cosmic_text::FontSystem;
49
50use crate::audio::AudioStore;
51use crate::resources::{FontId, FragmentId, ImageId, Resources, SessionId, SoundId};
52use crate::tree::OriginId;
53use crate::window::{WindowConfig, WindowId};
54
55/// The session's contents. Reached through [`Session::state`], one borrow
56/// at a time.
57pub(crate) struct SessionState {
58    /// Which session this is, for the handles the registry mints.
59    pub(crate) id: SessionId,
60    /// The font database every window in the session shapes against — a
61    /// face loaded by one window resolves for all of them.
62    pub(crate) fonts: FontSystem,
63    /// The scan of the system's fonts `fonts` holds the faces of, so a
64    /// newer scan can be applied as what came and went since
65    /// (`Core::reload_system_fonts`).
66    pub(crate) system: std::sync::Arc<crate::text::SystemFonts>,
67    /// Bumped by every rescan of the system's fonts that changed this
68    /// session's (`apply_system_fonts`) — not by a font the app loads —
69    /// so each window can report it as one `fonts` event.
70    pub(crate) system_fonts_rev: u64,
71    pub(crate) resources: Resources,
72    pub(crate) audio: AudioStore,
73    /// Bumped by every font registration and removal, so a `Core` can tell
74    /// whether its name mirror is behind without walking the slotmap.
75    pub(crate) fonts_rev: u64,
76    /// Bumped when the weights a registered family is asked at change
77    /// under it — a face of it loaded or removed — so each `Core`
78    /// drops the text it shaped at the old ones.
79    pub(crate) weights_rev: u64,
80    /// Whether the database's file-backed faces have been mapped once and
81    /// shared (`share_faces`): done on the first family an
82    /// app names, so an app that only ever shapes the stock families never
83    /// pays for it.
84    pub(crate) faces_shared: bool,
85    /// Bumped by every image removal, so a `Core` can tell whether its
86    /// atlas still holds a slot for an image the registry no longer has.
87    /// The atlas is per window, so every core re-checks its own.
88    pub(crate) images_rev: u64,
89    /// Handles removed and not yet handed to a display list: what
90    /// a backend frees on the device. Textures and fragment pipelines
91    /// live on the device every window of the session shares, so the
92    /// list is the session's and whichever core begins a frame next
93    /// carries it — a removal through a window that closes before its
94    /// next frame is not lost with the window.
95    pub(crate) dropped: Dropped,
96    /// The declared window set and the windows it has opened.
97    pub(crate) windows: WindowRegistry,
98    /// The devtools panel's state: one
99    /// panel for the session, whichever window draws it.
100    pub(crate) devtools: crate::runtime::devtools::State,
101}
102
103impl SessionState {
104    fn new() -> Self {
105        let id = SessionId::next();
106        let (fonts, system) = crate::text::new_font_system();
107        Self {
108            id,
109            fonts,
110            system,
111            system_fonts_rev: 0,
112            resources: Resources::new(id),
113            audio: AudioStore::default(),
114            fonts_rev: 0,
115            weights_rev: 0,
116            faces_shared: false,
117            images_rev: 0,
118            dropped: Dropped::default(),
119            windows: WindowRegistry::new(),
120            devtools: crate::runtime::devtools::State::default(),
121        }
122    }
123}
124
125impl SessionState {
126    /// Unregisters an image: the registry forgets it, every core's atlas
127    /// hears (through `images_rev`) and, for a texture-backed one, the
128    /// next display list any core builds tells the backend to drop the
129    /// texture. `None` for a handle the registry did not hold, which is
130    /// noted as a miss.
131    pub(crate) fn remove_image(&mut self, id: ImageId) -> Option<crate::resources::ImageEntry> {
132        let entry = self.resources.remove_image(id)?;
133        self.images_rev += 1;
134        if entry.backing == crate::resources::ImageBacking::Texture {
135            self.dropped.images.push(id);
136        }
137        Some(entry)
138    }
139
140    /// Unregisters a fragment; the next display list any core builds
141    /// tells the backend to drop its pipelines.
142    pub(crate) fn remove_fragment(&mut self, id: FragmentId) -> bool {
143        if self.resources.remove_fragment(id).is_none() {
144            return false;
145        }
146        self.dropped.fragments.push(id);
147        true
148    }
149}
150
151/// Handles the backend has yet to hear were removed; see
152/// [`SessionState::dropped`].
153#[derive(Default)]
154pub(crate) struct Dropped {
155    /// Texture-backed images (an atlas-backed one has nothing on the
156    /// device to free).
157    pub(crate) images: Vec<ImageId>,
158    pub(crate) fragments: Vec<FragmentId>,
159}
160
161/// One frame's declaration of a window, as `Core::declare_window` recorded
162/// it: the name, the config the declaration carried, and who declared it.
163#[derive(Clone, Debug, PartialEq)]
164pub(crate) struct WindowDecl {
165    pub(crate) name: Rc<str>,
166    pub(crate) config: WindowConfig,
167    pub(crate) origin: OriginId,
168    /// The same frame declared this name again with a different config.
169    /// The first declaration is the one kept; this remembers that there
170    /// was a disagreement, for the warning the opening edge raises.
171    pub(crate) conflict: bool,
172}
173
174/// A window the registry has opened: its name, its id, and whether it is
175/// still on screen. `live` goes false when the driver reports an OS close
176/// while the name is still declared — the window stays closed until the
177/// declaration lapses and starts again.
178struct WindowEntry {
179    name: Rc<str>,
180    id: WindowId,
181    live: bool,
182}
183
184/// What one diff of the declared set decided; the core turns each into a
185/// `WindowCommand`, a `{kind:"window"}` event and, sometimes, a warning.
186pub(crate) enum WindowChange {
187    Opened {
188        id: WindowId,
189        name: Rc<str>,
190        /// The window whose slot won the declaration — a popup's owner,
191        /// and the surface its anchor is measured against.
192        owner: WindowId,
193        origin: OriginId,
194        config: WindowConfig,
195        /// Two declarations of this name disagreed about the config on the
196        /// frame it opened: `duplicate-window-config`.
197        conflict: bool,
198    },
199    Closed {
200        id: WindowId,
201        name: Rc<str>,
202    },
203}
204
205/// The declared window set, across every core of the session.
206///
207/// `slots` holds each core's latest declarations, keyed by the core's
208/// window id and kept in id order — so "the lowest declaring `WindowId`
209/// wins" is the first slot that names a window, with no rule about which
210/// frame ran first. `windows` is what the diff has opened and not yet
211/// closed, seeded with the main window, which the launcher opens and
212/// nothing here ever closes.
213pub(crate) struct WindowRegistry {
214    slots: Vec<(WindowId, Vec<WindowDecl>)>,
215    windows: Vec<WindowEntry>,
216    next_id: u32,
217}
218
219/// The main window's name: what `view(model, window)` is called with in
220/// Node, and what `Core::window_name` answers for `WindowId::MAIN`.
221pub const MAIN_WINDOW_NAME: &str = "main";
222
223impl WindowRegistry {
224    fn new() -> Self {
225        Self {
226            slots: Vec::new(),
227            windows: vec![WindowEntry {
228                name: Rc::from(MAIN_WINDOW_NAME),
229                id: WindowId::MAIN,
230                live: true,
231            }],
232            next_id: 1,
233        }
234    }
235
236    /// Replaces what the core drawing `id` declared, from its latest frame.
237    pub(crate) fn set_slot(&mut self, id: WindowId, decls: &[WindowDecl]) {
238        match self.slots.binary_search_by_key(&id, |(i, _)| *i) {
239            Ok(i) if decls.is_empty() => {
240                self.slots.remove(i);
241            }
242            Ok(i) => {
243                self.slots[i].1.clear();
244                self.slots[i].1.extend_from_slice(decls);
245            }
246            Err(_) if decls.is_empty() => {}
247            Err(i) => self.slots.insert(i, (id, decls.to_vec())),
248        }
249    }
250
251    fn remove_slot(&mut self, id: WindowId) {
252        if let Ok(i) = self.slots.binary_search_by_key(&id, |(i, _)| *i) {
253            self.slots.remove(i);
254        }
255    }
256
257    /// The name the window `id` was declared under, while it is open.
258    pub(crate) fn name_of(&self, id: WindowId) -> Option<Rc<str>> {
259        self.windows
260            .iter()
261            .find(|w| w.id == id)
262            .map(|w| w.name.clone())
263    }
264
265    /// Every window on screen, main first, in the order they opened.
266    pub(crate) fn live(&self) -> Vec<(WindowId, Rc<str>)> {
267        self.windows
268            .iter()
269            .filter(|w| w.live)
270            .map(|w| (w.id, w.name.clone()))
271            .collect()
272    }
273
274    /// Whether any window was closed by the OS and is still declared —
275    /// the state `window-declared-while-closed` reports.
276    pub(crate) fn any_closed(&self) -> bool {
277        self.windows.iter().any(|w| !w.live)
278    }
279
280    /// The names among `decls` whose window the OS closed and nothing has
281    /// stopped declaring since.
282    pub(crate) fn closed_among<'a>(
283        &'a self,
284        decls: &'a [WindowDecl],
285    ) -> impl Iterator<Item = Rc<str>> + 'a {
286        decls.iter().filter_map(|d| {
287            self.windows
288                .iter()
289                .find(|w| !w.live && w.name == d.name)
290                .map(|w| w.name.clone())
291        })
292    }
293
294    /// The driver reports that the OS closed window `id`. Its declarations
295    /// leave the union with it; its name stays known, closed, until the
296    /// declaration lapses. Returns the name if the window was open; `None`
297    /// for the main window (which closing ends the app) and for an id the
298    /// diff already closed.
299    pub(crate) fn os_closed(&mut self, id: WindowId) -> Option<Rc<str>> {
300        if id == WindowId::MAIN {
301            return None;
302        }
303        let w = self.windows.iter_mut().find(|w| w.id == id && w.live)?;
304        w.live = false;
305        let name = w.name.clone();
306        self.remove_slot(id);
307        Some(name)
308    }
309
310    /// Diffs the union of every slot against the open windows.
311    ///
312    /// A live window nobody declares any more closes, and its own
313    /// declarations leave the union with it — so a window that declared a
314    /// child closes the child in the same diff, however deep. A name in
315    /// the union with no window opens, with the config from the lowest
316    /// declaring slot (main is 0, so main wins whenever it declares) and
317    /// the first declaration within that slot's frame; any other
318    /// declaration that disagrees marks the open as a conflict. A closed
319    /// name still in the union stays closed (decision 6's edge); one that
320    /// left it is forgotten, so declaring it again opens it anew.
321    pub(crate) fn diff(&mut self, out: &mut Vec<WindowChange>) {
322        loop {
323            let union = self.union();
324            let gone: Vec<usize> = (0..self.windows.len())
325                .rev()
326                .filter(|&i| {
327                    let w = &self.windows[i];
328                    w.id != WindowId::MAIN && !union.iter().any(|u| u.0 == w.name)
329                })
330                .collect();
331            if gone.is_empty() {
332                for (name, config, owner, origin, conflict) in union {
333                    if self.windows.iter().any(|w| w.name == name) {
334                        continue;
335                    }
336                    let id = WindowId(self.next_id);
337                    self.next_id += 1;
338                    self.windows.push(WindowEntry {
339                        name: name.clone(),
340                        id,
341                        live: true,
342                    });
343                    out.push(WindowChange::Opened {
344                        id,
345                        name,
346                        owner,
347                        origin,
348                        config,
349                        conflict,
350                    });
351                }
352                return;
353            }
354            for i in gone {
355                let w = self.windows.remove(i);
356                self.remove_slot(w.id);
357                if w.live {
358                    out.push(WindowChange::Closed {
359                        id: w.id,
360                        name: w.name,
361                    });
362                }
363            }
364        }
365    }
366
367    /// The declared set: one entry per name, from the lowest declaring
368    /// slot — whose window id rides along as the owner, since the slot key
369    /// *is* the window whose frame declared it — with whether any
370    /// declaration of it disagreed.
371    fn union(&self) -> Vec<(Rc<str>, WindowConfig, WindowId, OriginId, bool)> {
372        let mut union: Vec<(Rc<str>, WindowConfig, WindowId, OriginId, bool)> = Vec::new();
373        for (slot, decls) in &self.slots {
374            for d in decls {
375                match union.iter_mut().find(|u| u.0 == d.name) {
376                    Some(u) => u.4 |= u.1 != d.config,
377                    None => union.push((d.name.clone(), d.config, *slot, d.origin, d.conflict)),
378                }
379            }
380        }
381        union
382    }
383}
384
385/// The font database, registries and audio store a set of windows share.
386/// Cloning one clones the handle, not the contents: that is how a second
387/// `Core` joins (`Core::new_in(&session)`).
388#[derive(Clone)]
389pub struct Session(Rc<RefCell<SessionState>>);
390
391impl Session {
392    pub fn new() -> Self {
393        Self(Rc::new(RefCell::new(SessionState::new())))
394    }
395
396    /// Whether two handles name the same session.
397    pub fn is(&self, other: &Session) -> bool {
398        Rc::ptr_eq(&self.0, &other.0)
399    }
400
401    /// This session's process-wide id — the number a `foreign-resource`
402    /// warning names.
403    pub fn id(&self) -> SessionId {
404        self.0.borrow().id
405    }
406
407    pub(crate) fn state(&self) -> RefMut<'_, SessionState> {
408        self.0.borrow_mut()
409    }
410
411    /// [`Self::state`] for a `Drop`, which must not panic: `None` while
412    /// something else holds the state.
413    pub(crate) fn try_state(&self) -> Option<RefMut<'_, SessionState>> {
414        self.0.try_borrow_mut().ok()
415    }
416}
417
418impl Default for Session {
419    fn default() -> Self {
420        Self::new()
421    }
422}
423
424impl std::fmt::Debug for Session {
425    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426        f.write_str("Session")
427    }
428}
429
430/// A window's handle to the session's audio store — `Core`'s `audio`
431/// field. The process has one audio device, so the playbacks and the
432/// command queue are the session's and not this window's: whichever
433/// window's driver drains the queue applies every window's sounds. The
434/// `audio` nodes' mounts are per window inside it (see the module doc),
435/// which is why the readers here take one.
436#[derive(Clone)]
437pub struct SharedAudio(Session);
438
439impl SharedAudio {
440    pub(crate) fn new(session: &Session) -> Self {
441        Self(session.clone())
442    }
443
444    /// Drains the commands queued since the last drain.
445    pub fn take_commands(&self) -> Vec<crate::audio::AudioCommand> {
446        self.0.state().audio.take_commands()
447    }
448
449    /// The playback a keyed `audio` node of `window` is running, if it is
450    /// mounted. `Core::playback_of` asks for the core's own window.
451    pub fn playback_of(
452        &self,
453        window: WindowId,
454        key: crate::key::Key,
455    ) -> Option<crate::audio::PlaybackId> {
456        self.0.state().audio.playback_of(window, key)
457    }
458
459    /// Whether any `audio` node of `window` is mounted.
460    #[cfg(feature = "conformance")]
461    pub(crate) fn any_mounted(&self, window: WindowId) -> bool {
462        self.0.state().audio.any_mounted(window)
463    }
464}
465
466/// A window's handle to the session's resource registry — `Core`'s
467/// `resources` field. A font, image or sound registered through one is
468/// registered for every window in the session, and draws in all of them.
469#[derive(Clone)]
470pub struct SharedResources(Session);
471
472impl SharedResources {
473    pub(crate) fn new(session: &Session) -> Self {
474        Self(session.clone())
475    }
476
477    /// Registers an RGBA image (`width * height * 4` bytes).
478    pub fn add_image(&self, width: u32, height: u32, rgba: Vec<u8>) -> ImageId {
479        self.0.state().resources.add_image(width, height, rgba)
480    }
481
482    /// Drops an image from the registry. Every window's atlas forgets
483    /// its blit at that window's next frame, and a texture-backed one is
484    /// dropped from the device by the next frame any window draws.
485    pub fn remove_image(&self, id: ImageId) -> bool {
486        self.0.state().remove_image(id).is_some()
487    }
488
489    /// The pixel dimensions behind an image handle, if it is live.
490    pub fn image_size(&self, id: ImageId) -> Option<(u32, u32)> {
491        let state = self.0.state();
492        let e = state.resources.image(id)?;
493        Some((e.width, e.height))
494    }
495
496    /// Registers a sound from its encoded file bytes.
497    pub fn add_sound(&self, bytes: Vec<u8>) -> SoundId {
498        self.0.state().resources.add_sound(bytes)
499    }
500
501    /// Whether any sound is registered (see [`Resources::has_sounds`]).
502    pub fn has_sounds(&self) -> bool {
503        self.0.state().resources.has_sounds()
504    }
505
506    /// The encoded bytes behind a sound handle, if it is live. Cloning the
507    /// `Arc` rather than lending it is what lets the registry be shared:
508    /// the caller (an audio backend) holds the bytes, not the borrow.
509    pub fn sound(&self, id: SoundId) -> Option<Arc<[u8]>> {
510        self.0.state().resources.sound(id).cloned()
511    }
512
513    /// The registered family name behind a font handle, if it is live.
514    pub fn font_family(&self, id: FontId) -> Option<String> {
515        self.0.state().resources.font_family(id).map(str::to_owned)
516    }
517}