1use crate::geometry::{Rect, Size, clamp_u16};
4use crate::text;
5use crate::theme::State;
6use crate::widget::{Axis, Container, Flex, Length, MeasureCx, Node, PaintCx, Widget};
7
8use super::cells;
9
10const LABEL_GAP: u16 = 2;
12
13const MIN_CONTROL: u16 = 16;
15
16const UNBOUNDED: u16 = 4096;
19
20pub struct Field<Msg> {
38 label: String,
39 hint: Option<String>,
40 error: Option<String>,
41 required: bool,
42 disabled: bool,
43 value: bool,
44 pub(super) label_width: Option<u16>,
45 body: Vec<Node<Msg>>,
46}
47
48impl<Msg: 'static> Field<Msg> {
49 #[must_use]
52 pub fn new(label: impl Into<String>) -> Self {
53 Self {
54 label: label.into(),
55 hint: None,
56 error: None,
57 required: false,
58 disabled: false,
59 value: false,
60 label_width: None,
61 body: vec![Node::new(Flex::new(Axis::Column, Vec::new()), 0)],
62 }
63 }
64
65 #[must_use]
69 pub fn value(mut self, text: impl Into<String>) -> Self {
70 self.value = true;
71 self.body = vec![Node::new(Flex::new(Axis::Column, vec![Node::new(super::Text::new(text), 0)]), 0)];
72 self
73 }
74
75 #[must_use]
77 pub fn hint(mut self, hint: impl Into<String>) -> Self {
78 self.hint = Some(hint.into());
79 self
80 }
81
82 #[must_use]
85 pub fn error<S: Into<String>>(mut self, error: Option<S>) -> Self {
86 self.error = error.map(Into::into);
87 self
88 }
89
90 #[must_use]
92 pub fn required(mut self, required: bool) -> Self {
93 self.required = required;
94 self
95 }
96
97 #[must_use]
99 pub fn disabled(mut self, disabled: bool) -> Self {
100 self.disabled = disabled;
101 self
102 }
103
104 #[must_use]
107 pub fn label_width(mut self, cells: u16) -> Self {
108 self.label_width = Some(cells);
109 self
110 }
111
112 fn beside(&self, width: u16, natural: u16) -> Option<u16> {
118 self.label_width.filter(|label| {
119 let room = width.saturating_sub(cells::sum([*label, LABEL_GAP]));
120 room >= MIN_CONTROL && (natural <= room || natural >= UNBOUNDED)
121 })
122 }
123
124 fn required_under_control(&self, column: u16, word: &str) -> bool {
128 self.required && text::width(word) > column
129 }
130
131 fn message_lines(&self, width: u16, marker_width: u16) -> (Vec<String>, bool) {
133 match (&self.error, &self.hint) {
134 (Some(error), _) => (text::wrap(error, width.saturating_sub(marker_width + 1)), true),
135 (None, Some(hint)) => (text::wrap(hint, width), false),
136 (None, None) => (Vec::new(), false),
137 }
138 }
139}
140
141impl<Msg: 'static> Container<Msg> for Field<Msg> {
142 fn set_children(&mut self, children: Vec<Node<Msg>>) {
143 if self.value {
145 return;
146 }
147 let mut column = Node::new(Flex::new(Axis::Column, children), 0);
148 column.layout.width = Length::Fill(1);
149 self.body = vec![column];
150 }
151}
152
153struct Parts {
155 label: Rect,
156 required: Option<Rect>,
157 control: Rect,
158 message_x: i32,
159 message_width: u16,
160}
161
162fn required_word() -> String {
163 crate::t!("quvyta.form.required")
164}
165
166impl<Msg: 'static> Widget<Msg> for Field<Msg> {
167 fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
168 let marker = text::width(&cx.env().icons().glyph("error"));
169 let label_width = text::width(&self.label);
170 let required = if self.required { text::width(&required_word()) } else { 0 };
171 let Some(body) = self.body.first() else {
172 return Size::default();
173 };
174 let natural = cx.measure_child(body, Size::new(UNBOUNDED, available.height)).width;
175 if let Some(column) = self.beside(available.width, natural) {
176 let control_width = available.width - column - LABEL_GAP;
177 let control = cx.measure_child(body, Size::new(control_width, available.height));
178 let (lines, _) = self.message_lines(control_width, marker);
179 let below = self.required_under_control(column, &required_word());
180 let label_rows = 1 + u16::from(self.required && !below);
181 let messages = clamp_u16(i32::try_from(lines.len()).unwrap_or(i32::MAX)).saturating_add(u16::from(below));
182 let right = control.height.saturating_add(messages);
183 return Size::new(available.width, label_rows.max(right)).min(available);
184 }
185 let control = cx.measure_child(body, Size::new(available.width, available.height.saturating_sub(1)));
186 let (lines, error) = self.message_lines(available.width, marker);
187 let widest_line = lines.iter().map(|line| text::width(line)).max().unwrap_or(0).saturating_add(if error {
188 marker.saturating_add(1)
189 } else {
190 0
191 });
192 let label_line = label_width.saturating_add(if self.required { required.saturating_add(2) } else { 0 });
193 let height = cells::sum([1, control.height, clamp_u16(i32::try_from(lines.len()).unwrap_or(i32::MAX))]);
194 Size::new(label_line.max(control.width).max(widest_line), height).min(available)
195 }
196
197 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
198 let Some(body) = self.body.first() else {
199 return;
200 };
201 let marker = cx.env().icons().glyph("error").into_owned();
202 let marker_width = text::width(&marker);
203 let required = required_word();
204 let natural = cx.measure_child(body, Size::new(UNBOUNDED, area.height)).width;
205 let parts = match self.beside(area.width, natural) {
206 Some(column) => {
207 let control_x = area.x + i32::from(column + LABEL_GAP);
208 let control_width = area.width - column - LABEL_GAP;
209 let height = cx.measure_child(body, Size::new(control_width, area.height)).height;
210 let required = if self.required_under_control(column, &required) {
211 Rect::new(control_x, area.y + i32::from(height), control_width, 1)
212 } else {
213 Rect::new(area.x, area.y + 1, column, 1)
214 };
215 Parts {
216 label: Rect::new(area.x, area.y, column, 1),
217 required: self.required.then_some(required),
218 control: Rect::new(control_x, area.y, control_width, height),
219 message_x: control_x,
220 message_width: control_width,
221 }
222 }
223 None => {
224 let label_width = text::width(&self.label).min(area.width);
225 let height = cx.measure_child(body, Size::new(area.width, area.height.saturating_sub(1))).height;
226 let required_x = area.x + i32::from(label_width) + 2;
227 Parts {
228 label: Rect::new(area.x, area.y, label_width, 1),
229 required: self
230 .required
231 .then(|| Rect::new(required_x, area.y, clamp_u16(area.right() - required_x), 1)),
232 control: Rect::new(area.x, area.y + 1, area.width, height),
233 message_x: area.x,
234 message_width: area.width,
235 }
236 }
237 };
238 cx.paint_child(body, parts.control);
240
241 let mut states = Vec::new();
242 if cx.has_focus_within() {
243 states.push(State::Focus);
244 }
245 if self.disabled {
246 states.push(State::Disabled);
247 }
248 let label_style = cx.style("field-label", self.value.then_some("value"), &states).text();
249 let label = text::truncate(&self.label, parts.label.width).into_owned();
250 cx.text(parts.label.x, parts.label.y, &label, label_style, parts.label.width);
251 if let Some(rect) = parts.required {
252 let style = cx.style("field-required", None, &states).text();
253 let word = text::truncate(&required, rect.width).into_owned();
254 cx.text(rect.x, rect.y, &word, style, rect.width);
255 }
256
257 let (lines, error) = self.message_lines(parts.message_width, marker_width);
258 let top = match parts.required {
260 Some(rect) if rect.x == parts.control.x && rect.y >= parts.control.bottom() => rect.bottom(),
261 _ => parts.control.bottom(),
262 };
263 let indent = if error { i32::from(marker_width) + 1 } else { 0 };
264 let style = cx.style(if error { "field-error" } else { "field-hint" }, None, &states).text();
265 if error {
266 cx.text(parts.message_x, top, &marker, style, marker_width);
267 }
268 let width = clamp_u16(i32::from(parts.message_width) - indent);
269 for (y, line) in (top..).zip(lines) {
270 cx.text(parts.message_x + indent, y, &line, style, width);
271 }
272 }
273
274 fn children(&self) -> &[Node<Msg>] {
275 &self.body
276 }
277
278 fn children_mut(&mut self) -> &mut [Node<Msg>] {
279 &mut self.body
280 }
281}