swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! Navigation.

use super::*;
use crate::abi;
use crate::resolve;
use crate::mangle;

// ── Navigation ──

mangle::modifier_types! {
    /// `.navigationTitle(_:)` writes a preference and a toolbar key, so it
    /// nests twice over whatever receiver it is applied to.
    pub(crate) NAV_TITLE_INNER_T = ty("TransactionalPreferenceTransformModifier").of(vec![ty("NavigationTitleKey")]);
    /// The outer half of `.navigationTitle(_:)`.
    pub(crate) NAV_TITLE_OUTER_T = ty("_PreferenceTransformModifier").of(vec![ty("ToolbarKey")]);
}

mangle::result_types! {
    /// `NavigationLink<Text, AnyView>`
    /// `NavigationLink<Text, AnyView>`
    pub(crate) NAV_LINK_T = ty("NavigationLink").of(vec![ty("Text"), ty("AnyView")]);
}


/// Apply `.navigationTitle(_:)`.
pub fn navigation_title(view: &ViewHandle, title: &str) -> ViewHandle {
    unsafe {
        let ss = abi::swift_string(title);
        let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
        let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
        let (k0, k1, k2, k3) = localized_string_key(s0, s1);
        let (inner, _, _) = NAV_TITLE_INNER_T.applied_to(view.meta);
        let (meta, wt, size) = NAV_TITLE_OUTER_T.applied_to(inner);
        // x0-x3 = key, then Self.Type and the Self: View witness table
        let r = abi::call_modifier(resolve::nav_title_fn(), view.value_ptr(), view.meta, view.wt, &[k0, k1, k2, k3], [0.0; 4], size);
        wrap_owned(r, meta, wt)
    }
}

/// Apply `.navigationTitle(_:)` with a `Binding<String>` — an *editable* title.
///
/// The title bar becomes a rename field: the user edits it in place and the new
/// name is written back through the binding. Only the fixed-string overload was
/// bound, so a document could be shown under a name but never renamed from
/// where the name appears.
pub fn navigation_title_bound(
    view: &ViewHandle,
    state: &super::BoundState<String>,
) -> ViewHandle {
    unsafe {
        let binding = super::text_binding(state);
        let b = super::primitives::binding_words(&binding);
        // `Binding<String>` is four words, the same shape a `TextField` takes.
        super::opaque_modifier(
            resolve::nav_title_bound_fn(),
            resolve::nav_title_bound_oret(),
            view,
            &b,
            [0.0; 4],
        )
    }
}

/// `NavigationLink(_:destination:)`.
pub fn navigation_link(label: &str, destination: &ViewHandle) -> ViewHandle {
    unsafe {
        let ss = abi::swift_string(label);
        let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
        let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
        let (k0, k1, k2, k3) = localized_string_key(s0, s1);

        let (known_meta, known_wt, known_size) = result_type(&NAV_LINK_T);
        // `NavigationLink<Label, Destination>`: two parameters, then their
        // `View` conformances, so the destination is at 1 and its witness at 3.
        let typed = super::substitute_content(known_meta, 4, 1, 3, destination.meta, destination.wt);
        let (meta, wt) = typed.unwrap_or((known_meta, known_wt));
        let size = if typed.is_some() { abi::type_bytes(meta) } else { known_size };

        // `destination` is `@in`, which is *consumed*: whatever x4 points at is
        // taken over by the callee. Boxed, that was one `AnyView` word; typed,
        // it is the destination's own bytes, so hand over a copy made through
        // its value witness rather than the caller's only copy. Both buffers are
        // declared here because both have to outlive the call.
        let mut owned: Vec<u8> = match typed {
            Some(_) => {
                let vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(destination.meta);
                let mut buf = vec![0u8; abi::type_bytes(destination.meta)];
                if vwt.size > 0 {
                    (vwt.initialize_with_copy)(
                        buf.as_mut_ptr() as *mut c_void,
                        destination.value_ptr() as *mut c_void,
                        destination.meta,
                    );
                }
                buf
            }
            None => Vec::new(),
        };
        let mut boxed_word: u64 =
            if typed.is_some() { 0 } else { destination.to_anyview_word() };
        let (dest_ptr, dest_meta, dest_wt) = if typed.is_some() {
            (owned.as_mut_ptr() as *mut u64, destination.meta, destination.wt)
        } else {
            (&mut boxed_word as *mut u64, resolve::anyview_meta(), resolve::anyview_wt())
        };

        let mut result = vec![0u8; size];
        // init(_: LocalizedStringKey, destination: D)
        //   x0-x3 = key, x4 = @in D, x5 = D.Type, x6 = D: View WT, x8 = @out
        core::arch::asm!("blr {f}", f = in(reg) resolve::nav_link_init(),
            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
            in("x4") dest_ptr,
            in("x5") dest_meta,
            in("x6") dest_wt,
            in("x8") result.as_mut_ptr(),
            lateout("x7") _,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
            clobber_abi("C"),
        );
        // The callee consumed what x4 pointed at, so this side must not destroy
        // it — forgotten rather than dropped.
        core::mem::forget(owned);
        wrap_owned(result, meta, wt)
    }
}

/// `NavigationLink(destination:) { label }` — a link whose label is a view.
///
/// [`navigation_link`] takes a string, which is all a plain row needs; a list
/// row is a whole subtree, and this is the overload that accepts one.
///
/// The destination travels `@in` and is *consumed*, as in `navigation_link`, so
/// what x0 points at is handed over rather than borrowed. The label is an
/// ordinary `@ViewBuilder` closure, and the four generic words after it are
/// `Label.Type`, `Destination.Type` and their `View` witnesses — read off the
/// IR rather than assumed.
pub fn navigation_link_labelled(label: &ViewHandle, destination: &ViewHandle) -> ViewHandle {
    unsafe {
        let (known_meta, known_wt, _) = result_type(&NAV_LINK_T);
        // `NavigationLink<Label, Destination>`: two parameters, then their
        // conformances.
        let args = [label.meta, destination.meta, label.wt, destination.wt];
        let meta = abi::generic_metadata_from(known_meta, &args);
        let wt = if meta.is_null() { core::ptr::null() } else { abi::get_view_wt(meta) };
        if wt.is_null() {
            // Nothing is lost by the string form's shape: box the label instead.
            return navigation_link("", destination);
        }
        let _ = known_wt;

        let kid = super::Child::new(label, super::Content::Concrete);
        let handles = kid.element();
        let payload = super::TuplePayload { children: handles.as_ptr(), count: 1 };
        let mut ctx = abi::ClosureCtx::new(
            super::emit_tuple,
            &payload as *const super::TuplePayload as *mut c_void,
        );

        // Consumed, so hand over an owned copy made through the value witness.
        let vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(destination.meta);
        let mut owned = vec![0u8; abi::type_bytes(destination.meta)];
        if vwt.size > 0 {
            (vwt.initialize_with_copy)(
                owned.as_mut_ptr() as *mut c_void,
                destination.value_ptr() as *mut c_void,
                destination.meta,
            );
        }

        let mut result = vec![0u8; abi::type_bytes(meta)];
        core::arch::asm!("blr {f}", f = in(reg) resolve::nav_link_label_init(),
            in("x0") owned.as_mut_ptr(),
            in("x1") abi::swift_closure_thunk as *const c_void,
            in("x2") &mut ctx as *mut abi::ClosureCtx,
            in("x3") label.meta,
            in("x4") destination.meta,
            in("x5") label.wt,
            in("x6") destination.wt,
            in("x8") result.as_mut_ptr(),
            lateout("x7") _,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
            clobber_abi("C"),
        );
        core::mem::forget(owned);
        wrap_owned(result, meta, wt)
    }
}


/// `(Int) -> some View` — the destination builder, given the value navigated to.
///
/// `D` is generic, so the value arrives by *address* rather than in a register,
/// the same way a `ForEach`'s element does.
unsafe extern "C" fn emit_destination(out: *mut u8, value: i64, payload: *mut c_void) {
    unsafe {
        let builder = &*(payload as *const Box<dyn Fn(i64) -> ViewHandle>);
        let view = builder(*(value as *const i64));
        *(out as *mut u64) = view.into_anyview_word();
    }
}

/// `.navigationDestination(for: Int.self) { value in … }` — value-based routing.
///
/// This is how a `NavigationStack` is driven in modern SwiftUI: rows push a
/// *value*, and the stack asks this modifier what view that value means. Only
/// the `isPresented:` overload was bound, which is the older one-destination-
/// per-flag shape — fine for a single detail sheet, and no use for a stack that
/// pushes rows.
///
/// `Int` is the value type. Anything `Hashable` would do, but a Rust type
/// cannot be made to conform without synthesising a nominal Swift type, and an
/// integer identifier is what a Rust-owned model has to hand anyway — the same
/// reasoning [`super::list_identified`] follows.
pub fn navigation_destination_for(
    view: &ViewHandle,
    destination: impl Fn(i64) -> ViewHandle + 'static,
) -> ViewHandle {
    unsafe {
        let boxed: Box<dyn Fn(i64) -> ViewHandle> = Box::new(destination);
        let payload = Box::into_raw(Box::new(boxed)) as *mut c_void;
        let ctx = Box::leak(Box::new(abi::IndexClosureCtx::new(emit_destination, payload)))
            as *mut abi::IndexClosureCtx;

        let int_meta = resolve::int_meta();
        let hashable = super::conformance_wt(int_meta, resolve::hashable_proto());
        assert!(!hashable.is_null(), "Int: Hashable");

        let subst = [view.meta, int_meta, resolve::anyview_meta(),
                     view.wt, hashable, resolve::anyview_wt()];
        let (meta, wt, size) = super::opaque_result_type(resolve::nav_dest_for_oret(), &subst);
        let mut result = vec![0u8; size];
        let this = view.value_ptr();
        // navigationDestination<D: Hashable, C: View>(for: D.Type, destination: (D) -> C)
        //   x0 = the *metatype* argument — `D.self` is a value here, not a
        //   generic parameter — x1/x2 = the closure, x3 = Self.Type,
        //   x4 = D.Type, x5 = C.Type, x6 = Self: View WT,
        //   x7 = D: Hashable WT, [sp] = C: View WT, x8 = @out, x20 = self
        core::arch::asm!(
            "sub sp, sp, #16",
            "str {cw}, [sp]",
            "blr {f}",
            "add sp, sp, #16",
            f = in(reg) resolve::nav_dest_for_fn(),
            cw = in(reg) subst[5],
            in("x0") int_meta,
            in("x1") abi::index_closure_thunk as *const c_void,
            in("x2") ctx,
            in("x3") subst[0],
            in("x4") subst[1],
            in("x5") subst[2],
            in("x6") subst[3],
            in("x7") subst[4],
            in("x8") result.as_mut_ptr(),
            in("x20") this,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
        );
        wrap_owned(result, meta, wt)
    }
}