Skip to main content

kui_core/
session.rs

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