Repository navigation
Modern Tui Example Form
Paul Squires edited this page Nov 11, 2026
·
8 revisions
Home › Terminal TUI Library › Examples › form.tiko
Focus traversal and a confirm dialog. A complete, working program from Terminal TUI Library.
Text inputs, a checkbox, a radio group and a select, wired into a TuiFocusRing with Tab and Shift+Tab, and a TuiMessageBox on submit.
tiko form.tiko ' windows-x86_64 on Windows, linux-x86_64 on Linux, macos-arm64 on macOSRun it from a REAL TERMINAL. Redirected output is rejected with an honest error rather than a dashboard drawn into a pipe. tiko builds it with no diagnostics for windows-x86_64 and win64, and compiles it for linux-x86_64 with -c (built, not run: it is interactive).
- EVERY FOCUSABLE WIDGET IS A FIELD OF THE APP, not a local in View. The focus ring holds
TuiWidget ptrand a local dies at END SUB. - Widget STATE structs are fields too, and the widgets point at them. The widget never allocates or frees them.
-
normalandfocused_are set to visibly different styles. When they were equal the focus moved correctly and was invisible, which looks identical to broken. - The message box is drawn last, over everything, and consumes every event while it is up.
'' examples/form.tiko -- focus, input widgets, a modal, and CAPTURING LAMBDAS.
''
'' This is the example that exercises the library's closure-carrying design:
'' a widget that carries a closure.
''
'' Build (windows-x86_64, linux-x86_64 or macos-arm64):
'' tiko form.tiko
#import "std/numeric"
#import "std/outcome"
#import "std/tui"
using tui, numeric, outcome
'' ---------------------------------------------------------------------------
'' WHERE A CLOSURE-CARRYING WIDGET CAN BE BUILT
''
'' A capturing lambda's type is ANONYMOUS, and it cannot be given module-level
'' storage: `var shared h = sub[...]` is `error 11: Expected constant`, and
'' `dim shared h = ...` needs a type name there is no way to write. So the
'' closure lives where `var` can hold it -- module level or a local scope --
'' and the widget is BUILT THERE and handed to the app as a TuiWidget ptr.
''
'' The generic parameter is erased at the container boundary, so the app only
'' ever sees TuiWidget ptr and never needs to name the closure type at all.
'' ---------------------------------------------------------------------------
dim shared as ustring gStatus
dim shared as boolean gConfirm
'' The app's owned widget list. A NAMED type, so this one can be shared.
dim shared as TuiWidgetPtrArray gFields
sub BuildButtons( byref theme as TuiTheme )
'' the closures, and the widgets that carry them, in one scope
var onReset = sub[ byref gStatus ]( byval id as int32 )
gStatus = "cleared"
end sub
var onSubmit = sub[ byref gStatus, byref gConfirm ]( byval id as int32 )
gConfirm = TRUE
gStatus = "review and confirm"
end sub
'' The alias is how the anonymous type gets a name. Two DIFFERENT closures
'' give two DIFFERENT types -- and both are still TuiWidgets.
type ResetButton as TuiButtonOf( of typeof( onReset ) )
type SubmitButton as TuiButtonOf( of typeof( onSubmit ) )
dim as ResetButton ptr rb = new ResetButton
rb->label = " Reset "
rb->id = 1
rb->handler = onReset
rb->normal = theme.normal
rb->active = theme.selected
dim as SubmitButton ptr sb = new SubmitButton
sb->label = " Submit "
sb->id = 2
sb->handler = onSubmit
sb->normal = theme.normal
sb->active = theme.selected
gFields.Push( rb )
gFields.Push( sb )
end sub
'' ---------------------------------------------------------------------------
'' A FOCUSABLE WIDGET MUST BE PERSISTENT.
''
'' Immediate mode says widgets are values that live for one frame, and that is
'' true of DECORATION -- a Block or a Paragraph can be a local inside View.
'' It is NOT true of anything the focus ring points at: the ring holds
'' TuiWidget ptr, and a widget constructed inside View dies at the end of it.
''
'' So the focusable widgets are FIELDS of the app. Their state structs are
'' still caller-owned -- what changed is the lifetime of the widget, not of the state.
type FormApp extends TuiApp
as TuiTheme theme
as TuiTextInputState nameState, emailState
as boolean subscribed
as int32 planChoice
as TuiFocusRing ring
as TuiMessageBox confirmBox
'' persistent, because they are focusable
as TuiTextInput nameIn, emailIn
as TuiCheckbox subscribeBox
as TuiRadioGroup planGroup
declare destructor( ) override
declare function Init( ) as TuiCmd override
declare function Update( byref e as TuiEvent ) as TuiCmd override
declare sub View( byref f as TuiFrame ) override
end type
destructor FormApp( )
'' whoever holds the pointers owns them.
for each fld in gFields
if( fld <> 0 ) then delete fld
next
gFields.Clear( )
end destructor
function FormApp.Init( ) as TuiCmd
theme = TuiThemeDefault( )
TuiTextInputStateInit( nameState )
TuiTextInputStateInit( emailState )
subscribed = FALSE
planChoice = 0
gConfirm = FALSE
gStatus = "Tab cycles all six fields, Esc quits"
'' ---- the persistent, focusable widgets
'' A FOCUSED FIELD MUST LOOK DIFFERENT. Setting focused_ to the same style
'' as normal makes tabbing onto a text input change nothing on screen --
'' the field is focused and accepting keys, and the user cannot tell. The
'' terminal cursor alone is not enough of a cue.
dim as TuiStyle fieldNormal = TuiStyleOf( TuiDefaultColor( ), TuiRgb( 38, 38, 38 ) )
dim as TuiStyle fieldFocus = TuiStyleOf( TuiRgb( 20, 20, 20 ), TuiRgb( 150, 220, 255 ) )
nameIn.state = @nameState
nameIn.placeholder = "your name"
nameIn.normal = fieldNormal
nameIn.focused_ = fieldFocus
nameIn.placeholderStyle = TuiStylePatch( TuiRgb( 130, 130, 130 ), TuiDefaultColor( ), 0, 0 )
nameIn.selectionStyle = theme.selected
emailIn.state = @emailState
emailIn.placeholder = "you@example.com"
emailIn.normal = fieldNormal
emailIn.focused_ = fieldFocus
emailIn.placeholderStyle = TuiStylePatch( TuiRgb( 130, 130, 130 ), TuiDefaultColor( ), 0, 0 )
emailIn.selectionStyle = theme.selected
subscribeBox.label = "send me updates"
subscribeBox.checked = @subscribed
subscribeBox.useUnicode = TRUE
subscribeBox.normal = theme.normal
subscribeBox.focused_ = TuiStyleOf( TuiDefaultColor( ), TuiRgb( 38, 38, 38 ) )
planGroup.horizontal = TRUE
planGroup.selected = @planChoice
planGroup.useUnicode = TRUE
planGroup.normal = theme.normal
planGroup.focused_ = TuiStyleOf( TuiDefaultColor( ), TuiRgb( 38, 38, 38 ) )
planGroup.activeStyle = TuiStylePatch( TuiRgb( 90, 220, 220 ), TuiDefaultColor( ), TUIATTR_BOLD, TUIATTR_BOLD )
planGroup.options.Push( "free" )
planGroup.options.Push( "pro" )
planGroup.options.Push( "team" )
'' the two closure-carrying buttons, built where their closures live
BuildButtons( theme )
'' THE FOCUS RING, in tab order. It holds BORROWED pointers -- it does not
'' own anything, and every one of these outlives the frame.
ring.Add( @nameIn )
ring.Add( @emailIn )
ring.Add( @subscribeBox )
ring.Add( @planGroup )
for each w in gFields
ring.Add( w )
next
confirmBox.title = " Confirm "
confirmBox.message = "Submit this form?"
confirmBox.buttons.Push( "Yes" )
confirmBox.buttons.Push( "No" )
confirmBox.theme = theme
confirmBox.useUnicode = TRUE
confirmBox.msgKind = TUIMSG_QUESTION
return TuiNone( )
end function
function FormApp.Update( byref e as TuiEvent ) as TuiCmd
'' A MODAL CONSUMES EVERYTHING IT DOES NOT USE.
if( gConfirm ) then
confirmBox.HandleEvent( e )
if( confirmBox.Accepted( ) ) then return TuiQuit( 0 )
if( confirmBox.Cancelled( ) ) then
gConfirm = FALSE
gStatus = "cancelled"
end if
return TuiRedraw( )
end if
if( e.kind <> TUIEVENT_KEY ) then return TuiNone( )
if( e.key.code = TUIKEY_ESCAPE ) then return TuiQuit( 0 )
'' Dispatch order: the focused widget FIRST.
dim as Optional( of TuiWidget ptr ) cur = ring.Current( )
if( cur.HasValue( ) ) then
if( cur.Value( )->HandleEvent( e ) ) then return TuiRedraw( )
end if
'' then the global bindings
if( e.key.code = TUIKEY_TAB ) then
if( e.key.HasMod( TUIMOD_SHIFT ) ) then ring.Prev( ) else ring.Next_( )
return TuiRedraw( )
end if
return TuiNone( )
end function
sub FormApp.View( byref f as TuiFrame )
dim as TuiRect box = TuiCenter( f.Area( ), 56, 16 )
dim as TuiBlock blk
blk.borderKind = TUIBORDER_ROUNDED
blk.sides = TUISIDE_ALL
blk.borderStyle = theme.border
blk.fill = TRUE
blk.fillStyle = theme.normal
blk.AddTitle( " Sign up ", theme.title, TUISIDE_TOP, TUIALIGN_CENTER )
blk.AddTitle( " Esc quits ", theme.dim_, TUISIDE_BOTTOM, TUIALIGN_RIGHT )
blk.Render( f, box )
dim as TuiRect inner = TuiPadAll( blk.Inner( box ), 1 )
dim as TuiConstraintArray rows
rows.Push( TuiLength( 2 ) ) '' name
rows.Push( TuiLength( 2 ) ) '' email
rows.Push( TuiLength( 1 ) ) '' subscribe
rows.Push( TuiLength( 2 ) ) '' plan
rows.Push( TuiFill( ) ) '' spacer
rows.Push( TuiLength( 1 ) ) '' buttons
rows.Push( TuiLength( 1 ) ) '' status
dim as TuiRectArray r = TuiSplitV( inner, rows )
'' a label beside each input, so an empty focused field is still readable
dim as TuiConstraintArray fc
fc.Push( TuiLength( 8 ) )
fc.Push( TuiFill( ) )
dim as TuiRectArray nrow = TuiSplitH( TuiRectOf( r[0].x, r[0].y, r[0].w, 1 ), fc )
dim as TuiParagraph nlab
nlab.text = "Name" : nlab.st = theme.dim_
nlab.Render( f, nrow[0] )
nameIn.Render( f, nrow[1] )
dim as TuiRectArray erow = TuiSplitH( TuiRectOf( r[1].x, r[1].y, r[1].w, 1 ), fc )
dim as TuiParagraph elab
elab.text = "Email" : elab.st = theme.dim_
elab.Render( f, erow[0] )
emailIn.Render( f, erow[1] )
subscribeBox.Render( f, r[2] )
planGroup.Render( f, r[3] )
'' the two buttons -- both closure-backed, rendered through the SAME base
'' pointer
dim as TuiConstraintArray bc
bc.Push( TuiFill( ) )
bc.Push( TuiLength( 9 ) )
bc.Push( TuiLength( 10 ) )
dim as TuiRectArray bt = TuiSplitH( r[5], bc )
if( gFields.Count( ) >= 2 ) then
gFields[0]->Render( f, bt[1] )
gFields[1]->Render( f, bt[2] )
end if
'' the status line names the focused widget, so "did Tab do anything?" is
'' answerable without squinting
dim as ustring focusName = "(none)"
dim as Optional( of TuiWidget ptr ) cur = ring.Current( )
if( cur.HasValue( ) ) then focusName = cur.Value( )->Kind( )
dim as TuiParagraph st
st.text = gStatus & " [focus: " & focusName & "]"
st.st = theme.dim_
st.Render( f, r[6] )
'' The modal draws into its own LAYER, so what is beneath survives
'' dismissal intact.
if( gConfirm ) then
confirmBox.Render( f, TuiCenter( f.Area( ), 36, 7 ) )
end if
end sub
'' ---------------------------------------------------------------------------
dim as TuiTermOptions opts = TuiTermOptionsDefault( )
opts.hideCursor = FALSE '' a form wants a visible cursor
dim as FormApp app
dim as TuiRunResult r = TuiRun( app, opts )
if( r.IsOk( ) = false ) then
print "form: " & r.Failure( ).Message( )
end 1
end ifLibrary: Terminal TUI Library
Other examples: hello.tiko · dashboard.tiko
tiko-lang · Home · Feature Index · Keyword Index · License
FreeBASIC manual pages reproduced under the GNU FDL 1.3.
- Generics
- Interfaces
- For Each
- Iterators
- Tuples
- Array Results
- Defer
- Lambdas
- Block Strings
- Partial Types
- String Slices
- Embed
- Typed Variadics
- C Header Import
- Foreign Library Linking
- UString
- Fixed-Width Types
- Nullptr
- String Library
- Format
- Array
- Map
- Set
- LinkedList
- OrderedMap
- OrderedSet
- Sorting
- Optional
- Result
- Smart Pointers
- Date and Time (Chrono)
- Filesystem
- File Streams
- Config (INI and TOML)
- Codec
- Hash
- UUID
- OS
- Process
- Net
- HTTP and TLS
- Math Library
- Finance
- Logging
- Terminal TUI Library
- PsControls
- tkControls
- sys/win
- Goroutines and Channels
- LLVM Backend
- tiko build
- TIKO_COMPILER
- Variable Declarations
- Standard Data Types
- User Defined Types
- Procedures
- Operators
- Control Flow Statements
- Converting Data Types
- Array Functions
- String Functions
- Mathematical Functions
- Bit Manipulation
- Date and Time Functions
- File I/O Functions
- Console Functions
- User Input (Runtime Library)
- User Input Functions (Graphics Library)
- Screen Functions
- 2D Drawing Functions
- Graphics Library Reference
- Graphics Keyword List
- Memory Functions
- Threading Support Functions
- Error Handling Functions
- Operating System Functions
- Preprocessor
- Intrinsic Defines
- Miscellaneous Keywords
- Modularizing