Video support, comment section, more
This commit is contained in:
+269
-9
@@ -12,6 +12,10 @@
|
||||
#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";
|
||||
@@ -47,6 +51,7 @@ main :: () {
|
||||
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);
|
||||
}
|
||||
@@ -58,6 +63,7 @@ main :: () {
|
||||
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;
|
||||
@@ -100,6 +106,8 @@ main :: () {
|
||||
}
|
||||
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;
|
||||
@@ -128,6 +136,11 @@ main :: () {
|
||||
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;
|
||||
@@ -163,7 +176,7 @@ main :: () {
|
||||
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 then 15 else 500, QS_ALLINPUT_);
|
||||
if idle_frames > 2 MsgWaitForMultipleObjects_(0, null, 0, xx ifx http_jobs.count || snip_waiting then 15 else 500, QS_ALLINPUT_);
|
||||
}
|
||||
|
||||
app_shutdown();
|
||||
@@ -179,7 +192,10 @@ 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;
|
||||
return file_move(temp_path, project_path);
|
||||
// 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 {
|
||||
@@ -196,6 +212,8 @@ platform_server_label :: () -> string {
|
||||
|
||||
Http_Job :: struct {
|
||||
tag: Http_Tag;
|
||||
method: string;
|
||||
key: string;
|
||||
url: string;
|
||||
body: string;
|
||||
token: string;
|
||||
@@ -206,9 +224,11 @@ Http_Job :: struct {
|
||||
|
||||
http_jobs: [..] *Http_Job;
|
||||
|
||||
platform_http_post :: (tag: Http_Tag, path: string, body: string, token: string) {
|
||||
platform_http_request :: (tag: Http_Tag, method: string, path: string, body: string, token: string, key := "") {
|
||||
job := New(Http_Job);
|
||||
job.tag = tag;
|
||||
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);
|
||||
@@ -236,13 +256,17 @@ http_thread_proc :: (thread: *Thread) -> s64 {
|
||||
defer curl_slist_free_all(headers);
|
||||
|
||||
curl_easy_setopt(curl, .URL, temp_c_string(job.url));
|
||||
curl_easy_setopt(curl, .POST, cast(s64) 1);
|
||||
curl_easy_setopt(curl, .POSTFIELDS, job.body.data);
|
||||
curl_easy_setopt(curl, .POSTFIELDSIZE, cast(s64) job.body.count);
|
||||
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) 20);
|
||||
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 {
|
||||
@@ -264,8 +288,10 @@ poll_http_jobs :: () {
|
||||
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);
|
||||
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);
|
||||
@@ -374,6 +400,8 @@ simulate_frame :: (input: *Frame_Input, pw: s32, ph: s32) -> bool {
|
||||
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";
|
||||
@@ -415,3 +443,235 @@ save_screenshot :: (path: string, w: s32, h: s32) {
|
||||
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"; }
|
||||
|
||||
Reference in New Issue
Block a user