1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-15 11:03:00 +01:00
Files
OpenRCT2/src/openrct2-ui/input/MouseInput.cpp
2025-10-22 20:29:08 +02:00

1734 lines
64 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*****************************************************************************
* Copyright (c) 2014-2025 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#include "MouseInput.h"
#include "../UiStringIds.h"
#include <cassert>
#include <cmath>
#include <iterator>
#include <openrct2-ui/UiContext.h>
#include <openrct2-ui/input/InputManager.h>
#include <openrct2-ui/interface/Dropdown.h>
#include <openrct2-ui/interface/Viewport.h>
#include <openrct2-ui/interface/ViewportInteraction.h>
#include <openrct2-ui/interface/Widget.h>
#include <openrct2-ui/interface/Window.h>
#include <openrct2-ui/windows/Windows.h>
#include <openrct2/Context.h>
#include <openrct2/Game.h>
#include <openrct2/Input.h>
#include <openrct2/OpenRCT2.h>
#include <openrct2/audio/Audio.h>
#include <openrct2/config/Config.h>
#include <openrct2/interface/Chat.h>
#include <openrct2/interface/Cursors.h>
#include <openrct2/platform/Platform.h>
#include <openrct2/ride/RideData.h>
#include <openrct2/ui/UiContext.h>
#include <openrct2/ui/WindowManager.h>
#include <openrct2/world/Banner.h>
#include <openrct2/world/Map.h>
#include <openrct2/world/Scenery.h>
#include <optional>
namespace OpenRCT2
{
using namespace OpenRCT2::Ui;
using namespace OpenRCT2::Ui::Windows;
struct RCTMouseData
{
uint32_t x;
uint32_t y;
MouseState state;
};
static RCTMouseData _mouseInputQueue[64];
static uint8_t _mouseInputQueueReadIndex = 0;
static uint8_t _mouseInputQueueWriteIndex = 0;
static std::optional<uint32_t> _ticksSinceDragStart;
static WidgetRef _dragWidget;
static uint8_t _dragScrollIndex;
static int32_t _originalWindowWidth;
static int32_t _originalWindowHeight;
static uint8_t _currentScrollIndex;
static uint8_t _currentScrollArea;
ScreenCoordsXY gInputDragLast;
uint32_t gTooltipCloseTimeout;
WidgetRef gTooltipWidget;
ScreenCoordsXY gTooltipCursor;
static std::optional<uint32_t> _clickRepeatTicks;
static MouseState GameGetNextInput(ScreenCoordsXY& screenCoords);
static void InputWidgetOver(const ScreenCoordsXY& screenCoords, WindowBase* w, WidgetIndex widgetIndex);
static void InputWidgetOverChangeCheck(WindowClass windowClass, WindowNumber windowNumber, WidgetIndex widgetIndex);
static void InputWidgetOverFlatbuttonInvalidate();
void ProcessMouseOver(const ScreenCoordsXY& screenCoords);
void ProcessMouseTool(const ScreenCoordsXY& screenCoords);
void InvalidateScroll();
static RCTMouseData* GetMouseInput();
void TileElementRightClick(int32_t type, TileElement* tileElement, const ScreenCoordsXY& screenCoords);
static void GameHandleInputMouse(const ScreenCoordsXY& screenCoords, MouseState state);
static void InputWidgetLeft(const ScreenCoordsXY& screenCoords, WindowBase* w, WidgetIndex widgetIndex);
void InputStateWidgetPressed(
const ScreenCoordsXY& screenCoords, MouseState state, WidgetIndex widgetIndex, WindowBase* w, Widget* widget);
void SetCursor(CursorID cursor_id);
static void InputWindowPositionContinue(
WindowBase& w, const ScreenCoordsXY& lastScreenCoords, const ScreenCoordsXY& newScreenCoords);
static void InputWindowPositionEnd(WindowBase& w, const ScreenCoordsXY& screenCoords);
static void InputWindowResizeBegin(WindowBase& w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords);
static void InputWindowResizeContinue(WindowBase& w, const ScreenCoordsXY& screenCoords);
static void InputWindowResizeEnd();
static void InputViewportDragBegin(WindowBase& w);
static void InputViewportDragContinue();
static void InputViewportDragEnd();
static void InputScrollBegin(WindowBase& w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords);
static void InputScrollContinue(WindowBase& w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords);
static void InputScrollEnd();
static void InputScrollPartUpdateHThumb(WindowBase& w, WidgetIndex widgetIndex, int32_t x, int32_t scroll_id);
static void InputScrollPartUpdateHLeft(WindowBase& w, WidgetIndex widgetIndex, int32_t scroll_id);
static void InputScrollPartUpdateHRight(WindowBase& w, WidgetIndex widgetIndex, int32_t scroll_id);
static void InputScrollPartUpdateVThumb(WindowBase& w, WidgetIndex widgetIndex, int32_t y, int32_t scroll_id);
static void InputScrollPartUpdateVTop(WindowBase& w, WidgetIndex widgetIndex, int32_t scroll_id);
static void InputScrollPartUpdateVBottom(WindowBase& w, WidgetIndex widgetIndex, int32_t scroll_id);
static void InputUpdateTooltip(WindowBase* w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords);
#pragma region Mouse input
/**
*
* rct2: 0x006EA627
*/
void GameHandleInput()
{
InvalidateAllWindowsAfterInput();
MouseState state;
ScreenCoordsXY screenCoords;
while ((state = GameGetNextInput(screenCoords)) != MouseState::released)
{
GameHandleInputMouse(screenCoords, state);
}
if (gInputFlags.has(InputFlag::rightMousePressed))
{
GameHandleInputMouse(screenCoords, state);
}
else
{
int32_t screenWidth = ContextGetWidth();
int32_t screenHeight = ContextGetHeight();
screenCoords.x = std::clamp(screenCoords.x, 0, screenWidth - 1);
screenCoords.y = std::clamp(screenCoords.y, 0, screenHeight - 1);
GameHandleInputMouse(screenCoords, state);
ProcessMouseOver(screenCoords);
ProcessMouseTool(screenCoords);
}
}
/**
*
* rct2: 0x006E83C7
*/
static MouseState GameGetNextInput(ScreenCoordsXY& screenCoords)
{
RCTMouseData* input = GetMouseInput();
if (input == nullptr)
{
const CursorState* cursorState = ContextGetCursorState();
screenCoords = cursorState->position;
return MouseState::released;
}
screenCoords.x = input->x;
screenCoords.y = input->y;
return input->state;
}
/**
*
* rct2: 0x00407074
*/
static RCTMouseData* GetMouseInput()
{
// Check if that location has been written to yet
if (_mouseInputQueueReadIndex == _mouseInputQueueWriteIndex)
{
return nullptr;
}
RCTMouseData* result = &_mouseInputQueue[_mouseInputQueueReadIndex];
_mouseInputQueueReadIndex = (_mouseInputQueueReadIndex + 1) % std::size(_mouseInputQueue);
return result;
}
/**
*
* rct2: 0x006E957F
*/
static void InputScrollDragBegin(const ScreenCoordsXY& screenCoords, WindowBase* w, WidgetIndex widgetIndex)
{
_inputState = InputState::ScrollRight;
gInputDragLast = screenCoords;
_dragWidget.windowClassification = w->classification;
_dragWidget.windowNumber = w->number;
_dragWidget.widgetIndex = widgetIndex;
_ticksSinceDragStart = gCurrentRealTimeTicks;
_dragScrollIndex = WindowGetScrollDataIndex(*w, widgetIndex);
ContextHideCursor();
}
/**
* Based on (heavily changed)
* rct2: 0x006E9E0E, 0x006E9ED0
*/
static void InputScrollDragContinue(const ScreenCoordsXY& screenCoords, WindowBase* w)
{
WidgetIndex widgetIndex = _dragWidget.widgetIndex;
uint8_t scrollIndex = _dragScrollIndex;
const auto& widget = w->widgets[widgetIndex];
auto& scroll = w->scrolls[scrollIndex];
ScreenCoordsXY differentialCoords = screenCoords - gInputDragLast;
if (differentialCoords.x == 0 && differentialCoords.y == 0)
return;
if (scroll.flags & HSCROLLBAR_VISIBLE)
{
int16_t size = widget.width() - 1;
if (scroll.flags & VSCROLLBAR_VISIBLE)
size -= 11;
size = std::max(0, scroll.contentWidth - size);
scroll.contentOffsetX = std::min<uint16_t>(std::max(0, scroll.contentOffsetX + differentialCoords.x), size);
}
if (scroll.flags & VSCROLLBAR_VISIBLE)
{
int16_t size = widget.height() - 1;
if (scroll.flags & HSCROLLBAR_VISIBLE)
size -= 11;
size = std::max(0, scroll.contentHeight - size);
scroll.contentOffsetY = std::min<uint16_t>(std::max(0, scroll.contentOffsetY + differentialCoords.y), size);
}
widgetScrollUpdateThumbs(*w, widgetIndex);
auto* windowMgr = Ui::GetWindowManager();
windowMgr->InvalidateByNumber(w->classification, w->number);
ScreenCoordsXY fixedCursorPosition = {
static_cast<int32_t>(std::ceil(gInputDragLast.x * Config::Get().general.windowScale)),
static_cast<int32_t>(std::ceil(gInputDragLast.y * Config::Get().general.windowScale))
};
ContextSetCursorPosition(fixedCursorPosition);
}
/**
*
* rct2: 0x006E8ACB
*/
static void InputScrollRight(const ScreenCoordsXY& screenCoords, MouseState state)
{
auto* windowMgr = GetWindowManager();
WindowBase* w = windowMgr->FindByNumber(_dragWidget.windowClassification, _dragWidget.windowNumber);
if (w == nullptr)
{
ContextShowCursor();
_inputState = InputState::Reset;
return;
}
switch (state)
{
case MouseState::released:
if (screenCoords.x != 0 || screenCoords.y != 0)
{
_ticksSinceDragStart = std::nullopt;
InputScrollDragContinue(screenCoords, w);
}
break;
case MouseState::rightRelease:
_inputState = InputState::Reset;
ContextShowCursor();
break;
case MouseState::leftPress:
case MouseState::leftRelease:
case MouseState::rightPress:
// Function only handles right button, so it's the only one relevant
break;
}
}
/**
*
* rct2: 0x006E8655
*/
static void GameHandleInputMouse(const ScreenCoordsXY& screenCoords, MouseState state)
{
WindowBase* w;
Widget* widget;
WidgetIndex widgetIndex;
auto* windowMgr = GetWindowManager();
// Get window and widget under cursor position
w = windowMgr->FindFromPoint(screenCoords);
widgetIndex = w == nullptr ? kWidgetIndexNull : windowMgr->FindWidgetFromPoint(*w, screenCoords);
widget = widgetIndex == kWidgetIndexNull ? nullptr : &w->widgets[widgetIndex];
switch (_inputState)
{
case InputState::Reset:
WindowTooltipReset(screenCoords);
[[fallthrough]];
case InputState::Normal:
switch (state)
{
case MouseState::released:
InputWidgetOver(screenCoords, w, widgetIndex);
break;
case MouseState::leftPress:
InputWidgetLeft(screenCoords, w, widgetIndex);
break;
case MouseState::rightPress:
windowMgr->CloseByClass(WindowClass::tooltip);
if (w != nullptr)
{
w = windowMgr->BringToFront(*w);
}
if (widgetIndex != kWidgetIndexNull)
{
switch (widget->type)
{
case WidgetType::viewport:
if (!(gLegacyScene == LegacyScene::trackDesignsManager
|| gLegacyScene == LegacyScene::titleSequence))
{
InputViewportDragBegin(*w);
}
break;
case WidgetType::scroll:
InputScrollDragBegin(screenCoords, w, widgetIndex);
break;
default:
break;
}
}
break;
case MouseState::leftRelease:
case MouseState::rightRelease:
// In this switch only button presses are relevant
break;
}
break;
case InputState::WidgetPressed:
InputStateWidgetPressed(screenCoords, state, widgetIndex, w, widget);
break;
case InputState::PositioningWindow:
w = windowMgr->FindByNumber(_dragWidget.windowClassification, _dragWidget.windowNumber);
if (w == nullptr)
{
_inputState = InputState::Reset;
}
else
{
InputWindowPositionContinue(*w, gInputDragLast, screenCoords);
if (state == MouseState::leftRelease)
{
InputWindowPositionEnd(*w, screenCoords);
}
}
break;
case InputState::ViewportRight:
if (state == MouseState::released)
{
InputViewportDragContinue();
}
else if (state == MouseState::rightRelease)
{
InputViewportDragEnd();
if (_ticksSinceDragStart.has_value() && gCurrentRealTimeTicks - _ticksSinceDragStart.value() < 500)
{
// If the user pressed the right mouse button for less than 500 ticks, interpret as right click
ViewportInteractionRightClick(screenCoords);
}
}
break;
case InputState::DropdownActive:
InputStateWidgetPressed(screenCoords, state, widgetIndex, w, widget);
break;
case InputState::ViewportLeft:
w = windowMgr->FindByNumber(_dragWidget.windowClassification, _dragWidget.windowNumber);
if (w == nullptr)
{
_inputState = InputState::Reset;
break;
}
switch (state)
{
case MouseState::released:
if (w->viewport == nullptr)
{
_inputState = InputState::Reset;
break;
}
if (!gInputFlags.has(InputFlag::leftMousePressed))
break;
if (w->classification != _dragWidget.windowClassification || w->number != _dragWidget.windowNumber
|| !(gInputFlags.has(InputFlag::toolActive)))
{
break;
}
w = windowMgr->FindByNumber(gCurrentToolWidget.windowClassification, gCurrentToolWidget.windowNumber);
if (w == nullptr)
{
break;
}
w->onToolDrag(gCurrentToolWidget.widgetIndex, screenCoords);
break;
case MouseState::leftRelease:
_inputState = InputState::Reset;
if (_dragWidget.windowNumber == w->number)
{
if (gInputFlags.has(InputFlag::toolActive))
{
w = windowMgr->FindByNumber(
gCurrentToolWidget.windowClassification, gCurrentToolWidget.windowNumber);
if (w != nullptr)
{
w->onToolUp(gCurrentToolWidget.widgetIndex, screenCoords);
}
}
else if (!gInputFlags.has(InputFlag::leftMousePressed))
{
ViewportInteractionLeftClick(screenCoords);
}
}
break;
case MouseState::leftPress:
case MouseState::rightPress:
case MouseState::rightRelease:
// In this switch only left button release is relevant
break;
}
break;
case InputState::ScrollLeft:
switch (state)
{
case MouseState::released:
InputScrollContinue(*w, widgetIndex, screenCoords);
break;
case MouseState::leftRelease:
InputScrollEnd();
break;
case MouseState::leftPress:
case MouseState::rightPress:
case MouseState::rightRelease:
// In this switch only left button release is relevant
break;
}
break;
case InputState::Resizing:
w = windowMgr->FindByNumber(_dragWidget.windowClassification, _dragWidget.windowNumber);
if (w == nullptr)
{
_inputState = InputState::Reset;
}
else
{
if (state == MouseState::leftRelease)
{
InputWindowResizeEnd();
}
if (state == MouseState::released || state == MouseState::leftRelease)
{
InputWindowResizeContinue(*w, screenCoords);
}
}
break;
case InputState::ScrollRight:
InputScrollRight(screenCoords, state);
break;
}
}
#pragma region Window positioning / resizing
void InputWindowPositionBegin(WindowBase& w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords)
{
_inputState = InputState::PositioningWindow;
gInputDragLast = screenCoords - w.windowPos;
_dragWidget.windowClassification = w.classification;
_dragWidget.windowNumber = w.number;
_dragWidget.widgetIndex = widgetIndex;
}
static void InputWindowPositionContinue(
WindowBase& w, const ScreenCoordsXY& lastScreenCoords, const ScreenCoordsXY& newScreenCoords)
{
int32_t snapProximity;
snapProximity = (w.flags.has(WindowFlag::noSnapping)) ? 0 : Config::Get().general.windowSnapProximity;
WindowMoveAndSnap(w, newScreenCoords - lastScreenCoords, snapProximity);
}
static void InputWindowPositionEnd(WindowBase& w, const ScreenCoordsXY& screenCoords)
{
_inputState = InputState::Normal;
gTooltipCloseTimeout = 0;
gTooltipWidget = _dragWidget;
w.onMoved(screenCoords);
}
static void InputWindowResizeBegin(WindowBase& w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords)
{
_inputState = InputState::Resizing;
gInputDragLast = screenCoords;
_dragWidget.windowClassification = w.classification;
_dragWidget.windowNumber = w.number;
_dragWidget.widgetIndex = widgetIndex;
_originalWindowWidth = w.width;
_originalWindowHeight = w.height;
}
static void InputWindowResizeContinue(WindowBase& w, const ScreenCoordsXY& screenCoords)
{
if (screenCoords.y < static_cast<int32_t>(ContextGetHeight()) - 2)
{
auto differentialCoords = screenCoords - gInputDragLast;
int32_t targetWidth = _originalWindowWidth + differentialCoords.x - w.width;
int32_t targetHeight = _originalWindowHeight + differentialCoords.y - w.height;
WindowResizeByDelta(w, targetWidth, targetHeight);
}
}
static void InputWindowResizeEnd()
{
_inputState = InputState::Normal;
gTooltipCloseTimeout = 0;
gTooltipWidget = _dragWidget;
}
#pragma endregion
#pragma region Viewport dragging
static void InputViewportDragBegin(WindowBase& w)
{
w.flags.unset(WindowFlag::scrollingToLocation);
_inputState = InputState::ViewportRight;
_dragWidget.windowClassification = w.classification;
_dragWidget.windowNumber = w.number;
_ticksSinceDragStart = gCurrentRealTimeTicks;
auto cursorPosition = ContextGetCursorPosition();
gInputDragLast = cursorPosition;
if (!Config::Get().general.invertViewportDrag)
{
ContextHideCursor();
}
// Only unfollow sprites for the main window or extra viewport windows.
// Dont unfollow for windows where the viewport is always supposed to follow (e.g. Ride, Guest, Staff).
auto mainWindow = WindowGetMain();
if (&w == mainWindow || w.classification == WindowClass::viewport)
{
WindowUnfollowSprite(w);
}
}
static void InputViewportDragContinue()
{
WindowBase* w;
Viewport* viewport;
auto newDragCoords = ContextGetCursorPosition();
auto differentialCoords = newDragCoords - gInputDragLast;
if (differentialCoords.x == 0 && differentialCoords.y == 0)
return;
auto* windowMgr = GetWindowManager();
w = windowMgr->FindByNumber(_dragWidget.windowClassification, _dragWidget.windowNumber);
// #3294: Window can be closed during a drag session, so just finish
// the session if the window no longer exists
if (w == nullptr)
{
InputViewportDragEnd();
return;
}
viewport = w->viewport;
if (viewport == nullptr)
{
ContextShowCursor();
_inputState = InputState::Reset;
}
else if (differentialCoords.x != 0 || differentialCoords.y != 0)
{
if (!(w->flags.has(WindowFlag::noScrolling)))
{
// User dragged a scrollable viewport
// If the drag time is less than 500 the "drag" is usually interpreted as a right click.
// As the user moved the mouse, don't interpret it as right click in any case.
_ticksSinceDragStart = std::nullopt;
// applying the zoom only with negative values avoids a "deadzone" effect where small positive value round to
// zero.
const bool posX = differentialCoords.x > 0;
const bool posY = differentialCoords.y > 0;
differentialCoords.x = (viewport->zoom + 1).ApplyTo(-std::abs(differentialCoords.x));
differentialCoords.y = (viewport->zoom + 1).ApplyTo(-std::abs(differentialCoords.y));
differentialCoords.x = posX ? -differentialCoords.x : differentialCoords.x;
differentialCoords.y = posY ? -differentialCoords.y : differentialCoords.y;
if (Config::Get().general.invertViewportDrag)
{
w->savedViewPos -= differentialCoords;
}
else
{
w->savedViewPos += differentialCoords;
}
}
}
#ifndef __EMSCRIPTEN__
const CursorState* cursorState = ContextGetCursorState();
if (cursorState->touch || Config::Get().general.invertViewportDrag)
{
gInputDragLast = newDragCoords;
}
else
{
ContextSetCursorPosition(gInputDragLast);
}
#else
gInputDragLast = newDragCoords;
#endif
}
static void InputViewportDragEnd()
{
_inputState = InputState::Reset;
ContextShowCursor();
}
#pragma endregion
#pragma region Scroll bars
static void InputScrollBegin(WindowBase& w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords)
{
const auto& widget = w.widgets[widgetIndex];
_inputState = InputState::ScrollLeft;
gPressedWidget.windowClassification = w.classification;
gPressedWidget.windowNumber = w.number;
gPressedWidget.widgetIndex = widgetIndex;
gTooltipCursor = screenCoords;
int32_t scroll_area, scroll_id;
ScreenCoordsXY scrollCoords;
scroll_id = 0; // safety
widgetScrollGetPart(w, &widget, screenCoords, scrollCoords, &scroll_area, &scroll_id);
_currentScrollArea = scroll_area;
_currentScrollIndex = scroll_id;
w.onScrollSelect(scroll_id, scroll_area);
if (scroll_area == SCROLL_PART_VIEW)
{
w.onScrollMouseDown(scroll_id, scrollCoords);
return;
}
const auto& widg = w.widgets[widgetIndex];
auto& scroll = w.scrolls[scroll_id];
int32_t widget_width = widg.width() - 1;
if (scroll.flags & VSCROLLBAR_VISIBLE)
widget_width -= kScrollBarWidth + 1;
int32_t widget_content_width = std::max(scroll.contentWidth - widget_width, 0);
int32_t widget_height = widg.bottom - widg.top - 1;
if (scroll.flags & HSCROLLBAR_VISIBLE)
widget_height -= kScrollBarWidth + 1;
int32_t widget_content_height = std::max(scroll.contentHeight - widget_height, 0);
switch (scroll_area)
{
case SCROLL_PART_HSCROLLBAR_LEFT:
scroll.contentOffsetX = std::max(scroll.contentOffsetX - 3, 0);
break;
case SCROLL_PART_HSCROLLBAR_RIGHT:
scroll.contentOffsetX = std::min(scroll.contentOffsetX + 3, widget_content_width);
break;
case SCROLL_PART_HSCROLLBAR_LEFT_TROUGH:
scroll.contentOffsetX = std::max(scroll.contentOffsetX - widget_width, 0);
break;
case SCROLL_PART_HSCROLLBAR_RIGHT_TROUGH:
scroll.contentOffsetX = std::min(scroll.contentOffsetX + widget_width, widget_content_width);
break;
case SCROLL_PART_VSCROLLBAR_TOP:
scroll.contentOffsetY = std::max(scroll.contentOffsetY - 3, 0);
break;
case SCROLL_PART_VSCROLLBAR_BOTTOM:
scroll.contentOffsetY = std::min(scroll.contentOffsetY + 3, widget_content_height);
break;
case SCROLL_PART_VSCROLLBAR_TOP_TROUGH:
scroll.contentOffsetY = std::max(scroll.contentOffsetY - widget_height, 0);
break;
case SCROLL_PART_VSCROLLBAR_BOTTOM_TROUGH:
scroll.contentOffsetY = std::min(scroll.contentOffsetY + widget_height, widget_content_height);
break;
default:
break;
}
widgetScrollUpdateThumbs(w, widgetIndex);
auto* windowMgr = Ui::GetWindowManager();
windowMgr->InvalidateByNumber(w.classification, w.number);
}
static void InputScrollContinue(WindowBase& w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords)
{
int32_t scroll_part, scroll_id;
const auto& widget = w.widgets[widgetIndex];
if (w.classification != gPressedWidget.windowClassification || w.number != gPressedWidget.windowNumber
|| widgetIndex != gPressedWidget.widgetIndex)
{
InvalidateScroll();
return;
}
ScreenCoordsXY newScreenCoords;
widgetScrollGetPart(w, &widget, screenCoords, newScreenCoords, &scroll_part, &scroll_id);
if (_currentScrollArea == SCROLL_PART_HSCROLLBAR_THUMB)
{
int32_t originalTooltipCursorX = gTooltipCursor.x;
gTooltipCursor.x = screenCoords.x;
InputScrollPartUpdateHThumb(w, widgetIndex, screenCoords.x - originalTooltipCursorX, scroll_id);
return;
}
if (_currentScrollArea == SCROLL_PART_VSCROLLBAR_THUMB)
{
int32_t originalTooltipCursorY = gTooltipCursor.y;
gTooltipCursor.y = screenCoords.y;
InputScrollPartUpdateVThumb(w, widgetIndex, screenCoords.y - originalTooltipCursorY, scroll_id);
return;
}
if (scroll_part != _currentScrollArea)
{
InvalidateScroll();
return;
}
switch (scroll_part)
{
case SCROLL_PART_VIEW:
w.onScrollMouseDrag(scroll_id, newScreenCoords);
break;
case SCROLL_PART_HSCROLLBAR_LEFT:
InputScrollPartUpdateHLeft(w, widgetIndex, scroll_id);
break;
case SCROLL_PART_HSCROLLBAR_RIGHT:
InputScrollPartUpdateHRight(w, widgetIndex, scroll_id);
break;
case SCROLL_PART_VSCROLLBAR_TOP:
InputScrollPartUpdateVTop(w, widgetIndex, scroll_id);
break;
case SCROLL_PART_VSCROLLBAR_BOTTOM:
InputScrollPartUpdateVBottom(w, widgetIndex, scroll_id);
break;
}
}
static void InputScrollEnd()
{
_inputState = InputState::Reset;
InvalidateScroll();
}
/**
*
* rct2: 0x006E98F2
*/
static void InputScrollPartUpdateHThumb(WindowBase& w, WidgetIndex widgetIndex, int32_t x, int32_t scroll_id)
{
const auto& widget = w.widgets[widgetIndex];
auto& scroll = w.scrolls[scroll_id];
auto* windowMgr = GetWindowManager();
if (windowMgr->FindByNumber(w.classification, w.number) != nullptr)
{
int32_t newLeft;
newLeft = scroll.contentWidth;
newLeft *= x;
x = widget.width() - 21;
if (scroll.flags & VSCROLLBAR_VISIBLE)
x -= kScrollBarWidth + 1;
newLeft /= x;
x = newLeft;
scroll.flags |= HSCROLLBAR_THUMB_PRESSED;
newLeft = scroll.contentOffsetX;
newLeft += x;
if (newLeft < 0)
newLeft = 0;
x = widget.width() - 1;
if (scroll.flags & VSCROLLBAR_VISIBLE)
x -= kScrollBarWidth + 1;
x *= -1;
x += scroll.contentWidth;
if (x < 0)
x = 0;
if (newLeft > x)
newLeft = x;
scroll.contentOffsetX = newLeft;
widgetScrollUpdateThumbs(w, widgetIndex);
windowMgr->InvalidateWidgetByNumber(w.classification, w.number, widgetIndex);
}
}
/**
*
* rct2: 0x006E99A9
*/
static void InputScrollPartUpdateVThumb(WindowBase& w, WidgetIndex widgetIndex, int32_t y, int32_t scroll_id)
{
const auto& widget = w.widgets[widgetIndex];
auto& scroll = w.scrolls[scroll_id];
auto* windowMgr = GetWindowManager();
if (windowMgr->FindByNumber(w.classification, w.number) != nullptr)
{
int32_t newTop;
newTop = scroll.contentHeight;
newTop *= y;
y = widget.height() - 21;
if (scroll.flags & HSCROLLBAR_VISIBLE)
y -= kScrollBarWidth + 1;
newTop /= y;
y = newTop;
scroll.flags |= VSCROLLBAR_THUMB_PRESSED;
newTop = scroll.contentOffsetY;
newTop += y;
if (newTop < 0)
newTop = 0;
y = widget.height() - 1;
if (scroll.flags & HSCROLLBAR_VISIBLE)
y -= kScrollBarWidth + 1;
y *= -1;
y += scroll.contentHeight;
if (y < 0)
y = 0;
if (newTop > y)
newTop = y;
scroll.contentOffsetY = newTop;
widgetScrollUpdateThumbs(w, widgetIndex);
windowMgr->InvalidateWidgetByNumber(w.classification, w.number, widgetIndex);
}
}
/**
*
* rct2: 0x006E9A60
*/
static void InputScrollPartUpdateHLeft(WindowBase& w, WidgetIndex widgetIndex, int32_t scroll_id)
{
auto* windowMgr = GetWindowManager();
if (windowMgr->FindByNumber(w.classification, w.number) != nullptr)
{
auto& scroll = w.scrolls[scroll_id];
scroll.flags |= HSCROLLBAR_LEFT_PRESSED;
if (scroll.contentOffsetX >= 3)
scroll.contentOffsetX -= 3;
widgetScrollUpdateThumbs(w, widgetIndex);
windowMgr->InvalidateWidgetByNumber(w.classification, w.number, widgetIndex);
}
}
/**
*
* rct2: 0x006E9ABF
*/
static void InputScrollPartUpdateHRight(WindowBase& w, WidgetIndex widgetIndex, int32_t scroll_id)
{
const auto& widget = w.widgets[widgetIndex];
auto* windowMgr = GetWindowManager();
if (windowMgr->FindByNumber(w.classification, w.number) != nullptr)
{
auto& scroll = w.scrolls[scroll_id];
scroll.flags |= HSCROLLBAR_RIGHT_PRESSED;
scroll.contentOffsetX += 3;
int32_t newLeft = widget.width() - 1;
if (scroll.flags & VSCROLLBAR_VISIBLE)
newLeft -= kScrollBarWidth + 1;
newLeft *= -1;
newLeft += scroll.contentWidth;
if (newLeft < 0)
newLeft = 0;
if (scroll.contentOffsetX > newLeft)
scroll.contentOffsetX = newLeft;
widgetScrollUpdateThumbs(w, widgetIndex);
windowMgr->InvalidateWidgetByNumber(w.classification, w.number, widgetIndex);
}
}
/**
*
* rct2: 0x006E9C37
*/
static void InputScrollPartUpdateVTop(WindowBase& w, WidgetIndex widgetIndex, int32_t scroll_id)
{
auto* windowMgr = GetWindowManager();
if (windowMgr->FindByNumber(w.classification, w.number) != nullptr)
{
auto& scroll = w.scrolls[scroll_id];
scroll.flags |= VSCROLLBAR_UP_PRESSED;
if (scroll.contentOffsetY >= 3)
scroll.contentOffsetY -= 3;
widgetScrollUpdateThumbs(w, widgetIndex);
windowMgr->InvalidateWidgetByNumber(w.classification, w.number, widgetIndex);
}
}
/**
*
* rct2: 0x006E9C96
*/
static void InputScrollPartUpdateVBottom(WindowBase& w, WidgetIndex widgetIndex, int32_t scroll_id)
{
const auto& widget = w.widgets[widgetIndex];
auto* windowMgr = GetWindowManager();
if (windowMgr->FindByNumber(w.classification, w.number) != nullptr)
{
auto& scroll = w.scrolls[scroll_id];
scroll.flags |= VSCROLLBAR_DOWN_PRESSED;
scroll.contentOffsetY += 3;
int32_t newTop = widget.height() - 1;
if (scroll.flags & HSCROLLBAR_VISIBLE)
newTop -= kScrollBarWidth + 1;
newTop *= -1;
newTop += scroll.contentHeight;
if (newTop < 0)
newTop = 0;
if (scroll.contentOffsetY > newTop)
scroll.contentOffsetY = newTop;
widgetScrollUpdateThumbs(w, widgetIndex);
windowMgr->InvalidateWidgetByNumber(w.classification, w.number, widgetIndex);
}
}
#pragma endregion
#pragma region Widgets
/**
*
* rct2: 0x006E9253
*/
static void InputWidgetOver(const ScreenCoordsXY& screenCoords, WindowBase* w, WidgetIndex widgetIndex)
{
WindowClass windowClass = WindowClass::null;
WindowNumber windowNumber = 0;
Widget* widget = nullptr;
if (w != nullptr)
{
windowClass = w->classification;
windowNumber = w->number;
widget = widgetIndex == kWidgetIndexNull ? nullptr : &w->widgets[widgetIndex];
}
InputWidgetOverChangeCheck(windowClass, windowNumber, widgetIndex);
if (w != nullptr && widgetIndex != kWidgetIndexNull && widget->type == WidgetType::scroll)
{
int32_t scroll_part, scrollId;
ScreenCoordsXY newScreenCoords;
widgetScrollGetPart(*w, widget, screenCoords, newScreenCoords, &scroll_part, &scrollId);
if (scroll_part == SCROLL_PART_VIEW)
{
w->onScrollMouseOver(scrollId, newScreenCoords);
}
}
InputUpdateTooltip(w, widgetIndex, screenCoords);
}
/**
*
* rct2: 0x006E9269
*/
static void InputWidgetOverChangeCheck(WindowClass windowClass, WindowNumber windowNumber, WidgetIndex widgetIndex)
{
// Prevents invalid widgets being clicked source of bug is elsewhere
if (widgetIndex == kWidgetIndexNull)
return;
// Check if the widget that the cursor was over, has changed
if (windowClass != gHoverWidget.windowClassification || windowNumber != gHoverWidget.windowNumber
|| widgetIndex != gHoverWidget.widgetIndex)
{
// Invalidate last widget cursor was on if widget is a flat button
InputWidgetOverFlatbuttonInvalidate();
// Set new cursor over widget
gHoverWidget.windowClassification = windowClass;
gHoverWidget.windowNumber = windowNumber;
gHoverWidget.widgetIndex = widgetIndex;
// Invalidate new widget cursor is on if widget is a flat button
if (windowClass != WindowClass::null)
InputWidgetOverFlatbuttonInvalidate();
}
}
/**
* Used to invalidate flat button widgets when the mouse leaves and enters them. This should be generalised so that all
* widgets can use this in the future.
*/
static void InputWidgetOverFlatbuttonInvalidate()
{
auto* windowMgr = GetWindowManager();
WindowBase* w = windowMgr->FindByNumber(gHoverWidget.windowClassification, gHoverWidget.windowNumber);
if (w != nullptr)
{
w->onPrepareDraw();
if (w->widgets[gHoverWidget.widgetIndex].type == WidgetType::flatBtn)
{
windowMgr->InvalidateWidgetByNumber(
gHoverWidget.windowClassification, gHoverWidget.windowNumber, gHoverWidget.widgetIndex);
}
}
}
/**
*
* rct2: 0x006E95F9
*/
static void InputWidgetLeft(const ScreenCoordsXY& screenCoords, WindowBase* w, WidgetIndex widgetIndex)
{
WindowClass windowClass = WindowClass::null;
WindowNumber windowNumber = 0;
if (w != nullptr)
{
windowClass = w->classification;
windowNumber = w->number;
}
auto* windowMgr = GetWindowManager();
windowMgr->CloseByClass(WindowClass::error);
windowMgr->CloseByClass(WindowClass::tooltip);
// Window might have changed position in the list, therefore find it again
w = windowMgr->FindByNumber(windowClass, windowNumber);
if (w == nullptr)
return;
w = windowMgr->BringToFront(*w);
if (widgetIndex == kWidgetIndexNull)
return;
if (windowClass != GetCurrentTextBox().window.classification || windowNumber != GetCurrentTextBox().window.number
|| widgetIndex != GetCurrentTextBox().widgetIndex)
{
WindowCancelTextbox();
}
const auto& widget = w->widgets[widgetIndex];
switch (widget.type)
{
case WidgetType::frame:
case WidgetType::resize:
if (w->canBeResized()
&& (screenCoords.x >= w->windowPos.x + w->width - 19 && screenCoords.y >= w->windowPos.y + w->height - 19))
InputWindowResizeBegin(*w, widgetIndex, screenCoords);
break;
case WidgetType::viewport:
_inputState = InputState::ViewportLeft;
gInputDragLast = screenCoords;
_dragWidget.windowClassification = windowClass;
_dragWidget.windowNumber = windowNumber;
if (gInputFlags.has(InputFlag::toolActive))
{
w = windowMgr->FindByNumber(gCurrentToolWidget.windowClassification, gCurrentToolWidget.windowNumber);
if (w != nullptr)
{
gInputFlags.set(InputFlag::leftMousePressed);
w->onToolDown(gCurrentToolWidget.widgetIndex, screenCoords);
}
}
break;
case WidgetType::caption:
InputWindowPositionBegin(*w, widgetIndex, screenCoords);
break;
case WidgetType::scroll:
InputScrollBegin(*w, widgetIndex, screenCoords);
break;
case WidgetType::empty:
case WidgetType::labelCentred:
case WidgetType::label:
case WidgetType::groupbox:
case WidgetType::progressBar:
case WidgetType::placeholder:
case WidgetType::horizontalSeparator:
// Non-interactive widget type
break;
case WidgetType::imgBtn:
case WidgetType::colourBtn:
case WidgetType::trnBtn:
case WidgetType::tab:
case WidgetType::flatBtn:
case WidgetType::hiddenButton:
case WidgetType::button:
case WidgetType::tableHeader:
case WidgetType::spinner:
case WidgetType::dropdownMenu:
case WidgetType::closeBox:
case WidgetType::checkbox:
case WidgetType::textBox:
case WidgetType::custom:
if (!widgetIsDisabled(*w, widgetIndex))
{
OpenRCT2::Audio::Play(OpenRCT2::Audio::SoundId::click1, 0, w->windowPos.x + widget.midX());
// Set new cursor down widget
gPressedWidget.windowClassification = windowClass;
gPressedWidget.windowNumber = windowNumber;
gPressedWidget.widgetIndex = widgetIndex;
gInputFlags.set(InputFlag::widgetPressed);
_inputState = InputState::WidgetPressed;
_clickRepeatTicks = gCurrentRealTimeTicks;
windowMgr->InvalidateWidgetByNumber(windowClass, windowNumber, widgetIndex);
w->onMouseDown(widgetIndex);
}
break;
}
}
/**
*
* rct2: 0x006ED833
*/
void ProcessMouseOver(const ScreenCoordsXY& screenCoords)
{
CursorID cursorId = CursorID::Arrow;
auto ft = Formatter();
ft.Add<StringId>(kStringIdNone);
SetMapTooltip(ft);
auto* windowMgr = GetWindowManager();
WindowBase* window = windowMgr->FindFromPoint(screenCoords);
if (window != nullptr)
{
WidgetIndex widgetId = windowMgr->FindWidgetFromPoint(*window, screenCoords);
if (widgetId != kWidgetIndexNull)
{
switch (window->widgets[widgetId].type)
{
case WidgetType::viewport:
if (!gInputFlags.has(InputFlag::toolActive))
{
if (ViewportInteractionLeftOver(screenCoords))
{
SetCursor(CursorID::HandPoint);
return;
}
break;
}
cursorId = static_cast<CursorID>(gCurrentToolId);
break;
case WidgetType::frame:
case WidgetType::resize:
if (!window->canBeResized())
break;
if (screenCoords.x < window->windowPos.x + window->width - 0x13)
break;
if (screenCoords.y < window->windowPos.y + window->height - 0x13)
break;
cursorId = CursorID::DiagonalArrows;
break;
case WidgetType::scroll:
{
int32_t output_scroll_area, scroll_id;
ScreenCoordsXY scrollCoords;
widgetScrollGetPart(
*window, &window->widgets[widgetId], screenCoords, scrollCoords, &output_scroll_area, &scroll_id);
if (output_scroll_area != SCROLL_PART_VIEW)
{
cursorId = CursorID::Arrow;
break;
}
// Same as default but with scroll_x/y
cursorId = window->onCursor(widgetId, scrollCoords, CursorID::Arrow);
if (cursorId == CursorID::Undefined)
cursorId = CursorID::Arrow;
break;
}
default:
cursorId = window->onCursor(widgetId, screenCoords, CursorID::Arrow);
if (cursorId == CursorID::Undefined)
cursorId = CursorID::Arrow;
break;
}
}
}
ViewportInteractionRightOver(screenCoords);
SetCursor(cursorId);
}
/**
*
* rct2: 0x006ED801
*/
void ProcessMouseTool(const ScreenCoordsXY& screenCoords)
{
if (gInputFlags.has(InputFlag::toolActive))
{
auto* windowMgr = GetWindowManager();
WindowBase* w = windowMgr->FindByNumber(gCurrentToolWidget.windowClassification, gCurrentToolWidget.windowNumber);
if (w == nullptr)
ToolCancel();
else if (InputGetState() != InputState::ViewportRight)
w->onToolUpdate(gCurrentToolWidget.widgetIndex, screenCoords);
}
}
/**
*
* rct2: 0x006E8DA7
*/
void InputStateWidgetPressed(
const ScreenCoordsXY& screenCoords, MouseState state, WidgetIndex widgetIndex, WindowBase* w, Widget* widget)
{
WindowClass cursor_w_class;
WindowNumber cursor_w_number;
cursor_w_class = gPressedWidget.windowClassification;
cursor_w_number = gPressedWidget.windowNumber;
WidgetIndex cursor_widgetIndex = gPressedWidget.widgetIndex;
auto* windowMgr = GetWindowManager();
WindowBase* cursor_w = windowMgr->FindByNumber(cursor_w_class, cursor_w_number);
if (cursor_w == nullptr)
{
_inputState = InputState::Reset;
return;
}
if (w != nullptr && state == MouseState::leftRelease)
{
if (w->widgets[widgetIndex].type == WidgetType::closeBox && cursor_w_class == w->classification
&& cursor_w_number == w->number && widgetIndex == cursor_widgetIndex)
{
auto& im = GetInputManager();
if (im.isModifierKeyPressed(ModifierKey::shift))
{
gLastCloseModifier.window.number = w->number;
gLastCloseModifier.window.classification = w->classification;
gLastCloseModifier.modifier = CloseWindowModifier::shift;
}
else if (im.isModifierKeyPressed(ModifierKey::ctrl))
{
gLastCloseModifier.window.number = w->number;
gLastCloseModifier.window.classification = w->classification;
gLastCloseModifier.modifier = CloseWindowModifier::control;
}
}
}
switch (state)
{
case MouseState::released:
if (w == nullptr || cursor_w_class != w->classification || cursor_w_number != w->number
|| widgetIndex != cursor_widgetIndex)
break;
if (widgetIsDisabled(*w, widgetIndex))
break;
// If this variable is non-zero then its the last tick the mouse down event was fired.
if (_clickRepeatTicks.has_value())
{
// The initial amount of time in ticks to wait until the first click repeat.
constexpr auto kTicksUntilRepeats = 16u;
// The amount of ticks between each click repeat.
constexpr auto kEventDelayInTicks = 3u;
// The amount of ticks since the last click repeat.
const auto clickRepeatsDelta = gCurrentRealTimeTicks - _clickRepeatTicks.value();
// Handle click repeat, only start this when at least 16 ticks elapsed.
if (clickRepeatsDelta >= kTicksUntilRepeats && (clickRepeatsDelta & kEventDelayInTicks) == 0)
{
if (widgetIsHoldable(*w, widgetIndex))
{
w->onMouseDown(widgetIndex);
}
// Subtract initial delay from here on we want the event each third tick.
_clickRepeatTicks = gCurrentRealTimeTicks - kTicksUntilRepeats;
}
}
if (gInputFlags.has(InputFlag::widgetPressed))
{
if (_inputState == InputState::DropdownActive)
{
gDropdown.highlightedIndex = gDropdown.defaultIndex;
windowMgr->InvalidateByClass(WindowClass::dropdown);
}
return;
}
gInputFlags.set(InputFlag::widgetPressed);
windowMgr->InvalidateWidgetByNumber(cursor_w_class, cursor_w_number, widgetIndex);
return;
case MouseState::leftRelease:
case MouseState::rightPress:
if (_inputState == InputState::DropdownActive)
{
if (w != nullptr)
{
auto wClass = w->classification;
auto wNumber = w->number;
int32_t dropdown_index = 0;
bool dropdownCleanup = false;
if (w->classification == WindowClass::dropdown)
{
dropdown_index = DropdownIndexFromPoint(screenCoords, w);
dropdownCleanup = dropdown_index == -1
|| (dropdown_index < Dropdown::kItemsMaxSize && gDropdown.items[dropdown_index].isDisabled())
|| gDropdown.items[dropdown_index].isSeparator();
w = nullptr; // To be closed right next
}
else
{
if (cursor_w_class != w->classification || cursor_w_number != w->number
|| widgetIndex != cursor_widgetIndex)
{
dropdownCleanup = true;
}
else
{
dropdown_index = -1;
if (gInputFlags.has(InputFlag::dropdownStayOpen))
{
if (!gInputFlags.has(InputFlag::dropdownMouseUp))
{
gInputFlags.set(InputFlag::dropdownMouseUp);
return;
}
}
}
}
windowMgr->CloseByClass(WindowClass::dropdown);
if (dropdownCleanup)
{
// Update w as it will be invalid after closing the dropdown window
w = windowMgr->FindByNumber(wClass, wNumber);
}
else
{
cursor_w = windowMgr->FindByNumber(cursor_w_class, cursor_w_number);
if (gInputFlags.has(InputFlag::widgetPressed))
{
gInputFlags.unset(InputFlag::widgetPressed);
windowMgr->InvalidateWidgetByNumber(cursor_w_class, cursor_w_number, cursor_widgetIndex);
}
_inputState = InputState::Normal;
gTooltipCloseTimeout = 0;
gTooltipWidget.widgetIndex = cursor_widgetIndex;
gTooltipWidget.windowClassification = cursor_w_class;
gTooltipWidget.windowNumber = cursor_w_number;
if (dropdown_index == -1)
{
if (gDropdown.defaultIndex != -1 && !gDropdown.items[gDropdown.defaultIndex].isDisabled())
{
dropdown_index = gDropdown.defaultIndex;
}
}
cursor_w->onDropdown(cursor_widgetIndex, dropdown_index);
}
}
}
_inputState = InputState::Normal;
if (state == MouseState::rightPress)
{
return;
}
gTooltipCloseTimeout = 0;
gTooltipWidget.widgetIndex = cursor_widgetIndex;
if (w == nullptr)
break;
if (widget == nullptr)
break;
{
int32_t mid_point_x = widget->midX() + w->windowPos.x;
OpenRCT2::Audio::Play(OpenRCT2::Audio::SoundId::click2, 0, mid_point_x);
}
if (cursor_w_class != w->classification || cursor_w_number != w->number || widgetIndex != cursor_widgetIndex)
break;
if (widgetIsDisabled(*w, widgetIndex))
break;
windowMgr->InvalidateWidgetByNumber(cursor_w_class, cursor_w_number, widgetIndex);
w->onMouseUp(widgetIndex);
return;
default:
return;
}
_clickRepeatTicks = std::nullopt;
if (_inputState != InputState::DropdownActive)
{
// Hold down widget and drag outside of area??
if (gInputFlags.has(InputFlag::widgetPressed))
{
gInputFlags.unset(InputFlag::widgetPressed);
windowMgr->InvalidateWidgetByNumber(cursor_w_class, cursor_w_number, cursor_widgetIndex);
}
return;
}
else if (gDropdown.hasTooltips)
{
// This is ordinarily covered in InputWidgetOver but the dropdown with colours is a special case.
InputUpdateTooltip(w, widgetIndex, screenCoords);
}
gDropdown.highlightedIndex = -1;
windowMgr->InvalidateByClass(WindowClass::dropdown);
if (w == nullptr)
{
return;
}
if (w->classification == WindowClass::dropdown)
{
int32_t dropdown_index = DropdownIndexFromPoint(screenCoords, w);
if (dropdown_index == -1)
{
return;
}
if (gDropdown.hasTooltips && gDropdown.lastTooltipHover != dropdown_index)
{
gDropdown.lastTooltipHover = dropdown_index;
WindowTooltipClose();
WindowTooltipShow(OpenRCT2String{ gDropdown.items[dropdown_index].tooltip, {} }, screenCoords);
}
if (dropdown_index < Dropdown::kItemsMaxSize && gDropdown.items[dropdown_index].isDisabled())
{
return;
}
if (gDropdown.items[dropdown_index].isSeparator())
{
return;
}
gDropdown.highlightedIndex = dropdown_index;
windowMgr->InvalidateByClass(WindowClass::dropdown);
}
else
{
gDropdown.lastTooltipHover = -1;
WindowTooltipClose();
}
}
static void InputUpdateTooltip(WindowBase* w, WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords)
{
if (gTooltipWidget.windowClassification == WindowClass::null)
{
if (gTooltipCursor == screenCoords)
{
if (gCurrentRealTimeTicks >= _tooltipNotShownTimeout && w != nullptr && widgetIndex != kWidgetIndexNull
&& widgetIsVisible(*w, widgetIndex))
{
gTooltipCloseTimeout = gCurrentRealTimeTicks + 8000;
WindowTooltipOpen(w, widgetIndex, screenCoords);
}
}
else
{
ResetTooltipNotShown();
}
gTooltipCloseTimeout = gCurrentRealTimeTicks + 8000;
gTooltipCursor = screenCoords;
}
else
{
gTooltipCursor = screenCoords;
ResetTooltipNotShown();
if (w == nullptr || gTooltipWidget.windowClassification != w->classification
|| gTooltipWidget.windowNumber != w->number || gTooltipWidget.widgetIndex != widgetIndex
|| !widgetIsVisible(*w, widgetIndex))
{
WindowTooltipClose();
}
if (gCurrentRealTimeTicks >= gTooltipCloseTimeout)
{
auto* windowMgr = GetWindowManager();
windowMgr->CloseByClass(WindowClass::tooltip);
}
}
}
#pragma endregion
/**
*
* rct2: 0x006ED990
*/
void SetCursor(CursorID cursor_id)
{
assert(cursor_id != CursorID::Undefined);
if (_inputState == InputState::Resizing)
{
cursor_id = CursorID::DiagonalArrows;
}
ContextSetCurrentCursor(cursor_id);
}
/**
*
* rct2: 0x006E876D
*/
void InvalidateScroll()
{
auto* windowMgr = GetWindowManager();
WindowBase* w = windowMgr->FindByNumber(gPressedWidget.windowClassification, gPressedWidget.windowNumber);
if (w != nullptr)
{
// Reset to basic scroll
w->scrolls[_currentScrollIndex].flags &= 0xFF11;
windowMgr->InvalidateByNumber(gPressedWidget.windowClassification, gPressedWidget.windowNumber);
}
}
/**
* rct2: 0x00406C96
*/
void StoreMouseInput(MouseState state, const ScreenCoordsXY& screenCoords)
{
uint32_t writeIndex = _mouseInputQueueWriteIndex;
uint32_t nextWriteIndex = (writeIndex + 1) % std::size(_mouseInputQueue);
// Check if the queue is full
if (nextWriteIndex != _mouseInputQueueReadIndex)
{
RCTMouseData* item = &_mouseInputQueue[writeIndex];
item->x = screenCoords.x;
item->y = screenCoords.y;
item->state = state;
_mouseInputQueueWriteIndex = nextWriteIndex;
}
}
void GameHandleEdgeScroll()
{
WindowBase* mainWindow;
int32_t scrollX, scrollY;
mainWindow = WindowGetMain();
if (mainWindow == nullptr)
return;
if ((mainWindow->flags.has(WindowFlag::noScrolling))
|| (gLegacyScene == LegacyScene::trackDesignsManager || gLegacyScene == LegacyScene::titleSequence))
return;
if (mainWindow->viewport == nullptr)
return;
if (!ContextHasFocus())
return;
scrollX = 0;
scrollY = 0;
// Scroll left / right
const CursorState* cursorState = ContextGetCursorState();
if (cursorState->position.x == 0)
scrollX = -1;
else if (cursorState->position.x >= ContextGetWidth() - 1)
scrollX = 1;
// Scroll up / down
if (cursorState->position.y == 0)
scrollY = -1;
else if (cursorState->position.y >= ContextGetHeight() - 1)
scrollY = 1;
InputScrollViewport(ScreenCoordsXY(scrollX, scrollY));
}
void InputScrollViewport(const ScreenCoordsXY& scrollScreenCoords)
{
WindowBase* mainWindow = WindowGetMain();
if (mainWindow == nullptr)
return;
Viewport* viewport = mainWindow->viewport;
if (viewport == nullptr)
return;
const int32_t speed = Config::Get().general.edgeScrollingSpeed;
int32_t multiplier = viewport->zoom.ApplyTo(speed);
int32_t dx = scrollScreenCoords.x * multiplier;
int32_t dy = scrollScreenCoords.y * multiplier;
if (scrollScreenCoords.x != 0)
{
// Speed up scrolling horizontally when at the edge of the map
// so that the speed is consistent with vertical edge scrolling.
int32_t x = mainWindow->savedViewPos.x + viewport->ViewWidth() / 2 + dx;
int32_t y = mainWindow->savedViewPos.y + viewport->ViewHeight() / 2;
int32_t y_dy = mainWindow->savedViewPos.y + viewport->ViewHeight() / 2 + dy;
auto mapCoord = ViewportPosToMapPos({ x, y }, 0, viewport->rotation);
auto mapCoord_dy = ViewportPosToMapPos({ x, y_dy }, 0, viewport->rotation);
// Check if we're crossing the boundary
// Clamp to the map minimum value
int32_t at_map_edge = 0;
int32_t at_map_edge_dy = 0;
if (mapCoord.x < kMapMinimumXY || mapCoord.y < kMapMinimumXY)
{
at_map_edge = 1;
}
if (mapCoord_dy.x < kMapMinimumXY || mapCoord_dy.y < kMapMinimumXY)
{
at_map_edge_dy = 1;
}
// Clamp to the map maximum value (scenario specific)
auto mapSizeMinus2 = GetMapSizeMinus2();
if (mapCoord.x > mapSizeMinus2.x || mapCoord.y > mapSizeMinus2.y)
{
at_map_edge = 1;
}
if (mapCoord_dy.x > mapSizeMinus2.x || mapCoord_dy.y > mapSizeMinus2.y)
{
at_map_edge_dy = 1;
}
// If we crossed the boundary, multiply the distance by 2
if (at_map_edge && at_map_edge_dy)
{
dx *= 2;
}
mainWindow->savedViewPos.x += dx;
gInputFlags.set(InputFlag::viewportScrolling);
}
if (scrollScreenCoords.y != 0)
{
mainWindow->savedViewPos.y += dy;
gInputFlags.set(InputFlag::viewportScrolling);
}
}
void InputScrollViewportSmooth(const ScreenCoordsXY& scrollScreenCoords, WindowBase* targetWindow)
{
if (targetWindow == nullptr)
{
return;
}
Viewport* viewport = targetWindow->viewport;
if (viewport == nullptr)
{
return;
}
if (targetWindow->flags.has(WindowFlag::noScrolling))
{
return;
}
if (scrollScreenCoords.x == 0 && scrollScreenCoords.y == 0)
return;
// Apply smooth scrolling similar to mouse drag behavior
// Use zoom-based scaling like mouse dragging does
ScreenCoordsXY differentialCoords = scrollScreenCoords;
// Apply zoom scaling (same logic as mouse drag)
const bool posX = differentialCoords.x > 0;
const bool posY = differentialCoords.y > 0;
differentialCoords.x = (viewport->zoom + 1).ApplyTo(-std::abs(differentialCoords.x));
differentialCoords.y = (viewport->zoom + 1).ApplyTo(-std::abs(differentialCoords.y));
differentialCoords.x = posX ? -differentialCoords.x : differentialCoords.x;
differentialCoords.y = posY ? -differentialCoords.y : differentialCoords.y;
// Apply the movement (note: we don't invert for gamepad like mouse drag does)
targetWindow->savedViewPos += differentialCoords;
gInputFlags.set(InputFlag::viewportScrolling);
}
} // namespace OpenRCT2