kui_core/slot.rs
1//! Slots: the places a host declares in its own view for an extension to
2//! fill, with parameters in and replies out.
3//!
4//! A slot is a position, not a node. `Ui::slot` (or `slot_with`) is a call
5//! the host makes anywhere among its children, and whatever fills it draws
6//! then and there, as children of the node the host is inside. The filling
7//! is done by whatever implements [`Fill`], which the frame was begun with
8//! (`Core::frame_with`); the windowed runner hands in its [`Extensions`].
9//! An app that loads no extensions never meets this module.
10//!
11//! Slot names are namespaced, and the host decides the namespace. An
12//! extension names the slots it fills in its own vocabulary (`"panel"`,
13//! `"status"`, no `/` in them). The host gives each extension a namespace
14//! when it loads it ([`Extensions::push_as`]; [`Extensions::push`] uses the
15//! extension's own name) and declares slots by their full name:
16//! `ui.slot("fs/panel")` is the `"panel"` of the extension the host calls
17//! `fs`. The same plugin loaded twice is two namespaces with two sets of
18//! slots.
19//!
20//! ```rust
21//! use kui_core::slot::{full_name, split_name, Extensions, ROOT_SLOT};
22//!
23//! assert_eq!(full_name("fs", "panel"), "fs/panel");
24//! assert_eq!(split_name("left/fs/panel"), ("left/fs", "panel"));
25//! assert_eq!(split_name("root"), ("", ROOT_SLOT));
26//!
27//! // The runner's list; `push_as(namespace, Box<dyn Extension>)` loads one.
28//! let exts = Extensions::new();
29//! assert!(exts.is_empty());
30//! ```
31//!
32//! The reserved slot name `"root"` ([`ROOT_SLOT`]) is what an extension
33//! listing no slots fills: `ns/root`, once after the host's view. A host
34//! that declares `ui.slot("ns/root")` itself moves that fill to the
35//! position it chose.
36//!
37//! An extension may host extensions of its own, by the same mechanism one
38//! level down: [`Fill::add`] loads one while a frame is being built, under
39//! a namespace of its own in the same list; a guest's `ui.slot(..)`
40//! declares a slot like anyone's, except that it cannot fill itself (a
41//! cycle is the `recursive-slot` warning and an empty position); and
42//! replies go to whoever declared the slot ([`Extensions::route`]), which
43//! for every extension the host declared is the host.
44
45use crate::input::UiEvent;
46use crate::key::Key;
47use crate::runtime::Extension;
48use crate::tree::OriginId;
49use crate::ui::Ui;
50use crate::value::Value;
51
52/// Which slot an extension is filling, handed to `Extension::view`.
53#[derive(Clone, Copy, Debug)]
54pub struct Slot<'a> {
55 /// The name in the extension's own vocabulary — what it listed in
56 /// `slots`, or `"root"` for the fill after the host's view.
57 pub name: &'a str,
58 /// The namespace the host gave this extension: what makes `name` a
59 /// full slot name (`namespace/name`), and what tells one instance of
60 /// a plugin loaded twice from the other.
61 pub namespace: &'a str,
62 /// What the host passed with `slot_with`; `Value::Null` for `slot`.
63 /// Declared every frame and never retained — read it, do not keep it.
64 pub params: &'a Value,
65 /// The slot's own key, `enclosing.str("namespace/name")`: what
66 /// `key_of` answers for the full name, and the namespace the
67 /// extension's nodes are keyed under.
68 pub key: Key,
69}
70
71/// The reserved slot name an extension listing none fills.
72pub const ROOT_SLOT: &str = "root";
73
74/// What separates a namespace from a slot name in a full name. A
75/// namespace may contain it (the host chooses namespaces, and `"left/fs"`
76/// is a fine one); a slot name an extension lists may not, so a full name
77/// splits at its last one.
78pub const NAMESPACE_SEPARATOR: char = '/';
79
80/// `Value::Null` with a `'static` address, for a slot with no params.
81pub static NULL_PARAMS: Value = Value::Null;
82
83/// How many times a reply may be answered by another reply before the
84/// rest go to the host instead ([`Extensions::route`]). Nesting is a few
85/// levels deep in anything sane; this is the bound that keeps two
86/// extensions answering each other from being an infinite loop.
87pub const MAX_REPLY_HOPS: usize = 16;
88
89impl Slot<'_> {
90 /// The `"root"` slot as a test drives an extension without a runner:
91 /// no namespace, no params, keyed under the root. The runner's own
92 /// root fill is `Slot { name: "root", namespace: <the host's> }`, keyed
93 /// under `"ns/root"`.
94 pub fn root() -> Slot<'static> {
95 Slot {
96 name: ROOT_SLOT,
97 namespace: "",
98 params: &NULL_PARAMS,
99 key: Key::ROOT.str(ROOT_SLOT),
100 }
101 }
102
103 /// The full name the host declares: `namespace/name`, or just `name`
104 /// for a slot with no namespace (`Slot::root`).
105 pub fn full_name(&self) -> String {
106 full_name(self.namespace, self.name)
107 }
108}
109
110/// `namespace/name`, or `name` alone when the namespace is empty.
111pub fn full_name(namespace: &str, name: &str) -> String {
112 if namespace.is_empty() {
113 name.to_owned()
114 } else {
115 format!("{namespace}{NAMESPACE_SEPARATOR}{name}")
116 }
117}
118
119/// The one entry in `Extension::slots` that means "every name the host
120/// declares under my namespace": for an extension that learns its slots
121/// after it loads. `fill` matches any declared name against it and
122/// `finish` has nothing to warn about for it.
123pub const ANY_SLOT: &str = "*";
124
125/// Splits a full slot name at its last separator into (namespace, name);
126/// a name with none has the empty namespace.
127pub fn split_name(full: &str) -> (&str, &str) {
128 match full.rfind(NAMESPACE_SEPARATOR) {
129 Some(i) => (&full[..i], &full[i + 1..]),
130 None => ("", full),
131 }
132}
133
134/// What fills slots: the runner's extension list, or a test's stand-in.
135/// A frame begun with `Core::frame_with` carries one; `Ui::slot` calls
136/// [`Fill::fill`] at the position the host declared, and `Ui::finish`
137/// calls [`Fill::finish`] once the host's view is done.
138pub trait Fill {
139 /// Fill the slot declared as `full_name` now, at the cursor, with
140 /// `key` its key: for the extension it names,
141 /// `ui.fill(origin, slot, |ui| ext.view(slot, ui))`, which is where
142 /// the namespace and the bound come from.
143 fn fill(&mut self, full_name: &str, key: Key, params: &Value, ui: &mut Ui<'_>);
144 /// After the host's view: fill every `ns/root` the host did not
145 /// declare, and warn about every slot an extension names that
146 /// nothing declared this frame.
147 fn finish(&mut self, ui: &mut Ui<'_>);
148 /// Loads `ext` under `namespace`, mid-frame, and answers with the
149 /// origin it got — what a guest that hosts guests of its own calls
150 /// (`env.add_extension` in Lua). It joins the same list as everyone
151 /// else, so `namespace` has to be free of the host's names too.
152 ///
153 /// The default refuses: a `Fill` that is not a list of extensions has
154 /// nowhere to put one.
155 fn add(&mut self, namespace: &str, ext: Box<dyn Extension>) -> Result<OriginId, String> {
156 let _ = ext;
157 Err(format!(
158 "cannot load `{namespace}`: this frame was begun with something other than an \
159 extension list to fill its slots"
160 ))
161 }
162}
163
164/// One loaded extension: its namespace, itself, and the answers of its
165/// that are read while it is busy.
166struct Entry {
167 namespace: String,
168 /// `None` for exactly as long as this extension is filling a slot:
169 /// `fill_one` takes it out so the list is free to fill the slots the
170 /// extension itself declares, and puts it back after. A slot of its
171 /// own that it declares while filling therefore finds nobody, which
172 /// is the cycle warning rather than a borrow panic.
173 ext: Option<Box<dyn Extension>>,
174 /// `Extension::name` and `Extension::slots` as they answered at load.
175 /// Copied because both are read while `ext` is taken — and because
176 /// every binding already reads `slots` once, at load, so there was
177 /// never a second answer to miss.
178 name: String,
179 slots: Vec<String>,
180 /// The origin that declared the slot this extension last filled:
181 /// `OriginId::HOST` for one in the host's own view, another
182 /// extension's when that extension declared it. Where its replies go
183 /// (see `route`).
184 asked_by: OriginId,
185}
186
187/// The runner's extensions, each under the namespace the host gave it.
188/// Origins are positions here: the host is `OriginId::HOST` and the
189/// extension at index `i` is `OriginId(i + 1)`, which is what an event's
190/// origin indexes back into.
191#[derive(Default)]
192pub struct Extensions {
193 list: Vec<Entry>,
194}
195
196impl Extensions {
197 pub fn new() -> Self {
198 Self::default()
199 }
200
201 /// Adds `ext` under its own name as the namespace — `import fs` binds
202 /// `fs`. `push_as` with an empty namespace, which means the same.
203 pub fn push(&mut self, ext: Box<dyn Extension>) -> Result<(), String> {
204 self.push_as(String::new(), ext)
205 }
206
207 /// Adds `ext` under `namespace` — `import fs as left`. An empty
208 /// namespace means the extension's own name: the rule is here, once,
209 /// so that a host taking the namespace from outside the program (a C
210 /// argument, a Node option, a Lua call) passes it through rather than
211 /// substituting `name()` first — three of them did, and a fourth did
212 /// not. Fails when that name is empty too, when the namespace is
213 /// already taken (two extensions cannot share one, since slot names
214 /// would collide), or when a slot the extension lists contains the
215 /// separator (a slot name is the extension's own word; the host adds
216 /// the namespace).
217 pub fn push_as(
218 &mut self,
219 namespace: impl Into<String>,
220 ext: Box<dyn Extension>,
221 ) -> Result<(), String> {
222 self.insert(namespace.into(), ext).map(|_| ())
223 }
224
225 /// `push_as` answering with the origin the extension got, which is
226 /// what [`Fill::add`] hands back to a guest that loaded one.
227 fn insert(&mut self, namespace: String, ext: Box<dyn Extension>) -> Result<OriginId, String> {
228 let namespace = if namespace.is_empty() {
229 ext.name().to_owned()
230 } else {
231 namespace
232 };
233 if namespace.is_empty() {
234 return Err(
235 "extension needs a namespace: it names itself nothing and none was given"
236 .to_owned(),
237 );
238 }
239 if let Some(taken) = self.list.iter().find(|e| e.namespace == namespace) {
240 return Err(format!(
241 "namespace `{namespace}` is already `{}`'s; give `{}` another with `push_as`",
242 taken.name,
243 ext.name()
244 ));
245 }
246 if let Some(bad) = ext
247 .slots()
248 .iter()
249 .find(|s| s.is_empty() || s.contains(NAMESPACE_SEPARATOR))
250 {
251 return Err(format!(
252 "extension `{}` lists slot {bad:?}: a slot name is one word without `{}` — the \
253 host adds the namespace",
254 ext.name(),
255 NAMESPACE_SEPARATOR
256 ));
257 }
258 let origin = OriginId(self.list.len() as u16 + 1);
259 self.list.push(Entry {
260 namespace,
261 name: ext.name().to_owned(),
262 slots: ext.slots().to_vec(),
263 ext: Some(ext),
264 asked_by: OriginId::HOST,
265 });
266 Ok(origin)
267 }
268
269 pub fn len(&self) -> usize {
270 self.list.len()
271 }
272
273 pub fn is_empty(&self) -> bool {
274 self.list.is_empty()
275 }
276
277 /// The extension an event's origin names, if any. `None` while that
278 /// extension is drawing — nothing routes events mid-frame — and for
279 /// an origin no extension has.
280 pub fn by_origin(&mut self, origin: OriginId) -> Option<&mut (dyn Extension + 'static)> {
281 let i = (origin.0 as usize).checked_sub(1)?;
282 self.list.get_mut(i)?.ext.as_deref_mut()
283 }
284
285 /// The namespace the host gave the extension at `origin`.
286 pub fn namespace_of(&self, origin: OriginId) -> Option<&str> {
287 let i = (origin.0 as usize).checked_sub(1)?;
288 self.list.get(i).map(|e| e.namespace.as_str())
289 }
290
291 /// Who declared the slot the extension at `origin` last filled, and
292 /// so where its replies go: `OriginId::HOST` unless another extension
293 /// declared it.
294 pub fn asked_by(&self, origin: OriginId) -> OriginId {
295 let Some(i) = (origin.0 as usize).checked_sub(1) else {
296 return OriginId::HOST;
297 };
298 self.list.get(i).map_or(OriginId::HOST, |e| e.asked_by)
299 }
300
301 /// Every (namespace, extension), in origin order.
302 pub fn iter(&self) -> impl Iterator<Item = (&str, &dyn Extension)> {
303 self.list
304 .iter()
305 .filter_map(|e| Some((e.namespace.as_str(), &**e.ext.as_ref()?)))
306 }
307
308 /// Delivers `events` to the extensions they came from and hands
309 /// `to_host` everything addressed to the host: the host's own events,
310 /// and the replies of every extension whose slot the host declared.
311 ///
312 /// A reply from an extension a *guest* placed goes to that guest
313 /// instead, as one more event — its `origin` still the replier's, so
314 /// the receiver knows who spoke — and whatever the guest answers
315 /// travels the same way, up to [`MAX_REPLY_HOPS`] levels. This is the
316 /// loop all four hosts route with; one that grew its own would
317 /// disagree with the others about who hears a nested plugin.
318 pub fn route(
319 &mut self,
320 events: impl IntoIterator<Item = UiEvent>,
321 mut to_host: impl FnMut(UiEvent),
322 ) {
323 // Nobody to deliver to: every event is the host's, and the walk
324 // below would only queue them up to say so. This is every host
325 // that loaded nothing, so it is the common case.
326 if self.list.is_empty() {
327 events.into_iter().for_each(to_host);
328 return;
329 }
330 let mut queue: std::collections::VecDeque<(OriginId, usize, UiEvent)> =
331 events.into_iter().map(|ev| (ev.origin, 0, ev)).collect();
332 while let Some((to, depth, ev)) = queue.pop_front() {
333 if to == OriginId::HOST {
334 to_host(ev);
335 continue;
336 }
337 let Some(ext) = self.by_origin(to) else {
338 // An origin nothing answers to: the host asked for the
339 // frame that made it, so it still hears about it.
340 to_host(ev);
341 continue;
342 };
343 let replies = ext.on_event(&ev);
344 if replies.is_empty() {
345 continue;
346 }
347 let up = self.asked_by(to);
348 for payload in replies {
349 let reply = UiEvent {
350 origin: to,
351 window: ev.window,
352 key: ev.key,
353 payload,
354 // About the same node, so from the same slot.
355 slot: ev.slot,
356 };
357 if up == OriginId::HOST || depth + 1 >= MAX_REPLY_HOPS {
358 to_host(reply);
359 } else {
360 queue.push_back((up, depth + 1, reply));
361 }
362 }
363 }
364 }
365
366 /// Fills `name` of the extension at `i` as its origin, at the cursor.
367 ///
368 /// The extension comes *out* of the list for the duration, so the
369 /// list itself stays free to fill the slots this extension declares
370 /// while it draws — and so the one slot it cannot fill is its own,
371 /// which would be the cycle.
372 fn fill_one(&mut self, i: usize, name: &str, key: Key, params: &Value, ui: &mut Ui<'_>) {
373 let ns = self.list[i].namespace.clone();
374 let ext_name = self.list[i].name.clone();
375 let Some(mut ext) = self.list[i].ext.take() else {
376 let full = full_name(&ns, name);
377 ui.core()
378 .warn(crate::diag::recursive_slot(&ext_name, &full, key));
379 return;
380 };
381 // Who put it here, and so where its replies go.
382 self.list[i].asked_by = ui.origin();
383 let slot = Slot {
384 name,
385 namespace: &ns,
386 params,
387 key,
388 };
389 let origin = OriginId(i as u16 + 1);
390 ui.fill_within(origin, &slot, self, |ui| {
391 if let Err(err) = ext.view(&slot, ui) {
392 ui.core().warn(crate::diag::extension_view_error(
393 &ext_name,
394 &slot.full_name(),
395 slot.key,
396 &err,
397 ));
398 ui.text(
399 &format!("[{ext_name}] {err}"),
400 crate::spec::TextStyle::new(13.0)
401 .color(crate::color::Color::rgb8(0xe8, 0x5d, 0x5d)),
402 );
403 }
404 });
405 self.list[i].ext = Some(ext);
406 }
407}
408
409impl TryFrom<Vec<Box<dyn Extension>>> for Extensions {
410 type Error = String;
411
412 /// Each under its own name; fails as `push` does — two extensions of
413 /// one name need `push_as`.
414 fn try_from(exts: Vec<Box<dyn Extension>>) -> Result<Self, String> {
415 let mut out = Self::new();
416 for ext in exts {
417 out.push(ext)?;
418 }
419 Ok(out)
420 }
421}
422
423/// The runner's loop, as a `Fill`. A declared name is split at its last
424/// `/` into the namespace and the extension's own slot name; the
425/// extension under that namespace fills it if it lists the name (or the
426/// name is `"root"` and it lists none). A view that errors leaves its
427/// message in the tree (red, where the fill would have been) and raises
428/// `extension-view-error`, once per extension and slot.
429impl Fill for Extensions {
430 fn fill(&mut self, full_name: &str, key: Key, params: &Value, ui: &mut Ui<'_>) {
431 let (ns, name) = split_name(full_name);
432 let Some(i) = self.list.iter().position(|e| e.namespace == ns) else {
433 return;
434 };
435 let slots = &self.list[i].slots;
436 // A wildcard takes every declared name, `root` included: under it
437 // `root` is one more name the host chose, not the auto-fill.
438 let wants = slots.iter().any(|s| s == ANY_SLOT)
439 || if name == ROOT_SLOT {
440 slots.is_empty()
441 } else {
442 slots.iter().any(|s| s == name)
443 };
444 if wants {
445 self.fill_one(i, name, key, params, ui);
446 }
447 }
448
449 fn finish(&mut self, ui: &mut Ui<'_>) {
450 // By index rather than over a snapshot: a root fill here may load
451 // an extension of its own, which joins the end of the list, and
452 // the frame it arrived on is the frame it should draw on.
453 let mut i = 0;
454 while i < self.list.len() {
455 let ns = self.list[i].namespace.clone();
456 if self.list[i].slots.is_empty() {
457 // The fill every extension got before slots existed: after
458 // the host's view, in list order.
459 let full = full_name(&ns, ROOT_SLOT);
460 if !ui.slot_declared(&full)
461 && let Some(key) = ui.core().begin_slot(&full)
462 {
463 self.fill_one(i, ROOT_SLOT, key, &NULL_PARAMS, ui);
464 }
465 } else {
466 // A wildcard lists nothing to check: whatever the host
467 // declared under the namespace was filled above.
468 for name in self.list[i].slots.clone() {
469 if name == ANY_SLOT {
470 continue;
471 }
472 let full = full_name(&ns, &name);
473 if ui.slot_declared(&full) {
474 continue;
475 }
476 let ext_name = self.list[i].name.clone();
477 ui.core()
478 .warn(crate::diag::unknown_slot(&ext_name, &ns, &name));
479 }
480 }
481 i += 1;
482 }
483 }
484
485 fn add(&mut self, namespace: &str, ext: Box<dyn Extension>) -> Result<OriginId, String> {
486 self.insert(namespace.to_owned(), ext)
487 }
488}
489
490impl<F: Fill + ?Sized> Fill for &mut F {
491 fn fill(&mut self, full_name: &str, key: Key, params: &Value, ui: &mut Ui<'_>) {
492 F::fill(&mut **self, full_name, key, params, ui);
493 }
494 fn finish(&mut self, ui: &mut Ui<'_>) {
495 F::finish(&mut **self, ui);
496 }
497 fn add(&mut self, namespace: &str, ext: Box<dyn Extension>) -> Result<OriginId, String> {
498 F::add(&mut **self, namespace, ext)
499 }
500}
501
502impl<F: Fill + ?Sized> Fill for Box<F> {
503 fn fill(&mut self, full_name: &str, key: Key, params: &Value, ui: &mut Ui<'_>) {
504 F::fill(&mut **self, full_name, key, params, ui);
505 }
506 fn finish(&mut self, ui: &mut Ui<'_>) {
507 F::finish(&mut **self, ui);
508 }
509 fn add(&mut self, namespace: &str, ext: Box<dyn Extension>) -> Result<OriginId, String> {
510 F::add(&mut **self, namespace, ext)
511 }
512}