Skip to main content

gpui_kit/structured/
schema_form.rs

1//! A form generated from a description of the arguments a call takes.
2//!
3//! # Why there is a schema type here
4//!
5//! For the same reason [`JsonValue`](super::JsonValue) exists: this crate
6//! takes no serialization dependency, so it cannot read a schema document. A
7//! host converts whatever schema dialect it already parses into [`Schema`],
8//! which describes only what a form has to draw — a name, a label, whether the
9//! value is required, and which control the value is edited with.
10//!
11//! # A field the form cannot draw says so
12//!
13//! This is the rule the whole component is built around. A host converting a
14//! schema it does not fully understand puts
15//! [`SchemaKind::Unrenderable`] in place of the field, with the reason in its
16//! own words; the form also refuses a few shapes on its own, such as a choice
17//! among no choices. Either way the field keeps its place, keeps its label,
18//! and states that it cannot be filled in here — and
19//! [`SchemaForm::values`] still reports it, as
20//! [`FieldValue::Unrenderable`], so a caller cannot collect the answers and
21//! not notice one is missing.
22//!
23//! A form that quietly dropped a required argument it did not understand would
24//! produce an invalid call, and the reader would be told they got it wrong.
25//!
26//! # Whose error is on screen
27//!
28//! Two sources, kept apart. [`SchemaForm::validate`] marks required fields
29//! nobody filled in, which is all this component can judge on its own;
30//! [`SchemaForm::set_error`] shows an error the host returned, in the host's
31//! own words. A host error outranks a derived one on the same field, because
32//! the host knows something the form does not.
33
34use std::collections::BTreeMap;
35
36use gpui::{
37    AnyElement, App, AppContext, Context, Entity, EventEmitter, InteractiveElement, IntoElement,
38    ParentElement, Render, SharedString, Styled, Subscription, Window, div, prelude::FluentBuilder,
39    px,
40};
41use gpui_kit_semantics::{NodeSpec, Role, Semantic};
42use gpui_kit_theme::{ActiveTheme, ControlSize, Space, TextTone, TypeScale};
43
44use crate::controls::combobox::{Combobox, ComboboxEvent};
45use crate::controls::form_field::FormField;
46use crate::controls::input::{TextInput, TextInputEvent};
47use crate::controls::number_input::{NumberInput, NumberInputEvent};
48use crate::controls::select::{Select, SelectEvent, SelectOption};
49use crate::controls::tag_input::{TagInput, TagInputEvent};
50use crate::controls::toggle::Switch;
51use crate::display::badge::Tone;
52use crate::display::status::Callout;
53use crate::foundation::{Disableable, Ident, Sizable, StyledExt, text};
54use crate::strings::{ActiveStrings, StringKey};
55
56/// One option a closed or open choice offers.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct SchemaChoice {
59    id: SharedString,
60    label: SharedString,
61    description: Option<SharedString>,
62}
63
64impl SchemaChoice {
65    pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
66        Self {
67            id: id.into(),
68            label: label.into(),
69            description: None,
70        }
71    }
72
73    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
74        self.description = Some(description.into());
75        self
76    }
77
78    fn option(&self) -> SelectOption {
79        let option = SelectOption::new(self.id.clone(), self.label.clone());
80        match &self.description {
81            Some(description) => option.description(description.clone()),
82            None => option,
83        }
84    }
85}
86
87/// What a number may be, as far as the schema said.
88///
89/// Every bound is optional because a schema is allowed to state none of them,
90/// and a form that invented a range would refuse values the host accepts.
91#[derive(Debug, Clone, Copy, Default, PartialEq)]
92pub struct NumberBounds {
93    pub min: Option<f64>,
94    pub max: Option<f64>,
95    pub step: Option<f64>,
96}
97
98impl NumberBounds {
99    pub fn new() -> Self {
100        Self::default()
101    }
102
103    pub fn min(mut self, min: f64) -> Self {
104        self.min = Some(min);
105        self
106    }
107
108    pub fn max(mut self, max: f64) -> Self {
109        self.max = Some(max);
110        self
111    }
112
113    pub fn step(mut self, step: f64) -> Self {
114        self.step = Some(step);
115        self
116    }
117}
118
119/// What a value is, and therefore which control edits it.
120#[derive(Debug, Clone, PartialEq)]
121pub enum SchemaKind {
122    Text {
123        placeholder: Option<SharedString>,
124        /// Drawn as dots, and kept out of every snapshot.
125        secret: bool,
126    },
127    Number(NumberBounds),
128    /// A number with no fractional part, which is a different control setting
129    /// rather than a different control.
130    Integer(NumberBounds),
131    Boolean,
132    /// One of these, and nothing else.
133    Enum(Vec<SchemaChoice>),
134    /// One of these, or something the reader types.
135    OpenEnum(Vec<SchemaChoice>),
136    /// A list of short values.
137    TextList {
138        max: Option<usize>,
139    },
140    /// Fields under a name of their own.
141    Object(Vec<SchemaField>),
142    /// The host could not express this field, and said so rather than
143    /// dropping it. The text is the host's reason and is shown verbatim.
144    Unrenderable(SharedString),
145}
146
147/// One argument.
148#[derive(Debug, Clone, PartialEq)]
149pub struct SchemaField {
150    name: SharedString,
151    label: Option<SharedString>,
152    description: Option<SharedString>,
153    required: bool,
154    kind: SchemaKind,
155}
156
157impl SchemaField {
158    pub fn new(name: impl Into<SharedString>, kind: SchemaKind) -> Self {
159        Self {
160            name: name.into(),
161            label: None,
162            description: None,
163            required: false,
164            kind,
165        }
166    }
167
168    /// What the reader sees. Without one the field's own name is shown, which
169    /// is what a schema that named nothing else leaves to work with.
170    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
171        self.label = Some(label.into());
172        self
173    }
174
175    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
176        self.description = Some(description.into());
177        self
178    }
179
180    pub fn required(mut self, required: bool) -> Self {
181        self.required = required;
182        self
183    }
184
185    pub fn name(&self) -> &SharedString {
186        &self.name
187    }
188
189    pub fn kind(&self) -> &SchemaKind {
190        &self.kind
191    }
192
193    pub fn is_required(&self) -> bool {
194        self.required
195    }
196
197    fn shown_label(&self) -> SharedString {
198        self.label.clone().unwrap_or_else(|| self.name.clone())
199    }
200}
201
202/// The arguments a call takes, in the order they should be filled in.
203#[derive(Debug, Clone, Default, PartialEq)]
204pub struct Schema {
205    fields: Vec<SchemaField>,
206}
207
208impl Schema {
209    pub fn new() -> Self {
210        Self::default()
211    }
212
213    pub fn field(mut self, field: SchemaField) -> Self {
214        self.fields.push(field);
215        self
216    }
217
218    pub fn fields(mut self, fields: impl IntoIterator<Item = SchemaField>) -> Self {
219        self.fields.extend(fields);
220        self
221    }
222
223    pub fn is_empty(&self) -> bool {
224        self.fields.is_empty()
225    }
226}
227
228/// A field that has to be filled in somewhere other than this form.
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct UnrenderableField {
231    /// The path the field would have had in the answer, slash-joined through
232    /// any objects above it.
233    pub path: SharedString,
234    pub label: SharedString,
235    pub required: bool,
236    /// Why, in the words of whoever refused: the host's, or this library's
237    /// when the form itself is the one refusing.
238    pub reason: SharedString,
239}
240
241/// What one field currently holds.
242#[derive(Debug, Clone, PartialEq)]
243pub enum FieldValue {
244    Text(SharedString),
245    Number(f64),
246    Boolean(bool),
247    Choice(SharedString),
248    List(Vec<SharedString>),
249    /// Nothing was entered. Distinct from an empty string, which somebody
250    /// typed on purpose.
251    Absent,
252    /// The form could not draw this field, so it holds nothing and never
253    /// could. Reported rather than omitted.
254    Unrenderable,
255}
256
257/// What the form reports.
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub enum SchemaFormEvent {
260    /// The field at this path changed. The value is read from
261    /// [`SchemaForm::values`], because a form has more than one of them.
262    Changed(SharedString),
263    /// The primary key was pressed in a field. The form submits nothing.
264    Submitted,
265}
266
267impl EventEmitter<SchemaFormEvent> for SchemaForm {}
268
269/// The control that edits one field, and where the value lives.
270enum Control {
271    Text(Entity<TextInput>),
272    Number(Entity<NumberInput>),
273    /// A switch is a builder rather than a view, so the draft is here.
274    Boolean(bool),
275    Choice(Entity<Select>),
276    OpenChoice(Entity<Combobox>),
277    List(Entity<TagInput>),
278    /// A heading over the fields beneath it. It holds nothing.
279    Group,
280    Unrenderable(SharedString),
281}
282
283/// One field, flattened out of however many objects it sat inside.
284struct Field {
285    path: SharedString,
286    label: SharedString,
287    description: Option<SharedString>,
288    required: bool,
289    level: u32,
290    control: Control,
291}
292
293/// A form built from a schema.
294///
295/// It is a view rather than a builder because the fields it composes —
296/// [`TextInput`], [`NumberInput`], [`Select`], [`Combobox`], [`TagInput`] —
297/// each own a caret, an open menu, or a selection that has to survive a frame.
298pub struct SchemaForm {
299    ident: Ident,
300    fields: Vec<Field>,
301    /// What the host said is wrong, by path.
302    host_errors: BTreeMap<SharedString, SharedString>,
303    /// What the form worked out is missing, by path. Cleared by every edit to
304    /// that field, so an answered complaint does not stay on screen.
305    derived_errors: BTreeMap<SharedString, SharedString>,
306    unrenderable: Vec<UnrenderableField>,
307    size: ControlSize,
308    disabled: bool,
309    _subscriptions: Vec<Subscription>,
310}
311
312impl std::fmt::Debug for SchemaForm {
313    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        formatter
315            .debug_struct("SchemaForm")
316            .field("ident", &self.ident)
317            .field("fields", &self.fields.len())
318            .field("unrenderable", &self.unrenderable.len())
319            .field("disabled", &self.disabled)
320            .finish()
321    }
322}
323
324impl SchemaForm {
325    pub fn new(
326        ident: impl Into<Ident>,
327        schema: Schema,
328        window: &mut Window,
329        cx: &mut Context<Self>,
330    ) -> Self {
331        let ident = ident.into();
332        let mut form = Self {
333            ident,
334            fields: Vec::new(),
335            host_errors: BTreeMap::new(),
336            derived_errors: BTreeMap::new(),
337            unrenderable: Vec::new(),
338            size: ControlSize::Md,
339            disabled: false,
340            _subscriptions: Vec::new(),
341        };
342        let ident = form.ident.clone();
343        form.build(&schema.fields, "", 1, &ident, window, cx);
344        form
345    }
346
347    fn build(
348        &mut self,
349        fields: &[SchemaField],
350        prefix: &str,
351        level: u32,
352        ident: &Ident,
353        window: &mut Window,
354        cx: &mut Context<Self>,
355    ) {
356        for field in fields {
357            let path = if prefix.is_empty() {
358                field.name.clone()
359            } else {
360                SharedString::from(format!("{prefix}/{}", field.name))
361            };
362            let field_ident = ident.child(path.as_ref());
363            let label = field.shown_label();
364            let control = self.control_for(field, &field_ident, window, cx);
365
366            if let Control::Unrenderable(reason) = &control {
367                self.unrenderable.push(UnrenderableField {
368                    path: path.clone(),
369                    label: label.clone(),
370                    required: field.required,
371                    reason: reason.clone(),
372                });
373            }
374
375            let nested = match &field.kind {
376                SchemaKind::Object(children) => Some(children.clone()),
377                _ => None,
378            };
379
380            self.fields.push(Field {
381                path: path.clone(),
382                label,
383                description: field.description.clone(),
384                required: field.required,
385                level,
386                control,
387            });
388
389            if let Some(children) = nested {
390                self.build(&children, path.as_ref(), level + 1, ident, window, cx);
391            }
392        }
393    }
394
395    fn control_for(
396        &mut self,
397        field: &SchemaField,
398        ident: &Ident,
399        window: &mut Window,
400        cx: &mut Context<Self>,
401    ) -> Control {
402        let path = SharedString::from(ident.as_str().to_string());
403        let control_ident = ident.child("control");
404        match &field.kind {
405            SchemaKind::Unrenderable(reason) => Control::Unrenderable(reason.clone()),
406            // A choice among nothing is not a control the form can draw, and
407            // drawing an empty menu would look like a list that had not
408            // loaded. The form refuses this one itself.
409            SchemaKind::Enum(choices) | SchemaKind::OpenEnum(choices) if choices.is_empty() => {
410                Control::Unrenderable(cx.strings().text(StringKey::SchemaNoChoices))
411            }
412            SchemaKind::Object(_) => Control::Group,
413            SchemaKind::Text {
414                placeholder,
415                secret,
416            } => {
417                let secret = *secret;
418                let placeholder = placeholder.clone();
419                let input = cx.new(|cx| {
420                    let mut input = TextInput::new(control_ident, window, cx)
421                        .secret(secret)
422                        .required(field.required);
423                    if let Some(placeholder) = placeholder {
424                        input = input.placeholder(placeholder);
425                    }
426                    input
427                });
428                self.watch_text(&path, &input, cx);
429                Control::Text(input)
430            }
431            SchemaKind::Number(bounds) | SchemaKind::Integer(bounds) => {
432                let integer = matches!(field.kind, SchemaKind::Integer(_));
433                let bounds = *bounds;
434                let required = field.required;
435                let label = field.shown_label();
436                let number = cx.new(|cx| {
437                    let mut number = NumberInput::new(control_ident, window, cx)
438                        .required(required)
439                        .name(label);
440                    if let Some(min) = bounds.min {
441                        number = number.min(min);
442                    }
443                    if let Some(max) = bounds.max {
444                        number = number.max(max);
445                    }
446                    number = number.step(bounds.step.unwrap_or(1.0));
447                    if integer {
448                        number = number.precision(0);
449                    }
450                    number
451                });
452                self.watch_number(&path, &number, cx);
453                Control::Number(number)
454            }
455            SchemaKind::Boolean => Control::Boolean(false),
456            SchemaKind::Enum(choices) => {
457                let options: Vec<SelectOption> = choices.iter().map(SchemaChoice::option).collect();
458                let name = field.shown_label();
459                let select = cx.new(|cx| {
460                    Select::new(control_ident, window, cx)
461                        .name(name)
462                        .options(options)
463                });
464                self.watch_select(&path, &select, cx);
465                Control::Choice(select)
466            }
467            SchemaKind::OpenEnum(choices) => {
468                let options: Vec<SelectOption> = choices.iter().map(SchemaChoice::option).collect();
469                let name = field.shown_label();
470                let combobox = cx.new(|cx| {
471                    Combobox::new(control_ident, window, cx)
472                        .name(name)
473                        .options(options)
474                        .allow_custom(true)
475                });
476                self.watch_combobox(&path, &combobox, cx);
477                Control::OpenChoice(combobox)
478            }
479            SchemaKind::TextList { max } => {
480                let max = *max;
481                let tags = cx.new(|cx| {
482                    let field = TagInput::new(control_ident, window, cx);
483                    match max {
484                        Some(max) => field.max(max),
485                        None => field,
486                    }
487                });
488                self.watch_tags(&path, &tags, cx);
489                Control::List(tags)
490            }
491        }
492    }
493
494    fn watch_text(
495        &mut self,
496        path: &SharedString,
497        input: &Entity<TextInput>,
498        cx: &mut Context<Self>,
499    ) {
500        let path = path.clone();
501        self._subscriptions.push(cx.subscribe(
502            input,
503            move |form, _, event: &TextInputEvent, cx| match event {
504                TextInputEvent::Change(_) => form.changed(path.clone(), cx),
505                TextInputEvent::Submit => cx.emit(SchemaFormEvent::Submitted),
506                _ => {}
507            },
508        ));
509    }
510
511    fn watch_number(
512        &mut self,
513        path: &SharedString,
514        number: &Entity<NumberInput>,
515        cx: &mut Context<Self>,
516    ) {
517        let path = path.clone();
518        self._subscriptions.push(cx.subscribe(
519            number,
520            move |form, number, event: &NumberInputEvent, cx| match event {
521                NumberInputEvent::Changed(value) => {
522                    let value = *value;
523                    number.update(cx, |number, cx| number.set_value(value, cx));
524                    form.changed(path.clone(), cx);
525                }
526                NumberInputEvent::Unparsable(_) => form.changed(path.clone(), cx),
527                NumberInputEvent::Submit => cx.emit(SchemaFormEvent::Submitted),
528            },
529        ));
530    }
531
532    fn watch_select(
533        &mut self,
534        path: &SharedString,
535        select: &Entity<Select>,
536        cx: &mut Context<Self>,
537    ) {
538        let path = path.clone();
539        self._subscriptions.push(cx.subscribe(
540            select,
541            move |form, select, event: &SelectEvent, cx| {
542                if let SelectEvent::Selected(id) = event {
543                    let id = id.clone();
544                    select.update(cx, |select, cx| select.set_selected(Some(id), cx));
545                    form.changed(path.clone(), cx);
546                }
547            },
548        ));
549    }
550
551    fn watch_combobox(
552        &mut self,
553        path: &SharedString,
554        combobox: &Entity<Combobox>,
555        cx: &mut Context<Self>,
556    ) {
557        let path = path.clone();
558        self._subscriptions.push(cx.subscribe(
559            combobox,
560            move |form, combobox, event: &ComboboxEvent, cx| match event {
561                ComboboxEvent::Selected(id) => {
562                    let id = id.clone();
563                    combobox.update(cx, |combobox, cx| combobox.set_selected(Some(id), cx));
564                    form.changed(path.clone(), cx);
565                }
566                ComboboxEvent::Custom(_) => form.changed(path.clone(), cx),
567                _ => {}
568            },
569        ));
570    }
571
572    fn watch_tags(&mut self, path: &SharedString, tags: &Entity<TagInput>, cx: &mut Context<Self>) {
573        let path = path.clone();
574        self._subscriptions.push(cx.subscribe(
575            tags,
576            move |form, tags, event: &TagInputEvent, cx| {
577                let next = match event {
578                    TagInputEvent::Added(value) => {
579                        let mut next = tags.read(cx).current().to_vec();
580                        next.push(value.clone());
581                        Some(next)
582                    }
583                    TagInputEvent::Removed(value) => Some(
584                        tags.read(cx)
585                            .current()
586                            .iter()
587                            .filter(|tag| *tag != value)
588                            .cloned()
589                            .collect(),
590                    ),
591                    // A duplicate and a full field are refusals the tag field
592                    // already shows where the typist is looking. Applying one
593                    // would be applying a change nobody accepted.
594                    _ => None,
595                };
596                if let Some(next) = next {
597                    tags.update(cx, |tags, cx| tags.set_tags(next, cx));
598                    form.changed(path.clone(), cx);
599                }
600            },
601        ));
602    }
603
604    fn changed(&mut self, path: SharedString, cx: &mut Context<Self>) {
605        // The form's own complaint was about this field being empty, and it is
606        // not empty any more; the host's stands until the host withdraws it.
607        self.derived_errors.remove(&path);
608        cx.emit(SchemaFormEvent::Changed(path));
609        cx.notify();
610    }
611
612    /// Shows an error the host returned, next to the field it is about.
613    pub fn set_error(
614        &mut self,
615        path: impl Into<SharedString>,
616        message: impl Into<SharedString>,
617        cx: &mut Context<Self>,
618    ) {
619        self.host_errors.insert(path.into(), message.into());
620        cx.notify();
621    }
622
623    /// Withdraws every error the host reported. What the form worked out for
624    /// itself is untouched, because the host did not put it there.
625    pub fn clear_host_errors(&mut self, cx: &mut Context<Self>) {
626        self.host_errors.clear();
627        cx.notify();
628    }
629
630    /// Marks every required field nobody filled in, and reports whether the
631    /// form is answerable at all.
632    ///
633    /// A form holding a field it cannot draw is never answerable, whatever is
634    /// typed into the rest of it. Neither is one holding a number outside the
635    /// range the schema gave it: the control already draws that as wrong, and
636    /// a form that called it answerable anyway would be contradicting what is
637    /// on screen.
638    pub fn validate(&mut self, cx: &mut Context<Self>) -> bool {
639        self.derived_errors.clear();
640        let missing: Vec<SharedString> = self
641            .fields
642            .iter()
643            .filter(|field| {
644                field.required && matches!(self.value_of(field, cx), FieldValue::Absent)
645            })
646            .map(|field| field.path.clone())
647            .collect();
648        let message = cx.strings().text(StringKey::SchemaRequiredMissing);
649        for path in missing {
650            self.derived_errors.insert(path, message.clone());
651        }
652        let rejected: Vec<(SharedString, SharedString)> = self
653            .fields
654            .iter()
655            .filter_map(|field| match &field.control {
656                Control::Number(number) => number
657                    .read(cx)
658                    .invalid_reason(cx)
659                    .map(|reason| (field.path.clone(), reason)),
660                _ => None,
661            })
662            .collect();
663        for (path, reason) in rejected {
664            self.derived_errors.entry(path).or_insert(reason);
665        }
666        cx.notify();
667        self.derived_errors.is_empty() && !self.unrenderable.iter().any(|field| field.required)
668    }
669
670    /// Every field and what it holds, including the ones the form could not
671    /// draw. A caller that builds a call from this cannot lose a field without
672    /// seeing it.
673    pub fn values(&self, cx: &App) -> Vec<(SharedString, FieldValue)> {
674        self.fields
675            .iter()
676            .filter(|field| !matches!(field.control, Control::Group))
677            .map(|field| (field.path.clone(), self.value_of(field, cx)))
678            .collect()
679    }
680
681    /// The fields that have to be filled in somewhere else.
682    pub fn unrenderable(&self) -> &[UnrenderableField] {
683        &self.unrenderable
684    }
685
686    /// Whether any field the form could not draw is one the call requires.
687    pub fn has_unrenderable_required(&self) -> bool {
688        self.unrenderable.iter().any(|field| field.required)
689    }
690
691    pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
692        self.disabled = disabled;
693        for field in &self.fields {
694            match &field.control {
695                Control::Text(input) => {
696                    input.update(cx, |input, cx| input.set_disabled(disabled, cx))
697                }
698                Control::Number(number) => {
699                    number.update(cx, |number, cx| number.set_disabled(disabled, cx))
700                }
701                Control::Choice(select) => {
702                    select.update(cx, |select, cx| select.set_disabled(disabled, cx))
703                }
704                Control::OpenChoice(combobox) => {
705                    combobox.update(cx, |combobox, cx| combobox.set_disabled(disabled, cx))
706                }
707                Control::List(tags) => tags.update(cx, |tags, cx| tags.set_disabled(disabled, cx)),
708                Control::Boolean(_) | Control::Group | Control::Unrenderable(_) => {}
709            }
710        }
711        cx.notify();
712    }
713
714    fn value_of(&self, field: &Field, cx: &App) -> FieldValue {
715        match &field.control {
716            Control::Text(input) => match input.read(cx).value() {
717                text if text.is_empty() => FieldValue::Absent,
718                text => FieldValue::Text(text.clone()),
719            },
720            Control::Number(number) => match number.read(cx).shown(cx) {
721                Some(value) => FieldValue::Number(value),
722                None => FieldValue::Absent,
723            },
724            Control::Boolean(on) => FieldValue::Boolean(*on),
725            Control::Choice(select) => match select.read(cx).selected_id() {
726                Some(id) => FieldValue::Choice(id.clone()),
727                None => FieldValue::Absent,
728            },
729            Control::OpenChoice(combobox) => {
730                let combobox = combobox.read(cx);
731                match combobox.selected_id() {
732                    Some(id) => FieldValue::Choice(id.clone()),
733                    None => match combobox.query_text(cx) {
734                        query if query.is_empty() => FieldValue::Absent,
735                        query => FieldValue::Choice(query),
736                    },
737                }
738            }
739            Control::List(tags) => match tags.read(cx).current() {
740                [] => FieldValue::Absent,
741                tags => FieldValue::List(tags.to_vec()),
742            },
743            Control::Unrenderable(_) => FieldValue::Unrenderable,
744            Control::Group => FieldValue::Absent,
745        }
746    }
747
748    /// The error shown on a field. The host's outranks the form's, because the
749    /// host knows something the form does not.
750    ///
751    /// A control that draws itself as invalid is asked why last of all, so the
752    /// red border a number gets for leaving its range always arrives with the
753    /// range beside it rather than on its own.
754    fn error_for(&self, field: &Field, cx: &App) -> Option<SharedString> {
755        if let Some(error) = self
756            .host_errors
757            .get(&field.path)
758            .or_else(|| self.derived_errors.get(&field.path))
759        {
760            return Some(error.clone());
761        }
762        match &field.control {
763            Control::Number(number) => number.read(cx).invalid_reason(cx),
764            _ => None,
765        }
766    }
767}
768
769impl Sizable for SchemaForm {
770    fn control_size(mut self, size: ControlSize) -> Self {
771        self.size = size;
772        self
773    }
774}
775
776impl Disableable for SchemaForm {
777    fn disabled(mut self, disabled: bool) -> Self {
778        self.disabled = disabled;
779        self
780    }
781}
782
783impl Render for SchemaForm {
784    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
785        let theme = cx.theme().clone();
786        let count = self.fields.len();
787        let strings = cx.strings();
788        let unrenderable_required = self.has_unrenderable_required();
789        let summary = (!self.unrenderable.is_empty()).then(|| {
790            if self.unrenderable.len() == 1 {
791                strings.text(StringKey::SchemaUnrenderableOne)
792            } else {
793                strings.format(
794                    StringKey::SchemaUnrenderableMany,
795                    &[&self.unrenderable.len().to_string()],
796                )
797            }
798        });
799
800        let rows: Vec<AnyElement> = self
801            .fields
802            .iter()
803            .map(|field| self.field_element(field, cx))
804            .collect();
805
806        div()
807            .id(self.ident.element_id())
808            .column()
809            .w_full()
810            .gap_token(&theme, Space::Md)
811            .children(rows)
812            .when_some(summary, |element, summary| {
813                let ident = self.ident.child("unrenderable");
814                element.child(
815                    div()
816                        .child(
817                            Callout::new(
818                                summary.clone(),
819                                if unrenderable_required {
820                                    Tone::Danger
821                                } else {
822                                    Tone::Warning
823                                },
824                            )
825                            .id(ident.child("callout")),
826                        )
827                        .semantic_in(
828                            cx,
829                            NodeSpec::new(ident.semantic_id(), Role::Status)
830                                .parent(self.ident.semantic_id())
831                                .text(summary)
832                                .invalid(true)
833                                .required(unrenderable_required)
834                                .value(if unrenderable_required {
835                                    "unrenderable, required"
836                                } else {
837                                    "unrenderable"
838                                }),
839                        ),
840                )
841            })
842            .semantic_in(
843                cx,
844                NodeSpec::new(self.ident.semantic_id(), Role::Form).value(count.to_string()),
845            )
846    }
847}
848
849impl SchemaForm {
850    fn field_element(&self, field: &Field, cx: &mut Context<Self>) -> AnyElement {
851        let theme = cx.theme().clone();
852        let ident = self.ident.child(field.path.as_ref());
853        let indent = px(field.level.saturating_sub(1) as f32 * theme.space(Space::Lg));
854
855        if let Control::Group = field.control {
856            return div()
857                .ml(indent)
858                .column()
859                .gap_token(&theme, Space::Xs)
860                .child(text(&theme, TypeScale::Subtitle, field.label.clone()))
861                .when_some(field.description.clone(), |element, description| {
862                    element.child(
863                        text(&theme, TypeScale::Body, description)
864                            .text_tone(&theme, TextTone::Muted),
865                    )
866                })
867                .semantic_in(
868                    cx,
869                    NodeSpec::new(ident.semantic_id(), Role::Group)
870                        .parent(self.ident.semantic_id())
871                        .text(field.label.clone())
872                        .required(field.required)
873                        .level(field.level),
874                )
875                .into_any_element();
876        }
877
878        let control_ident = ident.child("control");
879        let error = self.error_for(field, cx);
880        let mut form_field = FormField::new(ident.clone(), field.label.clone())
881            .control(control_ident.semantic_id())
882            .required(field.required);
883        if let Some(description) = field.description.clone() {
884            form_field = form_field.description(description);
885        }
886        if let Some(error) = error.clone() {
887            form_field = form_field.error(error);
888        }
889
890        let body: AnyElement = match &field.control {
891            Control::Text(input) => input.clone().into_any_element(),
892            Control::Number(number) => number.clone().into_any_element(),
893            Control::Choice(select) => select.clone().into_any_element(),
894            Control::OpenChoice(combobox) => combobox.clone().into_any_element(),
895            Control::List(tags) => tags.clone().into_any_element(),
896            Control::Boolean(on) => {
897                let on = *on;
898                let path = field.path.clone();
899                // A switch takes effect at once, so the draft it moves lives
900                // here rather than in a control that would report and forget.
901                let form = cx.entity().downgrade();
902                Switch::new(control_ident.clone())
903                    // The visible label belongs to the field around it, so the
904                    // switch carries the same name rather than going unnamed.
905                    .named(field.label.clone())
906                    .on(on)
907                    .disabled(self.disabled)
908                    .when(!self.disabled, |switch| {
909                        switch.on_change(move |next, _, cx| {
910                            let path = path.clone();
911                            form.update(cx, |form, cx| {
912                                if let Some(field) =
913                                    form.fields.iter_mut().find(|field| field.path == path)
914                                {
915                                    field.control = Control::Boolean(next);
916                                }
917                                form.changed(path, cx);
918                            })
919                            .ok();
920                        })
921                    })
922                    .into_any_element()
923            }
924            // The field keeps its place, its label, and its required mark. It
925            // is the control that is missing, and the reason stands where the
926            // control would have been.
927            Control::Unrenderable(reason) => {
928                let refusal = ident.child("unrenderable");
929                div()
930                    .child(
931                        Callout::new(
932                            reason.clone(),
933                            if field.required {
934                                Tone::Danger
935                            } else {
936                                Tone::Warning
937                            },
938                        )
939                        .id(refusal.child("callout")),
940                    )
941                    .semantic_in(
942                        cx,
943                        NodeSpec::new(refusal.semantic_id(), Role::Status)
944                            .parent(ident.semantic_id())
945                            .text(reason.clone())
946                            .invalid(true)
947                            .required(field.required)
948                            .value(if field.required {
949                                "unrenderable, required"
950                            } else {
951                                "unrenderable"
952                            }),
953                    )
954                    .into_any_element()
955            }
956            Control::Group => div().into_any_element(),
957        };
958
959        div()
960            .ml(indent)
961            .child(form_field.child(body))
962            .into_any_element()
963    }
964}
965
966#[cfg(test)]
967mod tests {
968    use super::*;
969
970    #[test]
971    fn a_field_shows_its_name_when_the_schema_named_nothing_else() {
972        let field = SchemaField::new("max_tokens", SchemaKind::Integer(NumberBounds::new()));
973        assert_eq!(field.shown_label().as_ref(), "max_tokens");
974        assert_eq!(
975            field.label("Maximum tokens").shown_label().as_ref(),
976            "Maximum tokens"
977        );
978    }
979
980    #[test]
981    fn bounds_are_absent_until_a_schema_states_them() {
982        let bounds = NumberBounds::new();
983        assert_eq!(bounds.min, None);
984        assert_eq!(bounds.max, None);
985        let bounded = NumberBounds::new().min(1.0).max(4.0).step(0.5);
986        assert_eq!(bounded.min, Some(1.0));
987        assert_eq!(bounded.max, Some(4.0));
988        assert_eq!(bounded.step, Some(0.5));
989    }
990}