Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Core/GameEngine/Include/GameClient/Mouse.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ struct MouseIO

Int wheelPos; /**< mouse wheel position, 0 is no event, + is up/away from
user while - is down/toward user */
Real wheelPanX; /**< precise trackpad scroll delta X; 0 for real wheel events.
Devices without trackpad support leave these at 0. */
Real wheelPanY; ///< precise trackpad scroll delta Y; 0 for real wheel events
ICoord2D deltaPos; ///< overall change in mouse pointer this frame

MouseButtonState leftState; // button state: None (no event), Up, Down, DoubleClick
Expand Down
4 changes: 4 additions & 0 deletions Core/GameEngine/Source/GameClient/Input/Keyboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,10 @@ void Keyboard::resetKeys()
//-------------------------------------------------------------------------------------------------
void Keyboard::refreshAltKeys() const
{
// GeneralsX @bugfix 10/07/2026 Guard against focus events arriving before the message stream exists.
if (TheMessageStream == nullptr)
return;

if (BitIsSet(m_keyStatus[KEY_LALT].state, KEY_STATE_DOWN))
{
GameMessage* msg = TheMessageStream->appendMessage(GameMessage::MSG_RAW_KEY_UP);
Expand Down
10 changes: 9 additions & 1 deletion Core/GameEngine/Source/GameClient/Input/Mouse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ void Mouse::processMouseEvent( Int index )
m_currMouse.rightEvent = MOUSE_EVENT_NONE;
m_currMouse.middleEvent = MOUSE_EVENT_NONE;
m_currMouse.wheelPos = 0;
m_currMouse.wheelPanX = 0.0f;
m_currMouse.wheelPanY = 0.0f;

// what type of movement commands are we setup for
if( m_inputMovesAbsolute == TRUE )
Expand All @@ -218,6 +220,8 @@ void Mouse::processMouseEvent( Int index )

// Cumulate Wheel Adjustments
m_currMouse.wheelPos += m_mouseEvents[ index ].wheelPos;
m_currMouse.wheelPanX += m_mouseEvents[ index ].wheelPanX;
m_currMouse.wheelPanY += m_mouseEvents[ index ].wheelPanY;

// Check Left Mouse State
if( m_mouseEvents[ index ].leftState != MBS_None )
Expand Down Expand Up @@ -822,13 +826,17 @@ void Mouse::createStreamMessages()

// wheel pos
msg = nullptr;
if( m_currMouse.wheelPos != 0 )
if( m_currMouse.wheelPos != 0 || m_currMouse.wheelPanX != 0.0f || m_currMouse.wheelPanY != 0.0f )
{
msg = TheMessageStream->appendMessage( GameMessage::MSG_RAW_MOUSE_WHEEL );
msg->appendPixelArgument( m_currMouse.pos );
// TheSuperHackers @bugfix Use float wheel delta to preserve fractional values from touchpad input
msg->appendRealArgument( m_currMouse.wheelPos / (Real)MOUSE_WHEEL_DELTA );
msg->appendIntegerArgument( TheKeyboard->getModifierFlags() );
// GeneralsX @feature 21/07/2026 Precise trackpad pan deltas (both zero for real wheels);
// consumers that only read the first three arguments are unaffected.
msg->appendRealArgument( m_currMouse.wheelPanX );
msg->appendRealArgument( m_currMouse.wheelPanY );
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,40 @@ GameMessageDisposition LookAtTranslator::translateGameMessage(const GameMessage
{
m_lastMouseMoveTimeMsec = timeGetTime();

// GeneralsX @feature 21/07/2026 Trackpad two-finger scroll pans the camera
// (map follows the fingers); Alt/Option + scroll zooms instead, since a
// trackpad has no discrete wheel. Real mouse wheels keep the classic zoom.
Real panX = 0.0f;
Real panY = 0.0f;
if (msg->getArgumentCount() >= 5)
{
panX = msg->getArgument( 3 )->real;
panY = msg->getArgument( 4 )->real;
}
if (panX != 0.0f || panY != 0.0f)
{
const Int modifiers = msg->getArgument( 2 )->integer;
if (modifiers & KEY_STATE_ALT)
{
TheTacticalView->userZoom( -panY * View::ZoomHeightPerSecond );
}
else if (TheGameLogic->isInGame())
{
// Match the magnitude convention of the other scroll modes: keyboard
// scroll feeds scrollBy ~SCROLL_AMT * keyboardScrollFactor (~100) per
// frame, so one full-strength trackpad tick (~1.0) maps to that order.
// The user's scroll-speed option scales it like the other modes.
// Signs: SDL wheel Y is positive away from the user; the chosen
// orientation moves the camera with the swipe direction.
const Real trackpadPanFactor = 120.0f * TheGlobalData->m_keyboardScrollFactor;
Coord2D delta;
delta.x = -panX * TheGlobalData->m_horizontalScrollSpeedFactor * trackpadPanFactor;
delta.y = panY * TheGlobalData->m_verticalScrollSpeedFactor * trackpadPanFactor;
TheTacticalView->userScrollBy( &delta );
}
break;
}

const Real spin = msg->getArgument( 1 )->real;
const Real zoom = -spin * View::ZoomHeightPerSecond;
TheTacticalView->userZoom(zoom);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,12 @@ GameMessageDisposition WindowTranslator::translateGameMessage(const GameMessage
// get wheel position
Real wheelPos = msg->getArgument( 1 )->real;

// GeneralsX @feature 21/07/2026 Horizontal-only trackpad pan carries no
// vertical spin; UI windows have nothing to do with it, so let it pass
// through to the camera translator instead of faking a wheel direction.
if( wheelPos == 0.0f )
break;

// process wheel event
GameWindowMessage gwm = rawMouseToWindowMessage( msg );
if( TheWindowManager )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,13 @@ KeyVal SDL3Keyboard::translateScanCodeToKeyVal(unsigned char scan)
case SDL_SCANCODE_RSHIFT: return KEY_RSHIFT;
case SDL_SCANCODE_LCTRL: return KEY_LCTRL; // GeneralsX @bugfix BenderAI 13/02/2026 Fix key constant name
case SDL_SCANCODE_RCTRL: return KEY_RCTRL; // GeneralsX @bugfix BenderAI 13/02/2026 Fix key constant name
#ifdef __APPLE__
// GeneralsX @feature 22/07/2026 Cmd acts as Ctrl so Mac-idiomatic combos like
// Cmd+1 work for control groups. Physical Ctrl still works too. macOS only:
// on Linux the GUI key is the desktop's Super key and must stay unmapped.
case SDL_SCANCODE_LGUI: return KEY_LCTRL;
case SDL_SCANCODE_RGUI: return KEY_RCTRL;
#endif
case SDL_SCANCODE_LALT: return KEY_LALT; // GeneralsX @bugfix BenderAI 13/02/2026 Fix key constant name
case SDL_SCANCODE_RALT: return KEY_RALT; // GeneralsX @bugfix BenderAI 13/02/2026 Fix key constant name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "SDL3Device/GameClient/SDL3Mouse.h"
#include <cstdio>
#include <cstring>
#include <cmath>

// GeneralsX @bugfix felipebraz 18/02/2026 Include GameLogic for frame tracking
#include "GameLogic/GameLogic.h"
Expand Down Expand Up @@ -756,9 +757,43 @@ void SDL3Mouse::translateWheelEvent(const SDL_MouseWheelEvent& event, MouseIO *r
// GeneralsX @bugfix felipebraz 18/02/2026 Normalize timestamp to milliseconds (SDL3 uses nanoseconds)
result->time = (Uint32)(event.timestamp / 1000000);

// GeneralsX @feature 21/07/2026 Trackpad two-finger pan support.
// Honor macOS "natural scrolling": SDL flags flipped values with
// direction == SDL_MOUSEWHEEL_FLIPPED; un-flip so downstream sign
// conventions are stable regardless of the user's system setting.
float wheelX = event.x;
float wheelY = event.y;
if (event.direction == SDL_MOUSEWHEEL_FLIPPED) {
wheelX = -wheelX;
wheelY = -wheelY;
}

// Distinguish trackpad scrolls from wheel notches: trackpads produce
// precise fractional deltas and/or a horizontal axis, while wheels tick
// in whole notches on Y only. The latch keeps mid-gesture events that
// happen to land on whole numbers classified as trackpad.
const bool preciseNow = (wheelX != 0.0f) ||
(wheelY != truncf(wheelY)) ||
(wheelY != 0.0f && fabsf(wheelY) < 1.0f);
static Uint64 s_lastPreciseTimestampNs = 0;
bool isTrackpad = preciseNow;
if (preciseNow) {
s_lastPreciseTimestampNs = event.timestamp;
} else if (event.timestamp - s_lastPreciseTimestampNs < 300000000ULL) { // 300 ms
isTrackpad = true;
}

if (isTrackpad) {
result->wheelPanX = wheelX;
result->wheelPanY = wheelY;
} else {
result->wheelPanX = 0.0f;
result->wheelPanY = 0.0f;
}

// SDL3 wheel: positive = up/away, negative = down/toward user
// Multiply by MOUSE_WHEEL_DELTA (120) to match Windows behavior
result->wheelPos = (Int)(event.y * MOUSE_WHEEL_DELTA);
result->wheelPos = (Int)(wheelY * MOUSE_WHEEL_DELTA);

result->leftState = MBS_None;
result->rightState = MBS_None;
Expand Down Expand Up @@ -879,6 +914,11 @@ void SDL3Mouse::translateEvent(UnsignedInt eventIndex, MouseIO *result)
int rawX = 0, rawY = 0;
Uint32 windowID = 0;

// Pan deltas only exist on wheel events; clear them up front so motion and
// button translations never carry a stale pan from a previous buffer slot.
result->wheelPanX = 0.0f;
result->wheelPanY = 0.0f;

// Switch on event type and delegate to appropriate translation method
switch (event.type) {
case SDL_EVENT_MOUSE_MOTION:
Expand Down
7 changes: 7 additions & 0 deletions Generals/Code/GameEngineDevice/Source/SDL3GameEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,13 @@ void SDL3GameEngine::pollSDL3Events(void)

case SDL_EVENT_WINDOW_FOCUS_GAINED:
m_IsActive = true;
// GeneralsX @bugfix 10/07/2026 SDL3 has no DirectInput-style KEY_LOST, so modifier
// key-up events swallowed while unfocused (Cmd-Tab, Mission Control) leave Alt/Ctrl
// stuck down and silently break modifier hotkeys like Ctrl+number. Mirror the Win32
// device-loss path by resetting keyboard state when focus returns.
if (TheKeyboard) {
TheKeyboard->resetKeys();
}
break;

case SDL_EVENT_WINDOW_FOCUS_LOST:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,40 @@ GameMessageDisposition LookAtTranslator::translateGameMessage(const GameMessage
{
m_lastMouseMoveTimeMsec = timeGetTime();

// GeneralsX @feature 21/07/2026 Trackpad two-finger scroll pans the camera
// (map follows the fingers); Alt/Option + scroll zooms instead, since a
// trackpad has no discrete wheel. Real mouse wheels keep the classic zoom.
Real panX = 0.0f;
Real panY = 0.0f;
if (msg->getArgumentCount() >= 5)
{
panX = msg->getArgument( 3 )->real;
panY = msg->getArgument( 4 )->real;
}
if (panX != 0.0f || panY != 0.0f)
{
const Int modifiers = msg->getArgument( 2 )->integer;
if (modifiers & KEY_STATE_ALT)
{
TheTacticalView->userZoom( -panY * View::ZoomHeightPerSecond );
}
else if (TheGameLogic->isInGame())
{
// Match the magnitude convention of the other scroll modes: keyboard
// scroll feeds scrollBy ~SCROLL_AMT * keyboardScrollFactor (~100) per
// frame, so one full-strength trackpad tick (~1.0) maps to that order.
// The user's scroll-speed option scales it like the other modes.
// Signs: SDL wheel Y is positive away from the user; the chosen
// orientation moves the camera with the swipe direction.
const Real trackpadPanFactor = 120.0f * TheGlobalData->m_keyboardScrollFactor;
Coord2D delta;
delta.x = -panX * TheGlobalData->m_horizontalScrollSpeedFactor * trackpadPanFactor;
delta.y = panY * TheGlobalData->m_verticalScrollSpeedFactor * trackpadPanFactor;
TheTacticalView->userScrollBy( &delta );
}
break;
}

const Real spin = msg->getArgument( 1 )->real;
const Real zoom = -spin * View::ZoomHeightPerSecond;
TheTacticalView->userZoom(zoom);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ GameMessageDisposition WindowTranslator::translateGameMessage(const GameMessage
// get wheel position
Real wheelPos = msg->getArgument( 1 )->real;

// GeneralsX @feature 21/07/2026 Horizontal-only trackpad pan carries no
// vertical spin; UI windows have nothing to do with it, so let it pass
// through to the camera translator instead of faking a wheel direction.
if( wheelPos == 0.0f )
break;

// process wheel event
GameWindowMessage gwm = rawMouseToWindowMessage( msg );
if( TheWindowManager )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ KeyVal SDL3Keyboard::translateScanCodeToKeyVal(unsigned char scan)
case SDL_SCANCODE_RSHIFT: return KEY_RSHIFT;
case SDL_SCANCODE_LCTRL: return KEY_LCTRL; // GeneralsX @bugfix BenderAI 13/02/2026 Fix key constant name
case SDL_SCANCODE_RCTRL: return KEY_RCTRL; // GeneralsX @bugfix BenderAI 13/02/2026 Fix key constant name
#ifdef __APPLE__
// GeneralsX @feature 22/07/2026 Cmd acts as Ctrl so Mac-idiomatic combos like
// Cmd+1 work for control groups. Physical Ctrl still works too. macOS only:
// on Linux the GUI key is the desktop's Super key and must stay unmapped.
case SDL_SCANCODE_LGUI: return KEY_LCTRL;
case SDL_SCANCODE_RGUI: return KEY_RCTRL;
#endif
case SDL_SCANCODE_LALT: return KEY_LALT; // GeneralsX @bugfix BenderAI 13/02/2026 Fix key constant name
case SDL_SCANCODE_RALT: return KEY_RALT; // GeneralsX @bugfix BenderAI 13/02/2026 Fix key constant name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "SDL3Device/GameClient/SDL3Mouse.h"
#include <cstdio>
#include <cstring>
#include <cmath>

// GeneralsX @bugfix felipebraz 18/02/2026 Include GameLogic for frame tracking
#include "GameLogic/GameLogic.h"
Expand Down Expand Up @@ -755,9 +756,43 @@ void SDL3Mouse::translateWheelEvent(const SDL_MouseWheelEvent& event, MouseIO *r
// GeneralsX @bugfix felipebraz 18/02/2026 Normalize timestamp to milliseconds (SDL3 uses nanoseconds)
result->time = (Uint32)(event.timestamp / 1000000);

// GeneralsX @feature 21/07/2026 Trackpad two-finger pan support.
// Honor macOS "natural scrolling": SDL flags flipped values with
// direction == SDL_MOUSEWHEEL_FLIPPED; un-flip so downstream sign
// conventions are stable regardless of the user's system setting.
float wheelX = event.x;
float wheelY = event.y;
if (event.direction == SDL_MOUSEWHEEL_FLIPPED) {
wheelX = -wheelX;
wheelY = -wheelY;
}

// Distinguish trackpad scrolls from wheel notches: trackpads produce
// precise fractional deltas and/or a horizontal axis, while wheels tick
// in whole notches on Y only. The latch keeps mid-gesture events that
// happen to land on whole numbers classified as trackpad.
const bool preciseNow = (wheelX != 0.0f) ||
(wheelY != truncf(wheelY)) ||
(wheelY != 0.0f && fabsf(wheelY) < 1.0f);
static Uint64 s_lastPreciseTimestampNs = 0;
bool isTrackpad = preciseNow;
if (preciseNow) {
s_lastPreciseTimestampNs = event.timestamp;
} else if (event.timestamp - s_lastPreciseTimestampNs < 300000000ULL) { // 300 ms
isTrackpad = true;
}

if (isTrackpad) {
result->wheelPanX = wheelX;
result->wheelPanY = wheelY;
} else {
result->wheelPanX = 0.0f;
result->wheelPanY = 0.0f;
}

// SDL3 wheel: positive = up/away, negative = down/toward user
// Multiply by MOUSE_WHEEL_DELTA (120) to match Windows behavior
result->wheelPos = (Int)(event.y * MOUSE_WHEEL_DELTA);
result->wheelPos = (Int)(wheelY * MOUSE_WHEEL_DELTA);

result->leftState = MBS_None;
result->rightState = MBS_None;
Expand Down Expand Up @@ -878,6 +913,11 @@ void SDL3Mouse::translateEvent(UnsignedInt eventIndex, MouseIO *result)
int rawX = 0, rawY = 0;
Uint32 windowID = 0;

// Pan deltas only exist on wheel events; clear them up front so motion and
// button translations never carry a stale pan from a previous buffer slot.
result->wheelPanX = 0.0f;
result->wheelPanY = 0.0f;

// Switch on event type and delegate to appropriate translation method
switch (event.type) {
case SDL_EVENT_MOUSE_MOTION:
Expand Down
7 changes: 7 additions & 0 deletions GeneralsMD/Code/GameEngineDevice/Source/SDL3GameEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,13 @@ void SDL3GameEngine::pollSDL3Events(void)

case SDL_EVENT_WINDOW_FOCUS_GAINED:
m_IsActive = true;
// GeneralsX @bugfix 10/07/2026 SDL3 has no DirectInput-style KEY_LOST, so modifier
// key-up events swallowed while unfocused (Cmd-Tab, Mission Control) leave Alt/Ctrl
// stuck down and silently break modifier hotkeys like Ctrl+number. Mirror the Win32
// device-loss path by resetting keyboard state when focus returns.
if (TheKeyboard) {
TheKeyboard->resetKeys();
}
if (TheMouse) {
TheMouse->regainFocus();
TheMouse->refreshCursorCapture();
Expand Down
2 changes: 1 addition & 1 deletion Patches/SagePatch/src/common/Init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ void init() {
SAGEPATCH_LOG(" F11 screenshot (PNG to ~/Pictures/GeneralsX)");
SAGEPATCH_LOG(" Scroll Lock toggle cursor lock");
SAGEPATCH_LOG(" Ctrl+PageUp/Dn brightness +/-");
SAGEPATCH_LOG(" Ctrl+1..5 window position (center / TL / TR / BL / BR)");
SAGEPATCH_LOG(" Cmd+Option+1..5 window position (center / TL / TR / BL / BR)");
}

void shutdown() {}
Expand Down
16 changes: 11 additions & 5 deletions Patches/SagePatch/src/common/KeyHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ bool handleKeyDown(const SDL_KeyboardEvent& ev) {
if (!window) return false;

const bool ctrl = (ev.mod & SDL_KMOD_CTRL) != 0;
// Window snap uses Cmd+Option: the game binds Ctrl+1..0 to control groups,
// and on macOS Cmd also acts as Ctrl in-game, so any bare-modifier+number
// combo would shadow them. Cmd+Option+number is unbound in the game.
const bool gui = (ev.mod & SDL_KMOD_GUI) != 0;
const bool alt = (ev.mod & SDL_KMOD_ALT) != 0;
const bool snapMod = gui && alt;

switch (ev.key) {
case SDLK_F11:
Expand All @@ -29,19 +35,19 @@ bool handleKeyDown(const SDL_KeyboardEvent& ev) {
break;

case SDLK_1:
if (ctrl) { moveWindow(window, WindowPosition::Center); return true; }
if (snapMod) { moveWindow(window, WindowPosition::Center); return true; }
break;
case SDLK_2:
if (ctrl) { moveWindow(window, WindowPosition::TopLeft); return true; }
if (snapMod) { moveWindow(window, WindowPosition::TopLeft); return true; }
break;
case SDLK_3:
if (ctrl) { moveWindow(window, WindowPosition::TopRight); return true; }
if (snapMod) { moveWindow(window, WindowPosition::TopRight); return true; }
break;
case SDLK_4:
if (ctrl) { moveWindow(window, WindowPosition::BottomLeft); return true; }
if (snapMod) { moveWindow(window, WindowPosition::BottomLeft); return true; }
break;
case SDLK_5:
if (ctrl) { moveWindow(window, WindowPosition::BottomRight); return true; }
if (snapMod) { moveWindow(window, WindowPosition::BottomRight); return true; }
break;

default:
Expand Down
Loading