Repository navigation
LuaUI TextInput
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.
| 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
|
#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 codepageWhich 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.
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
endonEnter 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.
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.
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 spacesEscape 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
%anever matches one and the field would reject every keystroke. Leaveallowunset on those fields and letmaxBytesandstrictCodePagedo 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 = "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 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.
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 codepageSend 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.
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.
| 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.
MuLua Scripting Plugin | Home
🖼️ Lua UI
- File map
- Creating windows
- Client and server
- Designing a window
- Protocol
- Scrolling and scrollbars
- Controls
- Custom text
- Text input
- Debug console
- Bitmap Slicer
- Encrypting scripts
Maps
Monsters
Shared
Version 1.14 | 2026-09-09