mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-20 05:16:24 -04:00
1481 lines
49 KiB
C++
1481 lines
49 KiB
C++
#include "ui.hpp"
|
|
|
|
#include "config.hpp"
|
|
#include "registry.hpp"
|
|
#include "slot_map.hpp"
|
|
|
|
#include <borealis/log.hpp>
|
|
#include "dusk/mod_loader.hpp"
|
|
#include "dusk/mods/loader/loader.hpp"
|
|
#include "dusk/ui/menu_bar.hpp"
|
|
#include "dusk/ui/mod_window.hpp"
|
|
#include "dusk/ui/modal.hpp"
|
|
#include "dusk/ui/ui.hpp"
|
|
#include "mods/svc/ui.h"
|
|
|
|
#include <RmlUi/Core.h>
|
|
#include <aurora/rmlui.hpp>
|
|
#include <fmt/format.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <climits>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "SDL3/SDL_clipboard.h"
|
|
|
|
namespace dusk::mods::svc::ui_impl {
|
|
namespace {
|
|
|
|
constexpr borealis::Log Log{"dusk::mods::ui"};
|
|
|
|
enum class UiSlotKind : u8 {
|
|
Window,
|
|
Dialog,
|
|
Pane,
|
|
Text,
|
|
Progress,
|
|
Control,
|
|
Style,
|
|
MenuTab,
|
|
};
|
|
|
|
const char* slot_kind_name(UiSlotKind kind) {
|
|
switch (kind) {
|
|
case UiSlotKind::Window:
|
|
return "window";
|
|
case UiSlotKind::Dialog:
|
|
return "dialog";
|
|
case UiSlotKind::Pane:
|
|
return "pane";
|
|
case UiSlotKind::Text:
|
|
return "text";
|
|
case UiSlotKind::Progress:
|
|
return "progress";
|
|
case UiSlotKind::Control:
|
|
return "control";
|
|
case UiSlotKind::Style:
|
|
return "style";
|
|
case UiSlotKind::MenuTab:
|
|
return "menu tab";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
// Game thread only: all mutations happen in service calls made from mod code, in UI callbacks
|
|
// (ui::update), or in the loader's deactivate paths.
|
|
struct UiSlot {
|
|
UiSlotKind kind = UiSlotKind::Window;
|
|
// Pane/Text/Progress/Control: freed automatically when the element is destroyed
|
|
Rml::Element* element = nullptr;
|
|
// Pane payload
|
|
ui::Pane* pane = nullptr;
|
|
ui::Pane* helpPane = nullptr;
|
|
// Window/Dialog payload (non-owning; the document stack owns the document)
|
|
ui::Document* document = nullptr;
|
|
UiWindowClosedFn onClosed = nullptr;
|
|
void* onClosedUserData = nullptr;
|
|
// Style payload
|
|
ui::DocumentScope styleScope = ui::DocumentScope::None;
|
|
std::string styleId;
|
|
// Cached rendered values for element setters. These make the natural "set every update"
|
|
// style cheap when the displayed value has not changed.
|
|
std::string elementRml;
|
|
float elementFloat = 0.0f;
|
|
bool hasElementValue = false;
|
|
};
|
|
|
|
SlotMap<UiSlot> s_slots;
|
|
|
|
struct ModUiPanel {
|
|
UiPanelBuildFn build = nullptr;
|
|
UiPanelUpdateFn update = nullptr;
|
|
void* userData = nullptr;
|
|
};
|
|
std::unordered_map<const LoadedMod*, ModUiPanel> s_modPanels;
|
|
|
|
struct ModMenuTab {
|
|
uint64_t handle = 0;
|
|
std::string label;
|
|
UiPressedFn onSelected = nullptr;
|
|
void* userData = nullptr;
|
|
};
|
|
std::unordered_map<const LoadedMod*, std::vector<ModMenuTab>> s_modMenuTabs;
|
|
bool s_menuTabsDirty = false;
|
|
|
|
UiSlot* slot_from_handle(uint64_t handle) {
|
|
auto* entry = s_slots.find(handle);
|
|
return entry != nullptr ? &entry->value : nullptr;
|
|
}
|
|
|
|
// Note: s_slots may reallocate on any later allocation, so callers must not hold the returned
|
|
// slot reference across calls that can allocate (e.g. mod build callbacks); re-resolve instead.
|
|
UiSlot& alloc_slot(LoadedMod& mod, UiSlotKind kind, uint64_t& outHandle) {
|
|
outHandle = s_slots.emplace(mod, UiSlot{.kind = kind});
|
|
return s_slots.find(outHandle)->value;
|
|
}
|
|
|
|
UiSlot* resolve(LoadedMod& mod, uint64_t handle, UiSlotKind kind, const char* what) {
|
|
auto* entry = s_slots.find_owned(handle, mod);
|
|
if (entry == nullptr || entry->value.kind != kind) {
|
|
Log.error("[{}] {}: stale or invalid {} handle {:#x}", mod.metadata.id, what,
|
|
slot_kind_name(kind), handle);
|
|
return nullptr;
|
|
}
|
|
return &entry->value;
|
|
}
|
|
|
|
// Whether the registration a callback was created under is still live. Callbacks captured by
|
|
// host-owned UI must check this before calling into the mod: `mod->active` alone is true again
|
|
// once a reload completes, but captured fn pointers still target the unloaded image. Teardown
|
|
// frees the slots (ui_remove_mod), which invalidates every callback built under them.
|
|
bool slot_live(uint64_t handle) {
|
|
return s_slots.find(handle) != nullptr;
|
|
}
|
|
|
|
bool dialog_open(uint64_t handle) {
|
|
auto* slot = slot_from_handle(handle);
|
|
return slot != nullptr && slot->kind == UiSlotKind::Dialog && slot->document != nullptr &&
|
|
slot->document->active();
|
|
}
|
|
|
|
// Frees the slot when the tracked element is destroyed (tab rebuilds, window teardown, ...).
|
|
// The generation check makes a late detach of an already-recycled slot a no-op.
|
|
class SlotDetachListener final : public Rml::EventListener {
|
|
public:
|
|
explicit SlotDetachListener(uint64_t handle) : m_handle{handle} {}
|
|
|
|
void ProcessEvent(Rml::Event&) override {}
|
|
|
|
void OnDetach(Rml::Element*) override {
|
|
s_slots.erase(m_handle);
|
|
delete this;
|
|
}
|
|
|
|
private:
|
|
uint64_t m_handle;
|
|
};
|
|
|
|
void track_element(uint64_t handle, UiSlot& slot, Rml::Element& element) {
|
|
slot.element = &element;
|
|
element.AddEventListener(Rml::EventId::Click, new SlotDetachListener{handle});
|
|
}
|
|
|
|
template <typename T, typename Fn>
|
|
T guarded_call(LoadedMod& mod, const char* what, T fallback, Fn&& fn) {
|
|
if (!mod.active) {
|
|
return fallback;
|
|
}
|
|
try {
|
|
return fn();
|
|
} catch (const std::exception& e) {
|
|
fail_mod(mod, MOD_ERROR, fmt::format("exception in {}: {}", what, e.what()));
|
|
} catch (...) {
|
|
fail_mod(mod, MOD_ERROR, fmt::format("unknown exception in {}", what));
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
template <typename Fn>
|
|
void guarded_call(LoadedMod& mod, const char* what, Fn&& fn) {
|
|
if (!mod.active) {
|
|
return;
|
|
}
|
|
try {
|
|
fn();
|
|
} catch (const std::exception& e) {
|
|
fail_mod(mod, MOD_ERROR, fmt::format("exception in {}: {}", what, e.what()));
|
|
} catch (...) {
|
|
fail_mod(mod, MOD_ERROR, fmt::format("unknown exception in {}", what));
|
|
}
|
|
}
|
|
|
|
// Shared by panel/tab build and update callbacks: translates a non-OK result or an escaped
|
|
// exception into fail_mod, mirroring mod_update handling.
|
|
template <typename Fn>
|
|
void invoke_mod_ui_callback(LoadedMod& mod, const char* what, Fn&& fn) {
|
|
ModError error = MOD_ERROR_INIT;
|
|
const ModResult result = guarded_call(mod, what, MOD_OK, [&] { return fn(&error); });
|
|
if (result != MOD_OK && mod.active) {
|
|
fail_mod(
|
|
mod, result, error.message[0] != '\0' ? error.message : fmt::format("{} failed", what));
|
|
}
|
|
}
|
|
|
|
uint64_t wrap_pane(LoadedMod& mod, ui::Pane& pane, ui::Pane* helpPane) {
|
|
uint64_t handle = 0;
|
|
auto& slot = alloc_slot(mod, UiSlotKind::Pane, handle);
|
|
slot.pane = &pane;
|
|
slot.helpPane = helpPane;
|
|
track_element(handle, slot, *pane.root());
|
|
return handle;
|
|
}
|
|
|
|
int clamp_to_int(int64_t value) {
|
|
return static_cast<int>(std::clamp<int64_t>(value, INT_MIN, INT_MAX));
|
|
}
|
|
|
|
std::function<bool()> wrap_predicate(
|
|
LoadedMod& mod, UiPredicateFn fn, void* userData, uint64_t guardHandle) {
|
|
if (fn == nullptr) {
|
|
return {};
|
|
}
|
|
return [modPtr = &mod, fn, userData, guardHandle] {
|
|
if (!slot_live(guardHandle)) {
|
|
return false;
|
|
}
|
|
return guarded_call(*modPtr, "control predicate", false,
|
|
[&] { return fn(modPtr->context.get(), userData); });
|
|
};
|
|
}
|
|
|
|
void wire_callback_binding(
|
|
LoadedMod& mod, const UiControlDesc& desc, ui::ModControlSpec& spec, uint64_t guardHandle) {
|
|
auto* modPtr = &mod;
|
|
const auto get = desc.get;
|
|
const auto set = desc.set;
|
|
auto* userData = desc.user_data;
|
|
const auto getValue = [modPtr, get, userData, guardHandle] {
|
|
UiControlValue value = UI_CONTROL_VALUE_INIT;
|
|
if (!slot_live(guardHandle)) {
|
|
return value;
|
|
}
|
|
guarded_call(*modPtr, "control getter", [&] {
|
|
get(modPtr->context.get(), userData, &value);
|
|
});
|
|
return value;
|
|
};
|
|
const auto setValue = [modPtr, set, userData, guardHandle](const UiControlValue& value) {
|
|
if (!slot_live(guardHandle)) {
|
|
return;
|
|
}
|
|
guarded_call(*modPtr, "control setter", [&] {
|
|
set(modPtr->context.get(), userData, &value);
|
|
});
|
|
};
|
|
switch (desc.kind) {
|
|
case UI_CONTROL_TOGGLE:
|
|
spec.getBool = [getValue] { return getValue().bool_value; };
|
|
spec.setBool = [setValue](bool value) {
|
|
UiControlValue raw = UI_CONTROL_VALUE_INIT;
|
|
raw.bool_value = value;
|
|
setValue(raw);
|
|
};
|
|
break;
|
|
case UI_CONTROL_NUMBER:
|
|
case UI_CONTROL_SELECT:
|
|
spec.getInt = [getValue] { return clamp_to_int(getValue().int_value); };
|
|
spec.setInt = [setValue](int value) {
|
|
UiControlValue raw = UI_CONTROL_VALUE_INIT;
|
|
raw.int_value = value;
|
|
setValue(raw);
|
|
};
|
|
break;
|
|
case UI_CONTROL_STRING:
|
|
spec.getString = [getValue]() -> Rml::String {
|
|
const UiControlValue value = getValue();
|
|
return value.string_value != nullptr ? value.string_value : "";
|
|
};
|
|
spec.setString = [setValue](Rml::String value) {
|
|
UiControlValue raw = UI_CONTROL_VALUE_INIT;
|
|
raw.string_value = value.c_str();
|
|
setValue(raw);
|
|
};
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// The lambdas re-resolve the var on every call, so a control whose var was unregistered
|
|
// mid-flight degrades to a no-op instead of a dangling read.
|
|
bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModControlSpec& spec) {
|
|
auto* modPtr = &mod;
|
|
const uint64_t varHandle = desc.config_var;
|
|
switch (desc.kind) {
|
|
case UI_CONTROL_TOGGLE: {
|
|
const auto find = [modPtr, varHandle] {
|
|
return static_cast<ConfigVar<bool>*>(
|
|
config_find_var(*modPtr, varHandle, CONFIG_VAR_BOOL));
|
|
};
|
|
if (find() == nullptr) {
|
|
return false;
|
|
}
|
|
spec.getBool = [find] {
|
|
const auto* var = find();
|
|
return var != nullptr && var->getValue();
|
|
};
|
|
spec.setBool = [find](bool value) {
|
|
auto* var = find();
|
|
if (var == nullptr || var->getValue() == value) {
|
|
return;
|
|
}
|
|
var->setValue(value);
|
|
config_mark_dirty();
|
|
};
|
|
if (!spec.isModified) {
|
|
spec.isModified = [find] {
|
|
const auto* var = find();
|
|
return var != nullptr && var->getValue() != var->getDefaultValue();
|
|
};
|
|
}
|
|
return true;
|
|
}
|
|
case UI_CONTROL_NUMBER:
|
|
case UI_CONTROL_SELECT: {
|
|
const auto find = [modPtr, varHandle] {
|
|
return static_cast<ConfigVar<s64>*>(
|
|
config_find_var(*modPtr, varHandle, CONFIG_VAR_INT));
|
|
};
|
|
if (find() == nullptr) {
|
|
return false;
|
|
}
|
|
spec.getInt = [find] {
|
|
const auto* var = find();
|
|
return var != nullptr ? clamp_to_int(var->getValue()) : 0;
|
|
};
|
|
spec.setInt = [find](int value) {
|
|
auto* var = find();
|
|
if (var == nullptr || var->getValue() == value) {
|
|
return;
|
|
}
|
|
var->setValue(value);
|
|
config_mark_dirty();
|
|
};
|
|
if (!spec.isModified) {
|
|
spec.isModified = [find] {
|
|
const auto* var = find();
|
|
return var != nullptr && var->getValue() != var->getDefaultValue();
|
|
};
|
|
}
|
|
return true;
|
|
}
|
|
case UI_CONTROL_STRING: {
|
|
const auto find = [modPtr, varHandle] {
|
|
return static_cast<ConfigVar<std::string>*>(
|
|
config_find_var(*modPtr, varHandle, CONFIG_VAR_STRING));
|
|
};
|
|
if (find() == nullptr) {
|
|
return false;
|
|
}
|
|
spec.getString = [find]() -> Rml::String {
|
|
const auto* var = find();
|
|
return var != nullptr ? var->getValue() : "";
|
|
};
|
|
spec.setString = [find](Rml::String value) {
|
|
auto* var = find();
|
|
if (var == nullptr || var->getValue() == value) {
|
|
return;
|
|
}
|
|
var->setValue(std::move(value));
|
|
config_mark_dirty();
|
|
};
|
|
if (!spec.isModified) {
|
|
spec.isModified = [find] {
|
|
const auto* var = find();
|
|
return var != nullptr && var->getValue() != var->getDefaultValue();
|
|
};
|
|
}
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void on_mod_window_destroyed(uint64_t handle) {
|
|
const auto* entry = s_slots.find(handle);
|
|
if (entry == nullptr || entry->value.kind != UiSlotKind::Window) {
|
|
return;
|
|
}
|
|
auto released = s_slots.take(handle);
|
|
auto* mod = released->owner;
|
|
const UiWindowClosedFn onClosed = released->value.onClosed;
|
|
void* userData = released->value.onClosedUserData;
|
|
if (mod != nullptr && onClosed != nullptr) {
|
|
guarded_call(*mod, "window on_closed callback", [&] {
|
|
onClosed(mod->context.get(), handle, userData);
|
|
});
|
|
}
|
|
}
|
|
|
|
void on_mod_dialog_destroyed(uint64_t handle) {
|
|
auto* slot = slot_from_handle(handle);
|
|
if (slot != nullptr && slot->kind == UiSlotKind::Dialog) {
|
|
s_slots.erase(handle);
|
|
}
|
|
}
|
|
|
|
class ModDialog final : public ui::Modal {
|
|
public:
|
|
ModDialog(Props props, std::function<void()> onDestroyed)
|
|
: Modal{std::move(props)}, m_onDestroyed{std::move(onDestroyed)} {}
|
|
|
|
~ModDialog() override {
|
|
if (m_onDestroyed) {
|
|
m_onDestroyed();
|
|
}
|
|
}
|
|
|
|
void close() { pop(); }
|
|
void force_close() { Document::hide(true); }
|
|
|
|
private:
|
|
std::function<void()> m_onDestroyed;
|
|
};
|
|
|
|
void push_stacked_document(std::unique_ptr<ui::Document> document) {
|
|
if (auto* previousTop = ui::top_document()) {
|
|
previousTop->push(std::move(document));
|
|
} else {
|
|
ui::push_document(std::move(document));
|
|
}
|
|
}
|
|
|
|
// Shared by dialog_push and dialog_add_action; the guard handle keeps a
|
|
// pressed callback from calling into a torn-down mod.
|
|
ui::ModalAction make_dialog_action(LoadedMod& mod, uint64_t handle, const UiDialogAction& action) {
|
|
return {
|
|
.label = action.label,
|
|
.onPressed =
|
|
[modPtr = &mod, handle, fn = action.on_pressed, userData = action.user_data,
|
|
keepOpen = action.keep_open != 0](ui::Modal& modal) {
|
|
if (!dialog_open(handle)) {
|
|
return; // already being torn down
|
|
}
|
|
if (fn != nullptr) {
|
|
guarded_call(*modPtr, "dialog action callback", [&] {
|
|
fn(modPtr->context.get(), handle, userData);
|
|
});
|
|
}
|
|
// The callback may have closed the dialog already
|
|
if (!keepOpen && dialog_open(handle)) {
|
|
static_cast<ModDialog&>(modal).close();
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ModResult ui_register_mods_panel(LoadedMod& mod, const UiModsPanelDesc& desc) {
|
|
s_modPanels[&mod] = {desc.build, desc.update, desc.user_data};
|
|
return MOD_OK;
|
|
}
|
|
|
|
void ui_build_mods_panels(LoadedMod& mod, ui::Pane& pane) {
|
|
const auto it = s_modPanels.find(&mod);
|
|
if (it == s_modPanels.end()) {
|
|
return;
|
|
}
|
|
const uint64_t paneHandle = wrap_pane(mod, pane, nullptr);
|
|
const auto& panel = it->second;
|
|
if (!mod.active || panel.build == nullptr) {
|
|
return;
|
|
}
|
|
invoke_mod_ui_callback(mod, "mod UI panel build", [&](ModError* error) {
|
|
return panel.build(mod.context.get(), paneHandle, panel.userData, error);
|
|
});
|
|
}
|
|
|
|
void ui_update_mods_panels(LoadedMod& mod) {
|
|
const auto it = s_modPanels.find(&mod);
|
|
if (it == s_modPanels.end()) {
|
|
return;
|
|
}
|
|
const auto& panel = it->second;
|
|
if (!mod.active || panel.update == nullptr) {
|
|
return;
|
|
}
|
|
invoke_mod_ui_callback(mod, "mod UI panel update", [&](ModError* error) {
|
|
return panel.update(mod.context.get(), panel.userData, error);
|
|
});
|
|
}
|
|
|
|
ModResult ui_pane_add_section(LoadedMod& mod, uint64_t pane, const char* title) {
|
|
auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_section");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
slot->pane->add_section(title);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_pane_add_text(LoadedMod& mod, uint64_t pane, const char* text, uint64_t* outElem) {
|
|
auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_text");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
auto* elem = slot->pane->add_text(text);
|
|
if (outElem != nullptr) {
|
|
auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem);
|
|
elemSlot.elementRml = ui::escape(text);
|
|
elemSlot.hasElementValue = true;
|
|
track_element(*outElem, elemSlot, *elem);
|
|
}
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_pane_add_rml(LoadedMod& mod, uint64_t pane, const char* rml, uint64_t* outElem) {
|
|
auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_rml");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
auto* elem = slot->pane->add_rml(rml);
|
|
if (outElem != nullptr) {
|
|
auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem);
|
|
elemSlot.elementRml = rml;
|
|
elemSlot.hasElementValue = true;
|
|
track_element(*outElem, elemSlot, *elem);
|
|
}
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_pane_add_progress(LoadedMod& mod, uint64_t pane, float value, uint64_t* outElem) {
|
|
auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_progress");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
auto* elem = ui::append(slot->element, "progress");
|
|
elem->SetAttribute("value", value);
|
|
if (outElem != nullptr) {
|
|
auto& elemSlot = alloc_slot(mod, UiSlotKind::Progress, *outElem);
|
|
elemSlot.elementFloat = value;
|
|
elemSlot.hasElementValue = true;
|
|
track_element(*outElem, elemSlot, *elem);
|
|
}
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_pane_add_control(
|
|
LoadedMod& mod, uint64_t pane, const UiControlDesc& desc, uint64_t* outElem) {
|
|
auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_control");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
|
|
ui::ModControlSpec spec;
|
|
spec.label = desc.label;
|
|
spec.helpRml = desc.help_rml != nullptr ? desc.help_rml : "";
|
|
spec.isDisabled = wrap_predicate(mod, desc.is_disabled, desc.user_data, pane);
|
|
spec.isModified = wrap_predicate(mod, desc.is_modified, desc.user_data, pane);
|
|
switch (desc.kind) {
|
|
case UI_CONTROL_BUTTON:
|
|
spec.kind = ui::ModControlSpec::Kind::Button;
|
|
spec.onPressed = [modPtr = &mod, fn = desc.on_pressed, userData = desc.user_data,
|
|
guardHandle = pane] {
|
|
if (!slot_live(guardHandle)) {
|
|
return;
|
|
}
|
|
guarded_call(*modPtr, "control on_pressed callback", [&] {
|
|
fn(modPtr->context.get(), userData);
|
|
});
|
|
};
|
|
break;
|
|
case UI_CONTROL_TOGGLE:
|
|
spec.kind = ui::ModControlSpec::Kind::Toggle;
|
|
break;
|
|
case UI_CONTROL_NUMBER:
|
|
spec.kind = ui::ModControlSpec::Kind::Number;
|
|
if (desc.min != desc.max) {
|
|
spec.min = clamp_to_int(desc.min);
|
|
spec.max = clamp_to_int(desc.max);
|
|
if (spec.max < spec.min) {
|
|
std::swap(spec.min, spec.max);
|
|
}
|
|
}
|
|
spec.step = desc.step < 1 ? 1 : clamp_to_int(desc.step);
|
|
spec.prefix = desc.prefix != nullptr ? desc.prefix : "";
|
|
spec.suffix = desc.suffix != nullptr ? desc.suffix : "";
|
|
break;
|
|
case UI_CONTROL_STRING:
|
|
spec.kind = ui::ModControlSpec::Kind::String;
|
|
spec.maxLength = desc.max_length < 1 ? -1 : desc.max_length;
|
|
break;
|
|
case UI_CONTROL_SELECT:
|
|
spec.kind = ui::ModControlSpec::Kind::Select;
|
|
if (slot->helpPane == nullptr) {
|
|
Log.error("[{}] pane_add_control: SELECT controls need a help pane (mod window tabs)",
|
|
mod.metadata.id);
|
|
return MOD_UNSUPPORTED;
|
|
}
|
|
for (size_t i = 0; i < desc.option_count; ++i) {
|
|
spec.options.emplace_back(desc.options[i]);
|
|
}
|
|
break;
|
|
default:
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (desc.kind != UI_CONTROL_BUTTON) {
|
|
if (desc.binding == UI_BINDING_CONFIG_VAR) {
|
|
if (!wire_config_var_binding(mod, desc, spec)) {
|
|
Log.error("[{}] pane_add_control: config var handle {:#x} is unknown or its type "
|
|
"does not match the control kind",
|
|
mod.metadata.id, desc.config_var);
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
} else {
|
|
wire_callback_binding(mod, desc, spec, pane);
|
|
}
|
|
}
|
|
|
|
// Copy the pane pointers out: allocating the control's slot below may reallocate s_slots
|
|
auto* paneComponent = slot->pane;
|
|
auto* helpPane = slot->helpPane;
|
|
auto* control = ui::build_mod_control(*paneComponent, helpPane, std::move(spec));
|
|
if (control == nullptr) {
|
|
return MOD_UNSUPPORTED;
|
|
}
|
|
if (outElem != nullptr) {
|
|
auto& elemSlot = alloc_slot(mod, UiSlotKind::Control, *outElem);
|
|
track_element(*outElem, elemSlot, *control->root());
|
|
}
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_elem_set_text(LoadedMod& mod, uint64_t elem, const char* text) {
|
|
auto* slot = resolve(mod, elem, UiSlotKind::Text, "elem_set_text");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
const std::string rml = ui::escape(text);
|
|
if (slot->hasElementValue && slot->elementRml == rml) {
|
|
return MOD_OK;
|
|
}
|
|
slot->elementRml = rml;
|
|
slot->hasElementValue = true;
|
|
slot->element->SetInnerRML(slot->elementRml);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_elem_set_rml(LoadedMod& mod, uint64_t elem, const char* rml) {
|
|
auto* slot = resolve(mod, elem, UiSlotKind::Text, "elem_set_rml");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
if (slot->hasElementValue && slot->elementRml == rml) {
|
|
return MOD_OK;
|
|
}
|
|
slot->elementRml = rml;
|
|
slot->hasElementValue = true;
|
|
slot->element->SetInnerRML(rml);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_elem_set_progress(LoadedMod& mod, uint64_t elem, float value) {
|
|
auto* slot = resolve(mod, elem, UiSlotKind::Progress, "elem_set_progress");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
if (slot->hasElementValue && slot->elementFloat == value) {
|
|
return MOD_OK;
|
|
}
|
|
slot->elementFloat = value;
|
|
slot->hasElementValue = true;
|
|
slot->element->SetAttribute("value", value);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_elem_set_class(LoadedMod& mod, uint64_t elem, const char* name, bool active) {
|
|
auto* entry = s_slots.find_owned(elem, mod);
|
|
if (entry == nullptr || entry->value.element == nullptr) {
|
|
Log.error(
|
|
"[{}] elem_set_class: stale or invalid element handle {:#x}", mod.metadata.id, elem);
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
entry->value.element->SetClass(name, active);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_window_push(LoadedMod& mod, const UiWindowDesc& desc, uint64_t& outHandle) {
|
|
outHandle = 0;
|
|
if (!aurora::rmlui::is_initialized()) {
|
|
return MOD_UNAVAILABLE;
|
|
}
|
|
if (desc.rcss != nullptr && desc.rcss[0] != '\0' &&
|
|
Rml::Factory::InstanceStyleSheetString(desc.rcss) == nullptr)
|
|
{
|
|
Log.error("[{}] window_push: failed to parse window RCSS", mod.metadata.id);
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
|
|
uint64_t handle = 0;
|
|
{
|
|
auto& slot = alloc_slot(mod, UiSlotKind::Window, handle);
|
|
slot.onClosed = desc.on_closed;
|
|
slot.onClosedUserData = desc.user_data;
|
|
}
|
|
|
|
ui::ModWindow::Desc windowDesc;
|
|
windowDesc.modId = mod.metadata.id;
|
|
windowDesc.rcss = desc.rcss != nullptr ? desc.rcss : "";
|
|
windowDesc.onDestroyed = [handle] { on_mod_window_destroyed(handle); };
|
|
for (size_t i = 0; i < desc.tab_count; ++i) {
|
|
const UiTabDesc& tab = desc.tabs[i];
|
|
ui::ModWindow::Tab hostTab;
|
|
hostTab.title = tab.title;
|
|
hostTab.build = [modPtr = &mod, handle, build = tab.build, userData = tab.user_data](
|
|
ui::ModWindow&, ui::Pane& left, ui::Pane& right) {
|
|
if (build == nullptr || !slot_live(handle) || !modPtr->active) {
|
|
return;
|
|
}
|
|
const uint64_t leftHandle = wrap_pane(*modPtr, left, &right);
|
|
const uint64_t rightHandle = wrap_pane(*modPtr, right, nullptr);
|
|
invoke_mod_ui_callback(*modPtr, "mod UI tab build", [&](ModError* error) {
|
|
return build(
|
|
modPtr->context.get(), handle, leftHandle, rightHandle, userData, error);
|
|
});
|
|
};
|
|
if (tab.update != nullptr) {
|
|
hostTab.update = [modPtr = &mod, handle, update = tab.update,
|
|
userData = tab.user_data] {
|
|
if (!slot_live(handle) || !modPtr->active) {
|
|
return;
|
|
}
|
|
invoke_mod_ui_callback(*modPtr, "mod UI tab update", [&](ModError* error) {
|
|
return update(modPtr->context.get(), userData, error);
|
|
});
|
|
};
|
|
}
|
|
windowDesc.tabs.push_back(std::move(hostTab));
|
|
}
|
|
|
|
// The first tab builds during construction, which can allocate slots; only
|
|
// re-resolve the window slot afterwards.
|
|
auto window = std::make_unique<ui::ModWindow>(std::move(windowDesc));
|
|
if (auto* slot = slot_from_handle(handle)) {
|
|
slot->document = window.get();
|
|
}
|
|
push_stacked_document(std::move(window));
|
|
outHandle = handle;
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_window_close(LoadedMod& mod, uint64_t handle) {
|
|
auto* slot = resolve(mod, handle, UiSlotKind::Window, "window_close");
|
|
if (slot == nullptr || slot->document == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
slot->document->hide(true);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& outHandle) {
|
|
outHandle = 0;
|
|
if (!aurora::rmlui::is_initialized()) {
|
|
return MOD_UNAVAILABLE;
|
|
}
|
|
uint64_t handle = 0;
|
|
alloc_slot(mod, UiSlotKind::Dialog, handle);
|
|
|
|
const char* defaultIcon = "";
|
|
ui::Modal::Props props;
|
|
switch (desc.variant) {
|
|
case UI_DIALOG_WARNING:
|
|
defaultIcon = "warning";
|
|
break;
|
|
case UI_DIALOG_DANGER:
|
|
props.variant = "danger";
|
|
defaultIcon = "error";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
props.title = ui::escape(desc.title);
|
|
props.bodyRml = desc.body_rml;
|
|
props.icon = desc.icon != nullptr ? desc.icon : defaultIcon;
|
|
props.onDismiss = [modPtr = &mod, handle, fn = desc.on_dismiss, userData = desc.user_data](
|
|
ui::Modal& modal) {
|
|
if (!dialog_open(handle)) {
|
|
return; // already being torn down
|
|
}
|
|
if (fn != nullptr) {
|
|
guarded_call(*modPtr, "dialog on_dismiss callback", [&] {
|
|
fn(modPtr->context.get(), handle, userData);
|
|
});
|
|
}
|
|
if (dialog_open(handle)) {
|
|
static_cast<ModDialog&>(modal).close();
|
|
}
|
|
};
|
|
for (size_t i = 0; i < desc.action_count; ++i) {
|
|
props.actions.push_back(make_dialog_action(mod, handle, desc.actions[i]));
|
|
}
|
|
|
|
auto dialog =
|
|
std::make_unique<ModDialog>(std::move(props), [handle] { on_mod_dialog_destroyed(handle); });
|
|
if (auto* slot = slot_from_handle(handle)) {
|
|
slot->document = dialog.get();
|
|
}
|
|
push_stacked_document(std::move(dialog));
|
|
outHandle = handle;
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_dialog_close(LoadedMod& mod, uint64_t handle) {
|
|
auto* slot = resolve(mod, handle, UiSlotKind::Dialog, "dialog_close");
|
|
if (slot == nullptr || slot->document == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
// Programmatic close: no dismiss notification, no sound
|
|
static_cast<ModDialog*>(slot->document)->close();
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_dialog_set_body(LoadedMod& mod, uint64_t handle, const char* rml) {
|
|
auto* slot = resolve(mod, handle, UiSlotKind::Dialog, "dialog_set_body");
|
|
if (slot == nullptr || slot->document == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
static_cast<ModDialog*>(slot->document)->set_body(rml);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_dialog_set_icon(LoadedMod& mod, uint64_t handle, const char* icon) {
|
|
auto* slot = resolve(mod, handle, UiSlotKind::Dialog, "dialog_set_icon");
|
|
if (slot == nullptr || slot->document == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
static_cast<ModDialog*>(slot->document)->set_icon(icon);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_dialog_add_action(LoadedMod& mod, uint64_t handle, const UiDialogAction& action) {
|
|
auto* slot = resolve(mod, handle, UiSlotKind::Dialog, "dialog_add_action");
|
|
if (slot == nullptr || slot->document == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
static_cast<ModDialog*>(slot->document)->add_action(make_dialog_action(mod, handle, action));
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_register_menu_tab(LoadedMod& mod, const UiMenuTabDesc& desc, uint64_t& outHandle) {
|
|
outHandle = 0;
|
|
for (const auto& [owner, tabs] : s_modMenuTabs) {
|
|
for (const auto& tab : tabs) {
|
|
if (owner != &mod && tab.label == desc.label) {
|
|
Log.warn("[{}] register_menu_tab: label '{}' is already used by [{}]",
|
|
mod.metadata.id, desc.label, owner->metadata.id);
|
|
}
|
|
}
|
|
}
|
|
uint64_t handle = 0;
|
|
alloc_slot(mod, UiSlotKind::MenuTab, handle);
|
|
s_modMenuTabs[&mod].push_back({.handle = handle,
|
|
.label = desc.label,
|
|
.onSelected = desc.on_selected,
|
|
.userData = desc.user_data});
|
|
s_menuTabsDirty = true;
|
|
outHandle = handle;
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_unregister_menu_tab(LoadedMod& mod, uint64_t handle) {
|
|
auto* slot = resolve(mod, handle, UiSlotKind::MenuTab, "unregister_menu_tab");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
const auto it = s_modMenuTabs.find(&mod);
|
|
if (it != s_modMenuTabs.end()) {
|
|
std::erase_if(it->second, [&](const auto& tab) { return tab.handle == handle; });
|
|
if (it->second.empty()) {
|
|
s_modMenuTabs.erase(it);
|
|
}
|
|
}
|
|
s_slots.erase_owned(handle, mod);
|
|
s_menuTabsDirty = true;
|
|
return MOD_OK;
|
|
}
|
|
|
|
std::vector<ModMenuTabEntry> ui_mod_menu_tabs() {
|
|
// The consumer (a MenuBar being constructed) now reflects the current tab
|
|
// set, so a pending rebuild for earlier mutations is moot.
|
|
s_menuTabsDirty = false;
|
|
std::vector<ModMenuTabEntry> entries;
|
|
for (auto& mod : ModLoader::instance().mods()) {
|
|
if (!mod.active) {
|
|
continue;
|
|
}
|
|
const auto it = s_modMenuTabs.find(&mod);
|
|
if (it == s_modMenuTabs.end()) {
|
|
continue;
|
|
}
|
|
for (const auto& tab : it->second) {
|
|
entries.push_back({.label = tab.label,
|
|
.onSelected = [modPtr = &mod, handle = tab.handle, fn = tab.onSelected,
|
|
userData = tab.userData] {
|
|
if (!slot_live(handle) || !modPtr->active) {
|
|
return; // registered by a since-unloaded mod image
|
|
}
|
|
guarded_call(*modPtr, "menu tab on_selected callback", [&] {
|
|
fn(modPtr->context.get(), userData);
|
|
});
|
|
}});
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
void ui_sync_menu_tabs() {
|
|
if (!s_menuTabsDirty) {
|
|
return;
|
|
}
|
|
s_menuTabsDirty = false;
|
|
if (aurora::rmlui::is_initialized()) {
|
|
ui::MenuBar::rebuild();
|
|
}
|
|
}
|
|
|
|
bool ui_any_document_visible() {
|
|
return ui::any_document_visible();
|
|
}
|
|
|
|
ModResult ui_register_styles(
|
|
LoadedMod& mod, uint32_t scope, const char* rcss, uint64_t& outHandle) {
|
|
outHandle = 0;
|
|
ui::DocumentScope docScope;
|
|
switch (scope) {
|
|
case UI_SCOPE_PRELAUNCH:
|
|
docScope = ui::DocumentScope::Prelaunch;
|
|
break;
|
|
case UI_SCOPE_WINDOW:
|
|
docScope = ui::DocumentScope::Window;
|
|
break;
|
|
case UI_SCOPE_MENU_BAR:
|
|
docScope = ui::DocumentScope::MenuBar;
|
|
break;
|
|
case UI_SCOPE_OVERLAY:
|
|
docScope = ui::DocumentScope::Overlay;
|
|
break;
|
|
case UI_SCOPE_TOUCH_CONTROLS:
|
|
docScope = ui::DocumentScope::TouchControls;
|
|
break;
|
|
case UI_SCOPE_GRAPHICS_TUNER:
|
|
docScope = ui::DocumentScope::GraphicsTuner;
|
|
break;
|
|
default:
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
|
|
uint64_t handle = 0;
|
|
auto& slot = alloc_slot(mod, UiSlotKind::Style, handle);
|
|
slot.styleScope = docScope;
|
|
slot.styleId = fmt::format("{}:{:x}", mod.metadata.id, handle);
|
|
if (!ui::register_scoped_styles(docScope, slot.styleId, rcss)) {
|
|
Log.error("[{}] register_styles: failed to parse RCSS", mod.metadata.id);
|
|
s_slots.erase(handle);
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
outHandle = handle;
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_register_styles_file(
|
|
LoadedMod& mod, uint32_t scope, const char* path, uint64_t& outHandle) {
|
|
outHandle = 0;
|
|
if (mod.bundle == nullptr) {
|
|
return MOD_UNAVAILABLE;
|
|
}
|
|
std::vector<u8> data;
|
|
const std::string entry = std::string{"res/"} + path;
|
|
try {
|
|
data = mod.bundle->readFile(entry);
|
|
} catch (const std::runtime_error& e) {
|
|
Log.error("[{}] register_styles_file '{}' failed: {}", mod.metadata.id, entry, e.what());
|
|
return MOD_UNAVAILABLE;
|
|
}
|
|
const std::string rcss{data.begin(), data.end()};
|
|
return ui_register_styles(mod, scope, rcss.c_str(), outHandle);
|
|
}
|
|
|
|
ModResult ui_unregister_styles(LoadedMod& mod, uint64_t handle) {
|
|
auto* slot = resolve(mod, handle, UiSlotKind::Style, "unregister_styles");
|
|
if (slot == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
auto released = s_slots.take_owned(handle, mod);
|
|
ui::unregister_scoped_styles(released->value.styleScope, released->value.styleId);
|
|
return MOD_OK;
|
|
}
|
|
|
|
void ui_remove_mod(LoadedMod& mod) {
|
|
s_modPanels.erase(&mod);
|
|
if (s_modMenuTabs.erase(&mod) != 0) {
|
|
s_menuTabsDirty = true;
|
|
}
|
|
auto entries = s_slots.take_all(mod);
|
|
for (auto& entry : entries) {
|
|
auto& slot = entry.value;
|
|
switch (slot.kind) {
|
|
case UiSlotKind::Window: {
|
|
auto* window = static_cast<ui::ModWindow*>(slot.document);
|
|
if (window != nullptr) {
|
|
window->force_close();
|
|
}
|
|
break;
|
|
}
|
|
case UiSlotKind::Dialog: {
|
|
auto* dialog = static_cast<ModDialog*>(slot.document);
|
|
if (dialog != nullptr) {
|
|
dialog->force_close();
|
|
}
|
|
break;
|
|
}
|
|
case UiSlotKind::Style:
|
|
ui::unregister_scoped_styles(slot.styleScope, slot.styleId);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ModResult ui_get_clipboard_text(LoadedMod& mod, char* buffer, size_t bufferSize, size_t* outLength) {
|
|
if (outLength != nullptr) {
|
|
*outLength = 0;
|
|
}
|
|
|
|
std::string text;
|
|
if (SDL_HasClipboardText()) {
|
|
char* textPtr = SDL_GetClipboardText();
|
|
text = textPtr != nullptr ? textPtr : "";
|
|
SDL_free(textPtr);
|
|
if (text.empty()) {
|
|
return MOD_ERROR;
|
|
}
|
|
}
|
|
|
|
if (outLength != nullptr) {
|
|
*outLength = text.size();
|
|
}
|
|
|
|
if (buffer == nullptr) {
|
|
return MOD_OK;
|
|
}
|
|
if (bufferSize < text.size() + 1) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
|
|
memcpy(buffer, text.c_str(), text.size() + 1);
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_set_clipboard_text(LoadedMod& mod, const char* text) {
|
|
if (!SDL_SetClipboardText(text)) {
|
|
return MOD_ERROR;
|
|
}
|
|
return MOD_OK;
|
|
}
|
|
|
|
} // namespace dusk::mods::svc::ui_impl
|
|
|
|
namespace dusk::mods::svc {
|
|
namespace {
|
|
|
|
// Validation of the tagged control descriptor: required fields per kind/binding. Value
|
|
// translation and cvar wiring live in loader/ui.cpp.
|
|
bool valid_control_desc(const UiControlDesc& desc) {
|
|
if (desc.struct_size < sizeof(UiControlDesc) || desc.label == nullptr) {
|
|
return false;
|
|
}
|
|
switch (desc.kind) {
|
|
case UI_CONTROL_BUTTON:
|
|
return desc.on_pressed != nullptr;
|
|
case UI_CONTROL_TOGGLE:
|
|
case UI_CONTROL_NUMBER:
|
|
case UI_CONTROL_STRING:
|
|
case UI_CONTROL_SELECT:
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
if (desc.kind == UI_CONTROL_SELECT) {
|
|
if (desc.options == nullptr || desc.option_count == 0) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < desc.option_count; ++i) {
|
|
if (desc.options[i] == nullptr) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
switch (desc.binding) {
|
|
case UI_BINDING_CALLBACKS:
|
|
return desc.get != nullptr && desc.set != nullptr;
|
|
case UI_BINDING_CONFIG_VAR:
|
|
return desc.config_var != 0;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ModResult ui_register_mods_panel(ModContext* context, const UiModsPanelDesc* desc) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiModsPanelDesc) ||
|
|
desc->build == nullptr)
|
|
{
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_register_mods_panel(*mod, *desc);
|
|
}
|
|
|
|
ModResult ui_pane_add_section(ModContext* context, UiElementHandle pane, const char* title) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || pane == 0 || title == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_pane_add_section(*mod, pane, title);
|
|
}
|
|
|
|
ModResult ui_pane_add_text(
|
|
ModContext* context, UiElementHandle pane, const char* text, UiElementHandle* outElem) {
|
|
if (outElem != nullptr) {
|
|
*outElem = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || pane == 0 || text == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_pane_add_text(*mod, pane, text, outElem);
|
|
}
|
|
|
|
ModResult ui_pane_add_rml(
|
|
ModContext* context, UiElementHandle pane, const char* rml, UiElementHandle* outElem) {
|
|
if (outElem != nullptr) {
|
|
*outElem = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || pane == 0 || rml == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_pane_add_rml(*mod, pane, rml, outElem);
|
|
}
|
|
|
|
ModResult ui_pane_add_progress(
|
|
ModContext* context, UiElementHandle pane, float value, UiElementHandle* outElem) {
|
|
if (outElem != nullptr) {
|
|
*outElem = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || pane == 0) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_pane_add_progress(*mod, pane, value, outElem);
|
|
}
|
|
|
|
ModResult ui_pane_add_control(ModContext* context, UiElementHandle pane, const UiControlDesc* desc,
|
|
UiElementHandle* outElem) {
|
|
if (outElem != nullptr) {
|
|
*outElem = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || pane == 0 || desc == nullptr || !valid_control_desc(*desc)) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_pane_add_control(*mod, pane, *desc, outElem);
|
|
}
|
|
|
|
ModResult ui_elem_set_text(ModContext* context, UiElementHandle elem, const char* text) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || elem == 0 || text == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_elem_set_text(*mod, elem, text);
|
|
}
|
|
|
|
ModResult ui_elem_set_rml(ModContext* context, UiElementHandle elem, const char* rml) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || elem == 0 || rml == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_elem_set_rml(*mod, elem, rml);
|
|
}
|
|
|
|
ModResult ui_elem_set_progress(ModContext* context, UiElementHandle elem, float value) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || elem == 0) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_elem_set_progress(*mod, elem, value);
|
|
}
|
|
|
|
ModResult ui_elem_set_class(
|
|
ModContext* context, UiElementHandle elem, const char* name, bool active) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || elem == 0 || name == nullptr || name[0] == '\0') {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_elem_set_class(*mod, elem, name, active);
|
|
}
|
|
|
|
ModResult ui_window_push(ModContext* context, const UiWindowDesc* desc, UiWindowHandle* outWindow) {
|
|
if (outWindow != nullptr) {
|
|
*outWindow = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiWindowDesc) ||
|
|
desc->tabs == nullptr || desc->tab_count == 0)
|
|
{
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
for (size_t i = 0; i < desc->tab_count; ++i) {
|
|
const UiTabDesc& tab = desc->tabs[i];
|
|
if (tab.struct_size < sizeof(UiTabDesc) || tab.title == nullptr || tab.build == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
}
|
|
uint64_t handle = 0;
|
|
const auto result = ui_impl::ui_window_push(*mod, *desc, handle);
|
|
if (result == MOD_OK && outWindow != nullptr) {
|
|
*outWindow = handle;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ModResult ui_window_close(ModContext* context, UiWindowHandle window) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || window == 0) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_window_close(*mod, window);
|
|
}
|
|
|
|
ModResult ui_dialog_push(ModContext* context, const UiDialogDesc* desc, UiDialogHandle* outDialog) {
|
|
if (outDialog != nullptr) {
|
|
*outDialog = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiDialogDesc) ||
|
|
desc->title == nullptr || desc->body_rml == nullptr || desc->actions == nullptr ||
|
|
desc->action_count == 0 || desc->variant > UI_DIALOG_DANGER)
|
|
{
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
for (size_t i = 0; i < desc->action_count; ++i) {
|
|
if (desc->actions[i].label == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
}
|
|
uint64_t handle = 0;
|
|
const auto result = ui_impl::ui_dialog_push(*mod, *desc, handle);
|
|
if (result == MOD_OK && outDialog != nullptr) {
|
|
*outDialog = handle;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ModResult ui_dialog_close(ModContext* context, UiDialogHandle dialog) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || dialog == 0) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_dialog_close(*mod, dialog);
|
|
}
|
|
|
|
ModResult ui_is_any_document_visible(ModContext* context, bool* outVisible) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || outVisible == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
*outVisible = ui_impl::ui_any_document_visible();
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_register_styles(
|
|
ModContext* context, UiStyleScope scope, const char* rcss, UiStyleHandle* outStyle) {
|
|
if (outStyle != nullptr) {
|
|
*outStyle = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || rcss == nullptr || scope > UI_SCOPE_GRAPHICS_TUNER) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
uint64_t handle = 0;
|
|
const auto result = ui_impl::ui_register_styles(*mod, scope, rcss, handle);
|
|
if (result == MOD_OK && outStyle != nullptr) {
|
|
*outStyle = handle;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ModResult ui_register_styles_file(
|
|
ModContext* context, UiStyleScope scope, const char* path, UiStyleHandle* outStyle) {
|
|
if (outStyle != nullptr) {
|
|
*outStyle = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || path == nullptr || !is_safe_resource_path(path) ||
|
|
scope > UI_SCOPE_GRAPHICS_TUNER)
|
|
{
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
uint64_t handle = 0;
|
|
const auto result = ui_impl::ui_register_styles_file(*mod, scope, path, handle);
|
|
if (result == MOD_OK && outStyle != nullptr) {
|
|
*outStyle = handle;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ModResult ui_unregister_styles(ModContext* context, UiStyleHandle style) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || style == 0) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_unregister_styles(*mod, style);
|
|
}
|
|
|
|
ModResult ui_register_menu_tab(
|
|
ModContext* context, const UiMenuTabDesc* desc, UiMenuTabHandle* outTab) {
|
|
if (outTab != nullptr) {
|
|
*outTab = 0;
|
|
}
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiMenuTabDesc) ||
|
|
desc->label == nullptr || desc->label[0] == '\0' || desc->on_selected == nullptr)
|
|
{
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
uint64_t handle = 0;
|
|
const auto result = ui_impl::ui_register_menu_tab(*mod, *desc, handle);
|
|
if (result == MOD_OK && outTab != nullptr) {
|
|
*outTab = handle;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ModResult ui_unregister_menu_tab(ModContext* context, UiMenuTabHandle tab) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || tab == 0) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_unregister_menu_tab(*mod, tab);
|
|
}
|
|
|
|
ModResult ui_push_toast(ModContext* context, const UiToastDesc* desc) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiToastDesc) ||
|
|
((desc->title_rml == nullptr || desc->title_rml[0] == '\0') &&
|
|
(desc->body_rml == nullptr || desc->body_rml[0] == '\0')))
|
|
{
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
|
|
constexpr uint32_t kDefaultDurationMs = 5000;
|
|
const uint32_t durationMs = desc->duration_ms == 0 ? kDefaultDurationMs : desc->duration_ms;
|
|
ui::push_toast({
|
|
.type = desc->type != nullptr ? desc->type : "",
|
|
.title = desc->title_rml != nullptr ? desc->title_rml : "",
|
|
.content = desc->body_rml != nullptr ? desc->body_rml : "",
|
|
.duration = std::chrono::milliseconds{durationMs},
|
|
.modId = mod->metadata.id,
|
|
});
|
|
return MOD_OK;
|
|
}
|
|
|
|
ModResult ui_dialog_set_body(ModContext* context, UiDialogHandle dialog, const char* bodyRml) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || dialog == 0 || bodyRml == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_dialog_set_body(*mod, dialog, bodyRml);
|
|
}
|
|
|
|
ModResult ui_dialog_set_icon(ModContext* context, UiDialogHandle dialog, const char* icon) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || dialog == 0 || icon == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_dialog_set_icon(*mod, dialog, icon);
|
|
}
|
|
|
|
ModResult ui_dialog_add_action(
|
|
ModContext* context, UiDialogHandle dialog, const UiDialogAction* action) {
|
|
auto* mod = mod_from_context(context);
|
|
if (mod == nullptr || dialog == 0 || action == nullptr || action->label == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_dialog_add_action(*mod, dialog, *action);
|
|
}
|
|
|
|
ModResult ui_get_clipboard_text(ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength) {
|
|
auto* mod = mod_from_context(ctx);
|
|
if (mod == nullptr || (buffer == nullptr && bufferSize != 0)) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
|
|
return ui_impl::ui_get_clipboard_text(*mod, buffer, bufferSize, outLength);
|
|
}
|
|
|
|
ModResult ui_set_clipboard_text(ModContext* ctx, const char* text) {
|
|
auto* mod = mod_from_context(ctx);
|
|
if (mod == nullptr || text == nullptr) {
|
|
return MOD_INVALID_ARGUMENT;
|
|
}
|
|
return ui_impl::ui_set_clipboard_text(*mod, text);
|
|
}
|
|
|
|
constexpr UiService s_uiService{
|
|
.header = SERVICE_HEADER(UiService, UI_SERVICE_MAJOR, UI_SERVICE_MINOR),
|
|
.register_mods_panel = ui_register_mods_panel,
|
|
.pane_add_section = ui_pane_add_section,
|
|
.pane_add_text = ui_pane_add_text,
|
|
.pane_add_rml = ui_pane_add_rml,
|
|
.pane_add_progress = ui_pane_add_progress,
|
|
.pane_add_control = ui_pane_add_control,
|
|
.elem_set_text = ui_elem_set_text,
|
|
.elem_set_rml = ui_elem_set_rml,
|
|
.elem_set_progress = ui_elem_set_progress,
|
|
.elem_set_class = ui_elem_set_class,
|
|
.window_push = ui_window_push,
|
|
.window_close = ui_window_close,
|
|
.dialog_push = ui_dialog_push,
|
|
.dialog_close = ui_dialog_close,
|
|
.dialog_set_body = ui_dialog_set_body,
|
|
.dialog_set_icon = ui_dialog_set_icon,
|
|
.dialog_add_action = ui_dialog_add_action,
|
|
.is_any_document_visible = ui_is_any_document_visible,
|
|
.register_styles = ui_register_styles,
|
|
.register_styles_file = ui_register_styles_file,
|
|
.unregister_styles = ui_unregister_styles,
|
|
.register_menu_tab = ui_register_menu_tab,
|
|
.unregister_menu_tab = ui_unregister_menu_tab,
|
|
.push_toast = ui_push_toast,
|
|
.get_clipboard_text = ui_get_clipboard_text,
|
|
.set_clipboard_text = ui_set_clipboard_text,
|
|
};
|
|
|
|
} // namespace
|
|
|
|
void ui_build_mods_panels(LoadedMod& mod, ui::Pane& pane) {
|
|
ui_impl::ui_build_mods_panels(mod, pane);
|
|
}
|
|
|
|
void ui_update_mods_panels(LoadedMod& mod) {
|
|
ui_impl::ui_update_mods_panels(mod);
|
|
}
|
|
|
|
std::vector<ModMenuTabEntry> ui_mod_menu_tabs() {
|
|
return ui_impl::ui_mod_menu_tabs();
|
|
}
|
|
|
|
constinit const ServiceModule g_uiModule{
|
|
.id = UI_SERVICE_ID,
|
|
.majorVersion = UI_SERVICE_MAJOR,
|
|
.minorVersion = UI_SERVICE_MINOR,
|
|
.service = &s_uiService,
|
|
.modDetached = ui_impl::ui_remove_mod,
|
|
.frameEnd = ui_impl::ui_sync_menu_tabs,
|
|
};
|
|
|
|
} // namespace dusk::mods::svc
|