Skip to main content

swiftui_native/views/
customshape.rs

1//! A `Shape` written in Rust — a path computed from the rect it is given.
2//!
3//! [`crate::views::Shape::path`] already builds a fixed `Path` and renders it. A
4//! *conformance* is a different thing: SwiftUI asks the shape for its path each
5//! time the layout changes, so the path can be a function of the space available,
6//! and the result is a real `Shape` — so `.fill`, `.stroke`, `clipShape` and
7//! shape animation all apply to it.
8//!
9//! ```no_run
10//! # use swiftui_native::views;
11//! // A chevron that always spans its frame.
12//! let s = views::shape_fn(|p: &mut views::ShapePath, r: views::Rect| {
13//!     p.move_to(r.x, r.y);
14//!     p.line_to(r.x + r.width * 0.5, r.y + r.height);
15//!     p.line_to(r.x + r.width, r.y);
16//! });
17//! let v = views::custom_shape(&s);
18//! ```
19//!
20//! # The knot
21//!
22//! `Shape: Animatable, View`, and a shape's `View.Body` is
23//! `_ShapeView<Self, ForegroundStyle>` — whose metadata the runtime will only
24//! build once it can find `Self: Shape`. So neither witness table can be
25//! completed before the other one exists. Swift ties this off with *lazy* witness
26//! tables; here the laziness is written out:
27//!
28//! 1. the `Shape` conformance is built and registered with its `View` slot empty;
29//! 2. `_ShapeView<RustShape, ForegroundStyle>` is resolved by name, which the
30//!    runtime can now do;
31//! 3. a `View` conformance is built whose `Body` is that type and whose `body`
32//!    getter is SwiftUI's own `Shape` extension, reached through a thunk that
33//!    swaps the `View` witness table it is handed for the `Shape` one it wants;
34//! 4. the empty slot is filled in.
35//!
36//! `Animatable` is borrowed rather than synthesised — see
37//! [`resolve::rectangle_animatable_wt`].
38
39use super::{wrap_owned, PathBuilder, ViewHandle};
40use crate::abi;
41use crate::conformance::{synthesize, Conformance, Protocol, Witness};
42use crate::resolve;
43use core::ffi::c_void;
44use std::sync::{Mutex, OnceLock};
45
46// The rect a shape is asked to fill is [`super::Rect`] — the same one a custom
47// `Layout` places subviews in. Two identical structs with the same name in the
48// same module is an ambiguity, and there is no reason for a shape's rect to be a
49// different type from a layout's.
50pub use super::Rect;
51
52/// The path a shape is building.
53///
54/// A thin wrapper over [`PathBuilder`] so a shape closure reads like drawing
55/// rather than like building a value.
56pub struct ShapePath {
57    builder: PathBuilder,
58}
59
60impl ShapePath {
61    /// Start a new subpath at a point.
62    pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
63        self.builder.move_to(x, y);
64        self
65    }
66
67    /// Draw a straight line to a point.
68    pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
69        self.builder.line_to(x, y);
70        self
71    }
72
73    /// Draw a quadratic curve to a point, bending towards a control point.
74    pub fn quad_curve_to(&mut self, x: f64, y: f64, cx: f64, cy: f64) -> &mut Self {
75        self.builder.quad_curve_to(x, y, cx, cy);
76        self
77    }
78
79    /// Close the current subpath.
80    pub fn close(&mut self) -> &mut Self {
81        self.builder.close();
82        self
83    }
84}
85
86/// How a shape draws itself into a rect.
87pub trait Shaping: Send + Sync + 'static {
88    /// Add this shape's path for `rect`.
89    fn path(&self, path: &mut ShapePath, rect: Rect);
90}
91
92impl<F> Shaping for F
93where
94    F: Fn(&mut ShapePath, Rect) + Send + Sync + 'static,
95{
96    fn path(&self, path: &mut ShapePath, rect: Rect) {
97        self(path, rect)
98    }
99}
100
101/// A registered shape, as the one word SwiftUI stores.
102pub struct CustomShape {
103    index: i64,
104}
105
106fn registry() -> &'static Mutex<Vec<&'static dyn Shaping>> {
107    static R: OnceLock<Mutex<Vec<&'static dyn Shaping>>> = OnceLock::new();
108    R.get_or_init(|| Mutex::new(Vec::new()))
109}
110
111/// Register a shape. A closure taking the path and the rect is one.
112pub fn shape_fn<S: Shaping>(shape: S) -> CustomShape {
113    let leaked: &'static dyn Shaping = Box::leak(Box::new(shape));
114    let mut r = registry().lock().expect("shape registry");
115    r.push(leaked);
116    CustomShape { index: r.len() as i64 - 1 }
117}
118
119// `path(in:)` arrives with the `Path` result through x8, the `CGRect` in d0–d3
120// and `self` in x20. Only the two integer registers need shuffling; the rect is
121// already where a Rust `fn(_, _, f64, f64, f64, f64)` expects it.
122core::arch::global_asm!(
123    ".p2align 2",
124    ".globl _rswift_shape_path_thunk",
125    "_rswift_shape_path_thunk:",
126    "mov x1, x20",
127    "mov x0, x8",
128    "b _rswift_shape_path",
129);
130
131unsafe extern "C" {
132    #[link_name = "rswift_shape_path_thunk"]
133    fn shape_path_thunk();
134}
135
136/// `path(in:) -> Path`.
137///
138/// # Safety
139/// Reached through `rswift_shape_path_thunk`.
140#[unsafe(no_mangle)]
141unsafe extern "C" fn rswift_shape_path(
142    out: *mut u8,
143    this: *const u8,
144    x: f64,
145    y: f64,
146    width: f64,
147    height: f64,
148) {
149    let index = unsafe { *(this as *const i64) } as usize;
150    let shape = {
151        let r = registry().lock().expect("shape registry");
152        r.get(index).copied().expect("shape index out of range")
153    };
154    let mut path = ShapePath { builder: super::Shape::path() };
155    shape.path(&mut path, Rect { x, y, width, height });
156    // The `Path` the builder holds is the result, moved into SwiftUI's slot.
157    unsafe { path.builder.move_into(out) };
158}
159
160// The `View.body` witness for a shape is SwiftUI's own — the `Shape` extension's
161// `body`, which returns `_ShapeView<Self, ForegroundStyle>`. It has the same
162// convention as a `View.body` witness (result through x8, `self` in x20,
163// `Self.Type` in x0, a witness table in x1) and differs in *which* table: it
164// wants `Self: Shape` where the witness is handed `Self: View`. So the thunk
165// swaps x1 for the Shape table and tail-calls.
166#[unsafe(no_mangle)]
167static mut RSWIFT_SHAPE_WT: *const c_void = core::ptr::null();
168
169crate::thunk::runtime_thunk! {
170    /// `View.body` for a shape: the Shape witness table replaces the View one.
171    fn shape_body_thunk = "rswift_shape_body" via RSWIFT_SHAPE_BODY {
172        prologue: [
173            "adrp x1, _RSWIFT_SHAPE_WT@PAGE",
174            "add x1, x1, _RSWIFT_SHAPE_WT@PAGEOFF",
175            "ldr x1, [x1]",
176        ],
177        branch: "br",
178        epilogue: [],
179    }
180}
181
182/// The carrier type a shape value *is*: one `Int`, its index.
183fn carrier() -> (*const c_void, *const c_void) {
184    static C: OnceLock<(usize, usize)> = OnceLock::new();
185    let (d, m) = *C.get_or_init(|| unsafe {
186        let (d, m) = crate::state::emit_int_carrier(b"RustShape\0", b"6rswift9RustShapeV\0");
187        (d as usize, m as usize)
188    });
189    (d as *const c_void, m as *const c_void)
190}
191
192/// The `Shape` conformance, and the `View` one it needs.
193///
194/// Four steps, in this order because each needs the one before:
195///
196/// 1. `RustShape: Animatable`, registered with `Rectangle`'s own witnesses. It
197///    has to be *registered* and not merely referenced: instantiating
198///    `_ShapeView<RustShape, …>` checks that every conformance `Shape` requires
199///    can be found, and one that only exists inside another table cannot be.
200/// 2. the carrier's `View` table, which [`crate::state`] already built and
201///    registered when the type was minted — with a stub `body`.
202/// 3. `RustShape: Shape`, whose two base slots are those two tables.
203/// 4. `_ShapeView<RustShape, ForegroundStyle>`, which the runtime can build now,
204///    and whose metadata and `View` conformance are patched into the carrier's
205///    `View` table along with the real `body`. Patching rather than registering a
206///    second table: two conformances for the same type and protocol is a
207///    conflict, and this is the one the runtime already hands out.
208fn shape_conformance() -> &'static Conformance {
209    static C: OnceLock<Conformance> = OnceLock::new();
210    C.get_or_init(|| unsafe {
211        let carrier_meta = carrier().1;
212
213        // ── 1. Animatable, borrowed and registered ──
214        let animatable = Protocol::from_symbol(c"$s7SwiftUI10AnimatableMp");
215        assert_eq!(
216            animatable.requirement_count(),
217            6,
218            "Animatable changed shape — it used to be AnimatableData, its \
219             conformance, a getter, a setter, a modify coroutine and one default",
220        );
221        // `Rectangle`'s table, word by word. Every shape whose `AnimatableData` is
222        // `EmptyAnimatableData` has the same one — the getter returns a zero-size
223        // value and the setter ignores it — so there is nothing in these
224        // implementations that could depend on which shape they came from.
225        let borrowed = resolve::rectangle_animatable_wt() as *const *const c_void;
226        let witnesses: Vec<Witness> = (0..animatable.requirement_count())
227            .map(|i| Witness::Metadata(*borrowed.add(1 + i)))
228            .collect();
229        let animatable_wt = Box::leak(Box::new(synthesize(
230            &animatable,
231            carrier().0,
232            &witnesses,
233        )))
234        .witness_table();
235
236        // ── 2. the View table the type was minted with ──
237        let view_wt = abi::get_view_wt(carrier_meta);
238        assert!(
239            !view_wt.is_null(),
240            "the carrier type has no View conformance — `emit_int_carrier` used to \
241             register one",
242        );
243
244        // ── 3. Shape ──
245        let protocol = Protocol::from_symbol(c"$s7SwiftUI5ShapeMp");
246        assert_eq!(
247            protocol.requirement_count(),
248            6,
249            "Shape changed shape — it used to be Animatable, View, `path(in:)` and \
250             three defaulted requirements",
251        );
252        let shape = synthesize(
253            &protocol,
254            carrier().0,
255            &[
256                Witness::Metadata(animatable_wt),
257                Witness::Metadata(view_wt),
258                Witness::Function(shape_path_thunk as *const c_void),
259                Witness::Default,
260                Witness::Default,
261                Witness::Default,
262            ],
263        );
264        RSWIFT_SHAPE_WT = shape.witness_table();
265
266        // ── 4. the body, and the knot tied ──
267        // Built from `_ShapeView`'s descriptor and its four key arguments, not
268        // from a mangled name. A name cannot work here: the content type was
269        // minted at run time, and `swift_getTypeByMangledName` resolves a nominal
270        // name by searching the images' metadata sections — where a synthesised
271        // descriptor does not appear. Registering a *conformance* makes the type
272        // findable as a conformer, which is a different question and is why the
273        // lookups above succeed while a name lookup returns nothing.
274        let style_meta = abi::metadata0(resolve::foreground_style_ma());
275        assert!(!style_meta.is_null(), "ForegroundStyle metadata");
276        let body_meta = abi::generic_metadata_from_descriptor(
277            resolve::shape_view_desc(),
278            &[carrier_meta, style_meta, shape.witness_table(), resolve::foreground_style_wt()],
279        );
280        assert!(
281            !body_meta.is_null(),
282            "_ShapeView<RustShape, ForegroundStyle> could not be instantiated",
283        );
284        let body_wt = abi::get_view_wt(body_meta);
285        assert!(!body_wt.is_null(), "{} !: View", crate::diag::type_name(body_meta));
286
287        RSWIFT_SHAPE_BODY = resolve::shape_extension_body();
288        // The carrier's `View` table, as `crate::state` laid it out:
289        //   [0] conformance descriptor
290        //   [1] Body: View        [2] Body
291        //   [3..5] the three `_make…` defaults
292        //   [6] body
293        let words = view_wt as *mut *const c_void;
294        *words.add(1) = body_wt;
295        *words.add(2) = body_meta;
296        *words.add(6) = shape_body_thunk as *const c_void;
297
298        shape
299    })
300}
301
302/// A shape written in Rust, as a view.
303///
304/// Filled with the foreground style, which is what a bare `Shape` in a view
305/// builder means — `Circle()` on its own draws a filled circle.
306pub fn custom_shape(shape: &CustomShape) -> ViewHandle {
307    unsafe {
308        let conformance = shape_conformance();
309        let meta = carrier().1;
310        let wt = abi::get_view_wt(meta);
311        assert!(
312            !wt.is_null(),
313            "{} !: View — the synthesised conformance was not found",
314            crate::diag::type_name(meta),
315        );
316        let _ = conformance;
317        let mut bytes = vec![0u8; abi::type_bytes(meta)];
318        bytes[..8].copy_from_slice(&shape.index.to_le_bytes());
319        wrap_owned(bytes, meta, wt)
320    }
321}
322
323/// The shape as one of the crate's [`super::Shape`] values.
324///
325/// `.fill(_:)` and `.fill_style(…)` work: they reach `_ShapeView` through its
326/// *descriptor* and four key arguments, so no name is involved. The methods that
327/// compose a result *name* — `.inset`, `.rotation`, `.stroked` and the rest —
328/// cannot: they build a mangled name for the result type, and a type minted at
329/// run time has none that `swift_getTypeByMangledName` will resolve. Those fail
330/// with this shape's name in the message rather than silently.
331pub fn custom_shape_value(shape: &CustomShape) -> super::Shape {
332    unsafe {
333        let conformance = shape_conformance();
334        let meta = carrier().1;
335        let mut bytes = vec![0u8; abi::type_bytes(meta)];
336        bytes[..8].copy_from_slice(&shape.index.to_le_bytes());
337        super::Shape::of_metadata(
338            bytes,
339            meta,
340            conformance.witness_table(),
341            crate::mangle::Type::Verbatim("6rswift9RustShapeV".into()),
342        )
343    }
344}