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