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