mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-09 04:30:49 -04:00
UI: Add controller input
This commit is contained in:
@@ -1474,6 +1474,8 @@ set(DUSK_FILES
|
||||
src/dusk/ui/editor.hpp
|
||||
src/dusk/ui/event.cpp
|
||||
src/dusk/ui/event.hpp
|
||||
src/dusk/ui/input.cpp
|
||||
src/dusk/ui/input.hpp
|
||||
src/dusk/ui/nav_types.hpp
|
||||
src/dusk/ui/number_button.cpp
|
||||
src/dusk/ui/number_button.hpp
|
||||
|
||||
@@ -17,6 +17,17 @@ Rml::ElementDocument* load_document(const Rml::String& source) {
|
||||
} // namespace
|
||||
|
||||
Document::Document(const Rml::String& source) : mDocument(load_document(source)) {
|
||||
// Block keydown events while hidden (except for Menu)
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
[this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::Menu && !visible()) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
},
|
||||
true);
|
||||
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
|
||||
@@ -76,7 +87,18 @@ bool Document::can_destroy() const {
|
||||
return *mDocument->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Hidden;
|
||||
}
|
||||
|
||||
bool Document::visible() const {
|
||||
if (mDocument == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return *mDocument->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible;
|
||||
}
|
||||
|
||||
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Menu) {
|
||||
toggle_top_document();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ public:
|
||||
virtual void hide();
|
||||
virtual void update();
|
||||
virtual bool focus();
|
||||
virtual bool visible() const;
|
||||
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
@@ -33,4 +34,4 @@ protected:
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -0,0 +1,610 @@
|
||||
#include "input.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <aurora/rmlui.hpp>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
constexpr double kGamepadRepeatInitialDelay = 0.32;
|
||||
constexpr double kGamepadRepeatStartInterval = 0.12;
|
||||
constexpr double kGamepadRepeatMinInterval = 0.045;
|
||||
constexpr double kGamepadRepeatRampDuration = 1.0;
|
||||
constexpr double kGamepadMenuChordGraceDuration = 0.12;
|
||||
constexpr Sint16 kGamepadAxisPressThreshold = 16384;
|
||||
constexpr Sint16 kGamepadAxisReleaseThreshold = 12000;
|
||||
constexpr int kGamepadAxisDirectionCount = SDL_GAMEPAD_AXIS_COUNT * 2;
|
||||
|
||||
struct GamepadRepeatState {
|
||||
Rml::Input::KeyIdentifier key = Rml::Input::KI_UNKNOWN;
|
||||
double pressedAt = 0.0;
|
||||
double nextRepeatAt = 0.0;
|
||||
bool held = false;
|
||||
bool repeatable = false;
|
||||
bool pending = false;
|
||||
};
|
||||
|
||||
bool sPadInputBlocked = false;
|
||||
std::array<GamepadRepeatState, SDL_GAMEPAD_BUTTON_COUNT> sGamepadButtonRepeats;
|
||||
std::array<GamepadRepeatState, kGamepadAxisDirectionCount> sGamepadAxisRepeats;
|
||||
std::array<u32, PAD_MAX_CONTROLLERS> sPadHoldMasks;
|
||||
std::array<bool, PAD_MAX_CONTROLLERS> sMenuChordConsumed;
|
||||
|
||||
double now_seconds() noexcept {
|
||||
return static_cast<double>(SDL_GetTicksNS()) / 1000000000.0;
|
||||
}
|
||||
|
||||
bool is_menu_chord_part(PADButton button) noexcept {
|
||||
return button == PAD_TRIGGER_R || button == PAD_BUTTON_START;
|
||||
}
|
||||
|
||||
bool has_menu_chord_part_held(u32 port) noexcept {
|
||||
if (port >= sPadHoldMasks.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u32 held = sPadHoldMasks[port];
|
||||
return (held & (PAD_TRIGGER_R | PAD_BUTTON_START)) != 0;
|
||||
}
|
||||
|
||||
bool should_block_pad_for_menu_chord() noexcept {
|
||||
for (u32 port = 0; port < sPadHoldMasks.size(); ++port) {
|
||||
if (sMenuChordConsumed[port] && has_menu_chord_part_held(port)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PADButton pad_button_from_axis(PADAxis axis) noexcept {
|
||||
switch (axis) {
|
||||
case PAD_AXIS_TRIGGER_R:
|
||||
return PAD_TRIGGER_R;
|
||||
case PAD_AXIS_TRIGGER_L:
|
||||
return PAD_TRIGGER_L;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void set_pad_button_held(u32 port, PADButton button, bool held) noexcept {
|
||||
if (port >= sPadHoldMasks.size() || button == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (held) {
|
||||
sPadHoldMasks[port] |= button;
|
||||
} else {
|
||||
sPadHoldMasks[port] &= ~button;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_menu_chord(u32 port) noexcept {
|
||||
if (port >= sPadHoldMasks.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u32 held = sPadHoldMasks[port];
|
||||
return (held & PAD_TRIGGER_R) != 0 && (held & PAD_BUTTON_START) != 0;
|
||||
}
|
||||
|
||||
bool any_menu_chord() noexcept {
|
||||
return std::any_of(sPadHoldMasks.begin(), sPadHoldMasks.end(),
|
||||
[](u32 held) { return (held & PAD_TRIGGER_R) != 0 && (held & PAD_BUTTON_START) != 0; });
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_pad_button(PADButton button) noexcept {
|
||||
switch (button) {
|
||||
case PAD_BUTTON_UP:
|
||||
return Rml::Input::KI_UP;
|
||||
case PAD_BUTTON_DOWN:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case PAD_BUTTON_LEFT:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case PAD_BUTTON_RIGHT:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case PAD_BUTTON_B:
|
||||
return Rml::Input::KI_ESCAPE;
|
||||
case PAD_BUTTON_A:
|
||||
return Rml::Input::KI_RETURN;
|
||||
case PAD_TRIGGER_R:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case PAD_TRIGGER_L:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_pad_axis(PADAxis axis) noexcept {
|
||||
switch (axis) {
|
||||
case PAD_AXIS_LEFT_X_POS:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case PAD_AXIS_LEFT_X_NEG:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case PAD_AXIS_LEFT_Y_POS:
|
||||
return Rml::Input::KI_UP;
|
||||
case PAD_AXIS_LEFT_Y_NEG:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case PAD_AXIS_TRIGGER_R:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case PAD_AXIS_TRIGGER_L:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexcept {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||
return Rml::Input::KI_UP;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case SDL_GAMEPAD_BUTTON_EAST:
|
||||
return Rml::Input::KI_ESCAPE;
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||
return Rml::Input::KI_RETURN;
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
return Rml::Input::KI_PAUSE;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_button_alias(SDL_GamepadButton button) noexcept {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
return Rml::Input::KI_PAUSE;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_gamepad_axis(SDL_GamepadAxis axis, PADAxisSign sign) noexcept {
|
||||
switch (axis) {
|
||||
case SDL_GAMEPAD_AXIS_LEFTX:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_RIGHT : Rml::Input::KI_LEFT;
|
||||
case SDL_GAMEPAD_AXIS_LEFTY:
|
||||
return sign == AXIS_SIGN_NEGATIVE ? Rml::Input::KI_UP : Rml::Input::KI_DOWN;
|
||||
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_NEXT : Rml::Input::KI_UNKNOWN;
|
||||
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_PRIOR : Rml::Input::KI_UNKNOWN;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
bool find_event_port(SDL_JoystickID instance, u32& port) noexcept {
|
||||
for (u32 candidate = 0; candidate < PAD_MAX_CONTROLLERS; ++candidate) {
|
||||
const s32 index = PADGetIndexForPort(candidate);
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(static_cast<u32>(index));
|
||||
if (gamepad == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
if (joystick != nullptr && SDL_GetJoystickID(joystick) == instance) {
|
||||
port = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_mapped_pad_button(u32 port, SDL_GamepadButton nativeButton, PADButton& button) noexcept {
|
||||
u32 buttonCount = 0;
|
||||
PADButtonMapping* buttons = PADGetButtonMappings(port, &buttonCount);
|
||||
if (buttons != nullptr) {
|
||||
for (u32 i = 0; i < buttonCount; ++i) {
|
||||
if (buttons[i].nativeButton == static_cast<u32>(nativeButton)) {
|
||||
button = buttons[i].padButton;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u32 axisCount = 0;
|
||||
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
|
||||
if (axes != nullptr) {
|
||||
for (u32 i = 0; i < axisCount; ++i) {
|
||||
if (axes[i].nativeButton == nativeButton) {
|
||||
button = pad_button_from_axis(axes[i].padAxis);
|
||||
return button != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_mapped_pad_axis(
|
||||
u32 port, SDL_GamepadAxis nativeAxis, PADAxisSign sign, PADAxis& axis) noexcept {
|
||||
u32 buttonCount = 0;
|
||||
PADGetButtonMappings(port, &buttonCount);
|
||||
|
||||
u32 axisCount = 0;
|
||||
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
|
||||
if (axes == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < axisCount; ++i) {
|
||||
const PADSignedNativeAxis mappedAxis = axes[i].nativeAxis;
|
||||
if (mappedAxis.nativeAxis == nativeAxis && mappedAxis.sign == sign) {
|
||||
axis = axes[i].padAxis;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_event_pad_button(
|
||||
const SDL_GamepadButtonEvent& event, u32& port, PADButton& button) noexcept {
|
||||
return find_event_port(event.which, port) &&
|
||||
find_mapped_pad_button(port, static_cast<SDL_GamepadButton>(event.button), button);
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_gamepad_button(const SDL_GamepadButtonEvent& event) noexcept {
|
||||
const auto nativeButton = static_cast<SDL_GamepadButton>(event.button);
|
||||
if (nativeButton == SDL_GAMEPAD_BUTTON_BACK) {
|
||||
return Rml::Input::KI_PAUSE;
|
||||
}
|
||||
|
||||
u32 port = 0;
|
||||
if (!find_event_port(event.which, port)) {
|
||||
return map_raw_gamepad_button(nativeButton);
|
||||
}
|
||||
|
||||
PADButton button = 0;
|
||||
if (find_mapped_pad_button(port, nativeButton, button)) {
|
||||
const auto key = map_pad_button(button);
|
||||
return key == Rml::Input::KI_UNKNOWN ? map_raw_button_alias(nativeButton) : key;
|
||||
}
|
||||
|
||||
return map_raw_button_alias(nativeButton);
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_gamepad_axis(
|
||||
const SDL_GamepadAxisEvent& event, PADAxisSign sign) noexcept {
|
||||
u32 port = 0;
|
||||
if (!find_event_port(event.which, port)) {
|
||||
return map_raw_gamepad_axis(static_cast<SDL_GamepadAxis>(event.axis), sign);
|
||||
}
|
||||
|
||||
PADAxis axis = 0;
|
||||
if (find_mapped_pad_axis(port, static_cast<SDL_GamepadAxis>(event.axis), sign, axis)) {
|
||||
return map_pad_axis(axis);
|
||||
}
|
||||
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
|
||||
bool is_repeatable_key(Rml::Input::KeyIdentifier key) noexcept {
|
||||
switch (key) {
|
||||
case Rml::Input::KI_UP:
|
||||
case Rml::Input::KI_DOWN:
|
||||
case Rml::Input::KI_LEFT:
|
||||
case Rml::Input::KI_RIGHT:
|
||||
case Rml::Input::KI_NEXT:
|
||||
case Rml::Input::KI_PRIOR:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
double repeat_interval(double heldFor) noexcept {
|
||||
const double ramp = std::clamp(heldFor / kGamepadRepeatRampDuration, 0.0, 1.0);
|
||||
return kGamepadRepeatStartInterval +
|
||||
(kGamepadRepeatMinInterval - kGamepadRepeatStartInterval) * ramp;
|
||||
}
|
||||
|
||||
GamepadRepeatState* button_repeat_state(SDL_GamepadButton button) noexcept {
|
||||
const auto index = static_cast<int>(button);
|
||||
if (index < 0 || index >= static_cast<int>(sGamepadButtonRepeats.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
return &sGamepadButtonRepeats[index];
|
||||
}
|
||||
|
||||
GamepadRepeatState* axis_repeat_state(SDL_GamepadAxis axis, PADAxisSign sign) noexcept {
|
||||
const auto axisIndex = static_cast<int>(axis);
|
||||
if (axisIndex < 0 || axisIndex >= SDL_GAMEPAD_AXIS_COUNT) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int directionOffset = sign == AXIS_SIGN_POSITIVE ? 0 : 1;
|
||||
return &sGamepadAxisRepeats[axisIndex * 2 + directionOffset];
|
||||
}
|
||||
|
||||
void clear_gamepad_repeats() noexcept {
|
||||
for (auto& repeat : sGamepadButtonRepeats) {
|
||||
repeat = {};
|
||||
}
|
||||
for (auto& repeat : sGamepadAxisRepeats) {
|
||||
repeat = {};
|
||||
}
|
||||
sPadHoldMasks.fill(0);
|
||||
sMenuChordConsumed.fill(false);
|
||||
}
|
||||
|
||||
void begin_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
|
||||
if (repeat.held) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = now_seconds();
|
||||
repeat.key = key;
|
||||
repeat.pressedAt = now;
|
||||
repeat.held = true;
|
||||
repeat.repeatable = is_repeatable_key(key);
|
||||
repeat.nextRepeatAt = repeat.repeatable ? now + kGamepadRepeatInitialDelay : 0.0;
|
||||
repeat.pending = false;
|
||||
}
|
||||
|
||||
void begin_pending_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
|
||||
if (repeat.held) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = now_seconds();
|
||||
repeat.key = key;
|
||||
repeat.pressedAt = now;
|
||||
repeat.held = true;
|
||||
repeat.repeatable = is_repeatable_key(key);
|
||||
repeat.nextRepeatAt = 0.0;
|
||||
repeat.pending = true;
|
||||
}
|
||||
|
||||
void consume_menu_chord(u32 port, Rml::Context& context) noexcept {
|
||||
if (port < sMenuChordConsumed.size()) {
|
||||
sMenuChordConsumed[port] = true;
|
||||
}
|
||||
|
||||
auto cancel_next = [&context](GamepadRepeatState& repeat) {
|
||||
if (!repeat.held || repeat.key != Rml::Input::KI_NEXT) {
|
||||
return;
|
||||
}
|
||||
if (!repeat.pending) {
|
||||
context.ProcessKeyUp(repeat.key, 0);
|
||||
}
|
||||
repeat = {};
|
||||
};
|
||||
|
||||
for (auto& repeat : sGamepadButtonRepeats) {
|
||||
cancel_next(repeat);
|
||||
}
|
||||
for (auto& repeat : sGamepadAxisRepeats) {
|
||||
cancel_next(repeat);
|
||||
}
|
||||
}
|
||||
|
||||
void update_menu_chord_release(u32 port) noexcept {
|
||||
if (port >= sMenuChordConsumed.size() || has_menu_chord_part_held(port)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sMenuChordConsumed[port] = false;
|
||||
}
|
||||
|
||||
bool should_defer_menu_chord_part(PADButton button, Rml::Input::KeyIdentifier key) noexcept {
|
||||
return button == PAD_TRIGGER_R && key == Rml::Input::KI_NEXT;
|
||||
}
|
||||
|
||||
void process_axis_direction(
|
||||
Rml::Context& context, const SDL_GamepadAxisEvent& event, PADAxisSign sign) noexcept {
|
||||
GamepadRepeatState* repeat = axis_repeat_state(static_cast<SDL_GamepadAxis>(event.axis), sign);
|
||||
if (repeat == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool active = sign == AXIS_SIGN_POSITIVE ? event.value >= kGamepadAxisPressThreshold :
|
||||
event.value <= -kGamepadAxisPressThreshold;
|
||||
const bool released = sign == AXIS_SIGN_POSITIVE ? event.value <= kGamepadAxisReleaseThreshold :
|
||||
event.value >= -kGamepadAxisReleaseThreshold;
|
||||
|
||||
u32 port = 0;
|
||||
PADAxis padAxis = 0;
|
||||
const bool hasPadAxis =
|
||||
find_event_port(event.which, port) &&
|
||||
find_mapped_pad_axis(port, static_cast<SDL_GamepadAxis>(event.axis), sign, padAxis);
|
||||
const PADButton heldPadButton = hasPadAxis ? pad_button_from_axis(padAxis) : 0;
|
||||
|
||||
if (repeat->held) {
|
||||
if (released) {
|
||||
if (!repeat->pending) {
|
||||
context.ProcessKeyUp(repeat->key, 0);
|
||||
}
|
||||
set_pad_button_held(port, heldPadButton, false);
|
||||
*repeat = {};
|
||||
update_menu_chord_release(port);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
set_pad_button_held(port, heldPadButton, true);
|
||||
const bool chorded = heldPadButton == PAD_TRIGGER_R && is_menu_chord(port);
|
||||
if (chorded) {
|
||||
consume_menu_chord(port, context);
|
||||
}
|
||||
const auto key = chorded ? Rml::Input::KI_PAUSE : map_gamepad_axis(event, sign);
|
||||
if (key == Rml::Input::KI_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chorded && should_defer_menu_chord_part(heldPadButton, key)) {
|
||||
begin_pending_gamepad_key(*repeat, key);
|
||||
return;
|
||||
}
|
||||
|
||||
begin_gamepad_key(*repeat, key);
|
||||
context.ProcessMouseLeave();
|
||||
context.ProcessKeyDown(key, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void sync_input_block() noexcept {
|
||||
const bool shouldBlock = any_document_visible() || should_block_pad_for_menu_chord();
|
||||
if (sPadInputBlocked == shouldBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
PADBlockInput(shouldBlock);
|
||||
sPadInputBlocked = shouldBlock;
|
||||
}
|
||||
|
||||
void release_input_block() noexcept {
|
||||
if (!sPadInputBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
PADBlockInput(false);
|
||||
sPadInputBlocked = false;
|
||||
}
|
||||
|
||||
void reset_input_state() noexcept {
|
||||
clear_gamepad_repeats();
|
||||
}
|
||||
|
||||
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_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
|
||||
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
process_axis_direction(*context, event.gaxis, AXIS_SIGN_POSITIVE);
|
||||
process_axis_direction(*context, event.gaxis, AXIS_SIGN_NEGATIVE);
|
||||
sync_input_block();
|
||||
return;
|
||||
}
|
||||
|
||||
auto* repeat = button_repeat_state(static_cast<SDL_GamepadButton>(event.gbutton.button));
|
||||
u32 port = 0;
|
||||
PADButton button = 0;
|
||||
const bool hasPadButton = find_event_pad_button(event.gbutton, port, button);
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
set_pad_button_held(port, button, true);
|
||||
const bool chorded = hasPadButton && is_menu_chord_part(button) && is_menu_chord(port);
|
||||
if (chorded) {
|
||||
consume_menu_chord(port, *context);
|
||||
}
|
||||
const auto key = chorded ? Rml::Input::KI_PAUSE : map_gamepad_button(event.gbutton);
|
||||
if (key != Rml::Input::KI_UNKNOWN) {
|
||||
bool deferred = false;
|
||||
if (repeat != nullptr) {
|
||||
if (!chorded && should_defer_menu_chord_part(button, key)) {
|
||||
begin_pending_gamepad_key(*repeat, key);
|
||||
deferred = true;
|
||||
} else {
|
||||
begin_gamepad_key(*repeat, key);
|
||||
}
|
||||
}
|
||||
if (!deferred) {
|
||||
context->ProcessMouseLeave();
|
||||
context->ProcessKeyDown(key, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto key = repeat != nullptr && repeat->held ? repeat->key : Rml::Input::KI_UNKNOWN;
|
||||
const bool wasPending = repeat != nullptr && repeat->pending;
|
||||
set_pad_button_held(port, button, false);
|
||||
update_menu_chord_release(port);
|
||||
if (key != Rml::Input::KI_UNKNOWN) {
|
||||
if (repeat != nullptr) {
|
||||
*repeat = {};
|
||||
}
|
||||
if (!wasPending) {
|
||||
context->ProcessKeyUp(key, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
void update_input() noexcept {
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context != nullptr) {
|
||||
const double now = now_seconds();
|
||||
auto process_repeats = [context, now](auto& repeats) {
|
||||
for (auto& repeat : repeats) {
|
||||
if (!repeat.held) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (repeat.pending) {
|
||||
if (now < repeat.pressedAt + kGamepadMenuChordGraceDuration) {
|
||||
continue;
|
||||
}
|
||||
|
||||
repeat.pending = false;
|
||||
repeat.pressedAt = now;
|
||||
repeat.nextRepeatAt =
|
||||
repeat.repeatable ? now + kGamepadRepeatInitialDelay : 0.0;
|
||||
context->ProcessMouseLeave();
|
||||
context->ProcessKeyDown(repeat.key, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!repeat.repeatable || now < repeat.nextRepeatAt ||
|
||||
(repeat.key == Rml::Input::KI_NEXT && any_menu_chord()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
context->ProcessKeyDown(repeat.key, 0);
|
||||
const double heldFor = now - repeat.pressedAt;
|
||||
repeat.nextRepeatAt = now + repeat_interval(heldFor);
|
||||
}
|
||||
};
|
||||
process_repeats(sGamepadButtonRepeats);
|
||||
process_repeats(sGamepadAxisRepeats);
|
||||
} else {
|
||||
reset_input_state();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
void update_input() noexcept;
|
||||
void reset_input_state() noexcept;
|
||||
void sync_input_block() noexcept;
|
||||
void release_input_block() noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -12,6 +12,7 @@ enum class NavCommand {
|
||||
Previous, // L1
|
||||
Confirm, // A
|
||||
Cancel, // B
|
||||
Menu, // Back/Minus, or R + Start
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+12
-8
@@ -60,9 +60,8 @@ void set_value(GraphicsOption option, int value) {
|
||||
getSettings().game.shadowResolutionMultiplier.setValue(value);
|
||||
break;
|
||||
case GraphicsOption::BloomMode:
|
||||
getSettings().game.bloomMode.setValue(
|
||||
static_cast<BloomMode>(std::clamp(value, static_cast<int>(BloomMode::Off),
|
||||
static_cast<int>(BloomMode::Dusk))));
|
||||
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
|
||||
value, static_cast<int>(BloomMode::Off), static_cast<int>(BloomMode::Dusk))));
|
||||
break;
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
|
||||
@@ -209,10 +208,12 @@ Overlay::Overlay(OverlayProps props)
|
||||
|
||||
if (auto* footer = mDocument->GetElementById("footer")) {
|
||||
auto& returnButton = add_component<Button>(footer, "\xE2\x86\x90 Return", "footer-button")
|
||||
.on_pressed(pop_document);
|
||||
.on_pressed(pop_document);
|
||||
returnButton.root()->SetClass("return", true);
|
||||
auto& resetButton = add_component<Button>(footer, "Reset to default", "footer-button")
|
||||
.on_pressed([this] { reset_default(); });
|
||||
auto& resetButton =
|
||||
add_component<Button>(footer, "Reset to default", "footer-button").on_pressed([this] {
|
||||
reset_default();
|
||||
});
|
||||
resetButton.root()->SetClass("reset", true);
|
||||
}
|
||||
|
||||
@@ -221,7 +222,7 @@ Overlay::Overlay(OverlayProps props)
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot &&
|
||||
*mRoot->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible &&
|
||||
mDismissed)
|
||||
!mRoot->HasAttribute("open"))
|
||||
{
|
||||
Document::hide();
|
||||
}
|
||||
@@ -235,7 +236,6 @@ void Overlay::show() {
|
||||
|
||||
void Overlay::hide() {
|
||||
mRoot->RemoveAttribute("open");
|
||||
mDismissed = true;
|
||||
}
|
||||
|
||||
void Overlay::update() {
|
||||
@@ -254,6 +254,10 @@ bool Overlay::focus() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Overlay::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Overlay::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Cancel) {
|
||||
pop_document();
|
||||
|
||||
@@ -63,6 +63,7 @@ public:
|
||||
void hide() override;
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
+5
-19
@@ -47,7 +47,7 @@ Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("pop
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot &&
|
||||
*mRoot->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible &&
|
||||
!mVisible)
|
||||
!mRoot->HasAttribute("open"))
|
||||
{
|
||||
Document::hide();
|
||||
}
|
||||
@@ -58,39 +58,25 @@ Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("pop
|
||||
}
|
||||
|
||||
void Popup::show() {
|
||||
if (mDocument == nullptr || mVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
mTabBar->set_active_tab(-1);
|
||||
mVisible = true;
|
||||
}
|
||||
|
||||
void Popup::hide() {
|
||||
if (mDocument == nullptr) {
|
||||
mVisible = false;
|
||||
return;
|
||||
}
|
||||
if (!mVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
mRoot->RemoveAttribute("open");
|
||||
mVisible = false;
|
||||
}
|
||||
|
||||
void Popup::toggle() {
|
||||
if (is_visible()) {
|
||||
if (visible()) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
bool Popup::is_visible() const {
|
||||
return mVisible;
|
||||
bool Popup::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
@@ -98,7 +84,7 @@ bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
toggle();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
bool Popup::focus() {
|
||||
|
||||
@@ -18,9 +18,9 @@ public:
|
||||
void show() override;
|
||||
void hide() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
|
||||
void toggle();
|
||||
bool is_visible() const;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
+28
-4
@@ -8,6 +8,7 @@
|
||||
#include <filesystem>
|
||||
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "input.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
@@ -45,13 +46,11 @@ bool initialize() noexcept {
|
||||
|
||||
void shutdown() noexcept {
|
||||
sDocuments.clear();
|
||||
reset_input_state();
|
||||
release_input_block();
|
||||
sInitialized = false;
|
||||
}
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept {
|
||||
// TODO
|
||||
}
|
||||
|
||||
Document& push_document(std::unique_ptr<Document> doc, bool show) noexcept {
|
||||
if (auto* doc = top_document()) {
|
||||
doc->hide();
|
||||
@@ -61,6 +60,7 @@ Document& push_document(std::unique_ptr<Document> doc, bool show) noexcept {
|
||||
if (show) {
|
||||
ret.show();
|
||||
}
|
||||
sync_input_block();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,26 @@ void pop_document() noexcept {
|
||||
if (auto* doc = top_document()) {
|
||||
doc->show();
|
||||
}
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
void toggle_top_document() noexcept {
|
||||
auto* doc = top_document();
|
||||
if (doc == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc->visible()) {
|
||||
doc->hide();
|
||||
} else {
|
||||
doc->show();
|
||||
}
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
bool any_document_visible() noexcept {
|
||||
return std::any_of(sDocuments.begin(), sDocuments.end(),
|
||||
[](const OpenDocument& doc) { return doc.doc != nullptr && doc.doc->visible(); });
|
||||
}
|
||||
|
||||
Document* top_document() noexcept {
|
||||
@@ -87,6 +107,7 @@ Document* top_document() noexcept {
|
||||
}
|
||||
|
||||
void update() noexcept {
|
||||
update_input();
|
||||
for (const auto& doc : sDocuments) {
|
||||
doc.doc->update();
|
||||
}
|
||||
@@ -94,6 +115,7 @@ void update() noexcept {
|
||||
std::remove_if(sDocuments.begin(), sDocuments.end(),
|
||||
[](const OpenDocument& doc) { return doc.pendingDestroy && doc.doc->can_destroy(); }),
|
||||
sDocuments.end());
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {
|
||||
@@ -145,6 +167,8 @@ NavCommand map_nav_event(const Rml::Event& event) noexcept {
|
||||
return NavCommand::Cancel;
|
||||
case Rml::Input::KeyIdentifier::KI_RETURN:
|
||||
return NavCommand::Confirm;
|
||||
case Rml::Input::KeyIdentifier::KI_PAUSE:
|
||||
return NavCommand::Menu;
|
||||
case Rml::Input::KeyIdentifier::KI_NEXT:
|
||||
return NavCommand::Next;
|
||||
case Rml::Input::KeyIdentifier::KI_PRIOR:
|
||||
|
||||
@@ -34,6 +34,8 @@ void update() noexcept;
|
||||
|
||||
Document& push_document(std::unique_ptr<Document> doc, bool show = true) noexcept;
|
||||
void pop_document() noexcept;
|
||||
void toggle_top_document() noexcept;
|
||||
bool any_document_visible() noexcept;
|
||||
Document* top_document() noexcept;
|
||||
|
||||
Popup& add_popup(std::unique_ptr<Popup> popup) noexcept;
|
||||
|
||||
+10
-16
@@ -66,7 +66,7 @@ Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("w
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot &&
|
||||
*mRoot->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible &&
|
||||
!mVisible)
|
||||
!mRoot->HasAttribute("open"))
|
||||
{
|
||||
Document::hide();
|
||||
}
|
||||
@@ -88,26 +88,13 @@ Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("w
|
||||
}
|
||||
|
||||
void Window::show() {
|
||||
if (mVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
mVisible = true;
|
||||
}
|
||||
|
||||
void Window::hide() {
|
||||
if (mDocument == nullptr) {
|
||||
mVisible = false;
|
||||
return;
|
||||
}
|
||||
if (!mVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
mRoot->RemoveAttribute("open");
|
||||
mVisible = false;
|
||||
// Document will be hidden after transition
|
||||
}
|
||||
|
||||
void Window::update() {
|
||||
@@ -171,6 +158,10 @@ bool Window::focus() {
|
||||
return mTabBar->focus();
|
||||
}
|
||||
|
||||
bool Window::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
auto* target = event.GetTargetElement();
|
||||
if (cmd != NavCommand::Next && cmd != NavCommand::Previous && target->Closest("content")) {
|
||||
@@ -187,7 +178,10 @@ bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
pop_document();
|
||||
return true;
|
||||
}
|
||||
return mTabBar->handle_nav_command(event, cmd);
|
||||
if (mTabBar->handle_nav_command(event, cmd)) {
|
||||
return true;
|
||||
}
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
void hide() override;
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
bool set_active_tab(int index);
|
||||
|
||||
protected:
|
||||
|
||||
Reference in New Issue
Block a user