Skip to content

Layouts

Layout classes are Textual Widget subclasses (specifically VerticalScroll subclasses) that render a form's fields and handle submit/cancel interactions.

FormLayout

FormLayout

FormLayout(form, id=None, **kwargs)

Bases: VerticalScroll

Base class for form renderers.

Subclass and override compose() to create custom layouts. The base class handles button events and keyboard shortcuts.

BINDINGS class-attribute instance-attribute

BINDINGS = [
    Binding("enter", "submit", "Submit", show=False),
    Binding("escape", "cancel", "Cancel", show=False),
]

on_button_pressed

on_button_pressed(event)

action_submit

action_submit()

action_cancel

action_cancel()

FormLayout is the base class for all form layouts. It provides:

  • self.form — the BaseForm instance passed at construction
  • Enter keybinding → action_submit() → posts Form.Submitted (after successful clean())
  • Escape keybinding → action_cancel() → posts Form.Cancelled
  • on_button_pressed handler that routes button IDs "submit" and "cancel"

Subclass FormLayout directly only if you also need to handle widget events yourself. Otherwise subclass ControllerAwareLayout.

Bindings

Key Action Description
enter submit Validate and submit the form
escape cancel Cancel without submitting

Constructor

def __init__(self, form: BaseForm, id: str | None = None, **kwargs: Any) -> None
form
The BaseForm instance to render. All field data and validation logic lives on this object.
id
Optional Textual widget ID.

ControllerAwareLayout

ControllerAwareLayout

ControllerAwareLayout(form, id=None, **kwargs)

Bases: FormLayout

FormLayout mixin that routes widget events to FieldControllers.

When a form is composed with raw inner widgets (via BoundField.__call__ rather than BoundField.simple_layout), those widgets are not inside a FieldWidget that would handle events for them. This mixin catches the relevant Textual events and routes them to the appropriate FieldController based on the ._field_controller attribute stamped on each inner widget by BoundField.__call__.

Events that originate from inside a FieldWidget are ignored here (the FieldWidget handles them directly).

ControllerAwareLayout extends FormLayout by routing Textual widget events to FieldController objects. This is the class you should subclass when building custom layouts that use bf.__call__() to place raw widgets.

How event routing works

When you call bf() to get a raw widget, the BoundField stamps ._field_controller on the returned widget. ControllerAwareLayout listens for Input.Changed, Checkbox.Changed, Select.Changed, TextArea.Changed, and on_descendant_blur events. For each event, it looks up the controller and calls the appropriate method (handle_widget_input or validate_for).

Events originating from within a FieldWidget (the composite widget produced by bf.simple_layout()) are ignored here — the FieldWidget handles them internally.

When to use ControllerAwareLayout

Use ControllerAwareLayout (or its subclass DefaultFormLayout) as your layout base class whenever you use bf() to render raw widgets. If you use only bf.simple_layout(), the FieldWidget handles its own events and you could technically use bare FormLayout — but ControllerAwareLayout handles both cases gracefully, so it is always safe to use.


DefaultFormLayout

DefaultFormLayout

DefaultFormLayout(form, id=None, **kwargs)

Bases: ControllerAwareLayout

Renders all fields in declaration order with default styling.

Adds a title bar (if the form has a title) and Submit/Cancel buttons. Each field is rendered via BoundField.simple_layout().

compose

compose()

DefaultFormLayout is the layout returned by form.layout() when no custom layout_class is set. It renders:

  1. A bold title label (if form.title is non-empty)
  2. Each unrendered field via bf.simple_layout()
  3. A row of Submit (primary) and Cancel buttons

Default CSS

DefaultFormLayout {
    height: auto;
    max-height: 100%;
    padding: 1 2;
}
DefaultFormLayout .form-title {
    text-style: bold;
    margin-bottom: 1;
}
DefaultFormLayout #buttons {
    height: auto;
    margin-top: 1;
}

Override these styles in your app's CSS to resize or reposition the layout:

DefaultFormLayout {
    width: 60;
    max-height: 80%;
    border: solid $primary;
}

Unrendered guard

DefaultFormLayout.compose() skips any field that has already been rendered (bf.controller.is_consumed is True). This lets you render some fields manually in a custom section before calling form.layout(), though mixing the two approaches in the same form is unusual.