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