Files
tasksoftwaretest/src/platform_native.jai
T

678 lines
25 KiB
Plaintext

// Native platform layer: Win32 window + OpenGL 3.3 core context, via Jai's Window_Creation / Input / GL modules.
// The project is a plain text file (default: project.taskforge in the working directory, or the path given
// on the command line), so it can live in your game's repository.
#import "Window_Creation";
#import "Input";
#import "GL";
#import "File";
#import "File_Utilities";
#import "Clipboard";
#import "System";
#import "Windows";
#import "Thread";
#import "Curl";
#import "Windows_Utf8";
#import "stb_image";
#import "stb_image_resize";
#import "stb_image_write";
tf_user32 :: #system_library "user32";
SetProcessDpiAwarenessContext_ :: (value: *void) -> s32 #foreign tf_user32 "SetProcessDpiAwarenessContext";
GetDpiForWindow_ :: (hwnd: HWND) -> u32 #foreign tf_user32 "GetDpiForWindow";
MsgWaitForMultipleObjects_ :: (count: u32, handles: *HANDLE, wait_all: s32, milliseconds: u32, wake_mask: u32) -> u32 #foreign tf_user32 "MsgWaitForMultipleObjects";
QS_ALLINPUT_ :: 0x04FF;
project_path: string;
server_url: string; // Set with --server; empty = local project file.
native_window: HWND;
main :: () {
args := get_command_line_arguments();
project_path = "project.taskforge";
i := 1;
while i < args.count {
if args[i] == "--sim" && i + 1 < args.count {
sim_script = args[i + 1];
i += 2;
continue;
}
if args[i] == "--server" && i + 1 < args.count {
server_url = copy_string(args[i + 1]);
while server_url.count && server_url[server_url.count - 1] == #char "/" server_url.count -= 1;
i += 2;
continue;
}
project_path = copy_string(args[i]);
i += 1;
}
SetProcessDpiAwarenessContext_(cast(*void) -4); // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
native_window = create_window(1440, 900, "TaskForge", background_color_rgb = .[0.067, 0.075, 0.094]);
_, hdc := wgl_create_context(native_window, null, 3, 3);
gl_load(*gl);
DragAcceptFiles_(native_window, 1);
#if OS == .WINDOWS {
if wglSwapIntervalEXT wglSwapIntervalEXT(1);
}
exe_dir := path_strip_filename(get_path_of_running_executable());
font_data, ok := read_entire_file(tprint("%fonts.bin", exe_dir));
if !ok {
log_error("Missing fonts.bin next to the executable. Build with: jai build.jai");
return;
}
if server_url.count curl_global_init(CURL_GLOBAL_DEFAULT);
video_native_init();
if !app_init(font_data, server_url.count > 0) {
log_error("Failed to initialize.");
return;
}
input: Frame_Input;
idle_frames := 0;
last_mouse_x, last_mouse_y := -1.0, -1.0;
running := true;
while running {
update_window_events();
reset_per_frame_input(*input);
for event: events_this_frame {
if event.type == {
case .QUIT;
running = false;
case .KEYBOARD;
button := -1;
if event.key_code == {
case .MOUSE_BUTTON_LEFT; button = MOUSE_LEFT;
case .MOUSE_BUTTON_RIGHT; button = MOUSE_RIGHT;
case .MOUSE_BUTTON_MIDDLE; button = MOUSE_MIDDLE;
}
if button >= 0 {
if event.key_pressed {
input.mouse_down[button] = true;
input.mouse_pressed[button] = true;
} else {
input.mouse_down[button] = false;
input.mouse_released[button] = true;
}
} else if event.key_pressed {
key: Key_Press;
key.key = cast(u32) event.key_code;
if event.shift_pressed key.mods |= MOD_SHIFT;
if event.ctrl_pressed key.mods |= MOD_CTRL;
if event.alt_pressed key.mods |= MOD_ALT;
array_add(*input.keys, key);
}
case .TEXT_INPUT;
if event.utf32 >= 32 && event.utf32 != 127 array_add(*input.chars, event.utf32);
case .DRAG_AND_DROP_FILES;
for event.files queue_file_from_disk(it);
case .MOUSE_WHEEL;
if event.typical_wheel_delta {
lines := -cast(float) event.wheel_delta / cast(float) event.typical_wheel_delta;
input.wheel_y += lines;
}
}
}
input.mods = 0;
if input_button_states[Key_Code.SHIFT] & .DOWN input.mods |= MOD_SHIFT;
if input_button_states[Key_Code.CTRL] & .DOWN input.mods |= MOD_CTRL;
if input_button_states[Key_Code.ALT] & .DOWN input.mods |= MOD_ALT;
rect: RECT;
GetClientRect(native_window, *rect);
pw := rect.right - rect.left;
ph := rect.bottom - rect.top;
dpi := GetDpiForWindow_(native_window);
input.dpr = ifx dpi then dpi / 96.0 else 1.0;
if pw <= 0 || ph <= 0 {
// Minimized.
MsgWaitForMultipleObjects_(0, null, 0, 250, QS_ALLINPUT_);
continue;
}
input.width = pw / input.dpr;
input.height = ph / input.dpr;
input.time = seconds_since_init();
input.unix_time = unix_now();
input.utc_offset = utc_offset_seconds();
poll_snip();
for platform_pending_files array_add(*input.files, it);
platform_pending_files.count = 0;
if sim_script.count {
if !simulate_frame(*input, pw, ph) running = false;
} else {
mx, my := get_mouse_pointer_position(native_window, false);
input.mouse_x = mx / input.dpr;
input.mouse_y = my / input.dpr;
// If a button was released outside the window we never got the event.
if input.mouse_down[MOUSE_LEFT] && !(input_button_states[Key_Code.MOUSE_BUTTON_LEFT] & .DOWN) {
input.mouse_down[MOUSE_LEFT] = false;
input.mouse_released[MOUSE_LEFT] = true;
}
}
poll_http_jobs();
frame_start := seconds_since_init();
wants_more := app_frame(*input);
if sim_screenshot.count {
save_screenshot(sim_screenshot, pw, ph);
sim_screenshot = "";
}
SwapBuffers(hdc);
// Vsync isn't guaranteed (drivers may ignore the swap interval), so cap the frame rate ourselves.
MIN_FRAME_TIME :: 1.0 / 120.0;
elapsed := seconds_since_init() - frame_start;
if elapsed < MIN_FRAME_TIME sleep_milliseconds(cast(s32) ((MIN_FRAME_TIME - elapsed) * 1000) + 1);
if sim_script.count continue;
moved := input.mouse_x != last_mouse_x || input.mouse_y != last_mouse_y;
last_mouse_x, last_mouse_y = input.mouse_x, input.mouse_y;
if events_this_frame.count || moved || wants_more idle_frames = 0;
else idle_frames += 1;
// A task app should not burn a CPU core: sleep until input arrives (or the caret needs to blink).
if idle_frames > 2 MsgWaitForMultipleObjects_(0, null, 0, xx ifx http_jobs.count || snip_waiting then 15 else 500, QS_ALLINPUT_);
}
app_shutdown();
}
platform_load_project :: () -> string, bool {
if !file_exists(project_path) return "", false;
text, ok := read_entire_file(project_path);
return text, ok;
}
platform_save_project :: (data: string) -> bool {
// Write to a temp file first so a crash mid-write can't destroy the project.
temp_path := tprint("%.tmp", project_path);
if !write_entire_file(temp_path, data) return false;
// MoveFileW refuses to overwrite; replace the old file atomically instead.
MOVEFILE_REPLACE_EXISTING :: 0x1;
MOVEFILE_WRITE_THROUGH :: 0x8;
return MoveFileExW_(utf8_to_wide(temp_path,, temp), utf8_to_wide(project_path,, temp), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
}
platform_storage_description :: () -> string {
return tprint("Saved to %", path_filename(project_path));
}
platform_server_label :: () -> string {
return server_url;
}
//
// HTTP (server mode): each request runs libcurl on its own thread; results are picked up by the main loop.
//
Http_Job :: struct {
tag: Http_Tag;
method: string;
key: string;
url: string;
body: string;
token: string;
status: int;
response: String_Builder;
thread: Thread;
}
http_jobs: [..] *Http_Job;
platform_http_request :: (tag: Http_Tag, method: string, path: string, body: string, token: string, key := "") {
job := New(Http_Job);
job.tag = tag;
job.method = copy_string(method);
job.key = copy_string(key);
job.url = sprint("%1%2", server_url, path);
job.body = copy_string(body);
job.token = copy_string(token);
thread_init(*job.thread, http_thread_proc);
job.thread.data = job;
thread_start(*job.thread);
array_add(*http_jobs, job);
}
http_thread_proc :: (thread: *Thread) -> s64 {
job := cast(*Http_Job) thread.data;
curl := curl_easy_init();
if !curl return 0;
defer curl_easy_cleanup(curl);
write :: (data: *u8, size: u64, count: u64, builder: *String_Builder) -> u64 #c_call {
n := size * count;
push_context { append(builder, data, cast(s64) n); }
return n;
}
headers: *curl_slist;
headers = curl_slist_append(headers, "Content-Type: text/plain; charset=utf-8");
if job.token.count headers = curl_slist_append(headers, temp_c_string(tprint("Authorization: Bearer %", job.token)));
defer curl_slist_free_all(headers);
curl_easy_setopt(curl, .URL, temp_c_string(job.url));
if job.method == "GET" {
curl_easy_setopt(curl, .HTTPGET, cast(s64) 1);
} else {
curl_easy_setopt(curl, .POST, cast(s64) 1);
curl_easy_setopt(curl, .POSTFIELDS, job.body.data);
curl_easy_setopt(curl, .POSTFIELDSIZE_LARGE, cast(s64) job.body.count);
}
curl_easy_setopt(curl, .HTTPHEADER, headers);
curl_easy_setopt(curl, .WRITEFUNCTION, write);
curl_easy_setopt(curl, .WRITEDATA, *job.response);
curl_easy_setopt(curl, .TIMEOUT, cast(s64) ifx job.tag == .BLOB_PUT || job.tag == .BLOB_GET then 600 else 20);
curl_easy_setopt(curl, .CONNECTTIMEOUT, cast(s64) 10);
if curl_easy_perform(curl) == .OK {
code: s64;
curl_easy_getinfo(curl, .RESPONSE_CODE, *code);
job.status = code;
}
return 0;
}
poll_http_jobs :: () {
i := 0;
while i < http_jobs.count {
job := http_jobs[i];
if !thread_is_done(*job.thread) {
i += 1;
continue;
}
thread_deinit(*job.thread);
array_ordered_remove_by_index(*http_jobs, i);
response := builder_to_string(*job.response);
http_on_response(job.tag, job.status, response, job.key);
free(response);
free(job.method);
free(job.key);
free(job.url);
free(job.body);
free(job.token);
free(job);
}
}
// The session token is kept per user profile, tied to the server it belongs to.
session_path :: () -> string {
home := get_home_directory();
dir := tprint("%/AppData/Roaming/TaskForge", home);
make_directory_if_it_does_not_exist(dir);
return tprint("%/session.txt", dir);
}
platform_load_session :: () -> string {
text, ok := read_entire_file(session_path(), log_errors = false);
if !ok return "";
lines := split(text, "\n",, temp);
if lines.count < 2 || lines[0] != server_url return "";
return copy_string(trim(lines[1]));
}
platform_save_session :: (token: string) {
write_entire_file(session_path(), tprint("%\n%\n", server_url, token));
}
platform_clipboard_get :: () -> string {
text := os_clipboard_get_text();
result := copy_temporary_string(text);
free(text);
return result;
}
platform_clipboard_set :: (text: string) {
os_clipboard_set_text(text);
}
platform_set_cursor :: (kind: Cursor_Kind) {
name: *s16;
if kind == {
case .HAND; name = cast(*s16) IDC_HAND;
case .TEXT; name = cast(*s16) IDC_IBEAM;
case .GRAB; name = cast(*s16) IDC_SIZEALL;
case; name = cast(*s16) IDC_ARROW;
}
SetCursor(LoadCursorW(null, name));
}
platform_export_project :: (data: string) {}
platform_request_import :: () {}
//
// Test harness: --sim "<commands>" drives the app with scripted input, one command per frame, separated by ';'.
// move X Y | down | up | click X Y | type TEXT | key CODE [MODS] | wheel LINES | wait N | shot FILE.bmp | quit
// Coordinates are logical pixels.
//
sim_script: string;
sim_screenshot: string;
sim_wait: int;
sim_pending_up: bool;
simulate_frame :: (input: *Frame_Input, pw: s32, ph: s32) -> bool {
if sim_pending_up {
sim_pending_up = false;
input.mouse_down[MOUSE_LEFT] = false;
input.mouse_released[MOUSE_LEFT] = true;
return true;
}
if sim_wait > 0 {
sim_wait -= 1;
return true;
}
while sim_script.count && (sim_script[0] == #char ";" || sim_script[0] == #char " ") advance(*sim_script);
if !sim_script.count return false;
end := find_index_from_left(sim_script, #char ";");
if end < 0 end = sim_script.count;
command := substring(sim_script, 0, end);
advance(*sim_script, end);
parts := split(command, " ",, allocator = temp);
if !parts.count return true;
if parts[0] == {
case "move";
input.mouse_x = string_to_float(parts[1]);
input.mouse_y = string_to_float(parts[2]);
case "down";
input.mouse_down[MOUSE_LEFT] = true;
input.mouse_pressed[MOUSE_LEFT] = true;
case "up";
input.mouse_down[MOUSE_LEFT] = false;
input.mouse_released[MOUSE_LEFT] = true;
case "click";
input.mouse_x = string_to_float(parts[1]);
input.mouse_y = string_to_float(parts[2]);
input.mouse_down[MOUSE_LEFT] = true;
input.mouse_pressed[MOUSE_LEFT] = true;
sim_pending_up = true;
case "type";
text := substring(command, 5, command.count);
for 0..text.count-1 array_add(*input.chars, cast(u32) text[it]);
case "key";
k: Key_Press;
k.key = cast(u32) string_to_int(parts[1]);
if parts.count > 2 k.mods = cast(u32) string_to_int(parts[2]);
array_add(*input.keys, k);
case "drop";
queue_file_from_disk(substring(command, 5, command.count));
case "wheel";
input.wheel_y += string_to_float(parts[1]);
case "wait";
sim_wait = string_to_int(parts[1]);
case "shot";
sim_screenshot = copy_string(parts[1]);
case "quit";
return false;
}
return true;
}
save_screenshot :: (path: string, w: s32, h: s32) {
pixels := NewArray(w * h * 4, u8,, allocator = temp);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, xx w, xx h, GL_BGRA, GL_UNSIGNED_BYTE, pixels.data);
// 32-bit BMP, bottom-up rows (which is what glReadPixels gives us).
b: String_Builder;
put :: (b: *String_Builder, value: $T) {
v := value;
append(b, cast(*u8) *v, size_of(T));
}
append(*b, "BM");
put(*b, cast(u32) (54 + pixels.count));
put(*b, cast(u32) 0);
put(*b, cast(u32) 54);
put(*b, cast(u32) 40);
put(*b, cast(s32) w);
put(*b, cast(s32) h);
put(*b, cast(u16) 1);
put(*b, cast(u16) 32);
put(*b, cast(u32) 0);
put(*b, cast(u32) pixels.count);
put(*b, cast(s32) 2835);
put(*b, cast(s32) 2835);
put(*b, cast(u32) 0);
put(*b, cast(u32) 0);
append(*b, pixels.data, pixels.count);
write_entire_file(path, *b);
}
//
// Media: local blob files, image decoding, clipboard screenshots, file picker, drag & drop, snipping tool.
//
tf_shell32 :: #system_library "shell32";
tf_comdlg :: #system_library "comdlg32";
tf_kernel :: #system_library "kernel32";
DragAcceptFiles_ :: (hwnd: HWND, accept: s32) #foreign tf_shell32 "DragAcceptFiles";
ShellExecuteW_ :: (hwnd: HWND, op: *u16, file: *u16, params: *u16, dir: *u16, show: s32) -> *void #foreign tf_shell32 "ShellExecuteW";
IsClipboardFormatAvailable_ :: (format: u32) -> s32 #foreign tf_user32 "IsClipboardFormatAvailable";
GetClipboardSequenceNumber_ :: () -> u32 #foreign tf_user32 "GetClipboardSequenceNumber";
OpenClipboard_ :: (hwnd: HWND) -> s32 #foreign tf_user32 "OpenClipboard";
CloseClipboard_ :: () -> s32 #foreign tf_user32 "CloseClipboard";
GetClipboardData_ :: (format: u32) -> *void #foreign tf_user32 "GetClipboardData";
GlobalLock_ :: (mem: *void) -> *u8 #foreign tf_kernel "GlobalLock";
GlobalUnlock_ :: (mem: *void) -> s32 #foreign tf_kernel "GlobalUnlock";
GlobalSize_ :: (mem: *void) -> u64 #foreign tf_kernel "GlobalSize";
GetSystemTimeAsFileTime_ :: (time: *u64) #foreign tf_kernel "GetSystemTimeAsFileTime";
GetOpenFileNameW_ :: (ofn: *Open_File_Name) -> s32 #foreign tf_comdlg "GetOpenFileNameW";
MoveFileExW_ :: (from: *u16, to: *u16, flags: u32) -> s32 #foreign tf_kernel "MoveFileExW";
CF_DIB_ :: 8;
Open_File_Name :: struct {
lStructSize: u32;
hwndOwner: HWND;
hInstance: *void;
lpstrFilter: *u16;
lpstrCustomFilter: *u16;
nMaxCustFilter: u32;
nFilterIndex: u32;
lpstrFile: *u16;
nMaxFile: u32;
lpstrFileTitle: *u16;
nMaxFileTitle: u32;
lpstrInitialDir: *u16;
lpstrTitle: *u16;
Flags: u32;
nFileOffset: u16;
nFileExtension: u16;
lpstrDefExt: *u16;
lCustData: *void;
lpfnHook: *void;
lpTemplateName: *u16;
pvReserved: *void;
dwReserved: u32;
FlagsEx: u32;
}
// Files from the picker or the snipping tool; handed to the app with the next frame's input.
platform_pending_files: [..] Input_File;
unix_now :: () -> float64 {
ft: u64;
GetSystemTimeAsFileTime_(*ft);
return cast(float64) (ft - 116444736000000000) / 10000000.0;
}
utc_offset_seconds :: () -> float64 {
now := current_time_consensus();
local := to_calendar(now, .LOCAL);
utc := to_calendar(now, .UTC);
minutes: s64 = (cast(s64) local.hour * 60 + local.minute) - (cast(s64) utc.hour * 60 + utc.minute);
if local.day_of_month_starting_at_0 != utc.day_of_month_starting_at_0 {
// Crossed midnight: the side that is "ahead" is one day later.
if local.year > utc.year || (local.year == utc.year && (local.month_starting_at_0 > utc.month_starting_at_0 ||
(local.month_starting_at_0 == utc.month_starting_at_0 && local.day_of_month_starting_at_0 > utc.day_of_month_starting_at_0)))
minutes += 24 * 60;
else
minutes -= 24 * 60;
}
return cast(float64) minutes * 60;
}
// Blobs of a local project live next to the project file.
blob_dir :: () -> string {
return tprint("%.files", project_path);
}
platform_local_blob_save :: (hash: string, data: string) {
dir := blob_dir();
make_directory_if_it_does_not_exist(dir);
path := tprint("%/%", dir, hash);
if !file_exists(path) write_entire_file(path, data);
}
platform_local_blob_load :: (hash: string) {
data, ok := read_entire_file(tprint("%/%", blob_dir(), hash), log_errors = false);
blob_loaded(hash, ok, data);
if ok free(data);
}
platform_decode_image :: (key: string, data: string) {
w, h, n: s32;
pixels := stbi_load_from_memory(data.data, cast(s32) data.count, *w, *h, *n, 4);
if !pixels {
image_decoded(key, false, 0, 0, null);
return;
}
defer stbi_image_free(pixels);
if max(w, h) > MAX_IMAGE_SIDE {
scale := cast(float) MAX_IMAGE_SIDE / max(w, h);
sw := max(cast(s32) 1, cast(s32) (w * scale));
sh := max(cast(s32) 1, cast(s32) (h * scale));
small := alloc(sw * sh * 4);
defer free(small);
stbir_resize_uint8(pixels, w, h, 0, small, sw, sh, 0, 4);
image_decoded(key, true, sw, sh, small);
} else {
image_decoded(key, true, w, h, pixels);
}
}
encode_png :: (rgba: *u8, w: s32, h: s32) -> string {
b: String_Builder;
write :: (ctx: *void, data: *void, size: s32) #c_call {
push_context { append(cast(*String_Builder) ctx, data, size); }
}
stbi_write_png_to_func(write, *b, w, h, 4, rgba, w * 4);
return builder_to_string(*b);
}
// The clipboard's bitmap as PNG bytes, or "" if it holds no image.
platform_clipboard_image :: () -> string {
if !IsClipboardFormatAvailable_(CF_DIB_) return "";
if !OpenClipboard_(native_window) return "";
defer CloseClipboard_();
handle := GetClipboardData_(CF_DIB_);
if !handle return "";
dib := GlobalLock_(handle);
if !dib return "";
defer GlobalUnlock_(handle);
total := cast(s64) GlobalSize_(handle);
header_size := (cast(*u32) dib).*;
w := (cast(*s32) (dib + 4)).*;
h := (cast(*s32) (dib + 8)).*;
bits := (cast(*u16) (dib + 14)).*;
compression := (cast(*u32) (dib + 16)).*;
if (bits != 24 && bits != 32) || w <= 0 || h == 0 return "";
offset := cast(s64) header_size;
if compression == 3 && header_size == 40 offset += 12; // BI_BITFIELDS masks follow a plain header.
bottom_up := h > 0;
height := abs(h);
stride := ((w * bits + 31) / 32) * 4;
if offset + cast(s64) stride * height > total return "";
rgba := cast(*u8) alloc(w * height * 4);
defer free(rgba);
for y: 0..height-1 {
src := dib + offset + (ifx bottom_up then height - 1 - y else y) * stride;
dst := rgba + y * w * 4;
step := bits / 8;
for x: 0..w-1 {
dst[x*4 + 0] = src[x*step + 2];
dst[x*4 + 1] = src[x*step + 1];
dst[x*4 + 2] = src[x*step + 0];
dst[x*4 + 3] = 255; // Screenshot DIBs usually carry no meaningful alpha.
}
}
return encode_png(rgba, w, xx height);
}
screenshot_name :: () -> string {
c := to_calendar(current_time_consensus(), .LOCAL);
return tprint("Screenshot %-%-% %.%.%.png", c.year, formatInt(c.month_starting_at_0 + 1, minimum_digits = 2), formatInt(c.day_of_month_starting_at_0 + 1, minimum_digits = 2),
formatInt(c.hour, minimum_digits = 2), formatInt(c.minute, minimum_digits = 2), formatInt(c.second, minimum_digits = 2));
}
platform_pick_files :: () {
buffer := NewArray(65536, u16,, temp);
filter := utf8_to_wide("Images and videos\0*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.webp;*.mp4;*.mov;*.m4v;*.webm;*.mkv;*.avi\0All files\0*.*\0\0",, temp);
ofn: Open_File_Name;
ofn.lStructSize = size_of(Open_File_Name);
ofn.hwndOwner = native_window;
ofn.lpstrFilter = filter;
ofn.lpstrFile = buffer.data;
ofn.nMaxFile = xx buffer.count;
OFN_FILEMUSTEXIST :: 0x00001000;
OFN_ALLOWMULTISELECT :: 0x00000200;
OFN_EXPLORER :: 0x00080000;
OFN_NOCHANGEDIR :: 0x00000008;
ofn.Flags = OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT | OFN_EXPLORER | OFN_NOCHANGEDIR;
if !GetOpenFileNameW_(*ofn) return;
// Either one full path, or a directory followed by file names, each zero terminated.
parts: [..] string;
parts.allocator = temp;
start := 0;
for i: 0..buffer.count-1 {
if buffer[i] != 0 continue;
if i == start break;
array_add(*parts, wide_to_utf8(buffer.data + start, cast(s32) (i - start),, temp));
start = i + 1;
}
paths: [..] string;
paths.allocator = temp;
if parts.count == 1 array_add(*paths, parts[0]);
else for 1..parts.count-1 array_add(*paths, tprint("%\\%", parts[0], parts[it]));
for paths queue_file_from_disk(it);
}
queue_file_from_disk :: (path: string) {
data, ok := read_entire_file(path, log_errors = false);
if !ok return;
array_add(*platform_pending_files, .{copy_string(path_filename(path)), data});
}
// Opens the Windows snipping overlay; the snip is picked up from the clipboard when it arrives.
snip_waiting: bool;
snip_sequence: u32;
snip_deadline: float64;
platform_capture_screenshot :: () {
snip_sequence = GetClipboardSequenceNumber_();
snip_waiting = true;
snip_deadline = seconds_since_init() + 120;
ShellExecuteW_(null, utf8_to_wide("open",, temp), utf8_to_wide("ms-screenclip:",, temp), null, null, 1);
}
poll_snip :: () {
if !snip_waiting return;
if seconds_since_init() > snip_deadline { snip_waiting = false; return; }
if GetClipboardSequenceNumber_() == snip_sequence return;
png := platform_clipboard_image();
if !png.count return; // Clipboard changed but no image yet (the snip may still be processing).
snip_waiting = false;
array_add(*platform_pending_files, .{copy_string(screenshot_name()), png});
}
platform_capture_label :: () -> string { return "Snip screen"; }