diff --git a/extern/aurora b/extern/aurora index 1def8fa1ef..935756010c 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 1def8fa1efee3bb7ac0835b9733861427e00097b +Subproject commit 935756010cbe02c04ce78bc5ccb7185c271040d6 diff --git a/files.cmake b/files.cmake index 64cb21d225..d238203776 100644 --- a/files.cmake +++ b/files.cmake @@ -1469,6 +1469,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/src/dusk/ui/controller_config.cpp b/src/dusk/ui/controller_config.cpp new file mode 100644 index 0000000000..af39782c02 --- /dev/null +++ b/src/dusk/ui/controller_config.cpp @@ -0,0 +1,617 @@ +#include "controller_config.hpp" + +#include "bool_button.hpp" +#include "button.hpp" +#include "pane.hpp" +#include "select_button.hpp" + +#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; +} + +} // 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 (!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); + mPendingButtonMapping = nullptr; + mPendingPort = -1; + mPendingBindingArmed = false; + mSuppressNavigationUntilNeutral = true; + mSuppressNavigationPort = completedPort; + PADSerializeMappings(); + } + return; + } + + if (mPendingAxisMapping != nullptr) { + const PADSignedNativeAxis nativeAxis = PADGetNativeAxisPulled(mPendingPort); + if (nativeAxis.nativeAxis != -1) { + const int completedPort = mPendingPort; + mPendingAxisMapping->nativeAxis = nativeAxis; + mPendingAxisMapping->nativeButton = -1; + mPendingAxisMapping = nullptr; + mPendingPort = -1; + mPendingBindingArmed = false; + mSuppressNavigationUntilNeutral = true; + mSuppressNavigationPort = completedPort; + PADSerializeMappings(); + return; + } + + const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort); + if (nativeButton != -1) { + const int completedPort = mPendingPort; + mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE}; + mPendingAxisMapping->nativeButton = nativeButton; + mPendingAxisMapping = nullptr; + mPendingPort = -1; + mPendingBindingArmed = false; + mSuppressNavigationUntilNeutral = true; + mSuppressNavigationPort = completedPort; + PADSerializeMappings(); + } + } +} + +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..67379dbf87 --- /dev/null +++ b/src/dusk/ui/controller_config.hpp @@ -0,0 +1,45 @@ +#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(); + 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/select_button.cpp b/src/dusk/ui/select_button.cpp index 73742a60c2..bb19329125 100644 --- a/src/dusk/ui/select_button.cpp +++ b/src/dusk/ui/select_button.cpp @@ -51,6 +51,18 @@ void SelectButton::set_value_label(const Rml::String& 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)); diff --git a/src/dusk/ui/select_button.hpp b/src/dusk/ui/select_button.hpp index 4640f7235f..f050943673 100644 --- a/src/dusk/ui/select_button.hpp +++ b/src/dusk/ui/select_button.hpp @@ -3,8 +3,13 @@ #include "component.hpp" #include "ui.hpp" +#include +#include + namespace dusk::ui { +using SelectButtonCallback = std::function; + class SelectButton : public FluentComponent { public: struct Props { @@ -18,6 +23,7 @@ public: 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); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 1f32071d37..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" @@ -127,18 +126,6 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar(content, Pane::Type::Controlled); - pane.add_section("Coming soon"); - }); - } - } -}; - } // namespace SettingsWindow::SettingsWindow() {