diff --git a/code/gamewindow.cpp b/code/gamewindow.cpp
new file mode 100644
index 00000000..fb1fdbd4
--- /dev/null
+++ b/code/gamewindow.cpp
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * 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 "gamewindow.h"
+
+#include "_map.h"
+#include "_surface.h"
+#include "_xmouse.h"
+#include "globals.h"
+#include "gscreen.h"
+#include "init.h"
+#include "movies.h"
+#include "video.h"
+
+
+static bool _HandlingMouseWheel = false;
+
+
+///
+/// Updates and presents the frame when the application window needs repainting.
+///
+void Game_Window_On_Paint(bool update_surface)
+{
+ if (update_surface) {
+ if (MouseCursor != NULL && VisibleSurface != NULL && HiddenSurface != NULL && CompositeSurface != NULL) {
+ if (ScenarioActive == true) {
+ Map.Blit_Sidebar(true);
+ Update_Visible_Surface(CompositeSurface);
+ } else if (Movie_Is_Playing() == true) {
+ Movie_Update_Visible_Surface();
+ } else {
+ Update_Visible_Surface(HiddenSurface);
+ }
+ }
+ }
+ Video_Present_If_Dirty();
+}
+
+
+///
+/// Stops tactical scrolling from coasting after the right mouse button is released.
+///
+void Game_Window_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)
+{
+ if (_HandlingMouseWheel) {
+ return;
+ }
+
+ _HandlingMouseWheel = true;
+ Execute_Command(delta < 0 ? "SidebarDown" : "SidebarUp");
+ _HandlingMouseWheel = false;
+}
diff --git a/code/gamewindow.h b/code/gamewindow.h
new file mode 100644
index 00000000..b611bb86
--- /dev/null
+++ b/code/gamewindow.h
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * 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
+
+
+void Game_Window_On_Paint(bool update_surface);
+void Game_Window_On_Right_Mouse_Up(void);
+void Game_Window_On_Mouse_Wheel(int delta);
diff --git a/code/video.cpp b/code/video.cpp
index cdc999f3..f262d5b5 100644
--- a/code/video.cpp
+++ b/code/video.cpp
@@ -235,10 +235,9 @@ void Video_On_Resize(int drawablewidth, int drawableheight)
///
-/// Tells the presenter the desktop's display settings changed.
-/// The window may now be on a monitor that refreshes at a different rate.
+/// Sets the refresh rate used to pace presentation.
///
-void Video_On_Display_Change(int refreshrate)
+void Video_Set_Refresh_Rate(int refreshrate)
{
if (!_Initialized) {
return;
diff --git a/code/video.h b/code/video.h
index 08f80390..d3ec1b09 100644
--- a/code/video.h
+++ b/code/video.h
@@ -43,7 +43,7 @@ void Video_Shutdown(void);
bool Video_Set_Mode(int width, int height);
void Video_On_Resize(int drawablewidth, int drawableheight);
-void Video_On_Display_Change(int refreshrate);
+void Video_Set_Refresh_Rate(int refreshrate);
void Video_Mark_Dirty(void);
void Video_Present(void);
diff --git a/code/winstub.cpp b/code/winstub.cpp
index a0970f5b..ed8c483d 100644
--- a/code/winstub.cpp
+++ b/code/winstub.cpp
@@ -47,7 +47,6 @@
#include "_keyboar.h"
#include "_map.h"
#include "_rect.h"
-#include "_surface.h"
#include "_tooltip.h"
#include "ccfile.h"
#include "cctooltip.h"
@@ -57,12 +56,11 @@
#include "dsaudio.h"
#include "dsurface.h"
#include "except.h"
+#include "gamewindow.h"
#include "globals.h"
#include "goptions.h"
-#include "init.h"
#include "misc.h"
#include "movie.h"
-#include "movies.h"
#include "msgroute.h"
#include "nativewindow.hh"
#include "pcx.h"
@@ -88,7 +86,6 @@ HWND UnusedWindow;
HINSTANCE ProgramInstance;
bool _MouseCaptured;
-bool _MouseWheel;
//void output(short,short)
@@ -247,19 +244,7 @@ LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, UINT wPa
return(0);
case WM_PAINT:
- if (GameInFocus == true || WindowedMode == true) {
- if (MouseCursor != NULL && VisibleSurface != NULL && HiddenSurface != NULL && CompositeSurface != NULL) {
- if (ScenarioActive == true) {
- Map.Blit_Sidebar(true);
- Update_Visible_Surface(CompositeSurface);
- } else if (Movie_Is_Playing() == true) {
- Movie_Update_Visible_Surface();
- } else {
- Update_Visible_Surface(HiddenSurface);
- }
- }
- }
- Video_Present_If_Dirty();
+ Game_Window_On_Paint(GameInFocus == true || WindowedMode == true);
ValidateRect(hwnd, NULL);
break;
@@ -275,7 +260,7 @@ LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, UINT wPa
case WM_SIZE:
if (wParam != SIZE_MINIMIZED) {
Video_On_Resize(LOWORD(lParam), HIWORD(lParam));
- Video_On_Display_Change(Win_Window_Refresh_Rate(hwnd));
+ Video_Set_Refresh_Rate(Win_Window_Refresh_Rate(hwnd));
if (MouseCursor != NULL) {
((WWMouseClass *)MouseCursor)->Calc_Confining_Rect();
}
@@ -283,7 +268,7 @@ LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, UINT wPa
break;
case WM_DISPLAYCHANGE:
- Video_On_Display_Change(Win_Window_Refresh_Rate(hwnd));
+ Video_Set_Refresh_Rate(Win_Window_Refresh_Rate(hwnd));
break;
case WM_CLOSE:
@@ -343,22 +328,14 @@ LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, UINT wPa
return(0);
case WM_RBUTTONUP:
- Map.Set_Scroll_Coasting_Allowed(false);
+ Game_Window_On_Right_Mouse_Up();
break;
case WM_MOVING:
return(On_WM_MOVING(hwnd, wParam, lParam));
case WM_MOUSEWHEEL:
- if (!_MouseWheel) {
- _MouseWheel = true;
- if (GET_WHEEL_DELTA_WPARAM(wParam) < 0) {
- Execute_Command("SidebarDown");
- } else {
- Execute_Command("SidebarUp");
- }
- _MouseWheel = false;
- }
+ Game_Window_On_Mouse_Wheel(GET_WHEEL_DELTA_WPARAM(wParam));
break;
case WM_SYSCOMMAND:
diff --git a/manual/changes/game-window-event-boundary.md b/manual/changes/game-window-event-boundary.md
new file mode 100644
index 00000000..6cdae4e0
--- /dev/null
+++ b/manual/changes/game-window-event-boundary.md
@@ -0,0 +1,9 @@
+---
+title: Separate game window responses from native events
+category: internal
+release: 0.2.0
+targets: []
+credit: [Krisztiaan]
+---
+
+The Win32 window procedure now translates paint, right-button release, and mouse-wheel messages into native-independent game callbacks. Game responses live with the callbacks, while Win32-specific validation and message decoding remain in the application shell. The video presenter accepts refresh-rate updates directly instead of handling a native display-change event. Controls and rendering configuration are unchanged.