diff --git a/extern/aurora b/extern/aurora index 41d5c9c5a2..0d05404564 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 41d5c9c5a2462f57105356ae578d9246e37f6ef2 +Subproject commit 0d054045646dbc0033a768243ccc160ab02214c1 diff --git a/files.cmake b/files.cmake index 26f810861b..764c79e5ae 100644 --- a/files.cmake +++ b/files.cmake @@ -1471,6 +1471,8 @@ set(DUSK_FILES src/dusk/ui/button.hpp src/dusk/ui/component.cpp src/dusk/ui/component.hpp + src/dusk/ui/controller_config.cpp + src/dusk/ui/controller_config.hpp src/dusk/ui/document.cpp src/dusk/ui/document.hpp src/dusk/ui/editor.cpp diff --git a/include/d/actor/d_a_obj_klift00.h b/include/d/actor/d_a_obj_klift00.h index 865fab8c47..ab5403d51b 100644 --- a/include/d/actor/d_a_obj_klift00.h +++ b/include/d/actor/d_a_obj_klift00.h @@ -25,6 +25,10 @@ public: int Draw(); int Delete(); +#if TARGET_PC + void onInterpCallback(); +#endif + enum Param_e { LOCK_e = (1 << 6), NO_BASE_DISP = (1 << 7) }; @@ -50,6 +54,13 @@ private: /* 0x1020 */ dCcD_Cyl mCylinderCollider; /* 0x115C */ s32 mStopSwingingFrames; +#if TARGET_PC + cXyz mChainInterpPrev[64]; + cXyz mChainInterpCurr[64]; + bool mChainInterpPrevValid; + bool mChainInterpCurrValid; +#endif + // Number of chain models u32 getArg0() { return fopAcM_GetParamBit(this, 0, 6); diff --git a/res/FiraSans-Bold.ttf b/res/FiraSans-Bold.ttf new file mode 100644 index 0000000000..e3593fb0f3 Binary files /dev/null and b/res/FiraSans-Bold.ttf differ diff --git a/res/MaterialSymbolsRounded-Regular.ttf b/res/MaterialSymbolsRounded-Regular.ttf new file mode 100644 index 0000000000..782fc0a406 Binary files /dev/null and b/res/MaterialSymbolsRounded-Regular.ttf differ diff --git a/res/rml/tabbing.rcss b/res/rml/tabbing.rcss index e223a4cfad..194ca89f05 100644 --- a/res/rml/tabbing.rcss +++ b/res/rml/tabbing.rcss @@ -1,10 +1,21 @@ tab-bar { display: flex; + position: relative; min-width: 0; overflow: auto hidden; + clip: always; text-transform: uppercase; } +tab-bar scrollbarhorizontal, +tab-bar scrollbarhorizontal sliderarrowdec, +tab-bar scrollbarhorizontal sliderarrowinc, +tab-bar scrollbarhorizontal slidertrack, +tab-bar scrollbarhorizontal sliderbar { + width: 0; + height: 0; +} + tab-bar tab { flex: 0 0 auto; padding: 0 24dp; @@ -31,3 +42,40 @@ tab-bar tab:hover { tab-bar tab:active { decorator: vertical-gradient(#c2a42d10 #c2a42d40); } + +tab-bar[closable] tab-end-spacer { + display: block; + flex: 0 0 64dp; + width: 64dp; + pointer-events: none; +} + +tab-bar[closable] close { + display: block; + position: fixed; + top: 8dp; + right: 8dp; + z-index: 1; + width: 48dp; + height: 48dp; + font-family: "Material Symbols Rounded"; + font-weight: normal; + font-size: 24dp; + color: rgba(224, 219, 200, 70%); + backdrop-filter: blur(2dp); + border-radius: 6dp; + decorator: text("" center center); + transition: color background-color 0.12s linear-in-out; + cursor: pointer; +} + +tab-bar[closable] close:hover, +tab-bar[closable] close:focus-visible { + color: #fff; + background-color: rgba(194, 164, 45, 24%); +} + +tab-bar[closable] close:active { + color: #fff; + background-color: rgba(194, 164, 45, 40%); +} diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 197711f2a0..17e4dec6f3 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -233,6 +233,10 @@ select-button value { font-size: 20dp; } +select-button value.modified { + font-weight: bold; +} + select-button input { text-align: right; font-size: 20dp; diff --git a/src/d/actor/d_a_npc_toby.cpp b/src/d/actor/d_a_npc_toby.cpp index 543d4b9abf..d175d199b7 100644 --- a/src/d/actor/d_a_npc_toby.cpp +++ b/src/d/actor/d_a_npc_toby.cpp @@ -14,6 +14,7 @@ #include "d/actor/d_a_obj_automata.h" #include "d/d_msg_object.h" #include "d/actor/d_a_obj_scannon.h" +#include "dusk/frame_interpolation.h" #include const daNpc_Toby_HIOParam daNpc_Toby_Param_c::m = { @@ -1398,6 +1399,7 @@ int daNpc_Toby_c::cutRepairSCannon(int arg0) { old.pos = current.pos; setAngle(cM_deg2s(5.0f * f32(mPath.getArg0()))); mEventTimer = mPath.getArg2(); + dusk::frame_interp::request_presentation_sync(); } } else if (!mHide) { mHide = 1; diff --git a/src/d/actor/d_a_obj_klift00.cpp b/src/d/actor/d_a_obj_klift00.cpp index 99b204592e..e44cce7467 100644 --- a/src/d/actor/d_a_obj_klift00.cpp +++ b/src/d/actor/d_a_obj_klift00.cpp @@ -11,6 +11,8 @@ #include "d/d_bg_w.h" #include "d/d_cc_uty.h" #include "d/d_com_inf_game.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" struct daObjKLift00_HIO_c : public mDoHIO_entry_c { daObjKLift00_HIO_c(); @@ -295,6 +297,11 @@ int daObjKLift00_c::Create() { if(getLock()) mStopSwingingFrames = 5; +#if TARGET_PC + mChainInterpPrevValid = false; + mChainInterpCurrValid = false; +#endif + return 1; } @@ -436,6 +443,34 @@ int daObjKLift00_c::Execute(Mtx** i_mtx) { return 1; } +#if TARGET_PC +static void klift00_interp_callback(bool isSimFrame, void* pUserWork) { + static_cast(pUserWork)->onInterpCallback(); +} + +void daObjKLift00_c::onInterpCallback() { + if (!mChainInterpPrevValid || !mChainInterpCurrValid) { + return; + } + + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz savedPositions[64]; + + for (int i = 0; i < mNumChains; i++) { + savedPositions[i] = mChainPositions[i].mCurrentPos; + const cXyz& p0 = mChainInterpPrev[i]; + const cXyz& p1 = mChainInterpCurr[i]; + mChainPositions[i].mCurrentPos = p0 + (p1 - p0) * alpha; + } + + setMtx(); + + for (int i = 0; i < mNumChains; i++) { + mChainPositions[i].mCurrentPos = savedPositions[i]; + } +} +#endif + int daObjKLift00_c::Draw() { g_env_light.settingTevStruct(16, ¤t.pos, &tevStr); g_env_light.setLightTevColorType_MAJI(mpLiftPlatform, &tevStr); @@ -457,6 +492,22 @@ int daObjKLift00_c::Draw() { dComIfGd_setList(); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (mChainInterpCurrValid) { + memcpy(mChainInterpPrev, mChainInterpCurr, mNumChains * sizeof(cXyz)); + mChainInterpPrevValid = true; + } + + for (int i = 0; i < mNumChains; i++) { + mChainInterpCurr[i] = mChainPositions[i].mCurrentPos; + } + + mChainInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&klift00_interp_callback, this); + } +#endif + return 1; } diff --git a/src/dusk/OSReport.cpp b/src/dusk/OSReport.cpp index b89a708d78..a54bcf0c78 100644 --- a/src/dusk/OSReport.cpp +++ b/src/dusk/OSReport.cpp @@ -1,3 +1,5 @@ +#include + #include "aurora/lib/logging.hpp" #include "os_report.h" @@ -21,10 +23,35 @@ static bool checkEnabled() { static std::string FormatToString(const char* msg, va_list list) { int ret = vsnprintf(nullptr, 0, msg, list); - std::string buf(ret, '\0'); - vsnprintf(buf.data(), buf.size(), msg, list); - buf.pop_back(); - return buf; + if (ret <= 0) { + return {}; + } + ++ret; + std::unique_ptr buf(new char[ret]); + vsnprintf(buf.get(), ret, msg, list); + buf[ret - 1] = '\0'; + return {buf.get()}; +} + +void OSReport(const char* fmt, ...) { + if (!checkEnabled()) { + return; + } + va_list args; + va_start(args, fmt); + const auto str = FormatToString(fmt, args); + va_end(args); + + Log.info("{}", str); +} + +void OSPanic(const char* file, int line, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + const auto str = FormatToString(fmt, args); + va_end(args); + + Log.fatal("[{}:{}] {}", file, line, str); } void OSReport_Error(const char* fmt, ...) { diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 543fdd754b..53c595c21c 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -320,10 +320,6 @@ namespace dusk { ImGuiMenuGame::ToggleFullscreen(); } - if (ImGui::IsKeyPressed(ImGuiKey_Escape) && getSettings().video.enableFullscreen) { - ImGuiMenuGame::ToggleFullscreen(); - } - // if (!dusk::IsGameLaunched) { // m_preLaunchWindow.draw(); // } diff --git a/src/dusk/ui/bool_button.cpp b/src/dusk/ui/bool_button.cpp index cc43bb8b65..873274758e 100644 --- a/src/dusk/ui/bool_button.cpp +++ b/src/dusk/ui/bool_button.cpp @@ -5,7 +5,14 @@ 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)) {} + mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)) {} + +bool BoolButton::modified() const { + if (mIsModified) { + return mIsModified(); + } + return BaseControlledSelectButton::modified(); +} bool BoolButton::disabled() const { if (mIsDisabled) { diff --git a/src/dusk/ui/bool_button.hpp b/src/dusk/ui/bool_button.hpp index 750c7e5cdb..8573efbc23 100644 --- a/src/dusk/ui/bool_button.hpp +++ b/src/dusk/ui/bool_button.hpp @@ -10,10 +10,12 @@ public: std::function getValue; std::function setValue; std::function isDisabled; + std::function isModified; }; BoolButton(Rml::Element* parent, Props props); + bool modified() const override; bool disabled() const override; protected: @@ -24,6 +26,7 @@ private: std::function mGetValue; std::function mSetValue; std::function mIsDisabled; + std::function mIsModified; }; } // namespace dusk::ui diff --git a/src/dusk/ui/controller_config.cpp b/src/dusk/ui/controller_config.cpp new file mode 100644 index 0000000000..393f2e2e4f --- /dev/null +++ b/src/dusk/ui/controller_config.cpp @@ -0,0 +1,642 @@ +#include "controller_config.hpp" + +#include "bool_button.hpp" +#include "button.hpp" +#include "pane.hpp" +#include "select_button.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +namespace dusk::ui { +namespace { + +Rml::String current_controller_name(int port) { + const char* name = PADGetName(port); + return name == nullptr ? "None" : name; +} + +Rml::String controller_index_name(u32 index) { + const char* name = PADGetNameForControllerIndex(index); + if (name == nullptr) { + return fmt::format("Controller {}", index + 1); + } + return name; +} + +SDL_Gamepad* gamepad_for_port(int port) { + const s32 index = PADGetIndexForPort(port); + if (index < 0) { + return nullptr; + } + return PADGetSDLGamepadForIndex(static_cast(index)); +} + +struct SpecificButtonName { + SDL_GamepadType type; + const char* name; +}; + +struct ButtonNames { + SDL_GamepadButton button; + std::vector names; +}; + +// clang-format off +const std::vector kGamepadButtonNames = { + { SDL_GAMEPAD_BUTTON_LEFT_STICK, { + {SDL_GAMEPAD_TYPE_PS3, "L3"}, + {SDL_GAMEPAD_TYPE_PS4, "L3"}, + {SDL_GAMEPAD_TYPE_PS5, "L3"}, + {SDL_GAMEPAD_TYPE_XBOX360, "Left Stick"}, + {SDL_GAMEPAD_TYPE_XBOXONE, "Left Stick"}, + {SDL_GAMEPAD_TYPE_GAMECUBE, "Control Stick"}, + }}, + { SDL_GAMEPAD_BUTTON_RIGHT_STICK, { + {SDL_GAMEPAD_TYPE_PS3, "R3"}, + {SDL_GAMEPAD_TYPE_PS4, "R3"}, + {SDL_GAMEPAD_TYPE_PS5, "R3"}, + {SDL_GAMEPAD_TYPE_XBOX360, "Right Stick"}, + {SDL_GAMEPAD_TYPE_XBOXONE, "Right Stick"}, + {SDL_GAMEPAD_TYPE_GAMECUBE, "C Stick"}, + }}, + { SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, { + {SDL_GAMEPAD_TYPE_PS3, "L1"}, + {SDL_GAMEPAD_TYPE_PS4, "L1"}, + {SDL_GAMEPAD_TYPE_PS5, "L1"}, + {SDL_GAMEPAD_TYPE_XBOX360, "LB"}, + {SDL_GAMEPAD_TYPE_XBOXONE, "LB"}, + }}, + { SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, { + {SDL_GAMEPAD_TYPE_PS3, "R1"}, + {SDL_GAMEPAD_TYPE_PS4, "R1"}, + {SDL_GAMEPAD_TYPE_PS5, "R1"}, + {SDL_GAMEPAD_TYPE_XBOX360, "RB"}, + {SDL_GAMEPAD_TYPE_XBOXONE, "RB"}, + {SDL_GAMEPAD_TYPE_GAMECUBE, "Z"}, + }}, + { SDL_GAMEPAD_BUTTON_BACK, { + {SDL_GAMEPAD_TYPE_PS3, "Select"}, + {SDL_GAMEPAD_TYPE_PS4, "Share"}, + {SDL_GAMEPAD_TYPE_PS5, "Create"}, + {SDL_GAMEPAD_TYPE_XBOX360, "Back"}, + {SDL_GAMEPAD_TYPE_XBOXONE, "View"}, + }}, + { SDL_GAMEPAD_BUTTON_START, { + {SDL_GAMEPAD_TYPE_PS3, "Start"}, + {SDL_GAMEPAD_TYPE_PS4, "Options"}, + {SDL_GAMEPAD_TYPE_PS5, "Options"}, + {SDL_GAMEPAD_TYPE_XBOX360, "Start"}, + {SDL_GAMEPAD_TYPE_XBOXONE, "Menu"}, + {SDL_GAMEPAD_TYPE_GAMECUBE, "Start/Pause"}, + }}, +}; +// clang-format on + +Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) { + if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) { + return "Not bound"; + } + + auto button = static_cast(buttonUntyped); + if (gamepad != nullptr) { + switch (SDL_GetGamepadButtonLabel(gamepad, button)) { + case SDL_GAMEPAD_BUTTON_LABEL_A: + return "A"; + case SDL_GAMEPAD_BUTTON_LABEL_B: + return "B"; + case SDL_GAMEPAD_BUTTON_LABEL_X: + return "X"; + case SDL_GAMEPAD_BUTTON_LABEL_Y: + return "Y"; + case SDL_GAMEPAD_BUTTON_LABEL_CROSS: + return "Cross"; + case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: + return "Circle"; + case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: + return "Triangle"; + case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: + return "Square"; + default: + break; + } + } + + const SDL_GamepadType type = + gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN; + for (const auto& buttonNames : kGamepadButtonNames) { + if (buttonNames.button != button) { + continue; + } + + for (const auto& name : buttonNames.names) { + if (name.type == type) { + return name.name; + } + } + } + + switch (button) { + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + return "D-pad left"; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + return "D-pad right"; + case SDL_GAMEPAD_BUTTON_DPAD_UP: + return "D-pad up"; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + return "D-pad down"; + default: + break; + } + + if (const char* name = PADGetNativeButtonName(buttonUntyped)) { + return name; + } + return "Unknown"; +} + +Rml::String native_axis_name(const PADAxisMapping& mapping, SDL_Gamepad* gamepad) { + if (mapping.nativeAxis.nativeAxis != -1) { + Rml::String value = PADGetNativeAxisName(mapping.nativeAxis); + if (mapping.padAxis != PAD_AXIS_TRIGGER_L && mapping.padAxis != PAD_AXIS_TRIGGER_R) { + value += mapping.nativeAxis.sign == AXIS_SIGN_POSITIVE ? "+" : "-"; + } + return value; + } + + if (mapping.nativeButton != -1) { + return native_button_name(gamepad, static_cast(mapping.nativeButton)); + } + + return "Not bound"; +} + +bool is_dpad_button(PADButton button) { + return button == PAD_BUTTON_UP || button == PAD_BUTTON_DOWN || button == PAD_BUTTON_LEFT || + button == PAD_BUTTON_RIGHT; +} + +bool is_action_button(PADButton button) { + return button == PAD_BUTTON_A || button == PAD_BUTTON_B || button == PAD_BUTTON_X || + button == PAD_BUTTON_Y || button == PAD_BUTTON_START || button == PAD_TRIGGER_Z; +} + +bool input_neutral(int port) { + if (port < 0) { + return true; + } + return PADGetNativeButtonPressed(port) == -1 && PADGetNativeAxisPulled(port).nativeAxis == -1; +} + +// A Keydown event with KI_ESCAPE may have been dispatched from the controller bindings, +// so instead poll the keyboard input directly for Escape-to-unbind +bool keyboard_escape_pressed() { + int keyCount = 0; + const bool* keys = SDL_GetKeyboardState(&keyCount); + return keys != nullptr && SDL_SCANCODE_ESCAPE < keyCount && keys[SDL_SCANCODE_ESCAPE]; +} + +} // namespace + +ControllerConfigWindow::ControllerConfigWindow() { + listen( + Rml::EventId::Keydown, + [this](Rml::Event& event) { + if (capture_active() || mSuppressNavigationUntilNeutral) { + event.StopPropagation(); + } + }, + true); + if (auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr) { + if (auto* root = context->GetRootElement()) { + mListeners.emplace_back(std::make_unique( + root, "controllerchange", [this](Rml::Event&) { refresh_controller_page(); })); + } + } + + for (int port = PAD_CHAN0; port < PAD_CHANMAX; ++port) { + add_tab(fmt::format("Port {}", port + 1), [this, port](Rml::Element* content) { + if (mPendingPort != -1 && mPendingPort != port) { + cancel_pending_binding(); + } + build_port_tab(content, port); + }); + } +} + +void ControllerConfigWindow::hide(bool close) { + cancel_pending_binding(); + Window::hide(close); +} + +void ControllerConfigWindow::update() { + poll_pending_binding(); + Window::update(); +} + +void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) { + auto& leftPane = add_child(content, Pane::Type::Controlled); + auto& rightPane = add_child(content, Pane::Type::Uncontrolled); + mRightPane = &rightPane; + mActivePort = port; + + auto showPage = [this, &rightPane, port](Page page) { + mPage = page; + render_page(rightPane, port, page); + }; + auto addPageButton = [&leftPane, showPage](Page page, Rml::String key, auto getValue) { + leftPane + .add_select_button({ + .key = std::move(key), + .getValue = std::move(getValue), + }) + .on_focus([showPage, page](Rml::Event&) { showPage(page); }) + .on_pressed([showPage, page] { showPage(page); }); + }; + + addPageButton(Page::Controller, "Controller", [port] { return current_controller_name(port); }); + addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); }); + addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); }); + addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); }); + + leftPane.add_section("Options"); + leftPane + .add_child(BoolButton::Props{ + .key = "Enable Dead Zones", + .getValue = + [port] { + PADDeadZones* deadZones = PADGetDeadZones(port); + return deadZones != nullptr && deadZones->useDeadzones; + }, + .setValue = + [port](bool value) { + if (PADDeadZones* deadZones = PADGetDeadZones(port)) { + deadZones->useDeadzones = value; + PADSerializeMappings(); + } + }, + .isDisabled = [port] { return PADGetDeadZones(port) == nullptr; }, + }) + .on_focus([&rightPane](Rml::Event&) { + rightPane.clear(); + rightPane.add_text("Apply configured dead zones to the sticks and analog triggers."); + }); + leftPane + .add_child(BoolButton::Props{ + .key = "Emulate Triggers", + .getValue = + [port] { + PADDeadZones* deadZones = PADGetDeadZones(port); + return deadZones != nullptr && deadZones->emulateTriggers; + }, + .setValue = + [port](bool value) { + if (PADDeadZones* deadZones = PADGetDeadZones(port)) { + deadZones->emulateTriggers = value; + PADSerializeMappings(); + } + }, + .isDisabled = [port] { return PADGetDeadZones(port) == nullptr; }, + }) + .on_focus([&rightPane](Rml::Event&) { + rightPane.clear(); + rightPane.add_text("Treat analog trigger movement as digital L and R button input."); + }); + + render_page(rightPane, port, mPage); +} + +void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { + pane.clear(); + + switch (page) { + case Page::Controller: { + const u32 controllerCount = PADCount(); + if (controllerCount == 0) { + pane.add_text("No controllers detected"); + break; + } + + pane.add_button({ + .text = "None", + .isSelected = [port] { return PADGetIndexForPort(port) < 0; }, + }) + .on_pressed([this, port] { + cancel_pending_binding(); + PADClearPort(port); + PADSerializeMappings(); + }); + + for (u32 i = 0; i < controllerCount; ++i) { + pane.add_button( + { + .text = controller_index_name(i), + .isSelected = + [port, i] { return PADGetIndexForPort(port) == static_cast(i); }, + }) + .on_pressed([this, port, i] { + cancel_pending_binding(); + PADSetPortForIndex(i, port); + PADSerializeMappings(); + }); + } + break; + } + case Page::Buttons: { + u32 buttonCount = 0; + PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount); + if (mappings == nullptr) { + pane.add_text("No controller selected"); + break; + } + + SDL_Gamepad* gamepad = gamepad_for_port(port); + pane.add_section("Buttons"); + for (u32 i = 0; i < buttonCount; ++i) { + PADButtonMapping& mapping = mappings[i]; + if (!is_action_button(mapping.padButton)) { + continue; + } + + pane.add_select_button({ + .key = PADGetButtonName(mapping.padButton), + .getValue = + [this, &mapping, gamepad] { + if (mPendingButtonMapping == &mapping) { + return pending_button_label(); + } + return native_button_name( + gamepad, mapping.nativeButton); + }, + }) + .on_pressed([this, port, &mapping] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingButtonMapping = &mapping; + }); + } + + pane.add_section("D-Pad"); + for (u32 i = 0; i < buttonCount; ++i) { + PADButtonMapping& mapping = mappings[i]; + if (!is_dpad_button(mapping.padButton)) { + continue; + } + + pane.add_select_button({ + .key = PADGetButtonName(mapping.padButton), + .getValue = + [this, &mapping, gamepad] { + if (mPendingButtonMapping == &mapping) { + return pending_button_label(); + } + return native_button_name( + gamepad, mapping.nativeButton); + }, + }) + .on_pressed([this, port, &mapping] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingButtonMapping = &mapping; + }); + } + break; + } + case Page::Triggers: { + u32 axisCount = 0; + PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount); + u32 buttonCount = 0; + PADButtonMapping* buttons = PADGetButtonMappings(port, &buttonCount); + if (axes == nullptr && buttons == nullptr) { + pane.add_text("No controller selected"); + break; + } + + SDL_Gamepad* gamepad = gamepad_for_port(port); + pane.add_section("Analog"); + constexpr std::array kTriggerAxes = {PAD_AXIS_TRIGGER_L, PAD_AXIS_TRIGGER_R}; + if (axes != nullptr) { + for (PADAxis axis : kTriggerAxes) { + if (axis >= axisCount) { + continue; + } + PADAxisMapping& mapping = axes[axis]; + pane.add_select_button({ + .key = PADGetAxisName(mapping.padAxis), + .getValue = + [this, &mapping, gamepad] { + if (mPendingAxisMapping == &mapping) { + return pending_axis_label(); + } + return native_axis_name(mapping, gamepad); + }, + }) + .on_pressed([this, port, &mapping] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingAxisMapping = &mapping; + }); + } + } + + pane.add_section("Digital"); + if (buttons != nullptr) { + for (u32 i = 0; i < buttonCount; ++i) { + PADButtonMapping& mapping = buttons[i]; + if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) { + continue; + } + pane.add_select_button({ + .key = PADGetButtonName(mapping.padButton), + .getValue = + [this, &mapping, gamepad] { + if (mPendingButtonMapping == &mapping) { + return pending_button_label(); + } + return native_button_name( + gamepad, mapping.nativeButton); + }, + }) + .on_pressed([this, port, &mapping] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingButtonMapping = &mapping; + }); + } + } + break; + } + case Page::Sticks: { + u32 axisCount = 0; + PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount); + if (axes == nullptr) { + pane.add_text("No controller selected"); + break; + } + + SDL_Gamepad* gamepad = gamepad_for_port(port); + auto addAxis = [&](PADAxis axis) { + if (axis >= axisCount) { + return; + } + PADAxisMapping& mapping = axes[axis]; + pane.add_select_button({ + .key = PADGetAxisDirectionLabel(mapping.padAxis), + .getValue = + [this, &mapping, gamepad] { + if (mPendingAxisMapping == &mapping) { + return pending_axis_label(); + } + return native_axis_name(mapping, gamepad); + }, + }) + .on_pressed([this, port, &mapping] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingAxisMapping = &mapping; + }); + }; + + pane.add_section("Control Stick"); + addAxis(PAD_AXIS_LEFT_Y_POS); + addAxis(PAD_AXIS_LEFT_Y_NEG); + addAxis(PAD_AXIS_LEFT_X_NEG); + addAxis(PAD_AXIS_LEFT_X_POS); + + pane.add_section("C Stick"); + addAxis(PAD_AXIS_RIGHT_Y_POS); + addAxis(PAD_AXIS_RIGHT_Y_NEG); + addAxis(PAD_AXIS_RIGHT_X_NEG); + addAxis(PAD_AXIS_RIGHT_X_POS); + break; + } + } +} + +void ControllerConfigWindow::refresh_controller_page() { + if (!visible() || mPage != Page::Controller || mRightPane == nullptr) { + return; + } + render_page(*mRightPane, mActivePort, Page::Controller); +} + +void ControllerConfigWindow::poll_pending_binding() { + if (mSuppressNavigationUntilNeutral && input_neutral(mSuppressNavigationPort)) { + mSuppressNavigationUntilNeutral = false; + mSuppressNavigationPort = -1; + } + + if (!capture_active()) { + return; + } + + if (keyboard_escape_pressed()) { + unmap_pending_binding(); + return; + } + + if (!mPendingBindingArmed) { + if (pending_input_neutral()) { + mPendingBindingArmed = true; + } + return; + } + + if (mPendingButtonMapping != nullptr) { + const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort); + if (nativeButton != -1) { + const int completedPort = mPendingPort; + mPendingButtonMapping->nativeButton = static_cast(nativeButton); + finish_pending_binding(completedPort); + } + return; + } + + if (mPendingAxisMapping != nullptr) { + const PADSignedNativeAxis nativeAxis = PADGetNativeAxisPulled(mPendingPort); + if (nativeAxis.nativeAxis != -1) { + const int completedPort = mPendingPort; + mPendingAxisMapping->nativeAxis = nativeAxis; + mPendingAxisMapping->nativeButton = -1; + finish_pending_binding(completedPort); + return; + } + + const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort); + if (nativeButton != -1) { + const int completedPort = mPendingPort; + mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE}; + mPendingAxisMapping->nativeButton = nativeButton; + finish_pending_binding(completedPort); + } + } +} + +void ControllerConfigWindow::finish_pending_binding(int completedPort) { + mPendingButtonMapping = nullptr; + mPendingAxisMapping = nullptr; + mPendingPort = -1; + mPendingBindingArmed = false; + mSuppressNavigationUntilNeutral = true; + mSuppressNavigationPort = completedPort; + PADSerializeMappings(); +} + +void ControllerConfigWindow::unmap_pending_binding() { + if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr) { + return; + } + + const int completedPort = mPendingPort; + if (mPendingButtonMapping != nullptr) { + mPendingButtonMapping->nativeButton = PAD_NATIVE_BUTTON_INVALID; + } + if (mPendingAxisMapping != nullptr) { + mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE}; + mPendingAxisMapping->nativeButton = -1; + } + finish_pending_binding(completedPort); +} + +bool ControllerConfigWindow::capture_active() const { + return mPendingButtonMapping != nullptr || mPendingAxisMapping != nullptr; +} + +bool ControllerConfigWindow::pending_input_neutral() const { + return input_neutral(mPendingPort); +} + +Rml::String ControllerConfigWindow::pending_button_label() const { + return mPendingBindingArmed ? "Press a button..." : "Waiting..."; +} + +Rml::String ControllerConfigWindow::pending_axis_label() const { + return mPendingBindingArmed ? "Move axis or press a button..." : "Waiting..."; +} + +void ControllerConfigWindow::cancel_pending_binding() { + if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr && + !mSuppressNavigationUntilNeutral) + { + return; + } + mPendingButtonMapping = nullptr; + mPendingAxisMapping = nullptr; + mPendingPort = -1; + mPendingBindingArmed = false; + mSuppressNavigationUntilNeutral = false; + mSuppressNavigationPort = -1; +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/controller_config.hpp b/src/dusk/ui/controller_config.hpp new file mode 100644 index 0000000000..4bcf41ae8d --- /dev/null +++ b/src/dusk/ui/controller_config.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "window.hpp" + +#include + +namespace dusk::ui { + +class ControllerConfigWindow : public Window { +public: + ControllerConfigWindow(); + + void update() override; + void hide(bool close) override; + +private: + enum class Page { + Controller, + Buttons, + Triggers, + Sticks, + }; + + void build_port_tab(Rml::Element* content, int port); + void render_page(class Pane& pane, int port, Page page); + void refresh_controller_page(); + void poll_pending_binding(); + void finish_pending_binding(int completedPort); + void unmap_pending_binding(); + bool capture_active() const; + bool pending_input_neutral() const; + Rml::String pending_button_label() const; + Rml::String pending_axis_label() const; + void cancel_pending_binding(); + + Page mPage = Page::Controller; + Pane* mRightPane = nullptr; + int mActivePort = 0; + int mPendingPort = -1; + bool mPendingBindingArmed = false; + bool mSuppressNavigationUntilNeutral = false; + int mSuppressNavigationPort = -1; + PADButtonMapping* mPendingButtonMapping = nullptr; + PADAxisMapping* mPendingAxisMapping = nullptr; +}; + +} // namespace dusk::ui diff --git a/src/dusk/ui/event.cpp b/src/dusk/ui/event.cpp index 0ab2211645..38eeb8e9b7 100644 --- a/src/dusk/ui/event.cpp +++ b/src/dusk/ui/event.cpp @@ -10,9 +10,20 @@ ScopedEventListener::ScopedEventListener( mElement->AddEventListener(mEvent, this, mCapture); } +ScopedEventListener::ScopedEventListener( + Rml::Element* element, Rml::String event, Callback callback, bool capture) + : mElement(element), mEventName(std::move(event)), mCapture(capture), + mCallback(std::move(callback)) { + mElement->AddEventListener(mEventName, this, mCapture); +} + ScopedEventListener::~ScopedEventListener() { if (mElement != nullptr) { - mElement->RemoveEventListener(mEvent, this, mCapture); + if (!mEventName.empty()) { + mElement->RemoveEventListener(mEventName, this, mCapture); + } else { + mElement->RemoveEventListener(mEvent, this, mCapture); + } mElement = nullptr; } } diff --git a/src/dusk/ui/event.hpp b/src/dusk/ui/event.hpp index 4521fa1ea9..d4075150f2 100644 --- a/src/dusk/ui/event.hpp +++ b/src/dusk/ui/event.hpp @@ -12,6 +12,8 @@ public: ScopedEventListener( Rml::Element* element, Rml::EventId event, Callback callback, bool capture = false); + ScopedEventListener( + Rml::Element* element, Rml::String event, Callback callback, bool capture = false); ~ScopedEventListener() override; ScopedEventListener(const ScopedEventListener&) = delete; @@ -25,6 +27,7 @@ public: private: Rml::Element* mElement = nullptr; Rml::EventId mEvent = Rml::EventId::Invalid; + Rml::String mEventName; bool mCapture = false; Callback mCallback; }; diff --git a/src/dusk/ui/input.cpp b/src/dusk/ui/input.cpp index 891887f4bb..4c430540e9 100644 --- a/src/dusk/ui/input.cpp +++ b/src/dusk/ui/input.cpp @@ -64,6 +64,40 @@ bool should_block_pad_for_menu_chord() noexcept { return false; } +const char* controller_change_type(Uint32 eventType) noexcept { + switch (eventType) { + case SDL_EVENT_GAMEPAD_ADDED: + return "added"; + case SDL_EVENT_GAMEPAD_REMOVED: + return "removed"; + case SDL_EVENT_GAMEPAD_REMAPPED: + return "remapped"; + default: + return nullptr; + } +} + +void dispatch_controller_change_event(const SDL_Event& event) noexcept { + const char* type = controller_change_type(event.type); + if (type == nullptr) { + return; + } + + auto* context = aurora::rmlui::get_context(); + if (context == nullptr) { + return; + } + auto* root = context->GetRootElement(); + if (root == nullptr) { + return; + } + + Rml::Dictionary parameters; + parameters["type"] = Rml::String(type); + parameters["which"] = static_cast(event.gdevice.which); + root->DispatchEvent("controllerchange", parameters); +} + PADButton pad_button_from_axis(PADAxis axis) noexcept { switch (axis) { case PAD_AXIS_TRIGGER_R: @@ -502,8 +536,11 @@ void handle_event(const SDL_Event& event) noexcept { if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_WINDOW_FOCUS_LOST) { reset_input_state(); sync_input_block(); - return; + if (event.type != SDL_EVENT_GAMEPAD_REMOVED) { + return; + } } + dispatch_controller_change_event(event); if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP && event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION) { diff --git a/src/dusk/ui/number_button.cpp b/src/dusk/ui/number_button.cpp index 7a7488207a..97d16a885f 100644 --- a/src/dusk/ui/number_button.cpp +++ b/src/dusk/ui/number_button.cpp @@ -8,8 +8,16 @@ 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)), - mIsDisabled(std::move(props.isDisabled)), mMin(props.min), mMax(props.max), mStep(props.step), - mPrefix(std::move(props.prefix)), mSuffix(std::move(props.suffix)) {} + mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)), + mMin(props.min), mMax(props.max), mStep(props.step), mPrefix(std::move(props.prefix)), + mSuffix(std::move(props.suffix)) {} + +bool NumberButton::modified() const { + if (mIsModified) { + return mIsModified(); + } + return BaseStringButton::modified(); +} bool NumberButton::disabled() const { if (mIsDisabled) { diff --git a/src/dusk/ui/number_button.hpp b/src/dusk/ui/number_button.hpp index d7b6bbc939..29d36d682d 100644 --- a/src/dusk/ui/number_button.hpp +++ b/src/dusk/ui/number_button.hpp @@ -11,6 +11,7 @@ public: std::function getValue; std::function setValue; std::function isDisabled; + std::function isModified; int min = 0; int max = INT_MAX; int step = 1; @@ -20,6 +21,7 @@ public: NumberButton(Rml::Element* parent, Props props); + bool modified() const override; bool disabled() const override; protected: @@ -32,6 +34,7 @@ private: std::function mGetValue; std::function mSetValue; std::function mIsDisabled; + std::function mIsModified; int mMin; int mMax; int mStep; diff --git a/src/dusk/ui/popup.cpp b/src/dusk/ui/popup.cpp index a0cd3c6868..75658d965d 100644 --- a/src/dusk/ui/popup.cpp +++ b/src/dusk/ui/popup.cpp @@ -23,7 +23,7 @@ const Rml::String kDocumentSource = R"RML( - + )RML"; @@ -31,18 +31,21 @@ const Rml::String kDocumentSource = R"RML( } Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) { - mTabBar = std::make_unique(mRoot, TabBar::Props{.autoSelect = false}); + mTabBar = std::make_unique(mRoot, TabBar::Props{ + .onClose = [this] { hide(false); }, + .autoSelect = false, + }); mTabBar->add_tab("Settings", [] { push_document(std::make_unique()); }); - mTabBar->add_tab("Warp", [] { - // TODO - }); + // mTabBar->add_tab("Warp", [] { + // // TODO + // }); mTabBar->add_tab("Editor", [] { push_document(std::make_unique()); }); mTabBar->add_tab("Reset", [this] { JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; mTabBar->set_active_tab(-1); hide(false); }); - mTabBar->add_tab("Exit", [] { IsRunning = false; }); + mTabBar->add_tab("Quit", [] { IsRunning = false; }); // Hide document after transition completion listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) { @@ -106,6 +109,11 @@ void Popup::update_safe_area() noexcept { Rml::PropertyId::PaddingRight, Rml::Property(safeInsets.right, Rml::Unit::PX)); tabBar->SetProperty( Rml::PropertyId::PaddingLeft, Rml::Property(safeInsets.left, Rml::Unit::PX)); + if (auto* close = tabBar->QuerySelector("close")) { + close->SetProperty(Rml::PropertyId::Right, + Rml::Property(safeInsets.right + 8.0f * context->GetDensityIndependentPixelRatio(), + Rml::Unit::PX)); + } } bool Popup::visible() const { diff --git a/src/dusk/ui/select_button.cpp b/src/dusk/ui/select_button.cpp index 95cffd873d..bb19329125 100644 --- a/src/dusk/ui/select_button.cpp +++ b/src/dusk/ui/select_button.cpp @@ -2,6 +2,7 @@ #include "ui.hpp" +#include #include namespace dusk::ui { @@ -23,18 +24,51 @@ SelectButton::SelectButton(Rml::Element* parent, Props props) on_nav_command([this](Rml::Event&, NavCommand cmd) { return handle_nav_command(cmd); }); } +bool SelectButton::modified() const { + return mProps.modified; +} + +void SelectButton::set_modified(bool value) { + if (mProps.modified != value) { + mValueElem->SetClass("modified", value); + if (value) { + mValueElem->SetInnerRML(fmt::format("• {}", escape(mProps.value))); + } else { + mValueElem->SetInnerRML(escape(mProps.value)); + } + mProps.modified = value; + } +} + void SelectButton::set_value_label(const Rml::String& value) { if (mProps.value != value) { - mValueElem->SetInnerRML(escape(value)); + if (mProps.modified) { + mValueElem->SetInnerRML(fmt::format("• {}", escape(value))); + } else { + mValueElem->SetInnerRML(escape(value)); + } mProps.value = value; } } +SelectButton& SelectButton::on_pressed(SelectButtonCallback callback) { + if (!callback) { + return *this; + } + listen(Rml::EventId::Submit, [this, callback = std::move(callback)](Rml::Event& event) { + if (!disabled() && event.GetTargetElement() == mRoot) { + callback(); + } + }); + return *this; +} + void SelectButton::update_props(Props props) { if (mProps.key != props.key) { mKeyElem->SetInnerRML(escape(props.key)); } set_value_label(props.value); + set_modified(props.modified); mProps = std::move(props); } @@ -49,9 +83,17 @@ bool SelectButton::handle_nav_command(NavCommand cmd) { void BaseControlledSelectButton::update() { set_disabled(disabled()); set_value_label(format_value()); + set_modified(modified()); SelectButton::update(); } +bool ControlledSelectButton::modified() const { + if (mIsModified) { + return mIsModified(); + } + return BaseControlledSelectButton::modified(); +} + bool ControlledSelectButton::disabled() const { if (mIsDisabled) { return mIsDisabled(); diff --git a/src/dusk/ui/select_button.hpp b/src/dusk/ui/select_button.hpp index 4a9612076d..f050943673 100644 --- a/src/dusk/ui/select_button.hpp +++ b/src/dusk/ui/select_button.hpp @@ -3,18 +3,27 @@ #include "component.hpp" #include "ui.hpp" +#include +#include + namespace dusk::ui { +using SelectButtonCallback = std::function; + class SelectButton : public FluentComponent { public: struct Props { Rml::String key; Rml::String value; + bool modified = false; }; SelectButton(Rml::Element* parent, Props props); + virtual bool modified() const; + void set_modified(bool value); void set_value_label(const Rml::String& value); + SelectButton& on_pressed(SelectButtonCallback callback); protected: void update_props(Props props); @@ -23,7 +32,6 @@ protected: Props mProps; Rml::Element* mKeyElem = nullptr; Rml::Element* mValueElem = nullptr; - std::function mOnHover; }; class BaseControlledSelectButton : public SelectButton { @@ -43,12 +51,15 @@ public: Rml::String key; std::function getValue; std::function isDisabled; + std::function isModified; }; ControlledSelectButton(Rml::Element* parent, Props props) : BaseControlledSelectButton(parent, {std::move(props.key)}), - mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)) {} + mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)), + mIsModified(std::move(props.isModified)) {} + bool modified() const override; bool disabled() const override; protected: @@ -56,6 +67,7 @@ protected: std::function mGetValue; std::function mIsDisabled; + std::function mIsModified; }; } // namespace dusk::ui diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 2cdb2d50d9..eb33f92b95 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -1,9 +1,8 @@ #include "settings.hpp" -#include - #include "aurora/gfx.h" #include "bool_button.hpp" +#include "controller_config.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" #include "dusk/config.hpp" @@ -81,8 +80,7 @@ struct ConfigBoolProps { SelectButton& config_bool_select( Pane& leftPane, Pane& rightPane, ConfigVar& var, ConfigBoolProps props) { return leftPane - .add_child(BoolButton::Props{ - .key = std::move(props.key), + .add_child(BoolButton::Props{.key = std::move(props.key), .getValue = [&var] { return var.getValue(); }, .setValue = [&var, callback = std::move(props.onChange)](bool value) { @@ -96,7 +94,7 @@ SelectButton& config_bool_select( } }, .isDisabled = std::move(props.isDisabled), - }) + .isModified = [&var] { return var.getValue() != var.getDefaultValue(); }}) .on_focus([&rightPane, helpText = std::move(props.helpText)](Rml::Event&) { rightPane.clear(); rightPane.add_rml(helpText); @@ -116,6 +114,7 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar(content, Pane::Type::Controlled); - pane.add_section("Coming soon"); - }); - } - } -}; - } // namespace SettingsWindow::SettingsWindow() { @@ -158,6 +145,11 @@ SettingsWindow::SettingsWindow() { config::Save(); audio::SetMasterVolume(value / 100.f); }, + .isModified = + [] { + return getSettings().audio.masterVolume.getValue() != + getSettings().audio.masterVolume.getDefaultValue(); + }, .max = 100, .suffix = "%", }) @@ -284,6 +276,11 @@ SettingsWindow::SettingsWindow() { config::Save(); }, .isDisabled = [] { return getSettings().game.speedrunMode; }, + .isModified = + [] { + return getSettings().game.damageMultiplier.getValue() != + getSettings().game.damageMultiplier.getDefaultValue(); + }, .min = 1, .max = 8, .prefix = "x", @@ -471,6 +468,11 @@ SettingsWindow::SettingsWindow() { return format_graphics_setting_value(GraphicsOption::InternalResolution, getSettings().game.internalResolutionScale.getValue()); }, + .isModified = + [] { + return getSettings().game.internalResolutionScale.getValue() != + getSettings().game.internalResolutionScale.getDefaultValue(); + }, }) .on_nav_command([](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || @@ -499,6 +501,11 @@ SettingsWindow::SettingsWindow() { return format_graphics_setting_value(GraphicsOption::ShadowResolution, getSettings().game.shadowResolutionMultiplier.getValue()); }, + .isModified = + [] { + return getSettings().game.shadowResolutionMultiplier.getValue() != + getSettings().game.shadowResolutionMultiplier.getDefaultValue(); + }, }) .on_nav_command([](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || @@ -529,6 +536,11 @@ SettingsWindow::SettingsWindow() { return format_graphics_setting_value(GraphicsOption::BloomMode, static_cast(getSettings().game.bloomMode.getValue())); }, + .isModified = + [] { + return getSettings().game.bloomMode.getValue() != + getSettings().game.bloomMode.getDefaultValue(); + }, }) .on_nav_command([](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || @@ -559,6 +571,11 @@ SettingsWindow::SettingsWindow() { }, .isDisabled = [] { return getSettings().game.bloomMode.getValue() == BloomMode::Off; }, + .isModified = + [] { + return getSettings().game.bloomMultiplier.getValue() != + getSettings().game.bloomMultiplier.getDefaultValue(); + }, }) .on_nav_command([](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || @@ -619,7 +636,6 @@ SettingsWindow::SettingsWindow() { "- Account Username" }); #endif - leftPane.add_section("Advanced"); config_bool_select(leftPane, rightPane, getSettings().backend.skipPreLaunchUI, { .key = "Skip Pre-Launch UI", diff --git a/src/dusk/ui/tab_bar.cpp b/src/dusk/ui/tab_bar.cpp index 1329d5f205..1a2e74f763 100644 --- a/src/dusk/ui/tab_bar.cpp +++ b/src/dusk/ui/tab_bar.cpp @@ -9,16 +9,76 @@ Rml::Element* createRoot(Rml::Element* parent) { return parent->AppendChild(std::move(elem)); } +int key_modifiers_from_event(const Rml::Event& event) { + int modifiers = 0; + if (event.GetParameter("ctrl_key", 0)) { + modifiers |= Rml::Input::KM_CTRL; + } + if (event.GetParameter("shift_key", 0)) { + modifiers |= Rml::Input::KM_SHIFT; + } + if (event.GetParameter("alt_key", 0)) { + modifiers |= Rml::Input::KM_ALT; + } + if (event.GetParameter("meta_key", 0)) { + modifiers |= Rml::Input::KM_META; + } + if (event.GetParameter("caps_lock_key", 0)) { + modifiers |= Rml::Input::KM_CAPSLOCK; + } + if (event.GetParameter("num_lock_key", 0)) { + modifiers |= Rml::Input::KM_NUMLOCK; + } + if (event.GetParameter("scroll_lock_key", 0)) { + modifiers |= Rml::Input::KM_SCROLLLOCK; + } + return modifiers; +} + } // namespace TabBar::TabBar(Rml::Element* parent, Props props) : FluentComponent(createRoot(parent)), mProps(std::move(props)) { + if (mProps.onClose) { + mRoot->SetAttribute("closable", ""); + add_child