mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-10 21:00:55 -04:00
285 lines
13 KiB
C++
285 lines
13 KiB
C++
#pragma once
|
|
|
|
#include "mods/api.h"
|
|
#include "mods/svc/config.h"
|
|
|
|
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
|
|
#define UI_SERVICE_MAJOR 1u
|
|
#define UI_SERVICE_MINOR 0u
|
|
|
|
/*
|
|
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, scoped
|
|
* RCSS stylesheets and menu bar tabs.
|
|
*
|
|
* All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI
|
|
* callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with
|
|
* MOD_INVALID_ARGUMENT. Element handles die with the content that owns them: a panel or tab rebuild
|
|
* destroys the previous build's elements, so re-acquire handles inside the build callback rather
|
|
* than caching them. Strings are UTF-8 and, in both directions, only valid for the duration of the
|
|
* call.
|
|
*/
|
|
|
|
/* 0 is never a valid handle. */
|
|
typedef uint64_t UiWindowHandle;
|
|
typedef uint64_t UiDialogHandle;
|
|
typedef uint64_t UiElementHandle;
|
|
typedef uint64_t UiStyleHandle;
|
|
typedef uint64_t UiMenuTabHandle;
|
|
|
|
typedef enum UiStyleScope {
|
|
UI_SCOPE_PRELAUNCH = 0, /* the pre-launch menu */
|
|
UI_SCOPE_WINDOW = 1, /* every tabbed/small window, host and mod alike */
|
|
UI_SCOPE_MENU_BAR = 2, /* the in-game menu bar */
|
|
UI_SCOPE_OVERLAY = 3, /* the passive overlay (toasts, FPS counter, timers) */
|
|
UI_SCOPE_TOUCH_CONTROLS = 4, /* touch controls and their editor */
|
|
UI_SCOPE_GRAPHICS_TUNER = 5, /* the graphics tuner overlay window */
|
|
} UiStyleScope;
|
|
|
|
typedef enum UiDialogVariant {
|
|
UI_DIALOG_NORMAL = 0,
|
|
UI_DIALOG_WARNING = 1, /* warning icon by default */
|
|
UI_DIALOG_DANGER = 2, /* red styling, error icon by default */
|
|
} UiDialogVariant;
|
|
|
|
typedef enum UiControlKind {
|
|
UI_CONTROL_BUTTON = 0, /* action button (on_pressed) */
|
|
UI_CONTROL_TOGGLE = 1, /* boolean on/off */
|
|
UI_CONTROL_NUMBER = 2, /* integer stepper with min/max/step */
|
|
UI_CONTROL_STRING = 3, /* text input */
|
|
UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */
|
|
} UiControlKind;
|
|
|
|
typedef enum UiControlBinding {
|
|
/* Values flow through the `get`/`set` callbacks. Getters are polled every frame while the
|
|
control is visible and must be cheap. */
|
|
UI_BINDING_CALLBACKS = 0,
|
|
/* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod)
|
|
* directly: persistence, change notifications and the modified indicator (value != default) are
|
|
* wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and
|
|
* SELECT = int, STRING = string. Float vars are not bindable; use callbacks. */
|
|
UI_BINDING_CONFIG_VAR = 1,
|
|
} UiControlBinding;
|
|
|
|
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
|
|
* reads string_value. string_value passed to a setter is only valid during the call; a getter
|
|
* should point it at storage owned by the mod (e.g. a static buffer) that stays valid until the
|
|
* next call into the mod — the host copies it right after the getter returns. */
|
|
typedef struct UiControlValue {
|
|
uint32_t struct_size;
|
|
bool bool_value;
|
|
int64_t int_value;
|
|
const char* string_value;
|
|
} UiControlValue;
|
|
|
|
#define UI_CONTROL_VALUE_INIT {sizeof(UiControlValue), false, 0, NULL}
|
|
|
|
typedef void (*UiControlGetFn)(ModContext* ctx, void* user_data, UiControlValue* out_value);
|
|
typedef void (*UiControlSetFn)(ModContext* ctx, void* user_data, const UiControlValue* value);
|
|
/* Polled every frame while the control is visible. */
|
|
typedef bool (*UiPredicateFn)(ModContext* ctx, void* user_data);
|
|
typedef void (*UiPressedFn)(ModContext* ctx, void* user_data);
|
|
|
|
typedef struct UiControlDesc {
|
|
uint32_t struct_size;
|
|
UiControlKind kind;
|
|
/* Row label (plain text). Required. */
|
|
const char* label;
|
|
/* Optional RML shown as contextual help when the control is focused or hovered. Only rendered
|
|
* where a help pane exists (mod window tabs). */
|
|
const char* help_rml;
|
|
UiControlBinding binding; /* ignored for BUTTON */
|
|
ConfigVarHandle config_var; /* UI_BINDING_CONFIG_VAR */
|
|
UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
|
|
UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
|
|
UiPressedFn on_pressed; /* BUTTON only. Required for BUTTON. */
|
|
UiPredicateFn is_disabled; /* optional */
|
|
/* Optional override for the modified indicator. CONFIG_VAR controls derive it from value !=
|
|
* default when this is NULL. */
|
|
UiPredicateFn is_modified;
|
|
/* Passed to every callback above. */
|
|
void* user_data;
|
|
/* NUMBER: inclusive clamp range and step. min == max means the defaults (0 .. INT32_MAX); step
|
|
* < 1 means 1. */
|
|
int64_t min;
|
|
int64_t max;
|
|
int64_t step;
|
|
const char* prefix; /* NUMBER: optional text before the value */
|
|
const char* suffix; /* NUMBER: optional text after the value */
|
|
/* SELECT: option labels (plain text). Required for SELECT. SELECT controls
|
|
* present their options in the help pane, so they are only available where
|
|
* one exists (mod window tabs); MOD_UNSUPPORTED elsewhere. */
|
|
const char* const* options;
|
|
size_t option_count;
|
|
int32_t max_length; /* STRING: maximum input length; < 1 means unlimited */
|
|
} UiControlDesc;
|
|
|
|
#define UI_CONTROL_DESC_INIT \
|
|
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
|
|
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0}
|
|
|
|
/* Build the panel contents. `panel` accepts the pane_add_* functions; it and
|
|
* every element created in it are destroyed (handles invalidated) whenever the
|
|
* panel is rebuilt, e.g. on tab switches. A non-MOD_OK result fails the mod. */
|
|
typedef ModResult (*UiPanelBuildFn)(
|
|
ModContext* ctx, UiElementHandle panel, void* user_data, ModError* out_error);
|
|
/* Called every frame while the panel is the visible tab. */
|
|
typedef ModResult (*UiPanelUpdateFn)(ModContext* ctx, void* user_data, ModError* out_error);
|
|
|
|
/* A panel rendered inside the calling mod's tab of the host Mods window. */
|
|
typedef struct UiModsPanelDesc {
|
|
uint32_t struct_size;
|
|
UiPanelBuildFn build; /* required */
|
|
UiPanelUpdateFn update;
|
|
void* user_data;
|
|
} UiModsPanelDesc;
|
|
|
|
#define UI_MODS_PANEL_DESC_INIT {sizeof(UiModsPanelDesc), NULL, NULL, NULL}
|
|
|
|
/* Build one tab of a mod window. `left_pane` is the interactive column,
|
|
* `right_pane` shows contextual help (controls' help_rml and SELECT options
|
|
* render there). Rebuilt on every tab activation. */
|
|
typedef ModResult (*UiTabBuildFn)(ModContext* ctx, UiWindowHandle window, UiElementHandle left_pane,
|
|
UiElementHandle right_pane, void* user_data, ModError* out_error);
|
|
|
|
typedef struct UiTabDesc {
|
|
uint32_t struct_size;
|
|
const char* title; /* required */
|
|
UiTabBuildFn build; /* required */
|
|
UiPanelUpdateFn update;
|
|
void* user_data;
|
|
} UiTabDesc;
|
|
|
|
#define UI_TAB_DESC_INIT {sizeof(UiTabDesc), NULL, NULL, NULL, NULL}
|
|
|
|
typedef void (*UiWindowClosedFn)(ModContext* ctx, UiWindowHandle window, void* user_data);
|
|
|
|
typedef struct UiWindowDesc {
|
|
uint32_t struct_size;
|
|
const UiTabDesc* tabs; /* at least one */
|
|
size_t tab_count;
|
|
/* Optional RCSS applied to this window's document only (on top of any
|
|
* UI_SCOPE_WINDOW sheets, which apply automatically). */
|
|
const char* rcss;
|
|
/* Fired when the window is destroyed by user close or window_close. Not
|
|
* fired during owning-mod teardown/shutdown. The handle is already invalid. */
|
|
UiWindowClosedFn on_closed;
|
|
void* user_data;
|
|
} UiWindowDesc;
|
|
|
|
#define UI_WINDOW_DESC_INIT {sizeof(UiWindowDesc), NULL, 0u, NULL, NULL, NULL}
|
|
|
|
typedef void (*UiDialogActionFn)(ModContext* ctx, UiDialogHandle dialog, void* user_data);
|
|
|
|
/* Note: array element without struct_size; a future change requires appending
|
|
* a v2 desc struct rather than growing this one. */
|
|
typedef struct UiDialogAction {
|
|
const char* label; /* required */
|
|
UiDialogActionFn on_pressed;
|
|
void* user_data;
|
|
bool keep_open; /* false = the dialog closes after on_pressed returns */
|
|
} UiDialogAction;
|
|
|
|
typedef struct UiDialogDesc {
|
|
uint32_t struct_size;
|
|
const char* title; /* plain text; required */
|
|
const char* body_rml; /* RML body; required */
|
|
UiDialogVariant variant;
|
|
/* Optional icon name overriding the variant default: "warning", "error",
|
|
* "question-mark", "verifying", "celebration". */
|
|
const char* icon;
|
|
const UiDialogAction* actions; /* at least one */
|
|
size_t action_count;
|
|
/* Fired on cancel (B/Escape) before the dialog closes; the dialog always
|
|
* closes on dismiss. */
|
|
UiDialogActionFn on_dismiss;
|
|
void* user_data; /* passed to on_dismiss */
|
|
} UiDialogDesc;
|
|
|
|
#define UI_DIALOG_DESC_INIT \
|
|
{sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL}
|
|
|
|
/* A tab added to the in-game menu bar. */
|
|
typedef struct UiMenuTabDesc {
|
|
uint32_t struct_size;
|
|
const char* label; /* plain text; required */
|
|
UiPressedFn on_selected; /* fired when the user activates the tab; required */
|
|
void* user_data;
|
|
} UiMenuTabDesc;
|
|
|
|
#define UI_MENU_TAB_DESC_INIT {sizeof(UiMenuTabDesc), NULL, NULL, NULL}
|
|
|
|
typedef struct UiService {
|
|
ServiceHeader header;
|
|
|
|
/* Register or replace the panel shown in the calling mod's Mods-window tab. */
|
|
ModResult (*register_mods_panel)(ModContext* ctx, const UiModsPanelDesc* desc);
|
|
|
|
/* Content builders. `pane` is a panel or tab pane handle; out_elem (where
|
|
* present, optional) receives a handle for later elem_set_* updates. */
|
|
ModResult (*pane_add_section)(ModContext* ctx, UiElementHandle pane, const char* title);
|
|
ModResult (*pane_add_text)(
|
|
ModContext* ctx, UiElementHandle pane, const char* text, UiElementHandle* out_elem);
|
|
ModResult (*pane_add_rml)(
|
|
ModContext* ctx, UiElementHandle pane, const char* rml, UiElementHandle* out_elem);
|
|
ModResult (*pane_add_progress)(
|
|
ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem);
|
|
ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc,
|
|
UiElementHandle* out_elem);
|
|
|
|
/* Element updates. The handle kind must match the setter (text/rml on text
|
|
* rows, progress on progress bars). */
|
|
ModResult (*elem_set_text)(ModContext* ctx, UiElementHandle elem, const char* text);
|
|
ModResult (*elem_set_rml)(ModContext* ctx, UiElementHandle elem, const char* rml);
|
|
ModResult (*elem_set_progress)(ModContext* ctx, UiElementHandle elem, float value);
|
|
/* Set or clear an RCSS class on any element handle (rows, progress bars,
|
|
* controls, panes), for styling via scoped or window RCSS. */
|
|
ModResult (*elem_set_class)(
|
|
ModContext* ctx, UiElementHandle elem, const char* name, bool active);
|
|
|
|
/* Push a tabbed two-pane window onto the document stack and show it. */
|
|
ModResult (*window_push)(ModContext* ctx, const UiWindowDesc* desc, UiWindowHandle* out_window);
|
|
ModResult (*window_close)(ModContext* ctx, UiWindowHandle window);
|
|
|
|
/* Push a modal dialog onto the document stack and show it. */
|
|
ModResult (*dialog_push)(ModContext* ctx, const UiDialogDesc* desc, UiDialogHandle* out_dialog);
|
|
ModResult (*dialog_close)(ModContext* ctx, UiDialogHandle dialog);
|
|
ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml);
|
|
/* Replace the dialog icon ("" removes it; names as in UiDialogDesc.icon). */
|
|
ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon);
|
|
/* Append one action button (same callback rules as at push). */
|
|
ModResult (*dialog_add_action)(
|
|
ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action);
|
|
|
|
/* Whether any focus-stack document is currently visible (visible documents block gamepad
|
|
* input). */
|
|
ModResult (*is_any_document_visible)(ModContext* ctx, bool* out_visible);
|
|
|
|
/* Register an RCSS sheet applied to every document of `scope`, now and in the future, until
|
|
* unregistered or the calling mod is torn down. Sheets apply in registration order after the
|
|
* host styles. Fails with MOD_INVALID_ARGUMENT if the RCSS does not parse. */
|
|
ModResult (*register_styles)(
|
|
ModContext* ctx, UiStyleScope scope, const char* rcss, UiStyleHandle* out_style);
|
|
/* Like register_styles, but reads the RCSS from the mod bundle's res/ directory (same path
|
|
* rules as ResourceService::load). MOD_UNAVAILABLE if the file does not exist. */
|
|
ModResult (*register_styles_file)(
|
|
ModContext* ctx, UiStyleScope scope, const char* path, UiStyleHandle* out_style);
|
|
ModResult (*unregister_styles)(ModContext* ctx, UiStyleHandle style);
|
|
|
|
/* Add a tab to the in-game menu bar, on_selected runs with the menu open; pushing a window from
|
|
* it stacks the window over the menu like the host tabs do. The tab is removed when
|
|
* unregistered or the mod is torn down. */
|
|
ModResult (*register_menu_tab)(
|
|
ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab);
|
|
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
|
|
} UiService;
|
|
|
|
#ifdef __cplusplus
|
|
#include "mods/service.hpp"
|
|
|
|
template <>
|
|
struct dusk::mods::ServiceTraits<UiService> {
|
|
static constexpr const char* id = UI_SERVICE_ID;
|
|
static constexpr uint16_t major_version = UI_SERVICE_MAJOR;
|
|
};
|
|
#endif
|