1mod builders;
5mod check;
6mod choice;
7mod chrome;
8pub(crate) mod custom;
9pub(crate) mod file;
10mod placement;
11mod read_only;
12mod render;
13#[cfg(feature = "rich-text")]
14mod rich_text;
15mod text;
16
17use std::sync::Arc;
18
19pub use builders::{
20 ChoiceField, CustomField, FileField, IntoFormValue, IntoOptions, RepeaterField, TextField,
21};
22pub(crate) use check::Items;
23pub(crate) use choice::{ChoiceControl, Relationship, option_view};
24pub(crate) use chrome::{FieldChrome, error_kind, render_field};
25pub(crate) use custom::{Control, ControlInput};
26pub use custom::{Switch, Toggle};
27pub(crate) use file::{FileControl, MAX_FORM_BYTES, is_linkable, is_rooted, stored_upload};
28pub(crate) use placement::Placement;
29pub(crate) use read_only::{read_only, value_cell};
30#[cfg(feature = "rich-text")]
31use rich_text::RichTextControl;
32#[cfg(feature = "rich-text")]
33pub use rich_text::RichTextField;
34pub(crate) use text::{Kind, Pattern, TextControl, password_reveal};
35use toasty::stmt::{List, Path};
36use topcoat::context::Cx;
37
38use super::{condition::Condition, lenses::Binding, repeater::RepeaterControl};
39use crate::{
40 Text,
41 schema::FormInput,
42 values::{FieldError, FormScalar, FormValue},
43};
44
45pub struct Field {
95 binding: Binding,
96 label: Option<Text>,
98 required: bool,
101 control: ControlKind,
102 condition: Option<Condition>,
104 checkbox: bool,
106 accepted: bool,
108 help: Option<Text>,
110 disabled: Disabled,
112 default: Option<FormValue>,
114}
115
116#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub(crate) enum Disabled {
120 #[default]
121 Never,
122 Always,
123 OnEdit,
125}
126
127pub(crate) enum ControlKind {
129 Text(TextControl),
131 #[cfg(feature = "rich-text")]
133 RichText(RichTextControl),
134 Choice(ChoiceControl),
136 File(FileControl),
138 Custom(Arc<dyn Control>),
140 Repeater(RepeaterControl),
142}
143
144impl std::fmt::Debug for Field {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 let control = match &self.control {
147 ControlKind::Text(_) => "text",
148 #[cfg(feature = "rich-text")]
149 ControlKind::RichText(_) => "rich text",
150 ControlKind::Choice(_) => "choice",
151 ControlKind::File(_) => "file",
152 ControlKind::Custom(_) => "custom",
153 ControlKind::Repeater(_) => "repeater",
154 };
155 f.debug_struct("Field")
156 .field("name", &self.name())
157 .field("label", &self.label())
158 .field("required", &self.required)
159 .field("control", &control)
160 .field("condition", &self.condition)
161 .field("help", &self.help)
162 .field("disabled", &self.disabled)
163 .field("default", &self.default)
164 .finish()
165 }
166}
167
168impl Field {
169 fn bound(binding: Binding, control: ControlKind) -> Self {
170 Self {
171 binding,
172 label: None,
173 required: false,
174 control,
175 condition: None,
176 checkbox: false,
177 accepted: false,
178 help: None,
179 disabled: Disabled::Never,
180 default: None,
181 }
182 }
183
184 pub fn text<M, T>(lens: impl Into<Path<M, T>>) -> TextField
187 where
188 M: toasty::schema::Model,
189 T: FormScalar + toasty::stmt::IntoExpr<T> + 'static,
190 {
191 let path: Path<M, T> = lens.into();
192 let binding = Binding::of(&path);
193 let control =
194 TextControl::new::<M, T>(path, binding.unique(), binding.unique_with().to_vec());
195 TextField::new(Self::bound(binding, ControlKind::Text(control)))
196 }
197
198 #[doc(hidden)]
200 pub fn embedded_leaf<M, T>(path: Path<M, T>) -> TextField
201 where
202 M: toasty::schema::Model,
203 T: FormScalar,
204 {
205 TextField::new(Self::bound(
206 Binding::of(&path),
207 ControlKind::Text(TextControl::leaf::<T>()),
208 ))
209 }
210
211 pub fn choice<M, T>(lens: impl Into<Path<M, T>>) -> ChoiceField
214 where
215 M: toasty::schema::Model,
216 {
217 ChoiceField::new(Self::bound(
218 Binding::of::<M, T>(&lens.into()),
219 ControlKind::Choice(ChoiceControl::default()),
220 ))
221 }
222
223 pub fn file<M>(lens: impl Into<Path<M, String>>) -> FileField
226 where
227 M: toasty::schema::Model,
228 {
229 FileField::new(Self::bound(
230 Binding::of::<M, String>(&lens.into()),
231 ControlKind::File(FileControl::default()),
232 ))
233 }
234
235 pub fn toggle<M>(lens: impl Into<Path<M, bool>>) -> CustomField
237 where
238 M: toasty::schema::Model,
239 {
240 let mut field = Self::custom(lens, Toggle);
241 field.0.checkbox = true;
242 field
243 }
244
245 pub fn custom<M, T>(lens: impl Into<Path<M, T>>, control: impl Control + 'static) -> CustomField
247 where
248 M: toasty::schema::Model,
249 T: FormScalar,
250 {
251 CustomField::new(Self::bound(
252 Binding::of::<M, T>(&lens.into()),
253 ControlKind::Custom(Arc::new(control)),
254 ))
255 }
256
257 pub fn repeater<M, T>(lens: impl Into<Path<M, List<T>>>) -> RepeaterField
265 where
266 M: toasty::schema::Model,
267 T: FormInput,
268 {
269 RepeaterField::new(Self::bound(
270 Binding::of::<M, List<T>>(&lens.into()),
271 ControlKind::Repeater(RepeaterControl::new::<T>()),
272 ))
273 }
274
275 pub fn text_input<T: FormScalar>(name: impl Into<String>) -> TextField {
278 TextField::new(Self::bound(
279 Self::named(name.into()),
280 ControlKind::Text(TextControl::leaf::<T>()),
281 ))
282 }
283
284 pub fn choice_input(name: impl Into<String>) -> ChoiceField {
287 ChoiceField::new(Self::bound(
288 Self::named(name.into()),
289 ControlKind::Choice(ChoiceControl::default()),
290 ))
291 }
292
293 pub fn toggle_input(name: impl Into<String>) -> CustomField {
295 let mut field = Self::bound(
296 Self::named(name.into()),
297 ControlKind::Custom(Arc::new(Toggle)),
298 );
299 field.checkbox = true;
300 CustomField::new(field)
301 }
302
303 fn named(name: String) -> Binding {
305 let label = Text::field(&name);
306 Binding::named(name, label)
307 }
308
309 pub(crate) fn discriminant(binding: Binding, variants: Vec<(String, Text)>) -> Self {
312 let choice = ChoiceControl {
313 discriminant: true,
314 options: variants,
315 ..ChoiceControl::default()
316 };
317 Self::bound(binding, ControlKind::Choice(choice))
318 }
319
320 pub(crate) fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
321 self.binding.misdeclared()
322 }
323
324 pub fn name(&self) -> &str {
326 self.binding.name()
327 }
328
329 pub(crate) fn label(&self) -> Text {
331 self.label.clone().unwrap_or_else(|| self.binding.label())
332 }
333
334 pub(crate) fn offers_nothing(&self) -> bool {
336 self.as_choice().is_some_and(ChoiceControl::offers_nothing)
337 }
338
339 pub(crate) fn is_password(&self) -> bool {
341 matches!(&self.control, ControlKind::Text(text) if text.password)
342 }
343
344 pub(crate) fn as_repeater(&self) -> Option<&RepeaterControl> {
345 match &self.control {
346 ControlKind::Repeater(repeater) => Some(repeater),
347 _ => None,
348 }
349 }
350
351 pub(crate) fn prefix(&mut self, prefix: &str) {
353 let label = self.label();
354 self.binding = Binding::named(format!("{prefix}{}", self.name()), label);
355 self.prefix_comparisons(prefix);
356 }
357
358 pub(crate) fn as_choice(&self) -> Option<&ChoiceControl> {
359 match &self.control {
360 ControlKind::Choice(choice) => Some(choice),
361 _ => None,
362 }
363 }
364
365 pub(crate) fn is_file(&self) -> bool {
366 matches!(self.control, ControlKind::File(_))
367 }
368
369 pub(crate) fn is_unique(&self) -> bool {
370 matches!(&self.control, ControlKind::Text(text) if text.unique)
371 }
372
373 pub(crate) fn unique_with(&self) -> Vec<&[String]> {
375 match &self.control {
376 ControlKind::Text(text) if text.unique => text.unique_with(),
377 _ => Vec::new(),
378 }
379 }
380
381 pub(crate) fn condition(&self) -> Option<&Condition> {
383 self.condition.as_ref()
384 }
385
386 pub(crate) fn set_condition(&mut self, condition: Condition) {
388 self.condition = Some(condition);
389 }
390
391 pub(crate) fn is_checkbox(&self) -> bool {
393 self.checkbox
394 }
395
396 pub(crate) fn can_post(&self, value: &str) -> Option<bool> {
400 if self.checkbox {
401 return Some(value == "true" || value == "false");
402 }
403 match &self.control {
404 ControlKind::Choice(choice) if choice.is_multiple() => Some(false),
406 ControlKind::Choice(choice) if !choice.is_relationship() => {
407 Some(value.is_empty() || choice.label_of(value).is_some())
408 }
409 _ => None,
410 }
411 }
412
413 pub(crate) fn refuses(&self, value: &FormValue) -> bool {
417 match (&self.control, value) {
418 (ControlKind::Choice(choice), FormValue::List(items)) if choice.is_multiple() => {
419 !choice.is_relationship()
420 && items.iter().any(|item| choice.label_of(item).is_none())
421 }
422 (ControlKind::Choice(choice), _) if choice.is_multiple() => true,
423 (ControlKind::Text(text), FormValue::Text(value)) => {
424 !value.trim().is_empty() && !text.parses(value)
425 }
426 (_, FormValue::Text(value)) => self.can_post(value) == Some(false),
427 _ => true,
428 }
429 }
430
431 pub(crate) fn is_required(&self) -> bool {
433 self.required
434 }
435
436 pub(crate) fn is_disabled(&self, editing: bool) -> bool {
438 match self.disabled {
439 Disabled::Never => false,
440 Disabled::Always => true,
441 Disabled::OnEdit => editing,
442 }
443 }
444
445 pub(crate) fn is_disabled_at(&self, placement: &Placement<'_>) -> bool {
447 placement.locked || self.is_disabled(placement.editing)
448 }
449
450 pub(crate) fn is_always_disabled(&self) -> bool {
452 self.disabled == Disabled::Always
453 }
454
455 pub(crate) fn default_value(&self) -> Option<&FormValue> {
457 self.default.as_ref()
458 }
459
460 fn chrome(
462 &self,
463 cx: &Cx,
464 id: String,
465 error: Option<&FieldError>,
466 fallback: Option<String>,
467 ) -> FieldChrome {
468 let mut chrome = FieldChrome::new(cx, id, error, &self.label(), fallback);
469 chrome.help = self.help.clone();
470 chrome
471 }
472
473 pub(crate) fn set_required(&mut self, required: bool) {
475 self.required = required;
476 }
477
478 pub(crate) fn is_nullable(&self) -> bool {
480 self.binding.nullable()
481 }
482
483 pub(crate) fn same_value(&self, a: &str, b: &str) -> bool {
485 match &self.control {
486 ControlKind::Text(text) => text.same_value(a, b),
487 _ => a.trim() == b.trim(),
488 }
489 }
490
491 pub(crate) fn parent_key(&self) -> Option<&str> {
493 self.as_choice().and_then(ChoiceControl::parent_key)
494 }
495
496 pub(crate) async fn validate_exists(
499 &self,
500 cx: &Cx,
501 value: &FormValue,
502 parent: Option<&str>,
503 stored: Option<&FormValue>,
504 ) -> Vec<crate::Text> {
505 match &self.control {
506 ControlKind::Choice(choice) => {
507 choice
508 .validate_exists(cx, &self.label(), value, parent, stored)
509 .await
510 }
511 _ => Vec::new(),
512 }
513 }
514
515 pub(crate) async fn recheck(
518 &self,
519 cx: &Cx,
520 value: &FormValue,
521 parent: Option<&str>,
522 stored: Option<&FormValue>,
523 ex: &mut dyn toasty::Executor,
524 ) -> Vec<crate::Text> {
525 match &self.control {
526 ControlKind::Choice(choice) => {
527 choice
528 .recheck(cx, &self.label(), value, parent, stored, ex)
529 .await
530 }
531 _ => Vec::new(),
532 }
533 }
534
535 pub(crate) async fn keep_unseen(
538 &self,
539 cx: &Cx,
540 value: &FormValue,
541 stored: &FormValue,
542 ex: &mut dyn toasty::Executor,
543 ) -> Option<FormValue> {
544 match &self.control {
545 ControlKind::Choice(choice) => choice.keep_unseen(cx, value, stored, ex).await,
546 _ => None,
547 }
548 }
549}
550
551#[cfg(test)]
552mod test_support {
553 pub(super) use crate::test_support::DummyUser;
554}
555
556#[cfg(test)]
557mod tests;