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}