macos implementation.

This commit is contained in:
Peter Li 2026-09-18 02:16:42 -07:00
commit 0c8270fbef
32 changed files with 14320 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.zig-cache/
zig-out/
.DS_Store
zig-pkg/

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# Uhdk
A small macOS 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.
## 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
```
## Use from Zig
After cloning this directory's standalone mirror into `vendor/uhdk`, add it to
`build.zig.zon`:
```zig
.dependencies = .{
.uhdk = .{ .path = "vendor/uhdk" },
},
```
In your build, pass the application's target and optimization options:
```zig
const uhdk = b.dependency("uhdk", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("uhdk", uhdk.module("uhdk"));
```
`@import("uhdk")` provides translated public C declarations, including the bundled
OpenGL headers, and links the library and required system frameworks. The
`uhdk` artifact is also available through `uhdk.artifact("uhdk")` for C consumers
or custom bindings. Set `.shared = true` to build a dynamic library; applications
must arrange its runtime location/rpath.
Once a standalone mirror or archive is published, `zig fetch --save=uhdk <URL>`
can replace the local path dependency. No release URL has been assigned yet.
## C interface
- [uhdk.h](include/uhdk.h): desktop API, lifecycle, ownership, and threading contracts.
- [uhdk_opengl.h](include/uhdk_opengl.h): GLAD 2 OpenGL 3.3 core declarations and per-context loader table.
- [uhdk_native.h](include/uhdk_native.h): optional platform interoperability.
- [OPENGL-SOURCES.md](include/OPENGL-SOURCES.md): GLAD generation recipe and Khronos header provenance and licenses.
The public interface uses pointer-and-length text, `char` booleans, and `float`
coordinates. The native bridge is private; consumers do not need Objective-C.
The library includes the GLAD loader implementation. With a current context inside
`uhdk_gl_context_begin_frame` / `uhdk_gl_context_end_frame`, call
`gladLoadGLContextUserPtr` with a callback that resolves names using
`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.

65
build.zig Normal file
View File

@ -0,0 +1,65 @@
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.");
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(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
const native = b.addTranslateC(.{
.root_source_file = b.path("src/macos/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", .{});
const lib = b.addLibrary(.{
.name = "uhdk",
.linkage = if (shared) .dynamic else .static,
.root_module = module,
});
const install_lib = b.addInstallArtifact(lib, .{});
const install_headers = b.addInstallDirectory(.{
.source_dir = b.path("include"),
.install_dir = .header,
.install_subdir = "",
});
const lib_step = b.step("lib", "Build and install the Uhdk library and headers");
lib_step.dependOn(&install_lib.step);
lib_step.dependOn(&install_headers.step);
b.getInstallStep().dependOn(&install_lib.step);
b.getInstallStep().dependOn(&install_headers.step);
const unit = b.addTest(.{ .root_module = module });
const unit_run = b.addRunArtifact(unit);
b.step("test", "Run core library tests").dependOn(&unit_run.step);
const public_c = b.addTranslateC(.{
.root_source_file = b.path("include/uhdk_opengl.h"),
.target = target,
.optimize = optimize,
});
public_c.addIncludePath(b.path("include"));
const public_module = b.addModule("uhdk", .{
.root_source_file = public_c.getOutput(),
.target = target,
.optimize = optimize,
});
public_module.linkLibrary(lib);
}

8
build.zig.zon Normal file
View File

@ -0,0 +1,8 @@
.{
.name = .uhdk,
.fingerprint = 0xe0900359a69bf43d,
.version = "0.1.0",
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{ "build.zig", "build.zig.zon", "src", "include", "README.md" },
}

6132
include/GL/glcorearb.h Normal file

File diff suppressed because it is too large Load Diff

311
include/KHR/khrplatform.h Normal file
View File

@ -0,0 +1,311 @@
#ifndef __khrplatform_h_
#define __khrplatform_h_
/*
** Copyright (c) 2008-2018 The Khronos Group Inc.
**
** Permission is hereby granted, free of charge, to any person obtaining a
** copy of this software and/or associated documentation files (the
** "Materials"), to deal in the Materials without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Materials, and to
** permit persons to whom the Materials are furnished to do so, subject to
** the following conditions:
**
** The above copyright notice and this permission notice shall be included
** in all copies or substantial portions of the Materials.
**
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
/* Khronos platform-specific types and definitions.
*
* The master copy of khrplatform.h is maintained in the Khronos EGL
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
* The last semantic modification to khrplatform.h was at commit ID:
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
*
* Adopters may modify this file to suit their platform. Adopters are
* encouraged to submit platform specific modifications to the Khronos
* group so that they can be included in future versions of this file.
* Please submit changes by filing pull requests or issues on
* the EGL Registry repository linked above.
*
*
* See the Implementer's Guidelines for information about where this file
* should be located on your system and for more details of its use:
* http://www.khronos.org/registry/implementers_guide.pdf
*
* This file should be included as
* #include <KHR/khrplatform.h>
* by Khronos client API header files that use its types and defines.
*
* The types in khrplatform.h should only be used to define API-specific types.
*
* Types defined in khrplatform.h:
* khronos_int8_t signed 8 bit
* khronos_uint8_t unsigned 8 bit
* khronos_int16_t signed 16 bit
* khronos_uint16_t unsigned 16 bit
* khronos_int32_t signed 32 bit
* khronos_uint32_t unsigned 32 bit
* khronos_int64_t signed 64 bit
* khronos_uint64_t unsigned 64 bit
* khronos_intptr_t signed same number of bits as a pointer
* khronos_uintptr_t unsigned same number of bits as a pointer
* khronos_ssize_t signed size
* khronos_usize_t unsigned size
* khronos_float_t signed 32 bit floating point
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
* nanoseconds
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
* khronos_boolean_enum_t enumerated boolean type. This should
* only be used as a base type when a client API's boolean type is
* an enum. Client APIs which use an integer or other type for
* booleans cannot use this as the base type for their boolean.
*
* Tokens defined in khrplatform.h:
*
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
*
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
*
* Calling convention macros defined in this file:
* KHRONOS_APICALL
* KHRONOS_APIENTRY
* KHRONOS_APIATTRIBUTES
*
* These may be used in function prototypes as:
*
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
* int arg1,
* int arg2) KHRONOS_APIATTRIBUTES;
*/
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
# define KHRONOS_STATIC 1
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APICALL
*-------------------------------------------------------------------------
* This precedes the return type of the function in the function prototype.
*/
#if defined(KHRONOS_STATIC)
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
* header compatible with static linking. */
# define KHRONOS_APICALL
#elif defined(_WIN32)
# define KHRONOS_APICALL __declspec(dllimport)
#elif defined (__SYMBIAN32__)
# define KHRONOS_APICALL IMPORT_C
#elif defined(__ANDROID__)
# define KHRONOS_APICALL __attribute__((visibility("default")))
#else
# define KHRONOS_APICALL
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIENTRY
*-------------------------------------------------------------------------
* This follows the return type of the function and precedes the function
* name in the function prototype.
*/
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
/* Win32 but not WinCE */
# define KHRONOS_APIENTRY __stdcall
#else
# define KHRONOS_APIENTRY
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIATTRIBUTES
*-------------------------------------------------------------------------
* This follows the closing parenthesis of the function prototype arguments.
*/
#if defined (__ARMCC_2__)
#define KHRONOS_APIATTRIBUTES __softfp
#else
#define KHRONOS_APIATTRIBUTES
#endif
/*-------------------------------------------------------------------------
* basic type definitions
*-----------------------------------------------------------------------*/
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
/*
* Using <stdint.h>
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
/*
* To support platform where unsigned long cannot be used interchangeably with
* inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
* Ideally, we could just use (u)intptr_t everywhere, but this could result in
* ABI breakage if khronos_uintptr_t is changed from unsigned long to
* unsigned long long or similar (this results in different C++ name mangling).
* To avoid changes for existing platforms, we restrict usage of intptr_t to
* platforms where the size of a pointer is larger than the size of long.
*/
#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
#define KHRONOS_USE_INTPTR_T
#endif
#endif
#elif defined(__VMS ) || defined(__sgi)
/*
* Using <inttypes.h>
*/
#include <inttypes.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
/*
* Win32
*/
typedef __int32 khronos_int32_t;
typedef unsigned __int32 khronos_uint32_t;
typedef __int64 khronos_int64_t;
typedef unsigned __int64 khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__sun__) || defined(__digital__)
/*
* Sun or Digital
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#if defined(__arch64__) || defined(_LP64)
typedef long int khronos_int64_t;
typedef unsigned long int khronos_uint64_t;
#else
typedef long long int khronos_int64_t;
typedef unsigned long long int khronos_uint64_t;
#endif /* __arch64__ */
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif 0
/*
* Hypothetical platform with no float or int64 support
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#define KHRONOS_SUPPORT_INT64 0
#define KHRONOS_SUPPORT_FLOAT 0
#else
/*
* Generic fallback
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#endif
/*
* Types that are (so far) the same on all platforms
*/
typedef signed char khronos_int8_t;
typedef unsigned char khronos_uint8_t;
typedef signed short int khronos_int16_t;
typedef unsigned short int khronos_uint16_t;
/*
* Types that differ between LLP64 and LP64 architectures - in LLP64,
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
* to be the only LLP64 architecture in current use.
*/
#ifdef KHRONOS_USE_INTPTR_T
typedef intptr_t khronos_intptr_t;
typedef uintptr_t khronos_uintptr_t;
#elif defined(_WIN64)
typedef signed long long int khronos_intptr_t;
typedef unsigned long long int khronos_uintptr_t;
#else
typedef signed long int khronos_intptr_t;
typedef unsigned long int khronos_uintptr_t;
#endif
#if defined(_WIN64)
typedef signed long long int khronos_ssize_t;
typedef unsigned long long int khronos_usize_t;
#else
typedef signed long int khronos_ssize_t;
typedef unsigned long int khronos_usize_t;
#endif
#if KHRONOS_SUPPORT_FLOAT
/*
* Float type
*/
typedef float khronos_float_t;
#endif
#if KHRONOS_SUPPORT_INT64
/* Time types
*
* These types can be used to represent a time interval in nanoseconds or
* an absolute Unadjusted System Time. Unadjusted System Time is the number
* of nanoseconds since some arbitrary system event (e.g. since the last
* time the system booted). The Unadjusted System Time is an unsigned
* 64 bit value that wraps back to 0 every 584 years. Time intervals
* may be either signed or unsigned.
*/
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
typedef khronos_int64_t khronos_stime_nanoseconds_t;
#endif
/*
* Dummy value used to pad enum types to 32 bits.
*/
#ifndef KHRONOS_MAX_ENUM
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
#endif
/*
* Enumerated boolean type
*
* Values other than zero should be considered to be true. Therefore
* comparisons should not be made against KHRONOS_TRUE.
*/
typedef enum {
KHRONOS_FALSE = 0,
KHRONOS_TRUE = 1,
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
} khronos_boolean_enum_t;
#endif /* __khrplatform_h_ */

39
include/OPENGL-SOURCES.md Normal file
View File

@ -0,0 +1,39 @@
# Vendored OpenGL headers and loader
## GLAD 2.0.8
`uhdk_opengl.h` includes the generated `glad/gl.h`: OpenGL 3.3 core, no
extensions, header-only output, explicit per-context tables (`--mx`), and no
platform loader. `src/glad.c` compiles its implementation into libuhdk. Applications
load a `GladGLContext` through their adapter for `uhdk_gl_context_get_proc`.
Generated unmodified with the pinned generator and its bundled Khronos snapshot:
```sh
python3 -m venv /tmp/uhdk-glad
/tmp/uhdk-glad/bin/pip install glad2==2.0.8
/tmp/uhdk-glad/bin/python -m glad --api gl:core=3.3 --extensions '' \
--reproducible --out-path /tmp/uhdk-glad-output c --header-only --mx
```
Copy `/tmp/uhdk-glad-output/include/glad/gl.h` to `include/glad/gl.h`.
No generator or Python installation is required to build Uhdk.
- Generator: https://github.com/Dav1dde/glad/tree/v2.0.8
- License: `glad/LICENSE`, from https://raw.githubusercontent.com/Dav1dde/glad/v2.0.8/LICENSE
- Generated header SHA-256: `0dfcb12549b10a4c08372e99dd96c4a3b773ff8d89aec07ef878c8e1ca852456`
- Generated SPDX expression: `(WTFPL OR CC0-1.0) AND Apache-2.0`.
## Original Khronos headers
Retained unmodified for consumers choosing their own loader. Do not combine
`GL/glcorearb.h` with `glad/gl.h` or `uhdk_opengl.h` in a translation unit.
License notices are preserved in each file.
- `include/GL/glcorearb.h`
- Source: https://raw.githubusercontent.com/KhronosGroup/OpenGL-Registry/1cdd228e34966dd6b95bd203e9f84faba0f371a1/api/GL/glcorearb.h
- SHA-256: `65fae555a8b3b5709099837001e0c14228b3e45651e2d0fa2ad22649833921e1`
- `include/KHR/khrplatform.h`
- Source: https://raw.githubusercontent.com/KhronosGroup/EGL-Registry/5961a7fe64cf8a126890ced6f13d69e0a1e1b83e/api/KHR/khrplatform.h
- SHA-256: `7b1e01aaa7ad8f6fc34b5c7bdf79ebf5189bb09e2c4d2e79fc5d350623d11e83`

63
include/glad/LICENSE Normal file
View File

@ -0,0 +1,63 @@
The glad source code:
The MIT License (MIT)
Copyright (c) 2013-2022 David Herberth
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The Khronos Specifications:
Copyright (c) 2013-2020 The Khronos Group Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
The EGL Specification and various headers:
Copyright (c) 2007-2016 The Khronos Group Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and/or associated documentation files (the
"Materials"), to deal in the Materials without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Materials, and to
permit persons to whom the Materials are furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Materials.
THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.

2710
include/glad/gl.h Normal file

File diff suppressed because it is too large Load Diff

852
include/uhdk.h Normal file
View File

@ -0,0 +1,852 @@
#ifndef UHDK_H_INCLUDED
#define UHDK_H_INCLUDED
/*
* Uhdk desktop platform API — v0.1, macOS backend.
*
* Implementation: Zig. ABI: C. Header baseline: C99 (also usable from C++).
* No libc runtime objects or allocation contract.
* <stdint.h> supplies types only; this header requires no libc function calls.
* Native OS frameworks may themselves depend on system runtimes.
*
* Naming:
* - Types: uhdk_type; functions: uhdk_object_verb; constants: UHDK_CATEGORY_VALUE.
* - create/destroy allocate and dispose unique objects; retain/release manage
* shared ownership. Reference-counted objects also start with create.
* - get/set access properties; copy writes caller storage; is/has/can query bools.
* - *_async starts a request completed by UHDK_EVENT_REQUEST_DONE. Functions
* that merely schedule events (wake/invalidate) do not create async requests.
* - *_desc describes creation/configuration; *_fn names callbacks; *_flags names
* bitmask types. Pointer/span lengths use len or <field>_len; writable buffers
* use capacity. Measurement units are named explicitly where needed.
*
* Global rules:
* - Floating-point values use C float (32-bit / Zig f32 on supported targets).
* - All text is UTF-8 pointer + byte length, never implicitly NUL-terminated.
* Embedded NUL is rejected where an OS identifier/path cannot represent it.
* - NULL is allowed for a span's pointer only when its length is zero.
* - Input spans/descriptors are copied before return unless documented otherwise.
* - No allocator ownership crosses the ABI. Release objects through Uhdk.
* - Event spans are borrowed until the next poll or app destruction. Copy spans
* or retain files/menus to keep them. Each poll returns an immutable batch.
* - create/run/destroy belong to OS main. run invokes one application entry on
* a second thread. All other calls belong to that application thread except
* app_wake, which is thread-safe. Native work is queued to the main thread.
* - File operations are async to the caller and executed in bounded platform
* batches. Individual filesystem calls can still delay native input collection.
* - Descriptors start with struct_size; zero-initialize them, set struct_size to
* sizeof(the descriptor), then fill fields. Reserved fields must remain zero.
* Unknown trailing fields are ignored; smaller descriptors are rejected.
* - Integer enum values have fixed-width typedefs; C enums only name constants.
* - For output structs with struct_size, callers set it before the call; Uhdk
* writes only the supplied size. Event struct_size describes library output.
* - Functions return a status; output arguments are valid only on UHDK_STATUS_OK,
* except documented buffer-size queries.
* - The application owns rendering, document state, file encoding and layout.
* Uhdk provides optional OpenGL 3.3 core context management. Other renderers
* may attach through experimental uhdk_native.h; drawing remains app-owned.
*/
#include <stdint.h>
#ifndef UHDK_API
#if defined(_WIN32) && defined(UHDK_SHARED)
#if defined(UHDK_BUILD)
#define UHDK_API __declspec(dllexport)
#else
#define UHDK_API __declspec(dllimport)
#endif
#elif defined(__GNUC__) && defined(UHDK_SHARED)
#define UHDK_API __attribute__((visibility("default")))
#else
#define UHDK_API
#endif
#endif
#if defined(_WIN32) && !defined(_WIN64)
#define UHDK_CALL __cdecl
#else
#define UHDK_CALL
#endif
#ifdef __cplusplus
extern "C" {
#endif
#define UHDK_ABI_VERSION 1u
typedef char uhdk_bool; /* Values are UHDK_FALSE (0) or UHDK_TRUE (1). */
enum {
UHDK_FALSE = 0,
UHDK_TRUE = 1
};
typedef uint32_t uhdk_status;
enum {
UHDK_STATUS_OK = 0,
UHDK_STATUS_CANCELLED = 1,
UHDK_STATUS_UNSUPPORTED = 2,
UHDK_STATUS_INVALID_ARGUMENT = 3,
UHDK_STATUS_OUT_OF_MEMORY = 4,
UHDK_STATUS_PERMISSION_DENIED = 5,
UHDK_STATUS_UNAVAILABLE = 6,
UHDK_STATUS_NOT_FOUND = 7,
UHDK_STATUS_BUSY = 8,
UHDK_STATUS_WRONG_THREAD = 9,
UHDK_STATUS_OS_ERROR = 10,
UHDK_STATUS_BUFFER_TOO_SMALL = 11
};
typedef struct uhdk_app uhdk_app;
typedef struct uhdk_window uhdk_window;
typedef struct uhdk_gl_context uhdk_gl_context;
typedef struct uhdk_file uhdk_file;
typedef struct uhdk_watch uhdk_watch;
typedef struct uhdk_menu uhdk_menu;
typedef uint64_t uhdk_request_id; /* 0 is invalid; IDs never reused per app. */
typedef struct uhdk_text {
const char *ptr;
uint64_t len;
} uhdk_text;
typedef struct uhdk_bytes {
const uint8_t *ptr;
uint64_t len;
} uhdk_bytes;
typedef struct uhdk_text_list {
const uhdk_text *ptr;
uint64_t len;
} uhdk_text_list;
typedef struct uhdk_file_list {
uhdk_file *const *ptr;
uint64_t len;
} uhdk_file_list;
typedef struct uhdk_point {
float x;
float y;
} uhdk_point;
typedef struct uhdk_size {
float width;
float height;
} uhdk_size;
typedef struct uhdk_rect {
float x;
float y;
float width;
float height;
} uhdk_rect;
typedef struct uhdk_image {
uint32_t struct_size;
uint32_t width;
uint32_t height;
uint32_t reserved;
uint64_t stride_bytes;
uhdk_bytes pixels; /* Top-down, straight-alpha RGBA8 in sRGB. */
} uhdk_image;
typedef uint32_t uhdk_backend;
enum {
UHDK_BACKEND_AUTO = 0,
UHDK_BACKEND_MACOS = 1,
UHDK_BACKEND_WINDOWS = 2,
UHDK_BACKEND_WAYLAND = 3,
UHDK_BACKEND_X11 = 4
};
typedef uint32_t uhdk_capability;
enum {
UHDK_CAP_FILE_DIALOGS = 1,
UHDK_CAP_CLIPBOARD = 2,
UHDK_CAP_DROP_FILES = 4,
UHDK_CAP_APP_MENU = 5,
UHDK_CAP_CONTEXT_MENU = 6,
UHDK_CAP_APP_ICON = 8,
UHDK_CAP_POINTER_CAPTURE = 12,
UHDK_CAP_TEXT_INPUT = 16,
UHDK_CAP_OPENGL = 18
};
/* Every accepted async request returns a nonzero ID and exactly one terminal
* REQUEST_DONE during normal draining. Failure to accept produces no event.
* Cancellation is best effort; an operation already committed may succeed.
* Return from app_main early abandons undelivered results and initiates shutdown.
*/
typedef struct uhdk_event uhdk_event;
typedef struct uhdk_event_list {
const uhdk_event *ptr;
uint64_t len;
} uhdk_event_list;
#define UHDK_WAIT_FOREVER UINT64_MAX
typedef void(UHDK_CALL *uhdk_app_main_fn)(uhdk_app *, void *user);
typedef struct uhdk_app_desc {
uint32_t struct_size;
uint32_t abi_version;
uhdk_text application_id;
uhdk_text display_name;
uhdk_backend preferred_backend;
uint32_t reserved;
} uhdk_app_desc;
UHDK_API uint32_t UHDK_CALL uhdk_get_abi_version(void);
UHDK_API uhdk_status UHDK_CALL uhdk_app_create(const uhdk_app_desc *, uhdk_app **out);
UHDK_API uhdk_status UHDK_CALL uhdk_app_run(uhdk_app *, uhdk_app_main_fn, void *user); /* Once. */
/* Zero timeout does not wait; finite timeouts use monotonic nanoseconds.
* STOPPED is delivered once; later polls return UNAVAILABLE. */
UHDK_API uhdk_status UHDK_CALL uhdk_app_poll_events(uhdk_app *, uint64_t timeout_ns,
uhdk_event_list *out);
UHDK_API void UHDK_CALL uhdk_app_quit(uhdk_app *); /* Accept/request shutdown. */
UHDK_API void UHDK_CALL uhdk_app_wake(uhdk_app *); /* Thread-safe, coalesced WAKE event. */
/* Monotonic nanoseconds since app creation, in the same clock as event.time_ns. */
UHDK_API uint64_t UHDK_CALL uhdk_app_get_time_ns(uhdk_app *);
/* One pending deadline per app. Replaces any previous deadline; 0 cancels it.
* A past deadline queues WAKE at the next dispatch. On expiry the deadline is
* cleared before delivering WAKE. Explicit wakes may coalesce with expiry but
* do not cancel a future deadline. No event is delivered inline. App code maintains
* multiple timers by scheduling its earliest deadline. No real-time guarantee.
*/
UHDK_API uhdk_status UHDK_CALL uhdk_app_set_wake_deadline(uhdk_app *, uint64_t deadline_ns);
UHDK_API void UHDK_CALL uhdk_app_destroy(uhdk_app *); /* After run; releases child objects. */
UHDK_API uhdk_backend UHDK_CALL uhdk_app_get_backend(uhdk_app *);
UHDK_API uhdk_bool UHDK_CALL uhdk_app_has_capability(uhdk_app *, uhdk_capability);
UHDK_API uhdk_status UHDK_CALL uhdk_request_cancel(uhdk_app *, uhdk_request_id);
/* Application-thread CPU execution time in nanoseconds, not elapsed wall time.
* Bracket user update/input processing with begin_tick/end_tick, AFTER polling
* events and BEFORE rendering. Nested ticks return BUSY; end without begin is
* INVALID_ARGUMENT. Polling/idle time outside the bracket is not counted.
* Render timing uses GL begin_frame/end_frame brackets containing a successful
* swap_buffers. Setup/cleanup brackets without presentation are not frames.
* Blocked waits (including GPU waits), sleep, and time spent descheduled are
* excluded. The entire swap_buffers call is excluded, including driver spinning
* during presentation/vsync. Other CPU work in GL/driver calls is counted;
* this does not measure GPU execution or CPU work on other threads.
* tick_time_ns and render_time_ns are the most recent completed phase samples.
* frame_time_ns captures the latest completed tick + render at presentation's
* end_frame; it stays unchanged until another presented frame completes.
* frames_per_second is the reciprocal of elapsed wall time between the last two
* presented end_frame calls, across all contexts. It includes idle time, vsync,
* and GPU waits; it is NOT derived from CPU frame_time_ns or monitor refresh.
* It is zero until two frames complete, then retains the last sample while idle.
* Counts are cumulative; timings are zero until a sample is available. Calls
* do not generate events. Read the previous frame to display stats while drawing.
*/
typedef struct uhdk_app_stats {
uint32_t struct_size;
uint32_t reserved;
uint64_t tick_count;
uint64_t frame_count;
uint64_t event_count;
uint64_t tick_time_ns;
uint64_t render_time_ns;
uint64_t frame_time_ns;
float frames_per_second;
} uhdk_app_stats;
UHDK_API uhdk_status UHDK_CALL uhdk_app_begin_tick(uhdk_app *);
UHDK_API uhdk_status UHDK_CALL uhdk_app_end_tick(uhdk_app *);
UHDK_API uhdk_status UHDK_CALL uhdk_app_get_stats(uhdk_app *, uhdk_app_stats *out);
/* Logical coordinates: top-left origin, x right, y down. Window geometry uses
* content-area units. Framebuffer dimensions are pixels; use reported sizes,
* not a rounded logical-size * scale calculation. Global positions are not exposed.
*/
typedef uint32_t uhdk_window_flags;
enum {
UHDK_WINDOW_RESIZABLE = 1u << 0,
UHDK_WINDOW_DECORATED = 1u << 1,
UHDK_WINDOW_OPENGL = 1u << 3 /* Prepare a drawable for uhdk_gl_context_create. */
};
typedef uint32_t uhdk_window_state;
enum {
UHDK_WINDOW_NORMAL = 0,
UHDK_WINDOW_MINIMIZED = 1,
UHDK_WINDOW_MAXIMIZED = 2,
UHDK_WINDOW_FULLSCREEN = 3
};
typedef struct uhdk_window_desc {
uint32_t struct_size;
uhdk_window_flags flags;
uhdk_text title;
uhdk_size content_size;
uhdk_size min_size;
uhdk_size max_size; /* Zero limit = unconstrained. */
uhdk_window *parent; /* Optional; must outlive child. */
} uhdk_window_desc;
typedef struct uhdk_window_metrics {
uint32_t struct_size;
uint32_t framebuffer_width;
uint32_t framebuffer_height;
uhdk_window_state state;
uhdk_size logical_size;
float scale;
uhdk_bool focused;
uhdk_bool visible;
} uhdk_window_metrics;
UHDK_API uhdk_status UHDK_CALL uhdk_window_create_async(uhdk_app *, const uhdk_window_desc *,
uhdk_request_id *out);
UHDK_API void UHDK_CALL uhdk_window_destroy(uhdk_window *);
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_title_async(uhdk_window *, uhdk_text,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_size_async(uhdk_window *, uhdk_size,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_state_async(uhdk_window *, uhdk_window_state,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_visible_async(uhdk_window *, uhdk_bool,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL
uhdk_window_request_focus_async(uhdk_window *, uhdk_request_id *out); /* OS may decline. */
UHDK_API uhdk_status UHDK_CALL uhdk_window_get_metrics(uhdk_window *, uhdk_window_metrics *out);
UHDK_API void UHDK_CALL uhdk_window_invalidate(uhdk_window *); /* Coalesced redraw event. */
/* Optional OpenGL context management (application/render thread).
* Include <uhdk_opengl.h> for bundled Khronos GL types, constants and PFNGL*
* function-pointer types. This header alone does not introduce GL names.
*
* Creation requires UHDK_WINDOW_OPENGL and guarantees desktop OpenGL >= 3.3,
* core profile, with GLSL >= 3.30. macOS selects its 4.1 core profile because
* it cannot create an exact 3.3 profile. No legacy or OpenGL ES fallback.
* Initial framebuffer requirement: double-buffered RGBA8. Depth/stencil buffers
* are not required or guaranteed. No multisampling or sRGB is requested.
* Sharing, debug contexts and custom formats are future work.
*
* One context per window. Create does NOT change the current context. Explicitly
* begin_frame locks drawable updates and makes it current; end_frame unlocks.
* ALL raw GL calls (including initialization and deletion) and presentation must
* be inside this bracket. Never wait for platform results inside a bracket.
* make_current/clear_current are also available inside a bracket.
* Contexts cannot migrate to another thread. Destroy detaches a current context;
* window destruction also destroys its context and invalidates the handle.
* Creation failures leave the window valid and release partially created state.
* Uhdk does not modify viewport/scissor or other application GL state on resize;
* use WINDOW_METRICS framebuffer dimensions to update these yourself.
*/
#if defined(_WIN32)
#define UHDK_GL_CALL __stdcall
#else
#define UHDK_GL_CALL
#endif
/* A function pointer, never a data pointer. Cast to the matching Khronos PFNGL*
* type before calling. GL uses stdcall on Win32; Uhdk exports still use C ABI. */
typedef void(UHDK_GL_CALL *uhdk_gl_proc)(void);
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_create_async(uhdk_window *, uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_begin_frame(uhdk_gl_context *);
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_end_frame(uhdk_gl_context *);
UHDK_API void UHDK_CALL uhdk_gl_context_destroy(uhdk_gl_context *);
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_make_current(uhdk_gl_context *);
UHDK_API uhdk_status UHDK_CALL uhdk_gl_clear_current(uhdk_app *);
/* Actual negotiated version, not the requested minimum. */
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_get_version(uhdk_gl_context *, uint32_t *out_major,
uint32_t *out_minor);
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_swap_buffers(uhdk_gl_context *);
/* Explicitly set 0 (unsynchronized) or 1 (vsync); other values are invalid.
* No initial interval is guaranteed. Unsupported control returns UNSUPPORTED;
* compositor/driver policy may override presentation timing. */
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_set_swap_interval(uhdk_gl_context *,
int32_t interval);
/* Requires this context current. Accepts pointer+length ASCII GL symbol names.
* Missing symbols return NOT_FOUND. Pointers are valid until context destruction
* and must only be used with that context current. A non-NULL pointer alone does
* not prove extension/version support; check GL version/extension availability.
* Loads both platform-library exports and driver-provided GL entry points. */
UHDK_API uhdk_status UHDK_CALL uhdk_gl_context_get_proc(uhdk_gl_context *, uhdk_text name,
uhdk_gl_proc *out);
/* Keyboard events are not text. Physical key = USB HID keyboard-page usage;
* 0 means unknown. Logical key = Unicode scalar, or UHDK_KEY_* below.
* Modifier bits describe state after the event. Focus loss resets held state.
* With text input enabled, text/edit events are authoritative for editing;
* raw key events are observational for those actions (do not insert twice).
* Registered menu accelerators are dispatched as menu/edit commands instead of
* raw key events. Unregistered application shortcuts need their own policy.
*/
typedef uint32_t uhdk_modifier_flags;
enum {
UHDK_MOD_SHIFT = 1u << 0,
UHDK_MOD_CONTROL = 1u << 1,
UHDK_MOD_ALT = 1u << 2,
UHDK_MOD_SUPER = 1u << 3,
UHDK_MOD_CAPS_LOCK = 1u << 4,
UHDK_MOD_NUM_LOCK = 1u << 5
};
typedef uint32_t uhdk_key;
enum {
UHDK_KEY_UNKNOWN = 0,
UHDK_KEY_ESCAPE = 0x110000,
UHDK_KEY_ENTER,
UHDK_KEY_TAB,
UHDK_KEY_BACKSPACE,
UHDK_KEY_DELETE,
UHDK_KEY_INSERT,
UHDK_KEY_LEFT,
UHDK_KEY_RIGHT,
UHDK_KEY_UP,
UHDK_KEY_DOWN,
UHDK_KEY_HOME,
UHDK_KEY_END,
UHDK_KEY_PAGE_UP,
UHDK_KEY_PAGE_DOWN,
UHDK_KEY_F1 = 0x110100 /* F1..F24 are contiguous. */
};
typedef uint32_t uhdk_pointer_button;
enum {
UHDK_BUTTON_NONE = 0,
UHDK_BUTTON_PRIMARY = 1,
UHDK_BUTTON_SECONDARY = 2,
UHDK_BUTTON_MIDDLE = 3,
UHDK_BUTTON_BACK = 4,
UHDK_BUTTON_FORWARD = 5
};
typedef uint32_t uhdk_scroll_unit;
enum {
UHDK_SCROLL_LOGICAL_PIXELS = 0,
UHDK_SCROLL_LINES = 1
};
typedef uint32_t uhdk_scroll_phase;
enum {
UHDK_SCROLL_DISCRETE = 0,
UHDK_SCROLL_BEGIN = 1,
UHDK_SCROLL_UPDATE = 2,
UHDK_SCROLL_END = 3,
UHDK_SCROLL_CANCEL = 4
};
typedef uint32_t uhdk_cursor_shape;
enum {
UHDK_CURSOR_ARROW = 0,
UHDK_CURSOR_TEXT,
UHDK_CURSOR_CROSSHAIR,
UHDK_CURSOR_HAND,
UHDK_CURSOR_RESIZE_HORIZONTAL,
UHDK_CURSOR_RESIZE_VERTICAL,
UHDK_CURSOR_RESIZE_NW_SE,
UHDK_CURSOR_RESIZE_NE_SW,
UHDK_CURSOR_MOVE,
UHDK_CURSOR_NOT_ALLOWED,
UHDK_CURSOR_WAIT,
UHDK_CURSOR_HIDDEN
};
typedef uint32_t uhdk_pointer_mode;
enum {
UHDK_POINTER_NORMAL = 0,
UHDK_POINTER_CAPTURE = 1
};
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_cursor_shape_async(uhdk_window *, uhdk_cursor_shape,
uhdk_request_id *out);
/* Capture delivers selection-drag events outside the window. Focus loss or OS
* cancellation restores NORMAL and emits POINTER_MODE_CHANGED. */
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_pointer_mode_async(uhdk_window *, uhdk_pointer_mode,
uhdk_request_id *out);
/* Text offsets/lengths are UTF-8 byte counts, always at scalar boundaries.
* surrounding_text is a bounded snapshot; document_offset locates it in the
* document. Selection and replacement ranges are relative to this snapshot.
* Events are ordered. Apply all edits in order, including edits sharing a base
* revision; do not discard a burst just because its first edit changed revision.
* acknowledged_input_id is the last applied text/edit input ID. Snapshots behind
* outstanding native edits are ignored; publish again after consuming the batch.
* Commit/preedit events carry text + an optional replacement range. A preedit
* updates temporary composition, never the permanent document by itself.
*/
typedef struct uhdk_text_state {
uint32_t struct_size;
uint32_t reserved;
uhdk_text surrounding_text;
uint64_t revision;
uint64_t acknowledged_input_id;
uint64_t document_offset;
uint64_t selection_start;
uint64_t selection_len;
uhdk_rect caret; /* Window-local logical coordinates. */
} uhdk_text_state;
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_input_async(uhdk_window *, uhdk_bool enabled,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_text_state(uhdk_window *, const uhdk_text_state *);
UHDK_API uhdk_status UHDK_CALL uhdk_window_cancel_composition_async(uhdk_window *,
uhdk_request_id *out);
/* 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
* 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.
* Too-small buffers return UHDK_STATUS_BUFFER_TOO_SMALL and required count; no partial copy.
*/
typedef uint32_t uhdk_file_representation;
enum {
UHDK_FILE_DISPLAY_NAME = 0,
UHDK_FILE_URI = 1,
UHDK_FILE_NATIVE_PATH = 2
};
UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_native_path(uhdk_app *, uhdk_bytes path,
uhdk_file **out);
UHDK_API uhdk_status UHDK_CALL uhdk_file_create_from_uri(uhdk_app *, uhdk_text uri,
uhdk_file **out);
UHDK_API void UHDK_CALL uhdk_file_retain(uhdk_file *);
UHDK_API void UHDK_CALL uhdk_file_release(uhdk_file *);
UHDK_API uhdk_status UHDK_CALL uhdk_file_copy_representation(uhdk_file *, uhdk_file_representation,
uint8_t *buffer, uint64_t capacity,
uint64_t *out_len);
typedef uint32_t uhdk_file_type;
enum {
UHDK_FILE_REGULAR = 1,
UHDK_FILE_DIRECTORY = 2,
UHDK_FILE_OTHER = 3
};
typedef struct uhdk_file_info {
uint64_t size_bytes;
int64_t modified_time_ns; /* Unix epoch, nanoseconds. */
uhdk_file_type type;
uint32_t reserved;
} uhdk_file_info;
/* FIFO across accepted file requests. Read results are raw bytes, batch-owned.
* max_bytes is a hard limit; oversized files return BUFFER_TOO_SMALL.
* 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.
* 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.
* 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,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_file_write_all_async(uhdk_file *, uhdk_bytes,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_file_get_info_async(uhdk_file *, uhdk_request_id *out);
typedef uint32_t uhdk_dialog_kind;
enum {
UHDK_DIALOG_OPEN_FILE = 0,
UHDK_DIALOG_OPEN_FILES = 1,
UHDK_DIALOG_OPEN_DIRECTORY = 2,
UHDK_DIALOG_SAVE_FILE = 3
};
typedef struct uhdk_file_filter {
uhdk_text label;
uhdk_text_list extensions; /* Without dots; empty means all files. */
} uhdk_file_filter;
typedef struct uhdk_file_dialog_desc {
uint32_t struct_size;
uhdk_dialog_kind kind;
uhdk_window *parent;
uhdk_text title;
uhdk_text accept_label;
uhdk_text suggested_name; /* Empty = native default. */
uhdk_file *initial_directory;
const uhdk_file_filter *filters;
uint64_t filters_len;
} uhdk_file_dialog_desc;
UHDK_API uhdk_status UHDK_CALL uhdk_file_dialog_show_async(uhdk_app *,
const uhdk_file_dialog_desc *,
uhdk_request_id *out);
/* Plain-text clipboard only. Writes copy UTF-8 before returning; empty text is
* valid. Reads negotiate native text formats internally and complete with text
* borrowed until the next event poll. NOT_FOUND means no text format.
* max_bytes bounds the UTF-8 result; excess returns BUFFER_TOO_SMALL, never a
* truncated string. A read remains async even when a backend can read instantly.
*/
UHDK_API uhdk_status UHDK_CALL uhdk_clipboard_write_text_async(uhdk_app *, uhdk_text,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_clipboard_read_text_async(uhdk_app *, uint64_t max_bytes,
uhdk_request_id *out);
/* Incoming file drops only. When enabled, Uhdk negotiates copy-only file drops
* and emits DROP_FILES with borrowed file references plus the drop position.
* Retain references to use after the next poll. Directory references are allowed;
* the application decides whether to open them. No source deletion, outgoing
* drag, data offers, or application-visible drop-completion transaction.
*/
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_drop_enabled_async(uhdk_window *, uhdk_bool,
uhdk_request_id *out);
/* Menus are immutable snapshots. Command IDs are application-assigned, nonzero
* and unique within a menu tree. Shortcuts use logical keys + modifiers.
* Created menus have one reference; attached menus are retained internally.
* Retain/release manage references; the final release destroys the menu.
*/
typedef uint32_t uhdk_menu_role;
enum {
UHDK_MENU_ROLE_NONE = 0,
UHDK_MENU_ROLE_ABOUT,
UHDK_MENU_ROLE_QUIT,
UHDK_MENU_ROLE_COPY,
UHDK_MENU_ROLE_CUT,
UHDK_MENU_ROLE_PASTE,
UHDK_MENU_ROLE_SELECT_ALL,
UHDK_MENU_ROLE_UNDO,
UHDK_MENU_ROLE_REDO
};
typedef uint32_t uhdk_menu_flags;
enum {
UHDK_MENU_DISABLED = 1u << 0,
UHDK_MENU_CHECKED = 1u << 1,
UHDK_MENU_SEPARATOR = 1u << 2
};
typedef struct uhdk_menu_item {
uint64_t command_id;
uhdk_text label;
uhdk_menu_flags flags;
uhdk_menu_role role;
uhdk_key shortcut_key;
uhdk_modifier_flags shortcut_modifiers;
uhdk_menu *submenu; /* Retained by the created parent menu. */
} uhdk_menu_item;
UHDK_API uhdk_status UHDK_CALL uhdk_menu_create(uhdk_app *, const uhdk_menu_item *, uint64_t len,
uhdk_menu **out);
UHDK_API uhdk_status UHDK_CALL uhdk_menu_set_item_flags_async(uhdk_menu *, uint64_t command_id,
uhdk_menu_flags,
uhdk_request_id *out);
UHDK_API void UHDK_CALL uhdk_menu_retain(uhdk_menu *);
UHDK_API void UHDK_CALL uhdk_menu_release(uhdk_menu *);
UHDK_API uhdk_status UHDK_CALL uhdk_app_set_menu_async(uhdk_app *, uhdk_menu *,
uhdk_request_id *out); /* NULL clears. */
UHDK_API uhdk_status UHDK_CALL uhdk_window_set_menu_async(uhdk_window *, uhdk_menu *,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_menu_popup_async(uhdk_menu *, uhdk_window *, uhdk_point,
uhdk_request_id *out);
UHDK_API uhdk_status UHDK_CALL uhdk_app_set_icon_async(uhdk_app *, const uhdk_image *,
uhdk_request_id *out);
/* Successful shell completion means handed off to the OS, not that a target
* application finished loading. URLs are not shell command strings. */
UHDK_API uhdk_status UHDK_CALL uhdk_shell_open_url_async(uhdk_app *, uhdk_text url,
uhdk_request_id *out);
typedef uint32_t uhdk_change_flags;
enum {
UHDK_CHANGE_CREATED = 1u << 0,
UHDK_CHANGE_REMOVED = 1u << 1,
UHDK_CHANGE_CONTENT = 1u << 2,
UHDK_CHANGE_METADATA = 1u << 3,
UHDK_CHANGE_RENAMED = 1u << 4,
UHDK_CHANGE_RESCAN_REQUIRED = 1u << 5,
UHDK_CHANGE_ROOT_INVALIDATED = 1u << 6
};
/* Watch a document path or a directory's immediate children; never recursive.
* Document watches follow the named path across atomic-save replacement, using
* parent-directory observation where needed. Removal emits REMOVED; subsequent
* recreation emits CREATED. If observation can no longer continue (for example,
* parent removal), emit ROOT_INVALIDATED and let the app recreate the watch.
* Changes may coalesce and are hints, not a transaction log. Renames may arrive
* as remove/create; no rename pairing is promised. RESCAN_REQUIRED means inspect
* current state. Pending notifications are suppressed after destroy; already borrowed batches remain valid. */
UHDK_API uhdk_status UHDK_CALL uhdk_watch_create_async(uhdk_file *root, uhdk_request_id *out);
UHDK_API void UHDK_CALL uhdk_watch_destroy(uhdk_watch *);
typedef uint32_t uhdk_appearance;
enum {
UHDK_APPEARANCE_UNKNOWN = 0,
UHDK_APPEARANCE_LIGHT = 1,
UHDK_APPEARANCE_DARK = 2
};
typedef struct uhdk_preferences {
uint32_t struct_size;
uhdk_appearance appearance;
uhdk_bool reduced_motion;
uhdk_bool high_contrast;
} uhdk_preferences;
UHDK_API uhdk_status UHDK_CALL uhdk_app_get_preferences(uhdk_app *, uhdk_preferences *out);
/* Event payloads. Window is NULL for app-wide events. time_ns is monotonic time
* relative to app creation, not wall time. Do not persist event pointers.
* Destroying a window suppresses queued window input/redraw events and cancels
* its requests. REQUEST_DONE never includes a dangling window pointer.
*/
typedef uint32_t uhdk_event_type;
enum {
UHDK_EVENT_READY = 1,
UHDK_EVENT_STOPPED,
UHDK_EVENT_WAKE,
UHDK_EVENT_QUIT_REQUESTED,
UHDK_EVENT_APP_ACTIVATION,
UHDK_EVENT_OPEN_FILES,
UHDK_EVENT_PREFERENCES_CHANGED,
UHDK_EVENT_WINDOW_CLOSE_REQUESTED,
UHDK_EVENT_WINDOW_METRICS,
UHDK_EVENT_WINDOW_REDRAW,
UHDK_EVENT_KEY,
UHDK_EVENT_POINTER_ENTER,
UHDK_EVENT_POINTER_LEAVE,
UHDK_EVENT_POINTER_MOVE,
UHDK_EVENT_POINTER_BUTTON,
UHDK_EVENT_POINTER_MODE_CHANGED,
UHDK_EVENT_SCROLL,
UHDK_EVENT_TEXT_COMMIT,
UHDK_EVENT_TEXT_PREEDIT,
UHDK_EVENT_TEXT_COMPOSITION_END,
UHDK_EVENT_EDIT_COMMAND,
UHDK_EVENT_MENU_COMMAND,
UHDK_EVENT_DROP_FILES,
UHDK_EVENT_WATCH,
UHDK_EVENT_REQUEST_DONE
};
typedef uint32_t uhdk_request_kind;
enum {
UHDK_REQUEST_FILE_DIALOG = 1,
UHDK_REQUEST_CLIPBOARD_TEXT,
UHDK_REQUEST_MENU_POPUP,
UHDK_REQUEST_SHELL,
UHDK_REQUEST_COMMAND,
UHDK_REQUEST_WINDOW,
UHDK_REQUEST_GL_CONTEXT,
UHDK_REQUEST_WATCH,
UHDK_REQUEST_FILE_READ,
UHDK_REQUEST_FILE_WRITE,
UHDK_REQUEST_FILE_INFO
};
typedef uint32_t uhdk_edit_command;
enum {
UHDK_EDIT_COPY = 1,
UHDK_EDIT_CUT,
UHDK_EDIT_PASTE,
UHDK_EDIT_SELECT_ALL,
UHDK_EDIT_UNDO,
UHDK_EDIT_REDO,
UHDK_EDIT_DELETE_BACKWARD,
UHDK_EDIT_DELETE_FORWARD,
UHDK_EDIT_MOVE_LEFT,
UHDK_EDIT_MOVE_RIGHT,
UHDK_EDIT_MOVE_UP,
UHDK_EDIT_MOVE_DOWN,
UHDK_EDIT_LINE_START,
UHDK_EDIT_LINE_END,
UHDK_EDIT_DOCUMENT_START,
UHDK_EDIT_DOCUMENT_END,
UHDK_EDIT_WORD_LEFT,
UHDK_EDIT_WORD_RIGHT,
UHDK_EDIT_NEWLINE,
UHDK_EDIT_TAB
};
typedef struct uhdk_request_result {
uhdk_request_id id;
uhdk_request_kind kind;
uhdk_status status;
uhdk_text diagnostic; /* Optional; for humans, never parse for logic. */
union {
uhdk_file_list files; /* FILE_DIALOG */
uhdk_text text; /* CLIPBOARD_TEXT */
uhdk_window *window;
uhdk_gl_context *gl_context;
uhdk_watch *watch;
uhdk_bytes bytes;
uhdk_file_info info;
} value; /* Only valid on UHDK_STATUS_OK; SHELL/COMMAND/FILE_WRITE/MENU_POPUP have no payload. */
} uhdk_request_result;
struct uhdk_event {
uint32_t struct_size;
uhdk_event_type type;
uint64_t time_ns;
uint64_t input_id; /* Correlates raw key and native edit/text; zero for non-input. */
uhdk_window *window;
union {
uhdk_bool active;
uhdk_file_list files;
uhdk_window_metrics metrics;
uhdk_preferences preferences;
struct {
uint32_t physical_key;
uhdk_key logical_key;
uhdk_modifier_flags modifiers;
uhdk_bool pressed;
uhdk_bool repeat;
} key;
struct {
uhdk_point position;
uhdk_point delta; /* delta uses logical units. */
uhdk_pointer_button button;
uhdk_modifier_flags modifiers;
uhdk_bool pressed;
uint32_t click_count;
} pointer;
uhdk_pointer_mode pointer_mode;
struct {
uhdk_point position;
uhdk_point delta; /* Positive scroll = toward right/bottom. */
uhdk_scroll_unit unit;
uhdk_scroll_phase phase;
uhdk_modifier_flags modifiers;
uhdk_bool momentum;
} scroll;
struct {
uhdk_text text;
uint64_t revision;
uhdk_bool has_replacement;
uint32_t reserved;
uint64_t replacement_start;
uint64_t replacement_len;
uint64_t selection_start;
uint64_t selection_len; /* Within preedit text. */
} text;
struct {
uhdk_edit_command command;
uhdk_bool extend_selection;
} edit;
struct {
uhdk_file_list files;
uhdk_point position;
} drop_files;
struct {
uint64_t command_id;
} menu;
struct {
uhdk_watch *watch;
uhdk_change_flags flags;
uhdk_status status; /* Non-OK: watch failure; rescan/recreate as needed. */
uhdk_file *file; /* Nullable when only the watched root is known. */
} watch;
uhdk_request_result request;
} data;
};
/* Lifecycle policy: close/quit requests are advisory. Destroy the window or call
* uhdk_app_quit() to accept; otherwise the application stays alive. Closing the
* last window does not automatically quit. App destruction invalidates ALL child
* handles, including retained files/menus; callers must stop other threads from
* waking the app first. Native menu roles produce EDIT_COMMAND/QUIT_REQUESTED
* where applicable; custom menu items produce MENU_COMMAND. A popup selection
* delivers its command event first and REQUEST_DONE afterward; process the
* command only once. Popup dismissal completes as UHDK_STATUS_CANCELLED.
*
* Boundaries: no widget/layout engine, text rasterizer, high-level renderer,
* networking, process spawning, global input hooks, or streaming filesystem API.
* Deferred: generic clipboard data, outgoing drag/drop, custom cursors, relative
* pointer input, transparent windows, display enumeration/positioning, tray items,
* badges/progress/attention, recursive watching, accent colors, accessibility
* semantic trees, notifications, persistent access bookmarks and streaming data.
*/
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* UHDK_H_INCLUDED */

33
include/uhdk_native.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef UHDK_NATIVE_H_INCLUDED
#define UHDK_NATIVE_H_INCLUDED
/* Optional experimental platform escape hatch; outside the v0.1 core contract.
* Native handles may require OS headers. Not needed for Uhdk OpenGL rendering.
*/
#include "uhdk.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Platform escape hatch. Returned handles are borrowed and must not be released.
* macOS: primary=NSWindow*, secondary=NSView*; Windows: primary=HWND.
* Wayland: primary=wl_display*, secondary=wl_surface*.
* X11: primary=Display*, value=XID. Lifetime ends with the Uhdk window.
* With UHDK_WINDOW_OPENGL, Uhdk owns drawable setup; do not replace its native
* view/surface or pixel format. Otherwise the external renderer owns setup.
* Borrowing a handle does not change platform threading rules: AppKit window
* and view operations still belong on the macOS main thread.
*/
typedef struct uhdk_native_window {
uint32_t struct_size;
uhdk_backend backend;
void *primary;
void *secondary;
uint64_t value;
} uhdk_native_window;
UHDK_API uhdk_status UHDK_CALL uhdk_window_get_native(uhdk_window *, uhdk_native_window *out);
#ifdef __cplusplus
}
#endif
#endif /* UHDK_NATIVE_H_INCLUDED */

16
include/uhdk_opengl.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef UHDK_OPENGL_H_INCLUDED
#define UHDK_OPENGL_H_INCLUDED
#include "uhdk.h"
/* GLAD 2: OpenGL 3.3 core, no extensions, explicit per-context function table.
* Do not combine with platform GL headers or GL/glcorearb.h.
* The loader implementation is linked into libuhdk; consumers must not define
* GLAD_GL_IMPLEMENTATION when linking that library.
* Load a GladGLContext via gladLoadGLContextUserPtr and an adapter calling
* 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.
*/
#include "glad/gl.h"
#endif /* UHDK_OPENGL_H_INCLUDED */

61
src/Application.zig Normal file
View File

@ -0,0 +1,61 @@
//! Internal application ownership.
const std = @import("std");
const c = @import("native");
const runtime = @import("runtime.zig");
const abi = @import("abi.zig");
const Event = @import("Event.zig");
const Request = @import("Request.zig");
const Resource = @import("Resource.zig");
const Application = @This();
gpa: std.mem.Allocator,
io: std.Io,
io_backend: ?*std.Io.Threaded = null,
mutex: std.Io.Mutex = .init,
available: std.Io.Event = .unset,
in_tick: bool = false,
objects: ?*Resource = null,
queue_head: ?*Request = null,
queue_tail: ?*Request = null,
requests: ?*Request = null,
io_active: ?*Request = null,
io_head: ?*Request = null,
io_tail: ?*Request = null,
pending_head: ?*Event = null,
pending_tail: ?*Event = null,
published: ?*Event = null,
events: std.ArrayList(c.uhdk_event) = .empty,
name: []u8,
epoch: u64,
deadline: u64 = 0,
next_id: u64 = 1,
running: bool = false,
has_run: bool = false,
stopping: bool = false,
stopped_sent: bool = false,
stopped_seen: bool = false,
worker_done: bool = false,
event_error: abi.Status = abi.OK,
worker_id: ?std.Thread.Id = null,
entry: c.uhdk_app_main_fn = null,
user: ?*anyopaque = null,
preferences: c.uhdk_preferences = std.mem.zeroes(c.uhdk_preferences),
stats: c.uhdk_app_stats = std.mem.zeroes(c.uhdk_app_stats),
tick_start: ?u64 = null,
last_frame_time: ?u64 = null,
pub fn create(gpa: std.mem.Allocator, io: std.Io, name: []const u8) !*Application {
const self = try gpa.create(Application);
errdefer gpa.destroy(self);
const owned_name = try gpa.dupe(u8, name);
self.* = .{ .gpa = gpa, .io = io, .name = owned_name, .epoch = runtime.now(io) };
return self;
}
pub fn lock(self: *Application) void {
self.mutex.lockUncancelable(self.io);
}
pub fn unlock(self: *Application) void {
self.mutex.unlock(self.io);
}
pub fn notify(self: *Application) void {
self.available.set(self.io);
}

12
src/Event.zig Normal file
View File

@ -0,0 +1,12 @@
//! Internal event ownership.
const std = @import("std");
const c = @import("native");
const abi = @import("abi.zig");
const Resource = @import("Resource.zig");
const Event = @This();
next: ?*Event = null,
arena: abi.Arena,
gpa: std.mem.Allocator,
event: c.uhdk_event = std.mem.zeroes(c.uhdk_event),
pins: std.ArrayList(*Resource) = .empty,

164
src/FileOperation.zig Normal file
View File

@ -0,0 +1,164 @@
//! One platform-thread file operation. Each step transfers at most one chunk.
const FileOperation = @This();
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
pub const Kind = enum { read, write, info };
pub const Result = union(enum) {
pending,
bytes: []const u8,
info: Io.File.Stat,
written,
};
kind: Kind,
gpa: Allocator,
io: Io,
path: []const u8,
input: []const u8 = &.{},
limit: u64 = 0,
file: ?Io.File = null,
atomic_file: ?Io.File.Atomic = null,
contents: std.ArrayList(u8) = .empty,
offset: u64 = 0,
started: bool = false,
pub fn init(
gpa: Allocator,
io: Io,
kind: Kind,
path: []const u8,
input: []const u8,
limit: u64,
) FileOperation {
return .{ .gpa = gpa, .io = io, .kind = kind, .path = path, .input = input, .limit = limit };
}
pub fn deinit(self: *FileOperation) void {
if (self.file) |file| file.close(self.io);
if (self.atomic_file) |*file| file.deinit(self.io);
self.contents.deinit(self.gpa);
self.* = undefined;
}
/// The caller checks cancellation before each step. A successful replacement is
/// the commit point; subsequent cancellation must not change its successful result.
pub fn step(self: *FileOperation) !Result {
const cwd = Io.Dir.cwd();
if (self.kind == .info) {
return .{ .info = try cwd.statFile(self.io, self.path, .{}) };
}
if (!self.started) {
switch (self.kind) {
.read => {
// Reject nonregular files and do not follow the final symlink.
// Filesystem metadata remains advisory under concurrent changes.
const stat = try cwd.statFile(self.io, self.path, .{ .follow_symlinks = false });
if (stat.kind != .file) return error.NotRegularFile;
const file = try cwd.openFile(self.io, self.path, .{ .follow_symlinks = false });
errdefer file.close(self.io);
const opened = try file.stat(self.io);
if (opened.kind != .file) return error.NotRegularFile;
if (opened.size > self.limit) return error.StreamTooLong;
self.file = file;
},
.write => {
const stat: ?Io.File.Stat = cwd.statFile(self.io, self.path, .{
.follow_symlinks = false,
}) catch |err| switch (err) {
error.FileNotFound => null,
else => return err,
};
if (stat) |existing| {
if (existing.kind != .file) return error.NotRegularFile;
}
self.atomic_file = try cwd.createFileAtomic(self.io, self.path, .{
.permissions = .fromMode(0o600),
.replace = true,
});
if (stat) |existing| {
try self.atomic_file.?.file.setPermissions(
self.io,
.fromMode(existing.permissions.toMode() & 0o777),
);
}
},
.info => unreachable,
}
self.started = true;
return .pending;
}
const chunk_size = 256 * 1024;
if (self.kind == .read) {
var chunk: [chunk_size]u8 = undefined;
const remaining = self.limit -| self.contents.items.len;
const capacity = @min(chunk.len, remaining +| 1);
const count = try self.file.?.readPositional(self.io, &.{chunk[0..capacity]}, self.offset);
if (count == 0) return .{ .bytes = self.contents.items };
if (count > remaining) return error.StreamTooLong;
try self.contents.appendSlice(self.gpa, chunk[0..count]);
self.offset += count;
return .pending;
}
if (self.offset < self.input.len) {
const remaining = self.input[@intCast(self.offset)..];
const count = try self.atomic_file.?.file.writePositional(
self.io,
&.{remaining[0..@min(chunk_size, remaining.len)]},
self.offset,
);
if (count == 0) return error.WriteFailed;
self.offset += count;
return .pending;
}
try self.atomic_file.?.replace(self.io);
return .written;
}
test "chunked replacement, bounded read, and cancellation before commit" {
const gpa = std.testing.allocator;
const io = std.testing.io;
var temporary = std.testing.tmpDir(.{});
defer temporary.cleanup();
const directory = try temporary.dir.realPathFileAlloc(io, ".", gpa);
defer gpa.free(directory);
const path = try std.fs.path.join(gpa, &.{ directory, "document" });
defer gpa.free(path);
const contents = try gpa.alloc(u8, 600_000);
defer gpa.free(contents);
@memset(contents, 0xab);
var write = FileOperation.init(gpa, io, .write, path, contents, 0);
defer write.deinit();
var passes: usize = 0;
while (try write.step() == .pending) : (passes += 1) {}
try std.testing.expect(passes >= 4);
var read = FileOperation.init(gpa, io, .read, path, &.{}, contents.len);
defer read.deinit();
while (true) {
switch (try read.step()) {
.bytes => |result| {
try std.testing.expectEqualSlices(u8, contents, result);
break;
},
.pending => {},
else => unreachable,
}
}
var limited = FileOperation.init(gpa, io, .read, path, &.{}, 10);
defer limited.deinit();
try std.testing.expectError(error.StreamTooLong, limited.step());
var cancelled = FileOperation.init(gpa, io, .write, path, "changed", 0);
try std.testing.expect(try cancelled.step() == .pending);
try std.testing.expect(try cancelled.step() == .pending);
cancelled.deinit();
const preserved = try temporary.dir.readFileAlloc(io, "document", gpa, .limited(contents.len + 1));
defer gpa.free(preserved);
try std.testing.expectEqualSlices(u8, contents, preserved);
}

20
src/Request.zig Normal file
View File

@ -0,0 +1,20 @@
//! Internal request ownership.
const std = @import("std");
const c = @import("native");
const abi = @import("abi.zig");
const Event = @import("Event.zig");
const Resource = @import("Resource.zig");
const FileOperation = @import("FileOperation.zig");
const Request = @This();
next: ?*Request = null,
queued_next: ?*Request = null,
arena: abi.Arena,
gpa: std.mem.Allocator,
data: c.z_command = std.mem.zeroes(c.z_command),
node: *Event,
id: u64 = 0,
kind: u32 = c.UHDK_REQUEST_COMMAND,
cancelled: bool = false,
pins: std.ArrayList(*Resource) = .empty,
file_operation: ?FileOperation = null,

79
src/Resource.zig Normal file
View File

@ -0,0 +1,79 @@
//! Stable handle storage. The active payload determines which fields are valid.
const Resource = @This();
const std = @import("std");
const c = @import("native");
const Application = @import("Application.zig");
app: *Application,
gpa: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
next: ?*Resource = null,
alive: bool = true,
refs: usize = 1,
native: ?*anyopaque = null,
payload: Payload,
pub const Kind = enum(u32) {
window = c.Z_KIND_WINDOW,
gl = c.Z_KIND_GL,
file = c.Z_KIND_FILE,
menu = c.Z_KIND_MENU,
watch = c.Z_KIND_WATCH,
};
pub const Payload = union(Kind) {
window: Window,
gl: Gl,
file: File,
menu: Menu,
watch: Watch,
};
pub const Window = struct {
context: ?*Resource = null,
parent: ?*Resource = null,
metrics: c.uhdk_window_metrics = std.mem.zeroes(c.uhdk_window_metrics),
};
pub const Gl = struct {
owner: ?*Resource = null,
bracket: bool = false,
frame_cpu_start: u64 = 0,
swap_cpu_time: u64 = 0,
presented: bool = false,
};
pub const File = struct {
path: [:0]const u8 = "",
uri: []const u8 = "",
display: []const u8 = "",
};
pub const Menu = struct {
owns_submenus: bool = false,
items: []c.uhdk_menu_item = &.{},
};
pub const Watch = struct {
owner: ?*Resource = null,
};
pub fn init(application: *Application, resource_kind: Kind) Resource {
return .{
.app = application,
.gpa = application.gpa,
.arena = .init(application.gpa),
.payload = switch (resource_kind) {
.window => .{ .window = .{} },
.gl => .{ .gl = .{} },
.file => .{ .file = .{} },
.menu => .{ .menu = .{} },
.watch => .{ .watch = .{} },
},
};
}
pub fn kind(self: *const Resource) u32 {
return @intFromEnum(std.meta.activeTag(self.payload));
}

63
src/abi.zig Normal file
View File

@ -0,0 +1,63 @@
//! Validation and representation at the C ABI boundary.
const std = @import("std");
const c = @import("native");
const runtime = @import("runtime.zig");
pub const OK = c.UHDK_STATUS_OK;
pub const INVALID = c.UHDK_STATUS_INVALID_ARGUMENT;
pub const OOM = c.UHDK_STATUS_OUT_OF_MEMORY;
pub const UNAVAILABLE = c.UHDK_STATUS_UNAVAILABLE;
pub const CANCELLED = c.UHDK_STATUS_CANCELLED;
pub const Status = c.uhdk_status;
pub const Arena = std.heap.ArenaAllocator;
pub const IO_READ = 100;
pub const IO_WRITE = 101;
pub const IO_INFO = 102;
pub fn validSpan(ptr: ?*const anyopaque, len: u64) bool {
return len <= std.math.maxInt(usize) and (len == 0 or ptr != null);
}
pub fn bytes(t: c.uhdk_bytes) []const u8 {
return if (t.len == 0) "" else @as([*]const u8, @ptrCast(t.ptr))[0..@intCast(t.len)];
}
pub fn textBytes(text: c.uhdk_text) []const u8 {
return bytes(.{ .ptr = @ptrCast(text.ptr), .len = text.len });
}
pub fn validText(t: c.uhdk_text, no_nul: bool) bool {
return validSpan(t.ptr, t.len) and std.unicode.utf8ValidateSlice(textBytes(t)) and (!no_nul or std.mem.findScalar(u8, textBytes(t), 0) == null);
}
pub fn textSpan(s: []const u8) c.uhdk_text {
return .{ .ptr = s.ptr, .len = s.len };
}
pub fn copyText(arena: std.mem.Allocator, t: c.uhdk_text) !c.uhdk_text {
return textSpan(try arena.dupe(u8, textBytes(t)));
}
pub fn copyBytes(arena: std.mem.Allocator, t: c.uhdk_bytes) !c.uhdk_bytes {
const b = try arena.dupe(u8, bytes(t));
return .{ .ptr = b.ptr, .len = b.len };
}
pub fn validDescriptor(comptime T: type, handle: [*c]const T) bool {
return handle != null and handle.*.struct_size >= @sizeOf(T);
}
pub fn writeOutput(comptime T: type, dest: [*c]T, value: T) Status {
if (dest == null or dest.*.struct_size < @sizeOf(T)) return INVALID;
dest.* = value;
dest.*.struct_size = @sizeOf(T);
return OK;
}
pub fn finiteSize(s: c.uhdk_size) bool {
return std.math.isFinite(s.width) and std.math.isFinite(s.height) and s.width >= 0 and s.height >= 0 and s.width <= 65536 and s.height <= 65536;
}
pub fn boundary(s: []const u8, offset: u64) bool {
return offset <= s.len and (offset == s.len or (s[@intCast(offset)] & 0xC0) != 0x80);
}

271
src/api/app.zig Normal file
View File

@ -0,0 +1,271 @@
//! C ABI entry points for app.
const std = @import("std");
const c = @import("native");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
const Application = @import("../Application.zig");
pub var singleton: bool = false; // OS-main-thread lifecycle gate.
pub export fn uhdk_get_abi_version() u32 {
return c.UHDK_ABI_VERSION;
}
pub export fn uhdk_app_create(desc: [*c]const c.uhdk_app_desc, out: [*c]?*c.uhdk_app) abi.Status {
if (c.z_native_is_main_thread() == 0) return c.UHDK_STATUS_WRONG_THREAD;
if (!abi.validDescriptor(c.uhdk_app_desc, desc) or
out == null or
desc.*.abi_version != c.UHDK_ABI_VERSION or
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 (singleton) return c.UHDK_STATUS_BUSY;
const gpa = std.heap.smp_allocator;
const backend = gpa.create(std.Io.Threaded) catch return abi.OOM;
backend.* = .init(gpa, .{ .async_limit = .nothing, .concurrent_limit = .nothing });
const application = Application.create(gpa, backend.io(), abi.textBytes(desc.*.display_name)) catch {
backend.deinit();
gpa.destroy(backend);
return abi.OOM;
};
application.io_backend = backend;
singleton = true;
out.* = runtime.toAppHandle(application);
return abi.OK;
}
pub export fn uhdk_app_run(
handle: ?*c.uhdk_app,
entry: c.uhdk_app_main_fn,
user: ?*anyopaque,
) abi.Status {
if (handle == null or entry == null) return abi.INVALID;
if (c.z_native_is_main_thread() == 0) return c.UHDK_STATUS_WRONG_THREAD;
const application = runtime.fromAppHandle(handle);
if (application.has_run) return abi.UNAVAILABLE;
runtime.simpleEvent(application, c.UHDK_EVENT_READY);
const status = c.z_native_start(handle, abi.textSpan(application.name));
if (status != abi.OK) return status;
application.entry = entry;
application.user = user;
application.running = true;
application.has_run = true;
const worker = std.Thread.spawn(.{}, runtime.applicationMain, .{application}) catch {
application.running = false;
c.z_native_finish();
return abi.OOM;
};
c.z_native_run();
worker.join();
application.running = false;
// Native sources stop before any of their Zig records can be released.
var cursor = application.objects;
while (cursor) |object| : (cursor = object.next) {
if (object.kind() == c.Z_KIND_WATCH and object.native != null) {
c.z_native_destroy(object.kind(), object.native);
object.native = null;
}
}
cursor = application.objects;
while (cursor) |object| : (cursor = object.next) {
if (object.kind() == c.Z_KIND_GL and object.native != null) {
c.z_native_destroy(object.kind(), object.native);
object.native = null;
}
}
cursor = application.objects;
while (cursor) |object| : (cursor = object.next) {
if (object.kind() == c.Z_KIND_WINDOW and object.native != null) {
c.z_native_destroy(object.kind(), object.native);
object.native = null;
}
}
c.z_native_finish();
return abi.OK;
}
pub export fn uhdk_app_poll_events(
handle: ?*c.uhdk_app,
timeout: u64,
out: [*c]c.uhdk_event_list,
) abi.Status {
if (handle == null or out == null) return abi.INVALID;
const application = runtime.fromAppHandle(handle);
if (runtime.checkThread(application) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
runtime.destroyEvents(application.published);
application.published = null;
application.events.clearRetainingCapacity();
out.* = .{ .ptr = null, .len = 0 };
application.lock();
defer application.unlock();
if (application.stopped_seen) return abi.UNAVAILABLE;
if (application.deadline != 0 and runtime.now(application.io) - application.epoch >= application.deadline) {
application.deadline = 0;
application.unlock();
runtime.simpleEvent(application, c.UHDK_EVENT_WAKE);
application.lock();
}
const start = runtime.now(application.io);
const end = start +| timeout;
while (application.pending_head == null and application.event_error == abi.OK) {
const current = runtime.now(application.io);
if (application.deadline != 0 and current - application.epoch >= application.deadline) {
application.deadline = 0;
application.unlock();
runtime.simpleEvent(application, c.UHDK_EVENT_WAKE);
application.lock();
break;
}
if (current >= end) return abi.OK;
const wake_time = if (application.deadline == 0) end else @min(end, application.epoch +| application.deadline);
const ns = wake_time -| current;
application.available.reset();
application.unlock();
application.available.waitTimeout(application.io, .{ .duration = .{
.raw = .fromNanoseconds(ns),
.clock = .awake,
} }) catch |err| switch (err) {
error.Timeout => {},
error.Canceled => {
application.lock();
return abi.CANCELLED;
},
};
application.lock();
}
if (application.event_error != abi.OK) {
const err = application.event_error;
application.event_error = abi.OK;
return err;
}
var cursor = application.pending_head;
var count: usize = 0;
while (cursor) |node| : (cursor = node.next) {
count += 1;
}
application.events.ensureTotalCapacity(application.gpa, count) catch return abi.OOM;
application.published = application.pending_head;
application.pending_head = null;
application.pending_tail = null;
cursor = application.published;
while (cursor) |node| : (cursor = node.next) {
const event = node.event;
if (event.window != null and !runtime.fromObjectHandle(event.window).alive) continue;
if (event.type == c.UHDK_EVENT_WATCH and !runtime.fromObjectHandle(event.data.watch.watch).alive) continue;
application.events.appendAssumeCapacity(event);
if (event.type == c.UHDK_EVENT_STOPPED) application.stopped_seen = true;
}
out.* = .{ .ptr = application.events.items.ptr, .len = application.events.items.len };
application.stats.event_count +|= out.*.len;
return abi.OK;
}
pub export fn uhdk_app_begin_tick(handle: ?*c.uhdk_app) abi.Status {
if (handle == null) return abi.INVALID;
const application = runtime.fromAppHandle(handle);
if (runtime.checkThread(application) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
if (application.tick_start != null) return c.UHDK_STATUS_BUSY;
application.tick_start = runtime.threadCpuTime(application.io);
return abi.OK;
}
pub export fn uhdk_app_end_tick(handle: ?*c.uhdk_app) abi.Status {
if (handle == null) return abi.INVALID;
const application = runtime.fromAppHandle(handle);
if (runtime.checkThread(application) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
const start = application.tick_start orelse return abi.INVALID;
application.stats.tick_time_ns = runtime.threadCpuTime(application.io) -| start;
application.stats.tick_count +|= 1;
application.tick_start = null;
return abi.OK;
}
pub export fn uhdk_app_get_stats(handle: ?*c.uhdk_app, out: [*c]c.uhdk_app_stats) abi.Status {
if (handle == null) return abi.INVALID;
const application = runtime.fromAppHandle(handle);
if (runtime.checkThread(application) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
return abi.writeOutput(c.uhdk_app_stats, out, application.stats);
}
pub export fn uhdk_app_quit(handle: ?*c.uhdk_app) void {
if (handle == null) return;
const application = runtime.fromAppHandle(handle);
application.lock();
application.stopping = true;
application.unlock();
c.z_native_wake();
}
pub export fn uhdk_app_wake(handle: ?*c.uhdk_app) void {
if (handle != null) runtime.simpleEvent(runtime.fromAppHandle(handle), c.UHDK_EVENT_WAKE);
}
pub export fn uhdk_app_get_time_ns(handle: ?*c.uhdk_app) u64 {
const application = runtime.fromAppHandle(handle);
return runtime.now(application.io) - application.epoch;
}
pub export fn uhdk_app_set_wake_deadline(handle: ?*c.uhdk_app, deadline: u64) abi.Status {
if (handle == null) return abi.INVALID;
const application = runtime.fromAppHandle(handle);
if (runtime.checkThread(application) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
application.lock();
application.deadline = deadline;
application.notify();
application.unlock();
return abi.OK;
}
pub export fn uhdk_app_get_backend(_: ?*c.uhdk_app) c.uhdk_backend {
return c.UHDK_BACKEND_MACOS;
}
pub export fn uhdk_app_has_capability(_: ?*c.uhdk_app, cap: c.uhdk_capability) c.uhdk_bool {
return switch (cap) {
c.UHDK_CAP_FILE_DIALOGS, c.UHDK_CAP_CLIPBOARD, c.UHDK_CAP_DROP_FILES, c.UHDK_CAP_APP_MENU, c.UHDK_CAP_CONTEXT_MENU, c.UHDK_CAP_APP_ICON, c.UHDK_CAP_POINTER_CAPTURE, c.UHDK_CAP_TEXT_INPUT, c.UHDK_CAP_OPENGL => 1,
else => 0,
};
}
pub export fn uhdk_request_cancel(handle: ?*c.uhdk_app, id: u64) abi.Status {
if (handle == null or id == 0) return abi.INVALID;
const application = runtime.fromAppHandle(handle);
if (runtime.checkThread(application) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
application.lock();
var cursor = application.requests;
while (cursor) |command| : (cursor = command.next) {
if (command.id == id) {
command.cancelled = true;
application.unlock();
c.z_native_wake();
return abi.OK;
}
}
application.unlock();
return c.UHDK_STATUS_NOT_FOUND;
}
pub export fn uhdk_app_destroy(handle: ?*c.uhdk_app) void {
if (handle == null) return;
const application = runtime.fromAppHandle(handle);
std.debug.assert(c.z_native_is_main_thread() != 0 and !application.running);
runtime.destroyEvents(application.published);
runtime.destroyEvents(application.pending_head);
application.events.deinit(application.gpa);
var cursor = application.objects;
while (cursor) |object| {
cursor = object.next;
if (object.native != null) c.z_native_destroy(object.kind(), object.native);
object.arena.deinit();
application.gpa.destroy(object);
}
const gpa = application.gpa;
gpa.free(application.name);
if (application.io_backend) |backend| {
backend.deinit();
gpa.destroy(backend);
}
gpa.destroy(application);
singleton = false;
}

82
src/api/desktop.zig Normal file
View File

@ -0,0 +1,82 @@
//! C ABI entry points for desktop.
const std = @import("std");
const c = @import("native");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
pub export fn uhdk_clipboard_write_text_async(
handle: ?*c.uhdk_app,
text: c.uhdk_text,
out: [*c]u64,
) abi.Status {
if (!abi.validText(text, false)) return abi.INVALID;
const command = runtime.createAppCommand(handle, c.Z_CLIP_WRITE) catch |err| return runtime.applicationError(err);
command.data.text = abi.copyText(command.arena.allocator(), text) catch {
runtime.abandon(command);
return abi.OOM;
};
return runtime.submit(runtime.fromAppHandle(handle), command, out);
}
pub export fn uhdk_clipboard_read_text_async(
handle: ?*c.uhdk_app,
limit: u64,
out: [*c]u64,
) abi.Status {
const command = runtime.createAppCommand(handle, c.Z_CLIP_READ) catch |err| return runtime.applicationError(err);
command.kind = c.UHDK_REQUEST_CLIPBOARD_TEXT;
command.data.limit = limit;
return runtime.submit(runtime.fromAppHandle(handle), command, out);
}
pub export fn uhdk_shell_open_url_async(
handle: ?*c.uhdk_app,
text: c.uhdk_text,
out: [*c]u64,
) abi.Status {
if (!abi.validText(text, true) or text.len == 0) return abi.INVALID;
const command = runtime.createAppCommand(handle, c.Z_URL) catch |err| return runtime.applicationError(err);
command.kind = c.UHDK_REQUEST_SHELL;
command.data.text = abi.copyText(command.arena.allocator(), text) catch {
runtime.abandon(command);
return abi.OOM;
};
return runtime.submit(runtime.fromAppHandle(handle), command, out);
}
pub export fn uhdk_app_set_icon_async(
handle: ?*c.uhdk_app,
image: [*c]const c.uhdk_image,
out: [*c]u64,
) abi.Status {
if (!abi.validDescriptor(c.uhdk_image, image)) return abi.INVALID;
const i = image.*;
if (i.reserved != 0 or
i.width == 0 or
i.height == 0 or
i.width > 4096 or
i.height > 4096 or
i.stride_bytes < @as(u64, i.width) * 4 or
i.stride_bytes > std.math.maxInt(u64) / @as(u64, i.height) or
i.pixels.len < i.stride_bytes * i.height or
!abi.validSpan(i.pixels.ptr, i.pixels.len)) return abi.INVALID;
const command = runtime.createAppCommand(handle, c.Z_ICON) catch |err| return runtime.applicationError(err);
command.data.image = i;
command.data.image.pixels = abi.copyBytes(command.arena.allocator(), i.pixels) catch {
runtime.abandon(command);
return abi.OOM;
};
return runtime.submit(runtime.fromAppHandle(handle), command, out);
}
pub export fn uhdk_app_get_preferences(
handle: ?*c.uhdk_app,
out: [*c]c.uhdk_preferences,
) abi.Status {
if (handle == null) return abi.INVALID;
const application = runtime.fromAppHandle(handle);
if (runtime.checkThread(application) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
application.lock();
defer application.unlock();
return abi.writeOutput(c.uhdk_preferences, out, application.preferences);
}

148
src/api/files.zig Normal file
View File

@ -0,0 +1,148 @@
//! C ABI entry points for files.
const std = @import("std");
const c = @import("native");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
pub export fn uhdk_file_dialog_show_async(
handle: ?*c.uhdk_app,
desc: [*c]const c.uhdk_file_dialog_desc,
out: [*c]u64,
) abi.Status {
if (handle == null or !abi.validDescriptor(c.uhdk_file_dialog_desc, desc)) return abi.INVALID;
const d = desc.*;
if (d.kind > c.UHDK_DIALOG_SAVE_FILE or
!abi.validText(d.title, true) or
!abi.validText(d.accept_label, true) or
!abi.validText(d.suggested_name, true) or
!abi.validSpan(d.filters, d.filters_len) or
d.filters_len > 64) return abi.INVALID;
if (d.parent != null and (runtime.fromObjectHandle(d.parent).app != runtime.fromAppHandle(handle) or
!runtime.fromObjectHandle(d.parent).alive)) return abi.INVALID;
if (d.initial_directory != null and runtime.fromObjectHandle(d.initial_directory).app != runtime.fromAppHandle(handle)) return abi.INVALID;
const command = runtime.createAppCommand(handle, c.Z_DIALOG) catch |err| return runtime.applicationError(err);
command.kind = c.UHDK_REQUEST_FILE_DIALOG;
command.data.dialog = d;
runtime.copyDialog(command) catch |err| {
runtime.abandon(command);
return if (err == error.OutOfMemory) abi.OOM else abi.INVALID;
};
return runtime.submit(runtime.fromAppHandle(handle), command, out);
}
pub export fn uhdk_file_create_from_native_path(
handle: ?*c.uhdk_app,
path: c.uhdk_bytes,
out: [*c]?*c.uhdk_file,
) abi.Status {
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;
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);
if (status != abi.OK) {
runtime.release(object);
return status;
}
out.* = runtime.castHandle(*c.uhdk_file, object);
return abi.OK;
}
pub export fn uhdk_file_create_from_uri(
handle: ?*c.uhdk_app,
uri: c.uhdk_text,
out: [*c]?*c.uhdk_file,
) abi.Status {
if (handle == null or out == null or !abi.validText(uri, true)) 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), .{ .ptr = null, .len = 0 }, uri, null);
if (status != abi.OK) {
runtime.release(object);
return status;
}
out.* = runtime.castHandle(*c.uhdk_file, object);
return abi.OK;
}
pub export fn uhdk_file_retain(handle: ?*c.uhdk_file) void {
if (handle != null) runtime.retain(runtime.fromObjectHandle(handle));
}
pub export fn uhdk_file_release(handle: ?*c.uhdk_file) void {
if (handle != null) runtime.release(runtime.fromObjectHandle(handle));
}
pub export fn uhdk_file_copy_representation(
handle: ?*c.uhdk_file,
form: u32,
buffer: [*c]u8,
capacity: u64,
out: [*c]u64,
) abi.Status {
if (handle == null or out == null or !abi.validSpan(buffer, capacity)) return abi.INVALID;
const object = runtime.fromObjectHandle(handle);
const status = runtime.checkObject(object, c.Z_KIND_FILE);
if (status != abi.OK) return status;
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,
else => return abi.INVALID,
};
out.* = data.len;
if (buffer == null and capacity == 0) return abi.OK;
if (capacity < data.len) return c.UHDK_STATUS_BUFFER_TOO_SMALL;
if (data.len > 0) @memcpy(buffer[0..data.len], data);
return abi.OK;
}
pub export fn uhdk_file_read_all_async(handle: ?*c.uhdk_file, limit: u64, out: [*c]u64) abi.Status {
const command = runtime.createObjectCommand(handle, c.Z_KIND_FILE, abi.IO_READ) catch |err| return runtime.objectError(err);
command.kind = c.UHDK_REQUEST_FILE_READ;
command.data.limit = limit;
return runtime.submit(runtime.fromObjectHandle(handle).app, command, out);
}
pub export fn uhdk_file_write_all_async(
handle: ?*c.uhdk_file,
data: c.uhdk_bytes,
out: [*c]u64,
) abi.Status {
if (!abi.validSpan(data.ptr, data.len)) return abi.INVALID;
const command = runtime.createObjectCommand(handle, c.Z_KIND_FILE, abi.IO_WRITE) catch |err| return runtime.objectError(err);
command.kind = c.UHDK_REQUEST_FILE_WRITE;
command.data.bytes = abi.copyBytes(command.arena.allocator(), data) catch {
runtime.abandon(command);
return abi.OOM;
};
return runtime.submit(runtime.fromObjectHandle(handle).app, command, out);
}
pub export fn uhdk_file_get_info_async(handle: ?*c.uhdk_file, out: [*c]u64) abi.Status {
const command = runtime.createObjectCommand(handle, c.Z_KIND_FILE, abi.IO_INFO) catch |err| return runtime.objectError(err);
command.kind = c.UHDK_REQUEST_FILE_INFO;
return runtime.submit(runtime.fromObjectHandle(handle).app, command, out);
}
pub export fn uhdk_watch_create_async(handle: ?*c.uhdk_file, out: [*c]u64) abi.Status {
const command = runtime.createObjectCommand(handle, c.Z_KIND_FILE, c.Z_WATCH) catch |err| return runtime.objectError(err);
const f = runtime.fromObjectHandle(handle);
const object = runtime.createObject(f.app, c.Z_KIND_WATCH) catch {
runtime.abandon(command);
return abi.OOM;
};
object.payload.watch.owner = f;
runtime.retain(f);
command.data.other = object;
command.kind = c.UHDK_REQUEST_WATCH;
return runtime.submit(f.app, command, out);
}
pub export fn uhdk_watch_destroy(handle: ?*c.uhdk_watch) void {
if (handle != null) runtime.destroyObject(runtime.fromObjectHandle(handle));
}

131
src/api/graphics.zig Normal file
View File

@ -0,0 +1,131 @@
//! C ABI entry points for graphics.
const std = @import("std");
const c = @import("native");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
pub export fn uhdk_gl_context_create_async(handle: ?*c.uhdk_window, out: [*c]u64) abi.Status {
const command = runtime.createObjectCommand(handle, c.Z_KIND_WINDOW, c.Z_GL) catch |err| return runtime.objectError(err);
const w = runtime.fromObjectHandle(handle);
if (w.payload.window.context != null) {
runtime.abandon(command);
return c.UHDK_STATUS_BUSY;
}
const object = runtime.createObject(w.app, c.Z_KIND_GL) catch {
runtime.abandon(command);
return abi.OOM;
};
object.payload.gl.owner = w;
w.payload.window.context = object;
command.data.other = object;
command.kind = c.UHDK_REQUEST_GL_CONTEXT;
return runtime.submit(w.app, command, out);
}
pub export fn uhdk_gl_context_destroy(handle: ?*c.uhdk_gl_context) void {
if (handle == null) return;
const object = runtime.fromObjectHandle(handle);
if (object.payload.gl.owner) |w| w.payload.window.context = null;
runtime.destroyObject(object);
}
pub export fn uhdk_gl_context_begin_frame(handle: ?*c.uhdk_gl_context) abi.Status {
if (handle == null) return abi.INVALID;
const object = runtime.fromObjectHandle(handle);
if (object.payload.gl.bracket) return c.UHDK_STATUS_BUSY;
const status = runtime.glCall(handle, 1, null);
if (status == abi.OK) {
object.payload.gl.bracket = true;
object.payload.gl.frame_cpu_start = runtime.threadCpuTime(object.app.io);
object.payload.gl.swap_cpu_time = 0;
object.payload.gl.presented = false;
}
return status;
}
pub export fn uhdk_gl_context_end_frame(handle: ?*c.uhdk_gl_context) abi.Status {
const status = runtime.glCall(handle, 2, null);
if (status == abi.OK) {
const object = runtime.fromObjectHandle(handle);
object.payload.gl.bracket = false;
if (object.payload.gl.presented) {
const stats = &object.app.stats;
const elapsed = runtime.threadCpuTime(object.app.io) -| object.payload.gl.frame_cpu_start;
stats.render_time_ns = elapsed -| object.payload.gl.swap_cpu_time;
stats.frame_time_ns = stats.tick_time_ns +| stats.render_time_ns;
stats.frame_count +|= 1;
const now = runtime.now(object.app.io);
if (object.app.last_frame_time) |previous| {
const interval = now -| previous;
stats.frames_per_second = if (interval == 0) 0 else @as(f32, std.time.ns_per_s) / @as(f32, @floatFromInt(interval));
}
object.app.last_frame_time = now;
}
}
return status;
}
pub export fn uhdk_gl_context_make_current(handle: ?*c.uhdk_gl_context) abi.Status {
return runtime.glCall(handle, 3, null);
}
pub export fn uhdk_gl_clear_current(handle: ?*c.uhdk_app) abi.Status {
if (handle == null) return abi.INVALID;
if (runtime.checkThread(runtime.fromAppHandle(handle)) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
return c.z_native_gl(null, 4, null);
}
pub export fn uhdk_gl_context_get_version(
handle: ?*c.uhdk_gl_context,
major: [*c]u32,
minor: [*c]u32,
) abi.Status {
if (major == null or minor == null) return abi.INVALID;
var version: [2]u32 = undefined;
const status = runtime.glCall(handle, 5, &version);
if (status == abi.OK) {
major.* = version[0];
minor.* = version[1];
}
return status;
}
pub export fn uhdk_gl_context_swap_buffers(handle: ?*c.uhdk_gl_context) abi.Status {
if (handle == null) return abi.INVALID;
const object = runtime.fromObjectHandle(handle);
const valid = runtime.checkObject(object, c.Z_KIND_GL);
if (valid != abi.OK) return valid;
if (!object.payload.gl.bracket) return c.UHDK_STATUS_BUSY;
// Exclude even driver CPU work/spinning inside presentation, not just sleep.
const start = runtime.threadCpuTime(object.app.io);
const status = runtime.glCall(handle, 6, null);
object.payload.gl.swap_cpu_time +|= runtime.threadCpuTime(object.app.io) -| start;
if (status == abi.OK) object.payload.gl.presented = true;
return status;
}
pub export fn uhdk_gl_context_set_swap_interval(
handle: ?*c.uhdk_gl_context,
interval: i32,
) abi.Status {
if (interval != 0 and interval != 1) return abi.INVALID;
var value = interval;
return runtime.glCall(handle, 7, &value);
}
pub export fn uhdk_gl_context_get_proc(
handle: ?*c.uhdk_gl_context,
name: c.uhdk_text,
out: [*c]c.uhdk_gl_proc,
) abi.Status {
if (handle == null or
out == null or
!abi.validText(name, true) or
name.len == 0 or
name.len > 255) return abi.INVALID;
const object = runtime.fromObjectHandle(handle);
const status = runtime.checkObject(object, c.Z_KIND_GL);
if (status != abi.OK) return status;
if (!object.payload.gl.bracket) return c.UHDK_STATUS_BUSY;
return c.z_native_gl_proc(object.native, name, out);
}

80
src/api/input.zig Normal file
View File

@ -0,0 +1,80 @@
//! C ABI entry points for input.
const std = @import("std");
const c = @import("native");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
pub export fn uhdk_window_set_cursor_shape_async(
handle: ?*c.uhdk_window,
shape: u32,
out: [*c]u64,
) abi.Status {
if (shape > c.UHDK_CURSOR_HIDDEN) return abi.INVALID;
return runtime.scalarCommand(handle, c.Z_KIND_WINDOW, c.Z_CURSOR, shape, out);
}
pub export fn uhdk_window_set_pointer_mode_async(
handle: ?*c.uhdk_window,
mode: u32,
out: [*c]u64,
) abi.Status {
if (mode > 1) return abi.INVALID;
return runtime.scalarCommand(handle, c.Z_KIND_WINDOW, c.Z_POINTER, mode, out);
}
pub export fn uhdk_window_set_text_input_async(
handle: ?*c.uhdk_window,
enabled: c.uhdk_bool,
out: [*c]u64,
) abi.Status {
if (enabled != 0 and enabled != 1) return abi.INVALID;
return runtime.scalarCommand(handle, c.Z_KIND_WINDOW, c.Z_TEXT_ENABLE, @intCast(enabled), out);
}
pub export fn uhdk_window_cancel_composition_async(
handle: ?*c.uhdk_window,
out: [*c]u64,
) abi.Status {
return runtime.scalarCommand(handle, c.Z_KIND_WINDOW, c.Z_TEXT_CANCEL, 0, out);
}
pub export fn uhdk_window_set_text_state(
handle: ?*c.uhdk_window,
state: [*c]const c.uhdk_text_state,
) abi.Status {
if (!abi.validDescriptor(c.uhdk_text_state, state) or
state.*.reserved != 0 or
!abi.validText(state.*.surrounding_text, false)) return abi.INVALID;
const s = state.*;
const txt = abi.textBytes(s.surrounding_text);
if (s.selection_start > txt.len or
s.selection_len > txt.len - s.selection_start or
!abi.boundary(txt, s.selection_start) or
!abi.boundary(txt, s.selection_start + s.selection_len) or
s.document_offset > std.math.maxInt(u64) - txt.len) return abi.INVALID;
if (!std.math.isFinite(s.caret.x) or
!std.math.isFinite(s.caret.y) or
!abi.finiteSize(.{ .width = s.caret.width, .height = s.caret.height })) return abi.INVALID;
const command = runtime.createObjectCommand(handle, c.Z_KIND_WINDOW, c.Z_TEXT_STATE) catch |err| return runtime.objectError(err);
command.data.text_state = s;
command.data.text_state.surrounding_text = abi.copyText(command.arena.allocator(), s.surrounding_text) catch {
runtime.abandon(command);
return abi.OOM;
};
const application = runtime.fromObjectHandle(handle).app;
application.lock();
if (application.queue_tail) |tail| tail.queued_next = command else application.queue_head = command;
application.queue_tail = command;
application.unlock();
c.z_native_wake();
return abi.OK;
}
pub export fn uhdk_window_set_drop_enabled_async(
handle: ?*c.uhdk_window,
enabled: c.uhdk_bool,
out: [*c]u64,
) abi.Status {
if (enabled != 0 and enabled != 1) return abi.INVALID;
return runtime.scalarCommand(handle, c.Z_KIND_WINDOW, c.Z_DROP, @intCast(enabled), out);
}

115
src/api/menus.zig Normal file
View File

@ -0,0 +1,115 @@
//! C ABI entry points for menus.
const std = @import("std");
const c = @import("native");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
pub export fn uhdk_menu_create(
handle: ?*c.uhdk_app,
items: [*c]const c.uhdk_menu_item,
len: u64,
out: [*c]?*c.uhdk_menu,
) abi.Status {
if (handle == null or out == null or !abi.validSpan(items, len) or len > 4096) return abi.INVALID;
const application = runtime.fromAppHandle(handle);
if (runtime.checkThread(application) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
const object = runtime.createObject(application, c.Z_KIND_MENU) catch return abi.OOM;
object.payload.menu.items = object.arena.allocator().alloc(c.uhdk_menu_item, @intCast(len)) catch {
runtime.release(object);
return abi.OOM;
};
for (object.payload.menu.items, 0..) |*item, i| {
item.* = items[i];
if (!abi.validText(item.label, true) or
item.flags & ~@as(u32, 7) != 0 or
item.role > c.UHDK_MENU_ROLE_REDO or
(item.command_id == 0 and item.flags & c.UHDK_MENU_SEPARATOR == 0 and item.submenu == null and item.role == c.UHDK_MENU_ROLE_NONE))
{
runtime.release(object);
return abi.INVALID;
}
if (item.submenu != null and (runtime.fromObjectHandle(item.submenu).app != application or
!runtime.fromObjectHandle(item.submenu).alive))
{
runtime.release(object);
return abi.INVALID;
}
item.label = abi.copyText(object.arena.allocator(), item.label) catch {
runtime.release(object);
return abi.OOM;
};
}
var ids: std.AutoHashMap(u64, void) = .init(application.gpa);
defer ids.deinit();
runtime.validateMenu(object, &ids, 0) catch |err| {
runtime.release(object);
return if (err == error.OutOfMemory) abi.OOM else abi.INVALID;
};
for (object.payload.menu.items) |item| if (item.submenu) |sub| {
runtime.retain(runtime.fromObjectHandle(sub));
};
object.payload.menu.owns_submenus = true;
out.* = runtime.castHandle(*c.uhdk_menu, object);
return abi.OK;
}
pub export fn uhdk_menu_retain(handle: ?*c.uhdk_menu) void {
if (handle != null) runtime.retain(runtime.fromObjectHandle(handle));
}
pub export fn uhdk_menu_release(handle: ?*c.uhdk_menu) void {
if (handle != null) runtime.release(runtime.fromObjectHandle(handle));
}
pub export fn uhdk_app_set_menu_async(
handle: ?*c.uhdk_app,
menu: ?*c.uhdk_menu,
out: [*c]u64,
) abi.Status {
return runtime.attachMenu(handle, null, menu, out);
}
pub export fn uhdk_window_set_menu_async(
handle: ?*c.uhdk_window,
menu: ?*c.uhdk_menu,
out: [*c]u64,
) abi.Status {
if (handle == null) return abi.INVALID;
return runtime.attachMenu(runtime.toAppHandle(runtime.fromObjectHandle(handle).app), handle, menu, out);
}
pub export fn uhdk_menu_set_item_flags_async(
handle: ?*c.uhdk_menu,
id: u64,
flags: u32,
out: [*c]u64,
) abi.Status {
if (id == 0 or flags & ~@as(u32, 3) != 0) return abi.INVALID;
const command = runtime.createObjectCommand(handle, c.Z_KIND_MENU, c.Z_MENU_FLAGS) catch |err| return runtime.objectError(err);
command.data.number = id;
command.data.limit = flags;
return runtime.submit(runtime.fromObjectHandle(handle).app, command, out);
}
pub export fn uhdk_menu_popup_async(
handle: ?*c.uhdk_menu,
window: ?*c.uhdk_window,
point: c.uhdk_point,
out: [*c]u64,
) abi.Status {
if (handle == null or
window == null or
runtime.fromObjectHandle(handle).app != runtime.fromObjectHandle(window).app or
!runtime.fromObjectHandle(window).alive or
!std.math.isFinite(point.x) or
!std.math.isFinite(point.y)) return abi.INVALID;
const command = runtime.createObjectCommand(handle, c.Z_KIND_MENU, c.Z_POPUP) catch |err| return runtime.objectError(err);
command.data.other = runtime.fromObjectHandle(window);
runtime.pin(&command.pins, runtime.fromObjectHandle(window)) catch {
runtime.abandon(command);
return abi.OOM;
};
command.data.point = point;
command.kind = c.UHDK_REQUEST_MENU_POPUP;
return runtime.submit(runtime.fromObjectHandle(handle).app, command, out);
}

116
src/api/windows.zig Normal file
View File

@ -0,0 +1,116 @@
//! C ABI entry points for windows.
const std = @import("std");
const c = @import("native");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
pub export fn uhdk_window_create_async(
handle: ?*c.uhdk_app,
desc: [*c]const c.uhdk_window_desc,
out: [*c]u64,
) abi.Status {
if (handle == null or !abi.validDescriptor(c.uhdk_window_desc, desc)) return abi.INVALID;
const d = desc.*;
if (!abi.validText(d.title, true) or
!abi.finiteSize(d.content_size) or
d.content_size.width == 0 or
d.content_size.height == 0 or
!abi.finiteSize(d.min_size) or
!abi.finiteSize(d.max_size) or
d.flags & ~@as(u32, 11) != 0) return abi.INVALID;
if ((d.max_size.width != 0 and d.max_size.width < d.min_size.width) or
(d.max_size.height != 0 and d.max_size.height < d.min_size.height)) return abi.INVALID;
if (d.parent != null and (runtime.fromObjectHandle(d.parent).app != runtime.fromAppHandle(handle) or
!runtime.fromObjectHandle(d.parent).alive)) return abi.INVALID;
const command = runtime.createAppCommand(handle, c.Z_WINDOW) catch |err| return runtime.applicationError(err);
const object = runtime.createObject(runtime.fromAppHandle(handle), c.Z_KIND_WINDOW) catch {
runtime.abandon(command);
return abi.OOM;
};
object.payload.window.parent = if (d.parent) |parent| runtime.fromObjectHandle(parent) else null;
command.data.target = object;
command.data.window = d;
command.kind = c.UHDK_REQUEST_WINDOW;
command.data.window.title = abi.copyText(command.arena.allocator(), d.title) catch {
runtime.abandon(command);
object.alive = false;
return abi.OOM;
};
const status = runtime.submit(runtime.fromAppHandle(handle), command, out);
if (status != abi.OK) object.alive = false;
return status;
}
pub export fn uhdk_window_destroy(handle: ?*c.uhdk_window) void {
if (handle) |_| runtime.destroyObject(runtime.fromObjectHandle(handle));
}
pub export fn uhdk_window_set_title_async(
handle: ?*c.uhdk_window,
title: c.uhdk_text,
out: [*c]u64,
) abi.Status {
return runtime.textCommand(handle, c.Z_KIND_WINDOW, c.Z_TITLE, title, out);
}
pub export fn uhdk_window_set_size_async(
handle: ?*c.uhdk_window,
size: c.uhdk_size,
out: [*c]u64,
) abi.Status {
if (!abi.finiteSize(size) or size.width == 0 or size.height == 0) return abi.INVALID;
const command = runtime.createObjectCommand(handle, c.Z_KIND_WINDOW, c.Z_SIZE) catch |err| return runtime.objectError(err);
command.data.size = size;
return runtime.submit(runtime.fromObjectHandle(handle).app, command, out);
}
pub export fn uhdk_window_set_state_async(
handle: ?*c.uhdk_window,
state: c.uhdk_window_state,
out: [*c]u64,
) abi.Status {
if (state > c.UHDK_WINDOW_FULLSCREEN) return abi.INVALID;
return runtime.scalarCommand(handle, c.Z_KIND_WINDOW, c.Z_STATE, state, out);
}
pub export fn uhdk_window_set_visible_async(
handle: ?*c.uhdk_window,
visible: c.uhdk_bool,
out: [*c]u64,
) abi.Status {
if (visible != 0 and visible != 1) return abi.INVALID;
return runtime.scalarCommand(handle, c.Z_KIND_WINDOW, c.Z_VISIBLE, @intCast(visible), out);
}
pub export fn uhdk_window_request_focus_async(handle: ?*c.uhdk_window, out: [*c]u64) abi.Status {
return runtime.scalarCommand(handle, c.Z_KIND_WINDOW, c.Z_FOCUS, 0, out);
}
pub export fn uhdk_window_get_metrics(
handle: ?*c.uhdk_window,
out: [*c]c.uhdk_window_metrics,
) abi.Status {
if (handle == null) return abi.INVALID;
const object = runtime.fromObjectHandle(handle);
const status = runtime.checkObject(object, c.Z_KIND_WINDOW);
if (status != abi.OK) return status;
object.app.lock();
defer object.app.unlock();
return abi.writeOutput(c.uhdk_window_metrics, out, object.payload.window.metrics);
}
pub export fn uhdk_window_invalidate(handle: ?*c.uhdk_window) void {
if (handle != null and runtime.fromObjectHandle(handle).alive) runtime.notifyCommand(runtime.fromObjectHandle(handle), c.Z_REDRAW);
}
pub export fn uhdk_window_get_native(
handle: ?*c.uhdk_window,
out: [*c]c.uhdk_native_window,
) abi.Status {
if (handle == null or out == null or out.*.struct_size < @sizeOf(c.uhdk_native_window)) return abi.INVALID;
const object = runtime.fromObjectHandle(handle);
const status = runtime.checkObject(object, c.Z_KIND_WINDOW);
if (status != abi.OK) return status;
c.z_native_handles(object.native, out);
return abi.OK;
}

101
src/core_tests.zig Normal file
View File

@ -0,0 +1,101 @@
const std = @import("std");
const c = @import("native");
const Application = @import("Application.zig");
const runtime = @import("runtime.zig");
const abi = @import("abi.zig");
fn destroyApplication(application: *Application) void {
runtime.destroyEvents(application.pending_head);
runtime.destroyEvents(application.published);
application.events.deinit(application.gpa);
const gpa = application.gpa;
gpa.free(application.name);
application.* = undefined;
gpa.destroy(application);
}
fn allocationLifecycle(gpa: std.mem.Allocator) !void {
const application = try Application.create(gpa, std.testing.io, "test");
defer destroyApplication(application);
const file = try runtime.createObject(application, c.Z_KIND_FILE);
defer runtime.release(file);
const command = try runtime.createCommand(application, c.Z_CLIP_WRITE, file);
defer runtime.abandon(command);
command.data.text = try abi.copyText(command.arena.allocator(), .{
.ptr = "owned request text",
.len = "owned request text".len,
});
try std.testing.expectEqualStrings("owned request text", abi.textBytes(command.data.text));
try std.testing.expectEqual(@as(usize, 2), file.refs);
}
test "all request construction failures release partial ownership" {
try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationLifecycle, .{});
}
test "publication copies text and preserves batches across ordinary calls" {
const application = try Application.create(std.testing.allocator, std.testing.io, "events");
defer destroyApplication(application);
var source = [_]u8{ 'h', 'i' };
var event = std.mem.zeroes(c.uhdk_event);
event.type = c.UHDK_EVENT_TEXT_COMMIT;
event.data.text.text = .{ .ptr = &source, .len = source.len };
runtime.emit(runtime.toAppHandle(application), &event);
@memset(&source, '?');
const published = application.pending_head.?;
try std.testing.expectEqualStrings("hi", abi.textBytes(published.event.data.text.text));
runtime.simpleEvent(application, c.UHDK_EVENT_WAKE);
runtime.simpleEvent(application, c.UHDK_EVENT_WAKE);
try std.testing.expect(published.next != null);
try std.testing.expect(published.next.?.next == null);
try std.testing.expectEqualStrings("hi", abi.textBytes(published.event.data.text.text));
}
test "accepted request gets one terminal result when cancellation wins" {
const application = try Application.create(std.testing.allocator, std.testing.io, "requests");
defer destroyApplication(application);
application.running = true;
application.worker_id = std.Thread.getCurrentId();
const command = try runtime.createCommand(application, c.Z_CLIP_READ, null);
var request_id: u64 = 0;
try std.testing.expectEqual(@as(u32, c.UHDK_STATUS_OK), runtime.submit(application, command, &request_id));
try std.testing.expect(request_id != 0);
application.lock();
command.cancelled = true;
application.unlock();
runtime.tick(runtime.toAppHandle(application));
try std.testing.expect(application.requests == null);
try std.testing.expect(application.queue_head == null);
const event = application.pending_head.?;
try std.testing.expect(event.next == null);
try std.testing.expectEqual(@as(u32, c.UHDK_EVENT_REQUEST_DONE), event.event.type);
try std.testing.expectEqual(request_id, event.event.data.request.id);
try std.testing.expectEqual(@as(u32, c.UHDK_STATUS_CANCELLED), event.event.data.request.status);
}
test "invalid UTF-8 spans and partial scalar ranges are rejected" {
try std.testing.expect(!abi.validText(.{ .ptr = null, .len = 1 }, false));
try std.testing.expect(!abi.validText(.{ .ptr = "\xff", .len = 1 }, false));
try std.testing.expect(abi.validText(.{ .ptr = null, .len = 0 }, true));
try std.testing.expect(!abi.boundary("a😀b", 2));
try std.testing.expect(abi.boundary("a😀b", 5));
}
test "tick statistics exclude blocked time" {
const app_api = @import("api/app.zig");
const application = try Application.create(std.testing.allocator, std.testing.io, "stats");
defer destroyApplication(application);
application.running = true;
application.worker_id = std.Thread.getCurrentId();
const handle = runtime.toAppHandle(application);
const wall_start = runtime.now(application.io);
try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_begin_tick(handle));
try (std.Io.Clock.Duration{ .raw = .fromMilliseconds(40), .clock = .awake }).sleep(application.io);
try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_end_tick(handle));
const wall_time = runtime.now(application.io) - wall_start;
var stats = std.mem.zeroes(c.uhdk_app_stats);
stats.struct_size = @sizeOf(c.uhdk_app_stats);
try std.testing.expectEqual(@as(abi.Status, abi.OK), app_api.uhdk_app_get_stats(handle, &stats));
try std.testing.expectEqual(@as(u64, 1), stats.tick_count);
try std.testing.expect(stats.tick_time_ns < wall_time / 2);
}

3
src/glad.c Normal file
View File

@ -0,0 +1,3 @@
/* Implementation for the vendored GLAD 3.3 core header. */
#define GLAD_GL_IMPLEMENTATION
#include "glad/gl.h"

101
src/macos/callbacks.zig Normal file
View File

@ -0,0 +1,101 @@
//! C ABI entry points for callbacks.
const std = @import("std");
const c = @import("native");
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);
}
export fn z_complete(
handle: ?*c.uhdk_app,
id: u64,
status: abi.Status,
result: [*c]const c.uhdk_request_result,
) void {
runtime.complete(handle, id, status, result);
}
export fn z_object_native(handle: ?*anyopaque) ?*anyopaque {
return if (handle == null) null else runtime.fromObjectHandle(handle).native;
}
export fn z_object_set_native(handle: ?*anyopaque, native: ?*anyopaque) void {
runtime.fromObjectHandle(handle).native = native;
}
export fn z_object_kind(handle: ?*anyopaque) u32 {
return runtime.fromObjectHandle(handle).kind();
}
export fn z_object_app(handle: ?*anyopaque) ?*c.uhdk_app {
return runtime.toAppHandle(runtime.fromObjectHandle(handle).app);
}
export fn z_metrics(handle: ?*c.uhdk_window, value: [*c]const c.uhdk_window_metrics) void {
const object = runtime.fromObjectHandle(handle);
object.app.lock();
object.payload.window.metrics = value.*;
object.app.unlock();
var event = std.mem.zeroes(c.uhdk_event);
event.type = c.UHDK_EVENT_WINDOW_METRICS;
event.window = handle;
event.data.metrics = value.*;
c.z_emit(runtime.toAppHandle(object.app), &event);
}
export fn z_preferences(handle: ?*c.uhdk_app, value: [*c]const c.uhdk_preferences) void {
const application = runtime.fromAppHandle(handle);
application.lock();
application.preferences = value.*;
application.unlock();
var event = std.mem.zeroes(c.uhdk_event);
event.type = c.UHDK_EVENT_PREFERENCES_CHANGED;
event.data.preferences = value.*;
c.z_emit(handle, &event);
}
export fn z_file_init(
handle: ?*c.uhdk_file,
path: c.uhdk_bytes,
uri: c.uhdk_text,
display: c.uhdk_text,
grant: ?*anyopaque,
) 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.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;
return abi.OK;
}
export fn z_file_from_url(
handle: ?*c.uhdk_app,
path: c.uhdk_bytes,
grant: ?*anyopaque,
) ?*c.uhdk_file {
const object = runtime.createObject(runtime.fromAppHandle(handle), c.Z_KIND_FILE) catch return null;
if (c.z_native_file_init(runtime.castHandle(*c.uhdk_file, object), path, .{ .ptr = null, .len = 0 }, grant) != abi.OK) {
object.alive = false;
return null;
}
return runtime.castHandle(*c.uhdk_file, object);
}
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 };
}
export fn z_menu_items(handle: ?*c.uhdk_menu, len: [*c]u64) [*c]const c.uhdk_menu_item {
const object = runtime.fromObjectHandle(handle);
len.* = object.payload.menu.items.len;
return object.payload.menu.items.ptr;
}
export fn z_tick(handle: ?*c.uhdk_app) void {
runtime.tick(handle);
}

95
src/macos/native.h Normal file
View File

@ -0,0 +1,95 @@
#ifndef UHDK_PRIVATE_NATIVE_H
#define UHDK_PRIVATE_NATIVE_H
#include "uhdk.h"
#include "uhdk_native.h"
#include <stddef.h>
enum {
Z_EVENT_SIZE = sizeof(uhdk_event),
Z_EVENT_ALIGNMENT = _Alignof(uhdk_event),
Z_EVENT_DATA_OFFSET = offsetof(uhdk_event, data),
Z_RESULT_SIZE = sizeof(uhdk_request_result)
};
/* Private bridge. Ownership, request IDs, queues and file state machines live in Zig. */
enum {
Z_WINDOW = 1,
Z_TITLE,
Z_SIZE,
Z_STATE,
Z_VISIBLE,
Z_FOCUS,
Z_GL,
Z_CURSOR,
Z_POINTER,
Z_TEXT_ENABLE,
Z_TEXT_STATE,
Z_TEXT_CANCEL,
Z_DROP,
Z_CLIP_WRITE,
Z_CLIP_READ,
Z_DIALOG,
Z_APP_MENU,
Z_WINDOW_MENU,
Z_POPUP,
Z_MENU_FLAGS,
Z_ICON,
Z_URL,
Z_WATCH,
Z_DESTROY,
Z_REDRAW
};
enum {
Z_KIND_WINDOW = 1,
Z_KIND_GL,
Z_KIND_FILE,
Z_KIND_MENU,
Z_KIND_WATCH
};
#define Z_DEFERRED UINT32_MAX
/* Generic command storage is private, never exposed to consumers. */
typedef struct z_command {
uint32_t op;
void *target;
void *other;
uint64_t number;
uint64_t limit;
uhdk_text text;
uhdk_bytes bytes;
uhdk_size size;
uhdk_point point;
uhdk_window_desc window;
uhdk_text_state text_state;
uhdk_file_dialog_desc dialog;
uhdk_image image;
} z_command;
uhdk_status z_native_start(uhdk_app *, uhdk_text name);
void z_native_run(void);
void z_native_wake(void);
void z_native_again(void);
void z_native_stop(void);
void z_native_finish(void);
uhdk_status z_native_execute(uhdk_app *, const z_command *, uint64_t, uhdk_request_result *);
void z_native_cancel(uint64_t);
void z_native_destroy(uint32_t kind, void *native);
void *z_native_pool_push(void);
void z_native_pool_pop(void *);
uhdk_status z_native_gl(void *context, uint32_t op, void *out);
uhdk_status z_native_gl_proc(void *context, uhdk_text, uhdk_gl_proc *);
void z_native_handles(void *, uhdk_native_window *);
/* Pure path conversions (no file access); copied synchronously by z_file_init. */
uhdk_status z_native_file_init(uhdk_file *, uhdk_bytes path, uhdk_text uri, void *grant);
/* Zig callbacks, always return before native temporary spans expire. */
void z_tick(uhdk_app *);
void z_emit(uhdk_app *, const uhdk_event *);
void z_complete(uhdk_app *, uint64_t, uhdk_status, const uhdk_request_result *);
void *z_object_native(void *);
void z_object_set_native(void *, void *);
uint32_t z_object_kind(void *);
uhdk_app *z_object_app(void *);
uhdk_file *z_file_from_url(uhdk_app *, uhdk_bytes path, void *grant);
uhdk_status z_file_init(uhdk_file *, uhdk_bytes, uhdk_text uri, uhdk_text display, void *grant);
uhdk_bytes z_file_path(uhdk_file *);
const uhdk_menu_item *z_menu_items(uhdk_menu *, uint64_t *len);
void z_metrics(uhdk_window *, const uhdk_window_metrics *);
void z_preferences(uhdk_app *, const uhdk_preferences *);
int z_native_is_main_thread(void);
#endif

1641
src/macos/native.m Normal file

File diff suppressed because it is too large Load Diff

40
src/root.zig Normal file
View File

@ -0,0 +1,40 @@
//! Uhdk C ABI. Runtime state lives in Application; native code owns AppKit objects.
const std = @import("std");
const c = @import("native");
comptime {
checkExports(@import("api/app.zig"));
checkExports(@import("api/windows.zig"));
checkExports(@import("api/graphics.zig"));
checkExports(@import("api/input.zig"));
checkExports(@import("api/files.zig"));
checkExports(@import("api/menus.zig"));
checkExports(@import("api/desktop.zig"));
_ = @import("macos/callbacks.zig");
}
test {
std.testing.refAllDecls(@import("abi.zig"));
std.testing.refAllDecls(@import("FileOperation.zig"));
_ = @import("core_tests.zig");
}
fn checkExports(comptime module: type) void {
for (@typeInfo(module).@"struct".decls) |declaration| {
if (!std.mem.startsWith(u8, declaration.name, "uhdk_")) continue;
if (@TypeOf(@field(module, declaration.name)) != @TypeOf(@field(c, declaration.name))) {
@compileError("C signature mismatch: " ++ declaration.name);
}
}
}
comptime {
std.debug.assert(@sizeOf(c.uhdk_bool) == 1);
std.debug.assert(@sizeOf(c.uhdk_status) == 4);
std.debug.assert(@sizeOf(c.uhdk_request_id) == 8);
std.debug.assert(@sizeOf(@FieldType(c.uhdk_point, "x")) == 4);
std.debug.assert(@sizeOf(c.uhdk_event) == c.Z_EVENT_SIZE);
std.debug.assert(@alignOf(c.uhdk_event) == c.Z_EVENT_ALIGNMENT);
std.debug.assert(@offsetOf(c.uhdk_event, "data") == c.Z_EVENT_DATA_OFFSET);
std.debug.assert(@sizeOf(c.uhdk_request_result) == c.Z_RESULT_SIZE);
}

699
src/runtime.zig Normal file
View File

@ -0,0 +1,699 @@
//! Queue handoff and platform scheduling.
const std = @import("std");
const c = @import("native");
const abi = @import("abi.zig");
const Event = @import("Event.zig");
const Request = @import("Request.zig");
const Resource = @import("Resource.zig");
const Application = @import("Application.zig");
const FileOperation = @import("FileOperation.zig");
pub fn fromAppHandle(handle: ?*c.uhdk_app) *Application {
return @ptrCast(@alignCast(if (@typeInfo(@TypeOf(handle)) == .optional) handle.? else handle));
}
pub fn fromObjectHandle(handle: anytype) *Resource {
return @ptrCast(@alignCast(if (@typeInfo(@TypeOf(handle)) == .optional) handle.? else handle));
}
pub fn toAppHandle(application: *Application) *c.uhdk_app {
return @ptrCast(application);
}
pub fn castHandle(comptime T: type, handle: anytype) T {
return @ptrCast(@alignCast(handle));
}
pub fn now(io: std.Io) u64 {
return @intCast(@max(0, std.Io.Timestamp.now(io, .awake).nanoseconds));
}
pub fn threadCpuTime(io: std.Io) u64 {
return @intCast(@max(0, std.Io.Timestamp.now(io, .cpu_thread).nanoseconds));
}
pub fn checkThread(application: *Application) abi.Status {
return if (application.running and std.Thread.getCurrentId() == application.worker_id) abi.OK else c.UHDK_STATUS_WRONG_THREAD;
}
pub fn checkObject(object: *Resource, kind: u32) abi.Status {
if (checkThread(object.app) != abi.OK) return c.UHDK_STATUS_WRONG_THREAD;
return if (object.kind() == kind and object.alive) abi.OK else abi.UNAVAILABLE;
}
pub fn createObject(application: *Application, kind: u32) !*Resource {
const object = try application.gpa.create(Resource);
object.* = .init(application, @enumFromInt(kind));
application.lock();
object.next = application.objects;
application.objects = object;
application.unlock();
return object;
}
pub fn retain(object: *Resource) void {
object.app.lock();
object.refs += 1;
object.app.unlock();
}
/// File and menu storage dies with its final reference. Unique native handles
/// remain in the application registry until shutdown so borrowed batches stay safe.
pub fn release(object: *Resource) void {
const application = object.app;
application.lock();
std.debug.assert(object.refs > 0);
object.refs -= 1;
if (object.refs != 0 or (object.payload != .file and object.payload != .menu)) {
application.unlock();
return;
}
var link = &application.objects;
while (link.*) |resource| {
if (resource == object) {
link.* = resource.next;
break;
}
link = &resource.next;
}
application.unlock();
if (object.payload == .menu and object.payload.menu.owns_submenus) {
for (object.payload.menu.items) |item| {
if (item.submenu) |submenu| release(fromObjectHandle(submenu));
}
}
if (object.native) |native| c.z_native_destroy(object.kind(), native);
const gpa = object.gpa;
object.arena.deinit();
object.* = undefined;
gpa.destroy(object);
}
pub fn pin(list: *std.ArrayList(*Resource), object: *Resource) !void {
try list.append(object.app.gpa, object);
retain(object);
}
pub fn destroyEvent(node: *Event) void {
for (node.pins.items) |object| release(object);
node.pins.deinit(node.gpa);
node.arena.deinit();
node.gpa.destroy(node);
}
pub fn destroyEvents(head: ?*Event) void {
var cursor = head;
while (cursor) |node| {
cursor = node.next;
destroyEvent(node);
}
}
pub fn publishLocked(application: *Application, node: *Event) void {
node.event.struct_size = @sizeOf(c.uhdk_event);
node.event.time_ns = now(application.io) - application.epoch;
node.next = null;
if (application.pending_tail) |tail| tail.next = node else application.pending_head = node;
application.pending_tail = node;
application.notify();
}
pub fn cloneFiles(node: *Event, files: c.uhdk_file_list) !c.uhdk_file_list {
const out = try node.arena.allocator().alloc(?*c.uhdk_file, @intCast(files.len));
for (out, 0..) |*f, i| {
f.* = files.ptr[i];
try pin(&node.pins, fromObjectHandle(f.*));
}
return .{ .ptr = out.ptr, .len = out.len };
}
pub fn copyPayload(node: *Event) !void {
const event = &node.event;
const arena = node.arena.allocator();
switch (event.type) {
c.UHDK_EVENT_TEXT_COMMIT, c.UHDK_EVENT_TEXT_PREEDIT => event.data.text.text = try abi.copyText(arena, event.data.text.text),
c.UHDK_EVENT_DROP_FILES => event.data.drop_files.files = try cloneFiles(node, event.data.drop_files.files),
c.UHDK_EVENT_OPEN_FILES => event.data.files = try cloneFiles(node, event.data.files),
c.UHDK_EVENT_WATCH => {
if (event.data.watch.file) |f| try pin(&node.pins, fromObjectHandle(f));
},
c.UHDK_EVENT_REQUEST_DONE => {
const request = &event.data.request;
request.diagnostic = try abi.copyText(arena, request.diagnostic);
if (request.status == abi.OK) switch (request.kind) {
c.UHDK_REQUEST_FILE_DIALOG => request.value.files = try cloneFiles(node, request.value.files),
c.UHDK_REQUEST_CLIPBOARD_TEXT => request.value.text = try abi.copyText(arena, request.value.text),
c.UHDK_REQUEST_FILE_READ => request.value.bytes = try abi.copyBytes(arena, request.value.bytes),
else => {},
};
},
else => {},
}
}
pub fn simpleEvent(application: *Application, typ: u32) void {
var event = std.mem.zeroes(c.uhdk_event);
event.type = typ;
c.z_emit(toAppHandle(application), &event);
}
pub fn createCommand(application: *Application, op: u32, target: ?*Resource) !*Request {
const node = try application.gpa.create(Event);
node.* = .{ .gpa = application.gpa, .arena = .init(application.gpa) };
errdefer destroyEvent(node);
const command = try application.gpa.create(Request);
command.* = .{ .node = node, .gpa = application.gpa, .arena = .init(application.gpa) };
errdefer command.gpa.destroy(command);
command.data.op = op;
command.data.target = target;
if (target) |object| try pin(&command.pins, object);
return command;
}
pub fn destroyCommand(command: *Request) void {
if (command.file_operation) |*operation| operation.deinit();
for (command.pins.items) |object| release(object);
command.pins.deinit(command.gpa);
command.arena.deinit();
command.gpa.destroy(command);
}
pub fn abandon(command: *Request) void {
discardCreation(command);
destroyEvent(command.node);
destroyCommand(command);
}
/// Release reservations and access grants when creation never reaches the caller.
fn discardCreation(command: *Request) void {
switch (command.data.op) {
c.Z_WINDOW => if (command.data.target) |target| {
fromObjectHandle(target).alive = false;
},
c.Z_GL => if (command.data.other) |other| {
const object = fromObjectHandle(other);
object.alive = false;
if (object.payload.gl.owner) |window| {
if (window.payload.window.context == object) window.payload.window.context = null;
}
},
c.Z_WATCH => if (command.data.other) |other| {
const object = fromObjectHandle(other);
object.alive = false;
if (object.payload.watch.owner) |file| release(file);
object.payload.watch.owner = null;
},
else => {},
}
}
pub fn submit(application: *Application, command: *Request, out: [*c]u64) abi.Status {
if (out == null or checkThread(application) != abi.OK) {
abandon(command);
return if (out == null) abi.INVALID else c.UHDK_STATUS_WRONG_THREAD;
}
application.lock();
if (application.stopping or application.next_id == std.math.maxInt(u64)) {
application.unlock();
abandon(command);
return abi.UNAVAILABLE;
}
command.id = application.next_id;
application.next_id += 1;
out.* = command.id;
command.next = application.requests;
application.requests = command;
if (application.queue_tail) |tail| tail.queued_next = command else application.queue_head = command;
application.queue_tail = command;
application.unlock();
c.z_native_wake();
return abi.OK;
}
pub fn notifyCommand(object: *Resource, op: u32) void {
const command = createCommand(object.app, op, object) catch {
object.app.lock();
object.app.event_error = abi.OOM;
object.app.notify();
object.app.unlock();
return;
};
object.app.lock();
if (object.app.queue_tail) |tail| tail.queued_next = command else object.app.queue_head = command;
object.app.queue_tail = command;
object.app.unlock();
c.z_native_wake();
}
pub fn isCancelled(application: *Application, command: *Request) bool {
application.lock();
defer application.unlock();
return command.cancelled or application.stopping;
}
pub fn finish(
application: *Application,
command: *Request,
status: abi.Status,
value: ?*const c.uhdk_request_result,
) void {
if (command.id == 0) {
abandon(command);
return;
}
if (status != abi.OK) discardCreation(command);
const node = command.node;
node.event.type = c.UHDK_EVENT_REQUEST_DONE;
if (value) |v| node.event.data.request = v.*;
node.event.data.request.id = command.id;
node.event.data.request.kind = command.kind;
node.event.data.request.status = status;
copyPayload(node) catch {
node.event.data.request.status = abi.OOM;
node.event.data.request.value = std.mem.zeroes(@TypeOf(node.event.data.request.value));
node.event.data.request.diagnostic = .{ .ptr = null, .len = 0 };
};
application.lock();
var link = &application.requests;
while (link.*) |request| {
if (request == command) {
link.* = request.next;
break;
}
link = &request.next;
}
publishLocked(application, node);
application.unlock();
destroyCommand(command);
}
pub fn applicationMain(application: *Application) void {
const pool = c.z_native_pool_push();
defer c.z_native_pool_pop(pool);
application.lock();
application.worker_id = std.Thread.getCurrentId();
application.unlock();
application.entry.?(toAppHandle(application), application.user);
application.lock();
application.stopping = true;
application.worker_done = true;
application.unlock();
c.z_native_wake();
}
pub fn createObjectCommand(handle: anytype, kind: u32, op: u32) !*Request {
if (handle == null) return error.Invalid;
const object = fromObjectHandle(handle);
if (checkThread(object.app) != abi.OK) return error.WrongThread;
if (object.kind() != kind) return error.Invalid;
if (!object.alive) return error.Unavailable;
return createCommand(object.app, op, object);
}
pub fn objectError(err: anyerror) abi.Status {
return switch (err) {
error.OutOfMemory => abi.OOM,
error.WrongThread => c.UHDK_STATUS_WRONG_THREAD,
error.Unavailable => abi.UNAVAILABLE,
else => abi.INVALID,
};
}
pub fn scalarCommand(
handle: ?*c.uhdk_window,
kind: u32,
op: u32,
value: u64,
out: [*c]u64,
) abi.Status {
const command = createObjectCommand(handle, kind, op) catch |err| return objectError(err);
command.data.number = value;
return submit(fromObjectHandle(handle).app, command, out);
}
pub fn textCommand(
handle: ?*c.uhdk_window,
kind: u32,
op: u32,
text: c.uhdk_text,
out: [*c]u64,
) abi.Status {
if (!abi.validText(text, true)) return abi.INVALID;
const command = createObjectCommand(handle, kind, op) catch |err| return objectError(err);
command.data.text = abi.copyText(command.arena.allocator(), text) catch {
abandon(command);
return abi.OOM;
};
return submit(fromObjectHandle(handle).app, command, out);
}
pub fn createAppCommand(handle: ?*c.uhdk_app, op: u32) !*Request {
if (handle == null) return error.Invalid;
if (checkThread(fromAppHandle(handle)) != abi.OK) return error.WrongThread;
return createCommand(fromAppHandle(handle), op, null);
}
pub fn applicationError(err: anyerror) abi.Status {
return switch (err) {
error.OutOfMemory => abi.OOM,
error.WrongThread => c.UHDK_STATUS_WRONG_THREAD,
else => abi.INVALID,
};
}
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 == .window) if (object.payload.window.context) |ctx| {
destroyObject(ctx);
object.payload.window.context = null;
};
// Find one child under the registry lock, then recurse without the lock.
// Files can be released concurrently by the other thread.
while (true) {
object.app.lock();
var cursor = object.app.objects;
const child: ?*Resource = while (cursor) |candidate| : (cursor = candidate.next) {
if (candidate.payload == .window and
candidate.payload.window.parent == object and candidate.alive)
{
break candidate;
}
} else null;
object.app.unlock();
if (child) |resource| destroyObject(resource) else break;
}
object.app.lock();
object.alive = false;
var req = object.app.requests;
while (req) |request| : (req = request.next) {
if (request.data.target == castHandle(?*anyopaque, object) or
request.data.other == castHandle(?*anyopaque, object) or
(request.data.op == c.Z_DIALOG and request.data.dialog.parent == castHandle(?*c.uhdk_window, object))) request.cancelled = true;
}
object.app.unlock();
notifyCommand(object, c.Z_DESTROY);
}
pub fn glCall(handle: ?*c.uhdk_gl_context, op: u32, out: ?*anyopaque) abi.Status {
if (handle == null) return abi.INVALID;
const object = fromObjectHandle(handle);
const status = checkObject(object, c.Z_KIND_GL);
if (status != abi.OK) return status;
if (object.native == null) return abi.UNAVAILABLE;
if (op != 1 and !object.payload.gl.bracket) return c.UHDK_STATUS_BUSY;
return c.z_native_gl(object.native, op, out);
}
pub fn copyDialog(command: *Request) !void {
const d = &command.data.dialog;
const arena = command.arena.allocator();
d.title = try abi.copyText(arena, d.title);
d.accept_label = try abi.copyText(arena, d.accept_label);
d.suggested_name = try abi.copyText(arena, d.suggested_name);
if (d.parent) |handle| try pin(&command.pins, fromObjectHandle(handle));
if (d.initial_directory) |f| try pin(&command.pins, fromObjectHandle(f));
const filters = try arena.alloc(c.uhdk_file_filter, @intCast(d.filters_len));
for (filters, 0..) |*f, i| {
f.* = d.filters[i];
if (!abi.validText(f.label, true) or
!abi.validSpan(f.extensions.ptr, f.extensions.len) or
f.extensions.len > 256) return error.Invalid;
f.label = try abi.copyText(arena, f.label);
const extensions = try arena.alloc(c.uhdk_text, @intCast(f.extensions.len));
for (extensions, 0..) |*ext, j| {
const event = f.extensions.ptr[j];
if (!abi.validText(event, true) or
std.mem.findAny(u8, abi.textBytes(event), "/\\.") != null) return error.Invalid;
ext.* = try abi.copyText(arena, event);
}
f.extensions.ptr = extensions.ptr;
}
d.filters = filters.ptr;
}
pub fn fileError(err: anyerror) abi.Status {
return switch (err) {
error.OutOfMemory => abi.OOM,
error.Canceled => abi.CANCELLED,
error.FileNotFound, error.NotDir => c.UHDK_STATUS_NOT_FOUND,
error.AccessDenied, error.PermissionDenied => c.UHDK_STATUS_PERMISSION_DENIED,
error.StreamTooLong => c.UHDK_STATUS_BUFFER_TOO_SMALL,
error.NotRegularFile, error.SymLinkLoop => abi.INVALID,
else => c.UHDK_STATUS_OS_ERROR,
};
}
pub fn fileStep(application: *Application, command: *Request) bool {
if (isCancelled(application, command)) {
finish(application, command, abi.CANCELLED, null);
return true;
}
if (command.file_operation == null) {
command.file_operation = FileOperation.init(
application.gpa,
application.io,
switch (command.data.op) {
abi.IO_READ => .read,
abi.IO_WRITE => .write,
abi.IO_INFO => .info,
else => unreachable,
},
fromObjectHandle(command.data.target).payload.file.path,
abi.bytes(command.data.bytes),
command.data.limit,
);
}
const result = command.file_operation.?.step() catch |err| {
finish(application, command, fileError(err), null);
return true;
};
var value = std.mem.zeroes(c.uhdk_request_result);
switch (result) {
.pending => return false,
.written => {},
.bytes => |contents| value.value.bytes = .{ .ptr = contents.ptr, .len = contents.len },
.info => |info| value.value.info = .{
.size_bytes = info.size,
.modified_time_ns = std.math.lossyCast(i64, info.mtime.nanoseconds),
.type = switch (info.kind) {
.file => c.UHDK_FILE_REGULAR,
.directory => c.UHDK_FILE_DIRECTORY,
else => c.UHDK_FILE_OTHER,
},
.reserved = 0,
},
}
finish(application, command, abi.OK, &value);
return true;
}
pub fn validateMenu(object: *Resource, ids: *std.AutoHashMap(u64, void), depth: usize) !void {
if (depth > 16) return error.Invalid;
for (object.payload.menu.items) |item| {
if (item.command_id != 0) {
const entry = try ids.getOrPut(item.command_id);
if (entry.found_existing) return error.Invalid;
}
if (item.submenu) |sub| try validateMenu(fromObjectHandle(sub), ids, depth + 1);
}
}
pub fn attachMenu(
handle: ?*c.uhdk_app,
w: ?*c.uhdk_window,
menu: ?*c.uhdk_menu,
out: [*c]u64,
) abi.Status {
if (handle == null or
(menu != null and (fromObjectHandle(menu).app != fromAppHandle(handle) or
!fromObjectHandle(menu).alive))) return abi.INVALID;
const command = createAppCommand(handle, if (w == null) c.Z_APP_MENU else c.Z_WINDOW_MENU) catch |err| return applicationError(err);
if (w != null) {
if (!fromObjectHandle(w).alive) {
abandon(command);
return abi.UNAVAILABLE;
}
command.data.target = fromObjectHandle(w);
pin(&command.pins, fromObjectHandle(w)) catch {
abandon(command);
return abi.OOM;
};
}
if (menu) |m| {
command.data.other = fromObjectHandle(m);
pin(&command.pins, fromObjectHandle(m)) catch {
abandon(command);
return abi.OOM;
};
}
return submit(fromAppHandle(handle), command, out);
}
pub fn setMenuFlags(object: *Resource, id: u64, flags: u32) bool {
for (object.payload.menu.items) |*item| {
if (item.command_id == id) {
if (item.flags & c.UHDK_MENU_SEPARATOR != 0) return false;
item.flags = flags;
return true;
}
if (item.submenu) |sub| {
if (setMenuFlags(fromObjectHandle(sub), id, flags)) return true;
}
}
return false;
}
pub fn emit(handle: ?*c.uhdk_app, event: [*c]const c.uhdk_event) void {
const application = fromAppHandle(handle);
const node = application.gpa.create(Event) catch {
application.lock();
application.event_error = abi.OOM;
application.notify();
application.unlock();
return;
};
node.* = .{ .event = event.*, .gpa = application.gpa, .arena = .init(application.gpa) };
copyPayload(node) catch {
destroyEvent(node);
application.lock();
application.event_error = abi.OOM;
application.notify();
application.unlock();
return;
};
application.lock();
defer application.unlock();
if (application.stopped_sent or (node.event.window != null and !fromObjectHandle(node.event.window).alive)) {
application.unlock();
destroyEvent(node);
application.lock();
return;
}
// Redraw and wake have no payload; coalesce only while still pending.
if (event.*.type == c.UHDK_EVENT_WINDOW_REDRAW or event.*.type == c.UHDK_EVENT_WAKE) {
var cursor = application.pending_head;
while (cursor) |old| : (cursor = old.next) {
if (old.event.type == event.*.type and old.event.window == event.*.window) {
application.unlock();
destroyEvent(node);
application.lock();
return;
}
}
}
publishLocked(application, node);
}
pub fn complete(
handle: ?*c.uhdk_app,
id: u64,
status: abi.Status,
result: [*c]const c.uhdk_request_result,
) void {
const application = fromAppHandle(handle);
application.lock();
var cursor = application.requests;
while (cursor) |command| : (cursor = command.next) {
if (command.id == id) {
application.unlock();
finish(application, command, status, if (result == null) null else &result.*);
return;
}
}
application.unlock();
}
pub fn tick(handle: ?*c.uhdk_app) void {
const application = fromAppHandle(handle);
// Native tracking loops can reenter the run loop; never reenter application command.
if (application.in_tick) return;
application.in_tick = true;
defer application.in_tick = false;
const start = now(application.io);
var count: usize = 0;
while (count < 64 and now(application.io) - start < 2_000_000) : (count += 1) {
application.lock();
const command = application.queue_head orelse {
application.unlock();
break;
};
application.queue_head = command.queued_next;
if (application.queue_head == null) application.queue_tail = null;
command.queued_next = null;
application.unlock();
if (command.id != 0 and isCancelled(application, command)) {
finish(application, command, abi.CANCELLED, null);
continue;
}
if (command.data.target != null and !fromObjectHandle(command.data.target).alive and command.data.op != c.Z_DESTROY) {
finish(application, command, abi.CANCELLED, null);
continue;
}
if (command.data.op >= abi.IO_READ) {
if (application.io_tail) |tail| tail.queued_next = command else application.io_head = command;
application.io_tail = command;
continue;
}
var result = std.mem.zeroes(c.uhdk_request_result);
if (command.data.op == c.Z_MENU_FLAGS) {
application.lock();
const found = setMenuFlags(fromObjectHandle(command.data.target), command.data.number, @intCast(command.data.limit));
application.unlock();
if (!found) {
finish(application, command, c.UHDK_STATUS_NOT_FOUND, null);
continue;
}
}
const status = c.z_native_execute(handle, &command.data, command.id, &result);
if (status != c.Z_DEFERRED and command.data.op == c.Z_DESTROY) {
const resource = fromObjectHandle(command.data.target);
if (resource.payload == .watch) {
if (resource.payload.watch.owner) |root| release(root);
resource.payload.watch.owner = null;
}
}
if (status != c.Z_DEFERRED) {
finish(application, command, status, &result);
}
}
if (application.io_active == null) {
if (application.io_head) |command| {
application.io_head = command.queued_next;
if (application.io_head == null) application.io_tail = null;
application.io_active = command;
}
}
if (application.io_active) |command| {
if (fileStep(application, command)) application.io_active = null;
}
// Deferred native operations (panels/popups) have separate cancellation hooks.
application.lock();
var cursor = application.requests;
var ids: [64]u64 = undefined;
var node: usize = 0;
while (cursor) |request| : (cursor = request.next) {
if ((request.cancelled or
application.stopping) and node < ids.len and (request.data.op == c.Z_DIALOG or
request.data.op == c.Z_POPUP))
{
ids[node] = request.id;
node += 1;
}
}
application.unlock();
for (ids[0..node]) |id| c.z_native_cancel(id);
application.lock();
const empty = application.requests == null and application.queue_head == null and application.io_active == null and application.io_head == null;
if (application.stopping and empty and !application.stopped_sent) {
application.unlock();
simpleEvent(application, c.UHDK_EVENT_STOPPED);
application.lock();
application.stopped_sent = true;
}
const done = application.worker_done and empty;
const again = application.queue_head != null or application.io_head != null or application.io_active != null;
application.unlock();
if (done) c.z_native_stop() else if (again) c.z_native_again();
}