Skip to content

LuaUI TextInput

wezzzyrek1 edited this page Aug 13, 2026 · 1 revision

Lua UI - Text Input

A single-line field the player types into.

The running example uses two: an amount to convert, and an optional voucher code.

local amount = col:TextInput{
	w = 200, h = 20,
	numeric = true, maxValue = S.MAX,
	placeholder = "WCoin to convert",
	backColor = RGBA(0, 0, 0, 160),
	color = RGBA(255, 255, 255, 255),
}

Read what was typed off .text.

Options

Option Values Default Configures
w number, canvas units the group's usable width field width
h number, canvas units 18 field height
maxLen integer, characters 32 longest text accepted; refused per keystroke
maxBytes integer, bytes; 0 = off 0 size cap once converted to the client's codepage, for a value bound to a fixed-size column
minLen integer, characters 0 shortest text ui.textOk will accept
minBytes integer, bytes 0 same, measured in codepage bytes
numeric true / false false digits only
maxValue integer none numeric fields: refuses a keystroke that would exceed this value
allow Lua pattern describing one character, e.g. "[%a%d_]" none which characters may be typed at all - see below
placeholder string none hint shown while the field is empty and unfocused
placeholderColor RGBA(r, g, b, a), each 0-255 RGBA(150, 150, 150, 155) hint colour
placeholderAlign ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER follows align hint alignment, independent of the text's
align ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER ALIGN_LEFT where the text sits in the box
color RGBA(r, g, b, a), each 0-255 RGBA(255, 255, 255, 255) text colour
backColor RGBA(r, g, b, a), each 0-255 RGBA(0, 0, 0, 200) box fill behind the text
ime true / false true allow IME composition in this field; false gives direct input
strictCodePage true / false true refuse characters the codepage cannot hold; false accepts them and they leave as ?
onEnter function none called on Enter, once the field satisfies minLen/minBytes

Characters are not bytes

#text counts bytes. A six-character Chinese name is 18 bytes in UTF-8, so a byte-based length check rejects a perfectly valid name. Count characters with the helper instead:

ui.textLen(voucher.text)   -- characters
voucher.bytesUsed          -- what it costs in the client's codepage

Which one you should limit by depends on what the value is for:

The value goes to Limit by
a database column or a fixed-size game structure maxBytes
something you display, count or compare maxLen

Pair the ends in the same unit - minLen with maxLen, minBytes with maxBytes. Mixing them gives you a field that can never satisfy both.

Maximums bite immediately, minimums at submit

maxLen, maxBytes, numeric and maxValue are enforced per keystroke: the field simply refuses the character, so it can never hold a value that breaks them.

A minimum cannot work that way - a field has to pass through "too short" while it is being typed. So minLen/minBytes are checked when you submit:

if ui.textOk(voucher) == false then
	status.text = "That voucher code is too short."
	return
end

onEnter applies the same test for you and stays quiet until the field is long enough, so pressing Enter early does nothing rather than sending a bad value.

Digits only

col:TextInput{ w = 180, h = 20, numeric = true, maxValue = 100000 }

numeric refuses everything but digits. maxValue additionally refuses a keystroke that would push the number past the limit, which is friendlier than clamping after the fact - the player sees the field stop rather than watching their number change.

Validate the number on the server anyway. The field limits typing, not what arrives.

Restricting the character set

numeric allows digits and nothing else. allow is the general form: give it a pattern and only characters matching it can be typed.

Every keystroke is tested on its own - the field runs string.match on the single character the player just pressed and keeps it only if the pattern matches. So the pattern describes one character, which in practice means a character class in square brackets. Anchors, quantifiers and multi-character sequences have nothing to act on here.

The useful classes:

Pattern Accepts
%a letters
%d digits
%w letters and digits
%u / %l uppercase / lowercase letters
%s whitespace
%p punctuation
[abc] exactly those characters
[a-f] a range
[^%s] anything except whitespace

Combine them inside one set:

allow = "[%u%d]"        -- voucher code: uppercase letters and digits
allow = "[%a%d_]"       -- nickname: letters, digits, underscore
allow = "[%d%-]"        -- a signed number
allow = "[^%s]"         -- anything but spaces

Escape a literal -, % or ] with %, as in "[%d%-]" above.

Two things it cannot do:

  • Rules about the whole value. "Must start with a letter", "exactly 8 characters", "no two dashes in a row" - none of that is expressible per character. Check those when the value is submitted.
  • Non-Latin scripts. The classes above are defined over single bytes, and a Chinese or Korean character is several, so %a never matches one and the field would reject every keystroke. Leave allow unset on those fields and let maxBytes and strictCodePage do the work.

An invalid pattern does not lock the field: the test is skipped and the character is accepted, so a typo in the pattern shows up as a filter that does nothing rather than a field nobody can type into.

Placeholder

placeholder      = "Character name",
placeholderColor = RGBA(140, 140, 140, 255),
placeholderAlign = ALIGN_CENTER,

Shown while the field is empty and unfocused, gone the moment there is text. placeholderAlign defaults to the field's own alignment - set it only when you want the hint centred over a left-aligned field, which reads well on short fields.

IME and CJK input

ime is on by default, which is what Chinese, Japanese and Korean players need: the composition window follows the caret and the candidate list opens under it.

Turn it off per field when a value must be typed literally - a serial, a coupon code, an account name:

col:TextInput{ w = 180, h = 20, ime = false }

That gives direct input with no composition step, in any keyboard layout.

Encoding

The field holds UTF-8, which is what the renderer and your scripts use. The game's own structures and database columns hold the client's codepage. The field can hand you either:

input.text           -- UTF-8, for display, comparison, storage in Lua
input.textCodePage   -- the same text in the client's codepage

Send textCodePage when the value is bound for a name column or a game structure; send text for anything else. Do not convert everything by reflex - it corrupts values that were never text.

strictCodePage decides what happens when a character has no representation in the codepage in force. Left on, the field refuses the character and logs why. Turned off, it is accepted and later leaves as ? - which is occasionally what you want for a free-text comment, and never what you want for a name.

Reporting limits to the player

A byte count means nothing to a player: 10 bytes buys 10 Latin characters, 5 Chinese, or 3 in some scripts. Show the state rather than the number - a fill bar next to the field costs two panels:

The voucher field is the one this matters for: the code goes into a varchar(10), so its limit is bytes, not characters.

local BAR_MAX = 10   -- must match the field's maxBytes

local voucher = col:TextInput{ w = 200, h = 20, minBytes = 4, maxBytes = BAR_MAX,
	placeholder = "Voucher code (optional)" }

local status = col:Label("", { color = RGBA(255, 120, 120, 255), slide = false })

local barY = voucher.y + voucher.height + 2

win:Panel{ x = voucher.x, y = barY, w = voucher.width, h = 3,
	back = RGBA(0, 0, 0, 150) }

local fill = win:Panel{ x = voucher.x, y = barY, w = 1, h = 3,
	back = RGBA(198, 157, 0, 230) }

fill:show(false)

Event.on("update", function()
	local used = voucher.bytesUsed

	if used > 0 then
		fill:handle().width = voucher.width * math.min(used / BAR_MAX, 1)
		fill:show(true)
	else
		fill:show(false)
	end

	if used >= BAR_MAX then
		status.text = "Maximum length reached"
	elseif status.text == "Maximum length reached" then
		status.text = ""
	end
end)

The bar fills faster for Chinese than for Latin, which is what is actually happening.

Clear a message the moment the value becomes valid. A complaint left standing while the player looks at a corrected field contradicts what they can see.

Properties

Property Type Access What it is
.id integer read the id the layout group assigned
.text string, UTF-8 read/write what the player typed
.textCodePage string read the same text in the client's codepage
.bytesUsed integer read its size in that codepage
.focus boolean read/write whether the caret is in this field; set it to move the caret here
.numeric .ime boolean read/write the matching options
.allow .placeholder string read/write the matching options
.maxBytes integer read/write the matching option
.align ALIGN_LEFT / ALIGN_RIGHT / ALIGN_CENTER read/write the matching option
.onEnter function, or nil read/write the Enter handler
.visible boolean read/write whether it is drawn at all
.x .y .width .height number, canvas units read/write its box
:setColor(c) :setBackColor(c) :setAlign(a) call - setter form of color, backColor, align

Write-only options - set them through the constructor or by assignment, but they do not read back: placeholderColor, placeholderAlign, strictCodePage, maxValue.

See Also

Clone this wiki locally