Skip to main content

whisker_runtime/view/
renderer.rs

1//! Type-erased renderer + thread-local current-renderer plumbing.
2//!
3//! The `render!` macro emits calls to the free functions in this
4//! module ([`create_element`], [`set_attribute`], …). Each looks up
5//! the currently-installed [`DynRenderer`] from a `thread_local!`
6//! slot and forwards. This keeps the macro output renderer-agnostic
7//! while still letting tests swap in a `MockRenderer`.
8//!
9//! Lifecycle:
10//!
11//! ```ignore
12//! let renderer = Box::new(MyRenderer::new());
13//! let prev = install_renderer(renderer);
14//! // … all `view::create_element` etc. calls now go to MyRenderer
15//! uninstall_renderer(prev);                 // restore previous (None)
16//! ```
17//!
18//! In production the bridge driver installs the Lynx-backed renderer
19//! once at startup and keeps it for the life of the process.
20
21use std::cell::{Cell, RefCell};
22use std::collections::{HashMap, HashSet};
23use std::rc::Rc;
24
25use super::handle::Element;
26use crate::element::ElementTag;
27use crate::value::WhiskerValue;
28
29/// Event-handler propagation type — a faithful 1:1 mapping to Lynx's
30/// four handler kinds (`bind` / `catch` / `capture-bind` /
31/// `capture-catch`). The variant chosen when registering a listener is
32/// what drives Lynx's native event chain:
33///
34///   - **phase**: capture handlers fire on the way *down* (root →
35///     target); bind/catch (bubble) handlers fire on the way *up*
36///     (target → root).
37///   - **stop**: a `catch` handler stops propagation after it fires;
38///     a `bind` handler lets the event continue along the chain.
39///
40/// The discriminants match `lynx_event_bind_type_e` in the C bridge,
41/// so the value crosses the FFI as a plain `i32`.
42#[repr(i32)]
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum BindType {
45    /// `bind` — bubble phase, does not stop propagation. The default
46    /// (what plain `on_<event>` registers).
47    #[default]
48    Bind = 0,
49    /// `catch` — bubble phase, stops propagation at this element.
50    Catch = 1,
51    /// `capture-bind` — capture phase, does not stop propagation.
52    CaptureBind = 2,
53    /// `capture-catch` — capture phase, stops propagation.
54    CaptureCatch = 3,
55}
56
57/// One planned listener firing: the listener plus the event value it
58/// should receive (its `currentTarget` already rewritten to that
59/// listener's element).
60pub type EventFiring = (Rc<dyn Fn(WhiskerValue) + 'static>, WhiskerValue);
61
62/// The ordered firing plan produced by
63/// [`DynRenderer::plan_event_dispatch`]. Separates *planning* (done
64/// under the renderer borrow) from *firing* (done after the borrow is
65/// released, since a handler may re-enter the renderer).
66#[derive(Default)]
67pub struct EventDispatchPlan {
68    /// Whether any listener matched — relayed to the platform reporter
69    /// so Lynx can skip its own native chain for this event.
70    pub consumed: bool,
71    /// Listeners to invoke, in propagation order.
72    pub firings: Vec<EventFiring>,
73}
74
75/// Object-safe renderer trait. The renderer owns whatever per-element
76/// state it needs and answers in `Element` IDs.
77///
78/// Mirrors the shape of [`crate::renderer::Renderer`] but is
79/// type-erased — the handle type is always [`Element`]. Existing
80/// `R: Renderer` implementations bridge into here via a small adapter
81/// that maintains its own `Element → R::Element` map.
82/// All mutating methods take `&self`, not `&mut self`. This is the
83/// core of the re-entrancy fix for whisker issue #3: a native event
84/// can fire *synchronously* during a renderer operation (e.g. Lynx
85/// teardown inside [`remove_child`](Self::remove_child) triggering a
86/// UIKit callback that dispatches a custom event), which re-enters the
87/// renderer through [`dispatch_event`]. With `&self` methods the
88/// thread-local [`CURRENT_RENDERER`] is held by a *shared* borrow in
89/// [`with_renderer`], so a re-entrant call (also shared) is permitted
90/// rather than panicking with "RefCell already borrowed". Renderers
91/// own their mutable state behind per-field `RefCell`s and must scope
92/// each field borrow so it does **not** span a re-entrant FFI call.
93pub trait DynRenderer {
94    fn create_element(&self, tag: ElementTag) -> Element;
95    /// Phase 7: tag-by-name dispatch for custom / xelement-style
96    /// tags ("x-input", etc.) not in the built-in [`ElementTag`]
97    /// enum. Returns [`Element::INVALID`] when the tag is unknown
98    /// to Lynx's behaviour registry.
99    fn create_element_by_name(&self, tag_name: &str) -> Element;
100    fn release_element(&self, handle: Element);
101
102    fn set_attribute(&self, handle: Element, key: &str, value: &str);
103    /// Typed-attr variants. Lynx's prop dispatch on many UIs
104    /// (`<list>`, `<scroll-view>`, …) gates branches on
105    /// `value.IsNumber()` / `value.IsBool()` against the underlying
106    /// `lepus::Value`, so a stringified attr from
107    /// [`set_attribute`](Self::set_attribute) silently no-ops in
108    /// those branches. Use these for any prop whose Lynx handler
109    /// reads the value as anything other than a string. Default
110    /// impls forward to the string path (good enough for test
111    /// renderers that don't model the underlying type discrimination).
112    fn set_attribute_int(&self, handle: Element, key: &str, value: i64) {
113        self.set_attribute(handle, key, &value.to_string());
114    }
115    fn set_attribute_bool(&self, handle: Element, key: &str, value: bool) {
116        self.set_attribute(handle, key, if value { "true" } else { "false" });
117    }
118    fn set_attribute_double(&self, handle: Element, key: &str, value: f64) {
119        self.set_attribute(handle, key, &value.to_string());
120    }
121    fn set_inline_styles(&self, handle: Element, css: &str);
122
123    /// Underlying Lynx sign (`impl_id`) for `handle`, or 0 if the
124    /// renderer doesn't model signs (test renderers) or the handle
125    /// is unknown. The list provider closure needs this to tell the
126    /// C++ list which FiberElement to bind to an `index`. Whisker's
127    /// own [`Element`] is a Vec index inside the renderer and is
128    /// **not** the same number as Lynx's `impl_id`.
129    fn element_sign(&self, _handle: Element) -> i32 {
130        0
131    }
132
133    /// Hand a `<list>` element its item count so the bridge can build
134    /// the `update-list-info` map (positional item-keys `w_<i>`) that
135    /// Lynx's decoupled native list reads its items from. The `list`
136    /// builder calls this once at `__h()` finalize. Default no-op for
137    /// test renderers that don't model list virtualisation.
138    fn set_update_list_info(&self, _handle: Element, _count: i32) {}
139
140    /// Install a native item provider on a `<list>` element. The
141    /// `provider`'s callbacks are invoked by Lynx's list machinery to
142    /// fetch / recycle item elements on demand. Returns `true` if the
143    /// install reached the bridge — `false` is reported when the
144    /// renderer has no live native handle for `_handle` or doesn't
145    /// model list virtualisation (test renderers default here).
146    /// The default drops `provider` so test code doesn't leak boxed
147    /// closures.
148    fn install_list_native_item_provider(
149        &self,
150        _handle: Element,
151        provider: super::list_provider::NativeItemProvider,
152    ) -> bool {
153        drop(provider);
154        false
155    }
156
157    fn append_child(&self, parent: Element, child: Element);
158    fn remove_child(&self, parent: Element, child: Element);
159
160    /// Register `callback` for `event_name` on `handle`.
161    ///
162    /// The callback receives the event body Lynx hands the handler
163    /// as a [`WhiskerValue`] tree (the same wire as module
164    /// args/returns). A built-in builder's `on_<event>` method or a
165    /// `#[whisker::module_component]` `on_<event>` prop wraps a
166    /// typed-event / unit / raw-value closure into this single
167    /// shape, deserializing the payload as needed. An event with no
168    /// body fires the callback with [`WhiskerValue::Null`].
169    fn set_event_listener(
170        &self,
171        handle: Element,
172        event_name: &str,
173        bind_type: BindType,
174        callback: Box<dyn Fn(WhiskerValue) + 'static>,
175    );
176
177    /// Plan how a reported event (`event_name` at `target_sign`,
178    /// carrying `body`) propagates through Whisker's reconstructed
179    /// chain — capture phase (root → target) then bubble phase
180    /// (target → root), honoring each registered listener's
181    /// [`BindType`] (catch stops bubbling; capture-catch stops
182    /// everything).
183    ///
184    /// Returns the listeners to fire **in order**, each paired with the
185    /// event value it should receive (its `currentTarget` set to that
186    /// listener's element), plus whether the event was consumed.
187    ///
188    /// Crucially this only *plans* — it does not fire the listeners,
189    /// because firing happens after the renderer borrow is released
190    /// (a handler may mutate signals → effects → re-enter the
191    /// renderer). [`dispatch_event`] does the firing. The default impl
192    /// plans nothing (renderers without a native event source); the
193    /// Lynx bridge renderer overrides it.
194    fn plan_event_dispatch(
195        &self,
196        _target_sign: i32,
197        _event_name: &str,
198        _body: &WhiskerValue,
199    ) -> EventDispatchPlan {
200        EventDispatchPlan::default()
201    }
202
203    fn set_root(&self, page: Element);
204    fn flush(&self);
205
206    /// Opaque platform pointer the C bridge associates with this
207    /// `Element` handle (cast from `*mut WhiskerElement` for the
208    /// Lynx bridge renderer; `0` for renderers without a native
209    /// backing).
210    ///
211    /// Used by `whisker-driver`'s `ElementRef::invoke` to call
212    /// `whisker_bridge_invoke_element_method` without the runtime
213    /// crate having to know about the bridge's C types. Renderers
214    /// that don't have a native pointer return `0`, which the
215    /// driver surfaces as `WhiskerValue::Error` to the caller.
216    ///
217    /// Phase 7-Φ.H.2.3.
218    fn module_component_ptr(&self, _handle: Element) -> usize {
219        0
220    }
221}
222
223thread_local! {
224    /// The active renderer for this thread. `None` outside any mount.
225    ///
226    /// Wrapped in `RefCell<Option<Box<dyn>>>` rather than holding the
227    /// renderer directly so [`install_renderer`] can swap one out for
228    /// another atomically and tests can run with no renderer installed
229    /// (where dispatch functions silently no-op + warn).
230    static CURRENT_RENDERER: RefCell<Option<Box<dyn DynRenderer>>> = const { RefCell::new(None) };
231
232    /// Whisker-side mirror of every parent → ordered-children
233    /// relationship the runtime has emitted. Maintained by
234    /// [`append_child`] / [`remove_child`].
235    ///
236    /// Used by `mount_component_remountable` (#17 wrapper-removal
237    /// follow-up) to compute the "previous sibling at mount time"
238    /// anchor without asking Lynx — Lynx's C API doesn't expose a
239    /// child-position query, and we'd rather not add one. Side
240    /// effect: the mirror also enables `previous_sibling` /
241    /// `next_sibling` queries for any future need (e.g. insert_after
242    /// shimming when we ship the wrapper-less remount path).
243    static CHILDREN_OF: RefCell<HashMap<Element, Vec<Element>>> =
244        RefCell::new(HashMap::new());
245
246    /// Reverse direction of [`CHILDREN_OF`]: child → its mirror
247    /// parent. Maintained in lockstep with [`append_child`] /
248    /// [`remove_child`]. We need this to walk *up* the mirror — the
249    /// [phantom hoisting](create_phantom_element) machinery looks for
250    /// the nearest non-phantom ancestor on every tree mutation, and
251    /// the mirror-only direction is the only place that information
252    /// lives.
253    ///
254    /// Each child has at most one parent (we don't model the DOM's
255    /// "move from one parent to another" — every move is detach +
256    /// re-attach through us). Missing-entry = the child is currently
257    /// detached (no parent).
258    static PARENT_OF: RefCell<HashMap<Element, Element>> =
259        RefCell::new(HashMap::new());
260
261    /// IDs allocated by [`create_phantom_element`]. A phantom is an
262    /// Element that lives in [`CHILDREN_OF`] / [`PARENT_OF`] but is
263    /// **not** present in Lynx. It behaves like a *transparent
264    /// container*: any real child mounted under a phantom is hoisted
265    /// to the phantom's nearest non-phantom ancestor in Lynx; if
266    /// there is no such ancestor yet (the phantom is still
267    /// unattached), the real children stay in the mirror only and
268    /// land in Lynx when the phantom subtree is finally attached.
269    static PHANTOM_ELEMENTS: RefCell<HashSet<Element>> =
270        RefCell::new(HashSet::new());
271
272    /// Monotonic counter for phantom IDs, starting at [`PHANTOM_BASE`]
273    /// (`1 << 31`). The bridge renderer allocates real IDs from 0
274    /// upward, so the two ranges can't realistically collide (a
275    /// session would need 2 billion real elements before the real
276    /// counter reached `PHANTOM_BASE`).
277    static NEXT_PHANTOM_ID: Cell<u32> = const { Cell::new(PHANTOM_BASE) };
278}
279
280/// Phantom IDs occupy the high half of `u32`; real IDs start at 0
281/// from the bridge renderer's counter, so the two ranges stay
282/// disjoint without coordination.
283pub const PHANTOM_BASE: u32 = 1 << 31;
284
285/// Install `r` as the current renderer for this thread, returning
286/// whatever renderer was installed before (so the caller can restore
287/// it later if needed).
288///
289/// Most production callers install exactly once and never restore.
290/// Tests use the returned previous value to reset between cases.
291pub fn install_renderer(r: Box<dyn DynRenderer>) -> Option<Box<dyn DynRenderer>> {
292    CURRENT_RENDERER.with_borrow_mut(|slot| slot.replace(r))
293}
294
295/// Remove the current renderer, returning it to the caller. The
296/// thread-local slot is left `None`. Subsequent dispatch calls warn
297/// (in debug) and no-op.
298pub fn uninstall_renderer(prev: Option<Box<dyn DynRenderer>>) {
299    CURRENT_RENDERER.with_borrow_mut(|slot| *slot = prev);
300}
301
302/// Run `f` with `r` temporarily installed as the current renderer.
303/// Restores whatever was previously installed when `f` returns
304/// (including the `None` state). Useful for tests + scoped
305/// rendering.
306pub fn with_installed_renderer<R>(r: Box<dyn DynRenderer>, f: impl FnOnce() -> R) -> R {
307    let prev = install_renderer(r);
308    let result = f();
309    let _new = CURRENT_RENDERER.with_borrow_mut(|slot| slot.take());
310    if let Some(p) = prev {
311        let _ = install_renderer(p);
312    }
313    result
314}
315
316/// Crate-internal sigil for "no renderer installed" diagnostics —
317/// distinguishes "renderer panicked" from "no renderer in this
318/// scope" in tests.
319pub fn current_renderer_id() -> Option<&'static str> {
320    CURRENT_RENDERER.with_borrow(|slot| slot.as_ref().map(|_| "installed"))
321}
322
323/// Run `f` against the installed renderer under a **shared** borrow of
324/// the [`CURRENT_RENDERER`] slot.
325///
326/// The shared borrow is the re-entrancy fix: `RefCell` permits any
327/// number of simultaneous shared borrows, so if `f` (e.g. a
328/// `remove_child` that synchronously tears down native views) causes a
329/// native callback to re-enter Whisker through [`dispatch_event`] →
330/// another `with_renderer`, that nested shared borrow is granted
331/// instead of aborting with "already borrowed". This works because
332/// every [`DynRenderer`] method now takes `&self` and owns its mutable
333/// state behind interior `RefCell`s.
334///
335/// Slot *swapping* ([`install_renderer`] / [`uninstall_renderer`])
336/// still uses `with_borrow_mut`; those are never called during
337/// dispatch, so they can't conflict with an outstanding shared borrow.
338fn with_renderer<R>(f: impl FnOnce(&dyn DynRenderer) -> R, default: R) -> R {
339    CURRENT_RENDERER.with_borrow(|slot| match slot.as_ref() {
340        Some(r) => f(r.as_ref()),
341        None => {
342            #[cfg(debug_assertions)]
343            eprintln!("whisker-view: renderer call outside any installed renderer; ignored");
344            default
345        }
346    })
347}
348
349// Free-function dispatch — what the `render!` macro and reactive
350// effects call.
351
352/// Free-fn helper used by the `render!` macro and reactive effects to
353/// allocate an element of any tag the bridge knows. Routes both the
354/// built-in `ElementTag` enum and tag-by-name strings through the
355/// same owner-tracking + invalid-handle logic.
356pub fn create_element_by_name(tag_name: &str) -> Element {
357    let handle = with_renderer(|r| r.create_element_by_name(tag_name), Element(u32::MAX));
358    if handle.id() != u32::MAX {
359        crate::reactive::with_runtime(|rt| {
360            if let Some(owner_id) = rt.current_owner() {
361                if let Some(owner) = rt.owners.get_mut(owner_id) {
362                    owner.elements.push(handle);
363                }
364            }
365        });
366    }
367    handle
368}
369
370pub fn create_element(tag: ElementTag) -> Element {
371    let handle = with_renderer(|r| r.create_element(tag), Element(u32::MAX));
372    // Register the element with the current reactive owner so
373    // `Owner::dispose` releases it. Without this, `BridgeRenderer`'s
374    // element map (and Lynx FiberElement refcounts) accumulate across
375    // `<Show>` flips, `<For>` removals, and component remounts.
376    if handle.id() != u32::MAX {
377        crate::reactive::with_runtime(|rt| {
378            if let Some(owner_id) = rt.current_owner() {
379                if let Some(owner) = rt.owners.get_mut(owner_id) {
380                    owner.elements.push(handle);
381                }
382            }
383        });
384    }
385    handle
386}
387
388pub fn release_element(handle: Element) {
389    if is_phantom(handle) {
390        // Phantom never reached Lynx; tear down mirror state only.
391        PHANTOM_ELEMENTS.with_borrow_mut(|s| {
392            s.remove(&handle);
393        });
394        CHILDREN_OF.with_borrow_mut(|m| {
395            m.remove(&handle);
396        });
397        PARENT_OF.with_borrow_mut(|m| {
398            m.remove(&handle);
399        });
400        return;
401    }
402    with_renderer(|r| r.release_element(handle), ())
403}
404
405/// Allocate a phantom element — an opaque positional marker the
406/// runtime registers in the mirror but **never** forwards to Lynx.
407/// Phantoms behave as *transparent containers*: any real descendant
408/// attached under a phantom is hoisted to the phantom's nearest
409/// non-phantom mirror ancestor in Lynx, preserving source order.
410///
411/// Phantom IDs come from [`NEXT_PHANTOM_ID`], starting at
412/// [`PHANTOM_BASE`] (`1 << 31`); the bridge renderer's real-element
413/// counter starts at 0, so the two ranges are disjoint in any
414/// realistic session.
415///
416/// Owner-tracking parity: the freshly-allocated phantom is added to
417/// the currently-active reactive owner's `elements` list, so the
418/// same dispose-cascade that releases real elements also reaches
419/// phantoms — [`release_element`] detects the phantom case and
420/// clears its mirror + set membership without touching Lynx.
421///
422/// **Use case**: the wrapper-less `fragment` builtin and the
423/// `For` / `Show` control-flow components — each allocates one
424/// phantom as its "transparent grouping" element so its reactive
425/// children appear in the user's mirror tree as a group while
426/// landing in Lynx as flat siblings of the surrounding non-phantom
427/// container.
428pub fn create_phantom_element() -> Element {
429    let id = NEXT_PHANTOM_ID.with(|c| {
430        let id = c.get();
431        c.set(id.wrapping_add(1));
432        id
433    });
434    let handle = Element::from_raw(id);
435    PHANTOM_ELEMENTS.with_borrow_mut(|s| {
436        s.insert(handle);
437    });
438    crate::reactive::with_runtime(|rt| {
439        if let Some(owner_id) = rt.current_owner() {
440            if let Some(owner) = rt.owners.get_mut(owner_id) {
441                owner.elements.push(handle);
442            }
443        }
444    });
445    handle
446}
447
448/// Whether `handle` was allocated by [`create_phantom_element`].
449/// Cheap thread-local lookup — the bridge dispatchers below call
450/// this on every tree-mutation to decide whether to skip the FFI
451/// step.
452pub fn is_phantom(handle: Element) -> bool {
453    if handle.id() < PHANTOM_BASE {
454        return false;
455    }
456    PHANTOM_ELEMENTS.with_borrow(|s| s.contains(&handle))
457}
458
459/// Walk *up* the mirror from `start` (not including `start` itself)
460/// until a non-phantom ancestor is found. Returns `None` if `start`
461/// has no parent or the entire chain to the root is phantoms.
462///
463/// `start` may itself be either a phantom or a real element — the
464/// function just looks at its ancestors. For the hoisting path the
465/// caller usually passes the *parent* of the just-mutated child,
466/// because the child's own type isn't what determines the
467/// effective Lynx parent; the surrounding tree is.
468fn nearest_real_ancestor(start: Element) -> Option<Element> {
469    let mut current = start;
470    loop {
471        let parent = PARENT_OF.with_borrow(|m| m.get(&current).copied())?;
472        if !is_phantom(parent) {
473            return Some(parent);
474        }
475        current = parent;
476    }
477}
478
479/// Count the number of *real* (non-phantom) elements reachable from
480/// `root` through a strictly transparent path (phantom-only ancestors
481/// between `root` and the reached element) that appear in DFS
482/// pre-order before `target`. Used to compute the Lynx-side position
483/// at which a newly-attached real element should land in
484/// [`nearest_real_ancestor(target)`].
485///
486/// Excludes `root` itself; counts real descendants only. If `target`
487/// is not under `root`, returns the total count (= "append at end").
488fn count_real_descendants_before(root: Element, target: Element) -> usize {
489    fn walk(node: Element, target: Element, count: &mut usize, found: &mut bool) {
490        if *found {
491            return;
492        }
493        let children = CHILDREN_OF.with_borrow(|m| m.get(&node).cloned().unwrap_or_default());
494        for child in children {
495            if *found {
496                return;
497            }
498            if child == target {
499                *found = true;
500                return;
501            }
502            if is_phantom(child) {
503                walk(child, target, count, found);
504            } else {
505                *count += 1;
506            }
507        }
508    }
509    let mut count = 0usize;
510    let mut found = false;
511    walk(root, target, &mut count, &mut found);
512    count
513}
514
515/// DFS pre-order collect every real (non-phantom) descendant of
516/// `root` reachable through a strictly transparent chain (phantom-
517/// only ancestors). Used when a phantom subtree gets attached to a
518/// real parent — we walk it and hand the real descendants to Lynx
519/// in the right order.
520fn collect_transparent_real_descendants(root: Element) -> Vec<Element> {
521    let mut out = Vec::new();
522    fn walk(node: Element, out: &mut Vec<Element>) {
523        let children = CHILDREN_OF.with_borrow(|m| m.get(&node).cloned().unwrap_or_default());
524        for child in children {
525            if is_phantom(child) {
526                walk(child, out);
527            } else {
528                out.push(child);
529            }
530        }
531    }
532    walk(root, &mut out);
533    out
534}
535
536pub fn set_attribute(handle: Element, key: &str, value: &str) {
537    if is_phantom(handle) {
538        return; // phantoms carry no Lynx-side styling — silently no-op
539    }
540    with_renderer(|r| r.set_attribute(handle, key, value), ())
541}
542
543pub fn set_attribute_int(handle: Element, key: &str, value: i64) {
544    if is_phantom(handle) {
545        return;
546    }
547    with_renderer(|r| r.set_attribute_int(handle, key, value), ())
548}
549
550pub fn set_attribute_bool(handle: Element, key: &str, value: bool) {
551    if is_phantom(handle) {
552        return;
553    }
554    with_renderer(|r| r.set_attribute_bool(handle, key, value), ())
555}
556
557pub fn set_attribute_double(handle: Element, key: &str, value: f64) {
558    if is_phantom(handle) {
559        return;
560    }
561    with_renderer(|r| r.set_attribute_double(handle, key, value), ())
562}
563
564pub fn set_inline_styles(handle: Element, css: &str) {
565    if is_phantom(handle) {
566        return;
567    }
568    with_renderer(|r| r.set_inline_styles(handle, css), ())
569}
570
571/// See [`DynRenderer::element_sign`]. Returns 0 when no renderer is
572/// installed (e.g. test setups using the mock renderer) or when
573/// `handle` is a phantom (phantoms have no Lynx `impl_id`).
574pub fn element_sign(handle: Element) -> i32 {
575    if is_phantom(handle) {
576        return 0;
577    }
578    with_renderer(|r| r.element_sign(handle), 0)
579}
580
581pub fn set_update_list_info(handle: Element, count: i32) {
582    if is_phantom(handle) {
583        return;
584    }
585    with_renderer(|r| r.set_update_list_info(handle, count), ())
586}
587
588pub fn install_list_native_item_provider(
589    handle: Element,
590    provider: super::list_provider::NativeItemProvider,
591) -> bool {
592    if is_phantom(handle) {
593        drop(provider);
594        return false;
595    }
596    with_renderer(
597        |r| r.install_list_native_item_provider(handle, provider),
598        false,
599    )
600}
601
602/// Append `child` as the last mirror child of `parent`. The Lynx-
603/// side effect depends on whether either end of the edge is a
604/// phantom:
605///
606///   - both real → the bridge sees `append_child(parent, child)`
607///     exactly as before.
608///   - phantom child → no FFI for `child` itself (it never reaches
609///     Lynx); if `child` brings a transparent subtree of real
610///     descendants with it, they're replayed into the nearest real
611///     ancestor at the position the parent's transparent layout
612///     puts them.
613///   - phantom parent → `child` is hoisted up the phantom chain to
614///     the nearest real ancestor (if any); inserted there at the
615///     position the mirror order puts it.
616///   - phantom parent with no real ancestor → no Lynx call at all;
617///     the subtree is queued in the mirror only. When the topmost
618///     phantom is later attached to a real ancestor, the same
619///     replay path handles the queued descendants in source order.
620pub fn append_child(parent: Element, child: Element) {
621    // Mirror update — unconditional.
622    CHILDREN_OF.with_borrow_mut(|map| {
623        map.entry(parent).or_default().push(child);
624    });
625    PARENT_OF.with_borrow_mut(|map| {
626        map.insert(child, parent);
627    });
628
629    // Lynx-side effect depends on phantom-ness of either end.
630    let parent_is_phantom = is_phantom(parent);
631    let child_is_phantom = is_phantom(child);
632    if parent_is_phantom {
633        // Hoist into the nearest real ancestor. When no real ancestor
634        // exists yet (topmost phantom still detached), skip the
635        // bridge step — the next attach will replay things.
636        if let Some(real_anc) = nearest_real_ancestor(parent) {
637            let to_attach: Vec<Element> = if child_is_phantom {
638                collect_transparent_real_descendants(child)
639            } else {
640                vec![child]
641            };
642            for real in to_attach {
643                let pos = count_real_descendants_before(real_anc, real);
644                bridge_insert_or_append(real_anc, real, pos);
645            }
646        }
647    } else if child_is_phantom {
648        // Phantom child carries a transparent subtree; replay any
649        // real descendants now in DFS pre-order.
650        for real in collect_transparent_real_descendants(child) {
651            let pos = count_real_descendants_before(parent, real);
652            bridge_insert_or_append(parent, real, pos);
653        }
654    } else {
655        with_renderer(|r| r.append_child(parent, child), ());
656    }
657
658    // Wrapper-less component mount handshake: if `child` is the body
659    // root of a freshly-mounted `#[component]`, its MountSite now
660    // learns where it landed (parent + previous sibling). Hot-reload
661    // remount uses this to keep mount sites anchored across patches.
662    crate::reactive::on_component_root_attached(parent, child);
663}
664
665/// Detach `child` from `parent` in the mirror. Lynx-side: any real
666/// descendants of `child` (or `child` itself if it's real) are
667/// removed from the nearest real ancestor.
668pub fn remove_child(parent: Element, child: Element) {
669    let parent_is_phantom = is_phantom(parent);
670    let child_is_phantom = is_phantom(child);
671
672    if parent_is_phantom {
673        if let Some(real_anc) = nearest_real_ancestor(parent) {
674            let to_detach: Vec<Element> = if child_is_phantom {
675                collect_transparent_real_descendants(child)
676            } else {
677                vec![child]
678            };
679            for real in to_detach {
680                with_renderer(|r| r.remove_child(real_anc, real), ());
681            }
682        }
683    } else if child_is_phantom {
684        for real in collect_transparent_real_descendants(child) {
685            with_renderer(|r| r.remove_child(parent, real), ());
686        }
687    } else {
688        with_renderer(|r| r.remove_child(parent, child), ());
689    }
690
691    CHILDREN_OF.with_borrow_mut(|map| {
692        if let Some(children) = map.get_mut(&parent) {
693            children.retain(|c| *c != child);
694        }
695    });
696    PARENT_OF.with_borrow_mut(|map| {
697        map.remove(&child);
698    });
699}
700
701/// Internal helper: ask the bridge to place `real_child` at
702/// `position` inside `real_parent`'s Lynx child list. The C ABI
703/// doesn't expose `insert_at`, so we simulate by appending and
704/// rotating: every real sibling that should sit *after* the child
705/// (per the mirror's DFS pre-order of real-only descendants) is
706/// detached and re-appended, ending up to the right of the new
707/// child. O(siblings_to_move) bridge calls.
708fn bridge_insert_or_append(real_parent: Element, real_child: Element, position: usize) {
709    // Append lands the child at the tail in Lynx.
710    with_renderer(|r| r.append_child(real_parent, real_child), ());
711
712    // Mirror already includes the child at its target slot; compute
713    // the DFS real-only order to find the siblings that need to be
714    // rotated past it.
715    let real_descendants = collect_transparent_real_descendants(real_parent);
716
717    // Everything after `position` in mirror order must end up to the
718    // right of `real_child` in Lynx — detach and re-append in order.
719    // The "after" slice excludes `real_child` itself (at
720    // `real_descendants[position]`).
721    if position + 1 < real_descendants.len() {
722        let to_move: Vec<Element> = real_descendants[position + 1..].to_vec();
723        for sib in &to_move {
724            with_renderer(|r| r.remove_child(real_parent, *sib), ());
725        }
726        for sib in &to_move {
727            with_renderer(|r| r.append_child(real_parent, *sib), ());
728        }
729    }
730}
731
732/// Insert `child` into `parent`'s child list at position `index`.
733/// If `index >= current_len`, behaves like [`append_child`].
734///
735/// First-pass implementation: Lynx's C ABI doesn't yet expose
736/// `insert_before` / `insert_at`, so we simulate ordered insertion
737/// by detaching every sibling at or after `index`, appending the
738/// new child, then re-appending the detached siblings in order. The
739/// O(N) cost is fine for `<For>` reorders and #[component] remounts
740/// where N is the parent's current child count. Replace with a
741/// direct Lynx API once the bridge gains one.
742pub fn insert_child_at(parent: Element, child: Element, index: usize) {
743    let to_re_append: Vec<Element> = CHILDREN_OF.with_borrow(|map| {
744        map.get(&parent)
745            .map(|children| {
746                if index >= children.len() {
747                    Vec::new()
748                } else {
749                    children[index..].to_vec()
750                }
751            })
752            .unwrap_or_default()
753    });
754    for c in &to_re_append {
755        remove_child(parent, *c);
756    }
757    append_child(parent, child);
758    for c in to_re_append {
759        append_child(parent, c);
760    }
761}
762
763/// Return the element handle that appears immediately before `child`
764/// in `parent`'s child list, or `None` if `child` is the first child
765/// or `parent` has no recorded children.
766pub fn previous_sibling(parent: Element, child: Element) -> Option<Element> {
767    CHILDREN_OF.with_borrow(|map| {
768        let children = map.get(&parent)?;
769        let idx = children.iter().position(|c| *c == child)?;
770        if idx == 0 {
771            None
772        } else {
773            Some(children[idx - 1])
774        }
775    })
776}
777
778/// Index of `child` in `parent`'s ordered child list, or `None` if
779/// not tracked. Used by the wrapper-less remount path to re-insert
780/// the new body root at the same position as the old one.
781pub fn child_index(parent: Element, child: Element) -> Option<usize> {
782    CHILDREN_OF.with_borrow(|map| {
783        let children = map.get(&parent)?;
784        children.iter().position(|c| *c == child)
785    })
786}
787
788/// Snapshot of `parent`'s current ordered child list. Empty Vec if
789/// the parent has no tracked children. Used by the batched
790/// `remount_components_for` so it can compute the final desired
791/// child order before any mutation churns the indices.
792pub fn children_of(parent: Element) -> Vec<Element> {
793    CHILDREN_OF.with_borrow(|map| map.get(&parent).cloned().unwrap_or_default())
794}
795
796/// Test/internal: clear the parent → children mirror. Call between
797/// scenarios that share a thread (the production runtime never
798/// needs this).
799#[doc(hidden)]
800pub fn __reset_children_mirror_for_tests() {
801    CHILDREN_OF.with_borrow_mut(|map| map.clear());
802}
803
804pub fn set_event_listener(
805    handle: Element,
806    event_name: &str,
807    bind_type: BindType,
808    callback: Box<dyn Fn(WhiskerValue) + 'static>,
809) {
810    if is_phantom(handle) {
811        // Phantoms aren't in Lynx's event chain.
812        drop(callback);
813        return;
814    }
815    with_renderer(
816        |r| r.set_event_listener(handle, event_name, bind_type, callback),
817        (),
818    )
819}
820
821/// Dispatch a reported event through the installed renderer's
822/// reconstructed propagation chain. The driver's C entry point (the
823/// bridge reporter forwards here) calls this. Returns whether the
824/// event was consumed.
825///
826/// Planning runs under the renderer borrow; the listeners then fire
827/// **after** the borrow is released, so a handler is free to mutate
828/// signals / re-enter `view::*` without a re-entrant borrow panic.
829pub fn dispatch_event(target_sign: i32, event_name: &str, body: WhiskerValue) -> bool {
830    let plan = with_renderer(
831        |r| r.plan_event_dispatch(target_sign, event_name, &body),
832        EventDispatchPlan::default(),
833    );
834    for (listener, event) in plan.firings {
835        listener(event);
836    }
837    plan.consumed
838}
839
840pub fn set_root(page: Element) {
841    with_renderer(|r| r.set_root(page), ())
842}
843
844pub fn flush() {
845    with_renderer(|r| r.flush(), ())
846}
847
848/// Opaque platform pointer for `handle`. Phase 7-Φ.H.2.3 — used by
849/// `whisker-driver`'s `ElementRef::invoke` to call the C bridge
850/// without leaking the bridge's `WhiskerElement*` type into the
851/// runtime crate's public surface. Returns `0` if no renderer is
852/// installed or the renderer doesn't have a native pointer for
853/// `handle`.
854pub fn module_component_ptr(handle: Element) -> usize {
855    if is_phantom(handle) {
856        return 0;
857    }
858    CURRENT_RENDERER.with_borrow(|slot| match slot.as_ref() {
859        Some(r) => r.module_component_ptr(handle),
860        None => 0,
861    })
862}