Skip to main content

swiftui_native/view/
style.rs

1//! Swift's generic modifiers, as Rust traits.
2//!
3//! `foregroundStyle(_ style: some ShapeStyle)` is one modifier in Swift and
4//! six bindings here — `foreground_style_color`, `_hierarchical`, `_value` and
5//! the rest — because each concrete `ShapeStyle` is a distinct Swift type with
6//! its own entry point. Swift hides that behind a protocol; Rust hides it
7//! behind a trait, which is the same idea with the same result: one spelling
8//! at the call site, the argument's type choosing the binding.
9//!
10//! ```no_run
11//! # use swiftui_native::{prelude::*, views::{Hierarchical, SystemColor}};
12//! # let v = text("x");
13//! v.foreground_style(Color::RED)
14//!     .foreground_style(SystemColor::Blue)
15//!     .foreground_style(Hierarchical::Secondary);
16//! ```
17
18use super::View;
19use crate::views;
20use crate::Color;
21
22/// Anything `foregroundStyle` accepts — Swift's `some ShapeStyle`.
23///
24/// Implemented rather than derived, because each one reaches a different
25/// binding; adding a style is one `impl`.
26///
27/// # Leading dots do not work here
28///
29/// `.foreground_style(.blue)` does not compile, and cannot. A leading dot
30/// lowers to `Into::into(dot::Blue)`, which needs a concrete expected type to
31/// resolve against; a generic parameter gives it none, and Rust will not search
32/// impls backwards to find the one type that is both `From<dot::Blue>` and
33/// `ShapeStyleArg`. Name the style — `SystemColor::Blue` — or use a modifier
34/// whose parameter is that one type, such as
35/// `foreground_style_hierarchical`.
36pub trait ShapeStyleArg {
37    /// Apply this style to `view`.
38    fn foreground_style(self, view: View) -> View;
39}
40
41impl ShapeStyleArg for Color {
42    fn foreground_style(self, view: View) -> View {
43        view.foreground_style_color(self.r, self.g, self.b, self.a)
44    }
45}
46
47impl ShapeStyleArg for views::SystemColor {
48    fn foreground_style(self, view: View) -> View {
49        // A named colour is one word once resolved, which is what the
50        // by-value entry point takes.
51        view.foreground_style_value(views::system_color_value(self))
52    }
53}
54
55impl ShapeStyleArg for views::Hierarchical {
56    fn foreground_style(self, view: View) -> View {
57        view.foreground_style_hierarchical(self)
58    }
59}
60
61impl View {
62    /// `.foregroundStyle(_:)` — one spelling for every style.
63    ///
64    /// The argument's type picks the binding, the way its conformance does in
65    /// Swift. See [`ShapeStyleArg`].
66    pub fn foreground_style<S: ShapeStyleArg>(self, style: S) -> Self {
67        style.foreground_style(self)
68    }
69}
70
71/// Anything `backgroundStyle` accepts — the same `some ShapeStyle` as
72/// [`ShapeStyleArg`], reaching a different modifier.
73///
74/// Separate from `ShapeStyleArg` rather than a second method on it because the
75/// two modifiers do not accept the same set: a gradient is a `ShapeStyle` that
76/// `foregroundStyle` takes and this one has no binding for yet. One trait per
77/// modifier keeps that honest instead of a method that panics.
78pub trait BackgroundStyleArg {
79    /// Apply this style as `view`'s background style.
80    fn background_style(self, view: View) -> View;
81}
82
83impl BackgroundStyleArg for Color {
84    fn background_style(self, view: View) -> View {
85        View::new(views::background_style_color(
86            &view.handle,
87            self.r,
88            self.g,
89            self.b,
90            self.a,
91        ))
92    }
93}
94
95impl BackgroundStyleArg for views::SystemColor {
96    fn background_style(self, view: View) -> View {
97        View::new(views::background_style_value(
98            &view.handle,
99            views::system_color_value(self),
100        ))
101    }
102}
103
104impl BackgroundStyleArg for views::Hierarchical {
105    fn background_style(self, view: View) -> View {
106        View::new(views::background_style_hierarchical(&view.handle, self))
107    }
108}
109
110impl View {
111    /// `.backgroundStyle(_:)` — one spelling for every style.
112    ///
113    /// The style the views *inside* this one use for their backgrounds, which
114    /// is a different question from what [`View::background`] draws
115    /// behind this one. See [`BackgroundStyleArg`].
116    pub fn background_style<S: BackgroundStyleArg>(self, style: S) -> Self {
117        style.background_style(self)
118    }
119}
120
121// ── Families Swift writes as one name and this crate as several ──────────
122
123/// A value `.environment(_:_:)` can write.
124///
125/// Swift's is `environment<V>(_ keyPath: WritableKeyPath<EnvironmentValues, V>,
126/// _ value: V)`, one modifier over every property. Each property is a separate
127/// entry point here because each needs its own key path, so the trait puts
128/// them back behind one spelling — the argument's type choosing the property,
129/// the way the key path does in Swift.
130pub trait EnvironmentArg {
131    /// Write this value into `view`'s environment.
132    fn environment(self, view: View) -> View;
133}
134
135impl EnvironmentArg for views::SymbolVariants {
136    fn environment(self, view: View) -> View {
137        View::new(views::environment_symbol_variants(&view.handle, self))
138    }
139}
140
141impl EnvironmentArg for views::ImageScale {
142    fn environment(self, view: View) -> View {
143        View::new(views::environment_image_scale(&view.handle, self))
144    }
145}
146
147/// Which `NavigationView` layout to use — Swift's `.navigationViewStyle(_:)`.
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub enum NavigationViewStyle {
150    /// `.automatic`
151    Automatic,
152    /// `.columns`
153    Columns,
154    /// `DoubleColumnNavigationViewStyle()` — sidebar plus detail.
155    DoubleColumn,
156    /// `.stack`. iOS only: `StackNavigationViewStyle` is
157    /// `@available(macOS, unavailable)`.
158    #[cfg(not(target_os = "macos"))]
159    Stack,
160}
161
162/// Swift's deprecated `.accessibility(…)` family, as one argument.
163///
164/// Nine overloads sharing a name, distinguished by their argument label. Rust
165/// has no argument labels, so the label becomes a variant — which is the same
166/// information in the same place.
167///
168/// All nine are `@available(…, deprecated)` in Swift and the modern spellings
169/// are bound separately: `accessibility_label`, `accessibility_hint` and so
170/// on. This exists so a view transcribed from older documentation compiles,
171/// not because it is the way to write new code. `SelectionIdentifier` is the
172/// exception: it has no modern replacement, so it is only reachable here.
173#[derive(Clone, Copy, Debug)]
174pub enum Accessibility<'a> {
175    /// `.accessibility(label:)`
176    Label(&'a str),
177    /// `.accessibility(hint:)`
178    Hint(&'a str),
179    /// `.accessibility(value:)`
180    Value(&'a str),
181    /// `.accessibility(hidden:)`
182    Hidden(bool),
183    /// `.accessibility(identifier:)` — a test hook, not something spoken.
184    Identifier(&'a str),
185    /// `.accessibility(sortPriority:)` — higher is read first.
186    SortPriority(f64),
187    /// `.accessibility(activationPoint:)`, in unit coordinates.
188    ActivationPoint(f64, f64),
189    /// `.accessibility(inputLabels:)` — what a user may say to activate this.
190    InputLabels(&'a [&'a str]),
191    /// `.accessibility(selectionIdentifier:)` — which item of a group this is.
192    SelectionIdentifier(views::HashableValue<'a>),
193}
194
195/// What a drag carries — Swift's `some Transferable`, plus the container
196/// overload that is a different modifier of the same name.
197#[derive(Clone, Copy, Debug)]
198pub enum Draggable<'a> {
199    /// A piece of text, droppable anywhere that takes one.
200    Text(&'a str),
201    /// `.draggable(containerItemID:containerNamespace:)` — reordering within a
202    /// container rather than dragging anything out of it.
203    ContainerItem(views::HashableValue<'a>),
204}
205
206/// Where a scroll view should be — Swift's two `scrollPosition` overloads.
207pub enum ScrollTarget<'a> {
208    /// Pinned to an edge.
209    Edge(views::Edge),
210    /// Scrolled to the row with this identity, at an optional unit anchor.
211    Id {
212        /// The identity to scroll to, which SwiftUI also writes back.
213        state: &'a views::BoundState<String>,
214        /// `None` is SwiftUI's own default.
215        anchor: Option<(f64, f64)>,
216    },
217}
218
219/// Where `@AppStorage` below this point reads and writes.
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub enum AppStorageStore<'a> {
222    /// `UserDefaults.standard`.
223    Standard,
224    /// `UserDefaults(suiteName:)` — a shared container, usually.
225    Suite(&'a str),
226}
227
228impl View {
229    /// `.environment(_:_:)` — one spelling for every property.
230    ///
231    /// See [`EnvironmentArg`]. The leading-dot caveat from [`ShapeStyleArg`]
232    /// applies: name the value's type rather than writing `.environment(.large)`.
233    pub fn environment<V: EnvironmentArg>(self, value: V) -> Self {
234        value.environment(self)
235    }
236
237    /// `.navigationViewStyle(_:)`.
238    pub fn navigation_view_style(self, style: NavigationViewStyle) -> Self {
239        Self::new(match style {
240            NavigationViewStyle::Automatic => {
241                views::navigation_view_style_automatic(&self.handle)
242            }
243            NavigationViewStyle::Columns => views::navigation_view_style_columns(&self.handle),
244            NavigationViewStyle::DoubleColumn => {
245                views::navigation_view_style_double_column(&self.handle)
246            }
247            #[cfg(not(target_os = "macos"))]
248            NavigationViewStyle::Stack => views::navigation_view_style_stack(&self.handle),
249        })
250    }
251
252    /// `.modifier(_:)` — apply a `ViewModifier` this crate has no name for.
253    ///
254    /// The escape hatch. Every bound modifier goes through this internally;
255    /// exposing it means a SwiftUI modifier type the crate has never heard of
256    /// can still be applied, given its metadata and the bytes of a value.
257    ///
258    /// # Safety
259    /// `modifier_meta` must be metadata for a type conforming to
260    /// `ViewModifier`, and `bytes` a live value of exactly that type. Neither
261    /// is checked: a mismatch hands SwiftUI a value of the wrong shape and it
262    /// writes through it.
263    pub unsafe fn modifier(
264        self,
265        modifier_meta: *const core::ffi::c_void,
266        bytes: &[u8],
267    ) -> Self {
268        Self::new(unsafe { views::apply_modifier(&self.handle, modifier_meta, bytes) })
269    }
270
271    /// `.accessibility(…)` — the deprecated family, under its Swift name.
272    ///
273    /// See [`Accessibility`]. New code should prefer the modern spellings,
274    /// which are separate methods here as they are in Swift.
275    pub fn accessibility(self, what: Accessibility<'_>) -> Self {
276        Self::new(match what {
277            Accessibility::Label(s) => views::accessibility_label(&self.handle, s),
278            Accessibility::Hint(s) => views::accessibility_hint(&self.handle, s),
279            Accessibility::Value(s) => views::accessibility_value(&self.handle, s, true),
280            Accessibility::Hidden(b) => views::accessibility_hidden(&self.handle, b),
281            Accessibility::Identifier(s) => views::accessibility_identifier(&self.handle, s),
282            Accessibility::SortPriority(p) => {
283                views::accessibility_sort_priority(&self.handle, p)
284            }
285            Accessibility::ActivationPoint(x, y) => {
286                views::accessibility_activation_point(&self.handle, x, y)
287            }
288            Accessibility::InputLabels(l) => {
289                views::accessibility_input_labels(&self.handle, l, true)
290            }
291            Accessibility::SelectionIdentifier(id) => {
292                views::accessibility_selection_identifier(&self.handle, id)
293            }
294        })
295    }
296
297    /// `.draggable(_:)` — what comes out when this view is dragged.
298    pub fn draggable(self, payload: Draggable<'_>) -> Self {
299        Self::new(match payload {
300            Draggable::Text(t) => views::draggable_text(&self.handle, t),
301            Draggable::ContainerItem(id) => views::draggable_container_item(&self.handle, id),
302        })
303    }
304
305    /// `.scrollPosition(_:)` — where the scroll view should sit.
306    pub fn scroll_position(self, target: ScrollTarget<'_>) -> Self {
307        Self::new(match target {
308            ScrollTarget::Edge(e) => views::scroll_position_edge(&self.handle, e),
309            ScrollTarget::Id { state, anchor } => {
310                views::scroll_position_id(&self.handle, state, anchor)
311            }
312        })
313    }
314
315    /// `.transformEnvironment(_:transform:)` — read a property, change it,
316    /// write it back for everything below.
317    ///
318    /// The key is the type parameter, the way the key path is the argument in
319    /// Swift:
320    ///
321    /// ```no_run
322    /// # use swiftui_native::{prelude::*, state::ColorSchemeKey, views::ColorScheme};
323    /// text("x").transform_environment::<ColorSchemeKey>(|_| ColorScheme::Dark);
324    /// ```
325    pub fn transform_environment<K: crate::state::EnvKey>(
326        self,
327        action: impl Fn(K::Value) -> K::Value + 'static,
328    ) -> Self {
329        Self::new(views::transform_environment::<K>(&self.handle, action))
330    }
331
332    /// `.defaultAppStorage(_:)`.
333    ///
334    /// Panics on a suite name `UserDefaults` refuses — the app's own bundle
335    /// identifier, or the global domain. [`views::default_app_storage_suite`]
336    /// returns the `None` instead, for a caller that wants to handle it.
337    pub fn default_app_storage(self, store: AppStorageStore<'_>) -> Self {
338        Self::new(match store {
339            AppStorageStore::Standard => views::default_app_storage_standard(&self.handle),
340            AppStorageStore::Suite(name) => {
341                views::default_app_storage_suite(&self.handle, name)
342                    .unwrap_or_else(|| panic!("UserDefaults refused the suite {name:?}"))
343            }
344        })
345    }
346}
347
348// ── Swift's spelling for modifiers this crate named for what they do ─────
349//
350// SwiftUI names these for the *effect* they apply; the bindings here are named
351// for the operation. Both resolve to the same call, so a view transcribed from
352// Swift documentation compiles without a lookup table. This is the whole list —
353// four — rather than a policy of aliasing everything.
354
355impl View {
356    /// `.rotationEffect(_:)` — [`View::rotation`] about the centre, which is
357    /// the anchor SwiftUI defaults to.
358    pub fn rotation_effect(self, degrees: f64) -> Self {
359        self.rotation(degrees, 0.5, 0.5)
360    }
361
362    /// `.rotation3DEffect(_:axis:)` — [`View::rotation_3d`] under SwiftUI's
363    /// name.
364    pub fn rotation_3d_effect(self, degrees: f64, ax: f64, ay: f64, az: f64) -> Self {
365        self.rotation_3d(degrees, ax, ay, az)
366    }
367
368    /// `.scaleEffect(x:y:)` — [`View::scale`] under SwiftUI's name.
369    pub fn scale_effect(self, sx: f64, sy: f64) -> Self {
370        self.scale(sx, sy)
371    }
372
373    /// `.multilineTextAlignment(_:)` — [`View::multiline_alignment`] under
374    /// SwiftUI's name.
375    pub fn multiline_text_alignment(self, align: crate::views::TextAlign) -> Self {
376        self.multiline_alignment(align)
377    }
378}
379
380// ── `.clipShape(_:)` ─────────────────────────────────────────────────────
381
382/// A shape `clipShape` accepts — Swift's `some Shape`.
383///
384/// The same arrangement as [`ShapeStyleArg`]: one modifier in Swift, several
385/// bindings here because each concrete shape is its own Swift type, and a
386/// trait to put them back behind one spelling.
387///
388/// The leading-dot caveat applies here too — `.clip_shape(.circle)` cannot
389/// resolve, because a generic parameter gives `Into` nothing to infer from.
390pub trait ClipShapeArg {
391    /// Clip `view` to this shape.
392    fn clip_shape(self, view: View) -> View;
393}
394
395/// The shapes `clipShape` can take, as values rather than types.
396///
397/// A corner radius belongs to the rounded ones and nothing else, which an enum
398/// says and a set of parallel methods does not.
399#[derive(Clone, Copy, Debug, PartialEq)]
400pub enum ClipShape {
401    Circle,
402    Capsule,
403    Ellipse,
404    /// `RoundedRectangle(cornerRadius:)`, whose corner is one continuous
405    /// curve — leaving the style out in Swift means `.continuous`.
406    RoundedRect(f64),
407    /// The same with `style: .circular`, whose corner is a quarter circle.
408    /// This is the one that has to be asked for; measured, `RoundedRect`
409    /// above and `style: .continuous` build the same value.
410    CircularRoundedRect(f64),
411}
412
413impl ClipShapeArg for ClipShape {
414    fn clip_shape(self, view: View) -> View {
415        match self {
416            Self::Circle => view.clip_circle(),
417            Self::Capsule => view.clip_capsule(),
418            Self::Ellipse => view.clip_ellipse(),
419            Self::RoundedRect(r) => view.clip_rounded_rect(r),
420            Self::CircularRoundedRect(r) => view.clip_rounded_rect_styled(r, false),
421        }
422    }
423}
424
425impl View {
426    /// `.clipShape(_:)` — one spelling for every shape.
427    ///
428    /// ```no_run
429    /// # use swiftui_native::{prelude::*, view::style::ClipShape};
430    /// # let v = text("x");
431    /// v.clip_shape(ClipShape::Circle)
432    ///     .clip_shape(ClipShape::RoundedRect(8.0));
433    /// ```
434    pub fn clip_shape<S: ClipShapeArg>(self, shape: S) -> Self {
435        shape.clip_shape(self)
436    }
437}