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
//! `Form` — vertical labeled-field layout.
//!
//! Render fields inside the [`show`](Form::show) closure; each gets a label
//! (with an optional required marker) above the input, plus an optional
//! description line below.
//!
//! ```ignore
//! sc::Form::new().show(ui, |form| {
//! form.field("Email", |ui| { ui.add(sc::Input::new(&mut email)); });
//! form.field_with_hint("Password", "At least 8 characters", |ui| {
//! ui.add(sc::Input::new(&mut pw).password(true));
//! });
//! form.required("Full name", |ui| { ui.add(sc::Input::new(&mut name)); });
//! });
//! ```
use egui::Ui;
use crate::common::Size;
use crate::label::{Label, LabelTone};
pub struct Form {
field_gap: f32,
}
impl Default for Form {
fn default() -> Self {
Self::new()
}
}
impl Form {
pub fn new() -> Self {
Self { field_gap: 14.0 }
}
/// Vertical space between fields.
pub fn field_gap(mut self, gap: f32) -> Self {
self.field_gap = gap;
self
}
pub fn show(self, ui: &mut Ui, build: impl FnOnce(&mut FormUi)) {
let mut form = FormUi {
ui,
field_gap: self.field_gap,
first: true,
};
build(&mut form);
}
}
/// Builder handed to the [`Form::show`] closure.
pub struct FormUi<'a> {
ui: &'a mut Ui,
field_gap: f32,
first: bool,
}
impl FormUi<'_> {
fn header(&mut self, label: &str, required: bool) {
if !self.first {
self.ui.add_space(self.field_gap);
}
self.first = false;
self.ui.horizontal(|ui| {
ui.add(Label::new(label.to_string()).strong().size(Size::Small));
if required {
ui.add(
Label::new("*")
.tone(LabelTone::Danger)
.size(Size::Small),
);
}
});
self.ui.add_space(4.0);
}
/// A labeled field.
pub fn field(&mut self, label: impl AsRef<str>, content: impl FnOnce(&mut Ui)) {
self.header(label.as_ref(), false);
content(self.ui);
}
/// A labeled field with a required marker.
pub fn required(&mut self, label: impl AsRef<str>, content: impl FnOnce(&mut Ui)) {
self.header(label.as_ref(), true);
content(self.ui);
}
/// A labeled field with a muted hint line below the input.
pub fn field_with_hint(
&mut self,
label: impl AsRef<str>,
hint: impl Into<String>,
content: impl FnOnce(&mut Ui),
) {
self.header(label.as_ref(), false);
content(self.ui);
self.ui.add_space(3.0);
self.ui.add(Label::new(hint.into()).muted().size(Size::Small));
}
/// Escape hatch: arbitrary content between fields (e.g. a separator or a
/// submit-button row), with the standard field gap applied.
pub fn raw(&mut self, content: impl FnOnce(&mut Ui)) {
if !self.first {
self.ui.add_space(self.field_gap);
}
self.first = false;
content(self.ui);
}
/// Access the underlying [`Ui`] directly.
pub fn ui(&mut self) -> &mut Ui {
self.ui
}
}