Form Guide
LeviLamina provides a minimalist form API that allows developers to implement complex form functionalities with just a
few lines of code.
The header files for the Form API are stored in ll/api/form. Additionally, since Minecraft's form IDs are unique at
runtime, we provide FormIdManager to retrieve Minecraft's form IDs, allowing developers to implement their own form
API.
FormIdManager
The header file for FormIdManager is located at ll/api/form/FormIdManager.h, exporting the
ll::form::FormIdManager::genFormId method, which is used to obtain a unique form ID.
Example
#include "ll/api/form/FormIdManager.h"
void test() { uint formId = ll::form::FormIdManager::genFormId(); }SimpleForm
SimpleForm is a basic form that provides a title, content, and buttons.
The header file for SimpleForm is located at ll/api/form/SimpleForm.h.
Additionally, since methods like appendButton in SimpleForm return a reference to SimpleForm, they support method
chaining.
Usage
- Include the header file
ll/api/form/SimpleForm.h - Construct a SimpleForm object. You can use either a parameterized or parameterless constructor. The parameterized
constructor requires a title and content, while the parameterless constructor requires manually calling the
setTitleandsetContentmethods to set the title and content. - Add buttons using the
appendButtonmethod and add visual-only elements usingappendHeader,appendLabel, andappendDividermethods. - Use the
sendTomethod to send the form to a player. This requires passing a reference to aPlayerobject and a callback function. The callback function parameters include a reference to thePlayerobject, the index of the button selected by the player, and an enumeration of the cancellation reason. For detailed parameters, refer to theCallbackdeclaration in thell/api/form/SimpleForm.hheader file.
Tip
When the button index is -1, it means the player canceled the form.
FormCancelReason is essentially std::optional<ModalFormCancelReason>, so you need to check whether it has a value
before using it. You should also use the enumeration values of ModalFormCancelReason to determine the cancellation
reason. The example uses the magic_enum library to retrieve the enumeration value name.
Example
#include "ll/api/form/SimpleForm.h"
#include "magic_enum/magic_enum.hpp"
void sendSimpleFormToPlayer(Player& player) {
ll::form::SimpleForm form("I'm title", "I'm content");
// Or
// ll::form::SimpleForm form;
// form.setTitle("I'm title").setContent("I'm content");
form.appendHeader("header")
.appendButton("Button1")
.appendDivider()
.appendHeader("header")
.appendLabel("label")
.appendButton("Button2")
.sendTo(player, [](Player& player, int selected, ll::form::FormCancelReason reason) {
switch (selected) {
case 0: {
player.sendMessage("You clicked Button1");
break;
}
case 1: {
player.sendMessage("You clicked Button2");
break;
}
case -1: {
player.sendMessage("You closed the form");
player.sendMessage(
reason.transform(magic_enum::enum_name<ModalFormCancelReason>).value_or("Unknown Reason")
);
break;
}
}
});
}CustomForm
CustomForm is an advanced form that provides a title, labels, input fields, toggles, dropdowns, sliders, and step sliders.
Usage
- Include the header file
ll/api/form/CustomForm.h - Construct a CustomForm object. You can use either a parameterized or parameterless constructor. The parameterized
constructor requires a title, while the parameterless constructor requires manually calling the
setTitlemethod to set the title. - Add custom form elements using
appendInput,appendToggle,appendDropdown,appendSlider, andappendStepSlidermethods and add visual-only elements usingappendHeader,appendLabel, andappendDividermethods. - Use the
sendTomethod to send the form to a player. This requires passing a reference to aPlayerobject and a callback function. The callback function parameters include a reference to thePlayerobject, the form result, and an enumeration of the cancellation reason. For detailed parameters, refer to theCallbackdeclaration in thell/api/form/CustomForm.hheader file.
Tip
The form result CustomFormResult is actually
std::optional<std::unordered_map<std::string, CustomFormElementResult>>, an unordered associative container storing
element names and results. You can retrieve the corresponding result by element name. The result type is
CustomFormElementResult, which is a variant (std::variant<std::monostate, uint64, double, std::string>), so you need
to check its type using std::holds_alternative before retrieving it using std::get.
Example
#include "ll/api/form/CustomForm.h"
#include "magic_enum/magic_enum.hpp"
void sendCustomFormToPlayer(Player& player) {
ll::form::CustomForm form;
std::vector<std::string> names;
form.setTitle("CustomForm")
.appendHeader("header")
.appendLabel("label")
.appendInput("input1", "input")
.appendToggle("toggle", "toggle")
.appendSlider("slider", "slider", 0, 100, 1)
.appendDivider()
.appendStepSlider("stepSlider", "stepSlider", {"a", "b", "c"})
.appendDropdown("dropdown", "dropdown", {"a", "b", "c"})
.appendLabel("label")
.appendDropdown("emptydropdown", "empty dropdown", names)
.setSubmitButton("Apply")
.sendTo(
player,
[](Player& player, ll::form::CustomFormResult const& data, ll::form::FormCancelReason reason) {
if (!data) {
player.sendMessage("CustomForm callback canceled");
player.sendMessage(reason.transform(magic_enum::enum_name<ModalFormCancelReason>)
.value_or("Unknown Reason"));
return;
}
for (auto [name, result] : *data) {
if (std::holds_alternative<uint64_t>(result)) {
player.sendMessage(
fmt::format("CustomForm callback {} = {}", name, std::get<uint64_t>(result))
);
} else if (std::holds_alternative<double>(result)) {
player.sendMessage(
fmt::format("CustomForm callback {} = {}", name, std::get<double>(result))
);
} else if (std::holds_alternative<std::string>(result)) {
player.sendMessage(
fmt::format("CustomForm callback {} = {}", name, std::get<std::string>(result))
);
}
}
}
);
}ModalForm
ModalForm is a modal form that provides a title, content, and two buttons, typically used for binary choices.
Usage
- Include the header file
ll/api/form/ModalForm.h - Construct a ModalForm object. You can use either a parameterized or parameterless constructor. The parameterized
constructor requires a title, content, upper button, and lower button, while the parameterless constructor requires
manually calling
setTitle,setContent,setUpperButton, andsetLowerButtonmethods. - Use the
sendTomethod to send the form to a player. This requires passing a reference to aPlayerobject and a callback function. The callback function parameters include a reference to thePlayerobject, the button selection result, and an enumeration of the cancellation reason. For detailed parameters, refer to theCallbackdeclaration in thell/api/form/ModalForm.hheader file.
Tip
ModalFormResult is actually an alias for std::optional<ModalFormSelectedButton>, and ModalFormSelectedButton is a
Bool enumeration with two Upper = true and Lower = false values, representing the upper and lower buttons,
respectively.
Example
#include "ll/api/form/ModalForm.h"
#include "magic_enum/magic_enum.hpp"
void sendModalFormToPlayer(Player& player) {
ll::form::ModalForm form;
form.setTitle("ModalForm")
.setUpperButton("Upper")
.setLowerButton("Lower")
.sendTo(
player,
[](Player& player, ll::form::ModalFormResult selected, ll::form::FormCancelReason reason) {
if (!selected) {
player.sendMessage("ModalForm callback canceled");
player.sendMessage(reason.transform(magic_enum::enum_name<ModalFormCancelReason>)
.value_or("Unknown Reason"));
return;
}
player.sendMessage(fmt::format("ModalForm callback {}", (bool)selected));
}
);
}sendUpdate
When the player has the form open, SimpleForm::sendUpdate, CustomForm::sendUpdate, and ModalForm::sendUpdate can be used to update the form, and there are no restrictions on the content and type of the form to be updated. The parameters are the same as those of sendTo. These methods require that the player remains in the state of having the form open; otherwise, they will have no effect.
Example
#include "ll/api/chrono/GameChrono.h"
#include "ll/api/coro/CoroTask.h"
#include "ll/api/form/CustomForm.h"
#include "ll/api/service/Bedrock.h"
#include "ll/api/thread/ServerThreadExecutor.h"
#include "mc/server/ServerLevel.h"
#include <fmt/chrono.h>
std::unique_ptr<ll::form::CustomForm> buildStatusWindow() {
auto form = std::make_unique<ll::form::CustomForm>("Status");
auto now = std::chrono::system_clock::now();
auto ticks =
ll::service::getLevel().transform([](auto& level) { return level.getCurrentServerTick().tickID; }).value_or(-1);
form->appendHeader("System Info")
.appendLabel(fmt::format("Time: {}", now))
.appendLabel(fmt::format("Ticks: {}", ticks))
.appendDivider()
.setSubmitButton("Close");
return std::move(form);
}
void openStatusWindow(Player& player) {
ll::coro::keepThis([uid = player.getOrCreateUniqueID()]() -> ll::coro::CoroTask<void> {
auto closed = std::make_shared<bool>(false);
auto callback = [closed](auto const&...) { *closed = true; };
auto player = ll::service::getLevel().transform([uid](auto& level) { return level.getPlayer(uid); });
if (!player || *closed) co_return;
buildStatusWindow()->sendTo(*player, callback);
while (true) {
co_await ll::chrono::ticks{20};
player = ll::service::getLevel().transform([uid](Level& level) { return level.getPlayer(uid); });
if (!player || *closed) co_return;
buildStatusWindow()->sendUpdate(*player, callback);
}
}).launch(ll::thread::ServerThreadExecutor::getDefault());
}