From 68b2e0ee2d3dbcfd1c544ccc71c5f21a9bbc74f9 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 1 May 2026 16:14:20 -0600 Subject: [PATCH] Rework Settings components --- files.cmake | 2 + src/dusk/ui/bool_button.cpp | 29 ++ src/dusk/ui/bool_button.hpp | 29 ++ src/dusk/ui/button.cpp | 8 +- src/dusk/ui/component.cpp | 1 - src/dusk/ui/component.hpp | 19 ++ src/dusk/ui/editor.cpp | 16 +- src/dusk/ui/number_button.cpp | 16 +- src/dusk/ui/number_button.hpp | 9 + src/dusk/ui/pane.cpp | 44 +-- src/dusk/ui/pane.hpp | 1 - src/dusk/ui/popup.cpp | 1 + src/dusk/ui/select_button.cpp | 20 +- src/dusk/ui/select_button.hpp | 6 - src/dusk/ui/settings.cpp | 531 ++++++++++++++-------------------- src/dusk/ui/string_button.cpp | 13 +- src/dusk/ui/string_button.hpp | 1 + src/dusk/ui/window.cpp | 20 +- 18 files changed, 358 insertions(+), 408 deletions(-) create mode 100644 src/dusk/ui/bool_button.cpp create mode 100644 src/dusk/ui/bool_button.hpp diff --git a/files.cmake b/files.cmake index d7f51cdea9..7c5524e2b6 100644 --- a/files.cmake +++ b/files.cmake @@ -1462,6 +1462,8 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStateShare.cpp src/dusk/imgui/ImGuiAchievements.hpp src/dusk/imgui/ImGuiAchievements.cpp + src/dusk/ui/bool_button.cpp + src/dusk/ui/bool_button.hpp src/dusk/ui/button.cpp src/dusk/ui/button.hpp src/dusk/ui/component.cpp diff --git a/src/dusk/ui/bool_button.cpp b/src/dusk/ui/bool_button.cpp new file mode 100644 index 0000000000..cc43bb8b65 --- /dev/null +++ b/src/dusk/ui/bool_button.cpp @@ -0,0 +1,29 @@ +#include "bool_button.hpp" + +namespace dusk::ui { + +BoolButton::BoolButton(Rml::Element* parent, Props props) + : BaseControlledSelectButton(parent, {std::move(props.key)}), + mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)), + mIsDisabled(std::move(props.isDisabled)) {} + +bool BoolButton::disabled() const { + if (mIsDisabled) { + return mIsDisabled(); + } + return BaseControlledSelectButton::disabled(); +} + +Rml::String BoolButton::format_value() { + return mGetValue() ? "On" : "Off"; +} + +bool BoolButton::handle_nav_command(NavCommand cmd) { + if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) { + mSetValue(!mGetValue()); + return true; + } + return false; +} + +} // namespace dusk::ui \ No newline at end of file diff --git a/src/dusk/ui/bool_button.hpp b/src/dusk/ui/bool_button.hpp new file mode 100644 index 0000000000..750c7e5cdb --- /dev/null +++ b/src/dusk/ui/bool_button.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "select_button.hpp" + +namespace dusk::ui { + +class BoolButton : public BaseControlledSelectButton { +public: + struct Props { + Rml::String key; + std::function getValue; + std::function setValue; + std::function isDisabled; + }; + + BoolButton(Rml::Element* parent, Props props); + + bool disabled() const override; + +protected: + Rml::String format_value() override; + bool handle_nav_command(NavCommand cmd) override; + +private: + std::function mGetValue; + std::function mSetValue; + std::function mIsDisabled; +}; + +} // namespace dusk::ui diff --git a/src/dusk/ui/button.cpp b/src/dusk/ui/button.cpp index 07c6b0682a..a51918004e 100644 --- a/src/dusk/ui/button.cpp +++ b/src/dusk/ui/button.cpp @@ -31,13 +31,13 @@ Button& Button::on_pressed(ButtonCallback callback) { if (!callback) { return *this; } - listen(Rml::EventId::Click, [callback](Rml::Event&) { callback(); }); - listen(Rml::EventId::Keydown, [callback = std::move(callback)](Rml::Event& event) { - const auto cmd = map_nav_event(event); + // TODO: convert this to a FluentComponent method? + on_nav_command([callback = std::move(callback)](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm) { callback(); - event.StopPropagation(); + return true; } + return false; }); return *this; } diff --git a/src/dusk/ui/component.cpp b/src/dusk/ui/component.cpp index 3d6f54e591..466420eb1e 100644 --- a/src/dusk/ui/component.cpp +++ b/src/dusk/ui/component.cpp @@ -43,7 +43,6 @@ void Component::set_selected(bool value) { return; } mRoot->SetPseudoClass("selected", value); - mRoot->DispatchEvent(Rml::EventId::Change, {{"selected", Rml::Variant{value}}}); } void Component::set_disabled(bool value) { diff --git a/src/dusk/ui/component.hpp b/src/dusk/ui/component.hpp index 86e6bbf608..d955b58c0a 100644 --- a/src/dusk/ui/component.hpp +++ b/src/dusk/ui/component.hpp @@ -1,6 +1,7 @@ #pragma once #include "event.hpp" +#include "ui.hpp" #include @@ -73,6 +74,24 @@ public: } }); } + + Derived& on_nav_command(std::function callback) { + listen(Rml::EventId::Click, [this, callback](Rml::Event& event) { + if (!disabled() && callback(event, NavCommand::Confirm)) { + event.StopPropagation(); + } + }); + listen(Rml::EventId::Keydown, [this, callback = std::move(callback)](Rml::Event& event) { + if (disabled()) { + return; + } + const auto cmd = map_nav_event(event); + if (cmd != NavCommand::None && callback(event, cmd)) { + event.StopPropagation(); + } + }); + return static_cast(*this); + } }; } // namespace dusk::ui diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp index 5338bba068..c212eaceb0 100644 --- a/src/dusk/ui/editor.cpp +++ b/src/dusk/ui/editor.cpp @@ -3,6 +3,7 @@ #include #include +#include "bool_button.hpp" #include "button.hpp" #include "d/actor/d_a_player.h" #include "d/d_kankyo.h" @@ -1899,17 +1900,12 @@ EditorWindow::EditorWindow() { auto& rightPane = add_child(content, Pane::Type::Uncontrolled); leftPane.add_section("Options"); - // TODO: replace with generic bool component based on ConfigBoolSelect leftPane - .add_button( - { - .text = "Enable Vibration", - .isSelected = [] { return get_player_config()->getVibration() != 0; }, - }, - [] { - const bool enabled = get_player_config()->getVibration() != 0; - get_player_config()->setVibration(enabled ? 0 : 1); - }) + .add_child(BoolButton::Props{ + .key = "Enable Vibration", + .getValue = [] { return get_player_config()->getVibration() != 0; }, + .setValue = [](bool value) { get_player_config()->setVibration(value); }, + }) .on_focus([&rightPane](Rml::Event&) { rightPane.clear(); }); leftPane .add_select_button({ diff --git a/src/dusk/ui/number_button.cpp b/src/dusk/ui/number_button.cpp index c94afede1a..7a7488207a 100644 --- a/src/dusk/ui/number_button.cpp +++ b/src/dusk/ui/number_button.cpp @@ -7,10 +7,22 @@ namespace dusk::ui { NumberButton::NumberButton(Rml::Element* parent, Props props) : BaseStringButton(parent, {.key = std::move(props.key), .type = "number"}), - mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)), mMin(props.min), - mMax(props.max), mStep(props.step) {} + mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)), + mIsDisabled(std::move(props.isDisabled)), mMin(props.min), mMax(props.max), mStep(props.step), + mPrefix(std::move(props.prefix)), mSuffix(std::move(props.suffix)) {} + +bool NumberButton::disabled() const { + if (mIsDisabled) { + return mIsDisabled(); + } + return BaseStringButton::disabled(); +} Rml::String NumberButton::format_value() { + return fmt::format("{}{}{}", mPrefix, mGetValue(), mSuffix); +} + +Rml::String NumberButton::input_value() { return fmt::to_string(mGetValue()); } diff --git a/src/dusk/ui/number_button.hpp b/src/dusk/ui/number_button.hpp index a2665e24ac..d7b6bbc939 100644 --- a/src/dusk/ui/number_button.hpp +++ b/src/dusk/ui/number_button.hpp @@ -10,24 +10,33 @@ public: Rml::String key; std::function getValue; std::function setValue; + std::function isDisabled; int min = 0; int max = INT_MAX; int step = 1; + Rml::String prefix; + Rml::String suffix; }; NumberButton(Rml::Element* parent, Props props); + bool disabled() const override; + protected: Rml::String format_value() override; + Rml::String input_value() override; void set_value(Rml::String value) override; bool handle_nav_command(NavCommand cmd) override; private: std::function mGetValue; std::function mSetValue; + std::function mIsDisabled; int mMin; int mMax; int mStep; + Rml::String mPrefix; + Rml::String mSuffix; }; } // namespace dusk::ui \ No newline at end of file diff --git a/src/dusk/ui/pane.cpp b/src/dusk/ui/pane.cpp index 722d022ff6..64592cb42e 100644 --- a/src/dusk/ui/pane.cpp +++ b/src/dusk/ui/pane.cpp @@ -64,28 +64,19 @@ Pane::Pane(Rml::Element* parent, Type type) : FluentComponent(createRoot(parent) }); if (type == Type::Controlled) { - // Listen for selection change events - listen(Rml::EventId::Change, [this](Rml::Event& event) { - const auto it = std::find_if(event.GetParameters().begin(), event.GetParameters().end(), - [](const auto& param) { return param.first == "selected"; }); - if (it != event.GetParameters().end()) { - const auto selected = it->second.Get(); - int childIndex = -1; - for (int i = 0; i < mChildren.size(); ++i) { - if (event.GetTargetElement() == mChildren[i]->root()) { - childIndex = i; - } - } - if (childIndex != -1) { - if (selected) { - set_selected_item(childIndex); - } else if (childIndex == mSelectedItem) { - set_selected_item(-1); - } - } else { - set_selected_item(-1); + // For controlled panes, handle SelectButton Submit events for item selection + listen(Rml::EventId::Submit, [this](Rml::Event& event) { + int childIndex = -1; + for (int i = 0; i < mChildren.size(); ++i) { + if (event.GetTargetElement() == mChildren[i]->root()) { + childIndex = i; } } + set_selected_item(childIndex); + // If the selection was handled locally, don't allow it to bubble up to window + if (event.GetParameter("handled", false)) { + event.StopPropagation(); + } }); } } @@ -96,17 +87,11 @@ void Pane::update() { } void Pane::set_selected_item(int index) { - if (mSelectedItem == index) { + if (mType == Type::Uncontrolled) { return; } - if (mSelectedItem >= 0 && mSelectedItem < mChildren.size()) { - mChildren[mSelectedItem]->set_selected(false); - } - if (index >= 0 && index < mChildren.size()) { - mSelectedItem = index; - mChildren[index]->set_selected(true); - } else { - mSelectedItem = -1; + for (int i = 0; i < mChildren.size(); ++i) { + mChildren[i]->set_selected(i == index); } } @@ -117,6 +102,7 @@ bool Pane::focus() { return true; } } + // Otherwise, focus the first focusable child for (const auto& child : mChildren) { if (child->focus()) { return true; diff --git a/src/dusk/ui/pane.hpp b/src/dusk/ui/pane.hpp index ffa9d31ff0..659f69cf26 100644 --- a/src/dusk/ui/pane.hpp +++ b/src/dusk/ui/pane.hpp @@ -46,7 +46,6 @@ public: private: Type mType; bool finalized = false; - int mSelectedItem = -1; }; } // namespace dusk::ui diff --git a/src/dusk/ui/popup.cpp b/src/dusk/ui/popup.cpp index 53aeb6dafb..18f961e1a9 100644 --- a/src/dusk/ui/popup.cpp +++ b/src/dusk/ui/popup.cpp @@ -39,6 +39,7 @@ Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("pop mTabBar->add_tab("Reset", [this] { JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; mTabBar->set_active_tab(-1); + hide(); }); mTabBar->add_tab("Exit", [] { IsRunning = false; }); diff --git a/src/dusk/ui/select_button.cpp b/src/dusk/ui/select_button.cpp index 7e8dd894b3..95cffd873d 100644 --- a/src/dusk/ui/select_button.cpp +++ b/src/dusk/ui/select_button.cpp @@ -20,23 +20,7 @@ SelectButton::SelectButton(Rml::Element* parent, Props props) mKeyElem = append(mRoot, "key"); mValueElem = append(mRoot, "value"); update_props(std::move(props)); - listen(Rml::EventId::Click, [this](Rml::Event& event) { - if (disabled()) { - return; - } - if (handle_nav_command(NavCommand::Confirm)) { - event.StopPropagation(); - } - }); - listen(Rml::EventId::Keydown, [this](Rml::Event& event) { - if (disabled()) { - return; - } - const auto cmd = map_nav_event(event); - if (cmd != NavCommand::None && handle_nav_command(cmd)) { - event.StopPropagation(); - } - }); + on_nav_command([this](Rml::Event&, NavCommand cmd) { return handle_nav_command(cmd); }); } void SelectButton::set_value_label(const Rml::String& value) { @@ -56,7 +40,7 @@ void SelectButton::update_props(Props props) { bool SelectButton::handle_nav_command(NavCommand cmd) { if (cmd == NavCommand::Confirm) { - set_selected(!selected()); + mRoot->DispatchEvent(Rml::EventId::Submit, {}); return true; } return false; diff --git a/src/dusk/ui/select_button.hpp b/src/dusk/ui/select_button.hpp index b45f1db508..4a9612076d 100644 --- a/src/dusk/ui/select_button.hpp +++ b/src/dusk/ui/select_button.hpp @@ -16,12 +16,6 @@ public: void set_value_label(const Rml::String& value); - SelectButton& on_selected(std::function callback) { - return listen(Rml::EventId::Change, [callback = std::move(callback)](Rml::Event& event) { - callback(event.GetParameter("selected", false)); - }); - } - protected: void update_props(Props props); virtual bool handle_nav_command(NavCommand cmd); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index a842202ca1..fc3935aa74 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -3,16 +3,16 @@ #include #include "aurora/gfx.h" +#include "bool_button.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/livesplit.h" #include "m_Do/m_Do_main.h" +#include "number_button.hpp" #include "overlay.hpp" #include "pane.hpp" #include "ui.hpp" -#include - namespace dusk::ui { namespace { @@ -42,192 +42,46 @@ void reset_for_speedrun_mode() { getSettings().game.enableTurboKeybind.setValue(false); } -} // namespace +const Rml::String kInternalResolutionHelpText = + "Configure the resolution used for rendering the game. Higher values are more demanding on " + "your graphics hardware."; +const Rml::String kShadowResolutionHelpText = + "Configure the shadow-map resolution. Higher values improve shadow quality but increase GPU " + "and memory usage."; -template -struct ConfigProps { +struct ConfigBoolProps { Rml::String key; - ConfigVar* value; - std::function onChange; - std::function isDisabled; Rml::String helpText; - Pane* rightPane = nullptr; + std::function onChange; + std::function isDisabled; }; -template -class ConfigSelect : public SelectButton { -public: - using Props = ConfigProps; +SelectButton& config_bool_select( + Pane& leftPane, Pane& rightPane, ConfigVar& var, ConfigBoolProps props) { + return leftPane + .add_child(BoolButton::Props{ + .key = std::move(props.key), + .getValue = [&var] { return var.getValue(); }, + .setValue = + [&var, callback = std::move(props.onChange)](bool value) { + if (value == var.getValue()) { + return; + } + var.setValue(value); + config::Save(); + if (callback) { + callback(value); + } + }, + .isDisabled = std::move(props.isDisabled), + }) + .on_focus([&rightPane, helpText = std::move(props.helpText)](Rml::Event&) { + rightPane.clear(); + rightPane.add_rml(helpText); + }); +} - ConfigSelect(Rml::Element* parent, Props props) - : SelectButton(parent, {std::move(props.key)}), mVar(props.value), - mOnChange(std::move(props.onChange)), mIsDisabled(std::move(props.isDisabled)), - mHelpText(std::move(props.helpText)), mRightPane(props.rightPane) { - if (!mHelpText.empty() && mRightPane != nullptr) { - listen(Rml::EventId::Focus, [this](Rml::Event&) { - mRightPane->clear(); - mRightPane->add_rml(mHelpText); - }); - listen(Rml::EventId::Mouseover, [this](Rml::Event&) { - mRightPane->clear(); - mRightPane->add_rml(mHelpText); - }); - } - } - - void update() override { - if (mIsDisabled) { - set_disabled(mIsDisabled()); - } - set_value_label(get_value()); - SelectButton::update(); - } - -protected: - virtual Rml::String get_value() = 0; - - void set_value(T newValue) { - if (mVar->getValue() == newValue) { - return; - } - mVar->setValue(newValue); - if (mOnChange) { - mOnChange(newValue); - } - config::Save(); - } - - ConfigVar* mVar; - std::function mOnChange; - std::function mIsDisabled; - Rml::String mHelpText; - Pane* mRightPane; -}; - -class ConfigBoolSelect : public ConfigSelect { -public: - ConfigBoolSelect(Rml::Element* parent, Props props) : ConfigSelect(parent, std::move(props)) {} - -protected: - Rml::String get_value() override { return mVar->getValue() ? "On" : "Off"; } - bool handle_nav_command(NavCommand cmd) override { - if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) { - set_value(!mVar->getValue()); - return true; - } - return false; - } -}; - -class ConfigIntSelect : public ConfigSelect { -public: - struct Props { - Rml::String key; - ConfigVar* value; - std::function onChange; - std::function isDisabled; - Rml::String helpText; - Pane* rightPane = nullptr; - int min = 0; - int max = INT_MAX; - int step = 1; - Rml::String prefix; - Rml::String suffix; - }; - - ConfigIntSelect(Rml::Element* parent, Props props) - : ConfigSelect(parent, - { - .key = std::move(props.key), - .value = props.value, - .onChange = std::move(props.onChange), - .isDisabled = std::move(props.isDisabled), - .helpText = std::move(props.helpText), - .rightPane = props.rightPane, - }), - mMin(props.min), mMax(props.max), mStep(props.step), mPrefix(std::move(props.prefix)), - mSuffix(std::move(props.suffix)) {} - -protected: - Rml::String get_value() override { - return fmt::format("{}{}{}", mPrefix, mVar->getValue(), mSuffix); - } - - bool handle_nav_command(NavCommand cmd) override { - if (cmd == NavCommand::Left) { - set_value(std::clamp(mVar->getValue() - mStep, mMin, mMax)); - return true; - } else if (cmd == NavCommand::Right) { - set_value(std::clamp(mVar->getValue() + mStep, mMin, mMax)); - return true; - } - return false; - } - -private: - int mMin; - int mMax; - int mStep; - Rml::String mPrefix; - Rml::String mSuffix; -}; - -class ConfigOverlaySelect : public ConfigSelect { -public: - struct Props { - Rml::String key; - ConfigVar* value; - GraphicsOption option; - Rml::String title; - int min = 0; - int max = 0; - Rml::String helpText; - Pane* rightPane = nullptr; - std::function isDisabled; - }; - - ConfigOverlaySelect(Rml::Element* parent, Props props) - : ConfigSelect(parent, - { - .key = std::move(props.key), - .value = props.value, - .onChange = {}, - .isDisabled = std::move(props.isDisabled), - .helpText = std::move(props.helpText), - .rightPane = props.rightPane, - }), - mOption(props.option), mTitle(std::move(props.title)), mValueMin(props.min), - mValueMax(props.max) {} - -protected: - Rml::String get_value() override { - return format_graphics_setting_value(mOption, mVar->getValue()); - } - - bool handle_nav_command(NavCommand cmd) override { - if (cmd == NavCommand::Confirm) { - open_overlay(); - return true; - } - return false; - } - -private: - void open_overlay() { - push_document(std::make_unique(OverlayProps{ - .option = mOption, - .title = mTitle, - .helpText = mHelpText, - .valueMin = mValueMin, - .valueMax = mValueMax, - })); - } - - GraphicsOption mOption; - Rml::String mTitle; - int mValueMin = 0; - int mValueMax = 0; -}; +} // namespace SettingsWindow::SettingsWindow() { add_tab("Audio", [this](Rml::Element* content) { @@ -235,38 +89,43 @@ SettingsWindow::SettingsWindow() { auto& rightPane = add_child(content, Pane::Type::Uncontrolled); leftPane.add_section("Volume"); - leftPane.add_child(ConfigIntSelect::Props{ - .key = "Master Volume", - .value = &getSettings().audio.masterVolume, - .onChange = [](int value) { audio::SetMasterVolume(value / 100.f); }, - .helpText = "Adjusts the volume of all sounds in the game.", - .rightPane = &rightPane, - .max = 100, - .suffix = "%", - }); + leftPane + .add_child(NumberButton::Props{ + .key = "Master Volume", + .getValue = [] { return getSettings().audio.masterVolume.getValue(); }, + .setValue = + [](int value) { + getSettings().audio.masterVolume.setValue(value); + config::Save(); + audio::SetMasterVolume(value / 100.f); + }, + .max = 100, + .suffix = "%", + }) + .on_focus([&rightPane](Rml::Event&) { + rightPane.clear(); + rightPane.add_text("Adjusts the volume of all sounds in the game."); + }); leftPane.add_section("Effects"); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "Enable Reverb", - .value = &getSettings().audio.enableReverb, - .onChange = [](bool value) { audio::SetEnableReverb(value); }, - .helpText = "Enables the reverb effect in game audio.", - .rightPane = &rightPane, - }); + config_bool_select(leftPane, rightPane, getSettings().audio.enableReverb, + { + .key = "Enable Reverb", + .helpText = "Enables the reverb effect in game audio.", + .onChange = [](bool value) { audio::SetEnableReverb(value); }, + }); leftPane.add_section("Tweaks"); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "No Low HP Sound", - .value = &getSettings().game.noLowHpSound, - .helpText = "Disable the beeping sound when having low health.", - .rightPane = &rightPane, - }); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "Non-Stop Midna's Lament", - .value = &getSettings().game.midnasLamentNonStop, - .helpText = "Prevents enemy music while Midna's Lament is playing.", - .rightPane = &rightPane, - }); + config_bool_select(leftPane, rightPane, getSettings().game.noLowHpSound, + { + .key = "No Low HP Sound", + .helpText = "Disable the beeping sound when having low health.", + }); + config_bool_select(leftPane, rightPane, getSettings().game.midnasLamentNonStop, + { + .key = "Non-Stop Midna's Lament", + .helpText = "Prevents enemy music while Midna's Lament is playing.", + }); }); add_tab("Cheats", [this](Rml::Element* content) { @@ -275,13 +134,12 @@ SettingsWindow::SettingsWindow() { auto addCheat = [&](const Rml::String& key, ConfigVar& value, const Rml::String& helpText) { - leftPane.add_child(ConfigBoolSelect::Props{ - .key = key, - .value = &value, - .isDisabled = [] { return getSettings().game.speedrunMode; }, - .helpText = helpText, - .rightPane = &rightPane, - }); + config_bool_select(leftPane, rightPane, value, + { + .key = key, + .helpText = helpText, + .isDisabled = [] { return getSettings().game.speedrunMode; }, + }); }; leftPane.add_section("Resources"); @@ -320,23 +178,20 @@ SettingsWindow::SettingsWindow() { auto addOption = [&](const Rml::String& key, ConfigVar& value, const Rml::String& helpText) { - leftPane.add_child(ConfigBoolSelect::Props{ - .key = key, - .value = &value, - .helpText = helpText, - .rightPane = &rightPane, - }); + config_bool_select(leftPane, rightPane, value, + { + .key = key, + .helpText = helpText, + }); }; - auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar& value, const Rml::String& helpText) { - leftPane.add_child(ConfigBoolSelect::Props{ - .key = key, - .value = &value, - .isDisabled = [] { return getSettings().game.speedrunMode; }, - .helpText = helpText, - .rightPane = &rightPane, - }); + config_bool_select(leftPane, rightPane, value, + { + .key = key, + .helpText = helpText, + .isDisabled = [] { return getSettings().game.speedrunMode; }, + }); }; leftPane.add_section("General"); @@ -351,16 +206,24 @@ SettingsWindow::SettingsWindow() { "Enables rotating Link in the collection menu with the C-Stick."); leftPane.add_section("Difficulty"); - leftPane.add_child(ConfigIntSelect::Props{ - .key = "Damage Multiplier", - .value = &getSettings().game.damageMultiplier, - .isDisabled = [] { return getSettings().game.speedrunMode; }, - .helpText = "Multiplies incoming damage.", - .rightPane = &rightPane, - .min = 1, - .max = 8, - .prefix = "x", - }); + leftPane + .add_child(NumberButton::Props{ + .key = "Damage Multiplier", + .getValue = [] { return getSettings().game.damageMultiplier.getValue(); }, + .setValue = + [](int value) { + getSettings().game.damageMultiplier.setValue(value); + config::Save(); + }, + .isDisabled = [] { return getSettings().game.speedrunMode; }, + .min = 1, + .max = 8, + .prefix = "x", + }) + .on_focus([&rightPane](Rml::Event&) { + rightPane.clear(); + rightPane.add_text("Multiplies incoming damage."); + }); addSpeedrunDisabledOption( "Instant Death", getSettings().game.instantDeath, "Any hit will instantly kill you."); addSpeedrunDisabledOption("No Heart Drops", getSettings().game.noHeartDrops, @@ -396,29 +259,27 @@ SettingsWindow::SettingsWindow() { "Transform instantly by pressing R and Y simultaneously."); leftPane.add_section("Speedrunning"); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "Speedrun Mode", - .value = &getSettings().game.speedrunMode, - .onChange = [](bool) { reset_for_speedrun_mode(); }, - .helpText = "Enables speedrunning options while restricting certain " - "gameplay modifiers.", - .rightPane = &rightPane, - }); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "LiveSplit Connection", - .value = &getSettings().game.liveSplitEnabled, - .onChange = - [](bool enabled) { - if (enabled) { - speedrun::connectLiveSplit(); - } else { - speedrun::disconnectLiveSplit(); - } - }, - .isDisabled = [] { return !getSettings().game.speedrunMode; }, - .helpText = "Connect to LiveSplit server on localhost:16834.", - .rightPane = &rightPane, - }); + config_bool_select(leftPane, rightPane, getSettings().game.speedrunMode, + { + .key = "Speedrun Mode", + .helpText = + "Enables speedrunning options while restricting certain gameplay modifiers.", + .onChange = [](bool) { reset_for_speedrun_mode(); }, + }); + config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled, + { + .key = "LiveSplit Connection", + .helpText = "Connect to LiveSplit server on localhost:16834.", + .onChange = + [](bool enabled) { + if (enabled) { + speedrun::connectLiveSplit(); + } else { + speedrun::disconnectLiveSplit(); + } + }, + .isDisabled = [] { return !getSettings().game.speedrunMode; }, + }); }); add_tab("Graphics", [this](Rml::Element* content) { @@ -438,71 +299,99 @@ SettingsWindow::SettingsWindow() { VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); VICenterWindow(); }); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "Enable VSync", - .value = &getSettings().video.enableVsync, - .onChange = [](bool value) { aurora_enable_vsync(value); }, - .helpText = "Synchronizes the frame rate to your monitor's refresh rate.", - .rightPane = &rightPane, - }); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "Lock 4:3 Aspect Ratio", - .value = &getSettings().video.lockAspectRatio, - .onChange = - [](bool value) { - AuroraSetViewportPolicy(value ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH); - }, - .helpText = "Lock the game's aspect ratio to the original.", - .rightPane = &rightPane, - }); + config_bool_select(leftPane, rightPane, getSettings().video.enableVsync, + { + .key = "Enable VSync", + .helpText = "Synchronizes the frame rate to your monitor's refresh rate.", + .onChange = [](bool value) { aurora_enable_vsync(value); }, + }); + config_bool_select(leftPane, rightPane, getSettings().video.lockAspectRatio, + { + .key = "Lock 4:3 Aspect Ratio", + .helpText = "Lock the game's aspect ratio to the original.", + .onChange = + [](bool value) { + AuroraSetViewportPolicy( + value ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH); + }, + }); leftPane.add_section("Resolution"); - leftPane.add_child(ConfigOverlaySelect::Props{ - .key = "Internal Resolution", - .value = &getSettings().game.internalResolutionScale, - .option = GraphicsOption::InternalResolution, - .title = "Internal Resolution", - .min = 0, - .max = 12, - .helpText = - "Configure the resolution used for rendering the game. Higher values are more " - "demanding on your graphics hardware.", - .rightPane = &rightPane, - }); - leftPane.add_child(ConfigOverlaySelect::Props{ - .key = "Shadow Resolution", - .value = &getSettings().game.shadowResolutionMultiplier, - .option = GraphicsOption::ShadowResolution, - .title = "Shadow Resolution", - .min = 1, - .max = 8, - .helpText = - "Configure the shadow-map resolution. Higher values improve shadow quality but " - "increase GPU and memory usage.", - .rightPane = &rightPane, - }); + leftPane + .add_select_button({ + .key = "Internal Resolution", + .getValue = + [] { + return format_graphics_setting_value(GraphicsOption::InternalResolution, + getSettings().game.internalResolutionScale.getValue()); + }, + }) + .on_nav_command([](Rml::Event&, NavCommand cmd) { + if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || + cmd == NavCommand::Right) { + push_document(std::make_unique(OverlayProps{ + .option = GraphicsOption::InternalResolution, + .title = "Internal Resolution", + .helpText = kInternalResolutionHelpText, + .valueMin = 0, + .valueMax = 12, + })); + return true; + } + return false; + }) + .on_focus([&rightPane](Rml::Event&) { + rightPane.clear(); + rightPane.add_text(kInternalResolutionHelpText); + }); + leftPane + .add_select_button({ + .key = "Shadow Resolution", + .getValue = + [] { + return format_graphics_setting_value(GraphicsOption::ShadowResolution, + getSettings().game.shadowResolutionMultiplier.getValue()); + }, + }) + .on_nav_command([](Rml::Event&, NavCommand cmd) { + if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || + cmd == NavCommand::Right) { + push_document(std::make_unique(OverlayProps{ + .option = GraphicsOption::ShadowResolution, + .title = "Shadow Resolution", + .helpText = kShadowResolutionHelpText, + .valueMin = 0, + .valueMax = 8, + })); + return true; + } + return false; + }) + .on_focus([&rightPane](Rml::Event&) { + rightPane.clear(); + rightPane.add_text(kShadowResolutionHelpText); + }); leftPane.add_section("Post-Processing"); // TODO: Bloom // TODO: Bloom Brightness leftPane.add_section("Rendering"); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "Unlock Framerate", - .value = &getSettings().game.enableFrameInterpolation, - .helpText = - "Uses inter-frame interpolation to enable higher frame rates.

Visual " - "artifacts, animation glitches, or instability may occur.", - .rightPane = &rightPane, - }); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "Enable Depth of Field", - .value = &getSettings().game.enableDepthOfField, - }); - leftPane.add_child(ConfigBoolSelect::Props{ - .key = "Enable Mini-Map Shadows", - .value = &getSettings().game.enableMapBackground, - }); + config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation, + { + .key = "Unlock Framerate", + .helpText = + "Uses inter-frame interpolation to enable higher frame rates.

Visual " + "artifacts, animation glitches, or instability may occur.", + }); + config_bool_select(leftPane, rightPane, getSettings().game.enableDepthOfField, + { + .key = "Enable Depth of Field", + }); + config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground, + { + .key = "Enable Mini-Map Shadows", + }); }); } diff --git a/src/dusk/ui/string_button.cpp b/src/dusk/ui/string_button.cpp index 53a405df61..a267e5b0e0 100644 --- a/src/dusk/ui/string_button.cpp +++ b/src/dusk/ui/string_button.cpp @@ -36,7 +36,7 @@ void BaseStringButton::start_editing() { return; } mInputElem->SetAttribute("type", mType); - mInputElem->SetAttribute("value", format_value()); + mInputElem->SetAttribute("value", input_value()); if (mMaxLength > -1) { mInputElem->SetAttribute("maxlength", mMaxLength); } @@ -49,8 +49,9 @@ void BaseStringButton::start_editing() { // mobile keyboard placement gets a valid caret rectangle. mPendingInputFocusFrames = 2; - // Mark button as selected to indicate "active" - set_selected(true); + // Dispatch a submit event so the pane can handle item selection + // However, mark it as "handled" to ensure that we don't steal focus away + mRoot->DispatchEvent(Rml::EventId::Submit, {{"handled", Rml::Variant{true}}}); // Register input listeners mInputListeners.emplace_back(std::make_unique( @@ -85,8 +86,10 @@ bool BaseStringButton::handle_nav_command(NavCommand cmd) { } return true; } else if (cmd == NavCommand::Cancel) { - request_stop_editing(false, true); - return true; + if (mInputElem != nullptr) { + request_stop_editing(false, true); + return true; + } } return false; } diff --git a/src/dusk/ui/string_button.hpp b/src/dusk/ui/string_button.hpp index 30c391ed43..d84b977f37 100644 --- a/src/dusk/ui/string_button.hpp +++ b/src/dusk/ui/string_button.hpp @@ -22,6 +22,7 @@ public: protected: bool handle_nav_command(NavCommand cmd) override; virtual void set_value(Rml::String value) = 0; + virtual Rml::String input_value() { return format_value(); } private: void focus_input(); diff --git a/src/dusk/ui/window.cpp b/src/dusk/ui/window.cpp index 65350dde7c..e1ccb77e34 100644 --- a/src/dusk/ui/window.cpp +++ b/src/dusk/ui/window.cpp @@ -73,19 +73,17 @@ Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("w }); // If an item is selected in a pane, focus the next pane in the tree - listen(mRoot, Rml::EventId::Change, [this](Rml::Event& event) { - if (event.GetParameter("selected", false)) { - int paneIndex = -1; - for (int i = 0; i < mContentComponents.size(); i++) { - if (mContentComponents[i]->contains(event.GetTargetElement())) { - paneIndex = i; - break; - } - } - if (paneIndex >= 0 && paneIndex < mContentComponents.size() - 1) { - mContentComponents[paneIndex + 1]->focus(); + listen(mRoot, Rml::EventId::Submit, [this](Rml::Event& event) { + int paneIndex = -1; + for (int i = 0; i < mContentComponents.size(); i++) { + if (mContentComponents[i]->contains(event.GetTargetElement())) { + paneIndex = i; + break; } } + if (paneIndex >= 0 && paneIndex < mContentComponents.size() - 1) { + mContentComponents[paneIndex + 1]->focus(); + } }); }