mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-10 21:00:55 -04:00
Mod API: UI service (#2189)
This commit is contained in:
@@ -316,6 +316,99 @@ Change callbacks fire on the game thread whenever the value changes at runtime (
|
||||
Writes that store the same value are silent. Values applied from `config.json` or `--cvar` at registration do
|
||||
**not** fire callbacks; read the value after `register_var` for the starting state.
|
||||
|
||||
### UiService (`mods/svc/ui.h`)
|
||||
|
||||
Integrate seamlessly with Dusklight's UI system: add controls and buttons to your mod's detail pane in the Mods window,
|
||||
create custom windows and modal dialogs, apply custom RCSS stylesheets (anywhere!), and add menu bar tabs.
|
||||
|
||||
**Mod panel:** Registers or replaces the panel rendered in your mod's detail pane; `build` runs every time the detail
|
||||
content is rebuilt, and `update` runs every frame while that mod is selected. While your mod is selected, the detail
|
||||
pane carries your mod's id as a `mod-id` attribute (like custom window roots), so scoped RCSS can target it (e.g.
|
||||
`[mod-id="com.example.mod"]`).
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(UiService, svc_ui);
|
||||
|
||||
UiElementHandle statusText = 0;
|
||||
|
||||
ModResult build(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
svc_ui->pane_add_section(mod_ctx, panel, "Status");
|
||||
svc_ui->pane_add_text(mod_ctx, panel, "starting...", &statusText);
|
||||
svc_ui->pane_add_progress(mod_ctx, panel, 0.5f, nullptr);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult update(ModContext*, void*, ModError*) {
|
||||
svc_ui->elem_set_text(mod_ctx, statusText, "running");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
UiModsPanelDesc panel = UI_MODS_PANEL_DESC_INIT;
|
||||
panel.build = build;
|
||||
panel.update = update;
|
||||
svc_ui->register_mods_panel(mod_ctx, &panel);
|
||||
```
|
||||
|
||||
Element setters must match the element kind: `elem_set_text`/`elem_set_rml` on text rows, and `elem_set_progress` on
|
||||
progress bars. `elem_set_class` sets or clears an RCSS class on any element handle, for styling via scoped or
|
||||
per-window RCSS. A non-`MOD_OK` result from `build`/`update` fails your mod, as do exceptions thrown from any UI
|
||||
callback.
|
||||
|
||||
**Controls:** `pane_add_control` adds an input row described by a `UiControlDesc`: `UI_CONTROL_BUTTON`,
|
||||
`UI_CONTROL_TOGGLE`, `UI_CONTROL_NUMBER`, `UI_CONTROL_STRING`, or `UI_CONTROL_SELECT`. Values bind with callbacks or
|
||||
directly to a config var.
|
||||
|
||||
```cpp
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_TOGGLE;
|
||||
control.label = "Enable rainbows";
|
||||
control.help_rml = "Shown in the help pane while focused.";
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = myBoolVar; // from svc_config->register_var
|
||||
svc_ui->pane_add_control(mod_ctx, leftPane, &control, nullptr);
|
||||
```
|
||||
|
||||
`UI_BINDING_CONFIG_VAR` wires persistence, change notifications, and the modified indicator automatically. The var
|
||||
type must match the control: `TOGGLE` = bool, `NUMBER` and `SELECT` = int, `STRING` = string. Float vars are not
|
||||
bindable; use callbacks and convert. `help_rml` and `SELECT` option lists render in a help pane, so `SELECT` controls
|
||||
are only available inside window tabs.
|
||||
|
||||
**Windows:** `window_push` pushes a tabbed two-pane window onto the document stack and shows it. Each tab's `build`
|
||||
receives the window handle plus fresh left and right pane handles on every activation. The optional per-tab `update`
|
||||
runs each frame while that tab is active. `on_closed` fires when the window is destroyed. `desc.rcss` optionally styles
|
||||
that window's document only; custom windows carry the owning mod's id as a `mod-id` attribute on the window root, so
|
||||
scoped RCSS can target your specific mod's windows (e.g. `window[mod-id="com.example.mod"]`).
|
||||
|
||||
```cpp
|
||||
UiTabDesc tabs[1] = {UI_TAB_DESC_INIT};
|
||||
tabs[0].title = "Options";
|
||||
tabs[0].build = build_options_tab;
|
||||
|
||||
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
|
||||
desc.tabs = tabs;
|
||||
desc.tab_count = 1;
|
||||
desc.on_closed = options_window_closed;
|
||||
UiWindowHandle window = 0;
|
||||
svc_ui->window_push(mod_ctx, &desc, &window);
|
||||
```
|
||||
|
||||
**Dialogs:** `dialog_push` shows a modal dialog. `variant` picks the style, `icon` optionally overrides the variant's
|
||||
default icon, and actions become buttons. After an action's `on_pressed` returns, the dialog closes unless the action
|
||||
sets `keep_open`. A `keep_open` action can close it later (or immediately) with `dialog_close`. Cancel fires
|
||||
`on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live
|
||||
dialog.
|
||||
|
||||
**Menu bar tabs:** `register_menu_tab` adds a tab to the in-game menu bar. `on_selected` fires when the user activates
|
||||
the tab: typically you'd push a window from it. The tab is removed by `unregister_menu_tab`, or automatically when the
|
||||
mod is disabled.
|
||||
|
||||
**Custom styles:** `register_styles(scope, rcss, &handle)` applies an RCSS stylesheet to every document of a scope:
|
||||
existing documents restyle immediately, and future ones pick it up when created. `register_styles_file(scope, path,
|
||||
&handle)` reads the sheet from your bundle's `res/` directory. Scopes are `UI_SCOPE_PRELAUNCH`, `UI_SCOPE_WINDOW`,
|
||||
`UI_SCOPE_MENU_BAR`, `UI_SCOPE_OVERLAY`, `UI_SCOPE_TOUCH_CONTROLS`, and `UI_SCOPE_GRAPHICS_TUNER`. Sheets apply after
|
||||
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
|
||||
unless changing host UI is intentional.
|
||||
|
||||
---
|
||||
|
||||
## Hooking Game Functions
|
||||
|
||||
@@ -1500,6 +1500,8 @@ set(DUSK_FILES
|
||||
src/dusk/ui/logs_window.hpp
|
||||
src/dusk/ui/mod_texture_provider.cpp
|
||||
src/dusk/ui/mod_texture_provider.hpp
|
||||
src/dusk/ui/mod_window.cpp
|
||||
src/dusk/ui/mod_window.hpp
|
||||
src/dusk/ui/modal.cpp
|
||||
src/dusk/ui/modal.hpp
|
||||
src/dusk/ui/nav_types.hpp
|
||||
@@ -1569,6 +1571,8 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/overlay.cpp
|
||||
src/dusk/mods/svc/resource.cpp
|
||||
src/dusk/mods/svc/texture.cpp
|
||||
src/dusk/mods/svc/ui.cpp
|
||||
src/dusk/mods/svc/ui.hpp
|
||||
src/dusk/mods/svc/registry.cpp
|
||||
src/dusk/mods/svc/registry.hpp
|
||||
src/dusk/discord.cpp
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
#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
|
||||
@@ -438,6 +438,11 @@ void config_mark_dirty() {
|
||||
s_dirty = true;
|
||||
}
|
||||
|
||||
config::ConfigVarBase* config_find_var(
|
||||
LoadedMod& mod, const ConfigVarHandle handle, const uint32_t expectedType) {
|
||||
return find_var(mod, handle, expectedType);
|
||||
}
|
||||
|
||||
constinit const ServiceModule g_configModule{
|
||||
.id = CONFIG_SERVICE_ID,
|
||||
.majorVersion = CONFIG_SERVICE_MAJOR,
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "mods/svc/config.h"
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
} // namespace dusk::mods
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
// Returns a config var owned by `mod` when the handle exists and its type matches.
|
||||
config::ConfigVarBase* config_find_var(
|
||||
LoadedMod& mod, ConfigVarHandle handle, uint32_t expectedType);
|
||||
|
||||
// Marks the mods' config keys dirty for the config service's debounced save. The loader
|
||||
// calls this for the enabled cvars it owns.
|
||||
void config_mark_dirty();
|
||||
|
||||
@@ -206,6 +206,7 @@ void ModLoader::init_services() {
|
||||
&svc::g_overlayModule,
|
||||
&svc::g_textureModule,
|
||||
&svc::g_configModule,
|
||||
&svc::g_uiModule,
|
||||
&svc::g_gameModule,
|
||||
})
|
||||
{
|
||||
|
||||
@@ -68,6 +68,7 @@ extern const ServiceModule g_hookModule;
|
||||
extern const ServiceModule g_overlayModule;
|
||||
extern const ServiceModule g_textureModule;
|
||||
extern const ServiceModule g_configModule;
|
||||
extern const ServiceModule g_uiModule;
|
||||
extern const ServiceModule g_gameModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
class Pane;
|
||||
} // namespace dusk::ui
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
void ui_build_mods_panels(LoadedMod& mod, ui::Pane& pane);
|
||||
void ui_update_mods_panels(LoadedMod& mod);
|
||||
|
||||
struct ModMenuTabEntry {
|
||||
std::string label;
|
||||
std::function<void()> onSelected;
|
||||
};
|
||||
|
||||
std::vector<ModMenuTabEntry> ui_mod_menu_tabs();
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -243,11 +243,7 @@ int rumble_raw_to_percent(u16 raw) {
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerConfigWindow::ControllerConfigWindow(bool prelaunch) {
|
||||
if (prelaunch) {
|
||||
mSuppressNavFallback = true;
|
||||
}
|
||||
|
||||
ControllerConfigWindow::ControllerConfigWindow() {
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
[this](Rml::Event& event) {
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace dusk::ui {
|
||||
|
||||
class ControllerConfigWindow : public Window {
|
||||
public:
|
||||
ControllerConfigWindow(bool prelaunch);
|
||||
ControllerConfigWindow();
|
||||
|
||||
void update() override;
|
||||
void hide(bool close) override;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
@@ -18,8 +20,16 @@ Rml::ElementDocument* load_document(const Rml::String& source) {
|
||||
|
||||
} // namespace
|
||||
|
||||
Document::Document(const Rml::String& source, bool passive)
|
||||
: mDocument(load_document(source)), mPassive(passive) {
|
||||
Document::Document(const Rml::String& source, bool passive, DocumentScope scope)
|
||||
: mDocument(load_document(source)), mScope(scope), mPassive(passive) {
|
||||
if (mDocument != nullptr) {
|
||||
if (const auto* base = mDocument->GetStyleSheetContainer()) {
|
||||
// Clone a pristine snapshot to rebuild from on every restyle
|
||||
mBaseStyleSheets = base->CombineStyleSheetContainer(Rml::StyleSheetContainer{});
|
||||
}
|
||||
apply_scoped_styles(*this);
|
||||
}
|
||||
|
||||
// Block events while hidden (except for Menu command); play nav sounds when visible
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
@@ -91,6 +101,51 @@ bool Document::focus() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Document::set_document_styles(const Rml::String& rcss) {
|
||||
if (rcss.empty()) {
|
||||
mDocumentStyleSheets = nullptr;
|
||||
} else {
|
||||
auto sheet = Rml::Factory::InstanceStyleSheetString(rcss);
|
||||
if (sheet == nullptr) {
|
||||
return false;
|
||||
}
|
||||
mDocumentStyleSheets = std::move(sheet);
|
||||
}
|
||||
apply_scoped_styles(*this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Document::restyle(std::span<const Rml::StyleSheetContainer* const> sheets) {
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
const bool wantsExtra =
|
||||
mDocumentStyleSheets != nullptr ||
|
||||
std::ranges::any_of(sheets, [](const auto* sheet) { return sheet != nullptr; });
|
||||
// Nothing to add
|
||||
if (!wantsExtra && !mRestyled) {
|
||||
return;
|
||||
}
|
||||
auto combined = mBaseStyleSheets;
|
||||
const auto combine = [&combined](const Rml::StyleSheetContainer& sheet) {
|
||||
if (combined != nullptr) {
|
||||
combined = combined->CombineStyleSheetContainer(sheet);
|
||||
} else {
|
||||
combined = sheet.CombineStyleSheetContainer(Rml::StyleSheetContainer{});
|
||||
}
|
||||
};
|
||||
for (const auto* sheet : sheets) {
|
||||
if (sheet != nullptr) {
|
||||
combine(*sheet);
|
||||
}
|
||||
}
|
||||
if (mDocumentStyleSheets != nullptr) {
|
||||
combine(*mDocumentStyleSheets);
|
||||
}
|
||||
mDocument->SetStyleSheetContainer(std::move(combined));
|
||||
mRestyled = wantsExtra;
|
||||
}
|
||||
|
||||
void Document::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
@@ -139,6 +194,9 @@ bool Document::handle_nav_event(Rml::Event& event) {
|
||||
|
||||
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Menu) {
|
||||
if (game_obscured_below(*this)) {
|
||||
return true;
|
||||
}
|
||||
mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen);
|
||||
toggle();
|
||||
return true;
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
#include "component.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <span>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Document {
|
||||
public:
|
||||
explicit Document(const Rml::String& source, bool passive = false);
|
||||
explicit Document(
|
||||
const Rml::String& source, bool passive = false, DocumentScope scope = DocumentScope::None);
|
||||
virtual ~Document();
|
||||
|
||||
Document(const Document&) = delete;
|
||||
@@ -19,7 +22,22 @@ public:
|
||||
virtual bool focus();
|
||||
virtual bool visible() const;
|
||||
virtual bool active() const;
|
||||
virtual bool obscures_game() const { return false; }
|
||||
virtual void cover() {
|
||||
mWasVisible = visible();
|
||||
hide(false);
|
||||
}
|
||||
virtual void uncover() {
|
||||
if (mWasVisible) {
|
||||
show();
|
||||
} else {
|
||||
focus();
|
||||
}
|
||||
}
|
||||
|
||||
DocumentScope scope() const { return mScope; }
|
||||
bool set_document_styles(const Rml::String& rcss);
|
||||
void restyle(std::span<const Rml::StyleSheetContainer* const> sheets);
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
void listen(Rml::Element* element, const Rml::String& event,
|
||||
@@ -40,11 +58,11 @@ public:
|
||||
}
|
||||
void push(std::unique_ptr<Document> document) {
|
||||
push_document(std::move(document));
|
||||
hide(false);
|
||||
cover();
|
||||
}
|
||||
void pop(bool show = true) {
|
||||
void pop() {
|
||||
hide(true);
|
||||
focus_top_document(show);
|
||||
uncover_top_document();
|
||||
}
|
||||
|
||||
bool closed() const { return mClosed; }
|
||||
@@ -55,10 +73,15 @@ protected:
|
||||
virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd);
|
||||
|
||||
Rml::ElementDocument* mDocument;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
std::vector<std::unique_ptr<ScopedEventListener>> mListeners;
|
||||
Rml::SharedPtr<Rml::StyleSheetContainer> mBaseStyleSheets;
|
||||
Rml::SharedPtr<Rml::StyleSheetContainer> mDocumentStyleSheets;
|
||||
DocumentScope mScope = DocumentScope::None;
|
||||
bool mPendingClose = false;
|
||||
bool mClosed = false;
|
||||
bool mPassive = false;
|
||||
bool mRestyled = false;
|
||||
bool mWasVisible = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -244,9 +244,9 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch)
|
||||
: Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
|
||||
mValueMax(props.valueMax), mDefaultValue(props.defaultValue), mPrelaunch(prelaunch) {
|
||||
GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
|
||||
: Document(kDocumentSource, false, DocumentScope::GraphicsTuner), mOption(props.option),
|
||||
mValueMin(props.valueMin), mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -338,7 +338,7 @@ bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return mPrelaunch ? false : Document::handle_nav_command(event, cmd);
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
void GraphicsTuner::reset_default() {
|
||||
|
||||
@@ -63,7 +63,7 @@ struct GraphicsTunerProps {
|
||||
|
||||
class GraphicsTuner : public Document {
|
||||
public:
|
||||
explicit GraphicsTuner(GraphicsTunerProps props, bool prelaunch);
|
||||
explicit GraphicsTuner(GraphicsTunerProps props);
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
@@ -92,7 +92,6 @@ private:
|
||||
std::vector<std::unique_ptr<Component> > mComponents;
|
||||
SteppedCarousel* mCarousel;
|
||||
Rml::Element* mRoot;
|
||||
bool mPrelaunch;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/svc/ui.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/speedrun.h"
|
||||
#include "editor.hpp"
|
||||
@@ -42,7 +43,9 @@ const Rml::String kDocumentSource = R"RML(
|
||||
|
||||
}
|
||||
|
||||
MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
|
||||
MenuBar::MenuBar()
|
||||
: Document(kDocumentSource, false, DocumentScope::MenuBar),
|
||||
mRoot(mDocument->GetElementById("popup")) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
|
||||
.onClose =
|
||||
[this] {
|
||||
@@ -60,6 +63,9 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
|
||||
|
||||
mTabBar->add_tab("Achievements", [this] { push(std::make_unique<AchievementsWindow>()); });
|
||||
mTabBar->add_tab("Mods", [this] { push(std::make_unique<ModsWindow>()); });
|
||||
for (auto& tab : mods::svc::ui_mod_menu_tabs()) {
|
||||
mTabBar->add_tab(tab.label, std::move(tab.onSelected));
|
||||
}
|
||||
|
||||
mTabBar->add_tab("Reset", [this] {
|
||||
mTabBar->set_active_tab(-1);
|
||||
@@ -234,7 +240,10 @@ void MenuBar::rebuild() {
|
||||
for (auto& doc : get_document_stack()) {
|
||||
if (auto* menuBar = dynamic_cast<MenuBar*>(doc.get())) {
|
||||
const bool wasVisible = menuBar->visible();
|
||||
doc = std::make_unique<MenuBar>();
|
||||
auto next = std::make_unique<MenuBar>();
|
||||
next->mFocusedTabIndex = menuBar->mFocusedTabIndex;
|
||||
next->mWasVisible = menuBar->mWasVisible;
|
||||
doc = std::move(next);
|
||||
if (wasVisible) {
|
||||
doc->show();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "mod_window.hpp"
|
||||
|
||||
#include "bool_button.hpp"
|
||||
#include "number_button.hpp"
|
||||
#include "string_button.hpp"
|
||||
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
|
||||
const auto shared = std::make_shared<ModControlSpec>(std::move(spec));
|
||||
auto& s = *shared;
|
||||
Component* control = nullptr;
|
||||
switch (s.kind) {
|
||||
case ModControlSpec::Kind::Button:
|
||||
control = &pane.add_button(ControlledButton::Props{
|
||||
.text = s.label,
|
||||
.isDisabled = s.isDisabled,
|
||||
})
|
||||
.on_pressed([shared] {
|
||||
if (shared->onPressed) {
|
||||
shared->onPressed();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Toggle:
|
||||
control = &pane.add_child<BoolButton>(BoolButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getBool,
|
||||
.setValue = s.setBool,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Number:
|
||||
control = &pane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getInt,
|
||||
.setValue = s.setInt,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
.min = s.min,
|
||||
.max = s.max,
|
||||
.step = s.step,
|
||||
.prefix = s.prefix,
|
||||
.suffix = s.suffix,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::String:
|
||||
control = &pane.add_child<StringButton>(StringButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getString,
|
||||
.setValue = s.setString,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
.maxLength = s.maxLength,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Select:
|
||||
if (helpPane == nullptr || s.options.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
control = &pane.add_child<ControlledSelectButton>(ControlledSelectButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = [shared]() -> Rml::String {
|
||||
const int index = shared->getInt ? shared->getInt() : -1;
|
||||
if (index < 0 || index >= static_cast<int>(shared->options.size())) {
|
||||
return "?";
|
||||
}
|
||||
return shared->options[index];
|
||||
},
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (control == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (helpPane != nullptr && (s.kind == ModControlSpec::Kind::Select || !s.helpRml.empty())) {
|
||||
pane.register_control(*control, *helpPane, [shared](Pane& help) {
|
||||
help.clear();
|
||||
if (shared->kind == ModControlSpec::Kind::Select) {
|
||||
for (int i = 0; i < static_cast<int>(shared->options.size()); ++i) {
|
||||
help.add_button(
|
||||
ControlledButton::Props{
|
||||
.text = shared->options[i],
|
||||
.isSelected =
|
||||
[shared, i] { return shared->getInt && shared->getInt() == i; },
|
||||
})
|
||||
.on_pressed([shared, i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
if (shared->setInt) {
|
||||
shared->setInt(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!shared->helpRml.empty()) {
|
||||
help.add_rml(shared->helpRml);
|
||||
}
|
||||
});
|
||||
}
|
||||
return control;
|
||||
}
|
||||
|
||||
ModWindow::ModWindow(Desc desc) : mDesc(std::move(desc)) {
|
||||
mRoot->SetAttribute("mod-id", mDesc.modId);
|
||||
for (int i = 0; i < static_cast<int>(mDesc.tabs.size()); ++i) {
|
||||
add_tab(mDesc.tabs[i].title, [this, i](Rml::Element* content) {
|
||||
mActiveTab = i;
|
||||
auto& left = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& right = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
if (mDesc.tabs[i].build) {
|
||||
mDesc.tabs[i].build(*this, left, right);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!mDesc.rcss.empty()) {
|
||||
set_document_styles(mDesc.rcss);
|
||||
}
|
||||
}
|
||||
|
||||
ModWindow::~ModWindow() {
|
||||
if (mDesc.onDestroyed) {
|
||||
mDesc.onDestroyed();
|
||||
}
|
||||
}
|
||||
|
||||
void ModWindow::update() {
|
||||
if (mActiveTab >= 0 && mActiveTab < static_cast<int>(mDesc.tabs.size()) &&
|
||||
mDesc.tabs[mActiveTab].update)
|
||||
{
|
||||
mDesc.tabs[mActiveTab].update();
|
||||
}
|
||||
Window::update();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "pane.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <climits>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
struct ModControlSpec {
|
||||
enum class Kind : u8 {
|
||||
Button,
|
||||
Toggle,
|
||||
Number,
|
||||
String,
|
||||
Select,
|
||||
};
|
||||
|
||||
Kind kind = Kind::Button;
|
||||
Rml::String label;
|
||||
Rml::String helpRml;
|
||||
std::function<void()> onPressed;
|
||||
std::function<bool()> getBool;
|
||||
std::function<void(bool)> setBool;
|
||||
// Number value, or the selected option index for Select
|
||||
std::function<int()> getInt;
|
||||
std::function<void(int)> setInt;
|
||||
std::function<Rml::String()> getString;
|
||||
std::function<void(Rml::String)> setString;
|
||||
std::function<bool()> isDisabled;
|
||||
std::function<bool()> isModified;
|
||||
int min = 0;
|
||||
int max = INT_MAX;
|
||||
int step = 1;
|
||||
Rml::String prefix;
|
||||
Rml::String suffix;
|
||||
std::vector<Rml::String> options;
|
||||
int maxLength = -1;
|
||||
};
|
||||
|
||||
Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec);
|
||||
|
||||
// A mod-owned tabbed two-pane window.
|
||||
class ModWindow : public Window {
|
||||
public:
|
||||
struct Tab {
|
||||
Rml::String title;
|
||||
std::function<void(ModWindow&, Pane& left, Pane& right)> build;
|
||||
std::function<void()> update;
|
||||
};
|
||||
struct Desc {
|
||||
Rml::String modId;
|
||||
std::vector<Tab> tabs;
|
||||
Rml::String rcss;
|
||||
std::function<void()> onDestroyed;
|
||||
};
|
||||
|
||||
explicit ModWindow(Desc desc);
|
||||
~ModWindow() override;
|
||||
|
||||
void update() override;
|
||||
void force_close() { Document::hide(true); }
|
||||
|
||||
private:
|
||||
Desc mDesc;
|
||||
int mActiveTab = -1;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+32
-8
@@ -27,19 +27,43 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo
|
||||
actions->SetClass("modal-actions", true);
|
||||
|
||||
for (auto& action : mProps.actions) {
|
||||
auto btn = std::make_unique<Button>(actions, action.label);
|
||||
btn->root()->SetClass("modal-btn", true);
|
||||
btn->on_pressed([this, callback = std::move(action.onPressed)] {
|
||||
if (callback) {
|
||||
callback(*this);
|
||||
}
|
||||
});
|
||||
mButtons.push_back(std::move(btn));
|
||||
add_action(std::move(action));
|
||||
}
|
||||
|
||||
mDoAud_seStartMenu(kSoundWindowOpen);
|
||||
}
|
||||
|
||||
void Modal::add_action(ModalAction action) {
|
||||
auto* actions = mDialog->QuerySelector(".modal-actions");
|
||||
auto btn = std::make_unique<Button>(actions, action.label);
|
||||
btn->root()->SetClass("modal-btn", true);
|
||||
btn->on_pressed([this, callback = std::move(action.onPressed)] {
|
||||
if (callback) {
|
||||
callback(*this);
|
||||
}
|
||||
});
|
||||
mButtons.push_back(std::move(btn));
|
||||
}
|
||||
|
||||
void Modal::set_body(const Rml::String& bodyRml) {
|
||||
mDialog->QuerySelector(".modal-body")->SetInnerRML(bodyRml);
|
||||
}
|
||||
|
||||
void Modal::set_icon(const Rml::String& icon) {
|
||||
auto* iconElem = mDialog->QuerySelector("icon");
|
||||
if (icon.empty()) {
|
||||
if (iconElem != nullptr) {
|
||||
iconElem->GetParentNode()->RemoveChild(iconElem);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (iconElem == nullptr) {
|
||||
// The constructor only creates the icon element when Props.icon is set.
|
||||
iconElem = append(mDialog->QuerySelector(".modal-header"), "icon");
|
||||
}
|
||||
iconElem->SetClassNames(icon);
|
||||
}
|
||||
|
||||
bool Modal::focus() {
|
||||
if (!mButtons.empty()) {
|
||||
return mButtons.front()->focus();
|
||||
|
||||
@@ -26,6 +26,10 @@ public:
|
||||
|
||||
bool focus() override;
|
||||
|
||||
void add_action(ModalAction action);
|
||||
void set_body(const Rml::String& bodyRml);
|
||||
void set_icon(const Rml::String& icon);
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "mods_window.hpp"
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/svc/ui.hpp"
|
||||
#include "fmt/format.h"
|
||||
#include "logs_window.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
@@ -218,6 +219,7 @@ void ModsWindow::build_content(Rml::Element* content) {
|
||||
}
|
||||
|
||||
void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
|
||||
pane.root()->SetAttribute("mod-id", mod.metadata.id);
|
||||
pane.add_child<ModDetailHeader>(
|
||||
mod, [this, id = mod.metadata.id] { push(std::make_unique<LogsWindow>(id)); });
|
||||
|
||||
@@ -274,6 +276,10 @@ void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
|
||||
pane.add_text(mod.metadata.description)->SetClass("mod-description", true);
|
||||
}
|
||||
|
||||
if (mod.active) {
|
||||
mods::svc::ui_build_mods_panels(mod, pane);
|
||||
}
|
||||
|
||||
pane.finalize();
|
||||
}
|
||||
|
||||
@@ -319,6 +325,10 @@ void ModsWindow::update() {
|
||||
}
|
||||
}
|
||||
|
||||
if (mSelectedMod != nullptr && mSelectedMod->active) {
|
||||
mods::svc::ui_update_mods_panels(*mSelectedMod);
|
||||
}
|
||||
|
||||
Window::update();
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ static std::string FormatTime(OSTime ticks) {
|
||||
return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", t.hour, t.min, t.sec, t.msec);
|
||||
}
|
||||
|
||||
Overlay::Overlay() : Document(kDocumentSource, true) {
|
||||
Overlay::Overlay() : Document(kDocumentSource, true, DocumentScope::Overlay) {
|
||||
mFpsCounter = mDocument->GetElementById("fps");
|
||||
mPipelineProgress = mDocument->GetElementById("pipeline-progress");
|
||||
mPipelineProgressLabel = mDocument->GetElementById("pipeline-progress-label");
|
||||
|
||||
@@ -685,7 +685,9 @@ void try_apply_mirrored_layout(Rml::Element* body) {
|
||||
body->SetClass("mirrored", getSettings().game.enableMirrorMode.getValue());
|
||||
}
|
||||
|
||||
Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) {
|
||||
Prelaunch::Prelaunch()
|
||||
: Document(kDocumentSource, false, DocumentScope::Prelaunch),
|
||||
mRoot(mDocument->GetElementById("root")) {
|
||||
ensure_initialized();
|
||||
begin_update_check();
|
||||
|
||||
@@ -716,7 +718,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
|
||||
}
|
||||
|
||||
IsGameLaunched = true;
|
||||
pop(false);
|
||||
pop();
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
bool obscures_game() const override { return true; }
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
+11
-15
@@ -527,7 +527,7 @@ SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>&
|
||||
|
||||
template <typename T>
|
||||
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& var,
|
||||
const GraphicsTunerProps& props, bool prelaunch) {
|
||||
const GraphicsTunerProps& props) {
|
||||
leftPane.register_control(
|
||||
leftPane
|
||||
.add_select_button({
|
||||
@@ -545,10 +545,10 @@ void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, Con
|
||||
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
|
||||
.submit = false,
|
||||
})
|
||||
.on_nav_command([&window, props, prelaunch](Rml::Event&, NavCommand cmd) {
|
||||
.on_nav_command([&window, props](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
window.push(std::make_unique<GraphicsTuner>(props, prelaunch));
|
||||
window.push(std::make_unique<GraphicsTuner>(props));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -563,7 +563,6 @@ void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, Con
|
||||
|
||||
SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
if (prelaunch) {
|
||||
mSuppressNavFallback = true;
|
||||
add_tab("Prelaunch", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
@@ -862,7 +861,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = 0,
|
||||
.valueMax = 12,
|
||||
.defaultValue = 0,
|
||||
}, mPrelaunch);
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
getSettings().game.shadowResolutionMultiplier,
|
||||
GraphicsTunerProps{
|
||||
@@ -872,7 +871,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = 1,
|
||||
.valueMax = 8,
|
||||
.defaultValue = 1,
|
||||
}, mPrelaunch);
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.resampler,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::Resampler,
|
||||
@@ -881,7 +880,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = static_cast<int>(Resampler::Bilinear),
|
||||
.valueMax = static_cast<int>(Resampler::Area),
|
||||
.defaultValue = static_cast<int>(Resampler::Bilinear),
|
||||
}, mPrelaunch);
|
||||
});
|
||||
|
||||
leftPane.add_section("Post-Processing");
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
|
||||
@@ -892,7 +891,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = static_cast<int>(BloomMode::Off),
|
||||
.valueMax = static_cast<int>(BloomMode::Dusk),
|
||||
.defaultValue = static_cast<int>(BloomMode::Classic),
|
||||
}, mPrelaunch);
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::BloomMultiplier,
|
||||
@@ -902,8 +901,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMax = 100,
|
||||
.defaultValue = 100,
|
||||
.step = 10,
|
||||
},
|
||||
mPrelaunch);
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.depthOfFieldMode,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::DepthOfFieldMode,
|
||||
@@ -912,8 +910,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = static_cast<int>(DepthOfFieldMode::Off),
|
||||
.valueMax = static_cast<int>(DepthOfFieldMode::Dusk),
|
||||
.defaultValue = static_cast<int>(DepthOfFieldMode::Classic),
|
||||
},
|
||||
mPrelaunch);
|
||||
});
|
||||
|
||||
leftPane.add_section("Rendering");
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
@@ -925,8 +922,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = static_cast<int>(false),
|
||||
.valueMax = static_cast<int>(true),
|
||||
.defaultValue = static_cast<int>(false),
|
||||
},
|
||||
mPrelaunch);
|
||||
});
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Unlock Framerate",
|
||||
@@ -992,7 +988,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
|
||||
leftPane.add_section("Inputs");
|
||||
leftPane.register_control(leftPane.add_button("Configure Inputs").on_pressed([this] {
|
||||
push(std::make_unique<ControllerConfigWindow>(mPrelaunch));
|
||||
push(std::make_unique<ControllerConfigWindow>());
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.clear();
|
||||
|
||||
@@ -141,6 +141,21 @@ void BaseStringButton::stop_editing(bool commit, bool refocusRoot) {
|
||||
|
||||
StringButton::StringButton(Rml::Element* parent, Props props)
|
||||
: BaseStringButton(parent, {.key = std::move(props.key), .maxLength = props.maxLength}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)) {}
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
|
||||
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)) {}
|
||||
|
||||
bool StringButton::modified() const {
|
||||
if (mIsModified) {
|
||||
return mIsModified();
|
||||
}
|
||||
return BaseStringButton::modified();
|
||||
}
|
||||
|
||||
bool StringButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return BaseStringButton::disabled();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -45,10 +45,14 @@ public:
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<void(Rml::String)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
std::function<bool()> isModified;
|
||||
int maxLength = -1;
|
||||
};
|
||||
|
||||
StringButton(Rml::Element* parent, Props props);
|
||||
bool modified() const override;
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override { return mGetValue(); }
|
||||
@@ -61,6 +65,8 @@ protected:
|
||||
private:
|
||||
std::function<Rml::String()> mGetValue;
|
||||
std::function<void(Rml::String)> mSetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
std::function<bool()> mIsModified;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -374,7 +374,7 @@ void sync_virtual_input() noexcept {
|
||||
}
|
||||
|
||||
TouchControls::TouchControls()
|
||||
: Document(touch_controls_document_source(), true),
|
||||
: Document(touch_controls_document_source(), true, DocumentScope::TouchControls),
|
||||
mRoot(mDocument != nullptr ? mDocument->GetElementById("root") : nullptr),
|
||||
mControlStick(mDocument != nullptr ? mDocument->GetElementById("control-stick") : nullptr),
|
||||
mControlKnob(mDocument != nullptr ? mDocument->GetElementById("control-knob") : nullptr),
|
||||
|
||||
@@ -99,7 +99,7 @@ float squared_distance(Rml::Vector2f a, Rml::Vector2f b) noexcept {
|
||||
} // namespace
|
||||
|
||||
TouchControlsEditor::TouchControlsEditor()
|
||||
: Document(touch_controls_editor_document_source()),
|
||||
: Document(touch_controls_editor_document_source(), false, DocumentScope::TouchControls),
|
||||
mRoot(mDocument != nullptr ? mDocument->GetElementById("root") : nullptr),
|
||||
mSelectionFrame(
|
||||
mDocument != nullptr ? mDocument->GetElementById("editor-selection-frame") : nullptr),
|
||||
|
||||
+73
-6
@@ -34,6 +34,37 @@ bool sInitialized = false;
|
||||
std::vector<std::unique_ptr<Document> > sDocumentStack;
|
||||
// Documents that don't participate in the focus stack
|
||||
std::vector<std::unique_ptr<Document> > sPassiveDocuments;
|
||||
|
||||
struct ScopedStyles {
|
||||
DocumentScope scope;
|
||||
std::string id;
|
||||
Rml::SharedPtr<Rml::StyleSheetContainer> sheet;
|
||||
};
|
||||
std::vector<ScopedStyles> sScopedStyles;
|
||||
|
||||
std::vector<const Rml::StyleSheetContainer*> scoped_sheets(DocumentScope scope) {
|
||||
std::vector<const Rml::StyleSheetContainer*> sheets;
|
||||
for (const auto& entry : sScopedStyles) {
|
||||
if (entry.scope == scope) {
|
||||
sheets.push_back(entry.sheet.get());
|
||||
}
|
||||
}
|
||||
return sheets;
|
||||
}
|
||||
|
||||
void restyle_scope(DocumentScope scope) {
|
||||
const auto sheets = scoped_sheets(scope);
|
||||
const auto restyle_documents = [&sheets, scope](auto& documents) {
|
||||
for (auto& doc : documents) {
|
||||
if (doc != nullptr && doc->scope() == scope && !doc->closed()) {
|
||||
doc->restyle(sheets);
|
||||
}
|
||||
}
|
||||
};
|
||||
restyle_documents(sDocumentStack);
|
||||
restyle_documents(sPassiveDocuments);
|
||||
}
|
||||
|
||||
std::deque<Toast> sToasts;
|
||||
bool sMenuNotificationRequested = false;
|
||||
|
||||
@@ -184,6 +215,34 @@ void handle_event(const SDL_Event& event) noexcept {
|
||||
input::handle_event(event);
|
||||
}
|
||||
|
||||
bool register_scoped_styles(DocumentScope scope, std::string id, const std::string& rcss) noexcept {
|
||||
auto sheet = Rml::Factory::InstanceStyleSheetString(rcss);
|
||||
if (sheet == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto it = std::ranges::find_if(sScopedStyles,
|
||||
[scope, &id](const ScopedStyles& entry) { return entry.scope == scope && entry.id == id; });
|
||||
if (it != sScopedStyles.end()) {
|
||||
it->sheet = std::move(sheet);
|
||||
} else {
|
||||
sScopedStyles.push_back({scope, std::move(id), std::move(sheet)});
|
||||
}
|
||||
restyle_scope(scope);
|
||||
return true;
|
||||
}
|
||||
|
||||
void unregister_scoped_styles(DocumentScope scope, std::string_view id) noexcept {
|
||||
const auto erased = std::erase_if(sScopedStyles,
|
||||
[scope, id](const ScopedStyles& entry) { return entry.scope == scope && entry.id == id; });
|
||||
if (erased != 0) {
|
||||
restyle_scope(scope);
|
||||
}
|
||||
}
|
||||
|
||||
void apply_scoped_styles(Document& doc) noexcept {
|
||||
doc.restyle(scoped_sheets(doc.scope()));
|
||||
}
|
||||
|
||||
Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive) noexcept {
|
||||
Document& ret = *doc;
|
||||
if (passive) {
|
||||
@@ -198,13 +257,9 @@ Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive)
|
||||
return ret;
|
||||
}
|
||||
|
||||
void focus_top_document(bool show) noexcept {
|
||||
void uncover_top_document() noexcept {
|
||||
if (auto* doc = top_document()) {
|
||||
if (show) {
|
||||
doc->show();
|
||||
} else {
|
||||
doc->focus();
|
||||
}
|
||||
doc->uncover();
|
||||
}
|
||||
input::sync_input_block();
|
||||
}
|
||||
@@ -221,6 +276,18 @@ bool is_prelaunch_open() noexcept {
|
||||
});
|
||||
}
|
||||
|
||||
bool game_obscured_below(const Document& doc) noexcept {
|
||||
for (const auto& entry : sDocumentStack) {
|
||||
if (entry.get() == &doc) {
|
||||
break;
|
||||
}
|
||||
if (entry->active() && entry->obscures_game()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Document* top_document() noexcept {
|
||||
for (auto& doc : std::views::reverse(sDocumentStack)) {
|
||||
if (doc->active()) {
|
||||
|
||||
+16
-2
@@ -15,6 +15,16 @@ class Document;
|
||||
|
||||
using clock = std::chrono::steady_clock;
|
||||
|
||||
enum class DocumentScope : u8 {
|
||||
None,
|
||||
Prelaunch,
|
||||
Window,
|
||||
MenuBar,
|
||||
Overlay,
|
||||
TouchControls,
|
||||
GraphicsTuner,
|
||||
};
|
||||
|
||||
struct Toast {
|
||||
Rml::String type;
|
||||
Rml::String title;
|
||||
@@ -74,9 +84,13 @@ void update() noexcept;
|
||||
|
||||
Document& push_document(
|
||||
std::unique_ptr<Document> doc, bool show = true, bool passive = false) noexcept;
|
||||
void focus_top_document(bool show) noexcept;
|
||||
bool register_scoped_styles(DocumentScope scope, std::string id, const std::string& rcss) noexcept;
|
||||
void unregister_scoped_styles(DocumentScope scope, std::string_view id) noexcept;
|
||||
void apply_scoped_styles(Document& doc) noexcept;
|
||||
void uncover_top_document() noexcept;
|
||||
bool any_document_visible() noexcept;
|
||||
bool is_prelaunch_open() noexcept;
|
||||
bool game_obscured_below(const Document& doc) noexcept;
|
||||
Document* top_document() noexcept;
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
|
||||
@@ -86,7 +100,7 @@ Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept;
|
||||
NavCommand map_nav_event(const Rml::Event& event) noexcept;
|
||||
Insets safe_area_insets(Rml::Context* context) noexcept;
|
||||
|
||||
std::vector<std::unique_ptr<Document> >& get_document_stack() noexcept;
|
||||
std::vector<std::unique_ptr<Document>>& get_document_stack() noexcept;
|
||||
|
||||
void push_toast(Toast toast) noexcept;
|
||||
std::deque<Toast>& get_toasts() noexcept;
|
||||
|
||||
@@ -60,7 +60,7 @@ const Rml::String kDocumentSourceSmall = R"RML(
|
||||
} // namespace
|
||||
|
||||
Window::Window(Props props)
|
||||
: Document(window_document_source(props.styleSheets)),
|
||||
: Document(window_document_source(props.styleSheets), false, DocumentScope::Window),
|
||||
mRoot(mDocument->GetElementById("window")) {
|
||||
if (props.tabBar) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
|
||||
@@ -262,13 +262,13 @@ bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (mTabBar && mTabBar->handle_nav_command(event, cmd)) {
|
||||
return true;
|
||||
}
|
||||
return mSuppressNavFallback ? false : Document::handle_nav_command(event, cmd);
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
if (cmd == NavCommand::Up) {
|
||||
if (!mTabBar) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
if (focus()) {
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
@@ -331,8 +331,8 @@ bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
}
|
||||
|
||||
WindowSmall::WindowSmall(const Rml::String& windowClass, const Rml::String& dialogClass)
|
||||
: Document(kDocumentSourceSmall), mRoot(mDocument->GetElementById("window")),
|
||||
mDialog(mDocument->GetElementById("dialog")) {
|
||||
: Document(kDocumentSourceSmall, false, DocumentScope::Window),
|
||||
mRoot(mDocument->GetElementById("window")), mDialog(mDocument->GetElementById("dialog")) {
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
|
||||
Document::visible())
|
||||
|
||||
@@ -47,10 +47,10 @@ protected:
|
||||
void clear_content() noexcept;
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
bool handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept;
|
||||
bool mSuppressNavFallback = false;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
requires std::is_base_of_v<Component, T>
|
||||
T& add_child(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mContentComponents.emplace_back(std::move(child));
|
||||
@@ -63,7 +63,7 @@ protected:
|
||||
// Only set for tab-bar-less windows.
|
||||
std::unique_ptr<Button> mCloseButton;
|
||||
TabBuilder mContentBuilder;
|
||||
std::vector<std::unique_ptr<Component> > mContentComponents;
|
||||
std::vector<std::unique_ptr<Component>> mContentComponents;
|
||||
Insets mBodyPadding;
|
||||
bool mInitialOpen = true;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user