- Import window (and -import SOURCE DEST): every image under a folder (PNG, JPEG, HEIC, AVIF, WebP, GIF, BMP, TIFF, camera RAW) becomes an .mcap holding the file as it was, a preview turned by its EXIF orientation, and metadata.json and import.json Capture Text (date from EXIF, the file name or its time; file name as title; camera as app). .snagx files become the same .mcap SnagitConverter makes. Duplicates across the whole library are skipped by a hash index (.import-index); videos and sidecars are skipped; source files are only read. - modules/Image_Decode: stb_image, then WIC for everything else; EXIF; orientation; previews. tools/image_probe.jai shows what it reads. - modules/Snagx (and modules/jaison), copied from SnagitConverter. - The library is a folder of folders: recursive scan, a folder filter, and a folder: search prefix. - OCR, the viewer and ocr-bench decode any image format, turned the right way up. - Rename D:\SnagitIndex to D:\McapIndex.
857 lines
33 KiB
Plaintext
857 lines
33 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;
|
|
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);
|
|
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 .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;
|
|
}
|
|
|
|
// "" 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));
|
|
}
|
|
// 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 "";
|
|
}
|