Mod API: UI service (#2189)

This commit is contained in:
Luke Street
2026-07-09 10:31:34 -06:00
committed by GitHub
parent f3ca94b338
commit 6849baac06
33 changed files with 2385 additions and 66 deletions
+93
View File
@@ -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
+4
View File
@@ -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
+284
View File
@@ -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
+5
View File
@@ -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,
+11
View File
@@ -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();
+1
View File
@@ -206,6 +206,7 @@ void ModLoader::init_services() {
&svc::g_overlayModule,
&svc::g_textureModule,
&svc::g_configModule,
&svc::g_uiModule,
&svc::g_gameModule,
})
{
+1
View File
@@ -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
+25
View File
@@ -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
+1 -5
View File
@@ -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) {
+1 -1
View File
@@ -9,7 +9,7 @@ namespace dusk::ui {
class ControllerConfigWindow : public Window {
public:
ControllerConfigWindow(bool prelaunch);
ControllerConfigWindow();
void update() override;
void hide(bool close) override;
+60 -2
View File
@@ -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;
+28 -5
View File
@@ -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
+4 -4
View File
@@ -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() {
+1 -2
View File
@@ -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
+11 -2
View File
@@ -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();
}
+141
View File
@@ -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
+69
View File
@@ -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
View File
@@ -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();
+4
View File
@@ -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;
+10
View File
@@ -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();
}
+1 -1
View File
@@ -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");
+4 -2
View File
@@ -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");
+1
View File
@@ -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
View File
@@ -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();
+16 -1
View File
@@ -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
+6
View File
@@ -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
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+5 -5
View File
@@ -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())
+3 -3
View File
@@ -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;
};