1use std::cell::RefCell;
30use std::collections::{HashMap, HashSet};
31use std::rc::Rc;
32
33use gpui::App;
34
35use super::signal::Signal;
36
37pub type Validator = Box<dyn Fn(&str) -> Option<String> + 'static>;
39
40#[derive(Default)]
42pub struct FormState {
43 values: HashMap<&'static str, String>,
44 errors: HashMap<&'static str, String>,
45 validators: HashMap<&'static str, Validator>,
46}
47
48impl FormState {
49 pub fn new() -> Self {
50 FormState::default()
51 }
52
53 pub fn field(mut self, name: &'static str, initial: impl Into<String>) -> Self {
55 self.values.insert(name, initial.into());
56 self
57 }
58
59 pub fn validator(mut self, name: &'static str, validator: Validator) -> Self {
61 self.validators.insert(name, validator);
62 self
63 }
64
65 pub fn value(&self, name: &str) -> &str {
67 self.values.get(name).map(String::as_str).unwrap_or("")
68 }
69
70 pub fn set(&mut self, name: &'static str, value: impl Into<String>) {
72 self.values.insert(name, value.into());
73 self.errors.remove(name);
74 }
75
76 pub fn validate_field(&mut self, name: &'static str) -> bool {
78 if let Some(validator) = self.validators.get(name) {
79 let value = self.values.get(name).map(String::as_str).unwrap_or("");
80 match validator(value) {
81 Some(message) => {
82 self.errors.insert(name, message);
83 return false;
84 }
85 None => {
86 self.errors.remove(name);
87 }
88 }
89 }
90 true
91 }
92
93 pub fn validate(&mut self) -> bool {
95 let names: Vec<&'static str> = self.validators.keys().copied().collect();
96 let mut ok = true;
97 for name in names {
98 ok &= self.validate_field(name);
99 }
100 ok
101 }
102
103 pub fn error(&self, name: &str) -> Option<&str> {
105 self.errors.get(name).map(String::as_str)
106 }
107
108 pub fn is_valid(&self) -> bool {
110 self.errors.is_empty()
111 }
112}
113
114pub type FormValues = HashMap<&'static str, String>;
117
118pub type Rule = Box<dyn Fn(&str, &FormValues) -> Option<String> + 'static>;
121
122pub mod validators {
124 use super::{FormValues, Rule, Validator};
125
126 pub fn required() -> Validator {
128 Box::new(|v: &str| {
129 if v.trim().is_empty() {
130 Some("Required".to_string())
131 } else {
132 None
133 }
134 })
135 }
136
137 pub fn min_len(n: usize) -> Validator {
139 Box::new(move |v: &str| {
140 if v.chars().count() < n {
141 Some(format!("Must be at least {n} characters"))
142 } else {
143 None
144 }
145 })
146 }
147
148 pub fn max_len(n: usize) -> Validator {
150 Box::new(move |v: &str| {
151 if v.chars().count() > n {
152 Some(format!("Must be at most {n} characters"))
153 } else {
154 None
155 }
156 })
157 }
158
159 pub fn email() -> Validator {
161 Box::new(|v: &str| {
162 let ok = v
163 .split_once('@')
164 .map(|(user, domain)| !user.is_empty() && domain.contains('.') && !domain.starts_with('.'))
165 .unwrap_or(false);
166 if ok {
167 None
168 } else {
169 Some("Enter a valid email".to_string())
170 }
171 })
172 }
173
174 pub fn numeric() -> Validator {
176 Box::new(|v: &str| {
177 if v.trim().parse::<f64>().is_ok() {
178 None
179 } else {
180 Some("Enter a number".to_string())
181 }
182 })
183 }
184
185 pub fn min_value(min: f64) -> Validator {
187 Box::new(move |v: &str| match v.trim().parse::<f64>() {
188 Ok(n) if n >= min => None,
189 _ => Some(format!("Must be at least {min}")),
190 })
191 }
192
193 pub fn max_value(max: f64) -> Validator {
195 Box::new(move |v: &str| match v.trim().parse::<f64>() {
196 Ok(n) if n <= max => None,
197 _ => Some(format!("Must be at most {max}")),
198 })
199 }
200
201 pub fn one_of(options: &'static [&'static str]) -> Validator {
203 Box::new(move |v: &str| {
204 if options.contains(&v) {
205 None
206 } else {
207 Some("Not an allowed value".to_string())
208 }
209 })
210 }
211
212 pub fn matches(pred: impl Fn(&str) -> bool + 'static, message: &'static str) -> Validator {
214 Box::new(move |v: &str| {
215 if pred(v) {
216 None
217 } else {
218 Some(message.to_string())
219 }
220 })
221 }
222
223 pub fn equals_field(other: &'static str, message: &'static str) -> Rule {
226 Box::new(move |v: &str, values: &FormValues| {
227 if values.get(other).map(String::as_str) == Some(v) {
228 None
229 } else {
230 Some(message.to_string())
231 }
232 })
233 }
234}
235
236pub struct Form {
239 inner: Rc<FormInner>,
240}
241
242struct FormInner {
243 order: RefCell<Vec<&'static str>>,
244 fields: RefCell<HashMap<&'static str, Signal<String>>>,
245 rules: RefCell<HashMap<&'static str, Vec<Rule>>>,
246 errors: Signal<HashMap<&'static str, String>>,
247 touched: RefCell<HashSet<&'static str>>,
248}
249
250impl Clone for Form {
251 fn clone(&self) -> Self {
252 Form {
253 inner: self.inner.clone(),
254 }
255 }
256}
257
258impl Form {
259 pub fn new(cx: &mut App) -> Self {
260 Form {
261 inner: Rc::new(FormInner {
262 order: RefCell::new(Vec::new()),
263 fields: RefCell::new(HashMap::new()),
264 rules: RefCell::new(HashMap::new()),
265 errors: Signal::new(cx, HashMap::new()),
266 touched: RefCell::new(HashSet::new()),
267 }),
268 }
269 }
270
271 pub fn field(self, cx: &mut App, name: &'static str, initial: impl Into<String>) -> Self {
275 let signal = Signal::new(cx, initial.into());
276 let form = self.clone();
277 cx.observe(signal.entity(), move |_observed, cx| {
278 form.inner.touched.borrow_mut().insert(name);
279 if form.inner.errors.read(cx).contains_key(name) {
280 form.validate_field(cx, name);
281 }
282 })
283 .detach();
284 self.inner.order.borrow_mut().push(name);
285 self.inner.fields.borrow_mut().insert(name, signal);
286 self
287 }
288
289 pub fn rule(self, name: &'static str, validator: Validator) -> Self {
292 self.rule_form(name, Box::new(move |value, _values| validator(value)))
293 }
294
295 pub fn rule_form(self, name: &'static str, rule: Rule) -> Self {
298 self
299 .inner
300 .rules
301 .borrow_mut()
302 .entry(name)
303 .or_default()
304 .push(rule);
305 self
306 }
307
308 pub fn signal(&self, name: &str) -> Signal<String> {
311 self
312 .inner
313 .fields
314 .borrow()
315 .get(name)
316 .unwrap_or_else(|| panic!("guise: unknown form field {name:?}"))
317 .clone()
318 }
319
320 pub fn errors(&self) -> Signal<HashMap<&'static str, String>> {
323 self.inner.errors.clone()
324 }
325
326 pub fn value(&self, cx: &App, name: &str) -> String {
327 self.signal(name).get(cx)
328 }
329
330 pub fn set(&self, cx: &mut App, name: &str, value: impl Into<String>) {
331 self.signal(name).set_if_changed(cx, value.into());
332 }
333
334 pub fn values(&self, cx: &App) -> FormValues {
336 let fields = self.inner.fields.borrow();
337 fields
338 .iter()
339 .map(|(name, signal)| (*name, signal.get(cx)))
340 .collect()
341 }
342
343 pub fn error(&self, cx: &App, name: &str) -> Option<String> {
345 self.inner.errors.read(cx).get(name).cloned()
346 }
347
348 pub fn touched(&self, name: &str) -> bool {
350 self.inner.touched.borrow().contains(name)
351 }
352
353 pub fn validate_field(&self, cx: &mut App, name: &'static str) -> bool {
355 let values = self.values(cx);
356 let value = values.get(name).cloned().unwrap_or_default();
357 let failure = {
358 let rules = self.inner.rules.borrow();
359 rules
360 .get(name)
361 .and_then(|list| list.iter().find_map(|rule| rule(&value, &values)))
362 };
363 let ok = failure.is_none();
364 self.inner.errors.update(cx, |errors| match failure {
365 Some(message) => {
366 errors.insert(name, message);
367 }
368 None => {
369 errors.remove(name);
370 }
371 });
372 ok
373 }
374
375 pub fn validate(&self, cx: &mut App) -> bool {
378 let names: Vec<&'static str> = self.inner.order.borrow().clone();
379 let mut ok = true;
380 for name in names {
381 ok &= self.validate_field(cx, name);
382 }
383 ok
384 }
385
386 pub fn is_valid(&self, cx: &App) -> bool {
388 self.inner.errors.read(cx).is_empty()
389 }
390
391 pub fn submit(&self, cx: &mut App) -> Option<FormValues> {
394 if self.validate(cx) {
395 Some(self.values(cx))
396 } else {
397 None
398 }
399 }
400}
401
402pub fn use_form(cx: &mut App, state: FormState) -> Signal<FormState> {
404 Signal::new(cx, state)
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn required_and_min_len() {
413 let req = validators::required();
414 assert!(req("").is_some());
415 assert!(req(" ").is_some());
416 assert!(req("x").is_none());
417
418 let min = validators::min_len(3);
419 assert!(min("ab").is_some());
420 assert!(min("abc").is_none());
421 }
422
423 #[test]
424 fn email_shape() {
425 let email = validators::email();
426 assert!(email("nope").is_some());
427 assert!(email("a@b").is_some());
428 assert!(email("a@b.com").is_none());
429 }
430
431 #[test]
432 fn length_and_numeric_bounds() {
433 let max = validators::max_len(3);
434 assert!(max("abcd").is_some());
435 assert!(max("abc").is_none());
436
437 let num = validators::numeric();
438 assert!(num("12.5").is_none());
439 assert!(num(" 7 ").is_none());
440 assert!(num("seven").is_some());
441
442 let min = validators::min_value(18.0);
443 assert!(min("17").is_some());
444 assert!(min("18").is_none());
445 assert!(min("x").is_some());
446
447 let max = validators::max_value(100.0);
448 assert!(max("101").is_some());
449 assert!(max("99.9").is_none());
450 }
451
452 #[test]
453 fn one_of_and_matches() {
454 let choice = validators::one_of(&["red", "green", "blue"]);
455 assert!(choice("green").is_none());
456 assert!(choice("mauve").is_some());
457
458 let upper = validators::matches(|v| v.chars().any(char::is_uppercase), "Need a capital");
459 assert!(upper("hello").is_some());
460 assert_eq!(upper("Hello"), None);
461 }
462
463 #[test]
464 fn equals_field_reads_the_other_value() {
465 let rule = validators::equals_field("password", "Must match");
466 let mut values = FormValues::new();
467 values.insert("password", "hunter2".into());
468 assert!(rule("hunter2", &values).is_none());
469 assert_eq!(rule("hunter3", &values), Some("Must match".to_string()));
470 assert!(rule("", &FormValues::new()).is_some());
472 }
473
474 #[test]
475 fn set_clears_error_then_validate_repopulates() {
476 let mut form = FormState::new()
477 .field("name", "")
478 .validator("name", validators::required());
479 assert!(!form.validate());
480 assert_eq!(form.error("name"), Some("Required"));
481
482 form.set("name", "Ada");
483 assert_eq!(form.error("name"), None);
485 assert!(form.validate());
486 assert!(form.is_valid());
487 }
488}