Files
dusklight/mods/luau_runtime/src/ui.cpp
T
Luke Street 8551f5e0e4 Mods: Support Luau mods (#2377)
* Mods: Support Luau mods

* Enable `integer` type & use it

* Link to headers in modding.md

* Update link to webgpu.h
2026-09-03 09:20:51 -06:00

857 lines
33 KiB
C++

#include "runtime.hpp"
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
namespace luau_runtime {
namespace {
constexpr char kUiWindowMetatable[] = "dusklight.ui_window";
constexpr char kUiDialogMetatable[] = "dusklight.ui_dialog";
constexpr char kUiElementMetatable[] = "dusklight.ui_element";
constexpr char kUiStyleMetatable[] = "dusklight.ui_style";
constexpr char kUiMenuTabMetatable[] = "dusklight.ui_menu_tab";
constexpr char kUiListMetatable[] = "dusklight.ui_list";
const char* ui_metatable(HandleKind kind) {
switch (kind) {
case HandleKind::UiWindow:
return kUiWindowMetatable;
case HandleKind::UiDialog:
return kUiDialogMetatable;
case HandleKind::UiStyle:
return kUiStyleMetatable;
case HandleKind::UiMenuTab:
return kUiMenuTabMetatable;
case HandleKind::UiList:
return kUiListMetatable;
default:
return kUiElementMetatable;
}
}
ScriptHandle& check_ui_handle(lua_State* state, int index, HandleKind kind) {
return check_handle(state, index, ui_metatable(kind), kind);
}
ScriptHandle& check_config_var(lua_State* state, int index) {
luaL_checktype(state, index, LUA_TUSERDATA);
auto* handle = static_cast<ScriptHandle*>(lua_touserdata(state, index));
if (handle == nullptr || handle->kind != HandleKind::ConfigVar || handle->value == 0 ||
handle->vm == nullptr)
{
luaL_argerror(state, index, "expected a live ConfigVar");
}
return *handle;
}
bool call_callback(Callback& callback, int ref, int argumentCount, int resultCount,
const char* name, std::string* outError = nullptr) {
std::string error;
if (call_ref(*callback.vm, ref, argumentCount, resultCount, kCallbackBudget, error)) {
return true;
}
if (outError != nullptr) {
*outError = std::move(error);
} else {
fail_callback(*callback.vm, name, error);
}
return false;
}
ModResult call_build(
Callback& callback, int ref, int argumentCount, const char* name, ModError* outError) {
std::string error;
if (call_callback(callback, ref, argumentCount, 0, name, &error)) {
return MOD_OK;
}
return set_error(outError, MOD_ERROR, error);
}
bool call_predicate(Callback& callback, int ref, int argumentCount, const char* name) {
if (!call_callback(callback, ref, argumentCount, 1, name)) {
return false;
}
lua_State* state = callback.vm->state;
const bool result = lua_toboolean(state, -1) != 0;
lua_pop(state, 1);
return result;
}
void control_get(ModContext*, void* userData, UiControlValue* outValue) {
auto& callback = *static_cast<Callback*>(userData);
if (outValue == nullptr || !call_callback(callback, callback.refs[0], 0, 1, "control get")) {
return;
}
lua_State* state = callback.vm->state;
switch (static_cast<UiControlKind>(callback.tag)) {
case UI_CONTROL_TOGGLE:
outValue->bool_value = lua_toboolean(state, -1) != 0;
break;
case UI_CONTROL_NUMBER:
case UI_CONTROL_SELECT: {
int64_t value = 0;
if (!to_int64(state, -1, value)) {
fail_callback(*callback.vm, "control get", "callback must return an integer");
} else {
outValue->int_value = value;
}
break;
}
case UI_CONTROL_STRING:
case UI_CONTROL_COLOR:
case UI_CONTROL_FILE_PICKER: {
size_t length = 0;
const char* value = lua_tolstring(state, -1, &length);
if (value == nullptr) {
fail_callback(*callback.vm, "control get", "callback must return a string");
} else {
callback.returnedString.assign(value, length);
outValue->string_value = callback.returnedString.c_str();
}
break;
}
default:
break;
}
lua_pop(state, 1);
}
void control_set(ModContext*, void* userData, const UiControlValue* value) {
auto& callback = *static_cast<Callback*>(userData);
if (value == nullptr) {
return;
}
lua_State* state = callback.vm->state;
switch (static_cast<UiControlKind>(callback.tag)) {
case UI_CONTROL_TOGGLE:
lua_pushboolean(state, value->bool_value);
break;
case UI_CONTROL_NUMBER:
case UI_CONTROL_SELECT:
lua_pushinteger64(state, value->int_value);
break;
case UI_CONTROL_STRING:
case UI_CONTROL_COLOR:
case UI_CONTROL_FILE_PICKER:
lua_pushstring(state, value->string_value != nullptr ? value->string_value : "");
break;
default:
return;
}
call_callback(callback, callback.refs[1], 1, 0, "control set");
}
bool control_disabled(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
return call_predicate(callback, callback.refs[3], 0, "control is_disabled");
}
bool control_modified(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
return call_predicate(callback, callback.refs[4], 0, "control is_modified");
}
bool control_selected(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
return call_predicate(callback, callback.refs[5], 0, "control is_selected");
}
void control_pressed(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
call_callback(callback, callback.refs[2], 0, 0, "control on_pressed");
}
ModResult panel_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
return call_build(callback, callback.refs[0], 1, "panel build", outError);
}
ModResult panel_update(ModContext*, void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
return call_build(callback, callback.refs[1], 0, "panel update", outError);
}
ModResult group_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
return call_build(callback, callback.refs[0], 1, "group build", outError);
}
ModResult tab_build(ModContext*, UiWindowHandle window, UiElementHandle left, UiElementHandle right,
void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, window, HandleKind::UiWindow);
push_ui_handle(callback.vm->state, *callback.vm, left, HandleKind::UiElement);
push_ui_handle(callback.vm->state, *callback.vm, right, HandleKind::UiElement);
return call_build(callback, callback.refs[0], 3, "window tab build", outError);
}
void window_closed(ModContext*, UiWindowHandle window, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, window, HandleKind::UiWindow);
call_callback(callback, callback.refs[0], 1, 0, "window on_closed");
}
void dialog_action(ModContext*, UiDialogHandle dialog, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, dialog, HandleKind::UiDialog);
call_callback(callback, callback.refs[0], 1, 0, "dialog action");
}
bool dialog_action_disabled(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
return call_predicate(callback, callback.refs[1], 0, "dialog action is_disabled");
}
void dialog_dismissed(ModContext*, UiDialogHandle dialog, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, dialog, HandleKind::UiDialog);
call_callback(callback, callback.refs[0], 1, 0, "dialog on_dismiss");
}
ModResult dialog_build(ModContext*, UiElementHandle pane, void* userData, ModError* outError) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, pane, HandleKind::UiElement);
return call_build(callback, callback.refs[1], 1, "dialog build", outError);
}
void menu_selected(ModContext*, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
call_callback(callback, callback.refs[0], 0, 0, "menu tab on_selected");
}
void list_pressed(ModContext*, UiListHandle list, uint64_t key, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
call_callback(callback, callback.refs[0], 2, 0, "list on_pressed");
}
bool list_selected(ModContext*, UiListHandle list, uint64_t key, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
return call_predicate(callback, callback.refs[1], 2, "list is_selected");
}
bool list_disabled(ModContext*, UiListHandle list, uint64_t key, void* userData) {
auto& callback = *static_cast<Callback*>(userData);
push_ui_handle(callback.vm->state, *callback.vm, list, HandleKind::UiList);
lua_pushinteger64(callback.vm->state, static_cast<int64_t>(key));
return call_predicate(callback, callback.refs[2], 2, "list is_disabled");
}
std::vector<std::string> string_array(lua_State* state, int table, const char* field) {
std::vector<std::string> result;
lua_getfield(state, table, field);
if (lua_isnil(state, -1)) {
lua_pop(state, 1);
return result;
}
luaL_checktype(state, -1, LUA_TTABLE);
const int count = lua_objlen(state, -1);
result.reserve(count);
for (int i = 1; i <= count; ++i) {
lua_rawgeti(state, -1, i);
result.emplace_back(luaL_checkstring(state, -1));
lua_pop(state, 1);
}
lua_pop(state, 1);
return result;
}
std::vector<UiListItem> list_items(lua_State* state, int table, std::vector<std::string>& labels) {
luaL_checktype(state, table, LUA_TTABLE);
const int count = lua_objlen(state, table);
labels.reserve(count);
std::vector<UiListItem> items;
items.reserve(count);
for (int i = 1; i <= count; ++i) {
lua_rawgeti(state, table, i);
luaL_checktype(state, -1, LUA_TTABLE);
labels.push_back(get_optional_string(state, -1, "label"));
UiListItem item = UI_LIST_ITEM_INIT;
item.key = static_cast<uint64_t>(get_optional_int(state, -1, "key", 0));
items.push_back(item);
lua_pop(state, 1);
}
for (size_t i = 0; i < items.size(); ++i) {
items[i].label = labels[i].c_str();
}
return items;
}
UiControlKind control_kind(lua_State* state, const std::string& kind) {
if (kind == "button")
return UI_CONTROL_BUTTON;
if (kind == "toggle")
return UI_CONTROL_TOGGLE;
if (kind == "number")
return UI_CONTROL_NUMBER;
if (kind == "string")
return UI_CONTROL_STRING;
if (kind == "select")
return UI_CONTROL_SELECT;
if (kind == "color")
return UI_CONTROL_COLOR;
if (kind == "group")
return UI_CONTROL_GROUP;
if (kind == "file_picker")
return UI_CONTROL_FILE_PICKER;
luaL_error(state, "unknown UI control kind '%s'", kind.c_str());
}
UiStyleScope style_scope(lua_State* state, const std::string& scope) {
if (scope == "prelaunch")
return UI_SCOPE_PRELAUNCH;
if (scope == "window")
return UI_SCOPE_WINDOW;
if (scope == "menu_bar")
return UI_SCOPE_MENU_BAR;
if (scope == "overlay")
return UI_SCOPE_OVERLAY;
if (scope == "touch_controls")
return UI_SCOPE_TOUCH_CONTROLS;
if (scope == "graphics_tuner")
return UI_SCOPE_GRAPHICS_TUNER;
luaL_error(state, "unknown UI style scope '%s'", scope.c_str());
}
int pane_add_section(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->pane_add_section(pane.vm->subject, pane.value, luaL_checkstring(state, 2)),
"ui pane_add_section");
return 0;
}
int pane_add_text(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
UiElementHandle element = 0;
check_result(state,
svc_ui->pane_add_text(pane.vm->subject, pane.value, luaL_checkstring(state, 2), &element),
"ui pane_add_text");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_rml(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
UiElementHandle element = 0;
check_result(state,
svc_ui->pane_add_rml(pane.vm->subject, pane.value, luaL_checkstring(state, 2), &element),
"ui pane_add_rml");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_progress(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
UiElementHandle element = 0;
check_result(state,
svc_ui->pane_add_progress(
pane.vm->subject, pane.value, static_cast<float>(luaL_checknumber(state, 2)), &element),
"ui pane_add_progress");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_control(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
luaL_checktype(state, 2, LUA_TTABLE);
UiControlDesc desc = UI_CONTROL_DESC_INIT;
const std::string kind = get_optional_string(state, 2, "kind");
const std::string label = get_optional_string(state, 2, "label");
const std::string help = get_optional_string(state, 2, "help_rml");
const std::string prefix = get_optional_string(state, 2, "prefix");
const std::string suffix = get_optional_string(state, 2, "suffix");
desc.kind = control_kind(state, kind);
desc.label = label.c_str();
desc.help_rml = help.empty() ? nullptr : help.c_str();
desc.min = get_optional_int(state, 2, "min", 0);
desc.max = get_optional_int(state, 2, "max", 0);
desc.step = get_optional_int(state, 2, "step", 1);
desc.prefix = prefix.empty() ? nullptr : prefix.c_str();
desc.suffix = suffix.empty() ? nullptr : suffix.c_str();
desc.max_length = static_cast<int32_t>(get_optional_int(state, 2, "max_length", 0));
desc.color_alpha = get_optional_bool(state, 2, "color_alpha", false);
desc.directory_mode = get_optional_bool(state, 2, "directory_mode", false);
desc.string_set_mode = get_optional_string(state, 2, "string_set_mode") == "change" ?
UI_STRING_SET_ON_CHANGE :
UI_STRING_SET_ON_COMMIT;
std::vector<std::string> options = string_array(state, 2, "options");
std::vector<const char*> optionPointers;
optionPointers.reserve(options.size());
for (const auto& option : options)
optionPointers.push_back(option.c_str());
desc.options = optionPointers.data();
desc.option_count = optionPointers.size();
std::vector<std::string> presets = string_array(state, 2, "color_presets");
std::vector<const char*> presetPointers;
presetPointers.reserve(presets.size());
for (const auto& preset : presets)
presetPointers.push_back(preset.c_str());
desc.color_presets = presetPointers.data();
desc.color_preset_count = presetPointers.size();
std::vector<std::string> filterNames;
std::vector<std::string> filterPatterns;
std::vector<FileFilter> filters;
lua_getfield(state, 2, "file_filters");
if (!lua_isnil(state, -1)) {
luaL_checktype(state, -1, LUA_TTABLE);
const int count = lua_objlen(state, -1);
filterNames.reserve(count);
filterPatterns.reserve(count);
filters.resize(count);
for (int i = 1; i <= count; ++i) {
lua_rawgeti(state, -1, i);
filterNames.push_back(get_optional_string(state, -1, "name"));
filterPatterns.push_back(get_optional_string(state, -1, "pattern"));
lua_pop(state, 1);
}
for (int i = 0; i < count; ++i) {
filters[i] = {filterNames[i].c_str(), filterPatterns[i].c_str()};
}
}
lua_pop(state, 1);
desc.file_filters = filters.data();
desc.file_filter_count = filters.size();
Callback& callback = retain_callback(*pane.vm);
callback.tag = desc.kind;
callback.refs[2] = ref_optional_function(state, 2, "on_pressed");
callback.refs[3] = ref_optional_function(state, 2, "is_disabled");
callback.refs[4] = ref_optional_function(state, 2, "is_modified");
callback.refs[5] = ref_optional_function(state, 2, "is_selected");
desc.user_data = &callback;
desc.on_pressed = callback.refs[2] != LUA_NOREF ? control_pressed : nullptr;
desc.is_disabled = callback.refs[3] != LUA_NOREF ? control_disabled : nullptr;
desc.is_modified = callback.refs[4] != LUA_NOREF ? control_modified : nullptr;
desc.is_selected = callback.refs[5] != LUA_NOREF ? control_selected : nullptr;
lua_getfield(state, 2, "config_var");
if (!lua_isnil(state, -1)) {
auto& variable = check_config_var(state, -1);
desc.binding = UI_BINDING_CONFIG_VAR;
desc.config_var = variable.value;
} else if (desc.kind != UI_CONTROL_BUTTON && desc.kind != UI_CONTROL_GROUP) {
desc.binding = UI_BINDING_CALLBACKS;
callback.refs[0] = ref_required_function(state, 2, "get");
callback.refs[1] = ref_required_function(state, 2, "set");
desc.get = control_get;
desc.set = control_set;
}
lua_pop(state, 1);
UiElementHandle element = 0;
check_result(state, svc_ui->pane_add_control(pane.vm->subject, pane.value, &desc, &element),
"ui pane_add_control");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_group(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
auto& target = check_ui_handle(state, 2, HandleKind::UiElement);
luaL_checktype(state, 3, LUA_TTABLE);
const std::string label = get_optional_string(state, 3, "label");
Callback& callback = retain_callback(*pane.vm);
callback.refs[0] = ref_required_function(state, 3, "build");
UiGroupDesc desc = UI_GROUP_DESC_INIT;
desc.label = label.c_str();
desc.build = group_build;
desc.user_data = &callback;
UiElementHandle element = 0;
check_result(state,
svc_ui->pane_add_group(pane.vm->subject, pane.value, target.value, &desc, &element),
"ui pane_add_group");
push_ui_handle(state, *pane.vm, element, HandleKind::UiElement);
return 1;
}
int pane_add_list(lua_State* state) {
auto& pane = check_ui_handle(state, 1, HandleKind::UiElement);
luaL_checktype(state, 2, LUA_TTABLE);
Callback& callback = retain_callback(*pane.vm);
callback.refs[0] = ref_required_function(state, 2, "on_pressed");
callback.refs[1] = ref_optional_function(state, 2, "is_selected");
callback.refs[2] = ref_optional_function(state, 2, "is_disabled");
std::vector<std::string> labels;
std::vector<UiListItem> items;
lua_getfield(state, 2, "items");
if (!lua_isnil(state, -1))
items = list_items(state, -1, labels);
lua_pop(state, 1);
UiListDesc desc = UI_LIST_DESC_INIT;
desc.items = items.data();
desc.item_count = items.size();
desc.on_pressed = list_pressed;
desc.is_selected = callback.refs[1] != LUA_NOREF ? list_selected : nullptr;
desc.is_disabled = callback.refs[2] != LUA_NOREF ? list_disabled : nullptr;
desc.user_data = &callback;
UiListHandle list = 0;
check_result(state, svc_ui->pane_add_list(pane.vm->subject, pane.value, &desc, &list),
"ui pane_add_list");
push_ui_handle(state, *pane.vm, list, HandleKind::UiList);
return 1;
}
int element_set_text(lua_State* state) {
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->elem_set_text(element.vm->subject, element.value, luaL_checkstring(state, 2)),
"ui elem_set_text");
return 0;
}
int element_set_rml(lua_State* state) {
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->elem_set_rml(element.vm->subject, element.value, luaL_checkstring(state, 2)),
"ui elem_set_rml");
return 0;
}
int element_set_progress(lua_State* state) {
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->elem_set_progress(
element.vm->subject, element.value, static_cast<float>(luaL_checknumber(state, 2))),
"ui elem_set_progress");
return 0;
}
int element_set_class(lua_State* state) {
auto& element = check_ui_handle(state, 1, HandleKind::UiElement);
check_result(state,
svc_ui->elem_set_class(element.vm->subject, element.value, luaL_checkstring(state, 2),
luaL_checkboolean(state, 3) != 0),
"ui elem_set_class");
return 0;
}
int list_set_items(lua_State* state) {
auto& list = check_ui_handle(state, 1, HandleKind::UiList);
std::vector<std::string> labels;
auto items = list_items(state, 2, labels);
check_result(state,
svc_ui->list_set_items(list.vm->subject, list.value, items.data(), items.size()),
"ui list_set_items");
return 0;
}
int window_close(lua_State* state) {
auto& window = check_ui_handle(state, 1, HandleKind::UiWindow);
check_result(state, svc_ui->window_close(window.vm->subject, window.value), "ui window_close");
window.value = 0;
return 0;
}
int dialog_close(lua_State* state) {
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
check_result(state, svc_ui->dialog_close(dialog.vm->subject, dialog.value), "ui dialog_close");
dialog.value = 0;
return 0;
}
int dialog_set_body(lua_State* state) {
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
check_result(state,
svc_ui->dialog_set_body(dialog.vm->subject, dialog.value, luaL_checkstring(state, 2)),
"ui dialog_set_body");
return 0;
}
int dialog_set_icon(lua_State* state) {
auto& dialog = check_ui_handle(state, 1, HandleKind::UiDialog);
check_result(state,
svc_ui->dialog_set_icon(dialog.vm->subject, dialog.value, luaL_checkstring(state, 2)),
"ui dialog_set_icon");
return 0;
}
int style_unregister(lua_State* state) {
auto& style = check_ui_handle(state, 1, HandleKind::UiStyle);
check_result(
state, svc_ui->unregister_styles(style.vm->subject, style.value), "ui unregister_styles");
style.value = 0;
return 0;
}
int menu_tab_unregister(lua_State* state) {
auto& tab = check_ui_handle(state, 1, HandleKind::UiMenuTab);
check_result(
state, svc_ui->unregister_menu_tab(tab.vm->subject, tab.value), "ui unregister_menu_tab");
tab.value = 0;
return 0;
}
int register_mods_panel(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_required_function(state, 1, "build");
callback.refs[1] = ref_optional_function(state, 1, "update");
UiModsPanelDesc desc = UI_MODS_PANEL_DESC_INIT;
desc.build = panel_build;
desc.update = callback.refs[1] != LUA_NOREF ? panel_update : nullptr;
desc.user_data = &callback;
check_result(state, svc_ui->register_mods_panel(vm.subject, &desc), "ui register_mods_panel");
return 0;
}
int window_push(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
std::vector<UiTabDesc> tabs;
std::vector<std::string> titles;
lua_getfield(state, 1, "tabs");
luaL_checktype(state, -1, LUA_TTABLE);
const int tabCount = lua_objlen(state, -1);
tabs.reserve(tabCount);
titles.reserve(tabCount);
for (int i = 1; i <= tabCount; ++i) {
lua_rawgeti(state, -1, i);
titles.push_back(get_optional_string(state, -1, "title"));
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_required_function(state, -1, "build");
callback.refs[1] = ref_optional_function(state, -1, "update");
UiTabDesc tab = UI_TAB_DESC_INIT;
tab.build = tab_build;
tab.update = callback.refs[1] != LUA_NOREF ? panel_update : nullptr;
tab.user_data = &callback;
tabs.push_back(tab);
lua_pop(state, 1);
}
lua_pop(state, 1);
for (size_t i = 0; i < tabs.size(); ++i)
tabs[i].title = titles[i].c_str();
const std::string rcss = get_optional_string(state, 1, "rcss");
Callback& closed = retain_callback(vm);
closed.refs[0] = ref_optional_function(state, 1, "on_closed");
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
desc.tabs = tabs.data();
desc.tab_count = tabs.size();
desc.rcss = rcss.empty() ? nullptr : rcss.c_str();
desc.on_closed = closed.refs[0] != LUA_NOREF ? window_closed : nullptr;
desc.user_data = &closed;
UiWindowHandle window = 0;
check_result(state, svc_ui->window_push(vm.subject, &desc, &window), "ui window_push");
push_ui_handle(state, vm, window, HandleKind::UiWindow);
return 1;
}
UiDialogVariant dialog_variant(lua_State* state, const std::string& variant) {
if (variant.empty() || variant == "normal")
return UI_DIALOG_NORMAL;
if (variant == "warning")
return UI_DIALOG_WARNING;
if (variant == "danger")
return UI_DIALOG_DANGER;
luaL_error(state, "unknown dialog variant '%s'", variant.c_str());
}
int dialog_push(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
const std::string title = get_optional_string(state, 1, "title");
const std::string body = get_optional_string(state, 1, "body_rml");
const std::string icon = get_optional_string(state, 1, "icon");
const std::string variant = get_optional_string(state, 1, "variant");
std::vector<UiDialogAction> actions;
std::vector<std::string> labels;
lua_getfield(state, 1, "actions");
luaL_checktype(state, -1, LUA_TTABLE);
const int actionCount = lua_objlen(state, -1);
actions.reserve(actionCount);
labels.reserve(actionCount);
for (int i = 1; i <= actionCount; ++i) {
lua_rawgeti(state, -1, i);
labels.push_back(get_optional_string(state, -1, "label"));
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_optional_function(state, -1, "on_pressed");
callback.refs[1] = ref_optional_function(state, -1, "is_disabled");
UiDialogAction action = UI_DIALOG_ACTION_INIT;
action.on_pressed = callback.refs[0] != LUA_NOREF ? dialog_action : nullptr;
action.is_disabled = callback.refs[1] != LUA_NOREF ? dialog_action_disabled : nullptr;
action.user_data = &callback;
action.keep_open = get_optional_bool(state, -1, "keep_open", false);
actions.push_back(action);
lua_pop(state, 1);
}
lua_pop(state, 1);
for (size_t i = 0; i < actions.size(); ++i)
actions[i].label = labels[i].c_str();
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_optional_function(state, 1, "on_dismiss");
callback.refs[1] = ref_optional_function(state, 1, "build");
UiDialogDesc desc = UI_DIALOG_DESC_INIT;
desc.title = title.c_str();
desc.body_rml = body.c_str();
desc.icon = icon.empty() ? nullptr : icon.c_str();
desc.variant = dialog_variant(state, variant);
desc.actions = actions.data();
desc.action_count = actions.size();
desc.on_dismiss = callback.refs[0] != LUA_NOREF ? dialog_dismissed : nullptr;
desc.build = callback.refs[1] != LUA_NOREF ? dialog_build : nullptr;
desc.user_data = &callback;
UiDialogHandle dialog = 0;
check_result(state, svc_ui->dialog_push(vm.subject, &desc, &dialog), "ui dialog_push");
push_ui_handle(state, vm, dialog, HandleKind::UiDialog);
return 1;
}
int is_any_document_visible(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
bool visible = false;
check_result(
state, svc_ui->is_any_document_visible(vm.subject, &visible), "ui is_any_document_visible");
lua_pushboolean(state, visible);
return 1;
}
int register_styles(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
const auto scope = style_scope(state, luaL_checkstring(state, 1));
UiStyleHandle style = 0;
check_result(state,
svc_ui->register_styles(vm.subject, scope, luaL_checkstring(state, 2), &style),
"ui register_styles");
push_ui_handle(state, vm, style, HandleKind::UiStyle);
return 1;
}
int register_styles_file(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
const auto scope = style_scope(state, luaL_checkstring(state, 1));
UiStyleHandle style = 0;
check_result(state,
svc_ui->register_styles_file(vm.subject, scope, luaL_checkstring(state, 2), &style),
"ui register_styles_file");
push_ui_handle(state, vm, style, HandleKind::UiStyle);
return 1;
}
int register_menu_tab(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
const std::string label = get_optional_string(state, 1, "label");
Callback& callback = retain_callback(vm);
callback.refs[0] = ref_required_function(state, 1, "on_selected");
UiMenuTabDesc desc = UI_MENU_TAB_DESC_INIT;
desc.label = label.c_str();
desc.on_selected = menu_selected;
desc.user_data = &callback;
UiMenuTabHandle tab = 0;
check_result(state, svc_ui->register_menu_tab(vm.subject, &desc, &tab), "ui register_menu_tab");
push_ui_handle(state, vm, tab, HandleKind::UiMenuTab);
return 1;
}
int push_toast(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
luaL_checktype(state, 1, LUA_TTABLE);
const std::string type = get_optional_string(state, 1, "type");
const std::string title = get_optional_string(state, 1, "title_rml");
const std::string body = get_optional_string(state, 1, "body_rml");
UiToastDesc desc = UI_TOAST_DESC_INIT;
desc.type = type.empty() ? nullptr : type.c_str();
desc.title_rml = title.empty() ? nullptr : title.c_str();
desc.body_rml = body.empty() ? nullptr : body.c_str();
desc.duration_ms = static_cast<uint32_t>(get_optional_int(state, 1, "duration_ms", 0));
check_result(state, svc_ui->push_toast(vm.subject, &desc), "ui push_toast");
return 0;
}
int get_clipboard_text(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
size_t size = 0;
check_result(
state, svc_ui->get_clipboard_text(vm.subject, nullptr, 0, &size), "ui get_clipboard_text");
std::vector<char> text(size + 1);
check_result(state, svc_ui->get_clipboard_text(vm.subject, text.data(), text.size(), nullptr),
"ui get_clipboard_text");
lua_pushlstring(state, text.data(), size);
return 1;
}
int set_clipboard_text(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
check_result(state, svc_ui->set_clipboard_text(vm.subject, luaL_checkstring(state, 1)),
"ui set_clipboard_text");
return 0;
}
} // namespace
void push_ui_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind) {
push_handle(state, vm, value, kind, ui_metatable(kind));
}
int open_ui(lua_State* state) {
Vm& vm = vm_from_upvalue(state);
if (svc_ui == nullptr) {
service_unavailable(state, "UiService");
}
static const luaL_Reg kElementMethods[] = {
{"add_section", pane_add_section},
{"add_text", pane_add_text},
{"add_rml", pane_add_rml},
{"add_progress", pane_add_progress},
{"add_control", pane_add_control},
{"add_group", pane_add_group},
{"add_list", pane_add_list},
{"set_text", element_set_text},
{"set_rml", element_set_rml},
{"set_progress", element_set_progress},
{"set_class", element_set_class},
{nullptr, nullptr},
};
static const luaL_Reg kWindowMethods[] = {{"close", window_close}, {nullptr, nullptr}};
static const luaL_Reg kDialogMethods[] = {
{"close", dialog_close},
{"set_body", dialog_set_body},
{"set_icon", dialog_set_icon},
{nullptr, nullptr},
};
static const luaL_Reg kStyleMethods[] = {{"unregister", style_unregister}, {nullptr, nullptr}};
static const luaL_Reg kMenuMethods[] = {
{"unregister", menu_tab_unregister}, {nullptr, nullptr}};
static const luaL_Reg kListMethods[] = {{"set_items", list_set_items}, {nullptr, nullptr}};
create_handle_metatable(state, kUiElementMetatable, kElementMethods, "UiElement");
create_handle_metatable(state, kUiWindowMetatable, kWindowMethods, "UiWindow");
create_handle_metatable(state, kUiDialogMetatable, kDialogMethods, "UiDialog");
create_handle_metatable(state, kUiStyleMetatable, kStyleMethods, "UiStyle");
create_handle_metatable(state, kUiMenuTabMetatable, kMenuMethods, "UiMenuTab");
create_handle_metatable(state, kUiListMetatable, kListMethods, "UiList");
lua_newtable(state);
set_function(state, vm, "register_mods_panel", register_mods_panel);
set_function(state, vm, "window_push", window_push);
set_function(state, vm, "dialog_push", dialog_push);
set_function(state, vm, "is_any_document_visible", is_any_document_visible);
set_function(state, vm, "register_styles", register_styles);
set_function(state, vm, "register_styles_file", register_styles_file);
set_function(state, vm, "register_menu_tab", register_menu_tab);
set_function(state, vm, "push_toast", push_toast);
set_function(state, vm, "get_clipboard_text", get_clipboard_text);
set_function(state, vm, "set_clipboard_text", set_clipboard_text);
lua_setreadonly(state, -1, true);
return 1;
}
} // namespace luau_runtime