ailloli_ui_runtime/element/
reconcile.rs

1//! Deterministic reconciliation between a new view tree and retained elements.
2
3use std::collections::HashMap;
4use std::rc::Rc;
5
6use ailloli_ui_core::ElementId;
7
8use std::collections::HashSet;
9
10use super::{ElementKind, ElementTree, Key};
11use crate::app::RuntimeHandle;
12use crate::component::reactive::{
13    MountGeneration, ReactiveReadScope, ReactiveReadSet, ReactiveStage,
14};
15use crate::component::view::{component_mount_identity, widget_mount_identity};
16use crate::component::{Context, View, ViewKind};
17
18#[derive(Debug, Clone)]
19/// Existing direct-child identity supplied to [`reconcile_children`].
20///
21/// # Examples
22///
23/// ```
24/// use ailloli_ui_core::ElementId;
25/// use ailloli_ui_runtime::element::ReconcileInputChild;
26/// let child = ReconcileInputChild { id: ElementId(3), key: None };
27/// assert_eq!(child.id, ElementId(3));
28/// ```
29pub struct ReconcileInputChild {
30    /// Existing retained element ID.
31    pub id: ElementId,
32    /// Optional stable key; `None` permits positional reuse.
33    pub key: Option<Key>,
34}
35
36#[derive(Debug, Clone)]
37/// Reused direct-child identity returned by [`reconcile_children`].
38///
39/// The current algorithm only returns `Some` records with `reused == true`;
40/// creation is represented by a `None` slot.
41///
42/// # Examples
43///
44/// ```
45/// use ailloli_ui_core::ElementId;
46/// use ailloli_ui_runtime::element::ReconcileOutputChild;
47/// let child = ReconcileOutputChild { id: ElementId(3), reused: true };
48/// assert!(child.reused);
49/// ```
50pub struct ReconcileOutputChild {
51    /// Existing retained element ID selected for this new slot.
52    pub id: ElementId,
53    /// Always `true` for current `Some` outputs.
54    pub reused: bool,
55}
56
57/// Reconciles children by index when no keys exist, otherwise by stable key.
58///
59/// - If `new_keys[i]` is `Some(k)`, reuse the old child with the same key when present.
60/// - Otherwise try reuse by index when possible.
61/// - Unmatched slots return `None` for the caller to create new elements.
62///
63/// Key lookup is built from the old slice in order, so duplicate old keys use
64/// the last matching ID. Duplicate new keys may select the same old ID more
65/// than once. An unkeyed new slot can positionally reuse an old keyed child.
66/// This helper does not enforce uniqueness or mutate either input.
67///
68/// # Examples
69///
70/// ```
71/// use ailloli_ui_core::ElementId;
72/// use ailloli_ui_runtime::element::{reconcile_children, Key, ReconcileInputChild};
73/// let old = [
74///     ReconcileInputChild { id: ElementId(1), key: Some(Key::Static("a")) },
75///     ReconcileInputChild { id: ElementId(2), key: Some(Key::Static("b")) },
76/// ];
77/// let result = reconcile_children(&old, &[Some(Key::Static("b")), None, None]);
78/// assert_eq!(result[0].as_ref().unwrap().id, ElementId(2));
79/// assert_eq!(result[1].as_ref().unwrap().id, ElementId(2));
80/// assert!(result[2].is_none());
81/// ```
82pub fn reconcile_children(
83    old: &[ReconcileInputChild],
84    new_keys: &[Option<Key>],
85) -> Vec<Option<ReconcileOutputChild>> {
86    let mut by_key: HashMap<&Key, ElementId> = HashMap::new();
87    for c in old {
88        if let Some(k) = c.key.as_ref() {
89            by_key.insert(k, c.id);
90        }
91    }
92
93    let mut out = Vec::with_capacity(new_keys.len());
94    for (i, nk) in new_keys.iter().enumerate() {
95        if let Some(k) = nk.as_ref() {
96            if let Some(&id) = by_key.get(k) {
97                out.push(Some(ReconcileOutputChild { id, reused: true }));
98                continue;
99            }
100            out.push(None);
101            continue;
102        }
103
104        if let Some(old_child) = old.get(i) {
105            out.push(Some(ReconcileOutputChild {
106                id: old_child.id,
107                reused: true,
108            }));
109        } else {
110            out.push(None);
111        }
112    }
113
114    out
115}
116
117/// Copies a declarative string key into its retained owned representation.
118fn key_from_view<A>(view: &View<A>) -> Option<Key> {
119    view.key_ref().map(|k| Key::String(k.to_string()))
120}
121
122/// Returns whether two declarative payloads represent the same retained type.
123///
124/// Public constructors retain the concrete implementation type in a private
125/// sidecar. Directly constructed trait-object variants fail closed: only the
126/// exact same `Rc` allocation is considered the same mount.
127fn same_mount_payload_type<A: 'static>(old: &ElementKind<A>, new: &ViewKind<A>) -> bool {
128    match (old, new) {
129        (ElementKind::Empty, ViewKind::Empty) => true,
130        (ElementKind::Widget(old), ViewKind::Widget(new)) => {
131            match (widget_mount_identity(old), widget_mount_identity(new)) {
132                (Some(old), Some(new)) => old == new,
133                (None, None) => Rc::ptr_eq(old, new),
134                _ => false,
135            }
136        }
137        (ElementKind::Component(old), ViewKind::Component(new)) => {
138            match (component_mount_identity(old), component_mount_identity(new)) {
139                (Some(old), Some(new)) => old == new,
140                (None, None) => Rc::ptr_eq(old, new),
141                _ => false,
142            }
143        }
144        _ => false,
145    }
146}
147
148/// Recursively removes descendants, scoped component state, then the node.
149fn remove_subtree<A>(tree: &mut ElementTree<A>, id: ElementId, runtime: &RuntimeHandle<A>) {
150    let children = tree.get(id).map(|e| e.children.clone()).unwrap_or_default();
151    for c in children {
152        remove_subtree(tree, c, runtime);
153    }
154
155    runtime.unregister_element_mount(id);
156    runtime
157        .states()
158        .borrow_mut()
159        .remove_element_scoped(runtime.element_tree_id(), id);
160    let _ = tree.remove_element(id);
161}
162
163/// Publishes one successful Build observation set or schedules a clean retry.
164///
165/// A callback may synchronously mutate a source that it already read. Its
166/// returned view can still be reconciled for this traversal, but its stale
167/// dependency snapshot must never replace the last authoritative graph. The
168/// deferred Build is coalesced by the runtime and never re-enters the callback.
169fn publish_build_dependencies_or_retry<A: 'static>(
170    runtime: &RuntimeHandle<A>,
171    element_id: ElementId,
172    mount_generation: MountGeneration,
173    reads: &ReactiveReadSet,
174) {
175    if reads.is_current() {
176        runtime.replace_reactive_dependencies(
177            element_id,
178            mount_generation,
179            ReactiveStage::Build,
180            reads,
181        );
182    } else {
183        runtime.request_build(element_id);
184    }
185}
186
187/// Creates a retained subtree, building each component exactly once encountered.
188fn create_from_view<A: 'static>(
189    tree: &mut ElementTree<A>,
190    runtime: &RuntimeHandle<A>,
191    parent: Option<ElementId>,
192    view: View<A>,
193) -> ElementId {
194    let key = key_from_view(&view);
195    let flex_item = view.flex_item;
196    let size_hint = view.size_hint;
197
198    let kind = match &view.kind {
199        ViewKind::Empty => ElementKind::Empty,
200        ViewKind::Widget(w) => ElementKind::Widget(w.clone()),
201        ViewKind::Component(c) => ElementKind::Component(c.clone()),
202    };
203
204    let id = tree.create_element(kind, key, parent);
205    let mount_generation = tree
206        .get(id)
207        .expect("newly-created retained element must exist")
208        .mount_generation();
209    runtime.register_element_mount(id, mount_generation);
210    tree.set_view_metadata(id, flex_item, size_hint);
211
212    match view.kind {
213        ViewKind::Empty => {}
214        ViewKind::Widget(_) => {
215            let mut children = Vec::with_capacity(view.children.len());
216            for child_view in view.children {
217                let child_id = create_from_view(tree, runtime, Some(id), child_view);
218                children.push(child_id);
219            }
220            tree.set_children(id, children);
221        }
222        ViewKind::Component(component) => {
223            tree.record_build(id);
224            let mut ctx = Context::new(id, runtime.clone());
225            let scope = ReactiveReadScope::new();
226            let built = component.build(&mut ctx);
227            let reads = scope.finish();
228            publish_build_dependencies_or_retry(runtime, id, mount_generation, &reads);
229            let child_id = create_from_view(tree, runtime, Some(id), built);
230            tree.set_children(id, vec![child_id]);
231        }
232    }
233
234    id
235}
236
237/// Creates or reconciles the tree root and returns its retained ID.
238///
239/// The first call recursively creates the view. Later calls preserve the root
240/// ID and delegate to [`reconcile_element`], irrespective of root kind changes.
241/// Component build callbacks run synchronously and may dispatch state changes
242/// or panic; mutation is not rolled back on panic.
243///
244/// # Examples
245///
246/// ```
247/// use ailloli_ui_runtime::app::RuntimeHandle;
248/// use ailloli_ui_runtime::component::View;
249/// use ailloli_ui_runtime::element::ElementTree;
250/// use ailloli_ui_runtime::element::reconcile::reconcile_root;
251/// let runtime = RuntimeHandle::<()>::new();
252/// let mut tree = ElementTree::new();
253/// let first = reconcile_root(&mut tree, &runtime, View::empty());
254/// let second = reconcile_root(&mut tree, &runtime, View::empty());
255/// assert_eq!(first, second);
256/// ```
257pub fn reconcile_root<A: 'static>(
258    tree: &mut ElementTree<A>,
259    runtime: &RuntimeHandle<A>,
260    root_view: View<A>,
261) -> ElementId {
262    match tree.root() {
263        Some(root_id) => reconcile_element(tree, runtime, root_id, root_view),
264        None => create_from_view(tree, runtime, None, root_view),
265    }
266}
267
268/// Reconciles `new_view` into an existing element and returns the same ID.
269///
270/// The element's kind, key, and view metadata are replaced. Layout is always
271/// invalidated with a nonzero wrapping revision, even if inputs are equal.
272/// Widgets use their declarative children; components build exactly once and
273/// retain the built view as one child. Reused descendants are selected by
274/// [`reconcile_children`]; removed subtrees also lose tree-scoped state.
275///
276/// If `element_id` is missing, metadata update is a no-op but child creation can
277/// still occur with that missing ID as parent; callers should only pass IDs
278/// owned by `tree`. Component/widget panics propagate without rollback.
279///
280/// # Examples
281///
282/// ```
283/// use ailloli_ui_runtime::app::RuntimeHandle;
284/// use ailloli_ui_runtime::component::View;
285/// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
286/// use ailloli_ui_runtime::element::reconcile::reconcile_element;
287/// let runtime = RuntimeHandle::<()>::new();
288/// let mut tree = ElementTree::new();
289/// let id = tree.create_element(ElementKind::Empty, None, None);
290/// assert_eq!(reconcile_element(&mut tree, &runtime, id, View::empty()), id);
291/// assert!(tree.get(id).unwrap().dirty.layout);
292/// ```
293pub fn reconcile_element<A: 'static>(
294    tree: &mut ElementTree<A>,
295    runtime: &RuntimeHandle<A>,
296    element_id: ElementId,
297    new_view: View<A>,
298) -> ElementId {
299    let new_key = key_from_view(&new_view);
300    let flex_item = new_view.flex_item;
301    let size_hint = new_view.size_hint;
302
303    // Update kind and retire every dependency/state edge when the mounted
304    // payload category or concrete type changes at this stable element ID.
305    let mount_replaced = tree
306        .get(element_id)
307        .is_some_and(|element| !same_mount_payload_type(&element.kind, &new_view.kind));
308    let mut mount_generation = tree
309        .get(element_id)
310        .map(|element| element.mount_generation());
311    if let Some(el) = tree.get_mut(element_id) {
312        if mount_replaced {
313            mount_generation = Some(el.advance_mount_generation());
314            el.layout = None;
315            el.layout_reactive_dependencies = Default::default();
316            el.layout_commit_reactive_dependencies = Default::default();
317            el.committed_layout_generation = None;
318            el.committed_layout_attempt = None;
319            el.layout_callback_executed = false;
320            el.committed_bounds = None;
321        }
322        el.key = new_key;
323        el.kind = match &new_view.kind {
324            ViewKind::Empty => ElementKind::Empty,
325            ViewKind::Widget(w) => ElementKind::Widget(w.clone()),
326            ViewKind::Component(c) => ElementKind::Component(c.clone()),
327        };
328        // Replacing declarative inputs invalidates this element's cached
329        // layout; unaffected sibling components are not traversed here.
330        el.dirty = super::DirtyFlags::layout();
331        el.layout_revision = el.layout_revision.wrapping_add(1).max(1);
332        el.layout_cache_key = None;
333        el.measurement_layout = None;
334        el.measurement_layout_cache_key = None;
335        el.measurement_reactive_dependencies = Default::default();
336        el.commit_dirty = true;
337    }
338    if let Some(mount_generation) = mount_generation {
339        runtime.register_element_mount(element_id, mount_generation);
340    }
341    if mount_replaced {
342        runtime
343            .states()
344            .borrow_mut()
345            .remove_element_scoped(runtime.element_tree_id(), element_id);
346    }
347    tree.set_view_metadata(element_id, flex_item, size_hint);
348
349    // Resolve the effective children views:
350    // - for widgets: use `new_view.children`
351    // - for components: build exactly once here, and treat it as a single child View
352    let children_views: Vec<View<A>> = match new_view.kind {
353        ViewKind::Empty => Vec::new(),
354        ViewKind::Widget(_) => new_view.children,
355        ViewKind::Component(component) => {
356            tree.record_build(element_id);
357            let mut ctx = Context::new(element_id, runtime.clone());
358            let scope = ReactiveReadScope::new();
359            let built = component.build(&mut ctx);
360            let reads = scope.finish();
361            publish_build_dependencies_or_retry(
362                runtime,
363                element_id,
364                mount_generation.expect("reconciled retained element must have a generation"),
365                &reads,
366            );
367            vec![built]
368        }
369    };
370
371    let old_children = tree
372        .children_of(element_id)
373        .iter()
374        .filter_map(|&id| {
375            tree.get(id).map(|e| ReconcileInputChild {
376                id,
377                key: e.key.clone(),
378            })
379        })
380        .collect::<Vec<_>>();
381
382    let new_keys = children_views
383        .iter()
384        .map(|v| key_from_view(v))
385        .collect::<Vec<_>>();
386
387    let mapping = reconcile_children(&old_children, &new_keys);
388
389    let mut next_children = Vec::with_capacity(children_views.len());
390    let mut reused = HashSet::<ElementId>::new();
391
392    for (slot, view) in mapping.into_iter().zip(children_views) {
393        match slot {
394            Some(reuse) => {
395                reused.insert(reuse.id);
396                let cid = reconcile_element(tree, runtime, reuse.id, view);
397                next_children.push(cid);
398            }
399            None => {
400                let cid = create_from_view(tree, runtime, Some(element_id), view);
401                next_children.push(cid);
402            }
403        }
404    }
405
406    // Drop old children not reused.
407    for c in old_children {
408        if !reused.contains(&c.id) {
409            remove_subtree(tree, c.id, runtime);
410        }
411    }
412
413    tree.set_children(element_id, next_children);
414    element_id
415}
416
417/// Rebuilds and reconciles one retained component in place.
418///
419/// Returns `false` without mutation for a missing ID or non-component kind.
420/// Otherwise it invalidates layout, records one build, invokes the component,
421/// reconciles its single built child, and returns `true`. Panics propagate after
422/// partial mutation and are not rolled back.
423///
424/// # Examples
425///
426/// ```
427/// use ailloli_ui_runtime::app::RuntimeHandle;
428/// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
429/// use ailloli_ui_runtime::element::reconcile::reconcile_existing_component;
430/// let runtime = RuntimeHandle::<()>::new();
431/// let mut tree = ElementTree::new();
432/// let id = tree.create_element(ElementKind::Empty, None, None);
433/// assert!(!reconcile_existing_component(&mut tree, &runtime, id));
434/// ```
435pub fn reconcile_existing_component<A: 'static>(
436    tree: &mut ElementTree<A>,
437    runtime: &RuntimeHandle<A>,
438    element_id: ElementId,
439) -> bool {
440    let Some(component) = tree.get(element_id).and_then(|el| match &el.kind {
441        ElementKind::Component(component) => Some(component.clone()),
442        _ => None,
443    }) else {
444        return false;
445    };
446
447    if let Some(el) = tree.get_mut(element_id) {
448        el.dirty = super::DirtyFlags::layout();
449        el.layout_revision = el.layout_revision.wrapping_add(1).max(1);
450        el.layout_cache_key = None;
451        el.measurement_layout = None;
452        el.measurement_layout_cache_key = None;
453        el.measurement_reactive_dependencies = Default::default();
454        el.commit_dirty = true;
455    }
456
457    tree.record_build(element_id);
458    let mut ctx = Context::new(element_id, runtime.clone());
459    let mount_generation = tree
460        .get(element_id)
461        .expect("existing component must remain mounted")
462        .mount_generation();
463    let scope = ReactiveReadScope::new();
464    let built = component.build(&mut ctx);
465    let reads = scope.finish();
466    publish_build_dependencies_or_retry(runtime, element_id, mount_generation, &reads);
467    reconcile_child_views(tree, runtime, element_id, vec![built]);
468    true
469}
470
471/// Reconciles an already-built sequence of direct child views.
472fn reconcile_child_views<A: 'static>(
473    tree: &mut ElementTree<A>,
474    runtime: &RuntimeHandle<A>,
475    element_id: ElementId,
476    children_views: Vec<View<A>>,
477) {
478    let old_children = tree
479        .children_of(element_id)
480        .iter()
481        .filter_map(|&id| {
482            tree.get(id).map(|e| ReconcileInputChild {
483                id,
484                key: e.key.clone(),
485            })
486        })
487        .collect::<Vec<_>>();
488
489    let new_keys = children_views
490        .iter()
491        .map(|v| key_from_view(v))
492        .collect::<Vec<_>>();
493
494    let mapping = reconcile_children(&old_children, &new_keys);
495
496    let mut next_children = Vec::with_capacity(children_views.len());
497    let mut reused = HashSet::<ElementId>::new();
498
499    for (slot, view) in mapping.into_iter().zip(children_views) {
500        match slot {
501            Some(reuse) => {
502                reused.insert(reuse.id);
503                let cid = reconcile_element(tree, runtime, reuse.id, view);
504                next_children.push(cid);
505            }
506            None => {
507                let cid = create_from_view(tree, runtime, Some(element_id), view);
508                next_children.push(cid);
509            }
510        }
511    }
512
513    for c in old_children {
514        if !reused.contains(&c.id) {
515            remove_subtree(tree, c.id, runtime);
516        }
517    }
518
519    tree.set_children(element_id, next_children);
520}
521
522#[cfg(test)]
523/// Tests implementation details.
524mod tests {
525    use super::*;
526    use crate::app::Runtime;
527    use crate::component::{Component, ComponentNode, Widget};
528    use crate::layout::{LayoutChild, LayoutCtx, LayoutEngine, LayoutResult};
529    use crate::scene::PaintCtx;
530    use ailloli_ui_core::{Constraints, Rect};
531    use std::cell::RefCell;
532    use std::rc::Rc;
533
534    struct WidgetA;
535    struct WidgetB;
536
537    macro_rules! impl_empty_widget {
538        ($widget:ty, $name:literal) => {
539            impl Widget<()> for $widget {
540                fn debug_name(&self) -> &'static str {
541                    $name
542                }
543
544                fn layout(
545                    &self,
546                    _engine: &mut LayoutEngine<'_, ()>,
547                    _ctx: &mut LayoutCtx<'_>,
548                    _children: &mut [LayoutChild],
549                    _constraints: Constraints,
550                ) -> LayoutResult {
551                    LayoutResult::empty()
552                }
553
554                fn paint(&self, _ctx: &mut PaintCtx<'_>, _bounds: Rect, _layout: &LayoutResult) {}
555            }
556        };
557    }
558
559    impl_empty_widget!(WidgetA, "WidgetA");
560    impl_empty_widget!(WidgetB, "WidgetB");
561
562    struct ComponentA;
563    struct ComponentB;
564
565    impl ComponentNode<()> for ComponentA {
566        fn build(&self, _context: &mut Context<()>) -> View<()> {
567            View::empty()
568        }
569    }
570
571    impl ComponentNode<()> for ComponentB {
572        fn build(&self, _context: &mut Context<()>) -> View<()> {
573            View::empty()
574        }
575    }
576
577    fn render_u8_slot(context: &mut Context<()>, seen: Rc<RefCell<Vec<String>>>) -> View<()> {
578        let state = context.state(7_u8);
579        seen.borrow_mut().push(format!("u8:{}", state.read()));
580        View::empty()
581    }
582
583    fn render_string_slot(context: &mut Context<()>, seen: Rc<RefCell<Vec<String>>>) -> View<()> {
584        let state = context.state(String::from("fresh"));
585        seen.borrow_mut().push(format!("string:{}", state.read()));
586        View::empty()
587    }
588
589    fn render_initial_u8_slot(
590        context: &mut Context<()>,
591        (seen, initial): (Rc<RefCell<Vec<u8>>>, u8),
592    ) -> View<()> {
593        let state = context.state(initial);
594        seen.borrow_mut().push(state.read());
595        View::empty()
596    }
597
598    #[test]
599    /// Verifies that reconcile by index without keys.
600    fn reconcile_by_index_without_keys() {
601        let old = vec![
602            ReconcileInputChild {
603                id: ElementId(1),
604                key: None,
605            },
606            ReconcileInputChild {
607                id: ElementId(2),
608                key: None,
609            },
610        ];
611        let new_keys = vec![None, None, None];
612        let out = reconcile_children(&old, &new_keys);
613        assert_eq!(out.len(), 3);
614        assert_eq!(out[0].as_ref().unwrap().id, ElementId(1));
615        assert_eq!(out[1].as_ref().unwrap().id, ElementId(2));
616        assert!(out[2].is_none());
617    }
618
619    #[test]
620    /// Verifies that reconcile by key.
621    fn reconcile_by_key() {
622        let old = vec![
623            ReconcileInputChild {
624                id: ElementId(10),
625                key: Some(Key::U64(7)),
626            },
627            ReconcileInputChild {
628                id: ElementId(11),
629                key: Some(Key::Static("a")),
630            },
631        ];
632        let new_keys = vec![Some(Key::Static("a")), Some(Key::U64(7))];
633        let out = reconcile_children(&old, &new_keys);
634        assert_eq!(out[0].as_ref().unwrap().id, ElementId(11));
635        assert_eq!(out[1].as_ref().unwrap().id, ElementId(10));
636    }
637
638    #[test]
639    fn replacing_a_widget_with_another_concrete_type_advances_the_mount_generation() {
640        let mut runtime = Runtime::new(RuntimeHandle::<()>::new());
641        let root = runtime.reconcile_view(View::leaf(WidgetA));
642        let first_generation = runtime.tree.get(root).unwrap().mount_generation();
643
644        let reconciled = runtime.reconcile_view(View::leaf(WidgetB));
645        let replacement_generation = runtime.tree.get(root).unwrap().mount_generation();
646
647        assert_eq!(reconciled, root);
648        assert!(replacement_generation.get() > first_generation.get());
649    }
650
651    #[test]
652    fn replacing_a_component_with_another_concrete_type_advances_the_mount_generation() {
653        let mut runtime = Runtime::new(RuntimeHandle::<()>::new());
654        let root = runtime.reconcile_view(View::component(ComponentA));
655        let first_generation = runtime.tree.get(root).unwrap().mount_generation();
656
657        let reconciled = runtime.reconcile_view(View::component(ComponentB));
658        let replacement_generation = runtime.tree.get(root).unwrap().mount_generation();
659
660        assert_eq!(reconciled, root);
661        assert!(replacement_generation.get() > first_generation.get());
662    }
663
664    #[test]
665    fn rebuilding_the_same_component_type_preserves_the_mount_generation() {
666        let mut runtime = Runtime::new(RuntimeHandle::<()>::new());
667        let root = runtime.reconcile_view(View::component(ComponentA));
668        let first_generation = runtime.tree.get(root).unwrap().mount_generation();
669
670        assert_eq!(
671            runtime.reconcile_view(View::component(ComponentA)),
672            root,
673            "a fresh declarative payload of the same component type must reconcile in place"
674        );
675        assert_eq!(
676            runtime.tree.get(root).unwrap().mount_generation(),
677            first_generation
678        );
679
680        runtime.runtime.request_build(root);
681        runtime.prepare_frame();
682
683        assert_eq!(
684            runtime.tree.get(root).unwrap().mount_generation(),
685            first_generation
686        );
687    }
688
689    #[test]
690    fn changing_a_function_component_render_resets_slots_and_advances_generation() {
691        let seen = Rc::new(RefCell::new(Vec::new()));
692        let mut runtime = Runtime::new(RuntimeHandle::<()>::new());
693        let root = runtime.reconcile_view(View::component(Component::new(
694            seen.clone(),
695            render_u8_slot,
696        )));
697        let first_generation = runtime.tree.get(root).unwrap().mount_generation();
698
699        let reconciled = runtime.reconcile_view(View::component(Component::new(
700            seen.clone(),
701            render_string_slot,
702        )));
703        let replacement_generation = runtime.tree.get(root).unwrap().mount_generation();
704
705        assert_eq!(reconciled, root);
706        assert!(replacement_generation.get() > first_generation.get());
707        assert_eq!(
708            seen.borrow().as_slice(),
709            ["u8:7", "string:fresh"],
710            "the second render must receive a fresh state slot of its own type"
711        );
712    }
713
714    #[test]
715    fn fresh_function_item_wrappers_preserve_the_mount_and_slots() {
716        let seen = Rc::new(RefCell::new(Vec::new()));
717        let mut runtime = Runtime::new(RuntimeHandle::<()>::new());
718        let root = runtime.reconcile_view(View::component(Component::new(
719            (seen.clone(), 7),
720            render_initial_u8_slot,
721        )));
722        let first_generation = runtime.tree.get(root).unwrap().mount_generation();
723
724        runtime.reconcile_view(View::component(Component::new(
725            (seen.clone(), 99),
726            render_initial_u8_slot,
727        )));
728
729        assert_eq!(
730            runtime.tree.get(root).unwrap().mount_generation(),
731            first_generation
732        );
733        assert_eq!(seen.borrow().as_slice(), [7, 7]);
734    }
735
736    #[test]
737    fn explicitly_erased_function_pointer_preserves_the_historical_type_mount() {
738        type Props = (Rc<RefCell<Vec<u8>>>, u8);
739        type ErasedComponent = Component<Props, ()>;
740
741        let seen = Rc::new(RefCell::new(Vec::new()));
742        let first: ErasedComponent =
743            Component::<Props, ()>::new((seen.clone(), 7), render_initial_u8_slot);
744        let mut runtime = Runtime::new(RuntimeHandle::<()>::new());
745        let root = runtime.reconcile_view(View::component(first));
746        let first_generation = runtime.tree.get(root).unwrap().mount_generation();
747
748        let second: ErasedComponent =
749            Component::<Props, ()>::new((seen.clone(), 99), render_initial_u8_slot);
750        runtime.reconcile_view(View::component(second));
751
752        assert_eq!(
753            runtime.tree.get(root).unwrap().mount_generation(),
754            first_generation
755        );
756        assert_eq!(seen.borrow().as_slice(), [7, 7]);
757    }
758}