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}