swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
//! Swift's generic modifiers, as Rust traits.
//!
//! `foregroundStyle(_ style: some ShapeStyle)` is one modifier in Swift and
//! six bindings here — `foreground_style_color`, `_hierarchical`, `_value` and
//! the rest — because each concrete `ShapeStyle` is a distinct Swift type with
//! its own entry point. Swift hides that behind a protocol; Rust hides it
//! behind a trait, which is the same idea with the same result: one spelling
//! at the call site, the argument's type choosing the binding.
//!
//! ```no_run
//! # use swiftui_native::{prelude::*, views::{Hierarchical, SystemColor}};
//! # let v = text("x");
//! v.foreground_style(Color::RED)
//!     .foreground_style(SystemColor::Blue)
//!     .foreground_style(Hierarchical::Secondary);
//! ```

use super::View;
use crate::views;
use crate::Color;

/// Anything `foregroundStyle` accepts — Swift's `some ShapeStyle`.
///
/// Implemented rather than derived, because each one reaches a different
/// binding; adding a style is one `impl`.
///
/// # Leading dots do not work here
///
/// `.foreground_style(.blue)` does not compile, and cannot. A leading dot
/// lowers to `Into::into(dot::Blue)`, which needs a concrete expected type to
/// resolve against; a generic parameter gives it none, and Rust will not search
/// impls backwards to find the one type that is both `From<dot::Blue>` and
/// `ShapeStyleArg`. Name the style — `SystemColor::Blue` — or use a modifier
/// whose parameter is that one type, such as
/// `foreground_style_hierarchical`.
pub trait ShapeStyleArg {
    /// Apply this style to `view`.
    fn foreground_style(self, view: View) -> View;
}

impl ShapeStyleArg for Color {
    fn foreground_style(self, view: View) -> View {
        view.foreground_style_color(self.r, self.g, self.b, self.a)
    }
}

impl ShapeStyleArg for views::SystemColor {
    fn foreground_style(self, view: View) -> View {
        // A named colour is one word once resolved, which is what the
        // by-value entry point takes.
        view.foreground_style_value(views::system_color_value(self))
    }
}

impl ShapeStyleArg for views::Hierarchical {
    fn foreground_style(self, view: View) -> View {
        view.foreground_style_hierarchical(self)
    }
}

impl View {
    /// `.foregroundStyle(_:)` — one spelling for every style.
    ///
    /// The argument's type picks the binding, the way its conformance does in
    /// Swift. See [`ShapeStyleArg`].
    pub fn foreground_style<S: ShapeStyleArg>(self, style: S) -> Self {
        style.foreground_style(self)
    }
}

/// Anything `backgroundStyle` accepts — the same `some ShapeStyle` as
/// [`ShapeStyleArg`], reaching a different modifier.
///
/// Separate from `ShapeStyleArg` rather than a second method on it because the
/// two modifiers do not accept the same set: a gradient is a `ShapeStyle` that
/// `foregroundStyle` takes and this one has no binding for yet. One trait per
/// modifier keeps that honest instead of a method that panics.
pub trait BackgroundStyleArg {
    /// Apply this style as `view`'s background style.
    fn background_style(self, view: View) -> View;
}

impl BackgroundStyleArg for Color {
    fn background_style(self, view: View) -> View {
        View::new(views::background_style_color(
            &view.handle,
            self.r,
            self.g,
            self.b,
            self.a,
        ))
    }
}

impl BackgroundStyleArg for views::SystemColor {
    fn background_style(self, view: View) -> View {
        View::new(views::background_style_value(
            &view.handle,
            views::system_color_value(self),
        ))
    }
}

impl BackgroundStyleArg for views::Hierarchical {
    fn background_style(self, view: View) -> View {
        View::new(views::background_style_hierarchical(&view.handle, self))
    }
}

impl View {
    /// `.backgroundStyle(_:)` — one spelling for every style.
    ///
    /// The style the views *inside* this one use for their backgrounds, which
    /// is a different question from what [`View::background`] draws
    /// behind this one. See [`BackgroundStyleArg`].
    pub fn background_style<S: BackgroundStyleArg>(self, style: S) -> Self {
        style.background_style(self)
    }
}

// ── Families Swift writes as one name and this crate as several ──────────

/// A value `.environment(_:_:)` can write.
///
/// Swift's is `environment<V>(_ keyPath: WritableKeyPath<EnvironmentValues, V>,
/// _ value: V)`, one modifier over every property. Each property is a separate
/// entry point here because each needs its own key path, so the trait puts
/// them back behind one spelling — the argument's type choosing the property,
/// the way the key path does in Swift.
pub trait EnvironmentArg {
    /// Write this value into `view`'s environment.
    fn environment(self, view: View) -> View;
}

impl EnvironmentArg for views::SymbolVariants {
    fn environment(self, view: View) -> View {
        View::new(views::environment_symbol_variants(&view.handle, self))
    }
}

impl EnvironmentArg for views::ImageScale {
    fn environment(self, view: View) -> View {
        View::new(views::environment_image_scale(&view.handle, self))
    }
}

/// Which `NavigationView` layout to use — Swift's `.navigationViewStyle(_:)`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NavigationViewStyle {
    /// `.automatic`
    Automatic,
    /// `.columns`
    Columns,
    /// `DoubleColumnNavigationViewStyle()` — sidebar plus detail.
    DoubleColumn,
    /// `.stack`. iOS only: `StackNavigationViewStyle` is
    /// `@available(macOS, unavailable)`.
    #[cfg(not(target_os = "macos"))]
    Stack,
}

/// Swift's deprecated `.accessibility(…)` family, as one argument.
///
/// Nine overloads sharing a name, distinguished by their argument label. Rust
/// has no argument labels, so the label becomes a variant — which is the same
/// information in the same place.
///
/// All nine are `@available(…, deprecated)` in Swift and the modern spellings
/// are bound separately: `accessibility_label`, `accessibility_hint` and so
/// on. This exists so a view transcribed from older documentation compiles,
/// not because it is the way to write new code. `SelectionIdentifier` is the
/// exception: it has no modern replacement, so it is only reachable here.
#[derive(Clone, Copy, Debug)]
pub enum Accessibility<'a> {
    /// `.accessibility(label:)`
    Label(&'a str),
    /// `.accessibility(hint:)`
    Hint(&'a str),
    /// `.accessibility(value:)`
    Value(&'a str),
    /// `.accessibility(hidden:)`
    Hidden(bool),
    /// `.accessibility(identifier:)` — a test hook, not something spoken.
    Identifier(&'a str),
    /// `.accessibility(sortPriority:)` — higher is read first.
    SortPriority(f64),
    /// `.accessibility(activationPoint:)`, in unit coordinates.
    ActivationPoint(f64, f64),
    /// `.accessibility(inputLabels:)` — what a user may say to activate this.
    InputLabels(&'a [&'a str]),
    /// `.accessibility(selectionIdentifier:)` — which item of a group this is.
    SelectionIdentifier(views::HashableValue<'a>),
}

/// What a drag carries — Swift's `some Transferable`, plus the container
/// overload that is a different modifier of the same name.
#[derive(Clone, Copy, Debug)]
pub enum Draggable<'a> {
    /// A piece of text, droppable anywhere that takes one.
    Text(&'a str),
    /// `.draggable(containerItemID:containerNamespace:)` — reordering within a
    /// container rather than dragging anything out of it.
    ContainerItem(views::HashableValue<'a>),
}

/// Where a scroll view should be — Swift's two `scrollPosition` overloads.
pub enum ScrollTarget<'a> {
    /// Pinned to an edge.
    Edge(views::Edge),
    /// Scrolled to the row with this identity, at an optional unit anchor.
    Id {
        /// The identity to scroll to, which SwiftUI also writes back.
        state: &'a views::BoundState<String>,
        /// `None` is SwiftUI's own default.
        anchor: Option<(f64, f64)>,
    },
}

/// Where `@AppStorage` below this point reads and writes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AppStorageStore<'a> {
    /// `UserDefaults.standard`.
    Standard,
    /// `UserDefaults(suiteName:)` — a shared container, usually.
    Suite(&'a str),
}

impl View {
    /// `.environment(_:_:)` — one spelling for every property.
    ///
    /// See [`EnvironmentArg`]. The leading-dot caveat from [`ShapeStyleArg`]
    /// applies: name the value's type rather than writing `.environment(.large)`.
    pub fn environment<V: EnvironmentArg>(self, value: V) -> Self {
        value.environment(self)
    }

    /// `.navigationViewStyle(_:)`.
    pub fn navigation_view_style(self, style: NavigationViewStyle) -> Self {
        Self::new(match style {
            NavigationViewStyle::Automatic => {
                views::navigation_view_style_automatic(&self.handle)
            }
            NavigationViewStyle::Columns => views::navigation_view_style_columns(&self.handle),
            NavigationViewStyle::DoubleColumn => {
                views::navigation_view_style_double_column(&self.handle)
            }
            #[cfg(not(target_os = "macos"))]
            NavigationViewStyle::Stack => views::navigation_view_style_stack(&self.handle),
        })
    }

    /// `.modifier(_:)` — apply a `ViewModifier` this crate has no name for.
    ///
    /// The escape hatch. Every bound modifier goes through this internally;
    /// exposing it means a SwiftUI modifier type the crate has never heard of
    /// can still be applied, given its metadata and the bytes of a value.
    ///
    /// # Safety
    /// `modifier_meta` must be metadata for a type conforming to
    /// `ViewModifier`, and `bytes` a live value of exactly that type. Neither
    /// is checked: a mismatch hands SwiftUI a value of the wrong shape and it
    /// writes through it.
    pub unsafe fn modifier(
        self,
        modifier_meta: *const core::ffi::c_void,
        bytes: &[u8],
    ) -> Self {
        Self::new(unsafe { views::apply_modifier(&self.handle, modifier_meta, bytes) })
    }

    /// `.accessibility(…)` — the deprecated family, under its Swift name.
    ///
    /// See [`Accessibility`]. New code should prefer the modern spellings,
    /// which are separate methods here as they are in Swift.
    pub fn accessibility(self, what: Accessibility<'_>) -> Self {
        Self::new(match what {
            Accessibility::Label(s) => views::accessibility_label(&self.handle, s),
            Accessibility::Hint(s) => views::accessibility_hint(&self.handle, s),
            Accessibility::Value(s) => views::accessibility_value(&self.handle, s, true),
            Accessibility::Hidden(b) => views::accessibility_hidden(&self.handle, b),
            Accessibility::Identifier(s) => views::accessibility_identifier(&self.handle, s),
            Accessibility::SortPriority(p) => {
                views::accessibility_sort_priority(&self.handle, p)
            }
            Accessibility::ActivationPoint(x, y) => {
                views::accessibility_activation_point(&self.handle, x, y)
            }
            Accessibility::InputLabels(l) => {
                views::accessibility_input_labels(&self.handle, l, true)
            }
            Accessibility::SelectionIdentifier(id) => {
                views::accessibility_selection_identifier(&self.handle, id)
            }
        })
    }

    /// `.draggable(_:)` — what comes out when this view is dragged.
    pub fn draggable(self, payload: Draggable<'_>) -> Self {
        Self::new(match payload {
            Draggable::Text(t) => views::draggable_text(&self.handle, t),
            Draggable::ContainerItem(id) => views::draggable_container_item(&self.handle, id),
        })
    }

    /// `.scrollPosition(_:)` — where the scroll view should sit.
    pub fn scroll_position(self, target: ScrollTarget<'_>) -> Self {
        Self::new(match target {
            ScrollTarget::Edge(e) => views::scroll_position_edge(&self.handle, e),
            ScrollTarget::Id { state, anchor } => {
                views::scroll_position_id(&self.handle, state, anchor)
            }
        })
    }

    /// `.transformEnvironment(_:transform:)` — read a property, change it,
    /// write it back for everything below.
    ///
    /// The key is the type parameter, the way the key path is the argument in
    /// Swift:
    ///
    /// ```no_run
    /// # use swiftui_native::{prelude::*, state::ColorSchemeKey, views::ColorScheme};
    /// text("x").transform_environment::<ColorSchemeKey>(|_| ColorScheme::Dark);
    /// ```
    pub fn transform_environment<K: crate::state::EnvKey>(
        self,
        action: impl Fn(K::Value) -> K::Value + 'static,
    ) -> Self {
        Self::new(views::transform_environment::<K>(&self.handle, action))
    }

    /// `.defaultAppStorage(_:)`.
    ///
    /// Panics on a suite name `UserDefaults` refuses — the app's own bundle
    /// identifier, or the global domain. [`views::default_app_storage_suite`]
    /// returns the `None` instead, for a caller that wants to handle it.
    pub fn default_app_storage(self, store: AppStorageStore<'_>) -> Self {
        Self::new(match store {
            AppStorageStore::Standard => views::default_app_storage_standard(&self.handle),
            AppStorageStore::Suite(name) => {
                views::default_app_storage_suite(&self.handle, name)
                    .unwrap_or_else(|| panic!("UserDefaults refused the suite {name:?}"))
            }
        })
    }
}

// ── Swift's spelling for modifiers this crate named for what they do ─────
//
// SwiftUI names these for the *effect* they apply; the bindings here are named
// for the operation. Both resolve to the same call, so a view transcribed from
// Swift documentation compiles without a lookup table. This is the whole list —
// four — rather than a policy of aliasing everything.

impl View {
    /// `.rotationEffect(_:)` — [`View::rotation`] about the centre, which is
    /// the anchor SwiftUI defaults to.
    pub fn rotation_effect(self, degrees: f64) -> Self {
        self.rotation(degrees, 0.5, 0.5)
    }

    /// `.rotation3DEffect(_:axis:)` — [`View::rotation_3d`] under SwiftUI's
    /// name.
    pub fn rotation_3d_effect(self, degrees: f64, ax: f64, ay: f64, az: f64) -> Self {
        self.rotation_3d(degrees, ax, ay, az)
    }

    /// `.scaleEffect(x:y:)` — [`View::scale`] under SwiftUI's name.
    pub fn scale_effect(self, sx: f64, sy: f64) -> Self {
        self.scale(sx, sy)
    }

    /// `.multilineTextAlignment(_:)` — [`View::multiline_alignment`] under
    /// SwiftUI's name.
    pub fn multiline_text_alignment(self, align: crate::views::TextAlign) -> Self {
        self.multiline_alignment(align)
    }
}

// ── `.clipShape(_:)` ─────────────────────────────────────────────────────

/// A shape `clipShape` accepts — Swift's `some Shape`.
///
/// The same arrangement as [`ShapeStyleArg`]: one modifier in Swift, several
/// bindings here because each concrete shape is its own Swift type, and a
/// trait to put them back behind one spelling.
///
/// The leading-dot caveat applies here too — `.clip_shape(.circle)` cannot
/// resolve, because a generic parameter gives `Into` nothing to infer from.
pub trait ClipShapeArg {
    /// Clip `view` to this shape.
    fn clip_shape(self, view: View) -> View;
}

/// The shapes `clipShape` can take, as values rather than types.
///
/// A corner radius belongs to the rounded ones and nothing else, which an enum
/// says and a set of parallel methods does not.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ClipShape {
    Circle,
    Capsule,
    Ellipse,
    /// `RoundedRectangle(cornerRadius:)`, whose corner is one continuous
    /// curve — leaving the style out in Swift means `.continuous`.
    RoundedRect(f64),
    /// The same with `style: .circular`, whose corner is a quarter circle.
    /// This is the one that has to be asked for; measured, `RoundedRect`
    /// above and `style: .continuous` build the same value.
    CircularRoundedRect(f64),
}

impl ClipShapeArg for ClipShape {
    fn clip_shape(self, view: View) -> View {
        match self {
            Self::Circle => view.clip_circle(),
            Self::Capsule => view.clip_capsule(),
            Self::Ellipse => view.clip_ellipse(),
            Self::RoundedRect(r) => view.clip_rounded_rect(r),
            Self::CircularRoundedRect(r) => view.clip_rounded_rect_styled(r, false),
        }
    }
}

impl View {
    /// `.clipShape(_:)` — one spelling for every shape.
    ///
    /// ```no_run
    /// # use swiftui_native::{prelude::*, view::style::ClipShape};
    /// # let v = text("x");
    /// v.clip_shape(ClipShape::Circle)
    ///     .clip_shape(ClipShape::RoundedRect(8.0));
    /// ```
    pub fn clip_shape<S: ClipShapeArg>(self, shape: S) -> Self {
        shape.clip_shape(self)
    }
}