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}