mcap/src/base/platform/platform_windows.jai

881 lines
34 KiB
Plaintext

platform_load_platform_information :: () {
// load_platform_information_shared();
}
platform_get_path_program_data :: inline () -> string {
// #NOTE: This is slow due to loading shell32 - see comment attached to get_path_os_program_data();
return copy_string(get_path_os_program_data());
}
platform_get_path_config_data :: inline () -> string {
return platform_get_path_program_data();
}
Platform :: struct { // Windows
registered_global_hotkey_ids: [GLOBAL_HOTKEY_ACTION_COUNT * MAX_HOTKEY_BINDINGS] s32;
registered_global_hotkey_count: s64;
global_hotkeys_failed: string; // the combinations that could not be registered, for the main window; heap
tray_icon_configured: bool; // nid and its icon are ready; adding it can still fail while Explorer is not
tray_icon_failed: bool; // minimizing then goes to the taskbar; the main window says so
hwnd: HWND;
// WM_PAINT draws frames with this context while the modal size/move loop blocks the main loop.
main_context: *#Context;
pumping_messages: bool;
in_size_move: bool;
in_live_resize_frame: bool;
live_resize_frame_drawn: bool; // during this pump; see Windows_Read_Messages
swallow_hotkey_character: bool; // see win32_key_down_is_bound
resetting_window_placement: bool;
pending_dpi_scale: float32; // from WM_DPICHANGED, applied by win32_apply_dpi_change
nid: NOTIFYICONDATAW;
icon_menu: HMENU;
tray_icon: HICON;
taskbar_created_message: u32; // broadcast when Explorer restarts, which drops every tray icon
all_cpus: []Windows_CPUID;
performance_cpus: []Windows_CPUID;
efficiency_cpus: []Windows_CPUID;
}
windows_open_directory :: (path: string) -> bool {
push_allocator(temp);
quoted_path := windows_quote_argument(path);
result := cast(s64)ShellExecuteW(null, utf8_to_wide("open"), utf8_to_wide("explorer.exe"),
utf8_to_wide(quoted_path), null, SW_SHOWNORMAL);
if (result <= 32) {
log_error("[windows_open_directory] ShellExecuteW failed with args `open explorer.exe %`", quoted_path);
os_log_error();
return false;
}
return true;
}
windows_open_file_in_default_program :: (path: string, working_directory: string, args := "") -> bool {
if is_directory(path) {
return windows_open_directory(path);
}
push_allocator(temp);
path_copy := copy_string(path,, temp);
path_overwrite_separators(path_copy, #char "\\");
working_dir_copy := copy_string(working_directory,, temp);
path_overwrite_separators(working_dir_copy, #char "\\");
// We use ShellExecuteExW so we can properly detach from new launched process:
full_path_w := utf8_to_wide(path_copy);
working_dir_w := utf8_to_wide(working_dir_copy);
SEE_MASK_NOCLOSEPROCESS :: 0x00000040;
sei: SHELLEXECUTEINFOW;
defer CloseHandle(sei.hProcess);
sei.cbSize = size_of(SHELLEXECUTEINFOW);
sei.fMask = SEE_MASK_NOCLOSEPROCESS; // | SEE_MASK_NO_CONSOLE
sei.lpFile = full_path_w;
sei.lpDirectory = working_dir_w;
sei.nShow = SW_SHOWNORMAL;
if args.count sei.lpParameters = utf8_to_wide(args);
result := ShellExecuteExW(*sei);
if !result {
log_error("[windows_open_file_in_default_program] ShellExecuteExW failed with args % % %",
path, working_directory, args);
os_log_error();
}
return cast(bool)result;
}
// Quote one argument for a Windows command line, by the CommandLineToArgvW rules that
// ShellExecuteW's parameter string is parsed with: a run of backslashes is literal except before a
// quote, where it escapes. The backslash rule is the one that matters for paths -- without it
// "D:\TempLocal\" has its closing quote escaped by the trailing separator.
windows_quote_argument :: (s: string) -> string {
sb: String_Builder;
append(*sb, #char "\"");
backslashes := 0;
for i: 0..s.count-1 {
c := s[i];
if c == #char "\\" {
backslashes += 1;
continue;
}
run := ifx c == #char "\"" then backslashes * 2 + 1 else backslashes;
for 1..run append(*sb, #char "\\");
backslashes = 0;
append(*sb, c);
}
for 1..backslashes * 2 append(*sb, #char "\\");
append(*sb, #char "\"");
return builder_to_string(*sb);
}
platform_open_file_in_default_program :: (path: string, working_directory: string) {
windows_open_file_in_default_program(path, working_directory);
}
platform_open_directory :: windows_open_directory;
// Called every time the application receives a message
WndProc :: (hWnd: HWND, message: u32, wParam: WPARAM, lParam: LPARAM) -> LRESULT #c_call {
using platform;
TEMPORARY_STORAGE_SIZE :: 1000;
storage_buf: [TEMPORARY_STORAGE_SIZE] u8;
storage: Temporary_Storage;
storage.data = storage_buf.data;
storage.size = TEMPORARY_STORAGE_SIZE;
new_context: #Context;
new_context.temporary_storage = *storage;
use_app_logger(*new_context);
push_context,defer_pop new_context;
defer reset_temporary_storage(); // frees any overflow pages; the buffer itself is on the stack
push_allocator(temp);
if message == {
case WM_KEYDOWN; #through;
case WM_SYSKEYDOWN;
swallow_hotkey_character = win32_key_down_is_bound(reset_window_hotkey, wParam);
case WM_KEYUP; #through;
case WM_SYSKEYUP;
swallow_hotkey_character = false;
case WM_CHAR; #through;
case WM_SYSCHAR;
if swallow_hotkey_character return 0;
case WM_DPICHANGED;
// The X and Y DPI are always equal for a window.
if hWnd == hwnd pending_dpi_scale = cast(float32) (wParam & 0xFFFF) / 96.0;
// ImGui would apply the suggested rect, the old size scaled by the DPI ratio, over the
// reset's rect, which is already in the new monitor's pixels.
if resetting_window_placement return 0;
}
if (ImGui.ImplWin32_WndProcHandler(hwnd, message, wParam, lParam)) {
return cast(LRESULT) true;
}
if swallow_hotkey_character && (message == WM_KEYDOWN || message == WM_SYSKEYDOWN) return 0;
if taskbar_created_message && message == taskbar_created_message {
// Without the icon, a window hidden in the tray could only come back by hotkey.
if app.minimized_to_tray && !Win32_Show_Tray_Icon() {
ShowWindow(hwnd, SW_MINIMIZE);
app.minimized_to_tray = false;
}
return 0;
}
if message == {
case WM_SYSCOMMAND;
SC_MINIMIZE :: 0xF020; SC_CLOSE :: 0xF060;
// View mode (app/open_file.jai) has no tray: close ends it, and minimize is the system's own.
if app.view_mode && (wParam & 0xFFF0) == SC_CLOSE { app.should_exit = true; return 0; }
if app.view_mode && (wParam & 0xFFF0) == SC_MINIMIZE return DefWindowProcW(hWnd, message, wParam, lParam);
if (wParam & 0xFFF0) == {
case SC_CLOSE; #through;
case SC_MINIMIZE;
app.minimize_to_tray_this_frame = true;
case SC_KEYMENU;
// A tapped Alt would otherwise enter the system menu's modal loop, which stops frames.
case;
return DefWindowProcW(hWnd, message, wParam, lParam);
}
return 0;
case WM_SIZE;
if (wParam == SIZE_MINIMIZED) {
return 0;
}
g_resizeWidth = cast(u32)LOWORD(lParam);
g_resizeHeight = cast(u32)HIWORD(lParam);
return 0;
case WM_ENTERSIZEMOVE;
in_size_move = true;
SetTimer(hWnd, LIVE_RESIZE_TIMER_ID, LIVE_RESIZE_TIMER_MS, null);
return 0;
case WM_EXITSIZEMOVE;
in_size_move = false;
KillTimer(hWnd, LIVE_RESIZE_TIMER_ID);
return 0;
case WM_TIMER;
if wParam != LIVE_RESIZE_TIMER_ID return DefWindowProcW(hWnd, message, wParam, lParam);
if in_size_move InvalidateRect(hWnd, null, .FALSE);
return 0;
case WM_PAINT;
if !in_size_move || !pumping_messages || in_live_resize_frame || !main_context {
return DefWindowProcW(hWnd, message, wParam, lParam);
}
ValidateRect(hWnd, null);
in_live_resize_frame = true;
live_resize_frame_drawn = true;
push_context main_context.* {
auto_release_temp();
app_frame(in_live_resize = true);
}
in_live_resize_frame = false;
return 0;
case WM_CREATE;
icon_menu = CreatePopupMenu();
AppendMenuW(icon_menu, MF_STRING, xx THIS_ICON_CONTEXT_MENU_ITEM_ID, utf8_to_wide("Exit"));
return 0;
case WM_DESTROY;
PostQuitMessage(0);
return 0;
// Registered to the window rather than the thread: a modal loop (a live resize, the tray menu,
// a message box) dispatches window messages but discards the thread's own.
case WM_HOTKEY;
global_hotkey_fired(cast(s64) wParam);
return 0;
// wParam is true when the session really ends; Windows may end the app as soon as this returns.
case WM_ENDSESSION;
if wParam {
app_exit_shutdown_run_now();
logger_flush(2000); // close_logger may never run
}
return 0;
case WM_TRAYICON;
// Most applications perform actions on button up (after click), but we do
// on button down to make it feel as responsive as possible.
if lParam == WM_LBUTTONDOWN {
#if DEBUG { log("Restoring application"); }
app.restore_window_this_frame = true;
}
if lParam == WM_RBUTTONDOWN {
curPos: POINT;
GetCursorPos(*curPos);
// SetForegroundWindow so the popup shows on top
SetForegroundWindow(hwnd);
TPM_NONOTIFY :: 0x0080;
TPM_RETURNCMD :: 0x0100;
clicked := TrackPopupMenu(icon_menu, TPM_RETURNCMD | TPM_NONOTIFY, curPos.x, curPos.y, 0, hwnd, null);
if clicked == THIS_ICON_CONTEXT_MENU_ITEM_ID {
#if DEBUG { log("Posting Quit Message."); }
PostQuitMessage(0);
}
}
return 0;
case;
return DefWindowProcW(hWnd, message, wParam, lParam);
}
return 0;
}
// =-----------PROCEDURES-------------------------------------------------------= //
// Registers the [global_hotkeys] bindings, replacing whatever was registered before. Each binding
// is registered on its own: Alt+Space is PowerToys Run's default, and losing it must not cost the
// others.
platform_apply_global_hotkeys :: (actions: [] Hotkey_Action) { using platform;
platform_unregister_global_hotkeys();
failed: [..] string;
failed.allocator = temp;
for action, action_index: actions {
for combo, binding_index: array_view(action.hotkey.bindings, 0, action.hotkey.binding_count) {
vk := win32_virtual_key(combo.key);
if !vk {
log_error("[hotkeys] % cannot be a global hotkey (%); its key is not supported.", to_string(combo), action.name);
array_add(*failed, to_string(combo));
continue;
}
modifiers: u32 = MOD_NOREPEAT;
if combo.ctrl modifiers |= MOD_CONTROL;
if combo.shift modifiers |= MOD_SHIFT;
if combo.alt modifiers |= MOD_ALT;
id := global_hotkey_id(action_index, binding_index);
if !RegisterHotKey(hwnd, id, modifiers, vk) {
log_error("[hotkeys] Could not register the global hotkey % (%); another program may be using it.", to_string(combo), action.name);
if vk == VK_SNAPSHOT log_error("[hotkeys] PrintScreen is taken by Snagit when it runs, and by Windows when Settings > Accessibility > Keyboard > \"Use the Print screen key to open screen capture\" is on.");
array_add(*failed, to_string(combo));
continue;
}
registered_global_hotkey_ids[registered_global_hotkey_count] = id;
registered_global_hotkey_count += 1;
log("[hotkeys] Global hotkey % registered (%).", to_string(combo), action.name);
}
}
free(global_hotkeys_failed,, context.default_allocator);
global_hotkeys_failed = join(..failed, " and ",, context.default_allocator);
}
platform_unregister_global_hotkeys :: () { using platform;
for 0..registered_global_hotkey_count - 1 UnregisterHotKey(hwnd, registered_global_hotkey_ids[it]);
registered_global_hotkey_count = 0;
}
// The keys a global hotkey may use; 0 for any other.
win32_virtual_key :: (key: ImGui.Key) -> u32 {
k := cast(s64) key;
if k >= cast(s64) ImGui.Key.A && k <= cast(s64) ImGui.Key.Z return cast(u32) (#char "A" + k - cast(s64) ImGui.Key.A);
if k >= cast(s64) ImGui.Key._0 && k <= cast(s64) ImGui.Key._9 return cast(u32) (#char "0" + k - cast(s64) ImGui.Key._0);
if k >= cast(s64) ImGui.Key.F1 && k <= cast(s64) ImGui.Key.F24 return cast(u32) (VK_F1 + k - cast(s64) ImGui.Key.F1);
if key == {
case .PrintScreen; return VK_SNAPSHOT;
case .Space; return VK_SPACE;
case .Enter; return VK_RETURN;
case .Tab; return VK_TAB;
case .Escape; return VK_ESCAPE;
case .LeftArrow; return VK_LEFT;
case .RightArrow; return VK_RIGHT;
case .UpArrow; return VK_UP;
case .DownArrow; return VK_DOWN;
case .PageUp; return VK_PRIOR;
case .PageDown; return VK_NEXT;
case .Home; return VK_HOME;
case .End; return VK_END;
case .Insert; return VK_INSERT;
case .Delete; return VK_DELETE;
}
return 0;
}
// Also fails while Explorer is not running, so it is checked on every call.
Win32_Show_Tray_Icon :: () -> bool { using platform;
if !tray_icon_configured return false;
if !cast(bool) Shell_NotifyIconW(NIM_ADD, *nid) {
log_error("Shell_NotifyIconW(NIM_ADD) failed; minimizing to the taskbar instead of the tray.");
os_log_error();
tray_icon_failed = true;
return false;
}
tray_icon_failed = false; // an earlier failure while Explorer was down has passed
return true;
}
Win32_Hide_Tray_Icon :: () { using platform;
if tray_icon_configured Shell_NotifyIconW(NIM_DELETE, *nid);
}
Win32_Set_Tray_Icon :: () { using platform;
taskbar_created_message = RegisterWindowMessageW(utf8_to_wide("TaskbarCreated",, temp));
NIF_MESSAGE :: 0x0001; NIF_ICON :: 0x0002; NIF_TIP :: 0x0004;
// Tooltip text must be less than 64 chars for some reason...
Tooltip_Text :: APP_TRAY_TOOLTIP_TEXT;
Tooltip_Text_u16 := utf8_to_wide(Tooltip_Text,, temp);
nid.cbSize = size_of(NOTIFYICONDATAW);
nid.hWnd = hwnd;
nid.uID = THIS_ICON_ID; // ID of the icon
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_TRAYICON; // Custom message when interacting with the tray icon
tray_icon = LoadImageW(null, utf8_to_wide(get_app_tray_icon_path(),, temp), IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
if tray_icon == null {
log_error("LoadImageW failed for the tray icon; minimizing to the taskbar instead of the tray.");
os_log_error();
tray_icon_failed = true;
return;
}
nid.hIcon = tray_icon;
memcpy(nid.szTip.data, Tooltip_Text_u16, Tooltip_Text.count * size_of(u16));
// Configured even if this first add fails: at autostart Explorer's notification area may not be
// ready yet, and Win32_Show_Tray_Icon adds the icon again on every minimize.
tray_icon_configured = true;
if !cast(bool) Shell_NotifyIconW(NIM_ADD, *nid) {
log_error("Shell_NotifyIconW(NIM_ADD) failed; minimizing to the taskbar until it succeeds.");
os_log_error();
tray_icon_failed = true;
}
}
// ImGui rescales the fonts per monitor by itself, but the theme's sizes are baked at one scale, so
// they are baked again for the monitor the main window is now on. Must run outside a frame.
win32_apply_dpi_change :: () { using platform;
scale := pending_dpi_scale;
pending_dpi_scale = 0;
if scale <= 0 || scale == app.win_scale return;
log("[dpi] The window is now on a monitor with DPI scale % (was %); rescaling the layout.", scale, app.win_scale);
app.win_scale = scale;
app_theme_bake(scale);
}
// Returns as soon as a message is queued (a hotkey, a tray click, the window being uncovered), or
// after timeout_ms.
platform_wait_while_hidden :: (timeout_ms: u32) {
MsgWaitForMultipleObjectsEx(0, null, timeout_ms, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
}
platform_minimize_to_tray :: Win32_Minimize_Window_To_Tray;
Win32_Minimize_Window_To_Tray :: () {
using platform;
// Minimize normally if we're not able to show the tray icon.
if !Win32_Show_Tray_Icon() {
ShowWindow(hwnd, SW_MINIMIZE);
return;
}
ShowWindow(hwnd, SW_HIDE);
app.minimized_to_tray = true;
}
// Win32_Bring_Window_To_Foreground's first step; does nothing unless the window is in the tray.
Win32_Restore_Window_From_Tray :: () {
using platform;
if app.minimized_to_tray {
ShowWindow(hwnd, SW_SHOW);
app.minimized_to_tray = false;
Win32_Hide_Tray_Icon();
}
}
platform_restore_window :: Win32_Bring_Window_To_Foreground;
Win32_Bring_Window_To_Foreground :: () {
using platform;
#if DEBUG { log("Attempting to bring window to foreground"); }
Win32_Restore_Window_From_Tray();
if Win32_Window_Is_Minimized() {
ShowWindow(hwnd, SW_RESTORE);
}
SetForegroundWindow(hwnd);
}
Win32_Window_Is_Minimized :: () -> bool {
return cast(bool) IsIconic(platform.hwnd);
}
// Returns false when a live-resize frame drew during the pump. That frame rebuilt the per-frame
// state (anything app_ui_frame_start builds) in temporary storage it then released, so the calling
// frame must not go on to draw with what its own #frameStart built.
Windows_Read_Messages :: () -> frame_state_valid: bool {
platform.pumping_messages = true;
platform.live_resize_frame_drawn = false;
defer platform.pumping_messages = false;
msg: MSG;
while (PeekMessageW(*msg, null, 0, 0, PM_REMOVE)) {
TranslateMessage(*msg);
DispatchMessageW(*msg);
if (msg.message == WM_QUIT) {
app.should_exit = true;
}
}
return !platform.live_resize_frame_drawn;
}
windows_enable_large_pages_if_possible :: () {
token: HANDLE = ---;
if OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, *token) {
luid: u64;
if LookupPrivilegeValue(null, SE_LOCK_MEMORY_NAME, *luid) {
priv : TOKEN_PRIVILEGES;
priv.PrivilegeCount = 1;
priv.Privileges[0].Luid = luid;
priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
// A nonzero return only means the call itself worked, not that the privilege was
// granted: an account without SeLockMemoryPrivilege succeeds here and reports
// ERROR_NOT_ALL_ASSIGNED. AdjustTokenPrivileges sets the last error to ERROR_SUCCESS
// when it assigned everything, so the error code has to be read as well or this flag
// claims a privilege the process does not hold.
adjusted := AdjustTokenPrivileges(token, 0, *priv, size_of(TOKEN_PRIVILEGES), null, null) != 0;
error_code := GetLastError();
app.large_pages_allowed = adjusted && error_code == 0;
// The code is logged from the captured value rather than through os_log_error, because
// logging itself can reset the thread's last error before it could be read again.
if !adjusted {
log_error("[windows_enable_large_pages_if_possible] AdjustTokenPrivileges failed with code %: %",
error_code, get_error_string(error_code));
} else if error_code == ERROR_NOT_ALL_ASSIGNED {
// Granted under secpol.msc > Local Policies > User Rights Assignment > Lock pages in
// memory, and it only takes effect after a new logon. Not having it is not an error:
// arenas asking for large pages fall back to 4 KB ones.
// NOTE: (IMPORTANT) Lock pages in memory (SeLockMemoryPrivilege) is assigned to
// accounts/groups, not automatically to everyone who happens to have administrator privileges.
log("[windows_enable_large_pages_if_possible] SeLockMemoryPrivilege is not held by this account; large pages are unavailable.");
} else if error_code != 0 {
log_error("[windows_enable_large_pages_if_possible] AdjustTokenPrivileges reported an unexpected code %: %",
error_code, get_error_string(error_code));
}
log("app.large_pages_allowed = %", app.large_pages_allowed);
} else {
log_error("[windows_enable_large_pages_if_possible] LookupPrivilegeValue failed.");
}
CloseHandle(token);
} else {
log_error("[windows_enable_large_pages_if_possible] OpenProcessToken failed.");
}
}
platform_set_thread_affinity_all :: #bake_arguments platform_set_thread_affinity(affinity=.All);
platform_set_thread_affinity :: (thread: *Thread, affinity: CPUID_Type) {
using platform;
cpus: []Windows_CPUID;
if #complete affinity == {
case .All; cpus = all_cpus;
case .Performance; cpus = performance_cpus;
case .Efficiency; cpus = efficiency_cpus;
// case .Top_Performance; cpus = top_cpus;
}
windows_set_thread_affinity(thread.windows_thread, cpus);
}
windows_set_thread_affinity :: (thread: HANDLE, cpus: []Windows_CPUID) {
if !cpus.count || thread == INVALID_HANDLE_VALUE return;
success := SetThreadSelectedCpuSets(thread, cpus.data, xx cpus.count);
if !success {
log_error("SetThreadSelectedCpuSets failed on thread %!", thread);
}
}
windows_get_cpu_information :: () {
push_allocator(context.default_allocator);
using platform;
all_cpus = array_copy(windows_get_cpu_ids(.All,, temp));
performance_cpus = array_copy(windows_get_cpu_ids(.Performance,, temp));
efficiency_cpus = array_copy(windows_get_cpu_ids(.Efficiency,, temp));
// top_cpus = array_copy(windows_get_cpu_ids(.Top_Performance,, temp));
}
Windows_CPUID :: #type,isa u32;
windows_get_cpu_ids :: ($type: CPUID_Type) -> []Windows_CPUID {
length: DWORD;
GetSystemCpuSetInformation(null, 0, *length, GetCurrentProcess(), 0);
buffer := NewArray(length, u8,, temp);
GetSystemCpuSetInformation(xx buffer.data, length, *length, GetCurrentProcess(), 0);
item_count := length / size_of(SYSTEM_CPU_SET_INFORMATION);
info_array: []SYSTEM_CPU_SET_INFORMATION = .{item_count, cast(*SYSTEM_CPU_SET_INFORMATION)buffer.data};
core_ids: [..]Windows_CPUID;
for 0..info_array.count-1 {
// EfficiencyClass == 0 usually means E-Core, and 1 means P-core, 2 means best P-cores (usually 2 CPUs have this value)
#if type == .All { type_matches := info_array[it].CpuSet.EfficiencyClass >= 0; }
#if type == .Performance { type_matches := info_array[it].CpuSet.EfficiencyClass >= 1; }
#if type == .Efficiency { type_matches := info_array[it].CpuSet.EfficiencyClass == 0; }
// #if type == .Top_Performance { type_matches := info_array[it].CpuSet.EfficiencyClass == 2; }
if info_array[it].Type == .CpuSetInformation && type_matches {
cpu := info_array[it].CpuSet;
array_add(*core_ids, xx cpu.Id);
}
}
return core_ids;
}
platform_update_memory_usage_metrics :: () { using perf.memory_usage;
pmc: PROCESS_MEMORY_COUNTERS;
if (GetProcessMemoryInfo(GetCurrentProcess(), *pmc, size_of(PROCESS_MEMORY_COUNTERS))) {
working_set = xx pmc.WorkingSetSize;
virtual_memory = xx pmc.PagefileUsage;
peak_working_set = xx pmc.PeakWorkingSetSize;
peak_virtual_memory = xx pmc.PeakPagefileUsage;
}
}
windows_check_if_we_are_running_as_administrator :: () {
token: HANDLE;
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, *token) {
elevation: TOKEN_ELEVATION = {};
size: DWORD = size_of(TOKEN_ELEVATION);
ok := GetTokenInformation(token, .TokenElevation, *elevation, size_of(TOKEN_ELEVATION), *size);
CloseHandle(token);
app.is_admin = ok && elevation.TokenIsElevated;
}
}
// %NAME% expanded as cmd does. The path is returned as is when it has none or expansion fails. Temp.
windows_expand_environment :: (path: string) -> string {
if !contains(path, "%") return path;
ExpandEnvironmentStringsW :: (lpSrc: LPCWSTR, lpDst: LPWSTR, nSize: DWORD) -> DWORD #foreign kernel32;
buffer: [4096] u16;
count := ExpandEnvironmentStringsW(utf8_to_wide(path,, temp), buffer.data, buffer.count);
if count == 0 || count > buffer.count return path;
expanded, ok := wide_to_utf8(buffer.data,, temp);
return ifx ok then expanded else path;
}
// A BCP 47 tag such as ja-JP, or "". Temp.
platform_preferred_language :: () -> string {
buffer: [LOCALE_NAME_MAX_LENGTH] u16;
if GetUserDefaultLocaleName(buffer.data, buffer.count) <= 1 return "";
name, ok := wide_to_utf8(buffer.data,, temp);
return ifx ok then name else "";
}
// Temp.
platform_cjk_font_candidates :: () -> [] Cjk_Font {
fonts := windows_expand_environment("%WINDIR%\\Fonts");
list: [..] Cjk_Font;
list.allocator = temp;
cjk_candidate(*list, .SIMPLIFIED, tprint("%\\msyh.ttc", fonts), "Microsoft YaHei");
cjk_candidate(*list, .TRADITIONAL, tprint("%\\msjh.ttc", fonts), "Microsoft JhengHei");
cjk_candidate(*list, .JAPANESE, tprint("%\\YuGothM.ttc", fonts), "Yu Gothic");
cjk_candidate(*list, .JAPANESE, tprint("%\\meiryo.ttc", fonts), "Meiryo");
cjk_candidate(*list, .KOREAN, tprint("%\\malgun.ttf", fonts), "Malgun Gothic");
return list;
}
// Read-only for the life of the process; pages are read from the file only as they are touched.
platform_map_file_read_only :: (path: string) -> (data: string, ok: bool) {
file, opened := file_open(path);
if !opened return "", false;
defer file_close(*file);
size, size_ok := file_length(file);
if !size_ok || size <= 0 return "", false;
mapping := CreateFileMappingW(file.handle, null, PAGE_READONLY, 0, 0, null);
if !mapping return "", false;
defer CloseHandle(mapping); // the view keeps the mapping alive
view := MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0);
if !view return "", false;
return string.{size, cast(*u8) view}, true;
}
// Drops what the system's file cache holds of the file, so the next read comes from the disk (-cold).
// Opening a file unbuffered makes the cache manager flush and purge its pages, when nothing has the
// file mapped. False when it cannot be opened.
platform_evict_file_cache :: (path: string) -> bool {
FILE_FLAG_NO_BUFFERING :: 0x20000000;
wide := utf8_to_wide(path,, temp);
handle := CreateFileW(wide, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, null, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING);
if handle == INVALID_HANDLE_VALUE return false;
CloseHandle(handle);
return true;
}
// "" when the variable is unset or empty. In the current allocator.
windows_environment_variable :: (name: string) -> string {
name_w := utf8_to_wide(name,, temp);
length := GetEnvironmentVariableW(name_w, null, 0);
if !length return "";
buffer := NewArray(length, u16,, temp);
written := GetEnvironmentVariableW(name_w, buffer.data, length);
if !written || written >= length return "";
value, ok := wide_to_utf8(buffer.data, cast(s32) written);
return ifx ok then value else "";
}
platform_get_centered_window_dimensions :: (open_on_biggest: bool) -> s32, s32, s32, s32 {
if !monitors_enumerated {
enumerate_monitors();
monitors_enumerated = true;
}
assert(monitors.count > 0, "Unexpectedly no monitors found. This is a bug.");
monitor := monitors[0];
if open_on_biggest {
max_area := 0;
for monitors {
width := it.right - it.left;
height := it.bottom - it.top;
area := width * height;
if max_area < area {
monitor = it;
max_area = area;
} else if max_area == area && it.primary {
monitor = it; // prefer primary if there's no biggest monitor
}
}
} else {
for monitors {
if it.primary {
monitor = it;
}
}
}
monitor_width := monitor.right - monitor.left;
monitor_height := monitor.bottom - monitor.top;
// aspect_ratio := monitor_width / monitor_height;
w := cast(s32) (monitor_width * app_user_settings.window_width_fraction);
h := cast(s32) (monitor_height * app_user_settings.window_height_fraction);
x := cast(s32) (monitor.left + monitor_width / 2 - w / 2);
y := cast(s32) (monitor.top + monitor_height / 2 - h / 2);
return x, y, w, h;
}
// Back to the size and place the window starts at.
platform_reset_window_placement :: () -> bool { using platform;
// Displays may have been attached, removed or rearranged since startup.
monitors_enumerated = false;
monitor_index = 0;
for *monitors it.* = .{};
x, y, w, h := platform_get_centered_window_dimensions(open_on_biggest=true);
if IsZoomed(hwnd) != .FALSE ShowWindow(hwnd, SW_RESTORE); // otherwise it stays maximized at the new place
resetting_window_placement = true;
placed := SetWindowPos(hwnd, null, x, y, w, h, SWP_NOZORDER | SWP_NOACTIVATE) != .FALSE;
resetting_window_placement = false;
if !placed {
log_error("SetWindowPos failed; the window was not reset.");
os_log_error();
return false;
}
log("Window reset: x: %, y: %, size: %x% px", x, y, w, h);
return true;
}
// A key combination that types a character, such as Ctrl+Alt+Space, would also type it into the
// focused text field, and Alt+Space opens the window menu, so a bound key's character is dropped.
win32_key_down_is_bound :: (hotkey: Hotkey, virtual_key: WPARAM) -> bool {
key: ImGui.Key;
if virtual_key == VK_SPACE key = .Space;
else if virtual_key >= #char "A" && virtual_key <= #char "Z" key = cast(ImGui.Key) (cast(s64) ImGui.Key.A + cast(s64) (virtual_key - #char "A"));
else if virtual_key >= #char "0" && virtual_key <= #char "9" key = cast(ImGui.Key) (cast(s64) ImGui.Key._0 + cast(s64) (virtual_key - #char "0"));
else return false;
ctrl := GetKeyState(VK_CONTROL) < 0;
shift := GetKeyState(VK_SHIFT) < 0;
alt := GetKeyState(VK_MENU) < 0;
for array_view(hotkey.bindings, 0, hotkey.binding_count) {
if it.key == key && it.ctrl == ctrl && it.shift == shift && it.alt == alt return true;
}
return false;
}
get_primary_monitor :: () -> *Monitor {
if !monitors_enumerated {
enumerate_monitors();
monitors_enumerated = true;
}
for 0..monitors.count-1 {
if monitors[it].primary return *monitors[it];
}
return null;
}
// A monitor unplugged mid-enumeration fails GetMonitorInfoW; it is skipped, and only no monitors at
// all is fatal.
enumerate_monitors :: () {
EnumDisplayMonitors(null, null, monitor_enum_proc, 0);
if monitor_index == 0 panic("Couldn't enumerate monitors");
}
monitor_enum_proc :: (hMonitor: HMONITOR, hdc: HDC, rect: *RECT, data: LPARAM) -> BOOL #c_call {
using monitor : Monitor = ---;
monitor.left = rect.left;
monitor.top = rect.top;
monitor.right = rect.right;
monitor.bottom = rect.bottom;
monitorInfo.cbSize = size_of(MONITORINFO);
success := GetMonitorInfoW(hMonitor, *monitorInfo) != 0;
new_context : #Context;
use_app_logger(*new_context);
push_context new_context {
if !success {
log_error("[monitor_enum_proc] GetMonitorInfoW failed, monitor info unavailable; skipping that monitor.");
return xx true;
}
monitor.primary = !!(monitorInfo.dwFlags & 0x1);
monitor.present = true;
if monitor_index >= monitors.count {
log_error("[monitor_enum_proc] More than % monitors; ignoring the rest.", monitors.count);
return xx true;
}
monitors[monitor_index] = monitor;
monitor_index += 1;
}
return xx true;
}
// =-----------DATA-INTERNAL----------------------------------------------------= //
Monitors_Count :: () -> int {
count:=0; for monitors { count += xx it.present; }
return count;
}
monitors_enumerated : bool;
monitors: [8]Monitor;
monitor_index: int = 0;
Monitor :: struct {
monitorInfo: MONITORINFO;
left : int;
top : int;
right : int;
bottom : int;
primary: bool;
present: bool;
};
// =-----------CONSTANTS--------------------------------------------------------= //
THIS_ICON_ID :: 5001;
THIS_ICON_CONTEXT_MENU_ITEM_ID :: 5010;
LIVE_RESIZE_TIMER_ID :: 1;
// Windows raises anything below USER_TIMER_MINIMUM (10 ms) to it; Present's vsync paces the frames.
LIVE_RESIZE_TIMER_MS :: 1;
os_clear_unused_pages :: () {
SetProcessWorkingSetSize(GetCurrentProcess(), cast,no_check(SIZE_T)-1, cast,no_check(SIZE_T)-1);
log("[clear_unused_pages] Clearing unused pages.\n");
}
os_log_error :: () {
error_code := GetLastError();
log_error(" > GetLastError code: %, %", error_code, get_error_string(error_code));
}
// %TEMP%, without a separator at the end. Temp.
platform_temporary_folder :: () -> string {
return trim_right(windows_expand_environment("%TEMP%"), "/\\");
}
platform_choose_folder :: (title: string) -> string { return windows_choose_folder(title); }
// Asks for a folder with Explorer's own picker, over the app's window: its path (temp), or "" when
// none was chosen.
windows_choose_folder :: (title: string) -> string {
initialized := CoInitializeEx(null, COINIT_APARTMENTTHREADED);
defer if initialized >= 0 CoUninitialize();
dialog: *IFileOpenDialog;
if CoCreateInstance(*uid(CLSID_FileOpenDialog_UUID), null, CLSCTX_INPROC_SERVER, *uid(IFileOpenDialog_UUID), xx *dialog) < 0 return "";
defer IUnknown_Release(dialog);
options: u32;
dialog.GetOptions(dialog, *options);
dialog.SetOptions(dialog, options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
wide_title := utf8_to_wide(title,, temp);
dialog.SetTitle(dialog, wide_title);
if dialog.Show(dialog, platform.hwnd) < 0 return ""; // cancelled
item: *IShellItem;
if dialog.GetResult(dialog, *item) < 0 return "";
defer IUnknown_Release(item);
name: *u16;
if item.GetDisplayName(item, SIGDN_FILESYSPATH, *name) < 0 return "";
defer CoTaskMemFree(name);
path, ok := wide_to_utf8(name,, temp);
return ifx ok then path else "";
}