Skip to main content

egui_components/
form.rs

1//! `Form` — vertical labeled-field layout.
2//!
3//! Render fields inside the [`show`](Form::show) closure; each gets a label
4//! (with an optional required marker) above the input, plus an optional
5//! description line below.
6//!
7//! ```ignore
8//! sc::Form::new().show(ui, |form| {
9//!     form.field("Email", |ui| { ui.add(sc::Input::new(&mut email)); });
10//!     form.field_with_hint("Password", "At least 8 characters", |ui| {
11//!         ui.add(sc::Input::new(&mut pw).password(true));
12//!     });
13//!     form.required("Full name", |ui| { ui.add(sc::Input::new(&mut name)); });
14//! });
15//! ```
16
17use egui::Ui;
18
19use crate::common::Size;
20use crate::label::{Label, LabelTone};
21
22pub struct Form {
23    field_gap: f32,
24}
25
26impl Default for Form {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl Form {
33    pub fn new() -> Self {
34        Self { field_gap: 14.0 }
35    }
36    /// Vertical space between fields.
37    pub fn field_gap(mut self, gap: f32) -> Self {
38        self.field_gap = gap;
39        self
40    }
41
42    pub fn show(self, ui: &mut Ui, build: impl FnOnce(&mut FormUi)) {
43        let mut form = FormUi {
44            ui,
45            field_gap: self.field_gap,
46            first: true,
47        };
48        build(&mut form);
49    }
50}
51
52/// Builder handed to the [`Form::show`] closure.
53pub struct FormUi<'a> {
54    ui: &'a mut Ui,
55    field_gap: f32,
56    first: bool,
57}
58
59impl FormUi<'_> {
60    fn header(&mut self, label: &str, required: bool) {
61        if !self.first {
62            self.ui.add_space(self.field_gap);
63        }
64        self.first = false;
65        self.ui.horizontal(|ui| {
66            ui.add(Label::new(label.to_string()).strong().size(Size::Small));
67            if required {
68                ui.add(
69                    Label::new("*")
70                        .tone(LabelTone::Danger)
71                        .size(Size::Small),
72                );
73            }
74        });
75        self.ui.add_space(4.0);
76    }
77
78    /// A labeled field.
79    pub fn field(&mut self, label: impl AsRef<str>, content: impl FnOnce(&mut Ui)) {
80        self.header(label.as_ref(), false);
81        content(self.ui);
82    }
83
84    /// A labeled field with a required marker.
85    pub fn required(&mut self, label: impl AsRef<str>, content: impl FnOnce(&mut Ui)) {
86        self.header(label.as_ref(), true);
87        content(self.ui);
88    }
89
90    /// A labeled field with a muted hint line below the input.
91    pub fn field_with_hint(
92        &mut self,
93        label: impl AsRef<str>,
94        hint: impl Into<String>,
95        content: impl FnOnce(&mut Ui),
96    ) {
97        self.header(label.as_ref(), false);
98        content(self.ui);
99        self.ui.add_space(3.0);
100        self.ui.add(Label::new(hint.into()).muted().size(Size::Small));
101    }
102
103    /// Escape hatch: arbitrary content between fields (e.g. a separator or a
104    /// submit-button row), with the standard field gap applied.
105    pub fn raw(&mut self, content: impl FnOnce(&mut Ui)) {
106        if !self.first {
107            self.ui.add_space(self.field_gap);
108        }
109        self.first = false;
110        content(self.ui);
111    }
112
113    /// Access the underlying [`Ui`] directly.
114    pub fn ui(&mut self) -> &mut Ui {
115        self.ui
116    }
117}