Skip to main content

Forms

A view is the form; Form is the part that turns atoms into editable rows, each opening the modal that matches its type.

private readonly Surface _surface;
private readonly Form _form;

public SettingsView(
Surface surface,
SettingsStore settings,
ArlecchinoState state,
ArlecchinoOptions options)
{
_surface = surface;

_form = new Form(state, options)
{
Fields =
[
Field.Text(() => Loc(LocString.Profile), settings.Profile,
help: () => Loc(LocString.ProfileHelp)),
Field.Secret(() => Loc(LocString.Passphrase), settings.Passphrase),
Field.Choice(() => Loc(LocString.Theme), ["dark", "light"], settings.Theme),
Field.Slider(() => Loc(LocString.Volume), settings.Volume, 0, 100),
Field.Toggle(() => Loc(LocString.Fullscreen), settings.Fullscreen,
value => value ? Yes : No),
Field.Path(() => Loc(LocString.Folder), settings.Folder, ViewKind.Settings,
pickFolder: true),
Field.Action(() => Loc(LocString.Apply), Apply,
enabled: () => settings.IsComplete.Value),
],
};
}

public void Draw() => _form.Draw(_surface.Content);
public ViewRoute Handle(KeyPress key) => _form.Handle(key).Route;
public ViewRoute HandleMouse(MouseEvent mouse) => _form.HandleMouse(mouse).Route;

settings is the application's own store of atoms — Atom<string> Profile, Atom<decimal> Volume and so on — not anything the framework defines. state and options are ArlecchinoState and ArlecchinoOptions from the container: the form needs the first to open modals and the second for the keymap and the wording.

What it looks like

Rendered as label = value, labels padded to the longest, the help of the selected field on the line under it, actions as > Label:

Profile = empty
shown in the title bar
Passphrase = ••••••
Theme = dark
Volume = 60

> Apply

The help line exists only when the selected field actually has help, so a form of fields without any is a solid column of rows rather than a column with gaps in it.

The fields

FactoryOpens
Field.Text, Field.SecretText modal, masked for secrets
Field.Number, Field.SliderNumber and slider modals
Field.ToggleToggle modal
Field.Choice, Field.MultiChoiceChoice and multi-choice modals
Field.Date, Field.Time, Field.ColorSegment editors and the color picker
Field.Path, Field.PathFromThe file picker; returns its route so the view navigates
Field.ActionNothing — runs your delegate and returns a route

Labels and help are delegates, not strings, so a form follows the current language without being rebuilt.

Where a path field starts

Field.Path opens the picker at the path the field already holds. While the field is still empty there is nothing to open at and the picker lands on the drives, so Field.PathFrom says where it should start instead:

Field.PathFrom(() => "Save folder", settings.Folder, ViewKind.Settings, pickFolder: true,
start: () => state.PickerLastFolder);

A field that already has a value opens there and ignores start; a value that no longer exists on disk lands on the drives as before. start is a delegate rather than a string so that "wherever the user was last time" is answered when the picker opens rather than when the form is built — ArlecchinoState.PickerLastFolder, which the picker fills in as it closes, is exactly that answer.

Keys and clicks

Movement, Confirm and Erase come from the keymap; Erase resets a field to its empty value. Clicking a row selects it, clicking the selected row opens it, and the wheel moves the selection.

Form implements IArlecchinoFocusable, so a screen that is a form beside something else puts both in a ring and stops routing keys by hand.

Enabled and disabled

Field.Action takes an enabled predicate — usually a Computed<bool> — and draws itself muted while it is false:

Field.Action(() => "Apply", Apply, enabled: () => settings.IsComplete.Value)

Why atoms rather than properties

Because fields read and write atoms, two things come free:

  • an edit made through a modal is already undoable when the atom is TrackedAtom<T>;
  • a value changed from outside the form — by a command, a background load, another screen — redraws it without anyone telling it to.

A form over plain properties would need both wired by hand.