InlineInput is a reusable PAYDAY 2 BLT helper for text inputs inside mod option menus.
It renders a text field over a BLT menu row and handles focus, typing, caret movement, selection, clipboard shortcuts, Backspace/Delete, Enter, Esc, validation, refreshes, and lifecycle callbacks. Use it when a mod needs a search box, name field, numeric entry, or any other inline text value.
- PAYDAY 2 with BLT or SuperBLT.
Load the library, register an input, then add a menu row for it:
local ok, InlineInput = pcall(dofile, ModPath .. "../InlineInput/require.lua")
if not ok or not InlineInput then
log("[MyMod] InlineInput is not installed")
return
end
MyMod.search_text = MyMod.search_text or ""
local search_input = InlineInput:RegisterInput({
id = "my_mod:options_search",
menu_id = "my_mod_options",
item_id = "my_mod_options_search",
placeholder = "SEARCH",
max_length = 64,
get_value = function()
return MyMod.search_text
end,
set_value = function(value, node_gui, source)
MyMod.search_text = value
MyMod:ApplyOptionsFilter(value)
end,
on_submit = function(value)
MyMod:SaveSettings()
end,
on_cancel = function()
MyMod.search_text = ""
MyMod:ApplyOptionsFilter("")
end
})
search_input:add_menu_item(MenuHelper, {
priority = 1000
})Each registration describes one input field. InlineInput finds the matching menu row, creates a SearchBoxGuiObject over that row, and keeps it synced while the menu is visible.
The usual flow is:
- Register once with a globally unique
id. - Scope the input to a menu with
menu_id. - Add a row with
handle:add_menu_item(...), or create your own BLT row whoseidmatches the inputitem_id. - Store the value through
get_valueandset_value. - Use lifecycle callbacks for filtering, saving, cancelling, and cleanup.
Prefer ids prefixed by your mod id, for example my_mod:search. This prevents collisions when several mods use InlineInput.
Use this when the menu button row already exists, or when your mod needs to create the row itself. InlineInput binds the input to that BLT row by matching the input item_id to the row id. Your mod still owns creating the row and wiring its callback; InlineInput owns rendering and editing the text field on that row.
The callback should call handle:focus(item, "mouse") so clicking or selecting the row enters text-edit mode:
local name_input = InlineInput:RegisterInput({
id = "my_mod:profile_name",
menu_id = "my_mod_options",
item_id = "my_mod_profile_name",
placeholder = "PROFILE NAME",
get_value = function()
return MyMod.profile_name or ""
end,
set_value = function(value)
MyMod.profile_name = value
end
})
MenuHelper:AddButton({
id = "my_mod_profile_name",
title = " ",
callback = "my_mod_profile_name_focus",
menu_id = "my_mod_options",
localized = false
})
MenuCallbackHandler.my_mod_profile_name_focus = function(_, item)
return name_input:focus(item, "mouse")
endIf you do not need custom row creation, prefer handle:add_menu_item(...); it creates the BLT button row and installs the focus callback for you.
For dialogs or custom panels owned by another mod, create the host UI yourself and pass concrete row panels to AttachHostInputs.
local attachment = InlineInput:AttachHostInputs({
adapter_id = "my_dialog",
node_id = "my_dialog_id",
owner = dialog_gui,
ws = ws,
item_panel = canvas_panel,
row_items = {
{ config = input_a, gui_panel = row_panel_a },
{ config = input_b, gui_panel = row_panel_b }
},
focus_id = "input_b",
focus_on_attach = true,
refresh = function(node_gui, owner)
owner:refresh_items()
end
})config can be a RegisterInput handle or a raw config table. Raw configs are registered automatically. Every row item must provide gui_panel, and node_id is required.
Forward host events to the attachment:
function MyDialogGui:mouse_pressed(button, x, y)
return self._inline_inputs and self._inline_inputs:mouse_pressed(button, x, y)
end
function MyDialogGui:update(t, dt)
if self._inline_inputs then
self._inline_inputs:update(dt)
end
end
function MyDialogGui:close()
if self._inline_inputs then
self._inline_inputs:destroy("dialog_close")
end
endUseful attachment methods are sync, focus(id, source), blur(id, source), input_box(id), input_focus, finish_focused(source), mouse_pressed, mouse_moved, mouse_released, update(dt), and destroy(source).
Stock QuickMenu and managers.system_menu:show(...) dialogs do not expose a stable pre-creation API for injecting custom input panels. If you need native dialog styling, own the dialog class or panel script in your mod, create the row panels there, then attach InlineInput.
Pass these fields to InlineInput:RegisterInput(config). Color fields may be color values or functions that return a color.
| Field | Purpose |
|---|---|
id |
Globally unique registration id. Also used as item_id when item_id is omitted. |
menu_id |
BLT menu node id. Strongly recommended so the input only attaches inside that menu. |
item_id |
Row id to attach to. Defaults to id. |
title |
Row title used by handle:add_menu_item. Use " " for a pure input row. |
desc / description |
Row description used by handle:add_menu_item. |
priority |
MenuHelper row priority used by handle:add_menu_item. |
localized |
Passed to MenuHelper when adding the row. |
visible_callback_name |
Optional BLT visibility callback name for the generated row. |
callback_id / callback |
Optional existing MenuCallbackHandler callback. If omitted, InlineInput creates one. |
input_width_scale / width_scale |
Optional multiplier for the rendered input panel width, for example 0.5 for half width. |
input_width / field_width |
Optional maximum rendered input panel width in pixels. |
placeholder / placeholder_text |
Placeholder shown while the value is empty. May be a string or function. |
value |
Simple stored value when get_value and set_value are not provided. |
get_value(config) |
Returns the current value. Return nil to fall back to config.value. |
set_value(value, node_gui, source, config) |
Stores an accepted value. |
max_length / max_input_length |
Maximum text length. |
value_type = "number" |
Parses and validates the value as a number. |
numeric = true |
Alias for value_type = "number". |
min_value / max_value |
Numeric bounds when numeric parsing is enabled. |
validate(value, node_gui, source, config) |
Custom validation and conversion hook. |
commit_only = true |
Do not call set_value on each typed change; only apply on finish. |
apply_mode = "commit" |
Same behavior as commit_only = true. "submit" is also treated as commit-only for live edits. |
refresh |
false disables refresh, a function overrides refresh, otherwise InlineInput refreshes the active node after committed changes. |
show_brackets = false |
Hides the native SearchBoxGuiObject border brackets. hide_brackets = true and remove_brackets = true are aliases. |
background_color / input_background_color |
Native input background color. |
placeholder_color |
Placeholder color for inactive/passive empty text. |
active_placeholder_color / focused_placeholder_color |
Placeholder color while the input is focused. Falls back to placeholder_color. |
text_color / input_text_color |
Active and inactive value text color. Also used by the focused caret. |
selected_text_color / selection_color |
Text color used by the native selected-text rendering. |
selection_background_color / selection_bg_color |
Selection highlight background color. selected_text_highlight_bg_color is also accepted. |
not_editable / read_only / readonly |
Prevents text changes while preserving focus, caret movement, selection, and copy. |
clear_on_escape = false |
Makes Esc blur without clearing. |
escape_behavior / esc_behavior |
Esc behavior: "clear", "revert", or blur-style values "blur", "keep", "preserve", "none". |
allow_escape_propagation = true |
Allows Esc to continue to the menu after InlineInput handles it. Defaults to false. |
debug_events = true |
Writes lifecycle event lines to Console when the Console mod is installed. |
InlineInput supports the usual single-line editing shortcuts while the row has focus:
| Input | Behavior |
|---|---|
Enter |
Submit and blur. |
Esc |
Cancel/blur according to escape_behavior. |
Left / Right |
Move the caret by one character. |
Shift+Left / Shift+Right |
Extend the selection by one character. |
Ctrl+Left / Ctrl+Right |
Move the caret by one word. |
Home / Page Up |
Move the caret to the start. |
End / Page Down |
Move the caret to the end. |
Ctrl+A |
Select all text. |
Ctrl+C |
Copy the selection, or all text when nothing is selected. |
Ctrl+X |
Cut the selected text. |
Ctrl+V |
Paste clipboard text at the caret or over the selection. |
Backspace / Delete |
Remove the character before/after the caret, or remove the selection. |
Ctrl+Backspace / Ctrl+Delete |
Remove the previous/next word. |
Up and Down remain menu navigation keys: pressing either finishes the current input with source "navigation" and lets the menu move focus.
Return false or nil from validate to reject the value and restore the previous visible text.
Return true to accept the text as-is:
validate = function(value)
return string.len(value) >= 3
endReturn true, stored_value, display_value to store one value and show another:
validate = function(value)
local number = tonumber(value)
if not number then
return false
end
number = math.floor(number)
return true, number, tostring(number)
endFor simple numeric fields, prefer the built-in parser:
InlineInput:RegisterInput({
id = "my_mod:spawn_limit",
menu_id = "my_mod_options",
placeholder = "SPAWN LIMIT",
value_type = "number",
min_value = 0,
max_value = 999,
get_value = function()
return MyMod.spawn_limit or 0
end,
set_value = function(value)
MyMod.spawn_limit = tonumber(value) or 0
end
})All callbacks are optional. InlineInput calls them with pcall, so callback errors are logged instead of breaking the menu.
| Callback | Signature | When it runs |
|---|---|---|
on_focus |
(value, node_gui, source, config, input_box) |
Once when the input gains focus. |
on_blur |
(value, node_gui, source, config, input_box) |
Once when the focused input loses focus. |
on_finish |
(value, node_gui, source, config, input_box) |
After a valid finish path such as Enter, Esc, outside click, or navigation. |
on_change |
(value, node_gui, source, config) |
After an accepted live value change. Not called again for the same value. |
on_submit |
(value, node_gui, source, config) |
After Enter accepts and stores the value. |
on_cancel |
(value, node_gui, source, config) |
After Esc applies its configured behavior and stores the resulting value. |
on_disconnect |
(value, node_gui, source, config) |
When the underlying input box disconnects and applies its current value. |
on_invalid |
(value, node_gui, source, config) |
When validation rejects a value. |
Common source values are "change", "repeat", "enter", "esc", "disconnect", "outside_click", "navigation", "focus", and "blur".
Use on_submit when you specifically care about Enter. Use on_finish when you need cleanup after any completed input session.
RegisterInput returns the registration table with helper methods:
| Method | Purpose |
|---|---|
handle:add_menu_item(MenuHelper, overrides) |
Adds a blank button row wired to focus the input. |
handle:focus(target, source) |
Focuses the input for the active row or node. |
handle:blur(target, source) |
Blurs the input. |
handle:set_focus(target, focused, source) |
Shared focus/blur helper. |
handle:sync(node_gui) |
Ensures the input box exists and is synced for a node. |
handle:input_box(node_gui) |
Returns the active input box for that node. |
handle:input_focus(node_gui) |
Returns true when that input box has focus. |
handle:install_hooks() |
Installs InlineInput hooks if they are not installed yet. |
handle:available() |
Returns true while the registration is still active. |
handle:unregister() |
Removes the registration. |
handle:box(node_gui) is an alias for handle:input_box(node_gui), and handle:open(target, source) is an alias for handle:focus(target, source).
Set debug_events = true on a registration to print event traces to the Console mod:
InlineInput:RegisterInput({
id = "my_mod:debug_search",
menu_id = "my_mod_options",
placeholder = "DEBUG SEARCH",
debug_events = true,
get_value = function()
return MyMod.debug_search or ""
end,
set_value = function(value)
MyMod.debug_search = value
end
})Example Console output:
[InlineInput] event=focus id=my_mod:debug_search source=mouse value="" node=my_mod_options
[InlineInput] event=change id=my_mod:debug_search source=change value="abc" node=my_mod_options
[InlineInput] event=submit id=my_mod:debug_search source=enter value="abc" node=my_mod_options
[InlineInput] event=blur id=my_mod:debug_search source=enter value="abc" node=my_mod_options
[InlineInput] event=finish id=my_mod:debug_search source=enter value="abc" node=my_mod_options
If Console is not installed, debug event logging is a no-op. The input still works.
- Register each input once during menu setup.
- Use a globally unique
id. - Use
menu_idunless the row id is guaranteed to be unique across all menus. - Keep
get_valueandset_valuesmall. Do expensive filtering or saving in callbacks where possible. - Use
commit_only = truefor values that should not update while the user is typing.