mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
399 lines
18 KiB
C
399 lines
18 KiB
C
// The Win32 back end of the window (gfx_wsys.h): the window and its message loop on a thread of their own, F11
|
|
// fullscreen, the settings TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, the monitor's refresh rate, focus, the Vulkan surface.
|
|
// The swapchain and the present are gfx_present.c's. The in-game menu's keyboard and mouse come from the window
|
|
// procedure (ui/menu_win.cpp), and so do the game's (input_win.c).
|
|
//
|
|
// The process is per-monitor DPI aware (totk.manifest): every size here is physical pixels, TOTK_WINDOW_SIZE included,
|
|
// and the swapchain matches the screen pixel for pixel. A window moved to a monitor at another display scale keeps its
|
|
// client size (WM_GETDPISCALEDSIZE); the menu takes its text size floor from the window's DPI (gfx_window_dpi).
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <windows.h>
|
|
#include <imm.h>
|
|
|
|
#include "cfg.h"
|
|
#include "flight.h"
|
|
#include "fs.h"
|
|
#include "gfx.h"
|
|
#include "gfx_wsys.h"
|
|
#include "input.h"
|
|
#include "input_win.h"
|
|
#include "nvn_api.h"
|
|
#include "ui/menu.h"
|
|
#include "vk.h"
|
|
#include "platform/platform.h"
|
|
#include <vulkan/vulkan_win32.h>
|
|
|
|
// ---- window state
|
|
static HWND g_hwnd;
|
|
static PlatEvent g_wnd_ready; // manual-reset: the window thread made the window or gave up
|
|
static int g_wnd_failed;
|
|
static int g_background; // TOTK_BACKGROUND_WINDOW: the window stays behind the user's windows
|
|
static int g_fs_pending; // window thread: TOTK_FULLSCREEN waits for the first activation
|
|
static volatile int32_t g_dpi = USER_DEFAULT_SCREEN_DPI; // the window's DPI, set by the window thread
|
|
|
|
HWND gfx_window_handle(void) { return g_hwnd; }
|
|
|
|
int gfx_window_vk_extensions(const char **out, int max) {
|
|
if (max < 2) return 0;
|
|
out[0] = VK_KHR_SURFACE_EXTENSION_NAME;
|
|
out[1] = VK_KHR_WIN32_SURFACE_EXTENSION_NAME;
|
|
return 2;
|
|
}
|
|
|
|
int gfx_window_vk_presentable(VkInstance inst, VkPhysicalDevice pd, uint32_t family) {
|
|
(void)inst;
|
|
return vkGetPhysicalDeviceWin32PresentationSupportKHR(pd, family) != VK_FALSE;
|
|
}
|
|
|
|
// ---- window thread
|
|
#define WND_STYLE WS_OVERLAPPEDWINDOW
|
|
#define WM_APP_SETTINGS (WM_APP + 1)
|
|
#define WM_APP_FULLSCREEN (WM_APP + 2) // the fullscreen setting, held back until the window is first activated
|
|
|
|
static WINDOWPLACEMENT g_saved_place = {sizeof g_saved_place}; // the window behind fullscreen (window thread)
|
|
|
|
static int is_fullscreen(HWND h) { return !(GetWindowLongW(h, GWL_STYLE) & WS_OVERLAPPEDWINDOW); }
|
|
|
|
#define cfg_fullscreen gfx_cfg_fullscreen
|
|
#define cfg_window_size gfx_cfg_window_size
|
|
|
|
unsigned gfx_window_dpi(void) { return (unsigned)plat_cas32(&g_dpi, 0, 0); }
|
|
|
|
// The outer size of a framed window whose client area is cw x ch. The frame is measured at the window's DPI, or the
|
|
// system DPI before the window exists: plain AdjustWindowRect always takes the system DPI, which is wrong on a monitor
|
|
// at another display scale.
|
|
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
|
|
RECT r = {0, 0, cw, ch};
|
|
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, h ? GetDpiForWindow(h) : GetDpiForSystem());
|
|
*ow = r.right - r.left;
|
|
*oh = r.bottom - r.top;
|
|
}
|
|
|
|
// Windows turns the display off and sleeps after the idle timeout, and gamepad input does not count as activity. While
|
|
// the window is up and not minimized it holds the display and the system on, as other games do. The state belongs to
|
|
// the calling thread, so only the window thread (window_main and wnd_proc) calls this; the process exit releases it.
|
|
static void keep_awake(int on) {
|
|
static int cur = -1;
|
|
if (on == cur) return;
|
|
cur = on;
|
|
SetThreadExecutionState(on ? ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED : ES_CONTINUOUS);
|
|
}
|
|
|
|
// F11: borderless window over the whole monitor, and back to the saved window. The swapchain follows through WM_SIZE.
|
|
// Not Alt+Enter: Enter is the Plus button (input_win.c). A background window (test runs) keeps its place behind the
|
|
// user's windows and takes no focus.
|
|
static void toggle_fullscreen(HWND h) {
|
|
LONG style = GetWindowLongW(h, GWL_STYLE);
|
|
if (style & WS_OVERLAPPEDWINDOW) {
|
|
MONITORINFO mi = {sizeof mi};
|
|
if (!GetWindowPlacement(h, &g_saved_place) || !GetMonitorInfoW(MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY), &mi))
|
|
return;
|
|
SetWindowLongW(h, GWL_STYLE, (style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP);
|
|
SetWindowPos(h, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
|
|
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
|
SWP_NOOWNERZORDER | SWP_FRAMECHANGED | (g_background ? SWP_NOZORDER | SWP_NOACTIVATE : 0));
|
|
} else {
|
|
SetWindowLongW(h, GWL_STYLE, (style & ~WS_POPUP) | WND_STYLE);
|
|
if (g_background) g_saved_place.showCmd = SW_SHOWNOACTIVATE;
|
|
SetWindowPlacement(h, &g_saved_place);
|
|
SetWindowPos(h, NULL, 0, 0, 0, 0,
|
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED |
|
|
(g_background ? SWP_NOACTIVATE : 0));
|
|
}
|
|
}
|
|
|
|
// The window size and fullscreen state last taken from the settings (window thread). apply_settings acts on a change of
|
|
// either value only, so a window resized by hand keeps its size when another setting changes.
|
|
static char g_applied_size[32];
|
|
static int g_applied_fs;
|
|
|
|
static void note_applied(void) {
|
|
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("TOTK_WINDOW_SIZE"));
|
|
g_applied_fs = cfg_fullscreen();
|
|
}
|
|
|
|
// Window thread: the window takes the size and fullscreen state of the settings when they changed. In fullscreen the
|
|
// size goes into the placement restored when fullscreen ends.
|
|
static void apply_settings(HWND h) {
|
|
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = cfg_fullscreen(), fs_changed = fs != g_applied_fs;
|
|
note_applied();
|
|
if (size_changed) {
|
|
int cw, ch, ow, oh;
|
|
cfg_window_size(&cw, &ch);
|
|
outer_size(h, cw, ch, &ow, &oh);
|
|
if (is_fullscreen(h)) {
|
|
RECT *n = &g_saved_place.rcNormalPosition;
|
|
n->right = n->left + ow;
|
|
n->bottom = n->top + oh;
|
|
} else {
|
|
RECT have;
|
|
if (GetClientRect(h, &have) && (have.right != cw || have.bottom != ch)) {
|
|
if (IsZoomed(h)) ShowWindow(h, SW_RESTORE);
|
|
SetWindowPos(h, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
}
|
|
}
|
|
if (fs_changed && fs != is_fullscreen(h)) {
|
|
input_cursor_suspend(1);
|
|
toggle_fullscreen(h);
|
|
input_cursor_suspend(0);
|
|
}
|
|
}
|
|
|
|
void gfx_window_apply_settings(void) {
|
|
if (g_hwnd) PostMessageW(g_hwnd, WM_APP_SETTINGS, 0, 0);
|
|
}
|
|
|
|
|
|
void gfx_window_request_close(void) {
|
|
if (g_hwnd) PostMessageW(g_hwnd, WM_CLOSE, 0, 0);
|
|
}
|
|
|
|
// ---- refresh rate of the window's monitor (TOTK_FPS_LIMIT=display), measured when the window opens, lands on another
|
|
// monitor or the display mode changes (gfx_window_set_refresh keeps it). QueryDisplayConfig gives the exact rational
|
|
// rate (59.94, 143.998); EnumDisplaySettings' whole hertz is the fallback.
|
|
static HMONITOR g_rate_mon;
|
|
|
|
static double monitor_rate_hz(HMONITOR mon) {
|
|
MONITORINFOEXW mi = {.cbSize = sizeof mi};
|
|
if (!GetMonitorInfoW(mon, (MONITORINFO *)&mi)) return 0;
|
|
double hz = 0;
|
|
UINT32 np = 0, nm = 0;
|
|
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &np, &nm) == ERROR_SUCCESS && np && nm) {
|
|
DISPLAYCONFIG_PATH_INFO *paths = calloc(np, sizeof *paths);
|
|
DISPLAYCONFIG_MODE_INFO *modes = calloc(nm, sizeof *modes);
|
|
if (paths && modes && QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &np, paths, &nm, modes, NULL) == ERROR_SUCCESS)
|
|
for (UINT32 i = 0; i < np && !hz; i++) {
|
|
DISPLAYCONFIG_SOURCE_DEVICE_NAME src = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
|
|
.size = sizeof src,
|
|
.adapterId = paths[i].sourceInfo.adapterId,
|
|
.id = paths[i].sourceInfo.id}};
|
|
DISPLAYCONFIG_RATIONAL r = paths[i].targetInfo.refreshRate;
|
|
if (DisplayConfigGetDeviceInfo(&src.header) == ERROR_SUCCESS && !wcscmp(src.viewGdiDeviceName, mi.szDevice) &&
|
|
r.Denominator)
|
|
hz = (double)r.Numerator / r.Denominator;
|
|
}
|
|
free(paths);
|
|
free(modes);
|
|
}
|
|
DEVMODEW dm = {.dmSize = sizeof dm};
|
|
if (!hz && EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
|
|
hz = dm.dmDisplayFrequency; // 0 and 1 mean the hardware default
|
|
return hz;
|
|
}
|
|
|
|
// Window thread. force: the display mode changed, measure even on the same monitor.
|
|
static void refresh_rate_update(HWND h, int force) {
|
|
HMONITOR mon = MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY);
|
|
if (!force && mon == g_rate_mon) return;
|
|
g_rate_mon = mon;
|
|
gfx_window_set_refresh(monitor_rate_hz(mon));
|
|
}
|
|
|
|
int gfx_window_monitor_size(int *w, int *h) {
|
|
MONITORINFO mi = {sizeof mi};
|
|
if (!g_hwnd || !GetMonitorInfoW(MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) return 0;
|
|
*w = mi.rcMonitor.right - mi.rcMonitor.left;
|
|
*h = mi.rcMonitor.bottom - mi.rcMonitor.top;
|
|
return 1;
|
|
}
|
|
|
|
static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
|
|
LRESULT input_res;
|
|
if (input_window_msg(h, m, w, l, &input_res)) return input_res;
|
|
if (menu_window_msg(h, m, w, l, &input_res)) return input_res;
|
|
switch (m) {
|
|
case WM_KEYDOWN:
|
|
if (w == VK_F11 && !(l & (1 << 30))) { // bit 30: auto-repeat
|
|
input_cursor_suspend(1);
|
|
toggle_fullscreen(h);
|
|
input_cursor_suspend(0);
|
|
cfg_set("TOTK_FULLSCREEN", is_fullscreen(h) ? "1" : "0"); // the menu shows it; the next start keeps it
|
|
cfg_save();
|
|
note_applied(); // what cfg_get reads now (the environment may still override it)
|
|
}
|
|
break;
|
|
case WM_APP_SETTINGS:
|
|
apply_settings(h);
|
|
return 0;
|
|
case WM_APP_FULLSCREEN:
|
|
if (cfg_fullscreen() && !is_fullscreen(h)) {
|
|
input_cursor_suspend(1);
|
|
toggle_fullscreen(h);
|
|
input_cursor_suspend(0);
|
|
}
|
|
return 0;
|
|
case WM_SIZE:
|
|
keep_awake(w != SIZE_MINIMIZED);
|
|
if (w != SIZE_MINIMIZED) gfx_window_mark_dirty();
|
|
refresh_rate_update(h, 0);
|
|
return 0;
|
|
case WM_EXITSIZEMOVE: // dragged onto another monitor
|
|
refresh_rate_update(h, 0);
|
|
break;
|
|
case WM_DISPLAYCHANGE:
|
|
refresh_rate_update(h, 1);
|
|
break;
|
|
case WM_GETDPISCALEDSIZE: // a framed window keeps its client size in pixels on a monitor at another scale
|
|
if (!is_fullscreen(h) && !IsZoomed(h)) {
|
|
RECT c, r;
|
|
if (GetClientRect(h, &c)) {
|
|
r = (RECT){0, 0, c.right, c.bottom};
|
|
if (AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, LOWORD(w))) {
|
|
*(SIZE *)l = (SIZE){r.right - r.left, r.bottom - r.top};
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
break; // DefWindowProc: Windows scales the size with the DPI
|
|
case WM_DPICHANGED: { // display scale changed, or the window reached a monitor at another scale
|
|
const RECT *s = (const RECT *)l;
|
|
plat_xchg32(&g_dpi, LOWORD(w));
|
|
rt_log("gfx: window DPI %u (%u%%)", LOWORD(w), LOWORD(w) * 100u / USER_DEFAULT_SCREEN_DPI);
|
|
if (is_fullscreen(h)) { // keep covering the monitor the suggested rect lies on
|
|
MONITORINFO mi = {sizeof mi};
|
|
if (GetMonitorInfoW(MonitorFromRect(s, MONITOR_DEFAULTTONEAREST), &mi))
|
|
SetWindowPos(h, NULL, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
|
|
mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE);
|
|
} else {
|
|
SetWindowPos(h, NULL, s->left, s->top, s->right - s->left, s->bottom - s->top, SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
return 0;
|
|
}
|
|
case WM_ACTIVATEAPP: {
|
|
int in = w != 0;
|
|
gfx_window_focus_changed(in);
|
|
if (in && g_fs_pending) {
|
|
g_fs_pending = 0;
|
|
PostMessageW(h, WM_APP_FULLSCREEN, 0, 0);
|
|
}
|
|
return 0;
|
|
}
|
|
case WM_CLOSE:
|
|
gfx_window_closed();
|
|
case WM_ERASEBKGND:
|
|
return 1;
|
|
}
|
|
return DefWindowProcW(h, m, w, l);
|
|
}
|
|
|
|
// Brings the game window to the front. Windows refuses SetForegroundWindow to a process that was not the last to get
|
|
// input, which is the case for a game started by a frontend; attaching this thread's input queue to the foreground
|
|
// window's thread for the call is the documented way around it. The window is not left topmost.
|
|
static void raise_window(HWND h) {
|
|
HWND fg = GetForegroundWindow();
|
|
DWORD me = GetCurrentThreadId(), other = fg ? GetWindowThreadProcessId(fg, NULL) : 0;
|
|
int attached = other && other != me && AttachThreadInput(me, other, TRUE);
|
|
ShowWindow(h, IsIconic(h) ? SW_RESTORE : SW_SHOW);
|
|
BringWindowToTop(h);
|
|
SetForegroundWindow(h);
|
|
SetActiveWindow(h);
|
|
if (attached) AttachThreadInput(me, other, FALSE);
|
|
if (GetForegroundWindow() != h) { // still refused: a topmost toggle puts it above the others without keeping it there
|
|
SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
|
SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
|
}
|
|
}
|
|
|
|
static void window_main(void *arg) {
|
|
(void)arg;
|
|
HINSTANCE inst = GetModuleHandleW(NULL);
|
|
// Icon resource 1 of totk.rc (runtime/totk.ico): the title bar, the taskbar button and Alt+Tab.
|
|
WNDCLASSEXW wc = {.cbSize = sizeof wc, .lpfnWndProc = wnd_proc, .hInstance = inst,
|
|
.hIcon = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXICON),
|
|
GetSystemMetrics(SM_CYICON), LR_SHARED),
|
|
.hIconSm = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
|
|
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
|
|
.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW), .lpszClassName = L"TotKRecompWindow"};
|
|
RegisterClassExW(&wc);
|
|
int cw, ch, ow, oh;
|
|
cfg_window_size(&cw, &ch);
|
|
outer_size(NULL, cw, ch, &ow, &oh);
|
|
// The window opens behind the user's windows first and is then raised (raise_window): a game started by a frontend
|
|
// has no other way to reach it. The game counts itself in focus until the first WM_ACTIVATEAPP, so loading goes on,
|
|
// and keyboard input waits for the foreground (input_win.c). If Windows still refuses the foreground, the taskbar
|
|
// button flashes, and TOTK_FULLSCREEN waits for the first activation.
|
|
// TOTK_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
|
|
int background = g_background = cfg_get("TOTK_BACKGROUND_WINDOW") && atoi(cfg_get("TOTK_BACKGROUND_WINDOW"));
|
|
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
|
|
NULL, inst, NULL);
|
|
if (!g_hwnd) {
|
|
rt_log("gfx: CreateWindowEx failed (%lu)", GetLastError());
|
|
g_wnd_failed = 1;
|
|
} else {
|
|
// An IME switched on over the game attaches to this window and then shows its candidate window and eats the keys
|
|
// it composes, although there is nothing here to type into (a Chinese IME takes Shift, the B button by default).
|
|
// Detaching the game's window leaves the software keyboard alone: its edit box sits in a window of its own
|
|
// (swkbd_win.c) and gets the IME it needs.
|
|
ImmAssociateContext(g_hwnd, NULL);
|
|
// The frame was measured at the system DPI; the window may have opened on a monitor at another scale.
|
|
plat_xchg32(&g_dpi, (int32_t)GetDpiForWindow(g_hwnd));
|
|
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
|
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
if (!background) {
|
|
// Pending before the raise: the activation it gets is the first WM_ACTIVATEAPP, which takes the fullscreen
|
|
// setting; set after it, fullscreen waited for the next activation and the game started in a window
|
|
g_fs_pending = cfg_fullscreen();
|
|
raise_window(g_hwnd); // a frontend's player may have no way to reach a window left behind
|
|
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
|
|
FlashWindowEx(&fw);
|
|
}
|
|
note_applied();
|
|
refresh_rate_update(g_hwnd, 1);
|
|
keep_awake(1);
|
|
}
|
|
plat_event_set(&g_wnd_ready);
|
|
if (g_wnd_failed) return;
|
|
MSG msg;
|
|
while (GetMessageW(&msg, NULL, 0, 0) > 0) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ---- gfx_wsys.h
|
|
// Makes the window on its thread and waits until it is there.
|
|
int wsys_open(void) {
|
|
g_wnd_failed = 0;
|
|
plat_event_init(&g_wnd_ready, 1, 0);
|
|
if (!plat_thread_spawn(window_main, NULL, "window", 0)) rt_fatal("gfx: cannot start the window thread");
|
|
plat_event_wait(&g_wnd_ready);
|
|
return !g_wnd_failed;
|
|
}
|
|
|
|
VkSurfaceKHR wsys_surface(VkInstance inst) {
|
|
VkWin32SurfaceCreateInfoKHR si = {.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
|
|
.hinstance = GetModuleHandleW(NULL), .hwnd = g_hwnd};
|
|
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
|
VkResult r = vkCreateWin32SurfaceKHR(inst, &si, NULL, &surface);
|
|
if (r != VK_SUCCESS) rt_log("gfx: vkCreateWin32SurfaceKHR failed (%d)", (int)r);
|
|
return r == VK_SUCCESS ? surface : VK_NULL_HANDLE;
|
|
}
|
|
|
|
void wsys_client_size(int *w, int *h) {
|
|
RECT rc = {0};
|
|
GetClientRect(g_hwnd, &rc);
|
|
*w = rc.right, *h = rc.bottom;
|
|
}
|
|
|
|
int wsys_minimized(void) { return IsIconic(g_hwnd) != 0; }
|
|
|
|
void wsys_resize(int cw, int ch) {
|
|
int ow, oh;
|
|
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
|
SetWindowPos(g_hwnd, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
|
|
void wsys_probe_focus(int in) { SendMessageW(g_hwnd, WM_ACTIVATEAPP, in ? TRUE : FALSE, 0); }
|
|
|
|
void wsys_probe_escape(void) { SendMessageW(g_hwnd, WM_KEYDOWN, VK_ESCAPE, 0); }
|
|
|
|
int wsys_probe_click(int where) {
|
|
if (where == 0) SendMessageW(g_hwnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
|
|
else SendMessageW(g_hwnd, WM_MOUSEACTIVATE, (WPARAM)g_hwnd, MAKELPARAM(where == 1 ? HTCLIENT : HTCAPTION, WM_LBUTTONDOWN));
|
|
return 1;
|
|
}
|