macos implementation.
This commit is contained in:
commit
0c8270fbef
|
|
@ -0,0 +1,4 @@
|
|||
.zig-cache/
|
||||
zig-out/
|
||||
.DS_Store
|
||||
zig-pkg/
|
||||
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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" },
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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_ */
|
||||
|
|
@ -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`
|
||||
|
|
@ -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.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
/* Implementation for the vendored GLAD 3.3 core header. */
|
||||
#define GLAD_GL_IMPLEMENTATION
|
||||
#include "glad/gl.h"
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Reference in New Issue