TUI::ScrollableField(T)
Inherits TUI::FormField < Reference < Object
Adapts any Scrollable with a #value : String getter (TextEdit, MarkdownEdit, or any future syntax-specific TextEdit subclass) into a FormField, so a form field can reuse a Scrollable's existing scrolling multi-line editor instead of InputField's single-line-only edit state. Reuses 100% of the wrapped widget's editing logic — this adds only the same small amount of glue Window already does to host a Scrollable (own a Scroller, fabricate a ScrollControl each call, blit a scratch buffer into the region FormField#render was given), just at FormField's smaller scale instead of Widget's.
T is deliberately unconstrained (Crystal has no clean "includes
Scrollable and has #value" bound) — a T missing #value or the
Scrollable methods this class calls simply fails with Crystal's
ordinary "undefined method" compile error at the call site, the same
tradeoff TUI::Form.define's own macro already accepts for a model
property typo.
Esc always commits (there's no discard-in-place gesture, matching
InputField's/BoolField's stated Esc convention — this is a free-text
editing kind, not a picker). Esc is intercepted by this wrapper
itself, never passed to @content: TextEdit#handle_key has no Esc
case today and must not gain one just for this, since standalone
full-screen TextEdit/MarkdownEdit (Window.full_screen) has no commit
concept and must keep behaving exactly as before. Enter is NOT
intercepted — it flows through unchanged and inserts a newline,
exactly like TextEdit's existing standalone behavior.
Constructors
Instance methods
Returns :commit or :cancel once the edit finishes; nil while it's still in progress (the key was consumed either way).
height bounds how many buffer rows this field may draw into,
same contract as every other FormField#render. Always reserves the
last column of width for a scrollbar track — same convention
Window follows for its own border's right column (see
Window#render, which reserves inner_width = width - 2*inset
unconditionally, scrollbar or not) — rather than sizing content to
the full width when nothing needs to scroll and to width - 1
when it does: since content_size depends on the width content
wraps against, a conditional width would make "does this need a
scrollbar" and "how wide is the content" mutually dependent.
Reserving unconditionally keeps content_size stable across renders
and #handle_key calls, both of which must agree on the same
ScrollControl visible/width. Buffer#scrollbar itself no-ops
(leaving the column blank) when #fraction is nil, so a field that
never overflows just shows a blank last column, not a misleading
always-on track.
Builds a scratch Buffer sized to the remaining content width since
Scrollable#render_content expects one sized to exactly its own
content area (it has no notion of drawing into a sub-rectangle of
a larger buffer), then blits every cell into buffer at the given
(y, x) offset — this is Window#blit_inner's body, copied at field
scale since Buffer has no sub-region blit primitive to call into
directly and Window#blit_inner is private/Window-shaped.
Applied to the scrollbar track/thumb drawn by #render.
T has no reseed method (no TextEdit#value=), so this replaces
@content wholesale with a freshly built instance — mirrors every
other FormField getting a fresh instance per edit session, just
done one level down (Form::Host's field.build.call only
constructs this wrapper once; #start is the only hook that ever
sees the wire value).
Fragment of a host's status-bar hint describing this field's own key bindings while it's being edited — owned here (rather than left for a host to re-derive per kind) so the bindings shown to the user can never drift from what #handle_key actually does.