表單指南
LeviLamina提供了一套極簡的表單API,允許開發者用幾行代碼實現複雜的表單功能。
表單API的頭文件被存放於ll/api/form中,此外由於Minecraft的表單ID在運行時是唯一的,所以我們提供了FormIdManager
用於獲取Minecraft的表單ID,允許開發者自己實現一套表單API。
FormIdManager
FormIdManager的頭文件位於ll/api/form/FormIdManager.h,導出了ll::form::FormIdManager::genFormId方法,用於獲取一個唯一的表單ID。
示例
#include "ll/api/form/FormIdManager.h"
void test() { uint formId = ll::form::FormIdManager::genFormId(); }SimpleForm
SimpleForm是一個簡單的表單,提供了標題、內容和按鈕。
SimpleForm的頭文件位於ll/api/form/SimpleForm.h。
此外,由於SimpleForm的方法例如appendButton返回SimpleForm的引用,所以可以鏈式調用。
用法
- 引用頭文件
ll/api/form/SimpleForm.h - 構造一個SimpleForm對象,可以使用帶參數和不帶參數的構造函數,使用帶參數的構造函數需要傳入標題和內容,使用不帶參數的構造函數需要手動調用
setTitle和setContent方法來設置標題和內容 - 通過
appendButton方法添加按鈕,通過appendHeader、appendLabel和appendDivider方法添加僅視覺效果元素。 - 使用
sendTo方法將表單發送給玩家,需要傳入Player對象的引用和一個回調函數,回調函數的參數為Player對象的引用、玩家選擇的按鈕索引和取消原因的枚舉。具體參數可以參考頭文件ll/api/form/SimpleForm.h中Callback的聲明。
Tip
當按鈕的索引為-1時代表玩家取消的表單。
FormCancelReason的本質是std::optional<ModalFormCancelReason>,所以你需要先判斷其是否有值才能使用,並且你需要使用
ModalFormCancelReason的枚舉值來判斷取消的原因。示例中使用了magic_enum庫來獲取枚舉值的名稱。
示例
#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是一個複雜的表單,提供了標題、標籤(Label)、輸入框(Input)、開關(Toggle)、下拉框(Dropdown)、滾動條(Slider) 和分段滾動條(StepSlider)。
用法
- 引用頭文件
ll/api/form/CustomForm.h - 構造一個CustomForm對象,可以使用帶參數和不帶參數的構造函數,使用帶參數的構造函數需要傳入標題,使用不帶參數的構造函數需要手動調用
setTitle方法來設置標題 - 通過
appendInput、appendToggle、appendDropdown、appendSlider和appendStepSlider方法添加各種自定義表單元素,通過appendHeader、appendLabel、appendDivider方法添加各種僅視覺效果元素 - 使用
sendTo方法將表單發送給玩家,需要傳入Player對象的引用和一個回調函數,回調函數的參數為Player對象的引用、表單的結果和取消原因的枚舉。具體參數可以參考頭文件ll/api/form/CustomForm.h中Callback的聲明。
Tip
表單的結果CustomFormResult實際上是std::optional<std::unordered_map<std::string, CustomFormElementResult>>
,一個存儲了元素名稱和結果的無序關聯容器。你可以通過元素名稱來獲取對應的結果,結果的類型是CustomFormElementResult
,是一個聯合體(std::variant<std::monostate, uint64, double, std::string>),所以你需要先使用std::holds_alternative
判斷其類型通過std::get獲取。
示例
#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是一個模態表單,提供了標題、內容和兩個按鈕,通常用於實現雙項選擇。
用法
- 引用頭文件
ll/api/form/ModalForm.h - 構造一個ModalForm對象,可以使用帶參數和不帶參數的構造函數,使用帶參數的構造函數需要傳入標題、內容、上按鈕和下按鈕,使用不帶參數的構造函數需要手動調用
setTitle和setContent方法來設置標題和內容,setUpperButton和setLowerButton方法來設置上按鈕和下按鈕。 - 使用
sendTo方法將表單發送給玩家,需要傳入Player對象的引用和一個回調函數,回調函數的參數為Player對象的引用、玩家選擇的按鈕結果和取消原因的枚舉。具體參數可以參考頭文件ll/api/form/ModalForm.h中Callback的聲明。
Tip
ModalFormResult實際上是std::optional<ModalFormSelectedButton>的別名,而ModalFormSelectedButton是一個Bool枚舉,有Upper = true和
Lower = false兩個值,分別代表上按鈕和下按鈕。
示例
#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
玩家處於表單打開狀態時使用SimpleForm::sendUpdate、CustomForm::sendUpdate和ModalForm::sendUpdate可以更新表單,且不限制更新的表單內容和類型,參數同sendTo。這些方法需要保證玩家處於表單打開中的狀態,否則無效果。
示例
#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());
}