1mod availability;
5mod builders;
6mod check;
7mod choice;
8mod chrome;
9pub(crate) mod custom;
10pub(crate) mod file;
11mod placement;
12mod read_only;
13mod render;
14#[cfg(feature = "rich-text")]
15mod rich_text;
16mod text;
17
18use std::sync::Arc;
19
20pub(crate) use availability::{Disabled, Hidden};
21pub use builders::{
22 ChoiceField, CustomField, FileField, IntoFormValue, IntoOptions, RepeaterField, TextField,
23};
24pub(crate) use check::Items;
25pub(crate) use choice::{ChoiceControl, Relationship, option_view};
26pub(crate) use chrome::{FieldChrome, error_kind, render_field};
27pub(crate) use custom::{Control, ControlInput};
28pub use custom::{Switch, Toggle};
29pub(crate) use file::{FileControl, is_linkable, is_rooted, stored_upload};
30pub(crate) use placement::{Placement, Posting};
31pub(crate) use read_only::{read_only, value_cell};
32#[cfg(feature = "rich-text")]
33use rich_text::RichTextControl;
34#[cfg(feature = "rich-text")]
35pub use rich_text::RichTextField;
36pub(crate) use text::{Kind, Pattern, TextControl, password_reveal};
37use toasty::stmt::{List, Path};
38use topcoat::context::Cx;
39
40use super::{condition::Condition, layouts::Span, lenses::Binding, repeater::RepeaterControl};
41use crate::{
42 Text,
43 schema::FormInput,
44 values::{FieldError, FormScalar, FormValue},
45};
46
47pub struct Field {
98 binding: Binding,
99 label: Option<Text>,
101 required: bool,
104 control: ControlKind,
105 condition: Option<Condition>,
107 span: Option<Span>,
109 checkbox: bool,
111 accepted: bool,
113 help: Option<Text>,
115 disabled: Disabled,
117 disabling: Option<Condition>,
119 hidden: Hidden,
122 default: Option<FormValue>,
124}
125
126pub(crate) enum ControlKind {
128 Text(TextControl),
130 #[cfg(feature = "rich-text")]
132 RichText(RichTextControl),
133 Choice(ChoiceControl),
135 File(FileControl),
137 Custom(Arc<dyn Control>),
139 Repeater(RepeaterControl),
141}
142
143impl std::fmt::Debug for Field {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 let control = match &self.control {
146 ControlKind::Text(_) => "text",
147 #[cfg(feature = "rich-text")]
148 ControlKind::RichText(_) => "rich text",
149 ControlKind::Choice(_) => "choice",
150 ControlKind::File(_) => "file",
151 ControlKind::Custom(_) => "custom",
152 ControlKind::Repeater(_) => "repeater",
153 };
154 f.debug_struct("Field")
155 .field("name", &self.name())
156 .field("label", &self.label())
157 .field("required", &self.required)
158 .field("control", &control)
159 .field("condition", &self.condition)
160 .field("help", &self.help)
161 .field("disabled", &self.disabled)
162 .field("disabling", &self.disabling)
163 .field("hidden", &self.hidden)
164 .field("default", &self.default)
165 .finish()
166 }
167}
168
169impl Field {
170 fn bound(binding: Binding, control: ControlKind) -> Self {
171 Self {
172 binding,
173 label: None,
174 required: false,
175 control,
176 condition: None,
177 span: None,
178 checkbox: false,
179 accepted: false,
180 help: None,
181 disabled: Disabled::Never,
182 disabling: None,
183 hidden: Hidden::default(),
184 default: None,
185 }
186 }
187
188 pub fn text<M, T>(lens: impl Into<Path<M, T>>) -> TextField
191 where
192 M: toasty::schema::Model,
193 T: FormScalar + toasty::stmt::IntoExpr<T> + 'static,
194 {
195 let path: Path<M, T> = lens.into();
196 let binding = Binding::of(&path);
197 let control =
198 TextControl::new::<M, T>(path, binding.unique(), binding.unique_with().to_vec());
199 TextField::new(Self::bound(binding, ControlKind::Text(control)))
200 }
201
202 #[doc(hidden)]
204 pub fn embedded_leaf<M, T>(path: Path<M, T>) -> TextField
205 where
206 M: toasty::schema::Model,
207 T: FormScalar,
208 {
209 TextField::new(Self::bound(
210 Binding::of(&path),
211 ControlKind::Text(TextControl::leaf::<T>()),
212 ))
213 }
214
215 pub fn choice<M, T>(lens: impl Into<Path<M, T>>) -> ChoiceField
218 where
219 M: toasty::schema::Model,
220 {
221 ChoiceField::new(Self::bound(
222 Binding::of::<M, T>(&lens.into()),
223 ControlKind::Choice(ChoiceControl::default()),
224 ))
225 }
226
227 pub fn file<M>(lens: impl Into<Path<M, String>>) -> FileField
230 where
231 M: toasty::schema::Model,
232 {
233 FileField::new(Self::bound(
234 Binding::of::<M, String>(&lens.into()),
235 ControlKind::File(FileControl::default()),
236 ))
237 }
238
239 pub fn files<M>(lens: impl Into<Path<M, List<String>>>) -> FileField
251 where
252 M: toasty::schema::Model,
253 {
254 FileField::new(Self::bound(
255 Binding::of::<M, List<String>>(&lens.into()),
256 ControlKind::File(FileControl {
257 multiple: true,
258 ..FileControl::default()
259 }),
260 ))
261 }
262
263 pub fn toggle<M>(lens: impl Into<Path<M, bool>>) -> CustomField
265 where
266 M: toasty::schema::Model,
267 {
268 let mut field = Self::custom(lens, Toggle);
269 field.0.checkbox = true;
270 field
271 }
272
273 pub fn custom<M, T>(lens: impl Into<Path<M, T>>, control: impl Control + 'static) -> CustomField
275 where
276 M: toasty::schema::Model,
277 T: FormScalar,
278 {
279 CustomField::new(Self::bound(
280 Binding::of::<M, T>(&lens.into()),
281 ControlKind::Custom(Arc::new(control)),
282 ))
283 }
284
285 pub fn repeater<M, T>(lens: impl Into<Path<M, List<T>>>) -> RepeaterField
293 where
294 M: toasty::schema::Model,
295 T: FormInput,
296 {
297 RepeaterField::new(Self::bound(
298 Binding::of::<M, List<T>>(&lens.into()),
299 ControlKind::Repeater(RepeaterControl::new::<T>()),
300 ))
301 }
302
303 pub fn text_input<T: FormScalar>(name: impl Into<String>) -> TextField {
306 TextField::new(Self::bound(
307 Self::named(name.into()),
308 ControlKind::Text(TextControl::leaf::<T>()),
309 ))
310 }
311
312 pub fn choice_input(name: impl Into<String>) -> ChoiceField {
315 ChoiceField::new(Self::bound(
316 Self::named(name.into()),
317 ControlKind::Choice(ChoiceControl::default()),
318 ))
319 }
320
321 pub fn toggle_input(name: impl Into<String>) -> CustomField {
323 let mut field = Self::bound(
324 Self::named(name.into()),
325 ControlKind::Custom(Arc::new(Toggle)),
326 );
327 field.checkbox = true;
328 CustomField::new(field)
329 }
330
331 fn named(name: String) -> Binding {
333 let label = Text::field(&name);
334 Binding::named(name, label)
335 }
336
337 pub(crate) fn discriminant(binding: Binding, variants: Vec<(String, Text)>) -> Self {
340 let choice = ChoiceControl {
341 discriminant: true,
342 options: variants,
343 ..ChoiceControl::default()
344 };
345 Self::bound(binding, ControlKind::Choice(choice))
346 }
347
348 pub(crate) fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
349 self.binding.misdeclared()
350 }
351
352 pub fn name(&self) -> &str {
354 self.binding.name()
355 }
356
357 pub(crate) fn label(&self) -> Text {
359 self.label.clone().unwrap_or_else(|| self.binding.label())
360 }
361
362 pub(crate) fn offers_nothing(&self) -> bool {
364 self.as_choice().is_some_and(ChoiceControl::offers_nothing)
365 }
366
367 pub(crate) fn is_password(&self) -> bool {
369 matches!(&self.control, ControlKind::Text(text) if text.password)
370 }
371
372 pub(crate) fn as_repeater(&self) -> Option<&RepeaterControl> {
373 match &self.control {
374 ControlKind::Repeater(repeater) => Some(repeater),
375 _ => None,
376 }
377 }
378
379 pub(crate) fn prefix(&mut self, prefix: &str) {
381 let label = self.label();
382 self.binding = Binding::named(format!("{prefix}{}", self.name()), label);
383 self.prefix_comparisons(prefix);
384 }
385
386 pub(crate) fn as_choice(&self) -> Option<&ChoiceControl> {
387 match &self.control {
388 ControlKind::Choice(choice) => Some(choice),
389 _ => None,
390 }
391 }
392
393 pub(crate) fn is_file(&self) -> bool {
394 matches!(self.control, ControlKind::File(_))
395 }
396
397 pub(crate) fn is_unique(&self) -> bool {
398 matches!(&self.control, ControlKind::Text(text) if text.unique)
399 }
400
401 pub(crate) fn unique_with(&self) -> Vec<&[String]> {
403 match &self.control {
404 ControlKind::Text(text) if text.unique => text.unique_with(),
405 _ => Vec::new(),
406 }
407 }
408
409 pub(crate) fn condition(&self) -> Option<&Condition> {
411 self.condition.as_ref()
412 }
413
414 pub(crate) fn span(&self) -> Option<Span> {
416 self.span
417 }
418
419 pub(crate) fn set_condition(&mut self, condition: Condition) {
421 self.condition = Some(condition);
422 }
423
424 pub(crate) fn is_checkbox(&self) -> bool {
426 self.checkbox
427 }
428
429 pub(crate) fn can_post(&self, value: &str) -> Option<bool> {
433 if self.checkbox {
434 return Some(value == "true" || value == "false");
435 }
436 match &self.control {
437 ControlKind::Choice(choice) if choice.is_multiple() => Some(false),
439 ControlKind::Choice(choice) if !choice.is_relationship() => {
440 Some(value.is_empty() || choice.label_of(value).is_some())
441 }
442 _ => None,
443 }
444 }
445
446 pub(crate) fn refuses(&self, value: &FormValue) -> bool {
450 match (&self.control, value) {
451 (ControlKind::Choice(choice), FormValue::List(items)) if choice.is_multiple() => {
452 !choice.is_relationship()
453 && !choice.tags
454 && items.iter().any(|item| choice.label_of(item).is_none())
455 }
456 (ControlKind::Choice(choice), _) if choice.is_multiple() => true,
457 (ControlKind::Text(text), FormValue::Text(value)) => {
458 !value.trim().is_empty() && !text.parses(value)
459 }
460 (_, FormValue::Text(value)) => self.can_post(value) == Some(false),
461 _ => true,
462 }
463 }
464
465 pub(crate) fn is_required(&self) -> bool {
467 self.required
468 }
469
470 pub(crate) fn default_value(&self) -> Option<&FormValue> {
472 self.default.as_ref()
473 }
474
475 fn chrome(
477 &self,
478 cx: &Cx,
479 id: String,
480 error: Option<&FieldError>,
481 fallback: Option<String>,
482 ) -> FieldChrome {
483 let mut chrome = FieldChrome::new(cx, id, error, &self.label(), fallback);
484 chrome.help = self.help.clone();
485 chrome
486 }
487
488 pub(crate) fn set_required(&mut self, required: bool) {
490 self.required = required;
491 }
492
493 pub(crate) fn is_nullable(&self) -> bool {
495 self.binding.nullable()
496 }
497
498 pub(crate) fn same_value(&self, a: &str, b: &str) -> bool {
500 match &self.control {
501 ControlKind::Text(text) => text.same_value(a, b),
502 _ => a.trim() == b.trim(),
503 }
504 }
505
506 pub(crate) fn parent_key(&self) -> Option<&str> {
508 self.as_choice().and_then(ChoiceControl::parent_key)
509 }
510
511 pub(crate) async fn validate_exists(
514 &self,
515 cx: &Cx,
516 value: &FormValue,
517 parent: Option<&str>,
518 stored: Option<&FormValue>,
519 ) -> Vec<crate::Text> {
520 match &self.control {
521 ControlKind::Choice(choice) => {
522 choice
523 .validate_exists(cx, &self.label(), value, parent, stored)
524 .await
525 }
526 _ => Vec::new(),
527 }
528 }
529
530 pub(crate) async fn recheck(
533 &self,
534 cx: &Cx,
535 value: &FormValue,
536 parent: Option<&str>,
537 stored: Option<&FormValue>,
538 ex: &mut dyn toasty::Executor,
539 ) -> Vec<crate::Text> {
540 match &self.control {
541 ControlKind::Choice(choice) => {
542 choice
543 .recheck(cx, &self.label(), value, parent, stored, ex)
544 .await
545 }
546 _ => Vec::new(),
547 }
548 }
549
550 pub(crate) async fn keep_unseen(
553 &self,
554 cx: &Cx,
555 value: &FormValue,
556 stored: &FormValue,
557 ex: &mut dyn toasty::Executor,
558 ) -> Option<FormValue> {
559 match &self.control {
560 ControlKind::Choice(choice) => choice.keep_unseen(cx, value, stored, ex).await,
561 _ => None,
562 }
563 }
564}
565
566#[cfg(test)]
567mod test_support {
568 pub(super) use crate::test_support::DummyUser;
569}
570
571#[cfg(test)]
572mod tests;