Skip to main content

Shape

Struct Shape 

Source
pub struct Shape { /* private fields */ }
Expand description

A shape, before it is given something to draw with.

Circle() is a Shape, not yet a View: SwiftUI gives it a default fill when one is used as a view, and .fill() says what to use instead. Keeping that distinction here is what lets circle().fill(.red) build the type the same line of Swift builds, rather than a Circle with a foreground style over it.

Implementations§

Source§

impl Shape

Source

pub fn fill(self, c: Color) -> View

.fill(_) — draw the shape in one colour.

Examples found in repository?
examples/common/spec.rs (line 540)
487fn node_view(node: &Node) -> Result<View, String> {
488    let spacing = node.f64_attr("spacing").unwrap_or(8.0);
489    let base = match node.name.as_str() {
490        "vstack" => {
491            let kids = children(node)?;
492            match node.attr("align") {
493                Some(a) => vstack_aligned_spacing(&kids, h_align(a, node.line)?, Some(spacing)),
494                None => vstack_spacing(&kids, spacing),
495            }
496        }
497        "hstack" => {
498            let kids = children(node)?;
499            match node.attr("align") {
500                Some(a) => hstack_aligned_spacing(&kids, v_align(a, node.line)?, Some(spacing)),
501                None => hstack_spacing(&kids, spacing),
502            }
503        }
504        "zstack" => zstack_aligned(&children(node)?, align(node.attr("align").unwrap_or("center"), node.line)?),
505        "text" => {
506            let s = node.value.as_deref().unwrap_or("");
507            let mut v = text(s);
508            if let Some(style) = node.attr("style") {
509                v = v.text_style(text_style(style, node.line)?);
510            }
511            if let Some(size) = node.f64_attr("size") {
512                v = v.font_size(size);
513            }
514            if node.flag("bold") {
515                v = v.bold();
516            }
517            if node.flag("italic") {
518                v = v.italic();
519            }
520            if let Some(c) = node.attr("color") {
521                v = match tint(c, node.line)? {
522                    Tint::System(sc) => v.fg_system(sc),
523                    Tint::Literal(lc) => v.fg(lc),
524                };
525            }
526            v
527        }
528        "spacer" => spacer(),
529        "divider" => divider(),
530        // The action is a no-op on purpose: a spec read from a file has nowhere
531        // to put behaviour, and pretending otherwise would be worse than a
532        // button that visibly does nothing. `hot_reload.rs` is where a spec
533        // gains behaviour, by being reloaded.
534        "button" => button(node.value.as_deref().unwrap_or("Button"), || {}),
535        "color" => color(parse_color(node.value.as_deref().unwrap_or("#808080"), node.line)?),
536        "image" => system_image(node.value.as_deref().unwrap_or("star")).into_view(),
537        "rect" => match node.attr("color") {
538            Some(c) => match tint(c, node.line)? {
539                Tint::System(sc) => rectangle_shape().fill_system(sc),
540                Tint::Literal(lc) => rectangle_shape().fill(lc),
541            },
542            None => rectangle_shape().into_view(),
543        },
544        "circle" => match node.attr("color") {
545            Some(c) => match tint(c, node.line)? {
546                Tint::System(sc) => circle_shape().fill_system(sc),
547                Tint::Literal(lc) => circle_shape().fill(lc),
548            },
549            None => circle_shape().into_view(),
550        },
551        "capsule" => match node.attr("color") {
552            Some(c) => match tint(c, node.line)? {
553                Tint::System(sc) => capsule_shape().fill_system(sc),
554                Tint::Literal(lc) => capsule_shape().fill(lc),
555            },
556            None => capsule_shape().into_view(),
557        },
558        "progress" => progress(
559            node.value.as_deref().and_then(|v| v.parse().ok()).unwrap_or(0.5),
560            node.f64_attr("total").unwrap_or(1.0),
561        ),
562        "gauge" => gauge(
563            node.value.as_deref().and_then(|v| v.parse().ok()).unwrap_or(0.5),
564            node.f64_attr("min").unwrap_or(0.0),
565            node.f64_attr("max").unwrap_or(1.0),
566            text(node.attr("label").unwrap_or("")),
567        ),
568        "group_box" => group_box(stack_children(node)?),
569        "list" => list(stack_children(node)?),
570        "scroll" => stack_children(node)?.scroll(),
571        "form" => form(stack_children(node)?),
572        // Every bound control needs an explicit `@name`.
573        //
574        // Inventing one — from the line, or from position in the tree — looks
575        // friendlier and is a trap: adding a line above a `toggle` would
576        // change its derived name, and the control would silently reset on the
577        // next reload. A name the file states is a name that survives the file
578        // being edited, which is the entire point of the slots.
579        "toggle" => toggle_bound(
580            node.value.as_deref().unwrap_or("Toggle"),
581            &flag_slot(&require_slot(node)?, node.flag("on")),
582        ),
583        "slider" => {
584            let lo = node.f64_attr("min").unwrap_or(0.0);
585            let hi = node.f64_attr("max").unwrap_or(1.0);
586            slider_bound(
587                &number_slot(&require_slot(node)?, node.f64_attr("value").unwrap_or(lo)),
588                lo,
589                hi,
590            )
591        }
592        "stepper" => stepper_bound(
593            node.value.as_deref().unwrap_or("Stepper"),
594            &number_slot(&require_slot(node)?, node.f64_attr("value").unwrap_or(0.0)),
595            node.f64_attr("min").unwrap_or(0.0),
596            node.f64_attr("max").unwrap_or(100.0),
597            node.f64_attr("step").unwrap_or(1.0),
598        ),
599        "field" => textfield_bound(
600            node.value.as_deref().unwrap_or(""),
601            &string_slot(&require_slot(node)?, node.attr("text").unwrap_or("")),
602        ),
603        "label" => label(
604            node.value.as_deref().unwrap_or(""),
605            node.attr("icon").unwrap_or("star"),
606        ),
607        other => {
608            return Err(format!(
609                "line {}: no view called `{other}`; try one of {}",
610                node.line,
611                NAMES.join(", ")
612            ))
613        }
614    };
615    decorate(base, node)
616}
Source

pub fn fill_system(self, c: SystemColor) -> View

.fill(_) with one of SwiftUI’s own colours.

.blue is not a fixed triple: it is (0, 0.569, 1) on the Mac and #007AFF on iOS, and it changes again in dark appearance. Naming the colour asks SwiftUI for whichever one is right, where spelling out components picks one platform’s answer and freezes it.

Examples found in repository?
examples/inbox.rs (line 84)
77fn row(i: usize) -> View {
78    let m = &MESSAGES[i];
79    hstack_spacing(
80        &[
81            // An unread dot, or the space where one would be.
82            view_if!(
83                m.unread,
84                circle_shape().fill_system(SystemColor::Blue).frame(8.0, 8.0),
85                empty().frame(8.0, 8.0)
86            ),
87            vstack_aligned(
88                &[
89                    text(m.from).bold(),
90                    text(m.subject).font_size(12.0).fg_system(SystemColor::Gray),
91                ],
92                HorizontalAlignment::Leading,
93            ),
94            spacer(),
95        ],
96        8.0,
97    )
98    .padding_edges(EdgeSet::Vertical, Some(4.0))
99    .a11y_label(&format!("{}, {}", m.from, m.subject))
100    .a11y_hint(if m.unread { "Unread" } else { "Read" })
101}
More examples
Hide additional examples
examples/swift_org_tutorial.rs (line 137)
129fn body() -> View {
130    let colour = COLORS[COLOR.load(Ordering::Relaxed) % COLORS.len()];
131
132    vstack![
133        // Text("Why not try…").font(.largeTitle.bold())
134        text("Why not try…").text_style(TextStyle::LargeTitle).bold(),
135        vstack![
136            // Circle().fill(colors.randomElement() ?? .blue).padding().overlay(…)
137            circle_shape().fill_system(colour).padding(16.0).overlay(
138                // Image(systemName: "figure.\(selected.lowercased())")
139                //     .font(.system(size: 144)).foregroundColor(.white)
140                image!("figure.{}", selected().to_lowercase())
141                    .font_size(144.0)
142                    .fg(Color::WHITE),
143            ),
144            // Text("\(selected)!").font(.title)
145            text!("{}!", selected()).text_style(TextStyle::Title),
146        ]
147        .transition(Transition::Slide)
148        .id(ID.load(Ordering::Relaxed)),
149        spacer(),
150        // Button("Try again") { … }.buttonStyle(.borderedProminent)
151        button("Try again", try_again).button_style(ButtonStyle::BorderedProminent),
152    ]
153}
examples/hot_reload.rs (lines 139-143)
117fn status(error: Option<&str>, generation: u64, micros: u64) -> View {
118    let slots = spec::slot_summary();
119    let slot_line = if slots.is_empty() {
120        String::new()
121    } else {
122        slots
123            .iter()
124            .map(|(k, v)| format!("{k}={v}"))
125            .collect::<Vec<_>>()
126            .join("  ")
127    };
128
129    // The error row is *always* present, empty when there is nothing wrong.
130    // An error that appears and disappears would otherwise reflow the screen
131    // above it on every save — which looks exactly like the reloader having
132    // replaced the view, and makes "did the screen survive this?" impossible
133    // to answer by looking.
134    let rows = vec![
135        divider(),
136        hstack_spacing(
137            &[
138                circle_shape()
139                    .fill_system(if error.is_some() {
140                        SystemColor::Orange
141                    } else {
142                        SystemColor::Clear
143                    })
144                    .frame(8.0, 8.0),
145                text(error.unwrap_or(""))
146                    .font_size(11.0)
147                    .fg_system(SystemColor::Orange),
148                spacer(),
149            ],
150            6.0,
151        )
152        .padding(6.0)
153        .frame(348.0, 24.0),
154        hstack_spacing(
155            &[
156                circle_shape()
157                    .fill_system(if error.is_some() {
158                        SystemColor::Orange
159                    } else {
160                        SystemColor::Green
161                    })
162                    .frame(8.0, 8.0),
163                text(&format!("gen {generation}"))
164                    .font_size(11.0)
165                    .fg_system(SystemColor::Secondary),
166                // Microseconds below a millisecond: a reload of a short spec
167                // really is that fast, and "0.0 ms" reads as "not measured".
168                text(&if micros < 1000 {
169                    format!("{micros} \u{b5}s")
170                } else {
171                    format!("{:.1} ms", micros as f64 / 1000.0)
172                })
173                .font_size(11.0)
174                .fg_system(SystemColor::Secondary),
175                spacer(),
176                text(&slot_line).font_size(11.0).fg_system(SystemColor::Secondary),
177            ],
178            8.0,
179        )
180        .padding(6.0)
181        .frame(348.0, 24.0),
182    ];
183    vstack_spacing(&rows, 0.0)
184}
examples/common/spec.rs (line 539)
487fn node_view(node: &Node) -> Result<View, String> {
488    let spacing = node.f64_attr("spacing").unwrap_or(8.0);
489    let base = match node.name.as_str() {
490        "vstack" => {
491            let kids = children(node)?;
492            match node.attr("align") {
493                Some(a) => vstack_aligned_spacing(&kids, h_align(a, node.line)?, Some(spacing)),
494                None => vstack_spacing(&kids, spacing),
495            }
496        }
497        "hstack" => {
498            let kids = children(node)?;
499            match node.attr("align") {
500                Some(a) => hstack_aligned_spacing(&kids, v_align(a, node.line)?, Some(spacing)),
501                None => hstack_spacing(&kids, spacing),
502            }
503        }
504        "zstack" => zstack_aligned(&children(node)?, align(node.attr("align").unwrap_or("center"), node.line)?),
505        "text" => {
506            let s = node.value.as_deref().unwrap_or("");
507            let mut v = text(s);
508            if let Some(style) = node.attr("style") {
509                v = v.text_style(text_style(style, node.line)?);
510            }
511            if let Some(size) = node.f64_attr("size") {
512                v = v.font_size(size);
513            }
514            if node.flag("bold") {
515                v = v.bold();
516            }
517            if node.flag("italic") {
518                v = v.italic();
519            }
520            if let Some(c) = node.attr("color") {
521                v = match tint(c, node.line)? {
522                    Tint::System(sc) => v.fg_system(sc),
523                    Tint::Literal(lc) => v.fg(lc),
524                };
525            }
526            v
527        }
528        "spacer" => spacer(),
529        "divider" => divider(),
530        // The action is a no-op on purpose: a spec read from a file has nowhere
531        // to put behaviour, and pretending otherwise would be worse than a
532        // button that visibly does nothing. `hot_reload.rs` is where a spec
533        // gains behaviour, by being reloaded.
534        "button" => button(node.value.as_deref().unwrap_or("Button"), || {}),
535        "color" => color(parse_color(node.value.as_deref().unwrap_or("#808080"), node.line)?),
536        "image" => system_image(node.value.as_deref().unwrap_or("star")).into_view(),
537        "rect" => match node.attr("color") {
538            Some(c) => match tint(c, node.line)? {
539                Tint::System(sc) => rectangle_shape().fill_system(sc),
540                Tint::Literal(lc) => rectangle_shape().fill(lc),
541            },
542            None => rectangle_shape().into_view(),
543        },
544        "circle" => match node.attr("color") {
545            Some(c) => match tint(c, node.line)? {
546                Tint::System(sc) => circle_shape().fill_system(sc),
547                Tint::Literal(lc) => circle_shape().fill(lc),
548            },
549            None => circle_shape().into_view(),
550        },
551        "capsule" => match node.attr("color") {
552            Some(c) => match tint(c, node.line)? {
553                Tint::System(sc) => capsule_shape().fill_system(sc),
554                Tint::Literal(lc) => capsule_shape().fill(lc),
555            },
556            None => capsule_shape().into_view(),
557        },
558        "progress" => progress(
559            node.value.as_deref().and_then(|v| v.parse().ok()).unwrap_or(0.5),
560            node.f64_attr("total").unwrap_or(1.0),
561        ),
562        "gauge" => gauge(
563            node.value.as_deref().and_then(|v| v.parse().ok()).unwrap_or(0.5),
564            node.f64_attr("min").unwrap_or(0.0),
565            node.f64_attr("max").unwrap_or(1.0),
566            text(node.attr("label").unwrap_or("")),
567        ),
568        "group_box" => group_box(stack_children(node)?),
569        "list" => list(stack_children(node)?),
570        "scroll" => stack_children(node)?.scroll(),
571        "form" => form(stack_children(node)?),
572        // Every bound control needs an explicit `@name`.
573        //
574        // Inventing one — from the line, or from position in the tree — looks
575        // friendlier and is a trap: adding a line above a `toggle` would
576        // change its derived name, and the control would silently reset on the
577        // next reload. A name the file states is a name that survives the file
578        // being edited, which is the entire point of the slots.
579        "toggle" => toggle_bound(
580            node.value.as_deref().unwrap_or("Toggle"),
581            &flag_slot(&require_slot(node)?, node.flag("on")),
582        ),
583        "slider" => {
584            let lo = node.f64_attr("min").unwrap_or(0.0);
585            let hi = node.f64_attr("max").unwrap_or(1.0);
586            slider_bound(
587                &number_slot(&require_slot(node)?, node.f64_attr("value").unwrap_or(lo)),
588                lo,
589                hi,
590            )
591        }
592        "stepper" => stepper_bound(
593            node.value.as_deref().unwrap_or("Stepper"),
594            &number_slot(&require_slot(node)?, node.f64_attr("value").unwrap_or(0.0)),
595            node.f64_attr("min").unwrap_or(0.0),
596            node.f64_attr("max").unwrap_or(100.0),
597            node.f64_attr("step").unwrap_or(1.0),
598        ),
599        "field" => textfield_bound(
600            node.value.as_deref().unwrap_or(""),
601            &string_slot(&require_slot(node)?, node.attr("text").unwrap_or("")),
602        ),
603        "label" => label(
604            node.value.as_deref().unwrap_or(""),
605            node.attr("icon").unwrap_or("star"),
606        ),
607        other => {
608            return Err(format!(
609                "line {}: no view called `{other}`; try one of {}",
610                node.line,
611                NAMES.join(", ")
612            ))
613        }
614    };
615    decorate(base, node)
616}
Source

pub fn view(self) -> View

The shape as a view, with SwiftUI’s default fill.

Source

pub fn stroke(self, c: Color, line_width: f64) -> View

.stroke(_:lineWidth:) — the outline instead of the fill.

Source

pub fn stroke_system(self, c: SystemColor, line_width: f64) -> View

.stroke(_:lineWidth:) with one of SwiftUI’s own colours.

Examples found in repository?
examples/dashboard.rs (line 33)
28fn tile(i: usize) -> View {
29    let value = reading(i);
30    vstack_spacing(
31        &[
32            zstack![
33                circle_shape().stroke_system(SystemColor::Gray, 6.0).opacity(0.25),
34                // The filled part: the same circle trimmed to the fraction, and
35                // turned so the arc starts at the top rather than at three
36                // o'clock.
37                circle_shape()
38                    .trim(0.0, value)
39                    .stroke_system(TINTS[i], 6.0)
40                    .rotate(-90.0),
41                text!("{:.0}", value * 100.0).text_style(TextStyle::Title2).bold(),
42            ]
43            .frame(72.0, 72.0),
44            text(LABELS[i]).font_size(12.0).fg_system(SystemColor::Gray),
45        ],
46        8.0,
47    )
48    .padding(12.0)
49    .a11y_element(AccessibilityChildBehavior::Combine)
50    .a11y_label(LABELS[i])
51    .a11y_value(&format!("{:.0} percent", value * 100.0))
52}
Source

pub fn fill_then_stroke( self, fill: Color, stroke: Color, line_width: f64, ) -> View

.fill(_).stroke(_, lineWidth:) — both at once, which a shape cannot express by chaining because .fill already produces a view.

Works on transformed shapes too: .inset(2) gives a Circle._Inset, which this now spells correctly. It used to assert, because inset(by:) returns some InsettableShape and the binding kept the shape it started as — so the result was named StrokeShapeView<Circle, …>, a type that exists, is the wrong one, and is laid out differently.

The field offsets are still computed by padding each to eight rather than read from the metadata, and still checked against the type’s own size before anything is written.

Source

pub fn fill_then_stroke_border( self, fill: Color, border: Color, line_width: f64, ) -> View

.fill(_).strokeBorder(_, lineWidth:) — filled, then bordered inside its own edge.

The difference from fill_then_stroke is where the line sits: a stroke straddles the edge and a border is drawn within it, so a bordered shape never paints outside its bounds.

Source

pub fn stroke_default(self, line_width: f64) -> View

.stroke(lineWidth:) — outlined in the inherited foreground style, which is what picks up an enclosing .foregroundStyle.

Source

pub fn trim(self, from: f64, to: f64) -> Self

.trim(from:to:) — keep a fraction of the path, as unit fractions.

How a progress ring is drawn: a stroked circle trimmed to the fraction complete, usually with .rotate(-90.0) after it so the arc starts at the top rather than at three o’clock. Without it a “ring showing 70%” is a full circle.

Examples found in repository?
examples/dashboard.rs (line 38)
28fn tile(i: usize) -> View {
29    let value = reading(i);
30    vstack_spacing(
31        &[
32            zstack![
33                circle_shape().stroke_system(SystemColor::Gray, 6.0).opacity(0.25),
34                // The filled part: the same circle trimmed to the fraction, and
35                // turned so the arc starts at the top rather than at three
36                // o'clock.
37                circle_shape()
38                    .trim(0.0, value)
39                    .stroke_system(TINTS[i], 6.0)
40                    .rotate(-90.0),
41                text!("{:.0}", value * 100.0).text_style(TextStyle::Title2).bold(),
42            ]
43            .frame(72.0, 72.0),
44            text(LABELS[i]).font_size(12.0).fg_system(SystemColor::Gray),
45        ],
46        8.0,
47    )
48    .padding(12.0)
49    .a11y_element(AccessibilityChildBehavior::Combine)
50    .a11y_label(LABELS[i])
51    .a11y_value(&format!("{:.0} percent", value * 100.0))
52}
Source

pub fn inset(self, amount: f64) -> Self

.inset(by:) — still a shape, so it can be filled or stroked after.

Insetting before stroking is how you keep a lineWidth inside the frame; stroking alone centres the line on the edge and spills half of it.

Source

pub fn offset(self, x: f64, y: f64) -> Self

.offset(x:y:) on the shape itself, before it is filled.

Source

pub fn rotation(self, degrees: f64) -> Self

.rotation(.degrees(_)) on the shape’s path.

Trait Implementations§

Auto Trait Implementations§

§

impl !Send for Shape

§

impl !Sync for Shape

§

impl Freeze for Shape

§

impl RefUnwindSafe for Shape

§

impl Unpin for Shape

§

impl UnsafeUnpin for Shape

§

impl UnwindSafe for Shape

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.