swiftui-native 0.0.1

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

use super::*;
use crate::abi;
use crate::resolve;
use core::ffi::c_void;
use crate::mangle::{self, swift_type};

// ── Presentation ──

/// Apply `.alert(_:isPresented:actions:)`.
///
/// Both this and `sheet` push their generic arguments onto the stack: eight
/// registers are already taken by the title, the binding and the closure.
/// Shared tail of `alert` and `alert_bound`.
///
/// # Safety
/// `binding` must be a `Binding<Bool>`.
pub(crate) unsafe fn alert_with_binding(
    view: &ViewHandle,
    title: &str, actions: &ViewHandle,
    binding: &[u8],
) -> 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 b0 = u64::from_le_bytes(binding[0..8].try_into().unwrap());
        let b1 = u64::from_le_bytes(binding[8..16].try_into().unwrap());
        let b2 = binding[16] as u64;

        // The actions builder escapes, like a sheet's content: it runs when the
        // alert is presented, not now.
        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            emit_tuple,
            Child::leak(actions, Content::Concrete),
        ))) as *mut abi::ClosureCtx;

        let subst = [
            view.meta,                        // Self.Type — the concrete receiver
            actions.meta,          // content — the closure emits AnyView
            view.wt,                          // Self: View
            actions.wt,            // content: View
        ];
        let (meta, wt, size) = opaque_result_type(resolve::alert_oret(), &subst);
        let mut result = vec![0u8; size];
        let this = view.value_ptr();
        // alert(_:isPresented:actions:)
        //   x0-x3 = key, x4-x6 = Binding<Bool>, x7 = actions fn,
        //   [sp] = actions ctx, [sp+8] = Self.Type, [sp+16] = A.Type,
        //   [sp+24] = Self: View WT, [sp+32] = A: View WT
        core::arch::asm!(
            "sub sp, sp, #48",
            "stp {cx}, {sm}, [sp]",
            "stp {am}, {sw}, [sp, #16]",
            "str {aw}, [sp, #32]",
            "blr {f}",
            "add sp, sp, #48",
            f = in(reg) resolve::alert_fn(),
            cx = in(reg) ctx,
            sm = in(reg) view.meta,
            am = in(reg) actions.meta,
            sw = in(reg) view.wt,
            aw = in(reg) actions.wt,
            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
            in("x4") b0, in("x5") b1, in("x6") b2,
            in("x7") abi::swift_closure_thunk as *const c_void,
            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)
    }
}

/// `alert(_:isPresented:actions:message:)` — an alert that explains itself.
///
/// The `message:` closure is the line under the title: what the buttons are
/// about, and the difference between "Delete" and "Delete — this cannot be
/// undone". Only the title-and-buttons overload was bound, so an alert could
/// ask but not say why.
///
/// # Safety
/// `binding` must be a live `Binding<Bool>`.
pub(crate) unsafe fn alert_message_with_binding(
    view: &ViewHandle,
    title: &str,
    actions: &ViewHandle,
    message: &ViewHandle,
    binding: &[u8],
) -> 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 b0 = u64::from_le_bytes(binding[0..8].try_into().unwrap());
        let b1 = u64::from_le_bytes(binding[8..16].try_into().unwrap());
        let b2 = binding[16] as u64;

        // Both builders escape: they run when the alert is presented.
        let actions_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            emit_tuple,
            Child::leak(actions, Content::Concrete),
        ))) as *mut abi::ClosureCtx;
        let message_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            emit_tuple,
            Child::leak(message, Content::Concrete),
        ))) as *mut abi::ClosureCtx;

        let subst = [
            view.meta,
            actions.meta,
            message.meta,
            view.wt,
            actions.wt,
            message.wt,
        ];
        let (meta, wt, size) = opaque_result_type(resolve::alert_msg_oret(), &subst);
        let mut result = vec![0u8; size];
        let this = view.value_ptr();
        // alert(_:isPresented:actions:message:)
        //   x0-x3 = key, x4-x6 = Binding<Bool>, x7 = actions fn,
        //   [sp]    = actions ctx,  [sp+8]  = message fn,
        //   [sp+16] = message ctx,  [sp+24] = Self.Type,
        //   [sp+32] = A.Type,       [sp+40] = B.Type,
        //   [sp+48] = Self: View,   [sp+56] = A: View,   [sp+64] = B: View
        core::arch::asm!(
            "sub sp, sp, #80",
            "stp {ac}, {mf}, [sp]",
            "stp {mc}, {sm}, [sp, #16]",
            "stp {am}, {mm}, [sp, #32]",
            "stp {sw}, {aw}, [sp, #48]",
            "str {mw}, [sp, #64]",
            "blr {f}",
            "add sp, sp, #80",
            f = in(reg) resolve::alert_msg_fn(),
            ac = in(reg) actions_ctx,
            mf = in(reg) abi::swift_closure_thunk as *const c_void,
            mc = in(reg) message_ctx,
            sm = in(reg) subst[0],
            am = in(reg) subst[1],
            mm = in(reg) subst[2],
            sw = in(reg) subst[3],
            aw = in(reg) subst[4],
            mw = in(reg) subst[5],
            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
            in("x4") b0, in("x5") b1, in("x6") b2,
            in("x7") abi::swift_closure_thunk as *const c_void,
            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)
    }
}

/// `alert(_:isPresented:actions:message:)` with a fixed flag, which cannot be
/// dismissed — see [`alert`].
pub fn alert_message(
    view: &ViewHandle,
    title: &str,
    actions: &ViewHandle,
    message: &ViewHandle,
    presented: bool,
) -> ViewHandle {
    unsafe {
        let binding = constant_binding(
            &[u8::from(presented)],
            resolve::bool_meta(),
            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
        );
        alert_message_with_binding(view, title, actions, message, &binding)
    }
}

/// `alert(_:isPresented:actions:message:)` bound to Rust state.
pub fn alert_message_bound(
    view: &ViewHandle,
    title: &str,
    actions: &ViewHandle,
    message: &ViewHandle,
    state: &BoundState<bool>,
) -> ViewHandle {
    unsafe {
        let binding = bool_binding_of(state);
        alert_message_with_binding(view, title, actions, message, &binding)
    }
}

/// `alert` with a fixed flag. It cannot be dismissed: SwiftUI treats a
/// `Binding.constant` as read-only, so the presentation has no way to write
/// `false` back. Use [`alert_bound`] for one the user can close.
pub fn alert(view: &ViewHandle, title: &str, actions: &ViewHandle, presented: bool) -> ViewHandle {
    unsafe {
        let binding = constant_binding(
            &[u8::from(presented)],
            resolve::bool_meta(),
            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
        );
        alert_with_binding(view, title, actions, &binding)
    }
}

/// `alert` bound to Rust state, so dismissing it writes `false` back.
pub fn alert_bound(view: &ViewHandle, title: &str, actions: &ViewHandle, state: &BoundState<bool>) -> ViewHandle {
    unsafe {
        let binding = bool_binding_of(state);
        alert_with_binding(view, title, actions, &binding)
    }
}


/// Apply `.sheet(isPresented:content:)`.
/// Shared tail of `sheet` and `sheet_bound`.
///
/// # Safety
/// `binding` must be a `Binding<Bool>`.
pub(crate) unsafe fn sheet_with_binding(
    view: &ViewHandle,
    content: &ViewHandle,
    binding: &[u8],
) -> ViewHandle {
    unsafe { sheet_with_dismiss(view, content, binding, [0, 0]) }
}

/// The same, with the `onDismiss:` closure spelled out.
///
/// `(() -> ())?` is two words and `nil` is both of them null, which is what
/// [`sheet_with_binding`] passes — so a sheet could be opened and closed and
/// nothing on the Rust side would hear about the closing.
///
/// # Safety
/// `binding` must be a live `Binding<Bool>`, and `dismiss` either `[0, 0]` or a
/// live Swift closure's two words.
pub(crate) unsafe fn sheet_with_dismiss(
    view: &ViewHandle,
    content: &ViewHandle,
    binding: &[u8],
    dismiss: [u64; 2],
) -> ViewHandle {
    unsafe {
        presenting_with_dismiss(
            view,
            content,
            binding,
            dismiss,
            resolve::sheet_fn(),
            resolve::sheet_oret(),
        )
    }
}

/// `sheet(isPresented:onDismiss:content:)` and `fullScreenCover(…)`, which take
/// the same arguments in the same registers and differ only in which symbol is
/// called — so one body serves both rather than two copies of this assembly.
///
/// # Safety
/// As [`sheet_with_dismiss`], and `f`/`oret` must be a matching modifier and
/// opaque-result descriptor for a presentation of that shape.
pub(crate) unsafe fn presenting_with_dismiss(
    view: &ViewHandle,
    content: &ViewHandle,
    binding: &[u8],
    dismiss: [u64; 2],
    f: *const c_void,
    oret: *const c_void,
) -> ViewHandle {
    unsafe {
        let b0 = u64::from_le_bytes(binding[0..8].try_into().unwrap());
        let b1 = u64::from_le_bytes(binding[8..16].try_into().unwrap());
        let b2 = binding[16] as u64;

        // A sheet's content closure *escapes*: SwiftUI stores it and calls it
        // when the sheet is presented, long after this function returns. Both
        // the context and the child handles it reads must therefore outlive the
        // call, so they are leaked rather than left on the stack.
        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            emit_tuple,
            Child::leak(content, Content::Concrete),
        ))) as *mut abi::ClosureCtx;

        let subst = [
            view.meta,                        // Self.Type — the concrete receiver
            content.meta,          // content — the closure emits AnyView
            view.wt,                          // Self: View
            content.wt,            // content: View
        ];
        let (meta, wt, size) = opaque_result_type(oret, &subst);
        let mut result = vec![0u8; size];
        let this = view.value_ptr();
        // sheet(isPresented:onDismiss:content:)
        //   x0-x2 = Binding<Bool>, x3/x4 = onDismiss (nil = null fn, null ctx),
        //   x5 = content fn, x6 = content ctx, x7 = Self.Type,
        //   [sp] = C.Type, [sp+8] = Self: View WT, [sp+16] = C: View WT
        core::arch::asm!(
            "sub sp, sp, #32",
            "stp {cm}, {sw}, [sp]",
            "str {cw}, [sp, #16]",
            "blr {f}",
            "add sp, sp, #32",
            f = in(reg) f,
            cm = in(reg) content.meta,
            sw = in(reg) view.wt,
            cw = in(reg) content.wt,
            in("x0") b0, in("x1") b1, in("x2") b2,
            in("x3") dismiss[0], in("x4") dismiss[1],
            in("x5") abi::swift_closure_thunk as *const c_void,
            in("x6") ctx,
            in("x7") view.meta,
            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)
    }
}

/// `sheet(isPresented:onDismiss:content:)` bound to Rust state, with a
/// callback for the closing.
///
/// A sheet that saves what was edited has to know when it was dismissed, and
/// `onDismiss` was hardwired to `nil` — so the sheet could be opened and closed
/// and nothing on the Rust side would hear about it.
pub fn sheet_bound_dismiss(
    view: &ViewHandle,
    content: &ViewHandle,
    state: &BoundState<bool>,
    on_dismiss: impl Fn() + 'static,
) -> ViewHandle {
    unsafe {
        let binding = bool_binding_of(state);
        let words = super::closures::action_words(on_dismiss);
        sheet_with_dismiss(view, content, &binding, words)
    }
}

/// `sheet` with a fixed flag. It cannot be dismissed: SwiftUI treats a
/// `Binding.constant` as read-only, so the presentation has no way to write
/// `false` back. Use [`sheet_bound`] for one the user can close.
pub fn sheet(view: &ViewHandle, content: &ViewHandle, presented: bool) -> ViewHandle {
    unsafe {
        let binding = constant_binding(
            &[u8::from(presented)],
            resolve::bool_meta(),
            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
        );
        sheet_with_binding(view, content, &binding)
    }
}

/// `sheet` bound to Rust state, so dismissing it writes `false` back.
pub fn sheet_bound(view: &ViewHandle, content: &ViewHandle, state: &BoundState<bool>) -> ViewHandle {
    unsafe {
        let binding = bool_binding_of(state);
        sheet_with_binding(view, content, &binding)
    }
}