pub fn vstack_spacing(children: &[View], spacing: f64) -> ViewExpand description
VStack(spacing:) { … }.
Examples found in repository?
examples/common/spec.rs (line 478)
472pub fn build(nodes: &[Node]) -> Result<View, String> {
473 match nodes {
474 [] => Err("the spec is empty".to_string()),
475 [one] => node_view(one),
476 many => {
477 let kids: Result<Vec<View>, String> = many.iter().map(node_view).collect();
478 Ok(vstack_spacing(&kids?, 8.0))
479 }
480 }
481}
482
483fn children(node: &Node) -> Result<Vec<View>, String> {
484 node.children.iter().map(node_view).collect()
485}
486
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}
617
618/// A container that takes a single child gets its children stacked.
619fn stack_children(node: &Node) -> Result<View, String> {
620 let kids = children(node)?;
621 Ok(match kids.len() {
622 1 => kids.into_iter().next().expect("one"),
623 _ => vstack_spacing(&kids, node.f64_attr("spacing").unwrap_or(8.0)),
624 })
625}More examples
examples/hot_reload.rs (line 109)
96fn body() -> View {
97 let (src, error, generation, micros) = {
98 let g = state_cell().lock().expect("state");
99 (g.last_good.clone(), g.error.clone(), g.generation, g.micros)
100 };
101
102 // The last version that parsed. It cannot fail — it parsed before — but a
103 // spec that builds views from slots could in principle, so the fallback is
104 // still handled rather than unwrapped.
105 let screen = spec::parse(&src)
106 .and_then(|nodes| spec::build(&nodes))
107 .unwrap_or_else(|e| text(&e).fg_system(SystemColor::Red).padding(20.0));
108
109 vstack_spacing(&[screen, status(error.as_deref(), generation, micros)], 0.0)
110}
111
112/// A strip along the bottom: which generation is on screen, how long the last
113/// reload took, what the slots hold, and the error if there is one.
114///
115/// In the window rather than the terminal because that is where you are
116/// looking. Watching a reloader work from a log means watching two things.
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/settings.rs (lines 91-104)
89fn about_panel() -> View {
90 let s = settings();
91 vstack_spacing(
92 &[
93 system_image("gearshape.fill")
94 .symbol_rendering_mode(Some(views::SymbolRenderingMode::Hierarchical))
95 .view()
96 .font_size(48.0)
97 .fg_system(SystemColor::Blue),
98 text("Settings").text_style(TextStyle::Title).bold(),
99 text!("Signed in as {}", s.name.get()).fg_system(SystemColor::Gray),
100 spacer(),
101 button("Done", hide_about).button_style(views::ButtonStyle::BorderedProminent),
102 ],
103 16.0,
104 )
105 .padding(24.0)
106 .presentation_detents(&[views::PresentationDetent::Medium])
107 .presentation_drag_indicator(Visibility::Visible)
108}examples/dashboard.rs (lines 30-47)
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}examples/sugar.rs (lines 20-27)
4fn main() {
5 let show_detail = true;
6 let items = vec![String::from("alpha"), String::from("beta")];
7
8 let sugared = swiftui! {
9 VStack(spacing: 8.0) {
10 Text("Title").text_style(.large_title)
11 if show_detail {
12 Text("the details")
13 }
14 ForEach(items) { item in
15 Text(item)
16 }
17 }
18 };
19
20 let by_hand = vstack_spacing(
21 &[
22 text("Title").text_style(views::TextStyle::LargeTitle),
23 conditional(show_detail, text("the details"), empty()),
24 for_each_items(&items, |item| text(item)),
25 ],
26 8.0,
27 );
28
29 let a = views::dump_value_to_depth(&sugared.into_handle(), 6);
30 let b = views::dump_value_to_depth(&by_hand.into_handle(), 6);
31 eprintln!("macro == hand-written? {}", a == b);
32 if a != b {
33 for (x, y) in a.lines().zip(b.lines()) {
34 if x != y {
35 eprintln!(" macro: {}\n hand : {}", x.trim(), y.trim());
36 break;
37 }
38 }
39 }
40}examples/todo.rs (lines 108-115)
83fn body() -> View {
84 let todos = items().lock().unwrap().clone();
85 let remaining = todos.iter().filter(|i| !i.done).count();
86
87 vstack_aligned_spacing(
88 &[
89 hstack![
90 text("Todo").text_style(TextStyle::LargeTitle).bold(),
91 spacer(),
92 text!("{remaining} left").fg_system(SystemColor::Gray),
93 ],
94 hstack_spacing(
95 &[
96 textfield_bound("What needs doing?", draft())
97 .text_field_style(views::TextFieldStyle::RoundedBorderTextFieldStyle)
98 .on_submit(SubmitTrigger::Text, add)
99 .submit_label(SubmitLabel::Done),
100 button("Add", add).button_style(views::ButtonStyle::BorderedProminent),
101 ],
102 8.0,
103 ),
104 // The empty state is a branch, so it arrives and leaves rather than
105 // appearing from nothing.
106 view_if!(
107 todos.is_empty(),
108 vstack_spacing(
109 &[
110 system_image("checkmark.seal").view().font_size(40.0)
111 .fg_system(SystemColor::Green),
112 text("Nothing left to do").fg_system(SystemColor::Gray),
113 ],
114 8.0,
115 )
116 .frame_optional(None, Some(160.0), Alignment::Center)
117 .transition(Transition::ScaleWithOpacity { scale: 0.8, anchor: (0.5, 0.5) }),
118 list(for_each_items(
119 &todos.iter().cloned().enumerate().collect::<Vec<_>>(),
120 |(i, it)| row(*i, it),
121 ))
122 .list_style(views::ListStyle::PlainListStyle)
123 ),
124 ],
125 HorizontalAlignment::Leading,
126 Some(14.0),
127 )
128 .padding(20.0)
129}