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(¤t).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}