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)| {
165 !user.is_empty() && domain.contains('.') && !domain.starts_with('.')
166 })
167 .unwrap_or(false);
168 if ok {
169 None
170 } else {
171 Some("Enter a valid email".to_string())
172 }
173 })
174 }
175
176 pub fn numeric() -> Validator {
178 Box::new(|v: &str| {
179 if v.trim().parse::<f64>().is_ok() {
180 None
181 } else {
182 Some("Enter a number".to_string())
183 }
184 })
185 }
186
187 pub fn min_value(min: f64) -> Validator {
189 Box::new(move |v: &str| match v.trim().parse::<f64>() {
190 Ok(n) if n >= min => None,
191 _ => Some(format!("Must be at least {min}")),
192 })
193 }
194
195 pub fn max_value(max: f64) -> Validator {
197 Box::new(move |v: &str| match v.trim().parse::<f64>() {
198 Ok(n) if n <= max => None,
199 _ => Some(format!("Must be at most {max}")),
200 })
201 }
202
203 pub fn one_of(options: &'static [&'static str]) -> Validator {
205 Box::new(move |v: &str| {
206 if options.contains(&v) {
207 None
208 } else {
209 Some("Not an allowed value".to_string())
210 }
211 })
212 }
213
214 pub fn matches(pred: impl Fn(&str) -> bool + 'static, message: &'static str) -> Validator {
216 Box::new(move |v: &str| {
217 if pred(v) {
218 None
219 } else {
220 Some(message.to_string())
221 }
222 })
223 }
224
225 pub fn equals_field(other: &'static str, message: &'static str) -> Rule {
228 Box::new(move |v: &str, values: &FormValues| {
229 if values.get(other).map(String::as_str) == Some(v) {
230 None
231 } else {
232 Some(message.to_string())
233 }
234 })
235 }
236}
237
238pub struct Form {
241 inner: Rc<FormInner>,
242}
243
244struct FormInner {
245 order: RefCell<Vec<&'static str>>,
246 fields: RefCell<HashMap<&'static str, Signal<String>>>,
247 rules: RefCell<HashMap<&'static str, Vec<Rule>>>,
248 errors: Signal<HashMap<&'static str, String>>,
249 touched: RefCell<HashSet<&'static str>>,
250}
251
252impl Clone for Form {
253 fn clone(&self) -> Self {
254 Form {
255 inner: self.inner.clone(),
256 }
257 }
258}
259
260impl Form {
261 pub fn new(cx: &mut App) -> Self {
262 Form {
263 inner: Rc::new(FormInner {
264 order: RefCell::new(Vec::new()),
265 fields: RefCell::new(HashMap::new()),
266 rules: RefCell::new(HashMap::new()),
267 errors: Signal::new(cx, HashMap::new()),
268 touched: RefCell::new(HashSet::new()),
269 }),
270 }
271 }
272
273 pub fn field(self, cx: &mut App, name: &'static str, initial: impl Into<String>) -> Self {
277 let signal = Signal::new(cx, initial.into());
278 let form = self.clone();
279 cx.observe(signal.entity(), move |_observed, cx| {
280 form.inner.touched.borrow_mut().insert(name);
281 if form.inner.errors.read(cx).contains_key(name) {
282 form.validate_field(cx, name);
283 }
284 })
285 .detach();
286 self.inner.order.borrow_mut().push(name);
287 self.inner.fields.borrow_mut().insert(name, signal);
288 self
289 }
290
291 pub fn rule(self, name: &'static str, validator: Validator) -> Self {
294 self.rule_form(name, Box::new(move |value, _values| validator(value)))
295 }
296
297 pub fn rule_form(self, name: &'static str, rule: Rule) -> Self {
300 self.inner
301 .rules
302 .borrow_mut()
303 .entry(name)
304 .or_default()
305 .push(rule);
306 self
307 }
308
309 pub fn signal(&self, name: &str) -> Signal<String> {
312 self.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}