From 2d4e69466b41fab2743662bb835f11dc601a930a Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 17 Jun 2026 22:48:44 -0600 Subject: [PATCH] Refine menu_pointer click events Only short clicks/taps count & they must not move between targets --- include/d/d_menu_ring.h | 1 + include/dusk/menu_pointer.h | 9 +- src/d/d_file_select.cpp | 53 +++++++----- src/d/d_menu_collect.cpp | 1 + src/d/d_menu_insect.cpp | 1 + src/d/d_menu_letter.cpp | 1 + src/d/d_menu_option.cpp | 50 +++++++++-- src/d/d_menu_ring.cpp | 24 +++++- src/d/d_menu_save.cpp | 26 +++--- src/d/d_menu_skill.cpp | 1 + src/d/d_msg_scrn_3select.cpp | 3 +- src/dusk/menu_pointer.cpp | 156 +++++++++++++++++++++++++++++++---- 12 files changed, 272 insertions(+), 54 deletions(-) diff --git a/include/d/d_menu_ring.h b/include/d/d_menu_ring.h index f2137a9748..c7467d2a4b 100644 --- a/include/d/d_menu_ring.h +++ b/include/d/d_menu_ring.h @@ -218,6 +218,7 @@ private: bool mCursorInterpPrevAngular; bool mCursorInterpCurrAngular; bool mCursorInterpInit; + bool mPointerTouchPressHoveredCurrent; #endif }; diff --git a/include/dusk/menu_pointer.h b/include/dusk/menu_pointer.h index 17546877d6..86123f1163 100644 --- a/include/dusk/menu_pointer.h +++ b/include/dusk/menu_pointer.h @@ -6,6 +6,9 @@ class CPaneMgr; namespace dusk::menu_pointer { +using TargetId = u16; +constexpr TargetId InvalidTarget = 0xffff; + enum class Context { None, FileSelect, @@ -43,12 +46,14 @@ bool active() noexcept; bool enabled() noexcept; bool mouse_capture_active() noexcept; const State& state() noexcept; +void set_hover_target(TargetId target) noexcept; bool consume_click() noexcept; +bool peek_click() noexcept; void set_dialog_choice(u8 choice, bool clicked) noexcept; bool get_dialog_choice(u8& choice) noexcept; bool consume_dialog_click(u8& choice) noexcept; -void defer_activation(Context context, u8 target) noexcept; -bool consume_deferred_activation(Context context, u8 target) noexcept; +void defer_activation(Context context, TargetId target) noexcept; +bool consume_deferred_activation(Context context, TargetId target) noexcept; void clear_deferred_activation(Context context) noexcept; u32 suppressed_pad_buttons(u32 port) noexcept; void finish_pad_suppression_read(u32 port) noexcept; diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 6e5e2e4954..19cdcfe81c 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -777,6 +777,7 @@ bool dFile_select_c::pointerDataSelect() { if (!dusk::menu_pointer::hit_pane(mSelFilePanes[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerDataSelectTarget, i)); const bool clicked = dusk::menu_pointer::consume_click(); if (mSelectNum != i) { mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0); @@ -805,6 +806,7 @@ bool dFile_select_c::pointerMenuSelect() { if (!dusk::menu_pointer::hit_pane(m3mSelPane[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerMenuSelectTarget, i)); const bool clicked = dusk::menu_pointer::consume_click(); if (!mIsDataNew[mSelectNum] && mSelectMenuNum != i) { mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0); @@ -833,6 +835,7 @@ bool dFile_select_c::pointerCopyDataToSelect() { if (!dusk::menu_pointer::hit_pane(mCpSelPane[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerCopySelectTarget, i)); const bool clicked = dusk::menu_pointer::consume_click(); if (field_0x026b != i) { mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0); @@ -861,6 +864,7 @@ bool dFile_select_c::pointerYesNoSelect(bool errorSelect) { if (!dusk::menu_pointer::hit_pane(mYnSelPane[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, i)); const bool clicked = (!errorSelect || field_0x0268 == i) && dusk::menu_pointer::consume_click(); if (field_0x0268 != i) { @@ -1103,12 +1107,13 @@ void dFile_select_c::dataSelectAnmSet() { void dFile_select_c::dataSelectMoveAnime() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); - if (mSelectNum != 0xFF && dusk::menu_pointer::hit_pane(mSelFilePanes[mSelectNum], 8.0f) && - dusk::menu_pointer::consume_click()) - { - dusk::menu_pointer::defer_activation( - dusk::menu_pointer::Context::FileSelect, - pointer_target(s_pointerDataSelectTarget, mSelectNum)); + if (mSelectNum != 0xFF && dusk::menu_pointer::hit_pane(mSelFilePanes[mSelectNum], 8.0f)) { + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerDataSelectTarget, mSelectNum)); + if (dusk::menu_pointer::consume_click()) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerDataSelectTarget, mSelectNum)); + } } #endif bool iVar7 = true; @@ -1494,12 +1499,14 @@ void dFile_select_c::menuSelectMoveAnm() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); if (mSelectMenuNum != 0xFF && - dusk::menu_pointer::hit_pane(m3mSelPane[mSelectMenuNum], 8.0f) && - dusk::menu_pointer::consume_click()) + dusk::menu_pointer::hit_pane(m3mSelPane[mSelectMenuNum], 8.0f)) { - dusk::menu_pointer::defer_activation( - dusk::menu_pointer::Context::FileSelect, - pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum)); + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum)); + if (dusk::menu_pointer::consume_click()) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum)); + } } #endif bool tmp1 = true; @@ -1997,12 +2004,14 @@ void dFile_select_c::copyDataToSelectMoveAnm() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); if (field_0x026b != 0xFF && - dusk::menu_pointer::hit_pane(mCpSelPane[field_0x026b], 8.0f) && - dusk::menu_pointer::consume_click()) + dusk::menu_pointer::hit_pane(mCpSelPane[field_0x026b], 8.0f)) { - dusk::menu_pointer::defer_activation( - dusk::menu_pointer::Context::FileSelect, - pointer_target(s_pointerCopySelectTarget, field_0x026b)); + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerCopySelectTarget, field_0x026b)); + if (dusk::menu_pointer::consume_click()) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerCopySelectTarget, field_0x026b)); + } } #endif bool iVar7 = true; @@ -2522,12 +2531,14 @@ void dFile_select_c::yesNoCursorMoveAnm() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); if (field_0x0268 != 0xFF && - dusk::menu_pointer::hit_pane(mYnSelPane[field_0x0268], 8.0f) && - dusk::menu_pointer::consume_click()) + dusk::menu_pointer::hit_pane(mYnSelPane[field_0x0268], 8.0f)) { - dusk::menu_pointer::defer_activation( - dusk::menu_pointer::Context::FileSelect, - pointer_target(s_pointerYesNoSelectTarget, field_0x0268)); + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, field_0x0268)); + if (dusk::menu_pointer::consume_click()) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerYesNoSelectTarget, field_0x0268)); + } } #endif bool isYnSelMove = yesnoSelectMoveAnm(); diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index 3aea57e1d3..4e3ddf25f2 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -1960,6 +1960,7 @@ bool dMenu_Collect2D_c::pointerWait() { if (getItemTag(x, y, true) == 0 || !dusk::menu_pointer::hit_pane(mpSelPm[x][y], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(static_cast(x + y * 7)); if (mCursorX != x || mCursorY != y) { mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0); mCursorX = x; diff --git a/src/d/d_menu_insect.cpp b/src/d/d_menu_insect.cpp index 7967ca6cdf..aa18407b09 100644 --- a/src/d/d_menu_insect.cpp +++ b/src/d/d_menu_insect.cpp @@ -320,6 +320,7 @@ bool dMenu_Insect_c::pointerWait() { if (!isGetInsect(x, y) || !dusk::menu_pointer::hit_pane(mpINSParent[index], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(index); if (field_0xf4 != x || field_0xf5 != y) { field_0xf4 = x; diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp index 0d7efd3adb..3f997bbda5 100644 --- a/src/d/d_menu_letter.cpp +++ b/src/d/d_menu_letter.cpp @@ -482,6 +482,7 @@ bool dMenu_Letter_c::pointerWait() { if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(i); if (mIndex != i) { mIndex = i; diff --git a/src/d/d_menu_option.cpp b/src/d/d_menu_option.cpp index cc4bb01f2a..94babc8342 100644 --- a/src/d/d_menu_option.cpp +++ b/src/d/d_menu_option.cpp @@ -83,6 +83,12 @@ enum SelectType { SelectType8, }; +#if TARGET_PC +static dusk::menu_pointer::TargetId option_yes_no_target(u8 index) noexcept { + return static_cast(0x100 + index); +} +#endif + dMenu_Option_c::dMenu_Option_c(JKRArchive* i_archive, STControl* i_stick) { mUseFlag = 0; mBarScale[0] = g_drawHIO.mOptionScreen.mBarScale[0]; @@ -1098,18 +1104,28 @@ void dMenu_Option_c::confirm_move_move() { if (!dusk::menu_pointer::hit_pane(mpYesNoSelBase_c[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(option_yes_no_target(i)); + const bool clicked = dusk::menu_pointer::consume_click(); if (field_0x3f9 != i) { Z2GetAudioMgr()->seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); field_0x3fa = field_0x3f9; field_0x3f9 = i; + if (clicked) { + yesNoSelectStart(); + field_0x3ef = SelectType7; + dMeter2Info_set2DVibrationM(); + mpWarning->_move(); + setAnimation(); + return; + } yesnoSelectAnmSet(); field_0x3ef = SelectType6; mpWarning->_move(); setAnimation(); return; } - if (dusk::menu_pointer::consume_click()) { + if (clicked) { yesNoSelectStart(); field_0x3ef = SelectType7; dMeter2Info_set2DVibrationM(); @@ -1156,11 +1172,36 @@ void dMenu_Option_c::confirm_select_init() { } void dMenu_Option_c::confirm_select_move() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); + if (field_0x3f9 != 0xff && + dusk::menu_pointer::hit_pane(mpYesNoSelBase_c[field_0x3f9], 8.0f)) + { + const dusk::menu_pointer::TargetId target = option_yes_no_target(field_0x3f9); + dusk::menu_pointer::set_hover_target(target); + if (dusk::menu_pointer::consume_click()) { + dusk::menu_pointer::defer_activation(dusk::menu_pointer::Context::Options, target); + } + } +#endif u8 selectMoveAnm = yesnoSelectMoveAnm(); u8 wakuAlphaAnm = yesnoWakuAlpahAnm(field_0x3fa); if (selectMoveAnm == 1 && wakuAlphaAnm == 1) { yesnoCursorShow(); +#if TARGET_PC + if (field_0x3f9 != 0xff && + dusk::menu_pointer::consume_deferred_activation( + dusk::menu_pointer::Context::Options, option_yes_no_target(field_0x3f9))) + { + yesNoSelectStart(); + field_0x3ef = SelectType7; + dMeter2Info_set2DVibrationM(); + mpWarning->_move(); + setAnimation(); + return; + } +#endif field_0x3ef = SelectType5; } mpWarning->_move(); @@ -2196,16 +2237,14 @@ bool dMenu_Option_c::isRumbleSupported() { #if TARGET_PC bool dMenu_Option_c::pointerConfirmSelect() { dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); - if (!dusk::menu_pointer::state().clicked) { - return false; - } - for (u8 i = 0; i < SelectType3; ++i) { if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { + dusk::menu_pointer::set_hover_target(i); return false; } } + dusk::menu_pointer::set_hover_target(0x200); if (!dusk::menu_pointer::consume_click()) { return false; } @@ -2226,6 +2265,7 @@ bool dMenu_Option_c::dpdMenuMove() { if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(i); if (getSelectType() != i) { field_0x3ef = i; setCursorPos(i); diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index fde1f35af8..c3f1ed2a4a 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -198,6 +198,7 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i mCursorInterpPrevAngular = false; mCursorInterpCurrAngular = false; mCursorInterpInit = false; + mPointerTouchPressHoveredCurrent = false; #endif for (int i = 0; i < 4; i++) { field_0x674[i] = 0; @@ -1561,6 +1562,10 @@ bool dMenu_Ring_c::pointerMove() { if (hoveredSlot < 0) { return false; } + if (pointer.pressed) { + mPointerTouchPressHoveredCurrent = pointer.touch && hoveredSlot == mCurrentSlot; + } + dusk::menu_pointer::set_hover_target(static_cast(hoveredSlot)); if (mCurrentSlot != hoveredSlot) { mDirectSelectCursorPos.x = mItemSlotPosX[mCurrentSlot]; @@ -1573,10 +1578,27 @@ bool dMenu_Ring_c::pointerMove() { return true; } - if (dusk::menu_pointer::consume_click()) { + const bool clickOpensExplain = !pointer.touch || mPointerTouchPressHoveredCurrent; + if (clickOpensExplain && dusk::menu_pointer::consume_click()) { + const u8 item = dComIfGs_getItem(mItemSlots[mCurrentSlot], false); + if (!dMeter2Info_isTouchKeyCheck(0xe) && openExplain(item)) { + dMeter2Info_setItemExplainWindowStatus(1); + field_0x6c4 = mCurrentSlot; + setStatus(STATUS_EXPLAIN); + dMeter2Info_set2DVibration(); + setDoStatus(0); + } else { + Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + } + mPointerTouchPressHoveredCurrent = false; return true; } + if (pointer.released) { + mPointerTouchPressHoveredCurrent = false; + } + return false; } #endif diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index d272c57500..f97e50d331 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -1820,6 +1820,7 @@ bool dMenu_save_c::pointerSaveSelect() { if (!dusk::menu_pointer::hit_pane(mpSelData[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerSaveSelectTarget, i)); const bool clicked = dusk::menu_pointer::consume_click(); if (mSelectedFile != i) { mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0); @@ -1848,6 +1849,7 @@ bool dMenu_save_c::pointerYesNoSelect(bool errorSelect, u8 errParam, u8 soundPar if (!dusk::menu_pointer::hit_pane(mpNoYes[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, i)); const bool clicked = (!errorSelect || mYesNoCursor == i) && dusk::menu_pointer::consume_click(); if (mYesNoCursor != i) { @@ -1952,12 +1954,14 @@ void dMenu_save_c::saveSelectMoveAnime() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save); if (mSelectedFile != 0xFF && - dusk::menu_pointer::hit_pane(mpSelData[mSelectedFile], 8.0f) && - dusk::menu_pointer::consume_click()) + dusk::menu_pointer::hit_pane(mpSelData[mSelectedFile], 8.0f)) { - dusk::menu_pointer::defer_activation( - dusk::menu_pointer::Context::Save, - pointer_target(s_pointerSaveSelectTarget, mSelectedFile)); + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerSaveSelectTarget, mSelectedFile)); + if (dusk::menu_pointer::consume_click()) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::Save, + pointer_target(s_pointerSaveSelectTarget, mSelectedFile)); + } } #endif bool bookWakuAnmComplete = true; @@ -2130,12 +2134,14 @@ void dMenu_save_c::yesNoCursorMoveAnm() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save); if (mYesNoCursor != 0xFF && - dusk::menu_pointer::hit_pane(mpNoYes[mYesNoCursor], 8.0f) && - dusk::menu_pointer::consume_click()) + dusk::menu_pointer::hit_pane(mpNoYes[mYesNoCursor], 8.0f)) { - dusk::menu_pointer::defer_activation( - dusk::menu_pointer::Context::Save, - pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor)); + dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor)); + if (dusk::menu_pointer::consume_click()) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::Save, + pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor)); + } } #endif bool selAnmComplete = yesnoSelectMoveAnm(0); diff --git a/src/d/d_menu_skill.cpp b/src/d/d_menu_skill.cpp index 601bae1eb6..0c970e76c3 100644 --- a/src/d/d_menu_skill.cpp +++ b/src/d/d_menu_skill.cpp @@ -316,6 +316,7 @@ bool dMenu_Skill_c::pointerWait() { if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) { continue; } + dusk::menu_pointer::set_hover_target(i); if (mIndex != i) { mIndex = i; diff --git a/src/d/d_msg_scrn_3select.cpp b/src/d/d_msg_scrn_3select.cpp index b021b52dec..fd85da423f 100644 --- a/src/d/d_msg_scrn_3select.cpp +++ b/src/d/d_msg_scrn_3select.cpp @@ -560,7 +560,8 @@ bool dMsgScrn3Select_c::pointerMove() { mDPDPoint = choice; field_0x110 = paneIndex; - dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::state().clicked); + dusk::menu_pointer::set_hover_target(choice); + dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::peek_click()); return true; } diff --git a/src/dusk/menu_pointer.cpp b/src/dusk/menu_pointer.cpp index 91b66cce55..83ed7afc84 100644 --- a/src/dusk/menu_pointer.cpp +++ b/src/dusk/menu_pointer.cpp @@ -1,16 +1,34 @@ #include "dusk/menu_pointer.h" -#include "m_Do/m_Do_graphic.h" #include "d/d_pane_class.h" #include "dusk/settings.h" +#include "m_Do/m_Do_graphic.h" #include #include #include +#include namespace dusk::menu_pointer { namespace { +using Clock = std::chrono::steady_clock; + +constexpr auto kTapMaxDuration = std::chrono::milliseconds(300); +constexpr f32 kTapMoveThresholdDp = 12.0f; + +struct Gesture { + bool active = false; + bool movedTooFar = false; + bool crossedTarget = false; + bool pressTargetValid = false; + Context pressContext = Context::None; + TargetId pressTarget = InvalidTarget; + f32 startX = 0.0f; + f32 startY = 0.0f; + Clock::time_point startedAt{}; +}; + State s_state; bool s_clickConsumed = false; Context s_lastContext = Context::None; @@ -27,7 +45,14 @@ s32 s_mouseButton = -1; u32 s_suppressedPadHoldMask = 0; u32 s_suppressedPadNextReadMask = 0; Context s_deferredActivationContext = Context::None; -u8 s_deferredActivationTarget = 0xFF; +TargetId s_deferredActivationTarget = InvalidTarget; +Gesture s_gesture; +bool s_hoverTargetValid = false; +TargetId s_hoverTarget = InvalidTarget; +bool s_clickPending = false; +Context s_clickContext = Context::None; +TargetId s_clickTarget = InvalidTarget; +bool s_clickTargetValid = false; s32 scancode_from_rml_button(s32 button) noexcept { switch (button) { @@ -104,6 +129,37 @@ void suppress_pad_for_mouse_button(s32 button, bool held) noexcept { } } +f32 tap_move_threshold() noexcept { + auto* context = aurora::rmlui::get_context(); + if (context == nullptr) { + return kTapMoveThresholdDp; + } + + return kTapMoveThresholdDp * std::max(context->GetDensityIndependentPixelRatio(), 1.0f); +} + +void update_gesture_movement(f32 x, f32 y) noexcept { + if (!s_gesture.active || s_gesture.movedTooFar) { + return; + } + + const f32 dx = x - s_gesture.startX; + const f32 dy = y - s_gesture.startY; + const f32 threshold = tap_move_threshold(); + if (dx * dx + dy * dy > threshold * threshold) { + s_gesture.movedTooFar = true; + } +} + +void clear_click_state() noexcept { + s_clickConsumed = false; + s_clickPending = false; + s_clickContext = Context::None; + s_clickTarget = InvalidTarget; + s_clickTargetValid = false; + s_state.clicked = false; +} + void set_position_from_rml(f32 x, f32 y) noexcept { auto* context = aurora::rmlui::get_context(); if (context == nullptr) { @@ -121,7 +177,7 @@ void set_position_from_rml(f32 x, f32 y) noexcept { void clear_input_state() noexcept { s_state = {}; - s_clickConsumed = false; + clear_click_state(); s_lastDialogChoice = 0xFF; s_currentDialogChoice = 0xFF; s_lastDialogChoiceValid = false; @@ -134,7 +190,10 @@ void clear_input_state() noexcept { s_suppressedPadHoldMask = 0; s_suppressedPadNextReadMask = 0; s_deferredActivationContext = Context::None; - s_deferredActivationTarget = 0xFF; + s_deferredActivationTarget = InvalidTarget; + s_gesture = {}; + s_hoverTargetValid = false; + s_hoverTarget = InvalidTarget; } } // namespace @@ -144,8 +203,6 @@ bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouse return false; } - s_clickConsumed = false; - if (!touch) { if (phase == Phase::Press) { if (!mouse_button_is_menu_confirm(mouseButton)) { @@ -174,21 +231,41 @@ bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouse } if (phase != Phase::Cancel) { + update_gesture_movement(x, y); set_position_from_rml(x, y); } s_state.touch = touch; switch (phase) { case Phase::Press: + clear_click_state(); + s_gesture = { + .active = true, + .startX = x, + .startY = y, + .startedAt = Clock::now(), + }; s_state.down = true; s_state.pressed = true; break; - case Phase::Release: + case Phase::Release: { + const bool shortEnough = + s_gesture.active && Clock::now() - s_gesture.startedAt <= kTapMaxDuration; + const bool stillEnough = s_gesture.active && !s_gesture.movedTooFar; + const bool targetClean = s_gesture.active && !s_gesture.crossedTarget; + s_clickContext = s_gesture.pressContext; + s_clickTarget = s_gesture.pressTarget; + s_clickTargetValid = s_gesture.pressTargetValid; + s_clickPending = shortEnough && stillEnough && targetClean; s_state.down = false; s_state.released = true; - s_state.clicked = true; + s_state.clicked = s_clickPending; + s_gesture = {}; break; + } case Phase::Cancel: + clear_click_state(); + s_gesture = {}; s_state.down = false; break; case Phase::Move: @@ -211,6 +288,12 @@ void begin_game_frame() noexcept { } void end_game_frame() noexcept { + if (s_gesture.active && s_gesture.pressTargetValid && + s_currentContext == s_gesture.pressContext && !s_hoverTargetValid) + { + s_gesture.crossedTarget = true; + } + s_lastContext = s_currentContext; s_lastDialogChoice = s_currentDialogChoice; s_lastDialogChoiceValid = s_currentDialogChoiceValid; @@ -222,6 +305,12 @@ void end_game_frame() noexcept { s_state.valid = false; } s_clickConsumed = false; + s_clickPending = false; + s_clickContext = Context::None; + s_clickTarget = InvalidTarget; + s_clickTargetValid = false; + s_hoverTargetValid = false; + s_hoverTarget = InvalidTarget; } void begin_context(Context context) noexcept { @@ -237,7 +326,11 @@ void begin_context(Context context) noexcept { s_suppressedPadHoldMask = 0; s_suppressedPadNextReadMask = 0; s_deferredActivationContext = Context::None; - s_deferredActivationTarget = 0xFF; + s_deferredActivationTarget = InvalidTarget; + s_gesture = {}; + s_hoverTargetValid = false; + s_hoverTarget = InvalidTarget; + clear_click_state(); } s_currentContext = context; @@ -259,15 +352,50 @@ const State& state() noexcept { return s_state; } +void set_hover_target(TargetId target) noexcept { + s_hoverTargetValid = true; + s_hoverTarget = target; + + if (s_gesture.active && !s_gesture.pressTargetValid && s_state.down) { + s_gesture.pressContext = s_currentContext; + s_gesture.pressTarget = target; + s_gesture.pressTargetValid = true; + } + + if (s_gesture.active && s_gesture.pressTargetValid && + (s_currentContext != s_gesture.pressContext || target != s_gesture.pressTarget)) + { + s_gesture.crossedTarget = true; + } +} + +bool click_matches_hover_target() noexcept { + if (!s_clickPending || !s_hoverTargetValid) { + return false; + } + + if (!s_clickTargetValid) { + return true; + } + + return s_currentContext == s_clickContext && s_hoverTarget == s_clickTarget; +} + bool consume_click() noexcept { - if (!s_state.clicked || s_clickConsumed) { + if (s_clickConsumed || !click_matches_hover_target()) { return false; } s_clickConsumed = true; + s_clickPending = false; + s_state.clicked = false; return true; } +bool peek_click() noexcept { + return !s_clickConsumed && click_matches_hover_target(); +} + void set_dialog_choice(u8 choice, bool clicked) noexcept { s_currentDialogChoice = choice; s_currentDialogChoiceValid = true; @@ -300,18 +428,18 @@ bool consume_dialog_click(u8& choice) noexcept { return false; } -void defer_activation(Context context, u8 target) noexcept { +void defer_activation(Context context, TargetId target) noexcept { s_deferredActivationContext = context; s_deferredActivationTarget = target; } -bool consume_deferred_activation(Context context, u8 target) noexcept { +bool consume_deferred_activation(Context context, TargetId target) noexcept { if (s_deferredActivationContext != context || s_deferredActivationTarget != target) { return false; } s_deferredActivationContext = Context::None; - s_deferredActivationTarget = 0xFF; + s_deferredActivationTarget = InvalidTarget; return true; } @@ -321,7 +449,7 @@ void clear_deferred_activation(Context context) noexcept { } s_deferredActivationContext = Context::None; - s_deferredActivationTarget = 0xFF; + s_deferredActivationTarget = InvalidTarget; } u32 suppressed_pad_buttons(u32 port) noexcept {