mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-08 20:24:47 -04:00
Initial game menu implementation
This commit is contained in:
@@ -1472,6 +1472,8 @@ set(DUSK_FILES
|
||||
src/dusk/ui/element.cpp
|
||||
src/dusk/ui/focus_border.hpp
|
||||
src/dusk/ui/focus_border.cpp
|
||||
src/dusk/ui/game_menu.hpp
|
||||
src/dusk/ui/game_menu.cpp
|
||||
src/dusk/ui/game_option.hpp
|
||||
src/dusk/ui/game_option.cpp
|
||||
src/dusk/ui/label.hpp
|
||||
|
||||
@@ -0,0 +1,769 @@
|
||||
#include "game_menu.hpp"
|
||||
|
||||
#include "element.hpp"
|
||||
#include "game_option.hpp"
|
||||
#include "label.hpp"
|
||||
#include "theme.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <RmlUi/Core/ElementDocument.h>
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/gfx.h>
|
||||
#include <aurora/rmlui.hpp>
|
||||
#include <dolphin/vi.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace aurora::gx {
|
||||
extern bool enableLodBias;
|
||||
}
|
||||
|
||||
namespace dusk::ui::game_menu {
|
||||
namespace {
|
||||
|
||||
enum class Tab : int {
|
||||
Audio = 0,
|
||||
Cheats,
|
||||
Gameplay,
|
||||
Graphics,
|
||||
Input,
|
||||
Interface,
|
||||
Count,
|
||||
};
|
||||
|
||||
struct TabDef {
|
||||
const char* id;
|
||||
const char* label;
|
||||
};
|
||||
|
||||
constexpr std::array<TabDef, static_cast<size_t>(Tab::Count)> kTabs{{
|
||||
{"audio", "Audio"},
|
||||
{"cheats", "Cheats"},
|
||||
{"gameplay", "Gameplay"},
|
||||
{"graphics", "Graphics"},
|
||||
{"input", "Input"},
|
||||
{"interface", "Interface"},
|
||||
}};
|
||||
|
||||
constexpr int kInternalResolutionScaleMax = 12;
|
||||
constexpr int kShadowResolutionMax = 8;
|
||||
|
||||
constexpr std::array<float, 5> kBloomMultiplierStops{0.0f, 0.25f, 0.50f, 0.75f, 1.00f};
|
||||
constexpr std::array<const char*, 3> kBloomModeNames{"Off", "Classic", "Dusk"};
|
||||
|
||||
struct Row {
|
||||
std::string id;
|
||||
std::function<void()> activate;
|
||||
std::function<void(int direction)> cycle;
|
||||
};
|
||||
|
||||
class Screen : public Rml::EventListener {
|
||||
public:
|
||||
bool initialize() {
|
||||
if (m_initialized) {
|
||||
return true;
|
||||
}
|
||||
if (!ui::initialize()) {
|
||||
return false;
|
||||
}
|
||||
m_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
close_document();
|
||||
if (ui::is_active()) {
|
||||
ui::set_active(false);
|
||||
}
|
||||
m_initialized = false;
|
||||
m_focusIds.clear();
|
||||
m_rows.clear();
|
||||
}
|
||||
|
||||
bool is_open() const { return m_open; }
|
||||
|
||||
void set_open(bool open) {
|
||||
if (open == m_open) {
|
||||
return;
|
||||
}
|
||||
if (open && !initialize()) {
|
||||
return;
|
||||
}
|
||||
m_open = open;
|
||||
if (open) {
|
||||
ui::set_active(true);
|
||||
rebuild();
|
||||
} else {
|
||||
close_document();
|
||||
ui::set_active(false);
|
||||
}
|
||||
}
|
||||
|
||||
void toggle() { set_open(!m_open); }
|
||||
|
||||
void handle_event(const SDL_Event& event) {
|
||||
if (!m_open) {
|
||||
return;
|
||||
}
|
||||
ui::handle_event(event);
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (!m_open) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_requestClose) {
|
||||
m_requestClose = false;
|
||||
set_open(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_pendingTabId.empty()) {
|
||||
const std::string tabId = std::move(m_pendingTabId);
|
||||
m_pendingTabId.clear();
|
||||
apply_tab_selection(tabId);
|
||||
if (!m_open) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_requestedActivation.empty()) {
|
||||
const std::string id = std::move(m_requestedActivation);
|
||||
m_requestedActivation.clear();
|
||||
invoke_activate(id);
|
||||
if (!m_open) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_requestedCycleDirection != 0) {
|
||||
const std::string id = std::move(m_requestedCycleId);
|
||||
const int direction = m_requestedCycleDirection;
|
||||
m_requestedCycleId.clear();
|
||||
m_requestedCycleDirection = 0;
|
||||
invoke_cycle(id, direction);
|
||||
if (!m_open) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_needsRebuild) {
|
||||
m_needsRebuild = false;
|
||||
rebuild();
|
||||
}
|
||||
|
||||
ui::update();
|
||||
}
|
||||
|
||||
void ProcessEvent(Rml::Event& event) override {
|
||||
if (event.GetId() != Rml::EventId::Keydown) {
|
||||
return;
|
||||
}
|
||||
const auto key = static_cast<Rml::Input::KeyIdentifier>(
|
||||
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
||||
if (handle_key(key)) {
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
bool m_open = false;
|
||||
Tab m_tab = Tab::Graphics;
|
||||
Rml::ElementDocument* m_document = nullptr;
|
||||
std::unique_ptr<Window> m_window;
|
||||
std::vector<std::unique_ptr<GameOption> > m_options;
|
||||
std::vector<Row> m_rows;
|
||||
std::vector<std::string> m_focusIds;
|
||||
std::string m_pendingFocusId;
|
||||
std::string m_requestedActivation;
|
||||
std::string m_requestedCycleId;
|
||||
int m_requestedCycleDirection = 0;
|
||||
bool m_requestClose = false;
|
||||
bool m_needsRebuild = false;
|
||||
std::string m_pendingTabId;
|
||||
|
||||
Row* find_row(std::string_view id) {
|
||||
for (auto& row : m_rows) {
|
||||
if (row.id == id) {
|
||||
return &row;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void invoke_activate(const std::string& id) {
|
||||
if (Row* row = find_row(id); row && row->activate) {
|
||||
row->activate();
|
||||
}
|
||||
}
|
||||
|
||||
void invoke_cycle(const std::string& id, int direction) {
|
||||
if (Row* row = find_row(id); row && row->cycle) {
|
||||
row->cycle(direction);
|
||||
}
|
||||
}
|
||||
|
||||
void close_document() {
|
||||
if (m_document == nullptr) {
|
||||
return;
|
||||
}
|
||||
m_document->RemoveEventListener(Rml::EventId::Keydown, this);
|
||||
m_options.clear();
|
||||
m_rows.clear();
|
||||
m_focusIds.clear();
|
||||
m_window.reset();
|
||||
m_document->Close();
|
||||
m_document = nullptr;
|
||||
}
|
||||
|
||||
void style_document(Rml::ElementDocument* document) {
|
||||
using namespace theme;
|
||||
set_props(document, {
|
||||
{"width", "100%"},
|
||||
{"height", "100%"},
|
||||
{"margin", "0"},
|
||||
{"padding", "0"},
|
||||
{"font-family", "Inter"},
|
||||
{"color", rgba(Text)},
|
||||
});
|
||||
}
|
||||
|
||||
Rml::Element* add_screen() {
|
||||
using namespace theme;
|
||||
auto* screen = append(m_document, "div", "game-menu-screen");
|
||||
set_props(screen, {
|
||||
{"display", "flex"},
|
||||
{"position", "absolute"},
|
||||
{"left", "0"},
|
||||
{"top", "0"},
|
||||
{"right", "0"},
|
||||
{"bottom", "0"},
|
||||
{"flex-direction", "column"},
|
||||
{"align-items", "center"},
|
||||
{"justify-content", "center"},
|
||||
{"box-sizing", "border-box"},
|
||||
{"padding", "32dp"},
|
||||
{"background-color", rgba(ModalOverlay)},
|
||||
});
|
||||
return screen;
|
||||
}
|
||||
|
||||
Rml::Element* add_section_header(Rml::Element* parent, std::string_view title) {
|
||||
auto* row = append(parent, "div");
|
||||
set_props(row, {
|
||||
{"display", "flex"},
|
||||
{"flex-direction", "row"},
|
||||
{"align-items", "center"},
|
||||
{"width", "100%"},
|
||||
{"padding-top", "8dp"},
|
||||
{"padding-bottom", "4dp"},
|
||||
});
|
||||
auto* label = add_label(row, title, LabelStyle::Annotation);
|
||||
set_props(label, {
|
||||
{"font-size", "14dp"},
|
||||
{"letter-spacing", "3dp"},
|
||||
{"color", theme::rgba(theme::WindowAccentSoft)},
|
||||
{"flex-shrink", "0"},
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
Rml::Element* add_scroll_body(Rml::Element* parent) {
|
||||
auto* scroll = append(parent, "div");
|
||||
set_props(scroll, {
|
||||
{"display", "flex"},
|
||||
{"flex-direction", "column"},
|
||||
{"width", "100%"},
|
||||
{"flex-grow", "1"},
|
||||
{"min-height", "0"},
|
||||
{"gap", "8dp"},
|
||||
{"overflow-y", "auto"},
|
||||
});
|
||||
return scroll;
|
||||
}
|
||||
|
||||
std::function<void()> queue_activate(std::string id) {
|
||||
return [this, id = std::move(id)] { m_requestedActivation = id; };
|
||||
}
|
||||
|
||||
void register_row(Row row, std::unique_ptr<GameOption> option) {
|
||||
m_focusIds.push_back(row.id);
|
||||
m_rows.push_back(std::move(row));
|
||||
m_options.push_back(std::move(option));
|
||||
}
|
||||
|
||||
void add_toggle(Rml::Element* parent, std::string id, std::string_view title,
|
||||
config::ConfigVar<bool>& var, std::function<void(bool)> sideEffect = {},
|
||||
std::string_view detail = {}) {
|
||||
auto mutate = [this, &var, sideEffect = std::move(sideEffect)] {
|
||||
const bool next = !var.getValue();
|
||||
var.setValue(next);
|
||||
Save();
|
||||
if (sideEffect) {
|
||||
sideEffect(next);
|
||||
}
|
||||
m_needsRebuild = true;
|
||||
};
|
||||
const std::string_view valueText = var.getValue() ? "On" : "Off";
|
||||
auto option = std::make_unique<GameOption>(parent, id, title, valueText, detail,
|
||||
queue_activate(id));
|
||||
register_row(Row{id, mutate, [mutate](int) { mutate(); }}, std::move(option));
|
||||
}
|
||||
|
||||
void add_action(Rml::Element* parent, std::string id, std::string_view title,
|
||||
std::function<void()> action, std::string_view valueText = ">",
|
||||
std::string_view detail = {}) {
|
||||
auto mutate = [this, action = std::move(action)] {
|
||||
action();
|
||||
m_needsRebuild = true;
|
||||
};
|
||||
auto option = std::make_unique<GameOption>(parent, id, title, valueText, detail,
|
||||
queue_activate(id));
|
||||
register_row(Row{id, mutate, {}}, std::move(option));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void add_cycle_row(Rml::Element* parent, std::string id, std::string_view title,
|
||||
std::string_view valueText, std::string_view detail,
|
||||
std::function<void(int)> cycle) {
|
||||
auto mutate = [this, cycle] {
|
||||
cycle(1);
|
||||
m_needsRebuild = true;
|
||||
};
|
||||
auto cycleWithRebuild = [this, cycle](int direction) {
|
||||
cycle(direction);
|
||||
m_needsRebuild = true;
|
||||
};
|
||||
auto option = std::make_unique<GameOption>(parent, id, title, valueText, detail,
|
||||
queue_activate(id));
|
||||
register_row(Row{id, mutate, cycleWithRebuild}, std::move(option));
|
||||
}
|
||||
|
||||
void add_int_cycle(Rml::Element* parent, std::string id, std::string_view title,
|
||||
config::ConfigVar<int>& var, int minValue, int maxValue,
|
||||
std::function<std::string(int)> formatter,
|
||||
std::function<void(int)> sideEffect = {}) {
|
||||
const int current = std::clamp(var.getValue(), minValue, maxValue);
|
||||
const std::string valueText = formatter(current);
|
||||
auto cycle = [&var, minValue, maxValue, sideEffect = std::move(sideEffect)](int dir) {
|
||||
int next = std::clamp(var.getValue(), minValue, maxValue) + dir;
|
||||
if (next < minValue) {
|
||||
next = maxValue;
|
||||
} else if (next > maxValue) {
|
||||
next = minValue;
|
||||
}
|
||||
var.setValue(next);
|
||||
Save();
|
||||
if (sideEffect) {
|
||||
sideEffect(next);
|
||||
}
|
||||
};
|
||||
add_cycle_row<int>(parent, std::move(id), title, valueText, {}, std::move(cycle));
|
||||
}
|
||||
|
||||
void add_bloom_mode_row(Rml::Element* parent) {
|
||||
auto& var = getSettings().game.bloomMode;
|
||||
const int current = std::clamp(static_cast<int>(var.getValue()), 0,
|
||||
static_cast<int>(kBloomModeNames.size() - 1));
|
||||
const std::string_view valueText = kBloomModeNames[static_cast<size_t>(current)];
|
||||
auto cycle = [&var](int dir) {
|
||||
const int count = static_cast<int>(kBloomModeNames.size());
|
||||
int next = static_cast<int>(var.getValue()) + dir;
|
||||
next = (next % count + count) % count;
|
||||
var.setValue(static_cast<BloomMode>(next));
|
||||
Save();
|
||||
};
|
||||
add_cycle_row<int>(parent, "bloom-mode", "Bloom", valueText, {}, std::move(cycle));
|
||||
}
|
||||
|
||||
void add_bloom_brightness_row(Rml::Element* parent) {
|
||||
auto& var = getSettings().game.bloomMultiplier;
|
||||
const std::string valueText = fmt::format("{:.2f}", var.getValue());
|
||||
auto cycle = [&var](int dir) {
|
||||
const float currentValue = var.getValue();
|
||||
int closest = 0;
|
||||
float bestDelta = std::abs(currentValue - kBloomMultiplierStops[0]);
|
||||
for (int i = 1; i < static_cast<int>(kBloomMultiplierStops.size()); ++i) {
|
||||
const float delta = std::abs(currentValue - kBloomMultiplierStops[i]);
|
||||
if (delta < bestDelta) {
|
||||
bestDelta = delta;
|
||||
closest = i;
|
||||
}
|
||||
}
|
||||
const int count = static_cast<int>(kBloomMultiplierStops.size());
|
||||
const int next = (closest + dir + count) % count;
|
||||
var.setValue(kBloomMultiplierStops[next]);
|
||||
Save();
|
||||
};
|
||||
const std::string_view detail =
|
||||
getSettings().game.bloomMode.getValue() == BloomMode::Off ? "Bloom is disabled" : "";
|
||||
add_cycle_row<int>(parent, "bloom-brightness", "Bloom Brightness", valueText, detail,
|
||||
std::move(cycle));
|
||||
}
|
||||
|
||||
void build_description_pane() {
|
||||
m_descriptionElement = nullptr;
|
||||
m_lastDescriptionSyncFocus = nullptr;
|
||||
Rml::Element* right = m_window->right_pane();
|
||||
if (right == nullptr) {
|
||||
return;
|
||||
}
|
||||
m_descriptionElement = append_text(right, "p", " ", "option-description");
|
||||
set_props(m_descriptionElement, {{"color", theme::rgba(theme::TextActive)},
|
||||
{"font-size", "20dp"},
|
||||
{"line-height", "1.45em"},
|
||||
{"text-align", "left"},
|
||||
{"align-self", "stretch"},
|
||||
{"width", "100%"},
|
||||
{"box-sizing", "border-box"},
|
||||
{"white-space", "pre-line"},
|
||||
});
|
||||
}
|
||||
|
||||
void sync_description_pane() {
|
||||
if (m_descriptionElement == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (m_tab != Tab::Graphics) {
|
||||
return;
|
||||
}
|
||||
Rml::Context* context = aurora::rmlui::get_context();
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
Rml::Element* const focused = context->GetFocusElement();
|
||||
if (focused == m_lastDescriptionSyncFocus) {
|
||||
return;
|
||||
}
|
||||
m_lastDescriptionSyncFocus = focused;
|
||||
if (focused == nullptr) {
|
||||
set_text(m_descriptionElement, get_description_for_item({}));
|
||||
} else {
|
||||
set_text(m_descriptionElement, get_description_for_item(focused->GetId()));
|
||||
}
|
||||
}
|
||||
|
||||
void build_graphics_tab(Rml::Element* body) {
|
||||
auto* scroll = add_scroll_body(body);
|
||||
|
||||
add_section_header(scroll, "Display");
|
||||
|
||||
// RMLUI TODO: Replace this with a Display Mode toggle.
|
||||
add_toggle(scroll, "fullscreen", "Toggle Fullscreen",
|
||||
getSettings().video.enableFullscreen,
|
||||
[](bool enabled) { VISetWindowFullscreen(enabled); });
|
||||
|
||||
u32 internalWidth = 0;
|
||||
u32 internalHeight = 0;
|
||||
AuroraGetRenderSize(&internalWidth, &internalHeight);
|
||||
const std::string detail = fmt::format("Current: {}x{}", internalWidth, internalHeight);
|
||||
|
||||
const int currentScale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, kInternalResolutionScaleMax);
|
||||
const std::string scaleValue = currentScale == 0 ? std::string("Auto") : fmt::format("{}x", currentScale);
|
||||
|
||||
auto scaleCycle = [](int dir) {
|
||||
int next = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, kInternalResolutionScaleMax) + dir;
|
||||
if (next < 0) {
|
||||
next = kInternalResolutionScaleMax;
|
||||
} else if (next > kInternalResolutionScaleMax) {
|
||||
next = 0;
|
||||
}
|
||||
getSettings().game.internalResolutionScale.setValue(next);
|
||||
VISetFrameBufferScale(static_cast<float>(next));
|
||||
Save();
|
||||
};
|
||||
|
||||
add_cycle_row<int>(scroll, "internal-resolution", "Internal Resolution", scaleValue, detail,
|
||||
std::move(scaleCycle));
|
||||
|
||||
add_int_cycle(scroll, "shadow-resolution", "Shadow Resolution",
|
||||
getSettings().game.shadowResolutionMultiplier, 1, kShadowResolutionMax,
|
||||
[](int v) { return fmt::format("x{}", v); });
|
||||
|
||||
add_toggle(scroll, "lock-aspect", "Force 4:3 Aspect Ratio",
|
||||
getSettings().video.lockAspectRatio,
|
||||
[](bool enabled) { AuroraSetViewportPolicy(enabled ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH); });
|
||||
|
||||
add_toggle(scroll, "vsync", "Enable VSync",
|
||||
getSettings().video.enableVsync,
|
||||
[](bool enabled) { aurora_enable_vsync(enabled); });
|
||||
|
||||
add_toggle(scroll, "frame-interp", "Unlock Framerate",
|
||||
getSettings().game.enableFrameInterpolation, {}, "Experimental");
|
||||
|
||||
add_section_header(scroll, "Post-Processing");
|
||||
|
||||
add_bloom_mode_row(scroll);
|
||||
if (getSettings().game.bloomMode.getValue() != BloomMode::Off) {
|
||||
add_bloom_brightness_row(scroll);
|
||||
}
|
||||
|
||||
add_section_header(scroll, "Developer Options");
|
||||
|
||||
const std::string lodValue = aurora::gx::enableLodBias ? "On" : "Off";
|
||||
auto lodMutate = [this] {
|
||||
aurora::gx::enableLodBias = !aurora::gx::enableLodBias;
|
||||
m_needsRebuild = true;
|
||||
};
|
||||
auto lodOption = std::make_unique<GameOption>(scroll, "lod-bias", "Enable LOD Bias",
|
||||
lodValue, std::string_view{},
|
||||
queue_activate("lod-bias"));
|
||||
register_row(Row{"lod-bias", lodMutate, [lodMutate](int) { lodMutate(); }},
|
||||
std::move(lodOption));
|
||||
|
||||
add_toggle(scroll, "depth-of-field", "Enable Depth of Field",
|
||||
getSettings().game.enableDepthOfField);
|
||||
add_toggle(scroll, "minimap-shadows", "Enable Mini-Map Shadows",
|
||||
getSettings().game.enableMapBackground);
|
||||
}
|
||||
|
||||
void build_placeholder_tab(Rml::Element* body, std::string_view tabLabel) {
|
||||
auto* wrap = append(body, "div");
|
||||
set_props(wrap, {
|
||||
{"display", "flex"},
|
||||
{"flex-direction", "column"},
|
||||
{"align-items", "center"},
|
||||
{"justify-content", "center"},
|
||||
{"width", "100%"},
|
||||
{"flex-grow", "1"},
|
||||
{"gap", "12dp"},
|
||||
});
|
||||
auto* heading = add_label(wrap, tabLabel, LabelStyle::Large);
|
||||
set_props(heading, {{"text-align", "center"}});
|
||||
auto* sub = add_label(wrap, "Not yet ported.", LabelStyle::Body);
|
||||
set_props(sub, {{"text-align", "center"}, {"color", theme::rgba(theme::TextDim)}});
|
||||
}
|
||||
|
||||
void build_body() {
|
||||
Rml::Element* body = m_window->body();
|
||||
switch (m_tab) {
|
||||
case Tab::Graphics:
|
||||
build_graphics_tab(body);
|
||||
break;
|
||||
default:
|
||||
build_placeholder_tab(body, kTabs[static_cast<size_t>(m_tab)].label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void rebuild() {
|
||||
if (!m_open) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rml::Context* context = aurora::rmlui::get_context();
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string preferredFocus =
|
||||
m_pendingFocusId.empty() ? current_focus_id() : m_pendingFocusId;
|
||||
m_pendingFocusId.clear();
|
||||
|
||||
close_document();
|
||||
|
||||
m_document = context->CreateDocument();
|
||||
if (m_document == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
style_document(m_document);
|
||||
Rml::Element* screen = add_screen();
|
||||
|
||||
m_window = std::make_unique<Window>(screen, "game-menu", [this] { request_close(); });
|
||||
|
||||
for (const TabDef& tab : kTabs) {
|
||||
const std::string tabId = tab.id;
|
||||
m_window->add_tab(tabId, tab.label, [this, tabId] { m_pendingTabId = tabId; });
|
||||
}
|
||||
m_window->set_selected_tab(kTabs[static_cast<size_t>(m_tab)].id);
|
||||
|
||||
build_body();
|
||||
|
||||
m_document->AddEventListener(Rml::EventId::Keydown, this);
|
||||
m_document->Show();
|
||||
|
||||
focus_id(preferredFocus.empty() ? first_focus_id() : preferredFocus);
|
||||
}
|
||||
|
||||
void request_close() { m_requestClose = true; }
|
||||
|
||||
void switch_tab(int direction) {
|
||||
const int count = static_cast<int>(kTabs.size());
|
||||
const int next = (static_cast<int>(m_tab) + direction + count) % count;
|
||||
m_pendingTabId = kTabs[static_cast<size_t>(next)].id;
|
||||
}
|
||||
|
||||
void apply_tab_selection(std::string_view tabId) {
|
||||
for (size_t i = 0; i < kTabs.size(); ++i) {
|
||||
if (tabId == kTabs[i].id) {
|
||||
if (m_tab == static_cast<Tab>(i)) {
|
||||
return;
|
||||
}
|
||||
m_tab = static_cast<Tab>(i);
|
||||
m_pendingFocusId.clear();
|
||||
rebuild();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string current_focus_id() const {
|
||||
if (Rml::Context* context = aurora::rmlui::get_context()) {
|
||||
if (Rml::Element* focused = context->GetFocusElement()) {
|
||||
return focused->GetId();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string first_focus_id() const {
|
||||
return m_focusIds.empty() ? std::string{} : m_focusIds.front();
|
||||
}
|
||||
|
||||
int focus_index() const {
|
||||
const std::string id = current_focus_id();
|
||||
if (id.empty()) {
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(m_focusIds.size()); ++i) {
|
||||
if (m_focusIds[i] == id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void focus_id(std::string_view id) {
|
||||
if (m_document == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!id.empty()) {
|
||||
if (Rml::Element* element = m_document->GetElementById(std::string(id))) {
|
||||
element->Focus(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const std::string fallback = first_focus_id();
|
||||
if (!fallback.empty()) {
|
||||
if (Rml::Element* element = m_document->GetElementById(fallback)) {
|
||||
element->Focus(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void move_focus(int direction) {
|
||||
if (m_focusIds.empty()) {
|
||||
return;
|
||||
}
|
||||
const int index = focus_index();
|
||||
if (index < 0) {
|
||||
focus_id(m_focusIds.front());
|
||||
return;
|
||||
}
|
||||
const int next = index + direction;
|
||||
if (next < 0 || next >= static_cast<int>(m_focusIds.size())) {
|
||||
return;
|
||||
}
|
||||
focus_id(m_focusIds[static_cast<size_t>(next)]);
|
||||
}
|
||||
|
||||
void queue_activate_focused() {
|
||||
const std::string id = current_focus_id();
|
||||
if (!id.empty()) {
|
||||
m_requestedActivation = id;
|
||||
}
|
||||
}
|
||||
|
||||
void queue_cycle_focused(int direction) {
|
||||
const std::string id = current_focus_id();
|
||||
if (!id.empty()) {
|
||||
m_requestedCycleId = id;
|
||||
m_requestedCycleDirection = direction;
|
||||
}
|
||||
}
|
||||
|
||||
bool handle_key(Rml::Input::KeyIdentifier key) {
|
||||
switch (key) {
|
||||
case Rml::Input::KI_UP:
|
||||
move_focus(-1);
|
||||
return true;
|
||||
case Rml::Input::KI_DOWN:
|
||||
move_focus(1);
|
||||
return true;
|
||||
case Rml::Input::KI_LEFT:
|
||||
queue_cycle_focused(-1);
|
||||
return true;
|
||||
case Rml::Input::KI_RIGHT:
|
||||
queue_cycle_focused(1);
|
||||
return true;
|
||||
case Rml::Input::KI_F16:
|
||||
switch_tab(-1);
|
||||
return true;
|
||||
case Rml::Input::KI_F17:
|
||||
switch_tab(1);
|
||||
return true;
|
||||
case Rml::Input::KI_RETURN:
|
||||
queue_activate_focused();
|
||||
return true;
|
||||
case Rml::Input::KI_ESCAPE:
|
||||
case Rml::Input::KI_F15:
|
||||
request_close();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Screen s_screen;
|
||||
|
||||
} // namespace
|
||||
|
||||
bool initialize() {
|
||||
return s_screen.initialize();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
s_screen.shutdown();
|
||||
}
|
||||
|
||||
bool is_active() {
|
||||
return s_screen.is_open();
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
s_screen.toggle();
|
||||
}
|
||||
|
||||
void set_active(bool active) {
|
||||
s_screen.set_open(active);
|
||||
}
|
||||
|
||||
void handle_event(const SDL_Event& event) {
|
||||
s_screen.handle_event(event);
|
||||
}
|
||||
|
||||
void update() {
|
||||
s_screen.update();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui::game_menu
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
namespace dusk::ui::game_menu {
|
||||
|
||||
bool initialize();
|
||||
void shutdown();
|
||||
|
||||
bool is_active();
|
||||
void toggle();
|
||||
void set_active(bool active);
|
||||
|
||||
void handle_event(const SDL_Event& event);
|
||||
void update();
|
||||
|
||||
} // namespace dusk::ui::game_menu
|
||||
Reference in New Issue
Block a user