Layout inference
Status: ✅ Implemented
Intent
Section titled “Intent”Let the developer declare only the information — which fields exist, which are required, how they are grouped — and let Mateu decide how to present it: fold the optional fields away, turn a long read-only page into tabs, render a small enum as radio buttons. The class stays a plain data declaration; the UX pattern is inferred.
Problem
Section titled “Problem”Choosing the right presentation for a form is repetitive design work: a long editable form should not bury its required fields under twenty optional ones; a heavy read-only record reads better with random access than as an endless scroll; a three-value enum deserves radio buttons, not a dropdown hiding its options. Developers either hand-tune every screen with layout annotations or ship the default flat form and let usability suffer.
Solution
Section titled “Solution”Annotate the class with @AutoLayout:
@UI("/customers/new")@AutoLayoutpublic class NewCustomerForm {
public enum Segment { RETAIL, BUSINESS, ENTERPRISE }
@NotNull String name; @NotEmpty String email; @NotNull String phone; @NotNull Segment segment = Segment.RETAIL;
@Stereotype(FieldStereotype.textarea) String deliveryNotes; @Stereotype(FieldStereotype.textarea) String billingNotes; @Stereotype(FieldStereotype.textarea) String internalNotes; @Stereotype(FieldStereotype.textarea) String marketingPreferences; String website; String vatNumber;}Nothing in this class says “accordion”, “tabs” or “radio” — yet the rendered form keeps the four required fields visible, collapses the six optional ones into a More options panel, and renders segment as radio buttons.
Inference is deterministic: every decision is based on the declared structure — number of sections, estimated visual weight of the fields, required vs optional — never on runtime data. The same class always renders the same way, and adding one field never flips the whole layout.
The weight unit
Section titled “The weight unit”Rules are measured in standard field-row units (1 = one regular input), so thresholds hold across very different field mixes: a textarea, rich-text, HTML, markdown or image field counts as 4, a grid or nested component as 6, radio/checkbox as 2, and anything else as 1.
Rule 1 — fold optionals into “More options”
Section titled “Rule 1 — fold optionals into “More options””On an editable form where you declared no grouping at all (no sections, no tabs) and the estimated weight exceeds one screen (> 16 units), the required fields stay visible and the optional fields are collapsed into a More options accordion panel. The rule needs at least one required and at least four optional fields to be worth applying, and it never fires when the form contains tabs, @Inline subforms or embedded components — their layout is not Mateu’s to rearrange.

Folding regroups, never drops: every field still reaches the browser and round-trips normally.
Rule 2 — read-only sections become adaptable tabs
Section titled “Rule 2 — read-only sections become adaptable tabs”A long read-only view (the render context is read-only, or the class is @ReadOnly) with many substantial sections — at least 5 sections totalling ≥ 30 weight units — reads better with random access than as a long vertical stack, so the sections are presented as tabs, one per section:
@UI("/customers/360")@AutoLayout@ReadOnlypublic class Customer360 {
@Section("Profile") @Stereotype(FieldStereotype.textarea) String summary = ...; @Stereotype(FieldStereotype.textarea) String address = ...;
@Section("Billing") @Stereotype(FieldStereotype.textarea) String billingTerms = ...; // ... Shipping, Activity, Notes ...}
The rule applies to read-only views only: on an editable form, hiding groups behind tabs could hide an invalid required field from the user, so editable sections always stay stacked.
Rule 3 — small enums as radio buttons
Section titled “Rule 3 — small enums as radio buttons”An enum field with up to 4 constants renders as radio buttons instead of a dropdown: every option is exposed at a glance for the cost of one extra row. Beyond 4 constants the dropdown stays, as it is denser.
To force radio buttons regardless of the enum size — with or without @AutoLayout — annotate the field with @UseRadioButtons (equivalent to @Stereotype(FieldStereotype.radio), but self-documenting).
Staying in control
Section titled “Staying in control”Inference only fills the gaps you left open — it never overrides a decision you made:
- Explicit annotations always win.
@Section,@Tab,@Zones,@FoldedLayout,@Toc,@Stereotype,@UseRadioButtons… are respected as declared; inference works around them or steps aside. - The tabs rule has explicit opt-outs: a
@Section(sticky = true)section or a class-level@Tocmeans you chose the pinned/scroll layout, so sections are never turned into tabs;@Zonesand@FoldedLayoutlayouts are likewise untouched. - Per-class opt-out:
@AutoLayout(false)disables inference for one class when it is enabled globally. - Global opt-in: set the
mateu.layout.inferencesystem property totrueto enable inference for every class without annotating each one — then use@AutoLayout(false)on the exceptions. - Composable:
@AutoLayouttargetsANNOTATION_TYPEtoo, so it can be bundled into your own semantic annotations (e.g. a company-wide@StandardForm).
Renderer adaptation: groupRelationship and adaptable
Section titled “Renderer adaptation: groupRelationship and adaptable”Inference does not just pick a widget — it tells the renderer what the grouping means, so each renderer (web, desktop, mobile) can adapt it to its medium. The tab layout on the wire carries two semantic hints:
groupRelationship— the semantic relation between the groups:alternative(the user looks at one group at a time — what tabs express),sequential(steps), orsimultaneous(side by side). Developer-declared tabs and inferred section-tabs both emitalternative.adaptable—truewhen the class is under@AutoLayout: the developer delegated the presentation, so a renderer may degrade the tabs to an accordion on a narrow viewport without losing the disclosure semantics. Tabs declared on a class without@AutoLayoutstill carry theirgroupRelationship, but are markedadaptable: false— the developer asked for tabs, so tabs they get.
- The decision table lives in
LayoutInference(core,componentmapper), and it is the reference implementation: the C# and Python backends port the exact same rules and thresholds, so the wire JSON is identical whichever server emits it. - The thresholds are constants of that class:
RADIO_MAX_OPTIONS = 4,FOLD_WEIGHT_THRESHOLD = 16,FOLD_MIN_OPTIONAL = 4,TABS_MIN_SECTIONS = 5,TABS_WEIGHT_THRESHOLD = 30. - Behaviour is asserted end to end in
LayoutInferenceSyncTest(core), including that classes without@AutoLayoutkeep the exact previous rendering.
When to use it
Section titled “When to use it”Put @AutoLayout on forms and record views whose layout you have no strong opinion about — quickly-built back-office screens, generated CRUD models, prototypes — and keep hand-tuned layouts (@Zones, @Toc, sticky sections, explicit tabs) for the screens you designed deliberately. Since explicit annotations always win, it is safe to enable globally with mateu.layout.inference and refine individual screens later.