swiftui-native 0.0.1

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

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

// ── Namespace-keyed modifiers ──

/// A fresh `Namespace.ID` — what `@Namespace` produces.
///
/// The one property wrapper of the twelve that needs nothing installed: the id
/// comes from a counter inside SwiftUI, not from a view graph, so it can be
/// minted here and handed to any modifier that takes one.
///
/// Both halves are one `Int`, so both travel in a register. `Namespace()`
/// compiles to a literal zero — the SIL for `let a = Namespace()` is
/// `integer_literal $Builtin.Int64, 0` and nothing else — and the id is minted
/// by `wrappedValue`, whose convention is `(Namespace) -> Namespace.ID`: self
/// by *value* in x20, result in x0.
///
/// This used to pass a *pointer* to the namespace and read the result out of an
/// x8 buffer, as if both were address-only. Neither register was written, so
/// every call returned a leftover zero, and zero is what SwiftUI stores for a
/// namespace that was never installed in a view graph — so nothing ever
/// complained. Called properly it returns 1, 2, 3, … from SwiftUI's own
/// counter, exactly as Swift source does.
///
/// The counter advances on every *read*, not every `Namespace()`: in Swift,
/// `a.wrappedValue` twice yields two different ids. So the number depends on
/// how many namespaces the process has looked at, which is why the parity
/// suite compares these modifiers with the id normalised away.
/// # Safety
/// SwiftUI must be loaded.
pub unsafe fn namespace_id() -> u64 {
    unsafe {
        let namespace = abi::call_regs(resolve::namespace_init(), abi::ints(&[]), [0.0; 4], 0)[0];
        abi::call_regs(resolve::namespace_wrapped(), abi::ints(&[]), [0.0; 4], namespace)[0]
    }
}

/// A `@Namespace`, shared between the views that should match.
///
/// [`namespace_id`] mints a *fresh* id on every call, because SwiftUI's
/// counter advances on each read. That is right for a modifier that scopes one
/// subtree and wrong for one whose whole purpose is pairing: two views handed
/// separately-minted ids are in different namespaces and can never match, so
/// `matchedGeometryEffect` built the correct Swift value and did nothing. The
/// `_in` variants below take one of these instead, which is how SwiftUI spells
/// it — `@Namespace var ns` once, `in: ns` at each use.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Namespace(u64);

impl Namespace {
    /// Mint a new namespace.
    pub fn new() -> Self {
        unsafe { Self(namespace_id()) }
    }

    /// The `Namespace.ID` as SwiftUI stores it.
    pub fn raw(self) -> u64 {
        self.0
    }
}

impl Default for Namespace {
    fn default() -> Self {
        Self::new()
    }
}

/// Apply `.focusScope(_:)`.
pub fn focus_scope(view: &ViewHandle) -> ViewHandle {
    unsafe {
        opaque_modifier(
            resolve::focus_scope_fn(),
            resolve::focus_scope_oret(),
            view,
            &[namespace_id()],
            [0.0; 4],
        )
    }
}

/// Apply `.prefersDefaultFocus(_:in:)`.
pub fn prefers_default_focus(view: &ViewHandle, prefers: bool) -> ViewHandle {
    unsafe {
        opaque_modifier(
            resolve::prefers_default_focus_fn(),
            resolve::prefers_default_focus_oret(),
            view,
            &[u64::from(prefers), namespace_id()],
            [0.0; 4],
        )
    }
}

swift_statics! {
    /// A member of `SwiftUI.MatchedGeometryProperties`.
    pub enum GeometryProperties {
        Position = c"$s7SwiftUI25MatchedGeometryPropertiesV8positionACvgZ",
        Size = c"$s7SwiftUI25MatchedGeometryPropertiesV4sizeACvgZ",
        Frame = c"$s7SwiftUI25MatchedGeometryPropertiesV5frameACvgZ",
    }
}

/// Apply `.matchedGeometryEffect(id:in:properties:anchor:isSource:)`.
pub fn matched_geometry_effect(
    view: &ViewHandle,
    id: HashableValue<'_>,
    properties: &[GeometryProperties],
    anchor: (f64, f64),
    is_source: bool,
) -> ViewHandle {
    unsafe {
        let (bytes, meta, wt) = id.parts();
        let g: Vec<&core::ffi::CStr> = properties.iter().map(|p| p.getter()).collect();
        let props = option_set_word(resolve::matched_geometry_properties_ma(), &g);
        generic1_modifier_ints(
            resolve::matched_geometry_effect_fn(),
            resolve::matched_geometry_effect_oret(),
            view,
            meta,
            wt,
            &[bytes.as_ptr() as u64, namespace_id(), props, u64::from(is_source)],
            [anchor.0, anchor.1, 0.0, 0.0],
        )
    }
}

/// Apply `.matchedGeometryEffect(id:in:properties:anchor:isSource:)` in a
/// *shared* namespace.
///
/// The pairing spelling: give the source and the destination the same `id` and
/// the same [`Namespace`], and SwiftUI matches their geometry.
/// [`matched_geometry_effect`] mints a namespace per call, so nothing it makes
/// can ever pair.
pub fn matched_geometry_effect_in(
    view: &ViewHandle,
    id: HashableValue<'_>,
    namespace: Namespace,
    properties: &[GeometryProperties],
    anchor: (f64, f64),
    is_source: bool,
) -> ViewHandle {
    unsafe {
        let (bytes, meta, wt) = id.parts();
        let g: Vec<&core::ffi::CStr> = properties.iter().map(|p| p.getter()).collect();
        let props = option_set_word(resolve::matched_geometry_properties_ma(), &g);
        generic1_modifier_ints(
            resolve::matched_geometry_effect_fn(),
            resolve::matched_geometry_effect_oret(),
            view,
            meta,
            wt,
            &[bytes.as_ptr() as u64, namespace.raw(), props, u64::from(is_source)],
            [anchor.0, anchor.1, 0.0, 0.0],
        )
    }
}

/// Apply `.matchedTransitionSource(id:in:)`.
pub fn matched_transition_source(view: &ViewHandle, id: HashableValue<'_>) -> ViewHandle {
    unsafe {
        let (bytes, meta, wt) = id.parts();
        generic1_modifier_ints(
            resolve::matched_transition_source_fn(),
            resolve::matched_transition_source_oret(),
            view,
            meta,
            wt,
            &[bytes.as_ptr() as u64, namespace_id()],
            [0.0; 4],
        )
    }
}

/// Apply `.accessibilityLinkedGroup(id:in:)`.
pub fn accessibility_linked_group(view: &ViewHandle, id: HashableValue<'_>) -> ViewHandle {
    unsafe {
        let (bytes, meta, wt) = id.parts();
        generic1_modifier_ints(
            resolve::a11y_linked_group_fn(),
            resolve::a11y_linked_group_oret(),
            view,
            meta,
            wt,
            &[bytes.as_ptr() as u64, namespace_id()],
            [0.0; 4],
        )
    }
}

/// Apply `.accessibilityRotorEntry(id:in:)`.
pub fn accessibility_rotor_entry(view: &ViewHandle, id: HashableValue<'_>) -> ViewHandle {
    unsafe {
        let (bytes, meta, wt) = id.parts();
        generic1_modifier_ints(
            resolve::a11y_rotor_entry_fn(),
            resolve::a11y_rotor_entry_oret(),
            view,
            meta,
            wt,
            &[bytes.as_ptr() as u64, namespace_id()],
            [0.0; 4],
        )
    }
}

/// Apply `.glassEffectUnion(id:namespace:)`.
pub fn glass_effect_union(view: &ViewHandle, id: Option<HashableValue<'_>>) -> ViewHandle {
    unsafe {
        // The parameter is `A?`, and `A` is generic, so the optional is
        // address-only no matter what `A` turns out to be.
        let (bytes, meta, wt) = match id {
            Some(v) => v.parts(),
            None => HashableValue::Int(0).parts(),
        };
        let opt = abi::generic_metadata1(resolve::optional_ma(), meta);
        let mut buf = vec![0u8; abi::type_bytes(opt)];
        if id.is_some() {
            buf[..bytes.len()].copy_from_slice(&bytes);
        }
        abi::store_optional_tag(&mut buf, meta, id.is_some());
        generic1_modifier_ints(
            resolve::glass_effect_union_fn(),
            resolve::glass_effect_union_oret(),
            view,
            meta,
            wt,
            &[buf.as_ptr() as u64, namespace_id()],
            [0.0; 4],
        )
    }
}

/// Apply `.listRowBackground(_:)`.
pub fn list_row_background(view: &ViewHandle, background: Option<&ViewHandle>) -> ViewHandle {
    unsafe {
        // `V?` with `V` generic is address-only; for `AnyView` the payload is
        // one reference, so `nil` is a null word.
        let mut word: u64 = background.map_or(0, leak_retained);
        generic1_modifier_ints(
            resolve::list_row_background_fn(),
            resolve::list_row_background_oret(),
            view,
            resolve::anyview_meta(),
            resolve::anyview_wt(),
            &[&mut word as *mut u64 as u64],
            [0.0; 4],
        )
    }
}

swift_statics! {
    /// A member of `SwiftUI.PointerStyle`.
    pub enum PointerStyle {
        /// What the pointer would be with no modifier at all — an arrow
        /// over most things, an I-beam over text.
        Default = c"$s7SwiftUI12PointerStyleV7defaultACvgZ",
        Link = c"$s7SwiftUI12PointerStyleV4linkACvgZ",
        ZoomIn = c"$s7SwiftUI12PointerStyleV6zoomInACvgZ",
        GrabActive = c"$s7SwiftUI12PointerStyleV10grabActiveACvgZ",
        ColumnResize = c"$s7SwiftUI12PointerStyleV12columnResizeACvgZ",
        RectSelection = c"$s7SwiftUI12PointerStyleV13rectSelectionACvgZ",
        HorizontalText = c"$s7SwiftUI12PointerStyleV14horizontalTextACvgZ",
        VerticalText = c"$s7SwiftUI12PointerStyleV12verticalTextACvgZ",
        GrabIdle = c"$s7SwiftUI12PointerStyleV8grabIdleACvgZ",
        RowResize = c"$s7SwiftUI12PointerStyleV9rowResizeACvgZ",
        ZoomOut = c"$s7SwiftUI12PointerStyleV7zoomOutACvgZ",
    }
}

/// Apply `.pointerStyle(_:)`.
pub fn pointer_style(view: &ViewHandle, style: Option<PointerStyle>) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::pointer_style_ma());
        let v = style.map(|s| resilient_static(meta, s.getter()));
        let b = opt_resilient_bytes(meta, swift_type!(opt SwiftUI.PointerStyle).as_bytes(), v.as_deref());
        modifier_indirect(resolve::pointer_style_fn(), resolve::pointer_style_oret(), view, &b)
    }
}

swift_statics! {
    /// A member of `SwiftUI.Text.WritingDirectionStrategy`.
    pub enum WritingDirectionStrategy {
        Default = c"$s7SwiftUI4TextVAAE24WritingDirectionStrategyV7defaultAEvgZ",
        LayoutBased = c"$s7SwiftUI4TextVAAE24WritingDirectionStrategyV11layoutBasedAEvgZ",
        ContentBased = c"$s7SwiftUI4TextVAAE24WritingDirectionStrategyV12contentBasedAEvgZ",
    }
}

/// Apply `.writingDirection(strategy:)`.
pub fn writing_direction(view: &ViewHandle, strategy: WritingDirectionStrategy) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::writing_direction_strategy_ma());
        let s = resilient_static(meta, strategy.getter());
        modifier_indirect(
            resolve::writing_direction_fn(),
            resolve::writing_direction_oret(),
            view,
            &s,
        )
    }
}

#[cfg(test)]
mod namespace_tests {
    use super::*;

    /// A `Namespace.ID` read the wrong way is a leftover register.
    ///
    /// `wrappedValue` mints ids from a counter that starts at 1, so a correct
    /// read is non-zero and a repeat read is larger. Zero is what the getter
    /// yields when it is called as though self and the result were both
    /// address-only — the registers it would have written are simply never
    /// touched — and zero is also a value SwiftUI accepts without complaint,
    /// which is why this went unnoticed for so long.
    ///
    /// `>` rather than `+ 1`: the counter is process-wide and the other tests
    /// draw from it too.
    #[test]
    fn a_namespace_id_comes_from_swiftuis_counter() {
        unsafe {
            let first = namespace_id();
            let second = namespace_id();
            assert_ne!(first, 0, "a namespace read through the getter is never zero");
            assert!(second > first, "ids advance: {first} then {second}");
        }
    }

    /// Every namespace-keyed modifier builds without faulting.
    #[test]
    fn namespace_modifiers_build() {
        let t = || text("Fx");
        let _ = focus_scope(&t());
        let _ = prefers_default_focus(&t(), true);
        let _ = matched_geometry_effect(
            &t(),
            HashableValue::Str("g"),
            &[GeometryProperties::Frame],
            (0.5, 0.5),
            true,
        );
        let _ = matched_transition_source(&t(), HashableValue::Str("m"));
        let _ = accessibility_linked_group(&t(), HashableValue::Str("l"));
        let _ = accessibility_rotor_entry(&t(), HashableValue::Str("r"));
        let _ = glass_effect_union(&t(), Some(HashableValue::Int(4)));
        let _ = glass_effect_id(&t(), Some(HashableValue::Str("g")));
        let _ = glass_effect_id(&t(), None);
        let _ = accessibility_labeled_pair(&t(), LabeledPairRole::Label, HashableValue::Str("p"));
    }
}