#include "dusk/menu_pointer.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; Context s_currentContext = Context::None; u8 s_lastDialogChoice = 0xFF; u8 s_currentDialogChoice = 0xFF; bool s_lastDialogChoiceValid = false; bool s_currentDialogChoiceValid = false; bool s_lastDialogClicked = false; bool s_currentDialogClicked = false; bool s_mouseActive = false; bool s_mouseButtonCaptured = false; s32 s_mouseButton = -1; u32 s_suppressedPadHoldMask = 0; u32 s_suppressedPadNextReadMask = 0; Context s_deferredActivationContext = Context::None; 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) { case 0: return PAD_KEY_MOUSE_LEFT; case 1: return PAD_KEY_MOUSE_RIGHT; case 2: return PAD_KEY_MOUSE_MIDDLE; default: return PAD_KEY_INVALID; } } bool is_mouse_scancode(s32 scancode) noexcept { return scancode >= PAD_KEY_MOUSE_X2 && scancode <= PAD_KEY_MOUSE_LEFT; } PADButton pad_button_for_scancode(u32 port, s32 scancode) noexcept { u32 count = 0; PADKeyButtonBinding* bindings = PADGetKeyButtonBindings(port, &count); if (bindings == nullptr) { return 0; } for (u32 i = 0; i < count; ++i) { if (bindings[i].scancode == scancode) { return bindings[i].padButton; } } return 0; } s32 menu_confirm_mouse_scancode() noexcept { constexpr u32 port = PAD_CHAN0; u32 count = 0; PADKeyButtonBinding* bindings = PADGetKeyButtonBindings(port, &count); if (bindings == nullptr) { return PAD_KEY_MOUSE_LEFT; } for (u32 i = 0; i < count; ++i) { if (bindings[i].padButton == PAD_BUTTON_A && is_mouse_scancode(bindings[i].scancode)) { return bindings[i].scancode; } } return pad_button_for_scancode(port, PAD_KEY_MOUSE_LEFT) != 0 ? PAD_KEY_INVALID : PAD_KEY_MOUSE_LEFT; } bool mouse_button_is_menu_confirm(s32 button) noexcept { const s32 scancode = scancode_from_rml_button(button); return scancode != PAD_KEY_INVALID && scancode == menu_confirm_mouse_scancode(); } void suppress_pad_for_mouse_button(s32 button, bool held) noexcept { const s32 scancode = scancode_from_rml_button(button); if (scancode == PAD_KEY_INVALID) { return; } const PADButton padButton = pad_button_for_scancode(PAD_CHAN0, scancode); if (padButton == 0) { return; } s_suppressedPadNextReadMask |= padButton; if (held) { s_suppressedPadHoldMask |= padButton; } else { s_suppressedPadHoldMask &= ~padButton; } } 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) { return; } const auto dimensions = context->GetDimensions(); const f32 width = std::max(static_cast(dimensions.x), 1.0f); const f32 height = std::max(static_cast(dimensions.y), 1.0f); s_state.x = mDoGph_gInf_c::getMinXF() + x / width * mDoGph_gInf_c::getWidthF(); s_state.y = mDoGph_gInf_c::getMinYF() + y / height * mDoGph_gInf_c::getHeightF(); s_state.valid = true; } void clear_input_state() noexcept { s_state = {}; clear_click_state(); s_lastDialogChoice = 0xFF; s_currentDialogChoice = 0xFF; s_lastDialogChoiceValid = false; s_currentDialogChoiceValid = false; s_lastDialogClicked = false; s_currentDialogClicked = false; s_mouseActive = false; s_mouseButtonCaptured = false; s_mouseButton = -1; s_suppressedPadHoldMask = 0; s_suppressedPadNextReadMask = 0; s_deferredActivationContext = Context::None; s_deferredActivationTarget = InvalidTarget; s_gesture = {}; s_hoverTargetValid = false; s_hoverTarget = InvalidTarget; } } // namespace bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouseButton) noexcept { if (!enabled()) { return false; } if (!touch) { if (phase == Phase::Press) { if (!mouse_button_is_menu_confirm(mouseButton)) { return false; } s_mouseButtonCaptured = true; s_mouseButton = mouseButton; suppress_pad_for_mouse_button(mouseButton, true); } else if (phase == Phase::Release) { if (!s_mouseButtonCaptured || s_mouseButton != mouseButton) { return false; } suppress_pad_for_mouse_button(mouseButton, false); s_mouseButtonCaptured = false; s_mouseButton = -1; } else if (phase == Phase::Cancel) { if (s_mouseButtonCaptured) { suppress_pad_for_mouse_button(s_mouseButton, false); s_mouseButtonCaptured = false; s_mouseButton = -1; } else if (!s_mouseActive) { return false; } } s_mouseActive = true; } 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: { 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 = s_clickPending; s_gesture = {}; break; } case Phase::Cancel: clear_click_state(); s_gesture = {}; s_state.down = false; break; case Phase::Move: default: break; } return true; } void begin_game_frame() noexcept { s_currentContext = Context::None; s_currentDialogChoice = 0xFF; s_currentDialogChoiceValid = false; s_currentDialogClicked = false; s_clickConsumed = false; if (!enabled()) { clear_input_state(); } } 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; s_lastDialogClicked = s_currentDialogClicked; s_state.pressed = false; s_state.released = false; s_state.clicked = false; if (!s_state.down) { 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 { if (context == Context::None || !enabled()) { return; } if (s_lastContext == Context::None && s_currentContext == Context::None) { s_state = {}; s_mouseActive = false; s_mouseButtonCaptured = false; s_mouseButton = -1; s_suppressedPadHoldMask = 0; s_suppressedPadNextReadMask = 0; s_deferredActivationContext = Context::None; s_deferredActivationTarget = InvalidTarget; s_gesture = {}; s_hoverTargetValid = false; s_hoverTarget = InvalidTarget; clear_click_state(); } s_currentContext = context; } bool active() noexcept { return s_currentContext != Context::None || s_lastContext != Context::None; } bool enabled() noexcept { return getSettings().game.enableMenuPointer.getValue(); } bool mouse_capture_active() noexcept { return enabled() && s_mouseButtonCaptured; } 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_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; s_currentDialogClicked = clicked; } bool get_dialog_choice(u8& choice) noexcept { if (s_currentDialogChoiceValid) { choice = s_currentDialogChoice; return true; } if (s_lastDialogChoiceValid) { choice = s_lastDialogChoice; return true; } return false; } bool consume_dialog_click(u8& choice) noexcept { if (s_currentDialogChoiceValid && s_currentDialogClicked) { choice = s_currentDialogChoice; s_currentDialogClicked = false; return true; } if (s_lastDialogChoiceValid && s_lastDialogClicked) { choice = s_lastDialogChoice; s_lastDialogClicked = false; return true; } return false; } void defer_activation(Context context, TargetId target) noexcept { s_deferredActivationContext = context; s_deferredActivationTarget = target; } bool consume_deferred_activation(Context context, TargetId target) noexcept { if (s_deferredActivationContext != context || s_deferredActivationTarget != target) { return false; } s_deferredActivationContext = Context::None; s_deferredActivationTarget = InvalidTarget; return true; } void clear_deferred_activation(Context context) noexcept { if (s_deferredActivationContext != context) { return; } s_deferredActivationContext = Context::None; s_deferredActivationTarget = InvalidTarget; } u32 suppressed_pad_buttons(u32 port) noexcept { if (port != PAD_CHAN0) { return 0; } return s_suppressedPadHoldMask | s_suppressedPadNextReadMask; } void finish_pad_suppression_read(u32 port) noexcept { if (port != PAD_CHAN0) { return; } s_suppressedPadNextReadMask = 0; } bool hit_rect(f32 left, f32 top, f32 right, f32 bottom, f32 padding) noexcept { const auto& state = menu_pointer::state(); if (!state.valid) { return false; } if (left > right) { std::swap(left, right); } if (top > bottom) { std::swap(top, bottom); } return state.x >= left - padding && state.x <= right + padding && state.y >= top - padding && state.y <= bottom + padding; } bool hit_pane(CPaneMgr* pane, f32 padding) noexcept { if (pane == nullptr || pane->getPanePtr() == nullptr) { return false; } Mtx mtx; Vec v0 = pane->getGlobalVtx(&mtx, 0, false, 0); Vec v1 = pane->getGlobalVtx(&mtx, 1, false, 0); Vec v2 = pane->getGlobalVtx(&mtx, 2, false, 0); Vec v3 = pane->getGlobalVtx(&mtx, 3, false, 0); const f32 left = std::min({v0.x, v1.x, v2.x, v3.x}); const f32 right = std::max({v0.x, v1.x, v2.x, v3.x}); const f32 top = std::min({v0.y, v1.y, v2.y, v3.y}); const f32 bottom = std::max({v0.y, v1.y, v2.y, v3.y}); return hit_rect(left, top, right, bottom, padding); } bool hit_pane(J2DPane* pane, f32 padding) noexcept { if (pane == nullptr || !pane->isVisible()) { return false; } const JGeometry::TBox2& bounds = pane->getBounds(); return hit_rect(bounds.i.x, bounds.i.y, bounds.f.x, bounds.f.y, padding); } } // namespace dusk::menu_pointer