Skip to main content

kui_core/runtime/
replay.rs

1//! A slot replayed by its host (ADR 0045): the host says an extension's
2//! fill would come out as it did last frame, and the core pushes last
3//! frame's nodes again without asking the extension.
4//!
5//! `Ui::slot_kept` fills a slot as `slot_with` does and *keeps* what the
6//! fill did — every node it pushed, as the door it came through saw it,
7//! before hover, accent or a transition's easing touched the spec; the
8//! labels and data indices beside them; the slots it declared inside
9//! itself, with their params; and every fact of the frame it read while
10//! it ran (which node was hovered, where a scroller stood, the theme and
11//! the metrics, a role read by name).
12//! `Ui::slot_replay` is the host's claim that nothing *it* feeds the
13//! extension has changed. The core checks everything it can see itself
14//! — the slot's params, the facts the fill read, where the slot sits —
15//! and either pushes the kept nodes again through the same doors
16//! (`Replayed`) or runs the extension as `slot_kept` would and says
17//! why (`SlotFill`). A slot not declared for a frame forgets what it
18//! kept.
19//!
20//! What a replay re-issues is the fill's *nodes*: a nested slot is
21//! declared again and whoever fills it runs fresh, so an engine's field
22//! inside a script's pane blinks its caret while the pane around it is
23//! not rebuilt. What a replay does not re-issue is anything the fill
24//! declared of the *frame* — a title, a window, a frame asked for, a
25//! devtools tab — so a fill that declares one is kept as not replayable
26//! (it is a fill whose next frame is its own business), as is one that
27//! pushed a node through a door the journal does not know (a `cells`
28//! grid), or whose view failed. The check is by count: every node the
29//! fill pushed is either in the journal, a nested fill's, or the core's
30//! own (a hover hint), or the kept fill is refused whole.
31//!
32//! Nothing here keeps a frame: the tree is built from scratch as always,
33//! laid out and emitted as always; only the extension's `view` — and a
34//! binding's work between its tables and the tree — is skipped, and only
35//! when the host asked and the core found nothing it read moved.
36
37use std::cell::RefCell;
38
39use super::*;
40use crate::edit::EditOptions;
41use crate::fragment::FragmentRef;
42use crate::line::Stroke;
43use crate::path::{FillRule, PathOp, Turn};
44use crate::resources::{ImageId, ImageOpts};
45use crate::schema::EnvFacts;
46use crate::scroll::ScrollGeometry;
47use crate::slot::Slot;
48use crate::text::Span;
49
50/// How `Ui::slot_replay` filled a slot: replayed from what was kept, or
51/// run fresh, and why. Every answer but `Replayed` left the slot filled
52/// and kept as `slot_kept` would, so the next frame may replay it.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum SlotFill {
55    /// Last frame's nodes, pushed again; the extension was not asked.
56    Replayed,
57    /// Nothing was kept for the name: its first frame, a frame after
58    /// one that did not declare it, or one that filled it with
59    /// `slot_with`.
60    NotKept,
61    /// The params differ from the kept fill's.
62    Params,
63    /// A fact of the frame the fill read has moved since: a hover, a
64    /// focus, a scroll offset, the theme or the metrics, the clock.
65    Reads,
66    /// The kept fill cannot be replayed: it declared something of the
67    /// frame beyond its nodes, pushed through a door the journal does
68    /// not know, or its view failed.
69    NotReplayable,
70    /// The slot was declared under another parent than the kept fill's,
71    /// so its keys would not be the kept ones.
72    Moved,
73}
74
75impl SlotFill {
76    /// Whether the extension was spared.
77    pub fn replayed(self) -> bool {
78        self == Self::Replayed
79    }
80
81    /// A short name, the one the bindings hand out: `replayed`,
82    /// `not-kept`, `params`, `reads`, `not-replayable`, `moved`.
83    pub fn name(self) -> &'static str {
84        match self {
85            Self::Replayed => "replayed",
86            Self::NotKept => "not-kept",
87            Self::Params => "params",
88            Self::Reads => "reads",
89            Self::NotReplayable => "not-replayable",
90            Self::Moved => "moved",
91        }
92    }
93
94    /// A code for the C side: 0 replayed, then the others in order.
95    pub fn code(self) -> i32 {
96        self as i32
97    }
98
99    /// The inverse of [`Self::code`].
100    pub fn from_code(code: i32) -> Option<Self> {
101        Some(match code {
102            0 => Self::Replayed,
103            1 => Self::NotKept,
104            2 => Self::Params,
105            3 => Self::Reads,
106            4 => Self::NotReplayable,
107            5 => Self::Moved,
108            _ => return None,
109        })
110    }
111}
112
113/// One thing a kept fill did to the tree, as the door saw it.
114pub(crate) enum Op {
115    /// A box opened; a `Close` ends it.
116    Open {
117        key: Key,
118        spec: Box<NodeSpec>,
119    },
120    Close,
121    Text {
122        key: Key,
123        content: Box<str>,
124        style: TextStyle,
125    },
126    Rich {
127        key: Key,
128        /// Each span's text beside the span with its text left empty: a
129        /// `Span` is `Copy` but for the borrow, and this keeps every
130        /// other field it has or gains.
131        spans: Vec<(Box<str>, Span<'static>)>,
132        base: TextStyle,
133    },
134    Image {
135        key: Key,
136        id: ImageId,
137        opts: ImageOpts,
138        spec: Box<NodeSpec>,
139    },
140    /// A fragment opened; a `Close` ends it.
141    Fragment {
142        key: Key,
143        frag: FragmentRef,
144        params: Vec<f32>,
145        spec: Box<NodeSpec>,
146    },
147    Line {
148        key: Key,
149        points: Vec<Vec2>,
150        stroke: Stroke,
151        spec: Box<NodeSpec>,
152    },
153    Polygon {
154        key: Key,
155        points: Vec<Vec2>,
156        spec: Box<NodeSpec>,
157    },
158    Path {
159        key: Key,
160        ops: Vec<PathOp>,
161        rule: FillRule,
162        stroke: Option<Stroke>,
163        turn: Option<Turn>,
164        spec: Box<NodeSpec>,
165    },
166    Edit {
167        label: Box<str>,
168        initial: Box<str>,
169        opts: Box<EditOptions>,
170        spec: Box<NodeSpec>,
171    },
172    /// The node under `key` is named `label` for `key_of`.
173    Label {
174        key: Key,
175        label: Box<str>,
176    },
177    /// The node pushed last is at data index `i`.
178    Indexed(u64),
179    /// The node pushed last holds this many virtual rows (a prop list's).
180    RowCount(u64),
181    /// The node open now holds this many virtual rows (`Core::row_count`).
182    RowCountOpen(u64),
183    Hint {
184        key: Key,
185        text: Box<str>,
186    },
187    KeyFocus(Key),
188    /// A slot declared inside the fill: declared again on replay, and
189    /// filled fresh by whoever fills it.
190    Slot {
191        name: Box<str>,
192        params: Value,
193    },
194}
195
196/// A fact of the frame a kept fill read, with what it read: the replay
197/// holds only while every one reads the same.
198#[derive(Debug)]
199pub(crate) enum Read {
200    Hover(Key, bool),
201    Pressed(Key, bool),
202    Drop(Key, bool),
203    GroupHover(u64, bool),
204    GroupPressed(u64, bool),
205    Focus(Option<Key>),
206    Focused(Key, bool),
207    FocusVisible(bool),
208    Caret(bool),
209    /// The frame clock: never the same twice.
210    Clock,
211    Mods(crate::input::KeyMods),
212    Cursor(Option<Vec2>),
213    Scroll(Key, Vec2),
214    ScrollGeom(Key, Option<ScrollGeometry>),
215    Layout(Key, Option<Rect>),
216    TextHit(Key, Vec2, Option<crate::text::TextHit>),
217    EditText(Key, Option<String>),
218    /// The env reading handed out whole, less the clock and the caret
219    /// phase it carries (see [`env_same`]).
220    Env(EnvFacts),
221    /// Text was measured: the same while the fonts are.
222    Measure(u64),
223    /// The palette, read whole (`Core::theme`, and so `Ui::theme`, C's
224    /// `kui_theme`, the bindings' theme tables) or a role by name through
225    /// a token lookup: a colour drawn from it is baked into the spec the
226    /// journal keeps, so a palette that differs runs the fill again. Noted
227    /// once a fill, compared by value — a palette set again to what it
228    /// was is no change. Boxed, so the other reads stay small.
229    Theme(Box<crate::theme::Theme>),
230    /// The sizes, read as the palette is (`Core::metrics`, `kui_metrics`,
231    /// a length role by name).
232    Metrics(Box<crate::metrics::Metrics>),
233    /// Whether anyone chose the accent (`Core::has_accent`).
234    Accent(bool),
235    /// Something the core does not compare (the selection's text): a
236    /// fill that read it is run every frame.
237    Opaque,
238}
239
240/// [`Core::note_read_once`]'s bits: the palette, the sizes, the accent's
241/// being chosen.
242pub(crate) const ONCE_THEME: u8 = 1;
243pub(crate) const ONCE_METRICS: u8 = 2;
244pub(crate) const ONCE_ACCENT: u8 = 4;
245
246/// The fill being kept, while it runs.
247pub(crate) struct Recording {
248    name: String,
249    slot_key: Key,
250    origin: OriginId,
251    params: Value,
252    ops: Vec<Op>,
253    /// Through `&self` doors (`is_hovered`), so a cell.
254    reads: RefCell<Vec<Read>>,
255    /// The reads noted once a fill, as [`ONCE_THEME`] bits: the palette
256    /// and the sizes are read by every stock widget, and one note of each
257    /// is all a comparison needs.
258    once: std::cell::Cell<u8>,
259    taint: Option<&'static str>,
260    /// The tree's length when the fill began.
261    first: u32,
262    /// Node ops journaled.
263    nodes: u32,
264    /// Nodes pushed while paused: a nested fill's, a hover hint's.
265    foreign: u32,
266    paused: u32,
267    pause_from: u32,
268}
269
270/// What a slot kept, between frames.
271pub(crate) struct Kept {
272    slot_key: Key,
273    origin: OriginId,
274    params: Value,
275    ops: Vec<Op>,
276    reads: Vec<Read>,
277    taint: Option<&'static str>,
278    /// The frame it was kept or replayed in; one a frame behind at
279    /// `finish_frame` is forgotten.
280    frame: u64,
281}
282
283impl Kept {
284    /// How many nodes a replay pushes, for a reader.
285    pub(crate) fn nodes(&self) -> usize {
286        self.ops
287            .iter()
288            .filter(|op| {
289                matches!(
290                    op,
291                    Op::Open { .. }
292                        | Op::Text { .. }
293                        | Op::Rich { .. }
294                        | Op::Image { .. }
295                        | Op::Fragment { .. }
296                        | Op::Line { .. }
297                        | Op::Polygon { .. }
298                        | Op::Path { .. }
299                        | Op::Edit { .. }
300                )
301            })
302            .count()
303    }
304}
305
306/// Whether two env readings agree on everything but the clock and the
307/// caret phase. A binding that hands the reading out whole (Lua's `env`,
308/// Node's `ctx.env()`) cannot say which fields the script used, and
309/// comparing those two would make every fill stale every frame; a script
310/// that draws from them is one its host must not replay, and the
311/// explicit doors (`Ui::now`, `Ui::caret_visible`) still note the read.
312fn env_same(a: &EnvFacts, b: &EnvFacts) -> bool {
313    a.env == b.env
314        && a.viewport == b.viewport
315        && a.scale == b.scale
316        && a.focus == b.focus
317        && a.focus_visible == b.focus_visible
318        && a.region == b.region
319}
320
321impl Core {
322    /// Whether a fill is being kept right now — what every door asks
323    /// before it spends anything on the journal. One load.
324    #[inline]
325    pub(crate) fn keeping(&self) -> bool {
326        self.recording.as_ref().is_some_and(|r| r.paused == 0)
327    }
328
329    /// Journals `op` for the fill being kept, if one is and it is not
330    /// paused. Cold: the hot doors branch on [`Self::keeping`] first.
331    #[cold]
332    #[inline(never)]
333    pub(crate) fn keep_op(&mut self, op: Op) {
334        let Some(r) = self.recording.as_mut() else {
335            return;
336        };
337        if r.paused > 0 {
338            return;
339        }
340        if matches!(
341            op,
342            Op::Open { .. }
343                | Op::Text { .. }
344                | Op::Rich { .. }
345                | Op::Image { .. }
346                | Op::Fragment { .. }
347                | Op::Line { .. }
348                | Op::Polygon { .. }
349                | Op::Path { .. }
350                | Op::Edit { .. }
351        ) {
352            r.nodes += 1;
353        }
354        r.ops.push(op);
355    }
356
357    /// Notes a fact of the frame the fill being kept read.
358    #[inline]
359    pub(crate) fn note_read(&self, read: impl FnOnce() -> Read) {
360        if let Some(r) = self.recording.as_ref()
361            && r.paused == 0
362        {
363            r.reads.borrow_mut().push(read());
364        }
365    }
366
367    /// [`Self::note_read`] for a fact read whole and often — the palette,
368    /// the sizes — noted the first time the fill reads it and not again:
369    /// `bit` says which, one of the `ONCE_*` bits.
370    #[inline]
371    pub(crate) fn note_read_once(&self, bit: u8, read: impl FnOnce() -> Read) {
372        if let Some(r) = self.recording.as_ref()
373            && r.paused == 0
374            && r.once.get() & bit == 0
375        {
376            r.once.set(r.once.get() | bit);
377            r.reads.borrow_mut().push(read());
378        }
379    }
380
381    /// Marks the fill being kept as one that cannot be replayed, with
382    /// why: it declared something of the frame beyond its nodes, or
383    /// pushed through a door the journal does not know.
384    pub(crate) fn taint_kept(&mut self, why: &'static str) {
385        if let Some(r) = self.recording.as_mut()
386            && r.taint.is_none()
387        {
388            r.taint = Some(why);
389        }
390    }
391
392    /// Stops journaling until [`Self::resume_keeping`]: the nodes pushed
393    /// between are counted as not the fill's own (a nested fill, a hover
394    /// hint the core floats), so the count check at the end still
395    /// balances.
396    pub(crate) fn pause_keeping(&mut self) {
397        let len = self.tree.len() as u32;
398        if let Some(r) = self.recording.as_mut() {
399            if r.paused == 0 {
400                r.pause_from = len;
401            }
402            r.paused += 1;
403        }
404    }
405
406    pub(crate) fn resume_keeping(&mut self) {
407        let len = self.tree.len() as u32;
408        if let Some(r) = self.recording.as_mut()
409            && r.paused > 0
410        {
411            r.paused -= 1;
412            if r.paused == 0 {
413                r.foreign += len.saturating_sub(r.pause_from);
414            }
415        }
416    }
417
418    /// Asks that the next fill of the slot keyed `key` be kept under
419    /// `name`: `fill_within` begins the recording when it reaches that
420    /// slot. Nothing happens while a fill is already being kept — a kept
421    /// fill's nested slots are filled plainly.
422    pub(crate) fn keep_next_fill(&mut self, name: &str, key: Key, params: &Value) {
423        if self.recording.is_some() {
424            return;
425        }
426        self.keep_next = Some((name.to_owned(), key, params.clone()));
427    }
428
429    /// Nothing filled the slot asked for: the ask lapses.
430    pub(crate) fn forget_next_fill(&mut self) {
431        self.keep_next = None;
432    }
433
434    /// `fill_within`'s first act: the recording begins if this is the
435    /// fill asked for.
436    pub(crate) fn begin_keeping(&mut self, slot: &Slot<'_>, origin: OriginId) {
437        let Some((name, key, params)) = self.keep_next.take_if(|(_, k, _)| *k == slot.key) else {
438            return;
439        };
440        self.recording = Some(Recording {
441            name,
442            slot_key: key,
443            origin,
444            params,
445            ops: Vec::new(),
446            reads: RefCell::new(Vec::new()),
447            once: std::cell::Cell::new(0),
448            taint: None,
449            first: self.tree.len() as u32,
450            nodes: 0,
451            foreign: 0,
452            paused: 0,
453            pause_from: 0,
454        });
455    }
456
457    /// `fill_within`'s last act for the fill being kept: the journal is
458    /// checked against what the tree gained and kept for the next frame.
459    pub(crate) fn end_keeping(&mut self, slot: &Slot<'_>) {
460        let Some(r) = self.recording.as_ref() else {
461            return;
462        };
463        if r.slot_key != slot.key {
464            return;
465        }
466        let mut r = self.recording.take().expect("checked");
467        let pushed = (self.tree.len() as u32).saturating_sub(r.first);
468        if r.paused > 0 {
469            // A pause left open is a door that did not balance: not
470            // this module's to repair, and not a fill to replay.
471            r.taint = Some("a pause left open");
472        } else if pushed.saturating_sub(r.foreign) != r.nodes {
473            r.taint = Some("a node pushed through a door the journal does not know");
474        }
475        let kept = Kept {
476            slot_key: r.slot_key,
477            origin: r.origin,
478            params: r.params,
479            ops: r.ops,
480            reads: r.reads.into_inner(),
481            taint: r.taint,
482            frame: self.frame_no,
483        };
484        self.kept.insert(r.name, kept);
485    }
486
487    /// The first fact the kept fill read that reads otherwise now, as
488    /// words for a ledger; `None` while every one holds.
489    fn first_moved(&self, reads: &[Read]) -> Option<String> {
490        reads.iter().find(|r| !self.read_holds(r)).map(|r| match r {
491            // A palette's every field is no ledger line.
492            Read::Theme(_) => "Theme".to_owned(),
493            Read::Metrics(_) => "Metrics".to_owned(),
494            r => format!("{r:?}"),
495        })
496    }
497
498    /// Whether one fact the kept fill read still reads the same.
499    fn read_holds(&self, r: &Read) -> bool {
500        match r {
501            Read::Hover(k, v) => self.interaction.is_hovered(*k) == *v,
502            Read::Pressed(k, v) => self.interaction.is_pressed(*k) == *v,
503            Read::Drop(k, v) => self.interaction.is_drop_target(*k) == *v,
504            Read::GroupHover(g, v) => self.interaction.is_group_hovered(*g) == *v,
505            Read::GroupPressed(g, v) => self.interaction.is_group_pressed(*g) == *v,
506            Read::Focus(k) => self.focus == *k,
507            Read::Focused(k, v) => (self.focus == Some(*k)) == *v,
508            Read::FocusVisible(v) => self.focus_visible == *v,
509            Read::Caret(v) => self.edit.blink_visible() == *v,
510            Read::Clock => false,
511            Read::Mods(m) => self.interaction.modifiers() == *m,
512            Read::Cursor(p) => self.interaction.cursor().map(|c| c.minus(self.dt_shift())) == *p,
513            Read::Scroll(k, v) => self.scroll.offset(*k) == *v,
514            Read::ScrollGeom(k, g) => self.scroll_geometry(*k) == *g,
515            Read::Layout(k, r) => self.layout_of_raw(*k) == *r,
516            Read::TextHit(k, p, h) => self.text_hit_raw(*k, *p) == *h,
517            Read::EditText(k, s) => self.edit.text(*k) == *s,
518            Read::Env(f) => env_same(f, &self.env_facts_raw()),
519            Read::Measure(rev) => self.text_rev() == *rev,
520            Read::Theme(t) => **t == self.theme,
521            Read::Metrics(m) => **m == self.metrics,
522            Read::Accent(a) => self.has_accent_raw() == *a,
523            Read::Opaque => false,
524        }
525    }
526
527    /// The fonts' and weights' revision together: what a measurement
528    /// depends on beyond its string and style.
529    pub(crate) fn text_rev(&self) -> u64 {
530        self.fonts_rev
531            .wrapping_mul(0x9E37_79B9)
532            .wrapping_add(self.weights_rev)
533    }
534
535    /// Whether the slot `name`, declared at `key` with `params`, can be
536    /// replayed: the kept fill comes out when it can, the reason stays
537    /// when it cannot (and the kept fill is dropped, since the fresh
538    /// fill that follows replaces it).
539    pub(crate) fn take_replayable(
540        &mut self,
541        name: &str,
542        key: Key,
543        params: &Value,
544    ) -> Result<Kept, (SlotFill, Option<String>)> {
545        let Some(kept) = self.kept.remove(name) else {
546            return Err((SlotFill::NotKept, None));
547        };
548        if kept.slot_key != key {
549            return Err((SlotFill::Moved, None));
550        }
551        if let Some(why) = kept.taint {
552            return Err((SlotFill::NotReplayable, Some(why.to_owned())));
553        }
554        if kept.params != *params {
555            return Err((SlotFill::Params, None));
556        }
557        if let Some(moved) = self.first_moved(&kept.reads) {
558            return Err((SlotFill::Reads, Some(moved)));
559        }
560        Ok(kept)
561    }
562
563    /// Pushes the kept fill's nodes again as the fill of `slot`, under
564    /// the origin that made them, `filler` answering the slots it
565    /// declares inside; then keeps it for the frame after.
566    pub(crate) fn replay(
567        &mut self,
568        name: &str,
569        slot: &Slot<'_>,
570        mut kept: Kept,
571        filler: Option<&mut dyn crate::slot::Fill>,
572    ) {
573        let origin = kept.origin;
574        let ops = std::mem::take(&mut kept.ops);
575        self.fill_within(slot, origin, filler, |ui| {
576            for op in &ops {
577                replay_op(ui, op);
578            }
579        });
580        kept.ops = ops;
581        kept.frame = self.frame_no;
582        self.kept.insert(name.to_owned(), kept);
583    }
584
585    /// The last answer `Ui::slot_replay` gave for `name` this frame, or
586    /// the frame before while this one is being built — for a host's
587    /// own ledger of what its panes cost, and for a test.
588    ///
589    /// `None` for a frame that asked `slot_replay` nothing of `name`: one
590    /// that kept the slot with `slot_kept`, filled it with `slot_with`, or
591    /// did not declare it. Once a frame is finished its own answers are
592    /// the only ones read (backlog F156); the frame before's stand in
593    /// only while the next is built, until its slot is declared.
594    pub fn slot_fill(&self, name: &str) -> Option<SlotFill> {
595        self.find_slot_fill(name).map(|(f, _)| f)
596    }
597
598    /// Beside [`Self::slot_fill`]: for `Reads`, the fact that moved as
599    /// words (`Hover(Key(…), false)`), for `NotReplayable` what the
600    /// fill declared or drew; `None` for the other answers.
601    pub fn slot_fill_why(&self, name: &str) -> Option<&str> {
602        self.find_slot_fill(name).and_then(|(_, why)| why)
603    }
604
605    fn find_slot_fill(&self, name: &str) -> Option<(SlotFill, Option<&str>)> {
606        // This frame's answers first, newest first, then — while this
607        // frame is being built, and only then — the last frame's. After
608        // `finish_frame` the frame before's are a frame stale: a frame
609        // that kept, filled plainly or skipped the slot answered nothing.
610        let last: &[_] = if self.building {
611            &self.slot_fills_last
612        } else {
613            &[]
614        };
615        self.slot_fills
616            .iter()
617            .rev()
618            .chain(last.iter().rev())
619            .find(|(n, ..)| n == name)
620            .map(|(_, f, w)| (*f, w.as_deref()))
621    }
622
623    pub(crate) fn note_slot_fill(&mut self, name: &str, fill: SlotFill, why: Option<String>) {
624        self.slot_fills.push((name.to_owned(), fill, why));
625    }
626
627    /// How many nodes the kept fill of `name` holds, if one is kept.
628    pub fn slot_kept_nodes(&self, name: &str) -> Option<usize> {
629        self.kept.get(name).map(Kept::nodes)
630    }
631
632    /// At `begin_frame`: the fills a frame answered are the frame's.
633    pub(crate) fn replay_begin_frame(&mut self) {
634        // This frame's answers so far are the last frame's, and a view
635        // that asks before its slot is declared reads those.
636        self.slot_fills_last = std::mem::take(&mut self.slot_fills);
637        self.recording = None;
638        self.keep_next = None;
639    }
640
641    /// At `finish_frame`: a slot not declared this frame forgets what it
642    /// kept, as its params were never retained either.
643    pub(crate) fn replay_finish_frame(&mut self) {
644        let now = self.frame_no;
645        self.kept.retain(|_, k| k.frame == now);
646    }
647}
648
649/// One journaled op, through the door it came in by — the node under the
650/// key it had, the spec as it was declared, so hover, accent and easing
651/// are resolved for this frame as a fresh fill's would be.
652fn replay_op(ui: &mut Ui<'_>, op: &Op) {
653    let core = ui.core();
654    match op {
655        Op::Open { key, spec } => core.open_with_key(*key, (**spec).clone()),
656        Op::Close => core.close(),
657        Op::Text {
658            key,
659            content,
660            style,
661        } => core.text_with_key(*key, content, *style),
662        Op::Rich { key, spans, base } => {
663            let spans: Vec<Span<'_>> = spans.iter().map(|(text, s)| Span { text, ..*s }).collect();
664            core.rich_text_with_key(*key, &spans, *base);
665        }
666        Op::Image {
667            key,
668            id,
669            opts,
670            spec,
671        } => core.image_with_key(*key, *id, *opts, (**spec).clone()),
672        Op::Fragment {
673            key,
674            frag,
675            params,
676            spec,
677        } => core.fragment_with_key(*key, *frag, params, (**spec).clone()),
678        Op::Line {
679            key,
680            points,
681            stroke,
682            spec,
683        } => core.line_with_key(*key, points, stroke, (**spec).clone()),
684        Op::Polygon { key, points, spec } => {
685            core.polygon_with_key(*key, points, (**spec).clone());
686        }
687        Op::Path {
688            key,
689            ops,
690            rule,
691            stroke,
692            turn,
693            spec,
694        } => core.path_with_key(*key, ops, *rule, *stroke, *turn, (**spec).clone()),
695        Op::Edit {
696            label,
697            initial,
698            opts,
699            spec,
700        } => {
701            core.text_edit(label, initial, opts, (**spec).clone());
702        }
703        Op::Label { key, label } => core.note_label(*key, label),
704        Op::Indexed(i) => {
705            if let Some(at) = core.tree.len().checked_sub(1) {
706                core.tree.indexed.push((at as u32, *i));
707            }
708        }
709        Op::RowCount(n) => {
710            if let Some(at) = core.tree.len().checked_sub(1) {
711                core.tree.row_counts.push((at as u32, *n));
712            }
713        }
714        Op::RowCountOpen(n) => {
715            let at = core.current();
716            core.tree.row_counts.push((at, *n));
717        }
718        Op::Hint { key, text } => core.hint(*key, &**text),
719        Op::KeyFocus(key) => core.set_key_focus(Some(*key)),
720        Op::Slot { name, params } => {
721            ui.slot_with(name, params);
722        }
723    }
724}