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
16pub struct Field<Msg> {
31 label: String,
32 hint: Option<String>,
33 error: Option<String>,
34 required: bool,
35 disabled: bool,
36 pub(super) label_width: Option<u16>,
37 body: Vec<Node<Msg>>,
38}
39
40impl<Msg: 'static> Field<Msg> {
41 #[must_use]
44 pub fn new(label: impl Into<String>) -> Self {
45 Self {
46 label: label.into(),
47 hint: None,
48 error: None,
49 required: false,
50 disabled: false,
51 label_width: None,
52 body: vec![Node::new(Flex::new(Axis::Column, Vec::new()), 0)],
53 }
54 }
55
56 #[must_use]
58 pub fn hint(mut self, hint: impl Into<String>) -> Self {
59 self.hint = Some(hint.into());
60 self
61 }
62
63 #[must_use]
66 pub fn error<S: Into<String>>(mut self, error: Option<S>) -> Self {
67 self.error = error.map(Into::into);
68 self
69 }
70
71 #[must_use]
73 pub fn required(mut self, required: bool) -> Self {
74 self.required = required;
75 self
76 }
77
78 #[must_use]
80 pub fn disabled(mut self, disabled: bool) -> Self {
81 self.disabled = disabled;
82 self
83 }
84
85 #[must_use]
88 pub fn label_width(mut self, cells: u16) -> Self {
89 self.label_width = Some(cells);
90 self
91 }
92
93 fn beside(&self, width: u16) -> Option<u16> {
95 self.label_width.filter(|label| width >= cells::sum([*label, LABEL_GAP, MIN_CONTROL]))
96 }
97
98 fn message_lines(&self, width: u16, marker_width: u16) -> (Vec<String>, bool) {
100 match (&self.error, &self.hint) {
101 (Some(error), _) => (text::wrap(error, width.saturating_sub(marker_width + 1)), true),
102 (None, Some(hint)) => (text::wrap(hint, width), false),
103 (None, None) => (Vec::new(), false),
104 }
105 }
106}
107
108impl<Msg: 'static> Container<Msg> for Field<Msg> {
109 fn set_children(&mut self, children: Vec<Node<Msg>>) {
110 let mut column = Node::new(Flex::new(Axis::Column, children), 0);
111 column.layout.width = Length::Fill(1);
112 self.body = vec![column];
113 }
114}
115
116struct Parts {
118 label: Rect,
119 required: Option<Rect>,
120 control: Rect,
121 message_x: i32,
122 message_width: u16,
123}
124
125fn required_word() -> String {
126 crate::t!("quvyta.form.required")
127}
128
129impl<Msg: 'static> Widget<Msg> for Field<Msg> {
130 fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
131 let marker = text::width(&cx.env().icons().glyph("error"));
132 let label_width = text::width(&self.label);
133 let required = if self.required { text::width(&required_word()) } else { 0 };
134 let Some(body) = self.body.first() else {
135 return Size::default();
136 };
137 if let Some(column) = self.beside(available.width) {
138 let control_width = available.width - column - LABEL_GAP;
139 let control = cx.measure_child(body, Size::new(control_width, available.height));
140 let (lines, _) = self.message_lines(control_width, marker);
141 let label_rows = 1 + u16::from(self.required);
142 let right = control.height.saturating_add(clamp_u16(i32::try_from(lines.len()).unwrap_or(i32::MAX)));
143 return Size::new(available.width, label_rows.max(right)).min(available);
144 }
145 let control = cx.measure_child(body, Size::new(available.width, available.height.saturating_sub(1)));
146 let (lines, error) = self.message_lines(available.width, marker);
147 let widest_line = lines.iter().map(|line| text::width(line)).max().unwrap_or(0).saturating_add(if error {
148 marker.saturating_add(1)
149 } else {
150 0
151 });
152 let label_line = label_width.saturating_add(if self.required { required.saturating_add(2) } else { 0 });
153 let height = cells::sum([1, control.height, clamp_u16(i32::try_from(lines.len()).unwrap_or(i32::MAX))]);
154 Size::new(label_line.max(control.width).max(widest_line), height).min(available)
155 }
156
157 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
158 let Some(body) = self.body.first() else {
159 return;
160 };
161 let marker = cx.env().icons().glyph("error").into_owned();
162 let marker_width = text::width(&marker);
163 let required = required_word();
164 let parts = match self.beside(area.width) {
165 Some(column) => {
166 let control_x = area.x + i32::from(column + LABEL_GAP);
167 let control_width = area.width - column - LABEL_GAP;
168 let height = cx.measure_child(body, Size::new(control_width, area.height)).height;
169 Parts {
170 label: Rect::new(area.x, area.y, column, 1),
171 required: self.required.then(|| Rect::new(area.x, area.y + 1, column, 1)),
172 control: Rect::new(control_x, area.y, control_width, height),
173 message_x: control_x,
174 message_width: control_width,
175 }
176 }
177 None => {
178 let label_width = text::width(&self.label).min(area.width);
179 let height = cx.measure_child(body, Size::new(area.width, area.height.saturating_sub(1))).height;
180 let required_x = area.x + i32::from(label_width) + 2;
181 Parts {
182 label: Rect::new(area.x, area.y, label_width, 1),
183 required: self
184 .required
185 .then(|| Rect::new(required_x, area.y, clamp_u16(area.right() - required_x), 1)),
186 control: Rect::new(area.x, area.y + 1, area.width, height),
187 message_x: area.x,
188 message_width: area.width,
189 }
190 }
191 };
192 cx.paint_child(body, parts.control);
194
195 let mut states = Vec::new();
196 if cx.has_focus_within() {
197 states.push(State::Focus);
198 }
199 if self.disabled {
200 states.push(State::Disabled);
201 }
202 let label_style = cx.style("field-label", None, &states).text();
203 let label = text::truncate(&self.label, parts.label.width).into_owned();
204 cx.text(parts.label.x, parts.label.y, &label, label_style, parts.label.width);
205 if let Some(rect) = parts.required {
206 let style = cx.style("field-required", None, &states).text();
207 let word = text::truncate(&required, rect.width).into_owned();
208 cx.text(rect.x, rect.y, &word, style, rect.width);
209 }
210
211 let (lines, error) = self.message_lines(parts.message_width, marker_width);
212 let top = parts.control.bottom();
213 let indent = if error { i32::from(marker_width) + 1 } else { 0 };
214 let style = cx.style(if error { "field-error" } else { "field-hint" }, None, &states).text();
215 if error {
216 cx.text(parts.message_x, top, &marker, style, marker_width);
217 }
218 let width = clamp_u16(i32::from(parts.message_width) - indent);
219 for (y, line) in (top..).zip(lines) {
220 cx.text(parts.message_x + indent, y, &line, style, width);
221 }
222 }
223
224 fn children(&self) -> &[Node<Msg>] {
225 &self.body
226 }
227
228 fn children_mut(&mut self) -> &mut [Node<Msg>] {
229 &mut self.body
230 }
231}