diff --git a/.github/workflows/engine-build.yml b/.github/workflows/engine-build.yml index 3bb16fce2..bcdf0e967 100644 --- a/.github/workflows/engine-build.yml +++ b/.github/workflows/engine-build.yml @@ -110,6 +110,9 @@ jobs: cp thirdparty/imgui/LICENSE.txt artifact/OpenTS_THIRD_PARTY_LICENSES/dear-imgui.txt cp thirdparty/licenses/stb-imgui.txt artifact/OpenTS_THIRD_PARTY_LICENSES/stb-imgui.txt cp thirdparty/licenses/stb-image.txt artifact/OpenTS_THIRD_PARTY_LICENSES/stb-image.txt + cp thirdparty/SDL/LICENSE.txt artifact/OpenTS_THIRD_PARTY_LICENSES/sdl.txt + cp thirdparty/SDL/src/video/yuv2rgb/LICENSE artifact/OpenTS_THIRD_PARTY_LICENSES/yuv2rgb.txt + cp thirdparty/licenses/sdl-fdlibm.txt artifact/OpenTS_THIRD_PARTY_LICENSES/fdlibm.txt cp ui/OFL.txt artifact/OpenTS_THIRD_PARTY_LICENSES/arimo.txt - name: Upload runtime files diff --git a/.gitmodules b/.gitmodules index 7805f8147..b27d7828b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "thirdparty/imgui"] path = thirdparty/imgui url = https://github.com/ocornut/imgui.git +[submodule "thirdparty/SDL"] + path = thirdparty/SDL + url = https://github.com/libsdl-org/SDL.git diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index c9d47be17..ad083b34a 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -24,6 +24,9 @@ remains under its own license and copyright notices. | [Dear ImGui](https://github.com/ocornut/imgui) | Developer overlays | MIT | | [stb](https://github.com/nothings/stb) | Rectangle packing, text editing, and TrueType headers bundled with Dear ImGui | MIT or Unlicense | | [stb_image](https://github.com/nothings/stb) | PNG and TGA decoding for the UI, bundled with bimg | MIT or Unlicense | +| [SDL](https://github.com/libsdl-org/SDL) | Window, events, and keyboard and mouse input | zlib | +| [yuv2rgb](https://github.com/descampsa/yuv2rgb) | YUV conversion bundled with SDL | BSD 3-Clause | +| [fdlibm](https://www.netlib.org/fdlibm/) | Math routines bundled with SDL | fdlibm | | [Arimo](https://github.com/googlefonts/arimo) | The UI font | OFL-1.1 | The source checkout keeps the license texts under `thirdparty/`. Binary diff --git a/cmake/CheckToolkitHeaders.cmake b/cmake/CheckToolkitHeaders.cmake index dab2a6b0c..1a8e49d51 100644 --- a/cmake/CheckToolkitHeaders.cmake +++ b/cmake/CheckToolkitHeaders.cmake @@ -1,5 +1,7 @@ # Checks that no engine header outside code/ui/rml/ includes a UI toolkit or renderer header, -# and that no engine source outside code/ui/ includes RmlUi or Dear ImGui. +# that no engine source outside code/ui/ includes RmlUi or Dear ImGui, and that SDL stays in +# code/sdl/: its sources and private headers may include SDL, sdlwindow.h may not, and nothing +# outside code/sdl/ includes SDL or a code/sdl/ header other than sdlwindow.h. # # Expects OPENTS_SOURCE_DIR to be set. Run with `cmake -DOPENTS_SOURCE_DIR= -P`. @@ -7,8 +9,23 @@ if(NOT DEFINED OPENTS_SOURCE_DIR) message(FATAL_ERROR "CheckToolkitHeaders.cmake: OPENTS_SOURCE_DIR is not set.") endif() -set(TOOLKIT_INCLUDE "^[ \t]*#[ \t]*include[ \t]*[<\"](RmlUi/|imgui|bgfx/|bx/|bimg/|stb_)") +set(TOOLKIT_INCLUDE "^[ \t]*#[ \t]*include[ \t]*[<\"](RmlUi/|imgui|bgfx/|bx/|bimg/|stb_|SDL3/)") set(UI_TOOLKIT_INCLUDE "^[ \t]*#[ \t]*include[ \t]*[<\"](RmlUi/|imgui)") +set(SDL_INCLUDE "^[ \t]*#[ \t]*include[ \t]*[<\"]SDL3/") +set(RENDER_TOOLKIT_INCLUDE "^[ \t]*#[ \t]*include[ \t]*[<\"](RmlUi/|imgui|bgfx/|bx/|bimg/|stb_)") +set(SDL_LAYER_INCLUDE "^[ \t]*#[ \t]*include[ \t]*\"(\\.\\./)*[Ss][Dd][Ll]/") + +# Includes a code/sdl/ header other than sdlwindow.h, which keeps SDL types out of the file. +function(check_sdl_layer_includes file) + file(STRINGS "${OPENTS_SOURCE_DIR}/${file}" hits REGEX "${SDL_LAYER_INCLUDE}") + foreach(hit IN LISTS hits) + string(TOLOWER "${hit}" lowered) + if(NOT lowered MATCHES "sdl/sdlwindow\\.h\"") + list(APPEND violations "${file}: ${hit}") + endif() + endforeach() + set(violations "${violations}" PARENT_SCOPE) +endfunction() set(violations "") @@ -19,9 +36,16 @@ file(GLOB_RECURSE headers RELATIVE "${OPENTS_SOURCE_DIR}" ) foreach(header IN LISTS headers) if(header MATCHES "^code/ui/rml/") - continue() + set(pattern "${SDL_INCLUDE}") + elseif(header MATCHES "^code/sdl/" AND NOT header STREQUAL "code/sdl/sdlwindow.h") + set(pattern "${RENDER_TOOLKIT_INCLUDE}") + else() + set(pattern "${TOOLKIT_INCLUDE}") + endif() + if(NOT header MATCHES "^code/sdl/" OR header STREQUAL "code/sdl/sdlwindow.h") + check_sdl_layer_includes("${header}") endif() - file(STRINGS "${OPENTS_SOURCE_DIR}/${header}" hits REGEX "${TOOLKIT_INCLUDE}") + file(STRINGS "${OPENTS_SOURCE_DIR}/${header}" hits REGEX "${pattern}") foreach(hit IN LISTS hits) list(APPEND violations "${header}: ${hit}") endforeach() @@ -32,13 +56,19 @@ file(GLOB_RECURSE sources RELATIVE "${OPENTS_SOURCE_DIR}" "${OPENTS_SOURCE_DIR}/code/*.c" ) foreach(source IN LISTS sources) - if(source MATCHES "^code/ui/") - continue() + if(NOT source MATCHES "^code/ui/") + file(STRINGS "${OPENTS_SOURCE_DIR}/${source}" hits REGEX "${UI_TOOLKIT_INCLUDE}") + foreach(hit IN LISTS hits) + list(APPEND violations "${source}: ${hit}") + endforeach() + endif() + if(NOT source MATCHES "^code/sdl/") + file(STRINGS "${OPENTS_SOURCE_DIR}/${source}" hits REGEX "${SDL_INCLUDE}") + foreach(hit IN LISTS hits) + list(APPEND violations "${source}: ${hit}") + endforeach() + check_sdl_layer_includes("${source}") endif() - file(STRINGS "${OPENTS_SOURCE_DIR}/${source}" hits REGEX "${UI_TOOLKIT_INCLUDE}") - foreach(hit IN LISTS hits) - list(APPEND violations "${source}: ${hit}") - endforeach() endforeach() if(violations) @@ -46,4 +76,4 @@ if(violations) message(FATAL_ERROR "Toolkit headers included outside their module:\n ${text}") endif() -message(STATUS "No toolkit header leaks outside code/ui/rml/.") +message(STATUS "No toolkit header leaks outside its module.") diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 82fd93f35..e1d7c9fbc 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -223,6 +223,7 @@ target_link_libraries(OpenTS PRIVATE lzo RmlUi::Core imgui + SDL3::SDL3-static comctl32 dbghelp iphlpapi diff --git a/code/cctooltip.cpp b/code/cctooltip.cpp index 13127c898..fc3845786 100644 --- a/code/cctooltip.cpp +++ b/code/cctooltip.cpp @@ -47,14 +47,14 @@ bool CCToolTip::Update(ToolTipText * text) Rect * trect; if (Options.IsSidebarOnRight == true) { - if (text->Pos.x <= TacticalRect.X + TacticalRect.Width) { + if (text->Pos.X <= TacticalRect.X + TacticalRect.Width) { trect = &TacticalRect; } else { trect = &SidebarRect; Map.SidebarClass::IsToRedraw = true; } } else { - if (text->Pos.x <= SidebarRect.X + SidebarRect.Width) { + if (text->Pos.X <= SidebarRect.X + SidebarRect.Width) { trect = &SidebarRect; Map.SidebarClass::IsToRedraw = true; } else { @@ -72,17 +72,17 @@ bool CCToolTip::Update(ToolTipText * text) text->TextHeight = std::max(rect.Height, text->TextHeight); } - int x = text->Pos.x + text->TextWidth - trect->Width - trect->X; + int x = text->Pos.X + text->TextWidth - trect->Width - trect->X; if (x > 0) { - text->Pos.x -= x; + text->Pos.X -= x; } - text->Pos.y += 16; - if (text->Pos.y + text->TextHeight - trect->Height - trect->Y > 0) { - text->Pos.y = text->Pos.y - text->TextHeight - 16; + text->Pos.Y += 16; + if (text->Pos.Y + text->TextHeight - trect->Height - trect->Y > 0) { + text->Pos.Y = text->Pos.Y - text->TextHeight - 16; } - if (text->Pos.y < trect->Y) { - text->Pos.y = trect->Y; + if (text->Pos.Y < trect->Y) { + text->Pos.Y = trect->Y; } } @@ -103,11 +103,11 @@ void CCToolTip::Reset(const ToolTipText * text) { bool redraw = false; if (Options.IsSidebarOnRight == true) { - if (text->Pos.x >= TacticalRect.X + TacticalRect.Width) { + if (text->Pos.X >= TacticalRect.X + TacticalRect.Width) { redraw = true; } } else { - if (text->Pos.x <= SidebarRect.X + SidebarRect.Width) { + if (text->Pos.X <= SidebarRect.X + SidebarRect.Width) { redraw = true; } } @@ -146,7 +146,7 @@ void CCToolTip::Draw_Current(bool sidebar) void CCToolTip::Draw(const ToolTipText * text) { Rect drawrect; - Point2D point = Point2D(text->Pos.x, text->Pos.y); + Point2D point = Point2D(text->Pos.X, text->Pos.Y); Surface * surface = NULL; if (Options.IsSidebarOnRight == true) { diff --git a/code/cctooltip.h b/code/cctooltip.h index f0cebfe61..0ff46c725 100644 --- a/code/cctooltip.h +++ b/code/cctooltip.h @@ -19,8 +19,8 @@ class CCToolTip : public ToolTipManager typedef ToolTipManager BASECLASS; public: - CCToolTip(HWND hWnd) : /// Inlined in Windows_Procedure - BASECLASS(hWnd), + CCToolTip(void) : + BASECLASS(), UseSidebarSurface(false), Style(TPF_MAP) { diff --git a/code/conquer.cpp b/code/conquer.cpp index 9dedfd6e4..7c57136d5 100644 --- a/code/conquer.cpp +++ b/code/conquer.cpp @@ -112,6 +112,7 @@ #include "rules.h" #include "savemgr.h" #include "scenario.h" +#include "sdl/sdlwindow.h" #include "session.h" #include "sidebar.h" #include "sounddlg.h" @@ -353,18 +354,7 @@ void Main_Game(int argc, char * argv[]) int ret = Init_Game(argc, argv); if (ret) { if (ret < 0) { - MSGBOXPARAMS params; - params.cbSize = sizeof(MSGBOXPARAMS); - params.hwndOwner = MainWindow; - params.hInstance = ProgramInstance; - params.lpszText = Fetch_String(TXT_INITGAME_FAILED); - params.lpszCaption = Fetch_String(TXT_SHORT_TITLE); - params.dwStyle = (MB_OK | MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST); - params.lpszIcon = NULL; - params.dwContextHelpId = NULL; - params.lpfnMsgBoxCallback = NULL; - params.dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); - MessageBoxIndirect(¶ms); + Main_Window_Error_Box(Fetch_String(TXT_SHORT_TITLE), Fetch_String(TXT_INITGAME_FAILED)); } return; } diff --git a/code/desyncdlg.cpp b/code/desyncdlg.cpp index 989adc12e..1a91d8408 100644 --- a/code/desyncdlg.cpp +++ b/code/desyncdlg.cpp @@ -35,7 +35,6 @@ #include "syncreport.h" #include "ui/screens/desync/uidesync.h" #include "win.h" -#include "winfix.h" #include #include diff --git a/code/display.cpp b/code/display.cpp index b15ca19c0..30af79a6a 100644 --- a/code/display.cpp +++ b/code/display.cpp @@ -3686,10 +3686,6 @@ char const * DisplayClass::Help_Text(int id) /// void DisplayClass::Reposition_Sidebar(void) { - if (UnusedWindow) { - CloseWindow(UnusedWindow); - UnusedWindow = 0; - } } diff --git a/code/except.cpp b/code/except.cpp index 7df969168..676ba65b8 100644 --- a/code/except.cpp +++ b/code/except.cpp @@ -54,6 +54,7 @@ #include "version.h" #include "win.h" +#include #include #include @@ -1553,9 +1554,6 @@ static void Release_Display(void) { ClipCursor(NULL); - while (ShowCursor(TRUE) < 0) { - } - if (!WindowedMode && MainWindow != NULL) { // Asynchronous because the thread that owns the window is parked in the filter and // will never answer a message. The dialog is topmost, so this only tidies up behind. @@ -2178,12 +2176,24 @@ void Exception_Run_Immediate_Test(void) } +// SDL owns the window procedure, so the test hooks it to fault inside its dispatch. +static LRESULT CALLBACK Test_Window_Procedure(HWND window, UINT message, WPARAM wparam, LPARAM lparam, UINT_PTR, DWORD_PTR) +{ + if (message == WM_EXCEPTION_TEST) { + Exception_Wndproc_Test_Fault(); + return(0); + } + return(DefSubclassProc(window, message, wparam, lparam)); +} + + /// /// Raises the requested test fault, for the faults that need a window to happen inside. /// void Exception_Run_Post_Window_Test(void) { if (stricmp(TestMode, "wndproc") == 0) { + SetWindowSubclass(MainWindow, Test_Window_Procedure, 1, 0); PostMessage(MainWindow, WM_EXCEPTION_TEST, 0, 0); } else if (stricmp(TestMode, "timer") == 0) { diff --git a/code/gamewindow.cpp b/code/gamewindow.cpp index fb1fdbd4a..48116d6af 100644 --- a/code/gamewindow.cpp +++ b/code/gamewindow.cpp @@ -11,23 +11,70 @@ #include "gamewindow.h" +#include "_keyboar.h" #include "_map.h" #include "_surface.h" +#include "_tooltip.h" +#include "_ui.h" #include "_xmouse.h" +#include "cctooltip.h" +#include "dbgprint.h" #include "globals.h" +#include "goptions.h" #include "gscreen.h" #include "init.h" +#include "misc.h" #include "movies.h" +#include "queue.h" +#include "sdl/sdlwindow.h" +#include "session.h" +#include "ui/uishell.h" #include "video.h" +#include "vidscale.h" +#include "windowevent.hh" +#include "winstub.h" +#include "wwmouse.h" + +#include static bool _HandlingMouseWheel = false; +static bool _InterfaceTookKey = false; -/// -/// Updates and presents the frame when the application window needs repainting. -/// -void Game_Window_On_Paint(bool update_surface) +static bool Is_Mouse_Event(WindowEventType type) +{ + switch (type) { + case WINDOW_EVENT_MOUSE_MOVE: + case WINDOW_EVENT_MOUSE_DOWN: + case WINDOW_EVENT_MOUSE_UP: + case WINDOW_EVENT_MOUSE_WHEEL: + return(true); + + default: + return(false); + } +} + + +static void Set_Game_Focus(bool focused) +{ + if (GameInFocus == focused) { + return; + } + + GameInFocus = focused; + if (!GameInFocus) { + Focus_Loss(); + DebugString("Focus lost\n"); + } else { + Focus_Restore(); + DebugString("Focus gained\n"); + } +} + + +static void On_Paint(bool update_surface) { if (update_surface) { if (MouseCursor != NULL && VisibleSurface != NULL && HiddenSurface != NULL && CompositeSurface != NULL) { @@ -45,19 +92,13 @@ void Game_Window_On_Paint(bool update_surface) } -/// -/// Stops tactical scrolling from coasting after the right mouse button is released. -/// -void Game_Window_On_Right_Mouse_Up(void) +static void On_Right_Mouse_Up(void) { Map.Set_Scroll_Coasting_Allowed(false); } -/// -/// Applies a mouse wheel step to the sidebar. -/// -void Game_Window_On_Mouse_Wheel(int delta) +static void On_Mouse_Wheel(int delta) { if (_HandlingMouseWheel) { return; @@ -67,3 +108,187 @@ void Game_Window_On_Mouse_Wheel(int delta) Execute_Command(delta < 0 ? "SidebarDown" : "SidebarUp"); _HandlingMouseWheel = false; } + + +static bool Select_Cursor(void) +{ + return(UIShell.Handle_Set_Cursor() || (MouseCursor != NULL && ((WWMouseClass *)MouseCursor)->Show_Game_Pointer())); +} + + +// The window's arrow stays hidden while the game has hidden the pointer it released. +static void Update_Cursor(void) +{ + if (Select_Cursor()) { + return; + } + + bool const visible = (MouseCursor == NULL || MouseCursor->Get_Mouse_State() >= 0); + Main_Window_Set_Cursor(visible ? Main_Window_System_Cursor(UI_CURSOR_ARROW) : NULL); +} + + +static bool Handle_Event(WindowEvent const & event) +{ + if (ToolTips != NULL) { + ToolTips->Handle_Window_Event(event); + } + + bool const taken = UIShell.Handle_Window_Event(event); + + // A character belongs with the key press that typed it, so the game gets neither when + // the interface took the press. Text after the key's release, as from an input method, + // belongs to no press. + if (event.Type == WINDOW_EVENT_KEY_DOWN) { + _InterfaceTookKey = taken; + } else if (event.Type == WINDOW_EVENT_KEY_UP) { + _InterfaceTookKey = false; + } else if (event.Type == WINDOW_EVENT_TEXT && _InterfaceTookKey) { + return(true); + } + + if (taken) { + return(true); + } + + // The map and the key queue work in frame coordinates. + WindowEvent frame = event; + if (Is_Mouse_Event(event.Type) && Video_Scaling_Active()) { + Point2D point(event.X, event.Y); + Window_Point_To_Game(point); + frame.X = point.X; + frame.Y = point.Y; + } + + Map.Handle_Window_Event(frame); + + switch (event.Type) { + case WINDOW_EVENT_EXPOSED: + On_Paint(GameInFocus == true || WindowedMode == true); + break; + + case WINDOW_EVENT_RESIZED: + Video_On_Resize(event.Width, event.Height); + Video_Set_Refresh_Rate(Main_Window_Refresh_Rate()); + break; + + case WINDOW_EVENT_DISPLAY_CHANGED: + Video_Set_Refresh_Rate(Main_Window_Refresh_Rate()); + break; + + case WINDOW_EVENT_FOCUS_GAINED: + case WINDOW_EVENT_FOCUS_LOST: + Set_Game_Focus(event.Type == WINDOW_EVENT_FOCUS_GAINED); + return(false); + + case WINDOW_EVENT_MOUSE_UP: + if (event.Button == WINDOW_BUTTON_RIGHT) { + On_Right_Mouse_Up(); + } + break; + + case WINDOW_EVENT_MOUSE_WHEEL: + if (!event.Horizontal) { + On_Mouse_Wheel(event.Wheel < 0.0f ? -1 : 1); + } + break; + + // A running game resigns through the queue and then ends itself; otherwise the request + // is ignored. + case WINDOW_EVENT_CLOSE_REQUESTED: + if (GameActive && PlayerPtr != NULL && !Session.Play) { + Queue_Exit(); + } + break; + + default: + break; + } + + /* + ** Pass this event through to the keyboard handler. + */ + if (Keyboard != NULL) { + Keyboard->Handle_Window_Event(frame); + } + + return(false); +} + + +/// +/// Passes an event from the main window to the tooltips, the interface, the tactical map, +/// and the key queue, in that order, and applies the game's own response to it. +/// +void Game_Window_Handle_Event(WindowEvent const & event) +{ + Handle_Event(event); + + // Windows asked for the pointer on every mouse move; SDL leaves choosing it to the game. + if (event.Type == WINDOW_EVENT_MOUSE_MOVE || event.Type == WINDOW_EVENT_FOCUS_GAINED) { + Update_Cursor(); + } +} + + +/// +/// Opens the main window, with its tooltips and the window's arrow. A window for windowed play +/// has the client size the WindowWidth and WindowHeight settings give, taking the frame's size +/// for either one that is not set; otherwise the window covers the primary display. +/// +/// The width of the frame. +/// The height of the frame. +/// False when SDL could not start or the window could not be created. +bool Game_Window_Open(int width, int height) +{ + InitCommonControls(); + + int clientwidth = width; + int clientheight = height; + if (WindowedMode) { + if (Options.WindowWidth > 0) { + clientwidth = Options.WindowWidth; + } + if (Options.WindowHeight > 0) { + clientheight = Options.WindowHeight; + } + } + + if (!Main_Window_Create(WindowedMode, clientwidth, clientheight)) { + return(false); + } + + MainWindow = (HWND)Main_Window_Native().Handle; + + ToolTips = new CCToolTip(); + ToolTips->Set_Timer_Delay(500); + + Main_Window_Set_Cursor(Main_Window_System_Cursor(UI_CURSOR_ARROW)); + return(true); +} + + +/// +/// Stops the game using the main window at shutdown: its tooltips are deleted and nothing is +/// pumped from it afterwards. The window itself stays until Game_Window_Close. +/// +void Game_Window_Begin_Shutdown(void) +{ + if (ToolTips != NULL) { + delete ToolTips; + ToolTips = NULL; + } + MainWindow = NULL; +} + + +/// +/// Destroys the main window and stops SDL, first doing what Game_Window_Begin_Shutdown does +/// if it has not run. The renderer must have let go of the window. Calling it again does +/// nothing. +/// +void Game_Window_Close(void) +{ + Game_Window_Begin_Shutdown(); + Main_Window_Destroy(); +} diff --git a/code/gamewindow.h b/code/gamewindow.h index b611bb869..e340f9db0 100644 --- a/code/gamewindow.h +++ b/code/gamewindow.h @@ -10,6 +10,10 @@ #pragma once -void Game_Window_On_Paint(bool update_surface); -void Game_Window_On_Right_Mouse_Up(void); -void Game_Window_On_Mouse_Wheel(int delta); +struct WindowEvent; + +bool Game_Window_Open(int width, int height); +void Game_Window_Begin_Shutdown(void); +void Game_Window_Close(void); + +void Game_Window_Handle_Event(WindowEvent const & event); diff --git a/code/globals.cpp b/code/globals.cpp index 3f9fe4035..df34f36a5 100644 --- a/code/globals.cpp +++ b/code/globals.cpp @@ -473,7 +473,6 @@ bool LogDump_Print = false; // true = print the Log time Stuff /*************************************************************************** ** Win32 specific globals */ -int ReadyToQuit = 0; bool TacticalActive; diff --git a/code/globals.h b/code/globals.h index 923603285..bd3789672 100644 --- a/code/globals.h +++ b/code/globals.h @@ -305,7 +305,6 @@ extern const Coord COORD_NONE; /************************************************************ ** Win32 specific externs */ -extern int ReadyToQuit; //Are we about to exit cleanly void Memory_Error_Handler(void); //Memory error handler function extern bool TacticalActive; diff --git a/code/init.cpp b/code/init.cpp index 96b5475da..2e8807fe9 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -191,7 +191,6 @@ #include "vqoption.h" #include "wave.h" #include "waypoint.h" -#include "winfix.h" #include "winstub.h" #include "wsproto.h" #include "wspudp.h" @@ -255,8 +254,6 @@ static void Init_Threads(void); void Draw_Version_Text(Surface * surface); void Version_Dialog(void); -INT_PTR CALLBACK Rules_Choice_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - void Init_Random(void); #define ATTRACT_MODE_TIMEOUT TIMER_MINUTE // timeout for attract mode @@ -572,62 +569,6 @@ int Init_Game(int , char * []) } -/// -/// Handles the messages for the rules file choice dialog. -/// This routine lists the name of every rules file that was found, and ends the dialog -/// with the index of the one that the player settled upon. -/// -/// The dialog must be created with the vector of rules files as its parameter. -static INT_PTR CALLBACK Rules_Choice_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - char buffer[128]; - - switch (message) { - case WM_INITDIALOG: { - Center_Window_Within_Window(window); - - DynamicVectorClass * rules; - rules = (DynamicVectorClass *)lparam; - - HWND list = GetDlgItem(window, IDC_RULES_LIST); - - for (int index = 0; index < rules->Count(); index++) { - (*rules)[index]->Get_String("General", "Name", "", buffer, sizeof(buffer)); - ListBox_AddString(list, buffer); - } - ListBox_SetCurSel(list, 0); - } - break; - - case WM_HELP: - On_WM_HELP(lparam); - break; - - case WM_CONTEXTMENU: - On_WM_CONTEXTMENU(wparam); - break; - - case WM_MOVING: - return(On_WM_MOVING(window, wparam, lparam)); - - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDCANCEL: - case IDC_RULES_OK: - if (HIWORD(wparam) == BN_CLICKED) { - HWND list = GetDlgItem(window, IDC_RULES_LIST); - EndDialog(window, ListBox_GetCurSel(list)); - DestroyWindow(window); - } - break; - } - break; - } - - return(0); -} - - /// /// Reads the campaign definitions out of the battle control files. /// This routine gathers every battle file it can find, along with the expansion's own, @@ -770,44 +711,16 @@ static CampaignType Choose_Campaign(void) /// /// Loads the rules and the art control files. -/// This routine gathers every rules file it can find and, should there be more than one, -/// asks the player which of them to play with. It then loads the art, expansion, multiplayer, -/// AI and language override files, and seeds the multiplayer defaults from the rules just -/// read. The addon is chosen later, so the multiplayer expansion file cannot seed them. +/// This routine loads the configured rules file, then the art, expansion, multiplayer, AI and +/// language override files, and seeds the multiplayer defaults from the rules just read. The +/// addon is chosen later, so the multiplayer expansion file cannot seed them. /// /// bool; Were the rules loaded successfully? static bool Init_Rules(void) { - DynamicVectorClass Rules; - - bool found = false; - - for (std::string const & name : Search_Files("RULE*.INI")) { - CCFileClass file(name.c_str()); - CCINIClass * rule = new CCINIClass; - - rule->Load(file, false); - - if (stricmp(name.c_str(), DeploymentConfig.RulesFile.c_str()) == 0) { - found = true; - Rules.Add_Head(rule); - } else { - Rules.Add(rule); - } - } - - if (!found) { - CCFileClass file(DeploymentConfig.RulesFile.c_str()); - CCINIClass * rule = new CCINIClass; - rule->Load(file, false); - Rules.Add_Head(rule); - } - - assert(Rules.Count() > 0); - - if (Rules.Count() <= 0) { - return(false); - } + CCINIClass * rules = new CCINIClass; + CCFileClass rules_file(DeploymentConfig.RulesFile.c_str()); + rules->Load(rules_file, false); CCFileClass art_file(DeploymentConfig.ArtFile.c_str()); @@ -853,19 +766,7 @@ static bool Init_Rules(void) } } - if (Rules.Count() == 1) { - RuleINI = Rules[0]; - } else { - MouseCursor->Release_Mouse(); - int rules_choice = DialogBoxParam(ProgramInstance, MAKEINTRESOURCE(IDD_RULES_CHOICE), MainWindow, Rules_Choice_Dialog_Proc, (LPARAM)&Rules); - MouseCursor->Capture_Mouse(); - - if (rules_choice == -1) { - rules_choice = 0; - } - - RuleINI = Rules[rules_choice]; - } + RuleINI = rules; Rule->Color_Schemes(*RuleINI); Rule->Do_Movies(ArtINI); @@ -897,12 +798,6 @@ static bool Init_Rules(void) Rule->Addition(lang_ini); } - for (int index = 0; index < Rules.Count(); index++) { - if (Rules[index] != RuleINI) { - delete Rules[index]; - } - } - CCFileClass ai_file(DeploymentConfig.AIFile.c_str()); AIINI.Load(ai_file, true); diff --git a/code/keyboard.cpp b/code/keyboard.cpp index e80c99db4..b0c9cc78a 100644 --- a/code/keyboard.cpp +++ b/code/keyboard.cpp @@ -56,7 +56,9 @@ #include "_xmouse.h" #include "msgloop.h" +#include "sdl/sdlwindow.h" #include "vidscale.h" +#include "windowevent.hh" #include @@ -76,6 +78,19 @@ void Stop_Execution (void) } +// SDL sends no text for these keys, so the queue supplies the character each one types. +static int Control_Character(unsigned short key) +{ + switch (key & 0xFF) { + case VK_ESCAPE: return(0x1B); + case VK_RETURN: return(0x0D); + case VK_BACK: return(0x08); + case VK_TAB: return(0x09); + default: return(0); + } +} + + /*********************************************************************************************** * WWKeyboardClass::WWKeyBoardClass -- Construction for Westwood Keyboard Class * * * @@ -90,10 +105,14 @@ WWKeyboardClass::WWKeyboardClass(void) : MouseQX(0), MouseQY(0), MousePos(0,0), + TextSlot(-1), + DropText(false), + FetchedKey(0), + FetchedText(0), Head(0), Tail(0) { - memset(KeyState, '\0', sizeof(KeyState)); + memset(Text, '\0', sizeof(Text)); } @@ -113,7 +132,10 @@ unsigned short WWKeyboardClass::Buff_Get(void) { while (!Check()) {} // wait for key in buffer + int const slot = Head; unsigned short temp = Fetch_Element(); + FetchedKey = temp; + FetchedText = Text[slot]; if (Is_Mouse_Key(temp)) { MouseQX = Fetch_Element(); MouseQY = Fetch_Element(); @@ -207,7 +229,7 @@ bool WWKeyboardClass::Put(unsigned short key) /*********************************************************************************************** - * WWKeyboardClass::Put_Key_Message -- Translates and inserts wParam into Keyboard Buffer * + * WWKeyboardClass::Put_Key_Message -- Translates and inserts a key into the Keyboard Buffer * * * * INPUT: * * * @@ -218,25 +240,21 @@ bool WWKeyboardClass::Put(unsigned short key) * HISTORY: * * 10/16/1995 PWG : Created. * *=============================================================================================*/ -bool WWKeyboardClass::Put_Key_Message(unsigned short vk_key, bool release) +bool WWKeyboardClass::Put_Key_Message(unsigned short vk_key, bool release, int modifiers) { /* - ** Get the status of all of the different keyboard modifiers. Note, only pay attention - ** to numlock and caps lock if we are dealing with a key that is affected by them. Note - ** that we do not want to set the shift, ctrl and alt bits for Mouse keypresses as this - ** would be incompatible with the dos version. + ** Get the status of all of the different keyboard modifiers. Note that we do not + ** want to set the shift, ctrl and alt bits for Mouse keypresses as this would be + ** incompatible with the dos version. */ if (!Is_Mouse_Key(vk_key)) { - if (((GetKeyState(VK_SHIFT) & 0x8000) != 0) /*|| - ((GetKeyState(VK_CAPITAL) & 0x0008) != 0) || - ((GetKeyState(VK_NUMLOCK) & 0x0008) != 0)*/) { - + if ((modifiers & WINDOW_MOD_SHIFT) != 0) { vk_key |= WWKEY_SHIFT_BIT; } - if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) { + if ((modifiers & WINDOW_MOD_CTRL) != 0) { vk_key |= WWKEY_CTRL_BIT; } - if ((GetKeyState(VK_MENU) & 0x8000) != 0) { + if ((modifiers & WINDOW_MOD_ALT) != 0) { vk_key |= WWKEY_ALT_BIT; } } @@ -249,7 +267,41 @@ bool WWKeyboardClass::Put_Key_Message(unsigned short vk_key, bool release) ** Finally use the put command to enter the key into the keyboard ** system. */ - return(Put(vk_key)); + int const slot = Tail; + bool const queued = Put(vk_key); + + // The character a key press types arrives after it and belongs to it. + if (!release && !Is_Mouse_Key(vk_key)) { + TextSlot = (queued && Control_Character(vk_key) == 0) ? slot : -1; + DropText = false; + } + return(queued); +} + + +/// +/// Queues a typed character with the key press that typed it, or as a KN_TEXT entry when no +/// queued press can take it. A character typed by a held key's repeat is dropped. +/// +/// False when the character was dropped or the queue is full. +bool WWKeyboardClass::Put_Text(char32_t code) +{ + if (DropText) { + return(false); + } + + if (TextSlot >= 0 && Text[TextSlot] == 0) { + Text[TextSlot] = code; + TextSlot = -1; + return(true); + } + + int const slot = Tail; + if (!Put(KN_TEXT)) { + return(false); + } + Text[slot] = code; + return(true); } @@ -285,22 +337,14 @@ bool WWKeyboardClass::Put_Mouse_Message(unsigned short vk_key, int x, int y, boo } -/*********************************************************************************************** - * WWKeyboardClass::To_ASCII -- Convert the key value into an ASCII representation. * - * * - * This routine will convert the key code specified into an ASCII value. This takes into * - * consideration the language and keyboard mapping of the host Windows system. * - * * - * INPUT: key -- The key code to convert into ASCII. * - * * - * OUTPUT: Returns with the key converted into ASCII. If the key has no ASCII equivalent, * - * then '\0' is returned. * - * * - * WARNINGS: none * - * * - * HISTORY: * - * 09/30/1996 JLB : Created. * - *=============================================================================================*/ +/// +/// Converts a key code from the queue into the character it typed. Only the key the last Get +/// returned carries its character; Esc, Enter, Backspace and Tab give their control characters +/// whichever entry they come from. +/// +/// The key code to convert; the key the last Get returned. +/// The Unicode character the key typed, or 0 for a release or a key that typed +/// nothing. int WWKeyboardClass::To_ASCII(unsigned short key) { /* @@ -310,57 +354,11 @@ int WWKeyboardClass::To_ASCII(unsigned short key) return(0); } - /* - ** Set the KeyState buffer to reflect the shift bits stored in the key value. - */ - if (key & WWKEY_SHIFT_BIT) { - KeyState[VK_SHIFT] = 0x80; - } - if (key & WWKEY_CTRL_BIT) { - KeyState[VK_CONTROL] = 0x80; - } - if (key & WWKEY_ALT_BIT) { - KeyState[VK_MENU] = 0x80; - } - - /* - ** Ask windows to translate the key into a character. - */ - wchar_t buffer[4]; - int result; -// int result = 1; - int scancode; -// int scancode = 0; - - scancode = MapVirtualKey(key & 0xFF, 0); - result = ToUnicode((UINT)(key & 0xFF), (UINT)scancode, (PBYTE)KeyState, buffer, ARRAY_SIZE(buffer), 0); - - /* - ** Restore the KeyState buffer back to pristine condition. - */ - if (key & WWKEY_SHIFT_BIT) { - KeyState[VK_SHIFT] = 0; - } - if (key & WWKEY_CTRL_BIT) { - KeyState[VK_CONTROL] = 0; - } - if (key & WWKEY_ALT_BIT) { - KeyState[VK_MENU] = 0; - } - - if (result == 2 && IS_SURROGATE_PAIR(buffer[0], buffer[1])) { - return(0x10000 + ((buffer[0] - 0xD800) << 10) + (buffer[1] - 0xDC00)); - } - - /* - ** If Windows could not perform the translation as expected, then - ** return with a null character. - */ - if (result != 1) { - return(0); + if (key == FetchedKey && FetchedText != 0) { + return((int)FetchedText); } - return(buffer[0]); + return(Control_Character(key)); } @@ -380,13 +378,7 @@ int WWKeyboardClass::To_ASCII(unsigned short key) *=============================================================================================*/ bool WWKeyboardClass::Down(unsigned short key) { - key &= 0xFF; - - if ((key == VK_LBUTTON || key == VK_RBUTTON) && GetSystemMetrics(SM_SWAPBUTTON) == TRUE) { - key = (key != VK_LBUTTON) ? VK_LBUTTON : VK_RBUTTON; - } - - return(GetAsyncKeyState(key) != 0); + return(Main_Window_Key_Down(key & 0xFF)); } @@ -410,6 +402,9 @@ unsigned short WWKeyboardClass::Fetch_Element(void) { unsigned short val = 0; if (Head != Tail) { + if (Head == TextSlot) { + TextSlot = -1; + } val = Buffer[Head]; Head = (Head + 1) % ARRAY_SIZE(Buffer); @@ -465,6 +460,7 @@ bool WWKeyboardClass::Put_Element(unsigned short val) if (!Is_Buffer_Full()) { int temp = (Tail+1) % ARRAY_SIZE(Buffer); Buffer[Tail] = val; + Text[Tail] = 0; Tail = temp; return(true); } @@ -573,6 +569,7 @@ void WWKeyboardClass::Clear(void) */ Fill_Buffer_From_System(); Head = Tail; + TextSlot = -1; /* ** Perform a second clear to handle the rare case of the keyboard buffer being full and there @@ -580,182 +577,102 @@ void WWKeyboardClass::Clear(void) */ Fill_Buffer_From_System(); Head = Tail; + TextSlot = -1; } -/*********************************************************************************************** - * WWKeyboardClass::Message_Handler -- Process a windows message as it relates to the keyboard * - * * - * This routine will examine the Windows message specified. If the message relates to an * - * event that the keyboard input system needs to process, then it will be processed * - * accordingly. * - * * - * INPUT: window -- Handle to the window receiving the message. * - * * - * message -- The message number of this event. * - * * - * wParam -- The windows specific word parameter (meaning depends on message). * - * * - * lParam -- The windows specific long word parameter (meaning is message dependant)* - * * - * OUTPUT: bool; Was this keyboard message recognized and processed? A 'false' return value * - * means that the message should be processed normally. * - * * - * WARNINGS: none * - * * - * HISTORY: * - * 09/30/1996 JLB : Created. * - *=============================================================================================*/ -int WWKeyboardClass::Message_Handler(HWND window, UINT message, WPARAM wParam, LPARAM lParam) +/// +/// Queues what a window event means to the keyboard: key presses, key releases and typed +/// characters, and presses and releases of the left, right and middle mouse buttons with their +/// positions held inside the frame. A repeat of a held key is not queued, and neither is a +/// Scroll Lock press, which calls Stop_Execution instead. +/// +/// The window event; a mouse position must already be in frame +/// coordinates. +/// True when the event was a key, a character or one of those mouse buttons. +bool WWKeyboardClass::Handle_Window_Event(WindowEvent const & event) { - bool processed = false; - - /* - * The message router has already put mouse positions into frame coordinates, so - * there is no screen round trip to make here; the position only needs holding - * inside the frame. - */ - POINT point; - point.x = (short)LOWORD(lParam); - point.y = (short)HIWORD(lParam); + Point2D point(event.X, event.Y); Clamp_To_Game(point); - LONG x = point.x; - LONG y = point.y; /* - ** Examine the message to see if it is one that should be processed. Only keyboard and - ** pertinant mouse messages are processed. + ** Examine the event to see if it is one that should be processed. Only keyboard and + ** pertinent mouse events are processed. */ - switch (message) { + switch (event.Type) { /* - ** System key has been pressed. This is the normal keyboard event message. + ** A key has been pressed. This is the normal keyboard event. */ - case WM_SYSKEYDOWN: - case WM_KEYDOWN: - if (wParam == VK_SCROLL) { + case WINDOW_EVENT_KEY_DOWN: + if (event.VirtualKey == VK_SCROLL) { Stop_Execution(); - /* - * https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-keydown - * check the previous key-state flag. A value of 1 if the key is down before - * the message is sent, or it is zero if the key is up. - */ - } else if (!(lParam & (1 << 30))) { - Put_Key_Message((unsigned short)wParam); + } else if (!event.Repeat) { + Put_Key_Message((unsigned short)event.VirtualKey, false, event.Modifiers); + } else { + DropText = true; + TextSlot = -1; } - processed = true; - break; + return(true); /* - ** The key has been released. This is the normal key release message. + ** The key has been released. This is the normal key release event. */ - case WM_SYSKEYUP: - case WM_KEYUP: - Put_Key_Message((unsigned short)wParam, true); - processed = true; - break; + case WINDOW_EVENT_KEY_UP: + Put_Key_Message((unsigned short)event.VirtualKey, true, event.Modifiers); - /* - ** Press of the left mouse button. - */ - case WM_LBUTTONDOWN: - Put_Mouse_Message(VK_LBUTTON, x, y); - processed = true; - break; + // A repeat's character always arrives before the key's release. + DropText = false; + return(true); - /* - ** Release of the left mouse button. - */ - case WM_LBUTTONUP: - Put_Mouse_Message(VK_LBUTTON, x, y, true); - processed = true; - break; + case WINDOW_EVENT_TEXT: + Put_Text(event.Text); + return(true); - /* - ** Double click of the left mouse button. Fake this into being - ** just a rapid click of the left button twice. - */ - case WM_LBUTTONDBLCLK: - Put_Mouse_Message(VK_LBUTTON, x, y); - Put_Mouse_Message(VK_LBUTTON, x, y, true); - //Put_Mouse_Message(VK_LBUTTON, x, y); - //Put_Mouse_Message(VK_LBUTTON, x, y, true); - processed = true; + case WINDOW_EVENT_MOUSE_DOWN: + case WINDOW_EVENT_MOUSE_UP: break; /* - ** Press of the middle mouse button. + ** If the event is not pertinent to the keyboard system, + ** then do nothing. */ - case WM_MBUTTONDOWN: - Put_Mouse_Message(VK_MBUTTON, x, y); - processed = true; - break; + default: + return(false); + } - /* - ** Release of the middle mouse button. - */ - case WM_MBUTTONUP: - Put_Mouse_Message(VK_MBUTTON, x, y, true); - processed = true; + unsigned short button; + switch (event.Button) { + case WINDOW_BUTTON_LEFT: + button = VK_LBUTTON; break; - /* - ** Middle button double click gets translated into two - ** regular middle button clicks. - */ - case WM_MBUTTONDBLCLK: - Put_Mouse_Message(VK_MBUTTON, x, y); - Put_Mouse_Message(VK_MBUTTON, x, y, true); - //Put_Mouse_Message(VK_MBUTTON, x, y); - //Put_Mouse_Message(VK_MBUTTON, x, y, true); - processed = true; + case WINDOW_BUTTON_RIGHT: + button = VK_RBUTTON; break; - /* - ** Right mouse button press. - */ - case WM_RBUTTONDOWN: - Put_Mouse_Message(VK_RBUTTON, x, y); - processed = true; + case WINDOW_BUTTON_MIDDLE: + button = VK_MBUTTON; break; - /* - ** Right mouse button release. - */ - case WM_RBUTTONUP: - Put_Mouse_Message(VK_RBUTTON, x, y, true); - processed = true; - break; + default: + return(false); + } - /* - ** Translate a double click of the right button - ** into being just two regular right button clicks. - */ - case WM_RBUTTONDBLCLK: - Put_Mouse_Message(VK_RBUTTON, x, y); - Put_Mouse_Message(VK_RBUTTON, x, y, true); - //Put_Mouse_Message(VK_RBUTTON, x, y); - //Put_Mouse_Message(VK_RBUTTON, x, y, true); - processed = true; - break; + if (event.Type == WINDOW_EVENT_MOUSE_UP) { + Put_Mouse_Message(button, point.X, point.Y, true); + } else { + Put_Mouse_Message(button, point.X, point.Y); /* - ** If the message is not pertinant to the keyboard system, - ** then do nothing. + ** Double click of a mouse button. Fake this into being + ** just a rapid click of the button twice. */ - default: - break; - } - - /* - ** If this message has been processed, then pass it on to the system - ** directly. - */ - if (processed) { - DefWindowProcW(window, message, wParam, lParam); - return(true); + if (event.Clicks >= 2) { + Put_Mouse_Message(button, point.X, point.Y, true); + } } - return(false); + return(true); } diff --git a/code/keyboard.h b/code/keyboard.h index 018a4e63a..8d80678b9 100644 --- a/code/keyboard.h +++ b/code/keyboard.h @@ -34,6 +34,8 @@ #include "_xmouse.h" #include "win.h" +struct WindowEvent; + enum WWKey_Type { WWKEY_SHIFT_BIT = 0x100, WWKEY_CTRL_BIT = 0x200, @@ -61,7 +63,7 @@ class WWKeyboardClass bool Down(unsigned short key); /* Define the main hook for the message processing loop. */ - int Message_Handler(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + bool Handle_Window_Event(WindowEvent const & event); /* Define the public access variables which are used with the */ /* Keyboard Class. */ @@ -72,18 +74,25 @@ class WWKeyboardClass private: - /* - ** This is a keyboard state array that is used to aid in translating - ** KN_ keys into KA_ keys. - */ - unsigned char KeyState[256]; - /* ** This is the circular keyboard holding buffer. It holds the VK key and ** the current shift state at the time the key was added to the queue. */ unsigned short Buffer[256]; // buffer which holds actual keypresses + // The character each queued entry typed, at the entry's position in Buffer, or 0. + char32_t Text[256]; + + // The queued key press that the next typed character belongs to, or -1. + int TextSlot; + + // Set while a held key repeats; the characters it repeats are not queued. + bool DropText; + + // The entry the last Get returned, and the character it typed. + unsigned short FetchedKey; + char32_t FetchedText; + unsigned short Buff_Get(void); unsigned short Fetch_Element(void); unsigned short Peek_Element(void) const; @@ -92,8 +101,9 @@ class WWKeyboardClass bool Is_Buffer_Empty(void) const; static bool Is_Mouse_Key(unsigned short key); void Fill_Buffer_From_System(void); - bool Put_Key_Message(unsigned short vk_key, bool release = false); + bool Put_Key_Message(unsigned short vk_key, bool release = false, int modifiers = 0); bool Put_Mouse_Message(unsigned short vk_key, int x, int y, bool release = false); + bool Put_Text(char32_t code); int Available_Buffer_Room(void) const; int Noop(void) const; /// Empty routine added in a TS patch. @@ -636,6 +646,9 @@ enum KeyNumType { KN_Y = VK_Y, KN_Z = VK_Z, + // A queue entry for typed text that no key press carries; no key has this code. + KN_TEXT = 0xFF, + KN_SHIFT_BIT = WWKEY_SHIFT_BIT, KN_CTRL_BIT = WWKEY_CTRL_BIT, KN_ALT_BIT = WWKEY_ALT_BIT, diff --git a/code/language/language.h b/code/language/language.h index 7c53d06d3..90cc5f3c8 100644 --- a/code/language/language.h +++ b/code/language/language.h @@ -173,7 +173,6 @@ #define TXT_CWAIT_STAR70 145 #define TXT_CWAIT_70NUM 146 #define TXT_CWAIT_1170 147 -#define IDD_RULES_CHOICE 147 #define TXT_EASY 148 #define IDD_CAMPAIGN 148 #define TXT_NORMAL 149 @@ -894,8 +893,6 @@ #define IDC_EXCEPT_QUIT 1153 #define IDC_EXCEPT_MAINMENU 1154 #define IDC_EXCEPT_DETAILS 1156 -#define IDC_RULES_LIST 1187 -#define IDC_RULES_OK 1188 #define IDC_KEY_CATEGORY 1223 #define IDC_KEY_COMMANDS 1224 #define IDC_KEY_DESCRIPTION 1227 diff --git a/code/mainopt.cpp b/code/mainopt.cpp index 570b2dcc3..a304974bd 100644 --- a/code/mainopt.cpp +++ b/code/mainopt.cpp @@ -28,6 +28,7 @@ #include "mixfile.h" #include "msgbox.h" #include "newmenu.h" +#include "sdl/sdlwindow.h" #include "sidebar.h" #include "sounddlg.h" #include "stimer.h" @@ -35,7 +36,6 @@ #include "ui/screens/display/uidisplay.h" #include "ui/screens/mainopt/uimainopt.h" #include "video.h" -#include "wwmouse.h" #include "color.hh" @@ -151,36 +151,8 @@ bool Change_Display_Mode(int width, int height) * frame is scaled into them instead. */ if (WindowedMode && Options.WindowWidth <= 0 && Options.WindowHeight <= 0) { - RECT windowrect; - SetRect(&windowrect, 0, 0, width, height); - AdjustWindowRectEx(&windowrect, GetWindowLong(MainWindow, GWL_STYLE), FALSE, GetWindowLong(MainWindow, GWL_EXSTYLE)); - - int newwidth = windowrect.right - windowrect.left; - int newheight = windowrect.bottom - windowrect.top; - - /* - * The window grows about its middle rather than its corner, so the picture stays - * where the player was looking. - */ - RECT current; - GetWindowRect(MainWindow, ¤t); - int x = current.left + (((current.right - current.left) - newwidth) / 2); - int y = current.top + (((current.bottom - current.top) - newheight) / 2); - - /* - * Growing about the middle can push the window past the edges of the screen, and a - * title bar above the top of it cannot be grabbed to bring the window back. - */ - MONITORINFO monitor; - monitor.cbSize = sizeof(monitor); - if (GetMonitorInfo(MonitorFromWindow(MainWindow, MONITOR_DEFAULTTONEAREST), &monitor)) { - if (x + newwidth > monitor.rcWork.right) x = monitor.rcWork.right - newwidth; - if (y + newheight > monitor.rcWork.bottom) y = monitor.rcWork.bottom - newheight; - if (x < monitor.rcWork.left) x = monitor.rcWork.left; - if (y < monitor.rcWork.top) y = monitor.rcWork.top; - } - - SetWindowPos(MainWindow, NULL, x, y, newwidth, newheight, SWP_NOZORDER); + // The window grows about its middle, so the picture stays where the player was looking. + Main_Window_Resize(width, height); } Rect temp = VisibleRect; @@ -192,10 +164,6 @@ bool Change_Display_Mode(int width, int height) Allocate_Surfaces(VisibleRect, Rect(0, 0, temp.Width, VisibleRect.Height), Rect(0, 0, temp.Width, VisibleRect.Height), Rect(0, 0, SidebarClass::SIDE_WIDTH, VisibleRect.Height)); LogicalSurface = HiddenSurface; - if (MouseCursor != NULL) { - ((WWMouseClass*)MouseCursor)->Calc_Confining_Rect(); - } - Map.Set_View_Dimensions(temp); Map.Init_IO(); diff --git a/code/mapgen.cpp b/code/mapgen.cpp index 58b2705db..d632bfb78 100644 --- a/code/mapgen.cpp +++ b/code/mapgen.cpp @@ -60,7 +60,6 @@ #include "vector.h" #include "vein.h" #include "wdtnet.h" -#include "winfix.h" #include "worlddom.h" #include "ramp.hh" diff --git a/code/msgloop.cpp b/code/msgloop.cpp index 1fbf12580..1caf91397 100644 --- a/code/msgloop.cpp +++ b/code/msgloop.cpp @@ -41,8 +41,10 @@ #include "_tooltip.h" #include "_ui.h" #include "cctooltip.h" +#include "sdl/sdlwindow.h" #include "vector.h" #include "video.h" +#include "win.h" /*********************************************************************************************** @@ -68,27 +70,13 @@ void Windows_Message_Handler(void) { if (MainWindow == 0) return; - MSG msg; - /* - ** Process windows messages until the message queue is exhuasted. + ** Process windows messages until the message queue is exhausted. */ - while (PeekMessageW(&msg, NULL, 0, 0, PM_NOREMOVE)) { - if (!GetMessageW( &msg, NULL, 0, 0 )) { - return; - } - - if (ToolTips != NULL) { - ToolTips->Message_Handler(&msg); - } + Main_Window_Pump_Events(); - /* - ** If the message makes it to this point, then it must be a normal message. Process - ** it in the normal fashion. The message will appear in the window message handler - ** for the window that it was directed to. - */ - TranslateMessage(&msg); - DispatchMessageW(&msg); + if (ToolTips != NULL) { + ToolTips->Service(); } /* diff --git a/code/netdlg2.cpp b/code/netdlg2.cpp index fd1444b30..2bb2344a9 100644 --- a/code/netdlg2.cpp +++ b/code/netdlg2.cpp @@ -38,6 +38,7 @@ #include "newmenu.h" #include "rules.h" #include "scenario.h" +#include "sdl/sdlwindow.h" #include "sendfile.h" #include "srfcache.h" #include "stimer.h" @@ -397,11 +398,7 @@ bool Net2_Service_Lobby(void) Ipx.Service(); Title_Screen_Restore(); - MSG msg; - while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } + Main_Window_Pump_Events(); Call_Back(); if (_netresponse != UI_NET_NONE) { diff --git a/code/ownrdraw.cpp b/code/ownrdraw.cpp index c66e1fdae..ad862523e 100644 --- a/code/ownrdraw.cpp +++ b/code/ownrdraw.cpp @@ -24,6 +24,7 @@ #include "misc.h" #include "mixfile.h" #include "rgb.h" +#include "sdl/sdlwindow.h" #include "srfcache.h" #include "surface.h" #include "utf8.h" @@ -175,58 +176,27 @@ void OwnerDraw::Prepare_Resources(void) } -int Build_Hotkey_String(KeyNumType key, char * buffer) +/// +/// Names a hotkey for the player, as its modifiers and then its key, for example +/// "Ctrl+Shift+A". +/// +std::string Build_Hotkey_String(KeyNumType key) { - char key_name[32]; - unsigned char modifier = HIBYTE(key); - - buffer[0] = '\0'; + unsigned const code = (unsigned)key; + std::string name; - UINT lparam; - - if ((modifier & (WWKEY_ALT_BIT >> 8)) != 0) { - lparam = MapVirtualKey(VK_MENU, 0) ; - lparam = (lparam << 16); - lparam |= (1 << 0); - lparam |= (1 << 25); - GetKeyNameText(lparam, key_name, sizeof(key_name)); - strcat(buffer, key_name); - strcat(buffer, "+"); + if ((code & WWKEY_ALT_BIT) != 0) { + name += Main_Window_Key_Name(VK_MENU) + "+"; } - - if ((modifier & (WWKEY_CTRL_BIT >> 8)) != 0) { - lparam = MapVirtualKey(VK_CONTROL, 0); - lparam = (lparam << 16); - lparam |= (1 << 0); - lparam |= (1 << 25); - GetKeyNameText(lparam, key_name, sizeof(key_name)); - strcat(buffer, key_name); - strcat(buffer, "+"); + if ((code & WWKEY_CTRL_BIT) != 0) { + name += Main_Window_Key_Name(VK_CONTROL) + "+"; } - - if ((modifier & (WWKEY_SHIFT_BIT >> 8)) != 0) { - lparam = MapVirtualKey(VK_SHIFT, 0); - lparam = (lparam << 16); - lparam |= (1 << 0); - lparam |= (1 << 25); - GetKeyNameText(lparam, key_name, sizeof(key_name)); - strcat(buffer, key_name); - strcat(buffer, "+"); + if ((code & WWKEY_SHIFT_BIT) != 0) { + name += Main_Window_Key_Name(VK_SHIFT) + "+"; } - lparam = MapVirtualKey(key & 0xFF, 0); - lparam = (lparam << 16); - lparam |= (1 << 0); - lparam |= (1 << 25); - - if ((modifier & (WWKEY_RLS_BIT >> 8)) != 0) { - lparam |= (1 << 24); - } - - GetKeyNameText(lparam, key_name, sizeof(key_name)); - strcat(buffer, key_name); - - return(0); + name += Main_Window_Key_Name(code & 0xFF); + return(name); } diff --git a/code/ownrdraw.h b/code/ownrdraw.h index 0965fc3a8..93564d0da 100644 --- a/code/ownrdraw.h +++ b/code/ownrdraw.h @@ -13,6 +13,8 @@ #include "surface.h" #include "win.h" +#include + namespace OwnerDraw { @@ -43,7 +45,7 @@ int OD_Draw_Text_Remap(Surface & surface, const char * string, Rect const & rect int OD_Draw_Text(COLORREF color, HFONT font, Rect const & rect, const char * text, int len, int x_alignment, int y_alignment, Surface * surface); HFONT WS_Get_Font(HDC hdc, const char * face_name, int decipt_width, int decipt_height, int attributes); -int Build_Hotkey_String(KeyNumType key, char * buffer); +std::string Build_Hotkey_String(KeyNumType key); extern COLORREF ODColorText; extern COLORREF ODColorTextDim; diff --git a/code/scroll.cpp b/code/scroll.cpp index fc867fa10..d4d3c7c2c 100644 --- a/code/scroll.cpp +++ b/code/scroll.cpp @@ -57,11 +57,13 @@ #include "overtype.h" #include "rules.h" #include "savestream.h" +#include "sdl/sdlwindow.h" #include "suprtype.h" #include "surface.h" #include "tactical.h" #include "vidscale.h" #include "waypoint.h" +#include "windowevent.hh" #include "special.hh" @@ -598,13 +600,11 @@ bool ScrollClass::Is_Scrolling(void) const /// -/// Handles the mouse messages that drive the tactical map. -/// This routine is called from the main window procedure. It turns raw button presses and -/// releases into tactical map actions, and takes and releases the mouse capture so that a -/// drag survives the cursor leaving the window. Messages arriving while the game is not -/// running, or while input is being ignored, are quietly dropped. +/// Turns mouse button events, with positions in frame coordinates, into tactical map actions. +/// A drag keeps the mouse capture so it survives the cursor leaving the window. Events are +/// dropped while the map is inactive or input is ignored. /// -void ScrollClass::Message_Handler(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) +void ScrollClass::Handle_Window_Event(WindowEvent const & event) { if (!TacticalActive) { return; @@ -630,7 +630,7 @@ void ScrollClass::Message_Handler(HWND hwnd, UINT message, WPARAM wParam, LPARAM return; } - if (IgnoreInput && message != WM_CAPTURECHANGED) { + if (IgnoreInput && event.Type != WINDOW_EVENT_CAPTURE_LOST) { return; } @@ -641,67 +641,61 @@ void ScrollClass::Message_Handler(HWND hwnd, UINT message, WPARAM wParam, LPARAM bool shadow; // is the cell in shadow or not Point2D point; /// click position relative to the tactical view - switch (message) { + // The map does not act on the second press of a double click. + bool const press = (event.Type == WINDOW_EVENT_MOUSE_DOWN && event.Clicks == 1); + bool const release = (event.Type == WINDOW_EVENT_MOUSE_UP); - case WM_LBUTTONDOWN: - if (IsMouseDown == false) { + if (event.Type == WINDOW_EVENT_CAPTURE_LOST) { + if (IsMouseDown == true) { + Abort_Drag_Select(); + } - POINTS pts = MAKEPOINTS(lParam); - point.X = pts.x - TacticalRect.X; - point.Y = pts.y - TacticalRect.Y; + } else if (press && event.Button == WINDOW_BUTTON_LEFT) { + if (IsMouseDown == false) { - if (Resolve_Point(point, cell, coord, object, fog, shadow)) { - Map.Mouse_Left_Up(cell, shadow, object, What_Action(cell, object, true)); - Map.Mouse_Left_Press(point); - SetCapture(MainWindow); - IsMouseDown = true; - } + point.X = event.X - TacticalRect.X; + point.Y = event.Y - TacticalRect.Y; + + if (Resolve_Point(point, cell, coord, object, fog, shadow)) { + Map.Mouse_Left_Up(cell, shadow, object, What_Action(cell, object, true)); + Map.Mouse_Left_Press(point); + Main_Window_Capture_Mouse(true); + IsMouseDown = true; } - break; + } - case WM_LBUTTONUP: - if (IsMouseDown == true) { + } else if (release && event.Button == WINDOW_BUTTON_LEFT) { + if (IsMouseDown == true) { - POINTS pts = MAKEPOINTS(lParam); - point.X = pts.x - TacticalRect.X; - point.Y = pts.y - TacticalRect.Y; + point.X = event.X - TacticalRect.X; + point.Y = event.Y - TacticalRect.Y; - Resolve_Point(point, cell, coord, object, fog, shadow); - Map.Mouse_Left_Release(coord, cell, object, What_Action(cell, object, false)); - IsMouseDown = false; - ReleaseCapture(); - } - break; + Resolve_Point(point, cell, coord, object, fog, shadow); + Map.Mouse_Left_Release(coord, cell, object, What_Action(cell, object, false)); + IsMouseDown = false; + Main_Window_Capture_Mouse(false); + } - case WM_RBUTTONDOWN: - if (IsMouseDown == false) { + } else if (press && event.Button == WINDOW_BUTTON_RIGHT) { + if (IsMouseDown == false) { - POINTS pts = MAKEPOINTS(lParam); - point.X = pts.x - TacticalRect.X; - point.Y = pts.y - TacticalRect.Y; + point.X = event.X - TacticalRect.X; + point.Y = event.Y - TacticalRect.Y; - if (Resolve_Point(point, cell, coord, object, fog, shadow)) { - Map.Mouse_Right_Press(point); - SetCapture(MainWindow); - IsMouseDown = true; - } - } - break; - - case WM_RBUTTONUP: - if (IsMouseDown == true) { - Map.Mouse_Right_Release(point); - BASECLASS::Abort_Drag_Select(); - IsMouseDown = false; - ReleaseCapture(); + if (Resolve_Point(point, cell, coord, object, fog, shadow)) { + Map.Mouse_Right_Press(point); + Main_Window_Capture_Mouse(true); + IsMouseDown = true; } - break; + } - case WM_CAPTURECHANGED: - if (((HWND &)lParam) != MainWindow && IsMouseDown == true) { - Abort_Drag_Select(); - } - break; + } else if (release && event.Button == WINDOW_BUTTON_RIGHT) { + if (IsMouseDown == true) { + Map.Mouse_Right_Release(point); + BASECLASS::Abort_Drag_Select(); + IsMouseDown = false; + Main_Window_Capture_Mouse(false); + } } } @@ -791,11 +785,9 @@ void ScrollClass::Scroll_Coast(Point2D const & point) distx = abs(int((double)posx * (12.0 / (double)(Options.ScrollRate + 1)))); disty = abs(int((double)posy * (12.0 / (double)(Options.ScrollRate + 1)))); if (distx + disty > 0) { - POINT pt; - pt.x = RightPressPoint.X + TacticalRect.X; - pt.y = RightPressPoint.Y + TacticalRect.Y; - Game_Point_To_Screen(pt); - SetCursorPos(pt.x, pt.y); + Point2D pt(RightPressPoint.X + TacticalRect.X, RightPressPoint.Y + TacticalRect.Y); + Game_Point_To_Window(pt); + Main_Window_Warp_Cursor(pt.X, pt.Y); } break; @@ -805,11 +797,9 @@ void ScrollClass::Scroll_Coast(Point2D const & point) distx = abs(int((double)posx * (12.0 / (double)(Options.ScrollRate + 1)))); disty = abs(int((double)posy * (12.0 / (double)(Options.ScrollRate + 1)))); if (distx + disty > 0) { - POINT pt; - pt.x = RightPressPoint.X + TacticalRect.X; - pt.y = RightPressPoint.Y + TacticalRect.Y; - Game_Point_To_Screen(pt); - SetCursorPos(pt.x, pt.y); + Point2D pt(RightPressPoint.X + TacticalRect.X, RightPressPoint.Y + TacticalRect.Y); + Game_Point_To_Window(pt); + Main_Window_Warp_Cursor(pt.X, pt.Y); } break; } @@ -902,8 +892,5 @@ void ScrollClass::Abort_Drag_Select(void) { BASECLASS::Abort_Drag_Select(); IsMouseDown = false; - HWND hwnd = GetCapture(); - if (hwnd == MainWindow) { - ReleaseCapture(); - } + Main_Window_Capture_Mouse(false); } diff --git a/code/scroll.h b/code/scroll.h index 796ed3297..c8353be01 100644 --- a/code/scroll.h +++ b/code/scroll.h @@ -34,6 +34,8 @@ #include "tab.h" +struct WindowEvent; + class ScrollClass: public TabClass { @@ -102,7 +104,7 @@ class ScrollClass: public TabClass ActionType What_Action(Cell const & cell, ObjectClass * object, bool check_fog); - void Message_Handler(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + void Handle_Window_Event(WindowEvent const & event); protected: virtual void Mouse_Right_Press(Point2D const & point = Point2D()) override; diff --git a/code/sdl/sdlevents.cpp b/code/sdl/sdlevents.cpp new file mode 100644 index 000000000..341139bef --- /dev/null +++ b/code/sdl/sdlevents.cpp @@ -0,0 +1,166 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "sdl/sdlevents.h" + +#include "sdl/sdlkeys.h" +#include "utf8.h" +#include "win.h" + +#include + +#include + + +namespace +{ + +int Pixel(float coordinate, float pixeldensity) +{ + return((int)std::floor(coordinate * pixeldensity)); +} + + +bool Mouse_Button(Uint8 button, WindowMouseButton & result) +{ + switch (button) { + case SDL_BUTTON_LEFT: result = WINDOW_BUTTON_LEFT; return(true); + case SDL_BUTTON_RIGHT: result = WINDOW_BUTTON_RIGHT; return(true); + case SDL_BUTTON_MIDDLE: result = WINDOW_BUTTON_MIDDLE; return(true); + case SDL_BUTTON_X1: result = WINDOW_BUTTON_X1; return(true); + case SDL_BUTTON_X2: result = WINDOW_BUTTON_X2; return(true); + default: return(false); + } +} + + +WindowEvent Window_Event(WindowEventType type) +{ + WindowEvent event; + event.Type = type; + return(event); +} + +} + + +void Window_Events_From_SDL(SDL_Event const & sdlevent, float pixeldensity, int modifiers, std::vector & events) +{ + WindowEvent event; + + switch (sdlevent.type) { + case SDL_EVENT_MOUSE_MOTION: + event.Type = WINDOW_EVENT_MOUSE_MOVE; + event.Modifiers = modifiers; + event.X = Pixel(sdlevent.motion.x, pixeldensity); + event.Y = Pixel(sdlevent.motion.y, pixeldensity); + events.push_back(event); + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + if (!Mouse_Button(sdlevent.button.button, event.Button)) { + break; + } + event.Type = sdlevent.button.down ? WINDOW_EVENT_MOUSE_DOWN : WINDOW_EVENT_MOUSE_UP; + event.Modifiers = modifiers; + event.X = Pixel(sdlevent.button.x, pixeldensity); + event.Y = Pixel(sdlevent.button.y, pixeldensity); + + // Windows reports every second press of a run of clicks as a double click. + if (sdlevent.button.down && sdlevent.button.clicks != 0 && sdlevent.button.clicks % 2 == 0) { + event.Clicks = 2; + } + events.push_back(event); + break; + + case SDL_EVENT_MOUSE_WHEEL: + { + float direction = (sdlevent.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) ? -1.0f : 1.0f; + event.Type = WINDOW_EVENT_MOUSE_WHEEL; + event.Modifiers = modifiers; + event.X = Pixel(sdlevent.wheel.mouse_x, pixeldensity); + event.Y = Pixel(sdlevent.wheel.mouse_y, pixeldensity); + if (sdlevent.wheel.y != 0.0f) { + event.Wheel = sdlevent.wheel.y * direction; + event.Horizontal = false; + events.push_back(event); + } + if (sdlevent.wheel.x != 0.0f) { + event.Wheel = sdlevent.wheel.x * direction; + event.Horizontal = true; + events.push_back(event); + } + break; + } + + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + event.VirtualKey = Virtual_Key_From_SDL(sdlevent.key.scancode, sdlevent.key.key, sdlevent.key.mod, sdlevent.key.raw); + if (event.VirtualKey == 0) { + break; + } + event.Type = sdlevent.key.down ? WINDOW_EVENT_KEY_DOWN : WINDOW_EVENT_KEY_UP; + event.Modifiers = modifiers; + event.Repeat = sdlevent.key.down && sdlevent.key.repeat; + + // Windows treats keys pressed with Alt, but not with AltGr, as system keys, and F10. + event.System = ((event.Modifiers & WINDOW_MOD_ALT) != 0 && (event.Modifiers & WINDOW_MOD_CTRL) == 0) || event.VirtualKey == VK_F10; + events.push_back(event); + break; + + case SDL_EVENT_TEXT_INPUT: + if (sdlevent.text.text != NULL) { + char const * text = sdlevent.text.text; + event.Type = WINDOW_EVENT_TEXT; + while (*text != '\0') { + event.Text = UTF8::Decode(text); + events.push_back(event); + } + } + break; + + case SDL_EVENT_WINDOW_FOCUS_GAINED: + events.push_back(Window_Event(WINDOW_EVENT_FOCUS_GAINED)); + break; + + // Windows took the capture away from a window that lost the focus, which the game + // relies on to end a drag. + case SDL_EVENT_WINDOW_FOCUS_LOST: + events.push_back(Window_Event(WINDOW_EVENT_CAPTURE_LOST)); + events.push_back(Window_Event(WINDOW_EVENT_FOCUS_LOST)); + break; + + case SDL_EVENT_WINDOW_EXPOSED: + events.push_back(Window_Event(WINDOW_EVENT_EXPOSED)); + break; + + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + if (sdlevent.window.data1 > 0 && sdlevent.window.data2 > 0) { + event.Type = WINDOW_EVENT_RESIZED; + event.Width = sdlevent.window.data1; + event.Height = sdlevent.window.data2; + events.push_back(event); + } + break; + + case SDL_EVENT_WINDOW_DISPLAY_CHANGED: + case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED: + events.push_back(Window_Event(WINDOW_EVENT_DISPLAY_CHANGED)); + break; + + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + case SDL_EVENT_QUIT: + events.push_back(Window_Event(WINDOW_EVENT_CLOSE_REQUESTED)); + break; + + default: + break; + } +} diff --git a/code/sdl/sdlevents.h b/code/sdl/sdlevents.h new file mode 100644 index 000000000..4568b603c --- /dev/null +++ b/code/sdl/sdlevents.h @@ -0,0 +1,22 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include "windowevent.hh" + +#include + +union SDL_Event; + + +// Appends the window events an SDL event stands for, which is none for an event the game +// does not use. Positions are scaled by the window's pixel density into client pixels, and +// key and mouse events carry the given WINDOW_MOD_* modifiers. +void Window_Events_From_SDL(SDL_Event const & sdlevent, float pixeldensity, int modifiers, std::vector & events); diff --git a/code/sdl/sdlinput.cpp b/code/sdl/sdlinput.cpp new file mode 100644 index 000000000..943c0ff82 --- /dev/null +++ b/code/sdl/sdlinput.cpp @@ -0,0 +1,284 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "sdl/sdlinput.h" + +#include "sdl/sdlkeys.h" +#include "win.h" +#include "windowevent.hh" + +#include + +#include + + +namespace +{ + +// Windows also answers for each side of a modifier. +int Side_Key(int scancode) +{ + switch (scancode) { + case SDL_SCANCODE_LSHIFT: return(VK_LSHIFT); + case SDL_SCANCODE_RSHIFT: return(VK_RSHIFT); + case SDL_SCANCODE_LCTRL: return(VK_LCONTROL); + case SDL_SCANCODE_RCTRL: return(VK_RCONTROL); + case SDL_SCANCODE_LALT: return(VK_LMENU); + case SDL_SCANCODE_RALT: return(VK_RMENU); + default: return(0); + } +} + + +int Generic_Key(int sidekey) +{ + switch (sidekey) { + case VK_LSHIFT: + case VK_RSHIFT: return(VK_SHIFT); + case VK_LCONTROL: + case VK_RCONTROL: return(VK_CONTROL); + case VK_LMENU: + case VK_RMENU: return(VK_MENU); + default: return(0); + } +} + + +// Mouse buttons are read live, and a modifier is held by its sides. +bool Holdable_Key(int virtualkey) +{ + switch (virtualkey) { + case VK_LBUTTON: + case VK_RBUTTON: + case VK_MBUTTON: + case VK_XBUTTON1: + case VK_XBUTTON2: + case VK_SHIFT: + case VK_CONTROL: + case VK_MENU: + return(false); + + default: + return(virtualkey > 0 && virtualkey < 255); + } +} + + +bool Windows_Holds(int virtualkey) +{ + return((GetKeyState(virtualkey) & 0x8000) != 0); +} + +} + + +SDLInputStateClass::SDLInputStateClass(void) +{ + Reset(); +} + + +void SDLInputStateClass::Reset(void) +{ + std::memset(PressedAs, 0, sizeof(PressedAs)); + std::memset(Unreported, 0, sizeof(Unreported)); + UnreportedCount = 0; + RightAltPresses.clear(); + AltGr = false; + KeyModifiers = SDL_GetModState(); + Rebuild_Held_Keys(); +} + + +void SDLInputStateClass::Note_Queued_Key(SDL_KeyboardEvent const & key) +{ + if (key.down && key.scancode == SDL_SCANCODE_RALT && !key.repeat) { + bool const altgr = Windows_Holds(VK_LCONTROL) && !SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] && !Unreported[VK_LCONTROL]; + RightAltPresses.push_back({ key.timestamp, altgr }); + } +} + + +bool SDLInputStateClass::Track_Key(SDL_KeyboardEvent const & key) +{ + KeyModifiers = key.mod; + if (key.down && key.repeat) { + return(false); + } + + int const virtualkey = key.down ? Virtual_Key_From_SDL(key.scancode, key.key, key.mod, key.raw) : 0; + bool const continued = key.down && (Unreported_Key(Side_Key(key.scancode)) || Unreported_Key(virtualkey)); + Report_Key(Side_Key(key.scancode)); + Report_Key(virtualkey); + + Press_Key(key.scancode, virtualkey, key.down); + if (key.scancode == SDL_SCANCODE_RALT) { + AltGr = key.down && Pressed_As_AltGr(key.timestamp); + } + Rebuild_Held_Keys(); + return(continued); +} + + +void SDLInputStateClass::Focus_Gained(void) +{ + SDL_Keymod const locks = SDL_KMOD_CAPS | SDL_KMOD_NUM | SDL_KMOD_SCROLL; + KeyModifiers = (SDL_Keymod)((KeyModifiers & ~locks) | (SDL_GetModState() & locks)); + Hold_Windows_Keys(); +} + + +void SDLInputStateClass::Focus_Lost(void) +{ + Release_Unreported_Keys(true); +} + + +void SDLInputStateClass::Hold_Windows_Keys(void) +{ + for (int virtualkey = 1; virtualkey < 255; virtualkey++) { + if (Holdable_Key(virtualkey) && !HeldKeys[virtualkey] && Windows_Holds(virtualkey)) { + Unreported[virtualkey] = true; + UnreportedCount++; + } + } + Rebuild_Held_Keys(); +} + + +void SDLInputStateClass::Drop_Released_Keys(void) +{ + Release_Unreported_Keys(false); +} + + +int SDLInputStateClass::Modifiers(void) const +{ + int modifiers = 0; + if (HeldKeys[VK_SHIFT]) { + modifiers |= WINDOW_MOD_SHIFT; + } + if (HeldKeys[VK_CONTROL]) { + modifiers |= WINDOW_MOD_CTRL; + } + if (HeldKeys[VK_MENU]) { + modifiers |= WINDOW_MOD_ALT; + } + if ((KeyModifiers & SDL_KMOD_CAPS) != 0) { + modifiers |= WINDOW_MOD_CAPS; + } + if ((KeyModifiers & SDL_KMOD_NUM) != 0) { + modifiers |= WINDOW_MOD_NUM; + } + return(modifiers); +} + + +bool SDLInputStateClass::Key_Down(int virtualkey) +{ + if (virtualkey <= 0 || virtualkey >= 256) { + return(false); + } + + Release_Unreported_Keys(false); + return(HeldKeys[virtualkey]); +} + + +bool SDLInputStateClass::Key_Toggled(int virtualkey) const +{ + switch (virtualkey) { + case VK_CAPITAL: return((KeyModifiers & SDL_KMOD_CAPS) != 0); + case VK_NUMLOCK: return((KeyModifiers & SDL_KMOD_NUM) != 0); + case VK_SCROLL: return((KeyModifiers & SDL_KMOD_SCROLL) != 0); + default: return(false); + } +} + + +void SDLInputStateClass::Rebuild_Held_Keys(void) +{ + std::memset(HeldKeys, 0, sizeof(HeldKeys)); + for (int scancode = 0; scancode < SDL_SCANCODE_COUNT; scancode++) { + if (PressedAs[scancode] != 0) { + HeldKeys[PressedAs[scancode]] = true; + HeldKeys[Side_Key(scancode)] = true; + } + } + for (int virtualkey = 1; virtualkey < 255 && UnreportedCount > 0; virtualkey++) { + if (Unreported[virtualkey]) { + HeldKeys[virtualkey] = true; + HeldKeys[Generic_Key(virtualkey)] = true; + } + } + HeldKeys[0] = false; + + // Windows holds Left Ctrl down for as long as AltGr is held. + if (AltGr) { + HeldKeys[VK_CONTROL] = true; + HeldKeys[VK_LCONTROL] = true; + } +} + + +void SDLInputStateClass::Press_Key(SDL_Scancode scancode, int virtualkey, bool down) +{ + if (scancode <= SDL_SCANCODE_UNKNOWN || scancode >= SDL_SCANCODE_COUNT) { + return; + } + PressedAs[scancode] = (down && virtualkey > 0 && virtualkey < 256) ? (unsigned char)virtualkey : 0; +} + + +bool SDLInputStateClass::Unreported_Key(int virtualkey) const +{ + return(virtualkey > 0 && virtualkey < 256 && Unreported[virtualkey]); +} + + +void SDLInputStateClass::Report_Key(int virtualkey) +{ + if (Unreported_Key(virtualkey)) { + Unreported[virtualkey] = false; + UnreportedCount--; + } +} + + +// Takes what Note_Queued_Key noted when SDL queued this Right Alt press. +bool SDLInputStateClass::Pressed_As_AltGr(Uint64 timestamp) +{ + for (size_t index = 0; index < RightAltPresses.size(); index++) { + if (RightAltPresses[index].Timestamp == timestamp) { + bool const altgr = RightAltPresses[index].AltGr; + RightAltPresses.erase(RightAltPresses.begin(), RightAltPresses.begin() + index + 1); + return(altgr); + } + } + return(false); +} + + +void SDLInputStateClass::Release_Unreported_Keys(bool all) +{ + if (UnreportedCount == 0) { + return; + } + + bool released = false; + for (int virtualkey = 1; virtualkey < 255; virtualkey++) { + if (Unreported[virtualkey] && (all || !Windows_Holds(virtualkey))) { + Report_Key(virtualkey); + released = true; + } + } + if (released) { + Rebuild_Held_Keys(); + } +} diff --git a/code/sdl/sdlinput.h b/code/sdl/sdlinput.h new file mode 100644 index 000000000..8c5b016f5 --- /dev/null +++ b/code/sdl/sdlinput.h @@ -0,0 +1,81 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include + +#include + + +// The keys held and the lock states as of the input event being dispatched, by the Windows +// virtual-key codes the game reads. +class SDLInputStateClass +{ + public: + SDLInputStateClass(void); + + // SDL sees no key before its window has the focus, so only the lock keys can start set. + void Reset(void); + + // Runs as SDL queues a key event, while Windows still shows the Left Ctrl it presses + // with AltGr. + void Note_Queued_Key(SDL_KeyboardEvent const & key); + + // Returns true for a press of a key held since before SDL's last keyboard reset, which + // Windows reports as a repeat. + bool Track_Key(SDL_KeyboardEvent const & key); + + // SDL resets the keyboard on a focus loss and when a window drag, a resize or the system + // menu starts; these hold again the keys Windows still holds. + void Focus_Gained(void); + void Hold_Windows_Keys(void); + + void Focus_Lost(void); + + // Drops the keys held through a keyboard reset that Windows has since released, since SDL + // drops their release. + void Drop_Released_Keys(void); + + // The WINDOW_MOD_* modifiers held and locks on. + int Modifiers(void) const; + + bool Key_Down(int virtualkey); + bool Key_Toggled(int virtualkey) const; + + private: + struct RightAltPress + { + Uint64 Timestamp; + bool AltGr; + }; + + // Each held scancode keeps the code it was pressed as, so its release clears that code. + bool HeldKeys[256]; + unsigned char PressedAs[SDL_SCANCODE_COUNT]; + SDL_Keymod KeyModifiers; + + // Right Alt presses queued but not yet handled, and whether each was AltGr. + std::deque RightAltPresses; + + // Set while the Right Alt held was pressed as AltGr. + bool AltGr; + + // Set, by virtual-key code and by side for a modifier, for a key held through a keyboard + // reset, which is read from Windows until SDL reports the key. + bool Unreported[256]; + int UnreportedCount; + + void Rebuild_Held_Keys(void); + void Press_Key(SDL_Scancode scancode, int virtualkey, bool down); + bool Unreported_Key(int virtualkey) const; + void Report_Key(int virtualkey); + bool Pressed_As_AltGr(Uint64 timestamp); + void Release_Unreported_Keys(bool all); +}; diff --git a/code/sdl/sdlkeys.cpp b/code/sdl/sdlkeys.cpp new file mode 100644 index 000000000..eba5c86b3 --- /dev/null +++ b/code/sdl/sdlkeys.cpp @@ -0,0 +1,245 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "sdl/sdlkeys.h" + +#include "win.h" + +#include +#include +#include + + +namespace +{ + +// Windows keeps the keypad's own codes only while Num Lock is on and Shift is up. +int Keypad_Key(SDL_Scancode scancode, SDL_Keymod modifiers) +{ + bool const numbers = (modifiers & SDL_KMOD_NUM) != 0 && (modifiers & SDL_KMOD_SHIFT) == 0; + + switch (scancode) { + case SDL_SCANCODE_KP_0: return(numbers ? VK_NUMPAD0 : VK_INSERT); + case SDL_SCANCODE_KP_1: return(numbers ? VK_NUMPAD1 : VK_END); + case SDL_SCANCODE_KP_2: return(numbers ? VK_NUMPAD2 : VK_DOWN); + case SDL_SCANCODE_KP_3: return(numbers ? VK_NUMPAD3 : VK_NEXT); + case SDL_SCANCODE_KP_4: return(numbers ? VK_NUMPAD4 : VK_LEFT); + case SDL_SCANCODE_KP_5: return(numbers ? VK_NUMPAD5 : VK_CLEAR); + case SDL_SCANCODE_KP_6: return(numbers ? VK_NUMPAD6 : VK_RIGHT); + case SDL_SCANCODE_KP_7: return(numbers ? VK_NUMPAD7 : VK_HOME); + case SDL_SCANCODE_KP_8: return(numbers ? VK_NUMPAD8 : VK_UP); + case SDL_SCANCODE_KP_9: return(numbers ? VK_NUMPAD9 : VK_PRIOR); + case SDL_SCANCODE_KP_PERIOD: return(numbers ? VK_DECIMAL : VK_DELETE); + case SDL_SCANCODE_KP_DIVIDE: return(VK_DIVIDE); + case SDL_SCANCODE_KP_MULTIPLY: return(VK_MULTIPLY); + case SDL_SCANCODE_KP_MINUS: return(VK_SUBTRACT); + case SDL_SCANCODE_KP_PLUS: return(VK_ADD); + case SDL_SCANCODE_KP_ENTER: return(VK_RETURN); + default: return(-1); + } +} + + +// Without a scan code, a letter or digit key takes the code of what the layout prints on it, +// and the comma, period, minus and plus keys the code of their character. +int Layout_Key(SDL_Keycode keycode) +{ + if (keycode >= 'a' && keycode <= 'z') { + return((int)(keycode - 'a') + 'A'); + } + if (keycode >= '0' && keycode <= '9') { + return((int)keycode); + } + + switch (keycode) { + case ',': return(VK_OEM_COMMA); + case '.': return(VK_OEM_PERIOD); + case '-': return(VK_OEM_MINUS); + case '=': + case '+': return(VK_OEM_PLUS); + default: return(0); + } +} + + +// Windows gives these keys the codes of the active layout; every other key has a fixed code. +bool Layout_Position(SDL_Scancode scancode) +{ + return((scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_0) + || (scancode >= SDL_SCANCODE_MINUS && scancode <= SDL_SCANCODE_SLASH) + || scancode == SDL_SCANCODE_NONUSBACKSLASH); +} + + +HKL Layout_Or_Current(HKL layout) +{ + return((layout != NULL) ? layout : GetKeyboardLayout(0)); +} + + +// The remaining keys take the code Windows gives the same position on a US layout. +int Position_Key(SDL_Scancode scancode) +{ + if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) { + return((int)(scancode - SDL_SCANCODE_A) + 'A'); + } + if (scancode >= SDL_SCANCODE_1 && scancode <= SDL_SCANCODE_9) { + return((int)(scancode - SDL_SCANCODE_1) + '1'); + } + if (scancode >= SDL_SCANCODE_F1 && scancode <= SDL_SCANCODE_F12) { + return((int)(scancode - SDL_SCANCODE_F1) + VK_F1); + } + if (scancode >= SDL_SCANCODE_F13 && scancode <= SDL_SCANCODE_F24) { + return((int)(scancode - SDL_SCANCODE_F13) + VK_F13); + } + + switch (scancode) { + case SDL_SCANCODE_0: return('0'); + case SDL_SCANCODE_RETURN: return(VK_RETURN); + case SDL_SCANCODE_ESCAPE: return(VK_ESCAPE); + case SDL_SCANCODE_BACKSPACE: return(VK_BACK); + case SDL_SCANCODE_TAB: return(VK_TAB); + case SDL_SCANCODE_SPACE: return(VK_SPACE); + case SDL_SCANCODE_MINUS: return(VK_OEM_MINUS); + case SDL_SCANCODE_EQUALS: return(VK_OEM_PLUS); + case SDL_SCANCODE_LEFTBRACKET: return(VK_OEM_4); + case SDL_SCANCODE_RIGHTBRACKET: return(VK_OEM_6); + case SDL_SCANCODE_BACKSLASH: return(VK_OEM_5); + case SDL_SCANCODE_NONUSHASH: return(VK_OEM_5); + case SDL_SCANCODE_SEMICOLON: return(VK_OEM_1); + case SDL_SCANCODE_APOSTROPHE: return(VK_OEM_7); + case SDL_SCANCODE_GRAVE: return(VK_OEM_3); + case SDL_SCANCODE_COMMA: return(VK_OEM_COMMA); + case SDL_SCANCODE_PERIOD: return(VK_OEM_PERIOD); + case SDL_SCANCODE_SLASH: return(VK_OEM_2); + case SDL_SCANCODE_NONUSBACKSLASH: return(VK_OEM_102); + case SDL_SCANCODE_CAPSLOCK: return(VK_CAPITAL); + case SDL_SCANCODE_PRINTSCREEN: return(VK_SNAPSHOT); + case SDL_SCANCODE_SCROLLLOCK: return(VK_SCROLL); + case SDL_SCANCODE_PAUSE: return(VK_PAUSE); + case SDL_SCANCODE_INSERT: return(VK_INSERT); + case SDL_SCANCODE_HOME: return(VK_HOME); + case SDL_SCANCODE_PAGEUP: return(VK_PRIOR); + case SDL_SCANCODE_DELETE: return(VK_DELETE); + case SDL_SCANCODE_END: return(VK_END); + case SDL_SCANCODE_PAGEDOWN: return(VK_NEXT); + case SDL_SCANCODE_RIGHT: return(VK_RIGHT); + case SDL_SCANCODE_LEFT: return(VK_LEFT); + case SDL_SCANCODE_DOWN: return(VK_DOWN); + case SDL_SCANCODE_UP: return(VK_UP); + case SDL_SCANCODE_NUMLOCKCLEAR: return(VK_NUMLOCK); + case SDL_SCANCODE_APPLICATION: return(VK_APPS); + case SDL_SCANCODE_HELP: return(VK_HELP); + case SDL_SCANCODE_SELECT: return(VK_SELECT); + case SDL_SCANCODE_EXECUTE: return(VK_EXECUTE); + case SDL_SCANCODE_CLEAR: return(VK_CLEAR); + case SDL_SCANCODE_SLEEP: return(VK_SLEEP); + case SDL_SCANCODE_MUTE: return(VK_VOLUME_MUTE); + case SDL_SCANCODE_VOLUMEUP: return(VK_VOLUME_UP); + case SDL_SCANCODE_VOLUMEDOWN: return(VK_VOLUME_DOWN); + case SDL_SCANCODE_MEDIA_NEXT_TRACK: return(VK_MEDIA_NEXT_TRACK); + case SDL_SCANCODE_MEDIA_PREVIOUS_TRACK: return(VK_MEDIA_PREV_TRACK); + case SDL_SCANCODE_MEDIA_STOP: return(VK_MEDIA_STOP); + case SDL_SCANCODE_MEDIA_PLAY_PAUSE: return(VK_MEDIA_PLAY_PAUSE); + case SDL_SCANCODE_LCTRL: + case SDL_SCANCODE_RCTRL: return(VK_CONTROL); + case SDL_SCANCODE_LSHIFT: + case SDL_SCANCODE_RSHIFT: return(VK_SHIFT); + case SDL_SCANCODE_LALT: + case SDL_SCANCODE_RALT: return(VK_MENU); + case SDL_SCANCODE_LGUI: return(VK_LWIN); + case SDL_SCANCODE_RGUI: return(VK_RWIN); + default: return(0); + } +} + +} + + +int Virtual_Key_From_SDL(SDL_Scancode scancode, SDL_Keycode keycode, SDL_Keymod modifiers, Uint16 raw, HKL layout) +{ + int key = Keypad_Key(scancode, modifiers); + if (key >= 0) { + return(key); + } + + if (Layout_Position(scancode)) { + // Some layouts give an unused key 0xFF, which is no key. + if (raw != 0) { + key = (int)MapVirtualKeyExW(raw, MAPVK_VSC_TO_VK_EX, Layout_Or_Current(layout)); + if (key > 0 && key < 0xFF) { + return(key); + } + } + + key = Layout_Key(keycode); + if (key != 0) { + return(key); + } + } + + return(Position_Key(scancode)); +} + + +// Keypad keys are looked for with Num Lock on first, so a navigation key is named after the +// main block's key and a keypad digit after the keypad's. +static int Scancode_Of(int virtualkey) +{ + static SDL_Keymod const tries[] = { SDL_KMOD_NUM, SDL_KMOD_NONE }; + + for (SDL_Keymod modifiers : tries) { + for (int scancode = SDL_SCANCODE_UNKNOWN + 1; scancode < SDL_SCANCODE_COUNT; scancode++) { + SDL_Keycode keycode = SDL_GetKeyFromScancode((SDL_Scancode)scancode, SDL_KMOD_NONE, true); + if (Virtual_Key_From_SDL((SDL_Scancode)scancode, keycode, modifiers) == virtualkey) { + return(scancode); + } + } + } + return(SDL_SCANCODE_UNKNOWN); +} + + +std::string Virtual_Key_Name(int virtualkey, HKL layout) +{ + if (virtualkey <= 0) { + return(std::string()); + } + + switch (virtualkey) { + case VK_SHIFT: return("Shift"); + case VK_CONTROL: return("Ctrl"); + case VK_MENU: return("Alt"); + default: break; + } + + int const scancode = Scancode_Of(virtualkey); + if (scancode == SDL_SCANCODE_UNKNOWN || Layout_Position((SDL_Scancode)scancode)) { + + // The top bit marks a dead key, which still prints its accent. + UINT const character = MapVirtualKeyExW((UINT)virtualkey, MAPVK_VK_TO_CHAR, Layout_Or_Current(layout)) & 0x7FFFFFFF; + if (character != 0 && character <= 0xFFFF) { + wchar_t wide[2] = { (wchar_t)character, L'\0' }; + CharUpperW(wide); + + char utf8[8]; + int const length = WideCharToMultiByte(CP_UTF8, 0, wide, 1, utf8, sizeof(utf8), nullptr, nullptr); + if (length > 0) { + return(std::string(utf8, (size_t)length)); + } + } + } + + if (scancode == SDL_SCANCODE_UNKNOWN) { + return(std::string()); + } + + char const * name = SDL_GetKeyName(SDL_GetKeyFromScancode((SDL_Scancode)scancode, SDL_KMOD_NONE, false)); + return((name != nullptr) ? name : ""); +} diff --git a/code/sdl/sdlkeys.h b/code/sdl/sdlkeys.h new file mode 100644 index 000000000..b3167918c --- /dev/null +++ b/code/sdl/sdlkeys.h @@ -0,0 +1,24 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include "win.h" + +#include + +#include + + +// Returns the virtual-key code the layout gives the key, or 0 for none; raw is the Windows scan +// code or 0, and a null layout is the thread's current one. +int Virtual_Key_From_SDL(SDL_Scancode scancode, SDL_Keycode keycode, SDL_Keymod modifiers, Uint16 raw = 0, HKL layout = NULL); + +// Returns the layout's character or SDL's English name for a virtual-key code, or "" for none. +std::string Virtual_Key_Name(int virtualkey, HKL layout = NULL); diff --git a/code/sdl/sdlwindow.cpp b/code/sdl/sdlwindow.cpp new file mode 100644 index 000000000..ff7588962 --- /dev/null +++ b/code/sdl/sdlwindow.cpp @@ -0,0 +1,735 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "sdl/sdlwindow.h" + +#include "dbgprint.h" +#include "gamewindow.h" +#include "resource.h" +#include "sdl/sdlevents.h" +#include "sdl/sdlinput.h" +#include "sdl/sdlkeys.h" +#include "win.h" + +#include + +#define SDL_MAIN_HANDLED +#include +#include + +#include +#include +#include + + +namespace +{ + +bool _Started = false; +SDL_Window * _Window = nullptr; +int _NativeModals = 0; +bool _Watching = false; + +SDLInputStateClass _Input; + +SDL_Cursor * _SystemCursors[UI_CURSOR_COUNT]; + +// The SDL event types the main window posts when Windows takes its mouse capture away, and +// when a window drag, a resize or the system menu ends. +Uint32 _CaptureCancelled = 0; +Uint32 _ModalLoopEnded = 0; +UINT_PTR const WindowSubclass = 2; + + +void Set_Hint(char const * name, char const * value) +{ + if (!SDL_SetHint(name, value)) { + DebugString("SDL: hint %s was not set: %s\n", name, SDL_GetError()); + } +} + + +void Set_Hint(char const * name, int value) +{ + char text[16]; + std::snprintf(text, sizeof(text), "%d", value); + Set_Hint(name, text); +} + + +// Every hint is set, even where it repeats SDL's default, because the defaults change between +// SDL releases and each of these keeps a behavior the game had before SDL owned the window. +void Set_Hints(void) +{ + Set_Hint(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, "0"); + Set_Hint(SDL_HINT_NO_SIGNAL_HANDLERS, "1"); + Set_Hint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "1"); + Set_Hint(SDL_HINT_WINDOWS_ENABLE_MENU_MNEMONICS, "0"); + Set_Hint(SDL_HINT_WINDOWS_RAW_KEYBOARD, "0"); + Set_Hint(SDL_HINT_WINDOWS_GAMEINPUT, "0"); + Set_Hint(SDL_HINT_WINDOWS_ERASE_BACKGROUND_MODE, "0"); + Set_Hint(SDL_HINT_WINDOWS_INTRESOURCE_ICON, IDI_SUN); + Set_Hint(SDL_HINT_WINDOWS_INTRESOURCE_ICON_SMALL, IDI_SUN); + Set_Hint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0"); + Set_Hint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); + Set_Hint(SDL_HINT_WINDOW_ALLOW_TOPMOST, "0"); + Set_Hint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); + Set_Hint(SDL_HINT_MOUSE_AUTO_CAPTURE, "1"); + Set_Hint(SDL_HINT_MOUSE_EMULATE_WARP_WITH_RELATIVE, "0"); + Set_Hint(SDL_HINT_MOUSE_DOUBLE_CLICK_TIME, (int)GetDoubleClickTime()); + Set_Hint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, GetSystemMetrics(SM_CXDOUBLECLK) / 2); + Set_Hint(SDL_HINT_KEYCODE_OPTIONS, "french_numbers,latin_letters"); +} + + +float Pixel_Density(void) +{ + float density = (_Window != nullptr) ? SDL_GetWindowPixelDensity(_Window) : 1.0f; + return(density > 0.0f ? density : 1.0f); +} + + +void Dispatch(WindowEvent const & event) +{ + if (event.Type == WINDOW_EVENT_FOCUS_GAINED) { + _Input.Focus_Gained(); + } else if (event.Type == WINDOW_EVENT_FOCUS_LOST) { + _Input.Focus_Lost(); + } + + // The screen saver may start while the player is elsewhere, but not over the game. + if (event.Type == WINDOW_EVENT_FOCUS_GAINED) { + SDL_DisableScreenSaver(); + } else if (event.Type == WINDOW_EVENT_FOCUS_LOST) { + SDL_EnableScreenSaver(); + } + + Game_Window_Handle_Event(event); +} + + +HWND Window_Handle(void) +{ + if (_Window == nullptr) { + return(NULL); + } + return((HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(_Window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr)); +} + + +void Post_Event(Uint32 type) +{ + if (type != 0) { + SDL_Event event; + SDL_zero(event); + event.type = type; + SDL_PushEvent(&event); + } +} + + +// Windows can end the capture while the window keeps the focus, for a system menu or another +// window taking the mouse, and SDL reports neither. The window's own releases are ignored. +LRESULT CALLBACK Watch_Messages(HWND window, UINT message, WPARAM wparam, LPARAM lparam, UINT_PTR, DWORD_PTR) +{ + if (message == WM_CANCELMODE || (message == WM_CAPTURECHANGED && lparam != 0 && (HWND)lparam != window)) { + Post_Event(_CaptureCancelled); + } + + // SDL reset the keyboard when the drag, the resize or the system menu began. + LRESULT const result = DefSubclassProc(window, message, wparam, lparam); + if (message == WM_EXITSIZEMOVE || message == WM_EXITMENULOOP) { + Post_Event(_ModalLoopEnded); + } + return(result); +} + + +void Handle_SDL_Event(SDL_Event const & sdlevent) +{ + if (_CaptureCancelled != 0 && sdlevent.type == _CaptureCancelled) { + WindowEvent event; + event.Type = WINDOW_EVENT_CAPTURE_LOST; + Dispatch(event); + return; + } + + _Input.Drop_Released_Keys(); + + // Windows' key state is stale once the focus has gone elsewhere. + if (_ModalLoopEnded != 0 && sdlevent.type == _ModalLoopEnded) { + if (_Window != nullptr && (SDL_GetWindowFlags(_Window) & SDL_WINDOW_INPUT_FOCUS) != 0) { + _Input.Hold_Windows_Keys(); + } + return; + } + + if (sdlevent.type == SDL_EVENT_QUIT) { + DebugString("SDL: the system asked the game to quit\n"); + } + + int const before = _Input.Modifiers(); + bool continued = false; + if (sdlevent.type == SDL_EVENT_KEY_DOWN || sdlevent.type == SDL_EVENT_KEY_UP) { + continued = _Input.Track_Key(sdlevent.key); + } + + std::vector events; + Window_Events_From_SDL(sdlevent, Pixel_Density(), _Input.Modifiers(), events); + for (WindowEvent & event : events) { + if (event.Type == WINDOW_EVENT_KEY_DOWN && continued) { + event.Repeat = true; + } + + // Windows reports the release of Alt itself as a system key too. + if (event.Type == WINDOW_EVENT_KEY_UP && (before & WINDOW_MOD_ALT) != 0 && (before & WINDOW_MOD_CTRL) == 0) { + event.System = true; + } + Dispatch(event); + } +} + + +bool Watched(Uint32 type) +{ + return(type == SDL_EVENT_WINDOW_EXPOSED || type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED); +} + + +// Repaints and resizes are handled as SDL reports them, so the picture keeps up while +// Windows runs its own loop for a window being dragged or sized. +bool SDLCALL Watch_Window(void *, SDL_Event * sdlevent) +{ + if (!Watched(sdlevent->type) || _Watching) { + return(true); + } + + _Watching = true; + Handle_SDL_Event(*sdlevent); + _Watching = false; + return(true); +} + + +bool SDLCALL Watch_Keys(void *, SDL_Event * sdlevent) +{ + if (sdlevent->type == SDL_EVENT_KEY_DOWN) { + _Input.Note_Queued_Key(sdlevent->key); + } + return(true); +} + + +// The window's class cursor, a monochrome arrow among the game's resources, is the pointer +// shown wherever neither the game nor the interface chooses one. +SDL_Cursor * Resource_Arrow(void) +{ + HCURSOR handle = LoadCursorW(ProgramInstance, MAKEINTRESOURCEW(IDC_CURSOR1)); + ICONINFO info; + if (handle == NULL || !GetIconInfo(handle, &info)) { + return(nullptr); + } + + SDL_Cursor * cursor = nullptr; + BITMAP bitmap; + if (info.hbmColor == NULL && GetObject(info.hbmMask, sizeof(bitmap), &bitmap) != 0 && bitmap.bmWidth % 8 == 0 && bitmap.bmHeight >= 2) { + + // A monochrome cursor stacks its AND mask above its XOR mask. + int const width = bitmap.bmWidth; + int const height = bitmap.bmHeight / 2; + int const pitch = ((width + 31) / 32) * 4; + + struct + { + BITMAPINFOHEADER Header; + RGBQUAD Colors[2]; + } layout = {}; + layout.Header.biSize = sizeof(BITMAPINFOHEADER); + layout.Header.biWidth = width; + layout.Header.biHeight = -(height * 2); + layout.Header.biPlanes = 1; + layout.Header.biBitCount = 1; + layout.Header.biCompression = BI_RGB; + + std::vector bits((size_t)pitch * height * 2); + HDC dc = GetDC(NULL); + int const rows = GetDIBits(dc, info.hbmMask, 0, height * 2, bits.data(), (BITMAPINFO *)&layout, DIB_RGB_COLORS); + ReleaseDC(NULL, dc); + + if (rows == height * 2) { + int const stride = width / 8; + std::vector data((size_t)stride * height); + std::vector mask((size_t)stride * height); + for (int y = 0; y < height; y++) { + for (int x = 0; x < stride; x++) { + unsigned char const andbits = bits[(size_t)y * pitch + x]; + unsigned char const xorbits = bits[(size_t)(y + height) * pitch + x]; + data[(size_t)y * stride + x] = (Uint8)~(andbits ^ xorbits); + mask[(size_t)y * stride + x] = (Uint8)~andbits; + } + } + cursor = SDL_CreateCursor(data.data(), mask.data(), width, height, (int)info.xHotspot, (int)info.yHotspot); + } + } + + DeleteObject(info.hbmMask); + if (info.hbmColor != NULL) { + DeleteObject(info.hbmColor); + } + return(cursor); +} + + +SDL_SystemCursor System_Cursor_Of(UICursor shape) +{ + switch (shape) { + case UI_CURSOR_TEXT: return(SDL_SYSTEM_CURSOR_TEXT); + case UI_CURSOR_HAND: return(SDL_SYSTEM_CURSOR_POINTER); + case UI_CURSOR_RESIZE_NS: return(SDL_SYSTEM_CURSOR_NS_RESIZE); + case UI_CURSOR_RESIZE_EW: return(SDL_SYSTEM_CURSOR_EW_RESIZE); + case UI_CURSOR_RESIZE_NESW: return(SDL_SYSTEM_CURSOR_NESW_RESIZE); + case UI_CURSOR_RESIZE_NWSE: return(SDL_SYSTEM_CURSOR_NWSE_RESIZE); + case UI_CURSOR_MOVE: return(SDL_SYSTEM_CURSOR_MOVE); + case UI_CURSOR_UNAVAILABLE: return(SDL_SYSTEM_CURSOR_NOT_ALLOWED); + default: return(SDL_SYSTEM_CURSOR_DEFAULT); + } +} + + +bool Start_SDL(void) +{ + if (_Started) { + return(true); + } + + Set_Hints(); + + // The window class keeps the game's own name, which tools looking for the window use. + if (!SDL_RegisterApp("Tiberian Sun", 0, ProgramInstance)) { + DebugString("SDL: the window class was not registered: %s\n", SDL_GetError()); + } + SDL_SetMainReady(); + + if (!SDL_Init(SDL_INIT_VIDEO)) { + DebugString("SDL: video did not start: %s\n", SDL_GetError()); + SDL_UnregisterApp(); + return(false); + } + + Uint32 const events = SDL_RegisterEvents(2); + if (events != 0) { + _CaptureCancelled = events; + _ModalLoopEnded = events + 1; + } + _Started = true; + return(true); +} + + +// Brackets a dialog with a message loop of its own, so the focus it takes from the main +// window is not taken for the player switching away. +void Begin_Native_Modal(void) +{ + _NativeModals++; +} + + +// The focus changes a dialog of the game's own caused are dropped, and the current focus is +// passed on instead, because the player never left the game. +void End_Native_Modal(void) +{ + if (_NativeModals == 0 || --_NativeModals != 0 || _Window == nullptr) { + return; + } + + SDL_PumpEvents(); + SDL_FlushEvent(SDL_EVENT_WINDOW_FOCUS_LOST); + SDL_FlushEvent(SDL_EVENT_WINDOW_FOCUS_GAINED); + + WindowEvent focus; + focus.Type = ((SDL_GetWindowFlags(_Window) & SDL_WINDOW_INPUT_FOCUS) != 0) ? WINDOW_EVENT_FOCUS_GAINED : WINDOW_EVENT_FOCUS_LOST; + Dispatch(focus); +} + + +} + + +/// +/// Destroys the main window, if there is one, and stops SDL. Calling it again, or before SDL +/// started, does nothing. The renderer must have let go of the window first. +/// +void Main_Window_Destroy(void) +{ + if (!_Started) { + return; + } + + for (SDL_Cursor * & cursor : _SystemCursors) { + if (cursor != nullptr) { + SDL_DestroyCursor(cursor); + cursor = nullptr; + } + } + + if (_Window != nullptr) { + SDL_RemoveEventWatch(Watch_Window, nullptr); + SDL_RemoveEventWatch(Watch_Keys, nullptr); + RemoveWindowSubclass(Window_Handle(), Watch_Messages, WindowSubclass); + SDL_DestroyWindow(_Window); + _Window = nullptr; + } + + SDL_Quit(); + SDL_UnregisterApp(); + _Started = false; +} + + +/// +/// Starts SDL, then creates and shows the main window. A window for windowed play has a client +/// area of the size given and is centered on the primary display, kept clear of its top and +/// left edges; otherwise the window covers the display without changing its mode. +/// +/// False when SDL could not start or the window could not be created. +bool Main_Window_Create(bool windowed, int width, int height) +{ + if (!Start_SDL() || _Window != nullptr) { + return(false); + } + + SDL_WindowFlags flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY; + flags |= windowed ? SDL_WINDOW_RESIZABLE : SDL_WINDOW_FULLSCREEN; + + _Window = SDL_CreateWindow("Tiberian Sun", width, height, flags); + if (_Window == nullptr) { + DebugString("SDL: the window was not created: %s\n", SDL_GetError()); + return(false); + } + + if (windowed) { + float const density = Pixel_Density(); + int const clientwidth = (int)std::lround(width / density); + int const clientheight = (int)std::lround(height / density); + SDL_SetWindowSize(_Window, clientwidth, clientheight); + + int top = 0; + int left = 0; + int bottom = 0; + int right = 0; + SDL_GetWindowBordersSize(_Window, &top, &left, &bottom, &right); + + SDL_Rect display; + if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &display)) { + int const outerwidth = clientwidth + left + right; + int const outerheight = clientheight + top + bottom; + int const x = display.x + SDL_max((display.w - outerwidth) / 2, 0) + left; + int const y = display.y + SDL_max((display.h - outerheight) / 2, 0) + top; + SDL_SetWindowPosition(_Window, x, y); + } + } + + SDL_AddEventWatch(Watch_Window, nullptr); + SDL_AddEventWatch(Watch_Keys, nullptr); + SetWindowSubclass(Window_Handle(), Watch_Messages, WindowSubclass, 0); + _Input.Reset(); + + SDL_ShowWindow(_Window); + SDL_RaiseWindow(_Window); + + // Typed text is always on, as characters from Windows were. + SDL_StartTextInput(_Window); + + return(true); +} + + +NativeWindow Main_Window_Native(void) +{ + NativeWindow window = { NATIVE_WINDOW_DEFAULT, nullptr, nullptr }; + if (_Window != nullptr) { + window.Handle = SDL_GetPointerProperty(SDL_GetWindowProperties(_Window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); + } + return(window); +} + + +bool Main_Window_Drawable_Size(int & width, int & height) +{ + if (_Window == nullptr || !SDL_GetWindowSizeInPixels(_Window, &width, &height)) { + return(false); + } + return(width > 0 && height > 0); +} + + +bool Main_Window_Minimized(void) +{ + return(_Window != nullptr && (SDL_GetWindowFlags(_Window) & SDL_WINDOW_MINIMIZED) != 0); +} + + +bool Main_Window_Client_Rect(int & x, int & y, int & width, int & height) +{ + if (_Window == nullptr || !SDL_GetWindowPosition(_Window, &x, &y)) { + return(false); + } + + float const density = Pixel_Density(); + x = (int)std::floor((float)x * density); + y = (int)std::floor((float)y * density); + return(Main_Window_Drawable_Size(width, height)); +} + + +// A window grown about its middle can be pushed past the edges of the display, and a title +// bar above its top edge cannot be grabbed to bring the window back. +void Main_Window_Resize(int width, int height) +{ + if (_Window == nullptr) { + return; + } + + float const density = Pixel_Density(); + int const newwidth = (int)std::lround(width / density); + int const newheight = (int)std::lround(height / density); + + int x = 0; + int y = 0; + int oldwidth = 0; + int oldheight = 0; + SDL_GetWindowPosition(_Window, &x, &y); + SDL_GetWindowSize(_Window, &oldwidth, &oldheight); + x += (oldwidth - newwidth) / 2; + y += (oldheight - newheight) / 2; + + int top = 0; + int left = 0; + int bottom = 0; + int right = 0; + SDL_GetWindowBordersSize(_Window, &top, &left, &bottom, &right); + + SDL_Rect usable; + if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(_Window), &usable)) { + if (x + newwidth + right > usable.x + usable.w) x = usable.x + usable.w - newwidth - right; + if (y + newheight + bottom > usable.y + usable.h) y = usable.y + usable.h - newheight - bottom; + if (x - left < usable.x) x = usable.x + left; + if (y - top < usable.y) y = usable.y + top; + } + + SDL_SetWindowSize(_Window, newwidth, newheight); + SDL_SetWindowPosition(_Window, x, y); +} + + +int Main_Window_Refresh_Rate(void) +{ + if (_Window == nullptr) { + return(0); + } + + SDL_DisplayMode const * mode = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(_Window)); + return(mode != nullptr ? (int)std::lround(mode->refresh_rate) : 0); +} + + +void Main_Window_Pump_Events(void) +{ + if (_Window == nullptr) { + return; + } + + SDL_Event sdlevent; + while (SDL_PollEvent(&sdlevent)) { + if (!Watched(sdlevent.type)) { + Handle_SDL_Event(sdlevent); + } + } +} + + +void Main_Window_Capture_Mouse(bool capture) +{ + if (_Window == nullptr) { + return; + } + SDL_CaptureMouse(capture); + + // SDL still records a capture Windows took away, so it does not ask for it again. + HWND const window = Window_Handle(); + if (capture && (SDL_GetWindowFlags(_Window) & SDL_WINDOW_MOUSE_CAPTURE) != 0 && GetCapture() != window) { + SetCapture(window); + } +} + + +bool Main_Window_Mouse_Captured(void) +{ + HWND const window = Window_Handle(); + return(window != NULL && GetCapture() == window); +} + + +void Main_Window_Confine_Cursor(bool confine) +{ + if (_Window != nullptr) { + SDL_SetWindowMouseGrab(_Window, confine); + } +} + + +// The position is read from the system rather than from the last event, so it is current +// even while the game is not pumping events. +bool Main_Window_Cursor_Position(int & x, int & y) +{ + if (_Window == nullptr) { + return(false); + } + + float globalx = 0.0f; + float globaly = 0.0f; + SDL_GetGlobalMouseState(&globalx, &globaly); + + int windowx = 0; + int windowy = 0; + SDL_GetWindowPosition(_Window, &windowx, &windowy); + + float const density = Pixel_Density(); + x = (int)std::floor((globalx - (float)windowx) * density); + y = (int)std::floor((globaly - (float)windowy) * density); + return(true); +} + + +void Main_Window_Warp_Cursor(int x, int y) +{ + if (_Window != nullptr) { + float const density = Pixel_Density(); + SDL_WarpMouseInWindow(_Window, (float)x / density, (float)y / density); + } +} + + +// Mouse buttons follow the player's left-handed button setting. +bool Main_Window_Key_Down(int virtualkey) +{ + switch (virtualkey) { + case VK_LBUTTON: return((SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK) != 0); + case VK_RBUTTON: return((SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON_RMASK) != 0); + case VK_MBUTTON: return((SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON_MMASK) != 0); + case VK_XBUTTON1: return((SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON_X1MASK) != 0); + case VK_XBUTTON2: return((SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON_X2MASK) != 0); + default: break; + } + + return(_Started && _Input.Key_Down(virtualkey)); +} + + +bool Main_Window_Key_Toggled(int virtualkey) +{ + return(_Input.Key_Toggled(virtualkey)); +} + + +std::string Main_Window_Key_Name(int virtualkey) +{ + return(Virtual_Key_Name(virtualkey)); +} + + +SDL_Cursor * Main_Window_Create_Cursor(unsigned int const * pixels, int width, int height, int hotx, int hoty) +{ + if (!_Started || pixels == nullptr || width <= 0 || height <= 0) { + return(nullptr); + } + + SDL_Surface * surface = SDL_CreateSurfaceFrom(width, height, SDL_PIXELFORMAT_ARGB8888, (void *)pixels, width * 4); + if (surface == nullptr) { + return(nullptr); + } + + SDL_Cursor * cursor = SDL_CreateColorCursor(surface, hotx, hoty); + SDL_DestroySurface(surface); + return(cursor); +} + + +void Main_Window_Destroy_Cursor(SDL_Cursor * cursor) +{ + if (_Started && cursor != nullptr) { + SDL_DestroyCursor(cursor); + } +} + + +void Main_Window_Set_Cursor(SDL_Cursor * cursor) +{ + if (!_Started) { + return; + } + + if (cursor == nullptr) { + SDL_HideCursor(); + return; + } + + SDL_SetCursor(cursor); + SDL_ShowCursor(); +} + + +SDL_Cursor * Main_Window_System_Cursor(UICursor shape) +{ + if (!_Started || shape < 0 || shape >= UI_CURSOR_COUNT) { + return(nullptr); + } + + SDL_Cursor * & cursor = _SystemCursors[shape]; + if (cursor == nullptr) { + if (shape == UI_CURSOR_ARROW) { + cursor = Resource_Arrow(); + } + if (cursor == nullptr) { + cursor = SDL_CreateSystemCursor(System_Cursor_Of(shape)); + } + } + return(cursor); +} + + +std::string Main_Window_Clipboard_Text(void) +{ + if (!_Started) { + return(std::string()); + } + + char * text = SDL_GetClipboardText(); + std::string result = (text != nullptr) ? text : ""; + SDL_free(text); + return(result); +} + + +void Main_Window_Set_Clipboard_Text(std::string const & text) +{ + if (_Started) { + SDL_SetClipboardText(text.c_str()); + } +} + + +void Main_Window_Error_Box(char const * title, char const * text) +{ + Begin_Native_Modal(); + if (!SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, text, _Window)) { + DebugString("SDL: the message box was not shown: %s\n", SDL_GetError()); + } + End_Native_Modal(); +} diff --git a/code/sdl/sdlwindow.h b/code/sdl/sdlwindow.h new file mode 100644 index 000000000..279bb0b70 --- /dev/null +++ b/code/sdl/sdlwindow.h @@ -0,0 +1,74 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include "nativewindow.hh" +#include "ui/uiinput.h" + +#include + + +struct SDL_Cursor; + + +void Main_Window_Destroy(void); + +bool Main_Window_Create(bool windowed, int width, int height); +NativeWindow Main_Window_Native(void); +bool Main_Window_Drawable_Size(int & width, int & height); +bool Main_Window_Minimized(void); + +// The desktop position and size, in pixels, of the main window's client area. +bool Main_Window_Client_Rect(int & x, int & y, int & width, int & height); + +// Gives the client area the size in pixels, growing or shrinking the window about its +// middle and keeping it inside the usable area of its display. +void Main_Window_Resize(int width, int height); + +// Returns the refresh rate of the display showing the main window in hertz, or 0 when unknown. +int Main_Window_Refresh_Rate(void); + +void Main_Window_Pump_Events(void); + +void Main_Window_Capture_Mouse(bool capture); +bool Main_Window_Mouse_Captured(void); +void Main_Window_Confine_Cursor(bool confine); + +// Positions are in window client pixels. +bool Main_Window_Cursor_Position(int & x, int & y); +void Main_Window_Warp_Cursor(int x, int y); + +// Keys are Windows virtual-key codes, reported as of the input event being handled, except that +// a key held while the window gained the focus is read from Windows until SDL reports it. Mouse +// buttons report their current state, since a release outside the window may be lost. +bool Main_Window_Key_Down(int virtualkey); +bool Main_Window_Key_Toggled(int virtualkey); + +// The name the keyboard layout gives a key, in UTF-8, or an empty string for no key. +std::string Main_Window_Key_Name(int virtualkey); + +// The pixels are 32-bit ARGB, top row first. Returns NULL when the cursor cannot be made. +// The caller owns the cursor and frees it with Main_Window_Destroy_Cursor. +SDL_Cursor * Main_Window_Create_Cursor(unsigned int const * pixels, int width, int height, int hotx, int hoty); +void Main_Window_Destroy_Cursor(SDL_Cursor * cursor); + +// Shows the cursor over the main window, or hides the pointer there when cursor is NULL. +void Main_Window_Set_Cursor(SDL_Cursor * cursor); + +// UI_CURSOR_ARROW is the window's own arrow. The SDL layer owns these cursors and frees them in +// Main_Window_Destroy; do not destroy one. +SDL_Cursor * Main_Window_System_Cursor(UICursor shape); + +std::string Main_Window_Clipboard_Text(void); +void Main_Window_Set_Clipboard_Text(std::string const & text); + +// Shows an error over the main window, or on its own before the window exists, and waits +// for the player to dismiss it. The text is UTF-8. +void Main_Window_Error_Box(char const * title, char const * text); diff --git a/code/startup.cpp b/code/startup.cpp index a71bbc1fb..7cfaca2e6 100644 --- a/code/startup.cpp +++ b/code/startup.cpp @@ -78,6 +78,7 @@ #include "fly.h" #include "fog.h" #include "gamedirs.h" +#include "gamewindow.h" #include "globals.h" #include "goptions.h" #include "house.h" @@ -111,6 +112,7 @@ #include "scenario.h" #include "scheme.h" #include "script.h" +#include "sdl/sdlwindow.h" #include "session.h" #include "shapeset.h" #include "side.h" @@ -441,7 +443,7 @@ static bool Claim_Single_Instance(void) * HISTORY: * * 03/20/1995 JLB : Created. * *=============================================================================================*/ -int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_show ) +int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int ) { int argc; //Command line argument count char ** argv; //Pointers to command line arguments @@ -573,7 +575,10 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho VideoModeWidth = Options.ScreenWidth; VideoModeHeight = Options.ScreenHeight; - Create_Main_Window(instance, command_show, Options.ScreenWidth, Options.ScreenHeight); + if (!Game_Window_Open(Options.ScreenWidth, Options.ScreenHeight)) { + Main_Window_Error_Box(Fetch_String(TXT_SHORT_TITLE), Fetch_String(TXT_VIDEO_ERROR)); + exit(EXIT_FAILURE); + } Exception_Run_Post_Window_Test(); @@ -581,17 +586,17 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho int drawablewidth = 0; int drawableheight = 0; - int refreshrate = Win_Window_Refresh_Rate(MainWindow); - NativeWindow nativewindow = Win_Native_Window(MainWindow); - if (!Win_Window_Drawable_Size(MainWindow, drawablewidth, drawableheight) + int refreshrate = Main_Window_Refresh_Rate(); + NativeWindow nativewindow = Main_Window_Native(); + if (!Main_Window_Drawable_Size(drawablewidth, drawableheight) || !Video_Init(nativewindow, drawablewidth, drawableheight, refreshrate)) { - MessageBox(MainWindow, Fetch_String(TXT_VIDEO_ERROR), Fetch_String(TXT_SHORT_TITLE), MB_ICONWARNING); + Main_Window_Error_Box(Fetch_String(TXT_SHORT_TITLE), Fetch_String(TXT_VIDEO_ERROR)); exit(EXIT_FAILURE); } VisibleSurface = DSurface::Create_Primary(); if (VisibleSurface == NULL) { - MessageBox(MainWindow, Fetch_String(TXT_VIDEO_ERROR), Fetch_String(TXT_SHORT_TITLE), MB_ICONWARNING); + Main_Window_Error_Box(Fetch_String(TXT_SHORT_TITLE), Fetch_String(TXT_VIDEO_ERROR)); exit(EXIT_FAILURE); } @@ -617,7 +622,7 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho AlphaBuffer = new ABuffer(Rect(TacticalRect.X, TacticalRect.Y, 480, 480 - TacticalRect.Y)); - MouseCursor = new WWMouseClass(MainWindow); + MouseCursor = new WWMouseClass(); MouseCursor->Capture_Mouse(); /* @@ -650,25 +655,11 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho HiddenSurface->Fill(0); Update_Visible_Surface(HiddenSurface); - /* - ** Flag that this is a clean shutdown (not killed with Ctrl-Alt-Del) - */ - ReadyToQuit = 1; - AudioEngine.End(); - /* - ** Post a message to our message handler to tell it to clean up. - */ - PostMessage(MainWindow, WM_DESTROY, 0, 0); + Windows_Message_Handler(); - /* - ** Wait until the message handler has dealt with the message - */ - do - { - Windows_Message_Handler(); - }while (ReadyToQuit == 1); + Game_Window_Begin_Shutdown(); error_code = EXIT_SUCCESS; @@ -1023,6 +1014,9 @@ void __cdecl Prog_End(void) CloseHandle(AppMutex); AppMutex = NULL; } + + // The renderer let go of the window when the surfaces were reset above. + Game_Window_Close(); } /*********************************************************************************************** @@ -1043,19 +1037,9 @@ void Emergency_Exit(void) { AudioEngine.End(); - ReadyToQuit = 1; - - /* - ** Post a message to our message handler to tell it to clean up. - */ - PostMessage(MainWindow, WM_DESTROY, 0, 0); + Windows_Message_Handler(); - while (MainWindow) { - Windows_Message_Handler(); - if (ReadyToQuit != 1) { - break; - } - } + Game_Window_Begin_Shutdown(); if (MouseCursor) { @@ -1064,8 +1048,6 @@ void Emergency_Exit(void) } MouseCursor = NULL; - PostQuitMessage(EXIT_SUCCESS); - Shutdown_Network(); Prog_End(); diff --git a/code/tooltip.cpp b/code/tooltip.cpp index 4af322ed5..7d4d057d0 100644 --- a/code/tooltip.cpp +++ b/code/tooltip.cpp @@ -17,18 +17,20 @@ #include "data.h" #include "dbgprint.h" +#include "sdl/sdlwindow.h" #include "vidscale.h" +#include "windowevent.hh" /// -/// Constructs an empty tooltip manager for the window specified. +/// Constructs an empty tooltip manager. /// The manager starts out deactivated and carrying no tooltips, with the default hover /// delay and lifetime. /// -/// The window whose tooltips this manager will look after. /// No tooltip will appear until Activate is called. -ToolTipManager::ToolTipManager(HWND window) : - Window(window), +ToolTipManager::ToolTipManager(void) : + TimerRunning(false), + TimerDue(0), IsActive(false), CurrentToolTip(NULL), ToolTipDelay(TOOLTIP_DELAY), @@ -40,14 +42,10 @@ ToolTipManager::ToolTipManager(HWND window) : /// -/// Destroys the tooltip manager. -/// The hover timer is handed back to Windows and every tooltip registered with the manager -/// is destroyed along with it. +/// Destroys the tooltip manager and every tooltip registered with it. /// ToolTipManager::~ToolTipManager(void) { - KillTimer(Window, TOOLTIP_EVENT); - Reset_Current(); ToolTipIndex.Clear(); @@ -74,7 +72,7 @@ void ToolTipManager::Activate(bool state) if (IsActive != state) { IsActive = state; if (!IsActive) { - KillTimer(Window, TOOLTIP_EVENT); + TimerRunning = false; Reset_Current(); } @@ -84,54 +82,70 @@ void ToolTipManager::Activate(bool state) /// -/// Handles the window messages that drive the tooltip display. -/// This routine must be fed the message pump's traffic. Mouse movement restarts the hover -/// countdown, a button press dismisses whatever is showing, and the timer message is what -/// brings a tooltip up and later takes it away again. +/// Follows the mouse over the main window. Mouse movement restarts the hover countdown, and +/// a button press or release takes down whatever is showing. /// -/// The window message to examine. -void ToolTipManager::Message_Handler(MSG * msg) +void ToolTipManager::Handle_Window_Event(WindowEvent const & event) { - if (IsActive == true) { - - switch (msg->message) { - case WM_LBUTTONDOWN: - case WM_LBUTTONUP: - case WM_RBUTTONDOWN: - case WM_RBUTTONUP: - case WM_MBUTTONDOWN: - case WM_MBUTTONUP: - KillTimer(Window, TOOLTIP_EVENT); - Reset_Current(); - break; + if (IsActive != true) { + return; + } - case WM_MOUSEMOVE: - KillTimer(Window, TOOLTIP_EVENT); - SetTimer(Window, TOOLTIP_EVENT, ToolTipDelay, NULL); + switch (event.Type) { + case WINDOW_EVENT_MOUSE_DOWN: + case WINDOW_EVENT_MOUSE_UP: + if (event.Button <= WINDOW_BUTTON_MIDDLE && event.Clicks == 1) { + TimerRunning = false; Reset_Current(); - break; - - case WM_TIMER: - if (msg->wParam == TOOLTIP_EVENT) { - KillTimer(Window, TOOLTIP_EVENT); - if (CurrentToolTip != NULL) { - Reset_Current(); - } else { - GetCursorPos((LPPOINT)&LastMousePos); - Screen_Point_To_Game((POINT &)LastMousePos); - CurrentToolTip = Find_From_Pos((Point2D &)LastMousePos); - if (Process() == true) { - SetTimer(Window, TOOLTIP_EVENT, ToolTipLifetime, NULL); - } - } - } - break; + } + break; + + case WINDOW_EVENT_MOUSE_MOVE: + Start_Timer(ToolTipDelay); + Reset_Current(); + break; + + default: + break; + } +} + +/// +/// Brings up the tooltip under the mouse once it has rested for the hover delay, and takes it +/// down once its lifetime has passed. Tooltips change only when the message pump calls this. +/// +void ToolTipManager::Service(void) +{ + if (IsActive != true || !TimerRunning || (long)(timeGetTime() - TimerDue) < 0) { + return; + } + + TimerRunning = false; + if (CurrentToolTip != NULL) { + Reset_Current(); + } else { + int x = 0; + int y = 0; + Main_Window_Cursor_Position(x, y); + LastMousePos.X = x; + LastMousePos.Y = y; + Window_Point_To_Game(LastMousePos); + CurrentToolTip = Find_From_Pos(LastMousePos); + if (Process() == true) { + Start_Timer(ToolTipLifetime); } } } +void ToolTipManager::Start_Timer(int delay) +{ + TimerRunning = true; + TimerDue = timeGetTime() + (unsigned long)delay; +} + + /// /// Fetches how long the mouse must hover before a tooltip appears. /// @@ -294,8 +308,8 @@ bool ToolTipManager::Update(ToolTipText *text) /// The tooltip display record being retired. void ToolTipManager::Reset(const ToolTipText *text) { - CurrentToolTipInfo.Pos.x = 0; - CurrentToolTipInfo.Pos.y = 0; + CurrentToolTipInfo.Pos.X = 0; + CurrentToolTipInfo.Pos.Y = 0; CurrentToolTipInfo.TextWidth = 0; CurrentToolTipInfo.TextHeight = 0; CurrentToolTip = NULL; @@ -318,24 +332,24 @@ bool ToolTipManager::Process(void) strncpy(CurrentToolTipInfo.Text, str, sizeof(CurrentToolTipInfo.Text)); - CurrentToolTipInfo.Pos.x = LastMousePos.x; - CurrentToolTipInfo.Pos.y = LastMousePos.y; + CurrentToolTipInfo.Pos.X = LastMousePos.X; + CurrentToolTipInfo.Pos.Y = LastMousePos.Y; CurrentToolTipInfo.TextWidth = 0; CurrentToolTipInfo.TextHeight = 0; ToolTipText & txt = CurrentToolTipInfo; if (!Update(&CurrentToolTipInfo)) { - txt.Pos.x = 0; - txt.Pos.y = 0; + txt.Pos.X = 0; + txt.Pos.Y = 0; txt.TextWidth = 0; txt.TextHeight = 0; CurrentToolTip = NULL; } } else { - CurrentToolTipInfo.Pos.x = 0; - CurrentToolTipInfo.Pos.y = 0; + CurrentToolTipInfo.Pos.X = 0; + CurrentToolTipInfo.Pos.Y = 0; CurrentToolTipInfo.TextWidth = 0; CurrentToolTipInfo.TextHeight = 0; CurrentToolTip = NULL; diff --git a/code/tooltip.h b/code/tooltip.h index 46372058f..288915d68 100644 --- a/code/tooltip.h +++ b/code/tooltip.h @@ -14,9 +14,11 @@ #pragma once #include "index.h" +#include "point.h" #include "rect.h" #include "vector.h" -#include "win.h" + +struct WindowEvent; class ToolTip { @@ -48,7 +50,7 @@ class ToolTip struct ToolTipText { - POINT Pos; + Point2D Pos; int TextWidth; int TextHeight; char Text[256]; @@ -57,12 +59,13 @@ struct ToolTipText class ToolTipManager { public: - ToolTipManager(HWND window); + ToolTipManager(void); virtual ~ToolTipManager(void); void Activate(bool state); - void Message_Handler(MSG *msg); + void Handle_Window_Event(WindowEvent const & event); + void Service(void); int Get_Timer_Delay(void); void Set_Timer_Delay(int delay); @@ -91,18 +94,16 @@ class ToolTipManager void Reset_Current(void); enum { - TOOLTIP_EVENT = 'TTIP', TOOLTIP_DELAY = 1000, /// 1 second TOOLTIP_LIFETIME = 10000, /// 10 seconds }; private: - /* - * This is the window whose tooltips this manager looks after. Mouse positions are - * expressed in its client coordinates, and it is the window the hover timer is hung - * off of. - */ - HWND Window; + void Start_Timer(int delay); + + // The time Service next acts, while TimerRunning is set. + bool TimerRunning; + unsigned long TimerDue; /* * If this manager is allowed to display tooltips, then this flag will be true. A @@ -115,7 +116,7 @@ class ToolTipManager * This is where the cursor was, in frame coordinates, when the hover delay expired. * It decides which tooltip is chosen and where the tooltip box is placed. */ - POINT LastMousePos; + Point2D LastMousePos; /* * This points to the tooltip the mouse is currently resting over, or NULL when diff --git a/code/ui/rml/rmlsystem.cpp b/code/ui/rml/rmlsystem.cpp index 638633814..27f01168d 100644 --- a/code/ui/rml/rmlsystem.cpp +++ b/code/ui/rml/rmlsystem.cpp @@ -138,61 +138,14 @@ void UIRmlSystemClass::SetMouseCursor(Rml::String const & name) void UIRmlSystemClass::SetClipboardText(Rml::String const & text) { - std::wstring wide; - if (!UI_UTF8_To_UTF16(text, wide)) { - return; - } - - std::size_t bytes = (wide.size() + 1) * sizeof(wchar_t); - HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, bytes); - if (memory == NULL) { - return; - } - - wchar_t * buffer = (wchar_t *)GlobalLock(memory); - if (buffer == NULL) { - GlobalFree(memory); - return; - } - std::memcpy(buffer, wide.c_str(), bytes); - GlobalUnlock(memory); - - if (!OpenClipboard(Host.Main_Window())) { - GlobalFree(memory); - return; - } - if (!EmptyClipboard() || SetClipboardData(CF_UNICODETEXT, memory) == NULL) { - GlobalFree(memory); - } - CloseClipboard(); + Host.Set_Clipboard_Text(text); } void UIRmlSystemClass::GetClipboardText(Rml::String & text) { - text.clear(); - - if (!OpenClipboard(Host.Main_Window())) { - return; - } - - HANDLE memory = GetClipboardData(CF_UNICODETEXT); - if (memory != NULL) { - wchar_t const * buffer = (wchar_t const *)GlobalLock(memory); - std::size_t capacity = GlobalSize(memory) / sizeof(wchar_t); - if (buffer != NULL) { - if (capacity <= UI_CLIPBOARD_MAX_BYTES / sizeof(wchar_t)) { - std::size_t length = 0; - while (length < capacity && buffer[length] != L'\0') { - length++; - } - if (length < capacity) { - UI_UTF16_To_UTF8(std::wstring_view(buffer, length), text); - } - } - GlobalUnlock(memory); - } + text = Host.Clipboard_Text(); + if (text.size() > UI_CLIPBOARD_MAX_BYTES) { + text.clear(); } - - CloseClipboard(); } diff --git a/code/ui/screens/keyboard/uikeyboarddlg.cpp b/code/ui/screens/keyboard/uikeyboarddlg.cpp index bcf0ff879..cc649bbe3 100644 --- a/code/ui/screens/keyboard/uikeyboarddlg.cpp +++ b/code/ui/screens/keyboard/uikeyboarddlg.cpp @@ -53,9 +53,7 @@ class UIKeyboardEngineServiceClass : public UIKeyboardServiceClass public: virtual std::string Key_Name(int key) override { - char buffer[128]; - Build_Hotkey_String((KeyNumType)key, buffer); - return(buffer); + return(Build_Hotkey_String((KeyNumType)key)); } virtual bool Confirm_Reset(void) override diff --git a/code/ui/uienginehost.cpp b/code/ui/uienginehost.cpp index 0a3ac2408..3bd30cfaa 100644 --- a/code/ui/uienginehost.cpp +++ b/code/ui/uienginehost.cpp @@ -14,6 +14,7 @@ #include "_keyboar.h" #include "_rules.h" #include "_ui.h" +#include "_xmouse.h" #include "audio/audioengine.h" #include "conquer.h" #include "data.h" @@ -26,11 +27,12 @@ #include "movies.h" #include "msgloop.h" #include "rules.h" +#include "sdl/sdlwindow.h" #include "session.h" #include "ui/uishell.h" #include "video.h" #include "voc.h" -#include "wincursor.h" +#include "wwmouse.h" #include @@ -43,21 +45,9 @@ std::string UI_Color_Text(COLORREF color) } -static HCURSOR Window_Cursor(void) -{ - HCURSOR cursor = (HCURSOR)GetClassLongPtr(MainWindow, GCLP_HCURSOR); - return(cursor != NULL ? cursor : LoadCursor(NULL, IDC_ARROW)); -} - - class UIEngineHostClass : public UIShellHostClass { public: - virtual HWND Main_Window(void) const override - { - return(MainWindow); - } - virtual UIFrameRect Frame(void) const override { VideoScaleInfo const & scale = Video_Get_Scale_Info(); @@ -150,48 +140,26 @@ class UIEngineHostClass : public UIShellHostClass virtual bool Take_Capture(void) override { - if (GetCapture() == MainWindow) { + if (Main_Window_Mouse_Captured()) { return(false); } - SetCapture(MainWindow); - return(true); + Main_Window_Capture_Mouse(true); + return(Main_Window_Mouse_Captured()); } virtual void Release_Capture(void) override { - if (GetCapture() == MainWindow) { - ReleaseCapture(); - } - } - - virtual bool Screen_To_Client(int & x, int & y) const override - { - POINT point; - point.x = x; - point.y = y; - if (!ScreenToClient(MainWindow, &point)) { - return(false); - } - x = point.x; - y = point.y; - return(true); + Main_Window_Capture_Mouse(false); } virtual bool Key_Down(int virtualkey) const override { - if (GetSystemMetrics(SM_SWAPBUTTON) != 0) { - if (virtualkey == VK_LBUTTON) { - virtualkey = VK_RBUTTON; - } else if (virtualkey == VK_RBUTTON) { - virtualkey = VK_LBUTTON; - } - } - return((GetAsyncKeyState(virtualkey) & 0x8000) != 0); + return(Main_Window_Key_Down(virtualkey)); } virtual bool Key_Toggled(int virtualkey) const override { - return((GetKeyState(virtualkey) & 1) != 0); + return(Main_Window_Key_Toggled(virtualkey)); } virtual std::string System_Font_Path(char const * face) const override @@ -210,56 +178,30 @@ class UIEngineHostClass : public UIShellHostClass return(path); } - virtual bool Window_Is_Unicode(void) const override + virtual void Apply_Cursor(UICursor cursor) override { - return(IsWindowUnicode(MainWindow) != FALSE); + Main_Window_Set_Cursor(Main_Window_System_Cursor(cursor)); } - virtual unsigned int Text_Code_Page(void) const override + virtual void Restore_Game_Cursor(void) override { - return(GetACP()); + WWMouseClass * mouse = (WWMouseClass *)MouseCursor; + if (mouse != NULL) { + mouse->Refresh_Pointer_Scale(); + } + if (mouse == NULL || !mouse->Show_Game_Pointer()) { + Main_Window_Set_Cursor(Main_Window_System_Cursor(UI_CURSOR_ARROW)); + } } - virtual void Apply_Cursor(UICursor cursor) override + virtual std::string Clipboard_Text(void) const override { - LPCTSTR shape = NULL; - switch (cursor) { - case UI_CURSOR_TEXT: - shape = IDC_IBEAM; - break; - case UI_CURSOR_HAND: - shape = IDC_HAND; - break; - case UI_CURSOR_RESIZE_NS: - shape = IDC_SIZENS; - break; - case UI_CURSOR_RESIZE_EW: - shape = IDC_SIZEWE; - break; - case UI_CURSOR_RESIZE_NESW: - shape = IDC_SIZENESW; - break; - case UI_CURSOR_RESIZE_NWSE: - shape = IDC_SIZENWSE; - break; - case UI_CURSOR_MOVE: - shape = IDC_SIZEALL; - break; - case UI_CURSOR_UNAVAILABLE: - shape = IDC_NO; - break; - default: - break; - } - SetCursor(shape != NULL ? LoadCursor(NULL, shape) : Window_Cursor()); + return(Main_Window_Clipboard_Text()); } - virtual void Restore_Game_Cursor(void) override + virtual void Set_Clipboard_Text(std::string const & text) override { - Win_Cursor_Refresh(); - if (!Win_Cursor_Handle_Set_Cursor()) { - SetCursor(Window_Cursor()); - } + Main_Window_Set_Clipboard_Text(text); } virtual char const * String(int id) const override diff --git a/code/ui/uihost.h b/code/ui/uihost.h index f69758e05..901a56258 100644 --- a/code/ui/uihost.h +++ b/code/ui/uihost.h @@ -30,7 +30,6 @@ class UIShellHostClass { public: virtual ~UIShellHostClass(void) = default; - virtual HWND Main_Window(void) const = 0; virtual UIFrameRect Frame(void) const = 0; virtual void Mark_Overlay_Dirty(void) = 0; virtual void Present_If_Dirty(void) = 0; @@ -47,15 +46,14 @@ class UIShellHostClass virtual void Focus_Main_Window(void) = 0; virtual bool Take_Capture(void) = 0; virtual void Release_Capture(void) = 0; - virtual bool Screen_To_Client(int & x, int & y) const = 0; virtual bool Key_Down(int virtualkey) const = 0; virtual bool Key_Toggled(int virtualkey) const = 0; virtual std::string System_Font_Path(char const * face) const = 0; - virtual bool Window_Is_Unicode(void) const = 0; - virtual unsigned int Text_Code_Page(void) const = 0; virtual void Apply_Cursor(UICursor cursor) = 0; virtual void Restore_Game_Cursor(void) = 0; + virtual std::string Clipboard_Text(void) const = 0; + virtual void Set_Clipboard_Text(std::string const & text) = 0; virtual char const * String(int id) const = 0; virtual void Log(char const * text) = 0; diff --git a/code/ui/uiinput.cpp b/code/ui/uiinput.cpp index f4e22726a..a2cca247a 100644 --- a/code/ui/uiinput.cpp +++ b/code/ui/uiinput.cpp @@ -201,50 +201,3 @@ void UIInputStateClass::Reset(void) Keys.fill(UI_INPUT_NONE); Buttons.fill(UI_INPUT_NONE); } - - -UIInputText UIUTF8DecoderClass::Feed(unsigned char byte) -{ - UIInputText result; - - if (Remaining != 0) { - if ((byte & 0xC0) == 0x80) { - Value = (Value << 6) | (byte & 0x3F); - if (--Remaining == 0) { - bool valid = Value >= Minimum && Value <= 0x10FFFF && !(Value >= 0xD800 && Value <= 0xDFFF); - result.Codepoints[result.Count++] = valid ? Value : 0xFFFD; - Reset(); - } - return(result); - } - result.Codepoints[result.Count++] = 0xFFFD; - Reset(); - } - - if (byte < 0x80) { - result.Codepoints[result.Count++] = byte; - } else if (byte >= 0xC2 && byte <= 0xDF) { - Value = byte & 0x1F; - Minimum = 0x80; - Remaining = 1; - } else if (byte >= 0xE0 && byte <= 0xEF) { - Value = byte & 0x0F; - Minimum = 0x800; - Remaining = 2; - } else if (byte >= 0xF0 && byte <= 0xF4) { - Value = byte & 0x07; - Minimum = 0x10000; - Remaining = 3; - } else { - result.Codepoints[result.Count++] = 0xFFFD; - } - return(result); -} - - -void UIUTF8DecoderClass::Reset(void) -{ - Value = 0; - Minimum = 0; - Remaining = 0; -} diff --git a/code/ui/uiinput.h b/code/ui/uiinput.h index 59e3ba4fc..468733ab1 100644 --- a/code/ui/uiinput.h +++ b/code/ui/uiinput.h @@ -33,7 +33,8 @@ enum UICursor UI_CURSOR_RESIZE_NESW, UI_CURSOR_RESIZE_NWSE, UI_CURSOR_MOVE, - UI_CURSOR_UNAVAILABLE + UI_CURSOR_UNAVAILABLE, + UI_CURSOR_COUNT }; @@ -86,23 +87,3 @@ class UIInputStateClass std::array Keys {}; std::array Buttons {}; }; - - -struct UIInputText -{ - std::array Codepoints {}; - unsigned Count = 0; -}; - - -class UIUTF8DecoderClass -{ - public: - UIInputText Feed(unsigned char byte); - void Reset(void); - - private: - char32_t Value = 0; - char32_t Minimum = 0; - unsigned Remaining = 0; -}; diff --git a/code/ui/uishell.cpp b/code/ui/uishell.cpp index 8d252eb56..3aea4195b 100644 --- a/code/ui/uishell.cpp +++ b/code/ui/uishell.cpp @@ -18,6 +18,7 @@ #include "ui/uihost.h" #include "ui/uireveal.h" #include "ui/uiview.h" +#include "windowevent.hh" // windowsx.h defines macros with the names of these RmlUi Element methods. #undef GetFirstChild @@ -76,27 +77,16 @@ class UIHostClockClass : public UIClockClass }; -bool Input_Message(UINT message) +bool Input_Event(WindowEventType type) { - switch (message) { - case WM_MOUSEMOVE: - case WM_LBUTTONDOWN: - case WM_LBUTTONDBLCLK: - case WM_RBUTTONDOWN: - case WM_RBUTTONDBLCLK: - case WM_MBUTTONDOWN: - case WM_MBUTTONDBLCLK: - case WM_XBUTTONDOWN: - case WM_XBUTTONDBLCLK: - case WM_LBUTTONUP: - case WM_RBUTTONUP: - case WM_MBUTTONUP: - case WM_XBUTTONUP: - case WM_MOUSEWHEEL: - case WM_MOUSEHWHEEL: - case WM_KEYDOWN: - case WM_KEYUP: - case WM_CHAR: + switch (type) { + case WINDOW_EVENT_MOUSE_MOVE: + case WINDOW_EVENT_MOUSE_DOWN: + case WINDOW_EVENT_MOUSE_UP: + case WINDOW_EVENT_MOUSE_WHEEL: + case WINDOW_EVENT_KEY_DOWN: + case WINDOW_EVENT_KEY_UP: + case WINDOW_EVENT_TEXT: return(true); default: @@ -105,39 +95,32 @@ bool Input_Message(UINT message) } -int Message_Button(UINT message, WPARAM wparam) +int Button_Virtual_Key(unsigned button) { - switch (message) { - case WM_LBUTTONDOWN: - case WM_LBUTTONDBLCLK: - case WM_LBUTTONUP: - return(0); - - case WM_RBUTTONDOWN: - case WM_RBUTTONDBLCLK: - case WM_RBUTTONUP: - return(1); - - case WM_MBUTTONDOWN: - case WM_MBUTTONDBLCLK: - case WM_MBUTTONUP: - return(2); - - case WM_XBUTTONDOWN: - case WM_XBUTTONDBLCLK: - case WM_XBUTTONUP: - return(GET_XBUTTON_WPARAM(wparam) == XBUTTON1 ? 3 : 4); - - default: - return(-1); - } + static int const keys[UIInputStateClass::BUTTON_COUNT] = { VK_LBUTTON, VK_RBUTTON, VK_MBUTTON, VK_XBUTTON1, VK_XBUTTON2 }; + return(keys[button]); } -int Button_Virtual_Key(unsigned button) +int Rml_Modifiers(int modifiers) { - static int const keys[UIInputStateClass::BUTTON_COUNT] = { VK_LBUTTON, VK_RBUTTON, VK_MBUTTON, VK_XBUTTON1, VK_XBUTTON2 }; - return(keys[button]); + int result = 0; + if ((modifiers & WINDOW_MOD_SHIFT) != 0) { + result |= Rml::Input::KM_SHIFT; + } + if ((modifiers & WINDOW_MOD_CTRL) != 0) { + result |= Rml::Input::KM_CTRL; + } + if ((modifiers & WINDOW_MOD_ALT) != 0) { + result |= Rml::Input::KM_ALT; + } + if ((modifiers & WINDOW_MOD_CAPS) != 0) { + result |= Rml::Input::KM_CAPSLOCK; + } + if ((modifiers & WINDOW_MOD_NUM) != 0) { + result |= Rml::Input::KM_NUMLOCK; + } + return(result); } @@ -267,10 +250,10 @@ void UIShellClass::Drop_Cached_Files(void) } -UIPointerPosition UIShellClass::Pointer_Position(LPARAM clientlparam) const +UIPointerPosition UIShellClass::Pointer_Position(int x, int y) const { UIFrameRect frame = Host.Frame(); - return(UI_Client_To_Overlay(frame.X, frame.Y, frame.Width, frame.Height, GET_X_LPARAM(clientlparam), GET_Y_LPARAM(clientlparam))); + return(UI_Client_To_Overlay(frame.X, frame.Y, frame.Width, frame.Height, x, y)); } @@ -334,7 +317,6 @@ void UIShellClass::Drop_Presses(void) Input.Cancel_Mouse(); Release_UI_Capture(); - Reset_Text(); } @@ -347,14 +329,6 @@ void UIShellClass::Release_UI_Capture(void) } -void UIShellClass::Reset_Text(void) -{ - HighSurrogate = 0; - Utf8.Reset(); - LegacyLead = 0; -} - - int UIShellClass::Key_Modifiers(void) const { int modifiers = 0; @@ -669,7 +643,6 @@ void UIShellClass::Shutdown(void) Drop_Presses(); } Input.Reset(); - Reset_Text(); if (AppliedCursor.has_value()) { Restore_Cursor(); } @@ -827,9 +800,9 @@ void UIShellClass::Render_Overlay(void) } -bool UIShellClass::Handle_Mouse_Move(LPARAM clientlparam) +bool UIShellClass::Handle_Mouse_Move(int x, int y, int modifiers) { - UIPointerPosition position = Pointer_Position(clientlparam); + UIPointerPosition position = Pointer_Position(x, y); UIDev_Mouse_Position(position.X, position.Y); if (UIDev_Wants_Mouse()) { @@ -838,7 +811,7 @@ bool UIShellClass::Handle_Mouse_Move(LPARAM clientlparam) } if (Input.Has_UI_Mouse() || position.Inside) { - Context->ProcessMouseMove(position.X, position.Y, Key_Modifiers()); + Context->ProcessMouseMove(position.X, position.Y, modifiers); MouseInside = position.Inside; Host.Mark_Overlay_Dirty(); } else if (MouseInside) { @@ -851,7 +824,7 @@ bool UIShellClass::Handle_Mouse_Move(LPARAM clientlparam) } -bool UIShellClass::Handle_Button_Down(int button, LPARAM clientlparam) +bool UIShellClass::Handle_Button_Down(int button, int x, int y, int modifiers) { Input.Reconcile_Cancelled_Mouse(Physical_Buttons()); @@ -860,8 +833,7 @@ bool UIShellClass::Handle_Button_Down(int button, LPARAM clientlparam) return(true); } - UIPointerPosition position = Pointer_Position(clientlparam); - int modifiers = Key_Modifiers(); + UIPointerPosition position = Pointer_Position(x, y); bool haduimouse = Input.Has_UI_Mouse(); UIInputOwner owner = UI_INPUT_GAME; @@ -898,9 +870,9 @@ bool UIShellClass::Handle_Button_Down(int button, LPARAM clientlparam) } -bool UIShellClass::Handle_Button_Up(int button, LPARAM clientlparam) +bool UIShellClass::Handle_Button_Up(int button, int x, int y, int modifiers) { - UIPointerPosition position = Pointer_Position(clientlparam); + UIPointerPosition position = Pointer_Position(x, y); UIInputOwner owner = Input.Release_Mouse((unsigned)button); if (owner == UI_INPUT_IMGUI) { @@ -911,7 +883,6 @@ bool UIShellClass::Handle_Button_Up(int button, LPARAM clientlparam) UIDev_Mouse_Button(button, false); } if (owner == UI_INPUT_RML) { - int modifiers = Key_Modifiers(); Context->ProcessMouseMove(position.X, position.Y, modifiers); Context->ProcessMouseButtonUp(button, modifiers); MouseInside = position.Inside; @@ -927,16 +898,9 @@ bool UIShellClass::Handle_Button_Up(int button, LPARAM clientlparam) } -bool UIShellClass::Handle_Wheel(WPARAM wparam, LPARAM screenlparam, bool horizontal) +bool UIShellClass::Handle_Wheel(int x, int y, float delta, bool horizontal, int modifiers) { - int x = GET_X_LPARAM(screenlparam); - int y = GET_Y_LPARAM(screenlparam); - Host.Screen_To_Client(x, y); - - UIFrameRect frame = Host.Frame(); - UIPointerPosition position = UI_Client_To_Overlay(frame.X, frame.Y, frame.Width, frame.Height, x, y); - - float delta = (float)(short)HIWORD(wparam) / (float)WHEEL_DELTA; + UIPointerPosition position = Pointer_Position(x, y); if (!horizontal && UIDev_Active()) { UIDev_Mouse_Position(position.X, position.Y); @@ -951,25 +915,24 @@ bool UIShellClass::Handle_Wheel(WPARAM wparam, LPARAM screenlparam, bool horizon } Rml::Vector2f movement = horizontal ? Rml::Vector2f(delta, 0.0f) : Rml::Vector2f(0.0f, -delta); - bool consumed = !Context->ProcessMouseWheel(movement, Key_Modifiers()); + bool consumed = !Context->ProcessMouseWheel(movement, modifiers); Host.Mark_Overlay_Dirty(); return(consumed); } -bool UIShellClass::Handle_Key(UINT message, WPARAM wparam, LPARAM lparam) +bool UIShellClass::Handle_Key(bool down, int code, bool repeat, int modifiers) { - unsigned virtualkey = (unsigned)(wparam & 0xFF); - int modifiers = Key_Modifiers(); + unsigned virtualkey = (unsigned)(code & 0xFF); Rml::Input::KeyIdentifier key = UI_Key_Identifier((int)virtualkey); - if (message == WM_KEYUP) { + if (!down) { UIInputOwner owner = Input.Release_Key(virtualkey); if (owner == UI_INPUT_IMGUI) { - UIDev_Key(wparam, false); + UIDev_Key((int)virtualkey, false); } else if (owner != UI_INPUT_SUPPRESSED) { if (UIDev_Active()) { - UIDev_Key(wparam, false); + UIDev_Key((int)virtualkey, false); } if (key != Rml::Input::KI_UNKNOWN) { Context->ProcessKeyUp(key, modifiers); @@ -979,20 +942,24 @@ bool UIShellClass::Handle_Key(UINT message, WPARAM wparam, LPARAM lparam) return(UI_Consumes_Input(owner)); } - bool repeat = (lparam & (1 << 30)) != 0; if (!repeat) { Input.Release_Key(virtualkey); } + Rml::Element * textarea = Context->GetFocusElement(); + if (textarea != nullptr && textarea->GetTagName() != "textarea") { + textarea = nullptr; + } + UIInputOwner owner = Input.Key_Owner(virtualkey); if (owner != UI_INPUT_NONE) { if (owner == UI_INPUT_IMGUI) { - UIDev_Key(wparam, true); + UIDev_Key((int)virtualkey, true); } else if (owner == UI_INPUT_RML && key != Rml::Input::KI_UNKNOWN) { Context->ProcessKeyDown(key, modifiers); } } else { - if (UIDev_Key(wparam, true)) { + if (UIDev_Key((int)virtualkey, true)) { owner = UI_INPUT_IMGUI; } else if (!Modals.empty()) { if (key != Rml::Input::KI_UNKNOWN) { @@ -1008,89 +975,19 @@ bool UIShellClass::Handle_Key(UINT message, WPARAM wparam, LPARAM lparam) Input.Press_Key(virtualkey, owner); } - Host.Mark_Overlay_Dirty(); - return(UI_Consumes_Input(owner)); -} - - -bool UIShellClass::Handle_Char(WPARAM wparam) -{ - if (Host.Window_Is_Unicode()) { - return(Feed_Text_Unit((wchar_t)wparam)); - } - return(Feed_Text_Byte((unsigned char)wparam)); -} - - -bool UIShellClass::Feed_Text_Unit(wchar_t unit) -{ - if (unit >= 0xD800 && unit < 0xDC00) { - bool consumed = false; - if (HighSurrogate != 0) { - consumed = Handle_Text(0xFFFD); - } - HighSurrogate = unit; - return(consumed); - } - - char32_t code = unit; - if (unit >= 0xDC00 && unit < 0xE000) { - code = (HighSurrogate != 0) ? 0x10000 + (((char32_t)HighSurrogate - 0xD800) << 10) + ((char32_t)unit - 0xDC00) : 0xFFFD; - } else if (HighSurrogate != 0) { - Handle_Text(0xFFFD); + // A text area takes its newline as typed text, and SDL types none for Enter. + if (owner == UI_INPUT_RML && key == Rml::Input::KI_RETURN && textarea != nullptr && Context->GetFocusElement() == textarea) { + Context->ProcessTextInput('\n'); } - HighSurrogate = 0; - return(Handle_Text(code)); -} - - -bool UIShellClass::Feed_Text_Byte(unsigned char byte) -{ - unsigned int codepage = Host.Text_Code_Page(); - - if (codepage == CP_UTF8) { - UIInputText text = Utf8.Feed(byte); - bool consumed = false; - for (unsigned index = 0; index < text.Count; index++) { - consumed = Handle_Text(text.Codepoints[index]) || consumed; - } - return(consumed); - } - - char bytes[2]; - int count; - if (LegacyLead != 0) { - bytes[0] = (char)LegacyLead; - bytes[1] = (char)byte; - count = 2; - LegacyLead = 0; - } else if (IsDBCSLeadByteEx(codepage, byte)) { - LegacyLead = byte; - return(false); - } else { - bytes[0] = (char)byte; - count = 1; - } - - wchar_t wide[2]; - int converted = MultiByteToWideChar(codepage, MB_ERR_INVALID_CHARS, bytes, count, wide, 2); - char32_t code = 0xFFFD; - if (converted == 1) { - code = wide[0]; - } else if (converted == 2 && wide[0] >= 0xD800 && wide[0] < 0xDC00 && wide[1] >= 0xDC00 && wide[1] < 0xE000) { - code = 0x10000 + (((char32_t)wide[0] - 0xD800) << 10) + ((char32_t)wide[1] - 0xDC00); - } - return(Handle_Text(code)); + Host.Mark_Overlay_Dirty(); + return(UI_Consumes_Input(owner)); } bool UIShellClass::Handle_Text(char32_t code) { - if (code == '\r') { - code = '\n'; - } - if ((code < 32 && code != '\n') || code == 127) { + if (code < 32 || code == 127) { return(false); } @@ -1156,7 +1053,6 @@ UIResult UIShellClass::Run_Modal(UIViewClass & view, UIServiceCallback const & s Drop_Presses(); } Quarantine_Held_Input(); - Reset_Text(); char label[160]; @@ -1233,7 +1129,6 @@ UIResult UIShellClass::Run_Modal(UIViewClass & view, UIServiceCallback const & s Drop_Presses(); } Input.Cancel_UI(); - Reset_Text(); view.Presenter().Discard(); view.Release(); @@ -1375,27 +1270,28 @@ void UIShellClass::Refresh(void) } -/// True when the interface took the message, and the game must not act on it. +/// True when the interface took the event, and the game must not act on it. /// A notification such as a lost capture is acted on and still answered false, because the /// game needs it too. -bool UIShellClass::Handle_Window_Message(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) +bool UIShellClass::Handle_Window_Event(WindowEvent const & event) { - if (!Ready || InHook || hwnd != Host.Main_Window()) { + if (!Ready || InHook) { return(false); } + bool const key = (event.Type == WINDOW_EVENT_KEY_DOWN || event.Type == WINDOW_EVENT_KEY_UP); + #ifdef _DEBUG - if (Host.Developer_Keys_Armed() && (message == WM_KEYDOWN || message == WM_KEYUP) && wparam == VK_F6) { - if (message == WM_KEYDOWN && (lparam & (1 << 30)) == 0) { + if (Host.Developer_Keys_Armed() && key && !event.System && event.VirtualKey == VK_F6) { + if (event.Type == WINDOW_EVENT_KEY_DOWN && !event.Repeat) { Deferred.ToggleDev = true; } return(true); } #endif - if (message == WM_CAPTURECHANGED || message == WM_CANCELMODE) { - bool lost = (message == WM_CANCELMODE) || (HWND)lparam != Host.Main_Window(); - if (lost && Input.Gesture_Owner() != UI_INPUT_NONE) { + if (event.Type == WINDOW_EVENT_CAPTURE_LOST) { + if (Input.Gesture_Owner() != UI_INPUT_NONE) { TookCapture = false; if (InContext) { Deferred.DropPresses = true; @@ -1407,8 +1303,8 @@ bool UIShellClass::Handle_Window_Message(HWND hwnd, UINT message, WPARAM wparam, return(false); } - if (message == WM_ACTIVATEAPP) { - bool activated = (wparam != 0); + if (event.Type == WINDOW_EVENT_FOCUS_GAINED || event.Type == WINDOW_EVENT_FOCUS_LOST) { + bool activated = (event.Type == WINDOW_EVENT_FOCUS_GAINED); if (InContext) { Deferred.DevFocus = activated ? 1 : 0; } else { @@ -1434,16 +1330,14 @@ bool UIShellClass::Handle_Window_Message(HWND hwnd, UINT message, WPARAM wparam, } Input.Cancel_Keys(); Release_UI_Capture(); - Reset_Text(); } else if (Active()) { Quarantine_Held_Input(); - Reset_Text(); } return(false); } - if (message == WM_INPUTLANGCHANGE) { - Reset_Text(); + // System keys, such as those pressed with Alt held, belong to the game. + if (!Input_Event(event.Type) || (key && event.System)) { return(false); } @@ -1452,59 +1346,43 @@ bool UIShellClass::Handle_Window_Message(HWND hwnd, UINT message, WPARAM wparam, } if (ModalClosing) { - return(Input_Message(message)); + return(true); } UIReentryGuardClass hooking(InHook); - bool consumed = false; + bool consumed = Handle_Input_Event(event); - switch (message) { - case WM_MOUSEMOVE: - consumed = Handle_Mouse_Move(lparam); - break; + if (!Modals.empty()) { + consumed = true; + } - case WM_LBUTTONDOWN: - case WM_LBUTTONDBLCLK: - case WM_RBUTTONDOWN: - case WM_RBUTTONDBLCLK: - case WM_MBUTTONDOWN: - case WM_MBUTTONDBLCLK: - case WM_XBUTTONDOWN: - case WM_XBUTTONDBLCLK: - consumed = Handle_Button_Down(Message_Button(message, wparam), lparam); - break; + return(consumed); +} - case WM_LBUTTONUP: - case WM_RBUTTONUP: - case WM_MBUTTONUP: - case WM_XBUTTONUP: - consumed = Handle_Button_Up(Message_Button(message, wparam), lparam); - break; - case WM_MOUSEWHEEL: - consumed = Handle_Wheel(wparam, lparam, false); - break; +bool UIShellClass::Handle_Input_Event(WindowEvent const & event) +{ + switch (event.Type) { + case WINDOW_EVENT_MOUSE_MOVE: + return(Handle_Mouse_Move(event.X, event.Y, Rml_Modifiers(event.Modifiers))); - case WM_MOUSEHWHEEL: - consumed = Handle_Wheel(wparam, lparam, true); - break; + case WINDOW_EVENT_MOUSE_DOWN: + return(Handle_Button_Down((int)event.Button, event.X, event.Y, Rml_Modifiers(event.Modifiers))); - case WM_KEYDOWN: - case WM_KEYUP: - consumed = Handle_Key(message, wparam, lparam); - break; + case WINDOW_EVENT_MOUSE_UP: + return(Handle_Button_Up((int)event.Button, event.X, event.Y, Rml_Modifiers(event.Modifiers))); - case WM_CHAR: - consumed = Handle_Char(wparam); - break; + case WINDOW_EVENT_MOUSE_WHEEL: + return(Handle_Wheel(event.X, event.Y, event.Wheel, event.Horizontal, Rml_Modifiers(event.Modifiers))); - default: - break; - } + case WINDOW_EVENT_KEY_DOWN: + case WINDOW_EVENT_KEY_UP: + return(Handle_Key(event.Type == WINDOW_EVENT_KEY_DOWN, event.VirtualKey, event.Repeat, Rml_Modifiers(event.Modifiers))); - if (!Modals.empty() && Input_Message(message)) { - consumed = true; - } + case WINDOW_EVENT_TEXT: + return(Handle_Text(event.Text)); - return(consumed); + default: + return(false); + } } diff --git a/code/ui/uishell.h b/code/ui/uishell.h index 76a0e32a7..b0b363684 100644 --- a/code/ui/uishell.h +++ b/code/ui/uishell.h @@ -11,7 +11,6 @@ #include "ui/uiinput.h" #include "ui/uiscreen.h" -#include "win.h" #include #include @@ -30,6 +29,7 @@ class UIRmlRenderClass; class UIRmlSystemClass; class UIViewClass; class UIShellHostClass; +struct WindowEvent; using UIServiceCallback = std::function; @@ -62,7 +62,7 @@ class UIShellClass void Tick(void); void Render_Overlay(void); - bool Handle_Window_Message(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + bool Handle_Window_Event(WindowEvent const & event); bool Handle_Set_Cursor(void); Rml::Context * Rml_Context(void) const { return(Context); } @@ -89,28 +89,25 @@ class UIShellClass bool Text_Input_Focused(void) const; void Apply_Dimensions(void); void Drop_Cached_Files(void); - UIPointerPosition Pointer_Position(LPARAM clientlparam) const; + UIPointerPosition Pointer_Position(int x, int y) const; std::array Physical_Buttons(void) const; int Key_Modifiers(void) const; void Quarantine_Held_Input(void); void Reconcile_Held_Input(void); void Drop_Presses(void); void Release_UI_Capture(void); - void Reset_Text(void); bool Pointer_Owned(void) const; void Apply_Cursor_Request(void); void Restore_Cursor(void); bool Prepare_View(UIViewClass & view); void Uncover(UIViewClass * covered); void Drain_Deferred(void); - bool Handle_Mouse_Move(LPARAM clientlparam); - bool Handle_Button_Down(int button, LPARAM clientlparam); - bool Handle_Button_Up(int button, LPARAM clientlparam); - bool Handle_Wheel(WPARAM wparam, LPARAM screenlparam, bool horizontal); - bool Handle_Key(UINT message, WPARAM wparam, LPARAM lparam); - bool Handle_Char(WPARAM wparam); - bool Feed_Text_Unit(wchar_t unit); - bool Feed_Text_Byte(unsigned char byte); + bool Handle_Input_Event(WindowEvent const & event); + bool Handle_Mouse_Move(int x, int y, int modifiers); + bool Handle_Button_Down(int button, int x, int y, int modifiers); + bool Handle_Button_Up(int button, int x, int y, int modifiers); + bool Handle_Wheel(int x, int y, float delta, bool horizontal, int modifiers); + bool Handle_Key(bool down, int code, bool repeat, int modifiers); bool Handle_Text(char32_t code); UIShellHostClass & Host; @@ -142,10 +139,6 @@ class UIShellClass bool TookCapture = false; bool MouseInside = false; - wchar_t HighSurrogate = 0; - UIUTF8DecoderClass Utf8; - unsigned char LegacyLead = 0; - std::optional AppliedCursor; std::vector Modals; diff --git a/code/ui/uiunicode.cpp b/code/ui/uiunicode.cpp deleted file mode 100644 index fb488cb90..000000000 --- a/code/ui/uiunicode.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#include "ui/uiunicode.h" - -#include -#include -#include - - -bool UI_UTF8_To_UTF16(std::string_view text, std::wstring & wide) -{ - wide.clear(); - - if (text.size() > UI_CLIPBOARD_MAX_BYTES || text.size() > INT_MAX) { - return(false); - } - if (text.empty()) { - return(true); - } - - int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), (int)text.size(), nullptr, 0); - if (length == 0) { - return(false); - } - - try { - wide.resize((std::size_t)length); - } catch (std::bad_alloc const &) { - return(false); - } - - if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), (int)text.size(), wide.data(), length) != length) { - wide.clear(); - return(false); - } - return(true); -} - - -bool UI_UTF16_To_UTF8(std::wstring_view wide, std::string & text) -{ - text.clear(); - - if (wide.size() > UI_CLIPBOARD_MAX_BYTES / sizeof(wchar_t) || wide.size() > INT_MAX) { - return(false); - } - if (wide.empty()) { - return(true); - } - - int length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), (int)wide.size(), nullptr, 0, nullptr, nullptr); - if (length == 0) { - return(false); - } - - try { - text.resize((std::size_t)length); - } catch (std::bad_alloc const &) { - return(false); - } - - if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), (int)wide.size(), text.data(), length, nullptr, nullptr) != length) { - text.clear(); - return(false); - } - return(true); -} diff --git a/code/ui/uiunicode.h b/code/ui/uiunicode.h index f1b6d3270..fa6bf85b4 100644 --- a/code/ui/uiunicode.h +++ b/code/ui/uiunicode.h @@ -10,11 +10,6 @@ #pragma once #include -#include -#include constexpr std::size_t UI_CLIPBOARD_MAX_BYTES = 16 * 1024 * 1024; - -bool UI_UTF8_To_UTF16(std::string_view text, std::wstring & wide); -bool UI_UTF16_To_UTF8(std::wstring_view wide, std::string & text); diff --git a/code/video.cpp b/code/video.cpp index fdf14cb33..2037b4525 100644 --- a/code/video.cpp +++ b/code/video.cpp @@ -17,16 +17,18 @@ #include "_surface.h" #include "_ui.h" +#include "_xmouse.h" #include "bgfxbackend.h" #include "dbgprint.h" #include "dsurface.h" #include "globals.h" #include "goptions.h" #include "misc.h" +#include "sdl/sdlwindow.h" #include "surface.h" #include "ui/uishell.h" #include "videodirty.h" -#include "wincursor.h" +#include "wwmouse.h" #include #include @@ -68,6 +70,14 @@ static std::uint64_t _PresentCount = 0; static std::uint64_t _FrameUploadCount = 0; +static void Refresh_Mouse_Pointer(void) +{ + if (MouseCursor != NULL) { + ((WWMouseClass *)MouseCursor)->Refresh_Pointer_Scale(); + } +} + + /// /// Works out the shortest sensible gap between presents from the display's refresh rate. /// @@ -193,7 +203,6 @@ void Video_Shutdown(void) return; } - Win_Cursor_Shutdown(); Backend_Shutdown(); _Initialized = false; _Dirty.Reset(); @@ -228,7 +237,7 @@ bool Video_Set_Mode(int width, int height) VideoModeHeight = height; Update_Scale_Info(); - Win_Cursor_Refresh(); + Refresh_Mouse_Pointer(); UIShell.On_Video_Change(); _Dirty.Invalidate_Frame(); return(true); @@ -256,7 +265,7 @@ void Video_On_Resize(int drawablewidth, int drawableheight) _ScaleInfo.DrawableHeight = drawableheight; Backend_On_Resize(drawablewidth, drawableheight); Update_Scale_Info(); - Win_Cursor_Refresh(); + Refresh_Mouse_Pointer(); UIShell.On_Video_Change(); Video_Mark_Overlay_Dirty(); } @@ -299,7 +308,7 @@ static void Present(void) if (!_Initialized || _Presenting || VisibleSurface == NULL) { return; } - if (MainWindow != NULL && IsIconic(MainWindow)) { + if (Main_Window_Minimized()) { return; } diff --git a/code/vidscale.cpp b/code/vidscale.cpp index 52b8e4d4c..cefdb9388 100644 --- a/code/vidscale.cpp +++ b/code/vidscale.cpp @@ -12,7 +12,6 @@ #include "vidscale.h" #include "video.h" -#include "win.h" #include @@ -35,13 +34,13 @@ bool Video_Scaling_Active(void) /// pulled onto its edge. /// /// The position to convert in place. -void Window_Point_To_Game(POINT & point) +void Window_Point_To_Game(Point2D & point) { VideoScaleInfo const & scale = Video_Get_Scale_Info(); if (scale.DestWidth > 0 && scale.DestHeight > 0) { - point.x = (LONG)floor((point.x - scale.DestX) * (double)scale.GameWidth / (double)scale.DestWidth); - point.y = (LONG)floor((point.y - scale.DestY) * (double)scale.GameHeight / (double)scale.DestHeight); + point.X = (int)floor((point.X - scale.DestX) * (double)scale.GameWidth / (double)scale.DestWidth); + point.Y = (int)floor((point.Y - scale.DestY) * (double)scale.GameHeight / (double)scale.DestHeight); } } @@ -51,77 +50,27 @@ void Window_Point_To_Game(POINT & point) /// /// The position to convert in place. It comes back at the top left /// corner of the area the frame pixel covers on screen. -void Game_Point_To_Window(POINT & point) +void Game_Point_To_Window(Point2D & point) { VideoScaleInfo const & scale = Video_Get_Scale_Info(); if (scale.GameWidth > 0 && scale.GameHeight > 0) { - point.x = scale.DestX + (LONG)floor(point.x * (double)scale.DestWidth / (double)scale.GameWidth); - point.y = scale.DestY + (LONG)floor(point.y * (double)scale.DestHeight / (double)scale.GameHeight); + point.X = scale.DestX + (int)floor(point.X * (double)scale.DestWidth / (double)scale.GameWidth); + point.Y = scale.DestY + (int)floor(point.Y * (double)scale.DestHeight / (double)scale.GameHeight); } } -/// -/// Converts a position on the desktop into one in the frame. -/// -/// The position to convert in place. -void Screen_Point_To_Game(POINT & point) -{ - ScreenToClient(MainWindow, &point); - Window_Point_To_Game(point); -} - - -/// -/// Converts a position in the frame into one on the desktop. -/// -/// The position to convert in place. -void Game_Point_To_Screen(POINT & point) -{ - Game_Point_To_Window(point); - ClientToScreen(MainWindow, &point); -} - - /// /// Pulls a position onto the frame if it lies outside it. /// /// The position to clamp in place. -void Clamp_To_Game(POINT & point) +void Clamp_To_Game(Point2D & point) { VideoScaleInfo const & scale = Video_Get_Scale_Info(); - if (point.x < 0) point.x = 0; - if (point.y < 0) point.y = 0; - if (scale.GameWidth > 0 && point.x >= scale.GameWidth) point.x = scale.GameWidth - 1; - if (scale.GameHeight > 0 && point.y >= scale.GameHeight) point.y = scale.GameHeight - 1; -} - - -/// -/// Fetches where the cursor is, in the frame. -/// -/// A dialog control to report the position relative to, or NULL for -/// the frame itself. The controls sit at positions in the frame rather than in the -/// window, so their own client coordinates are frame coordinates too. -/// Receives the position, pulled onto the frame. -void Get_Logical_Cursor_Pos(HWND window, POINT & point) -{ - GetCursorPos(&point); - Screen_Point_To_Game(point); - Clamp_To_Game(point); - - if (window != NULL && window != MainWindow) { - RECT window_rect; - GetWindowRect(window, &window_rect); - - POINT origin; - origin.x = 0; - origin.y = 0; - ClientToScreen(MainWindow, &origin); - - point.x -= window_rect.left - origin.x; - point.y -= window_rect.top - origin.y; - } + if (point.X < 0) point.X = 0; + if (point.Y < 0) point.Y = 0; + if (scale.GameWidth > 0 && point.X >= scale.GameWidth) point.X = scale.GameWidth - 1; + if (scale.GameHeight > 0 && point.Y >= scale.GameHeight) point.Y = scale.GameHeight - 1; } diff --git a/code/vidscale.h b/code/vidscale.h index 1a0ff906a..9490968e1 100644 --- a/code/vidscale.h +++ b/code/vidscale.h @@ -9,19 +9,16 @@ // Conversions between the window's own pixels and the frame the game draws in. The two // differ whenever the frame is scaled or letterboxed to fit the window, so anything that -// reads a position from Windows has to come through here before the game sees it. +// reads a position from the system has to come through here before the game sees it. #pragma once -#include "win.h" +#include "point.h" bool Video_Scaling_Active(void); -void Window_Point_To_Game(POINT & point); -void Game_Point_To_Window(POINT & point); -void Screen_Point_To_Game(POINT & point); -void Game_Point_To_Screen(POINT & point); +void Window_Point_To_Game(Point2D & point); +void Game_Point_To_Window(Point2D & point); -void Clamp_To_Game(POINT & point); -void Get_Logical_Cursor_Pos(HWND window, POINT & point); +void Clamp_To_Game(Point2D & point); diff --git a/code/vqa.cpp b/code/vqa.cpp index a8e48ae7f..18a401c79 100644 --- a/code/vqa.cpp +++ b/code/vqa.cpp @@ -21,6 +21,7 @@ #include "goptions.h" #include "mixfile.h" #include "movies.h" +#include "sdl/sdlwindow.h" #include "session.h" #include "unvqtblc.h" #include "vector.h" @@ -43,19 +44,9 @@ intptr_t __cdecl VQACCFileHandler(VQAHandle * vqa, long action, void * buffer, l intptr_t __cdecl VQAEventHandler(VQAHandle * vqa, long action, void * buffer, long nbytes); intptr_t __cdecl VQAMemoryHandler(VQAHandle * vqa, long action, void * buffer, long nbytes); -bool VQA_Message_Handler(void) +void VQA_Message_Handler(void) { - MSG msg; - - if (PeekMessageW(&msg, NULL, 0, 0, PM_NOREMOVE)) { - if (GetMessageW(&msg, NULL, 0, 0)) { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } else { - return(false); - } - } - return(true); + Main_Window_Pump_Events(); } diff --git a/code/vqa.h b/code/vqa.h index 3337ec551..8e272723e 100644 --- a/code/vqa.h +++ b/code/vqa.h @@ -167,4 +167,4 @@ class VQAClass }; /* VQAClass */ -bool VQA_Message_Handler(void); +void VQA_Message_Handler(void); diff --git a/code/win.h b/code/win.h index 2a817ac46..7c2ae2c90 100644 --- a/code/win.h +++ b/code/win.h @@ -42,8 +42,6 @@ #include #include -extern int ShowCommand; extern HINSTANCE ProgramInstance; extern HWND MainWindow; -extern HWND UnusedWindow; extern bool GameInFocus; diff --git a/code/wincursor.cpp b/code/wincursor.cpp deleted file mode 100644 index 90eec3b09..000000000 --- a/code/wincursor.cpp +++ /dev/null @@ -1,311 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#include "always.h" - -#include "wincursor.h" - -#include "_convert.h" -#include "_xmouse.h" -#include "convert.h" -#include "globals.h" -#include "goptions.h" -#include "shapeset.h" -#include "video.h" -#include "win.h" -#include "xmouse.h" - -#include - - -struct CursorCacheEntry -{ - ShapeSet const * Shape; - int Frame; - int HotX; - int HotY; - HCURSOR Cursor; -}; - -// One cursor per shape frame the game actually asks for. MOUSE.SHP holds a few hundred -// of them, so the cache is emptied rather than grown when it fills. -static CursorCacheEntry _CursorCache[384]; -static int _CursorCacheCount = 0; -static int _CacheScale = 0; - -static ShapeSet const * _CurrentShape = NULL; -static int _CurrentFrame = 0; -static int _CurrentHotX = 0; -static int _CurrentHotY = 0; -static HCURSOR _CurrentCursor = NULL; -static bool _CursorVisible = true; - - -/// -/// Works out how much larger than its shape the cursor should be drawn. -/// -/// int; A whole multiple between one and eight. -static int Cursor_Scale(void) -{ - if (Options.CursorScale < 0) { - return(1); - } - - if (Options.CursorScale > 0) { - return(Options.CursorScale > 8 ? 8 : Options.CursorScale); - } - - VideoScaleInfo const & scale = Video_Get_Scale_Info(); - float smaller = scale.ScaleX < scale.ScaleY ? scale.ScaleX : scale.ScaleY; - - int result = (int)(smaller + 0.5f); - if (result < 1) result = 1; - if (result > 8) result = 8; - return(result); -} - - -/// -/// Draws one shape frame into a Windows cursor. -/// The canvas covers the shape's whole frame rather than the trimmed part that holds -/// pixels, so the hotspot, which is measured from the frame's corner, still lands in the -/// right place. Palette entry zero is the transparent one. -/// -/// The cursor, or NULL if it could not be built. -static HCURSOR Build_Cursor(ShapeSet const * shape, int frame, int hotx, int hoty, int scale) -{ - if (shape == NULL || MouseDrawer == NULL) { - return(NULL); - } - - Rect rect = shape->Get_Rect(frame); - unsigned char const * data = (unsigned char const *)shape->Get_Data(frame); - - if (!rect.Is_Valid() || data == NULL) { - return(NULL); - } - - int width = shape->Get_Width() * scale; - int height = shape->Get_Height() * scale; - - if (width <= 0 || height <= 0) { - return(NULL); - } - - BITMAPINFO info; - memset(&info, '\0', sizeof(info)); - info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - info.bmiHeader.biWidth = width; - info.bmiHeader.biHeight = -height; - info.bmiHeader.biPlanes = 1; - info.bmiHeader.biBitCount = 32; - info.bmiHeader.biCompression = BI_RGB; - - void * bits = NULL; - HBITMAP color = CreateDIBSection(NULL, &info, DIB_RGB_COLORS, &bits, NULL, 0); - - if (color == NULL) { - return(NULL); - } - - memset(bits, '\0', width * height * 4); - - // The shapes are palette indices and the primary is 565, so the drawer's table is - // what turns one into the other. - unsigned short const * table = (unsigned short const *)MouseDrawer->Get_Translate_Table(); - - for (int y = 0; y < rect.Height; y++) { - for (int x = 0; x < rect.Width; x++) { - - unsigned char index = data[y * rect.Width + x]; - if (index == 0) { - continue; - } - - unsigned short pixel = table[index]; - unsigned long red = ((pixel >> 11) & 0x1F) << 3; - unsigned long green = ((pixel >> 5) & 0x3F) << 2; - unsigned long blue = (pixel & 0x1F) << 3; - unsigned long argb = 0xFF000000UL | (red << 16) | (green << 8) | blue; - - for (int suby = 0; suby < scale; suby++) { - unsigned long * row = (unsigned long *)bits + ((rect.Y + y) * scale + suby) * width + (rect.X + x) * scale; - for (int subx = 0; subx < scale; subx++) { - row[subx] = argb; - } - } - } - } - - // A color cursor carries its transparency in the alpha channel, but Windows still - // wants a mask bitmap alongside it. - int mask_pitch = ((width + 15) / 16) * 2; - char * mask_bits = new char[mask_pitch * height]; - memset(mask_bits, '\0', mask_pitch * height); - HBITMAP mask = CreateBitmap(width, height, 1, 1, mask_bits); - delete [] mask_bits; - - int cursor_hotx = hotx * scale; - int cursor_hoty = hoty * scale; - if (cursor_hotx < 0) cursor_hotx = 0; - if (cursor_hoty < 0) cursor_hoty = 0; - if (cursor_hotx >= width) cursor_hotx = width - 1; - if (cursor_hoty >= height) cursor_hoty = height - 1; - - ICONINFO icon; - icon.fIcon = FALSE; - icon.xHotspot = cursor_hotx; - icon.yHotspot = cursor_hoty; - icon.hbmMask = mask; - icon.hbmColor = color; - HCURSOR cursor = (HCURSOR)CreateIconIndirect(&icon); - - DeleteObject(mask); - DeleteObject(color); - return(cursor); -} - - -static void Flush_Cursor_Cache(void) -{ - for (int index = 0; index < _CursorCacheCount; index++) { - if (_CursorCache[index].Cursor != NULL) { - DestroyCursor(_CursorCache[index].Cursor); - } - } - - _CursorCacheCount = 0; - _CurrentCursor = NULL; -} - - -/// -/// Selects the cursor for a shape frame, building it if it has not been seen before. -/// -/// The shape set the frame belongs to. -/// Which frame of it to show. -/// The point within the frame that does the pointing. -/// The same, vertically. -/// Should the cursor be shown straight away? -void Win_Cursor_Set(ShapeSet const * shape, int frame, int hotx, int hoty, bool apply) -{ - int scale = Cursor_Scale(); - - if (scale != _CacheScale) { - Flush_Cursor_Cache(); - _CacheScale = scale; - } - - _CurrentShape = shape; - _CurrentFrame = frame; - _CurrentHotX = hotx; - _CurrentHotY = hoty; - - HCURSOR cursor = NULL; - - for (int index = 0; index < _CursorCacheCount; index++) { - CursorCacheEntry & entry = _CursorCache[index]; - if (entry.Shape == shape && entry.Frame == frame) { - if (entry.HotX != hotx || entry.HotY != hoty) { - if (entry.Cursor != NULL) { - DestroyCursor(entry.Cursor); - } - entry.Cursor = Build_Cursor(shape, frame, hotx, hoty, scale); - entry.HotX = hotx; - entry.HotY = hoty; - } - cursor = entry.Cursor; - break; - } - } - - if (cursor == NULL) { - if (_CursorCacheCount >= (int)(sizeof(_CursorCache) / sizeof(_CursorCache[0]))) { - Flush_Cursor_Cache(); - } - - cursor = Build_Cursor(shape, frame, hotx, hoty, scale); - - if (cursor != NULL) { - CursorCacheEntry & entry = _CursorCache[_CursorCacheCount++]; - entry.Shape = shape; - entry.Frame = frame; - entry.HotX = hotx; - entry.HotY = hoty; - entry.Cursor = cursor; - } - } - - _CurrentCursor = cursor; - - if (apply) { - SetCursor(_CursorVisible ? _CurrentCursor : NULL); - } -} - - -/// -/// Shows or hides the pointer. -/// -void Win_Cursor_Set_Visible(bool visible) -{ - _CursorVisible = visible; - - if (_CurrentCursor == NULL) { - return; - } - - if (MouseCursor != NULL && MouseCursor->Is_Captured()) { - SetCursor(visible ? _CurrentCursor : NULL); - } -} - - -/// -/// Puts the game's pointer back after Windows has asked what the cursor should be. -/// -/// bool; Was the cursor the game's to choose? It is not while the game has released -/// the mouse, as it does when it loses the focus, or before it has built a pointer; Windows -/// then keeps its arrow. -bool Win_Cursor_Handle_Set_Cursor(void) -{ - if (MouseCursor == NULL || !MouseCursor->Is_Captured()) { - return(false); - } - - if (_CurrentCursor == NULL) { - return(false); - } - - SetCursor(_CursorVisible ? _CurrentCursor : NULL); - return(true); -} - - -/// -/// Rebuilds the pointer if the window has been resized enough to want a different size. -/// -void Win_Cursor_Refresh(void) -{ - if (_CurrentShape != NULL && Cursor_Scale() != _CacheScale) { - Win_Cursor_Set(_CurrentShape, _CurrentFrame, _CurrentHotX, _CurrentHotY, - MouseCursor != NULL && MouseCursor->Is_Captured()); - } -} - - -/// -/// Releases every cursor the game built. -/// -void Win_Cursor_Shutdown(void) -{ - SetCursor(NULL); - Flush_Cursor_Cache(); - _CurrentShape = NULL; -} diff --git a/code/wincursor.h b/code/wincursor.h deleted file mode 100644 index eb38d8719..000000000 --- a/code/wincursor.h +++ /dev/null @@ -1,26 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -// The mouse pointer, as a real Windows cursor built from the game's own shapes. -// -// Windows composites it over the presented frame, so pointing the mouse costs nothing: -// the cursor never touches a game surface and moving it needs no new frame. - -#pragma once - -#include "win.h" - -class ShapeSet; - - -void Win_Cursor_Set(ShapeSet const * shape, int frame, int hotx, int hoty, bool apply); -void Win_Cursor_Set_Visible(bool visible); -bool Win_Cursor_Handle_Set_Cursor(void); -void Win_Cursor_Refresh(void); -void Win_Cursor_Shutdown(void); diff --git a/code/windowevent.hh b/code/windowevent.hh new file mode 100644 index 000000000..aa8e0daa2 --- /dev/null +++ b/code/windowevent.hh @@ -0,0 +1,84 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + + +enum WindowEventType +{ + WINDOW_EVENT_NONE, + WINDOW_EVENT_MOUSE_MOVE, + WINDOW_EVENT_MOUSE_DOWN, + WINDOW_EVENT_MOUSE_UP, + WINDOW_EVENT_MOUSE_WHEEL, + WINDOW_EVENT_KEY_DOWN, + WINDOW_EVENT_KEY_UP, + WINDOW_EVENT_TEXT, + WINDOW_EVENT_FOCUS_GAINED, + WINDOW_EVENT_FOCUS_LOST, + WINDOW_EVENT_CAPTURE_LOST, + WINDOW_EVENT_EXPOSED, + WINDOW_EVENT_RESIZED, + WINDOW_EVENT_DISPLAY_CHANGED, + WINDOW_EVENT_CLOSE_REQUESTED, +}; + + +// The order matches the interface's button slots. +enum WindowMouseButton +{ + WINDOW_BUTTON_LEFT, + WINDOW_BUTTON_RIGHT, + WINDOW_BUTTON_MIDDLE, + WINDOW_BUTTON_X1, + WINDOW_BUTTON_X2, +}; + + +enum WindowKeyModifier +{ + WINDOW_MOD_SHIFT = 0x01, + WINDOW_MOD_CTRL = 0x02, + WINDOW_MOD_ALT = 0x04, + WINDOW_MOD_CAPS = 0x08, + WINDOW_MOD_NUM = 0x10, +}; + + +// Mouse positions are in window client pixels, before the frame is scaled to the window. +struct WindowEvent +{ + WindowEventType Type = WINDOW_EVENT_NONE; + + int X = 0; + int Y = 0; + WindowMouseButton Button = WINDOW_BUTTON_LEFT; + + // Two or more on the press that completes a double click. + int Clicks = 1; + + // Wheel notches; positive is away from the player, or to the right when Horizontal. + float Wheel = 0.0f; + bool Horizontal = false; + + // The Windows virtual-key code, which is the engine's identity for a key. + int VirtualKey = 0; + + // The modifiers held and the locks on as of this event, on key and mouse events alike. + int Modifiers = 0; + bool Repeat = false; + + // Set for keys Windows reports as system keys: those pressed with Alt but not AltGr, and F10. + bool System = false; + + char32_t Text = 0; + + int Width = 0; + int Height = 0; +}; diff --git a/code/winfix.cpp b/code/winfix.cpp deleted file mode 100644 index ef5c040e9..000000000 --- a/code/winfix.cpp +++ /dev/null @@ -1,819 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2025 Electronic Arts Inc. - * Copyright 2026 OpenTS contributors - * - * Contains material derived from Electronic Arts source code. - * Modified by OpenTS contributors, 2026. - * EA's GPLv3 Section 7 additional terms and supplemental warranty - * disclaimers apply; see LICENSE.md. - ******************************************************************************/ - -/*********************************************************************************************** - *** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S *** - *********************************************************************************************** - * * - * Project Name : Command & Conquer * - * * - * $Archive:: /Renegade Setup/Autorun/WinFix.CPP $* - * * - * $Author:: Maria_l $* - * * - * $Modtime:: 11/15/01 10:44a $* - * * - * $Revision:: 6 $* - * * - *---------------------------------------------------------------------------------------------* - * Functions: * - * Make_Identifier -- Creates a temporary string identifer. * - * WindowsVersionInfo::WindowsVersionInfo -- Windows Version Info constructor. * - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - -#include "always.h" - -#include "winfix.h" - -#include "_xmouse.h" -#include "ini.h" -#include "misc.h" -#include "trim.h" - -#include -#include - -char *TreeView_Text; -HWND TreeView_LastHandle; -static bool s_dragging; -HTREEITEM TreeView_DraggedItem; -HIMAGELIST TreeView_ImageList; - -struct item_data -{ - HWND hwnd1; - HWND hwnd2; - HWND hwnd3; - HTREEITEM item; - POINT point1; - POINT point2; - POINT point3; -}; - -item_data TreeView_get_item_data(DWORD xy); -bool TreeView_On_Mouse_Move(LPARAM lParam); - - -/// -/// Starts dragging an item out of a tree view. -/// This routine asks the dialog that owns the tree for permission with a TVDRAG_BEGIN -/// message, and if it is granted the drag image is built, the mouse is captured, and the -/// edge scroll timer is started. -/// -/// The dialog that owns the tree view. -/// The tree view the item is being dragged out of. -/// The begin drag notification describing the item. -/// bool; Is a drag now under way? -bool TreeView_Begin_Drag(HWND window, HWND hwndTV, LPNMTREEVIEW lpnmtv) -{ - if (lpnmtv->itemNew.lParam != 0) { - if (SendMessage(window, TVDRAG_BEGIN, (WPARAM)hwndTV, (LPARAM)lpnmtv->itemNew.hItem) == 0) { - return(false); - } - TreeView_DraggedItem = lpnmtv->itemNew.hItem; - - TreeView_SelectItem(hwndTV, 0); - - /* - * Tell the tree-view control to create an image to use - * for dragging. - */ - TreeView_ImageList = TreeView_CreateDragImage(hwndTV, lpnmtv->itemNew.hItem); - - /* - * bounding rectangle of item - */ - RECT rcItem; - /// Get the bounding rectangle of the item being dragged. - TreeView_GetItemRect(hwndTV, lpnmtv->itemNew.hItem, &rcItem, false); - - /* - * amount that child items are indented - */ - int indent = TreeView_GetIndent(hwndTV); - - int hotspotx = lpnmtv->ptDrag.x - indent - rcItem.left; - int hotspoty = lpnmtv->ptDrag.y - rcItem.top; - - /* - * Capture the mouse. - */ - SetCapture(window); - - /* - * Start the drag operation. - */ - ImageList_BeginDrag(TreeView_ImageList, 0, hotspotx, hotspoty); - ImageList_DragEnter(window, 0, 0); - - SetTimer(window, 1, 1, NULL); - - /* - * Set a global flag that tells whether dragging is occurring. - */ - s_dragging = true; - TreeView_LastHandle = hwndTV; - } - return(s_dragging); -} - - -/// -/// Finishes a tree view drag operation. -/// This routine tears down the drag image and gives the mouse back, then tells the -/// dialog that owns the tree to take the item with a TVDRAG_DROP message -- but only if -/// the cursor came to rest somewhere that will have it. -/// -/// The cursor position, packed as it arrives with the message. -/// bool; Was a drag actually under way? -bool TreeView_End_Drag(LPARAM lParam) -{ - if (s_dragging) { - - bool ok = TreeView_On_Mouse_Move(lParam); - ImageList_EndDrag(); - - ReleaseCapture(); - - ImageList_Destroy(TreeView_ImageList); - - TreeView_ImageList = NULL; - s_dragging = false; - - item_data data; - data = TreeView_get_item_data(lParam); - - if (ok) { - SendMessage(data.hwnd2, TVDRAG_DROP, (WPARAM)&data, lParam); - } - - KillTimer(data.hwnd2, 1); - - TreeView_SelectDropTarget(TreeView_LastHandle, 0); - - return(true); - } - - return(false); -} - - -/// -/// Fetches the drop context under the cursor during a tree view drag. -/// This routine works out which window the cursor is over and bundles it up with the -/// item being dragged and the cursor position in each of the coordinate spaces the -/// receiver might want. The bundle is what rides along with the TVDRAG messages. -/// -/// The cursor position, packed as it arrives with the message. -/// Returns with the drop context describing what lies under the cursor. -item_data TreeView_get_item_data(DWORD xy) -{ - item_data data; - - POINT point; - point.x = LOWORD(xy); - point.y = HIWORD(xy); - - HWND parent = (HWND)GetWindowLongPtr(TreeView_LastHandle, GWLP_HWNDPARENT); - HWND hwnd1 = ChildWindowFromPoint(parent, point); - HWND hwnd2 = (HWND)GetWindowLongPtr(hwnd1, GWLP_HWNDPARENT); - if (hwnd1 == parent) { - hwnd2 = hwnd1; - } - - RECT rect; - POINT screen; - screen.x = point.x; - screen.y = point.y; - - ClientToScreen(hwnd2, &screen); - data.point1 = screen; - - GetWindowRect(hwnd1, &rect); - screen.x -= rect.left; - screen.y -= rect.top; - - data.hwnd1 = hwnd1; - data.hwnd2 = hwnd2; - data.hwnd3 = TreeView_LastHandle; - data.item = TreeView_DraggedItem; - data.point2 = point; - data.point3 = screen; - return(data); -} - - -/// -/// Scrolls a tree view that an item is being dragged off the end of. -/// This routine is called from the drag timer. Whenever the cursor has strayed above or -/// below the tree, the view is scrolled toward it and the timer is retuned so that the -/// further out the cursor has wandered, the faster the tree scrolls. -/// -/// The timer identifier reported with the message. -void TreeView_handle_item_drag(int timer_id) -{ - if (timer_id == 1 && s_dragging) { - POINT cursor; - GetCursorPos(&cursor); - - RECT rect; - GetWindowRect(TreeView_LastHandle, &rect); - - LONG i1 = rect.top - cursor.y; - LONG i2 = cursor.y - rect.bottom; - - i2 = std::max(rect.top - cursor.y, cursor.y - rect.bottom); - - int i3 = std::max(i2, 0L); - - if (i3 > 0) { - int time = 500 - 40 * i3; - time = std::max(time, 5); - SetTimer((HWND)GetWindowLongPtr(TreeView_LastHandle, GWLP_HWNDPARENT), 1, time, NULL); - HTREEITEM item; - HTREEITEM visible; - - item = TreeView_GetFirstVisible(TreeView_LastHandle); - - if (i1 > 0) { - visible = TreeView_GetPrevVisible(TreeView_LastHandle, item); - } else { - visible = TreeView_GetNextVisible(TreeView_LastHandle, item); - } - - ImageList_DragShowNolock(FALSE); - TreeView_SelectSetFirstVisible(TreeView_LastHandle, visible); - ImageList_DragShowNolock(TRUE); - } - } -} - - -/// -/// Handles mouse movement during a tree view drag. -/// This routine asks the dialog that owns the tree, by way of a TVDRAG_OVER message, -/// whether the item may be dropped where the cursor now rests. The cursor shape is set -/// to suit the answer and the drag image is dragged along behind it. -/// -/// The cursor position, packed as it arrives with the message. -/// bool; Is the cursor over somewhere the item may be dropped? -bool TreeView_On_Mouse_Move(LPARAM lParam) -{ - bool ok; - if (s_dragging) { - - ImageList_DragShowNolock(FALSE); - - item_data data; - data = TreeView_get_item_data(lParam); - - if (SendMessage(data.hwnd2, TVDRAG_OVER, (WPARAM)&data, lParam) != 0) { - SetCursor(LoadCursor(NULL, IDC_ARROW)); - ok = true; - } else { - SetCursor(LoadCursor(NULL, IDC_NO)); - ok = false; - } - - ImageList_DragShowNolock(TRUE); - - RECT rect; - GetWindowRect(data.hwnd2, &rect); - - ImageList_DragMove(data.point1.x - rect.left, data.point1.y - rect.top); - - return(ok); - } - return(false); -} - - -/// -/// Sets the icon of a tree view item that is opening or closing. -/// This routine is called from the item expand notification so that a folder style item -/// can show the appropriate icon for its new state. -/// -/// The expand notification naming the item and what is happening to -/// it. -/// The image to show when the item closes. -/// The image to show when the item opens. -/// bool; Is the item now open? -bool TreeView_set_item_image(HWND window, NMTREEVIEW *nmtv, int collapse_image, int expand_image) -{ - bool ret; - TVITEMA item; - - ret = false; - item.hItem = nmtv->itemNew.hItem; - /// item, image, handle - item.mask = TVIF_HANDLE|TVIF_IMAGE|TVIF_SELECTEDIMAGE; - - switch (nmtv->action) { - - case TVE_EXPAND: - item.iImage = expand_image; - item.iSelectedImage = expand_image; - ret = true; - break; - - case TVE_COLLAPSE: - item.iImage = collapse_image; - item.iSelectedImage = collapse_image; - ret = false; - break; - } - - TreeView_SetItem(window, &item); - return(ret); -} - - -/// -/// Prepares a tree view label for editing. -/// This routine is called when the label editor appears. It caps how much the player may -/// type and remembers the buffer that TreeView_set_item_text should write the finished -/// label back into. -/// -/// The buffer that will receive the edited label. -/// The most characters the player may type. -void TreeView_set_edit_text(HWND window, char *text, WPARAM limit) -{ - HWND ctrl = TreeView_GetEditControl(window); - if (ctrl != NULL) { - SendMessage(ctrl, EM_SETLIMITTEXT, limit, 0); - TreeView_Text = text; - TreeView_LastHandle = window; - } -} - - -/// -/// Commits an edited tree view label. -/// This routine is called when the label editor closes. The new text is trimmed of the -/// white space the player left around it, copied back into the buffer that -/// TreeView_set_edit_text was given, and put back into the item. -/// -/// The label edit notification carrying the new text. -/// Returns with the length of the text accepted, or zero if the edit was -/// abandoned. -int TreeView_set_item_text(NMTVDISPINFO *info) -{ - char * text = info->item.pszText; - if (text != NULL) { - strtrim(text); - strcpy(TreeView_Text, text); - TVITEMA item; - item.hItem = info->item.hItem; - item.mask = TVIF_TEXT; - item.pszText = TreeView_Text; - TreeView_SetItem(TreeView_LastHandle, &item); - return(strlen(TreeView_Text)); - } - return(0); -} - - -/// -/// Handles the WM_HELP message for a dialog. -/// This routine pops up the context help for whichever control the player asked about, -/// drawing the text from SUN.HLP. -/// -/// The help information that arrived with the message. -/// bool; Was the help popup displayed? -BOOL On_WM_HELP(LPARAM help_info) -{ - HELPINFO *info = (HELPINFO *)help_info; - HWND window = (HWND)info->hItemHandle; - return(WinHelp(window, "SUN.HLP", HELP_CONTEXTPOPUP, GetWindowContextHelpId(window))); -} - - -/// -/// Handles the WM_CONTEXTMENU message for a dialog control. -/// This routine puts up the "What's This?" help menu for the control that was right -/// clicked, drawing the text from SUN.HLP. -/// -/// Handle of the control that was clicked. -/// bool; Was the help menu put up? -BOOL On_WM_CONTEXTMENU(WPARAM window) -{ - DWORD identifiers[4]; - - identifiers[3] = identifiers[2] = identifiers[1] = identifiers[0] = 0; - - identifiers[0] = GetWindowLong((HWND)window, GWL_ID); - identifiers[1] = GetWindowContextHelpId((HWND)window); - return(WinHelp((HWND)window, "SUN.HLP", HELP_CONTEXTMENU, (ULONG_PTR)identifiers)); -} - - -char const *last_view_ini_section_name; - -/// -/// Reads one child view's state back from the INI database. -/// This is the child window callback that read_views_from_ini hands to Windows. A tree -/// view has its root items expanded or collapsed as recorded, a list view has its column -/// widths restored, and anything else is left alone. -/// -/// Always true, so the enumeration carries on to the next child. -BOOL CALLBACK read_view_from_ini(HWND window, INIClass const &ini) -{ - char buffer[64]; - char const *section; - - GetClassName(window, buffer, sizeof(buffer)); - - if (strcmp(buffer, WC_TREEVIEW) == 0) { - //if (window != NULL) { - int i = 0; - section = last_view_ini_section_name; - HTREEITEM item = TreeView_GetRoot(window); - while (item != NULL) { - i++; - snprintf(buffer, sizeof(buffer), "TV%d", i); - - if (ini.Get_Bool(section, buffer, false)) { - TreeView_Expand(window, item, TVE_EXPAND); - } else { - TreeView_Expand(window, item, TVE_COLLAPSE); - } - - item = TreeView_GetNextSibling(window, item); - } - //} - //return(true); - } - - GetClassName(window, buffer, sizeof(buffer)); - - if (strcmp(buffer, WC_LISTVIEW) == 0) { - section = last_view_ini_section_name; - if (window != NULL) { - for (int i = 0; i < 10; i++) { - snprintf(buffer, sizeof(buffer), "LV%d", i); - unsigned int width = ListView_GetColumnWidth(window, i); - width = ini.Get_Int(section, buffer, width); - if (width < 1000) { - ListView_SetColumnWidth(window, i, width); - } - } - } - } - - return(true); -} - - -/// -/// Reads a window's position and view layout back from the INI database. -/// This routine restores the position that write_views_to_ini recorded, nudging the -/// window back onto the display if the screen has shrunk since, then visits every child -/// control so each tree view and list view can restore its own state. -/// -/// The INI section holding the window's state. -void read_views_from_ini(char const *section, HWND window, INIClass const &ini) -{ - LONG x; - LONG y; - int screen_x; - int screen_y; - - if (window != NULL) - { - RECT rect; - GetWindowRect(window, &rect); - - x = ini.Get_Int(section, "X", rect.left); - y = ini.Get_Int(section, "Y", rect.top); - - screen_x = GetSystemMetrics(SM_CXSCREEN); - if (x + rect.right - rect.left > screen_x) { - x = rect.left + screen_x - rect.right; - } - screen_y = GetSystemMetrics(SM_CYSCREEN); - if (y + rect.bottom - rect.top > screen_y) { - y = rect.top + screen_y - rect.bottom; - } - - if (x < 0) { - x = 0; - } - if (y < 0) { - y = 0; - } - - SetWindowPos(window, 0, x, y, 0, 0, SWP_NOSIZE|SWP_NOZORDER); - last_view_ini_section_name = section; - EnumChildWindows(window, (WNDENUMPROC)read_view_from_ini, (LPARAM)&ini); - } -} - - -/// -/// Writes one child view's state to the INI database. -/// This is the child window callback that write_views_to_ini hands to Windows. A tree -/// view records which of its root items are expanded, a list view records its column -/// widths, and anything else is left alone. -/// -/// Always true, so the enumeration carries on to the next child. -BOOL CALLBACK write_view_to_ini(HWND window, INIClass &ini) -{ - char buf[32]; - char buffer[64]; - char const *section; - - GetClassName(window, buffer, sizeof(buffer)); - - if (strcmp(buffer, WC_TREEVIEW) == 0) { - //if (window != NULL) { - int i = 0; - - section = last_view_ini_section_name; - HTREEITEM item = TreeView_GetRoot(window); - - while (item != NULL) { - i++; - snprintf(buf, sizeof(buf), "TV%d", i); - - TVITEM *tmp = (TVITEM *)buffer; - tmp->mask = TVIF_HANDLE|TVIF_STATE; - tmp->stateMask = TVIS_EXPANDED; - tmp->hItem = item; - - if (TreeView_GetItem(window, tmp)) { - - bool expanded = false; - if (tmp->state & TVIS_EXPANDED) { - expanded = true; - } - ini.Put_Bool(section, buf, expanded); - } - item = TreeView_GetNextSibling(window, item); - } - //} - //return(true); - } - - GetClassName(window, buffer, sizeof(buffer)); - - if (strcmp(buffer, WC_LISTVIEW) == 0) { - section = last_view_ini_section_name; - if (window != NULL) { - for (int i = 0; i < 10; i++) { - snprintf(buf, sizeof(buf), "LV%d", i); - unsigned int width = ListView_GetColumnWidth(window, i); - if (width < 1000) { - ini.Put_Int(section, buf, width); - } - } - } - } - - return(true); -} - - -/// -/// Writes a window's position and view layout to the INI database. -/// This routine records where the window sits, then visits every child control so that -/// each tree view and list view can store its own state. Read it back in with -/// read_views_from_ini. -/// -/// The INI section to store the window's state under. -void write_views_to_ini(char const *section, HWND window, INIClass &ini) -{ - if (window != NULL) { - RECT rcl; - GetWindowRect(window, &rcl); - ini.Put_Int(section, "X", rcl.left); - ini.Put_Int(section, "Y", rcl.top); - last_view_ini_section_name = section; - EnumChildWindows(window, (WNDENUMPROC)write_view_to_ini, (LPARAM)&ini); - } -} - - -/// -/// Creates a temporary string identifier. -/// This routine is used to build the numbered entry names that the view state is stored -/// under. A NULL prefix yields just the number. -/// -/// The text to put in front of the number, or NULL for none. -/// The number to tack on the end. -/// Returns with a pointer to the assembled identifier. -/// The identifier is built in a static buffer, so use it before calling this -/// routine again. -const char *Make_Identifier(char *str, int num) -{ - static char _buffer[32]; - - if ( str ) - { - snprintf(_buffer, sizeof(_buffer), "%s%d", str, num); - } - else - { - snprintf(_buffer, sizeof(_buffer), "%d", num); - } - return(_buffer); -} - - -/// -/// Handles the WM_MOVING message for a window. -/// This routine keeps a window from being dragged off the edge of the display. The -/// dialog handlers call it from their WM_MOVING case and let Windows carry out the move -/// with the corrected rectangle. -/// -/// The proposed window rectangle, corrected in place. -/// bool; Was the rectangle pulled back onto the screen? -bool On_WM_MOVING(HWND window, WPARAM wparam, LPARAM lparam) -{ - RECT *rcl = (RECT *)lparam; - - bool res = false; - - if (rcl->left < 0) { - rcl->right -= rcl->left; - rcl->left = 0; - res = true; - } - - if (rcl->top < 0) { - rcl->bottom -= rcl->top; - rcl->top = 0; - res = true; - } - - if (rcl->right > GetSystemMetrics(SM_CXFULLSCREEN)) - { - rcl->left += GetSystemMetrics(SM_CXFULLSCREEN) - rcl->right; - rcl->right = GetSystemMetrics(SM_CXFULLSCREEN); - res = true; - } - if (rcl->bottom > GetSystemMetrics(SM_CYFULLSCREEN)) - { - rcl->top += GetSystemMetrics(SM_CYFULLSCREEN) - rcl->bottom; - rcl->bottom = GetSystemMetrics(SM_CYFULLSCREEN); - res = true; - } - - return(res); -} - - -/// -/// Finds the combo box entry that carries a particular item data value. -/// This routine stands in for the Windows macro of the same purpose, walking the entries -/// from the starting index onward and stopping at the first one whose attached data -/// matches. -/// -/// The entry to begin the search at. Pass -1 to search from the -/// top of the list. -/// The item data value being looked for. -/// Returns with the index of the matching entry, or -1 if no entry carries that -/// data. -int ComboBox_Find_Item_Data(HWND hwndCtl, WPARAM indexStart, LPARAM data) -{ - int count = SendMessage(hwndCtl, CB_GETCOUNT, 0, 0); - - for (int index = indexStart; index < count; index++) { - LRESULT res = SendMessage(hwndCtl, CB_GETITEMDATA, index, 0); - if (res == data) { - return(index); - } - } - - return(-1); -} - - -/// -/// Takes the mouse for the game, or hands it back to Windows. -/// This routine is used when a window has to be given the mouse for a while. Handing the -/// mouse back also forces a full repaint of the window and everything inside it, since -/// the game cursor will have scribbled over the frame while it had control. -/// -/// The window to repaint when the mouse is handed back. -/// Zero to take the mouse, otherwise hand it back. -void Capture_Or_Release_Mouse(HWND window, short release) -{ - if (MouseCursor != NULL) { - if (release == 0) { - MouseCursor->Capture_Mouse(); - } else { - MouseCursor->Release_Mouse(); - UINT flags = RDW_INVALIDATE|RDW_INTERNALPAINT|RDW_ERASE|RDW_ALLCHILDREN|RDW_UPDATENOW|RDW_FRAME; - RedrawWindow(window, NULL, NULL, flags); - } - } -} - - -void noop(void) -{ - -} - - -/// -/// Centers a window over its owner. -/// This is the convenient form for dialog handlers that have no parent handle at hand. -/// A window with no owner is left where it is. -/// -/// The window to be moved. -void Center_Window_Within_Window(HWND window) -{ - HWND parent = (HWND)GetWindowLongPtr(window, GWLP_HWNDPARENT); - if (parent != NULL) { - Center_Window_Within_Window(window, parent); - } -} - - -/// -/// Centers a window over another window. -/// This routine is used by the dialog handlers to place a dialog over the window that -/// spawned it. The main game window is measured by the current video mode rather than by -/// its client area, so a dialog lands centered on what the player can actually see. -/// -/// The window to be moved. -/// The window to center over. -void Center_Window_Within_Window(HWND window, HWND parent) -{ - RECT rcl; - GetClientRect(parent, &rcl); - - if (parent == MainWindow) { - rcl.right = VideoModeWidth; - rcl.bottom = VideoModeHeight; - } - - ClientToScreen(parent, (LPPOINT)&rcl); - ClientToScreen(parent, (LPPOINT)&rcl.right); - rcl.right -= rcl.left; - rcl.bottom -= rcl.top; - - RECT rect; - GetClientRect(window, &rect); - ClientToScreen(window, (LPPOINT)&rect); - ClientToScreen(window, (LPPOINT)&rect.right); - rect.right -= rect.left; - rect.bottom -= rect.top; - int x = (rcl.right - rect.right + 1) / 2; - int y = (rcl.bottom - rect.bottom + 1) / 2; - - if (x < 0) { - x = 0; - } - if (y < 0) { - y = 0; - } - - SetWindowPos(window, 0, x, y, -1, -1, SWP_NOSIZE|SWP_NOZORDER); -} - - -/// -/// Walks an audio buffer without disturbing it. -/// This routine is called by the sound stream service just before the play buffer is -/// locked and refilled. It reads through the pending sample data and throws the result -/// away, so the buffer is paged in and warm by the time the lock is taken. -/// -/// Pointer to the sample data to walk over. -/// Number of bytes of sample data on hand. -#define AUD_UNCOMP_CHUNK_SIZE 2048 -#define AUD_FILL_SIZE ((unsigned)AUD_UNCOMP_CHUNK_SIZE * sizeof(unsigned short)) -void Prefetch_Audio_Buffer(char *src, int size) -{ - - int n1 = (unsigned short)src % AUD_FILL_SIZE; - int n2 = size - n1; - char *ptr = src + n1; - - if (n2 >= 1) { - n2 -= n2 % AUD_FILL_SIZE; - - if (n2 >= 1) { - int n3 = (16 * AUD_FILL_SIZE); - int n4 = 0; - - while (n3 < n2) { - int n5 = ptr[n3 - (16 * AUD_FILL_SIZE)]; - int n6 = ptr[n3] + n4; - n4 = n5 + n6; - n3 += AUD_FILL_SIZE; - } - } - } -} diff --git a/code/winfix.h b/code/winfix.h deleted file mode 100644 index cb0538726..000000000 --- a/code/winfix.h +++ /dev/null @@ -1,199 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2025 Electronic Arts Inc. - * Copyright 2026 OpenTS contributors - * - * Contains material derived from Electronic Arts source code. - * Modified by OpenTS contributors, 2026. - * EA's GPLv3 Section 7 additional terms and supplemental warranty - * disclaimers apply; see LICENSE.md. - ******************************************************************************/ - -/*********************************************************************************************** - *** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S *** - *********************************************************************************************** - * * - * Project Name : Command & Conquer * - * * - * $Archive:: /Renegade Setup/Autorun/WinFix.H $* - * * - * $Author:: Maria_l $* - * * - * $Modtime:: 11/07/01 5:57p $* - * * - * $Revision:: 6 $* - * * - *---------------------------------------------------------------------------------------------* - * Functions: * - * WindowsVersionInfo::Major -- Get the major version of the OS * - * WindowsVersionInfo::Minor -- Get the minor version of the OS * - * WindowsVersionInfo::Build -- Get the build level of the OS * - * WindowsVersionInfo::Info -- Get additional system information * - * WindowsVersionInfo::Is_Win9x -- Determine if we are running on a non-NT system. * - * WindowsVersionInfo::Is_Win95 -- Determine if we are running on a Win95 system. * - * WindowsVersionInfo::Is_Win98 -- Determine if we are running on a Win98 system. * - * WindowsVersionInfo::Is_WinNT -- Determine if we are running on an NT system. * - * WindowsVersionInfo::Is_WinNT5 -- Determine if we are running on an NT 5 system. * - * WindowsVersionInfo::Version -- * - * WindowsVersionInfo::IsOSR2Release -- * - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - -#pragma once - -#include "win.h" - -#include - -/* - * Custom messages for the TreeView drag-and-drop helpers below. The helpers - * send these to the dialog that owns the TreeView so it can approve and act on - * a drag. wParam/lParam are documented per message; all expect the receiver to - * return a BOOL where noted. - */ -#define TVDRAG_OVER (WM_USER + 200) /// drag-over hit-test. wParam = item_data*, lParam = cursor xy. Returns BOOL (valid drop target). -#define TVDRAG_BEGIN (WM_USER + 201) /// begin-drag query. wParam = HWND tree, lParam = HTREEITEM. Returns BOOL (allow drag). -#define TVDRAG_DROP (WM_USER + 202) /// drop on a valid target. wParam = item_data*, lParam = cursor xy. - -void Center_Window_Within_Window(HWND window); -void Center_Window_Within_Window(HWND window, HWND parent); - -BOOL On_WM_HELP(LPARAM help_info); -BOOL On_WM_CONTEXTMENU(WPARAM window); -bool On_WM_MOVING(HWND window, WPARAM wparam, LPARAM lparam); - -int ComboBox_Find_Item_Data(HWND hwndCtl, WPARAM indexStart, LPARAM data); - -void Prefetch_Audio_Buffer(char *src, int size); - -#define Slider_GetPos(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETPOS, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_GetRangeMin(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETRANGEMIN, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_GetRangeMax(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETRANGEMAX, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_GetTic(hwndCtl, index) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETTIC, (WPARAM)(index), (LPARAM)(0L))) -#define Slider_SetTic(hwndCtl, position) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETTIC, (WPARAM)(0L), (LPARAM)(position))) -#define Slider_SetPos(hwndCtl, position) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETPOS, (WPARAM)(TRUE), (LPARAM)(position))) -#define Slider_SetRange(hwndCtl, min, max) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETRANGE, (WPARAM)(TRUE), (LPARAM)(MAKELONG(min, max)))) -#define Slider_SetRangeMin(hwndCtl, min) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETRANGEMIN, (WPARAM)(TRUE), (LPARAM)(min))) -#define Slider_SetRangeMax(hwndCtl, max) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETRANGEMAX, (WPARAM)(TRUE), (LPARAM)(max))) -#define Slider_ClearTics(hwndCtl, redraw) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_CLEARTICS, (WPARAM)(redraw), (LPARAM)(0L))) -#define Slider_SetSel(hwndCtl, minSel, maxSel) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETSEL, (WPARAM)(TRUE), (LPARAM)(MAKELONG(minSel, maxSel)))) -#define Slider_SetSelStart(hwndCtl, start) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETSELSTART, (WPARAM)(0L), (LPARAM)(start))) -#define Slider_SetSelEnd(hwndCtl, end) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETSELEND, (WPARAM)(0L), (LPARAM)(end))) -#define Slider_GetPTics(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETPTICS, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_GetTicPos(hwndCtl, index) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETTICPOS, (WPARAM)(index), (LPARAM)(0L))) -#define Slider_GetNumTics(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETNUMTICS, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_GetSelStart(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETSELSTART, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_GetSelEnd(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETSELEND, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_ClearSel(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_CLEARSEL, (WPARAM)(TRUE), (LPARAM)(0L))) -#define Slider_SetTicFreq(hwndCtl, freq) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETTICFREQ, (WPARAM)(freq), (LPARAM)(0L))) -#define Slider_SetPageSize(hwndCtl, pagesize) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETPAGESIZE, (WPARAM)(0L), (LPARAM)(pagesize))) -#define Slider_GetPageSize(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETPAGESIZE, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_SetLineSize(hwndCtl, linesize) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETLINESIZE, (WPARAM)(0L), (LPARAM)(linesize))) -#define Slider_GetLineSize(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETLINESIZE, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_GetThumbRect(hwndCtl, lprc) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETTHUMBRECT, (WPARAM)(0L), (LPARAM)(lprc))) -#define Slider_GetChannelRect(hwndCtl, lprc) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETCHANNELRECT, (WPARAM)(0L), (LPARAM)(lprc))) -#define Slider_SetThumbLength(hwndCtl, length) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETTHUMBLENGTH, (WPARAM)(0L), (LPARAM)(length))) -#define Slider_GetThumbLength(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETTHUMBLENGTH, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_SetToolTips(hwndCtl, hwndTT) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETTOOLTIPS, (WPARAM)(0L), (LPARAM)(hwndTT))) -#define Slider_GetToolTips(hwndCtl) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETTOOLTIPS, (WPARAM)(0L), (LPARAM)(0L))) -#define Slider_SetTipSide(hwndCtl, side) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETTIPSIDE, (WPARAM)(side), (LPARAM)(0L))) -#define Slider_SetBuddy(hwndCtl, fLeft, hwndBuddy) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_SETBUDDY, (WPARAM)(fLeft), (LPARAM)(hwndBuddy))) -#define Slider_GetBuddy(hwndCtl, fLeft) ((LRESULT)(DWORD)SNDMSG((hwndCtl), TBM_GETBUDDY, (WPARAM)(fLeft), (LPARAM)(0L))) - - -inline void SetSliderRangeAndPos(HWND handle, unsigned short min, unsigned short max, int pos) -{ - Slider_SetRange(handle, min, max); - Slider_SetPos(handle, pos); -} - -#define _SetSliderRangeAndPos(handle, min, max, pos) \ -{ \ - Slider_SetRange(handle, min, max); \ - Slider_SetPos(handle, pos); \ -} \ - -/// These were added to the platform headers after the version this project builds against, -/// so each is defined here when it is missing. - -#ifndef WC_LISTBOX - -/// Listbox Class Name -#define WC_LISTBOXA "ListBox" -#define WC_LISTBOXW L"ListBox" - -#ifdef UNICODE -#define WC_LISTBOX WC_LISTBOXW -#else -#define WC_LISTBOX WC_LISTBOXA -#endif - -#endif /// WC_LISTBOX - -#ifndef WC_SCROLLBAR - -#define WC_STATICA "Static" -#define WC_STATICW L"Static" - -#ifdef UNICODE -#define WC_STATIC WC_STATICW -#else -#define WC_STATIC WC_STATICA -#endif - -#endif /// WC_SCROLLBAR - -#ifndef WC_SCROLLBAR - -#define WC_SCROLLBARA "ScrollBar" -#define WC_SCROLLBARW L"ScrollBar" - -#ifdef UNICODE -#define WC_SCROLLBAR WC_SCROLLBARW -#else -#define WC_SCROLLBAR WC_SCROLLBARA -#endif - -#endif /// WC_SCROLLBAR - -#ifndef WC_COMBOBOX - -#define WC_COMBOBOXA "ComboBox" -#define WC_COMBOBOXW L"ComboBox" - -#ifdef UNICODE -#define WC_COMBOBOX WC_COMBOBOXW -#else -#define WC_COMBOBOX WC_COMBOBOXA -#endif - -#endif /// WC_COMBOBOX - - -#ifndef WC_BUTTON - -#define WC_BUTTONA "Button" -#define WC_BUTTONW L"Button" - -#ifdef UNICODE -#define WC_BUTTON WC_BUTTONW -#else -#define WC_BUTTON WC_BUTTONA -#endif - -#endif /// WC_BUTTON - - -#ifndef WC_EDIT - -#define WC_EDITA "Edit" -#define WC_EDITW L"Edit" - -#ifdef UNICODE -#define WC_EDIT WC_EDITW -#else -#define WC_EDIT WC_EDITA -#endif - -#endif /// WC_EDIT diff --git a/code/winstub.cpp b/code/winstub.cpp index 55d0c6525..02e93bd36 100644 --- a/code/winstub.cpp +++ b/code/winstub.cpp @@ -44,49 +44,23 @@ #include "winstub.h" -#include "_keyboar.h" #include "_map.h" #include "_rect.h" -#include "_tooltip.h" -#include "_ui.h" #include "audio/audioengine.h" #include "ccfile.h" -#include "cctooltip.h" -#include "conquer.h" #include "convert.h" #include "dbgprint.h" #include "draw.h" -#include "dsurface.h" -#include "except.h" -#include "gamewindow.h" #include "globals.h" -#include "goptions.h" -#include "mainopt.h" #include "misc.h" #include "movie.h" #include "opents_version.h" #include "pcx.h" -#include "queue.h" -#include "resource.h" -#include "session.h" -#include "theme.h" -#include "ui/uishell.h" -#include "video.h" -#include "vidscale.h" #include "win.h" -#include "wincursor.h" -#include "winfix.h" #include "wwmouse.h" -#include "nativewindow.hh" -#include -#include -#include - -int ShowCommand; HWND MainWindow; -HWND UnusedWindow; HINSTANCE ProgramInstance; bool _MouseCaptured; @@ -95,19 +69,6 @@ bool _MouseCaptured; //void output(short,short) //{} -/* - * Taken from later Windows SDK after what is shipped in VS6 - */ - -#ifndef WM_MOUSEWHEEL -#define WM_MOUSEWHEEL (WM_MOUSELAST+1) /// message that will be supported -#endif - -#ifndef GET_WHEEL_DELTA_WPARAM -#define GET_WHEEL_DELTA_WPARAM(wParam) ((short)HIWORD(wParam)) -#endif -/////////////////////////////////////////////////////////// - //unsigned long CCFocusMessage = WM_USER+50; //Private message for receiving application focus extern void VQA_PauseAudio(void); extern void VQA_ResumeAudio(void); @@ -160,278 +121,6 @@ void Focus_Restore(void) } -extern bool InMovie; - - -static bool Is_Mouse_Coordinate_Message(UINT message) -{ - switch (message) { - case WM_MOUSEMOVE: - case WM_LBUTTONDOWN: - case WM_LBUTTONUP: - case WM_LBUTTONDBLCLK: - case WM_RBUTTONDOWN: - case WM_RBUTTONUP: - case WM_RBUTTONDBLCLK: - case WM_MBUTTONDOWN: - case WM_MBUTTONUP: - case WM_MBUTTONDBLCLK: - case WM_MOUSEWHEEL: - case WM_XBUTTONDOWN: - case WM_XBUTTONUP: - case WM_XBUTTONDBLCLK: - return(true); - - default: - return(false); - } -} - - -static LPARAM Frame_Mouse_LParam(UINT message, LPARAM lparam) -{ - if (MainWindow == NULL || !Is_Mouse_Coordinate_Message(message) || !Video_Scaling_Active()) { - return(lparam); - } - - POINT point; - point.x = GET_X_LPARAM(lparam); - point.y = GET_Y_LPARAM(lparam); - - bool const screen_space = (message == WM_MOUSEWHEEL); - if (screen_space) { - ScreenToClient(MainWindow, &point); - } - - Window_Point_To_Game(point); - - if (screen_space) { - Game_Point_To_Screen(point); - } - - return(MAKELPARAM((short)point.x, (short)point.y)); -} - -/// -/// Handles the Windows messages sent to the main game window. -/// This is the window procedure registered for the main window. It offers each -/// message to the network transport, the map and the keyboard handlers, deals with -/// the messages the game must react to itself -- focus changes, painting, tray -/// locking and shutdown -- and passes everything else back to Windows. -/// -/// Returns with the result Windows expects for the message handled. -LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - - LPARAM client_lparam = lParam; - lParam = Frame_Mouse_LParam(message, lParam); - - if (UIShell.Handle_Window_Message(hwnd, message, wParam, client_lparam)) { - return(0); - } - - int low_param = LOWORD(wParam); - - Map.Message_Handler(hwnd, message, wParam, lParam); - - if (MainWindow) { - GetMenu(MainWindow); - } - - switch ( message ) { - // Raised on request so that crash reporting can be exercised from inside window - // procedure dispatch, which the operating system unwinds differently from a call. - case WM_EXCEPTION_TEST: - Exception_Wndproc_Test_Fault(); - return(0); - -// case WM_SYSKEYDOWN: -// Mono_Printf("wparam=%08X lparam=%08X\n", (long)wParam, (long)lParam); - // fall through - -// case WM_MOUSEMOVE: -// case WM_KEYDOWN: -// case WM_SYSKEYUP: -// case WM_KEYUP: -// case WM_LBUTTONDOWN: -// case WM_LBUTTONUP: -// case WM_LBUTTONDBLCLK: -// case WM_MBUTTONDOWN: -// case WM_MBUTTONUP: -// case WM_MBUTTONDBLCLK: -// case WM_RBUTTONDOWN: -// case WM_RBUTTONUP: -// case WM_RBUTTONDBLCLK: -// Keyboard->Message_Handler(hwnd, message, wParam, lParam); -// return(0); - - case WM_SHOWWINDOW: - return(0); - - case WM_PAINT: - Game_Window_On_Paint(GameInFocus == true || WindowedMode == true); - ValidateRect(hwnd, NULL); - break; - - case WM_ERASEBKGND: - return(1); - - case WM_SETCURSOR: - if (LOWORD(lParam) == HTCLIENT && (UIShell.Handle_Set_Cursor() || Win_Cursor_Handle_Set_Cursor())) { - return(TRUE); - } - break; - - case WM_SIZE: - if (wParam != SIZE_MINIMIZED) { - Video_On_Resize(LOWORD(lParam), HIWORD(lParam)); - Video_Set_Refresh_Rate(Win_Window_Refresh_Rate(hwnd)); - if (MouseCursor != NULL) { - ((WWMouseClass *)MouseCursor)->Calc_Confining_Rect(); - } - } - break; - - case WM_DISPLAYCHANGE: - Video_Set_Refresh_Rate(Win_Window_Refresh_Rate(hwnd)); - break; - - case WM_CLOSE: - break; - - case WM_CREATE: - ToolTips = new CCToolTip(hwnd); - if (ToolTips) { - ToolTips->Set_Timer_Delay(500); - } - break; - - case WM_MOVE: - if (WindowedMode == true && MouseCursor != NULL) { - ((WWMouseClass *)MouseCursor)->Calc_Confining_Rect(); - } - break; - - /* - ** Windoze message says we have to shut down. Try and do it cleanly. - */ - case WM_DESTROY: - if (ToolTips != NULL) { - delete ToolTips; - ToolTips = NULL; - } - MainWindow = 0; - - /* - ** If we are shutting down gracefully than flag that the message loop has finished. - ** If this is a forced shutdown (ReadyToQuit == 0) then try and close down everything - ** before we exit. - */ - switch (ReadyToQuit) { - default: - case 1: - ReadyToQuit = 2; - break; - - case 0: - break; - - } - return(0); - - case WM_ACTIVATEAPP: - if (hwnd == MainWindow && GameInFocus != (wParam != 0)) { - GameInFocus = (wParam != 0); - if (!GameInFocus) { - Focus_Loss(); - DebugString("Focus lost\n"); - } else { - Focus_Restore(); - DebugString("Focus gained\n"); - } - } - return(0); - - case WM_RBUTTONUP: - Game_Window_On_Right_Mouse_Up(); - break; - - case WM_MOVING: - return(On_WM_MOVING(hwnd, wParam, lParam)); - - case WM_MOUSEWHEEL: - Game_Window_On_Mouse_Wheel(GET_WHEEL_DELTA_WPARAM(wParam)); - break; - - case WM_SYSCOMMAND: - switch ( wParam ) { - - case SC_CLOSE: - // A running game resigns rather than closing, and keeps its window: the exit - // is played through the queue, and the game ends itself once it arrives. - if (GameActive && PlayerPtr != NULL && !Session.Play) { - Queue_Exit(); - } - return(0); - - case SC_SCREENSAVE: - /* - ** Windoze is about to start the screen saver. If we just return without passing - ** this message to DefWindowProc then the screen saver will not be allowed to start. - */ - return(0); - } - break; - - } - - /* - ** Pass this message through to the keyboard handler. If the message - ** was processed and requires no further action, then return with - ** this information. - */ - if (Keyboard->Message_Handler(hwnd, message, wParam, lParam)) { - return(0); - } - - return(DefWindowProcW (hwnd, message, wParam, lParam)); -} - - -NativeWindow Win_Native_Window(HWND window) -{ - return(NativeWindow{ NATIVE_WINDOW_DEFAULT, nullptr, window }); -} - - -// Client dimensions are physical pixels because the process is per-monitor DPI aware. -bool Win_Window_Drawable_Size(HWND window, int & width, int & height) -{ - RECT client; - if (window == NULL || !GetClientRect(window, &client)) { - return(false); - } - - width = client.right - client.left; - height = client.bottom - client.top; - return(width > 0 && height > 0); -} - - -int Win_Window_Refresh_Rate(HWND window) -{ - int refreshrate = 0; - HDC dc = GetDC(window); - - if (dc != NULL) { - refreshrate = GetDeviceCaps(dc, VREFRESH); - ReleaseDC(window, dc); - } - - return(refreshrate); -} - - /// /// Fetches the build number of this executable. /// This routine is used by the network code to check that every machine joining a @@ -444,126 +133,6 @@ unsigned int Build_Number(void) } -/*********************************************************************************************** - * Create_Main_Window -- opens the MainWindow for C&C * - * * - * * - * * - * INPUT: instance -- handle to program instance * - * * - * OUTPUT: Nothing * - * * - * WARNINGS: None * - * * - * HISTORY: * - * 10/10/95 4:08PM ST : Created * - *=============================================================================================*/ - -#define CC_ICON IDI_SUN -#define CC_CURSOR IDC_CURSOR1 - -#define WINDOW_NAME L"Tiberian Sun" - - -void Create_Main_Window ( HINSTANCE instance , int command_show , int width , int height ) -{ - InitCommonControls(); - - WNDCLASSW wndclass ; - // - // Register the window class - // - - /* - * The dialog controls are hit tested through the main window, so its class has to - * report the double clicks they expect. - */ - wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ; - wndclass.lpfnWndProc = Windows_Procedure ; - wndclass.cbClsExtra = 0 ; - wndclass.cbWndExtra = 0 ; - wndclass.hInstance = instance ; - wndclass.hIcon = LoadIconW (instance, MAKEINTRESOURCEW(CC_ICON)) ; - wndclass.hCursor = LoadCursorW(ProgramInstance, MAKEINTRESOURCEW(CC_CURSOR)); - wndclass.hbrBackground = NULL; - wndclass.lpszMenuName = NULL; ///WINDOW_NAME - wndclass.lpszClassName = WINDOW_NAME; - - RegisterClassW (&wndclass) ; - - - // - // Create our main window - // - /* - * The dialogs paint themselves onto the game's surfaces rather than into their own - * windows, so clipping their regions out of the main window would leave holes where - * they sit. - */ - if (WindowedMode) { - int clientwidth = (Options.WindowWidth > 0) ? Options.WindowWidth : width; - int clientheight = (Options.WindowHeight > 0) ? Options.WindowHeight : height; - - MainWindow = CreateWindowExW ( - 0, - WINDOW_NAME, - WINDOW_NAME, - WS_OVERLAPPEDWINDOW, - 0, - 0, - 0, - 0, - NULL, - NULL, - instance, - NULL ); - - RECT rect; - SetRect(&rect, 0, 0, clientwidth, clientheight); - AdjustWindowRectEx(&rect, GetWindowLong(MainWindow, GWL_STYLE), FALSE, GetWindowLong(MainWindow, GWL_EXSTYLE)); - - int windowwidth = rect.right - rect.left; - int windowheight = rect.bottom - rect.top; - int x = (GetSystemMetrics(SM_CXSCREEN) - windowwidth) / 2; - int y = (GetSystemMetrics(SM_CYSCREEN) - windowheight) / 2; - - MoveWindow(MainWindow, std::max(x, 0), std::max(y, 0), windowwidth, windowheight, 1); - - } else { - /* - * The desktop keeps its own resolution and the window simply covers it. The - * frame is scaled to fit at presentation time. - */ - MainWindow = CreateWindowExW ( - 0, - WINDOW_NAME, - WINDOW_NAME, - WS_POPUP, - 0, - 0, - GetSystemMetrics(SM_CXSCREEN), - GetSystemMetrics(SM_CYSCREEN), - NULL, - NULL, - instance, - NULL ); - } - - ShowWindow (MainWindow, SW_NORMAL); - ShowCommand = command_show; - UpdateWindow (MainWindow); - SetFocus (MainWindow); - - RegisterHotKey(MainWindow, 1, MOD_ALT|MOD_CONTROL|MOD_SHIFT, VK_M); - - SetCursor(LoadCursor(ProgramInstance, MAKEINTRESOURCE(CC_CURSOR))); - - //Misc_Focus_Loss_Function = &Focus_Loss; - //Misc_Focus_Restore_Function = &Focus_Restore; - //Gbuffer_Focus_Loss_Function = &Focus_Loss; -} - - /// /// Loads a title screen picture and centers it on the surface. /// This routine is used by the startup and scenario loading sequences to put some diff --git a/code/winstub.h b/code/winstub.h index ea3ef4518..9871a3dd7 100644 --- a/code/winstub.h +++ b/code/winstub.h @@ -17,12 +17,9 @@ class Surface; class PaletteClass; -struct NativeWindow; -void Create_Main_Window ( HINSTANCE instance , int command_show , int width , int height); -NativeWindow Win_Native_Window(HWND window); -bool Win_Window_Drawable_Size(HWND window, int & width, int & height); -int Win_Window_Refresh_Rate(HWND window); +void Focus_Loss(void); +void Focus_Restore(void); void Load_Title_Screen(char const * name, Surface * surface, PaletteClass * palette); diff --git a/code/wwmouse.cpp b/code/wwmouse.cpp index 927339b02..3a3f54786 100644 --- a/code/wwmouse.cpp +++ b/code/wwmouse.cpp @@ -58,53 +58,140 @@ #include "wwmouse.h" +#include "_convert.h" +#include "convert.h" #include "dbgprint.h" +#include "globals.h" +#include "goptions.h" #include "misc.h" +#include "sdl/sdlwindow.h" +#include "shapeset.h" #include "video.h" #include "vidscale.h" #include "win.h" -#include "wincursor.h" + +#include /// -/// Constructs the mouse handler object. -/// The handler is bound to the game window and derives the confining rectangle from it. -/// The mouse begins in a non-captured state. +/// Works out how much larger than its shape the cursor should be drawn. /// -/// Handle to the game window that the mouse is bound to. -WWMouseClass::WWMouseClass(HWND window) : - MouseState(-1), - IsCaptured(false), - Window(window), - ConfiningRect(RECT_NONE) +/// int; A whole multiple between one and eight. +static int Cursor_Scale(void) { - Calc_Confining_Rect(); + if (Options.CursorScale < 0) { + return(1); + } + + if (Options.CursorScale > 0) { + return(Options.CursorScale > 8 ? 8 : Options.CursorScale); + } + + VideoScaleInfo const & scale = Video_Get_Scale_Info(); + float smaller = scale.ScaleX < scale.ScaleY ? scale.ScaleX : scale.ScaleY; + + int result = (int)(smaller + 0.5f); + if (result < 1) result = 1; + if (result > 8) result = 8; + return(result); } /// -/// Recalculates the screen rectangle that the mouse is confined to. -/// This routine converts the game window's client area into screen coordinates. The mouse -/// capture logic clips the cursor to this rectangle, so the window creation and window -/// move handlers call this routine to keep it current. +/// Draws one shape frame into a cursor. +/// The canvas covers the shape's whole frame rather than the trimmed part that holds +/// pixels, so the hotspot, which is measured from the frame's corner, still lands in the +/// right place. Palette entry zero is the transparent one. /// -void WWMouseClass::Calc_Confining_Rect(void) +/// The cursor, or NULL if it could not be built. +static SDL_Cursor * Build_Cursor(ShapeSet const * shape, int frame, int hotx, int hoty, int scale) { - RECT rect; - GetClientRect(Window, &rect); + if (shape == NULL || MouseDrawer == NULL) { + return(NULL); + } + + Rect rect = shape->Get_Rect(frame); + unsigned char const * data = (unsigned char const *)shape->Get_Data(frame); + + if (!rect.Is_Valid() || data == NULL) { + return(NULL); + } + + int width = shape->Get_Width() * scale; + int height = shape->Get_Height() * scale; + + if (width <= 0 || height <= 0) { + return(NULL); + } + + std::vector bits((size_t)width * height, 0); + + // The shapes are palette indices and the primary is 565, so the drawer's table is + // what turns one into the other. + unsigned short const * table = (unsigned short const *)MouseDrawer->Get_Translate_Table(); + + for (int y = 0; y < rect.Height; y++) { + for (int x = 0; x < rect.Width; x++) { + + unsigned char index = data[y * rect.Width + x]; + if (index == 0) { + continue; + } + + unsigned short pixel = table[index]; + unsigned int red = ((pixel >> 11) & 0x1F) << 3; + unsigned int green = ((pixel >> 5) & 0x3F) << 2; + unsigned int blue = (pixel & 0x1F) << 3; + unsigned int argb = 0xFF000000U | (red << 16) | (green << 8) | blue; + + for (int suby = 0; suby < scale; suby++) { + unsigned int * row = bits.data() + ((rect.Y + y) * scale + suby) * width + (rect.X + x) * scale; + for (int subx = 0; subx < scale; subx++) { + row[subx] = argb; + } + } + } + } - POINT point; - point.x = rect.left; - point.y = rect.top; - ClientToScreen(Window, &point); + int cursor_hotx = hotx * scale; + int cursor_hoty = hoty * scale; + if (cursor_hotx < 0) cursor_hotx = 0; + if (cursor_hoty < 0) cursor_hoty = 0; + if (cursor_hotx >= width) cursor_hotx = width - 1; + if (cursor_hoty >= height) cursor_hoty = height - 1; + + return(Main_Window_Create_Cursor(bits.data(), width, height, cursor_hotx, cursor_hoty)); +} - POINT lr; - lr.x = rect.right; - lr.y = rect.bottom; - ClientToScreen(Window, &lr); - ConfiningRect = Rect(point.x, point.y, lr.x-point.x, lr.y-point.y); - DebugString("Calc_Confining_Rect(%d,%d,%d,%d)\n", point.x, point.y, lr.x-point.x, lr.y-point.y); +/// +/// Constructs the mouse handler object for the main window, which must already exist. +/// The mouse begins in a non-captured state. +/// +WWMouseClass::WWMouseClass(void) : + MouseState(-1), + IsCaptured(false), + ReleasedState(0), + CursorShape(NULL), + CursorCacheScale(0), + CurrentShape(NULL), + CurrentFrame(0), + CurrentHotX(0), + CurrentHotY(0), + CurrentCursor(NULL), + CursorVisible(true) +{ +} + + +/// +/// Destroys the mouse handler and every cursor it built. Delete it before Main_Window_Destroy. +/// +WWMouseClass::~WWMouseClass(void) +{ + Main_Window_Set_Cursor(NULL); + Flush_Cursor_Cache(); + CurrentShape = NULL; } @@ -127,9 +214,7 @@ void WWMouseClass::Calc_Confining_Rect(void) int WWMouseClass::Get_Mouse_State(void) const { if (!Is_Captured()) { - ShowCursor(FALSE); - int state = ShowCursor(TRUE); - return(state); + return(ReleasedState); } return(MouseState); } @@ -160,7 +245,7 @@ int WWMouseClass::Get_Mouse_State(void) const void WWMouseClass::Set_Cursor(Point2D const & hotspot, ShapeSet const * cursor, int shape) { if (cursor != NULL) { - Win_Cursor_Set(cursor, shape, hotspot.X, hotspot.Y, Is_Captured()); + Select_Cursor(cursor, shape, hotspot.X, hotspot.Y, Is_Captured()); } } @@ -182,11 +267,12 @@ void WWMouseClass::Set_Cursor(Point2D const & hotspot, ShapeSet const * cursor, void WWMouseClass::Show_Mouse(void) { if (!Is_Captured()) { - ShowCursor(TRUE); + ReleasedState++; + Show_Released_Pointer(); } else { MouseState++; if (MouseState > 0) MouseState = 0; - Win_Cursor_Set_Visible(!Is_Hidden()); + Set_Cursor_Visible(!Is_Hidden()); } } @@ -209,10 +295,11 @@ void WWMouseClass::Show_Mouse(void) void WWMouseClass::Hide_Mouse(void) { if (!Is_Captured()) { - ShowCursor(FALSE); + ReleasedState--; + Show_Released_Pointer(); } else { MouseState--; - Win_Cursor_Set_Visible(!Is_Hidden()); + Set_Cursor_Visible(!Is_Hidden()); } } @@ -240,22 +327,18 @@ void WWMouseClass::Capture_Mouse(void) Hide_Mouse(); IsCaptured = true; - /* - * The game's pointer is the O/S pointer, so its display count has to come - * back up; it was left negative while the game drew a pointer of its own. - */ - while (ShowCursor(TRUE) < 0) {} + // Hide_Mouse above counted against the released pointer, which starts over for the + // next release. + if (ReleasedState < 0) { + ReleasedState = 0; + } /* * There is no exclusive display mode any more, so the pointer is kept inside * the window by hand while the game covers the screen. */ if (!WindowedMode) { - RECT clip_rect; - GetClientRect(Window, &clip_rect); - ClientToScreen(Window, (LPPOINT)&clip_rect.left); - ClientToScreen(Window, (LPPOINT)&clip_rect.right); - ClipCursor(&clip_rect); + Main_Window_Confine_Cursor(true); } Show_Mouse(); @@ -290,9 +373,11 @@ void WWMouseClass::Release_Mouse(void) DebugString("Release_Mouse()\n"); Hide_Mouse(); IsCaptured = false; - ClipCursor(NULL); - if (GetCapture() == Window) ReleaseCapture(); - while (ShowCursor(TRUE) < 0) {} + Main_Window_Confine_Cursor(false); + Main_Window_Capture_Mouse(false); + if (ReleasedState < 0) { + ReleasedState = 0; + } Show_Mouse(); } } @@ -351,24 +436,34 @@ void WWMouseClass::Conditional_Show_Mouse(void) * * * OUTPUT: none * * * - * WARNINGS: The coordinates will be bound as well as transformed by the confining rectangle.* + * WARNINGS: The coordinates will be bound as well as transformed by the window's client area. * * * * HISTORY: * * 03/10/1997 JLB : Created. * *=============================================================================================*/ void WWMouseClass::Convert_Coordinate(int & x, int & y) const { - /* - ** Convert the mouse position to legal bounds. - */ - POINT point; - point.x = x - ConfiningRect.X; - point.y = y - ConfiningRect.Y; + int left = 0; + int top = 0; + int width = 0; + int height = 0; + Main_Window_Client_Rect(left, top, width, height); + + x -= left; + y -= top; + Client_To_Game(x, y); +} + + +// Converts a position in the window's client area into one held inside the frame. +void WWMouseClass::Client_To_Game(int & x, int & y) const +{ + Point2D point(x, y); Window_Point_To_Game(point); VideoScaleInfo const & scale = Video_Get_Scale_Info(); - x = point.x; - y = point.y; + x = point.X; + y = point.Y; if (x < 0) x = 0; if (y < 0) y = 0; if (x >= scale.GameWidth) x = scale.GameWidth-1; @@ -376,6 +471,13 @@ void WWMouseClass::Convert_Coordinate(int & x, int & y) const } +// Shows the window's arrow while the released pointer's show count allows it. +void WWMouseClass::Show_Released_Pointer(void) const +{ + Main_Window_Set_Cursor(ReleasedState >= 0 ? Main_Window_System_Cursor(UI_CURSOR_ARROW) : NULL); +} + + /*********************************************************************************************** * WWMouseClass::Get_Bounded_Position -- Fetches the mouse position from the O/S. * * * @@ -395,9 +497,104 @@ void WWMouseClass::Get_Bounded_Position(int & x, int & y) const /* ** Get the mouse's current real cursor position */ - POINT pt; - GetCursorPos(&pt); // get the current cursor position - x = pt.x; - y = pt.y; - Convert_Coordinate(x, y); + x = 0; + y = 0; + Main_Window_Cursor_Position(x, y); + Client_To_Game(x, y); +} + + +void WWMouseClass::Flush_Cursor_Cache(void) +{ + for (CachedCursor const & entry : CursorCache) { + Main_Window_Destroy_Cursor(entry.Cursor); + } + + CursorCache.clear(); + CursorShape = NULL; + CurrentCursor = NULL; +} + + +void WWMouseClass::Select_Cursor(ShapeSet const * shape, int frame, int hotx, int hoty, bool apply) +{ + int scale = Cursor_Scale(); + + if (shape != CursorShape || scale != CursorCacheScale) { + Flush_Cursor_Cache(); + CursorShape = shape; + CursorCacheScale = scale; + CursorCache.resize(shape->Get_Count() > 0 ? (size_t)shape->Get_Count() : 0); + } + + CurrentShape = shape; + CurrentFrame = frame; + CurrentHotX = hotx; + CurrentHotY = hoty; + + SDL_Cursor * cursor = NULL; + + if (frame >= 0 && frame < (int)CursorCache.size()) { + CachedCursor & entry = CursorCache[frame]; + if (entry.Cursor != NULL && (entry.HotX != hotx || entry.HotY != hoty)) { + Main_Window_Destroy_Cursor(entry.Cursor); + entry.Cursor = NULL; + } + if (entry.Cursor == NULL) { + entry.Cursor = Build_Cursor(shape, frame, hotx, hoty, scale); + entry.HotX = hotx; + entry.HotY = hoty; + } + cursor = entry.Cursor; + } + + CurrentCursor = cursor; + + if (apply) { + Main_Window_Set_Cursor(CursorVisible ? CurrentCursor : NULL); + } +} + + +void WWMouseClass::Set_Cursor_Visible(bool visible) +{ + CursorVisible = visible; + + if (!Is_Captured()) { + return; + } + + // Until the game builds a pointer, the window's arrow stands in for it. + SDL_Cursor * cursor = (CurrentCursor != NULL) ? CurrentCursor : Main_Window_System_Cursor(UI_CURSOR_ARROW); + Main_Window_Set_Cursor(visible ? cursor : NULL); +} + + +/// +/// Shows the game's pointer over the window. +/// +/// False while the mouse is released or before a pointer has been built. +bool WWMouseClass::Show_Game_Pointer(void) +{ + if (!Is_Captured()) { + return(false); + } + + if (CurrentCursor == NULL) { + return(false); + } + + Main_Window_Set_Cursor(CursorVisible ? CurrentCursor : NULL); + return(true); +} + + +/// +/// Rebuilds the pointer if the window has been resized enough to want a different size. +/// +void WWMouseClass::Refresh_Pointer_Scale(void) +{ + if (CurrentShape != NULL && Cursor_Scale() != CursorCacheScale) { + Select_Cursor(CurrentShape, CurrentFrame, CurrentHotX, CurrentHotY, Is_Captured()); + } } diff --git a/code/wwmouse.h b/code/wwmouse.h index e40ea0af2..914aa858d 100644 --- a/code/wwmouse.h +++ b/code/wwmouse.h @@ -31,10 +31,12 @@ #pragma once -#include "win.h" #include "xmouse.h" +#include + class ShapeSet; +struct SDL_Cursor; /* ** Handles the mouse as it relates to the C&C game engine. It is expected that only @@ -47,7 +49,8 @@ class WWMouseClass : public Mouse { /* ** Private constructor. */ - WWMouseClass(HWND window); + WWMouseClass(void); + virtual ~WWMouseClass(void) override; /* ** Sets the game-drawn mouse imagery. @@ -82,11 +85,7 @@ class WWMouseClass : public Mouse { */ virtual int Get_Mouse_State(void) const override; - /* - * The position is asked of Windows on demand. There used to be a timer thread - * keeping a copy fresh, but its real job was repainting a software pointer, - * and the pointer is Windows' own now. - */ + // The position is asked of the system on demand. virtual int Get_Mouse_X(void) const override {int x; int y; Get_Bounded_Position(x, y); return(x);} virtual int Get_Mouse_Y(void) const override {int x; int y; Get_Bounded_Position(x, y); return(y);} virtual Point2D Get_Mouse_Point(void) const override {int x; int y; Get_Bounded_Position(x, y); return(Point2D(x, y));} @@ -96,10 +95,8 @@ class WWMouseClass : public Mouse { */ virtual void Convert_Coordinate(int & x, int & y) const override; - /* - ** Recalculate the confining rectangle from the window. - */ - void Calc_Confining_Rect(void); + bool Show_Game_Pointer(void); + void Refresh_Pointer_Scale(void); private: @@ -117,19 +114,37 @@ class WWMouseClass : public Mouse { */ bool IsCaptured; - /* - ** This is the window handle that is used to bind and bias the mouse - ** position. - */ - HWND Window; - - /* - * The screen rectangle that the mouse is confined to while it is captured. - * It tracks the game window's client area. - */ - Rect ConfiningRect; + // The pointer's show count while the mouse is released, which hides the window's + // arrow while it is below zero. + int ReleasedState; + + // The pointer is a system cursor built from the game's shapes, so moving it needs no new frame. + struct CachedCursor + { + int HotX; + int HotY; + SDL_Cursor * Cursor; + }; + + // One slot per frame of CursorShape, holding the cursor once the game has asked for + // that frame at CursorCacheScale. + ShapeSet const * CursorShape; + std::vector CursorCache; + int CursorCacheScale; + + ShapeSet const * CurrentShape; + int CurrentFrame; + int CurrentHotX; + int CurrentHotY; + SDL_Cursor * CurrentCursor; + bool CursorVisible; void Get_Bounded_Position(int & x, int & y) const; + void Client_To_Game(int & x, int & y) const; + void Show_Released_Pointer(void) const; + void Select_Cursor(ShapeSet const * shape, int frame, int hotx, int hoty, bool apply); + void Set_Cursor_Visible(bool visible); + void Flush_Cursor_Cache(void); virtual bool Is_Hidden(void) const override {return(MouseState < 0);} }; diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 436e88b6e..795b2fc6a 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -62,6 +62,13 @@ vendored through `thirdparty/RmlUi`, `thirdparty/freetype`, and and without bzip2, PNG, HarfBuzz, or Brotli; Dear ImGui is compiled from its core sources without any of its bundled backends. +[SDL](https://github.com/libsdl-org/SDL) 3 is vendored through `thirdparty/SDL` +at a tested tag and linked statically. Only its video subsystem, which covers +the window, its events, and keyboard and mouse input, is built. Its audio, +rendering, GPU, camera, joystick, haptic, HIDAPI, sensor, power, dialog, and +tray subsystems are left out, along with its OpenGL, OpenGL ES, and Vulkan +support. + For a fresh clone, use `git clone --recurse-submodules`. Configuration stops with instructions if a submodule is missing. Update a pinned tag in a separate change. @@ -102,6 +109,10 @@ test harnesses build into `/test-bin//`, so `bin/` holds only what the game runs. Compiler and linker intermediates stay in the selected build directory. +The `sdlwindow` test opens a window and moves the pointer into it, so CTest +needs a desktop session. Checks that need the keyboard focus or the pointer +over the window are reported as not run when the window does not get them. + | Configuration | Runtime files | | --- | --- | | Debug | `GameD.exe`, `GameD.pdb`, `GameD.map`, `Language.dll`, `ui/` | diff --git a/manual/changes/deployment-search-folders.md b/manual/changes/deployment-search-folders.md index 659773079..11e24692d 100644 --- a/manual/changes/deployment-search-folders.md +++ b/manual/changes/deployment-search-folders.md @@ -15,7 +15,7 @@ credit: [ZivDero] A deployment can now keep its files in folders and list them in `SearchPaths` under `[Paths]` in `OPENTS.INI`. Without that setting the game searches the `INI`, `MIX` and `Maps` folders, so a deployment sorted into those folders needs no configuration. -A search by file pattern used to stop at the first folder holding a match. The searches for rules files, campaign files, map packs, loose multiplayer maps, and map and movie archives now collect matches from every searched folder. A name found in more than one folder is taken from the folder searched first, whatever order the file system lists files in. The loose `PATCH.MIX` and `EXPAND??.MIX` archives are now found in any searched folder, where only the game's own directory used to count, and are still never read from inside another archive. +A search by file pattern used to stop at the first folder holding a match. The searches for campaign files, map packs, loose multiplayer maps, and map and movie archives now collect matches from every searched folder. A name found in more than one folder is taken from the folder searched first, whatever order the file system lists files in. The loose `PATCH.MIX` and `EXPAND??.MIX` archives are now found in any searched folder, where only the game's own directory used to count, and are still never read from inside another archive. The settings file, the hotkey file, the hall of fame and a saved random map used to be written back to whichever copy the search found. A copy shipped in a searched folder could therefore be overwritten, and resetting the hotkeys could delete it. The game now writes and deletes files only in the user data directory, or in the game's own directory when there is none. diff --git a/manual/changes/sdl3-window-and-input.md b/manual/changes/sdl3-window-and-input.md new file mode 100644 index 000000000..e812750fd --- /dev/null +++ b/manual/changes/sdl3-window-and-input.md @@ -0,0 +1,23 @@ +--- +title: Run the game window and its input through SDL +category: internal +release: 0.2.0 +targets: [] +credit: [ZivDero] +--- + +The game window, its events, and keyboard and mouse input now go through SDL 3. The window opens at the same size and place, saved hotkeys keep their keys, and the pointer keeps its artwork and scale. + +Tapping Alt or F10 no longer puts the window's system menu into keyboard mode, and Alt+Space no longer opens that menu. + +A game played in a window can now be dragged onto another display or partly off the screen. It used to be held inside the primary display. + +The game no longer reserves Ctrl+Alt+Shift+M for itself, so other programs can use that shortcut. + +While the game window has the focus, the display no longer turns off after the system's idle time. The screen saver was already kept from starting. + +When the game window or the renderer cannot start, the message now shows an error icon in place of a warning icon. + +In-game chat, the high-score name and the older dialogs' text boxes now take the characters the keyboard layout types, including accents typed with a dead key and text from an input method. + +The keyboard options screen names a key by the character the layout prints on it, or by an English name such as `Home` for a key that prints none. It used to show the names Windows gives in its own language. diff --git a/manual/content/commands/selectoneless.md b/manual/content/commands/selectoneless.md index 0e5867fda..54ace9d24 100644 --- a/manual/content/commands/selectoneless.md +++ b/manual/content/commands/selectoneless.md @@ -4,4 +4,4 @@ command_id: SelectOneLess Deselects one object and leaves the rest selected. It removes the most recently selected object that is not kept at the front of the selection order. When every selected object is kept at the front, it removes the one that was selected first. If the view is following the removed object, it stops following. -An object is kept at the front when its type's primary weapon has a positive [`Damage`](/keys/damage/#scope-weapontype), but only if the type's section appears again in a rules file read after the selected `RULE*.INI`. [Multiplayer rules](/formats/multiplayer-rules/#when-they-are-read) lists the order the files are read in. The `Damage` that counts is the value set before that later file is read. A type whose section appears only in `RULE*.INI` is never kept at the front, whatever its weapon. +An object is kept at the front when its type's primary weapon has a positive [`Damage`](/keys/damage/#scope-weapontype), but only if the type's section appears again in a rules file read after `RULES.INI`. [Multiplayer rules](/formats/multiplayer-rules/#when-they-are-read) lists the order the files are read in. The `Damage` that counts is the value set before that later file is read. A type whose section appears only in `RULES.INI` is never kept at the front, whatever its weapon. diff --git a/manual/content/formats/multiplayer-rules.md b/manual/content/formats/multiplayer-rules.md index 170d3e131..a6f790066 100644 --- a/manual/content/formats/multiplayer-rules.md +++ b/manual/content/formats/multiplayer-rules.md @@ -35,7 +35,7 @@ In every game that is not a campaign, the `155mm` weapon uses these two values u Each time a scenario loads, the game applies the rules files in this order, and a later file overrides an earlier one: -1. the selected `RULE*.INI` +1. `RULES.INI` 2. `LANGRULE.INI` 3. `FIRESTRM.INI`, when Firestorm is enabled 4. `LANGFS.INI` @@ -45,7 +45,7 @@ Each time a scenario loads, the game applies the rules files in this order, and A scenario that sets one of these values therefore overrides both files, as it overrides every other rules file. -`MultiplayerRules=` and `MultiplayerRulesExpansion=` in [`OPENTS.INI`](/formats/opents-ini/#the-files-it-reads) name the two files. The game looks for them in the same folders, in the same order, as every other file it opens. Only the named file is read: unlike `RULE*.INI`, no wildcard gathers other multiplayer rules files. +`MultiplayerRules=` and `MultiplayerRulesExpansion=` in [`OPENTS.INI`](/formats/opents-ini/#the-files-it-reads) name the two files. The game looks for them in the same folders, in the same order, as every other file it opens. Only the named file is read; no wildcard gathers other multiplayer rules files. ## Write only keys the rules also write @@ -59,16 +59,16 @@ The expansion rules and a map's overrides carry the same risk. `MPLAYER.INI` and ## What they cannot do -- **Declare a theater or change its settings:** `[Theaters]` and each theater's section are read once at startup, from the selected rules file and `FIRESTRM.INI` only. [Rules registration](/formats/rules-registries/) explains why the list of theaters cannot change after that. -- **Set `[Maximums]`:** that section is read only from the selected rules file. -- **Add a country a player can pick:** the countries a player chooses from are read from the selected rules file alone, so a country declared only here is missing from that list. The country does exist once the scenario loads, and a map may place houses of it. `FIRESTRM.INI` has the same limit. +- **Declare a theater or change its settings:** `[Theaters]` and each theater's section are read once at startup, from `RULES.INI` and `FIRESTRM.INI` only. [Rules registration](/formats/rules-registries/) explains why the list of theaters cannot change after that. +- **Set `[Maximums]`:** that section is read only from `RULES.INI`. +- **Add a country a player can pick:** the countries a player chooses from are read from `RULES.INI` alone, so a country declared only here is missing from that list. The country does exist once the scenario loads, and a map may place houses of it. `FIRESTRM.INI` has the same limit. - **Set the lobby's starting values from the expansion:** `[MultiplayerDefaults]` in `MPLAYER.INI` sets the starting credits, unit count, tech level and match options a host sees, because the game reads that file before the menus. `MPLAYERFS.INI` cannot change them, and neither can `FIRESTRM.INI`, because the game does not know whether Firestorm is enabled until later. The section is still read from every rules file when a scenario loads, so a key that acts during the match, such as [`BuildOffAllyAnyStructure=`](/keys/buildoffallyanystructure/), takes effect from either file. Anything else `RULES.INI` can do, these files can do. Object types that only these files declare exist in the multiplayer game and are stored in its saves, so loading such a save restores them even when the file is no longer on disk. ## Every player needs the same copy -In a game hosted from the game's own network lobby, the host compares a checksum of its rules with each joining player's. The checksum covers the selected rules file and `MPLAYER.INI`, plus `FIRESTRM.INI` and `MPLAYERFS.INI` while Firestorm is enabled. A player whose copy of any of them differs is refused, including a player who has one of these files where the host does not. +In a game hosted from the game's own network lobby, the host compares a checksum of its rules with each joining player's. The checksum covers `RULES.INI` and `MPLAYER.INI`, plus `FIRESTRM.INI` and `MPLAYERFS.INI` while Firestorm is enabled. A player whose copy of any of them differs is refused, including a player who has one of these files where the host does not. A file that is absent from every machine, or that has no sections, changes no checksum. diff --git a/manual/content/formats/opents-ini.md b/manual/content/formats/opents-ini.md index 046dd1145..d3fb38110 100644 --- a/manual/content/formats/opents-ini.md +++ b/manual/content/formats/opents-ini.md @@ -78,7 +78,7 @@ Each of the eight `Expansion` keys names the expansion's copy of the matching ba These keys change only a file's name. The game looks for each named file in the [search order](#the-order-files-are-searched-for-in) below, as it does for every other file. -The game also gathers rules files by the fixed pattern `RULE*.INI`. The file `Rules` names is always read, even when its name does not match the pattern. When it is the only rules file, the game uses it. When other files in the searched folders match `RULE*.INI`, the game asks the player at startup which rules file to play with. Keep backups and other files out of that pattern, because every match is offered as a rules file. +The game reads its rules from the one file `Rules` names. Other files matching `RULE*.INI` are not read. Campaign files are gathered by the fixed pattern `BATTLE*.INI`. Every matching file is read and adds its campaigns to the list. The file `Battle` names is read as well when its name does not match the pattern. diff --git a/manual/content/formats/powerups.md b/manual/content/formats/powerups.md index 17659adc7..83c2e2c13 100644 --- a/manual/content/formats/powerups.md +++ b/manual/content/formats/powerups.md @@ -4,7 +4,7 @@ title: Crate powerups summary: Sets each crate result's share of the random crate draw, the animation it plays, and the number its effect uses. kind: record files: - - RULE*.INI + - RULES.INI - LANGRULE.INI - FIRESTRM.INI - LANGFS.INI diff --git a/manual/content/formats/rules-registries.md b/manual/content/formats/rules-registries.md index 67bd9bf25..6c3c942f5 100644 --- a/manual/content/formats/rules-registries.md +++ b/manual/content/formats/rules-registries.md @@ -4,7 +4,7 @@ title: Rules registration lists summary: Lists in the rules files that register each kind of rules type and assign countries to sides. kind: registry files: - - RULE*.INI + - RULES.INI - LANGRULE.INI - FIRESTRM.INI - LANGFS.INI @@ -68,7 +68,7 @@ A later file that lists an existing side replaces that side's country list, so r ## Theaters -`[Theaters]` is read once, as the game starts, and only from the selected `RULE*.INI` and from `FIRESTRM.INI`. `FIRESTRM.INI` counts whenever it is installed, even when Firestorm is not enabled; [Game data](/using/game-data/) covers what makes it count as installed. A map, the language rules files and the multiplayer rules files cannot add a theater. Saves record each theater by its position in the list, so the list has to stay the same from one game to the next. +`[Theaters]` is read once, as the game starts, and only from `RULES.INI` and from `FIRESTRM.INI`. `FIRESTRM.INI` counts whenever it is installed, even when Firestorm is not enabled; [Game data](/using/game-data/) covers what makes it count as installed. A map, the language rules files and the multiplayer rules files cannot add a theater. Saves record each theater by its position in the list, so the list has to stay the same from one game to the next. When neither file registers a theater, the game uses the two theaters Tiberian Sun shipped, `TEMPERATE` and `SNOW`, in that order. Unmodified rules files get this list. A `[Theaters]` list that registers at least one theater replaces those two completely: it may drop `SNOW`, reorder the pair, or replace both, so a list that means to keep them has to name them. diff --git a/manual/content/keys/money.md b/manual/content/keys/money.md index 235a1905d..2f64989ae 100644 --- a/manual/content/keys/money.md +++ b/manual/content/keys/money.md @@ -9,7 +9,7 @@ when_omitted: `Money` is where the credits slider starts on the skirmish and network setup screens. When the match begins, every player and computer house the session sets up starts with the amount the slider shows. A computer house then receives an extra share of that money, set by [`MultiplayerAICM`](/keys/multiplayeraicm/). After a player starts a skirmish match, or the host moves the credits slider on the network screen, later setup screens in the same session open at that amount instead. -OpenTS reads the value once, when the program starts. It comes from the selected rules file, or from [`MPLAYER.INI`](/formats/multiplayer-rules/) when that file sets it. A map or any other rules layer can set the key, but that does not change the starting money. +OpenTS reads the value once, when the program starts. It comes from `RULES.INI`, or from [`MPLAYER.INI`](/formats/multiplayer-rules/) when that file sets it. A map or any other rules layer can set the key, but that does not change the starting money. Each credits slider runs from `2500` to [`MaxMoney`](/keys/maxmoney/). The skirmish slider moves in steps of `250` and the network slider in steps of `100`, both counted from `2500`. Moving a slider keeps the amount inside that range. Set `Money` to a multiple of `500` in that range so that it lies on the steps of both sliders. diff --git a/manual/content/systems/ui-files.md b/manual/content/systems/ui-files.md index c1d3e5f87..67d8941f5 100644 --- a/manual/content/systems/ui-files.md +++ b/manual/content/systems/ui-files.md @@ -57,4 +57,4 @@ These screens are documents: - the out-of-sync screen and the [reconnect dialog](/systems/reconnect-dialog/) - the message boxes, and the notices shown while a game saves or loads -The score screens, the graphical main menu, the mission briefing and the sidebar are drawn by the game's older systems and are not documents. The crash report, and the box that asks which rules file to use when several `RULE*.INI` files are found, remain Windows dialogs. +The score screens, the graphical main menu, the mission briefing and the sidebar are drawn by the game's older systems and are not documents. The crash report remains a Windows dialog. diff --git a/manual/data/command-adapters.yaml b/manual/data/command-adapters.yaml index 3bc62da5f..a81120856 100644 --- a/manual/data/command-adapters.yaml +++ b/manual/data/command-adapters.yaml @@ -288,7 +288,7 @@ fixed_controls: context: Any game window focused, with Debug_Flag enabled availability: *debug sites: - - { file: code/ui/uishell.cpp, function: UIShellClass::Handle_Window_Message, expression: VK_F6, guard: _DEBUG } + - { file: code/ui/uishell.cpp, function: UIShellClass::Handle_Window_Event, expression: VK_F6, guard: _DEBUG } fixed_exclusions: - site: { file: code/debug.cpp, function: Debug_Key, expression: KN_BUTTON, guard: _DEBUG } @@ -371,17 +371,57 @@ fixed_exclusions: - { file: code/keyboard.cpp, function: WWKeyboardClass::Is_Mouse_Key, expression: VK_RBUTTON } reason: Keyboard-backend classification of mouse virtual keys. - sites: - - { file: code/keyboard.cpp, function: WWKeyboardClass::Put_Key_Message, expression: VK_SHIFT } - - { file: code/keyboard.cpp, function: WWKeyboardClass::Put_Key_Message, expression: VK_CONTROL } - - { file: code/keyboard.cpp, function: WWKeyboardClass::Put_Key_Message, expression: VK_MENU } - reason: Keyboard-backend modifier-state encoding, not separate controls. + - { file: code/sdl/sdlwindow.cpp, function: Main_Window_Key_Down, expression: VK_LBUTTON } + - { file: code/sdl/sdlwindow.cpp, function: Main_Window_Key_Down, expression: VK_RBUTTON } + - { file: code/sdl/sdlwindow.cpp, function: Main_Window_Key_Down, expression: VK_MBUTTON } + - { file: code/sdl/sdlwindow.cpp, function: Main_Window_Key_Down, expression: VK_XBUTTON1 } + - { file: code/sdl/sdlwindow.cpp, function: Main_Window_Key_Down, expression: VK_XBUTTON2 } + reason: SDL-layer mouse button state, not controls. - sites: - - { file: code/keyboard.cpp, function: WWKeyboardClass::Down, expression: VK_LBUTTON } - - { file: code/keyboard.cpp, function: WWKeyboardClass::Down, expression: VK_RBUTTON } - - { file: code/keyboard.cpp, function: WWKeyboardClass::Down, expression: VK_LBUTTON+VK_RBUTTON } - reason: Keyboard-backend normalization for the Windows swapped-button setting. - - site: { file: code/keyboard.cpp, function: WWKeyboardClass::Message_Handler, expression: VK_SCROLL } + - { file: code/sdl/sdlinput.cpp, function: SDLInputStateClass::Modifiers, expression: VK_SHIFT } + - { file: code/sdl/sdlinput.cpp, function: SDLInputStateClass::Modifiers, expression: VK_CONTROL } + - { file: code/sdl/sdlinput.cpp, function: SDLInputStateClass::Modifiers, expression: VK_MENU } + reason: SDL-layer modifier state attached to key and mouse events, not controls. + - sites: + - { file: code/sdl/sdlinput.cpp, function: SDLInputStateClass::Key_Toggled, expression: VK_CAPITAL } + - { file: code/sdl/sdlinput.cpp, function: SDLInputStateClass::Key_Toggled, expression: VK_NUMLOCK } + - { file: code/sdl/sdlinput.cpp, function: SDLInputStateClass::Key_Toggled, expression: VK_SCROLL } + reason: SDL-layer lock-key state, not controls. + - sites: + - { file: code/sdl/sdlinput.cpp, function: Generic_Key, expression: VK_LSHIFT } + - { file: code/sdl/sdlinput.cpp, function: Generic_Key, expression: VK_RSHIFT } + - { file: code/sdl/sdlinput.cpp, function: Generic_Key, expression: VK_LCONTROL } + - { file: code/sdl/sdlinput.cpp, function: Generic_Key, expression: VK_RCONTROL } + - { file: code/sdl/sdlinput.cpp, function: Generic_Key, expression: VK_LMENU } + - { file: code/sdl/sdlinput.cpp, function: Generic_Key, expression: VK_RMENU } + reason: SDL-layer mapping of each side of a modifier to the modifier, not controls. + - sites: + - { file: code/sdl/sdlinput.cpp, function: Holdable_Key, expression: VK_LBUTTON } + - { file: code/sdl/sdlinput.cpp, function: Holdable_Key, expression: VK_RBUTTON } + - { file: code/sdl/sdlinput.cpp, function: Holdable_Key, expression: VK_MBUTTON } + - { file: code/sdl/sdlinput.cpp, function: Holdable_Key, expression: VK_XBUTTON1 } + - { file: code/sdl/sdlinput.cpp, function: Holdable_Key, expression: VK_XBUTTON2 } + - { file: code/sdl/sdlinput.cpp, function: Holdable_Key, expression: VK_SHIFT } + - { file: code/sdl/sdlinput.cpp, function: Holdable_Key, expression: VK_CONTROL } + - { file: code/sdl/sdlinput.cpp, function: Holdable_Key, expression: VK_MENU } + reason: SDL-layer choice of the keys read from Windows after a keyboard reset, not controls. + - site: { file: code/sdl/sdlevents.cpp, function: Window_Events_From_SDL, expression: VK_F10 } + reason: SDL-layer classification of F10 as a system key, as Windows reports it; it does not dispatch a game command. + - site: { file: code/keyboard.cpp, function: WWKeyboardClass::Handle_Window_Event, expression: VK_SCROLL } reason: Synchronizes the backend Scroll Lock state; it does not dispatch a game command. + - sites: + - { file: code/keyboard.cpp, function: Control_Character, expression: VK_ESCAPE } + - { file: code/keyboard.cpp, function: Control_Character, expression: VK_RETURN } + - { file: code/keyboard.cpp, function: Control_Character, expression: VK_BACK } + - { file: code/keyboard.cpp, function: Control_Character, expression: VK_TAB } + reason: Gives text fields the control character each key types; it does not dispatch a game command. + - site: { file: code/keyboard.cpp, function: WWKeyboardClass::Put_Text, expression: KN_TEXT } + reason: Queues typed text that no key press carries; it is not a key. + - sites: + - { file: code/sdl/sdlkeys.cpp, function: Virtual_Key_Name, expression: VK_SHIFT } + - { file: code/sdl/sdlkeys.cpp, function: Virtual_Key_Name, expression: VK_CONTROL } + - { file: code/sdl/sdlkeys.cpp, function: Virtual_Key_Name, expression: VK_MENU } + reason: Converts modifier virtual keys to display names; it does not consume input. - sites: - { file: code/list.cpp, function: ListClass::Action, expression: KN_UP } - { file: code/list.cpp, function: ListClass::Action, expression: KN_DOWN } @@ -399,11 +439,6 @@ fixed_exclusions: - { file: code/msglist.cpp, function: MessageListClass::Input, expression: KN_BACKSPACE } - { file: code/msglist.cpp, function: MessageListClass::Input, expression: KN_ESC } reason: Message editor polling, cursor placement, and text-edit termination. - - sites: - - { file: code/ownrdraw.cpp, function: Build_Hotkey_String, expression: VK_MENU } - - { file: code/ownrdraw.cpp, function: Build_Hotkey_String, expression: VK_CONTROL } - - { file: code/ownrdraw.cpp, function: Build_Hotkey_String, expression: VK_SHIFT } - reason: Converts modifier virtual keys to display names; it does not consume input. - sites: - { file: code/restate.cpp, function: RestateMission::User_Input, expression: KN_NONE } - { file: code/restate.cpp, function: RestateMission::User_Input, expression: KN_BUTTON } @@ -430,8 +465,6 @@ fixed_exclusions: - { file: code/wdtsel.cpp, function: Selection::Pick_Territory, expression: KN_NONE } - { file: code/wdtsel.cpp, function: Selection::Pick_Territory, expression: KN_LMOUSE } reason: World Domination Tour territory-screen polling and hit-testing. - - site: { file: code/winstub.cpp, function: Create_Main_Window, expression: VK_M } - reason: Registers a Windows hotkey that has no WM_HOTKEY handler in the current source tree. - sites: - { file: code/ui/uishell.cpp, function: UIShellClass::Key_Modifiers, expression: VK_SHIFT } - { file: code/ui/uishell.cpp, function: UIShellClass::Key_Modifiers, expression: VK_CONTROL } @@ -452,10 +485,6 @@ fixed_exclusions: reason: UI shell modifier keys kept out of press ownership, not separate controls. - site: { file: code/ui/uishell.cpp, function: UIShellClass::Quarantine_Held_Input, expression: VK_XBUTTON2 } reason: UI shell loop bound over the virtual keys past the mouse buttons, not a control. - - sites: - - { file: code/ui/uienginehost.cpp, function: , expression: VK_LBUTTON } - - { file: code/ui/uienginehost.cpp, function: , expression: VK_RBUTTON } - reason: UI shell host naming the primary and secondary buttons as the messages do when the user swapped them, not controls. launch_options: - id: launch:help diff --git a/manual/data/ini-read-exclusions.yaml b/manual/data/ini-read-exclusions.yaml index 45bdce60d..f5326d335 100644 --- a/manual/data/ini-read-exclusions.yaml +++ b/manual/data/ini-read-exclusions.yaml @@ -120,17 +120,6 @@ site_exclusions: classification: excluded reason: These fields configure the legacy campaign-selection UI territory map, not game or mod data. - - path: code/winfix.cpp - function: read_views_from_ini - keys: [X, Y] - classification: excluded - reason: These coordinates restore private diagnostic window positions and are not player-facing settings. - - path: code/init.cpp - function: Rules_Choice_Dialog_Proc - keys: [Name] - classification: excluded - reason: This lookup reads an internal rules-choice dialog label rather than an engine setting. - - path: code/deploymentconfig.cpp function: DeploymentConfigClass::Read_INI keys: [SearchPaths, CarryScenarioFile, Rules, RulesExpansion, Art, ArtExpansion, AI, AIExpansion, Sound, SoundExpansion, Theme, ThemeExpansion, Battle, BattleExpansion, LanguageRules, LanguageRulesExpansion, MultiplayerRules, MultiplayerRulesExpansion, Tutorial, UI, Settings, Scheme, Game] diff --git a/manual/site/tests/documentation-source-contract.test.mjs b/manual/site/tests/documentation-source-contract.test.mjs index af4b985f8..98ce6d010 100644 --- a/manual/site/tests/documentation-source-contract.test.mjs +++ b/manual/site/tests/documentation-source-contract.test.mjs @@ -1153,14 +1153,8 @@ test('The deployment names the files the game reads', () => { assert.match( init, - /stricmp\(name\.c_str\(\), DeploymentConfig\.RulesFile\.c_str\(\)\) == 0/, - 'the wildcard search knows the rules file by the name the deployment gives it', - ); - - assert.match( - init, - /CCFileClass file\(DeploymentConfig\.RulesFile\.c_str\(\)\);/, - 'and the file it falls back on is that same one', + /CCFileClass rules_file\(DeploymentConfig\.RulesFile\.c_str\(\)\);/, + 'the rules are read from the file the deployment names', ); assert.match( diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7b3550d2d..3d9973ca6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -110,6 +110,11 @@ add_subdirectory(png) add_subdirectory(houseset) add_subdirectory(uishell) add_subdirectory(uilogic) +add_subdirectory(sdlkeys) +add_subdirectory(sdlevents) +add_subdirectory(sdlinput) +add_subdirectory(keyqueue) +add_subdirectory(sdlwindow) # The script reads the sources directly, so the check needs no compiler and runs in every CI # job's ctest step. diff --git a/tests/keyqueue/CMakeLists.txt b/tests/keyqueue/CMakeLists.txt new file mode 100644 index 000000000..5cc5ce20b --- /dev/null +++ b/tests/keyqueue/CMakeLists.txt @@ -0,0 +1,9 @@ +# The key queue is compiled into the harness with stubs for the window, the SDL layer and the frame. + +opents_add_test(KeyQueue + NAME keyqueue + SOURCES keyqueuecontract.cpp keyqueuestub.cpp + ENGINE + keyboard.cpp + DEFINITIONS WIN32 _WINDOWS NOMINMAX +) diff --git a/tests/keyqueue/keyqueuecontract.cpp b/tests/keyqueue/keyqueuecontract.cpp new file mode 100644 index 000000000..d06bc0980 --- /dev/null +++ b/tests/keyqueue/keyqueuecontract.cpp @@ -0,0 +1,162 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Pins the key queue the game's input loops read, including the character each press typed, +// which chat, edit boxes and the high-score name read back. + +#include "keyboard.h" +#include "windowevent.hh" + +#include + +namespace { + +int Failures = 0; + + +void Check(bool condition, char const * what) +{ + std::printf("%-76s %s\n", what, condition ? "ok" : "FAILED"); + + if (!condition) { + Failures++; + } +} + + +WindowEvent Key(bool down, int virtualkey, int modifiers = 0, bool repeat = false) +{ + WindowEvent event; + event.Type = down ? WINDOW_EVENT_KEY_DOWN : WINDOW_EVENT_KEY_UP; + event.VirtualKey = virtualkey; + event.Modifiers = modifiers; + event.Repeat = repeat; + return(event); +} + + +WindowEvent Text(char32_t code) +{ + WindowEvent event; + event.Type = WINDOW_EVENT_TEXT; + event.Text = code; + return(event); +} + + +WindowEvent Click(bool down, WindowMouseButton button, int x, int y, int clicks = 1) +{ + WindowEvent event; + event.Type = down ? WINDOW_EVENT_MOUSE_DOWN : WINDOW_EVENT_MOUSE_UP; + event.Button = button; + event.X = x; + event.Y = y; + event.Clicks = clicks; + return(event); +} + +} + + +int main(void) +{ + WWKeyboardClass keyboard; + + keyboard.Handle_Window_Event(Key(true, 'A', WINDOW_MOD_SHIFT)); + keyboard.Handle_Window_Event(Text('A')); + keyboard.Handle_Window_Event(Key(false, 'A', WINDOW_MOD_SHIFT)); + unsigned short key = keyboard.Get(); + Check(key == ('A' | WWKEY_SHIFT_BIT), "a key press is queued with the modifiers held"); + Check(keyboard.To_ASCII(key) == 'A', "and gives back the character it typed"); + key = keyboard.Get(); + Check(key == ('A' | WWKEY_SHIFT_BIT | WWKEY_RLS_BIT), "its release follows it"); + Check(keyboard.To_ASCII(key) == 0, "and types nothing"); + Check(keyboard.Check() == 0, "and nothing else was queued for the character"); + + keyboard.Handle_Window_Event(Key(true, 'E')); + keyboard.Handle_Window_Event(Text(0x00E9)); + key = keyboard.Get(); + Check(key == 'E' && keyboard.To_ASCII(key) == 0x00E9, "an accented character is read back from its key"); + + keyboard.Handle_Window_Event(Key(true, 'Q')); + keyboard.Handle_Window_Event(Key(true, 'Q', 0, true)); + keyboard.Handle_Window_Event(Text('q')); + key = keyboard.Get(); + Check(key == 'Q' && keyboard.To_ASCII(key) == 0, "a held key's repeat neither queues nor types"); + Check(keyboard.Check() == 0, "and its character is dropped"); + + keyboard.Handle_Window_Event(Key(false, 'Q')); + keyboard.Get(); + keyboard.Handle_Window_Event(Text(0x65E5)); + key = (keyboard.Check() != 0) ? keyboard.Get() : 0; + Check(key == KN_TEXT && keyboard.To_ASCII(key) == 0x65E5, "text after the repeating key's release is kept"); + + keyboard.Handle_Window_Event(Key(true, VK_OEM_7)); + keyboard.Handle_Window_Event(Key(true, 'X')); + keyboard.Handle_Window_Event(Text(0x00B4)); + keyboard.Handle_Window_Event(Text('x')); + key = keyboard.Get(); + Check(key == VK_OEM_7 && keyboard.To_ASCII(key) == 0, "a dead key types nothing of its own"); + key = keyboard.Get(); + Check(key == 'X' && keyboard.To_ASCII(key) == 0x00B4, "the next key carries the first character the two typed"); + key = keyboard.Get(); + Check(key == KN_TEXT && keyboard.To_ASCII(key) == 'x', "and a second character arrives as a text entry of its own"); + + keyboard.Handle_Window_Event(Text(0x65E5)); + key = keyboard.Get(); + Check(key == KN_TEXT && keyboard.To_ASCII(key) == 0x65E5, "text with no key press, as an input method sends it, is a text entry"); + + keyboard.Handle_Window_Event(Key(true, 'Z')); + keyboard.Clear(); + keyboard.Handle_Window_Event(Text('z')); + key = keyboard.Get(); + Check(key == KN_TEXT && keyboard.To_ASCII(key) == 'z', "clearing the queue forgets the press a character would have joined"); + + keyboard.Handle_Window_Event(Key(true, VK_ESCAPE)); + Check(keyboard.To_ASCII(keyboard.Get()) == 0x1B, "Esc gives its control character"); + keyboard.Handle_Window_Event(Key(true, VK_RETURN, WINDOW_MOD_SHIFT)); + Check(keyboard.To_ASCII(keyboard.Get()) == 0x0D, "Enter gives a carriage return with any modifier"); + keyboard.Handle_Window_Event(Key(true, VK_BACK)); + Check(keyboard.To_ASCII(keyboard.Get()) == 0x08, "Backspace gives a backspace"); + keyboard.Handle_Window_Event(Key(true, VK_TAB)); + Check(keyboard.To_ASCII(keyboard.Get()) == 0x09, "Tab gives a tab"); + + keyboard.Handle_Window_Event(Key(true, VK_RETURN)); + keyboard.Handle_Window_Event(Text(0x65E5)); + key = keyboard.Get(); + Check(keyboard.To_ASCII(key) == 0x0D, "text arriving after Enter leaves Enter its carriage return"); + key = keyboard.Get(); + Check(key == KN_TEXT && keyboard.To_ASCII(key) == 0x65E5, "and arrives as a text entry"); + + keyboard.Handle_Window_Event(Key(true, 'B')); + keyboard.Handle_Window_Event(Text('b')); + keyboard.Handle_Window_Event(Key(true, 'C')); + keyboard.Handle_Window_Event(Text('c')); + unsigned short first = keyboard.Get(); + unsigned short second = keyboard.Get(); + Check(keyboard.To_ASCII(second) == 'c' && keyboard.To_ASCII(first) == 0, "only the key fetched last reads back a character"); + + keyboard.Handle_Window_Event(Key(true, 'M', WINDOW_MOD_CTRL | WINDOW_MOD_ALT)); + key = keyboard.Get(); + Check(key == ('M' | WWKEY_CTRL_BIT | WWKEY_ALT_BIT), "Ctrl and Alt become their modifier bits"); + + keyboard.Handle_Window_Event(Click(true, WINDOW_BUTTON_LEFT, 30, 40)); + key = keyboard.Get(); + Check(key == KN_LMOUSE && keyboard.MouseQX == 30 && keyboard.MouseQY == 40, "a click carries where it happened"); + Check(keyboard.To_ASCII(key) == 0, "and types nothing"); + + keyboard.Handle_Window_Event(Click(true, WINDOW_BUTTON_RIGHT, 5, 6, 2)); + Check(keyboard.Get() == KN_RMOUSE && keyboard.Get() == (KN_RMOUSE | KN_RLSE_BIT), "a double click is a press and a release"); + + keyboard.Handle_Window_Event(Click(true, WINDOW_BUTTON_X1, 1, 1)); + Check(keyboard.Check() == 0, "the extra mouse buttons are not queued"); + + std::printf("\n%s\n", Failures == 0 ? "PASSED" : "FAILED"); + return(Failures == 0 ? 0 : 1); +} diff --git a/tests/keyqueue/keyqueuestub.cpp b/tests/keyqueue/keyqueuestub.cpp new file mode 100644 index 000000000..91075684b --- /dev/null +++ b/tests/keyqueue/keyqueuestub.cpp @@ -0,0 +1,31 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Stands in for the window, the SDL layer and the frame: nothing is pumped, no key is held, +// and every position lies inside the frame. + +#include "msgloop.h" +#include "sdl/sdlwindow.h" +#include "vidscale.h" + + +void Windows_Message_Handler(void) +{ +} + + +bool Main_Window_Key_Down(int) +{ + return(false); +} + + +void Clamp_To_Game(Point2D &) +{ +} diff --git a/tests/sdlevents/CMakeLists.txt b/tests/sdlevents/CMakeLists.txt new file mode 100644 index 000000000..ed8405726 --- /dev/null +++ b/tests/sdlevents/CMakeLists.txt @@ -0,0 +1,13 @@ +# The events are built by hand, so SDL is linked but never started. + +opents_add_test(SDLEventsContract + NAME sdleventscontract + SOURCES sdleventscontract.cpp + ENGINE + sdl/sdlevents.cpp + sdl/sdlkeys.cpp + utf8.cpp + DEFINITIONS WIN32 _WINDOWS NOMINMAX + LIBRARIES SDL3::SDL3-static + UTF8 +) diff --git a/tests/sdlevents/sdleventscontract.cpp b/tests/sdlevents/sdleventscontract.cpp new file mode 100644 index 000000000..01ea79d6d --- /dev/null +++ b/tests/sdlevents/sdleventscontract.cpp @@ -0,0 +1,191 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Pins how SDL events become the window events the game reads. + +#include "sdl/sdlevents.h" + +#include "win.h" + +#include + +#include +#include +#include + +namespace { + +int Failures = 0; + + +void Check(bool condition, char const * what) +{ + std::printf("%-76s %s\n", what, condition ? "ok" : "FAILED"); + + if (!condition) { + Failures++; + } +} + + +SDL_Event Event_Of(Uint32 type) +{ + SDL_Event event; + std::memset(&event, 0, sizeof(event)); + event.type = type; + return(event); +} + + +std::vector Translate(SDL_Event const & event, float pixeldensity = 1.0f, int modifiers = 0) +{ + std::vector events; + Window_Events_From_SDL(event, pixeldensity, modifiers, events); + return(events); +} + + +bool One(std::vector const & events, WindowEventType type) +{ + return(events.size() == 1 && events[0].Type == type); +} + + +SDL_Event Button(bool down, Uint8 button, Uint8 clicks, float x, float y) +{ + SDL_Event event = Event_Of(down ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP); + event.button.down = down; + event.button.button = button; + event.button.clicks = clicks; + event.button.x = x; + event.button.y = y; + return(event); +} + + +SDL_Event Key(bool down, SDL_Scancode scancode, SDL_Keycode keycode, SDL_Keymod modifiers, bool repeat = false) +{ + SDL_Event event = Event_Of(down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP); + event.key.down = down; + event.key.scancode = scancode; + event.key.key = keycode; + event.key.mod = modifiers; + event.key.repeat = repeat; + return(event); +} + +} + + +int main(void) +{ + SDL_Event motion = Event_Of(SDL_EVENT_MOUSE_MOTION); + motion.motion.x = 12.75f; + motion.motion.y = 34.0f; + std::vector events = Translate(motion); + Check(One(events, WINDOW_EVENT_MOUSE_MOVE) && events[0].X == 12 && events[0].Y == 34, "a mouse move lands on the pixel it is over"); + + events = Translate(motion, 2.0f); + Check(One(events, WINDOW_EVENT_MOUSE_MOVE) && events[0].X == 25 && events[0].Y == 68, "a window with a pixel density of two doubles the position"); + + motion.motion.x = -0.5f; + events = Translate(motion); + Check(One(events, WINDOW_EVENT_MOUSE_MOVE) && events[0].X == -1, "a position left of the client area stays outside it"); + + events = Translate(Button(true, SDL_BUTTON_LEFT, 1, 5.0f, 6.0f)); + Check(One(events, WINDOW_EVENT_MOUSE_DOWN) && events[0].Button == WINDOW_BUTTON_LEFT && events[0].Clicks == 1 && events[0].X == 5 && events[0].Y == 6, "a left press is a single click where it happened"); + + events = Translate(Button(true, SDL_BUTTON_LEFT, 1, 5.0f, 6.0f), 1.0f, WINDOW_MOD_SHIFT); + Check(One(events, WINDOW_EVENT_MOUSE_DOWN) && events[0].Modifiers == WINDOW_MOD_SHIFT, "and carries the modifiers held"); + + events = Translate(Button(true, SDL_BUTTON_RIGHT, 2, 0.0f, 0.0f)); + Check(One(events, WINDOW_EVENT_MOUSE_DOWN) && events[0].Button == WINDOW_BUTTON_RIGHT && events[0].Clicks == 2, "the second press of a run of clicks is a double click"); + + events = Translate(Button(true, SDL_BUTTON_MIDDLE, 3, 0.0f, 0.0f)); + Check(One(events, WINDOW_EVENT_MOUSE_DOWN) && events[0].Clicks == 1, "the third press starts over, as Windows counts them"); + + events = Translate(Button(true, SDL_BUTTON_X2, 4, 0.0f, 0.0f)); + Check(One(events, WINDOW_EVENT_MOUSE_DOWN) && events[0].Button == WINDOW_BUTTON_X2 && events[0].Clicks == 2, "and the fourth is double again, on the second extra button too"); + + events = Translate(Button(false, SDL_BUTTON_LEFT, 2, 0.0f, 0.0f)); + Check(One(events, WINDOW_EVENT_MOUSE_UP) && events[0].Clicks == 1, "a release is never a double click"); + + events = Translate(Button(true, 9, 1, 0.0f, 0.0f)); + Check(events.empty(), "a button beyond the fifth is dropped"); + + SDL_Event wheel = Event_Of(SDL_EVENT_MOUSE_WHEEL); + wheel.wheel.y = -2.0f; + wheel.wheel.mouse_x = 30.0f; + wheel.wheel.mouse_y = 40.0f; + events = Translate(wheel); + Check(One(events, WINDOW_EVENT_MOUSE_WHEEL) && events[0].Wheel == -2.0f && !events[0].Horizontal && events[0].X == 30 && events[0].Y == 40, "two notches toward the player are -2 on the vertical wheel, where the mouse is"); + + wheel.wheel.direction = SDL_MOUSEWHEEL_FLIPPED; + events = Translate(wheel); + Check(One(events, WINDOW_EVENT_MOUSE_WHEEL) && events[0].Wheel == 2.0f, "a flipped wheel is turned back the way the hand moved"); + + wheel.wheel.direction = SDL_MOUSEWHEEL_NORMAL; + wheel.wheel.x = 1.0f; + wheel.wheel.y = 0.5f; + events = Translate(wheel); + Check(events.size() == 2 && !events[0].Horizontal && events[0].Wheel == 0.5f && events[1].Horizontal && events[1].Wheel == 1.0f, "a diagonal turn is a vertical turn and then a horizontal one"); + + events = Translate(Key(true, SDL_SCANCODE_A, SDLK_A, (SDL_Keymod)(SDL_KMOD_LSHIFT | SDL_KMOD_RCTRL)), 1.0f, WINDOW_MOD_SHIFT | WINDOW_MOD_CTRL); + Check(One(events, WINDOW_EVENT_KEY_DOWN) && events[0].VirtualKey == 'A' && events[0].Modifiers == (WINDOW_MOD_SHIFT | WINDOW_MOD_CTRL), "a key press carries its virtual key and the modifiers held"); + Check(events.size() == 1 && !events[0].Repeat && !events[0].System, "and is neither a repeat nor a system key"); + + events = Translate(Key(true, SDL_SCANCODE_A, SDLK_A, SDL_KMOD_NONE, true)); + Check(One(events, WINDOW_EVENT_KEY_DOWN) && events[0].Repeat, "a held key's repeat is marked as one"); + + events = Translate(Key(false, SDL_SCANCODE_A, SDLK_A, SDL_KMOD_NONE, true)); + Check(One(events, WINDOW_EVENT_KEY_UP) && !events[0].Repeat, "a release is never a repeat"); + + events = Translate(Key(true, SDL_SCANCODE_F4, SDLK_F4, SDL_KMOD_LALT), 1.0f, WINDOW_MOD_ALT); + Check(One(events, WINDOW_EVENT_KEY_DOWN) && events[0].System && events[0].Modifiers == WINDOW_MOD_ALT, "a key pressed with Alt held is a system key"); + + events = Translate(Key(true, SDL_SCANCODE_F10, SDLK_F10, SDL_KMOD_NONE)); + Check(One(events, WINDOW_EVENT_KEY_DOWN) && events[0].System, "F10 is a system key on its own"); + + events = Translate(Key(true, SDL_SCANCODE_Q, SDLK_Q, (SDL_Keymod)(SDL_KMOD_LALT | SDL_KMOD_LCTRL)), 1.0f, WINDOW_MOD_CTRL | WINDOW_MOD_ALT); + Check(One(events, WINDOW_EVENT_KEY_DOWN) && !events[0].System, "a key pressed with Ctrl and Alt held is not a system key"); + + events = Translate(Key(true, SDL_SCANCODE_LANG1, SDLK_UNKNOWN, SDL_KMOD_NONE)); + Check(events.empty(), "a key Windows has no code for is dropped"); + + SDL_Event text = Event_Of(SDL_EVENT_TEXT_INPUT); + text.text.text = "a\xD0\x9F\xF0\x9F\x98\x80"; + events = Translate(text); + Check(events.size() == 3 && events[0].Type == WINDOW_EVENT_TEXT && events[0].Text == 'a' && events[1].Text == 0x041F && events[2].Text == 0x1F600, "typed text arrives one character at a time"); + + text.text.text = ""; + Check(Translate(text).empty(), "empty text is no character"); + + events = Translate(Event_Of(SDL_EVENT_WINDOW_FOCUS_LOST)); + Check(events.size() == 2 && events[0].Type == WINDOW_EVENT_CAPTURE_LOST && events[1].Type == WINDOW_EVENT_FOCUS_LOST, "losing the focus loses the capture first"); + Check(One(Translate(Event_Of(SDL_EVENT_WINDOW_FOCUS_GAINED)), WINDOW_EVENT_FOCUS_GAINED), "gaining the focus is a gain of focus"); + + SDL_Event resize = Event_Of(SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED); + resize.window.data1 = 800; + resize.window.data2 = 600; + events = Translate(resize); + Check(One(events, WINDOW_EVENT_RESIZED) && events[0].Width == 800 && events[0].Height == 600, "a new pixel size is a resize to that size"); + resize.window.data2 = 0; + Check(Translate(resize).empty(), "a size with no area is not a resize"); + Check(Translate(Event_Of(SDL_EVENT_WINDOW_RESIZED)).empty(), "a size in window coordinates is left to the pixel size"); + + Check(One(Translate(Event_Of(SDL_EVENT_WINDOW_EXPOSED)), WINDOW_EVENT_EXPOSED), "an exposed window is exposed"); + Check(One(Translate(Event_Of(SDL_EVENT_WINDOW_DISPLAY_CHANGED)), WINDOW_EVENT_DISPLAY_CHANGED), "a window moving to another display changes the display"); + Check(One(Translate(Event_Of(SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED)), WINDOW_EVENT_DISPLAY_CHANGED), "a display changing mode changes the display"); + Check(One(Translate(Event_Of(SDL_EVENT_WINDOW_CLOSE_REQUESTED)), WINDOW_EVENT_CLOSE_REQUESTED), "the close button asks to close"); + Check(One(Translate(Event_Of(SDL_EVENT_QUIT)), WINDOW_EVENT_CLOSE_REQUESTED), "a request to quit asks to close"); + Check(Translate(Event_Of(SDL_EVENT_WINDOW_MOUSE_ENTER)).empty(), "an event the game does not use produces nothing"); + + std::printf("\n%s\n", Failures == 0 ? "PASSED" : "FAILED"); + return(Failures == 0 ? 0 : 1); +} diff --git a/tests/sdlinput/CMakeLists.txt b/tests/sdlinput/CMakeLists.txt new file mode 100644 index 000000000..10d7f666c --- /dev/null +++ b/tests/sdlinput/CMakeLists.txt @@ -0,0 +1,12 @@ +# The state is fed SDL key events built by hand, so no window is needed; Windows' own key +# state is set with SetKeyboardState. + +opents_add_test(SDLInputContract + NAME sdlinputcontract + SOURCES sdlinputcontract.cpp + ENGINE + sdl/sdlinput.cpp + sdl/sdlkeys.cpp + DEFINITIONS WIN32 _WINDOWS NOMINMAX + LIBRARIES SDL3::SDL3-static +) diff --git a/tests/sdlinput/sdlinputcontract.cpp b/tests/sdlinput/sdlinputcontract.cpp new file mode 100644 index 000000000..666c82523 --- /dev/null +++ b/tests/sdlinput/sdlinputcontract.cpp @@ -0,0 +1,177 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Pins the keys the SDL layer reports held, by the Windows codes the game reads, as SDL's key +// events and Windows' own key state change. + +#include "sdl/sdlinput.h" + +#include "win.h" +#include "windowevent.hh" + +#include +#include + +#include +#include + +namespace { + +int Failures = 0; + + +void Check(bool condition, char const * what) +{ + std::printf("%-76s %s\n", what, condition ? "ok" : "FAILED"); + + if (!condition) { + Failures++; + } +} + + +Uint64 Clock = 0; + + +SDL_KeyboardEvent Key(bool down, SDL_Scancode scancode, SDL_Keycode keycode, SDL_Keymod modifiers = SDL_KMOD_NONE) +{ + SDL_KeyboardEvent key = {}; + key.type = down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP; + key.timestamp = ++Clock; + key.scancode = scancode; + key.key = keycode; + key.mod = modifiers; + key.down = down; + return(key); +} + + +// Queues and then handles a key, as the SDL layer does. +void Press(SDLInputStateClass & input, SDL_KeyboardEvent const & key) +{ + input.Note_Queued_Key(key); + input.Track_Key(key); +} + + +void Windows_Holds(std::initializer_list keys) +{ + BYTE state[256] = {}; + for (int key : keys) { + state[key] = 0x80; + } + SetKeyboardState(state); +} + +} + + +int main(void) +{ + Windows_Holds({}); + SDLInputStateClass input; + + Press(input, Key(true, SDL_SCANCODE_LSHIFT, SDLK_LSHIFT, SDL_KMOD_LSHIFT)); + Check(input.Key_Down(VK_SHIFT) && input.Key_Down(VK_LSHIFT) && !input.Key_Down(VK_RSHIFT) && input.Modifiers() == WINDOW_MOD_SHIFT, "a Shift press holds Shift and the side it was pressed on"); + Press(input, Key(false, SDL_SCANCODE_LSHIFT, SDLK_LSHIFT)); + Check(!input.Key_Down(VK_SHIFT) && !input.Key_Down(VK_LSHIFT) && input.Modifiers() == 0, "and its release lets both go"); + + // Windows shows the Left Ctrl it presses for AltGr only while the Right Alt press is queued. + SDL_KeyboardEvent altgr = Key(true, SDL_SCANCODE_RALT, SDLK_RALT, SDL_KMOD_RALT); + Windows_Holds({ VK_CONTROL, VK_LCONTROL }); + input.Note_Queued_Key(altgr); + Windows_Holds({}); + input.Track_Key(altgr); + Check(input.Key_Down(VK_CONTROL) && input.Key_Down(VK_LCONTROL) && input.Modifiers() == (WINDOW_MOD_CTRL | WINDOW_MOD_ALT), "a Right Alt queued while Windows shows Left Ctrl is AltGr, with its Ctrl"); + Press(input, Key(false, SDL_SCANCODE_RALT, SDLK_RALT)); + Check(!input.Key_Down(VK_CONTROL) && input.Modifiers() == 0, "and Ctrl is released with AltGr"); + + Press(input, Key(true, SDL_SCANCODE_RALT, SDLK_RALT, SDL_KMOD_RALT)); + Check(!input.Key_Down(VK_CONTROL) && input.Key_Down(VK_RMENU) && input.Modifiers() == WINDOW_MOD_ALT, "a Right Alt queued without it is Alt alone"); + Press(input, Key(false, SDL_SCANCODE_RALT, SDLK_RALT)); + + SDL_SetModState(SDL_KMOD_CAPS); + input.Focus_Gained(); + Check(input.Key_Toggled(VK_CAPITAL) && (input.Modifiers() & WINDOW_MOD_CAPS) != 0, "the lock keys are read again when the window regains the focus"); + SDL_SetModState(SDL_KMOD_NONE); + input.Focus_Gained(); + Check(!input.Key_Toggled(VK_CAPITAL), "and again when a lock goes off"); + + // SDL reports no key still held after it resets the keyboard, and drops such a key's release. + Windows_Holds({ VK_SHIFT, VK_LSHIFT }); + input.Focus_Gained(); + Check(input.Key_Down(VK_SHIFT) && input.Key_Down(VK_LSHIFT) && !input.Key_Down(VK_RSHIFT) && input.Modifiers() == WINDOW_MOD_SHIFT, "a Shift Windows holds as the window gains the focus is held"); + Windows_Holds({}); + input.Drop_Released_Keys(); + Check(input.Modifiers() == 0, "and released for the next event once Windows releases it"); + Windows_Holds({ VK_SHIFT, VK_LSHIFT }); + input.Focus_Gained(); + Windows_Holds({}); + Check(!input.Key_Down(VK_SHIFT), "and read released at once, with no event"); + + Windows_Holds({ VK_SHIFT, VK_LSHIFT }); + input.Focus_Gained(); + input.Focus_Lost(); + Check(!input.Key_Down(VK_SHIFT), "losing the focus releases it"); + + Windows_Holds({ VK_SHIFT, VK_LSHIFT }); + input.Focus_Gained(); + Press(input, Key(true, SDL_SCANCODE_LSHIFT, SDLK_LSHIFT, SDL_KMOD_LSHIFT)); + Windows_Holds({}); + Check(input.Key_Down(VK_SHIFT), "once SDL reports the key, Windows no longer releases it"); + Press(input, Key(false, SDL_SCANCODE_LSHIFT, SDLK_LSHIFT)); + Check(!input.Key_Down(VK_SHIFT), "and SDL's release does"); + + Windows_Holds({ VK_MENU, VK_RMENU }); + input.Hold_Windows_Keys(); + Check(input.Key_Down(VK_MENU) && input.Key_Down(VK_RMENU) && !input.Key_Down(VK_LMENU), "a modifier Windows holds after a window drag is held"); + Windows_Holds({}); + input.Drop_Released_Keys(); + + Windows_Holds({ VK_CONTROL, VK_LCONTROL }); + input.Focus_Gained(); + SDL_KeyboardEvent ralt = Key(true, SDL_SCANCODE_RALT, SDLK_RALT, SDL_KMOD_RALT); + input.Note_Queued_Key(ralt); + Windows_Holds({}); + input.Drop_Released_Keys(); + input.Track_Key(ralt); + Check(!input.Key_Down(VK_CONTROL) && input.Modifiers() == WINDOW_MOD_ALT, "a Right Alt pressed with a Left Ctrl held into the window is not AltGr"); + Press(input, Key(false, SDL_SCANCODE_RALT, SDLK_RALT)); + + Windows_Holds({ 'A' }); + input.Focus_Gained(); + Check(input.Key_Down('A') && input.Modifiers() == 0, "any key Windows holds as the window gains the focus is held"); + Windows_Holds({}); + Check(!input.Key_Down('A'), "and released when Windows releases it"); + Windows_Holds({ 'A' }); + input.Focus_Gained(); + SDL_KeyboardEvent a = Key(true, SDL_SCANCODE_A, SDLK_A); + input.Note_Queued_Key(a); + Check(input.Track_Key(a), "its first press from SDL continues the key held into the window"); + Windows_Holds({}); + Check(input.Key_Down('A'), "and from then on SDL reports it"); + a = Key(false, SDL_SCANCODE_A, SDLK_A); + Press(input, a); + Check(!input.Key_Down('A'), "including its release"); + SDL_KeyboardEvent b = Key(true, SDL_SCANCODE_B, SDLK_B); + input.Note_Queued_Key(b); + Check(!input.Track_Key(b), "a key pressed after the window gains the focus is a new press"); + Press(input, Key(false, SDL_SCANCODE_B, SDLK_B)); + + Windows_Holds({ VK_SHIFT, VK_LSHIFT, VK_RSHIFT }); + input.Focus_Gained(); + Press(input, Key(true, SDL_SCANCODE_LSHIFT, SDLK_LSHIFT, SDL_KMOD_LSHIFT)); + Press(input, Key(false, SDL_SCANCODE_LSHIFT, SDLK_LSHIFT)); + Check(input.Key_Down(VK_SHIFT) && input.Key_Down(VK_RSHIFT) && !input.Key_Down(VK_LSHIFT), "releasing one Shift held into the window leaves the other held"); + Windows_Holds({}); + input.Drop_Released_Keys(); + + std::printf("\n%s\n", Failures == 0 ? "PASSED" : "FAILED"); + return(Failures == 0 ? 0 : 1); +} diff --git a/tests/sdlkeys/CMakeLists.txt b/tests/sdlkeys/CMakeLists.txt new file mode 100644 index 000000000..8762197e9 --- /dev/null +++ b/tests/sdlkeys/CMakeLists.txt @@ -0,0 +1,10 @@ +# Links the static SDL library as the engine does, so a runtime library mismatch fails the link. + +opents_add_test(SDLKeysContract + NAME sdlkeyscontract + SOURCES sdlkeyscontract.cpp + ENGINE + sdl/sdlkeys.cpp + DEFINITIONS WIN32 _WINDOWS NOMINMAX + LIBRARIES SDL3::SDL3-static +) diff --git a/tests/sdlkeys/sdlkeyscontract.cpp b/tests/sdlkeys/sdlkeyscontract.cpp new file mode 100644 index 000000000..dca332d9c --- /dev/null +++ b/tests/sdlkeys/sdlkeyscontract.cpp @@ -0,0 +1,184 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Pins the virtual-key code each SDL key becomes, since saved hotkeys store those codes, and +// the names the keyboard screen shows for them. + +#include "sdl/sdlkeys.h" + +#include "win.h" + +#include +#include +#include + +#include + +namespace { + +int Failures = 0; + + +void Check(bool condition, char const * what) +{ + std::printf("%-76s %s\n", what, condition ? "ok" : "FAILED"); + + if (!condition) { + Failures++; + } +} + + +int Key(SDL_Scancode scancode, SDL_Keycode keycode, SDL_Keymod modifiers = SDL_KMOD_NONE) +{ + return(Virtual_Key_From_SDL(scancode, keycode, modifiers)); +} + + +// A key whose layout gives it no character, as SDL reports for keys like the arrows. +int Unprinted(SDL_Scancode scancode, SDL_Keymod modifiers = SDL_KMOD_NONE) +{ + return(Key(scancode, SDLK_UNKNOWN, modifiers)); +} + + +int Scanned(SDL_Scancode scancode, Uint16 raw, HKL layout, SDL_Keymod modifiers = SDL_KMOD_NONE) +{ + return(Virtual_Key_From_SDL(scancode, SDLK_UNKNOWN, modifiers, raw, layout)); +} + + +// A keyboard layout, which the checks load only when the session lacks it and unload after. +struct TestLayout +{ + HKL Layout = NULL; + bool Loaded = false; + + TestLayout(WORD language, wchar_t const * name) + { + HKL layouts[64]; + int const count = GetKeyboardLayoutList(64, layouts); + for (int index = 0; index < count; index++) { + if (LOWORD((UINT_PTR)layouts[index]) == language) { + Layout = layouts[index]; + return; + } + } + Layout = LoadKeyboardLayoutW(name, KLF_NOTELLSHELL); + Loaded = (Layout != NULL); + } + + ~TestLayout(void) + { + if (Loaded) { + UnloadKeyboardLayout(Layout); + } + } +}; + +} + + +int main(void) +{ + Check(Key(SDL_SCANCODE_A, SDLK_A) == 'A', "a letter key is its capital letter"); + Check(Key(SDL_SCANCODE_Q, SDLK_A) == 'A', "the key an AZERTY layout prints A on is A"); + Check(Key(SDL_SCANCODE_Y, SDLK_Z) == 'Z', "the key a QWERTZ layout prints Z on is Z"); + Check(Key(SDL_SCANCODE_SEMICOLON, SDLK_M) == 'M', "a letter off the US letter keys is still that letter"); + Check(Key(SDL_SCANCODE_1, SDLK_1) == '1' && Key(SDL_SCANCODE_0, SDLK_0) == '0', "a digit key is its digit"); + Check(Key(SDL_SCANCODE_1, SDLK_AMPERSAND) == '1', "a digit key printing a symbol is still its digit"); + Check(Key(SDL_SCANCODE_M, SDLK_COMMA) == VK_OEM_COMMA, "the key a layout prints a comma on is the comma key"); + Check(Key(SDL_SCANCODE_RIGHTBRACKET, SDLK_PLUS) == VK_OEM_PLUS, "the key a layout prints a plus on is the plus key"); + Check(Key(SDL_SCANCODE_EQUALS, SDLK_EQUALS) == VK_OEM_PLUS, "the US equals key is the plus key"); + Check(Key(SDL_SCANCODE_MINUS, (SDL_Keycode)0xDF) == VK_OEM_MINUS, "without a scan code, a key printing a letter with no code keeps its US position"); + Check(Key(SDL_SCANCODE_GRAVE, SDLK_GRAVE) == VK_OEM_3 && Key(SDL_SCANCODE_SLASH, SDLK_SLASH) == VK_OEM_2, "the other punctuation keys keep their US positions"); + Check(Key(SDL_SCANCODE_NONUSBACKSLASH, SDLK_LESS) == VK_OEM_102, "the extra key beside left Shift is its own key"); + + Check(Unprinted(SDL_SCANCODE_KP_7, SDL_KMOD_NUM) == VK_NUMPAD7, "a keypad digit with Num Lock on is the keypad digit"); + Check(Unprinted(SDL_SCANCODE_KP_7) == VK_HOME, "the same key with Num Lock off is Home"); + Check(Unprinted(SDL_SCANCODE_KP_7, (SDL_Keymod)(SDL_KMOD_NUM | SDL_KMOD_LSHIFT)) == VK_HOME, "and with Shift held is Home too"); + Check(Unprinted(SDL_SCANCODE_KP_5) == VK_CLEAR && Unprinted(SDL_SCANCODE_KP_0) == VK_INSERT, "the keypad centre and zero are Clear and Insert without Num Lock"); + Check(Unprinted(SDL_SCANCODE_KP_PERIOD, SDL_KMOD_NUM) == VK_DECIMAL && Unprinted(SDL_SCANCODE_KP_PERIOD) == VK_DELETE, "the keypad point is the decimal key or Delete"); + Check(Unprinted(SDL_SCANCODE_KP_ENTER) == VK_RETURN, "the keypad Enter is Enter"); + Check(Unprinted(SDL_SCANCODE_KP_PLUS) == VK_ADD && Unprinted(SDL_SCANCODE_KP_MINUS) == VK_SUBTRACT, "the keypad operators are the keypad's own keys"); + Check(Unprinted(SDL_SCANCODE_KP_MULTIPLY) == VK_MULTIPLY && Unprinted(SDL_SCANCODE_KP_DIVIDE) == VK_DIVIDE, "whether or not Num Lock is on"); + + Check(Unprinted(SDL_SCANCODE_LSHIFT) == VK_SHIFT && Unprinted(SDL_SCANCODE_RSHIFT) == VK_SHIFT, "either Shift is Shift"); + Check(Unprinted(SDL_SCANCODE_LCTRL) == VK_CONTROL && Unprinted(SDL_SCANCODE_RCTRL) == VK_CONTROL, "either Ctrl is Ctrl"); + Check(Unprinted(SDL_SCANCODE_LALT) == VK_MENU && Unprinted(SDL_SCANCODE_RALT) == VK_MENU, "either Alt is Alt"); + Check(Unprinted(SDL_SCANCODE_LGUI) == VK_LWIN && Unprinted(SDL_SCANCODE_RGUI) == VK_RWIN, "the Windows keys keep their sides"); + + Check(Unprinted(SDL_SCANCODE_F1) == VK_F1 && Unprinted(SDL_SCANCODE_F12) == VK_F12, "the function keys run from F1 to F12"); + Check(Unprinted(SDL_SCANCODE_F13) == VK_F13 && Unprinted(SDL_SCANCODE_F24) == VK_F24, "and on from F13 to F24"); + Check(Unprinted(SDL_SCANCODE_PAGEUP) == VK_PRIOR && Unprinted(SDL_SCANCODE_PAGEDOWN) == VK_NEXT, "Page Up and Page Down are Prior and Next"); + Check(Unprinted(SDL_SCANCODE_LEFT) == VK_LEFT && Unprinted(SDL_SCANCODE_DOWN) == VK_DOWN, "the arrows are the arrows"); + Check(Unprinted(SDL_SCANCODE_ESCAPE) == VK_ESCAPE && Unprinted(SDL_SCANCODE_BACKSPACE) == VK_BACK, "Escape and Backspace are Escape and Back"); + Check(Key(SDL_SCANCODE_RETURN, SDLK_RETURN) == VK_RETURN && Key(SDL_SCANCODE_SPACE, SDLK_SPACE) == VK_SPACE, "Enter and Space are Return and Space"); + Check(Unprinted(SDL_SCANCODE_SCROLLLOCK) == VK_SCROLL && Unprinted(SDL_SCANCODE_PAUSE) == VK_PAUSE, "Scroll Lock and Pause are their own keys"); + Check(Unprinted(SDL_SCANCODE_PRINTSCREEN) == VK_SNAPSHOT, "Print Screen is the snapshot key"); + Check(Unprinted(SDL_SCANCODE_CAPSLOCK) == VK_CAPITAL && Unprinted(SDL_SCANCODE_NUMLOCKCLEAR) == VK_NUMLOCK, "Caps Lock and Num Lock are their own keys"); + Check(Unprinted(SDL_SCANCODE_LANG1) == 0, "a key Windows has no code for is dropped"); + + bool named = true; + for (int letter = 0; letter < 26; letter++) { + char name[2] = { (char)('A' + letter), '\0' }; + SDL_Scancode scancode = SDL_GetScancodeFromName(name); + named = named && Unprinted(scancode) == 'A' + letter; + } + Check(named, "every letter key named by SDL is its letter when its layout prints none"); + + HKL const current = GetKeyboardLayout(0); + Check(Scanned(SDL_SCANCODE_F1, 0x3B, current) == VK_F1 && Scanned(SDL_SCANCODE_UP, 0xE048, current) == VK_UP, "a scan code leaves the function and arrow keys their fixed codes"); + Check(Scanned(SDL_SCANCODE_KP_8, 0x48, current, SDL_KMOD_NUM) == VK_NUMPAD8 && Scanned(SDL_SCANCODE_KP_8, 0x48, current) == VK_UP, "and the keypad still follows Num Lock"); + Check(Scanned(SDL_SCANCODE_RSHIFT, 0x36, current) == VK_SHIFT && Scanned(SDL_SCANCODE_RCTRL, 0xE01D, current) == VK_CONTROL, "and either side of a modifier is still the modifier"); + Check(Scanned(SDL_SCANCODE_PAUSE, 0xE046, current) == VK_PAUSE, "and Pause is Pause"); + + bool native = true; + for (unsigned int raw = 0x02; raw <= 0x35; raw++) { + UINT const expected = MapVirtualKeyExW(raw, MAPVK_VSC_TO_VK_EX, current); + if (expected >= 'A' && expected <= 'Z') { + native = native && Scanned(SDL_SCANCODE_A, raw, current) == (int)expected; + } + } + Check(native, "a character key takes the code the active layout gives its scan code"); + + { + TestLayout german(0x0407, L"00000407"); + if (german.Layout != NULL && MapVirtualKeyExW(0x0C, MAPVK_VSC_TO_VK_EX, german.Layout) == 0xDB) { + Check(Scanned(SDL_SCANCODE_MINUS, 0x0C, german.Layout) == 0xDB, "on a German layout, the sharp s key keeps its own code"); + Check(Scanned(SDL_SCANCODE_SLASH, 0x35, german.Layout) == VK_OEM_MINUS, "and the minus key keeps the minus code"); + Check(Scanned(SDL_SCANCODE_SEMICOLON, 0x27, german.Layout) == 0xC0 && Scanned(SDL_SCANCODE_LEFTBRACKET, 0x1A, german.Layout) == 0xBA, "and the umlaut keys keep theirs"); + Check(Scanned(SDL_SCANCODE_Y, 0x15, german.Layout) == 'Z' && Scanned(SDL_SCANCODE_Z, 0x2C, german.Layout) == 'Y', "and the Z and Y keys swap"); + Check(Virtual_Key_Name(0xDB, german.Layout) == "\xC3\x9F" && Virtual_Key_Name(0xC0, german.Layout) == "\xC3\x96", "and those keys are named by what German prints on them"); + } else { + std::printf("%-76s %s\n", "the German layout checks", "skipped: the system has no German layout"); + } + } + + { + TestLayout us(0x0409, L"00000409"); + if (us.Layout != NULL) { + Check(Virtual_Key_Name('A', us.Layout) == "A" && Virtual_Key_Name('7', us.Layout) == "7", "a letter or digit key is named by what it prints"); + Check(Virtual_Key_Name(VK_OEM_COMMA, us.Layout) == ",", "a punctuation key is named by what it prints"); + } else { + std::printf("%-76s %s\n", "the US layout checks", "skipped: the system has no US layout"); + } + } + + // A key the layout prints nothing on takes the English name SDL gives its position. + Check(Virtual_Key_Name(VK_NUMPAD7) == "Keypad 7", "a keypad digit is named as the keypad's"); + Check(Virtual_Key_Name(VK_HOME) == "Home" && Virtual_Key_Name(VK_INSERT) == "Insert", "a navigation key is named after the main block's key"); + Check(Virtual_Key_Name(VK_F1) == "F1" && Virtual_Key_Name(VK_F12) == "F12", "a function key is named by its number"); + Check(Virtual_Key_Name(VK_RETURN) == "Return" && Virtual_Key_Name(VK_SPACE) == "Space", "Enter and Space have SDL's names"); + Check(Virtual_Key_Name(VK_SHIFT) == "Shift" && Virtual_Key_Name(VK_CONTROL) == "Ctrl" && Virtual_Key_Name(VK_MENU) == "Alt", "the modifiers are named without a side"); + Check(Virtual_Key_Name(0).empty() && Virtual_Key_Name(0xFF).empty(), "a code no key produces has no name"); + + std::printf("\n%s\n", Failures == 0 ? "PASSED" : "FAILED"); + return(Failures == 0 ? 0 : 1); +} diff --git a/tests/sdlwindow/CMakeLists.txt b/tests/sdlwindow/CMakeLists.txt new file mode 100644 index 000000000..284f0adbc --- /dev/null +++ b/tests/sdlwindow/CMakeLists.txt @@ -0,0 +1,15 @@ +# Runs the real SDL layer over a small window with a recording stand-in for the game's +# handler. The test needs a desktop session for the window. +opents_add_test(SDLWindow + NAME sdlwindow + SOURCES sdlwindowcontract.cpp sdlwindowstub.cpp + ENGINE + sdl/sdlwindow.cpp + sdl/sdlevents.cpp + sdl/sdlinput.cpp + sdl/sdlkeys.cpp + utf8.cpp + DEFINITIONS WIN32 _WINDOWS NOMINMAX + LIBRARIES SDL3::SDL3-static comctl32 + UTF8 +) diff --git a/tests/sdlwindow/sdlwindowcontract.cpp b/tests/sdlwindow/sdlwindowcontract.cpp new file mode 100644 index 000000000..152ca08c7 --- /dev/null +++ b/tests/sdlwindow/sdlwindowcontract.cpp @@ -0,0 +1,297 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Pins what the game receives from the SDL layer's pump: each event once and in order, with +// the keys reported held as of that event, however many events one pump delivers. + +#include "gamewindow.h" +#include "sdl/sdlwindow.h" +#include "windowevent.hh" + +#include "win.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +int Failures = 0; + + +void Check(bool condition, char const * what) +{ + std::printf("%-76s %s\n", what, condition ? "ok" : "FAILED"); + + if (!condition) { + Failures++; + } +} + + +std::vector Received; + +// Whether the SDL layer reported Ctrl held while each received event was handled. +std::vector CtrlHeld; + + +void Push_Key(bool down, SDL_Scancode scancode, SDL_Keycode keycode, SDL_Keymod modifiers = SDL_KMOD_NONE) +{ + SDL_Event event = {}; + event.type = down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP; + event.key.scancode = scancode; + event.key.key = keycode; + event.key.mod = modifiers; + event.key.down = down; + SDL_PushEvent(&event); +} + + +void Push_Window(SDL_EventType type) +{ + SDL_Event event = {}; + event.type = type; + SDL_PushEvent(&event); +} + + +void Push_Click(void) +{ + SDL_Event event = {}; + event.type = SDL_EVENT_MOUSE_BUTTON_DOWN; + event.button.button = SDL_BUTTON_LEFT; + event.button.down = true; + event.button.clicks = 1; + SDL_PushEvent(&event); +} + + +// An event the last pump delivered, and whether Ctrl was reported held while it was handled. +struct Delivered +{ + WindowEvent Event; + bool Ctrl; +}; + + +// The events of one type the last pump delivered. +std::vector Pumped(WindowEventType type) +{ + Received.clear(); + CtrlHeld.clear(); + Main_Window_Pump_Events(); + + std::vector result; + for (size_t index = 0; index < Received.size(); index++) { + if (Received[index].Type == type) { + result.push_back({ Received[index], CtrlHeld[index] }); + } + } + return(result); +} + + +// SDL takes the capture only for the window it last saw the pointer over, and a released +// capture can clear that until the pointer moves or a button is pressed. +bool Point_At(SDL_Window * window) +{ + int width = 0; + int height = 0; + SDL_GetWindowSize(window, &width, &height); + SDL_WarpMouseInWindow(window, width / 2.0f, height / 2.0f); + Pumped(WINDOW_EVENT_NONE); + return(SDL_GetMouseFocus() == window); +} + +} + + +// Stands in for the game's window handler and records what the SDL layer hands it. +void Game_Window_Handle_Event(WindowEvent const & event) +{ + Received.push_back(event); + CtrlHeld.push_back(Main_Window_Key_Down(VK_CONTROL)); +} + + +int main(void) +{ + // Windows keeps a program from taking the foreground from another, so the window may get + // no keyboard focus otherwise. + SDL_SetHint(SDL_HINT_FORCE_RAISEWINDOW, "1"); + if (!Main_Window_Create(true, 64, 48)) { + std::printf("the main window could not be created\n\nFAILED\n"); + return(1); + } + Main_Window_Pump_Events(); + + Push_Key(true, SDL_SCANCODE_A, SDLK_A); + Push_Key(true, SDL_SCANCODE_B, SDLK_B); + std::vector keys = Pumped(WINDOW_EVENT_KEY_DOWN); + Check(keys.size() == 2 && keys[0].Event.VirtualKey == 'A' && keys[1].Event.VirtualKey == 'B', "each queued key reaches the game once, in order"); + + Push_Key(false, SDL_SCANCODE_A, SDLK_A); + Push_Key(false, SDL_SCANCODE_B, SDLK_B); + Pumped(WINDOW_EVENT_KEY_UP); + + Check(!Main_Window_Key_Down(VK_CONTROL), "no key is held before one is pressed"); + Push_Key(true, SDL_SCANCODE_LCTRL, SDLK_LCTRL, SDL_KMOD_LCTRL); + Push_Key(true, SDL_SCANCODE_A, SDLK_A, SDL_KMOD_LCTRL); + keys = Pumped(WINDOW_EVENT_KEY_DOWN); + Check(keys.size() == 2 && keys[1].Ctrl && (keys[1].Event.Modifiers & WINDOW_MOD_CTRL) != 0, "a key pressed after Ctrl in the same pump sees Ctrl held"); + Check(Main_Window_Key_Down(VK_LCONTROL) && !Main_Window_Key_Down(VK_RCONTROL), "and the side it was pressed on"); + + Push_Click(); + std::vector clicks = Pumped(WINDOW_EVENT_MOUSE_DOWN); + Check(clicks.size() == 1 && (clicks[0].Event.Modifiers & WINDOW_MOD_CTRL) != 0, "a click while Ctrl is held carries Ctrl"); + + Push_Key(false, SDL_SCANCODE_A, SDLK_A, SDL_KMOD_LCTRL); + Push_Key(false, SDL_SCANCODE_LCTRL, SDLK_LCTRL); + Push_Key(true, SDL_SCANCODE_B, SDLK_B); + Push_Click(); + Received.clear(); + CtrlHeld.clear(); + Main_Window_Pump_Events(); + bool released = false; + bool clicked = false; + for (size_t index = 0; index < Received.size(); index++) { + if (Received[index].Type == WINDOW_EVENT_KEY_DOWN && Received[index].VirtualKey == 'B') { + released = !CtrlHeld[index] && (Received[index].Modifiers & WINDOW_MOD_CTRL) == 0; + } + if (Received[index].Type == WINDOW_EVENT_MOUSE_DOWN) { + clicked = true; + released = released && (Received[index].Modifiers & WINDOW_MOD_CTRL) == 0; + } + } + Check(released && clicked, "a key or click after Ctrl's release in the same pump sees it released"); + + Push_Key(false, SDL_SCANCODE_B, SDLK_B); + Pumped(WINDOW_EVENT_KEY_UP); + + // Windows reports the Left Ctrl it presses for AltGr only while the Right Alt press is queued. + BYTE state[256] = {}; + state[VK_CONTROL] = 0x80; + state[VK_LCONTROL] = 0x80; + SetKeyboardState(state); + Push_Key(true, SDL_SCANCODE_RALT, SDLK_RALT, SDL_KMOD_RALT); + std::memset(state, 0, sizeof(state)); + SetKeyboardState(state); + Push_Key(true, SDL_SCANCODE_Q, SDLK_Q, SDL_KMOD_RALT); + Push_Key(false, SDL_SCANCODE_Q, SDLK_Q, SDL_KMOD_RALT); + Push_Key(false, SDL_SCANCODE_RALT, SDLK_RALT); + keys = Pumped(WINDOW_EVENT_KEY_DOWN); + Check(keys.size() == 2 && keys[1].Ctrl && keys[1].Event.Modifiers == (WINDOW_MOD_CTRL | WINDOW_MOD_ALT) && !keys[1].Event.System, "a key typed with AltGr in one pump carries AltGr's Ctrl"); + Check(!Main_Window_Key_Down(VK_CONTROL), "and Ctrl is released with AltGr"); + + Push_Key(true, SDL_SCANCODE_LALT, SDLK_LALT, SDL_KMOD_LALT); + Push_Key(false, SDL_SCANCODE_LALT, SDLK_LALT); + keys = Pumped(WINDOW_EVENT_KEY_UP); + Check(keys.size() == 1 && keys[0].Event.VirtualKey == VK_MENU && keys[0].Event.System, "Alt's own release is a system key, as Windows reports it"); + + HWND const window = (HWND)Main_Window_Native().Handle; + + // SDL reports no key still held after it resets the keyboard, and drops such a key's release. + std::memset(state, 0, sizeof(state)); + state[VK_SHIFT] = 0x80; + state[VK_LSHIFT] = 0x80; + SetKeyboardState(state); + Push_Window(SDL_EVENT_WINDOW_FOCUS_GAINED); + Pumped(WINDOW_EVENT_FOCUS_GAINED); + Check(Main_Window_Key_Down(VK_SHIFT) && Main_Window_Key_Down(VK_LSHIFT) && !Main_Window_Key_Down(VK_RSHIFT), "a Shift held as the window gains the focus is held"); + Push_Click(); + clicks = Pumped(WINDOW_EVENT_MOUSE_DOWN); + Push_Key(true, SDL_SCANCODE_A, SDLK_A); + keys = Pumped(WINDOW_EVENT_KEY_DOWN); + Check(clicks.size() == 1 && (clicks[0].Event.Modifiers & WINDOW_MOD_SHIFT) != 0 && keys.size() == 1 && (keys[0].Event.Modifiers & WINDOW_MOD_SHIFT) != 0, "and a click or a key after it carries Shift"); + Push_Key(false, SDL_SCANCODE_A, SDLK_A); + Pumped(WINDOW_EVENT_KEY_UP); + + std::memset(state, 0, sizeof(state)); + SetKeyboardState(state); + Push_Click(); + clicks = Pumped(WINDOW_EVENT_MOUSE_DOWN); + Check(clicks.size() == 1 && (clicks[0].Event.Modifiers & WINDOW_MOD_SHIFT) == 0 && !Main_Window_Key_Down(VK_SHIFT), "it is released when Windows releases it, though SDL reports no release"); + + // SDL's first press of a key held as the window gains the focus is one Windows reports as a + // repeat. + state['A'] = 0x80; + SetKeyboardState(state); + Push_Window(SDL_EVENT_WINDOW_FOCUS_GAINED); + Pumped(WINDOW_EVENT_FOCUS_GAINED); + Push_Key(true, SDL_SCANCODE_A, SDLK_A); + keys = Pumped(WINDOW_EVENT_KEY_DOWN); + Check(keys.size() == 1 && keys[0].Event.Repeat && Main_Window_Key_Down('A'), "a key held as the window gains the focus reaches the game as a repeat"); + std::memset(state, 0, sizeof(state)); + SetKeyboardState(state); + Push_Key(false, SDL_SCANCODE_A, SDLK_A); + keys = Pumped(WINDOW_EVENT_KEY_UP); + Check(keys.size() == 1 && !Main_Window_Key_Down('A'), "and its release reaches the game"); + + // The drag loop and the mouse capture need the keyboard focus. + if (SDL_GetKeyboardFocus() == nullptr) { + std::printf("%-76s %s\n", "the checks that need the keyboard focus", "not run"); + } else { + SendMessageW(window, WM_ENTERSIZEMOVE, 0, 0); + state[VK_SHIFT] = 0x80; + state[VK_LSHIFT] = 0x80; + SetKeyboardState(state); + SendMessageW(window, WM_EXITSIZEMOVE, 0, 0); + Pumped(WINDOW_EVENT_NONE); + Check(Main_Window_Key_Down(VK_SHIFT), "a Shift held through a drag of the window is held after it"); + std::memset(state, 0, sizeof(state)); + SetKeyboardState(state); + Pumped(WINDOW_EVENT_NONE); + + SDL_Window * const sdlwindow = SDL_GetKeyboardFocus(); + POINT pointer = {}; + GetCursorPos(&pointer); + if (!Point_At(sdlwindow)) { + std::printf("%-76s %s\n", "the checks that need the pointer over the window", "not run"); + } else { + Main_Window_Capture_Mouse(true); + bool const captured = Main_Window_Mouse_Captured() && GetCapture() == window; + Check(captured, "the window can take the mouse capture"); + if (captured) { + SendMessageW(window, WM_CANCELMODE, 0, 0); + Check(!Main_Window_Mouse_Captured(), "a capture Windows cancels is reported gone at once"); + Check(Pumped(WINDOW_EVENT_CAPTURE_LOST).size() == 1, "and reaches the game as one lost capture"); + + Main_Window_Capture_Mouse(true); + Check(Main_Window_Mouse_Captured() && GetCapture() == window, "the capture can be taken again after it was cancelled"); + + Main_Window_Capture_Mouse(false); + Check(!Main_Window_Mouse_Captured() && Pumped(WINDOW_EVENT_CAPTURE_LOST).empty(), "releasing it is no lost capture"); + + HWND other = CreateWindowExW(0, L"STATIC", L"", WS_POPUP, 0, 0, 8, 8, NULL, NULL, GetModuleHandleW(NULL), NULL); + Point_At(sdlwindow); + Main_Window_Capture_Mouse(true); + Check(Main_Window_Mouse_Captured() && GetCapture() == window, "the capture can be taken again after it was released"); + SetCapture(other); + Check(!Main_Window_Mouse_Captured(), "another window taking the capture is reported at once"); + Check(Pumped(WINDOW_EVENT_CAPTURE_LOST).size() == 1, "and reaches the game as one lost capture"); + ReleaseCapture(); + DestroyWindow(other); + Main_Window_Capture_Mouse(false); + Pumped(WINDOW_EVENT_CAPTURE_LOST); + } + } + SetCursorPos(pointer.x, pointer.y); + } + + Main_Window_Destroy(); + + std::printf("\n%s\n", Failures == 0 ? "PASSED" : "FAILED"); + return(Failures == 0 ? 0 : 1); +} diff --git a/tests/sdlwindow/sdlwindowstub.cpp b/tests/sdlwindow/sdlwindowstub.cpp new file mode 100644 index 000000000..183550922 --- /dev/null +++ b/tests/sdlwindow/sdlwindowstub.cpp @@ -0,0 +1,22 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Stands in for the game's instance handle and debug log. The executable has no cursor +// resource, so the SDL layer falls back to the system arrow. + +#include "dbgprint.h" +#include "win.h" + + +HINSTANCE ProgramInstance = GetModuleHandleW(NULL); + + +void __cdecl DebugString(char const *, ...) +{ +} diff --git a/tests/uilogic/uilogictest.cpp b/tests/uilogic/uilogictest.cpp index ba38c1887..e628e691c 100644 --- a/tests/uilogic/uilogictest.cpp +++ b/tests/uilogic/uilogictest.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include @@ -37,19 +36,6 @@ void Check(bool condition, char const * what) } -std::vector Decode(UIUTF8DecoderClass & decoder, std::initializer_list bytes) -{ - std::vector result; - for (unsigned char byte : bytes) { - UIInputText text = decoder.Feed(byte); - for (unsigned index = 0; index < text.Count; index++) { - result.push_back(text.Codepoints[index]); - } - } - return(result); -} - - void Test_Ownership(void) { UIInputStateClass state; @@ -135,34 +121,6 @@ void Test_Reconciliation(void) } -void Test_Text(void) -{ - UIUTF8DecoderClass decoder; - - Check(Decode(decoder, { 0x41, 0x0A }) == std::vector { U'A', U'\n' }, "ASCII text passes through"); - Check(Decode(decoder, { 0xC3, 0xA9, 0xE2, 0x82, 0xAC, 0xF0, 0x9F, 0x98, 0x80 }) == std::vector { 0xE9, 0x20AC, 0x1F600 }, "two-, three- and four-byte sequences decode"); - Check(decoder.Feed(0xE2).Count == 0 && decoder.Feed(0x82).Count == 0, "a partial sequence waits for its bytes"); - UIInputText completed = decoder.Feed(0xAC); - Check(completed.Count == 1 && completed.Codepoints[0] == 0x20AC, "a sequence can span messages"); - Check(decoder.Feed(0xE2).Count == 0, "a truncated prefix stays pending"); - UIInputText repaired = decoder.Feed(0x42); - Check(repaired.Count == 2 && repaired.Codepoints[0] == 0xFFFD && repaired.Codepoints[1] == U'B', "a malformed prefix does not swallow the ASCII after it"); - Check(Decode(decoder, { 0xE2, 0xC3, 0xA9 }) == std::vector { 0xFFFD, 0xE9 }, "a malformed prefix does not swallow the sequence after it"); - Check(Decode(decoder, { 0xC0, 0xAF }) == std::vector { 0xFFFD, 0xFFFD }, "invalid lead bytes are rejected"); - Check(Decode(decoder, { 0xE0, 0x80, 0x80 }) == std::vector { 0xFFFD }, "an overlong encoding is rejected"); - Check(Decode(decoder, { 0xED, 0xA0, 0x80 }) == std::vector { 0xFFFD }, "an encoded surrogate is rejected"); - Check(Decode(decoder, { 0xF4, 0x90, 0x80, 0x80 }) == std::vector { 0xFFFD }, "a code point past U+10FFFF is rejected"); - Check(Decode(decoder, { 0x80, 0x41 }) == std::vector { 0xFFFD, U'A' }, "a stray continuation byte does not swallow the text after it"); - decoder.Feed(0xF0); - decoder.Feed(0x9F); - decoder.Reset(); - Check(Decode(decoder, { 0x43 }) == std::vector { U'C' }, "a reset drops an incomplete sequence"); - decoder.Feed(0xE2); - decoder.Reset(); - Check(Decode(decoder, { 0x82, 0xAC }) == std::vector { 0xFFFD, 0xFFFD }, "a reset cannot splice fragments across owners"); -} - - void Test_Render_Math(void) { std::uint32_t bytes = 0; @@ -558,7 +516,6 @@ int main(void) { Test_Ownership(); Test_Reconciliation(); - Test_Text(); Test_Render_Math(); Test_Render_Transform(); Test_Render_Mask(); diff --git a/tests/uishell/CMakeLists.txt b/tests/uishell/CMakeLists.txt index b08547aac..8dae2545e 100644 --- a/tests/uishell/CMakeLists.txt +++ b/tests/uishell/CMakeLists.txt @@ -12,7 +12,6 @@ opents_add_test(UIShell ui/uiinput.cpp ui/uishell.cpp ui/uireveal.cpp - ui/uiunicode.cpp utf8.cpp hsv.cpp rgb.cpp diff --git a/tests/uishell/uishell.cpp b/tests/uishell/uishell.cpp index f41a5a3d7..b63f49573 100644 --- a/tests/uishell/uishell.cpp +++ b/tests/uishell/uishell.cpp @@ -36,8 +36,8 @@ #include "ui/uihost.h" #include "ui/uiinput.h" #include "ui/uiscreen.h" -#include "ui/uiunicode.h" #include "ui/uiview.h" +#include "windowevent.hh" #include #include @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include FT_FREETYPE_H @@ -290,8 +291,6 @@ class TestHostClass : public UIShellHostClass public: UIFrameRect Rect = { 0, 0, 1280, 800, 1.0f, 1.0f }; bool Captured = false; - bool Unicode = false; - unsigned int CodePage = 65001; bool Down[256] = {}; bool Toggled[256] = {}; int Presents = 0; @@ -350,16 +349,6 @@ class TestHostClass : public UIShellHostClass return(true); } - virtual bool Window_Is_Unicode(void) const override - { - return(Unicode); - } - - virtual unsigned int Text_Code_Page(void) const override - { - return(CodePage); - } - int Applied = 0; int Restored = 0; UICursor LastCursor = UI_CURSOR_ARROW; @@ -376,9 +365,16 @@ class TestHostClass : public UIShellHostClass LastCursor = UI_CURSOR_ARROW; } - virtual HWND Main_Window(void) const override + std::string Clipboard; + + virtual std::string Clipboard_Text(void) const override + { + return(Clipboard); + } + + virtual void Set_Clipboard_Text(std::string const & text) override { - return(nullptr); + Clipboard = text; } virtual UIFrameRect Frame(void) const override @@ -442,11 +438,6 @@ class TestHostClass : public UIShellHostClass Captured = false; } - virtual bool Screen_To_Client(int &, int &) const override - { - return(true); - } - virtual char const * String(int) const override { return("string"); @@ -501,9 +492,105 @@ struct ShellFixtureType }; -bool Send(UIShellClass & shell, UINT message, WPARAM wparam = 0, LPARAM lparam = 0) +struct PointType +{ + int X; + int Y; +}; + + +bool Send(UIShellClass & shell, WindowEvent const & event) +{ + return(shell.Handle_Window_Event(event)); +} + + +bool Move(UIShellClass & shell, PointType at) +{ + WindowEvent event; + event.Type = WINDOW_EVENT_MOUSE_MOVE; + event.X = at.X; + event.Y = at.Y; + return(Send(shell, event)); +} + + +bool Press(UIShellClass & shell, WindowMouseButton button, PointType at) +{ + WindowEvent event; + event.Type = WINDOW_EVENT_MOUSE_DOWN; + event.Button = button; + event.X = at.X; + event.Y = at.Y; + return(Send(shell, event)); +} + + +bool Release(UIShellClass & shell, WindowMouseButton button, PointType at) +{ + WindowEvent event; + event.Type = WINDOW_EVENT_MOUSE_UP; + event.Button = button; + event.X = at.X; + event.Y = at.Y; + return(Send(shell, event)); +} + + +bool Wheel(UIShellClass & shell, PointType at, float notches, bool horizontal) +{ + WindowEvent event; + event.Type = WINDOW_EVENT_MOUSE_WHEEL; + event.X = at.X; + event.Y = at.Y; + event.Wheel = notches; + event.Horizontal = horizontal; + return(Send(shell, event)); +} + + +bool Key_Down(UIShellClass & shell, int virtualkey, int modifiers = 0) +{ + WindowEvent event; + event.Type = WINDOW_EVENT_KEY_DOWN; + event.VirtualKey = virtualkey; + event.Modifiers = modifiers; + return(Send(shell, event)); +} + + +bool Key_Up(UIShellClass & shell, int virtualkey, int modifiers = 0) +{ + WindowEvent event; + event.Type = WINDOW_EVENT_KEY_UP; + event.VirtualKey = virtualkey; + event.Modifiers = modifiers; + return(Send(shell, event)); +} + + +bool Text(UIShellClass & shell, char32_t code) +{ + WindowEvent event; + event.Type = WINDOW_EVENT_TEXT; + event.Text = code; + return(Send(shell, event)); +} + + +bool Focus(UIShellClass & shell, bool focused) +{ + WindowEvent event; + event.Type = focused ? WINDOW_EVENT_FOCUS_GAINED : WINDOW_EVENT_FOCUS_LOST; + return(Send(shell, event)); +} + + +bool Capture_Lost(UIShellClass & shell) { - return(shell.Handle_Window_Message(nullptr, message, wparam, lparam)); + WindowEvent event; + event.Type = WINDOW_EVENT_CAPTURE_LOST; + return(Send(shell, event)); } @@ -513,22 +600,22 @@ Rml::Vector2f Center_Of(Rml::Element * element) } -LPARAM Element_Point(TestHostClass const & host, Rml::Element * element) +PointType Element_Point(TestHostClass const & host, Rml::Element * element) { Rml::Vector2f center = Center_Of(element); - return(MAKELPARAM((int)center.x + host.Rect.X, (int)center.y + host.Rect.Y)); + return(PointType{ (int)center.x + host.Rect.X, (int)center.y + host.Rect.Y }); } void Click_Through_Hook(UIShellClass & shell, TestHostClass & host, Rml::Element * element) { - LPARAM at = Element_Point(host, element); + PointType at = Element_Point(host, element); - Send(shell, WM_MOUSEMOVE, 0, at); + Move(shell, at); host.Down[VK_LBUTTON] = true; - Send(shell, WM_LBUTTONDOWN, MK_LBUTTON, at); + Press(shell, WINDOW_BUTTON_LEFT, at); host.Down[VK_LBUTTON] = false; - Send(shell, WM_LBUTTONUP, 0, at); + Release(shell, WINDOW_BUTTON_LEFT, at); } @@ -550,11 +637,11 @@ bool Slider_Parts(Rml::Element * slider, Rml::Element * & bar, Rml::Element * & } -LPARAM Past_Track_End(TestHostClass const & host, Rml::Element * bar, Rml::Element * track) +PointType Past_Track_End(TestHostClass const & host, Rml::Element * bar, Rml::Element * track) { int const y = (int)Center_Of(bar).y + host.Rect.Y; int const past = (int)(track->GetAbsoluteOffset(Rml::BoxArea::Border).x + track->GetBox().GetSize(Rml::BoxArea::Border).x) + host.Rect.X + 40; - return(MAKELPARAM(past, y)); + return(PointType{ past, y }); } @@ -566,15 +653,15 @@ bool Drag_Slider_To_End(UIShellClass & shell, TestHostClass & host, Rml::Element return(false); } - LPARAM const start = Element_Point(host, bar); - LPARAM const end = Past_Track_End(host, bar, track); + PointType const start = Element_Point(host, bar); + PointType const end = Past_Track_End(host, bar, track); - bool consumed = Send(shell, WM_MOUSEMOVE, 0, start); + bool consumed = Move(shell, start); host.Down[VK_LBUTTON] = true; - consumed = Send(shell, WM_LBUTTONDOWN, MK_LBUTTON, start) && consumed; - consumed = Send(shell, WM_MOUSEMOVE, MK_LBUTTON, end) && consumed; + consumed = Press(shell, WINDOW_BUTTON_LEFT, start) && consumed; + consumed = Move(shell, end) && consumed; host.Down[VK_LBUTTON] = false; - consumed = Send(shell, WM_LBUTTONUP, 0, end) && consumed; + consumed = Release(shell, WINDOW_BUTTON_LEFT, end) && consumed; return(consumed); } @@ -4039,7 +4126,7 @@ void Test_Shell(void) host.OnClear = [&](void) { if (host.Clears == 1) { - consumedWhileOpening = Send(shell, WM_LBUTTONDOWN, 0, MAKELPARAM(10, 10)); + consumedWhileOpening = Press(shell, WINDOW_BUTTON_LEFT, PointType{ 10, 10 }); } }; @@ -4049,7 +4136,7 @@ void Test_Shell(void) shownInside = shell.Screen_Shown() && shell.Modal() == view.get() && shell.Modal_Depth() == 1; } if (passes == 3) { - Send(shell, WM_KEYDOWN, VK_RETURN); + Key_Down(shell, VK_RETURN); } return(false); }); @@ -4069,7 +4156,7 @@ void Test_Shell(void) UIVersionPresenterClass presenter({ "escape" }); std::unique_ptr view = UI_Version_View(presenter); UIResult result = shell.Run_Modal(*view, [&](void) { - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); return(false); }); Check(result == UI_RESULT_CANCELLED, "Escape cancels the modal"); @@ -4103,7 +4190,7 @@ void Test_Shell(void) return(true); }); Check(inner.has_value() && *inner == UI_RESULT_SESSION_ENDED && result == UI_RESULT_SESSION_ENDED && outerPasses == 2, "a game ending under a nested screen closes each runner on its next pass"); - Check(!shell.Screen_Shown() && !Send(shell, WM_KEYDOWN, VK_SPACE), "and leaves nothing shown to take the score screen's key"); + Check(!shell.Screen_Shown() && !Key_Down(shell, VK_SPACE), "and leaves nothing shown to take the score screen's key"); } { @@ -4128,7 +4215,7 @@ void Test_Shell(void) passes++; if (passes == 2) { appliedBefore = shell.Rml_Context()->GetDimensions() == Rml::Vector2i(640, 400); - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); } return(false); }); @@ -4163,7 +4250,7 @@ void Test_Shell(void) } if (passes == 2) { refetched = fixture.Render->Loaded + fixture.Render->Generated > fetched; - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); } return(false); }); @@ -4180,7 +4267,7 @@ void Test_Shell(void) shell.Run_Modal(*afterview, [&](void) { Rml::ElementDocument * document = Rml(*afterview).Document(); built = document != nullptr && document->GetElementById("chrome") != nullptr; - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); return(false); }); @@ -4209,7 +4296,7 @@ void Test_Shell(void) passes++; if (passes == 2) { appliedBefore = fixture.Render->ReleasedTextures > released; - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); } return(false); }); @@ -4233,13 +4320,13 @@ void Test_Shell(void) passes++; if (passes == 1) { suppressed = shell.Input_State().Mouse_Owner(0) == UI_INPUT_SUPPRESSED && shell.Input_State().Key_Owner('A') == UI_INPUT_SUPPRESSED; - swallowed = Send(shell, WM_LBUTTONUP, 0, MAKELPARAM(10, 10)) && Send(shell, WM_KEYUP, 'A'); + swallowed = Release(shell, WINDOW_BUTTON_LEFT, PointType{ 10, 10 }) && Key_Up(shell, 'A'); host.Down[VK_LBUTTON] = false; host.Down['A'] = false; quiet = !presenter.Has_Pending() && shell.Input_State().Mouse_Owner(0) == UI_INPUT_NONE && shell.Input_State().Key_Owner('A') == UI_INPUT_NONE; } if (passes == 2) { - Send(shell, WM_KEYDOWN, VK_RETURN); + Key_Down(shell, VK_RETURN); } return(false); }); @@ -4259,17 +4346,17 @@ void Test_Shell(void) shell.Run_Modal(*view, [&](void) { passes++; if (passes == 1) { - Send(shell, WM_LBUTTONDOWN, 0, MAKELPARAM(10, 10)); - Send(shell, WM_KEYDOWN, 'A'); + Press(shell, WINDOW_BUTTON_LEFT, PointType{ 10, 10 }); + Key_Down(shell, 'A'); owned = shell.Input_State().Mouse_Owner(0) == UI_INPUT_RML && shell.Input_State().Key_Owner('A') == UI_INPUT_RML && host.Captured; host.Captured = false; - Send(shell, WM_CAPTURECHANGED, 0, (LPARAM)1); + Capture_Lost(shell); cancelled = shell.Input_State().Mouse_Owner(0) == UI_INPUT_SUPPRESSED && shell.Input_State().Key_Owner('A') == UI_INPUT_RML && !host.Captured && shell.Modal() == view.get(); - swallowed = Send(shell, WM_LBUTTONUP, 0, MAKELPARAM(10, 10)) && !presenter.Has_Pending(); - Send(shell, WM_KEYUP, 'A'); + swallowed = Release(shell, WINDOW_BUTTON_LEFT, PointType{ 10, 10 }) && !presenter.Has_Pending(); + Key_Up(shell, 'A'); } if (passes == 2) { - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); } return(false); }); @@ -4298,25 +4385,25 @@ void Test_Shell(void) Rml::Element * bar = nullptr; Rml::Element * track = nullptr; if (passes == 2 && score != nullptr && Slider_Parts(score, bar, track)) { - LPARAM const start = Element_Point(host, bar); - Send(shell, WM_MOUSEMOVE, 0, start); + PointType const start = Element_Point(host, bar); + Move(shell, start); host.Down[VK_LBUTTON] = true; - Send(shell, WM_LBUTTONDOWN, MK_LBUTTON, start); + Press(shell, WINDOW_BUTTON_LEFT, start); owned = shell.Input_State().Mouse_Owner(0) == UI_INPUT_RML && host.Captured; host.Down[VK_LBUTTON] = false; - Send(shell, WM_ACTIVATEAPP, 0, 0); + Focus(shell, false); dropped = shell.Input_State().Mouse_Owner(0) == UI_INPUT_SUPPRESSED && !host.Captured; - Send(shell, WM_ACTIVATEAPP, 1, 0); - Send(shell, WM_MOUSEMOVE, 0, Past_Track_End(host, bar, track)); + Focus(shell, true); + Move(shell, Past_Track_End(host, bar, track)); } if (passes == 3 && score != nullptr) { still = presenter.State.Score == 5 && score->GetAttribute("value", -1) == 5 && !presenter.Has_Pending(); - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); } return(false); }); - Send(shell, WM_KEYUP, VK_ESCAPE); + Key_Up(shell, VK_ESCAPE); shell.Tick(); Check(owned, "a press on a slider's bar is the document's and takes the capture"); @@ -4343,17 +4430,17 @@ void Test_Shell(void) if (innerpasses == 1) { nested = shell.Modal() == innerview.get() && shell.Modal_Depth() == 2; host.Down[VK_LBUTTON] = true; - Send(shell, WM_LBUTTONDOWN, 0, MAKELPARAM(10, 10)); - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Press(shell, WINDOW_BUTTON_LEFT, PointType{ 10, 10 }); + Key_Down(shell, VK_ESCAPE); } return(innerpasses >= 5); }); restored = innerresult == UI_RESULT_CANCELLED && shell.Modal() == outerview.get() && shell.Modal_Depth() == 1 && shell.Input_State().Mouse_Owner(0) == UI_INPUT_SUPPRESSED; - swallowed = Send(shell, WM_LBUTTONUP, 0, MAKELPARAM(10, 10)); + swallowed = Release(shell, WINDOW_BUTTON_LEFT, PointType{ 10, 10 }); host.Down[VK_LBUTTON] = false; } if (passes == 2) { - Send(shell, WM_KEYDOWN, VK_RETURN); + Key_Down(shell, VK_RETURN); } return(false); }); @@ -4377,7 +4464,7 @@ void Test_Shell(void) innerpasses++; if (innerpasses == 1) { innerseen = shell.Running_Service() == &innerservice; - Send(shell, WM_KEYDOWN, VK_RETURN); + Key_Down(shell, VK_RETURN); } return(innerpasses >= 4); }; @@ -4389,14 +4476,14 @@ void Test_Shell(void) outerback = shell.Running_Service() == &outerservice; } if (passes == 2) { - Send(shell, WM_KEYDOWN, VK_RETURN); + Key_Down(shell, VK_RETURN); } return(false); }; Check(shell.Running_Service() == nullptr, "no service runs while no screen is shown"); shell.Run_Modal(*outerview, outerservice); - Send(shell, WM_KEYUP, VK_RETURN); + Key_Up(shell, VK_RETURN); shell.Tick(); Check(outerseen && innerseen, "each running screen reports the service driving it"); Check(outerback && shell.Running_Service() == nullptr, "closing a screen hands the service back to the one below, and the last to nobody"); @@ -4422,7 +4509,7 @@ void Test_Shell(void) innerpasses++; hidden = hidden && !outerview->Is_Shown(); if (innerpasses == 1) { - Send(shell, WM_KEYDOWN, VK_RETURN); + Key_Down(shell, VK_RETURN); } return(innerpasses >= 4); }, true); @@ -4430,7 +4517,7 @@ void Test_Shell(void) stillopen = !Rml(*outerview).Document()->IsClassSet("revealing") && !shell.Revealing_Shown(); } if (passes == 2) { - Send(shell, WM_KEYDOWN, VK_RETURN); + Key_Down(shell, VK_RETURN); } return(false); }); @@ -4446,30 +4533,30 @@ void Test_Shell(void) bool focused = false; shell.Run_Modal(*view, [&](void) { - taken = Send(shell, WM_MOUSEMOVE, 0, MAKELPARAM(2000, 2000)) - && Send(shell, WM_XBUTTONDOWN, MAKEWPARAM(0, XBUTTON1), MAKELPARAM(10, 10)) - && Send(shell, WM_XBUTTONUP, MAKEWPARAM(0, XBUTTON1), MAKELPARAM(10, 10)) - && Send(shell, WM_MOUSEHWHEEL, MAKEWPARAM(0, WHEEL_DELTA), MAKELPARAM(10, 10)); + taken = Move(shell, PointType{ 2000, 2000 }) + && Press(shell, WINDOW_BUTTON_X1, PointType{ 10, 10 }) + && Release(shell, WINDOW_BUTTON_X1, PointType{ 10, 10 }) + && Wheel(shell, PointType{ 10, 10 }, 1.0f, true); host.Down['C'] = true; - Send(shell, WM_ACTIVATEAPP, 1); + Focus(shell, true); focused = shell.Input_State().Key_Owner('C') == UI_INPUT_SUPPRESSED; host.Down['C'] = false; - Send(shell, WM_KEYUP, 'C'); - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Up(shell, 'C'); + Key_Down(shell, VK_ESCAPE); return(false); }); Check(taken, "a modal takes moves, side buttons and the horizontal wheel"); Check(focused, "focus returning to a shown screen quarantines what is held"); - Check(Send(shell, WM_KEYUP, VK_ESCAPE), "the release of the key that closed a screen is swallowed"); + Check(Key_Up(shell, VK_ESCAPE), "the release of the key that closed a screen is swallowed"); shell.Tick(); Check(!shell.Input_State().Any_Owned(), "nothing stays owned once the closing key is up and the shell has ticked"); host.Down['C'] = true; - Send(shell, WM_ACTIVATEAPP, 1); + Focus(shell, true); Check(shell.Input_State().Key_Owner('C') == UI_INPUT_NONE, "focus returning to an idle shell quarantines nothing"); host.Down['C'] = false; - Check(!Send(shell, WM_KEYUP, 'Z'), "a stray release meets an idle shell and reaches the game"); + Check(!Key_Up(shell, 'Z'), "a stray release meets an idle shell and reaches the game"); } { @@ -4490,47 +4577,62 @@ void Test_Shell(void) shell.Run_Modal(*view, [&](void) { Rml(*view).Document()->AddEventListener(Rml::EventId::Textinput, &recorder); - Send(shell, WM_CHAR, 0xC3); - Send(shell, WM_CHAR, 0xA9); + Text(shell, 0xE9); Rml(*view).Document()->RemoveEventListener(Rml::EventId::Textinput, &recorder); - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); return(false); }); - Check(recorder.Texts.size() == 1 && recorder.Texts[0] == "\xC3\xA9", "two UTF-8 bytes on a narrow window reach the document as one character"); + Check(recorder.Texts.size() == 1 && recorder.Texts[0] == "\xC3\xA9", "a typed character reaches the document as its UTF-8 text"); } { - class TextRecorderClass : public Rml::EventListener - { - public: - std::vector Texts; - - virtual void ProcessEvent(Rml::Event & event) override - { - Texts.push_back(event.GetParameter("text", "")); - } - }; - - TextRecorderClass recorder; - unsigned int page = host.CodePage; - host.CodePage = 1251; - - UIVersionPresenterClass presenter({ "cyrillic" }); + UIVersionPresenterClass presenter({ "text" }); std::unique_ptr view = UI_Version_View(presenter); + Rml::String written; + Rml::String repeated; + Rml::String kept; + bool focused = false; shell.Run_Modal(*view, [&](void) { - Rml(*view).Document()->AddEventListener(Rml::EventId::Textinput, &recorder); - Send(shell, WM_CHAR, 0xCF); - Send(shell, WM_CHAR, 0xF0); - Send(shell, WM_CHAR, 0xE8); - Rml(*view).Document()->RemoveEventListener(Rml::EventId::Textinput, &recorder); - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Rml::ElementDocument * document = Rml(*view).Document(); + Rml::ElementFormControlTextArea * area = rmlui_dynamic_cast(document->AppendChild(document->CreateElement("textarea"))); + Rml::ElementFormControlInput * field = rmlui_dynamic_cast(document->AppendChild(document->CreateElement("input"))); + if (area != nullptr && field != nullptr) { + area->Focus(); + Key_Down(shell, 'A'); + Text(shell, 'A'); + Key_Up(shell, 'A'); + Key_Down(shell, VK_RETURN); + Key_Up(shell, VK_RETURN); + Key_Down(shell, 'B'); + Text(shell, 'B'); + Key_Up(shell, 'B'); + written = area->GetValue(); + + WindowEvent repeat; + repeat.Type = WINDOW_EVENT_KEY_DOWN; + repeat.VirtualKey = VK_RETURN; + Key_Down(shell, VK_RETURN); + repeat.Repeat = true; + Send(shell, repeat); + Key_Up(shell, VK_RETURN); + repeated = area->GetValue(); + + field->SetAttribute("type", "text"); + field->SetValue("Name"); + field->Focus(); + focused = document->GetContext()->GetFocusElement() == field; + field->Select(); + Key_Down(shell, VK_RETURN); + kept = field->GetValue(); + Key_Up(shell, VK_RETURN); + } + Key_Down(shell, VK_ESCAPE); return(false); }); - host.CodePage = page; - - Check(recorder.Texts.size() == 3, "a code page byte each reaches the document as its own character"); - Check(recorder.Texts.size() == 3 && recorder.Texts[0] == "\xD0\x9F" && recorder.Texts[1] == "\xD1\x80" && recorder.Texts[2] == "\xD0\xB8", "and each is the Cyrillic letter its byte names, not a replacement"); + Check(written == "A\nB", "Enter in a text area types a new line"); + Check(repeated == "A\nB\n\n", "and each repeat of Enter types another"); + Check(focused && kept == "Name", "Enter in a one-line field leaves its selected text alone"); } { @@ -4542,9 +4644,6 @@ void Test_Shell(void) slot.Description = "[EMPTY SLOT]"; state.Entries.push_back(slot); - bool wide = host.Unicode; - host.Unicode = true; - UISaveGamePresenterClass presenter(state); std::unique_ptr view = UI_Save_Game_View(presenter); Rml::String typed; @@ -4556,39 +4655,29 @@ void Test_Shell(void) if (field != nullptr) { field->SetValue(""); field->Focus(); - Send(shell, WM_CHAR, 0x041F); - Send(shell, WM_CHAR, 0x0440); - Send(shell, WM_CHAR, 0x0438); - Send(shell, WM_CHAR, 0x0432); - Send(shell, WM_CHAR, 0x0435); - Send(shell, WM_CHAR, 0x0442); + Text(shell, 0x041F); + Text(shell, 0x0440); + Text(shell, 0x0438); + Text(shell, 0x0432); + Text(shell, 0x0435); + Text(shell, 0x0442); typed = field->GetValue(); } - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); return(false); }); - host.Unicode = wide; - - Check(typed == "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82", "Cyrillic typed on a wide window reaches the field as the letters themselves"); + Check(typed == "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82", "Cyrillic typed into a field reaches it as the letters themselves"); } { char const * sample = "\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80"; - std::wstring wide; - std::string text; - - std::wstring expected = { (wchar_t)0x00E9, (wchar_t)0x20AC, (wchar_t)0xD83D, (wchar_t)0xDE00 }; - Check(UI_UTF8_To_UTF16(sample, wide) && wide == expected, "UTF-8 converts to UTF-16"); - Check(UI_UTF16_To_UTF8(wide, text) && text == sample, "UTF-16 converts back to the same UTF-8"); - Check(!UI_UTF8_To_UTF16("\xC0\xAF", wide), "an overlong sequence is refused, not repaired"); - Check(!UI_UTF16_To_UTF8(std::wstring(1, (wchar_t)0xD800), text), "an unpaired surrogate is refused, not repaired"); Rml::String before; fixture.System->GetClipboardText(before); fixture.System->SetClipboardText(sample); Rml::String after; fixture.System->GetClipboardText(after); - Check(after == sample, "clipboard text survives a round trip"); + Check(after == sample && host.Clipboard == sample, "clipboard text goes through the host and survives a round trip"); fixture.System->SetClipboardText(before); } @@ -4608,15 +4697,15 @@ void Test_Shell(void) fixture.System->SetMouseCursor("text"); requested = fixture.System->Cursor_Request() == UI_CURSOR_TEXT; shown = shell.Handle_Set_Cursor() && host.LastCursor == UI_CURSOR_TEXT; - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); return(false); }); Check(opened, "opening a screen puts the window's arrow on the pointer before any request"); - Check(arrow, "WM_SETCURSOR over a shown screen is the screen's even with no request"); + Check(arrow, "choosing the pointer over a shown screen is the screen's even with no request"); Check(requested, "a document's pointer request is kept"); - Check(shown, "WM_SETCURSOR shows the requested shape while a screen is shown"); + Check(shown, "choosing the pointer shows the requested shape while a screen is shown"); Check(host.Restored - restored == 1 && fixture.System->Cursor_Request() == UI_CURSOR_ARROW && host.LastCursor == UI_CURSOR_ARROW, "closing a screen puts the game's pointer back once and forgets the request"); - Check(!shell.Handle_Set_Cursor(), "WM_SETCURSOR is the game's again once nothing is shown"); + Check(!shell.Handle_Set_Cursor(), "choosing the pointer is the game's again once nothing is shown"); } { @@ -4647,11 +4736,11 @@ void Test_Shell(void) } if (passes == 4 && score != nullptr) { kept = presenter.State.Score == dragged && score->GetAttribute("value", -1) == dragged; - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); } return(false); }); - Send(shell, WM_KEYUP, VK_ESCAPE); + Key_Up(shell, VK_ESCAPE); shell.Tick(); Check(consumed && dragged == UISoundPresenterClass::LEVELS, "a drag to the end of the music slider reaches the document and lands on the top level"); @@ -4674,21 +4763,21 @@ void Test_Shell(void) if (buttons.empty()) { return(true); } - LPARAM at = Element_Point(host, buttons[0]); + PointType at = Element_Point(host, buttons[0]); if (passes == 2) { host.Down[VK_LBUTTON] = true; - Send(shell, WM_ACTIVATEAPP, 1, 0); - consumed = Send(shell, WM_LBUTTONDOWN, MK_LBUTTON, at); + Focus(shell, true); + consumed = Press(shell, WINDOW_BUTTON_LEFT, at); unpressed = !buttons[0]->IsPseudoClassSet("active"); host.Down[VK_LBUTTON] = false; - Send(shell, WM_LBUTTONUP, 0, at); + Release(shell, WINDOW_BUTTON_LEFT, at); } if (passes == 3) { answered = !presenter.Result.has_value(); - Send(shell, WM_MOUSEMOVE, 0, at); - Send(shell, WM_LBUTTONDOWN, MK_LBUTTON, at); - Send(shell, WM_LBUTTONUP, 0, at); + Move(shell, at); + Press(shell, WINDOW_BUTTON_LEFT, at); + Release(shell, WINDOW_BUTTON_LEFT, at); } return(false); }); @@ -4714,25 +4803,25 @@ void Test_Shell(void) if (buttons.empty()) { return(true); } - LPARAM at = Element_Point(host, buttons[0]); + PointType at = Element_Point(host, buttons[0]); if (passes == 2) { - Send(shell, WM_MOUSEMOVE, 0, at); + Move(shell, at); host.Down[VK_LBUTTON] = true; - Send(shell, WM_LBUTTONDOWN, MK_LBUTTON, at); + Press(shell, WINDOW_BUTTON_LEFT, at); held = shell.Input_State().Mouse_Owner(0) == UI_INPUT_RML; host.Down[VK_LBUTTON] = false; if (loss == 0) { - Send(shell, WM_ACTIVATEAPP, 0, 0); - Send(shell, WM_ACTIVATEAPP, 1, 0); + Focus(shell, false); + Focus(shell, true); } else { - Send(shell, WM_CAPTURECHANGED, 0, (LPARAM)1); + Capture_Lost(shell); } } if (passes == 3) { quiet = !presenter.Result.has_value() && !presenter.Has_Pending(); - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); } return(false); }); @@ -4887,7 +4976,7 @@ void Test_Shell(void) shell.Run_Modal(*innerview, [&](void) { innerpasses++; if (innerpasses == 1) { - Send(shell, WM_KEYDOWN, VK_RETURN); + Key_Down(shell, VK_RETURN); } return(false); }, hide != 0); @@ -4895,7 +4984,7 @@ void Test_Shell(void) } if (passes == 3) { stayed = uncovered(); - Send(shell, WM_KEYDOWN, VK_ESCAPE); + Key_Down(shell, VK_ESCAPE); } return(false); }); @@ -5008,16 +5097,15 @@ void Test_Shell(void) } if (passes == 3) { focused = shell.Rml_Context()->GetFocusElement() == capture; - Send(shell, WM_KEYDOWN, 'X'); - Send(shell, WM_KEYUP, 'X'); + Key_Down(shell, 'X'); + Key_Up(shell, 'X'); } if (passes == 4) { captured = presenter.State.Captured == 88 && presenter.State.AssignedTo == "Scatter" && capture->GetInnerRML().find("K88") != std::string::npos; - host.Down[VK_SHIFT] = true; - Send(shell, WM_KEYDOWN, 'R'); - Send(shell, WM_KEYUP, 'R'); - host.Down[VK_SHIFT] = false; + // The host reports no keys held, so Shift can only come from the events. + Key_Down(shell, 'R', WINDOW_MOD_SHIFT); + Key_Up(shell, 'R', WINDOW_MOD_SHIFT); } if (passes == 5) { modified = presenter.State.Captured == 338 && presenter.State.AssignedTo == "Toggle Repair"; @@ -5033,7 +5121,7 @@ void Test_Shell(void) Check(listed && presenter.State.Selected == 3, "a click on a command row selects the command it names"); Check(focused, "and moves the focus to the capture element the keys go to"); Check(captured, "a key sent through the hook becomes its hotkey number there and names the command holding it"); - Check(modified, "the same key with a modifier the host reports is a different number naming a different command"); + Check(modified, "the same key with a modifier on its event is a different number naming a different command"); Check(moved, "assigning it moves the key to the selected command and off its old owner"); Check(service.Calls == std::vector{ "save 0=577 2=88 3=338" }, "and accepting saves the table the edits left behind"); } @@ -5107,7 +5195,7 @@ void Test_Shell(void) host.Held = true; host.Now = 10000; - Send(shell, WM_MOUSEMOVE, 0, Element_Point(host, cells[0])); + Move(shell, Element_Point(host, cells[0])); shell.Tick(); Check(tip->GetInnerRML() == "GDI", "the tip reads the country the cell names"); Check(!tip->IsClassSet("up"), "the tip waits while the pointer has only just arrived"); @@ -5128,25 +5216,25 @@ void Test_Shell(void) && at.y + size.y <= (float)host.Rect.Height, "the tip stays on the screen"); host.Down[VK_LBUTTON] = true; - Send(shell, WM_LBUTTONDOWN, MK_LBUTTON, Element_Point(host, cells[0])); + Press(shell, WINDOW_BUTTON_LEFT, Element_Point(host, cells[0])); host.Down[VK_LBUTTON] = false; - Send(shell, WM_LBUTTONUP, 0, Element_Point(host, cells[0])); + Release(shell, WINDOW_BUTTON_LEFT, Element_Point(host, cells[0])); shell.Tick(); Check(!tip->IsClassSet("up"), "a press takes the tip down"); host.Now += 100; - Send(shell, WM_MOUSEMOVE, 0, MAKELPARAM(host.Rect.Width / 2, host.Rect.Height / 2)); - Send(shell, WM_MOUSEMOVE, 0, Element_Point(host, cells[0])); + Move(shell, PointType{ host.Rect.Width / 2, host.Rect.Height / 2 }); + Move(shell, Element_Point(host, cells[0])); shell.Tick(); host.Now += 301; shell.Tick(); Check(tip->IsClassSet("up"), "a tip that follows one just taken down comes sooner"); - Send(shell, WM_MOUSEMOVE, 0, MAKELPARAM(host.Rect.Width - 2, host.Rect.Height - 2)); + Move(shell, PointType{ host.Rect.Width - 2, host.Rect.Height - 2 }); shell.Tick(); Check(!tip->IsClassSet("up"), "the tip goes once the pointer leaves the control"); - Send(shell, WM_MOUSEMOVE, 0, Element_Point(host, cells[0])); + Move(shell, Element_Point(host, cells[0])); shell.Tick(); host.Now += 1001; shell.Tick(); diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 071296b9f..6989c9c3c 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -181,6 +181,37 @@ target_include_directories(imgui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/imgui") # same set as the implementation. target_compile_definitions(imgui PUBLIC IMGUI_DISABLE_OBSOLETE_FUNCTIONS) +# +# --------------------------------------------------------- +# SDL (window, events, and input) +# --------------------------------------------------------- +# +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/SDL/CMakeLists.txt") + message(FATAL_ERROR + "thirdparty/SDL is empty. Fetch the submodules with:\n" + " git submodule update --init --recursive") +endif() + +set(SDL_SHARED OFF CACHE BOOL "" FORCE) +set(SDL_STATIC ON CACHE BOOL "" FORCE) +set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE) +set(SDL_TESTS OFF CACHE BOOL "" FORCE) +set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE) +set(SDL_INSTALL OFF CACHE BOOL "" FORCE) + +# Only the window, its events, and the keyboard and mouse are used. miniaudio plays the +# sound and bgfx creates its own rendering contexts on the window. +set(SDL_VIDEO ON CACHE BOOL "" FORCE) +foreach(subsystem AUDIO GPU RENDER CAMERA JOYSTICK HAPTIC HIDAPI POWER SENSOR DIALOG TRAY) + set(SDL_${subsystem} OFF CACHE BOOL "" FORCE) +endforeach() +set(SDL_OPENGL OFF CACHE BOOL "" FORCE) +set(SDL_OPENGLES OFF CACHE BOOL "" FORCE) +set(SDL_VULKAN OFF CACHE BOOL "" FORCE) +set(SDL_OFFSCREEN OFF CACHE BOOL "" FORCE) + +add_subdirectory(SDL) + # # --------------------------------------------------------- diff --git a/thirdparty/SDL b/thirdparty/SDL new file mode 160000 index 000000000..fa2c02bb6 --- /dev/null +++ b/thirdparty/SDL @@ -0,0 +1 @@ +Subproject commit fa2c02bb6e21974a89ea9824bc53c9932abe5f9c diff --git a/thirdparty/licenses/sdl-fdlibm.txt b/thirdparty/licenses/sdl-fdlibm.txt new file mode 100644 index 000000000..fb80e0766 --- /dev/null +++ b/thirdparty/licenses/sdl-fdlibm.txt @@ -0,0 +1,10 @@ +fdlibm (thirdparty/SDL/src/libm) + +==================================================== +Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + +Developed at SunPro, a Sun Microsystems, Inc. business. +Permission to use, copy, modify, and distribute this +software is freely granted, provided that this notice +is preserved. +====================================================