diff --git a/README.md b/README.md index b6b31bc..0599d93 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,25 @@ # Uhdk -A small macOS desktop platform library written in Zig 0.16.0 with a C99 ABI. +A small desktop platform library written in Zig 0.16.0 with a C99 ABI. It provides windows, input and IME, clipboard, file dialogs, drag and drop, menus, async file operations, file watching, and OpenGL context creation. The macOS backend uses AppKit, OpenGL, and FSEvents through a private Objective-C bridge. This directory is a standalone source package. It does not depend on the parent -repository, its examples, fonts, skills, or development tools. Currently macOS is -the only implemented backend. Building requires Xcode Command Line Tools. +repository, its examples, fonts, skills, or development tools. macOS builds require +Xcode Command Line Tools. The Windows Win32/WGL backend cross-compiles for +`x86_64-windows-gnu`; runtime verification on Windows is pending. It requires +Windows 10 version 1703 or later and an OpenGL 3.3-capable driver. ## Build ```sh zig build # zig-out/lib/libuhdk.a and zig-out/include/ zig build -Dshared=true # zig-out/lib/libuhdk.dylib and headers -zig build test # core library tests +zig build test # run core library tests on the native target +zig build -Dtarget=x86_64-windows-gnu +zig build -Dtarget=x86_64-windows-gnu -Dshared=true +zig build check -Dtarget=x86_64-windows-gnu # compile only ``` ## Use from Zig @@ -63,3 +68,12 @@ The library includes the GLAD loader implementation. With a current context insi `uhdk_gl_context_get_proc`. Keep a `GladGLContext` per native context; its members are named `Clear`, `DrawArrays`, etc. Check `VERSION_3_3` after loading. Do not define `GLAD_GL_IMPLEMENTATION` in your application when linking libuhdk. + +On Windows, the static artifact is `zig-out/lib/uhdk.lib`; shared builds install +`zig-out/bin/uhdk.dll` and its import library under `zig-out/lib/`. Keep the DLL +beside your executable. The first lifecycle caller establishes the Windows +platform thread; its message loop also owns COM dialogs, menus, clipboard and +watch completion. Input uses IMM32 composition; TSF reconversion and pen/touch +are not implemented. Native UTF-16 path bytes are kept separately from the WTF-8 +paths passed to Zig I/O. If a native path cannot be exported losslessly as a shell +URI, URI export returns `UNSUPPORTED`; native-path export still preserves it. diff --git a/build.zig b/build.zig index 7a18dfc..2e3e280 100644 --- a/build.zig +++ b/build.zig @@ -2,7 +2,7 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); - if (target.result.os.tag != .macos) @panic("Uhdk currently implements macOS only."); + if (target.result.os.tag != .macos and target.result.os.tag != .windows) @panic("Uhdk supports macOS and Windows."); const optimize = b.standardOptimizeOption(.{}); const shared = b.option(bool, "shared", "Build libuhdk as a shared library instead of static") orelse false; const module = b.createModule(.{ @@ -12,25 +12,30 @@ pub fn build(b: *std.Build) void { .link_libc = true, }); const native = b.addTranslateC(.{ - .root_source_file = b.path("src/macos/native.h"), + .root_source_file = b.path("src/native.h"), .target = target, .optimize = optimize, }); native.addIncludePath(b.path("include")); module.addImport("native", native.createModule()); module.addIncludePath(b.path("include")); - module.addIncludePath(b.path("src/macos")); - module.addCSourceFile(.{ - .file = b.path("src/macos/native.m"), - .flags = &.{ - "-fobjc-arc", "-fblocks", "-Wno-deprecated-declarations", - "-Wall", "-Wextra", "-Werror", - }, - }); - module.addCSourceFile(.{ .file = b.path("src/glad.c"), .flags = &.{"-std=c99"} }); - module.linkFramework("Cocoa", .{}); - module.linkFramework("OpenGL", .{}); - module.linkFramework("CoreServices", .{}); + module.addIncludePath(b.path("src")); + if (target.result.os.tag == .macos) { + module.addCSourceFile(.{ + .file = b.path("src/macos/native.m"), + .flags = &.{ + "-fobjc-arc", "-fblocks", "-Wno-deprecated-declarations", + "-Wall", "-Wextra", "-Werror", + }, + }); + module.linkFramework("Cocoa", .{}); + module.linkFramework("OpenGL", .{}); + module.linkFramework("CoreServices", .{}); + } else { + module.addCSourceFile(.{ .file = b.path("src/windows/native.c"), .flags = &.{ "-std=c11", "-Wall", "-Wextra", "-Werror", "-DUNICODE", "-D_UNICODE" } }); + for ([_][]const u8{ "user32", "gdi32", "opengl32", "shell32", "ole32", "uuid", "imm32", "comctl32", "shlwapi", "advapi32" }) |name| module.linkSystemLibrary(name, .{}); + } + module.addCSourceFile(.{ .file = b.path("src/glad.c"), .flags = if (shared) &.{ "-std=c99", "-DGLAD_API_CALL_EXPORT", "-DGLAD_API_CALL_EXPORT_BUILD" } else &.{"-std=c99"} }); const lib = b.addLibrary(.{ .name = "uhdk", .linkage = if (shared) .dynamic else .static, @@ -48,6 +53,9 @@ pub fn build(b: *std.Build) void { b.getInstallStep().dependOn(&install_lib.step); b.getInstallStep().dependOn(&install_headers.step); const unit = b.addTest(.{ .root_module = module }); + const check_step = b.step("check", "Compile library and core tests without running them"); + check_step.dependOn(&lib.step); + check_step.dependOn(&unit.step); const unit_run = b.addRunArtifact(unit); b.step("test", "Run core library tests").dependOn(&unit_run.step); const public_c = b.addTranslateC(.{ @@ -56,6 +64,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); public_c.addIncludePath(b.path("include")); + if (shared) public_c.defineCMacro("UHDK_SHARED", "1"); const public_module = b.addModule("uhdk", .{ .root_source_file = public_c.getOutput(), .target = target, diff --git a/include/uhdk.h b/include/uhdk.h index e262606..82466d0 100644 --- a/include/uhdk.h +++ b/include/uhdk.h @@ -2,7 +2,7 @@ #define UHDK_H_INCLUDED /* - * Uhdk desktop platform API — v0.1, macOS backend. + * Uhdk desktop platform API — v0.1, macOS and Windows backends. * * Implementation: Zig. ABI: C. Header baseline: C99 (also usable from C++). * No libc runtime objects or allocation contract. @@ -484,7 +484,10 @@ UHDK_API uhdk_status UHDK_CALL uhdk_window_cancel_composition_async(uhdk_window /* File references preserve native names, including non-UTF-8 POSIX paths. * Display names are UTF-8. URI/native-path export is explicit and owned by the * caller's buffer. Windows native path bytes are UTF-16LE (no terminator), - * POSIX native paths are uninterpreted bytes. A reference can carry an access + * POSIX native paths are uninterpreted bytes. Windows URI export returns + * UNSUPPORTED when the shell cannot represent a native name losslessly (such + * as unpaired UTF-16 surrogates); native-path export remains available. + * A reference can carry an access * grant; retaining it keeps that grant alive where the OS permits. Persistence * across launches/bookmark serialization is deferred to a future extension. * Buffer queries: NULL/0 gets required byte count in out_len and returns UHDK_STATUS_OK. @@ -523,9 +526,11 @@ typedef struct uhdk_file_info { * Read/write accept regular files only; final-component symlinks are rejected. * Write copies bytes at submission, stages an exclusive same-directory temp, * preserves POSIX mode of an existing regular file, then renames atomically. + * Windows rejects read-only destinations; replacement gets the directory's + * inherited permissions and does not preserve other destination attributes. * Symlinks/nonregular destinations are rejected. ACLs/xattrs/ownership are not * preserved; replacement requires a directory writable by the current user. - * New files use mode 0600. No power-loss durability or conflict detection promise. + * New POSIX files use mode 0600. No power-loss durability or conflict detection promise. * No silent truncate fallback. Cancellation before rename leaves the target intact. */ UHDK_API uhdk_status UHDK_CALL uhdk_file_read_all_async(uhdk_file *, uint64_t max_bytes, diff --git a/include/uhdk_opengl.h b/include/uhdk_opengl.h index 0c5a14d..12578c7 100644 --- a/include/uhdk_opengl.h +++ b/include/uhdk_opengl.h @@ -11,6 +11,9 @@ * uhdk_gl_context_get_proc, inside a begin_frame/end_frame bracket. Keep one * table per context and use it only while that context is current. */ +#if defined(UHDK_SHARED) +#define GLAD_API_CALL_EXPORT +#endif #include "glad/gl.h" #endif /* UHDK_OPENGL_H_INCLUDED */ diff --git a/src/FileOperation.zig b/src/FileOperation.zig index ec6b165..fa53eb3 100644 --- a/src/FileOperation.zig +++ b/src/FileOperation.zig @@ -73,17 +73,19 @@ pub fn step(self: *FileOperation) !Result { }; if (stat) |existing| { if (existing.kind != .file) return error.NotRegularFile; + if (@import("builtin").os.tag == .windows and existing.permissions.toAttributes().READONLY) + return error.AccessDenied; } self.atomic_file = try cwd.createFileAtomic(self.io, self.path, .{ - .permissions = .fromMode(0o600), + .permissions = if (@import("builtin").os.tag == .windows) .default_file else .fromMode(0o600), .replace = true, }); - if (stat) |existing| { + if (@import("builtin").os.tag != .windows) if (stat) |existing| { try self.atomic_file.?.file.setPermissions( self.io, .fromMode(existing.permissions.toMode() & 0o777), ); - } + }; }, .info => unreachable, } diff --git a/src/Resource.zig b/src/Resource.zig index 6d308ad..e903e53 100644 --- a/src/Resource.zig +++ b/src/Resource.zig @@ -45,6 +45,7 @@ pub const Gl = struct { }; pub const File = struct { + native_path: []const u8 = &.{}, path: [:0]const u8 = "", uri: []const u8 = "", display: []const u8 = "", diff --git a/src/api/app.zig b/src/api/app.zig index ecb017b..e13416d 100644 --- a/src/api/app.zig +++ b/src/api/app.zig @@ -1,5 +1,6 @@ //! C ABI entry points for app. const std = @import("std"); +const backend_kind = if (@import("builtin").os.tag == .windows) c.UHDK_BACKEND_WINDOWS else c.UHDK_BACKEND_MACOS; const c = @import("native"); const runtime = @import("../runtime.zig"); const abi = @import("../abi.zig"); @@ -19,7 +20,7 @@ pub export fn uhdk_app_create(desc: [*c]const c.uhdk_app_desc, out: [*c]?*c.uhdk desc.*.reserved != 0 or !abi.validText(desc.*.display_name, true) or !abi.validText(desc.*.application_id, true)) return abi.INVALID; - if (desc.*.preferred_backend != c.UHDK_BACKEND_AUTO and desc.*.preferred_backend != c.UHDK_BACKEND_MACOS) return c.UHDK_STATUS_UNSUPPORTED; + if (desc.*.preferred_backend != c.UHDK_BACKEND_AUTO and desc.*.preferred_backend != backend_kind) return c.UHDK_STATUS_UNSUPPORTED; if (singleton) return c.UHDK_STATUS_BUSY; const gpa = std.heap.smp_allocator; const backend = gpa.create(std.Io.Threaded) catch return abi.OOM; @@ -218,7 +219,7 @@ pub export fn uhdk_app_set_wake_deadline(handle: ?*c.uhdk_app, deadline: u64) ab } pub export fn uhdk_app_get_backend(_: ?*c.uhdk_app) c.uhdk_backend { - return c.UHDK_BACKEND_MACOS; + return backend_kind; } pub export fn uhdk_app_has_capability(_: ?*c.uhdk_app, cap: c.uhdk_capability) c.uhdk_bool { diff --git a/src/api/files.zig b/src/api/files.zig index b38f639..15baa8a 100644 --- a/src/api/files.zig +++ b/src/api/files.zig @@ -38,9 +38,9 @@ pub export fn uhdk_file_create_from_native_path( if (handle == null or out == null or !abi.validSpan(path.ptr, path.len) or - path.len == 0 or - abi.bytes(path)[0] != '/' or - std.mem.findScalar(u8, abi.bytes(path), 0) != null) return abi.INVALID; + path.len == 0) return abi.INVALID; + if (@import("builtin").os.tag != .windows and + (abi.bytes(path)[0] != '/' or std.mem.findScalar(u8, abi.bytes(path), 0) != null)) return abi.INVALID; if (runtime.checkThread(runtime.fromAppHandle(handle)) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD; const object = runtime.createObject(runtime.fromAppHandle(handle), c.Z_KIND_FILE) catch return abi.OOM; const status = c.z_native_file_init(runtime.castHandle(*c.uhdk_file, object), path, .{ .ptr = null, .len = 0 }, null); @@ -91,9 +91,11 @@ pub export fn uhdk_file_copy_representation( const data = switch (form) { c.UHDK_FILE_DISPLAY_NAME => object.payload.file.display, c.UHDK_FILE_URI => object.payload.file.uri, - c.UHDK_FILE_NATIVE_PATH => object.payload.file.path, + c.UHDK_FILE_NATIVE_PATH => object.payload.file.native_path, else => return abi.INVALID, }; + if (@import("builtin").os.tag == .windows and form == c.UHDK_FILE_URI and data.len == 0) + return c.UHDK_STATUS_UNSUPPORTED; out.* = data.len; if (buffer == null and capacity == 0) return abi.OK; if (capacity < data.len) return c.UHDK_STATUS_BUFFER_TOO_SMALL; diff --git a/src/macos/callbacks.zig b/src/callbacks.zig similarity index 81% rename from src/macos/callbacks.zig rename to src/callbacks.zig index 30f62aa..caa8d83 100644 --- a/src/macos/callbacks.zig +++ b/src/callbacks.zig @@ -1,8 +1,8 @@ //! C ABI entry points for callbacks. const std = @import("std"); const c = @import("native"); -const abi = @import("../abi.zig"); -const runtime = @import("../runtime.zig"); +const abi = @import("abi.zig"); +const runtime = @import("runtime.zig"); export fn z_emit(handle: ?*c.uhdk_app, event: [*c]const c.uhdk_event) void { runtime.emit(handle, event); @@ -65,7 +65,15 @@ export fn z_file_init( ) abi.Status { const object = runtime.fromObjectHandle(handle); const arena = object.arena.allocator(); - object.payload.file.path = arena.dupeZ(u8, abi.bytes(path)) catch return abi.OOM; + object.payload.file.native_path = arena.dupe(u8, abi.bytes(path)) catch return abi.OOM; + if (@import("builtin").os.tag == .windows) { + if (path.len % 2 != 0) return abi.INVALID; + const wide = arena.alloc(u16, @intCast(path.len / 2)) catch return abi.OOM; + @memcpy(std.mem.sliceAsBytes(wide), abi.bytes(path)); + object.payload.file.path = std.unicode.wtf16LeToWtf8AllocZ(arena, wide) catch return abi.OOM; + } else { + object.payload.file.path = arena.dupeZ(u8, abi.bytes(path)) catch return abi.OOM; + } object.payload.file.uri = arena.dupe(u8, abi.textBytes(uri)) catch return abi.OOM; object.payload.file.display = arena.dupe(u8, abi.textBytes(display)) catch return abi.OOM; object.native = grant; @@ -87,7 +95,7 @@ export fn z_file_from_url( export fn z_file_path(handle: ?*c.uhdk_file) c.uhdk_bytes { const object = runtime.fromObjectHandle(handle); - return .{ .ptr = object.payload.file.path.ptr, .len = object.payload.file.path.len }; + return .{ .ptr = object.payload.file.native_path.ptr, .len = object.payload.file.native_path.len }; } export fn z_menu_items(handle: ?*c.uhdk_menu, len: [*c]u64) [*c]const c.uhdk_menu_item { diff --git a/src/macos/native.h b/src/native.h similarity index 100% rename from src/macos/native.h rename to src/native.h diff --git a/src/root.zig b/src/root.zig index 2535f37..3de47f0 100644 --- a/src/root.zig +++ b/src/root.zig @@ -10,7 +10,7 @@ comptime { checkExports(@import("api/files.zig")); checkExports(@import("api/menus.zig")); checkExports(@import("api/desktop.zig")); - _ = @import("macos/callbacks.zig"); + _ = @import("callbacks.zig"); } test { diff --git a/src/runtime.zig b/src/runtime.zig index 9f58c9a..7af46cb 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -294,6 +294,7 @@ pub fn applicationMain(application: *Application) void { application.worker_id = std.Thread.getCurrentId(); application.unlock(); application.entry.?(toAppHandle(application), application.user); + if (@import("builtin").os.tag == .windows) _ = c.z_native_gl(null, 4, null); application.lock(); application.stopping = true; application.worker_done = true; @@ -364,7 +365,11 @@ pub fn applicationError(err: anyerror) abi.Status { pub fn destroyObject(object: *Resource) void { if (!object.alive) return; std.debug.assert(checkThread(object.app) == abi.OK); - if (object.payload == .gl) std.debug.assert(!object.payload.gl.bracket); + if (object.payload == .gl) { + std.debug.assert(!object.payload.gl.bracket); + if (@import("builtin").os.tag == .windows and object.native != null) + _ = c.z_native_gl(object.native, 8, null); + } if (object.payload == .window) if (object.payload.window.context) |ctx| { destroyObject(ctx); object.payload.window.context = null; diff --git a/src/windows/native.c b/src/windows/native.c new file mode 100644 index 0000000..6a2611a --- /dev/null +++ b/src/windows/native.c @@ -0,0 +1,1861 @@ +/* Win32 bridge. Queues, handles, ownership and bounded file I/O remain in Zig. + * Cross-compiled implementation: native Windows runtime validation is pending. */ +#define COBJMACROS +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "native.h" +#include "glad/gl.h" + +#define Z_WAKE (WM_APP + 1) +#define Z_DIALOG_MESSAGE (WM_APP + 2) +#define Z_POPUP_MESSAGE (WM_APP + 3) +static const WCHAR window_class[] = L"UhdkWindow"; +typedef struct Window Window; +typedef struct Watch Watch; +typedef struct Graphics { + HDC dc; + HGLRC rc; + HWND window; + CRITICAL_SECTION lock; + uint32_t major, minor; +} Graphics; +struct Window { + Window *next; + HWND hwnd; + uhdk_window *handle; + uint32_t flags; + uhdk_size min_size, max_size; + float scale; + BOOL text_enabled, tracking, fullscreen, composing, suppress_char; + WCHAR high_surrogate; + uint64_t input_id, last_edit; + uhdk_text_state text_state; + uhdk_point pointer; + HCURSOR cursor; + HMENU menu; + uhdk_menu *menu_source; + WINDOWPLACEMENT placement; + LONG_PTR saved_style; +}; +struct Watch { + Watch *next; + HANDLE directory; + OVERLAPPED overlapped; + DWORD buffer[16384]; + WCHAR *leaf; + uhdk_watch *handle; + BOOL pending; +}; +static struct { + uhdk_app *app; + HWND dispatch; + Window *windows; + Watch *watches; + uhdk_menu *menu; + HICON icon; + BOOL running, com_initialized; + DPI_AWARENESS_CONTEXT previous_dpi; + IFileDialog *dialog; + uint64_t dialog_id, popup_id; + BOOL popup_selected; + const z_command *pending_dialog, *pending_popup; + uint64_t pending_dialog_id, pending_popup_id; + uint64_t input_id; +} host; +static volatile LONG main_thread; +static LRESULT CALLBACK windowProc(HWND, UINT, WPARAM, LPARAM); +static void refreshMenus(void); +static void pollWatches(void); +static void showDialog(void); +static void showPopup(void); + +static uhdk_status osError(void) { + switch (GetLastError()) { + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return UHDK_STATUS_OUT_OF_MEMORY; + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + return UHDK_STATUS_NOT_FOUND; + case ERROR_ACCESS_DENIED: + return UHDK_STATUS_PERMISSION_DENIED; + case ERROR_OPERATION_ABORTED: + return UHDK_STATUS_CANCELLED; + default: + return UHDK_STATUS_OS_ERROR; + } +} +static WCHAR *wide(uhdk_text text) { + if (text.len > INT_MAX) + return NULL; + int n = text.len ? MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.ptr, (int)text.len, + NULL, 0) + : 0; + if (text.len && !n) + return NULL; + WCHAR *s = calloc((size_t)n + 1, sizeof(WCHAR)); + if (s && n) + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.ptr, (int)text.len, s, n); + return s; +} +static char *utf8(const WCHAR *text, int len, uint64_t *out_len) { + int n = len ? WideCharToMultiByte(CP_UTF8, 0, text, len, NULL, 0, NULL, NULL) : 0; + char *s = calloc((size_t)n + 1, 1); + if (s && n) + WideCharToMultiByte(CP_UTF8, 0, text, len, s, n, NULL, NULL); + *out_len = (uint64_t)n; + return s; +} +static WCHAR *filePath(uhdk_file *file) { + uhdk_bytes bytes = z_file_path(file); + WCHAR *s = calloc((size_t)bytes.len / 2 + 1, sizeof(WCHAR)); + if (s && bytes.len) + memcpy(s, bytes.ptr, (size_t)bytes.len); + return s; +} +static Window *windowOf(void *handle) { + return handle ? z_object_native(handle) : NULL; +} +static void emit(Window *w, uint32_t type) { + uhdk_event e = {0}; + e.type = type; + e.window = w ? w->handle : NULL; + z_emit(host.app, &e); +} +static uint32_t modifiers(void) { + uint32_t m = 0; + if (GetKeyState(VK_SHIFT) & 0x8000) + m |= UHDK_MOD_SHIFT; + if (GetKeyState(VK_CONTROL) & 0x8000) + m |= UHDK_MOD_CONTROL; + if (GetKeyState(VK_MENU) & 0x8000) + m |= UHDK_MOD_ALT; + if ((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) + m |= UHDK_MOD_SUPER; + if (GetKeyState(VK_CAPITAL) & 1) + m |= UHDK_MOD_CAPS_LOCK; + if (GetKeyState(VK_NUMLOCK) & 1) + m |= UHDK_MOD_NUM_LOCK; + return m; +} +static void metrics(Window *w) { + RECT r; + GetClientRect(w->hwnd, &r); + UINT dpi = GetDpiForWindow(w->hwnd); + w->scale = dpi ? (float)dpi / 96.0f : 1.0f; + uhdk_window_metrics m = {0}; + m.struct_size = sizeof(m); + m.scale = w->scale; + m.framebuffer_width = (uint32_t)(r.right - r.left); + m.framebuffer_height = (uint32_t)(r.bottom - r.top); + m.logical_size = (uhdk_size){m.framebuffer_width / w->scale, m.framebuffer_height / w->scale}; + m.state = w->fullscreen ? UHDK_WINDOW_FULLSCREEN + : IsIconic(w->hwnd) ? UHDK_WINDOW_MINIMIZED + : IsZoomed(w->hwnd) ? UHDK_WINDOW_MAXIMIZED + : UHDK_WINDOW_NORMAL; + m.focused = GetFocus() == w->hwnd; + m.visible = IsWindowVisible(w->hwnd); + z_metrics(w->handle, &m); +} +static void preferences(void) { + uhdk_preferences p = {0}; + p.struct_size = sizeof(p); + DWORD light = 1, size = sizeof(light); + RegGetValueW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + L"AppsUseLightTheme", RRF_RT_REG_DWORD, NULL, &light, &size); + p.appearance = light ? UHDK_APPEARANCE_LIGHT : UHDK_APPEARANCE_DARK; + HIGHCONTRASTW contrast = {sizeof(contrast), 0, NULL}; + SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(contrast), &contrast, 0); + p.high_contrast = !!(contrast.dwFlags & HCF_HIGHCONTRASTON); + BOOL animations = TRUE; + SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animations, 0); + p.reduced_motion = !animations; + z_preferences(host.app, &p); +} +static void textEvent(Window *w, uint32_t type, const WCHAR *text, int length, int cursor) { + uint64_t len; + char *s = utf8(text, length, &len); + if (!s) + return; + uhdk_event e = {0}; + e.type = type; + e.window = w->handle; + e.input_id = w->input_id = ++host.input_id; + w->last_edit = e.input_id; + e.data.text.text = (uhdk_text){s, len}; + e.data.text.revision = w->text_state.revision; + if (cursor >= 0 && cursor <= length) { + uint64_t position; + char *prefix = utf8(text, cursor, &position); + if (prefix) { + e.data.text.selection_start = position; + free(prefix); + } + } + z_emit(host.app, &e); + free(s); +} +static void editEvent(Window *w, uint32_t command) { + uhdk_event e = {0}; + e.type = UHDK_EVENT_EDIT_COMMAND; + e.window = w->handle; + e.input_id = w->input_id = ++host.input_id; + w->last_edit = e.input_id; + e.data.edit.command = command; + e.data.edit.extend_selection = !!(modifiers() & UHDK_MOD_SHIFT); + z_emit(host.app, &e); +} +static void menuEvent(Window *w, const uhdk_menu_item *item) { + if (!item || item->flags & UHDK_MENU_DISABLED) + return; + if (item->role == UHDK_MENU_ROLE_QUIT) { + emit(NULL, UHDK_EVENT_QUIT_REQUESTED); + return; + } + if (w && item->role >= UHDK_MENU_ROLE_COPY && item->role <= UHDK_MENU_ROLE_REDO) { + editEvent(w, UHDK_EDIT_COPY + item->role - UHDK_MENU_ROLE_COPY); + return; + } + uhdk_event e = {0}; + e.type = UHDK_EVENT_MENU_COMMAND; + e.window = w ? w->handle : NULL; + e.data.menu.command_id = item->command_id; + z_emit(host.app, &e); +} +static const uhdk_menu_item *shortcut(uhdk_menu *menu, uint32_t key, uint32_t mods) { + if (!menu) + return NULL; + uint64_t count; + const uhdk_menu_item *items = z_menu_items(menu, &count); + for (uint64_t i = 0; i < count; ++i) { + const uhdk_menu_item *item = &items[i]; + if (item->flags & UHDK_MENU_DISABLED) + continue; + if (item->shortcut_key == key && item->shortcut_modifiers == mods && key) + return item; + const uhdk_menu_item *found = shortcut(item->submenu, key, mods); + if (found) + return found; + } + return NULL; +} +static uint32_t logicalKey(WPARAM vk) { + switch (vk) { + case VK_ESCAPE: + return UHDK_KEY_ESCAPE; + case VK_RETURN: + return UHDK_KEY_ENTER; + case VK_TAB: + return UHDK_KEY_TAB; + case VK_BACK: + return UHDK_KEY_BACKSPACE; + case VK_DELETE: + return UHDK_KEY_DELETE; + case VK_INSERT: + return UHDK_KEY_INSERT; + case VK_LEFT: + return UHDK_KEY_LEFT; + case VK_RIGHT: + return UHDK_KEY_RIGHT; + case VK_UP: + return UHDK_KEY_UP; + case VK_DOWN: + return UHDK_KEY_DOWN; + case VK_HOME: + return UHDK_KEY_HOME; + case VK_END: + return UHDK_KEY_END; + case VK_PRIOR: + return UHDK_KEY_PAGE_UP; + case VK_NEXT: + return UHDK_KEY_PAGE_DOWN; + default: + break; + } + if (vk >= VK_F1 && vk <= VK_F24) + return UHDK_KEY_F1 + (uint32_t)(vk - VK_F1); + uint32_t ch = MapVirtualKeyW((UINT)vk, MAPVK_VK_TO_CHAR) & 0x7fffffff; + if (ch >= 'A' && ch <= 'Z') + ch += 'a' - 'A'; + return ch; +} +static uint32_t physicalKey(LPARAM param) { + static const uint8_t keys[128] = { + [1] = 41, [2] = 30, [3] = 31, [4] = 32, [5] = 33, [6] = 34, [7] = 35, [8] = 36, + [9] = 37, [10] = 38, [11] = 39, [12] = 45, [13] = 46, [14] = 42, [15] = 43, [16] = 20, + [17] = 26, [18] = 8, [19] = 21, [20] = 23, [21] = 28, [22] = 24, [23] = 12, [24] = 18, + [25] = 19, [26] = 47, [27] = 48, [28] = 40, [29] = 224, [30] = 4, [31] = 22, [32] = 7, + [33] = 9, [34] = 10, [35] = 11, [36] = 13, [37] = 14, [38] = 15, [39] = 51, [40] = 52, + [41] = 53, [42] = 225, [43] = 49, [44] = 29, [45] = 27, [46] = 6, [47] = 25, [48] = 5, + [49] = 17, [50] = 16, [51] = 54, [52] = 55, [53] = 56, [54] = 229, [55] = 85, [56] = 226, + [57] = 44, [58] = 57, [59] = 58, [60] = 59, [61] = 60, [62] = 61, [63] = 62, [64] = 63, + [65] = 64, [66] = 65, [67] = 66, [68] = 67, [69] = 83, [70] = 71, [71] = 95, [72] = 96, + [73] = 97, [74] = 86, [75] = 92, [76] = 93, [77] = 94, [78] = 87, [79] = 89, [80] = 90, + [81] = 91, [82] = 98, [83] = 99, [86] = 100, [87] = 68, [88] = 69}; + unsigned scan = (unsigned)(param >> 16) & 127; + if (param & (1 << 24)) + switch (scan) { + case 28: + return 88; + case 29: + return 228; + case 53: + return 84; + case 55: + return 70; + case 56: + return 230; + case 71: + return 74; + case 72: + return 82; + case 73: + return 75; + case 75: + return 80; + case 77: + return 79; + case 79: + return 77; + case 80: + return 81; + case 81: + return 78; + case 82: + return 73; + case 83: + return 76; + case 91: + return 227; + case 92: + return 231; + case 93: + return 101; + } + return keys[scan]; +} +static uint32_t editKey(WPARAM key, uint32_t mods) { + BOOL ctrl = !!(mods & UHDK_MOD_CONTROL); + switch (key) { + case VK_BACK: + return UHDK_EDIT_DELETE_BACKWARD; + case VK_DELETE: + return UHDK_EDIT_DELETE_FORWARD; + case VK_LEFT: + return ctrl ? UHDK_EDIT_WORD_LEFT : UHDK_EDIT_MOVE_LEFT; + case VK_RIGHT: + return ctrl ? UHDK_EDIT_WORD_RIGHT : UHDK_EDIT_MOVE_RIGHT; + case VK_UP: + return UHDK_EDIT_MOVE_UP; + case VK_DOWN: + return UHDK_EDIT_MOVE_DOWN; + case VK_HOME: + return ctrl ? UHDK_EDIT_DOCUMENT_START : UHDK_EDIT_LINE_START; + case VK_END: + return ctrl ? UHDK_EDIT_DOCUMENT_END : UHDK_EDIT_LINE_END; + default: + return 0; + } +} +static void imePosition(Window *w) { + HIMC ime = ImmGetContext(w->hwnd); + if (!ime) + return; + POINT point = {(LONG)(w->text_state.caret.x * w->scale), + (LONG)((w->text_state.caret.y + w->text_state.caret.height) * w->scale)}; + COMPOSITIONFORM composition = {0}; + composition.dwStyle = CFS_POINT; + composition.ptCurrentPos = point; + CANDIDATEFORM candidate = {0}; + candidate.dwStyle = CFS_CANDIDATEPOS; + candidate.ptCurrentPos = point; + ImmSetCompositionWindow(ime, &composition); + ImmSetCandidateWindow(ime, &candidate); + ImmReleaseContext(w->hwnd, ime); +} +static void composition(Window *w, LPARAM flags) { + HIMC ime = ImmGetContext(w->hwnd); + if (!ime) + return; + DWORD kinds[] = {GCS_RESULTSTR, GCS_COMPSTR}; + for (unsigned i = 0; i < 2; ++i) + if (flags & kinds[i]) { + LONG n = ImmGetCompositionStringW(ime, kinds[i], NULL, 0); + if (n < 0) + continue; + WCHAR *s = calloc((size_t)n + 2, 1); + if (!s) + continue; + ImmGetCompositionStringW(ime, kinds[i], s, (DWORD)n); + int cursor = i ? ImmGetCompositionStringW(ime, GCS_CURSORPOS, NULL, 0) : -1; + textEvent(w, i ? UHDK_EVENT_TEXT_PREEDIT : UHDK_EVENT_TEXT_COMMIT, s, n / 2, cursor); + free(s); + } + ImmReleaseContext(w->hwnd, ime); +} +static void dropFiles(Window *w, HDROP drop) { + UINT count = DragQueryFileW(drop, UINT_MAX, NULL, 0); + uhdk_file **files = calloc(count ? count : 1, sizeof(*files)); + uint64_t used = 0; + if (files) + for (UINT i = 0; i < count; ++i) { + UINT n = DragQueryFileW(drop, i, NULL, 0); + WCHAR *p = calloc((size_t)n + 1, sizeof(WCHAR)); + if (!p) + break; + DragQueryFileW(drop, i, p, n + 1); + uhdk_file *file = + z_file_from_url(host.app, (uhdk_bytes){(uint8_t *)p, (uint64_t)n * 2}, NULL); + if (file) + files[used++] = file; + free(p); + } + POINT point; + DragQueryPoint(drop, &point); + if (used) { + uhdk_event e = {0}; + e.type = UHDK_EVENT_DROP_FILES; + e.window = w->handle; + e.data.drop_files.files = (uhdk_file_list){files, used}; + e.data.drop_files.position = (uhdk_point){point.x / w->scale, point.y / w->scale}; + z_emit(host.app, &e); + } + for (uint64_t i = 0; i < used; ++i) + uhdk_file_release(files[i]); + free(files); + DragFinish(drop); +} +static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + Window *w = (Window *)GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (msg == WM_NCCREATE) { + w = ((CREATESTRUCTW *)lp)->lpCreateParams; + if (w) { + w->hwnd = hwnd; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)w); + } + } + if (hwnd == host.dispatch) { + switch (msg) { + case Z_WAKE: + z_tick(host.app); + return 0; + case Z_DIALOG_MESSAGE: + showDialog(); + return 0; + case Z_POPUP_MESSAGE: + showPopup(); + return 0; + case WM_TIMER: + pollWatches(); + return 0; + } + } + if (!w) + return DefWindowProcW(hwnd, msg, wp, lp); + switch (msg) { + case WM_CLOSE: + emit(w, UHDK_EVENT_WINDOW_CLOSE_REQUESTED); + return 0; + case WM_PAINT: { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + EndPaint(hwnd, &ps); + emit(w, UHDK_EVENT_WINDOW_REDRAW); + return 0; + } + case WM_ERASEBKGND: + return 1; + case WM_SIZE: + case WM_SHOWWINDOW: + metrics(w); + break; + case WM_SETFOCUS: + metrics(w); + break; + case WM_KILLFOCUS: + w->high_surrogate = 0; + if (GetCapture() == hwnd) + ReleaseCapture(); + metrics(w); + break; + case WM_ACTIVATEAPP: { + uhdk_event e = {0}; + e.type = UHDK_EVENT_APP_ACTIVATION; + e.data.active = !!wp; + z_emit(host.app, &e); + break; + } + case WM_SETTINGCHANGE: + preferences(); + break; + case WM_DPICHANGED: { + RECT *r = (RECT *)lp; + w->scale = (float)HIWORD(wp) / 96.0f; + SetWindowPos(hwnd, NULL, r->left, r->top, r->right - r->left, r->bottom - r->top, + SWP_NOZORDER | SWP_NOACTIVATE); + metrics(w); + imePosition(w); + return 0; + } + case WM_GETMINMAXINFO: { + if (w->fullscreen) + break; + MINMAXINFO *m = (MINMAXINFO *)lp; + UINT dpi = GetDpiForWindow(hwnd); + float scale = dpi ? dpi / 96.0f : 1; + uhdk_size sizes[] = {w->min_size, w->max_size}; + for (unsigned i = 0; i < 2; ++i) { + RECT r = {0, 0, (LONG)(sizes[i].width * scale), (LONG)(sizes[i].height * scale)}; + AdjustWindowRectExForDpi(&r, (DWORD)GetWindowLongPtrW(hwnd, GWL_STYLE), + GetMenu(hwnd) != NULL, 0, dpi ? dpi : 96); + POINT *p = i ? &m->ptMaxTrackSize : &m->ptMinTrackSize; + if (sizes[i].width > 0) + p->x = r.right - r.left; + if (sizes[i].height > 0) + p->y = r.bottom - r.top; + } + return 0; + } + case WM_SETCURSOR: + if (LOWORD(lp) == HTCLIENT) { + SetCursor(w->cursor); + return TRUE; + } + break; + case WM_MOUSEMOVE: { + if (!w->tracking) { + TRACKMOUSEEVENT t = {sizeof(t), TME_LEAVE, hwnd, 0}; + TrackMouseEvent(&t); + w->tracking = TRUE; + emit(w, UHDK_EVENT_POINTER_ENTER); + } + uhdk_event e = {0}; + e.type = UHDK_EVENT_POINTER_MOVE; + e.window = w->handle; + e.data.pointer.position = + (uhdk_point){GET_X_LPARAM(lp) / w->scale, GET_Y_LPARAM(lp) / w->scale}; + e.data.pointer.delta = (uhdk_point){e.data.pointer.position.x - w->pointer.x, + e.data.pointer.position.y - w->pointer.y}; + e.data.pointer.modifiers = modifiers(); + w->pointer = e.data.pointer.position; + z_emit(host.app, &e); + return 0; + } + case WM_MOUSELEAVE: + w->tracking = FALSE; + emit(w, UHDK_EVENT_POINTER_LEAVE); + return 0; + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_LBUTTONDBLCLK: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_RBUTTONDBLCLK: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_MBUTTONDBLCLK: + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + case WM_XBUTTONDBLCLK: { + BOOL up = msg == WM_LBUTTONUP || msg == WM_RBUTTONUP || msg == WM_MBUTTONUP || + msg == WM_XBUTTONUP; + if (!up) + SetFocus(hwnd); + uhdk_event e = {0}; + e.type = UHDK_EVENT_POINTER_BUTTON; + e.window = w->handle; + e.data.pointer.position = + (uhdk_point){GET_X_LPARAM(lp) / w->scale, GET_Y_LPARAM(lp) / w->scale}; + e.data.pointer.button = msg <= WM_LBUTTONDBLCLK ? UHDK_BUTTON_PRIMARY + : msg <= WM_RBUTTONDBLCLK ? UHDK_BUTTON_SECONDARY + : msg <= WM_MBUTTONDBLCLK ? UHDK_BUTTON_MIDDLE + : HIWORD(wp) == XBUTTON1 ? UHDK_BUTTON_BACK + : UHDK_BUTTON_FORWARD; + e.data.pointer.pressed = !up; + e.data.pointer.modifiers = modifiers(); + e.data.pointer.click_count = (msg == WM_LBUTTONDBLCLK || msg == WM_RBUTTONDBLCLK || + msg == WM_MBUTTONDBLCLK || msg == WM_XBUTTONDBLCLK) + ? 2 + : 1; + z_emit(host.app, &e); + return 0; + } + case WM_CAPTURECHANGED: { + uhdk_event e = {0}; + e.type = UHDK_EVENT_POINTER_MODE_CHANGED; + e.window = w->handle; + e.data.pointer_mode = UHDK_POINTER_NORMAL; + z_emit(host.app, &e); + return 0; + } + case WM_CANCELMODE: + if (GetCapture() == hwnd) + ReleaseCapture(); + break; + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: { + POINT p = {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)}; + ScreenToClient(hwnd, &p); + uhdk_event e = {0}; + e.type = UHDK_EVENT_SCROLL; + e.window = w->handle; + e.data.scroll.position = (uhdk_point){p.x / w->scale, p.y / w->scale}; + float amount = (float)GET_WHEEL_DELTA_WPARAM(wp) / WHEEL_DELTA; + UINT lines = 3; + SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &lines, 0); + if (lines == WHEEL_PAGESCROLL) + lines = 20; + if (msg == WM_MOUSEHWHEEL) + e.data.scroll.delta.x = amount * lines; + else + e.data.scroll.delta.y = -amount * lines; + e.data.scroll.unit = UHDK_SCROLL_LINES; + e.data.scroll.modifiers = modifiers(); + z_emit(host.app, &e); + return 0; + } + case WM_KEYDOWN: + case WM_KEYUP: + case WM_SYSKEYDOWN: + case WM_SYSKEYUP: { + BOOL down = msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN; + uint32_t mods = modifiers(); + uint32_t key = logicalKey(wp); + if (down) { + w->suppress_char = FALSE; + const uhdk_menu_item *item = + shortcut(w->menu_source ? w->menu_source : host.menu, key, mods & 15); + if (item) { + w->suppress_char = TRUE; + menuEvent(w, item); + return 0; + } + } + uhdk_event e = {0}; + e.type = UHDK_EVENT_KEY; + e.window = w->handle; + e.input_id = w->input_id = ++host.input_id; + e.data.key.physical_key = physicalKey(lp); + e.data.key.logical_key = key; + e.data.key.modifiers = mods; + e.data.key.pressed = down; + e.data.key.repeat = down && !!(lp & (1u << 30)); + z_emit(host.app, &e); + if (down && w->text_enabled && !w->composing) { + uint32_t command = editKey(wp, mods); + if (command) + editEvent(w, command); + } + break; + } + case WM_CHAR: + case WM_UNICHAR: { + if (msg == WM_UNICHAR && wp == UNICODE_NOCHAR) + return TRUE; + if (!w->text_enabled || w->composing || w->suppress_char) + return 0; + if (wp == '\r') { + editEvent(w, UHDK_EDIT_NEWLINE); + return 0; + } + if (wp == '\t') { + editEvent(w, UHDK_EDIT_TAB); + return 0; + } + if (wp < 32 || wp == 127) + return 0; + WCHAR chars[2]; + int count = 1; + if (msg == WM_UNICHAR && wp > 0xffff) { + if (wp > 0x10ffff) + return 0; + chars[0] = (WCHAR)(0xd800 + ((wp - 0x10000) >> 10)); + chars[1] = (WCHAR)(0xdc00 + ((wp - 0x10000) & 1023)); + count = 2; + } else if (wp >= 0xd800 && wp <= 0xdbff) { + w->high_surrogate = (WCHAR)wp; + return 0; + } else if (wp >= 0xdc00 && wp <= 0xdfff && w->high_surrogate) { + chars[0] = w->high_surrogate; + chars[1] = (WCHAR)wp; + count = 2; + } else + chars[0] = (WCHAR)wp; + w->high_surrogate = 0; + textEvent(w, UHDK_EVENT_TEXT_COMMIT, chars, count, -1); + return 0; + } + case WM_IME_SETCONTEXT: + lp &= ~ISC_SHOWUICOMPOSITIONWINDOW; + break; + case WM_IME_STARTCOMPOSITION: + if (w->text_enabled) { + w->composing = TRUE; + imePosition(w); + return 0; + } + break; + case WM_IME_COMPOSITION: + if (w->text_enabled) { + composition(w, lp); + return 0; + } + break; + case WM_IME_ENDCOMPOSITION: + w->composing = FALSE; + emit(w, UHDK_EVENT_TEXT_COMPOSITION_END); + return 0; + case WM_DROPFILES: + dropFiles(w, (HDROP)wp); + return 0; + case WM_MENUCOMMAND: { + MENUITEMINFOW item = {0}; + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW((HMENU)lp, (UINT)wp, TRUE, &item)) { + host.popup_selected = TRUE; + menuEvent(w, (const uhdk_menu_item *)item.dwItemData); + } + return 0; + } + } + return DefWindowProcW(hwnd, msg, wp, lp); +} + +/* Native file references retain UTF-16 verbatim. Shared Zig callbacks separately + * create the WTF-8 path used by std.Io, including unpaired UTF-16 surrogates. */ +uhdk_status z_native_file_init(uhdk_file *file, uhdk_bytes path, uhdk_text uri, void *grant) { + (void)grant; + WCHAR *name = NULL; + if (path.len) { + if (path.len % 2 || path.len > 65534) + return UHDK_STATUS_INVALID_ARGUMENT; + name = calloc((size_t)path.len / 2 + 1, sizeof(WCHAR)); + if (!name) + return UHDK_STATUS_OUT_OF_MEMORY; + memcpy(name, path.ptr, (size_t)path.len); + for (size_t i = 0; i < path.len / 2; i++) + if (!name[i]) { + free(name); + return UHDK_STATUS_INVALID_ARGUMENT; + } + } else { + WCHAR *url = wide(uri); + if (!url) + return UHDK_STATUS_INVALID_ARGUMENT; + if (_wcsnicmp(url, L"file:", 5)) { + free(url); + return UHDK_STATUS_INVALID_ARGUMENT; + } + DWORD capacity = 32768; + name = calloc(capacity, sizeof(WCHAR)); + if (!name) { + free(url); + return UHDK_STATUS_OUT_OF_MEMORY; + } + HRESULT hr = PathCreateFromUrlW(url, name, &capacity, 0); + free(url); + if (FAILED(hr)) { + free(name); + return UHDK_STATUS_INVALID_ARGUMENT; + } + } + size_t n = wcslen(name); + BOOL drive = n >= 3 && + ((name[0] >= L'A' && name[0] <= L'Z') || (name[0] >= L'a' && name[0] <= L'z')) && + name[1] == L':' && (name[2] == L'\\' || name[2] == L'/'); + BOOL unc = n >= 3 && name[0] == L'\\' && name[1] == L'\\'; + if (!drive && !unc) { + free(name); + return UHDK_STATUS_INVALID_ARGUMENT; + } + DWORD uri_capacity = 3 * 32768; + WCHAR *url = calloc(uri_capacity, sizeof(WCHAR)); + if (!url) { + free(name); + return UHDK_STATUS_OUT_OF_MEMORY; + } + HRESULT hr = UrlCreateFromPathW(name, url, &uri_capacity, 0); + uint64_t uri_len = 0, display_len = 0; + /* A native filename may contain unpaired surrogates. Never return a URI + * that silently replaces those units and addresses a different file. */ + BOOL unicode_name = + WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, name, (int)n, NULL, 0, NULL, NULL) != 0; + char *url8 = FAILED(hr) || !unicode_name ? NULL : utf8(url, (int)wcslen(url), &uri_len); + const WCHAR *leaf = name; + for (const WCHAR *p = name; *p; p++) + if ((*p == L'\\' || *p == L'/') && p[1]) + leaf = p + 1; + char *display = utf8(leaf, (int)wcslen(leaf), &display_len); + uhdk_status status = + display ? z_file_init(file, (uhdk_bytes){(uint8_t *)name, n * 2}, + (uhdk_text){url8, uri_len}, (uhdk_text){display, display_len}, NULL) + : UHDK_STATUS_OS_ERROR; + free(display); + free(url8); + free(url); + free(name); + return status; +} + +static PROC glProc(const char *name) { + PROC p = wglGetProcAddress(name); + if (!p || p == (PROC)(uintptr_t)1 || p == (PROC)(uintptr_t)2 || p == (PROC)(uintptr_t)3 || + p == (PROC)(intptr_t)-1) + p = GetProcAddress(GetModuleHandleW(L"opengl32.dll"), name); + return p; +} +static uhdk_status createGraphics(Window *w, void *handle) { + if (!(w->flags & UHDK_WINDOW_OPENGL)) + return UHDK_STATUS_INVALID_ARGUMENT; + Graphics *g = calloc(1, sizeof(*g)); + if (!g) + return UHDK_STATUS_OUT_OF_MEMORY; + g->window = w->hwnd; + g->dc = GetDC(w->hwnd); + if (!g->dc) { + free(g); + return UHDK_STATUS_OS_ERROR; + } + PIXELFORMATDESCRIPTOR pfd = {0}; + pfd.nSize = sizeof(pfd); + pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pfd.iPixelType = PFD_TYPE_RGBA; + pfd.cColorBits = 32; + pfd.cAlphaBits = 8; + if (!GetPixelFormat(g->dc)) { + int format = ChoosePixelFormat(g->dc, &pfd); + if (!format || !SetPixelFormat(g->dc, format, &pfd)) + goto failed; + } + PIXELFORMATDESCRIPTOR actual; + if (!DescribePixelFormat(g->dc, GetPixelFormat(g->dc), sizeof(actual), &actual) || + actual.iPixelType != PFD_TYPE_RGBA || actual.cColorBits < 24 || actual.cAlphaBits < 8 || + !(actual.dwFlags & PFD_DOUBLEBUFFER)) + goto failed; + HGLRC previous = wglGetCurrentContext(); + HDC previous_dc = wglGetCurrentDC(); + HGLRC temporary = wglCreateContext(g->dc); + if (!temporary) + goto failed; + if (!wglMakeCurrent(g->dc, temporary)) { + wglDeleteContext(temporary); + goto failed; + } + typedef HGLRC(WINAPI * CreateContext)(HDC, HGLRC, const int *); + CreateContext create = (CreateContext)glProc("wglCreateContextAttribsARB"); + const int attributes[] = {0x2091, 3, 0x2092, 3, 0x9126, 1, 0}; + if (create) + g->rc = create(g->dc, NULL, attributes); + wglMakeCurrent(NULL, NULL); + wglDeleteContext(temporary); + if (!g->rc) { + wglMakeCurrent(previous_dc, previous); + goto failed; + } + if (!wglMakeCurrent(g->dc, g->rc)) { + wglMakeCurrent(previous_dc, previous); + goto failed; + } + PFNGLGETINTEGERVPROC get = (PFNGLGETINTEGERVPROC)glProc("glGetIntegerv"); + GLint major = 0, minor = 0, profile = 0; + if (get) { + get(GL_MAJOR_VERSION, &major); + get(GL_MINOR_VERSION, &minor); + get(GL_CONTEXT_PROFILE_MASK, &profile); + } + wglMakeCurrent(previous_dc, previous); + if (major < 3 || (major == 3 && minor < 3) || !(profile & GL_CONTEXT_CORE_PROFILE_BIT)) + goto failed; + g->major = (uint32_t)major; + g->minor = (uint32_t)minor; + InitializeCriticalSection(&g->lock); + z_object_set_native(handle, g); + return UHDK_STATUS_OK; +failed: + if (g->rc) + wglDeleteContext(g->rc); + ReleaseDC(g->window, g->dc); + free(g); + return UHDK_STATUS_UNSUPPORTED; +} +uhdk_status z_native_gl(void *native, uint32_t op, void *out) { + Graphics *g = native; + if (op == 4) + return wglMakeCurrent(NULL, NULL) ? UHDK_STATUS_OK : UHDK_STATUS_OS_ERROR; + if (!g) + return UHDK_STATUS_UNAVAILABLE; + if (op == 8) + return wglGetCurrentContext() != g->rc || wglMakeCurrent(NULL, NULL) ? UHDK_STATUS_OK + : UHDK_STATUS_OS_ERROR; + if (op == 1) { + EnterCriticalSection(&g->lock); + if (wglMakeCurrent(g->dc, g->rc)) + return UHDK_STATUS_OK; + LeaveCriticalSection(&g->lock); + return UHDK_STATUS_OS_ERROR; + } + if (op == 2) { + BOOL ok = wglMakeCurrent(NULL, NULL); + LeaveCriticalSection(&g->lock); + return ok ? UHDK_STATUS_OK : UHDK_STATUS_OS_ERROR; + } + if (op == 3) + return wglMakeCurrent(g->dc, g->rc) ? UHDK_STATUS_OK : UHDK_STATUS_OS_ERROR; + if (wglGetCurrentContext() != g->rc) + return UHDK_STATUS_UNAVAILABLE; + if (op == 5) { + ((uint32_t *)out)[0] = g->major; + ((uint32_t *)out)[1] = g->minor; + return UHDK_STATUS_OK; + } + if (op == 6) + return SwapBuffers(g->dc) ? UHDK_STATUS_OK : UHDK_STATUS_OS_ERROR; + if (op == 7) { + typedef BOOL(WINAPI * SwapInterval)(int); + SwapInterval set = (SwapInterval)glProc("wglSwapIntervalEXT"); + return !set ? UHDK_STATUS_UNSUPPORTED + : set(*(int32_t *)out) ? UHDK_STATUS_OK + : UHDK_STATUS_OS_ERROR; + } + return UHDK_STATUS_INVALID_ARGUMENT; +} +uhdk_status z_native_gl_proc(void *native, uhdk_text name, uhdk_gl_proc *out) { + Graphics *g = native; + if (!g || wglGetCurrentContext() != g->rc) + return UHDK_STATUS_UNAVAILABLE; + if (name.len > 255) + return UHDK_STATUS_INVALID_ARGUMENT; + char symbol[256]; + memcpy(symbol, name.ptr, (size_t)name.len); + symbol[name.len] = 0; + *out = (uhdk_gl_proc)glProc(symbol); + return *out ? UHDK_STATUS_OK : UHDK_STATUS_NOT_FOUND; +} +void z_native_handles(void *native, uhdk_native_window *out) { + Window *w = native; + out->backend = UHDK_BACKEND_WINDOWS; + out->primary = w->hwnd; + out->secondary = NULL; + out->value = 0; +} + +static HMENU buildMenu(uhdk_menu *source, BOOL popup) { + HMENU menu = popup ? CreatePopupMenu() : CreateMenu(); + if (!menu) + return NULL; + MENUINFO info = {0}; + info.cbSize = sizeof(info); + info.fMask = MIM_STYLE; + info.dwStyle = MNS_NOTIFYBYPOS; + SetMenuInfo(menu, &info); + uint64_t count; + const uhdk_menu_item *items = z_menu_items(source, &count); + for (uint64_t i = 0; i < count; i++) { + const uhdk_menu_item *item = &items[i]; + WCHAR *label = wide(item->label); + MENUITEMINFOW native = {0}; + native.cbSize = sizeof(native); + native.fMask = MIIM_FTYPE | MIIM_STATE | MIIM_DATA | MIIM_STRING; + native.fType = (item->flags & UHDK_MENU_SEPARATOR) ? MFT_SEPARATOR : MFT_STRING; + native.fState = ((item->flags & UHDK_MENU_DISABLED) ? MFS_DISABLED : 0) | + ((item->flags & UHDK_MENU_CHECKED) ? MFS_CHECKED : 0); + native.dwItemData = (ULONG_PTR)item; + native.dwTypeData = label; + if (item->submenu) { + native.fMask |= MIIM_SUBMENU; + native.hSubMenu = buildMenu(item->submenu, TRUE); + } + if (!label || (item->submenu && !native.hSubMenu) || + !InsertMenuItemW(menu, (UINT)i, TRUE, &native)) { + if (native.hSubMenu) + DestroyMenu(native.hSubMenu); + free(label); + DestroyMenu(menu); + return NULL; + } + free(label); + } + return menu; +} +static void refreshMenus(void) { + for (Window *w = host.windows; w; w = w->next) { + uhdk_menu *source = w->menu_source ? w->menu_source : host.menu; + HMENU replacement = source ? buildMenu(source, FALSE) : NULL; + SetMenu(w->hwnd, replacement); + if (w->menu) + DestroyMenu(w->menu); + w->menu = replacement; + DrawMenuBar(w->hwnd); + metrics(w); + } +} +static void showPopup(void) { + const z_command *cmd = host.pending_popup; + uint64_t id = host.pending_popup_id; + host.pending_popup = NULL; + host.pending_popup_id = 0; + if (!cmd) + return; + host.popup_id = id; + host.popup_selected = FALSE; + Window *w = windowOf(cmd->other); + HMENU menu = buildMenu(cmd->target, TRUE); + uhdk_status status = UHDK_STATUS_OS_ERROR; + if (menu && w) { + POINT p = {(LONG)(cmd->point.x * w->scale), (LONG)(cmd->point.y * w->scale)}; + ClientToScreen(w->hwnd, &p); + TrackPopupMenuEx(menu, TPM_LEFTALIGN | TPM_TOPALIGN, p.x, p.y, w->hwnd, NULL); + status = host.popup_selected ? UHDK_STATUS_OK : UHDK_STATUS_CANCELLED; + } + if (menu) + DestroyMenu(menu); + host.popup_id = 0; + z_complete(host.app, id, status, NULL); +} +static uhdk_file *shellFile(IShellItem *item) { + PWSTR path = NULL; + if (FAILED(IShellItem_GetDisplayName(item, SIGDN_FILESYSPATH, &path))) + return NULL; + uhdk_file *file = + z_file_from_url(host.app, (uhdk_bytes){(uint8_t *)path, wcslen(path) * 2}, NULL); + CoTaskMemFree(path); + return file; +} +static void showDialog(void) { + const z_command *cmd = host.pending_dialog; + uint64_t id = host.pending_dialog_id; + host.pending_dialog = NULL; + host.pending_dialog_id = 0; + if (!cmd) + return; + const uhdk_file_dialog_desc *d = &cmd->dialog; + IFileDialog *dialog = NULL; + IFileOpenDialog *open = NULL; + IFileSaveDialog *save = NULL; + HRESULT hr; + if (d->kind == UHDK_DIALOG_SAVE_FILE) { + hr = CoCreateInstance(&CLSID_FileSaveDialog, NULL, CLSCTX_INPROC_SERVER, + &IID_IFileSaveDialog, (void **)&save); + dialog = (IFileDialog *)save; + } else { + hr = CoCreateInstance(&CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, + &IID_IFileOpenDialog, (void **)&open); + dialog = (IFileDialog *)open; + } + if (FAILED(hr)) { + z_complete(host.app, id, UHDK_STATUS_OS_ERROR, NULL); + return; + } + host.dialog = dialog; + host.dialog_id = id; + DWORD options = FOS_FORCEFILESYSTEM | FOS_NOCHANGEDIR | FOS_PATHMUSTEXIST; + if (d->kind == UHDK_DIALOG_OPEN_DIRECTORY) + options |= FOS_PICKFOLDERS; + else if (d->kind == UHDK_DIALOG_SAVE_FILE) + options |= FOS_OVERWRITEPROMPT; + else + options |= FOS_FILEMUSTEXIST; + if (d->kind == UHDK_DIALOG_OPEN_FILES) + options |= FOS_ALLOWMULTISELECT; + IFileDialog_SetOptions(dialog, options); + WCHAR *title = wide(d->title), *label = wide(d->accept_label), + *suggestion = wide(d->suggested_name); + if (title && *title) + IFileDialog_SetTitle(dialog, title); + if (label && *label) + IFileDialog_SetOkButtonLabel(dialog, label); + if (suggestion && *suggestion) + IFileDialog_SetFileName(dialog, suggestion); + if (d->initial_directory) { + WCHAR *path = filePath(d->initial_directory); + IShellItem *folder = NULL; + if (path && + SUCCEEDED(SHCreateItemFromParsingName(path, NULL, &IID_IShellItem, (void **)&folder))) { + IFileDialog_SetFolder(dialog, folder); + IShellItem_Release(folder); + } + free(path); + } + COMDLG_FILTERSPEC *filters = + calloc(d->filters_len ? (size_t)d->filters_len : 1, sizeof(*filters)); + BOOL filter_ok = filters != NULL; + if (filters) + for (uint64_t i = 0; i < d->filters_len; i++) { + filters[i].pszName = wide(d->filters[i].label); + uhdk_text_list extensions = d->filters[i].extensions; + size_t capacity = 2; + for (uint64_t j = 0; j < extensions.len; j++) + capacity += (size_t)extensions.ptr[j].len + 3; + WCHAR *pattern = calloc(capacity, sizeof(WCHAR)); + filters[i].pszSpec = pattern; + if (!filters[i].pszName || !pattern) { + filter_ok = FALSE; + break; + } + if (!extensions.len) + wcscpy(pattern, L"*"); + for (uint64_t j = 0; j < extensions.len; j++) { + WCHAR *ext = wide(extensions.ptr[j]); + if (!ext) { + filter_ok = FALSE; + break; + } + if (j) + wcscat(pattern, L";"); + wcscat(pattern, L"*."); + wcscat(pattern, ext); + free(ext); + } + } + if (filter_ok && d->filters_len) + hr = IFileDialog_SetFileTypes(dialog, (UINT)d->filters_len, filters); + if (!filter_ok) + hr = E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + hr = IFileDialog_Show(dialog, d->parent ? windowOf(d->parent)->hwnd : NULL); + uhdk_file **files = NULL; + uint64_t count = 0; + uhdk_status status = hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) ? UHDK_STATUS_CANCELLED + : FAILED(hr) ? UHDK_STATUS_OS_ERROR + : UHDK_STATUS_OK; + if (status == UHDK_STATUS_OK && open) { + IShellItemArray *items = NULL; + hr = IFileOpenDialog_GetResults(open, &items); + if (SUCCEEDED(hr)) { + DWORD n = 0; + IShellItemArray_GetCount(items, &n); + files = calloc(n ? n : 1, sizeof(*files)); + if (files) + for (DWORD i = 0; i < n; i++) { + IShellItem *item = NULL; + if (SUCCEEDED(IShellItemArray_GetItemAt(items, i, &item))) { + uhdk_file *file = shellFile(item); + IShellItem_Release(item); + if (file) + files[count++] = file; + else { + status = UHDK_STATUS_OS_ERROR; + break; + } + } else { + status = UHDK_STATUS_OS_ERROR; + break; + } + } + else + status = UHDK_STATUS_OUT_OF_MEMORY; + IShellItemArray_Release(items); + } else + status = UHDK_STATUS_OS_ERROR; + } else if (status == UHDK_STATUS_OK) { + IShellItem *item = NULL; + files = calloc(1, sizeof(*files)); + if (files && SUCCEEDED(IFileDialog_GetResult(dialog, &item))) { + files[0] = shellFile(item); + IShellItem_Release(item); + if (files[0]) + count = 1; + else + status = UHDK_STATUS_OS_ERROR; + } else + status = files ? UHDK_STATUS_OS_ERROR : UHDK_STATUS_OUT_OF_MEMORY; + } + uhdk_request_result result = {0}; + result.value.files = (uhdk_file_list){files, count}; + host.dialog = NULL; + host.dialog_id = 0; + IFileDialog_Release(dialog); + free(title); + free(label); + free(suggestion); + if (filters) { + for (uint64_t i = 0; i < d->filters_len; i++) { + free((void *)filters[i].pszName); + free((void *)filters[i].pszSpec); + } + free(filters); + } + z_complete(host.app, id, status, &result); + for (uint64_t i = 0; i < count; i++) + uhdk_file_release(files[i]); + free(files); +} + +static BOOL armWatch(Watch *watch) { + ResetEvent(watch->overlapped.hEvent); + watch->pending = ReadDirectoryChangesW( + watch->directory, watch->buffer, sizeof(watch->buffer), FALSE, + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_ATTRIBUTES, + NULL, &watch->overlapped, NULL); + return watch->pending; +} +static uhdk_status createWatch(uhdk_file *root, uhdk_watch *handle) { + WCHAR *path = filePath(root); + if (!path) + return UHDK_STATUS_OUT_OF_MEMORY; + Watch *watch = calloc(1, sizeof(*watch)); + if (!watch) { + free(path); + return UHDK_STATUS_OUT_OF_MEMORY; + } + DWORD attributes = GetFileAttributesW(path); + if (attributes == INVALID_FILE_ATTRIBUTES) { + free(path); + free(watch); + return osError(); + } + if (!(attributes & FILE_ATTRIBUTE_DIRECTORY)) { + WCHAR *slash = wcsrchr(path, L'\\'); + WCHAR *other = wcsrchr(path, L'/'); + if (other && (!slash || other > slash)) + slash = other; + if (!slash) { + free(path); + free(watch); + return UHDK_STATUS_INVALID_ARGUMENT; + } + watch->leaf = _wcsdup(slash + 1); + if (!watch->leaf) { + free(path); + free(watch); + return UHDK_STATUS_OUT_OF_MEMORY; + } + slash[1] = 0; + } + watch->directory = CreateFileW( + path, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); + free(path); + watch->overlapped.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); + watch->handle = handle; + if (watch->directory == INVALID_HANDLE_VALUE || !watch->overlapped.hEvent || !armWatch(watch)) { + uhdk_status status = osError(); + if (watch->directory != INVALID_HANDLE_VALUE) + CloseHandle(watch->directory); + if (watch->overlapped.hEvent) + CloseHandle(watch->overlapped.hEvent); + free(watch->leaf); + free(watch); + return status; + } + watch->next = host.watches; + host.watches = watch; + z_object_set_native(handle, watch); + return UHDK_STATUS_OK; +} +static void watchEvent(Watch *watch, uint32_t flags, uhdk_status status) { + uhdk_event e = {0}; + e.type = UHDK_EVENT_WATCH; + e.data.watch.watch = watch->handle; + e.data.watch.flags = flags; + e.data.watch.status = status; + z_emit(host.app, &e); +} +static void pollWatches(void) { + for (Watch *w = host.watches; w; w = w->next) { + if (!w->pending) + continue; + DWORD size = 0; + if (!GetOverlappedResult(w->directory, &w->overlapped, &size, FALSE)) { + DWORD error = GetLastError(); + if (error == ERROR_IO_INCOMPLETE) + continue; + w->pending = FALSE; + if (error == ERROR_NOTIFY_ENUM_DIR) { + watchEvent(w, UHDK_CHANGE_RESCAN_REQUIRED, UHDK_STATUS_OK); + if (!armWatch(w)) + watchEvent(w, UHDK_CHANGE_ROOT_INVALIDATED, osError()); + } else + watchEvent(w, UHDK_CHANGE_ROOT_INVALIDATED, UHDK_STATUS_OS_ERROR); + continue; + } + w->pending = FALSE; + uint32_t flags = 0; + if (!size) + flags = UHDK_CHANGE_RESCAN_REQUIRED; + size_t offset = 0; + while (offset + offsetof(FILE_NOTIFY_INFORMATION, FileName) <= size) { + FILE_NOTIFY_INFORMATION *info = + (FILE_NOTIFY_INFORMATION *)((uint8_t *)w->buffer + offset); + if (info->FileNameLength > size - offset - offsetof(FILE_NOTIFY_INFORMATION, FileName)) + break; + BOOL match = !w->leaf || + (wcslen(w->leaf) * 2 == info->FileNameLength && + CompareStringOrdinal(w->leaf, -1, info->FileName, + (int)info->FileNameLength / 2, TRUE) == CSTR_EQUAL); + if (match) + switch (info->Action) { + case FILE_ACTION_ADDED: + flags |= UHDK_CHANGE_CREATED; + break; + case FILE_ACTION_REMOVED: + flags |= UHDK_CHANGE_REMOVED; + break; + case FILE_ACTION_MODIFIED: + flags |= UHDK_CHANGE_CONTENT | UHDK_CHANGE_METADATA; + break; + case FILE_ACTION_RENAMED_OLD_NAME: + flags |= UHDK_CHANGE_RENAMED | UHDK_CHANGE_REMOVED; + break; + case FILE_ACTION_RENAMED_NEW_NAME: + flags |= UHDK_CHANGE_RENAMED | UHDK_CHANGE_CREATED; + break; + } + if (!info->NextEntryOffset) + break; + if (info->NextEntryOffset > size - offset) + break; + offset += info->NextEntryOffset; + } + if (flags) + watchEvent(w, flags, UHDK_STATUS_OK); + if (!armWatch(w)) + watchEvent(w, UHDK_CHANGE_ROOT_INVALIDATED, osError()); + } +} + +int z_native_is_main_thread(void) { + LONG current = (LONG)GetCurrentThreadId(); + /* Windows has no process-main-thread query. The first app_create caller + * establishes the platform thread, which owns all subsequent lifecycle work. */ + LONG previous = InterlockedCompareExchange(&main_thread, current, 0); + return !previous || previous == current; +} +uhdk_status z_native_start(uhdk_app *app, uhdk_text name) { + (void)name; + memset(&host, 0, sizeof(host)); + host.app = app; + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (FAILED(hr)) + return UHDK_STATUS_OS_ERROR; + host.com_initialized = TRUE; + /* Thread-local policy also works when an embedding host already set its + * process policy. Windows created here inherit Per-Monitor V2 awareness. */ + host.previous_dpi = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + if (!host.previous_dpi) { + z_native_finish(); + return UHDK_STATUS_UNSUPPORTED; + } + WNDCLASSEXW cls = {0}; + cls.cbSize = sizeof(cls); + cls.style = CS_OWNDC | CS_DBLCLKS; + cls.lpfnWndProc = windowProc; + cls.hInstance = GetModuleHandleW(NULL); + cls.hCursor = LoadCursorW(NULL, IDC_ARROW); + cls.lpszClassName = window_class; + if (!RegisterClassExW(&cls) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { + uhdk_status status = osError(); + z_native_finish(); + return status; + } + host.dispatch = CreateWindowExW(0, window_class, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, + cls.hInstance, NULL); + if (!host.dispatch) { + z_native_finish(); + return osError(); + } + if (!SetTimer(host.dispatch, 1, 16, NULL)) { + z_native_finish(); + return osError(); + } + host.running = TRUE; + preferences(); + return UHDK_STATUS_OK; +} +void z_native_run(void) { + MSG message; + while (host.running) { + int result = GetMessageW(&message, NULL, 0, 0); + if (result <= 0) { + uhdk_event e = {0}; + e.type = UHDK_EVENT_QUIT_REQUESTED; + z_emit(host.app, &e); + if (result < 0) + break; + continue; + } + TranslateMessage(&message); + DispatchMessageW(&message); + } +} +void z_native_wake(void) { + if (host.dispatch) + PostMessageW(host.dispatch, Z_WAKE, 0, 0); +} +void z_native_again(void) { + z_native_wake(); +} +void z_native_stop(void) { + host.running = FALSE; + if (host.dialog) + IFileDialog_Close(host.dialog, HRESULT_FROM_WIN32(ERROR_CANCELLED)); + if (host.popup_id) + EndMenu(); +} +void z_native_finish(void) { + if (host.dispatch) { + KillTimer(host.dispatch, 1); + DestroyWindow(host.dispatch); + host.dispatch = NULL; + } + if (host.menu) { + uhdk_menu_release(host.menu); + host.menu = NULL; + } + if (host.icon) { + DestroyIcon(host.icon); + host.icon = NULL; + } + UnregisterClassW(window_class, GetModuleHandleW(NULL)); + if (host.previous_dpi) { + SetThreadDpiAwarenessContext(host.previous_dpi); + host.previous_dpi = NULL; + } + if (host.com_initialized) { + CoUninitialize(); + host.com_initialized = FALSE; + } +} +void *z_native_pool_push(void) { + return NULL; +} +void z_native_pool_pop(void *pool) { + (void)pool; +} +void z_native_cancel(uint64_t id) { + if (host.pending_dialog_id == id) { + host.pending_dialog = NULL; + host.pending_dialog_id = 0; + z_complete(host.app, id, UHDK_STATUS_CANCELLED, NULL); + } + if (host.pending_popup_id == id) { + host.pending_popup = NULL; + host.pending_popup_id = 0; + z_complete(host.app, id, UHDK_STATUS_CANCELLED, NULL); + } + if (host.dialog_id == id && host.dialog) + IFileDialog_Close(host.dialog, HRESULT_FROM_WIN32(ERROR_CANCELLED)); + if (host.popup_id == id) + EndMenu(); +} +void z_native_destroy(uint32_t kind, void *native) { + if (!native) + return; + if (kind == Z_KIND_GL) { + Graphics *g = native; + EnterCriticalSection(&g->lock); + wglDeleteContext(g->rc); + ReleaseDC(g->window, g->dc); + LeaveCriticalSection(&g->lock); + DeleteCriticalSection(&g->lock); + free(g); + } else if (kind == Z_KIND_WINDOW) { + Window *w = native; + Window **link = &host.windows; + while (*link && *link != w) + link = &(*link)->next; + if (*link) + *link = w->next; + if (host.dialog) + IFileDialog_Close(host.dialog, HRESULT_FROM_WIN32(ERROR_CANCELLED)); + if (host.popup_id) + EndMenu(); + SetMenu(w->hwnd, NULL); + if (w->menu) + DestroyMenu(w->menu); + SetWindowLongPtrW(w->hwnd, GWLP_USERDATA, 0); + DestroyWindow(w->hwnd); + if (w->menu_source) + uhdk_menu_release(w->menu_source); + free(w); + } else if (kind == Z_KIND_WATCH) { + Watch *w = native; + Watch **link = &host.watches; + while (*link && *link != w) + link = &(*link)->next; + if (*link) + *link = w->next; + if (w->pending) { + CancelIoEx(w->directory, &w->overlapped); + DWORD ignored; + GetOverlappedResult(w->directory, &w->overlapped, &ignored, TRUE); + } + CloseHandle(w->directory); + CloseHandle(w->overlapped.hEvent); + free(w->leaf); + free(w); + } +} +static uhdk_status clipboardWrite(uhdk_text text) { + if (text.len && memchr(text.ptr, 0, (size_t)text.len)) + return UHDK_STATUS_INVALID_ARGUMENT; + WCHAR *value = wide(text); + if (!value) + return UHDK_STATUS_OUT_OF_MEMORY; + size_t length = (wcslen(value) + 1) * sizeof(WCHAR); + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, length); + if (!memory) { + free(value); + return UHDK_STATUS_OUT_OF_MEMORY; + } + void *bytes = GlobalLock(memory); + if (!bytes) { + GlobalFree(memory); + free(value); + return UHDK_STATUS_OS_ERROR; + } + memcpy(bytes, value, length); + GlobalUnlock(memory); + free(value); + if (!OpenClipboard(host.dispatch)) { + GlobalFree(memory); + return UHDK_STATUS_BUSY; + } + BOOL ok = EmptyClipboard() && SetClipboardData(CF_UNICODETEXT, memory) != NULL; + CloseClipboard(); + if (!ok) + GlobalFree(memory); + return ok ? UHDK_STATUS_OK : UHDK_STATUS_OS_ERROR; +} +static uhdk_status clipboardRead(uint64_t id, uint64_t limit) { + if (!OpenClipboard(host.dispatch)) + return UHDK_STATUS_BUSY; + HGLOBAL memory = GetClipboardData(CF_UNICODETEXT); + if (!memory) { + CloseClipboard(); + return UHDK_STATUS_NOT_FOUND; + } + const WCHAR *value = GlobalLock(memory); + if (!value) { + CloseClipboard(); + return UHDK_STATUS_OS_ERROR; + } + size_t max = GlobalSize(memory) / 2, n = 0; + while (n < max && value[n]) + n++; + uint64_t len = 0; + char *text = n < max && n <= INT_MAX ? utf8(value, (int)n, &len) : NULL; + GlobalUnlock(memory); + CloseClipboard(); + if (!text) + return UHDK_STATUS_OS_ERROR; + if (len > limit) { + free(text); + return UHDK_STATUS_BUFFER_TOO_SMALL; + } + uhdk_request_result result = {0}; + result.value.text = (uhdk_text){text, len}; + z_complete(host.app, id, UHDK_STATUS_OK, &result); + free(text); + return Z_DEFERRED; +} +static uhdk_status setIcon(const uhdk_image *image) { + BITMAPV5HEADER header = {0}; + header.bV5Size = sizeof(header); + header.bV5Width = (LONG)image->width; + header.bV5Height = -(LONG)image->height; + header.bV5Planes = 1; + header.bV5BitCount = 32; + header.bV5Compression = BI_BITFIELDS; + header.bV5RedMask = 0x00ff0000; + header.bV5GreenMask = 0x0000ff00; + header.bV5BlueMask = 0x000000ff; + header.bV5AlphaMask = 0xff000000; + void *pixels = NULL; + HDC dc = GetDC(NULL); + HBITMAP color = CreateDIBSection(dc, (BITMAPINFO *)&header, DIB_RGB_COLORS, &pixels, NULL, 0); + ReleaseDC(NULL, dc); + if (!color) + return UHDK_STATUS_OS_ERROR; + for (uint32_t y = 0; y < image->height; y++) + for (uint32_t x = 0; x < image->width; x++) { + const uint8_t *p = image->pixels.ptr + y * image->stride_bytes + x * 4; + uint8_t *out = (uint8_t *)pixels + ((size_t)y * image->width + x) * 4; + out[0] = (uint8_t)((uint32_t)p[2] * p[3] / 255); + out[1] = (uint8_t)((uint32_t)p[1] * p[3] / 255); + out[2] = (uint8_t)((uint32_t)p[0] * p[3] / 255); + out[3] = p[3]; + } + HBITMAP mask = CreateBitmap((int)image->width, (int)image->height, 1, 1, NULL); + ICONINFO info = {0}; + info.fIcon = TRUE; + info.hbmColor = color; + info.hbmMask = mask; + HICON icon = mask ? CreateIconIndirect(&info) : NULL; + DeleteObject(color); + if (mask) + DeleteObject(mask); + if (!icon) + return UHDK_STATUS_OS_ERROR; + for (Window *w = host.windows; w; w = w->next) { + SendMessageW(w->hwnd, WM_SETICON, ICON_BIG, (LPARAM)icon); + SendMessageW(w->hwnd, WM_SETICON, ICON_SMALL, (LPARAM)icon); + } + if (host.icon) + DestroyIcon(host.icon); + host.icon = icon; + return UHDK_STATUS_OK; +} +uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t request, + uhdk_request_result *result) { + (void)app; + Window *w = + cmd->target && z_object_kind(cmd->target) == Z_KIND_WINDOW ? windowOf(cmd->target) : NULL; + switch (cmd->op) { + case Z_WINDOW: { + w = calloc(1, sizeof(*w)); + if (!w) + return UHDK_STATUS_OUT_OF_MEMORY; + w->handle = cmd->target; + w->flags = cmd->window.flags; + w->min_size = cmd->window.min_size; + w->max_size = cmd->window.max_size; + w->scale = 1; + w->cursor = LoadCursorW(NULL, IDC_ARROW); + DWORD style = cmd->window.flags & UHDK_WINDOW_DECORATED + ? WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX + : WS_POPUP; + if (cmd->window.flags & UHDK_WINDOW_RESIZABLE) + style |= WS_THICKFRAME | WS_MAXIMIZEBOX; + UINT dpi = GetDpiForSystem(); + float scale = (float)dpi / 96; + RECT r = {0, 0, (LONG)(cmd->window.content_size.width * scale), + (LONG)(cmd->window.content_size.height * scale)}; + AdjustWindowRectExForDpi(&r, style, FALSE, 0, dpi); + WCHAR *title = wide(cmd->window.title); + if (!title) { + free(w); + return UHDK_STATUS_OUT_OF_MEMORY; + } + HWND parent = cmd->window.parent ? windowOf(cmd->window.parent)->hwnd : NULL; + HWND hwnd = CreateWindowExW(0, window_class, title, style, CW_USEDEFAULT, CW_USEDEFAULT, + r.right - r.left, r.bottom - r.top, parent, NULL, + GetModuleHandleW(NULL), w); + free(title); + if (!hwnd) { + free(w); + return osError(); + } + ImmAssociateContextEx(hwnd, NULL, 0); + w->next = host.windows; + host.windows = w; + z_object_set_native(cmd->target, w); + result->value.window = cmd->target; + if (host.icon) { + SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)host.icon); + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)host.icon); + } + refreshMenus(); + metrics(w); + return UHDK_STATUS_OK; + } + case Z_TITLE: { + WCHAR *title = wide(cmd->text); + if (!title) + return UHDK_STATUS_OUT_OF_MEMORY; + BOOL ok = SetWindowTextW(w->hwnd, title); + free(title); + return ok ? UHDK_STATUS_OK : osError(); + } + case Z_SIZE: { + RECT r = {0, 0, (LONG)(cmd->size.width * w->scale), (LONG)(cmd->size.height * w->scale)}; + AdjustWindowRectExForDpi(&r, (DWORD)GetWindowLongPtrW(w->hwnd, GWL_STYLE), + GetMenu(w->hwnd) != NULL, 0, GetDpiForWindow(w->hwnd)); + if (!SetWindowPos(w->hwnd, NULL, 0, 0, r.right - r.left, r.bottom - r.top, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE)) + return osError(); + metrics(w); + break; + } + case Z_STATE: { + BOOL full = cmd->number == UHDK_WINDOW_FULLSCREEN; + if (full && !w->fullscreen) { + w->placement.length = sizeof(w->placement); + GetWindowPlacement(w->hwnd, &w->placement); + w->saved_style = GetWindowLongPtrW(w->hwnd, GWL_STYLE); + MONITORINFO m = {0}; + m.cbSize = sizeof(m); + GetMonitorInfoW(MonitorFromWindow(w->hwnd, MONITOR_DEFAULTTONEAREST), &m); + w->fullscreen = TRUE; + SetWindowLongPtrW(w->hwnd, GWL_STYLE, + (w->saved_style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP); + SetWindowPos(w->hwnd, HWND_TOP, m.rcMonitor.left, m.rcMonitor.top, + m.rcMonitor.right - m.rcMonitor.left, m.rcMonitor.bottom - m.rcMonitor.top, + SWP_FRAMECHANGED); + } else if (!full && w->fullscreen) { + w->fullscreen = FALSE; + SetWindowLongPtrW(w->hwnd, GWL_STYLE, w->saved_style); + SetWindowPlacement(w->hwnd, &w->placement); + SetWindowPos(w->hwnd, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } + if (!full) + ShowWindow(w->hwnd, cmd->number == UHDK_WINDOW_MINIMIZED ? SW_MINIMIZE + : cmd->number == UHDK_WINDOW_MAXIMIZED ? SW_MAXIMIZE + : SW_RESTORE); + metrics(w); + break; + } + case Z_VISIBLE: + ShowWindow(w->hwnd, cmd->number ? SW_SHOWNOACTIVATE : SW_HIDE); + metrics(w); + break; + case Z_FOCUS: + ShowWindow(w->hwnd, SW_SHOW); + SetForegroundWindow(w->hwnd); + SetFocus(w->hwnd); + metrics(w); + break; + case Z_GL: { + uhdk_status status = createGraphics(w, cmd->other); + if (status == UHDK_STATUS_OK) + result->value.gl_context = cmd->other; + return status; + } + case Z_CURSOR: { + LPCWSTR cursors[] = {IDC_ARROW, IDC_IBEAM, IDC_CROSS, IDC_HAND, + IDC_SIZEWE, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZENESW, + IDC_SIZEALL, IDC_NO, IDC_WAIT}; + w->cursor = + cmd->number == UHDK_CURSOR_HIDDEN ? NULL : LoadCursorW(NULL, cursors[cmd->number]); + SetCursor(w->cursor); + break; + } + case Z_POINTER: { + if (cmd->number == UHDK_POINTER_CAPTURE) { + SetCapture(w->hwnd); + if (GetCapture() != w->hwnd) + return UHDK_STATUS_OS_ERROR; + } else if (GetCapture() == w->hwnd) { + ReleaseCapture(); + break; + } + uhdk_event e = {0}; + e.type = UHDK_EVENT_POINTER_MODE_CHANGED; + e.window = w->handle; + e.data.pointer_mode = (uint32_t)cmd->number; + z_emit(host.app, &e); + break; + } + case Z_TEXT_ENABLE: + w->text_enabled = !!cmd->number; + ImmAssociateContextEx(w->hwnd, NULL, w->text_enabled ? IACE_DEFAULT : 0); + break; + case Z_TEXT_STATE: + if (cmd->text_state.acknowledged_input_id >= w->last_edit) { + w->text_state = cmd->text_state; + w->text_state.surrounding_text = (uhdk_text){0}; + imePosition(w); + } + break; + case Z_TEXT_CANCEL: { + HIMC ime = ImmGetContext(w->hwnd); + if (ime) { + ImmNotifyIME(ime, NI_COMPOSITIONSTR, CPS_CANCEL, 0); + ImmReleaseContext(w->hwnd, ime); + } + break; + } + case Z_DROP: + DragAcceptFiles(w->hwnd, !!cmd->number); + break; + case Z_CLIP_WRITE: + return clipboardWrite(cmd->text); + case Z_CLIP_READ: + return clipboardRead(request, cmd->limit); + case Z_DIALOG: + if (host.dialog || host.pending_dialog) + return UHDK_STATUS_BUSY; + host.pending_dialog = cmd; + host.pending_dialog_id = request; + if (!PostMessageW(host.dispatch, Z_DIALOG_MESSAGE, 0, 0)) { + host.pending_dialog = NULL; + host.pending_dialog_id = 0; + return osError(); + } + return Z_DEFERRED; + case Z_APP_MENU: + if (cmd->other) + uhdk_menu_retain(cmd->other); + { + uhdk_menu *old = host.menu; + host.menu = cmd->other; + refreshMenus(); + if (old) + uhdk_menu_release(old); + } + break; + case Z_WINDOW_MENU: + if (cmd->other) + uhdk_menu_retain(cmd->other); + { + uhdk_menu *old = w->menu_source; + w->menu_source = cmd->other; + refreshMenus(); + if (old) + uhdk_menu_release(old); + } + break; + case Z_MENU_FLAGS: + refreshMenus(); + break; + case Z_POPUP: + if (host.popup_id || host.pending_popup) + return UHDK_STATUS_BUSY; + host.pending_popup = cmd; + host.pending_popup_id = request; + if (!PostMessageW(host.dispatch, Z_POPUP_MESSAGE, 0, 0)) { + host.pending_popup = NULL; + host.pending_popup_id = 0; + return osError(); + } + return Z_DEFERRED; + case Z_ICON: + return setIcon(&cmd->image); + case Z_URL: { + WCHAR *url = wide(cmd->text); + if (!url) + return UHDK_STATUS_OUT_OF_MEMORY; + if (!UrlIsW(url, URLIS_URL)) { + free(url); + return UHDK_STATUS_INVALID_ARGUMENT; + } + INT_PTR status = (INT_PTR)ShellExecuteW(NULL, L"open", url, NULL, NULL, SW_SHOWNORMAL); + free(url); + return status > 32 ? UHDK_STATUS_OK : UHDK_STATUS_OS_ERROR; + } + case Z_WATCH: { + uhdk_status status = createWatch(cmd->target, cmd->other); + if (status == UHDK_STATUS_OK) + result->value.watch = cmd->other; + return status; + } + case Z_DESTROY: { + void *native = z_object_native(cmd->target); + z_object_set_native(cmd->target, NULL); + z_native_destroy(z_object_kind(cmd->target), native); + break; + } + case Z_REDRAW: + if (w) + InvalidateRect(w->hwnd, NULL, FALSE); + break; + default: + return UHDK_STATUS_INVALID_ARGUMENT; + } + return UHDK_STATUS_OK; +}