Skip to main content

swiftui_native/views/
namespace.rs

1//! Namespace-keyed modifiers.
2
3use super::*;
4use crate::abi;
5use crate::resolve;
6use crate::mangle::swift_type;
7
8// ── Namespace-keyed modifiers ──
9
10/// A fresh `Namespace.ID` — what `@Namespace` produces.
11///
12/// The one property wrapper of the twelve that needs nothing installed: the id
13/// comes from a counter inside SwiftUI, not from a view graph, so it can be
14/// minted here and handed to any modifier that takes one.
15///
16/// Both halves are one `Int`, so both travel in a register. `Namespace()`
17/// compiles to a literal zero — the SIL for `let a = Namespace()` is
18/// `integer_literal $Builtin.Int64, 0` and nothing else — and the id is minted
19/// by `wrappedValue`, whose convention is `(Namespace) -> Namespace.ID`: self
20/// by *value* in x20, result in x0.
21///
22/// This used to pass a *pointer* to the namespace and read the result out of an
23/// x8 buffer, as if both were address-only. Neither register was written, so
24/// every call returned a leftover zero, and zero is what SwiftUI stores for a
25/// namespace that was never installed in a view graph — so nothing ever
26/// complained. Called properly it returns 1, 2, 3, … from SwiftUI's own
27/// counter, exactly as Swift source does.
28///
29/// The counter advances on every *read*, not every `Namespace()`: in Swift,
30/// `a.wrappedValue` twice yields two different ids. So the number depends on
31/// how many namespaces the process has looked at, which is why the parity
32/// suite compares these modifiers with the id normalised away.
33/// # Safety
34/// SwiftUI must be loaded.
35pub unsafe fn namespace_id() -> u64 {
36    unsafe {
37        let namespace = abi::call_regs(resolve::namespace_init(), abi::ints(&[]), [0.0; 4], 0)[0];
38        abi::call_regs(resolve::namespace_wrapped(), abi::ints(&[]), [0.0; 4], namespace)[0]
39    }
40}
41
42/// A `@Namespace`, shared between the views that should match.
43///
44/// [`namespace_id`] mints a *fresh* id on every call, because SwiftUI's
45/// counter advances on each read. That is right for a modifier that scopes one
46/// subtree and wrong for one whose whole purpose is pairing: two views handed
47/// separately-minted ids are in different namespaces and can never match, so
48/// `matchedGeometryEffect` built the correct Swift value and did nothing. The
49/// `_in` variants below take one of these instead, which is how SwiftUI spells
50/// it — `@Namespace var ns` once, `in: ns` at each use.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct Namespace(u64);
53
54impl Namespace {
55    /// Mint a new namespace.
56    pub fn new() -> Self {
57        unsafe { Self(namespace_id()) }
58    }
59
60    /// The `Namespace.ID` as SwiftUI stores it.
61    pub fn raw(self) -> u64 {
62        self.0
63    }
64}
65
66impl Default for Namespace {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72/// Apply `.focusScope(_:)`.
73pub fn focus_scope(view: &ViewHandle) -> ViewHandle {
74    unsafe {
75        opaque_modifier(
76            resolve::focus_scope_fn(),
77            resolve::focus_scope_oret(),
78            view,
79            &[namespace_id()],
80            [0.0; 4],
81        )
82    }
83}
84
85/// Apply `.prefersDefaultFocus(_:in:)`.
86pub fn prefers_default_focus(view: &ViewHandle, prefers: bool) -> ViewHandle {
87    unsafe {
88        opaque_modifier(
89            resolve::prefers_default_focus_fn(),
90            resolve::prefers_default_focus_oret(),
91            view,
92            &[u64::from(prefers), namespace_id()],
93            [0.0; 4],
94        )
95    }
96}
97
98swift_statics! {
99    /// A member of `SwiftUI.MatchedGeometryProperties`.
100    pub enum GeometryProperties {
101        Position = c"$s7SwiftUI25MatchedGeometryPropertiesV8positionACvgZ",
102        Size = c"$s7SwiftUI25MatchedGeometryPropertiesV4sizeACvgZ",
103        Frame = c"$s7SwiftUI25MatchedGeometryPropertiesV5frameACvgZ",
104    }
105}
106
107/// Apply `.matchedGeometryEffect(id:in:properties:anchor:isSource:)`.
108pub fn matched_geometry_effect(
109    view: &ViewHandle,
110    id: HashableValue<'_>,
111    properties: &[GeometryProperties],
112    anchor: (f64, f64),
113    is_source: bool,
114) -> ViewHandle {
115    unsafe {
116        let (bytes, meta, wt) = id.parts();
117        let g: Vec<&core::ffi::CStr> = properties.iter().map(|p| p.getter()).collect();
118        let props = option_set_word(resolve::matched_geometry_properties_ma(), &g);
119        generic1_modifier_ints(
120            resolve::matched_geometry_effect_fn(),
121            resolve::matched_geometry_effect_oret(),
122            view,
123            meta,
124            wt,
125            &[bytes.as_ptr() as u64, namespace_id(), props, u64::from(is_source)],
126            [anchor.0, anchor.1, 0.0, 0.0],
127        )
128    }
129}
130
131/// Apply `.matchedGeometryEffect(id:in:properties:anchor:isSource:)` in a
132/// *shared* namespace.
133///
134/// The pairing spelling: give the source and the destination the same `id` and
135/// the same [`Namespace`], and SwiftUI matches their geometry.
136/// [`matched_geometry_effect`] mints a namespace per call, so nothing it makes
137/// can ever pair.
138pub fn matched_geometry_effect_in(
139    view: &ViewHandle,
140    id: HashableValue<'_>,
141    namespace: Namespace,
142    properties: &[GeometryProperties],
143    anchor: (f64, f64),
144    is_source: bool,
145) -> ViewHandle {
146    unsafe {
147        let (bytes, meta, wt) = id.parts();
148        let g: Vec<&core::ffi::CStr> = properties.iter().map(|p| p.getter()).collect();
149        let props = option_set_word(resolve::matched_geometry_properties_ma(), &g);
150        generic1_modifier_ints(
151            resolve::matched_geometry_effect_fn(),
152            resolve::matched_geometry_effect_oret(),
153            view,
154            meta,
155            wt,
156            &[bytes.as_ptr() as u64, namespace.raw(), props, u64::from(is_source)],
157            [anchor.0, anchor.1, 0.0, 0.0],
158        )
159    }
160}
161
162/// Apply `.matchedTransitionSource(id:in:)`.
163pub fn matched_transition_source(view: &ViewHandle, id: HashableValue<'_>) -> ViewHandle {
164    unsafe {
165        let (bytes, meta, wt) = id.parts();
166        generic1_modifier_ints(
167            resolve::matched_transition_source_fn(),
168            resolve::matched_transition_source_oret(),
169            view,
170            meta,
171            wt,
172            &[bytes.as_ptr() as u64, namespace_id()],
173            [0.0; 4],
174        )
175    }
176}
177
178/// Apply `.accessibilityLinkedGroup(id:in:)`.
179pub fn accessibility_linked_group(view: &ViewHandle, id: HashableValue<'_>) -> ViewHandle {
180    unsafe {
181        let (bytes, meta, wt) = id.parts();
182        generic1_modifier_ints(
183            resolve::a11y_linked_group_fn(),
184            resolve::a11y_linked_group_oret(),
185            view,
186            meta,
187            wt,
188            &[bytes.as_ptr() as u64, namespace_id()],
189            [0.0; 4],
190        )
191    }
192}
193
194/// Apply `.accessibilityRotorEntry(id:in:)`.
195pub fn accessibility_rotor_entry(view: &ViewHandle, id: HashableValue<'_>) -> ViewHandle {
196    unsafe {
197        let (bytes, meta, wt) = id.parts();
198        generic1_modifier_ints(
199            resolve::a11y_rotor_entry_fn(),
200            resolve::a11y_rotor_entry_oret(),
201            view,
202            meta,
203            wt,
204            &[bytes.as_ptr() as u64, namespace_id()],
205            [0.0; 4],
206        )
207    }
208}
209
210/// Apply `.glassEffectUnion(id:namespace:)`.
211pub fn glass_effect_union(view: &ViewHandle, id: Option<HashableValue<'_>>) -> ViewHandle {
212    unsafe {
213        // The parameter is `A?`, and `A` is generic, so the optional is
214        // address-only no matter what `A` turns out to be.
215        let (bytes, meta, wt) = match id {
216            Some(v) => v.parts(),
217            None => HashableValue::Int(0).parts(),
218        };
219        let opt = abi::generic_metadata1(resolve::optional_ma(), meta);
220        let mut buf = vec![0u8; abi::type_bytes(opt)];
221        if id.is_some() {
222            buf[..bytes.len()].copy_from_slice(&bytes);
223        }
224        abi::store_optional_tag(&mut buf, meta, id.is_some());
225        generic1_modifier_ints(
226            resolve::glass_effect_union_fn(),
227            resolve::glass_effect_union_oret(),
228            view,
229            meta,
230            wt,
231            &[buf.as_ptr() as u64, namespace_id()],
232            [0.0; 4],
233        )
234    }
235}
236
237/// Apply `.listRowBackground(_:)`.
238pub fn list_row_background(view: &ViewHandle, background: Option<&ViewHandle>) -> ViewHandle {
239    unsafe {
240        // `V?` with `V` generic is address-only; for `AnyView` the payload is
241        // one reference, so `nil` is a null word.
242        let mut word: u64 = background.map_or(0, leak_retained);
243        generic1_modifier_ints(
244            resolve::list_row_background_fn(),
245            resolve::list_row_background_oret(),
246            view,
247            resolve::anyview_meta(),
248            resolve::anyview_wt(),
249            &[&mut word as *mut u64 as u64],
250            [0.0; 4],
251        )
252    }
253}
254
255swift_statics! {
256    /// A member of `SwiftUI.PointerStyle`.
257    pub enum PointerStyle {
258        /// What the pointer would be with no modifier at all — an arrow
259        /// over most things, an I-beam over text.
260        Default = c"$s7SwiftUI12PointerStyleV7defaultACvgZ",
261        Link = c"$s7SwiftUI12PointerStyleV4linkACvgZ",
262        ZoomIn = c"$s7SwiftUI12PointerStyleV6zoomInACvgZ",
263        GrabActive = c"$s7SwiftUI12PointerStyleV10grabActiveACvgZ",
264        ColumnResize = c"$s7SwiftUI12PointerStyleV12columnResizeACvgZ",
265        RectSelection = c"$s7SwiftUI12PointerStyleV13rectSelectionACvgZ",
266        HorizontalText = c"$s7SwiftUI12PointerStyleV14horizontalTextACvgZ",
267        VerticalText = c"$s7SwiftUI12PointerStyleV12verticalTextACvgZ",
268        GrabIdle = c"$s7SwiftUI12PointerStyleV8grabIdleACvgZ",
269        RowResize = c"$s7SwiftUI12PointerStyleV9rowResizeACvgZ",
270        ZoomOut = c"$s7SwiftUI12PointerStyleV7zoomOutACvgZ",
271    }
272}
273
274/// Apply `.pointerStyle(_:)`.
275pub fn pointer_style(view: &ViewHandle, style: Option<PointerStyle>) -> ViewHandle {
276    unsafe {
277        let meta = abi::metadata0(resolve::pointer_style_ma());
278        let v = style.map(|s| resilient_static(meta, s.getter()));
279        let b = opt_resilient_bytes(meta, swift_type!(opt SwiftUI.PointerStyle).as_bytes(), v.as_deref());
280        modifier_indirect(resolve::pointer_style_fn(), resolve::pointer_style_oret(), view, &b)
281    }
282}
283
284swift_statics! {
285    /// A member of `SwiftUI.Text.WritingDirectionStrategy`.
286    pub enum WritingDirectionStrategy {
287        Default = c"$s7SwiftUI4TextVAAE24WritingDirectionStrategyV7defaultAEvgZ",
288        LayoutBased = c"$s7SwiftUI4TextVAAE24WritingDirectionStrategyV11layoutBasedAEvgZ",
289        ContentBased = c"$s7SwiftUI4TextVAAE24WritingDirectionStrategyV12contentBasedAEvgZ",
290    }
291}
292
293/// Apply `.writingDirection(strategy:)`.
294pub fn writing_direction(view: &ViewHandle, strategy: WritingDirectionStrategy) -> ViewHandle {
295    unsafe {
296        let meta = abi::metadata0(resolve::writing_direction_strategy_ma());
297        let s = resilient_static(meta, strategy.getter());
298        modifier_indirect(
299            resolve::writing_direction_fn(),
300            resolve::writing_direction_oret(),
301            view,
302            &s,
303        )
304    }
305}
306
307#[cfg(test)]
308mod namespace_tests {
309    use super::*;
310
311    /// A `Namespace.ID` read the wrong way is a leftover register.
312    ///
313    /// `wrappedValue` mints ids from a counter that starts at 1, so a correct
314    /// read is non-zero and a repeat read is larger. Zero is what the getter
315    /// yields when it is called as though self and the result were both
316    /// address-only — the registers it would have written are simply never
317    /// touched — and zero is also a value SwiftUI accepts without complaint,
318    /// which is why this went unnoticed for so long.
319    ///
320    /// `>` rather than `+ 1`: the counter is process-wide and the other tests
321    /// draw from it too.
322    #[test]
323    fn a_namespace_id_comes_from_swiftuis_counter() {
324        unsafe {
325            let first = namespace_id();
326            let second = namespace_id();
327            assert_ne!(first, 0, "a namespace read through the getter is never zero");
328            assert!(second > first, "ids advance: {first} then {second}");
329        }
330    }
331
332    /// Every namespace-keyed modifier builds without faulting.
333    #[test]
334    fn namespace_modifiers_build() {
335        let t = || text("Fx");
336        let _ = focus_scope(&t());
337        let _ = prefers_default_focus(&t(), true);
338        let _ = matched_geometry_effect(
339            &t(),
340            HashableValue::Str("g"),
341            &[GeometryProperties::Frame],
342            (0.5, 0.5),
343            true,
344        );
345        let _ = matched_transition_source(&t(), HashableValue::Str("m"));
346        let _ = accessibility_linked_group(&t(), HashableValue::Str("l"));
347        let _ = accessibility_rotor_entry(&t(), HashableValue::Str("r"));
348        let _ = glass_effect_union(&t(), Some(HashableValue::Int(4)));
349        let _ = glass_effect_id(&t(), Some(HashableValue::Str("g")));
350        let _ = glass_effect_id(&t(), None);
351        let _ = accessibility_labeled_pair(&t(), LabeledPairRole::Label, HashableValue::Str("p"));
352    }
353}